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.
Files changed (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +170 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +563 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2258 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,343 @@
1
+ import { mat4 } from "../math.js"
2
+
3
+ /**
4
+ * InstanceManager - Manages instance batches for efficient rendering
5
+ *
6
+ * Groups entities by ModelID for instanced draw calls.
7
+ * Handles buffer pools for instance data (model matrix + bsphere + sprite data).
8
+ *
9
+ * Instance data format (112 bytes per instance):
10
+ * - mat4x4f: model matrix (64 bytes)
11
+ * - vec4f: position.xyz + boundingRadius (16 bytes)
12
+ * - vec4f: uvOffset.xy + uvScale.xy (16 bytes) - for sprite sheets
13
+ * - vec4f: color.rgba (16 bytes) - for sprite tinting
14
+ */
15
+ class InstanceManager {
16
+ constructor(engine = null) {
17
+ // Reference to engine for settings access
18
+ this.engine = engine
19
+
20
+ // Buffer pools: size -> array of available buffers
21
+ this._bufferPool = new Map()
22
+
23
+ // Active batches: modelId -> batch info
24
+ this._batches = new Map()
25
+
26
+ // Instance data stride (28 floats = 112 bytes)
27
+ this.INSTANCE_STRIDE = 28
28
+
29
+ // Default pool size
30
+ this.DEFAULT_POOL_SIZE = 1000
31
+ }
32
+
33
+ /**
34
+ * Get or create a buffer from the pool
35
+ * @param {number} capacity - Number of instances the buffer should hold
36
+ * @returns {Object} Buffer info
37
+ */
38
+ _getBuffer(capacity) {
39
+ const { device } = this.engine
40
+
41
+ // Round up to nearest pool size
42
+ const poolSize = Math.max(this.DEFAULT_POOL_SIZE, Math.pow(2, Math.ceil(Math.log2(capacity))))
43
+
44
+ // Check pool
45
+ if (!this._bufferPool.has(poolSize)) {
46
+ this._bufferPool.set(poolSize, [])
47
+ }
48
+
49
+ const pool = this._bufferPool.get(poolSize)
50
+ if (pool.length > 0) {
51
+ return pool.pop()
52
+ }
53
+
54
+ // Create new buffer
55
+ const byteSize = poolSize * this.INSTANCE_STRIDE * 4 // 4 bytes per float
56
+ const gpuBuffer = device.createBuffer({
57
+ size: byteSize,
58
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
59
+ label: `Instance buffer (${poolSize})`
60
+ })
61
+
62
+ return {
63
+ gpuBuffer,
64
+ cpuData: new Float32Array(poolSize * this.INSTANCE_STRIDE),
65
+ capacity: poolSize,
66
+ count: 0
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Return a buffer to the pool
72
+ * @param {Object} bufferInfo - Buffer info to return
73
+ */
74
+ _releaseBuffer(bufferInfo) {
75
+ const pool = this._bufferPool.get(bufferInfo.capacity)
76
+ if (pool) {
77
+ bufferInfo.count = 0
78
+ pool.push(bufferInfo)
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Build instance batches from visible entities
84
+ *
85
+ * @param {Map<string, Array>} groups - Groups from CullingSystem.groupByModel()
86
+ * @param {AssetManager} assetManager - Asset manager for mesh lookup
87
+ * @returns {Map<string, Object>} Batches ready for rendering
88
+ */
89
+ buildBatches(groups, assetManager) {
90
+ const { device } = this.engine
91
+
92
+ // Release old batches back to pool
93
+ for (const [modelId, batch] of this._batches) {
94
+ if (batch.buffer) {
95
+ this._releaseBuffer(batch.buffer)
96
+ }
97
+ }
98
+ this._batches.clear()
99
+
100
+ // Build new batches
101
+ for (const [modelId, entities] of groups) {
102
+ // Get asset
103
+ const asset = assetManager.get(modelId)
104
+ if (!asset?.ready) continue
105
+
106
+ // Get or create buffer
107
+ const buffer = this._getBuffer(entities.length)
108
+
109
+ // Fill instance data
110
+ let offset = 0
111
+ for (const item of entities) {
112
+ const entity = item.entity
113
+
114
+ // Copy model matrix (16 floats)
115
+ buffer.cpuData.set(entity._matrix, offset)
116
+
117
+ // Copy position + radius (4 floats)
118
+ buffer.cpuData[offset + 16] = entity._bsphere.center[0]
119
+ buffer.cpuData[offset + 17] = entity._bsphere.center[1]
120
+ buffer.cpuData[offset + 18] = entity._bsphere.center[2]
121
+ buffer.cpuData[offset + 19] = entity._bsphere.radius
122
+
123
+ // Copy UV transform (4 floats): offset.xy, scale.xy
124
+ // Default: no transform (offset 0,0, scale 1,1)
125
+ const uvTransform = entity._uvTransform || [0, 0, 1, 1]
126
+ buffer.cpuData[offset + 20] = uvTransform[0]
127
+ buffer.cpuData[offset + 21] = uvTransform[1]
128
+ buffer.cpuData[offset + 22] = uvTransform[2]
129
+ buffer.cpuData[offset + 23] = uvTransform[3]
130
+
131
+ // Copy color tint (4 floats): r, g, b, a
132
+ // Default: white (no tint)
133
+ const color = entity.color || [1, 1, 1, 1]
134
+ buffer.cpuData[offset + 24] = color[0]
135
+ buffer.cpuData[offset + 25] = color[1]
136
+ buffer.cpuData[offset + 26] = color[2]
137
+ buffer.cpuData[offset + 27] = color[3]
138
+
139
+ offset += this.INSTANCE_STRIDE
140
+ }
141
+
142
+ buffer.count = entities.length
143
+
144
+ // Upload to GPU
145
+ device.queue.writeBuffer(
146
+ buffer.gpuBuffer,
147
+ 0,
148
+ buffer.cpuData,
149
+ 0,
150
+ entities.length * this.INSTANCE_STRIDE
151
+ )
152
+
153
+ // Store batch
154
+ this._batches.set(modelId, {
155
+ modelId,
156
+ mesh: asset.mesh,
157
+ geometry: asset.geometry,
158
+ material: asset.material,
159
+ skin: asset.skin,
160
+ hasSkin: asset.hasSkin,
161
+ buffer,
162
+ instanceCount: entities.length,
163
+ entities
164
+ })
165
+ }
166
+
167
+ return this._batches
168
+ }
169
+
170
+ /**
171
+ * Build instance batches for skinned meshes grouped by animation
172
+ *
173
+ * @param {Map<string, Array>} groups - Groups from CullingSystem.groupByModelAndAnimation()
174
+ * @param {AssetManager} assetManager - Asset manager
175
+ * @returns {Map<string, Object>} Batches with animation info
176
+ */
177
+ buildSkinnedBatches(groups, assetManager) {
178
+ const { device } = this.engine
179
+ const batches = new Map()
180
+
181
+ for (const [key, entities] of groups) {
182
+ // Parse key: "modelId|animation|phase"
183
+ const parts = key.split('|')
184
+ const modelId = parts[0] + (parts.length > 1 ? '|' + parts[1].split('|')[0] : '')
185
+
186
+ // For skinned meshes, we need to check the base modelId
187
+ const baseModelId = key.includes('|') ? key.split('|').slice(0, 2).join('|') : key
188
+
189
+ // Try to find the asset
190
+ let asset = null
191
+ for (const [assetKey, assetValue] of Object.entries(assetManager.assets)) {
192
+ if (assetKey.includes('|') && assetKey.startsWith(key.split('|')[0])) {
193
+ if (assetValue.ready) {
194
+ asset = assetValue
195
+ break
196
+ }
197
+ }
198
+ }
199
+
200
+ if (!asset?.ready) continue
201
+
202
+ // Get or create buffer
203
+ const buffer = this._getBuffer(entities.length)
204
+
205
+ // Fill instance data
206
+ let offset = 0
207
+ for (const item of entities) {
208
+ const entity = item.entity
209
+
210
+ buffer.cpuData.set(entity._matrix, offset)
211
+ buffer.cpuData[offset + 16] = entity._bsphere.center[0]
212
+ buffer.cpuData[offset + 17] = entity._bsphere.center[1]
213
+ buffer.cpuData[offset + 18] = entity._bsphere.center[2]
214
+ buffer.cpuData[offset + 19] = entity._bsphere.radius
215
+
216
+ // Copy UV transform (4 floats): offset.xy, scale.xy
217
+ const uvTransform = entity._uvTransform || [0, 0, 1, 1]
218
+ buffer.cpuData[offset + 20] = uvTransform[0]
219
+ buffer.cpuData[offset + 21] = uvTransform[1]
220
+ buffer.cpuData[offset + 22] = uvTransform[2]
221
+ buffer.cpuData[offset + 23] = uvTransform[3]
222
+
223
+ // Copy color tint (4 floats): r, g, b, a
224
+ const color = entity.color || [1, 1, 1, 1]
225
+ buffer.cpuData[offset + 24] = color[0]
226
+ buffer.cpuData[offset + 25] = color[1]
227
+ buffer.cpuData[offset + 26] = color[2]
228
+ buffer.cpuData[offset + 27] = color[3]
229
+
230
+ offset += this.INSTANCE_STRIDE
231
+ }
232
+
233
+ buffer.count = entities.length
234
+
235
+ // Upload to GPU
236
+ device.queue.writeBuffer(
237
+ buffer.gpuBuffer,
238
+ 0,
239
+ buffer.cpuData,
240
+ 0,
241
+ entities.length * this.INSTANCE_STRIDE
242
+ )
243
+
244
+ // Extract animation info from key
245
+ const animation = parts.length > 2 ? parts[1] : null
246
+ const phase = parts.length > 3 ? parseFloat(parts[2]) : 0
247
+
248
+ batches.set(key, {
249
+ modelId: baseModelId,
250
+ mesh: asset.mesh,
251
+ geometry: asset.geometry,
252
+ material: asset.material,
253
+ skin: asset.skin,
254
+ hasSkin: asset.hasSkin,
255
+ animation,
256
+ phase,
257
+ buffer,
258
+ instanceCount: entities.length,
259
+ entities
260
+ })
261
+ }
262
+
263
+ return batches
264
+ }
265
+
266
+ /**
267
+ * Get current batches
268
+ */
269
+ getBatches() {
270
+ return this._batches
271
+ }
272
+
273
+ /**
274
+ * Get instance buffer layout for pipeline creation
275
+ */
276
+ static getBufferLayout() {
277
+ return {
278
+ arrayStride: 112, // 28 floats * 4 bytes
279
+ stepMode: 'instance',
280
+ attributes: [
281
+ { format: "float32x4", offset: 0, shaderLocation: 6 }, // matrix column 0
282
+ { format: "float32x4", offset: 16, shaderLocation: 7 }, // matrix column 1
283
+ { format: "float32x4", offset: 32, shaderLocation: 8 }, // matrix column 2
284
+ { format: "float32x4", offset: 48, shaderLocation: 9 }, // matrix column 3
285
+ { format: "float32x4", offset: 64, shaderLocation: 10 }, // position + radius
286
+ { format: "float32x4", offset: 80, shaderLocation: 11 }, // uvTransform (offset.xy, scale.xy)
287
+ { format: "float32x4", offset: 96, shaderLocation: 12 }, // color (r, g, b, a)
288
+ ]
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Clear all batches and return buffers to pool
294
+ */
295
+ clear() {
296
+ for (const [modelId, batch] of this._batches) {
297
+ if (batch.buffer) {
298
+ this._releaseBuffer(batch.buffer)
299
+ }
300
+ }
301
+ this._batches.clear()
302
+ }
303
+
304
+ /**
305
+ * Destroy all buffers
306
+ */
307
+ destroy() {
308
+ this.clear()
309
+ for (const [size, pool] of this._bufferPool) {
310
+ for (const buffer of pool) {
311
+ buffer.gpuBuffer.destroy()
312
+ }
313
+ }
314
+ this._bufferPool.clear()
315
+ }
316
+
317
+ /**
318
+ * Get statistics about buffer usage
319
+ */
320
+ getStats() {
321
+ let pooledBuffers = 0
322
+ let pooledCapacity = 0
323
+ for (const [size, pool] of this._bufferPool) {
324
+ pooledBuffers += pool.length
325
+ pooledCapacity += pool.length * size
326
+ }
327
+
328
+ let activeBuffers = this._batches.size
329
+ let activeInstances = 0
330
+ for (const [modelId, batch] of this._batches) {
331
+ activeInstances += batch.instanceCount
332
+ }
333
+
334
+ return {
335
+ pooledBuffers,
336
+ pooledCapacity,
337
+ activeBuffers,
338
+ activeInstances
339
+ }
340
+ }
341
+ }
342
+
343
+ export { InstanceManager }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * ParticleEmitter - Configuration class for particle spawning behavior
3
+ *
4
+ * Defines how particles are spawned, their initial properties, physics behavior,
5
+ * and rendering options. Used by ParticleSystem to create and manage particles.
6
+ */
7
+
8
+ let _emitterUID = 1
9
+
10
+ class ParticleEmitter {
11
+ constructor(config = {}) {
12
+ this.uid = _emitterUID++
13
+ this.name = config.name || `emitter_${this.uid}`
14
+
15
+ // Apply behavior preset FIRST so user config can override
16
+ const behavior = config.behavior || 'default'
17
+ this.behavior = behavior
18
+
19
+ // Get preset defaults (empty for 'default')
20
+ const presetDefaults = this._getPresetDefaults(behavior)
21
+
22
+ // Merge: defaults -> preset -> user config
23
+ const merged = { ...presetDefaults, ...config }
24
+
25
+ // === Spawning Configuration ===
26
+ // Emitter position in world space
27
+ this.position = merged.position || [0, 0, 0]
28
+
29
+ // Spawn volume: 'point' | 'box' | 'sphere'
30
+ this.volume = merged.volume || 'point'
31
+
32
+ // Size of spawn volume (for box: [x,y,z], for sphere: [radius])
33
+ this.volumeSize = merged.volumeSize || [1, 1, 1]
34
+
35
+ // Particles spawned per second (0 = manual spawn only)
36
+ this.spawnRate = merged.spawnRate ?? 10
37
+
38
+ // Initial burst of particles on activation
39
+ this.spawnBurst = merged.spawnBurst ?? 0
40
+
41
+ // Maximum particles in this emitter's pool
42
+ this.maxParticles = merged.maxParticles ?? 1000
43
+
44
+ // === Particle Initial Properties ===
45
+ // Lifetime range [min, max] in seconds
46
+ this.lifetime = merged.lifetime || [1.0, 2.0]
47
+
48
+ // Initial speed range [min, max]
49
+ this.speed = merged.speed || [1.0, 3.0]
50
+
51
+ // Emission direction (normalized) - particles shoot this way
52
+ this.direction = merged.direction || [0, 1, 0]
53
+
54
+ // Cone spread (0 = tight beam, 1 = hemisphere)
55
+ this.spread = merged.spread ?? 0.5
56
+
57
+ // Size over lifetime [start, end]
58
+ this.size = merged.size || [0.5, 0.1]
59
+
60
+ // Particle color tint [r, g, b, a]
61
+ this.color = merged.color || [1, 1, 1, 1]
62
+
63
+ // Fade timing in seconds
64
+ this.fadeIn = merged.fadeIn ?? 0.1
65
+ this.fadeOut = merged.fadeOut ?? 0.3
66
+
67
+ // === Physics ===
68
+ // Gravity acceleration [x, y, z]
69
+ this.gravity = merged.gravity || [0, -9.8, 0]
70
+
71
+ // Air resistance (0 = none, 1 = high drag)
72
+ this.drag = merged.drag ?? 0.1
73
+
74
+ // Random turbulence strength
75
+ this.turbulence = merged.turbulence ?? 0.0
76
+
77
+ // === Rendering ===
78
+ // Texture URL for particles
79
+ this.texture = merged.texture || null
80
+
81
+ // Frames per row for sprite sheets
82
+ this.framesPerRow = merged.framesPerRow ?? 1
83
+
84
+ // Total frames in sprite sheet (optional, for animation)
85
+ this.totalFrames = merged.totalFrames ?? null
86
+
87
+ // Animation FPS (0 = no animation)
88
+ this.animationFPS = merged.animationFPS ?? 0
89
+
90
+ // Blend mode: 'additive' | 'alpha'
91
+ this.blendMode = merged.blendMode || 'additive'
92
+
93
+ // Depth offset to prevent z-fighting
94
+ this.zOffset = merged.zOffset ?? 0.01
95
+
96
+ // Soft particle depth fade distance (meters)
97
+ this.softness = merged.softness ?? 0.25
98
+
99
+ // Rotation speed in radians per second
100
+ this.rotationSpeed = merged.rotationSpeed ?? 0.5
101
+
102
+ // Whether particles receive basic lighting
103
+ this.lit = merged.lit ?? false
104
+
105
+ // Emissive multiplier (1.0 = normal, >1 = brighter for fire/sparks)
106
+ this.emissive = merged.emissive ?? 1.0
107
+
108
+ // === Runtime State ===
109
+ this.enabled = true
110
+ this.spawnAccumulator = 0 // Fractional particle spawn buildup
111
+ this.totalSpawned = 0
112
+ this.aliveCount = 0
113
+
114
+ // GPU buffer references (set by ParticleSystem)
115
+ this.particleBuffer = null
116
+ this.indirectBuffer = null
117
+ }
118
+
119
+ /**
120
+ * Get behavior preset defaults
121
+ * @param {string} behavior - Preset name
122
+ * @returns {Object} Default values for the preset
123
+ */
124
+ _getPresetDefaults(behavior) {
125
+ const presets = {
126
+ smoke: {
127
+ lifetime: [2.0, 4.0],
128
+ speed: [0.5, 1.5],
129
+ direction: [0, 1, 0],
130
+ spread: 0.3,
131
+ size: [0.3, 2.0],
132
+ color: [0.5, 0.5, 0.5, 0.6],
133
+ fadeIn: 0.2,
134
+ fadeOut: 1.0,
135
+ gravity: [0, 0.5, 0],
136
+ drag: 0.3,
137
+ turbulence: 0.3,
138
+ blendMode: 'alpha',
139
+ rotationSpeed: 0.3,
140
+ softness: 0.25
141
+ },
142
+ fire: {
143
+ lifetime: [0.3, 0.8],
144
+ speed: [2.0, 4.0],
145
+ direction: [0, 1, 0],
146
+ spread: 0.2,
147
+ size: [0.5, 0.1],
148
+ color: [1.0, 0.6, 0.2, 1.0],
149
+ fadeIn: 0.05,
150
+ fadeOut: 0.3,
151
+ gravity: [0, 2.0, 0],
152
+ drag: 0.1,
153
+ turbulence: 0.5,
154
+ blendMode: 'additive',
155
+ emissive: 3.0, // Bright fire
156
+ lit: false // Fire is self-illuminating
157
+ },
158
+ sparks: {
159
+ lifetime: [0.5, 1.5],
160
+ speed: [5.0, 10.0],
161
+ direction: [0, 1, 0],
162
+ spread: 0.8,
163
+ size: [0.1, 0.05],
164
+ color: [1.0, 0.8, 0.3, 1.0],
165
+ fadeIn: 0.0,
166
+ fadeOut: 0.5,
167
+ gravity: [0, -9.8, 0],
168
+ drag: 0.05,
169
+ turbulence: 0.1,
170
+ blendMode: 'additive',
171
+ emissive: 5.0, // Very bright sparks
172
+ lit: false // Sparks are self-illuminating
173
+ },
174
+ fog: {
175
+ lifetime: [5.0, 10.0],
176
+ speed: [0.1, 0.3],
177
+ direction: [0, 0, 0],
178
+ spread: 1.0,
179
+ size: [2.0, 3.0],
180
+ color: [0.8, 0.8, 0.9, 0.3],
181
+ fadeIn: 1.0,
182
+ fadeOut: 2.0,
183
+ gravity: [0, 0, 0],
184
+ drag: 0.5,
185
+ turbulence: 0.1,
186
+ blendMode: 'alpha',
187
+ volume: 'box'
188
+ }
189
+ }
190
+ return presets[behavior] || {}
191
+ }
192
+
193
+ /**
194
+ * Get a random value within a [min, max] range
195
+ * @param {number[]} range - [min, max]
196
+ * @param {number} seed - Random seed (0-1)
197
+ * @returns {number}
198
+ */
199
+ static randomInRange(range, seed) {
200
+ return range[0] + (range[1] - range[0]) * seed
201
+ }
202
+
203
+ /**
204
+ * Get a random point within the spawn volume
205
+ * @param {number[]} seeds - [seed1, seed2, seed3] random values (0-1)
206
+ * @returns {number[]} - [x, y, z] position
207
+ */
208
+ getSpawnPosition(seeds) {
209
+ const [s1, s2, s3] = seeds
210
+ const [px, py, pz] = this.position
211
+
212
+ switch (this.volume) {
213
+ case 'point':
214
+ return [px, py, pz]
215
+
216
+ case 'box': {
217
+ const [sx, sy, sz] = this.volumeSize
218
+ return [
219
+ px + (s1 - 0.5) * sx,
220
+ py + (s2 - 0.5) * sy,
221
+ pz + (s3 - 0.5) * sz
222
+ ]
223
+ }
224
+
225
+ case 'sphere': {
226
+ // Uniform distribution within sphere
227
+ const radius = this.volumeSize[0] * Math.cbrt(s1)
228
+ const theta = s2 * 2 * Math.PI
229
+ const phi = Math.acos(2 * s3 - 1)
230
+ return [
231
+ px + radius * Math.sin(phi) * Math.cos(theta),
232
+ py + radius * Math.cos(phi),
233
+ pz + radius * Math.sin(phi) * Math.sin(theta)
234
+ ]
235
+ }
236
+
237
+ default:
238
+ return [px, py, pz]
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Get a random emission direction with spread
244
+ * @param {number[]} seeds - [seed1, seed2] random values (0-1)
245
+ * @returns {number[]} - [x, y, z] normalized direction
246
+ */
247
+ getEmissionDirection(seeds) {
248
+ const [s1, s2] = seeds
249
+ const [dx, dy, dz] = this.direction
250
+
251
+ if (this.spread <= 0) {
252
+ // No spread - use exact direction
253
+ return [dx, dy, dz]
254
+ }
255
+
256
+ // Create cone around direction
257
+ // Map spread to cone half-angle (0=0°, 1=90°)
258
+ const halfAngle = this.spread * Math.PI * 0.5
259
+ const theta = s1 * 2 * Math.PI
260
+ const cosAngle = Math.cos(halfAngle * s2)
261
+ const sinAngle = Math.sqrt(1 - cosAngle * cosAngle)
262
+
263
+ // Random direction in cone around +Y
264
+ let rx = sinAngle * Math.cos(theta)
265
+ let ry = cosAngle
266
+ let rz = sinAngle * Math.sin(theta)
267
+
268
+ // Rotate from +Y to emission direction
269
+ // Use simplified rotation (assumes direction is normalized)
270
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz)
271
+ if (len < 0.001) return [rx, ry, rz] // No direction
272
+
273
+ const ndx = dx / len
274
+ const ndy = dy / len
275
+ const ndz = dz / len
276
+
277
+ // If direction is close to +Y, return as-is
278
+ if (ndy > 0.999) return [rx, ry, rz]
279
+ if (ndy < -0.999) return [rx, -ry, rz] // Flip for -Y
280
+
281
+ // Build rotation matrix from +Y to direction
282
+ const ax = -ndz, az = ndx // Cross product Y × dir (simplified)
283
+ const alen = Math.sqrt(ax * ax + az * az)
284
+ const nax = ax / alen, naz = az / alen
285
+ const c = ndy, s = Math.sqrt(1 - c * c)
286
+
287
+ // Rodrigues' rotation formula (simplified for rotation around axis in XZ plane)
288
+ const outX = rx * (c + nax * nax * (1 - c)) + ry * (-naz * s) + rz * (nax * naz * (1 - c))
289
+ const outY = rx * (naz * s) + ry * c + rz * (-nax * s)
290
+ const outZ = rx * (naz * nax * (1 - c)) + ry * (nax * s) + rz * (c + naz * naz * (1 - c))
291
+
292
+ return [outX, outY, outZ]
293
+ }
294
+
295
+ /**
296
+ * Clone this emitter with optional overrides
297
+ * @param {Object} overrides - Properties to override
298
+ * @returns {ParticleEmitter}
299
+ */
300
+ clone(overrides = {}) {
301
+ const config = {
302
+ ...this.toJSON(),
303
+ ...overrides,
304
+ _presetApplied: true // Don't re-apply preset
305
+ }
306
+ return new ParticleEmitter(config)
307
+ }
308
+
309
+ /**
310
+ * Serialize emitter configuration to JSON
311
+ * @returns {Object}
312
+ */
313
+ toJSON() {
314
+ return {
315
+ name: this.name,
316
+ position: [...this.position],
317
+ volume: this.volume,
318
+ volumeSize: [...this.volumeSize],
319
+ spawnRate: this.spawnRate,
320
+ spawnBurst: this.spawnBurst,
321
+ maxParticles: this.maxParticles,
322
+ lifetime: [...this.lifetime],
323
+ speed: [...this.speed],
324
+ direction: [...this.direction],
325
+ spread: this.spread,
326
+ size: [...this.size],
327
+ color: [...this.color],
328
+ fadeIn: this.fadeIn,
329
+ fadeOut: this.fadeOut,
330
+ gravity: [...this.gravity],
331
+ drag: this.drag,
332
+ turbulence: this.turbulence,
333
+ texture: this.texture,
334
+ framesPerRow: this.framesPerRow,
335
+ totalFrames: this.totalFrames,
336
+ animationFPS: this.animationFPS,
337
+ blendMode: this.blendMode,
338
+ zOffset: this.zOffset,
339
+ softness: this.softness,
340
+ rotationSpeed: this.rotationSpeed,
341
+ lit: this.lit,
342
+ emissive: this.emissive,
343
+ behavior: this.behavior,
344
+ enabled: this.enabled
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Create emitter from JSON
350
+ * @param {Object} json - Serialized configuration
351
+ * @returns {ParticleEmitter}
352
+ */
353
+ static fromJSON(json) {
354
+ return new ParticleEmitter({ ...json, _presetApplied: true })
355
+ }
356
+ }
357
+
358
+ export { ParticleEmitter }