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:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,47 @@
{
"id": "mesh-3d",
"name": "@esengine/mesh-3d",
"globalKey": "mesh-3d",
"displayName": "3D Mesh Rendering",
"description": "3D mesh rendering with GLTF/GLB support | 3D 网格渲染",
"version": "1.0.0",
"category": "Rendering",
"icon": "Box",
"tags": [
"3d",
"mesh",
"gltf",
"rendering"
],
"isCore": false,
"defaultEnabled": false,
"isEngineModule": true,
"canContainContent": true,
"platforms": [
"web",
"desktop"
],
"dependencies": [
"core",
"engine-core",
"asset-system",
"ecs-engine-bindgen"
],
"exports": {
"components": [
"MeshComponent",
"Animation3DComponent",
"SkeletonComponent"
],
"systems": [
"MeshRenderSystem",
"Animation3DSystem",
"SkeletonBakingSystem",
"MeshAssetLoaderSystem"
]
},
"editorPackage": "@esengine/mesh-3d-editor",
"requiresWasm": true,
"outputPath": "dist/index.js",
"pluginExport": "Mesh3DPlugin"
}

View File

@@ -0,0 +1,49 @@
{
"name": "@esengine/mesh-3d",
"version": "1.0.0",
"description": "ECS-based 3D mesh rendering system with GLTF support",
"esengine": {
"plugin": true,
"pluginExport": "Mesh3DPlugin",
"category": "rendering",
"isEnginePlugin": true
},
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/asset-system": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/ecs-engine-bindgen": "workspace:*",
"@esengine/build-config": "workspace:*",
"rimraf": "^5.0.5",
"tsup": "^8.0.0",
"typescript": "^5.3.3"
},
"keywords": [
"ecs",
"mesh",
"3d",
"gltf",
"webgl"
],
"author": "yhh",
"license": "MIT"
}

View File

