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:
47
packages/rendering/mesh-3d/module.json
Normal file
47
packages/rendering/mesh-3d/module.json
Normal 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"
|
||||
}
|
||||
49
packages/rendering/mesh-3d/package.json
Normal file
49
packages/rendering/mesh-3d/package.json
Normal 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"
|
||||
}
|
||||
341
packages/rendering/mesh-3d/src/Animation3DComponent.ts
Normal file
341
packages/rendering/mesh-3d/src/Animation3DComponent.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
115
packages/rendering/mesh-3d/src/Mesh3DRuntimeModule.ts
Normal file
115
packages/rendering/mesh-3d/src/Mesh3DRuntimeModule.ts
Normal 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 };
|
||||
151
packages/rendering/mesh-3d/src/MeshComponent.ts
Normal file
151
packages/rendering/mesh-3d/src/MeshComponent.ts
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
279
packages/rendering/mesh-3d/src/SkeletonComponent.ts
Normal file
279
packages/rendering/mesh-3d/src/SkeletonComponent.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
275
packages/rendering/mesh-3d/src/animation/AnimationEvaluator.ts
Normal file
275
packages/rendering/mesh-3d/src/animation/AnimationEvaluator.ts
Normal 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(四元数),平移/缩放 = 3(vec3),权重 = 可变
|
||||
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];
|
||||
}
|
||||
}
|
||||
39
packages/rendering/mesh-3d/src/index.ts
Normal file
39
packages/rendering/mesh-3d/src/index.ts
Normal 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';
|
||||
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;
|
||||
}
|
||||
}
|
||||
13
packages/rendering/mesh-3d/src/tokens.ts
Normal file
13
packages/rendering/mesh-3d/src/tokens.ts
Normal 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');
|
||||
12
packages/rendering/mesh-3d/tsconfig.build.json
Normal file
12
packages/rendering/mesh-3d/tsconfig.build.json
Normal 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"]
|
||||
}
|
||||
29
packages/rendering/mesh-3d/tsconfig.json
Normal file
29
packages/rendering/mesh-3d/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/rendering/mesh-3d/tsup.config.ts
Normal file
7
packages/rendering/mesh-3d/tsup.config.ts
Normal 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'
|
||||
});
|
||||
Reference in New Issue
Block a user