Skip to content
Merged

Dev #87

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 158 additions & 1 deletion .cursor/rules/lwgui-base-rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,168 @@ description: LWGUI Base Rules
alwaysApply: true
---

该项目是个开源Unity Shader GUI插件, 具体介绍在README.md中.
## 项目定位与核心目标

LWGUI是个开源Unity Shader GUI插件, 目标是在 Unity Inspector 中, 将 Shader 属性从“线性参数列表”升级为“可分组、可条件显示、可扩展交互”的编辑体验。

它以 `ShaderGUI` 为入口, 通过属性标签与自定义规则把不同属性分发给对应 Drawer/Decorator, 并借助 Helper 与 MetaData 管理状态、缓存和资源联动。

## 架构与分层原理

整体可理解为三层:

1. **Editor 层(核心能力)**
- 负责 Inspector 绘制、属性解析、UI 交互、菜单行为、状态缓存、资产同步。
- 主要目录: `Editor/`
2. **Runtime 层(运行时补充)**
- 提供少量可在运行时复用的数据结构与 Timeline 相关功能。
- 主要目录: `Runtime/`
3. **UnityEditorExtension 层(编辑器增强)**
- 放置附加编辑器窗口和扩展工具, 如 `LwguiGradientEditor`。
- 主要目录: `UnityEditorExtension/`

核心调用链可概括为:

1. `Editor/LWGUI.cs` 接管材质 Inspector 绘制入口。
2. 解析 Shader 属性、MaterialProperty 与标签信息。
3. 将属性分发给 `ShaderDrawers` / `BasicDrawers` / `ExtraDrawers` / `ExtraDecorators`。
4. 在绘制过程中通过 `Helper` 处理上下文菜单、Ramp/Preset/Toolbar 等跨模块行为。
5. 通过 `MetaData` 维护跨帧、跨材质、跨 Shader 的状态作用域。
6. 由 `AssetProcessor` 与 `ScriptableObject` 处理资源导入、引用同步、图集维护等生命周期事件。

## 代码结构与职责总览

- `Editor/LWGUI.cs`
- ShaderGUI 主入口, 组织一次 Inspector 绘制流程与各阶段事件。
- `Editor/ShaderDrawerBase.cs`
- Drawer 基类与通用能力, 定义扩展点与基础契约。
- `Editor/BasicDrawers/`
- 基础结构 Drawer, 如折叠组、子项容器等。
- `Editor/ShaderDrawers/`
- Shader 属性级绘制器, 处理属性到 UI 的核心映射。
- `Editor/ShaderDrawers/ExtraDrawers/`
- 额外类型 Drawer, 如 Numeric/Texture/Vector/Other。
- `Editor/ExtraDecorators/`
- 装饰器能力, 包括显示样式、条件显示、逻辑控制、结构组织。
- `Editor/Helper/`
- 跨模块工具集, 如 `ContextMenuHelper`、`RampHelper`、`PresetHelper`、`ToolbarHelper`。
- `Editor/MetaData/`
- 缓存与状态域管理, 分层处理 Inspector/Material/Shader 维度数据。
- `Editor/ScriptableObject/`
- 资源数据定义, 如 `LwguiRampAtlas`、`LwguiShaderPropertyPreset`、`GradientObject`。
- `Editor/AssetProcessor/`
- 处理资产导入、改名、变更监听, 保证编辑器逻辑与资源状态一致。
- `Editor/Timeline/` 与 `Runtime/Timeline/`
- Timeline 相关编辑器与运行时能力。
- `Runtime/LwguiGradient/`
- 运行时可用的渐变数据结构与相关逻辑。
- `Test/`
- 回归和示例资源(Shader/Material/Preset), 用于验证主要路径。

## MetaData 详细说明: 数据结构与生命周期

`Editor/MetaData/` 的核心目的是“避免状态串扰并减少重复计算”, 通过不同作用域隔离缓存。

### 1) PerInspectorData (Inspector 作用域)

- 作用域: 单个 Inspector 窗口/会话内。
- 典型用途:
- UI 折叠展开状态。
- 临时交互态(当前编辑目标、当前菜单上下文等)。
- 本次绘制周期可复用的瞬时缓存。
- 生命周期:
- Inspector 首次绘制时初始化。
- 每次 `OnGUI` 过程中读写更新。
- Inspector 销毁、重载或上下文变化时释放/重建。
- 设计意义:
- 避免不同 Inspector 实例互相污染状态。

### 2) PerMaterialData (Material 作用域)

- 作用域: 单个 Material 资产(或实例)维度。
- 典型用途:
- 与具体材质强相关的缓存结果。
- 针对材质属性计算出的派生信息。
- 材质级别的 UI 辅助状态。
- 生命周期:
- 材质首次被 Inspector/工具访问时创建。
- 在材质属性变更、重导入、替换 Shader 时刷新关键字段。
- 材质失效、移除引用或缓存清理策略触发时回收。
- 设计意义:
- 保证同一 Shader 的不同材质不会共享错误状态。