@@ -0,0 +1,341 @@
/**
* Animation3DComponent - 3D animation playback component.
* Animation3DComponent - 3D 动画播放组件。
*/
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
import type { IGLTFAnimationClip } from '@esengine/asset-system';
/**
* Animation play state.
* 动画播放状态。
*/
export enum AnimationPlayState {
/** Stopped - not playing. | 停止 - 未播放。 */
Stopped = 'stopped',
/** Playing forward. | 正向播放。 */
Playing = 'playing',
/** Paused. | 暂停。 */
Paused = 'paused'
}
/**
* Animation wrap mode.
* 动画循环模式。
*/
export enum AnimationWrapMode {
/** Play once and stop. | 播放一次后停止。 */
Once = 'once',
/** Loop continuously. | 连续循环。 */
Loop = 'loop',
/** Play forward then backward (ping-pong). | 往返播放。 */
PingPong = 'pingpong',
/** Clamp to last frame. | 停在最后一帧。 */
ClampForever = 'clampForever'
}
/**
* 3D Animation component for playing skeletal/node animations.
* 用于播放骨骼/节点动画的 3D 动画组件。
*
* Requires MeshComponent for animation data source.
* 需要 MeshComponent 作为动画数据来源。
*/
@ECSComponent('Animation3D', { requires: ['Mesh'] })
@Serializable({ version: 1, typeId: 'Animation3D' })
export class Animation3DComponent extends Component {
/**
* 默认动画片段名称
* Default animation clip name
*/
@Serialize()
@Property({ type: 'string', label: 'Default Clip' })
public defaultClip: string = '';
/**
* 播放速度1.0 = 正常速度)
* Playback speed (1.0 = normal speed)
*/
@Serialize()
@Property({ type: 'number', label: 'Speed', min: 0, max: 10 })
public speed: number = 1.0;
/**
* 循环模式
* Wrap mode
*/
@Serialize()
@Property({
type: 'enum',
label: 'Wrap Mode',
options: ['once', 'loop', 'pingpong', 'clampForever']
})
public wrapMode: AnimationWrapMode = AnimationWrapMode.Loop;
/**
* 是否启动时自动播放
* Whether to auto-play on start
*/
@Serialize()
@Property({ type: 'boolean', label: 'Play On Awake' })
public playOnAwake: boolean = true;
// ===== Runtime State | 运行时状态 =====
/**
* 当前播放状态
* Current play state
*/
private _playState: AnimationPlayState = AnimationPlayState.Stopped;
/**
* 当前播放的动画片段
* Currently playing animation clip
*/
private _currentClip: IGLTFAnimationClip | null = null;
/**
* 当前播放时间(秒)
* Current playback time (seconds)
*/
private _currentTime: number = 0;
/**
* 播放方向1 = 正向,-1 = 反向)
* Playback direction (1 = forward, -1 = backward)
*/
private _direction: number = 1;
/**
* 可用的动画片段列表
* Available animation clips
*/
private _clips: IGLTFAnimationClip[] = [];
/**
* 当前片段名称到索引的映射
* Map of clip name to index
*/
private _clipNameToIndex: Map<string, number> = new Map();
// ===== Public Getters | 公共获取器 =====
/**
* 获取当前播放状态
* Get current play state
*/
public get playState(): AnimationPlayState {
return this._playState;
}
/**
* 获取当前播放的动画片段
* Get currently playing clip
*/
public get currentClip(): IGLTFAnimationClip | null {
return this._currentClip;
}
/**
* 获取当前播放时间
* Get current playback time
*/
public get currentTime(): number {
return this._currentTime;
}
/**
* 获取当前片段的持续时间
* Get duration of current clip
*/
public get duration(): number {
return this._currentClip?.duration ?? 0;
}
/**
* 获取归一化时间0-1
* Get normalized time (0-1)
*/
public get normalizedTime(): number {
if (!this._currentClip || this._currentClip.duration <= 0) return 0;
return this._currentTime / this._currentClip.duration;
}
/**
* 是否正在播放
* Whether playing
*/
public get isPlaying(): boolean {
return this._playState === AnimationPlayState.Playing;
}
/**
* 获取所有可用的动画片段
* Get all available clips
*/
public get clips(): readonly IGLTFAnimationClip[] {
return this._clips;
}
/**
* 获取所有动画片段名称
* Get all clip names
*/
public get clipNames(): string[] {
return this._clips.map(c => c.name);
}
// ===== Public Methods | 公共方法 =====
/**
* 设置动画片段列表(由 Animation3DSystem 调用)
* Set animation clips (called by Animation3DSystem)
*/
public setClips(clips: IGLTFAnimationClip[]): void {
this._clips = clips;
this._clipNameToIndex.clear();
clips.forEach((clip, index) => {
this._clipNameToIndex.set(clip.name, index);
});
}
/**
* 播放动画
* Play animation
*
* @param clipName - 动画片段名称,不指定则播放当前/默认片段
*/
public play(clipName?: string): void {
const name = clipName ?? this.defaultClip ?? (this._clips[0]?.name ?? '');
if (!name) {
console.warn('[Animation3DComponent] No clip to play');
return;
}
const index = this._clipNameToIndex.get(name);
if (index === undefined) {
console.warn(`[Animation3DComponent] Clip not found: ${name}`);
return;
}
this._currentClip = this._clips[index];
this._currentTime = 0;
this._direction = 1;
this._playState = AnimationPlayState.Playing;
}
/**
* 停止动画
* Stop animation
*/
public stop(): void {
this._playState = AnimationPlayState.Stopped;
this._currentTime = 0;
}
/**
* 暂停动画
* Pause animation
*/
public pause(): void {
if (this._playState === AnimationPlayState.Playing) {
this._playState = AnimationPlayState.Paused;
}
}
/**
* 恢复播放
* Resume playback
*/
public resume(): void {
if (this._playState === AnimationPlayState.Paused) {
this._playState = AnimationPlayState.Playing;
}
}
/**
* 设置播放时间
* Set playback time
*/
public setTime(time: number): void {
this._currentTime = Math.max(0, Math.min(time, this.duration));
}
/**
* 设置归一化时间
* Set normalized time
*/
public setNormalizedTime(t: number): void {
this._currentTime = Math.max(0, Math.min(t, 1)) * this.duration;
}
/**
* 更新播放时间(由 Animation3DSystem 调用)
* Update playback time (called by Animation3DSystem)
*
* @param deltaTime - 时间增量(秒)
*/
public updateTime(deltaTime: number): void {
if (this._playState !== AnimationPlayState.Playing || !this._currentClip) {
return;
}
const scaledDelta = deltaTime * this.speed * this._direction;
this._currentTime += scaledDelta;
const duration = this._currentClip.duration;
if (duration <= 0) return;
// Handle wrap mode
// 处理循环模式
switch (this.wrapMode) {
case AnimationWrapMode.Once:
if (this._currentTime >= duration || this._currentTime < 0) {
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
this._playState = AnimationPlayState.Stopped;
}
break;
case AnimationWrapMode.Loop:
while (this._currentTime >= duration) {
this._currentTime -= duration;
}
while (this._currentTime < 0) {
this._currentTime += duration;
}
break;
case AnimationWrapMode.PingPong:
if (this._currentTime >= duration) {
this._currentTime = duration - (this._currentTime - duration);
this._direction = -1;
} else if (this._currentTime < 0) {
this._currentTime = -this._currentTime;
this._direction = 1;
}
break;
case AnimationWrapMode.ClampForever:
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
break;
}
}
/**
* 重置组件
* Reset component
*/
reset(): void {
this.defaultClip = '';
this.speed = 1.0;
this.wrapMode = AnimationWrapMode.Loop;
this.playOnAwake = true;
this._playState = AnimationPlayState.Stopped;
this._currentClip = null;
this._currentTime = 0;
this._direction = 1;
this._clips = [];
this._clipNameToIndex.clear();
}
}

