Files
esengine/packages/ui/src/systems/render/UIGraphicRenderSystem.ts
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

388 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* UI Graphic Render System
* UI 图形渲染系统
*
* Renders entities with the new base components (UIGraphicComponent, UIImageComponent).
* This system follows the new architecture pattern with clearer component separation.
*
* 渲染使用新基础组件UIGraphicComponent、UIImageComponent的实体。
* 此系统遵循新架构模式,组件职责分离更清晰。
*/
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import { UITransformComponent } from '../../components/UITransformComponent';
import { UIGraphicComponent } from '../../components/base/UIGraphicComponent';
import { UIImageComponent } from '../../components/base/UIImageComponent';
import { getUIRenderCollector } from './UIRenderCollector';
import { getUIRenderTransform, getNinePatchPosition, type UIRenderTransform } from './UIRenderUtils';
import { isValidTextureGuid, defaultUV } from '../../utils/UITextureUtils';
/**
* UI Graphic Render System
* UI 图形渲染系统
*
* Handles rendering of the new base graphic components:
* - UIGraphicComponent: Base visual element (color rectangle)
* - UIImageComponent: Texture display (simple, sliced, tiled, filled)
*
* 处理新基础图形组件的渲染:
* - UIGraphicComponent基础可视元素颜色矩形
* - UIImageComponent纹理显示简单、切片、平铺、填充
*/
@ECSSystem('UIGraphicRender', { updateOrder: 102, runInEditMode: true })
export class UIGraphicRenderSystem extends EntitySystem {
constructor() {
// Match entities with UITransformComponent and UIGraphicComponent
// 匹配具有 UITransformComponent 和 UIGraphicComponent 的实体
super(Matcher.empty().all(UITransformComponent, UIGraphicComponent));
}
protected process(entities: readonly Entity[]): void {
const collector = getUIRenderCollector();
for (const entity of entities) {
const transform = entity.getComponent(UITransformComponent);
const graphic = entity.getComponent(UIGraphicComponent);
if (!transform || !graphic) continue;
// Get render transform data
// 获取渲染变换数据
const rt = getUIRenderTransform(transform);
if (!rt) continue;
// Check if entity has UIImageComponent for texture rendering
// 检查实体是否有 UIImageComponent 用于纹理渲染
const image = entity.getComponent(UIImageComponent);
if (image && image.hasTexture()) {
this.renderImage(collector, rt, graphic, image, entity.id);
} else {
this.renderColorRect(collector, rt, graphic, entity.id);
}
// Mark graphic as rendered (clear dirty flag)
// 标记图形已渲染(清除脏标记)
graphic.clearDirtyFlags();
}
}
/**
* Render a color rectangle
* 渲染颜色矩形
*/
private renderColorRect(
collector: ReturnType<typeof getUIRenderCollector>,
rt: UIRenderTransform,
graphic: UIGraphicComponent,
entityId: number
): void {
collector.addRect(
rt.renderX, rt.renderY,
rt.width, rt.height,
graphic.color,
graphic.alpha * rt.alpha,
rt.sortingLayer,
rt.orderInLayer,
{
rotation: rt.rotation,
pivotX: rt.pivotX,
pivotY: rt.pivotY,
materialId: graphic.materialId > 0 ? graphic.materialId : undefined,
entityId
}
);
}
/**
* Render an image with various modes
* 渲染各种模式的图像
*/
private renderImage(
collector: ReturnType<typeof getUIRenderCollector>,
rt: UIRenderTransform,
graphic: UIGraphicComponent,
image: UIImageComponent,
entityId: number
): void {
const alpha = graphic.alpha * rt.alpha;
const color = graphic.color;
const materialId = graphic.materialId > 0 ? graphic.materialId : undefined;
// Get validated texture GUID
// 获取验证后的纹理 GUID
const textureGuid = isValidTextureGuid(image.textureGuid) ? image.textureGuid : undefined;
// Handle different image types
// 处理不同的图像类型
if (image.isSliced()) {
// Nine-patch (sliced) rendering
// 九宫格(切片)渲染
const pos = getNinePatchPosition(rt);
collector.addNinePatch(
pos.x, pos.y,
rt.width, rt.height,
image.sliceBorder,
image.textureWidth,
image.textureHeight,
color,
alpha,
rt.sortingLayer,
rt.orderInLayer,
{
rotation: rt.rotation,
pivotX: pos.pivotX,
pivotY: pos.pivotY,
textureGuid,
textureId: image.textureId,
materialId,
entityId
}
);
} else if (image.isFilled()) {
// Filled rendering (for progress bars, etc.)
// 填充渲染(用于进度条等)
this.renderFilledImage(collector, rt, graphic, image, entityId);
} else {
// Simple image rendering
// 简单图像渲染
collector.addRect(
rt.renderX, rt.renderY,
rt.width, rt.height,
color,
alpha,
rt.sortingLayer,
rt.orderInLayer,
{
rotation: rt.rotation,
pivotX: rt.pivotX,
pivotY: rt.pivotY,
textureGuid,
textureId: image.textureId,
uv: image.uv ?? defaultUV(),
materialId,
entityId
}
);
}
}
/**
* Render a filled image (horizontal/vertical fill)
* 渲染填充图像(水平/垂直填充)
*/
private renderFilledImage(
collector: ReturnType<typeof getUIRenderCollector>,
rt: UIRenderTransform,
graphic: UIGraphicComponent,
image: UIImageComponent,
entityId: number
): void {
const alpha = graphic.alpha * rt.alpha;
const color = graphic.color;
const materialId = graphic.materialId > 0 ? graphic.materialId : undefined;
const textureGuid = isValidTextureGuid(image.textureGuid) ? image.textureGuid : undefined;
// Calculate filled dimensions based on fillAmount and fillMethod
// 根据 fillAmount 和 fillMethod 计算填充尺寸
let fillWidth = rt.width;
let fillHeight = rt.height;
let fillX = rt.renderX;
let fillY = rt.renderY;
let fillU0 = 0, fillV0 = 0, fillU1 = 1, fillV1 = 1;
const fillAmount = Math.max(0, Math.min(1, image.fillAmount));
switch (image.fillMethod) {
case 'horizontal':
if (image.fillOrigin === 'left' || image.fillOrigin === 'center') {
fillWidth = rt.width * fillAmount;
fillU1 = fillAmount;
} else {
// Right origin
fillWidth = rt.width * fillAmount;
fillX = rt.renderX + rt.width * (1 - fillAmount) * (1 - rt.pivotX);
fillU0 = 1 - fillAmount;
}
break;
case 'vertical':
if (image.fillOrigin === 'bottom' || image.fillOrigin === 'center') {
fillHeight = rt.height * fillAmount;
fillV1 = fillAmount;
} else {
// Top origin
fillHeight = rt.height * fillAmount;
fillY = rt.renderY + rt.height * (1 - fillAmount) * rt.pivotY;
fillV0 = 1 - fillAmount;
}
break;
// Radial fill modes - approximate with multiple segments
// 径向填充模式 - 使用多个分段近似
case 'radial90':
case 'radial180':
case 'radial360':
this.renderRadialFill(collector, rt, graphic, image, entityId);
return; // Early return - radial fill handles its own rendering
}
// Apply original UV mapping if present
// 如果存在原始 UV 映射,应用它
if (image.uv) {
const [u0, v0, u1, v1] = image.uv;
const uvWidth = u1 - u0;
const uvHeight = v1 - v0;
fillU0 = u0 + fillU0 * uvWidth;
fillV0 = v0 + fillV0 * uvHeight;
fillU1 = u0 + fillU1 * uvWidth;
fillV1 = v0 + fillV1 * uvHeight;
}
collector.addRect(
fillX, fillY,
fillWidth, fillHeight,
color,
alpha,
rt.sortingLayer,
rt.orderInLayer,
{
rotation: rt.rotation,
pivotX: rt.pivotX,
pivotY: rt.pivotY,
textureGuid,
textureId: image.textureId,
uv: [fillU0, fillV0, fillU1, fillV1],
materialId,
entityId
}
);
}
/**
* Render radial fill using multiple quad segments
* 使用多个矩形分段渲染径向填充
*
* This approximates a pie-shaped fill by rendering multiple narrow quads
* that fan out from the center.
* 通过渲染多个从中心扇形展开的窄矩形来近似饼形填充。
*/
private renderRadialFill(
collector: ReturnType<typeof getUIRenderCollector>,
rt: UIRenderTransform,
graphic: UIGraphicComponent,
image: UIImageComponent,
entityId: number
): void {
const alpha = graphic.alpha * rt.alpha;
const color = graphic.color;
const materialId = graphic.materialId > 0 ? graphic.materialId : undefined;
const textureGuid = isValidTextureGuid(image.textureGuid) ? image.textureGuid : undefined;
const fillAmount = Math.max(0, Math.min(1, image.fillAmount));
if (fillAmount <= 0) return;
// Determine the total angle range based on fill method
// 根据填充方法确定总角度范围
let totalAngle: number;
switch (image.fillMethod) {
case 'radial90': totalAngle = Math.PI / 2; break;
case 'radial180': totalAngle = Math.PI; break;
case 'radial360': totalAngle = Math.PI * 2; break;
default: return;
}
// Calculate fill angle
// 计算填充角度
const fillAngle = totalAngle * fillAmount;
// Determine start angle based on origin
// 根据起点确定起始角度
let startAngle: number;
switch (image.fillOrigin) {
case 'top': startAngle = -Math.PI / 2; break;
case 'right': startAngle = 0; break;
case 'bottom': startAngle = Math.PI / 2; break;
case 'left': startAngle = Math.PI; break;
default: startAngle = -Math.PI / 2; break; // Default: top
}
// Direction: clockwise or counter-clockwise
// 方向:顺时针或逆时针
const direction = image.fillClockwise ? 1 : -1;
// Calculate center and radius
// 计算中心和半径
const centerX = rt.x + rt.width / 2;
const centerY = rt.y + rt.height / 2;
const radiusX = rt.width / 2;
const radiusY = rt.height / 2;
// Number of segments for smooth appearance (more segments = smoother)
// 分段数量(更多分段 = 更平滑)
const numSegments = Math.max(4, Math.ceil(fillAngle * 16 / Math.PI));
// Render segments as quads from center
// 从中心渲染分段为矩形
const angleStep = fillAngle / numSegments;
for (let i = 0; i < numSegments; i++) {
const angle1 = startAngle + direction * angleStep * i;
const angle2 = startAngle + direction * angleStep * (i + 1);
// Calculate quad corners
// 计算矩形角点
const cos1 = Math.cos(angle1);
const sin1 = Math.sin(angle1);
const cos2 = Math.cos(angle2);
const sin2 = Math.sin(angle2);
// For each segment, render a triangle-like quad
// 对于每个分段,渲染一个类似三角形的矩形
// We approximate by rendering a small rect at the outer edge
// 我们通过在外边缘渲染一个小矩形来近似
// Calculate midpoint of the arc segment
// 计算弧段的中点
const midAngle = (angle1 + angle2) / 2;
const midCos = Math.cos(midAngle);
const midSin = Math.sin(midAngle);
// Segment width and position
// 分段宽度和位置
const segmentWidth = Math.abs(radiusX * (cos2 - cos1)) + Math.abs(radiusY * (sin2 - sin1));
const segmentHeight = Math.sqrt(radiusX * radiusX + radiusY * radiusY);
// Position at the midpoint direction from center
// 从中心沿中点方向定位
const segX = centerX + midCos * radiusX * 0.5;
const segY = centerY + midSin * radiusY * 0.5;
// Calculate UV for this segment
// 计算此分段的 UV
const u0 = 0.5 + midCos * 0.5 * fillAmount;
const v0 = 0.5 + midSin * 0.5 * fillAmount;
collector.addRect(
segX, segY,
Math.max(2, segmentWidth + 2), // Ensure minimum width with overlap
segmentHeight * 0.55, // Slightly more than half to ensure coverage
color,
alpha,
rt.sortingLayer,
rt.orderInLayer,
{
rotation: midAngle + Math.PI / 2, // Rotate to face outward
pivotX: 0.5,
pivotY: 0,
textureGuid,
textureId: image.textureId,
uv: [u0 - 0.1, v0 - 0.1, u0 + 0.1, v0 + 0.1],
materialId,
entityId
}
);
}
}
}