### 3) PerShaderData (Shader 作用域)

- 作用域: 单个 Shader 维度, 被多个材质共享。
- 典型用途:
- Shader 属性元信息解析缓存(属性列表、标签解析结果、分组结构等)。
- 与 Shader 文本/结构相关且可复用的静态或半静态数据。
- 生命周期:
- Shader 首次被使用时构建缓存。
- Shader 重新导入、源码变化或相关依赖更新时失效重建。
- 编辑器域重载后按需懒重建。
- 设计意义:
- 减少重复解析成本, 提升大材质集场景下的 Inspector 性能。

### MetaData 整体生命周期流转(推荐理解模型)

1. **进入 Inspector**
- 定位当前材质与 Shader, 获取/创建 `PerInspectorData`、`PerMaterialData`、`PerShaderData`。
2. **绘制阶段**
- Drawer/Decorator 读取对应作用域数据, 执行条件显示、结构布局和交互逻辑。
3. **交互与修改阶段**
- 用户修改属性后, 写回材质并更新必要缓存; 需要时触发上下文工具或资源逻辑。
4. **资产变化阶段**
- 若 Shader/材质/关联资源发生导入或结构变化, 由监听逻辑使相关缓存失效并重建。
5. **退出或重载阶段**
- Inspector 级临时状态释放; 材质/Shader 级缓存按策略保留或清理。

## ShaderGUI 重要事件与调用时机

以下描述按 Unity Inspector 常见生命周期理解, 便于排查时对照:

1. **入口阶段(`LWGUI` 作为 `ShaderGUI` 被调用)**
- 当材质在 Inspector 中被选中并需要重绘时触发。
- 典型动作: 建立上下文、准备属性列表、拉取 MetaData。

2. **OnGUI 主绘制阶段**
- 每次 Inspector Repaint / Layout / 交互事件中都会进入。
- 典型动作:
- 解析并遍历 `MaterialProperty`。
- 调用各类 Drawer/Decorator 完成结构与控件绘制。
- 根据条件装饰器决定显示/隐藏和禁用态。

3. **属性变更检测与写回阶段**
- 在 GUI 变更检查通过后触发。
- 典型动作:
- 将新值写回材质属性。
- 触发关键字、依赖属性、联动逻辑刷新。
- 更新 `PerMaterialData` 相关缓存。

4. **上下文行为阶段(菜单/工具条/快捷动作)**
- 用户打开右键菜单或触发工具条功能时触发。
- 典型动作:
- 走 `ContextMenuHelper`、`ToolbarHelper`、`RampHelper`、`PresetHelper` 的逻辑路径。
- 可能引发资源引用更新、预设应用或图集操作。

5. **资源生命周期联动阶段(导入/改名/重建)**
- 当 Shader、贴图、预设、ScriptableObject 等相关资产变化时触发。
- 典型动作:
- `AssetProcessor` 响应变化并同步引用关系。
- 标记并重建受影响的 `PerShaderData` / `PerMaterialData`。

6. **域重载与重初始化阶段**
- 脚本重编译、进入/退出 PlayMode(视配置)后发生。
- 典型动作:
- 静态缓存失效或重置。
- 下一次 Inspector 绘制时按需懒初始化。

## 注意事项

你在修改代码时需要遵循以下要求:

- 沿用现有代码风格
- 保持代码简洁, 不做不必要的修改
- 使用清晰的命名替代简单代码的注释
- 仅对于大段复杂代码和一些不常见的情况做注释
- 创建或修改Drawer后, 在ReadMe文件中按现有格式同步修改. README.md中用英文, README_CN.md中用中文.
3 changes: 2 additions & 1 deletion Editor/LWGUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ private void DrawProperty(MaterialProperty prop)
var revertButtonRect = RevertableHelper.SplitRevertButtonRect(ref rect);

var enabled = GUI.enabled;
if (propStaticData.isReadOnly) GUI.enabled = false;
if (propStaticData.isReadOnly || !propDynamicData.isActive)
GUI.enabled = false;
Helper.BeginProperty(rect, prop, metaDatas);
ContextMenuHelper.DoPropertyContextMenus(rect, prop, metaDatas);

Expand Down
5 changes: 5 additions & 0 deletions Editor/MetaData/PerMaterialData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class PropertyDynamicData
public bool hasChildrenModified = false; // Are Children properties modified in the material?
public bool hasRevertChanged = false; // Used to call property EndChangeCheck()
public bool isShowing = true; // ShowIf() result
public bool isActive = true; // ActiveIf() result
public bool isAnimated = false; // Material Parameter Animation preview in Timeline is activated
}

