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,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 }
|