View File

@@ -0,0 +1,115 @@
/**
* Mesh3D Runtime Module - Plugin for 3D mesh rendering.
* Mesh3D 运行时模块 - 3D 网格渲染插件。
*/
import type { IComponentRegistry, IScene } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { EngineBridgeToken } from '@esengine/ecs-engine-bindgen';
import { AssetManagerToken } from '@esengine/asset-system';
import { MeshComponent } from './MeshComponent';
import { Animation3DComponent } from './Animation3DComponent';
import { SkeletonComponent } from './SkeletonComponent';
import { MeshRenderSystem } from './systems/MeshRenderSystem';
import { MeshAssetLoaderSystem } from './systems/MeshAssetLoaderSystem';
import { Animation3DSystem } from './systems/Animation3DSystem';
import { SkeletonBakingSystem } from './systems/SkeletonBakingSystem';
import { MeshRenderSystemToken } from './tokens';
export type { SystemContext, ModuleManifest, IRuntimeModule, IRuntimePlugin };
// Re-export tokens
// 重新导出令牌
export { MeshRenderSystemToken } from './tokens';
/**
* Runtime module for 3D mesh rendering.
* 3D 网格渲染的运行时模块。
*/
class Mesh3DRuntimeModule implements IRuntimeModule {
registerComponents(registry: IComponentRegistry): void {
registry.register(MeshComponent);
registry.register(Animation3DComponent);
registry.register(SkeletonComponent);
}
createSystems(scene: IScene, context: SystemContext): void {
// Get engine bridge from services
// 从服务获取引擎桥接
const bridge = context.services.get(EngineBridgeToken) ?? null;
if (!bridge) {
console.warn('[Mesh3D] EngineBridge not found, MeshRenderSystem will be disabled');
}
// Get asset manager
// 获取资产管理器
const assetManager = context.services.get(AssetManagerToken);
// Create asset loader system
// 创建资产加载器系统
const loaderSystem = new MeshAssetLoaderSystem();
if (assetManager) {
loaderSystem.setAssetManager(assetManager);
} else {
console.warn('[Mesh3D] AssetManager not found, mesh loading will be disabled');
}
scene.addSystem(loaderSystem);
// Create animation system (runs before rendering to update bone transforms)
// 创建动画系统(在渲染前运行以更新骨骼变换)
const animationSystem = new Animation3DSystem();
scene.addSystem(animationSystem);
// Create skeleton baking system (computes final bone matrices)
// 创建骨骼烘焙系统(计算最终骨骼矩阵)
const skeletonSystem = new SkeletonBakingSystem();
scene.addSystem(skeletonSystem);
// Create render system with bridge
// 使用桥接创建渲染系统
const renderSystem = new MeshRenderSystem(bridge);
// Add to scene
// 添加到场景
scene.addSystem(renderSystem);
// Register service
// 注册服务
context.services.register(MeshRenderSystemToken, renderSystem);
}
}
/**
* Module manifest.
* 模块清单。
*/
const manifest: ModuleManifest = {
id: 'mesh-3d',
name: '@esengine/mesh-3d',
displayName: 'Mesh 3D',
version: '1.0.0',
description: '3D mesh rendering with GLTF/GLB support',
category: 'Rendering',
icon: 'Box',
isCore: false,
defaultEnabled: true,
isEngineModule: true,
canContainContent: true,
dependencies: ['core', 'math', 'asset-system'],
exports: {
components: ['MeshComponent', 'Animation3DComponent', 'SkeletonComponent']
},
editorPackage: '@esengine/mesh-3d-editor',
requiresWasm: true
};
/**
* Mesh3D Plugin export.
* Mesh3D 插件导出。
*/
export const Mesh3DPlugin: IRuntimePlugin = {
manifest,
runtimeModule: new Mesh3DRuntimeModule()
};
export { Mesh3DRuntimeModule };

