15 KiB
NBShaderGUI Rule
Purpose
This rule defines the structure constraints for NBShaders2/Editor/NBShaderGUI and shared ShaderGUIItems.
Core Principles
- Keep the GUI structure minimal and explicit.
- Do not add wrapper abstractions without a clear responsibility.
FoldOutbehavior andparent-child compositionare two different concerns and must not be mixed.- Common drawing items live in
XuanXuanRenderUtility/Editor/ShaderGUIItems. - Shared item names must not use the
NBShaderprefix.
Shared Item Rules
All common material-property drawing items must live in XuanXuanRenderUtility/Editor/ShaderGUIItems, not in NBShaders2/Editor/ShaderGUIItems.
Use shared items for common material property drawing:
ToggleItemColorItemTextureItemVector2LineItemVectorComponentItemHelpBoxItem
Vector-related shared item classes should stay in one VectorItem.cs file.
Do not create NBShader-prefixed wrappers for common drawing behavior. These names are not allowed for shared/general controls:
NBShaderTogglePropertyItemNBShaderColorPropertyItemNBShaderTexturePropertyItemNBShaderVector2LinePropertyItemNBShaderVectorComponentPropertyItemNBShaderHelpBoxItem
Do not create wrappers that only set PropertyName, GuiContent, ranges, or visibility around an existing shared item. Prefer using the shared item directly, or create a concrete business item when there is real shader-specific behavior.
Pure configuration items should be instantiated directly inside the owning block constructor.
Use direct shared item construction for cases that only assign:
PropertyNameGuiContentorGUIContentproviderRangePropertyNameMin/Max- simple visibility predicates
Example pattern:
_alphaAllItem = new ShaderGUISliderItem(rootItem, this)
{
PropertyName = "_AlphaAll",
GuiContent = NBShaderInspectorLocalization.MakeInspectorContent("base.alphaAll", "Overall Alpha"),
RangePropertyName = "AlphaAllRangeVec"
};
_alphaAllItem.InitTriggerByChild();
Do not create a named class for this pattern, such as AlphaAllItem, unless it adds real behavior.
A concrete item class is justified only when it owns shader-specific behavior, such as:
- overriding
OnGUI,DrawBlock,OnEndChange,CheckIsPropertyModified, orExecuteReset - synchronizing keywords, render queue, blend state, pass state, runtime flags, or context
- implementing a reusable layout/control behavior that belongs in shared
ShaderGUIItems
Current examples:
ZTestItemis allowed because it forces UIEffect depth behavior.AddToPreMultiplySlideris allowed because its reset default depends onBlendMode.- Popup items are allowed when they update
Context, keywords, render state, or child visibility. BaseColorIntensityItem,AlphaAllItem, andCutOffSliderare not allowed as named wrappers because they only configure shared float/slider items.
Shared items must not reference:
NBShaderRootItemNBShaderInspectorLocalization- NBShader-specific keyword, pass, or flag logic
NBShader-specific labels should be passed into shared items through GUIContent providers from the call site.
Localization Rules
NBShader Inspector user-facing text must go through NBShaderInspectorLocalization.
- Use CSV keys in
NBShaders2/Editor/Localization/NBShaderInspectorLocalization.csv. - CSV format is
key,zh-CN,en-US,zh-CN-tip,en-US-tip. - Tooltip text belongs to the same row as its label key. If a row has no tooltip, leave the tip columns empty.
- Legacy
.tiprows are only compatibility fallback for existing non-label message reads. New label tooltips must not create a separate.tipkey. - Language selection is project-wide and stored by
NBFXProjectSettingsinProjectSettings/NB_FXSettings.asset. - Language selection is edited through Project Settings > Project > NB_FX, not through Unity menu items.
Fallback order:
- current language
zh-CN- code fallback
Key conventions:
- normal labels use
inspector.<block>.<feature>.<name>.label - label tooltips use the same key as the label and read the
<language>-tipcolumn - standalone tooltip/help text that is not attached to a
GUIContentshould use.messageor another explicit text key - pure messages use
inspector.<block>.<feature>.<name>.message - buttons use
inspector.<block>.<feature>.<name>.button - popup options use
inspector.<block>.<feature>.<name>.option.<index>
Code conventions:
- Normal controls should call
NBShaderInspectorLocalization.MakeInspectorContent(key, fallback, tip)or pass a provider that calls it. The helper reads the label frominspector.<key>.labeland the tooltip from the same CSV row first. - HelpBox/message strings should call
NBShaderInspectorLocalization.GetInspectorText(key, fallback). - Popup options should call
NBShaderInspectorLocalization.GetInspectorOptions(key, fallbackArray)or updatePopUpNamesfrom an options provider. - Shared
XuanXuanRenderUtility/Editor/ShaderGUIItemsmust not referenceNBShaderInspectorLocalization. - Shared items that draw visible labels should expose
GUIContentproviders; NBShader call sites provide localized content. - Technical identifiers stay untranslated unless they are deliberately shown as UI copy: shader property names, keyword names, pass names, enum protocol values, render queue numbers, and stencil config keys remain stable.
NBShaders2 Item Rules
NBShaders2/Editor/ShaderGUIItems should only contain NBShader business structure:
- top-level blocks
- secondary business blocks
- shader-specific popup items with side effects
- composite layout items such as the main texture layout
- feature-specific items that synchronize NBShader keywords, pass state, or runtime flags
NBShader-specific item names are acceptable only when the item has NBShader business meaning. They must not be used for generic drawing controls.
Inheritance Rules
1. ShaderGUIItem
ShaderGUIItem is the base class for normal GUI items.
Use it for:
- leaf controls
- composite layout items
- parent-child grouping items without foldout
- texture layout parent items
Important:
- parent-child composition already belongs to
ShaderGUIItem - do not introduce extra “control base class” layers just to express parent-child grouping
2. BlockItem
BlockItem exists only to provide FoldOut behavior.
Responsibilities:
- owns
FoldOutPropertyName - owns foldout open/close lifecycle
- draws foldout title row
- draws children inside foldout content area
It must not be used for:
- non-foldout layout items
- generic feature/control wrappers
3. BigBlockItem
BigBlockItem inherits from BlockItem.
Responsibilities:
- defines the visual style of top-level blocks
- bold title
- spacing
- separator line
BigBlockItem is a style specialization of BlockItem, not a different composition system.
Top-Level Block Rules
These top-level sections should inherit BigBlockItem:
ModeBigBlockItemBaseOptionBigBlockItemMainTexBigBlockItemLightBigBlockItemFeatureBigBlockItemTABigBlockItem
These are first-level inspector sections and should use persisted foldout state.
Secondary Block Rules
Only use BlockItem for a secondary section when:
- it has its own foldout state
- it is semantically a collapsible section
- it contains a group of child items
If a section is only doing layout and grouping, it must stay on ShaderGUIItem.
Layout Rules
All normal inspector row layout must be owned by ShaderGUIItem.
Do not calculate label/control/reset positions independently in leaf items. Use the base rect pipeline:
GetRect()ApplyGlobalRectCompensation()SplitLineRect()SplitControlAndResetRect()
Standard row responsibilities:
BaseRectis the compensated full row rect.LabelRectusesGetLabelWidth(BaseRect).GetLabelWidth()has a fixed minimum (MinLabelWidth) and then grows byLabelWidthRatioonce the inspector row is wide enough.- Do not read or hardcode a fixed label width in business items. If a custom layout needs the normal label/control split, call
SplitLineRect(). ControlRectstarts afterLabelRectand reserves reset space.ResetRectis fixed to the right side and usesResetButtonSize.ControlResetGapis the only standard gap between control and reset.
Do not add local magic numbers for reset width, reset gap, or standard control expansion in business items. If a layout constant is needed by more than one item, add it to ShaderGUIItem.
Global row compensation:
GlobalRectXOffsetshifts the whole row left/right.GlobalRectWidthExpansionexpands the whole row width.- These constants compensate meaningless inspector-side margin and must be applied at the rect entry point, before splitting label/control/reset.
Any item that calls EditorGUILayout.GetControlRect() directly must immediately pass the result through ApplyGlobalRectCompensation(), unless there is a specific documented reason not to.
Editor indent rules:
EditorGUI.indentLevelis only a hierarchy level counter.- Do not introduce a separate per-block indent-count constant.
- Unity's internal per-level indent width is represented by
UnityEditorGUIIndentWidth. - The desired NBShader GUI visual indent width is represented by
EditorGUIIndentWidth. ApplyLabelIndentWidth()converts Unity's built-inEditorGUI.LabelFieldindent width into the desired visual indent width for the NBShader GUI rect system.- Direct
GUI.Label,GUI.Toggle, texture preview rects, and other non-EditorGUI.LabelFieldlabels do not get Unity's built-in label indent. UseApplyDirectLabelIndentWidth()for their label start. - Foldout arrows must be positioned from the final visual label text x, not from
BaseRect.x. UseShaderGUIFoldOutHelper/GetEditorLabelTextX()and do not callEditorGUI.Foldout()directly in business items. - Do not hardcode a local texture or foldout indent width. Three-line texture groups must use
ApplyDirectLabelIndentWidth()for the texture preview left edge andSplitLineRect()for Tilling/Offset control/reset positions.
Control indent compensation:
ControlIndentCompensationis not the same thing as row indent.- It only adjusts
ControlRectby movingxleft and increasingwidth. - Use it only to compensate controls whose Unity internal drawing is shifted right inside the passed rect.
- Do not apply it to controls that draw exactly inside the passed rect, such as single-line
ColorField; callGetRect(false)orSplitLineRect(..., false)for those rows. EditorGUI.ColorFielddraws the visible swatch throughEditorStyles.colorField.padding.Remove(position). Labeled color rows may compensate this padding insideColorItem; no-label color rows keep their own local inset. Do not add Color-only constants toShaderGUIItem.- Keep
LabelRectandResetRectindependent from this compensation. - Composite layouts may disable it with
applyControlIndentCompensation: falseand then apply a local compensation only to the actual control rect when needed.
Full-width or no-label rows:
- Rows such as full-width color bars should not automatically use label/control semantics.
- If a row has no label, call
SplitControlAndResetRect()directly and disable control compensation when the row must preserve its left edge. - Reset alignment should still come from
SplitControlAndResetRect().
Texture rows:
- All texture object inputs should use the shared three-row texture layout (
TexturePropertyGroupItemthroughTextureItem) instead ofMaterialEditor.TexturePropertySingleLine(). - Texture rows without editable Tilling/Offset should still reserve the three-row texture object area and leave the Tilling/Offset rows empty.
- Texture color rows remain separate no-label color rows after the three-row texture object group.
Animated Property Rules
Animated property highlight must follow Unity's native material inspector behavior.
Do not manually set animated controls to a fixed color such as Color.red.
Use the shared ShaderGUIItem animated scope helpers:
BeginAnimatedPropertyBackground(Rect totalPosition, MaterialProperty property)EndAnimatedPropertyBackground(bool scopeActive)
These helpers must use MaterialEditor.BeginAnimatedCheck() and MaterialEditor.EndAnimatedCheck(). They must not implement their own color decision with AnimationMode.IsPropertyAnimated().
Reason:
- Unity's original
MaterialEditor.ShaderProperty()wraps controls withBeginAnimatedCheck()/EndAnimatedCheck(). - Unity chooses between animated, recorded, and candidate colors internally.
- Unity restores the previous
GUI.backgroundColorthrough a stack, so nested drawing does not leak color state.
Standard item rule:
- Normal leaf items that use
ShaderGUIItem.OnGUI()should rely on the base animated scope aroundDrawController().
Custom OnGUI() rule:
- If an item overrides
OnGUI()or draws multiple independent controls manually, it must explicitly wrap each actual editable control with the correctMaterialProperty. - Do not wrap a composite row with one property if the row contains controls backed by different properties.
- Always call
EndAnimatedPropertyBackground()in the same draw path after the control is drawn.
Multi-property control rule:
ShaderGUISliderItemwithRangePropertyNamemust wrap the min/max fields with the range vector property and the slider with the main float property.- Texture scale/offset controls must wrap their
Vector2Fieldcontrols with the texture property when editingtextureScaleAndOffset, or with the vector property when editing a vector ST property.
Animation refresh rule:
ShaderGUIRootItemmust update incomingMaterialProperty[]intoPropertyInfoDiceveryOnGUI.- In animation mode, the material inspector should repaint so animated values and highlight state refresh while scrubbing or recording.
- Do not cache animated-state booleans. Caching property path strings is allowed for diagnostics or non-background queries.
Texture GUI Rules
For texture-related GUI:
- the texture layout parent item should inherit
ShaderGUIItem - it is a layout/composition item, not a
BlockItem - texture object field, texture color, and texture
Tilling/Offsetare separate GUI items - the parent texture layout item is responsible only for arranging these child items
- texture layout must not move label text as a side effect of control compensation
- for
Tilling/Offset, calculate label rects from the uncompensated vector rect first, then apply control compensation only to theVector2Fieldrect if visual control alignment requires it - texture, texture label, texture color,
Tilling/Offset, and reset buttons must still use sharedShaderGUIItemlayout constants where applicable - texture ST animated highlight must use Unity's texture
MaterialPropertyanimated check, not manual_ST.x/y/z/wcolor logic
Required structure:
- texture parent layout item
- texture object field item
- texture color item
- texture
Tilling/Offsetitem
Anti-Rules
Do not introduce these kinds of abstractions again unless there is a concrete need:
- generic
ControlItembase classes with no real behavior - generic
FeatureItembase classes with no real semantics - wrapper layers that only rename existing base classes
If an abstraction does not define a stable responsibility boundary, do not keep it.