refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
112
packages/rendering/mesh-3d/src/systems/Animation3DSystem.ts
Normal file
112
packages/rendering/mesh-3d/src/systems/Animation3DSystem.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Animation3DSystem - System for updating 3D animations.
|
||||
* Animation3DSystem - 3D 动画更新系统。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, ECSSystem, Entity, Time } from '@esengine/ecs-framework';
|
||||
import { Animation3DComponent } from '../Animation3DComponent';
|
||||
import { SkeletonComponent } from '../SkeletonComponent';
|
||||
import { MeshComponent } from '../MeshComponent';
|
||||
import { AnimationEvaluator } from '../animation/AnimationEvaluator';
|
||||
|
||||
/**
|
||||
* System for updating 3D animation playback.
|
||||
* 用于更新 3D 动画播放的系统。
|
||||
*
|
||||
* Queries all entities with Animation3DComponent,
|
||||
* updates animation time, and applies animation values to skeleton bones.
|
||||
* 查询所有具有 Animation3DComponent 的实体,
|
||||
* 更新动画时间,并将动画值应用到骨骼。
|
||||
*/
|
||||
@ECSSystem('Animation3D', { updateOrder: 100 })
|
||||
export class Animation3DSystem extends EntitySystem {
|
||||
private evaluator: AnimationEvaluator;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(Animation3DComponent).all(MeshComponent));
|
||||
this.evaluator = new AnimationEvaluator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process entities each frame.
|
||||
* 每帧处理实体。
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!entity.enabled) continue;
|
||||
this.updateEntity(entity, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single entity's animation.
|
||||
* 更新单个实体的动画。
|
||||
*/
|
||||
private updateEntity(entity: Entity, deltaTime: number): void {
|
||||
const anim = entity.getComponent(Animation3DComponent);
|
||||
const mesh = entity.getComponent(MeshComponent);
|
||||
|
||||
if (!anim || !mesh) return;
|
||||
|
||||
// Initialize animation clips from mesh asset if needed
|
||||
// 如果需要,从网格资产初始化动画片段
|
||||
if (anim.clips.length === 0 && mesh.meshAsset?.animations) {
|
||||
anim.setClips(mesh.meshAsset.animations);
|
||||
|
||||
// Auto-play if configured
|
||||
// 如果配置了自动播放
|
||||
if (anim.playOnAwake && anim.clips.length > 0) {
|
||||
anim.play();
|
||||
}
|
||||
}
|
||||
|
||||
// Update animation time
|
||||
// 更新动画时间
|
||||
anim.updateTime(deltaTime);
|
||||
|
||||
// Apply animation to skeleton
|
||||
// 将动画应用到骨骼
|
||||
if (anim.isPlaying && anim.currentClip) {
|
||||
this.applyAnimation(entity, anim);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply animation values to skeleton.
|
||||
* 将动画值应用到骨骼。
|
||||
*/
|
||||
private applyAnimation(entity: Entity, anim: Animation3DComponent): void {
|
||||
const skeleton = entity.getComponent(SkeletonComponent);
|
||||
const clip = anim.currentClip;
|
||||
|
||||
if (!clip || !skeleton?.isLoaded) return;
|
||||
|
||||
// Evaluate animation at current time
|
||||
// 在当前时间评估动画
|
||||
const evaluatedValues = this.evaluator.evaluate(clip, anim.currentTime);
|
||||
|
||||
// Apply values to skeleton bones
|
||||
// 将值应用到骨骼
|
||||
for (const [nodeIndex, value] of evaluatedValues) {
|
||||
if (value.path === 'translation') {
|
||||
skeleton.setBoneTransform(nodeIndex, {
|
||||
position: value.value as [number, number, number]
|
||||
});
|
||||
} else if (value.path === 'rotation') {
|
||||
skeleton.setBoneTransform(nodeIndex, {
|
||||
rotation: value.value as [number, number, number, number]
|
||||
});
|
||||
} else if (value.path === 'scale') {
|
||||
skeleton.setBoneTransform(nodeIndex, {
|
||||
scale: value.value as [number, number, number]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mark skeleton as dirty for matrix update
|
||||
// 标记骨骼为脏以更新矩阵
|
||||
skeleton.markDirty();
|
||||
}
|
||||
}
|
||||
124
packages/rendering/mesh-3d/src/systems/MeshAssetLoaderSystem.ts
Normal file
124
packages/rendering/mesh-3d/src/systems/MeshAssetLoaderSystem.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* MeshAssetLoaderSystem - System for loading mesh assets on demand.
|
||||
* MeshAssetLoaderSystem - 按需加载网格资产的系统。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
|
||||
import type { IAssetManager, IGLTFAsset } from '@esengine/asset-system';
|
||||
import { MeshComponent } from '../MeshComponent';
|
||||
|
||||
/**
|
||||
* System for loading mesh assets when modelGuid changes.
|
||||
* 当 modelGuid 变化时加载网格资产的系统。
|
||||
*
|
||||
* This system monitors MeshComponents and loads their model assets
|
||||
* when the modelGuid property is set and the asset isn't loaded yet.
|
||||
* 此系统监视 MeshComponent 并在设置 modelGuid 属性且资产尚未加载时加载其模型资产。
|
||||
*/
|
||||
@ECSSystem('MeshAssetLoader', { updateOrder: 50 })
|
||||
export class MeshAssetLoaderSystem extends EntitySystem {
|
||||
private assetManager: IAssetManager | null = null;
|
||||
private loadingSet: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(MeshComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the asset manager for loading assets.
|
||||
* 设置用于加载资产的资产管理器。
|
||||
*/
|
||||
public setAssetManager(manager: IAssetManager): void {
|
||||
this.assetManager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process entities each frame.
|
||||
* 每帧处理实体。
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
if (!entity.enabled) continue;
|
||||
this.checkAndLoadAsset(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a mesh component needs its asset loaded.
|
||||
* 检查网格组件是否需要加载其资产。
|
||||
*/
|
||||
private checkAndLoadAsset(entity: Entity): void {
|
||||
const mesh = entity.getComponent(MeshComponent);
|
||||
if (!mesh) return;
|
||||
|
||||
// Skip if no modelGuid
|
||||
// 如果没有 modelGuid 则跳过
|
||||
if (!mesh.modelGuid) return;
|
||||
|
||||
// Skip if already loaded
|
||||
// 如果已加载则跳过
|
||||
if (mesh.isLoaded) return;
|
||||
|
||||
// Skip if already loading
|
||||
// 如果正在加载则跳过
|
||||
const loadKey = `${entity.id}:${mesh.modelGuid}`;
|
||||
if (this.loadingSet.has(loadKey)) return;
|
||||
|
||||
// Start loading
|
||||
// 开始加载
|
||||
this.loadingSet.add(loadKey);
|
||||
this.loadMeshAsset(entity, mesh, loadKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a mesh asset using the asset manager.
|
||||
* 使用资产管理器加载网格资产。
|
||||
*/
|
||||
private async loadMeshAsset(entity: Entity, mesh: MeshComponent, loadKey: string): Promise<void> {
|
||||
try {
|
||||
if (!this.assetManager) {
|
||||
console.warn('[MeshAssetLoaderSystem] No asset manager available');
|
||||
return;
|
||||
}
|
||||
|
||||
const modelGuid = mesh.modelGuid;
|
||||
|
||||
// Try to load using asset manager
|
||||
// 尝试使用资产管理器加载
|
||||
console.log(`[MeshAssetLoaderSystem] Loading: ${modelGuid}`);
|
||||
|
||||
// Check if it's a GUID or a path
|
||||
// 检查是否是 GUID 还是路径
|
||||
const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(modelGuid);
|
||||
|
||||
let result;
|
||||
if (isGuid) {
|
||||
result = await this.assetManager.loadAsset<IGLTFAsset>(modelGuid);
|
||||
} else {
|
||||
result = await this.assetManager.loadAssetByPath<IGLTFAsset>(modelGuid);
|
||||
}
|
||||
|
||||
// Check if entity still exists and has the same modelGuid
|
||||
// 检查实体是否仍然存在且 modelGuid 是否相同
|
||||
if (!entity.enabled || mesh.modelGuid !== modelGuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// IAssetLoadResult contains: asset, handle, metadata, loadTime
|
||||
// API throws on error, returns result directly on success
|
||||
// IAssetLoadResult 包含:asset, handle, metadata, loadTime
|
||||
// API 在错误时抛出异常,成功时直接返回结果
|
||||
if (result && result.asset) {
|
||||
mesh.meshAsset = result.asset;
|
||||
console.log(`[MeshAssetLoaderSystem] Loaded: ${modelGuid} (${result.asset.meshes?.length ?? 0} meshes)`);
|
||||
} else {
|
||||
console.warn(`[MeshAssetLoaderSystem] No asset returned for ${modelGuid}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[MeshAssetLoaderSystem] Failed to load ${mesh.modelGuid}:`, error);
|
||||
} finally {
|
||||
this.loadingSet.delete(loadKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
304
packages/rendering/mesh-3d/src/systems/MeshRenderSystem.ts
Normal file
304
packages/rendering/mesh-3d/src/systems/MeshRenderSystem.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* MeshRenderSystem - System for rendering 3D meshes.
|
||||
* MeshRenderSystem - 3D 网格渲染系统。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
|
||||
import type { EngineBridge } from '@esengine/ecs-engine-bindgen';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import type { IMeshData } from '@esengine/asset-system';
|
||||
import { MeshComponent } from '../MeshComponent';
|
||||
|
||||
/**
|
||||
* System for rendering 3D mesh components.
|
||||
* 用于渲染 3D 网格组件的系统。
|
||||
*
|
||||
* Queries all entities with MeshComponent and TransformComponent,
|
||||
* builds interleaved vertex data, and submits to the Rust engine.
|
||||
* 查询所有具有 MeshComponent 和 TransformComponent 的实体,
|
||||
* 构建交错顶点数据并提交到 Rust 引擎。
|
||||
*/
|
||||
@ECSSystem('MeshRender', { updateOrder: 900 })
|
||||
export class MeshRenderSystem extends EntitySystem {
|
||||
private bridge: EngineBridge | null;
|
||||
|
||||
// Reusable buffers for performance
|
||||
// 可重用缓冲区以提高性能
|
||||
private vertexBuffer: Float32Array = new Float32Array(0);
|
||||
private transformBuffer: Float32Array = new Float32Array(16);
|
||||
|
||||
constructor(bridge: EngineBridge | null = null) {
|
||||
super(Matcher.empty().all(MeshComponent).all(TransformComponent));
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the engine bridge (can be called after construction).
|
||||
* 设置引擎桥接(可在构造后调用)。
|
||||
*/
|
||||
public setEngineBridge(bridge: EngineBridge): void {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
// 调试帧计数 | Debug frame counter
|
||||
private _frameCount = 0;
|
||||
private _lastLogTime = 0;
|
||||
|
||||
/**
|
||||
* Process entities each frame.
|
||||
* 每帧处理实体。
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this._frameCount++;
|
||||
|
||||
if (!this.bridge) {
|
||||
if (this._frameCount % 300 === 1) {
|
||||
console.warn('[MeshRenderSystem] No bridge available');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if in 3D mode (mode 1 = 3D)
|
||||
// 检查是否在 3D 模式
|
||||
const renderMode = this.bridge.getRenderMode();
|
||||
|
||||
// Debug: log mode and entity count periodically
|
||||
// 调试:定期记录模式和实体数量
|
||||
const now = Date.now();
|
||||
if (now - this._lastLogTime > 3000) {
|
||||
this._lastLogTime = now;
|
||||
console.log(`[MeshRenderSystem] Mode: ${renderMode}, Entities: ${entities.length}`);
|
||||
|
||||
// Log mesh status for each entity
|
||||
// 记录每个实体的网格状态
|
||||
for (const entity of entities) {
|
||||
const mesh = entity.getComponent(MeshComponent);
|
||||
if (mesh) {
|
||||
console.log(` - Entity ${entity.name}: modelGuid=${mesh.modelGuid?.substring(0, 8)}..., isLoaded=${mesh.isLoaded}, meshCount=${mesh.allMeshes.length}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (renderMode !== 1) {
|
||||
// 2D mode, skip 3D rendering
|
||||
// 2D 模式,跳过 3D 渲染
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!entity.enabled) continue;
|
||||
this.renderEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 调试:上次提交时间 | Debug: last submit time
|
||||
private _lastSubmitLogTime = 0;
|
||||
|
||||
/**
|
||||
* Render a single entity's mesh.
|
||||
* 渲染单个实体的网格。
|
||||
*/
|
||||
private renderEntity(entity: Entity): void {
|
||||
const mesh = entity.getComponent(MeshComponent);
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
|
||||
if (!mesh || !transform || !mesh.visible || !mesh.isLoaded) {
|
||||
// Debug skip reason
|
||||
// 调试跳过原因
|
||||
const now = Date.now();
|
||||
if (now - this._lastSubmitLogTime > 5000) {
|
||||
this._lastSubmitLogTime = now;
|
||||
const reason = !mesh ? 'no mesh' :
|
||||
!transform ? 'no transform' :
|
||||
!mesh.visible ? 'not visible' :
|
||||
!mesh.isLoaded ? 'not loaded' : 'unknown';
|
||||
console.log(`[MeshRenderSystem] Skip ${entity.name}: ${reason}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all meshes to render
|
||||
// 获取所有要渲染的网格
|
||||
const meshesToRender = mesh.allMeshes;
|
||||
if (meshesToRender.length === 0) {
|
||||
console.log(`[MeshRenderSystem] Skip ${entity.name}: no meshes`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build world transform matrix
|
||||
// 构建世界变换矩阵
|
||||
this.buildTransformMatrix(transform);
|
||||
|
||||
// Debug: log transform
|
||||
// 调试:记录变换
|
||||
const now = Date.now();
|
||||
if (now - this._lastSubmitLogTime > 5000) {
|
||||
this._lastSubmitLogTime = now;
|
||||
const pos = transform.position;
|
||||
console.log(`[MeshRenderSystem] Rendering ${entity.name}: ${meshesToRender.length} meshes`);
|
||||
console.log(` Transform: pos(${pos.x?.toFixed(2) ?? 0}, ${pos.y?.toFixed(2) ?? 0}, ${pos.z?.toFixed(2) ?? 0})`);
|
||||
}
|
||||
|
||||
// Render each mesh
|
||||
// 渲染每个网格
|
||||
for (let i = 0; i < meshesToRender.length; i++) {
|
||||
const meshData = meshesToRender[i];
|
||||
if (!meshData) continue;
|
||||
|
||||
// Build interleaved vertex data
|
||||
// 构建交错顶点数据
|
||||
const vertexData = this.buildVertexData(meshData);
|
||||
if (!vertexData) {
|
||||
console.warn(`[MeshRenderSystem] Failed to build vertex data for mesh ${i}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get material and texture IDs
|
||||
// 获取材质和纹理 ID
|
||||
const materialId = mesh.runtimeMaterialIds[i] ?? 0;
|
||||
const textureId = mesh.runtimeTextureIds[i] ?? 0;
|
||||
|
||||
// Debug: log submission
|
||||
// 调试:记录提交
|
||||
if (now - this._lastSubmitLogTime < 100) {
|
||||
console.log(` Submitting mesh ${i}: ${vertexData.length / 9} vertices, ${meshData.indices.length} indices`);
|
||||
}
|
||||
|
||||
// Submit to engine
|
||||
// 提交到引擎
|
||||
try {
|
||||
this.bridge!.submitSimpleMesh3D(
|
||||
vertexData,
|
||||
new Uint32Array(meshData.indices),
|
||||
this.transformBuffer,
|
||||
materialId,
|
||||
textureId
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`[MeshRenderSystem] submitSimpleMesh3D failed:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build 4x4 transform matrix from TransformComponent.
|
||||
* 从 TransformComponent 构建 4x4 变换矩阵。
|
||||
*/
|
||||
private buildTransformMatrix(transform: TransformComponent): void {
|
||||
// Get world position, rotation, scale with safe defaults
|
||||
// 获取世界位置、旋转、缩放(带安全默认值)
|
||||
const rawPos = transform.worldPosition;
|
||||
const rawRot = transform.worldRotation; // Euler angles in degrees
|
||||
const rawScl = transform.worldScale;
|
||||
|
||||
// Safe extraction with defaults for 2D components
|
||||
// 2D 组件的安全提取(带默认值)
|
||||
const pos = { x: rawPos.x ?? 0, y: rawPos.y ?? 0, z: rawPos.z ?? 0 };
|
||||
const rot = { x: rawRot.x ?? 0, y: rawRot.y ?? 0, z: rawRot.z ?? 0 };
|
||||
const scl = { x: rawScl.x ?? 1, y: rawScl.y ?? 1, z: rawScl.z ?? 1 };
|
||||
|
||||
// Convert rotation to radians
|
||||
// 将旋转转换为弧度
|
||||
const rx = (rot.x * Math.PI) / 180;
|
||||
const ry = (rot.y * Math.PI) / 180;
|
||||
const rz = (rot.z * Math.PI) / 180;
|
||||
|
||||
// Build rotation matrix (ZYX order)
|
||||
// 构建旋转矩阵(ZYX 顺序)
|
||||
const cx = Math.cos(rx), sx = Math.sin(rx);
|
||||
const cy = Math.cos(ry), sy = Math.sin(ry);
|
||||
const cz = Math.cos(rz), sz = Math.sin(rz);
|
||||
|
||||
// Combined rotation matrix
|
||||
// 组合旋转矩阵
|
||||
const r00 = cy * cz;
|
||||
const r01 = cy * sz;
|
||||
const r02 = -sy;
|
||||
const r10 = sx * sy * cz - cx * sz;
|
||||
const r11 = sx * sy * sz + cx * cz;
|
||||
const r12 = sx * cy;
|
||||
const r20 = cx * sy * cz + sx * sz;
|
||||
const r21 = cx * sy * sz - sx * cz;
|
||||
const r22 = cx * cy;
|
||||
|
||||
// Build column-major 4x4 matrix with scale and translation
|
||||
// 构建带缩放和平移的列优先 4x4 矩阵
|
||||
const m = this.transformBuffer;
|
||||
|
||||
// Column 0
|
||||
m[0] = r00 * scl.x;
|
||||
m[1] = r10 * scl.x;
|
||||
m[2] = r20 * scl.x;
|
||||
m[3] = 0;
|
||||
|
||||
// Column 1
|
||||
m[4] = r01 * scl.y;
|
||||
m[5] = r11 * scl.y;
|
||||
m[6] = r21 * scl.y;
|
||||
m[7] = 0;
|
||||
|
||||
// Column 2
|
||||
m[8] = r02 * scl.z;
|
||||
m[9] = r12 * scl.z;
|
||||
m[10] = r22 * scl.z;
|
||||
m[11] = 0;
|
||||
|
||||
// Column 3 (translation)
|
||||
m[12] = pos.x;
|
||||
m[13] = pos.y;
|
||||
m[14] = pos.z;
|
||||
m[15] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build interleaved vertex data for simple 3D mesh.
|
||||
* 构建简化 3D 网格的交错顶点数据。
|
||||
*
|
||||
* Format: [x, y, z, u, v, r, g, b, a] per vertex (9 floats)
|
||||
* 格式:每个顶点 [x, y, z, u, v, r, g, b, a](9 个浮点数)
|
||||
*/
|
||||
private buildVertexData(meshData: IMeshData): Float32Array | null {
|
||||
const vertices = meshData.vertices;
|
||||
const uvs = meshData.uvs;
|
||||
const colors = meshData.colors;
|
||||
|
||||
if (!vertices || vertices.length === 0) return null;
|
||||
|
||||
const vertexCount = vertices.length / 3;
|
||||
const floatsPerVertex = 9;
|
||||
const totalFloats = vertexCount * floatsPerVertex;
|
||||
|
||||
// Resize buffer if needed
|
||||
// 如果需要,调整缓冲区大小
|
||||
if (this.vertexBuffer.length < totalFloats) {
|
||||
this.vertexBuffer = new Float32Array(totalFloats);
|
||||
}
|
||||
|
||||
const hasUVs = uvs && uvs.length >= vertexCount * 2;
|
||||
const hasColors = colors && colors.length >= vertexCount * 4;
|
||||
|
||||
for (let i = 0; i < vertexCount; i++) {
|
||||
const vBase = i * 3;
|
||||
const uvBase = i * 2;
|
||||
const colorBase = i * 4;
|
||||
const outBase = i * floatsPerVertex;
|
||||
|
||||
// Position
|
||||
this.vertexBuffer[outBase] = vertices[vBase];
|
||||
this.vertexBuffer[outBase + 1] = vertices[vBase + 1];
|
||||
this.vertexBuffer[outBase + 2] = vertices[vBase + 2];
|
||||
|
||||
// UV
|
||||
this.vertexBuffer[outBase + 3] = hasUVs ? uvs![uvBase] : 0;
|
||||
this.vertexBuffer[outBase + 4] = hasUVs ? uvs![uvBase + 1] : 0;
|
||||
|
||||
// Color (RGBA)
|
||||
this.vertexBuffer[outBase + 5] = hasColors ? colors![colorBase] : 1;
|
||||
this.vertexBuffer[outBase + 6] = hasColors ? colors![colorBase + 1] : 1;
|
||||
this.vertexBuffer[outBase + 7] = hasColors ? colors![colorBase + 2] : 1;
|
||||
this.vertexBuffer[outBase + 8] = hasColors ? colors![colorBase + 3] : 1;
|
||||
}
|
||||
|
||||
return this.vertexBuffer.subarray(0, totalFloats);
|
||||
}
|
||||
}
|
||||
186
packages/rendering/mesh-3d/src/systems/SkeletonBakingSystem.ts
Normal file
186
packages/rendering/mesh-3d/src/systems/SkeletonBakingSystem.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* SkeletonBakingSystem - System for baking skeleton matrices.
|
||||
* SkeletonBakingSystem - 骨骼矩阵烘焙系统。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
|
||||
import { SkeletonComponent, type BoneTransform } from '../SkeletonComponent';
|
||||
import { MeshComponent } from '../MeshComponent';
|
||||
|
||||
/**
|
||||
* System for computing skeleton bone matrices.
|
||||
* 用于计算骨骼矩阵的系统。
|
||||
*
|
||||
* Runs after Animation3DSystem to compute world matrices and final skinning matrices.
|
||||
* 在 Animation3DSystem 之后运行,计算世界矩阵和最终蒙皮矩阵。
|
||||
*/
|
||||
@ECSSystem('SkeletonBaking', { updateOrder: 110 })
|
||||
export class SkeletonBakingSystem extends EntitySystem {
|
||||
// Temporary matrix for calculations
|
||||
// 用于计算的临时矩阵
|
||||
private tempMatrix: Float32Array = new Float32Array(16);
|
||||
private tempMatrix2: Float32Array = new Float32Array(16);
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(SkeletonComponent).all(MeshComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process entities each frame.
|
||||
* 每帧处理实体。
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
if (!entity.enabled) continue;
|
||||
this.updateEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single entity's skeleton matrices.
|
||||
* 更新单个实体的骨骼矩阵。
|
||||
*/
|
||||
private updateEntity(entity: Entity): void {
|
||||
const skeleton = entity.getComponent(SkeletonComponent);
|
||||
|
||||
if (!skeleton || !skeleton.isLoaded || !skeleton.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const joints = skeleton.joints;
|
||||
const boneTransforms = skeleton.boneTransforms;
|
||||
|
||||
// Phase 1: Compute world matrices (parent-to-child order)
|
||||
// 阶段1: 计算世界矩阵(父到子顺序)
|
||||
for (let i = 0; i < joints.length; i++) {
|
||||
const joint = joints[i];
|
||||
const localTransform = boneTransforms[i];
|
||||
|
||||
// Build local transform matrix
|
||||
// 构建局部变换矩阵
|
||||
this.buildTransformMatrix(localTransform, this.tempMatrix);
|
||||
|
||||
if (joint.parentIndex >= 0) {
|
||||
// Multiply parent world matrix by local matrix
|
||||
// 将父世界矩阵乘以局部矩阵
|
||||
const parentWorld = skeleton.getWorldMatrix(joint.parentIndex);
|
||||
if (parentWorld) {
|
||||
this.multiplyMatrices(parentWorld, this.tempMatrix, this.tempMatrix2);
|
||||
skeleton.setWorldMatrix(i, this.tempMatrix2);
|
||||
} else {
|
||||
skeleton.setWorldMatrix(i, this.tempMatrix);
|
||||
}
|
||||
} else {
|
||||
// Root bone - world matrix is local matrix
|
||||
// 根骨骼 - 世界矩阵就是局部矩阵
|
||||
skeleton.setWorldMatrix(i, this.tempMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Compute final matrices (world * inverseBindMatrix)
|
||||
// 阶段2: 计算最终矩阵(世界矩阵 * 逆绑定矩阵)
|
||||
for (let i = 0; i < joints.length; i++) {
|
||||
const joint = joints[i];
|
||||
const worldMatrix = skeleton.getWorldMatrix(i);
|
||||
|
||||
if (worldMatrix && joint.inverseBindMatrix) {
|
||||
this.multiplyMatrices(worldMatrix, joint.inverseBindMatrix, this.tempMatrix);
|
||||
skeleton.setFinalMatrix(i, this.tempMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear dirty flag
|
||||
// 清除脏标记
|
||||
skeleton.clearDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build 4x4 transform matrix from BoneTransform.
|
||||
* 从 BoneTransform 构建 4x4 变换矩阵。
|
||||
*/
|
||||
private buildTransformMatrix(transform: BoneTransform, out: Float32Array): void {
|
||||
const [px, py, pz] = transform.position;
|
||||
const [qx, qy, qz, qw] = transform.rotation;
|
||||
const [sx, sy, sz] = transform.scale;
|
||||
|
||||
// Build rotation matrix from quaternion
|
||||
// 从四元数构建旋转矩阵
|
||||
const x2 = qx + qx;
|
||||
const y2 = qy + qy;
|
||||
const z2 = qz + qz;
|
||||
const xx = qx * x2;
|
||||
const xy = qx * y2;
|
||||
const xz = qx * z2;
|
||||
const yy = qy * y2;
|
||||
const yz = qy * z2;
|
||||
const zz = qz * z2;
|
||||
const wx = qw * x2;
|
||||
const wy = qw * y2;
|
||||
const wz = qw * z2;
|
||||
|
||||
// Column 0 (with scale)
|
||||
out[0] = (1 - (yy + zz)) * sx;
|
||||
out[1] = (xy + wz) * sx;
|
||||
out[2] = (xz - wy) * sx;
|
||||
out[3] = 0;
|
||||
|
||||
// Column 1 (with scale)
|
||||
out[4] = (xy - wz) * sy;
|
||||
out[5] = (1 - (xx + zz)) * sy;
|
||||
out[6] = (yz + wx) * sy;
|
||||
out[7] = 0;
|
||||
|
||||
// Column 2 (with scale)
|
||||
out[8] = (xz + wy) * sz;
|
||||
out[9] = (yz - wx) * sz;
|
||||
out[10] = (1 - (xx + yy)) * sz;
|
||||
out[11] = 0;
|
||||
|
||||
// Column 3 (translation)
|
||||
out[12] = px;
|
||||
out[13] = py;
|
||||
out[14] = pz;
|
||||
out[15] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply two 4x4 matrices (column-major).
|
||||
* 乘以两个 4x4 矩阵(列优先)。
|
||||
*/
|
||||
private multiplyMatrices(a: Float32Array, b: Float32Array, out: Float32Array): void {
|
||||
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
|
||||
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
|
||||
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
|
||||
const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
|
||||
|
||||
let b0, b1, b2, b3;
|
||||
|
||||
// Column 0
|
||||
b0 = b[0]; b1 = b[1]; b2 = b[2]; b3 = b[3];
|
||||
out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
||||
out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
||||
out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
||||
out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
||||
|
||||
// Column 1
|
||||
b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
|
||||
out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
||||
out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
||||
out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
||||
out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
||||
|
||||
// Column 2
|
||||
b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
|
||||
out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
||||
out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
||||
out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
||||
out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
||||
|
||||
// Column 3
|
||||
b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
|
||||
out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
|
||||
out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
||||
out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
||||
out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user