View File

@@ -0,0 +1,151 @@
/**
* MeshComponent - 3D mesh rendering component.
* MeshComponent - 3D 网格渲染组件。
*/
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
import { SortingLayers, type ISortable } from '@esengine/engine-core';
import type { IGLTFAsset, IMeshData } from '@esengine/asset-system';
/**
* 3D Mesh component for rendering GLTF models.
* 用于渲染 GLTF 模型的 3D 网格组件。
*
* Requires TransformComponent for positioning and MeshRenderSystem for rendering.
* 需要 TransformComponent 进行定位MeshRenderSystem 进行渲染。
*/
@ECSComponent('Mesh', { requires: ['Transform'] })
@Serializable({ version: 1, typeId: 'Mesh' })
export class MeshComponent extends Component implements ISortable {
/**
* 模型资产 GUID
* Model asset GUID
*
* Stores the unique identifier of the GLTF/GLB/OBJ/FBX model asset.
* 存储 GLTF/GLB/OBJ/FBX 模型资产的唯一标识符。
*/
@Serialize()
@Property({ type: 'asset', label: 'Model', assetType: 'any', extensions: ['.gltf', '.glb', '.obj', '.fbx'] })
public modelGuid: string = '';
/**
* 运行时网格数据(从资产加载)
* Runtime mesh data (loaded from asset)
*/
public meshAsset: IGLTFAsset | null = null;
/**
* 当前活动的网格索引(用于多网格模型)
* Active mesh index (for multi-mesh models)
*/
@Serialize()
@Property({ type: 'integer', label: 'Mesh Index', min: 0 })
public meshIndex: number = 0;
/**
* 是否投射阴影
* Whether to cast shadows
*/
@Serialize()
@Property({ type: 'boolean', label: 'Cast Shadows' })
public castShadows: boolean = true;
/**
* 是否接收阴影
* Whether to receive shadows
*/
@Serialize()
@Property({ type: 'boolean', label: 'Receive Shadows' })
public receiveShadows: boolean = true;
/**
* 可见性
* Visibility
*/
@Serialize()
@Property({ type: 'boolean', label: 'Visible' })
public visible: boolean = true;
/**
* 排序层(用于透明物体排序)
* Sorting layer (for transparent object sorting)
*/
@Serialize()
@Property({
type: 'enum',
label: 'Sorting Layer',
options: ['Background', 'Default', 'Foreground', 'WorldOverlay', 'UI', 'ScreenOverlay', 'Modal']
})
public sortingLayer: string = SortingLayers.Default;
/**
* 层内排序顺序
* Order in layer
*/
@Serialize()
@Property({ type: 'integer', label: 'Order In Layer' })
public orderInLayer: number = 0;
/**
* 材质覆盖 GUID 列表(可选)
* Material override GUIDs (optional)
*/
@Serialize()
public materialOverrides: string[] = [];
/**
* 运行时材质 ID 列表
* Runtime material IDs
*/
public runtimeMaterialIds: number[] = [];
/**
* 运行时纹理 ID 列表
* Runtime texture IDs
*/
public runtimeTextureIds: number[] = [];
/**
* 资产是否已加载
* Whether asset is loaded
*/
public get isLoaded(): boolean {
return this.meshAsset !== null;
}
/**
* 获取当前网格数据
* Get current mesh data
*/
public get currentMesh(): IMeshData | null {
if (!this.meshAsset || !this.meshAsset.meshes.length) return null;
const index = Math.min(this.meshIndex, this.meshAsset.meshes.length - 1);
return this.meshAsset.meshes[index];
}
/**
* 获取所有网格数据
* Get all mesh data
*/
public get allMeshes(): IMeshData[] {
return this.meshAsset?.meshes ?? [];
}
/**
* 重置组件
* Reset component
*/
reset(): void {
this.modelGuid = '';
this.meshAsset = null;
this.meshIndex = 0;
this.castShadows = true;
this.receiveShadows = true;
this.visible = true;
this.sortingLayer = 'Default';
this.orderInLayer = 0;
this.materialOverrides = [];
this.runtimeMaterialIds = [];
this.runtimeTextureIds = [];
}
}

