Files
esengine/packages/particle/src/rendering/ParticleRenderDataProvider.ts
YHH ed8f6e283b feat: 纹理路径稳定 ID 与架构改进 (#305)
* feat(asset-system): 实现路径稳定 ID 生成器

使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID:
- 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定
- 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID
- 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID
- clearTextureMappings() 不再清除 _pathIdCache

这解决了 Play/Stop 后纹理 ID 失效的根本问题。

* fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用

使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存:
- saveSceneSnapshot() 移除 clearTextureMappings() 调用
- restoreSceneSnapshot() 移除 clearTextureMappings() 调用
- 组件保存的 textureId 在 Play/Stop 后仍然有效

* fix(editor-core): 修复场景切换时的资源泄漏

在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性

* fix(runtime-core): 修复 PluginManager 组件注册类型错误

将 ComponentRegistry 类改为 GlobalComponentRegistry 实例:
- registerComponents() 期望 IComponentRegistry 接口实例
- GlobalComponentRegistry 是 ComponentRegistry 的全局实例

* refactor(core): 提取 IComponentRegistry 接口

将组件注册表抽象为接口,支持场景级组件注册:
- 新增 IComponentRegistry 接口定义
- Scene 持有独立的 componentRegistry 实例
- 支持从 GlobalComponentRegistry 克隆
- 各系统支持传入自定义注册表

* refactor(engine-core): 改进插件服务注册机制

- 更新 IComponentRegistry 类型引用
- 优化 PluginServiceRegistry 服务管理

* refactor(modules): 适配新的组件注册接口

更新各模块 RuntimeModule 使用 IComponentRegistry 接口:
- audio, behavior-tree, camera
- sprite, tilemap, world-streaming

* fix(physics-rapier2d): 修复物理插件组件注册

- PhysicsEditorPlugin 添加 runtimeModule 引用
- 适配 IComponentRegistry 接口
- 修复物理组件在场景加载时未注册的问题

* feat(editor-core): 添加 UserCodeService 就绪信号机制

- 新增 waitForReady()/signalReady() API
- 支持等待用户脚本编译完成
- 解决场景加载时组件未注册的时序问题

* fix(editor-app): 在编译完成后调用 signalReady()

确保用户脚本编译完成后发出就绪信号:
- 编译成功后调用 userCodeService.signalReady()
- 编译失败也要发出信号,避免阻塞场景加载

* feat(editor-core): 改进编辑器核心服务

- EntityStoreService 添加调试日志
- AssetRegistryService 优化资产注册
- PluginManager 改进插件管理
- IFileAPI 添加 getFileMtime 接口

* feat(engine): 改进 Rust 纹理管理器

- 支持任意 ID 的纹理加载(非递增)
- 添加纹理状态追踪 API
- 优化纹理缓存清理机制
- 更新 TypeScript 绑定

* feat(ui): 添加场景切换和文本闪烁组件

新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化

* feat(editor-app): 添加外部文件修改检测

- 新增 ExternalModificationDialog 组件
- TauriFileAPI 支持 getFileMtime
- 场景文件被外部修改时提示用户

* feat(editor-app): 添加渲染调试面板

- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进

* refactor(editor-app): 编辑器服务和组件优化

- EngineService 改进引擎集成
- EditorEngineSync 同步优化
- AssetFileInspector 改进
- VectorFieldEditors 优化
- InstantiatePrefabCommand 改进

* feat(i18n): 更新国际化翻译

- 添加新功能相关翻译
- 更新中文、英文、西班牙文

* feat(tauri): 添加文件修改时间查询命令

- 新增 get_file_mtime 命令
- 支持检测文件外部修改

* refactor(particle): 粒子系统改进

- 适配新的组件注册接口
- ParticleSystem 优化
- 添加单元测试

* refactor(platform): 平台适配层优化

- BrowserRuntime 改进
- 新增 RuntimeSceneManager 服务
- 导出优化

* refactor(asset-system-editor): 资产元数据改进

- AssetMetaFile 优化
- 导出调整

* fix(asset-system): 移除未使用的 TextureLoader 导入

* fix(tests): 更新测试以使用 GlobalComponentRegistry 实例

修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更:
- ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset()
- EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例
- IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例
- SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例
- ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry
- SystemTypes.test.ts: 在 Scene 创建前注册组件
- QuerySystem.test.ts: mockScene 添加 componentRegistry
2025-12-16 12:46:14 +08:00

280 lines
11 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.
import { type ParticleSystemComponent, RenderSpace } from '../ParticleSystemComponent';
import { Color } from '@esengine/ecs-framework-math';
import { sortingLayerManager, SortingLayers } from '@esengine/engine-core';
/**
* 粒子渲染数据(与 EngineRenderSystem 兼容)
* Particle render data (compatible with EngineRenderSystem)
*
* This interface is compatible with ProviderRenderData from EngineRenderSystem.
* 此接口与 EngineRenderSystem 的 ProviderRenderData 兼容。
*/
export interface ParticleProviderRenderData {
transforms: Float32Array;
textureIds: Uint32Array;
uvs: Float32Array;
colors: Uint32Array;
tileCount: number;
/**
* 排序层名称
* Sorting layer name
*/
sortingLayer: string;
/**
* 层内排序顺序
* Order within the sorting layer
*/
orderInLayer: number;
/** 纹理 GUID | Texture GUID */
textureGuid?: string;
/**
* 是否在屏幕空间渲染
* Whether to render in screen space
*/
bScreenSpace?: boolean;
}
/**
* Transform 接口(避免直接依赖 engine-core
* Transform interface (avoid direct dependency on engine-core)
*/
interface ITransformLike {
worldPosition?: { x: number; y: number };
position: { x: number; y: number };
}
/**
* 渲染数据提供者接口(与 EngineRenderSystem 兼容)
* Render data provider interface (compatible with EngineRenderSystem)
*
* This interface matches IRenderDataProvider from @esengine/ecs-engine-bindgen.
* 此接口与 @esengine/ecs-engine-bindgen 的 IRenderDataProvider 匹配。
*/
export interface IRenderDataProvider {
getRenderData(): readonly ParticleProviderRenderData[];
}
/**
* 粒子渲染数据提供者
* Particle render data provider
*
* Collects render data from all active particle systems.
* 从所有活跃的粒子系统收集渲染数据。
*
* Implements IRenderDataProvider for integration with EngineRenderSystem.
* 实现 IRenderDataProvider 以便与 EngineRenderSystem 集成。
*/
export class ParticleRenderDataProvider implements IRenderDataProvider {
private _particleSystems: Map<ParticleSystemComponent, ITransformLike> = new Map();
private _renderDataCache: ParticleProviderRenderData[] = [];
private _dirty: boolean = true;
// 预分配的缓冲区 | Pre-allocated buffers
private _maxParticles: number = 0;
private _transforms: Float32Array = new Float32Array(0);
private _textureIds: Uint32Array = new Uint32Array(0);
private _uvs: Float32Array = new Float32Array(0);
private _colors: Uint32Array = new Uint32Array(0);
/**
* 注册粒子系统
* Register particle system
*/
register(component: ParticleSystemComponent, transform: ITransformLike): void {
this._particleSystems.set(component, transform);
this._dirty = true;
}
/**
* 注销粒子系统
* Unregister particle system
*/
unregister(component: ParticleSystemComponent): void {
this._particleSystems.delete(component);
this._dirty = true;
}
/**
* 标记为脏
* Mark as dirty
*/
markDirty(): void {
this._dirty = true;
}
/**
* 获取渲染数据
* Get render data
*/
getRenderData(): readonly ParticleProviderRenderData[] {
this._updateRenderData();
return this._renderDataCache;
}
private _updateRenderData(): void {
this._renderDataCache.length = 0;
// 计算总粒子数 | Calculate total particle count
let totalParticles = 0;
for (const [component] of this._particleSystems) {
if (component.isPlaying && component.pool) {
totalParticles += component.pool.activeCount;
}
}
if (totalParticles === 0) return;
// 确保缓冲区足够大 | Ensure buffers are large enough
if (totalParticles > this._maxParticles) {
this._maxParticles = Math.max(totalParticles, this._maxParticles * 2, 1000);
this._transforms = new Float32Array(this._maxParticles * 7);
this._textureIds = new Uint32Array(this._maxParticles);
this._uvs = new Float32Array(this._maxParticles * 4);
this._colors = new Uint32Array(this._maxParticles);
}
// 按 sortKey + renderSpace 分组
// Group by sortKey + renderSpace
// 使用 string key 来区分不同渲染空间的相同 sortKey
// Use string key to distinguish same sortKey with different render spaces
const groups = new Map<string, {
component: ParticleSystemComponent;
transform: ITransformLike;
sortingLayer: string;
orderInLayer: number;
bScreenSpace: boolean;
sortKey: number;
}[]>();
for (const [component, transform] of this._particleSystems) {
if (!component.isPlaying || !component.pool || component.pool.activeCount === 0) {
continue;
}
const sortingLayer = component.sortingLayer ?? SortingLayers.Default;
const orderInLayer = component.orderInLayer ?? 0;
const sortKey = sortingLayerManager.getSortKey(sortingLayer, orderInLayer);
const bScreenSpace = component.renderSpace === RenderSpace.Screen;
const groupKey = `${sortKey}:${bScreenSpace ? 'screen' : 'world'}`;
if (!groups.has(groupKey)) {
groups.set(groupKey, []);
}
groups.get(groupKey)!.push({ component, transform, sortingLayer, orderInLayer, bScreenSpace, sortKey });
}
// 按 sortKey 排序后生成渲染数据
// Generate render data sorted by sortKey
// 字符串 key 格式: "sortKey:space",按 sortKey 数值排序
const sortedKeys = [...groups.keys()].sort((a, b) => {
const aKey = parseInt(a.split(':')[0], 10);
const bKey = parseInt(b.split(':')[0], 10);
return aKey - bKey;
});
for (const groupKey of sortedKeys) {
const systems = groups.get(groupKey)!;
let particleIndex = 0;
for (const { component } of systems) {
const pool = component.pool!;
const size = component.particleSize;
pool.forEachActive((p) => {
const tOffset = particleIndex * 7;
const uvOffset = particleIndex * 4;
// Transform: x, y, rotation, scaleX, scaleY, originX, originY
this._transforms[tOffset] = p.x;
this._transforms[tOffset + 1] = p.y;
this._transforms[tOffset + 2] = p.rotation;
this._transforms[tOffset + 3] = size * p.scaleX;
this._transforms[tOffset + 4] = size * p.scaleY;
this._transforms[tOffset + 5] = 0.5; // originX
this._transforms[tOffset + 6] = 0.5; // originY
// Texture ID: 优先使用组件上预加载的 textureId否则让 EngineRenderSystem 通过 textureGuid 解析
// Prefer using pre-loaded textureId from component, otherwise let EngineRenderSystem resolve via textureGuid
this._textureIds[particleIndex] = component.textureId;
// UV - 支持精灵图帧动画 | Support spritesheet animation
if (p._animTilesX !== undefined && p._animTilesY !== undefined && p._animFrame !== undefined) {
// 计算帧的 UV 坐标 | Calculate frame UV coordinates
// WebGL 纹理坐标V=0 采样纹理行0即图像顶部
// WebGL texture coords: V=0 samples texture row 0 (image top)
const tilesX = p._animTilesX;
const tilesY = p._animTilesY;
const frame = p._animFrame;
const col = frame % tilesX;
const row = Math.floor(frame / tilesX);
const uWidth = 1 / tilesX;
const vHeight = 1 / tilesY;
// UV: u0, v0, u1, v1
const u0 = col * uWidth;
const u1 = (col + 1) * uWidth;
const v0 = row * vHeight;
const v1 = (row + 1) * vHeight;
this._uvs[uvOffset] = u0;
this._uvs[uvOffset + 1] = v0;
this._uvs[uvOffset + 2] = u1;
this._uvs[uvOffset + 3] = v1;
} else {
// 默认:使用完整纹理 | Default: use full texture
this._uvs[uvOffset] = 0;
this._uvs[uvOffset + 1] = 0;
this._uvs[uvOffset + 2] = 1;
this._uvs[uvOffset + 3] = 1;
}
// Color (packed ABGR for WebGL)
this._colors[particleIndex] = Color.packABGR(
Math.round(p.r * 255),
Math.round(p.g * 255),
Math.round(p.b * 255),
p.alpha
);
particleIndex++;
});
}
if (particleIndex > 0) {
// 获取纹理 GUID | Get texture GUID
const firstSystem = systems[0];
const firstComponent = firstSystem?.component;
const asset = firstComponent?.loadedAsset as { textureGuid?: string } | null;
const textureGuid = asset?.textureGuid || firstComponent?.textureGuid || undefined;
// 创建当前组的渲染数据 | Create render data for current group
const renderData: ParticleProviderRenderData = {
transforms: this._transforms.subarray(0, particleIndex * 7),
textureIds: this._textureIds.subarray(0, particleIndex),
uvs: this._uvs.subarray(0, particleIndex * 4),
colors: this._colors.subarray(0, particleIndex),
tileCount: particleIndex,
sortingLayer: firstSystem?.sortingLayer ?? SortingLayers.Default,
orderInLayer: firstSystem?.orderInLayer ?? 0,
textureGuid,
bScreenSpace: firstSystem?.bScreenSpace ?? false
};
this._renderDataCache.push(renderData);
}
}
this._dirty = false;
}
/**
* 清理
* Cleanup
*/
dispose(): void {
this._particleSystems.clear();
this._renderDataCache.length = 0;
}
}