Files
esengine/docs/architecture/material-system-refactor.md
YHH 536c4c5593 refactor(ui): UI 系统架构重构 (#309)
* feat(ui): 动态图集系统与渲染调试增强

## 核心功能

### 动态图集系统 (Dynamic Atlas)
- 新增 DynamicAtlasManager:运行时纹理打包,支持 MaxRects 算法
- 新增 DynamicAtlasService:自动纹理加载与图集管理
- 新增 BinPacker:高效矩形打包算法
- 支持动态/固定两种扩展策略
- 自动 UV 重映射,实现 UI 元素合批渲染

### Frame Debugger 增强
- 新增合批分析面板,显示批次中断原因
- 新增 UI 元素层级信息(depth, worldOrderInLayer)
- 新增实体高亮功能,点击可在场景中定位
- 新增动态图集可视化面板
- 改进渲染原语详情展示

### 闪光效果 (Shiny Effect)
- 新增 UIShinyEffectComponent:UI 闪光参数配置
- 新增 UIShinyEffectSystem:材质覆盖驱动的闪光动画
- 新增 ShinyEffectComponent/System(Sprite 版本)

## 引擎层改进

### Rust 纹理管理扩展
- create_blank_texture:创建空白 GPU 纹理
- update_texture_region:局部纹理更新
- 支持动态图集的 GPU 端操作

### 材质系统
- 新增 effects/ 目录:ShinyEffect 等效果实现
- 新增 interfaces/ 目录:IMaterial 等接口定义
- 新增 mixins/ 目录:可组合的材质功能

### EngineBridge 扩展
- 新增 createBlankTexture/updateTextureRegion 方法
- 改进纹理加载回调机制

## UI 渲染改进
- UIRenderCollector:支持合批调试信息
- 稳定排序:addIndex 保证渲染顺序一致性
- 九宫格渲染优化
- 材质覆盖支持

## 其他改进
- 国际化:新增 Frame Debugger 相关翻译
- 编辑器:新增渲染调试入口
- 文档:新增架构设计文档目录

* refactor(ui): 引入新基础组件架构与渲染工具函数

Phase 1 重构 - 组件职责分离与代码复用:

新增基础组件层:
- UIGraphicComponent: 所有可视 UI 元素的基类(颜色、透明度、raycast)
- UIImageComponent: 纹理显示组件(支持简单、切片、平铺、填充模式)
- UISelectableComponent: 可交互元素的基类(状态管理、颜色过渡)

新增渲染工具:
- UIRenderUtils: 提取共享的坐标计算、边框渲染、阴影渲染等工具函数
- getUIRenderTransform: 统一的变换数据提取
- renderBorder/renderShadow: 复用的边框和阴影渲染逻辑

新增渲染系统:
- UIGraphicRenderSystem: 处理新基础组件的统一渲染器

重构现有系统:
- UIRectRenderSystem: 使用新工具函数,移除重复代码
- UIButtonRenderSystem: 使用新工具函数,移除重复代码

这些改动为后续统一渲染系统奠定基础。

* refactor(ui): UIProgressBarRenderSystem 使用渲染工具函数

- 使用 getUIRenderTransform 替代手动变换计算
- 使用 renderBorder 工具函数替代重复的边框渲染
- 使用 lerpColor 工具函数替代重复的颜色插值
- 简化方法签名,使用 UIRenderTransform 类型
- 移除约 135 行重复代码

* refactor(ui): Slider 和 ScrollView 渲染系统使用工具函数

- UISliderRenderSystem: 使用 getUIRenderTransform,简化方法签名
- UIScrollViewRenderSystem: 使用 getUIRenderTransform,简化方法签名
- 统一使用 UIRenderTransform 类型减少参数传递
- 消除重复的变换计算代码

* refactor(ui): 使用 UIWidgetMarker 消除硬编码组件依赖

- 新增 UIWidgetMarker 标记组件
- UIRectRenderSystem 改为检查标记而非硬编码4种组件类型
- 各 Widget 渲染系统自动添加标记组件
- 减少模块间耦合,提高可扩展性

* feat(ui): 实现 Canvas 隔离机制

- 新增 UICanvasComponent 定义 Canvas 渲染组
- UITransformComponent 添加 Canvas 相关字段:canvasEntityId, worldSortingLayer, pixelPerfect
- UILayoutSystem 传播 Canvas 设置给子元素
- UIRenderUtils 使用 Canvas 继承的排序层
- 支持嵌套 Canvas 和不同渲染模式

* refactor(ui): 统一纹理管理工具函数

Phase 4: 纹理管理统一

新增:
- UITextureUtils.ts: 统一的纹理描述符接口和验证函数
  - UITextureDescriptor: 支持 GUID/textureId/path 多种纹理源
  - isValidTextureGuid: GUID 验证
  - getTextureKey: 获取用于合批的纹理键
  - normalizeTextureDescriptor: 规范化各种输入格式
- utils/index.ts: 工具函数导出

修改:
- UIGraphicRenderSystem: 使用新的纹理工具函数
- index.ts: 导出纹理工具类型和函数

* refactor(ui): 实现统一的脏标记机制

Phase 5: Dirty 标记机制

新增:
- UIDirtyFlags.ts: 位标记枚举和追踪工具
  - UIDirtyFlags: Visual/Layout/Transform/Material/Text 标记
  - IDirtyTrackable: 脏追踪接口
  - DirtyTracker: 辅助工具类
  - 帧级别脏状态追踪 (markFrameDirty, isFrameDirty)

修改:
- UIGraphicComponent: 实现 IDirtyTrackable
  - 属性 setter 自动设置脏标记
  - 保留 setDirty/clearDirty 向后兼容
- UIImageComponent: 所有属性支持脏追踪
  - textureGuid/imageType/fillAmount 等变化自动标记
- UIGraphicRenderSystem: 使用 clearDirtyFlags()

导出:
- UIDirtyFlags, IDirtyTrackable, DirtyTracker
- markFrameDirty, isFrameDirty, clearFrameDirty

* refactor(ui): 移除过时的 dirty flag API

移除 UIGraphicComponent 中的兼容性 API:
- 移除 _isDirty getter/setter
- 移除 setDirty() 方法
- 移除 clearDirty() 方法

现在统一使用新的 dirty flag 系统:
- isDirty() / hasDirtyFlag(flags)
- markDirty(flags) / clearDirtyFlags()

* fix(ui): 修复两个 TODO 功能

1. 滑块手柄命中测试 (UIInputSystem)
   - UISliderComponent 添加 getHandleBounds() 计算手柄边界
   - UISliderComponent 添加 isPointInHandle() 精确命中测试
   - UIInputSystem.handleSlider() 使用精确测试更新悬停状态

2. 径向填充渲染 (UIGraphicRenderSystem)
   - 实现 renderRadialFill() 方法
   - 支持 radial90/radial180/radial360 三种模式
   - 支持 fillOrigin (top/right/bottom/left) 和 fillClockwise
   - 使用多段矩形近似饼形填充效果

* feat(ui): 完善 UI 系统架构和九宫格渲染

* fix(ui): 修复文本渲染层级问题并清理调试代码

- 修复纹理就绪后调用 invalidateUIRenderCaches() 导致的无限循环
- 移除 UITextRenderSystem、UIButtonRenderSystem、UIRectRenderSystem 中的首帧调试输出
- 移除 UILayoutSystem 中的布局调试日志
- 清理所有 __UI_RENDER_DEBUG__ 条件日志

* refactor(ui): 优化渲染批处理和输入框组件

渲染系统:
- 修复 RenderBatcher 保持渲染顺序
- 优化 Rust SpriteBatch 避免合并非连续精灵
- 增强 EngineRenderSystem 纹理就绪检测

输入框组件:
- 增强 UIInputFieldComponent 功能
- 改进 UIInputSystem 输入处理
- 新增 TextMeasureService 文本测量服务

* fix(ui): 修复九宫格首帧渲染和InputField输入问题

- 修复九宫格首帧 size=0x0 问题:
  - Viewport.tsx: 预览模式读取图片尺寸存储到 importSettings
  - AssetDatabase: ISpriteSettings 添加 width/height 字段
  - AssetMetadataService: getTextureSpriteInfo 使用元数据尺寸作为后备
  - UIRectRenderSystem: 当 atlasEntry 不存在时使用 spriteInfo 尺寸
  - WebBuildPipeline: 构建时包含 importSettings
  - AssetManager: 从 catalog 初始化时复制 importSettings
  - AssetTypes: IAssetCatalogEntry 添加 importSettings 字段

- 修复 InputField 无法输入问题:
  - UIRuntimeModule: manifest 添加 pluginExport: 'UIPlugin'
  - 确保预览模式正确加载 UI 插件并绑定 UIInputSystem

- 添加调试日志用于排查纹理加载问题

* fix(sprite): 修复类型导出错误

MaterialPropertyOverride 和 MaterialOverrides 应从 @esengine/material-system 导出

* fix(ui-editor): 补充 AnchorPreset 拉伸预设的映射

添加 StretchTop, StretchMiddle, StretchBottom, StretchLeft, StretchCenter, StretchRight 的位置和锚点值映射
2025-12-19 15:33:36 +08:00

19 KiB
Raw Blame History

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 包中定义统一接口:

// 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<string, MaterialPropertyOverride>;

/**
 * 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 模式避免代码重复:

// packages/material-system/src/mixins/MaterialOverridableMixin.ts

import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';

/**
 * Mixin that provides material override functionality.
 * 提供材质覆盖功能的 Mixin。
 */
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(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 定义属性元数据接口

// 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<string, ShaderPropertyMeta>;

    /** Render queue / order | 渲染队列/顺序 */
    renderQueue?: number;

    /** Preset blend mode | 预设混合模式 */
    blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
}

2.2 .shader 资产文件格式

{
  "$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 基类

// 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 通用动画更新逻辑

// 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 核心组件

// 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<MaterialPropertiesEditorProps> = ({
    target,
    shaderDef,
    onChange
}) => {
    // Group properties by their group field
    const groupedProps = useMemo(() => {
        if (!shaderDef?.properties) return {};

        const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
        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 (
        <div className="material-properties-editor">
            <ShaderSelector
                currentShaderId={target.getMaterialId()}
                onSelect={(id) => target.setMaterialId(id)}
            />

            {Object.entries(groupedProps).map(([group, props]) => (
                <PropertyGroup key={group} title={group}>
                    {props.map(([name, meta]) => (
                        <PropertyField
                            key={name}
                            name={name}
                            meta={meta}
                            value={target.getOverride(name)?.value ?? meta.default}
                            onChange={(value) => {
                                applyOverride(target, name, meta.type, value);
                                onChange?.(name, target.getOverride(name)!);
                            }}
                        />
                    ))}
                </PropertyGroup>
            ))}
        </div>
    );
};

五、实施计划

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 结论

不建议完全合并,但可提取公共基类:

// 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. 渐进迁移:可分阶段进行,不影响现有功能