View File

@@ -0,0 +1,279 @@
/**
* SkeletonComponent - 3D skeleton data component for skinned meshes.
* SkeletonComponent - 用于蒙皮网格的 3D 骨骼数据组件。
*/
import { Component, ECSComponent, Serializable } from '@esengine/ecs-framework';
import type { ISkeletonData, ISkeletonJoint } from '@esengine/asset-system';
/**
* Local transform for a bone/joint.
* 骨骼/关节的局部变换。
*/
export interface BoneTransform {
/** Position XYZ. | 位置 XYZ。 */
position: [number, number, number];
/** Rotation quaternion XYZW. | 旋转四元数 XYZW。 */
rotation: [number, number, number, number];
/** Scale XYZ. | 缩放 XYZ。 */
scale: [number, number, number];
}
/**
* 3D Skeleton component for skeletal animation.
* 用于骨骼动画的 3D 骨骼组件。
*
* Requires MeshComponent for skeleton data source.
* 需要 MeshComponent 作为骨骼数据来源。
*/
@ECSComponent('Skeleton', { requires: ['Mesh', 'Animation3D'] })
@Serializable({ version: 1, typeId: 'Skeleton' })
export class SkeletonComponent extends Component {
// ===== Runtime Data | 运行时数据 =====
/**
* 骨骼数据(从 MeshAsset 加载)
* Skeleton data (loaded from MeshAsset)
*/
private _skeletonData: ISkeletonData | null = null;
/**
* 烘烤的骨骼矩阵(输出给渲染器)
* Baked bone matrices (output for renderer)
*
* Each matrix is a 4x4 column-major matrix (16 floats).
* 每个矩阵是 4x4 列优先矩阵16 个浮点数)。
*/
private _boneMatrices: Float32Array = new Float32Array(0);
/**
* 当前帧的骨骼局部变换
* Current frame's bone local transforms
*/
private _boneTransforms: BoneTransform[] = [];
/**
* 骨骼世界变换矩阵缓存
* Bone world transform matrix cache
*/
private _worldMatrices: Float32Array = new Float32Array(0);
/**
* 是否需要更新骨骼矩阵
* Whether bone matrices need update
*/
private _dirty: boolean = true;
// ===== Public Getters | 公共获取器 =====
/**
* 获取骨骼数据
* Get skeleton data
*/
public get skeletonData(): ISkeletonData | null {
return this._skeletonData;
}
/**
* 获取关节数量
* Get joint count
*/
public get jointCount(): number {
return this._skeletonData?.joints.length ?? 0;
}
/**
* 获取烘烤的骨骼矩阵
* Get baked bone matrices
*/
public get boneMatrices(): Float32Array {
return this._boneMatrices;
}
/**
* 获取骨骼局部变换
* Get bone local transforms
*/
public get boneTransforms(): readonly BoneTransform[] {
return this._boneTransforms;
}
/**
* 骨骼是否已加载
* Whether skeleton is loaded
*/
public get isLoaded(): boolean {
return this._skeletonData !== null && this._skeletonData.joints.length > 0;
}
/**
* 获取关节列表
* Get joint list
*/
public get joints(): readonly ISkeletonJoint[] {
return this._skeletonData?.joints ?? [];
}
// ===== Public Methods | 公共方法 =====
/**
* 设置骨骼数据(由系统调用)
* Set skeleton data (called by system)
*/
public setSkeletonData(data: ISkeletonData): void {
this._skeletonData = data;
const jointCount = data.joints.length;
// Initialize bone matrices (each joint has a 4x4 matrix = 16 floats)
// 初始化骨骼矩阵(每个关节有 4x4 矩阵 = 16 个浮点数)
this._boneMatrices = new Float32Array(jointCount * 16);
this._worldMatrices = new Float32Array(jointCount * 16);
// Initialize bone transforms with identity
// 用单位变换初始化骨骼变换
this._boneTransforms = [];
for (let i = 0; i < jointCount; i++) {
this._boneTransforms.push({
position: [0, 0, 0],
rotation: [0, 0, 0, 1], // Identity quaternion
scale: [1, 1, 1]
});
}
// Initialize bone matrices to identity
// 将骨骼矩阵初始化为单位矩阵
for (let i = 0; i < jointCount; i++) {
this.setIdentityMatrix(this._boneMatrices, i * 16);
this.setIdentityMatrix(this._worldMatrices, i * 16);
}
this._dirty = true;
}
/**
* 设置指定骨骼的局部变换
* Set local transform for a bone
*/
public setBoneTransform(jointIndex: number, transform: Partial<BoneTransform>): void {
if (jointIndex < 0 || jointIndex >= this._boneTransforms.length) {
return;
}
const bone = this._boneTransforms[jointIndex];
if (transform.position) {
bone.position = [...transform.position];
}
if (transform.rotation) {
bone.rotation = [...transform.rotation];
}
if (transform.scale) {
bone.scale = [...transform.scale];
}
this._dirty = true;
}
/**
* 标记骨骼矩阵需要更新
* Mark bone matrices as dirty
*/
public markDirty(): void {
this._dirty = true;
}
/**
* 检查是否需要更新
* Check if update is needed
*/
public isDirty(): boolean {
return this._dirty;
}
/**
* 清除脏标记(由系统在更新后调用)
* Clear dirty flag (called by system after update)
*/
public clearDirty(): void {
this._dirty = false;
}
/**
* 获取指定骨骼的世界矩阵
* Get world matrix for a bone
*/
public getWorldMatrix(jointIndex: number): Float32Array | null {
if (jointIndex < 0 || jointIndex >= this.jointCount) {
return null;
}
return this._worldMatrices.subarray(jointIndex * 16, (jointIndex + 1) * 16);
}
/**
* 设置指定骨骼的世界矩阵(由 SkeletonBakingSystem 调用)
* Set world matrix for a bone (called by SkeletonBakingSystem)
*/
public setWorldMatrix(jointIndex: number, matrix: Float32Array): void {
if (jointIndex < 0 || jointIndex >= this.jointCount) {
return;
}
const offset = jointIndex * 16;
for (let i = 0; i < 16; i++) {
this._worldMatrices[offset + i] = matrix[i];
}
}
/**
* 设置指定骨骼的最终矩阵(由 SkeletonBakingSystem 调用)
* Set final matrix for a bone (called by SkeletonBakingSystem)
*/
public setFinalMatrix(jointIndex: number, matrix: Float32Array): void {
if (jointIndex < 0 || jointIndex >= this.jointCount) {
return;
}
const offset = jointIndex * 16;
for (let i = 0; i < 16; i++) {
this._boneMatrices[offset + i] = matrix[i];
}
}
/**
* 按名称查找骨骼索引
* Find bone index by name
*/
public findBoneIndex(name: string): number {
if (!this._skeletonData) return -1;
for (let i = 0; i < this._skeletonData.joints.length; i++) {
if (this._skeletonData.joints[i].name === name) {
return i;
}
}
return -1;
}
/**
* 重置组件
* Reset component
*/
reset(): void {
this._skeletonData = null;
this._boneMatrices = new Float32Array(0);
this._worldMatrices = new Float32Array(0);
this._boneTransforms = [];
this._dirty = true;
}
// ===== Private Methods | 私有方法 =====
/**
* Set identity matrix at offset in array.
* 在数组的偏移位置设置单位矩阵。
*/
private setIdentityMatrix(arr: Float32Array, offset: number): void {
arr[offset] = 1; arr[offset + 1] = 0; arr[offset + 2] = 0; arr[offset + 3] = 0;
arr[offset + 4] = 0; arr[offset + 5] = 1; arr[offset + 6] = 0; arr[offset + 7] = 0;
arr[offset + 8] = 0; arr[offset + 9] = 0; arr[offset + 10] = 1; arr[offset + 11] = 0;
arr[offset + 12] = 0; arr[offset + 13] = 0; arr[offset + 14] = 0; arr[offset + 15] = 1;
}
}

