topazcube 0.1.30 → 0.1.33
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 +18200 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +18183 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +94 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +71 -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} +173 -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 +572 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +61 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +643 -0
- package/src/renderer/Renderer.js +1324 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +359 -0
- package/src/renderer/core/CullingSystem.js +307 -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 +546 -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 +2064 -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 +417 -0
- package/src/renderer/rendering/passes/FogPass.js +419 -0
- package/src/renderer/rendering/passes/GBufferPass.js +706 -0
- package/src/renderer/rendering/passes/HiZPass.js +714 -0
- package/src/renderer/rendering/passes/LightingPass.js +739 -0
- package/src/renderer/rendering/passes/ParticlePass.js +835 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +282 -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 +265 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +296 -0
- package/src/renderer/rendering/passes/ShadowPass.js +1822 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -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/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +550 -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 +525 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +272 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +76 -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/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -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,835 @@
|
|
|
1
|
+
import { BasePass } from "./BasePass.js"
|
|
2
|
+
import { Texture } from "../../Texture.js"
|
|
3
|
+
|
|
4
|
+
import particleSimulateWGSL from "../shaders/particle_simulate.wgsl"
|
|
5
|
+
import particleRenderWGSL from "../shaders/particle_render.wgsl"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ParticlePass - GPU particle simulation and rendering
|
|
9
|
+
*
|
|
10
|
+
* Handles:
|
|
11
|
+
* - Compute shader dispatch for particle spawning and simulation
|
|
12
|
+
* - Forward rendering with additive or alpha blending
|
|
13
|
+
* - Soft particle depth fade
|
|
14
|
+
* - Billboard rendering using camera vectors
|
|
15
|
+
*/
|
|
16
|
+
class ParticlePass extends BasePass {
|
|
17
|
+
constructor(engine = null) {
|
|
18
|
+
super('Particles', engine)
|
|
19
|
+
|
|
20
|
+
// Compute pipelines
|
|
21
|
+
this.spawnPipeline = null
|
|
22
|
+
this.simulatePipeline = null
|
|
23
|
+
this.resetPipeline = null
|
|
24
|
+
|
|
25
|
+
// Render pipelines (by blend mode)
|
|
26
|
+
this.renderPipelineAdditive = null
|
|
27
|
+
this.renderPipelineAlpha = null
|
|
28
|
+
|
|
29
|
+
// Bind groups
|
|
30
|
+
this.computeBindGroupLayout = null
|
|
31
|
+
this.renderBindGroupLayout = null
|
|
32
|
+
|
|
33
|
+
// Uniform buffers
|
|
34
|
+
this.simulationUniformBuffer = null
|
|
35
|
+
this.renderUniformBuffer = null
|
|
36
|
+
this.emitterSettingsBuffer = null
|
|
37
|
+
this.emitterRenderSettingsBuffer = null // For lit + emissive per emitter
|
|
38
|
+
|
|
39
|
+
// References
|
|
40
|
+
this.particleSystem = null
|
|
41
|
+
this.gbuffer = null
|
|
42
|
+
this.outputTexture = null
|
|
43
|
+
this.shadowPass = null // For shadow maps
|
|
44
|
+
this.environmentMap = null // For IBL
|
|
45
|
+
this.environmentEncoding = 0 // 0=equirect, 1=octahedral
|
|
46
|
+
this.lightingPass = null // For point/spot lights buffer
|
|
47
|
+
|
|
48
|
+
// Per-emitter texture cache
|
|
49
|
+
this._textureCache = new Map() // textureUrl -> {texture, bindGroup}
|
|
50
|
+
|
|
51
|
+
// Placeholder resources
|
|
52
|
+
this._placeholderTexture = null
|
|
53
|
+
this._placeholderSampler = null
|
|
54
|
+
|
|
55
|
+
// Frame timing
|
|
56
|
+
this._lastTime = 0
|
|
57
|
+
this._deltaTime = 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set particle system reference
|
|
62
|
+
*/
|
|
63
|
+
setParticleSystem(particleSystem) {
|
|
64
|
+
this.particleSystem = particleSystem
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set GBuffer for depth testing
|
|
69
|
+
*/
|
|
70
|
+
setGBuffer(gbuffer) {
|
|
71
|
+
this.gbuffer = gbuffer
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set output texture to render onto
|
|
76
|
+
*/
|
|
77
|
+
setOutputTexture(texture) {
|
|
78
|
+
this.outputTexture = texture
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set shadow pass for accessing shadow maps
|
|
83
|
+
*/
|
|
84
|
+
setShadowPass(shadowPass) {
|
|
85
|
+
this.shadowPass = shadowPass
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set environment map for IBL lighting
|
|
90
|
+
* @param {Texture} envMap - Environment map texture
|
|
91
|
+
* @param {number} encoding - 0=equirectangular, 1=octahedral
|
|
92
|
+
*/
|
|
93
|
+
setEnvironmentMap(envMap, encoding = 0) {
|
|
94
|
+
this.environmentMap = envMap
|
|
95
|
+
this.environmentEncoding = encoding
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set lighting pass for accessing point/spot lights buffer
|
|
100
|
+
* @param {LightingPass} lightingPass
|
|
101
|
+
*/
|
|
102
|
+
setLightingPass(lightingPass) {
|
|
103
|
+
this.lightingPass = lightingPass
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async _init() {
|
|
107
|
+
const { device } = this.engine
|
|
108
|
+
|
|
109
|
+
// Create uniform buffers
|
|
110
|
+
// SimulationUniforms: dt, time, maxParticles, emitterCount + lighting params
|
|
111
|
+
// dt(f32) + time(f32) + maxParticles(u32) + emitterCount(u32) = 16 bytes
|
|
112
|
+
// cameraPosition(vec3f) + shadowBias(f32) = 16 bytes
|
|
113
|
+
// lightDir(vec3f) + shadowStrength(f32) = 16 bytes
|
|
114
|
+
// lightColor(vec4f) = 16 bytes
|
|
115
|
+
// ambientColor(vec4f) = 16 bytes
|
|
116
|
+
// cascadeSizes(vec4f) = 16 bytes
|
|
117
|
+
// lightCount(u32) + pad(3xu32) = 16 bytes
|
|
118
|
+
// Total: 112 bytes
|
|
119
|
+
this.simulationUniformBuffer = device.createBuffer({
|
|
120
|
+
size: 112,
|
|
121
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
122
|
+
label: 'Particle Simulation Uniforms'
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
this.renderUniformBuffer = device.createBuffer({
|
|
126
|
+
size: 368, // ParticleUniforms struct (increased for lighting + IBL + light count)
|
|
127
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
128
|
+
label: 'Particle Render Uniforms'
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Emitter settings buffer: 16 emitters * 48 bytes each = 768 bytes
|
|
132
|
+
this.emitterSettingsBuffer = device.createBuffer({
|
|
133
|
+
size: 16 * 48,
|
|
134
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
135
|
+
label: 'Particle Emitter Settings'
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Emitter render settings buffer: 16 emitters * 16 bytes each = 256 bytes
|
|
139
|
+
// Contains: lit, emissive, softness, zOffset per emitter
|
|
140
|
+
this.emitterRenderSettingsBuffer = device.createBuffer({
|
|
141
|
+
size: 16 * 16,
|
|
142
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
143
|
+
label: 'Particle Emitter Render Settings'
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Create placeholder resources
|
|
147
|
+
this._placeholderTexture = await Texture.fromRGBA(this.engine, 1, 1, 1, 1)
|
|
148
|
+
this._placeholderSampler = device.createSampler({
|
|
149
|
+
magFilter: 'linear',
|
|
150
|
+
minFilter: 'linear',
|
|
151
|
+
mipmapFilter: 'linear'
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Placeholder comparison sampler for shadow sampling
|
|
155
|
+
this._placeholderComparisonSampler = device.createSampler({
|
|
156
|
+
compare: 'less',
|
|
157
|
+
magFilter: 'linear',
|
|
158
|
+
minFilter: 'linear'
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Placeholder depth texture array for shadow maps (when shadows disabled)
|
|
162
|
+
this._placeholderDepthTexture = device.createTexture({
|
|
163
|
+
size: [1, 1, 1],
|
|
164
|
+
format: 'depth32float',
|
|
165
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
166
|
+
})
|
|
167
|
+
this._placeholderDepthView = this._placeholderDepthTexture.createView({
|
|
168
|
+
dimension: '2d-array',
|
|
169
|
+
arrayLayerCount: 1
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// Placeholder buffer for cascade matrices (3 mat4 = 192 bytes)
|
|
173
|
+
this._placeholderMatricesBuffer = device.createBuffer({
|
|
174
|
+
size: 192,
|
|
175
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
176
|
+
label: 'Placeholder Cascade Matrices'
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Placeholder lights buffer (64 lights * 64 bytes each = 4096 bytes)
|
|
180
|
+
// Light struct: enabled(u32) + position(vec3f) + color(vec4f) + direction(vec3f) + geom(vec4f) + shadowIndex(i32)
|
|
181
|
+
this._placeholderLightsBuffer = device.createBuffer({
|
|
182
|
+
size: 64 * 64,
|
|
183
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
184
|
+
label: 'Placeholder Lights Buffer'
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Placeholder spot shadow atlas (1x1 depth texture)
|
|
188
|
+
this._placeholderSpotShadowTexture = device.createTexture({
|
|
189
|
+
size: [1, 1],
|
|
190
|
+
format: 'depth32float',
|
|
191
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
192
|
+
})
|
|
193
|
+
this._placeholderSpotShadowView = this._placeholderSpotShadowTexture.createView()
|
|
194
|
+
|
|
195
|
+
// Placeholder spot shadow matrices buffer (8 mat4 = 512 bytes)
|
|
196
|
+
this._placeholderSpotMatricesBuffer = device.createBuffer({
|
|
197
|
+
size: 8 * 64,
|
|
198
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
199
|
+
label: 'Placeholder Spot Shadow Matrices'
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Create compute pipelines
|
|
203
|
+
await this._createComputePipelines()
|
|
204
|
+
|
|
205
|
+
// Create render pipelines
|
|
206
|
+
await this._createRenderPipelines()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async _createComputePipelines() {
|
|
210
|
+
const { device } = this.engine
|
|
211
|
+
|
|
212
|
+
// Compute shader module
|
|
213
|
+
const computeModule = device.createShaderModule({
|
|
214
|
+
label: 'Particle Compute Shader',
|
|
215
|
+
code: particleSimulateWGSL
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// Bind group layout for compute (includes lighting bindings for shadow sampling)
|
|
219
|
+
this.computeBindGroupLayout = device.createBindGroupLayout({
|
|
220
|
+
label: 'Particle Compute BindGroup Layout',
|
|
221
|
+
entries: [
|
|
222
|
+
// Core bindings
|
|
223
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
|
|
224
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
|
225
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
|
226
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
|
227
|
+
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
|
228
|
+
// Lighting bindings
|
|
229
|
+
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // emitterRenderSettings
|
|
230
|
+
{ binding: 6, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'depth', viewDimension: '2d-array' } }, // shadowMapArray
|
|
231
|
+
{ binding: 7, visibility: GPUShaderStage.COMPUTE, sampler: { type: 'comparison' } }, // shadowSampler
|
|
232
|
+
{ binding: 8, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // cascadeMatrices
|
|
233
|
+
{ binding: 9, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // lights
|
|
234
|
+
{ binding: 10, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'depth' } }, // spotShadowAtlas
|
|
235
|
+
{ binding: 11, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // spotMatrices
|
|
236
|
+
]
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const computePipelineLayout = device.createPipelineLayout({
|
|
240
|
+
bindGroupLayouts: [this.computeBindGroupLayout]
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// Spawn pipeline
|
|
244
|
+
this.spawnPipeline = await device.createComputePipelineAsync({
|
|
245
|
+
label: 'Particle Spawn Pipeline',
|
|
246
|
+
layout: computePipelineLayout,
|
|
247
|
+
compute: {
|
|
248
|
+
module: computeModule,
|
|
249
|
+
entryPoint: 'spawn'
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Simulate pipeline
|
|
254
|
+
this.simulatePipeline = await device.createComputePipelineAsync({
|
|
255
|
+
label: 'Particle Simulate Pipeline',
|
|
256
|
+
layout: computePipelineLayout,
|
|
257
|
+
compute: {
|
|
258
|
+
module: computeModule,
|
|
259
|
+
entryPoint: 'simulate'
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Reset counters pipeline
|
|
264
|
+
this.resetPipeline = await device.createComputePipelineAsync({
|
|
265
|
+
label: 'Particle Reset Pipeline',
|
|
266
|
+
layout: computePipelineLayout,
|
|
267
|
+
compute: {
|
|
268
|
+
module: computeModule,
|
|
269
|
+
entryPoint: 'resetCounters'
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async _createRenderPipelines() {
|
|
275
|
+
const { device } = this.engine
|
|
276
|
+
|
|
277
|
+
// Render shader module
|
|
278
|
+
const renderModule = device.createShaderModule({
|
|
279
|
+
label: 'Particle Render Shader',
|
|
280
|
+
code: particleRenderWGSL
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Bind group layout for rendering
|
|
284
|
+
this.renderBindGroupLayout = device.createBindGroupLayout({
|
|
285
|
+
label: 'Particle Render BindGroup Layout',
|
|
286
|
+
entries: [
|
|
287
|
+
// Uniforms
|
|
288
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
289
|
+
// Particle storage buffer (read only in render)
|
|
290
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
291
|
+
// Particle texture
|
|
292
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
293
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
294
|
+
// Depth texture for soft particles (GBuffer depth32float)
|
|
295
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
296
|
+
// Shadow map array (cascade shadows)
|
|
297
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth', viewDimension: '2d-array' } },
|
|
298
|
+
// Shadow comparison sampler
|
|
299
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
300
|
+
// Cascade matrices (storage buffer)
|
|
301
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
|
|
302
|
+
// Emitter render settings (lit, emissive, softness, zOffset)
|
|
303
|
+
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
|
|
304
|
+
// Environment map for IBL
|
|
305
|
+
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
306
|
+
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
307
|
+
// Point/spot lights buffer
|
|
308
|
+
{ binding: 11, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
|
|
309
|
+
// Spot shadow atlas
|
|
310
|
+
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
311
|
+
{ binding: 13, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
312
|
+
// Spot shadow matrices
|
|
313
|
+
{ binding: 14, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
|
|
314
|
+
]
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const renderPipelineLayout = device.createPipelineLayout({
|
|
318
|
+
bindGroupLayouts: [this.renderBindGroupLayout]
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// Additive blend pipeline
|
|
322
|
+
this.renderPipelineAdditive = await device.createRenderPipelineAsync({
|
|
323
|
+
label: 'Particle Render Pipeline (Additive)',
|
|
324
|
+
layout: renderPipelineLayout,
|
|
325
|
+
vertex: {
|
|
326
|
+
module: renderModule,
|
|
327
|
+
entryPoint: 'vertexMain',
|
|
328
|
+
buffers: [] // All data from storage buffer
|
|
329
|
+
},
|
|
330
|
+
fragment: {
|
|
331
|
+
module: renderModule,
|
|
332
|
+
entryPoint: 'fragmentMainAdditive',
|
|
333
|
+
targets: [{
|
|
334
|
+
format: 'rgba16float',
|
|
335
|
+
blend: {
|
|
336
|
+
color: {
|
|
337
|
+
srcFactor: 'one', // Additive: src + dst
|
|
338
|
+
dstFactor: 'one',
|
|
339
|
+
operation: 'add'
|
|
340
|
+
},
|
|
341
|
+
alpha: {
|
|
342
|
+
srcFactor: 'one',
|
|
343
|
+
dstFactor: 'one',
|
|
344
|
+
operation: 'add'
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}]
|
|
348
|
+
},
|
|
349
|
+
depthStencil: {
|
|
350
|
+
format: 'depth32float',
|
|
351
|
+
depthWriteEnabled: false, // No depth write for particles
|
|
352
|
+
depthCompare: 'less-equal', // Standard depth test with linear depth
|
|
353
|
+
},
|
|
354
|
+
primitive: {
|
|
355
|
+
topology: 'triangle-list',
|
|
356
|
+
cullMode: 'none',
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// Alpha blend pipeline
|
|
361
|
+
this.renderPipelineAlpha = await device.createRenderPipelineAsync({
|
|
362
|
+
label: 'Particle Render Pipeline (Alpha)',
|
|
363
|
+
layout: renderPipelineLayout,
|
|
364
|
+
vertex: {
|
|
365
|
+
module: renderModule,
|
|
366
|
+
entryPoint: 'vertexMain',
|
|
367
|
+
buffers: []
|
|
368
|
+
},
|
|
369
|
+
fragment: {
|
|
370
|
+
module: renderModule,
|
|
371
|
+
entryPoint: 'fragmentMainAlpha',
|
|
372
|
+
targets: [{
|
|
373
|
+
format: 'rgba16float',
|
|
374
|
+
blend: {
|
|
375
|
+
color: {
|
|
376
|
+
srcFactor: 'src-alpha',
|
|
377
|
+
dstFactor: 'one-minus-src-alpha',
|
|
378
|
+
operation: 'add'
|
|
379
|
+
},
|
|
380
|
+
alpha: {
|
|
381
|
+
srcFactor: 'one',
|
|
382
|
+
dstFactor: 'one-minus-src-alpha',
|
|
383
|
+
operation: 'add'
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}]
|
|
387
|
+
},
|
|
388
|
+
depthStencil: {
|
|
389
|
+
format: 'depth32float',
|
|
390
|
+
depthWriteEnabled: false,
|
|
391
|
+
depthCompare: 'less-equal', // Standard depth test with linear depth
|
|
392
|
+
},
|
|
393
|
+
primitive: {
|
|
394
|
+
topology: 'triangle-list',
|
|
395
|
+
cullMode: 'none',
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async _execute(context) {
|
|
401
|
+
const { device, canvas, stats } = this.engine
|
|
402
|
+
const { camera, time, mainLight } = context
|
|
403
|
+
|
|
404
|
+
if (!this.particleSystem || !this.outputTexture || !this.gbuffer) {
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Initialize particle system if needed
|
|
409
|
+
await this.particleSystem.init()
|
|
410
|
+
|
|
411
|
+
// Calculate delta time
|
|
412
|
+
const currentTime = time ?? (performance.now() / 1000)
|
|
413
|
+
this._deltaTime = this._lastTime > 0 ? currentTime - this._lastTime : 0.016
|
|
414
|
+
this._lastTime = currentTime
|
|
415
|
+
|
|
416
|
+
// Clamp delta time to prevent huge jumps
|
|
417
|
+
this._deltaTime = Math.min(this._deltaTime, 0.1)
|
|
418
|
+
|
|
419
|
+
// Update particle system (CPU side - spawn rate accumulation)
|
|
420
|
+
this.particleSystem.update(this._deltaTime)
|
|
421
|
+
|
|
422
|
+
const emitters = this.particleSystem.getActiveEmitters()
|
|
423
|
+
if (emitters.length === 0) {
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Create command encoder
|
|
428
|
+
const commandEncoder = device.createCommandEncoder({ label: 'Particle Pass' })
|
|
429
|
+
|
|
430
|
+
// === COMPUTE PHASE (includes lighting calculation) ===
|
|
431
|
+
await this._executeCompute(commandEncoder, emitters, camera, mainLight)
|
|
432
|
+
|
|
433
|
+
// === RENDER PHASE ===
|
|
434
|
+
await this._executeRender(commandEncoder, emitters, camera, mainLight)
|
|
435
|
+
|
|
436
|
+
// Submit
|
|
437
|
+
device.queue.submit([commandEncoder.finish()])
|
|
438
|
+
|
|
439
|
+
// Update stats
|
|
440
|
+
if (stats) {
|
|
441
|
+
stats.particleEmitters = emitters.length
|
|
442
|
+
stats.particleCount = this.particleSystem.getTotalParticleCount()
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async _executeCompute(commandEncoder, emitters, camera, mainLight) {
|
|
447
|
+
const { device } = this.engine
|
|
448
|
+
|
|
449
|
+
// Get spawn count BEFORE executing (executeSpawn clears the queue)
|
|
450
|
+
const spawnCount = Math.min(this.particleSystem._spawnQueue?.length || 0, 1000)
|
|
451
|
+
|
|
452
|
+
// Execute spawns - writes data to GPU buffers
|
|
453
|
+
this.particleSystem.executeSpawn(commandEncoder)
|
|
454
|
+
|
|
455
|
+
// Get lighting settings
|
|
456
|
+
const mainLightEnabled = mainLight?.enabled !== false
|
|
457
|
+
const mainLightIntensity = mainLight?.intensity ?? 1.0
|
|
458
|
+
const mainLightColor = mainLight?.color || [1.0, 0.95, 0.9]
|
|
459
|
+
const lightDir = mainLight?.direction || [-1, 1, -0.5]
|
|
460
|
+
|
|
461
|
+
// Normalize light direction
|
|
462
|
+
const lightDirLen = Math.sqrt(lightDir[0] * lightDir[0] + lightDir[1] * lightDir[1] + lightDir[2] * lightDir[2])
|
|
463
|
+
const normalizedLightDir = [
|
|
464
|
+
lightDir[0] / lightDirLen,
|
|
465
|
+
lightDir[1] / lightDirLen,
|
|
466
|
+
lightDir[2] / lightDirLen
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
// Get ambient and shadow settings
|
|
470
|
+
const ambientColor = this.settings?.ambient?.color || [0.1, 0.12, 0.15]
|
|
471
|
+
const ambientIntensity = this.settings?.ambient?.intensity ?? 0.3
|
|
472
|
+
const shadowConfig = this.settings?.shadow || {}
|
|
473
|
+
const shadowBias = shadowConfig.bias ?? 0.0005
|
|
474
|
+
const shadowStrength = shadowConfig.strength ?? 0.8
|
|
475
|
+
const cascadeSizes = shadowConfig.cascadeSizes || [10, 25, 100]
|
|
476
|
+
const lightCount = this.lightingPass?.lights?.length ?? 0
|
|
477
|
+
|
|
478
|
+
// Update simulation uniforms (28 floats = 112 bytes)
|
|
479
|
+
const uniformData = new Float32Array(28)
|
|
480
|
+
uniformData[0] = this._deltaTime // dt
|
|
481
|
+
uniformData[1] = this._lastTime // time
|
|
482
|
+
const uniformIntView = new Uint32Array(uniformData.buffer)
|
|
483
|
+
uniformIntView[2] = this.particleSystem.globalMaxParticles // maxParticles
|
|
484
|
+
uniformIntView[3] = emitters.length // emitterCount
|
|
485
|
+
// cameraPosition (vec3f) + shadowBias (f32) - floats 4-7
|
|
486
|
+
uniformData[4] = camera.position[0]
|
|
487
|
+
uniformData[5] = camera.position[1]
|
|
488
|
+
uniformData[6] = camera.position[2]
|
|
489
|
+
uniformData[7] = shadowBias
|
|
490
|
+
// lightDir (vec3f) + shadowStrength (f32) - floats 8-11
|
|
491
|
+
uniformData[8] = normalizedLightDir[0]
|
|
492
|
+
uniformData[9] = normalizedLightDir[1]
|
|
493
|
+
uniformData[10] = normalizedLightDir[2]
|
|
494
|
+
uniformData[11] = shadowStrength
|
|
495
|
+
// lightColor (vec4f) - floats 12-15
|
|
496
|
+
uniformData[12] = mainLightColor[0]
|
|
497
|
+
uniformData[13] = mainLightColor[1]
|
|
498
|
+
uniformData[14] = mainLightColor[2]
|
|
499
|
+
uniformData[15] = mainLightEnabled ? mainLightIntensity * 12.0 : 0.0
|
|
500
|
+
// ambientColor (vec4f) - floats 16-19
|
|
501
|
+
uniformData[16] = ambientColor[0]
|
|
502
|
+
uniformData[17] = ambientColor[1]
|
|
503
|
+
uniformData[18] = ambientColor[2]
|
|
504
|
+
uniformData[19] = ambientIntensity
|
|
505
|
+
// cascadeSizes (vec4f) - floats 20-23
|
|
506
|
+
uniformData[20] = cascadeSizes[0]
|
|
507
|
+
uniformData[21] = cascadeSizes[1]
|
|
508
|
+
uniformData[22] = cascadeSizes[2]
|
|
509
|
+
uniformData[23] = 0.0 // padding
|
|
510
|
+
// lightCount (u32) + padding - floats 24-27
|
|
511
|
+
uniformIntView[24] = lightCount
|
|
512
|
+
uniformIntView[25] = 0
|
|
513
|
+
uniformIntView[26] = 0
|
|
514
|
+
uniformIntView[27] = 0
|
|
515
|
+
|
|
516
|
+
device.queue.writeBuffer(this.simulationUniformBuffer, 0, uniformData)
|
|
517
|
+
|
|
518
|
+
// Update per-emitter settings buffer
|
|
519
|
+
// EmitterSettings: gravity(vec3f), drag, turbulence, fadeIn, fadeOut, rotationSpeed, startSize, endSize, baseAlpha, padding
|
|
520
|
+
// = 12 floats = 48 bytes per emitter
|
|
521
|
+
const emitterData = new Float32Array(16 * 12) // 16 emitters max
|
|
522
|
+
for (let i = 0; i < Math.min(emitters.length, 16); i++) {
|
|
523
|
+
const e = emitters[i]
|
|
524
|
+
const offset = i * 12
|
|
525
|
+
// gravity (vec3f) + drag (f32)
|
|
526
|
+
emitterData[offset + 0] = e.gravity[0]
|
|
527
|
+
emitterData[offset + 1] = e.gravity[1]
|
|
528
|
+
emitterData[offset + 2] = e.gravity[2]
|
|
529
|
+
emitterData[offset + 3] = e.drag
|
|
530
|
+
// turbulence + fadeIn + fadeOut + rotationSpeed
|
|
531
|
+
emitterData[offset + 4] = e.turbulence
|
|
532
|
+
emitterData[offset + 5] = e.fadeIn
|
|
533
|
+
emitterData[offset + 6] = e.fadeOut
|
|
534
|
+
emitterData[offset + 7] = e.rotationSpeed ?? 0.5
|
|
535
|
+
// startSize + endSize + baseAlpha + padding
|
|
536
|
+
emitterData[offset + 8] = e.size[0]
|
|
537
|
+
emitterData[offset + 9] = e.size[1]
|
|
538
|
+
emitterData[offset + 10] = e.color[3] // baseAlpha
|
|
539
|
+
emitterData[offset + 11] = 0 // padding
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
device.queue.writeBuffer(this.emitterSettingsBuffer, 0, emitterData)
|
|
543
|
+
|
|
544
|
+
// Create compute bind group with lighting resources
|
|
545
|
+
const computeBindGroup = device.createBindGroup({
|
|
546
|
+
layout: this.computeBindGroupLayout,
|
|
547
|
+
entries: [
|
|
548
|
+
// Core bindings
|
|
549
|
+
{ binding: 0, resource: { buffer: this.simulationUniformBuffer } },
|
|
550
|
+
{ binding: 1, resource: { buffer: this.particleSystem.getParticleBuffer() } },
|
|
551
|
+
{ binding: 2, resource: { buffer: this.particleSystem.getCounterBuffer() } },
|
|
552
|
+
{ binding: 3, resource: { buffer: this.particleSystem.getSpawnBuffer() } },
|
|
553
|
+
{ binding: 4, resource: { buffer: this.emitterSettingsBuffer } },
|
|
554
|
+
// Lighting bindings
|
|
555
|
+
{ binding: 5, resource: { buffer: this.emitterRenderSettingsBuffer } },
|
|
556
|
+
{ binding: 6, resource: this.shadowPass?.getShadowMapView() || this._placeholderDepthView },
|
|
557
|
+
{ binding: 7, resource: this.shadowPass?.getShadowSampler() || this._placeholderComparisonSampler },
|
|
558
|
+
{ binding: 8, resource: { buffer: this.shadowPass?.getCascadeMatricesBuffer() || this._placeholderMatricesBuffer } },
|
|
559
|
+
{ binding: 9, resource: { buffer: this.lightingPass?.lightBuffer || this._placeholderLightsBuffer } },
|
|
560
|
+
{ binding: 10, resource: this.shadowPass?.getSpotShadowAtlasView?.() || this._placeholderSpotShadowView },
|
|
561
|
+
{ binding: 11, resource: { buffer: this.shadowPass?.getSpotMatricesBuffer?.() || this._placeholderSpotMatricesBuffer } },
|
|
562
|
+
]
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// Dispatch spawn compute (using spawnCount captured before executeSpawn cleared the queue)
|
|
566
|
+
if (spawnCount > 0) {
|
|
567
|
+
const spawnPass = commandEncoder.beginComputePass({ label: 'Particle Spawn' })
|
|
568
|
+
spawnPass.setPipeline(this.spawnPipeline)
|
|
569
|
+
spawnPass.setBindGroup(0, computeBindGroup)
|
|
570
|
+
spawnPass.dispatchWorkgroups(Math.ceil(spawnCount / 64))
|
|
571
|
+
spawnPass.end()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Dispatch simulate compute
|
|
575
|
+
const maxParticles = this.particleSystem.globalMaxParticles
|
|
576
|
+
const simulatePass = commandEncoder.beginComputePass({ label: 'Particle Simulate' })
|
|
577
|
+
simulatePass.setPipeline(this.simulatePipeline)
|
|
578
|
+
simulatePass.setBindGroup(0, computeBindGroup)
|
|
579
|
+
simulatePass.dispatchWorkgroups(Math.ceil(maxParticles / 64))
|
|
580
|
+
simulatePass.end()
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async _executeRender(commandEncoder, emitters, camera, mainLight) {
|
|
584
|
+
const { device, canvas } = this.engine
|
|
585
|
+
|
|
586
|
+
// Extract camera right/up from view matrix if not provided
|
|
587
|
+
// View matrix rows are: right, up, forward (transposed world axes)
|
|
588
|
+
let cameraRight = camera.right
|
|
589
|
+
let cameraUp = camera.up
|
|
590
|
+
|
|
591
|
+
if (!cameraRight && camera.view) {
|
|
592
|
+
// First row of view matrix is camera right vector
|
|
593
|
+
cameraRight = [camera.view[0], camera.view[4], camera.view[8]]
|
|
594
|
+
}
|
|
595
|
+
if (!cameraUp && camera.view) {
|
|
596
|
+
// Second row of view matrix is camera up vector
|
|
597
|
+
cameraUp = [camera.view[1], camera.view[5], camera.view[9]]
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Fallback defaults
|
|
601
|
+
cameraRight = cameraRight || [1, 0, 0]
|
|
602
|
+
cameraUp = cameraUp || [0, 1, 0]
|
|
603
|
+
|
|
604
|
+
// Get lighting settings
|
|
605
|
+
const mainLightEnabled = mainLight?.enabled !== false
|
|
606
|
+
const mainLightIntensity = mainLight?.intensity ?? 1.0
|
|
607
|
+
const mainLightColor = mainLight?.color || [1.0, 0.95, 0.9]
|
|
608
|
+
const lightDir = mainLight?.direction || [-1, 1, -0.5]
|
|
609
|
+
|
|
610
|
+
// Normalize light direction
|
|
611
|
+
const lightDirLen = Math.sqrt(lightDir[0] * lightDir[0] + lightDir[1] * lightDir[1] + lightDir[2] * lightDir[2])
|
|
612
|
+
const normalizedLightDir = [
|
|
613
|
+
lightDir[0] / lightDirLen,
|
|
614
|
+
lightDir[1] / lightDirLen,
|
|
615
|
+
lightDir[2] / lightDirLen
|
|
616
|
+
]
|
|
617
|
+
|
|
618
|
+
// Get ambient color from settings
|
|
619
|
+
const ambientColor = this.settings?.ambient?.color || [0.1, 0.12, 0.15]
|
|
620
|
+
const ambientIntensity = this.settings?.ambient?.intensity ?? 0.3
|
|
621
|
+
|
|
622
|
+
// Get shadow settings
|
|
623
|
+
const shadowConfig = this.settings?.shadow || {}
|
|
624
|
+
const shadowBias = shadowConfig.bias ?? 0.0005
|
|
625
|
+
const shadowStrength = shadowConfig.strength ?? 0.8
|
|
626
|
+
const shadowMapSize = shadowConfig.mapSize ?? 2048
|
|
627
|
+
const cascadeSizes = shadowConfig.cascadeSizes || [10, 25, 100]
|
|
628
|
+
|
|
629
|
+
// Get IBL settings - use actual mip count from environment map
|
|
630
|
+
const envSettings = this.settings?.environment || {}
|
|
631
|
+
const envDiffuseLevel = envSettings.diffuseLevel ?? 0.5
|
|
632
|
+
const envMipCount = this.environmentMap?.mipCount ?? envSettings.envMipCount ?? 8
|
|
633
|
+
const envExposure = envSettings.exposure ?? 1.0
|
|
634
|
+
|
|
635
|
+
// Get light count from lighting pass
|
|
636
|
+
const lightCount = this.lightingPass?.lights?.length ?? 0
|
|
637
|
+
|
|
638
|
+
// Update render uniforms (92 floats = 368 bytes)
|
|
639
|
+
const uniformData = new Float32Array(92)
|
|
640
|
+
// viewMatrix (mat4x4f) - floats 0-15
|
|
641
|
+
uniformData.set(camera.view, 0)
|
|
642
|
+
// projectionMatrix (mat4x4f) - floats 16-31
|
|
643
|
+
uniformData.set(camera.proj, 16)
|
|
644
|
+
// cameraPosition (vec3f) + time (f32) - floats 32-35
|
|
645
|
+
uniformData.set(camera.position, 32)
|
|
646
|
+
uniformData[35] = this._lastTime
|
|
647
|
+
// cameraRight (vec3f) + softness (f32) - floats 36-39
|
|
648
|
+
uniformData.set(cameraRight, 36)
|
|
649
|
+
uniformData[39] = emitters[0]?.softness ?? 0.25
|
|
650
|
+
// cameraUp (vec3f) + zOffset (f32) - floats 40-43
|
|
651
|
+
uniformData.set(cameraUp, 40)
|
|
652
|
+
uniformData[43] = emitters[0]?.zOffset ?? 0.01
|
|
653
|
+
// screenSize (vec2f) + near + far - floats 44-47
|
|
654
|
+
uniformData[44] = canvas.width
|
|
655
|
+
uniformData[45] = canvas.height
|
|
656
|
+
uniformData[46] = camera.near ?? 0.1
|
|
657
|
+
uniformData[47] = camera.far ?? 1000
|
|
658
|
+
// blendMode + lit + shadowBias + shadowStrength - floats 48-51
|
|
659
|
+
// blendMode set per-pass below
|
|
660
|
+
uniformData[49] = emitters[0]?.lit ? 1.0 : 0.0
|
|
661
|
+
uniformData[50] = shadowBias
|
|
662
|
+
uniformData[51] = shadowStrength
|
|
663
|
+
// lightDir (vec3f) + shadowMapSize (f32) - floats 52-55
|
|
664
|
+
uniformData.set(normalizedLightDir, 52)
|
|
665
|
+
uniformData[55] = shadowMapSize
|
|
666
|
+
// lightColor (vec4f) - floats 56-59
|
|
667
|
+
// Multiply intensity by 12.0 to match LightingPass convention
|
|
668
|
+
uniformData[56] = mainLightColor[0]
|
|
669
|
+
uniformData[57] = mainLightColor[1]
|
|
670
|
+
uniformData[58] = mainLightColor[2]
|
|
671
|
+
uniformData[59] = mainLightEnabled ? mainLightIntensity * 12.0 : 0.0
|
|
672
|
+
// ambientColor (vec4f) - floats 60-63
|
|
673
|
+
uniformData[60] = ambientColor[0]
|
|
674
|
+
uniformData[61] = ambientColor[1]
|
|
675
|
+
uniformData[62] = ambientColor[2]
|
|
676
|
+
uniformData[63] = ambientIntensity
|
|
677
|
+
// cascadeSizes (vec4f) - floats 64-67
|
|
678
|
+
uniformData[64] = cascadeSizes[0]
|
|
679
|
+
uniformData[65] = cascadeSizes[1]
|
|
680
|
+
uniformData[66] = cascadeSizes[2]
|
|
681
|
+
uniformData[67] = 0.0 // padding
|
|
682
|
+
// envParams (vec4f) - floats 68-71
|
|
683
|
+
// x = diffuse level, y = mip count, z = encoding (0=equirect, 1=octahedral), w = exposure
|
|
684
|
+
uniformData[68] = this.environmentMap ? envDiffuseLevel : 0.0
|
|
685
|
+
uniformData[69] = envMipCount
|
|
686
|
+
uniformData[70] = this.environmentEncoding === 'octahedral' || this.environmentEncoding === 1 ? 1.0 : 0.0
|
|
687
|
+
uniformData[71] = envExposure
|
|
688
|
+
|
|
689
|
+
// lightParams (vec4u) - floats 72-75 (stored as u32)
|
|
690
|
+
// x = light count, y = unused, z = unused, w = unused
|
|
691
|
+
const uniformIntView = new Uint32Array(uniformData.buffer)
|
|
692
|
+
uniformIntView[72] = lightCount
|
|
693
|
+
uniformIntView[73] = 0
|
|
694
|
+
uniformIntView[74] = 0
|
|
695
|
+
uniformIntView[75] = 0
|
|
696
|
+
|
|
697
|
+
// Fog uniforms - floats 76-91
|
|
698
|
+
const fogSettings = this.settings?.environment?.fog || {}
|
|
699
|
+
const fogEnabled = fogSettings.enabled ?? false
|
|
700
|
+
const fogColor = fogSettings.color ?? [0.8, 0.85, 0.9]
|
|
701
|
+
const fogDistances = fogSettings.distances ?? [0, 50, 200]
|
|
702
|
+
const fogAlphas = fogSettings.alpha ?? [0.0, 0.3, 0.8]
|
|
703
|
+
const fogHeightFade = fogSettings.heightFade ?? [-10, 100]
|
|
704
|
+
const fogBrightResist = fogSettings.brightResist ?? 0.8
|
|
705
|
+
|
|
706
|
+
// fogColor (vec3f) + fogEnabled (f32) - floats 76-79
|
|
707
|
+
uniformData[76] = fogColor[0]
|
|
708
|
+
uniformData[77] = fogColor[1]
|
|
709
|
+
uniformData[78] = fogColor[2]
|
|
710
|
+
uniformData[79] = fogEnabled ? 1.0 : 0.0
|
|
711
|
+
// fogDistances (vec3f) + fogBrightResist (f32) - floats 80-83
|
|
712
|
+
uniformData[80] = fogDistances[0]
|
|
713
|
+
uniformData[81] = fogDistances[1]
|
|
714
|
+
uniformData[82] = fogDistances[2]
|
|
715
|
+
uniformData[83] = fogBrightResist
|
|
716
|
+
// fogAlphas (vec3f) + fogPad1 (f32) - floats 84-87
|
|
717
|
+
uniformData[84] = fogAlphas[0]
|
|
718
|
+
uniformData[85] = fogAlphas[1]
|
|
719
|
+
uniformData[86] = fogAlphas[2]
|
|
720
|
+
uniformData[87] = 0.0 // padding
|
|
721
|
+
// fogHeightFade (vec2f) + fogPad2 (vec2f) - floats 88-91
|
|
722
|
+
uniformData[88] = fogHeightFade[0] // bottomY
|
|
723
|
+
uniformData[89] = fogHeightFade[1] // topY
|
|
724
|
+
uniformData[90] = 0.0 // padding
|
|
725
|
+
uniformData[91] = 0.0 // padding
|
|
726
|
+
|
|
727
|
+
// Update emitter render settings buffer (lit, emissive, softness, zOffset per emitter)
|
|
728
|
+
const emitterRenderData = new Float32Array(16 * 4) // 16 emitters * 4 floats
|
|
729
|
+
for (let i = 0; i < Math.min(emitters.length, 16); i++) {
|
|
730
|
+
const e = emitters[i]
|
|
731
|
+
const offset = i * 4
|
|
732
|
+
emitterRenderData[offset + 0] = e.lit ? 1.0 : 0.0
|
|
733
|
+
emitterRenderData[offset + 1] = e.emissive ?? 1.0
|
|
734
|
+
emitterRenderData[offset + 2] = e.softness ?? 0.25
|
|
735
|
+
emitterRenderData[offset + 3] = e.zOffset ?? 0.01
|
|
736
|
+
}
|
|
737
|
+
device.queue.writeBuffer(this.emitterRenderSettingsBuffer, 0, emitterRenderData)
|
|
738
|
+
|
|
739
|
+
// Get or load particle texture
|
|
740
|
+
const textureUrl = emitters[0]?.texture
|
|
741
|
+
const particleTexture = textureUrl
|
|
742
|
+
? await this.particleSystem.loadTexture(textureUrl)
|
|
743
|
+
: this.particleSystem.getDefaultTexture()
|
|
744
|
+
|
|
745
|
+
// Create render bind group - use GBuffer depth directly (depthReadOnly allows this)
|
|
746
|
+
const renderBindGroup = device.createBindGroup({
|
|
747
|
+
layout: this.renderBindGroupLayout,
|
|
748
|
+
entries: [
|
|
749
|
+
{ binding: 0, resource: { buffer: this.renderUniformBuffer } },
|
|
750
|
+
{ binding: 1, resource: { buffer: this.particleSystem.getParticleBuffer() } },
|
|
751
|
+
{ binding: 2, resource: particleTexture?.view || this._placeholderTexture.view },
|
|
752
|
+
{ binding: 3, resource: particleTexture?.sampler || this._placeholderSampler },
|
|
753
|
+
{ binding: 4, resource: this.gbuffer.depth.view },
|
|
754
|
+
{ binding: 5, resource: this.shadowPass?.getShadowMapView() || this._placeholderDepthView },
|
|
755
|
+
{ binding: 6, resource: this.shadowPass?.getShadowSampler() || this._placeholderComparisonSampler },
|
|
756
|
+
{ binding: 7, resource: { buffer: this.shadowPass?.getCascadeMatricesBuffer() || this._placeholderMatricesBuffer } },
|
|
757
|
+
{ binding: 8, resource: { buffer: this.emitterRenderSettingsBuffer } },
|
|
758
|
+
{ binding: 9, resource: this.environmentMap?.view || this._placeholderTexture.view },
|
|
759
|
+
{ binding: 10, resource: this.environmentMap?.sampler || this._placeholderSampler },
|
|
760
|
+
// Point/spot lights buffer from lighting pass
|
|
761
|
+
{ binding: 11, resource: { buffer: this.lightingPass?.lightBuffer || this._placeholderLightsBuffer } },
|
|
762
|
+
// Spot shadow atlas (uses same sampler as cascade shadows)
|
|
763
|
+
{ binding: 12, resource: this.shadowPass?.getSpotShadowAtlasView?.() || this._placeholderSpotShadowView },
|
|
764
|
+
{ binding: 13, resource: this.shadowPass?.getShadowSampler?.() || this._placeholderComparisonSampler },
|
|
765
|
+
// Spot shadow matrices
|
|
766
|
+
{ binding: 14, resource: { buffer: this.shadowPass?.getSpotMatricesBuffer?.() || this._placeholderSpotMatricesBuffer } },
|
|
767
|
+
]
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
const maxParticles = this.particleSystem.globalMaxParticles
|
|
771
|
+
|
|
772
|
+
// Check which blend modes are in use
|
|
773
|
+
const hasAlpha = emitters.some(e => e.blendMode !== 'additive')
|
|
774
|
+
const hasAdditive = emitters.some(e => e.blendMode === 'additive')
|
|
775
|
+
|
|
776
|
+
// Draw alpha-blended particles first (back-to-front would be ideal, but we draw all)
|
|
777
|
+
if (hasAlpha) {
|
|
778
|
+
uniformData[48] = 0.0 // blendMode = alpha
|
|
779
|
+
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData)
|
|
780
|
+
|
|
781
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
782
|
+
colorAttachments: [{
|
|
783
|
+
view: this.outputTexture.view,
|
|
784
|
+
loadOp: 'load',
|
|
785
|
+
storeOp: 'store',
|
|
786
|
+
}],
|
|
787
|
+
depthStencilAttachment: {
|
|
788
|
+
view: this.gbuffer.depth.view,
|
|
789
|
+
depthReadOnly: true,
|
|
790
|
+
},
|
|
791
|
+
label: 'Particle Render (Alpha)'
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
renderPass.setPipeline(this.renderPipelineAlpha)
|
|
795
|
+
renderPass.setBindGroup(0, renderBindGroup)
|
|
796
|
+
renderPass.draw(6, maxParticles, 0, 0)
|
|
797
|
+
renderPass.end()
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Draw additive particles (order doesn't matter for additive)
|
|
801
|
+
if (hasAdditive) {
|
|
802
|
+
uniformData[48] = 1.0 // blendMode = additive
|
|
803
|
+
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData)
|
|
804
|
+
|
|
805
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
806
|
+
colorAttachments: [{
|
|
807
|
+
view: this.outputTexture.view,
|
|
808
|
+
loadOp: 'load',
|
|
809
|
+
storeOp: 'store',
|
|
810
|
+
}],
|
|
811
|
+
depthStencilAttachment: {
|
|
812
|
+
view: this.gbuffer.depth.view,
|
|
813
|
+
depthReadOnly: true,
|
|
814
|
+
},
|
|
815
|
+
label: 'Particle Render (Additive)'
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
renderPass.setPipeline(this.renderPipelineAdditive)
|
|
819
|
+
renderPass.setBindGroup(0, renderBindGroup)
|
|
820
|
+
renderPass.draw(6, maxParticles, 0, 0)
|
|
821
|
+
renderPass.end()
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async _resize(width, height) {
|
|
826
|
+
// Nothing to resize - uses shared resources
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
_destroy() {
|
|
830
|
+
this._textureCache.clear()
|
|
831
|
+
this.particleSystem = null
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export { ParticlePass }
|