refactor(render): 统一渲染API并修复双重渲染问题 (#312)
- 将 submitSpritesDirectly 重命名为 submitSprites,移除旧的对象数组 API - RenderBatcher 采用 SoA 模式,预分配类型数组避免每帧 GC - 拆分 worldBatcher 和 screenBatcher 修复 play 模式下的双重渲染 - 移除未使用的材质实例管理代码
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* TypeScript ECS与Rust引擎之间的主桥接层。
|
||||
*/
|
||||
|
||||
import type { SpriteRenderData, TextureLoadRequest, EngineStats, CameraConfig } from '../types';
|
||||
import type { EngineStats, CameraConfig } from '../types';
|
||||
import type { ITextureEngineBridge } from '@esengine/asset-system';
|
||||
import type { GameEngine } from '../wasm/es_engine';
|
||||
import type {
|
||||
@@ -58,14 +58,6 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
|
||||
// 用于将文件路径转换为URL的路径解析器
|
||||
private pathResolver: ((path: string) => string) | null = null;
|
||||
|
||||
// Pre-allocated typed arrays for batch submission
|
||||
// 预分配的类型数组用于批量提交
|
||||
private transformBuffer: Float32Array;
|
||||
private textureIdBuffer: Uint32Array;
|
||||
private uvBuffer: Float32Array;
|
||||
private colorBuffer: Uint32Array;
|
||||
private materialIdBuffer: Uint32Array;
|
||||
|
||||
// Statistics | 统计信息
|
||||
private stats: EngineStats = {
|
||||
fps: 0,
|
||||
@@ -92,14 +84,6 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
|
||||
maxSprites: config.maxSprites ?? 10000,
|
||||
debug: config.debug ?? false
|
||||
};
|
||||
|
||||
// Pre-allocate buffers | 预分配缓冲区
|
||||
const maxSprites = this.config.maxSprites;
|
||||
this.transformBuffer = new Float32Array(maxSprites * 7); // x, y, rot, sx, sy, ox, oy
|
||||
this.textureIdBuffer = new Uint32Array(maxSprites);
|
||||
this.uvBuffer = new Float32Array(maxSprites * 4); // u0, v0, u1, v1
|
||||
this.colorBuffer = new Uint32Array(maxSprites);
|
||||
this.materialIdBuffer = new Uint32Array(maxSprites);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,53 +194,24 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
|
||||
* Submit sprite data for rendering.
|
||||
* 提交精灵数据进行渲染。
|
||||
*
|
||||
* @param sprites - Array of sprite render data | 精灵渲染数据数组
|
||||
* @param transforms - Transform data [x,y,rot,sx,sy,ox,oy] * count
|
||||
* @param textureIds - Texture IDs
|
||||
* @param uvs - UV coordinates [u0,v0,u1,v1] * count
|
||||
* @param colors - Packed RGBA colors
|
||||
* @param materialIds - Material IDs
|
||||
* @param count - Number of sprites
|
||||
*/
|
||||
submitSprites(sprites: SpriteRenderData[]): void {
|
||||
if (!this.initialized || sprites.length === 0) return;
|
||||
|
||||
const count = Math.min(sprites.length, this.config.maxSprites);
|
||||
|
||||
// Fill typed arrays | 填充类型数组
|
||||
for (let i = 0; i < count; i++) {
|
||||
const sprite = sprites[i];
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
|
||||
// Transform data | 变换数据
|
||||
this.transformBuffer[tOffset] = sprite.x;
|
||||
this.transformBuffer[tOffset + 1] = sprite.y;
|
||||
this.transformBuffer[tOffset + 2] = sprite.rotation;
|
||||
this.transformBuffer[tOffset + 3] = sprite.scaleX;
|
||||
this.transformBuffer[tOffset + 4] = sprite.scaleY;
|
||||
this.transformBuffer[tOffset + 5] = sprite.originX;
|
||||
this.transformBuffer[tOffset + 6] = sprite.originY;
|
||||
|
||||
// Texture ID | 纹理ID
|
||||
this.textureIdBuffer[i] = sprite.textureId;
|
||||
|
||||
// UV coordinates | UV坐标
|
||||
this.uvBuffer[uvOffset] = sprite.uv[0];
|
||||
this.uvBuffer[uvOffset + 1] = sprite.uv[1];
|
||||
this.uvBuffer[uvOffset + 2] = sprite.uv[2];
|
||||
this.uvBuffer[uvOffset + 3] = sprite.uv[3];
|
||||
|
||||
// Color | 颜色
|
||||
this.colorBuffer[i] = sprite.color;
|
||||
|
||||
// Material ID (0 = default) | 材质ID(0 = 默认)
|
||||
this.materialIdBuffer[i] = sprite.materialId ?? 0;
|
||||
}
|
||||
|
||||
// Submit to engine (single WASM call) | 提交到引擎(单次WASM调用)
|
||||
this.getEngine().submitSpriteBatch(
|
||||
this.transformBuffer.subarray(0, count * 7),
|
||||
this.textureIdBuffer.subarray(0, count),
|
||||
this.uvBuffer.subarray(0, count * 4),
|
||||
this.colorBuffer.subarray(0, count),
|
||||
this.materialIdBuffer.subarray(0, count)
|
||||
);
|
||||
submitSprites(
|
||||
transforms: Float32Array,
|
||||
textureIds: Uint32Array,
|
||||
uvs: Float32Array,
|
||||
colors: Uint32Array,
|
||||
materialIds: Uint32Array,
|
||||
count: number
|
||||
): void {
|
||||
if (!this.initialized || count === 0) return;
|
||||
|
||||
this.getEngine().submitSpriteBatch(transforms, textureIds, uvs, colors, materialIds);
|
||||
this.stats.spriteCount = count;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,106 +1,386 @@
|
||||
/**
|
||||
* Render batcher for collecting sprite data.
|
||||
* 用于收集精灵数据的渲染批处理器。
|
||||
* High-performance render batcher using Structure of Arrays (SoA) pattern.
|
||||
* 使用结构数组 (SoA) 模式的高性能渲染批处理器。
|
||||
*
|
||||
* Optimizations:
|
||||
* 优化:
|
||||
* - Pre-allocated typed arrays to avoid per-frame GC
|
||||
* 预分配类型数组以避免每帧 GC
|
||||
* - SoA layout for cache-friendly access
|
||||
* SoA 布局提供缓存友好的访问
|
||||
* - Object pool for SpriteRenderData when interface compatibility is needed
|
||||
* 当需要接口兼容性时使用 SpriteRenderData 对象池
|
||||
*/
|
||||
|
||||
import type { SpriteRenderData } from '../types';
|
||||
import type { SpriteRenderData, MaterialOverrides } from '../types';
|
||||
|
||||
/**
|
||||
* Collects and sorts sprite render data for batch submission.
|
||||
* 收集和排序精灵渲染数据用于批量提交。
|
||||
*
|
||||
* This class is used to collect sprites during the ECS update loop
|
||||
* and then submit them all at once to the engine.
|
||||
* 此类用于在ECS更新循环中收集精灵,然后一次性提交到引擎。
|
||||
* Default maximum sprites per batch.
|
||||
* 默认每批次最大精灵数。
|
||||
*/
|
||||
const DEFAULT_MAX_SPRITES = 10000;
|
||||
|
||||
/**
|
||||
* High-performance render batcher with SoA storage.
|
||||
* 使用 SoA 存储的高性能渲染批处理器。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const batcher = new RenderBatcher();
|
||||
* const batcher = new RenderBatcher(10000);
|
||||
*
|
||||
* // During ECS update | 在ECS更新期间
|
||||
* batcher.addSprite({
|
||||
* x: 100, y: 200,
|
||||
* rotation: 0,
|
||||
* scaleX: 1, scaleY: 1,
|
||||
* originX: 0.5, originY: 0.5,
|
||||
* textureId: 1,
|
||||
* uv: [0, 0, 1, 1],
|
||||
* color: 0xFFFFFFFF
|
||||
* });
|
||||
* // Add sprites using SoA API (fastest) | 使用 SoA API 添加精灵(最快)
|
||||
* batcher.addSpriteSoA(x, y, rot, sx, sy, ox, oy, texId, u0, v0, u1, v1, color, matId);
|
||||
*
|
||||
* // At end of frame | 在帧结束时
|
||||
* bridge.submitSprites(batcher.getSprites());
|
||||
* // Or use object API for compatibility | 或使用对象 API 以保持兼容性
|
||||
* batcher.addSprite(spriteData);
|
||||
*
|
||||
* // Get typed arrays for submission | 获取类型数组用于提交
|
||||
* const { transforms, textureIds, uvs, colors, materialIds, count } = batcher.getBuffers();
|
||||
*
|
||||
* // At end of frame | 帧结束时
|
||||
* batcher.clear();
|
||||
* ```
|
||||
*/
|
||||
export class RenderBatcher {
|
||||
private sprites: SpriteRenderData[] = [];
|
||||
// ===== SoA Buffers (pre-allocated) =====
|
||||
// ===== SoA 缓冲区(预分配)=====
|
||||
|
||||
/** Transform data: [x, y, rotation, scaleX, scaleY, originX, originY] per sprite */
|
||||
private _transforms: Float32Array;
|
||||
|
||||
/** Texture IDs | 纹理 ID */
|
||||
private _textureIds: Uint32Array;
|
||||
|
||||
/** UV coordinates: [u0, v0, u1, v1] per sprite */
|
||||
private _uvs: Float32Array;
|
||||
|
||||
/** Packed RGBA colors | 打包的 RGBA 颜色 */
|
||||
private _colors: Uint32Array;
|
||||
|
||||
/** Material IDs | 材质 ID */
|
||||
private _materialIds: Uint32Array;
|
||||
|
||||
/** Current sprite count | 当前精灵数量 */
|
||||
private _count: number = 0;
|
||||
|
||||
/** Maximum sprites capacity | 最大精灵容量 */
|
||||
private _capacity: number;
|
||||
|
||||
// ===== Object Pool for SpriteRenderData compatibility =====
|
||||
// ===== SpriteRenderData 对象池用于兼容性 =====
|
||||
|
||||
/** Pool of reusable SpriteRenderData objects | 可复用的 SpriteRenderData 对象池 */
|
||||
private _spritePool: SpriteRenderData[] = [];
|
||||
|
||||
/** Current pool index | 当前池索引 */
|
||||
private _poolIndex: number = 0;
|
||||
|
||||
// ===== Material Overrides Storage =====
|
||||
// ===== 材质覆盖存储 =====
|
||||
|
||||
/** Material overrides by sprite index | 按精灵索引存储的材质覆盖 */
|
||||
private _materialOverrides: Map<number, MaterialOverrides> = new Map();
|
||||
|
||||
/** Clip rects by sprite index | 按精灵索引存储的裁剪矩形 */
|
||||
private _clipRects: Map<number, { x: number; y: number; width: number; height: number }> = new Map();
|
||||
|
||||
/**
|
||||
* Create a new render batcher.
|
||||
* 创建新的渲染批处理器。
|
||||
* Create a new render batcher with pre-allocated buffers.
|
||||
* 创建具有预分配缓冲区的新渲染批处理器。
|
||||
*
|
||||
* Sprites are stored in insertion order. The caller is responsible
|
||||
* for adding sprites in the correct render order (back-to-front for 2D).
|
||||
* 精灵按插入顺序存储。调用者负责以正确的渲染顺序添加精灵(2D 中从后到前)。
|
||||
* @param capacity - Maximum sprites (default: 10000) | 最大精灵数(默认:10000)
|
||||
*/
|
||||
constructor() {}
|
||||
constructor(capacity: number = DEFAULT_MAX_SPRITES) {
|
||||
this._capacity = capacity;
|
||||
|
||||
// Pre-allocate all buffers | 预分配所有缓冲区
|
||||
this._transforms = new Float32Array(capacity * 7);
|
||||
this._textureIds = new Uint32Array(capacity);
|
||||
this._uvs = new Float32Array(capacity * 4);
|
||||
this._colors = new Uint32Array(capacity);
|
||||
this._materialIds = new Uint32Array(capacity);
|
||||
|
||||
// Pre-populate object pool | 预填充对象池
|
||||
this._initPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sprite to the batch.
|
||||
* 将精灵添加到批处理。
|
||||
* Initialize the object pool with reusable objects.
|
||||
* 使用可复用对象初始化对象池。
|
||||
*/
|
||||
private _initPool(): void {
|
||||
// Create a smaller initial pool, expand on demand
|
||||
// 创建较小的初始池,按需扩展
|
||||
const initialPoolSize = Math.min(1000, this._capacity);
|
||||
for (let i = 0; i < initialPoolSize; i++) {
|
||||
this._spritePool.push(this._createSpriteData());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SpriteRenderData object.
|
||||
* 创建新的 SpriteRenderData 对象。
|
||||
*/
|
||||
private _createSpriteData(): SpriteRenderData {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
originX: 0.5,
|
||||
originY: 0.5,
|
||||
textureId: 0,
|
||||
uv: [0, 0, 1, 1],
|
||||
color: 0xFFFFFFFF,
|
||||
materialId: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a SpriteRenderData object from pool.
|
||||
* 从池中获取 SpriteRenderData 对象。
|
||||
*/
|
||||
private _getFromPool(): SpriteRenderData {
|
||||
if (this._poolIndex >= this._spritePool.length) {
|
||||
// Expand pool | 扩展池
|
||||
this._spritePool.push(this._createSpriteData());
|
||||
}
|
||||
return this._spritePool[this._poolIndex++];
|
||||
}
|
||||
|
||||
// ===== High-Performance SoA API =====
|
||||
// ===== 高性能 SoA API =====
|
||||
|
||||
/**
|
||||
* Add a sprite using direct SoA parameters (fastest method).
|
||||
* 使用直接 SoA 参数添加精灵(最快方法)。
|
||||
*
|
||||
* @returns Sprite index for additional data (overrides, clipRect) | 精灵索引用于附加数据
|
||||
*/
|
||||
addSpriteSoA(
|
||||
x: number,
|
||||
y: number,
|
||||
rotation: number,
|
||||
scaleX: number,
|
||||
scaleY: number,
|
||||
originX: number,
|
||||
originY: number,
|
||||
textureId: number,
|
||||
u0: number,
|
||||
v0: number,
|
||||
u1: number,
|
||||
v1: number,
|
||||
color: number,
|
||||
materialId: number = 0
|
||||
): number {
|
||||
if (this._count >= this._capacity) {
|
||||
console.warn('RenderBatcher capacity exceeded | RenderBatcher 容量已满');
|
||||
return -1;
|
||||
}
|
||||
|
||||
const i = this._count;
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
|
||||
// Write transform data | 写入变换数据
|
||||
this._transforms[tOffset] = x;
|
||||
this._transforms[tOffset + 1] = y;
|
||||
this._transforms[tOffset + 2] = rotation;
|
||||
this._transforms[tOffset + 3] = scaleX;
|
||||
this._transforms[tOffset + 4] = scaleY;
|
||||
this._transforms[tOffset + 5] = originX;
|
||||
this._transforms[tOffset + 6] = originY;
|
||||
|
||||
// Write texture ID | 写入纹理 ID
|
||||
this._textureIds[i] = textureId;
|
||||
|
||||
// Write UV coordinates | 写入 UV 坐标
|
||||
this._uvs[uvOffset] = u0;
|
||||
this._uvs[uvOffset + 1] = v0;
|
||||
this._uvs[uvOffset + 2] = u1;
|
||||
this._uvs[uvOffset + 3] = v1;
|
||||
|
||||
// Write color and material | 写入颜色和材质
|
||||
this._colors[i] = color;
|
||||
this._materialIds[i] = materialId;
|
||||
|
||||
this._count++;
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set material overrides for a sprite.
|
||||
* 为精灵设置材质覆盖。
|
||||
*
|
||||
* @param index - Sprite index from addSpriteSoA | 来自 addSpriteSoA 的精灵索引
|
||||
* @param overrides - Material overrides | 材质覆盖
|
||||
*/
|
||||
setMaterialOverrides(index: number, overrides: MaterialOverrides): void {
|
||||
if (index >= 0 && index < this._count) {
|
||||
this._materialOverrides.set(index, overrides);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clip rect for a sprite.
|
||||
* 为精灵设置裁剪矩形。
|
||||
*
|
||||
* @param index - Sprite index from addSpriteSoA | 来自 addSpriteSoA 的精灵索引
|
||||
* @param clipRect - Clip rectangle | 裁剪矩形
|
||||
*/
|
||||
setClipRect(index: number, clipRect: { x: number; y: number; width: number; height: number }): void {
|
||||
if (index >= 0 && index < this._count) {
|
||||
this._clipRects.set(index, clipRect);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Object API (for compatibility) =====
|
||||
// ===== 对象 API(用于兼容性)=====
|
||||
|
||||
/**
|
||||
* Add a sprite using SpriteRenderData object.
|
||||
* 使用 SpriteRenderData 对象添加精灵。
|
||||
*
|
||||
* This method is kept for backward compatibility but internally uses SoA storage.
|
||||
* 此方法保留用于向后兼容,但内部使用 SoA 存储。
|
||||
*
|
||||
* @param sprite - Sprite render data | 精灵渲染数据
|
||||
*/
|
||||
addSprite(sprite: SpriteRenderData): void {
|
||||
this.sprites.push(sprite);
|
||||
const index = this.addSpriteSoA(
|
||||
sprite.x,
|
||||
sprite.y,
|
||||
sprite.rotation,
|
||||
sprite.scaleX,
|
||||
sprite.scaleY,
|
||||
sprite.originX,
|
||||
sprite.originY,
|
||||
sprite.textureId,
|
||||
sprite.uv[0],
|
||||
sprite.uv[1],
|
||||
sprite.uv[2],
|
||||
sprite.uv[3],
|
||||
sprite.color,
|
||||
sprite.materialId ?? 0
|
||||
);
|
||||
|
||||
// Store optional data | 存储可选数据
|
||||
if (index >= 0) {
|
||||
if (sprite.materialOverrides) {
|
||||
this._materialOverrides.set(index, sprite.materialOverrides);
|
||||
}
|
||||
if (sprite.clipRect) {
|
||||
this._clipRects.set(index, sprite.clipRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple sprites to the batch.
|
||||
* 将多个精灵添加到批处理。
|
||||
* Add multiple sprites.
|
||||
* 添加多个精灵。
|
||||
*
|
||||
* @param sprites - Array of sprite render data | 精灵渲染数据数组
|
||||
*/
|
||||
addSprites(sprites: SpriteRenderData[]): void {
|
||||
this.sprites.push(...sprites);
|
||||
for (let i = 0; i < sprites.length; i++) {
|
||||
this.addSprite(sprites[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Buffer Access =====
|
||||
// ===== 缓冲区访问 =====
|
||||
|
||||
/**
|
||||
* Get raw typed array buffers for direct submission to engine.
|
||||
* 获取原始类型数组缓冲区以直接提交到引擎。
|
||||
*
|
||||
* Returns subarray views (zero-copy) for only the used portion.
|
||||
* 返回仅已使用部分的 subarray 视图(零拷贝)。
|
||||
*/
|
||||
getBuffers(): {
|
||||
transforms: Float32Array;
|
||||
textureIds: Uint32Array;
|
||||
uvs: Float32Array;
|
||||
colors: Uint32Array;
|
||||
materialIds: Uint32Array;
|
||||
count: number;
|
||||
} {
|
||||
return {
|
||||
transforms: this._transforms.subarray(0, this._count * 7),
|
||||
textureIds: this._textureIds.subarray(0, this._count),
|
||||
uvs: this._uvs.subarray(0, this._count * 4),
|
||||
colors: this._colors.subarray(0, this._count),
|
||||
materialIds: this._materialIds.subarray(0, this._count),
|
||||
count: this._count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sprites in the batch.
|
||||
* 获取批处理中的所有精灵。
|
||||
* Get sprites as SpriteRenderData array (for legacy compatibility).
|
||||
* 获取精灵作为 SpriteRenderData 数组(用于旧版兼容性)。
|
||||
*
|
||||
* Sprites are returned in insertion order to preserve z-ordering.
|
||||
* The rendering system is responsible for sorting sprites before adding them.
|
||||
* 精灵按插入顺序返回以保持 z 顺序。
|
||||
* 渲染系统负责在添加精灵前对其进行排序。
|
||||
* Uses object pool to avoid allocations.
|
||||
* 使用对象池以避免分配。
|
||||
*
|
||||
* @returns Array of sprites in insertion order | 按插入顺序排列的精灵数组
|
||||
* @returns Array of sprites from pool | 来自池的精灵数组
|
||||
*/
|
||||
getSprites(): SpriteRenderData[] {
|
||||
// NOTE: Previously sorted by materialId/textureId for batching optimization,
|
||||
// but this broke z-ordering for UI elements where render order is critical.
|
||||
// Sprites should be added in the correct render order by the caller.
|
||||
// 注意:之前按 materialId/textureId 排序以优化批处理,
|
||||
// 但这破坏了 UI 元素的 z 排序,而 UI 的渲染顺序至关重要。
|
||||
// 调用者应该以正确的渲染顺序添加精灵。
|
||||
return this.sprites;
|
||||
// Reset pool index to reuse objects | 重置池索引以复用对象
|
||||
this._poolIndex = 0;
|
||||
|
||||
const result: SpriteRenderData[] = [];
|
||||
for (let i = 0; i < this._count; i++) {
|
||||
const sprite = this._getFromPool();
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
|
||||
// Fill from SoA buffers | 从 SoA 缓冲区填充
|
||||
sprite.x = this._transforms[tOffset];
|
||||
sprite.y = this._transforms[tOffset + 1];
|
||||
sprite.rotation = this._transforms[tOffset + 2];
|
||||
sprite.scaleX = this._transforms[tOffset + 3];
|
||||
sprite.scaleY = this._transforms[tOffset + 4];
|
||||
sprite.originX = this._transforms[tOffset + 5];
|
||||
sprite.originY = this._transforms[tOffset + 6];
|
||||
|
||||
sprite.textureId = this._textureIds[i];
|
||||
|
||||
sprite.uv[0] = this._uvs[uvOffset];
|
||||
sprite.uv[1] = this._uvs[uvOffset + 1];
|
||||
sprite.uv[2] = this._uvs[uvOffset + 2];
|
||||
sprite.uv[3] = this._uvs[uvOffset + 3];
|
||||
|
||||
sprite.color = this._colors[i];
|
||||
sprite.materialId = this._materialIds[i];
|
||||
|
||||
// Attach optional data | 附加可选数据
|
||||
sprite.materialOverrides = this._materialOverrides.get(i);
|
||||
sprite.clipRect = this._clipRects.get(i);
|
||||
|
||||
result.push(sprite);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== State =====
|
||||
// ===== 状态 =====
|
||||
|
||||
/**
|
||||
* Get sprite count.
|
||||
* 获取精灵数量。
|
||||
*/
|
||||
get count(): number {
|
||||
return this.sprites.length;
|
||||
return this._count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all sprites from the batch.
|
||||
* 清除批处理中的所有精灵。
|
||||
* Get capacity.
|
||||
* 获取容量。
|
||||
*/
|
||||
clear(): void {
|
||||
this.sprites.length = 0;
|
||||
get capacity(): number {
|
||||
return this._capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +388,52 @@ export class RenderBatcher {
|
||||
* 检查批处理是否为空。
|
||||
*/
|
||||
get isEmpty(): boolean {
|
||||
return this.sprites.length === 0;
|
||||
return this._count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all sprites from the batch.
|
||||
* 清除批处理中的所有精灵。
|
||||
*
|
||||
* Does NOT deallocate buffers - they are reused next frame.
|
||||
* 不会释放缓冲区 - 它们在下一帧被复用。
|
||||
*/
|
||||
clear(): void {
|
||||
this._count = 0;
|
||||
this._poolIndex = 0;
|
||||
this._materialOverrides.clear();
|
||||
this._clipRects.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if material overrides exist for any sprite.
|
||||
* 检查是否有任何精灵存在材质覆盖。
|
||||
*/
|
||||
hasMaterialOverrides(): boolean {
|
||||
return this._materialOverrides.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get material overrides for a sprite index.
|
||||
* 获取精灵索引的材质覆盖。
|
||||
*/
|
||||
getMaterialOverrides(index: number): MaterialOverrides | undefined {
|
||||
return this._materialOverrides.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if clip rects exist for any sprite.
|
||||
* 检查是否有任何精灵存在裁剪矩形。
|
||||
*/
|
||||
hasClipRects(): boolean {
|
||||
return this._clipRects.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clip rect for a sprite index.
|
||||
* 获取精灵索引的裁剪矩形。
|
||||
*/
|
||||
getClipRect(index: number): { x: number; y: number; width: number; height: number } | undefined {
|
||||
return this._clipRects.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,11 @@ export class SpriteRenderHelper {
|
||||
*/
|
||||
render(): void {
|
||||
if (!this.batcher.isEmpty) {
|
||||
this.bridge.submitSprites(this.batcher.getSprites());
|
||||
const buffers = this.batcher.getBuffers();
|
||||
this.bridge.submitSprites(
|
||||
buffers.transforms, buffers.textureIds, buffers.uvs,
|
||||
buffers.colors, buffers.materialIds, buffers.count
|
||||
);
|
||||
}
|
||||
this.bridge.render();
|
||||
}
|
||||
|
||||
@@ -179,28 +179,28 @@ export type TransformComponentType = ComponentType & (new (...args: any[]) => Co
|
||||
*/
|
||||
export type AssetPathResolverFn = (guidOrPath: string) => string;
|
||||
|
||||
/**
|
||||
* Render item for sorting.
|
||||
* 用于排序的渲染项。
|
||||
* @internal
|
||||
*/
|
||||
interface RenderItem {
|
||||
sortKey: number;
|
||||
addIndex: number;
|
||||
/** Start index in batcher | 在批处理器中的起始索引 */
|
||||
startIndex: number;
|
||||
/** Number of sprites | 精灵数量 */
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ECS System for rendering sprites using the Rust engine.
|
||||
* 使用Rust引擎渲染精灵的ECS系统。
|
||||
*
|
||||
* This system extends EntitySystem and integrates with the ECS lifecycle.
|
||||
* 此系统扩展EntitySystem并与ECS生命周期集成。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create transform component | 创建变换组件
|
||||
* @ECSComponent('Transform')
|
||||
* class Transform extends Component implements ITransformComponent {
|
||||
* position = { x: 0, y: 0 };
|
||||
* rotation = 0;
|
||||
* scale = { x: 1, y: 1 };
|
||||
* }
|
||||
*
|
||||
* // Initialize bridge | 初始化桥接
|
||||
* const bridge = new EngineBridge({ canvasId: 'canvas' });
|
||||
* await bridge.initialize();
|
||||
*
|
||||
* // Add system to scene | 将系统添加到场景
|
||||
* const renderSystem = new EngineRenderSystem(bridge, Transform);
|
||||
* scene.addSystem(renderSystem);
|
||||
* ```
|
||||
@@ -208,7 +208,8 @@ export type AssetPathResolverFn = (guidOrPath: string) => string;
|
||||
@ECSSystem('EngineRender', { updateOrder: 1000 }) // Render system executes last | 渲染系统最后执行
|
||||
export class EngineRenderSystem extends EntitySystem {
|
||||
private bridge: EngineBridge;
|
||||
private batcher: RenderBatcher;
|
||||
private worldBatcher: RenderBatcher;
|
||||
private screenBatcher: RenderBatcher;
|
||||
private transformType: TransformComponentType;
|
||||
private showGizmos = true;
|
||||
private selectedEntityIds: Set<number> = new Set();
|
||||
@@ -218,6 +219,17 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
// 可重用的映射以避免每帧分配
|
||||
private entityRenderMap: Map<number, SpriteRenderData> = new Map();
|
||||
|
||||
// ===== Pre-allocated arrays for render items =====
|
||||
// ===== 渲染项的预分配数组 =====
|
||||
private worldSpaceItems: RenderItem[] = [];
|
||||
private screenSpaceItems: RenderItem[] = [];
|
||||
private worldSpaceItemCount = 0;
|
||||
private screenSpaceItemCount = 0;
|
||||
|
||||
// Frame counter for dirty tracking
|
||||
// 用于脏标记追踪的帧计数器
|
||||
private frameNumber = 0;
|
||||
|
||||
// Additional render data providers (e.g., tilemap)
|
||||
// 额外的渲染数据提供者(如瓦片地图)
|
||||
private renderDataProviders: IRenderDataProvider[] = [];
|
||||
@@ -252,18 +264,6 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
// 为 false(编辑器模式)时,UI 在世界空间渲染,跟随编辑器相机
|
||||
private previewMode: boolean = false;
|
||||
|
||||
// ===== Material Instance Management =====
|
||||
// ===== 材质实例管理 =====
|
||||
// Maps (baseMaterialId, overridesHash) → instanceMaterialId
|
||||
// 映射 (基础材质ID, 覆盖哈希) → 实例材质ID
|
||||
private materialInstanceMap: Map<string, number> = new Map();
|
||||
// Next instance ID (starts at 10000 to avoid collision with built-in materials)
|
||||
// 下一个实例 ID(从 10000 开始以避免与内置材质冲突)
|
||||
private nextMaterialInstanceId: number = 10000;
|
||||
// Track instances used this frame for cleanup
|
||||
// 跟踪本帧使用的实例以便清理
|
||||
private usedInstancesThisFrame: Set<number> = new Set();
|
||||
|
||||
/**
|
||||
* Create a new engine render system.
|
||||
* 创建新的引擎渲染系统。
|
||||
@@ -277,7 +277,8 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
super(Matcher.empty().all(SpriteComponent, transformType));
|
||||
|
||||
this.bridge = bridge;
|
||||
this.batcher = new RenderBatcher();
|
||||
this.worldBatcher = new RenderBatcher();
|
||||
this.screenBatcher = new RenderBatcher();
|
||||
this.transformType = transformType;
|
||||
}
|
||||
|
||||
@@ -295,8 +296,16 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
* 处理实体之前调用。
|
||||
*/
|
||||
protected override onBegin(): void {
|
||||
// Clear the batch | 清空批处理
|
||||
this.batcher.clear();
|
||||
// Increment frame counter | 递增帧计数器
|
||||
this.frameNumber++;
|
||||
|
||||
// Clear the batches | 清空批处理
|
||||
this.worldBatcher.clear();
|
||||
this.screenBatcher.clear();
|
||||
|
||||
// Reset render item counts (reuse arrays) | 重置渲染项计数(复用数组)
|
||||
this.worldSpaceItemCount = 0;
|
||||
this.screenSpaceItemCount = 0;
|
||||
|
||||
// Clear screen with dark background | 用深色背景清屏
|
||||
this.bridge.clear(0.1, 0.1, 0.12, 1);
|
||||
@@ -325,603 +334,291 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
// 清空并重用映射用于绘制 gizmo
|
||||
this.entityRenderMap.clear();
|
||||
|
||||
// Collect all render items separated by render space
|
||||
// 按渲染空间分离收集所有渲染项
|
||||
// addIndex is used for stable sorting when sortKeys are equal
|
||||
// addIndex 用于当 sortKey 相等时实现稳定排序
|
||||
const worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[]; addIndex?: number }> = [];
|
||||
const screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[]; addIndex?: number }> = [];
|
||||
this.collectEntitySprites(entities);
|
||||
this.collectProviderRenderData();
|
||||
this.collectUIRenderData();
|
||||
|
||||
// Collect sprites from entities (all in world space)
|
||||
// 收集实体的 sprites(都在世界空间)
|
||||
this.collectEntitySprites(entities, worldSpaceItems);
|
||||
|
||||
// Collect render data from providers (e.g., tilemap, particle)
|
||||
// 收集渲染数据提供者的数据(如瓦片地图、粒子)
|
||||
this.collectProviderRenderData(worldSpaceItems, screenSpaceItems);
|
||||
|
||||
// Collect UI render data
|
||||
// 收集 UI 渲染数据
|
||||
if (this.uiRenderDataProvider) {
|
||||
const uiRenderData = this.uiRenderDataProvider.getRenderData();
|
||||
// Use addIndex to preserve original order for stable sorting
|
||||
// 使用 addIndex 保持原始顺序以实现稳定排序
|
||||
let uiAddIndex = 0;
|
||||
|
||||
// DEBUG: 输出 UI 渲染数据
|
||||
// DEBUG: Output UI render data
|
||||
if ((globalThis as any).__UI_RENDER_DEBUG__) {
|
||||
console.log('[EngineRenderSystem] UI render batches:', uiRenderData.map((data, i) => ({
|
||||
index: i,
|
||||
orderInLayer: data.orderInLayer,
|
||||
sortingLayer: data.sortingLayer,
|
||||
tileCount: data.tileCount,
|
||||
sortKey: sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer),
|
||||
textureIds: Array.from(data.textureIds).slice(0, 3), // 只显示前3个 | Show first 3 only
|
||||
textureGuid: data.textureGuid
|
||||
})));
|
||||
}
|
||||
|
||||
for (const data of uiRenderData) {
|
||||
const uiSprites = this.convertProviderDataToSprites(data);
|
||||
if (uiSprites.length > 0) {
|
||||
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
|
||||
// UI always goes to screen space in preview mode, world space in editor mode
|
||||
// UI 在预览模式下始终在屏幕空间,编辑器模式下在世界空间
|
||||
if (this.previewMode) {
|
||||
screenSpaceItems.push({ sortKey, sprites: uiSprites, addIndex: uiAddIndex++ });
|
||||
} else {
|
||||
worldSpaceItems.push({ sortKey, sprites: uiSprites, addIndex: uiAddIndex++ });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.renderWorldSpace();
|
||||
if (this.previewMode && this.screenSpaceItemCount > 0) {
|
||||
this.renderScreenSpace();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Pass 1: World Space Rendering =====
|
||||
// ===== 阶段 1:世界空间渲染 =====
|
||||
this.renderWorldSpacePass(worldSpaceItems);
|
||||
|
||||
// ===== Pass 2: Screen Space Rendering (Preview Mode Only) =====
|
||||
// ===== 阶段 2:屏幕空间渲染(仅预览模式)=====
|
||||
if (this.previewMode && screenSpaceItems.length > 0) {
|
||||
this.renderScreenSpacePass(screenSpaceItems);
|
||||
/** @internal */
|
||||
private addRenderItem(
|
||||
items: RenderItem[],
|
||||
itemCount: number,
|
||||
sortKey: number,
|
||||
addIndex: number,
|
||||
startIndex: number,
|
||||
count: number
|
||||
): number {
|
||||
if (itemCount >= items.length) {
|
||||
items.push({ sortKey: 0, addIndex: 0, startIndex: 0, count: 0 });
|
||||
}
|
||||
|
||||
// ===== Cleanup unused material instances =====
|
||||
// ===== 清理未使用的材质实例 =====
|
||||
this.cleanupUnusedMaterialInstances();
|
||||
const item = items[itemCount];
|
||||
item.sortKey = sortKey;
|
||||
item.addIndex = addIndex;
|
||||
item.startIndex = startIndex;
|
||||
item.count = count;
|
||||
return itemCount + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect sprites from matched entities.
|
||||
* 收集匹配实体的 sprites。
|
||||
*/
|
||||
private collectEntitySprites(
|
||||
entities: readonly Entity[],
|
||||
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
|
||||
): void {
|
||||
private collectEntitySprites(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
const transform = entity.getComponent(this.transformType) as unknown as ITransformComponent | null;
|
||||
if (!sprite || !transform) continue;
|
||||
|
||||
if (!sprite || !transform) {
|
||||
continue;
|
||||
}
|
||||
let u0 = sprite.uv[0], v0 = sprite.uv[1], u1 = sprite.uv[2], v1 = sprite.uv[3];
|
||||
if (sprite.flipX) { [u0, u1] = [u1, u0]; }
|
||||
if (sprite.flipY) { [v0, v1] = [v1, v0]; }
|
||||
|
||||
// Calculate UV with flip | 计算带翻转的 UV
|
||||
const uv: [number, number, number, number] = [...sprite.uv];
|
||||
if (sprite.flipX) {
|
||||
[uv[0], uv[2]] = [uv[2], uv[0]];
|
||||
}
|
||||
if (sprite.flipY) {
|
||||
[uv[1], uv[3]] = [uv[3], uv[1]];
|
||||
}
|
||||
|
||||
// 使用世界变换(由 TransformSystem 计算,考虑父级变换),回退到本地变换
|
||||
const pos = transform.worldPosition ?? transform.position;
|
||||
const scl = transform.worldScale ?? transform.scale;
|
||||
const rot = transform.worldRotation
|
||||
? transform.worldRotation.z
|
||||
: (typeof transform.rotation === 'number' ? transform.rotation : transform.rotation.z);
|
||||
|
||||
// Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的 RGBA
|
||||
const color = Color.packHexAlpha(sprite.color, sprite.alpha);
|
||||
|
||||
// Get texture ID from sprite component
|
||||
// 从精灵组件获取纹理 ID
|
||||
let textureId = 0;
|
||||
const textureSource = sprite.getTextureSource();
|
||||
if (textureSource) {
|
||||
const texturePath = this.resolveAssetPath(textureSource);
|
||||
textureId = this.bridge.getOrLoadTextureByPath(texturePath);
|
||||
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(textureSource));
|
||||
}
|
||||
|
||||
// Get material ID from GUID
|
||||
// 从 GUID 获取材质 ID
|
||||
const materialGuidOrPath = sprite.materialGuid;
|
||||
const materialPath = materialGuidOrPath
|
||||
? this.resolveAssetPath(materialGuidOrPath)
|
||||
: materialGuidOrPath;
|
||||
const materialId = materialPath
|
||||
? getMaterialManager().getMaterialIdByPath(materialPath)
|
||||
: 0;
|
||||
const materialPath = materialGuidOrPath ? this.resolveAssetPath(materialGuidOrPath) : materialGuidOrPath;
|
||||
const materialId = materialPath ? getMaterialManager().getMaterialIdByPath(materialPath) : 0;
|
||||
|
||||
const hasOverrides = sprite.hasOverrides();
|
||||
// Determine if this sprite should render in screen space
|
||||
// 确定此精灵是否应在屏幕空间渲染
|
||||
const bScreenSpace = this.previewMode && sortingLayerManager.isScreenSpace(sprite.sortingLayer);
|
||||
const batcher = bScreenSpace ? this.screenBatcher : this.worldBatcher;
|
||||
|
||||
const renderData: SpriteRenderData = {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
rotation: rot,
|
||||
scaleX: sprite.width * scl.x,
|
||||
scaleY: sprite.height * scl.y,
|
||||
originX: sprite.anchorX,
|
||||
originY: sprite.anchorY,
|
||||
textureId,
|
||||
uv,
|
||||
color,
|
||||
materialId,
|
||||
...(hasOverrides ? { materialOverrides: sprite.materialOverrides } : {})
|
||||
};
|
||||
const startIndex = batcher.count;
|
||||
const index = batcher.addSpriteSoA(
|
||||
pos.x, pos.y, rot,
|
||||
sprite.width * scl.x, sprite.height * scl.y,
|
||||
sprite.anchorX, sprite.anchorY,
|
||||
textureId, u0, v0, u1, v1, color, materialId
|
||||
);
|
||||
|
||||
if (index >= 0 && sprite.hasOverrides()) {
|
||||
batcher.setMaterialOverrides(index, sprite.materialOverrides!);
|
||||
}
|
||||
|
||||
const sortKey = sortingLayerManager.getSortKey(sprite.sortingLayer, sprite.orderInLayer);
|
||||
worldSpaceItems.push({ sortKey, sprites: [renderData] });
|
||||
this.entityRenderMap.set(entity.id, renderData);
|
||||
if (bScreenSpace) {
|
||||
this.screenSpaceItemCount = this.addRenderItem(
|
||||
this.screenSpaceItems, this.screenSpaceItemCount,
|
||||
sortKey, this.screenSpaceItemCount, startIndex, 1
|
||||
);
|
||||
} else {
|
||||
this.worldSpaceItemCount = this.addRenderItem(
|
||||
this.worldSpaceItems, this.worldSpaceItemCount,
|
||||
sortKey, this.worldSpaceItemCount, startIndex, 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect render data from providers (tilemap, particle, etc.).
|
||||
* 收集渲染数据提供者的数据(瓦片地图、粒子等)。
|
||||
* 收集渲染数据提供者的数据。
|
||||
*/
|
||||
private collectProviderRenderData(
|
||||
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>,
|
||||
screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
|
||||
): void {
|
||||
private collectProviderRenderData(): void {
|
||||
for (const provider of this.renderDataProviders) {
|
||||
const renderDataList = provider.getRenderData();
|
||||
for (const data of renderDataList) {
|
||||
// Determine render space: explicit flag > layer config
|
||||
// 确定渲染空间:显式标志 > 层配置
|
||||
const bScreenSpace = data.bScreenSpace ?? sortingLayerManager.isScreenSpace(data.sortingLayer);
|
||||
for (const data of provider.getRenderData()) {
|
||||
const bScreenSpace = this.previewMode && (data.bScreenSpace ?? sortingLayerManager.isScreenSpace(data.sortingLayer));
|
||||
const batcher = bScreenSpace ? this.screenBatcher : this.worldBatcher;
|
||||
|
||||
// Get texture ID - load from GUID if needed
|
||||
// 获取纹理 ID - 如果需要从 GUID 加载
|
||||
let textureId = data.textureIds[0] || 0;
|
||||
if (textureId === 0 && data.textureGuid) {
|
||||
const resolvedPath = this.resolveAssetPath(data.textureGuid);
|
||||
textureId = this.bridge.getOrLoadTextureByPath(resolvedPath);
|
||||
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(data.textureGuid));
|
||||
}
|
||||
|
||||
// Convert render data to sprites
|
||||
// 转换渲染数据为 sprites
|
||||
const sprites: SpriteRenderData[] = [];
|
||||
const startIndex = batcher.count;
|
||||
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
|
||||
|
||||
for (let i = 0; i < data.tileCount; i++) {
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
|
||||
const renderData: SpriteRenderData = {
|
||||
x: data.transforms[tOffset],
|
||||
y: data.transforms[tOffset + 1],
|
||||
rotation: data.transforms[tOffset + 2],
|
||||
scaleX: data.transforms[tOffset + 3],
|
||||
scaleY: data.transforms[tOffset + 4],
|
||||
originX: data.transforms[tOffset + 5],
|
||||
originY: data.transforms[tOffset + 6],
|
||||
batcher.addSpriteSoA(
|
||||
data.transforms[tOffset], data.transforms[tOffset + 1], data.transforms[tOffset + 2],
|
||||
data.transforms[tOffset + 3], data.transforms[tOffset + 4],
|
||||
data.transforms[tOffset + 5], data.transforms[tOffset + 6],
|
||||
textureId,
|
||||
uv: [data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3]],
|
||||
color: data.colors[i]
|
||||
};
|
||||
|
||||
sprites.push(renderData);
|
||||
data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3],
|
||||
data.colors[i], data.materialIds?.[i] ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
if (sprites.length > 0) {
|
||||
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
|
||||
|
||||
// Route to appropriate render space
|
||||
// 路由到适当的渲染空间
|
||||
if (this.previewMode && bScreenSpace) {
|
||||
screenSpaceItems.push({ sortKey, sprites });
|
||||
} else {
|
||||
worldSpaceItems.push({ sortKey, sprites });
|
||||
}
|
||||
if (bScreenSpace) {
|
||||
this.screenSpaceItemCount = this.addRenderItem(
|
||||
this.screenSpaceItems, this.screenSpaceItemCount,
|
||||
sortKey, this.screenSpaceItemCount, startIndex, data.tileCount
|
||||
);
|
||||
} else {
|
||||
this.worldSpaceItemCount = this.addRenderItem(
|
||||
this.worldSpaceItems, this.worldSpaceItemCount,
|
||||
sortKey, this.worldSpaceItemCount, startIndex, data.tileCount
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect UI render data.
|
||||
* 收集 UI 渲染数据。
|
||||
*/
|
||||
private collectUIRenderData(): void {
|
||||
if (!this.uiRenderDataProvider) return;
|
||||
|
||||
const uiRenderData = this.uiRenderDataProvider.getRenderData();
|
||||
|
||||
if ((globalThis as any).__UI_RENDER_DEBUG__) {
|
||||
console.log('[EngineRenderSystem] UI render batches:', uiRenderData.map((data, i) => ({
|
||||
index: i, orderInLayer: data.orderInLayer, sortingLayer: data.sortingLayer,
|
||||
tileCount: data.tileCount,
|
||||
sortKey: sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer),
|
||||
textureIds: Array.from(data.textureIds).slice(0, 3), textureGuid: data.textureGuid
|
||||
})));
|
||||
}
|
||||
|
||||
for (const data of uiRenderData) {
|
||||
if (data.tileCount === 0) continue;
|
||||
|
||||
// UI goes to screen space in preview mode, world space in editor mode
|
||||
// UI 在预览模式下进入屏幕空间,在编辑器模式下进入世界空间
|
||||
const bScreenSpace = this.previewMode;
|
||||
const batcher = bScreenSpace ? this.screenBatcher : this.worldBatcher;
|
||||
|
||||
let textureId = data.textureIds[0] || 0;
|
||||
if (textureId === 0 && data.textureGuid) {
|
||||
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(data.textureGuid));
|
||||
}
|
||||
|
||||
const startIndex = batcher.count;
|
||||
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
|
||||
|
||||
for (let i = 0; i < data.tileCount; i++) {
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
const index = batcher.addSpriteSoA(
|
||||
data.transforms[tOffset], data.transforms[tOffset + 1], data.transforms[tOffset + 2],
|
||||
data.transforms[tOffset + 3], data.transforms[tOffset + 4],
|
||||
data.transforms[tOffset + 5], data.transforms[tOffset + 6],
|
||||
textureId,
|
||||
data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3],
|
||||
data.colors[i], data.materialIds?.[i] ?? 0
|
||||
);
|
||||
|
||||
if (index >= 0) {
|
||||
if (data.clipRect) batcher.setClipRect(index, data.clipRect);
|
||||
if (data.materialOverrides) batcher.setMaterialOverrides(index, data.materialOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
if (bScreenSpace) {
|
||||
this.screenSpaceItemCount = this.addRenderItem(
|
||||
this.screenSpaceItems, this.screenSpaceItemCount,
|
||||
sortKey, this.screenSpaceItemCount, startIndex, data.tileCount
|
||||
);
|
||||
} else {
|
||||
this.worldSpaceItemCount = this.addRenderItem(
|
||||
this.worldSpaceItems, this.worldSpaceItemCount,
|
||||
sortKey, this.worldSpaceItemCount, startIndex, data.tileCount
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render world space content.
|
||||
* 渲染世界空间内容。
|
||||
*/
|
||||
private renderWorldSpacePass(
|
||||
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[]; addIndex?: number }>
|
||||
): void {
|
||||
// Sort by sortKey (lower values render first, appear behind)
|
||||
// Use addIndex as secondary key for stable sorting when sortKeys are equal
|
||||
// 按 sortKey 排序(值越小越先渲染,显示在后面)
|
||||
// 当 sortKey 相等时使用 addIndex 作为次要排序键以实现稳定排序
|
||||
worldSpaceItems.sort((a, b) => {
|
||||
const diff = a.sortKey - b.sortKey;
|
||||
if (diff !== 0) return diff;
|
||||
return (a.addIndex ?? 0) - (b.addIndex ?? 0);
|
||||
});
|
||||
private renderWorldSpace(): void {
|
||||
const items = this.worldSpaceItems;
|
||||
const count = this.worldSpaceItemCount;
|
||||
|
||||
// DEBUG: 输出排序后的世界空间渲染项
|
||||
// DEBUG: Output sorted world space items
|
||||
if ((globalThis as any).__UI_RENDER_DEBUG__) {
|
||||
console.log('[EngineRenderSystem] World items after sort:', worldSpaceItems.map((item, i) => ({
|
||||
index: i,
|
||||
sortKey: item.sortKey,
|
||||
addIndex: item.addIndex,
|
||||
spriteCount: item.sprites.length,
|
||||
firstTextureId: item.sprites[0]?.textureId
|
||||
})));
|
||||
}
|
||||
|
||||
// Submit all sprites in sorted order
|
||||
// 按排序顺序提交所有 sprites
|
||||
for (const item of worldSpaceItems) {
|
||||
for (const sprite of item.sprites) {
|
||||
this.batcher.addSprite(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.batcher.isEmpty) {
|
||||
const sprites = this.batcher.getSprites();
|
||||
|
||||
// Apply material overrides before rendering
|
||||
// 在渲染前应用材质覆盖
|
||||
this.applySpriteMaterialOverrides(sprites);
|
||||
|
||||
this.bridge.submitSprites(sprites);
|
||||
}
|
||||
|
||||
// Draw gizmos
|
||||
// 绘制 Gizmo
|
||||
if (this.showGizmos) {
|
||||
this.drawComponentGizmos();
|
||||
}
|
||||
|
||||
if (this.showGizmos && this.selectedEntityIds.size > 0) {
|
||||
this.drawSelectedEntityGizmos();
|
||||
}
|
||||
|
||||
if (this.showGizmos) {
|
||||
this.drawCameraFrustums();
|
||||
}
|
||||
|
||||
if (this.showGizmos && this.showUICanvasBoundary && this.uiCanvasWidth > 0 && this.uiCanvasHeight > 0) {
|
||||
this.drawUICanvasBoundary();
|
||||
}
|
||||
|
||||
// Render world content
|
||||
// 渲染世界内容
|
||||
this.bridge.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render screen space content (UI, ScreenOverlay, Modal).
|
||||
* 渲染屏幕空间内容(UI、屏幕覆盖层、模态层)。
|
||||
*/
|
||||
private renderScreenSpacePass(
|
||||
screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[]; addIndex?: number }>
|
||||
): void {
|
||||
// Sort by sortKey, use addIndex for stable sorting when equal
|
||||
// 按 sortKey 排序,当相等时使用 addIndex 实现稳定排序
|
||||
screenSpaceItems.sort((a, b) => {
|
||||
const diff = a.sortKey - b.sortKey;
|
||||
if (diff !== 0) return diff;
|
||||
return (a.addIndex ?? 0) - (b.addIndex ?? 0);
|
||||
});
|
||||
|
||||
// Switch to screen space projection
|
||||
// 切换到屏幕空间投影
|
||||
const canvasWidth = this.uiCanvasWidth > 0 ? this.uiCanvasWidth : 1920;
|
||||
const canvasHeight = this.uiCanvasHeight > 0 ? this.uiCanvasHeight : 1080;
|
||||
|
||||
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
|
||||
|
||||
// Group sprites by clipRect (in render order)
|
||||
// 按 clipRect 分组 sprites(按渲染顺序)
|
||||
type ClipGroup = {
|
||||
clipRect: { x: number; y: number; width: number; height: number } | undefined;
|
||||
sprites: SpriteRenderData[];
|
||||
};
|
||||
|
||||
const clipGroups: ClipGroup[] = [];
|
||||
let currentClipRect: { x: number; y: number; width: number; height: number } | undefined = undefined;
|
||||
let currentGroup: SpriteRenderData[] = [];
|
||||
|
||||
// Helper to check if two clip rects are equal
|
||||
// 辅助函数检查两个裁剪矩形是否相等
|
||||
const clipRectsEqual = (
|
||||
a: { x: number; y: number; width: number; height: number } | undefined,
|
||||
b: { x: number; y: number; width: number; height: number } | undefined
|
||||
): boolean => {
|
||||
if (a === b) return true;
|
||||
if (!a || !b) return false;
|
||||
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
||||
};
|
||||
|
||||
// Group sprites by consecutive clipRect
|
||||
// 按连续的 clipRect 分组 sprites
|
||||
for (const item of screenSpaceItems) {
|
||||
for (const sprite of item.sprites) {
|
||||
const spriteClipRect = sprite.clipRect;
|
||||
|
||||
if (!clipRectsEqual(spriteClipRect, currentClipRect)) {
|
||||
// Save current group if not empty
|
||||
// 如果当前组不为空则保存
|
||||
if (currentGroup.length > 0) {
|
||||
clipGroups.push({ clipRect: currentClipRect, sprites: currentGroup });
|
||||
// Sort by sortKey, use addIndex for stable sorting
|
||||
if (count > 1) {
|
||||
for (let i = 0; i < count - 1; i++) {
|
||||
for (let j = i + 1; j < count; j++) {
|
||||
const a = items[i], b = items[j];
|
||||
const diff = a.sortKey - b.sortKey;
|
||||
if (diff > 0 || (diff === 0 && a.addIndex > b.addIndex)) {
|
||||
items[i] = b;
|
||||
items[j] = a;
|
||||
}
|
||||
// Start new group
|
||||
// 开始新组
|
||||
currentClipRect = spriteClipRect;
|
||||
currentGroup = [sprite];
|
||||
} else {
|
||||
currentGroup.push(sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last group
|
||||
// 别忘了最后一组
|
||||
if (currentGroup.length > 0) {
|
||||
clipGroups.push({ clipRect: currentClipRect, sprites: currentGroup });
|
||||
if (!this.worldBatcher.isEmpty) {
|
||||
const buffers = this.worldBatcher.getBuffers();
|
||||
this.bridge.submitSprites(
|
||||
buffers.transforms, buffers.textureIds, buffers.uvs,
|
||||
buffers.colors, buffers.materialIds, buffers.count
|
||||
);
|
||||
}
|
||||
|
||||
// Render each clip group
|
||||
// 渲染每个裁剪组
|
||||
for (const group of clipGroups) {
|
||||
// Set or clear scissor rect
|
||||
// 设置或清除裁剪矩形
|
||||
if (group.clipRect) {
|
||||
this.bridge.setScissorRect(
|
||||
group.clipRect.x,
|
||||
group.clipRect.y,
|
||||
group.clipRect.width,
|
||||
group.clipRect.height
|
||||
);
|
||||
} else {
|
||||
this.bridge.clearScissorRect();
|
||||
}
|
||||
|
||||
// Clear batcher and add sprites
|
||||
// 清空批处理器并添加 sprites
|
||||
this.batcher.clear();
|
||||
for (const sprite of group.sprites) {
|
||||
this.batcher.addSprite(sprite);
|
||||
}
|
||||
|
||||
if (!this.batcher.isEmpty) {
|
||||
const sprites = this.batcher.getSprites();
|
||||
|
||||
// Apply material overrides before rendering
|
||||
// 在渲染前应用材质覆盖
|
||||
this.applySpriteMaterialOverrides(sprites);
|
||||
|
||||
this.bridge.submitSprites(sprites);
|
||||
// Render overlay (without clearing screen)
|
||||
// 渲染叠加层(不清屏)
|
||||
this.bridge.renderOverlay();
|
||||
if (this.showGizmos) {
|
||||
this.drawComponentGizmos();
|
||||
if (this.selectedEntityIds.size > 0) this.drawSelectedEntityGizmos();
|
||||
this.drawCameraFrustums();
|
||||
if (this.showUICanvasBoundary && this.uiCanvasWidth > 0 && this.uiCanvasHeight > 0) {
|
||||
this.drawUICanvasBoundary();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear scissor rect after all groups
|
||||
// 所有组渲染完后清除裁剪矩形
|
||||
this.bridge.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render screen space content (UI overlay).
|
||||
* 渲染屏幕空间内容(UI 叠加层)。
|
||||
*/
|
||||
private renderScreenSpace(): void {
|
||||
const items = this.screenSpaceItems;
|
||||
const count = this.screenSpaceItemCount;
|
||||
|
||||
if (count > 1) {
|
||||
for (let i = 0; i < count - 1; i++) {
|
||||
for (let j = i + 1; j < count; j++) {
|
||||
const a = items[i], b = items[j];
|
||||
const diff = a.sortKey - b.sortKey;
|
||||
if (diff > 0 || (diff === 0 && a.addIndex > b.addIndex)) {
|
||||
items[i] = b;
|
||||
items[j] = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const canvasWidth = this.uiCanvasWidth > 0 ? this.uiCanvasWidth : 1920;
|
||||
const canvasHeight = this.uiCanvasHeight > 0 ? this.uiCanvasHeight : 1080;
|
||||
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
|
||||
|
||||
if (!this.screenBatcher.isEmpty) {
|
||||
const buffers = this.screenBatcher.getBuffers();
|
||||
this.bridge.submitSprites(
|
||||
buffers.transforms, buffers.textureIds, buffers.uvs,
|
||||
buffers.colors, buffers.materialIds, buffers.count
|
||||
);
|
||||
this.bridge.renderOverlay();
|
||||
}
|
||||
|
||||
this.bridge.clearScissorRect();
|
||||
|
||||
// Restore world space camera
|
||||
// 恢复世界空间相机
|
||||
this.bridge.popScreenSpaceMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash key for material overrides.
|
||||
* 为材质覆盖生成哈希键。
|
||||
*
|
||||
* @param overrides - Material overrides | 材质覆盖
|
||||
* @returns Hash string | 哈希字符串
|
||||
*/
|
||||
private hashMaterialOverrides(overrides: MaterialOverrides): string {
|
||||
// Sort keys for consistent hashing
|
||||
// 排序键以保持一致的哈希
|
||||
const sortedKeys = Object.keys(overrides).sort();
|
||||
const parts: string[] = [];
|
||||
for (const key of sortedKeys) {
|
||||
const override = overrides[key];
|
||||
if (override) {
|
||||
const valueStr = Array.isArray(override.value)
|
||||
? override.value.map(v => v.toFixed(4)).join(',')
|
||||
: override.value.toFixed(4);
|
||||
parts.push(`${key}:${valueStr}`);
|
||||
}
|
||||
}
|
||||
return parts.join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a material instance for a specific base material + overrides combination.
|
||||
* 为特定的基础材质+覆盖组合获取或创建材质实例。
|
||||
*
|
||||
* This ensures each unique (baseMaterial, overrides) combination gets its own
|
||||
* material instance, preventing shared material state issues.
|
||||
* 这确保每个唯一的(基础材质,覆盖)组合都有自己的材质实例,
|
||||
* 防止共享材质状态问题。
|
||||
*
|
||||
* @param baseMaterialId - Base material ID (e.g., 1 for Grayscale) | 基础材质ID
|
||||
* @param overrides - Material property overrides | 材质属性覆盖
|
||||
* @returns Instance material ID | 实例材质ID
|
||||
*/
|
||||
private getOrCreateMaterialInstance(baseMaterialId: number, overrides: MaterialOverrides): number {
|
||||
const overridesHash = this.hashMaterialOverrides(overrides);
|
||||
const instanceKey = `${baseMaterialId}:${overridesHash}`;
|
||||
|
||||
// Check if instance already exists
|
||||
// 检查实例是否已存在
|
||||
let instanceId = this.materialInstanceMap.get(instanceKey);
|
||||
if (instanceId !== undefined) {
|
||||
this.usedInstancesThisFrame.add(instanceId);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
// Create new instance
|
||||
// 创建新实例
|
||||
instanceId = this.nextMaterialInstanceId++;
|
||||
this.materialInstanceMap.set(instanceKey, instanceId);
|
||||
this.usedInstancesThisFrame.add(instanceId);
|
||||
|
||||
// Clone the base material with the new ID
|
||||
// 使用新ID克隆基础材质
|
||||
// For built-in materials, shaderId = materialId (1:1 mapping)
|
||||
// 对于内置材质,shaderId = materialId(1:1 映射)
|
||||
const shaderId = baseMaterialId;
|
||||
const blendMode = 1; // Alpha blending
|
||||
this.bridge.createMaterialWithId(instanceId, `Instance_${baseMaterialId}_${instanceId}`, shaderId, blendMode);
|
||||
|
||||
// Apply overrides to the new instance
|
||||
// 将覆盖应用到新实例
|
||||
this.bridge.applyMaterialOverrides(instanceId, overrides);
|
||||
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up unused material instances.
|
||||
* 清理未使用的材质实例。
|
||||
*
|
||||
* Called at the end of each frame to remove instances that were not used.
|
||||
* 在每帧结束时调用,移除未使用的实例。
|
||||
*/
|
||||
private cleanupUnusedMaterialInstances(): void {
|
||||
const toRemove: string[] = [];
|
||||
|
||||
for (const [key, instanceId] of this.materialInstanceMap.entries()) {
|
||||
if (!this.usedInstancesThisFrame.has(instanceId)) {
|
||||
this.bridge.removeMaterial(instanceId);
|
||||
toRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of toRemove) {
|
||||
this.materialInstanceMap.delete(key);
|
||||
}
|
||||
|
||||
// Clear the used set for next frame
|
||||
// 清除已用集合以便下一帧
|
||||
this.usedInstancesThisFrame.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply material overrides from sprites to the engine.
|
||||
* 将 sprites 的材质覆盖应用到引擎。
|
||||
*
|
||||
* For sprites with overrides, this creates unique material instances
|
||||
* to ensure each sprite's overrides don't affect other sprites.
|
||||
* 对于有覆盖的精灵,这会创建唯一的材质实例,
|
||||
* 确保每个精灵的覆盖不会影响其他精灵。
|
||||
*/
|
||||
private applySpriteMaterialOverrides(sprites: SpriteRenderData[]): void {
|
||||
// Track which instance materials we've already applied overrides to this frame
|
||||
// 跟踪本帧已应用覆盖的实例材质
|
||||
const appliedInstances = new Set<number>();
|
||||
|
||||
for (const sprite of sprites) {
|
||||
const baseMaterialId = sprite.materialId;
|
||||
|
||||
// Skip if no material or no overrides
|
||||
// 如果没有材质或没有覆盖,跳过
|
||||
if (!baseMaterialId || baseMaterialId <= 0 || !sprite.materialOverrides) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const overrideKeys = Object.keys(sprite.materialOverrides);
|
||||
if (overrideKeys.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get or create a unique material instance for this sprite's overrides
|
||||
// 为此精灵的覆盖获取或创建唯一的材质实例
|
||||
const instanceId = this.getOrCreateMaterialInstance(baseMaterialId, sprite.materialOverrides);
|
||||
|
||||
// Update the sprite to use the instance material
|
||||
// 更新精灵以使用实例材质
|
||||
sprite.materialId = instanceId;
|
||||
|
||||
// Apply overrides if not already done for this instance
|
||||
// 如果尚未为此实例应用覆盖,则应用
|
||||
if (!appliedInstances.has(instanceId)) {
|
||||
this.bridge.applyMaterialOverrides(instanceId, sprite.materialOverrides);
|
||||
appliedInstances.add(instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert provider render data to sprite render data array.
|
||||
* 将提供者渲染数据转换为 Sprite 渲染数据数组。
|
||||
*/
|
||||
private convertProviderDataToSprites(data: ProviderRenderData): SpriteRenderData[] {
|
||||
// Get texture ID - load from GUID if needed
|
||||
// 获取纹理 ID - 如果需要从 GUID 加载
|
||||
let textureId = data.textureIds[0] || 0;
|
||||
if (textureId === 0 && data.textureGuid) {
|
||||
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(data.textureGuid));
|
||||
}
|
||||
|
||||
// Check for material data
|
||||
// 检查材质数据
|
||||
const hasMaterialIds = data.materialIds && data.materialIds.length > 0;
|
||||
const hasMaterialOverrides = data.materialOverrides && Object.keys(data.materialOverrides).length > 0;
|
||||
const hasClipRect = !!data.clipRect;
|
||||
|
||||
const sprites: SpriteRenderData[] = [];
|
||||
for (let i = 0; i < data.tileCount; i++) {
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
|
||||
const uv: [number, number, number, number] = [
|
||||
data.uvs[uvOffset],
|
||||
data.uvs[uvOffset + 1],
|
||||
data.uvs[uvOffset + 2],
|
||||
data.uvs[uvOffset + 3]
|
||||
];
|
||||
|
||||
const renderData: SpriteRenderData = {
|
||||
x: data.transforms[tOffset],
|
||||
y: data.transforms[tOffset + 1],
|
||||
rotation: data.transforms[tOffset + 2],
|
||||
scaleX: data.transforms[tOffset + 3],
|
||||
scaleY: data.transforms[tOffset + 4],
|
||||
originX: data.transforms[tOffset + 5],
|
||||
originY: data.transforms[tOffset + 6],
|
||||
textureId,
|
||||
uv,
|
||||
color: data.colors[i]
|
||||
};
|
||||
|
||||
// Add material data if present
|
||||
// 如果存在材质数据,添加它
|
||||
if (hasMaterialIds) {
|
||||
renderData.materialId = data.materialIds![i];
|
||||
}
|
||||
if (hasMaterialOverrides) {
|
||||
renderData.materialOverrides = data.materialOverrides;
|
||||
}
|
||||
// Add clipRect if present (all sprites in batch share same clipRect)
|
||||
// 如果存在 clipRect,添加它(批次中所有精灵共享相同 clipRect)
|
||||
if (hasClipRect) {
|
||||
renderData.clipRect = data.clipRect;
|
||||
}
|
||||
|
||||
sprites.push(renderData);
|
||||
}
|
||||
|
||||
return sprites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw gizmos from components that have registered gizmo providers.
|
||||
* 绘制已注册 gizmo 提供者的组件的 gizmo。
|
||||
@@ -1564,7 +1261,7 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
* 获取渲染的精灵数量。
|
||||
*/
|
||||
get spriteCount(): number {
|
||||
return this.batcher.count;
|
||||
return this.worldBatcher.count + this.screenBatcher.count;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,21 +17,121 @@ export interface Matrix2D {
|
||||
ty: number; // translateY
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactive Vector3 that automatically sets dirty flag on parent transform.
|
||||
* 响应式 Vector3,在修改时自动设置父变换的脏标记。
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ReactiveVector3 implements IVector3 {
|
||||
private _x: number;
|
||||
private _y: number;
|
||||
private _z: number;
|
||||
private _owner: TransformComponent;
|
||||
|
||||
constructor(owner: TransformComponent, x: number = 0, y: number = 0, z: number = 0) {
|
||||
this._owner = owner;
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._z = z;
|
||||
}
|
||||
|
||||
get x(): number { return this._x; }
|
||||
set x(value: number) {
|
||||
if (this._x !== value) {
|
||||
this._x = value;
|
||||
this._owner.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
get y(): number { return this._y; }
|
||||
set y(value: number) {
|
||||
if (this._y !== value) {
|
||||
this._y = value;
|
||||
this._owner.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
get z(): number { return this._z; }
|
||||
set z(value: number) {
|
||||
if (this._z !== value) {
|
||||
this._z = value;
|
||||
this._owner.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all components at once (more efficient than setting individually).
|
||||
* 一次性设置所有分量(比单独设置更高效)。
|
||||
*/
|
||||
set(x: number, y: number, z: number = this._z): void {
|
||||
const changed = this._x !== x || this._y !== y || this._z !== z;
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._z = z;
|
||||
if (changed) {
|
||||
this._owner.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy from another vector.
|
||||
* 从另一个向量复制。
|
||||
*/
|
||||
copyFrom(v: IVector3): void {
|
||||
this.set(v.x, v.y, v.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw values without triggering getters (for serialization).
|
||||
* 获取原始值而不触发 getter(用于序列化)。
|
||||
*/
|
||||
toObject(): IVector3 {
|
||||
return { x: this._x, y: this._y, z: this._z };
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Transform')
|
||||
@Serializable({ version: 1, typeId: 'Transform' })
|
||||
export class TransformComponent extends Component {
|
||||
// ===== 内部响应式存储 =====
|
||||
private _position: ReactiveVector3;
|
||||
private _rotation: ReactiveVector3;
|
||||
private _scale: ReactiveVector3;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'vector3', label: 'Position' })
|
||||
position: IVector3 = { x: 0, y: 0, z: 0 };
|
||||
get position(): IVector3 { return this._position; }
|
||||
set position(value: IVector3) {
|
||||
if (this._position) {
|
||||
this._position.copyFrom(value);
|
||||
} else {
|
||||
this._position = new ReactiveVector3(this, value.x, value.y, value.z);
|
||||
}
|
||||
}
|
||||
|
||||
/** 欧拉角,单位:度 */
|
||||
/** 欧拉角,单位:度 | Euler angles in degrees */
|
||||
@Serialize()
|
||||
@Property({ type: 'vector3', label: 'Rotation' })
|
||||
rotation: IVector3 = { x: 0, y: 0, z: 0 };
|
||||
get rotation(): IVector3 { return this._rotation; }
|
||||
set rotation(value: IVector3) {
|
||||
if (this._rotation) {
|
||||
this._rotation.copyFrom(value);
|
||||
} else {
|
||||
this._rotation = new ReactiveVector3(this, value.x, value.y, value.z);
|
||||
}
|
||||
}
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'vector3', label: 'Scale' })
|
||||
scale: IVector3 = { x: 1, y: 1, z: 1 };
|
||||
get scale(): IVector3 { return this._scale; }
|
||||
set scale(value: IVector3) {
|
||||
if (this._scale) {
|
||||
this._scale.copyFrom(value);
|
||||
} else {
|
||||
this._scale = new ReactiveVector3(this, value.x, value.y, value.z);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 世界变换(由 TransformSystem 计算)=====
|
||||
|
||||
@@ -47,31 +147,64 @@ export class TransformComponent extends Component {
|
||||
/** 本地到世界的 2D 变换矩阵(只读,由 TransformSystem 计算) */
|
||||
localToWorldMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
||||
|
||||
/** 变换是否需要更新 */
|
||||
/** 变换是否需要更新 | Whether transform needs update */
|
||||
bDirty: boolean = true;
|
||||
|
||||
/**
|
||||
* Frame counter when last updated (for change detection).
|
||||
* 上次更新的帧计数器(用于变化检测)。
|
||||
* @internal
|
||||
*/
|
||||
_lastUpdateFrame: number = -1;
|
||||
|
||||
/**
|
||||
* Cached render data version (incremented when transform changes).
|
||||
* 缓存的渲染数据版本(变换改变时递增)。
|
||||
* @internal
|
||||
*/
|
||||
_version: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.position = { x, y, z };
|
||||
this._position = new ReactiveVector3(this, x, y, z);
|
||||
this._rotation = new ReactiveVector3(this, 0, 0, 0);
|
||||
this._scale = new ReactiveVector3(this, 1, 1, 1);
|
||||
// 初始化世界变换为本地变换值(在 TransformSystem 更新前使用)
|
||||
this.worldPosition = { x, y, z };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark transform as dirty and increment version.
|
||||
* 标记变换为脏并递增版本号。
|
||||
*/
|
||||
markDirty(): void {
|
||||
if (!this.bDirty) {
|
||||
this.bDirty = true;
|
||||
this._version++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear dirty flag (called by TransformSystem after update).
|
||||
* 清除脏标记(由 TransformSystem 更新后调用)。
|
||||
*/
|
||||
clearDirty(frameNumber: number): void {
|
||||
this.bDirty = false;
|
||||
this._lastUpdateFrame = frameNumber;
|
||||
}
|
||||
|
||||
setPosition(x: number, y: number, z: number = 0): this {
|
||||
this.position = { x, y, z };
|
||||
this.bDirty = true;
|
||||
this._position.set(x, y, z);
|
||||
return this;
|
||||
}
|
||||
|
||||
setRotation(x: number, y: number, z: number): this {
|
||||
this.rotation = { x, y, z };
|
||||
this.bDirty = true;
|
||||
this._rotation.set(x, y, z);
|
||||
return this;
|
||||
}
|
||||
|
||||
setScale(x: number, y: number, z: number = 1): this {
|
||||
this.scale = { x, y, z };
|
||||
this.bDirty = true;
|
||||
this._scale.set(x, y, z);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user