View File

@@ -0,0 +1,275 @@
/**
* AnimationEvaluator - Utility for evaluating animation clips.
* AnimationEvaluator - 动画片段评估工具。
*/
import type { IGLTFAnimationClip, IAnimationSampler, IAnimationChannel } from '@esengine/asset-system';
/**
* Animation channel target path types.
* 动画通道目标路径类型。
*/
export type AnimationTargetPath = 'translation' | 'rotation' | 'scale' | 'weights';
/**
* Evaluated animation value.
* 评估后的动画值。
*/
export interface EvaluatedValue {
/** Target path (translation, rotation, scale, weights). | 目标路径。 */
path: AnimationTargetPath;
/** Evaluated value (vec3, quat, or morph weights). | 评估后的值。 */
value: number[];
}
/**
* Animation clip evaluator.
* 动画片段评估器。
*
* Samples animation channels at a given time and returns interpolated values.
* 在给定时间采样动画通道并返回插值后的值。
*/
export class AnimationEvaluator {
/**
* Evaluate animation clip at a given time.
* 在给定时间评估动画片段。
*
* @param clip - Animation clip to evaluate. | 要评估的动画片段。
* @param time - Time in seconds. | 时间(秒)。
* @returns Map of node index to evaluated values. | 节点索引到评估值的映射。
*/
public evaluate(clip: IGLTFAnimationClip, time: number): Map<number, EvaluatedValue> {
const result = new Map<number, EvaluatedValue>();
// Clamp time to clip duration
// 将时间限制在片段持续时间内
const sampleTime = Math.max(0, Math.min(time, clip.duration));
for (const channel of clip.channels) {
const sampler = clip.samplers[channel.samplerIndex];
if (!sampler) continue;
const value = this.sampleChannel(sampler, channel.target.path, sampleTime);
if (value) {
result.set(channel.target.nodeIndex, {
path: channel.target.path,
value
});
}
}
return result;
}
/**
* Sample a single animation channel.
* 采样单个动画通道。
*/
private sampleChannel(
sampler: IAnimationSampler,
path: AnimationTargetPath,
time: number
): number[] | null {
const { input, output, interpolation } = sampler;
if (!input || !output || input.length === 0) {
return null;
}
// Find keyframe index
// 查找关键帧索引
const frameIndex = this.findKeyframe(input, time);
// Components per value
// 每个值的分量数
// rotation = 4 (quaternion), translation/scale = 3 (vec3), weights = variable
// 旋转 = 4四元数平移/缩放 = 3vec3权重 = 可变
let componentCount: number;
if (path === 'rotation') {
componentCount = 4;
} else if (path === 'weights') {
// For morph targets, infer from output length / input length
// 对于变形目标,从输出长度 / 输入长度推断
componentCount = input.length > 0 ? Math.floor(output.length / input.length) : 1;
} else {
componentCount = 3;
}
// Handle edge cases
// 处理边界情况
if (frameIndex <= 0) {
return this.getOutputValue(output, 0, componentCount);
}
if (frameIndex >= input.length) {
return this.getOutputValue(output, input.length - 1, componentCount);
}
// Get surrounding keyframes
// 获取周围的关键帧
const prevIndex = frameIndex - 1;
const nextIndex = frameIndex;
const prevTime = input[prevIndex];
const nextTime = input[nextIndex];
// Calculate interpolation factor
// 计算插值因子
const duration = nextTime - prevTime;
const t = duration > 0 ? (time - prevTime) / duration : 0;
// Get values
// 获取值
const prevValue = this.getOutputValue(output, prevIndex, componentCount);
const nextValue = this.getOutputValue(output, nextIndex, componentCount);
if (!prevValue || !nextValue) {
return null;
}
// Interpolate
// 插值
switch (interpolation) {
case 'STEP':
return prevValue;
case 'LINEAR':
if (path === 'rotation') {
return this.slerp(prevValue, nextValue, t);
} else {
// translation, scale, weights all use linear interpolation
// 平移、缩放、权重都使用线性插值
return this.lerp(prevValue, nextValue, t);
}
case 'CUBICSPLINE':
// For cubicspline, output has 3 values per keyframe: in-tangent, value, out-tangent
// 对于三次样条,输出每个关键帧有 3 个值:入切线、值、出切线
// Simplified: just use linear for now
// 简化:暂时只使用线性
if (path === 'rotation') {
return this.slerp(prevValue, nextValue, t);
} else {
return this.lerp(prevValue, nextValue, t);
}
default:
return prevValue;
}
}
/**
* Find keyframe index for given time using binary search.
* 使用二分查找为给定时间查找关键帧索引。
*
* Returns the index of the first keyframe with time > input time.
* 返回第一个时间 > 输入时间的关键帧索引。
*/
private findKeyframe(input: Float32Array, time: number): number {
let low = 0;
let high = input.length;
while (low < high) {
const mid = (low + high) >>> 1;
if (input[mid] <= time) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
/**
* Get output value at keyframe index.
* 获取关键帧索引处的输出值。
*/
private getOutputValue(output: Float32Array, index: number, componentCount: number): number[] {
const offset = index * componentCount;
const result: number[] = [];
for (let i = 0; i < componentCount; i++) {
result.push(output[offset + i] ?? 0);
}
return result;
}
/**
* Linear interpolation for vec3.
* vec3 的线性插值。
*/
private lerp(a: number[], b: number[], t: number): number[] {
const result: number[] = [];
for (let i = 0; i < a.length; i++) {
result.push(a[i] + (b[i] - a[i]) * t);
}
return result;
}
/**
* Spherical linear interpolation for quaternion.
* 四元数的球面线性插值。
*/
private slerp(a: number[], b: number[], t: number): number[] {
// Normalize quaternions
// 归一化四元数
const ax = a[0], ay = a[1], az = a[2], aw = a[3];
let bx = b[0], by = b[1], bz = b[2], bw = b[3];
// Calculate angle between quaternions
// 计算四元数之间的角度
let dot = ax * bx + ay * by + az * bz + aw * bw;
// Negate b if dot product is negative (to take shorter path)
// 如果点积为负则取反 b取较短路径
if (dot < 0) {
bx = -bx;
by = -by;
bz = -bz;
bw = -bw;
dot = -dot;
}
// If very close, use linear interpolation
// 如果非常接近,使用线性插值
if (dot > 0.9995) {
return this.normalizeQuat([
ax + (bx - ax) * t,
ay + (by - ay) * t,
az + (bz - az) * t,
aw + (bw - aw) * t
]);
}
// Calculate slerp
// 计算球面线性插值
const theta0 = Math.acos(dot);
const theta = theta0 * t;
const sinTheta = Math.sin(theta);
const sinTheta0 = Math.sin(theta0);
const s0 = Math.cos(theta) - dot * sinTheta / sinTheta0;
const s1 = sinTheta / sinTheta0;
return [
ax * s0 + bx * s1,
ay * s0 + by * s1,
az * s0 + bz * s1,
aw * s0 + bw * s1
];
}
/**
* Normalize quaternion.
* 归一化四元数。
*/
private normalizeQuat(q: number[]): number[] {
const len = Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
if (len === 0) {
return [0, 0, 0, 1];
}
const inv = 1 / len;
return [q[0] * inv, q[1] * inv, q[2] * inv, q[3] * inv];
}
}

View File

@@ -0,0 +1,39 @@
/**
* @esengine/mesh-3d - 3D Mesh Rendering Module
* 3D 网格渲染模块
*
* Provides components and systems for rendering GLTF/GLB 3D models.
* 提供用于渲染 GLTF/GLB 3D 模型的组件和系统。
*/
// Components
// 组件
export { MeshComponent } from './MeshComponent';
export { Animation3DComponent, AnimationPlayState, AnimationWrapMode } from './Animation3DComponent';
export { SkeletonComponent, type BoneTransform } from './SkeletonComponent';
// Systems
// 系统
export { MeshRenderSystem } from './systems/MeshRenderSystem';
export { MeshAssetLoaderSystem } from './systems/MeshAssetLoaderSystem';
export { Animation3DSystem } from './systems/Animation3DSystem';
export { SkeletonBakingSystem } from './systems/SkeletonBakingSystem';
// Animation utilities
// 动画工具
export { AnimationEvaluator } from './animation/AnimationEvaluator';
// Tokens
// 令牌
export { MeshRenderSystemToken } from './tokens';
// Plugin
// 插件
export {
Mesh3DPlugin,
Mesh3DRuntimeModule,
type SystemContext,
type ModuleManifest,
type IRuntimeModule,
type IRuntimePlugin
} from './Mesh3DRuntimeModule';

View 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();
}
}

View 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);
}
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -0,0 +1,13 @@
/**
* Service tokens for mesh-3d module.
* mesh-3d 模块的服务令牌。
*/
import { createServiceToken } from '@esengine/ecs-framework';
import type { MeshRenderSystem } from './systems/MeshRenderSystem';
/**
* Token for MeshRenderSystem service.
* MeshRenderSystem 服务的令牌。
*/
export const MeshRenderSystemToken = createServiceToken<MeshRenderSystem>('meshRenderSystem');

View File

@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -0,0 +1,29 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
"references": [
{
"path": "../../framework/core"
},
{
"path": "../../engine/asset-system"
},
{
"path": "../../engine/engine-core"
},
{
"path": "../../engine/ecs-engine-bindgen"
}
]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'tsup';
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
export default defineConfig({
...runtimeOnlyPreset(),
tsconfig: 'tsconfig.build.json'
});