topazcube 0.1.31 → 0.1.35
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/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- package/src/utils.ts +0 -403
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { mat4, vec3, quat } from "../math.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EntityManager - Data-oriented entity storage and management
|
|
5
|
+
*
|
|
6
|
+
* Entities are plain objects with:
|
|
7
|
+
* - position: [x, y, z] (Float64)
|
|
8
|
+
* - rotation: [x, y, z, w] (Quaternion)
|
|
9
|
+
* - scale: [x, y, z]
|
|
10
|
+
* - model: "path/to/model.glb|meshName" (ModelID)
|
|
11
|
+
* - animation: "animationName" (optional)
|
|
12
|
+
* - phase: 0.0-1.0 (animation phase, optional)
|
|
13
|
+
* - light: { enabled, position, direction, color, geom, animation } (optional)
|
|
14
|
+
*
|
|
15
|
+
* Sprite properties (for billboard rendering):
|
|
16
|
+
* - sprite: "texture.png|8" (texture path | framesPerRow, optional)
|
|
17
|
+
* - pivot: 'center' | 'bottom' | 'horizontal' (billboard mode)
|
|
18
|
+
* - frame: number or "start|end|fps" (animation frame)
|
|
19
|
+
* - roughness: 0-1 (material roughness, default 0.7)
|
|
20
|
+
* - color: [r, g, b, a] (tint color, default [1,1,1,1])
|
|
21
|
+
*
|
|
22
|
+
* Particle properties (for GPU particle emitters):
|
|
23
|
+
* - particles: ParticleEmitter config object or emitter UID
|
|
24
|
+
* - _emitterUID: internal - UID of registered particle emitter
|
|
25
|
+
*
|
|
26
|
+
* Runtime fields (calculated):
|
|
27
|
+
* - _bsphere: { center: [x,y,z], radius: r }
|
|
28
|
+
* - _matrix: mat4
|
|
29
|
+
* - _uvTransform: [offsetX, offsetY, scaleX, scaleY] (for sprite sheets)
|
|
30
|
+
* - _animState: { currentAnim, time, blendFrom, blendFromTime, blendWeight, blendDuration, isBlending }
|
|
31
|
+
*/
|
|
32
|
+
class EntityManager {
|
|
33
|
+
constructor() {
|
|
34
|
+
// Main entity storage: id -> entity
|
|
35
|
+
this.entities = {}
|
|
36
|
+
|
|
37
|
+
// Indices for fast lookup
|
|
38
|
+
this._byModel = {} // modelId -> Set<entityId>
|
|
39
|
+
this._byAnimation = {} // modelId|animation -> Set<entityId>
|
|
40
|
+
this._lights = new Set() // entityIds with lights
|
|
41
|
+
this._particles = new Set() // entityIds with particle emitters
|
|
42
|
+
|
|
43
|
+
// ID generation
|
|
44
|
+
this._nextId = 1
|
|
45
|
+
|
|
46
|
+
// Dirty tracking for batch updates
|
|
47
|
+
this._dirtyEntities = new Set()
|
|
48
|
+
this._dirtyLights = new Set()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate a unique entity ID
|
|
53
|
+
*/
|
|
54
|
+
_generateId() {
|
|
55
|
+
return `e${this._nextId++}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a new entity
|
|
60
|
+
* @param {Object} data - Entity data
|
|
61
|
+
* @returns {string} Entity ID
|
|
62
|
+
*/
|
|
63
|
+
create(data = {}) {
|
|
64
|
+
const id = data.id || this._generateId()
|
|
65
|
+
|
|
66
|
+
const entity = {
|
|
67
|
+
position: data.position || [0, 0, 0],
|
|
68
|
+
rotation: data.rotation || [0, 0, 0, 1], // identity quaternion
|
|
69
|
+
scale: data.scale || [1, 1, 1],
|
|
70
|
+
model: data.model || null,
|
|
71
|
+
animation: data.animation || null,
|
|
72
|
+
phase: data.phase || 0,
|
|
73
|
+
light: data.light || null,
|
|
74
|
+
noRounding: data.noRounding || false, // Exempt from pixel/position rounding
|
|
75
|
+
|
|
76
|
+
// Sprite properties (for billboard rendering)
|
|
77
|
+
sprite: data.sprite || null, // "texture.png|8" (texture|framesPerRow)
|
|
78
|
+
pivot: data.pivot || 'center', // 'center', 'bottom', 'horizontal'
|
|
79
|
+
frame: data.frame ?? 0, // integer or "start|end|fps"
|
|
80
|
+
roughness: data.roughness ?? 0.7, // Material roughness (0-1)
|
|
81
|
+
color: data.color || null, // [r, g, b, a] tint color (null = white)
|
|
82
|
+
|
|
83
|
+
// Particle properties (for GPU particle emitters)
|
|
84
|
+
particles: data.particles || null, // ParticleEmitter config or emitter UID
|
|
85
|
+
_emitterUID: null, // Internal: UID of registered emitter
|
|
86
|
+
|
|
87
|
+
// Runtime fields
|
|
88
|
+
_bsphere: { center: [0, 0, 0], radius: 0 }, // radius 0 = not initialized
|
|
89
|
+
_matrix: mat4.create(),
|
|
90
|
+
_uvTransform: null, // [offsetX, offsetY, scaleX, scaleY] for sprites
|
|
91
|
+
_visible: true,
|
|
92
|
+
_dirty: true,
|
|
93
|
+
|
|
94
|
+
// Animation state for blending (used by individual skins)
|
|
95
|
+
_animState: {
|
|
96
|
+
currentAnim: data.animation || null,
|
|
97
|
+
time: (data.phase || 0) * 1.0, // Will be scaled by animation duration
|
|
98
|
+
blendFrom: null,
|
|
99
|
+
blendFromTime: 0,
|
|
100
|
+
blendWeight: 1.0,
|
|
101
|
+
blendDuration: 0.3,
|
|
102
|
+
isBlending: false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.entities[id] = entity
|
|
107
|
+
|
|
108
|
+
// Update indices
|
|
109
|
+
if (entity.model) {
|
|
110
|
+
this._addToModelIndex(id, entity.model)
|
|
111
|
+
if (entity.animation) {
|
|
112
|
+
this._addToAnimationIndex(id, entity.model, entity.animation)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (entity.light && entity.light.enabled) {
|
|
117
|
+
this._lights.add(id)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (entity.particles) {
|
|
121
|
+
this._particles.add(id)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this._dirtyEntities.add(id)
|
|
125
|
+
this._updateEntityMatrix(id)
|
|
126
|
+
|
|
127
|
+
return id
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get entity by ID
|
|
132
|
+
*/
|
|
133
|
+
get(id) {
|
|
134
|
+
return this.entities[id]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Update entity properties
|
|
139
|
+
*/
|
|
140
|
+
update(id, data) {
|
|
141
|
+
const entity = this.entities[id]
|
|
142
|
+
if (!entity) return false
|
|
143
|
+
|
|
144
|
+
const oldModel = entity.model
|
|
145
|
+
const oldAnimation = entity.animation
|
|
146
|
+
|
|
147
|
+
// Update transform
|
|
148
|
+
if (data.position) entity.position = data.position
|
|
149
|
+
if (data.rotation) entity.rotation = data.rotation
|
|
150
|
+
if (data.scale) entity.scale = data.scale
|
|
151
|
+
|
|
152
|
+
// Update model reference
|
|
153
|
+
if (data.model !== undefined && data.model !== oldModel) {
|
|
154
|
+
if (oldModel) {
|
|
155
|
+
this._removeFromModelIndex(id, oldModel)
|
|
156
|
+
if (oldAnimation) {
|
|
157
|
+
this._removeFromAnimationIndex(id, oldModel, oldAnimation)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
entity.model = data.model
|
|
161
|
+
if (data.model) {
|
|
162
|
+
this._addToModelIndex(id, data.model)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Update animation
|
|
167
|
+
if (data.animation !== undefined && data.animation !== oldAnimation) {
|
|
168
|
+
const model = data.model !== undefined ? data.model : entity.model
|
|
169
|
+
if (oldAnimation && oldModel) {
|
|
170
|
+
this._removeFromAnimationIndex(id, oldModel, oldAnimation)
|
|
171
|
+
}
|
|
172
|
+
entity.animation = data.animation
|
|
173
|
+
if (data.animation && model) {
|
|
174
|
+
this._addToAnimationIndex(id, model, data.animation)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (data.phase !== undefined) entity.phase = data.phase
|
|
179
|
+
|
|
180
|
+
// Update light
|
|
181
|
+
if (data.light !== undefined) {
|
|
182
|
+
entity.light = data.light
|
|
183
|
+
if (data.light && data.light.enabled) {
|
|
184
|
+
this._lights.add(id)
|
|
185
|
+
this._dirtyLights.add(id)
|
|
186
|
+
} else {
|
|
187
|
+
this._lights.delete(id)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
entity._dirty = true
|
|
192
|
+
this._dirtyEntities.add(id)
|
|
193
|
+
this._updateEntityMatrix(id)
|
|
194
|
+
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Delete entity
|
|
200
|
+
*/
|
|
201
|
+
delete(id) {
|
|
202
|
+
const entity = this.entities[id]
|
|
203
|
+
if (!entity) return false
|
|
204
|
+
|
|
205
|
+
// Remove from indices
|
|
206
|
+
if (entity.model) {
|
|
207
|
+
this._removeFromModelIndex(id, entity.model)
|
|
208
|
+
if (entity.animation) {
|
|
209
|
+
this._removeFromAnimationIndex(id, entity.model, entity.animation)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this._lights.delete(id)
|
|
214
|
+
this._particles.delete(id)
|
|
215
|
+
this._dirtyEntities.delete(id)
|
|
216
|
+
this._dirtyLights.delete(id)
|
|
217
|
+
|
|
218
|
+
delete this.entities[id]
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Update entity's model matrix from transform
|
|
224
|
+
*/
|
|
225
|
+
_updateEntityMatrix(id) {
|
|
226
|
+
const entity = this.entities[id]
|
|
227
|
+
if (!entity) return
|
|
228
|
+
|
|
229
|
+
mat4.identity(entity._matrix)
|
|
230
|
+
mat4.translate(entity._matrix, entity._matrix, entity.position)
|
|
231
|
+
|
|
232
|
+
// Apply rotation from quaternion
|
|
233
|
+
const rotMat = mat4.create()
|
|
234
|
+
mat4.fromQuat(rotMat, entity.rotation)
|
|
235
|
+
mat4.multiply(entity._matrix, entity._matrix, rotMat)
|
|
236
|
+
|
|
237
|
+
mat4.scale(entity._matrix, entity._matrix, entity.scale)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Update entity's bounding sphere (called when asset is loaded)
|
|
242
|
+
*/
|
|
243
|
+
updateBoundingSphere(id, baseBsphere) {
|
|
244
|
+
const entity = this.entities[id]
|
|
245
|
+
if (!entity || !baseBsphere) return
|
|
246
|
+
|
|
247
|
+
// Transform bsphere center by entity matrix
|
|
248
|
+
const center = vec3.create()
|
|
249
|
+
vec3.transformMat4(center, baseBsphere.center, entity._matrix)
|
|
250
|
+
|
|
251
|
+
// Scale radius by max scale component
|
|
252
|
+
const maxScale = Math.max(
|
|
253
|
+
Math.abs(entity.scale[0]),
|
|
254
|
+
Math.abs(entity.scale[1]),
|
|
255
|
+
Math.abs(entity.scale[2])
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
entity._bsphere = {
|
|
259
|
+
center: [center[0], center[1], center[2]],
|
|
260
|
+
radius: baseBsphere.radius * maxScale
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get all entities using a specific model
|
|
266
|
+
*/
|
|
267
|
+
getByModel(modelId) {
|
|
268
|
+
const ids = this._byModel[modelId]
|
|
269
|
+
if (!ids) return []
|
|
270
|
+
return Array.from(ids).map(id => ({ id, entity: this.entities[id] }))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get all entities with a specific model and animation
|
|
275
|
+
*/
|
|
276
|
+
getByModelAndAnimation(modelId, animation) {
|
|
277
|
+
const key = `${modelId}|${animation}`
|
|
278
|
+
const ids = this._byAnimation[key]
|
|
279
|
+
if (!ids) return []
|
|
280
|
+
return Array.from(ids).map(id => ({ id, entity: this.entities[id] }))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get all unique model IDs currently in use
|
|
285
|
+
*/
|
|
286
|
+
getActiveModels() {
|
|
287
|
+
return Object.keys(this._byModel)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get all entities with lights
|
|
292
|
+
*/
|
|
293
|
+
getLights() {
|
|
294
|
+
return Array.from(this._lights).map(id => ({ id, entity: this.entities[id] }))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get all entities with particle emitters
|
|
299
|
+
*/
|
|
300
|
+
getParticles() {
|
|
301
|
+
return Array.from(this._particles).map(id => ({ id, entity: this.entities[id] }))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get dirty entities and clear dirty flags
|
|
306
|
+
*/
|
|
307
|
+
consumeDirtyEntities() {
|
|
308
|
+
const dirty = Array.from(this._dirtyEntities)
|
|
309
|
+
this._dirtyEntities.clear()
|
|
310
|
+
for (const id of dirty) {
|
|
311
|
+
const entity = this.entities[id]
|
|
312
|
+
if (entity) entity._dirty = false
|
|
313
|
+
}
|
|
314
|
+
return dirty
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get dirty lights and clear dirty flags
|
|
319
|
+
*/
|
|
320
|
+
consumeDirtyLights() {
|
|
321
|
+
const dirty = Array.from(this._dirtyLights)
|
|
322
|
+
this._dirtyLights.clear()
|
|
323
|
+
return dirty
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Set visibility for entity
|
|
328
|
+
*/
|
|
329
|
+
setVisible(id, visible) {
|
|
330
|
+
const entity = this.entities[id]
|
|
331
|
+
if (entity) {
|
|
332
|
+
entity._visible = visible
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get all visible entities
|
|
338
|
+
*/
|
|
339
|
+
getVisible() {
|
|
340
|
+
const result = []
|
|
341
|
+
for (const id in this.entities) {
|
|
342
|
+
if (this.entities[id]._visible) {
|
|
343
|
+
result.push({ id, entity: this.entities[id] })
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return result
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Iterate over all entities
|
|
351
|
+
*/
|
|
352
|
+
forEach(callback) {
|
|
353
|
+
for (const id in this.entities) {
|
|
354
|
+
callback(id, this.entities[id])
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get entity count
|
|
360
|
+
*/
|
|
361
|
+
get count() {
|
|
362
|
+
return Object.keys(this.entities).length
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Private index management methods
|
|
366
|
+
|
|
367
|
+
_addToModelIndex(id, modelId) {
|
|
368
|
+
if (!this._byModel[modelId]) {
|
|
369
|
+
this._byModel[modelId] = new Set()
|
|
370
|
+
}
|
|
371
|
+
this._byModel[modelId].add(id)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_removeFromModelIndex(id, modelId) {
|
|
375
|
+
if (this._byModel[modelId]) {
|
|
376
|
+
this._byModel[modelId].delete(id)
|
|
377
|
+
if (this._byModel[modelId].size === 0) {
|
|
378
|
+
delete this._byModel[modelId]
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_addToAnimationIndex(id, modelId, animation) {
|
|
384
|
+
const key = `${modelId}|${animation}`
|
|
385
|
+
if (!this._byAnimation[key]) {
|
|
386
|
+
this._byAnimation[key] = new Set()
|
|
387
|
+
}
|
|
388
|
+
this._byAnimation[key].add(id)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
_removeFromAnimationIndex(id, modelId, animation) {
|
|
392
|
+
const key = `${modelId}|${animation}`
|
|
393
|
+
if (this._byAnimation[key]) {
|
|
394
|
+
this._byAnimation[key].delete(id)
|
|
395
|
+
if (this._byAnimation[key].size === 0) {
|
|
396
|
+
delete this._byAnimation[key]
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Set animation for entity with optional blending
|
|
403
|
+
* @param {string} id - Entity ID
|
|
404
|
+
* @param {string} animation - Animation name
|
|
405
|
+
* @param {number} blendTime - Blend duration (0 = instant switch)
|
|
406
|
+
*/
|
|
407
|
+
setAnimation(id, animation, blendTime = 0.3) {
|
|
408
|
+
const entity = this.entities[id]
|
|
409
|
+
if (!entity) return false
|
|
410
|
+
|
|
411
|
+
const animState = entity._animState
|
|
412
|
+
|
|
413
|
+
// If same animation, do nothing
|
|
414
|
+
if (animState.currentAnim === animation) return true
|
|
415
|
+
|
|
416
|
+
// Start blend if we have a current animation and blend time > 0
|
|
417
|
+
if (blendTime > 0 && animState.currentAnim) {
|
|
418
|
+
animState.blendFrom = animState.currentAnim
|
|
419
|
+
animState.blendFromTime = animState.time
|
|
420
|
+
animState.blendWeight = 0
|
|
421
|
+
animState.blendDuration = blendTime
|
|
422
|
+
animState.isBlending = true
|
|
423
|
+
} else {
|
|
424
|
+
animState.isBlending = false
|
|
425
|
+
animState.blendFrom = null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
animState.currentAnim = animation
|
|
429
|
+
animState.time = 0
|
|
430
|
+
|
|
431
|
+
// Also update the legacy animation field for compatibility
|
|
432
|
+
const oldAnimation = entity.animation
|
|
433
|
+
if (oldAnimation !== animation) {
|
|
434
|
+
if (oldAnimation && entity.model) {
|
|
435
|
+
this._removeFromAnimationIndex(id, entity.model, oldAnimation)
|
|
436
|
+
}
|
|
437
|
+
entity.animation = animation
|
|
438
|
+
if (animation && entity.model) {
|
|
439
|
+
this._addToAnimationIndex(id, entity.model, animation)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Update animation time for entity
|
|
448
|
+
* @param {string} id - Entity ID
|
|
449
|
+
* @param {number} dt - Delta time
|
|
450
|
+
*/
|
|
451
|
+
updateAnimationTime(id, dt) {
|
|
452
|
+
const entity = this.entities[id]
|
|
453
|
+
if (!entity) return
|
|
454
|
+
|
|
455
|
+
const animState = entity._animState
|
|
456
|
+
animState.time += dt
|
|
457
|
+
|
|
458
|
+
if (animState.isBlending) {
|
|
459
|
+
animState.blendFromTime += dt
|
|
460
|
+
animState.blendWeight = Math.min(
|
|
461
|
+
animState.blendWeight + dt / animState.blendDuration,
|
|
462
|
+
1.0
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if (animState.blendWeight >= 1.0) {
|
|
466
|
+
animState.isBlending = false
|
|
467
|
+
animState.blendFrom = null
|
|
468
|
+
animState.blendWeight = 1.0
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Check if entity is currently blending animations
|
|
475
|
+
*/
|
|
476
|
+
isBlending(id) {
|
|
477
|
+
const entity = this.entities[id]
|
|
478
|
+
return entity?._animState?.isBlending || false
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get animation state for entity
|
|
483
|
+
*/
|
|
484
|
+
getAnimationState(id) {
|
|
485
|
+
const entity = this.entities[id]
|
|
486
|
+
return entity?._animState || null
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Clear all entities
|
|
491
|
+
*/
|
|
492
|
+
clear() {
|
|
493
|
+
this.entities = {}
|
|
494
|
+
this._byModel = {}
|
|
495
|
+
this._byAnimation = {}
|
|
496
|
+
this._lights.clear()
|
|
497
|
+
this._particles.clear()
|
|
498
|
+
this._dirtyEntities.clear()
|
|
499
|
+
this._dirtyLights.clear()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Serialize entities for saving
|
|
504
|
+
*/
|
|
505
|
+
serialize() {
|
|
506
|
+
const data = {}
|
|
507
|
+
for (const id in this.entities) {
|
|
508
|
+
const e = this.entities[id]
|
|
509
|
+
data[id] = {
|
|
510
|
+
position: [...e.position],
|
|
511
|
+
rotation: [...e.rotation],
|
|
512
|
+
scale: [...e.scale],
|
|
513
|
+
model: e.model,
|
|
514
|
+
animation: e.animation,
|
|
515
|
+
phase: e.phase,
|
|
516
|
+
light: e.light ? { ...e.light } : null,
|
|
517
|
+
// Sprite properties
|
|
518
|
+
sprite: e.sprite,
|
|
519
|
+
pivot: e.pivot,
|
|
520
|
+
frame: e.frame,
|
|
521
|
+
roughness: e.roughness,
|
|
522
|
+
color: e.color ? [...e.color] : null,
|
|
523
|
+
// Particle properties (serialize config, not runtime UID)
|
|
524
|
+
particles: e.particles
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return data
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Deserialize entities from saved data
|
|
532
|
+
*/
|
|
533
|
+
deserialize(data) {
|
|
534
|
+
this.clear()
|
|
535
|
+
for (const id in data) {
|
|
536
|
+
this.create({ ...data[id], id })
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export { EntityManager }
|