# ESEngine 材质系统统一架构重构方案 ## 问题概述 当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复: | 重复项 | Sprite | UI | 重复度 | |--------|--------|----|----| | 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% | | 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% | | ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% | | ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% | **根本原因**:缺乏统一的材质覆盖接口抽象层。 --- ## 一、统一材质覆盖接口 ### 1.1 定义通用接口 在 `@esengine/material-system` 包中定义统一接口: ```typescript // packages/material-system/src/interfaces/IMaterialOverridable.ts /** * Material property override definition. * 材质属性覆盖定义。 */ export interface MaterialPropertyOverride { type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int'; value: number | number[]; } export type MaterialOverrides = Record; /** * Interface for components that support material property overrides. * 支持材质属性覆盖的组件接口。 */ export interface IMaterialOverridable { /** Material GUID for asset reference | 材质资产引用的 GUID */ materialGuid: string; /** Current material overrides | 当前材质覆盖 */ readonly materialOverrides: MaterialOverrides; /** Get current material ID | 获取当前材质 ID */ getMaterialId(): number; /** Set material ID | 设置材质 ID */ setMaterialId(id: number): void; // Uniform setters setOverrideFloat(name: string, value: number): this; setOverrideVec2(name: string, x: number, y: number): this; setOverrideVec3(name: string, x: number, y: number, z: number): this; setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this; setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this; setOverrideInt(name: string, value: number): this; // Uniform getters getOverride(name: string): MaterialPropertyOverride | undefined; removeOverride(name: string): this; clearOverrides(): this; hasOverrides(): boolean; } ``` ### 1.2 创建 Mixin 实现 使用 Mixin 模式避免代码重复: ```typescript // packages/material-system/src/mixins/MaterialOverridableMixin.ts import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable'; /** * Mixin that provides material override functionality. * 提供材质覆盖功能的 Mixin。 */ export function MaterialOverridableMixin {}>(Base: TBase) { return class extends Base { materialGuid: string = ''; private _materialId: number = 0; private _materialOverrides: MaterialOverrides = {}; get materialOverrides(): MaterialOverrides { return this._materialOverrides; } getMaterialId(): number { return this._materialId; } setMaterialId(id: number): void { this._materialId = id; } setOverrideFloat(name: string, value: number): this { this._materialOverrides[name] = { type: 'float', value }; return this; } setOverrideVec2(name: string, x: number, y: number): this { this._materialOverrides[name] = { type: 'vec2', value: [x, y] }; return this; } setOverrideVec3(name: string, x: number, y: number, z: number): this { this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] }; return this; } setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this { this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] }; return this; } setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this { this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] }; return this; } setOverrideInt(name: string, value: number): this { this._materialOverrides[name] = { type: 'int', value: Math.floor(value) }; return this; } getOverride(name: string): MaterialPropertyOverride | undefined { return this._materialOverrides[name]; } removeOverride(name: string): this { delete this._materialOverrides[name]; return this; } clearOverrides(): this { this._materialOverrides = {}; return this; } hasOverrides(): boolean { return Object.keys(this._materialOverrides).length > 0; } }; } ``` --- ## 二、Shader Property 元数据系统 ### 2.1 定义属性元数据接口 ```typescript // packages/material-system/src/interfaces/IShaderProperty.ts /** * Shader property UI metadata. * 着色器属性 UI 元数据。 */ export interface ShaderPropertyMeta { /** Property type | 属性类型 */ type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture'; /** Display label (supports i18n key) | 显示标签(支持 i18n 键) */ label: string; /** Property group for organization | 属性分组 */ group?: string; /** Default value | 默认值 */ default?: number | number[] | string; // Numeric constraints min?: number; max?: number; step?: number; /** UI hints | UI 提示 */ hint?: 'range' | 'angle' | 'hdr' | 'normal'; /** Tooltip description | 工具提示描述 */ tooltip?: string; /** Whether to hide in inspector | 是否在检查器中隐藏 */ hidden?: boolean; } /** * Extended shader definition with property metadata. * 带属性元数据的扩展着色器定义。 */ export interface ShaderAssetDefinition { /** Shader name | 着色器名称 */ name: string; /** Display name for UI | UI 显示名称 */ displayName?: string; /** Shader description | 着色器描述 */ description?: string; /** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/ vertexSource: string; /** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/ fragmentSource: string; /** Property metadata for inspector | 检查器属性元数据 */ properties?: Record; /** Render queue / order | 渲染队列/顺序 */ renderQueue?: number; /** Preset blend mode | 预设混合模式 */ blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque'; } ``` ### 2.2 .shader 资产文件格式 ```json { "$schema": "esengine://schemas/shader.json", "version": 1, "name": "Shiny", "displayName": "闪光效果 | Shiny Effect", "description": "扫光高亮动画着色器 | Sweeping highlight animation shader", "vertexSource": "./shaders/sprite.vert", "fragmentSource": "./shaders/shiny.frag", "blendMode": "alpha", "renderQueue": 2000, "properties": { "u_shinyProgress": { "type": "float", "label": "进度 | Progress", "group": "Animation", "default": 0, "min": 0, "max": 1, "step": 0.01, "hidden": true }, "u_shinyWidth": { "type": "float", "label": "宽度 | Width", "group": "Effect", "default": 0.25, "min": 0, "max": 1, "step": 0.01, "tooltip": "闪光带宽度 | Width of the shiny band" }, "u_shinyRotation": { "type": "float", "label": "角度 | Rotation", "group": "Effect", "default": 2.25, "min": 0, "max": 6.28, "step": 0.01, "hint": "angle" }, "u_shinySoftness": { "type": "float", "label": "柔和度 | Softness", "group": "Effect", "default": 1.0, "min": 0, "max": 1, "step": 0.01 }, "u_shinyBrightness": { "type": "float", "label": "亮度 | Brightness", "group": "Effect", "default": 1.0, "min": 0, "max": 2, "step": 0.01 }, "u_shinyGloss": { "type": "float", "label": "光泽度 | Gloss", "group": "Effect", "default": 1.0, "min": 0, "max": 1, "step": 0.01, "tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted" } } } ``` --- ## 三、统一效果组件/系统架构 ### 3.1 抽取通用 ShinyEffect 基类 ```typescript // packages/material-system/src/effects/BaseShinyEffect.ts import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework'; /** * Base shiny effect configuration (shared between UI and Sprite). * 基础闪光效果配置(UI 和 Sprite 共享)。 */ export abstract class BaseShinyEffect extends Component { // ============= Effect Parameters ============= @Serialize() @Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 }) public width: number = 0.25; @Serialize() @Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 }) public rotation: number = 129; @Serialize() @Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 }) public softness: number = 1.0; @Serialize() @Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 }) public brightness: number = 1.0; @Serialize() @Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 }) public gloss: number = 1.0; // ============= Animation Settings ============= @Serialize() @Property({ type: 'boolean', label: 'Play' }) public play: boolean = true; @Serialize() @Property({ type: 'boolean', label: 'Loop' }) public loop: boolean = true; @Serialize() @Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 }) public duration: number = 2.0; @Serialize() @Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 }) public loopDelay: number = 2.0; @Serialize() @Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 }) public initialDelay: number = 0; // ============= Runtime State ============= public progress: number = 0; public elapsedTime: number = 0; public inDelay: boolean = false; public delayRemaining: number = 0; public initialDelayProcessed: boolean = false; reset(): void { this.progress = 0; this.elapsedTime = 0; this.inDelay = false; this.delayRemaining = 0; this.initialDelayProcessed = false; } start(): void { this.reset(); this.play = true; } stop(): void { this.play = false; } getRotationRadians(): number { return this.rotation * Math.PI / 180; } } ``` ### 3.2 通用动画更新逻辑 ```typescript // packages/material-system/src/effects/ShinyEffectAnimator.ts import type { BaseShinyEffect } from './BaseShinyEffect'; import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable'; import { BuiltInShaders } from '../types'; /** * Shared animator logic for shiny effect. * 闪光效果共享的动画逻辑。 */ export class ShinyEffectAnimator { /** * Update animation state. * 更新动画状态。 */ static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void { if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) { shiny.delayRemaining = shiny.initialDelay; shiny.inDelay = true; shiny.initialDelayProcessed = true; } if (shiny.inDelay) { shiny.delayRemaining -= deltaTime; if (shiny.delayRemaining <= 0) { shiny.inDelay = false; shiny.elapsedTime = 0; } return; } shiny.elapsedTime += deltaTime; shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0); if (shiny.progress >= 1.0) { if (shiny.loop) { shiny.inDelay = true; shiny.delayRemaining = shiny.loopDelay; shiny.progress = 0; shiny.elapsedTime = 0; } else { shiny.play = false; shiny.progress = 1.0; } } } /** * Apply material overrides. * 应用材质覆盖。 */ static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void { if (target.getMaterialId() === 0) { target.setMaterialId(BuiltInShaders.Shiny); } target.setOverrideFloat('u_shinyProgress', shiny.progress); target.setOverrideFloat('u_shinyWidth', shiny.width); target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians()); target.setOverrideFloat('u_shinySoftness', shiny.softness); target.setOverrideFloat('u_shinyBrightness', shiny.brightness); target.setOverrideFloat('u_shinyGloss', shiny.gloss); } } ``` --- ## 四、Material Inspector 设计 ### 4.1 组件架构 ``` MaterialPropertiesEditor (容器组件) ├── ShaderSelector (着色器选择器) ├── PropertyGroup (属性分组) │ ├── FloatProperty (浮点属性) │ ├── VectorProperty (向量属性) │ ├── ColorProperty (颜色属性) │ └── TextureProperty (纹理属性) └── OverrideIndicator (覆盖指示器) ``` ### 4.2 核心组件 ```typescript // packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx interface MaterialPropertiesEditorProps { /** Target component implementing IMaterialOverridable */ target: IMaterialOverridable; /** Current shader definition with property metadata */ shaderDef?: ShaderAssetDefinition; /** Callback when property changes */ onChange?: (name: string, value: MaterialPropertyOverride) => void; } export const MaterialPropertiesEditor: React.FC = ({ target, shaderDef, onChange }) => { // Group properties by their group field const groupedProps = useMemo(() => { if (!shaderDef?.properties) return {}; const groups: Record> = {}; for (const [name, meta] of Object.entries(shaderDef.properties)) { if (meta.hidden) continue; const group = meta.group || 'Default'; if (!groups[group]) groups[group] = []; groups[group].push([name, meta]); } return groups; }, [shaderDef]); return (
target.setMaterialId(id)} /> {Object.entries(groupedProps).map(([group, props]) => ( {props.map(([name, meta]) => ( { applyOverride(target, name, meta.type, value); onChange?.(name, target.getOverride(name)!); }} /> ))} ))}
); }; ``` --- ## 五、实施计划 ### Phase 1: 接口层 (1-2 天) 1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`) 2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`) 3. **导出新接口** (`packages/material-system/src/index.ts`) ### Phase 2: 重构现有组件 (2-3 天) 1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin 2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin 3. **删除重复代码**:移除各组件中的重复材质方法 ### Phase 3: 统一效果系统 (2-3 天) 1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`) 2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`) 3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect 4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect 5. **重构系统**:使用 ShinyEffectAnimator ### Phase 4: Shader Property 系统 (2-3 天) 1. **定义 ShaderPropertyMeta 接口** 2. **扩展 ShaderDefinition** 添加 properties 字段 3. **创建 ShaderLoader** 支持 .shader 文件 4. **注册内置着色器属性元数据** ### Phase 5: Material Inspector (3-4 天) 1. **创建 MaterialPropertiesEditor 组件** 2. **创建 PropertyField 组件** (Float, Vector, Color, Texture) 3. **集成到现有 Inspector 系统** 4. **支持实时预览** --- ## 六、文件修改清单 | 优先级 | 包 | 文件 | 操作 | |--------|-----|------|------| | P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 | | P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 | | P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 | | P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 | | P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 | | P1 | sprite | `src/SpriteComponent.ts` | 重构 | | P1 | ui | `src/components/UIRenderComponent.ts` | 重构 | | P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 | | P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 | | P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 | | P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 | | P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 | | P3 | editor-app | `src/components/inspectors/material/*` | 新建 | --- ## 七、Transform 组件统一(可选) ### 7.1 现状分析 | 特性 | TransformComponent | UITransformComponent | |------|-------------------|---------------------| | **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) | | **尺寸** | ❌ 无 | ✅ width/height + 约束 | | **锚点系统** | ❌ 无 | ✅ anchorMin/Max | | **3D 支持** | ✅ IVector3 | ❌ 纯 2D | | **可见性** | ❌ 无 | ✅ visible, alpha | ### 7.2 结论 **不建议完全合并**,但可提取公共基类: ```typescript // packages/engine-core/src/interfaces/ITransformBase.ts export interface ITransformBase { /** 旋转角度(度) | Rotation in degrees */ rotation: number; /** X 缩放 | Scale X */ scaleX: number; /** Y 缩放 | Scale Y */ scaleY: number; /** 本地到世界矩阵 | Local to world matrix */ readonly localToWorldMatrix: Matrix2D; /** 是否需要更新 | Dirty flag */ isDirty: boolean; /** 世界坐标 X | World position X */ readonly worldX: number; /** 世界坐标 Y | World position Y */ readonly worldY: number; /** 世界旋转 | World rotation */ readonly worldRotation: number; /** 世界缩放 X | World scale X */ readonly worldScaleX: number; /** 世界缩放 Y | World scale Y */ readonly worldScaleY: number; } ``` ### 7.3 收益 - 渲染系统可以统一处理 `ITransformBase` - 减少 SpriteRenderSystem 和 UIRenderSystem 的重复 - Gizmo 系统可以共享变换操作逻辑 --- ## 八、向后兼容性 1. **接口兼容**:现有组件的 API 保持不变 2. **序列化兼容**:不改变现有序列化格式 3. **渐进迁移**:可分阶段进行,不影响现有功能