@galfus/engine 0.0.0-stage → 0.31.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -2
- package/package.json +59 -4
- package/src/anchors/cache.ts +101 -0
- package/src/anchors/component.ts +123 -0
- package/src/anchors/hierarchy.ts +192 -0
- package/src/anchors/index.ts +8 -0
- package/src/anchors/solver.ts +247 -0
- package/src/anchors/strategy.ts +142 -0
- package/src/animation/blending.ts +163 -0
- package/src/animation/clip.ts +132 -0
- package/src/animation/coordinator.ts +568 -0
- package/src/animation/index.ts +7 -0
- package/src/animation/native-targets.ts +37 -0
- package/src/animation/player.ts +287 -0
- package/src/audio/index.ts +4 -0
- package/src/audio/manager.ts +371 -0
- package/src/collision/components.ts +211 -0
- package/src/collision/index.ts +16 -0
- package/src/collision/query2d.ts +167 -0
- package/src/collision/query3d.ts +170 -0
- package/src/ecs/component.ts +130 -0
- package/src/ecs/entity.ts +75 -0
- package/src/ecs/index.ts +9 -0
- package/src/ecs/intent.ts +162 -0
- package/src/ecs/metadata.ts +44 -0
- package/src/ecs/system.ts +224 -0
- package/src/ecs/world.ts +291 -0
- package/src/index.ts +19 -0
- package/src/interaction/focus.ts +327 -0
- package/src/interaction/index.ts +9 -0
- package/src/interaction/listener.ts +58 -0
- package/src/interaction/snapshot2d.ts +210 -0
- package/src/interaction/snapshot3d.ts +214 -0
- package/src/interaction/system.ts +544 -0
- package/src/interaction/types.ts +27 -0
- package/src/math/common.ts +67 -0
- package/src/math/index.ts +17 -0
- package/src/math/mat2d.ts +320 -0
- package/src/math/mat4.ts +576 -0
- package/src/math/quat.ts +438 -0
- package/src/math/vec2.ts +161 -0
- package/src/math/vec3.ts +242 -0
- package/src/math/vec4.ts +171 -0
- package/src/multiverse/coordinator.ts +540 -0
- package/src/multiverse/index.ts +13 -0
- package/src/multiverse/presentation-graph.ts +306 -0
- package/src/multiverse/presentation-links.ts +134 -0
- package/src/multiverse/realm.ts +49 -0
- package/src/platform/index.ts +2 -0
- package/src/platform/platform.ts +149 -0
- package/src/platform/window.ts +181 -0
- package/src/realm2d/components.ts +254 -0
- package/src/realm2d/index.ts +12 -0
- package/src/realm2d/presets/index.ts +36 -0
- package/src/realm2d/realm.ts +522 -0
- package/src/realm2d/sync.ts +284 -0
- package/src/realm2d/utils/index.ts +32 -0
- package/src/realm3d/components.ts +341 -0
- package/src/realm3d/geometry.ts +224 -0
- package/src/realm3d/index.ts +12 -0
- package/src/realm3d/presets/index.ts +67 -0
- package/src/realm3d/realm.ts +588 -0
- package/src/realm3d/resource-ids.ts +62 -0
- package/src/realm3d/skinning.ts +128 -0
- package/src/realm3d/sync.ts +442 -0
- package/src/realm3d/utils/index.ts +26 -0
- package/src/resources/index.ts +4 -0
- package/src/resources/manager.ts +92 -0
- package/src/runtime.ts +588 -0
- package/src/spatial/dirty_registry.ts +217 -0
- package/src/spatial/flags.ts +14 -0
- package/src/spatial/index.ts +6 -0
- package/src/spatial/metrics.ts +124 -0
- package/src/text/editor.ts +224 -0
- package/src/text/encoding.ts +38 -0
- package/src/text/index.ts +13 -0
- package/src/text/input.ts +160 -0
- package/src/text/keyboard.ts +50 -0
- package/src/text/svg-decorations.ts +147 -0
- package/src/types.ts +85 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeletal skinning resolution system for Galfus Engine.
|
|
3
|
+
*
|
|
4
|
+
* Runs during the `preRender` stage after AnchorSolveSystem, reading world matrices
|
|
5
|
+
* directly from AnchorMatrixCache and uploading deformation matrices via realm3dUpdateBones.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GalfusEngine } from '@galfus/transport-types';
|
|
9
|
+
import type { AnchorMatrixCache } from '../anchors/cache';
|
|
10
|
+
import type { System } from '../ecs/system';
|
|
11
|
+
import type { EcsWorld } from '../ecs/world';
|
|
12
|
+
import { mat4, type Mat4 } from '../math/index';
|
|
13
|
+
import { SpatialDirtyFlags } from '../spatial/flags';
|
|
14
|
+
import type { Model3DComponent, Skin3DComponent } from './components';
|
|
15
|
+
|
|
16
|
+
export class SkinningSolveSystem {
|
|
17
|
+
// Pre-allocated scratch matrices for zero-GC execution
|
|
18
|
+
private readonly _invModelMat: Mat4 = mat4.create();
|
|
19
|
+
private readonly _tempMat: Mat4 = mat4.create();
|
|
20
|
+
private readonly _skinMat: Mat4 = mat4.create();
|
|
21
|
+
private readonly _invBindMat: Mat4 = mat4.create();
|
|
22
|
+
private readonly _identityMat: Mat4 = mat4.create();
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
readonly realmId: number,
|
|
26
|
+
readonly cache: AnchorMatrixCache
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Evaluates all active skins and submits bone matrices to the engine.
|
|
31
|
+
*/
|
|
32
|
+
solve(world: EcsWorld, engine: GalfusEngine | null): void {
|
|
33
|
+
const skinEntities = world.query('Skin3D');
|
|
34
|
+
if (skinEntities.length === 0) return;
|
|
35
|
+
|
|
36
|
+
for (const skinEntityId of skinEntities) {
|
|
37
|
+
const skin = world.getComponent<Skin3DComponent>(skinEntityId, 'Skin3D');
|
|
38
|
+
if (!skin || skin.joints.length === 0) continue;
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
!world.isAlive(skin.modelEntityId) ||
|
|
42
|
+
!world.getComponent<Model3DComponent>(skin.modelEntityId, 'Model3D')
|
|
43
|
+
) {
|
|
44
|
+
world.destroyEntity(skinEntityId);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const model = world.getComponent<Model3DComponent>(skin.modelEntityId, 'Model3D');
|
|
49
|
+
if (!model) continue;
|
|
50
|
+
|
|
51
|
+
// Incremental optimization: skip re-computation and upload if model, joints, and skin are clean
|
|
52
|
+
const isModelDirty = (model.flags & SpatialDirtyFlags.WorldDirty) !== 0;
|
|
53
|
+
let isAnyJointDirty = false;
|
|
54
|
+
for (let j = 0; j < skin.joints.length; j++) {
|
|
55
|
+
const jointEntityId = skin.joints[j];
|
|
56
|
+
if (jointEntityId === undefined) continue;
|
|
57
|
+
const jointModel = world.getComponent<Model3DComponent>(jointEntityId, 'Model3D');
|
|
58
|
+
if (jointModel && (jointModel.flags & SpatialDirtyFlags.WorldDirty) !== 0) {
|
|
59
|
+
isAnyJointDirty = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!skin.isDirty && !isModelDirty && !isAnyJointDirty) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 1. Get model world matrix (from model component or cache)
|
|
69
|
+
const modelWorld =
|
|
70
|
+
model.worldMatrix ?? this.cache.getWorld3D(skin.modelEntityId) ?? this._identityMat;
|
|
71
|
+
|
|
72
|
+
// 2. Invert model world matrix
|
|
73
|
+
if (!mat4.invert(this._invModelMat, modelWorld)) {
|
|
74
|
+
mat4.identity(this._invModelMat);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. For each joint bone, compute: SkinMatrix = inv(ModelWorld) * JointWorld * InvBind
|
|
78
|
+
const jointCount = skin.joints.length;
|
|
79
|
+
for (let j = 0; j < jointCount; j++) {
|
|
80
|
+
const jointEntityId = skin.joints[j];
|
|
81
|
+
if (jointEntityId === undefined) continue;
|
|
82
|
+
|
|
83
|
+
const jointModel = world.getComponent<Model3DComponent>(jointEntityId, 'Model3D');
|
|
84
|
+
const jointWorld =
|
|
85
|
+
jointModel?.worldMatrix ?? this.cache.getWorld3D(jointEntityId) ?? this._identityMat;
|
|
86
|
+
const offset = j * 16;
|
|
87
|
+
|
|
88
|
+
// Copy 16 floats of the inverse bind matrix into scratch
|
|
89
|
+
for (let k = 0; k < 16; k++) {
|
|
90
|
+
this._invBindMat[k] = skin.inverseBindMatrices[offset + k] ?? (k % 5 === 0 ? 1 : 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// temp = JointWorld * InvBind
|
|
94
|
+
mat4.multiply(this._tempMat, jointWorld, this._invBindMat);
|
|
95
|
+
|
|
96
|
+
// skinMat = inv(ModelWorld) * temp
|
|
97
|
+
mat4.multiply(this._skinMat, this._invModelMat, this._tempMat);
|
|
98
|
+
|
|
99
|
+
// Store into pre-allocated jointMatrices buffer
|
|
100
|
+
for (let k = 0; k < 16; k++) {
|
|
101
|
+
skin.jointMatrices[offset + k] = this._skinMat[k] ?? 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Upload bone matrices to native core
|
|
106
|
+
if (engine) {
|
|
107
|
+
engine.realm3dUpdateBones(
|
|
108
|
+
this.realmId,
|
|
109
|
+
model.modelId,
|
|
110
|
+
skin.jointMatrices,
|
|
111
|
+
skin.jointMatrices.length
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
skin.isDirty = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Helper function creating an ECS System instance running this solver in `preRender`.
|
|
121
|
+
*/
|
|
122
|
+
createSystem(getEngine: () => GalfusEngine | null): System<EcsWorld> {
|
|
123
|
+
return (world, context) => {
|
|
124
|
+
const engine = context.engine ?? getEngine();
|
|
125
|
+
this.solve(world, engine);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realm 3D synchronization system syncing ECS state and resource registries with core GalfusEngine.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GalfusEngine, Model3DDesc, Transform3D } from '@galfus/transport-types';
|
|
6
|
+
import type { AnchorMatrixCache } from '../anchors/cache';
|
|
7
|
+
import type { EntityId } from '../ecs/entity';
|
|
8
|
+
import type { EcsWorld } from '../ecs/world';
|
|
9
|
+
import { mat4, quat, vec3 } from '../math/index';
|
|
10
|
+
import { SpatialDirtyFlags } from '../spatial/flags';
|
|
11
|
+
import type {
|
|
12
|
+
Camera3DComponent,
|
|
13
|
+
GeometryResourceEntry,
|
|
14
|
+
InstancedModel3DComponent,
|
|
15
|
+
Light3DComponent,
|
|
16
|
+
MaterialResourceEntry,
|
|
17
|
+
Mesh3DComponent,
|
|
18
|
+
Model3DComponent,
|
|
19
|
+
ParticleEmitter3DComponent,
|
|
20
|
+
Portal3DComponent,
|
|
21
|
+
Primitive3DKind
|
|
22
|
+
} from './components';
|
|
23
|
+
|
|
24
|
+
export interface Realm3DResourceProvider {
|
|
25
|
+
getGeometries(): Iterable<GeometryResourceEntry>;
|
|
26
|
+
getMaterials(): Iterable<MaterialResourceEntry>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const scratchTrans = vec3.create();
|
|
30
|
+
const scratchRot = quat.create();
|
|
31
|
+
const scratchScale = vec3.create();
|
|
32
|
+
|
|
33
|
+
export class Realm3DSyncSystem {
|
|
34
|
+
private readonly _syncedGeometries = new Set<number>();
|
|
35
|
+
private readonly _syncedMaterials = new Set<number>();
|
|
36
|
+
private readonly _syncedModels = new Set<number>();
|
|
37
|
+
private readonly _syncedCameras = new Set<number>();
|
|
38
|
+
private readonly _syncedLights = new Set<number>();
|
|
39
|
+
private readonly _syncedEmitters = new Set<number>();
|
|
40
|
+
private readonly _syncedPortals = new Set<number>();
|
|
41
|
+
private readonly _syncedInstancedModels = new Set<number>();
|
|
42
|
+
private readonly _transformCache = new Map<EntityId, Transform3D>();
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
readonly realmId: number,
|
|
46
|
+
readonly cache: AnchorMatrixCache
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extracts a Transform3D from the resolved model world matrix or cache.
|
|
51
|
+
*/
|
|
52
|
+
getResolvedTransform(world: EcsWorld, id: EntityId): Transform3D {
|
|
53
|
+
let tf = this._transformCache.get(id);
|
|
54
|
+
if (!tf) {
|
|
55
|
+
tf = {
|
|
56
|
+
position: [0, 0, 0],
|
|
57
|
+
rotation: [0, 0, 0, 1],
|
|
58
|
+
scale: [1, 1, 1]
|
|
59
|
+
};
|
|
60
|
+
this._transformCache.set(id, tf);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const model = world.getComponent<Model3DComponent>(id, 'Model3D');
|
|
64
|
+
const mat = model ? model.worldMatrix : this.cache.getWorld3D(id);
|
|
65
|
+
|
|
66
|
+
if (mat) {
|
|
67
|
+
mat4.getTranslation(scratchTrans, mat);
|
|
68
|
+
mat4.getRotation(scratchRot, mat);
|
|
69
|
+
mat4.getScaling(scratchScale, mat);
|
|
70
|
+
|
|
71
|
+
const p = tf.position;
|
|
72
|
+
p[0] = scratchTrans[0]!;
|
|
73
|
+
p[1] = scratchTrans[1]!;
|
|
74
|
+
p[2] = scratchTrans[2]!;
|
|
75
|
+
|
|
76
|
+
const r = tf.rotation!;
|
|
77
|
+
r[0] = scratchRot[0]!;
|
|
78
|
+
r[1] = scratchRot[1]!;
|
|
79
|
+
r[2] = scratchRot[2]!;
|
|
80
|
+
r[3] = scratchRot[3]!;
|
|
81
|
+
|
|
82
|
+
const s = tf.scale!;
|
|
83
|
+
s[0] = scratchScale[0]!;
|
|
84
|
+
s[1] = scratchScale[1]!;
|
|
85
|
+
s[2] = scratchScale[2]!;
|
|
86
|
+
} else {
|
|
87
|
+
tf.position[0] = 0;
|
|
88
|
+
tf.position[1] = 0;
|
|
89
|
+
tf.position[2] = 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return tf;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private _uploadPrimitive(
|
|
96
|
+
engine: GalfusEngine,
|
|
97
|
+
geometryId: number,
|
|
98
|
+
primitive: Primitive3DKind
|
|
99
|
+
): void {
|
|
100
|
+
switch (primitive.kind) {
|
|
101
|
+
case 'cube':
|
|
102
|
+
engine.realm3dCreateCube(this.realmId, geometryId, primitive.size);
|
|
103
|
+
break;
|
|
104
|
+
case 'plane':
|
|
105
|
+
engine.realm3dCreatePlane(this.realmId, geometryId, primitive.width, primitive.depth);
|
|
106
|
+
break;
|
|
107
|
+
case 'quad':
|
|
108
|
+
engine.realm3dCreateQuad(this.realmId, geometryId, primitive.width, primitive.height);
|
|
109
|
+
break;
|
|
110
|
+
case 'sphere':
|
|
111
|
+
engine.realm3dCreateSphere(
|
|
112
|
+
this.realmId,
|
|
113
|
+
geometryId,
|
|
114
|
+
primitive.radius,
|
|
115
|
+
primitive.rings,
|
|
116
|
+
primitive.sectors
|
|
117
|
+
);
|
|
118
|
+
break;
|
|
119
|
+
case 'cylinder':
|
|
120
|
+
engine.realm3dCreateCylinder(
|
|
121
|
+
this.realmId,
|
|
122
|
+
geometryId,
|
|
123
|
+
primitive.radius,
|
|
124
|
+
primitive.height,
|
|
125
|
+
primitive.sectors
|
|
126
|
+
);
|
|
127
|
+
break;
|
|
128
|
+
case 'capsule':
|
|
129
|
+
engine.realm3dCreateCapsule(
|
|
130
|
+
this.realmId,
|
|
131
|
+
geometryId,
|
|
132
|
+
primitive.radius,
|
|
133
|
+
primitive.cylinderHeight,
|
|
134
|
+
primitive.rings,
|
|
135
|
+
primitive.sectors
|
|
136
|
+
);
|
|
137
|
+
break;
|
|
138
|
+
case 'pyramid':
|
|
139
|
+
engine.realm3dCreatePyramid(
|
|
140
|
+
this.realmId,
|
|
141
|
+
geometryId,
|
|
142
|
+
primitive.width,
|
|
143
|
+
primitive.depth,
|
|
144
|
+
primitive.height
|
|
145
|
+
);
|
|
146
|
+
break;
|
|
147
|
+
case 'torus':
|
|
148
|
+
engine.realm3dCreateTorus(
|
|
149
|
+
this.realmId,
|
|
150
|
+
geometryId,
|
|
151
|
+
primitive.majorRadius,
|
|
152
|
+
primitive.minorRadius,
|
|
153
|
+
primitive.majorSegments,
|
|
154
|
+
primitive.minorSegments
|
|
155
|
+
);
|
|
156
|
+
break;
|
|
157
|
+
case 'custom':
|
|
158
|
+
engine.realm3dCreateGeometry(
|
|
159
|
+
this.realmId,
|
|
160
|
+
geometryId,
|
|
161
|
+
primitive.topology,
|
|
162
|
+
primitive.vertexData,
|
|
163
|
+
primitive.vertexData.byteLength,
|
|
164
|
+
primitive.indices,
|
|
165
|
+
primitive.indices.length
|
|
166
|
+
);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Synchronizes all active 3D resources and ECS components to the engine driver.
|
|
173
|
+
*/
|
|
174
|
+
sync(world: EcsWorld, engine: GalfusEngine, resourceProvider?: Realm3DResourceProvider): void {
|
|
175
|
+
// 1. Sync geometries from the resource registry
|
|
176
|
+
const currentGeometries = new Set<number>();
|
|
177
|
+
if (resourceProvider) {
|
|
178
|
+
for (const geo of resourceProvider.getGeometries()) {
|
|
179
|
+
currentGeometries.add(geo.geometryId);
|
|
180
|
+
if (!this._syncedGeometries.has(geo.geometryId) || geo.isDirty) {
|
|
181
|
+
this._uploadPrimitive(engine, geo.geometryId, geo.primitive);
|
|
182
|
+
if (geo.morphTargets) {
|
|
183
|
+
const { data, count } = geo.morphTargets;
|
|
184
|
+
engine.realm3dSetGeometryMorphTargets(
|
|
185
|
+
this.realmId,
|
|
186
|
+
geo.geometryId,
|
|
187
|
+
data,
|
|
188
|
+
data.byteLength,
|
|
189
|
+
count
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
this._syncedGeometries.add(geo.geometryId);
|
|
193
|
+
geo.isDirty = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const geoId of this._syncedGeometries) {
|
|
198
|
+
if (!currentGeometries.has(geoId)) {
|
|
199
|
+
engine.realm3dDestroyGeometry(this.realmId, geoId);
|
|
200
|
+
this._syncedGeometries.delete(geoId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 2. Sync materials from the resource registry
|
|
205
|
+
const currentMaterials = new Set<number>();
|
|
206
|
+
if (resourceProvider) {
|
|
207
|
+
for (const mat of resourceProvider.getMaterials()) {
|
|
208
|
+
currentMaterials.add(mat.materialId);
|
|
209
|
+
if (!this._syncedMaterials.has(mat.materialId) || mat.isDirty) {
|
|
210
|
+
engine.realm3dCreateMaterial(this.realmId, mat.materialId, mat.desc);
|
|
211
|
+
this._syncedMaterials.add(mat.materialId);
|
|
212
|
+
mat.isDirty = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const matId of this._syncedMaterials) {
|
|
217
|
+
if (!currentMaterials.has(matId)) {
|
|
218
|
+
engine.realm3dDestroyMaterial(this.realmId, matId);
|
|
219
|
+
this._syncedMaterials.delete(matId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 3. Sync Models & Meshes
|
|
224
|
+
const modelEntities = world.query('Model3D');
|
|
225
|
+
const currentModels = new Set<number>();
|
|
226
|
+
for (const id of modelEntities) {
|
|
227
|
+
const mesh = world.getComponent<Mesh3DComponent>(id, 'Mesh3D');
|
|
228
|
+
// If the entity has no mesh AND has a dedicated spatial component (Camera, Light, Emitter, Portal, InstancedModel),
|
|
229
|
+
// we do not register an empty Rust model for it.
|
|
230
|
+
if (
|
|
231
|
+
!mesh &&
|
|
232
|
+
(world.hasComponent(id, 'Camera3D') ||
|
|
233
|
+
world.hasComponent(id, 'Light3D') ||
|
|
234
|
+
world.hasComponent(id, 'ParticleEmitter3D') ||
|
|
235
|
+
world.hasComponent(id, 'Portal3D') ||
|
|
236
|
+
world.hasComponent(id, 'InstancedModel3D'))
|
|
237
|
+
) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const model = world.getComponent<Model3DComponent>(id, 'Model3D');
|
|
241
|
+
if (!model) continue;
|
|
242
|
+
currentModels.add(model.modelId);
|
|
243
|
+
|
|
244
|
+
const tf = this.getResolvedTransform(world, id);
|
|
245
|
+
const isRegistered = this._syncedModels.has(model.modelId);
|
|
246
|
+
const isSpatialDirty = (model.flags & SpatialDirtyFlags.WorldDirty) !== 0;
|
|
247
|
+
const isMeshDirty = mesh ? mesh.isDirty : false;
|
|
248
|
+
|
|
249
|
+
if (!isRegistered || isMeshDirty) {
|
|
250
|
+
const desc: Model3DDesc = {
|
|
251
|
+
geometryId: mesh ? mesh.geometryId : 0,
|
|
252
|
+
materialId: mesh ? mesh.materialId : 0,
|
|
253
|
+
castShadow: mesh ? mesh.castShadow : true,
|
|
254
|
+
receiveShadow: mesh ? mesh.receiveShadow : true,
|
|
255
|
+
castOutline: mesh ? mesh.castOutline : false,
|
|
256
|
+
outlineColor: mesh ? mesh.outlineColor : [1, 1, 1, 1],
|
|
257
|
+
active: mesh ? mesh.active : true,
|
|
258
|
+
boneOffset: mesh ? mesh.boneOffset : 0,
|
|
259
|
+
boneCount: mesh ? mesh.boneCount : 0
|
|
260
|
+
};
|
|
261
|
+
engine.realm3dCreateModel(this.realmId, model.modelId, desc, tf);
|
|
262
|
+
this._syncedModels.add(model.modelId);
|
|
263
|
+
if (mesh) mesh.isDirty = false;
|
|
264
|
+
} else if (isSpatialDirty) {
|
|
265
|
+
engine.realm3dSetModelTransform(this.realmId, model.modelId, tf);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const modelId of this._syncedModels) {
|
|
269
|
+
if (!currentModels.has(modelId)) {
|
|
270
|
+
engine.realm3dDestroyModel(this.realmId, modelId);
|
|
271
|
+
this._syncedModels.delete(modelId);
|
|
272
|
+
this._transformCache.delete(modelId);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 4. Sync Cameras
|
|
277
|
+
const cameraEntities = world.query('Camera3D');
|
|
278
|
+
const currentCameras = new Set<number>();
|
|
279
|
+
for (const id of cameraEntities) {
|
|
280
|
+
const camera = world.getComponent<Camera3DComponent>(id, 'Camera3D');
|
|
281
|
+
if (!camera) continue;
|
|
282
|
+
currentCameras.add(camera.cameraId);
|
|
283
|
+
|
|
284
|
+
const model = world.getComponent<Model3DComponent>(id, 'Model3D');
|
|
285
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
286
|
+
const tf = this.getResolvedTransform(world, id);
|
|
287
|
+
if (!this._syncedCameras.has(camera.cameraId) || camera.isDirty) {
|
|
288
|
+
engine.realm3dCreateCamera(this.realmId, camera.cameraId, camera.desc, tf);
|
|
289
|
+
this._syncedCameras.add(camera.cameraId);
|
|
290
|
+
camera.isDirty = false;
|
|
291
|
+
} else if (isSpatialDirty) {
|
|
292
|
+
engine.realm3dSetCameraTransform(this.realmId, camera.cameraId, tf);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const camId of this._syncedCameras) {
|
|
296
|
+
if (!currentCameras.has(camId)) {
|
|
297
|
+
engine.realm3dDestroyCamera(this.realmId, camId);
|
|
298
|
+
this._syncedCameras.delete(camId);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 5. Sync Lights
|
|
303
|
+
const lightEntities = world.query('Light3D');
|
|
304
|
+
const currentLights = new Set<number>();
|
|
305
|
+
for (const id of lightEntities) {
|
|
306
|
+
const light = world.getComponent<Light3DComponent>(id, 'Light3D');
|
|
307
|
+
if (!light) continue;
|
|
308
|
+
currentLights.add(light.lightId);
|
|
309
|
+
|
|
310
|
+
const model = world.getComponent<Model3DComponent>(id, 'Model3D');
|
|
311
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
312
|
+
const tf = this.getResolvedTransform(world, id);
|
|
313
|
+
if (!this._syncedLights.has(light.lightId) || light.isDirty) {
|
|
314
|
+
engine.realm3dCreateLight(this.realmId, light.lightId, light.desc, tf);
|
|
315
|
+
this._syncedLights.add(light.lightId);
|
|
316
|
+
light.isDirty = false;
|
|
317
|
+
} else if (isSpatialDirty) {
|
|
318
|
+
engine.realm3dSetLightTransform(this.realmId, light.lightId, tf);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const lightId of this._syncedLights) {
|
|
322
|
+
if (!currentLights.has(lightId)) {
|
|
323
|
+
engine.realm3dDestroyLight(this.realmId, lightId);
|
|
324
|
+
this._syncedLights.delete(lightId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 6. Sync Particle Emitters
|
|
329
|
+
const emitterEntities = world.query('ParticleEmitter3D');
|
|
330
|
+
const currentEmitters = new Set<number>();
|
|
331
|
+
for (const id of emitterEntities) {
|
|
332
|
+
const emitter = world.getComponent<ParticleEmitter3DComponent>(id, 'ParticleEmitter3D');
|
|
333
|
+
if (!emitter) continue;
|
|
334
|
+
currentEmitters.add(emitter.emitterId);
|
|
335
|
+
|
|
336
|
+
const model = world.getComponent<Model3DComponent>(id, 'Model3D');
|
|
337
|
+
const isSpatialDirty = model ? (model.flags & SpatialDirtyFlags.WorldDirty) !== 0 : false;
|
|
338
|
+
const tf = this.getResolvedTransform(world, id);
|
|
339
|
+
if (!this._syncedEmitters.has(emitter.emitterId) || emitter.isDirty) {
|
|
340
|
+
engine.realm3dCreateParticleEmitter(this.realmId, emitter.emitterId, emitter.desc, tf);
|
|
341
|
+
this._syncedEmitters.add(emitter.emitterId);
|
|
342
|
+
emitter.isDirty = false;
|
|
343
|
+
} else if (isSpatialDirty) {
|
|
344
|
+
engine.realm3dSetParticleEmitterTransform(this.realmId, emitter.emitterId, tf);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const emitterId of this._syncedEmitters) {
|
|
348
|
+
if (!currentEmitters.has(emitterId)) {
|
|
349
|
+
engine.realm3dDestroyParticleEmitter(this.realmId, emitterId);
|
|
350
|
+
this._syncedEmitters.delete(emitterId);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 7. Sync Portals
|
|
355
|
+
const portalEntities = world.query('Portal3D');
|
|
356
|
+
const currentPortals = new Set<number>();
|
|
357
|
+
for (const id of portalEntities) {
|
|
358
|
+
const portal = world.getComponent<Portal3DComponent>(id, 'Portal3D');
|
|
359
|
+
if (!portal) continue;
|
|
360
|
+
currentPortals.add(portal.portalId);
|
|
361
|
+
|
|
362
|
+
if (!this._syncedPortals.has(portal.portalId) || portal.isDirty) {
|
|
363
|
+
engine.realm3dCreatePortal(this.realmId, portal.portalId, portal.desc);
|
|
364
|
+
this._syncedPortals.add(portal.portalId);
|
|
365
|
+
portal.isDirty = false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const portalId of this._syncedPortals) {
|
|
369
|
+
if (!currentPortals.has(portalId)) {
|
|
370
|
+
engine.realm3dDestroyPortal(this.realmId, portalId);
|
|
371
|
+
this._syncedPortals.delete(portalId);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 8. Sync Instanced Models
|
|
376
|
+
const instancedEntities = world.query('InstancedModel3D');
|
|
377
|
+
const currentInstanced = new Set<number>();
|
|
378
|
+
for (const id of instancedEntities) {
|
|
379
|
+
const instanced = world.getComponent<InstancedModel3DComponent>(id, 'InstancedModel3D');
|
|
380
|
+
if (!instanced) continue;
|
|
381
|
+
currentInstanced.add(instanced.instancedModelId);
|
|
382
|
+
|
|
383
|
+
if (!this._syncedInstancedModels.has(instanced.instancedModelId) || instanced.isDirty) {
|
|
384
|
+
engine.realm3dCreateInstancedModel(
|
|
385
|
+
this.realmId,
|
|
386
|
+
instanced.instancedModelId,
|
|
387
|
+
instanced.desc
|
|
388
|
+
);
|
|
389
|
+
this._syncedInstancedModels.add(instanced.instancedModelId);
|
|
390
|
+
instanced.isDirty = false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const instancedId of this._syncedInstancedModels) {
|
|
394
|
+
if (!currentInstanced.has(instancedId)) {
|
|
395
|
+
engine.realm3dDestroyInstancedModel(this.realmId, instancedId);
|
|
396
|
+
this._syncedInstancedModels.delete(instancedId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 9. Clear WorldDirty for all Model3D entities after all Core sync steps are complete
|
|
401
|
+
for (const id of modelEntities) {
|
|
402
|
+
const model = world.getComponent<Model3DComponent>(id, 'Model3D');
|
|
403
|
+
if (model) {
|
|
404
|
+
model.flags &= ~SpatialDirtyFlags.WorldDirty;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
isCameraSynced(cameraId: number): boolean {
|
|
410
|
+
return this._syncedCameras.has(cameraId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
isEmitterSynced(emitterId: number): boolean {
|
|
414
|
+
return this._syncedEmitters.has(emitterId);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Disposes all tracked IDs on the engine when the realm is destroyed.
|
|
419
|
+
*/
|
|
420
|
+
dispose(engine: GalfusEngine | null): void {
|
|
421
|
+
if (engine) {
|
|
422
|
+
for (const id of this._syncedModels) engine.realm3dDestroyModel(this.realmId, id);
|
|
423
|
+
for (const id of this._syncedLights) engine.realm3dDestroyLight(this.realmId, id);
|
|
424
|
+
for (const id of this._syncedCameras) engine.realm3dDestroyCamera(this.realmId, id);
|
|
425
|
+
for (const id of this._syncedMaterials) engine.realm3dDestroyMaterial(this.realmId, id);
|
|
426
|
+
for (const id of this._syncedGeometries) engine.realm3dDestroyGeometry(this.realmId, id);
|
|
427
|
+
for (const id of this._syncedEmitters) engine.realm3dDestroyParticleEmitter(this.realmId, id);
|
|
428
|
+
for (const id of this._syncedPortals) engine.realm3dDestroyPortal(this.realmId, id);
|
|
429
|
+
for (const id of this._syncedInstancedModels)
|
|
430
|
+
engine.realm3dDestroyInstancedModel(this.realmId, id);
|
|
431
|
+
}
|
|
432
|
+
this._syncedModels.clear();
|
|
433
|
+
this._syncedLights.clear();
|
|
434
|
+
this._syncedCameras.clear();
|
|
435
|
+
this._syncedMaterials.clear();
|
|
436
|
+
this._syncedGeometries.clear();
|
|
437
|
+
this._syncedEmitters.clear();
|
|
438
|
+
this._syncedPortals.clear();
|
|
439
|
+
this._syncedInstancedModels.clear();
|
|
440
|
+
this._transformCache.clear();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realm 3D math and spatial utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { vec3 } from '../../math/index';
|
|
6
|
+
import type { Vec3 } from '../../math/vec3';
|
|
7
|
+
|
|
8
|
+
export interface BoundingSphere3D {
|
|
9
|
+
center: [number, number, number];
|
|
10
|
+
radius: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createBoundingSphere3D(
|
|
14
|
+
center: [number, number, number] | Vec3,
|
|
15
|
+
radius: number
|
|
16
|
+
): BoundingSphere3D {
|
|
17
|
+
return {
|
|
18
|
+
center: [center[0], center[1], center[2]],
|
|
19
|
+
radius
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function intersectsSphere3D(a: BoundingSphere3D, b: BoundingSphere3D): boolean {
|
|
24
|
+
const d = vec3.distance(a.center, b.center);
|
|
25
|
+
return d <= a.radius + b.radius;
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceManager manages allocation and GPU binding for shared textures.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GalfusEngine, GpuTextureDesc, TextureUpdateDesc } from '@galfus/transport-types';
|
|
6
|
+
import { IdAllocator, type TextureId } from '../types';
|
|
7
|
+
|
|
8
|
+
export interface LoadTextureOptions {
|
|
9
|
+
/** Debug label for the GPU texture resource */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Whether pixels represent sRGB color (defaults to true; set false for normal/data maps) */
|
|
12
|
+
srgb?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ResourceManager {
|
|
16
|
+
private _engine: GalfusEngine | null = null;
|
|
17
|
+
private readonly _idAllocator = new IdAllocator<TextureId>(1);
|
|
18
|
+
private readonly _textures = new Set<TextureId>();
|
|
19
|
+
|
|
20
|
+
bindEngine(engine: GalfusEngine): void {
|
|
21
|
+
this._engine = engine;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Loads raw image bytes (PNG, JPEG, WebP, ICO) directly into a tracked GPU texture.
|
|
26
|
+
* Auto-detection and decoding are handled natively inside the Galfus core.
|
|
27
|
+
* Follows payload-first convention: (data, options?).
|
|
28
|
+
*
|
|
29
|
+
* @param data Encoded image file bytes (PNG, JPEG, WebP, ICO).
|
|
30
|
+
* @param options Configuration options (sRGB color space, debug label).
|
|
31
|
+
* @returns The allocated logical TextureId.
|
|
32
|
+
*/
|
|
33
|
+
loadTexture(data: Uint8Array, options?: LoadTextureOptions): TextureId {
|
|
34
|
+
const srgb = options?.srgb ?? true;
|
|
35
|
+
return this.createTexture(data, {
|
|
36
|
+
label: options?.label ?? 'image-texture',
|
|
37
|
+
width: 0,
|
|
38
|
+
height: 0,
|
|
39
|
+
format: srgb ? 'rgba8UnormSrgb' : 'rgba8Unorm'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a shared GPU texture from raw byte data under a tracked logical TextureId.
|
|
45
|
+
* If `data` contains encoded image container bytes (PNG, JPEG, WebP, ICO), the Galfus core
|
|
46
|
+
* automatically decodes it into RGBA8 and derives width/height.
|
|
47
|
+
*
|
|
48
|
+
* Follows payload-first convention: (data, desc).
|
|
49
|
+
*/
|
|
50
|
+
createTexture(data: Uint8Array, desc: GpuTextureDesc): TextureId {
|
|
51
|
+
const textureId = this._idAllocator.allocate('TextureId');
|
|
52
|
+
if (this._engine) {
|
|
53
|
+
this._engine.resourceCreateTexture(textureId, desc, data, data.byteLength);
|
|
54
|
+
}
|
|
55
|
+
this._textures.add(textureId);
|
|
56
|
+
return textureId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Updates a sub-rectangle of an existing GPU texture.
|
|
61
|
+
* Follows payload-first convention: (textureId, data, rect).
|
|
62
|
+
*/
|
|
63
|
+
updateTexture(textureId: TextureId, data: Uint8Array, rect: TextureUpdateDesc): void {
|
|
64
|
+
if (this._engine) {
|
|
65
|
+
this._engine.resourceUpdateTexture(textureId, rect, data, data.byteLength);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Disposes a GPU texture and frees its logical ID.
|
|
71
|
+
*/
|
|
72
|
+
disposeTexture(textureId: TextureId): void {
|
|
73
|
+
if (!this._textures.has(textureId)) return;
|
|
74
|
+
if (this._engine) {
|
|
75
|
+
this._engine.resourceDisposeTexture(textureId);
|
|
76
|
+
}
|
|
77
|
+
this._textures.delete(textureId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Disposes all active textures.
|
|
82
|
+
*/
|
|
83
|
+
dispose(): void {
|
|
84
|
+
if (this._engine) {
|
|
85
|
+
for (const textureId of this._textures) {
|
|
86
|
+
this._engine.resourceDisposeTexture(textureId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this._textures.clear();
|
|
90
|
+
this._engine = null;
|
|
91
|
+
}
|
|
92
|
+
}
|