Expand Down Expand Up @@ -168,6 +169,10 @@ public void Init(Shader shader, Material material, MaterialEditor editor, Materi

// Get ShowIf() results
ShowIfDecorator.GetShowIfResult(propStaticData, propDynamicData, this);

// Get ActiveIf() results
if (propStaticData.activeIfDatas.Count > 0)
propDynamicData.isActive = ShowIfDecorator.GetShowIfResultFromMaterial(propStaticData.activeIfDatas, this.material);
}

// Get Shader Perf Stats
Expand Down
1 change: 1 addition & 0 deletions Editor/MetaData/PerShaderData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public partial class PropertyStaticData
public bool isReadOnly = false; // [ReadOnly]
public bool isHidden = false; // [Hidden]
public List<ShowIfDecorator.ShowIfData> showIfDatas = new List<ShowIfDecorator.ShowIfData>(); // [ShowIf()]
public List<ShowIfDecorator.ShowIfData> activeIfDatas = new List<ShowIfDecorator.ShowIfData>(); // [ActiveIf()]
public string conditionalDisplayKeyword = string.Empty; // [Group(groupName_conditionalDisplayKeyword)]

// Drawers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Jason Ma

using UnityEditor;
using UnityEngine;

namespace LWGUI
{
/// <summary>
/// Control whether the property can be edited based on multiple conditions.
///
/// logicalOperator: And | Or (Default: And).
/// propName: Target Property Name used for comparison.
/// compareFunction: Less (L) | Equal (E) | LessEqual (LEqual / LE) | Greater (G) | NotEqual (NEqual / NE) | GreaterEqual (GEqual / GE).
/// value: Target Property Value used for comparison.
/// </summary>
public class ActiveIfDecorator : SubDrawer
{
public ShowIfDecorator.ShowIfData activeIfData = new();

public ActiveIfDecorator(string propName, string comparisonMethod, float value) : this("And", propName, comparisonMethod, value) { }

public ActiveIfDecorator(string logicalOperator, string propName, string compareFunction, float value)
{
activeIfData.logicalOperator = logicalOperator.ToLower() == "or" ? ShowIfDecorator.LogicalOperator.Or : ShowIfDecorator.LogicalOperator.And;
activeIfData.targetPropertyName = propName;
activeIfData.compareFunction = ShowIfDecorator.ParseCompareFunction(compareFunction);
activeIfData.value = value;
}

protected override float GetVisibleHeight(MaterialProperty prop) { return 0; }

public override void BuildStaticMetaData(Shader inShader, MaterialProperty inProp, MaterialProperty[] inProps, PropertyStaticData inoutPropertyStaticData)
{
inoutPropertyStaticData.activeIfDatas.Add(activeIfData);
}

public override void DrawProp(Rect position, MaterialProperty prop, GUIContent label, MaterialEditor editor) { }
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class ShowIfData

public ShowIfData showIfData = new();

private readonly Dictionary<string, string> _compareFunctionLUT = new()
private static readonly Dictionary<string, string> _compareFunctionLUT = new()
{
{ "Less", "Less" },
{ "L", "Less" },
Expand All @@ -54,18 +54,27 @@ public class ShowIfData
{ "GE", "GreaterEqual" },
};

public static CompareFunction ParseCompareFunction(string compareFunction)
{
if (!_compareFunctionLUT.TryGetValue(compareFunction, out var compareFunctionName)
|| !Enum.IsDefined(typeof(CompareFunction), compareFunctionName))
{
Debug.LogError("LWGUI: Invalid compareFunction: '"
+ compareFunction
+ "', Must be one of the following: Less (L) | Equal (E) | LessEqual (LEqual / LE) | Greater (G) | NotEqual (NEqual / NE) | GreaterEqual (GEqual / GE).");
return CompareFunction.Equal;
}

return (CompareFunction)Enum.Parse(typeof(CompareFunction), compareFunctionName);
}

public ShowIfDecorator(string propName, string comparisonMethod, float value) : this("And", propName, comparisonMethod, value) { }

public ShowIfDecorator(string logicalOperator, string propName, string compareFunction, float value)
{
showIfData.logicalOperator = logicalOperator.ToLower() == "or" ? LogicalOperator.Or : LogicalOperator.And;
showIfData.targetPropertyName = propName;
if (!_compareFunctionLUT.ContainsKey(compareFunction) || !Enum.IsDefined(typeof(CompareFunction), _compareFunctionLUT[compareFunction]))
Debug.LogError("LWGUI: Invalid compareFunction: '"
+ compareFunction
+ "', Must be one of the following: Less (L) | Equal (E) | LessEqual (LEqual / LE) | Greater (G) | NotEqual (NEqual / NE) | GreaterEqual (GEqual / GE).");
else
showIfData.compareFunction = (CompareFunction)Enum.Parse(typeof(CompareFunction), _compareFunctionLUT[compareFunction]);
showIfData.compareFunction = ParseCompareFunction(compareFunction);
showIfData.value = value;
}

Expand Down
Loading