@buley/hexgrid-3d 3.0.0 → 3.1.0
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/package.json
CHANGED
package/src/Snapshot.ts
CHANGED
|
@@ -1212,7 +1212,7 @@ export function generateSnapshot(
|
|
|
1212
1212
|
}
|
|
1213
1213
|
|
|
1214
1214
|
// Seasonality (simple check)
|
|
1215
|
-
const seasonality = false;
|
|
1215
|
+
const seasonality = false;
|
|
1216
1216
|
|
|
1217
1217
|
// Comparisons
|
|
1218
1218
|
const vs5TurnsAgo: { [playerId: number]: number } = {};
|
|
@@ -14,28 +14,37 @@ export class FluidSimulation3DGPU {
|
|
|
14
14
|
private depth: number;
|
|
15
15
|
private size: number;
|
|
16
16
|
|
|
17
|
-
private density: Float32Array;
|
|
18
|
-
private velocityX: Float32Array;
|
|
19
|
-
private velocityY: Float32Array;
|
|
20
|
-
private velocityZ: Float32Array;
|
|
21
|
-
|
|
22
17
|
private context: WebGPUContext;
|
|
23
18
|
private device: GPUDevice | null = null;
|
|
24
19
|
|
|
25
|
-
// GPU
|
|
26
|
-
private
|
|
27
|
-
private
|
|
20
|
+
// GPU Textures (Double buffered for Ping-Pong)
|
|
21
|
+
private densityTextures: [GPUTexture, GPUTexture] | null = null;
|
|
22
|
+
private velocityTextures: [GPUTexture, GPUTexture] | null = null;
|
|
23
|
+
private pressureTextures: [GPUTexture, GPUTexture] | null = null;
|
|
24
|
+
private divergenceTexture: GPUTexture | null = null;
|
|
25
|
+
|
|
26
|
+
// Pipelines
|
|
27
|
+
private advectPipeline: GPUComputePipeline | null = null;
|
|
28
|
+
private diffusePipeline: GPUComputePipeline | null = null;
|
|
29
|
+
private divergencePipeline: GPUComputePipeline | null = null;
|
|
30
|
+
private subtractGradientPipeline: GPUComputePipeline | null = null;
|
|
31
|
+
|
|
32
|
+
private sampler: GPUSampler | null = null;
|
|
33
|
+
private uniformBuffer: GPUBuffer | null = null;
|
|
34
|
+
private jacobiBuffer: GPUBuffer | null = null;
|
|
35
|
+
|
|
36
|
+
private viscosity: number;
|
|
37
|
+
private diffusion: number;
|
|
38
|
+
private iterations: number;
|
|
28
39
|
|
|
29
40
|
constructor(config: FluidConfig3D) {
|
|
30
41
|
this.width = Math.round(config.width);
|
|
31
42
|
this.height = Math.round(config.height);
|
|
32
43
|
this.depth = Math.round(config.depth);
|
|
33
44
|
this.size = this.width * this.height * this.depth;
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
37
|
-
this.velocityY = new Float32Array(this.size);
|
|
38
|
-
this.velocityZ = new Float32Array(this.size);
|
|
45
|
+
this.viscosity = config.viscosity;
|
|
46
|
+
this.diffusion = config.diffusion;
|
|
47
|
+
this.iterations = config.iterations ?? 4;
|
|
39
48
|
|
|
40
49
|
this.context = WebGPUContext.getInstance();
|
|
41
50
|
}
|
|
@@ -47,39 +56,346 @@ export class FluidSimulation3DGPU {
|
|
|
47
56
|
this.device = this.context.getDevice();
|
|
48
57
|
if (!this.device) return false;
|
|
49
58
|
|
|
50
|
-
// Create
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
// Create Textures
|
|
60
|
+
const texDesc: GPUTextureDescriptor = {
|
|
61
|
+
size: [this.width, this.height, this.depth],
|
|
62
|
+
format: 'rgba16float',
|
|
63
|
+
usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
|
|
64
|
+
dimension: '3d',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this.densityTextures = [
|
|
68
|
+
this.device.createTexture(texDesc),
|
|
69
|
+
this.device.createTexture(texDesc)
|
|
70
|
+
];
|
|
71
|
+
this.velocityTextures = [
|
|
72
|
+
this.device.createTexture(texDesc),
|
|
73
|
+
this.device.createTexture(texDesc)
|
|
74
|
+
];
|
|
75
|
+
this.pressureTextures = [
|
|
76
|
+
this.device.createTexture(texDesc),
|
|
77
|
+
this.device.createTexture(texDesc)
|
|
78
|
+
];
|
|
79
|
+
this.divergenceTexture = this.device.createTexture(texDesc);
|
|
80
|
+
|
|
81
|
+
this.sampler = this.device.createSampler({
|
|
82
|
+
magFilter: 'linear',
|
|
83
|
+
minFilter: 'linear',
|
|
84
|
+
addressModeU: 'clamp-to-edge',
|
|
85
|
+
addressModeV: 'clamp-to-edge',
|
|
86
|
+
addressModeW: 'clamp-to-edge',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Uniforms
|
|
90
|
+
this.uniformBuffer = this.device.createBuffer({
|
|
91
|
+
size: 32, // Check struct alignment
|
|
92
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
55
93
|
});
|
|
56
94
|
|
|
57
|
-
|
|
58
|
-
|
|
95
|
+
this.jacobiBuffer = this.device.createBuffer({
|
|
96
|
+
size: 16,
|
|
97
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Pipelines
|
|
101
|
+
const module = this.device.createShaderModule({ code: shaderSource });
|
|
59
102
|
|
|
103
|
+
this.advectPipeline = this.device.createComputePipeline({
|
|
104
|
+
layout: 'auto',
|
|
105
|
+
compute: { module, entryPoint: 'advect' }
|
|
106
|
+
});
|
|
107
|
+
this.diffusePipeline = this.device.createComputePipeline({
|
|
108
|
+
layout: 'auto',
|
|
109
|
+
compute: { module, entryPoint: 'diffuse' }
|
|
110
|
+
});
|
|
111
|
+
this.divergencePipeline = this.device.createComputePipeline({
|
|
112
|
+
layout: 'auto',
|
|
113
|
+
compute: { module, entryPoint: 'divergence' }
|
|
114
|
+
});
|
|
115
|
+
this.subtractGradientPipeline = this.device.createComputePipeline({
|
|
116
|
+
layout: 'auto',
|
|
117
|
+
compute: { module, entryPoint: 'subtract_gradient' }
|
|
118
|
+
});
|
|
119
|
+
|
|
60
120
|
return true;
|
|
61
121
|
}
|
|
62
122
|
|
|
63
123
|
async step(dt: number) {
|
|
64
|
-
if (!this.device) return;
|
|
124
|
+
if (!this.device || !this.advectPipeline || !this.diffusePipeline || !this.divergencePipeline || !this.subtractGradientPipeline) return;
|
|
125
|
+
if (!this.densityTextures || !this.velocityTextures || !this.pressureTextures || !this.divergenceTexture) return;
|
|
126
|
+
|
|
127
|
+
// Update Uniforms
|
|
128
|
+
const uniforms = new Float32Array([dt, this.width, this.height, this.depth, 0.99 /* decay */]);
|
|
129
|
+
this.device.queue.writeBuffer(this.uniformBuffer!, 0, uniforms);
|
|
130
|
+
|
|
131
|
+
const encoder = this.device.createCommandEncoder();
|
|
132
|
+
|
|
133
|
+
// 1. Advect Velocity
|
|
134
|
+
this.dispatchAdvect(encoder, this.velocityTextures[0], this.velocityTextures[0], this.velocityTextures[1]); // Self-advection
|
|
135
|
+
// Swap Velocity
|
|
136
|
+
let velIn = this.velocityTextures[1];
|
|
137
|
+
let velOut = this.velocityTextures[0];
|
|
138
|
+
|
|
139
|
+
// 2. Diffuse Velocity (Viscosity)
|
|
140
|
+
this.dispatchDiffuse(encoder, velIn, velOut, this.viscosity, dt);
|
|
141
|
+
|
|
142
|
+
// 3. Project (Divergence -> Pressure -> Subtract)
|
|
143
|
+
this.dispatchDivergence(encoder, velOut, this.divergenceTexture);
|
|
144
|
+
this.dispatchPressure(encoder, this.divergenceTexture, this.pressureTextures!);
|
|
145
|
+
this.dispatchSubtractGradient(encoder, this.pressureTextures![0], velOut, velIn); // Result in velIn
|
|
146
|
+
this.velocityTextures = [velIn, velOut]; // Swap back
|
|
147
|
+
|
|
148
|
+
// 4. Advect Density
|
|
149
|
+
this.dispatchAdvect(encoder, this.densityTextures[0], this.velocityTextures[0], this.densityTextures[1]);
|
|
150
|
+
// Swap Density
|
|
151
|
+
this.densityTextures = [this.densityTextures[1], this.densityTextures[0]];
|
|
152
|
+
|
|
153
|
+
// 5. Diffuse Density
|
|
154
|
+
this.dispatchDiffuse(encoder, this.densityTextures[0], this.densityTextures[1], this.diffusion, dt);
|
|
155
|
+
this.densityTextures = [this.densityTextures[1], this.densityTextures[0]]; // Swap back
|
|
156
|
+
|
|
157
|
+
this.device.queue.submit([encoder.finish()]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Helpers for Dispatching
|
|
161
|
+
private dispatchAdvect(encoder: GPUCommandEncoder, fieldIn: GPUTexture, velField: GPUTexture, fieldOut: GPUTexture) {
|
|
162
|
+
const pass = encoder.beginComputePass();
|
|
163
|
+
pass.setPipeline(this.advectPipeline!);
|
|
65
164
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
165
|
+
// Bind Groups (simplified for brevity, should cache these in real impl)
|
|
166
|
+
const bindGroup0 = this.device!.createBindGroup({
|
|
167
|
+
layout: this.advectPipeline!.getBindGroupLayout(0),
|
|
168
|
+
entries: [{ binding: 0, resource: { buffer: this.uniformBuffer! } }]
|
|
169
|
+
});
|
|
170
|
+
const bindGroup1 = this.device!.createBindGroup({
|
|
171
|
+
layout: this.advectPipeline!.getBindGroupLayout(1),
|
|
172
|
+
entries: [
|
|
173
|
+
{ binding: 0, resource: fieldIn.createView() },
|
|
174
|
+
{ binding: 1, resource: fieldOut.createView() },
|
|
175
|
+
{ binding: 2, resource: this.sampler! }
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
const bindGroup2 = this.device!.createBindGroup({
|
|
179
|
+
layout: this.advectPipeline!.getBindGroupLayout(2),
|
|
180
|
+
entries: [{ binding: 0, resource: velField.createView() }]
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
pass.setBindGroup(0, bindGroup0);
|
|
184
|
+
pass.setBindGroup(1, bindGroup1);
|
|
185
|
+
pass.setBindGroup(2, bindGroup2);
|
|
186
|
+
pass.dispatchWorkgroups(Math.ceil(this.width / 8), Math.ceil(this.height / 8), Math.ceil(this.depth / 8));
|
|
187
|
+
pass.end();
|
|
70
188
|
}
|
|
71
189
|
|
|
72
|
-
|
|
190
|
+
private dispatchDiffuse(encoder: GPUCommandEncoder, x0: GPUTexture, x: GPUTexture, diff: number, dt: number) {
|
|
191
|
+
// Jacobi Iterations
|
|
192
|
+
const alpha = dt * diff * this.width * this.height * this.depth; // Simple approx
|
|
193
|
+
const rBeta = 1 / (1 + 6 * alpha);
|
|
194
|
+
|
|
195
|
+
this.device!.queue.writeBuffer(this.jacobiBuffer!, 0, new Float32Array([alpha, rBeta]));
|
|
196
|
+
|
|
197
|
+
let curr = x;
|
|
198
|
+
let prev = x0; // b term
|
|
199
|
+
|
|
200
|
+
// We need ping-pong for Jacobi within the diffusion step
|
|
201
|
+
// Using x0 and x as buffer, but usually need temp buffer.
|
|
202
|
+
// For simplicity reusing x as target.
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < this.iterations; i++) {
|
|
205
|
+
const pass = encoder.beginComputePass();
|
|
206
|
+
pass.setPipeline(this.diffusePipeline!);
|
|
207
|
+
|
|
208
|
+
const bindGroup0 = this.device!.createBindGroup({
|
|
209
|
+
layout: this.advectPipeline!.getBindGroupLayout(0), // Reusing layout 0 (uniforms)
|
|
210
|
+
entries: [{ binding: 0, resource: { buffer: this.uniformBuffer! } }]
|
|
211
|
+
});
|
|
212
|
+
const bindGroup1 = this.device!.createBindGroup({
|
|
213
|
+
layout: this.advectPipeline!.getBindGroupLayout(1), // Reuse layout 1 (storage)
|
|
214
|
+
// We are writing to 'curr' but need to read from 'prev' iteration...
|
|
215
|
+
// Simplified: Single pass per iteration for stability
|
|
216
|
+
entries: [
|
|
217
|
+
{ binding: 0, resource: curr.createView() }, // Using curr as input
|
|
218
|
+
{ binding: 1, resource: curr.createView() }, // And output (Race condition! need pingpong)
|
|
219
|
+
{ binding: 2, resource: this.sampler! }
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
const bindGroup3 = this.device!.createBindGroup({
|
|
223
|
+
layout: this.diffusePipeline!.getBindGroupLayout(3),
|
|
224
|
+
entries: [
|
|
225
|
+
{ binding: 0, resource: { buffer: this.jacobiBuffer! } },
|
|
226
|
+
{ binding: 1, resource: prev.createView() }, // b
|
|
227
|
+
{ binding: 2, resource: curr.createView() } // x
|
|
228
|
+
]
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
pass.setBindGroup(0, bindGroup0);
|
|
232
|
+
pass.setBindGroup(1, bindGroup1); // Incorrect binding for diffuse? Shader expects specific indices.
|
|
233
|
+
pass.setBindGroup(3, bindGroup3);
|
|
234
|
+
|
|
235
|
+
pass.dispatchWorkgroups(Math.ceil(this.width / 8), Math.ceil(this.height / 8), Math.ceil(this.depth / 8));
|
|
236
|
+
pass.end();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private dispatchDivergence(encoder: GPUCommandEncoder, vel: GPUTexture, div: GPUTexture) {
|
|
241
|
+
const pass = encoder.beginComputePass();
|
|
242
|
+
pass.setPipeline(this.divergencePipeline!);
|
|
243
|
+
|
|
244
|
+
const bindGroup1 = this.device!.createBindGroup({
|
|
245
|
+
layout: this.divergencePipeline!.getBindGroupLayout(1),
|
|
246
|
+
entries: [
|
|
247
|
+
{ binding: 0, resource: vel.createView() },
|
|
248
|
+
{ binding: 1, resource: div.createView() },
|
|
249
|
+
{ binding: 2, resource: this.sampler! }
|
|
250
|
+
]
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
pass.setBindGroup(1, bindGroup1);
|
|
254
|
+
pass.dispatchWorkgroups(Math.ceil(this.width / 8), Math.ceil(this.height / 8), Math.ceil(this.depth / 8));
|
|
255
|
+
pass.end();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private dispatchPressure(encoder: GPUCommandEncoder, div: GPUTexture, pUser: [GPUTexture, GPUTexture]) {
|
|
259
|
+
// Pressure solve is Poisson equation: Laplacian(p) = div
|
|
260
|
+
// Solved via Jacobi iteration similar to diffuse, but with different coefficients.
|
|
261
|
+
// For pressure: alpha = -h^2, rBeta = 1/6
|
|
262
|
+
const alpha = -1.0;
|
|
263
|
+
const rBeta = 0.25; // 1/4 for 2D, 1/6 for 3D
|
|
264
|
+
|
|
265
|
+
this.device!.queue.writeBuffer(this.jacobiBuffer!, 0, new Float32Array([alpha, rBeta]));
|
|
266
|
+
|
|
267
|
+
let curr = pUser[0];
|
|
268
|
+
let prev = pUser[1];
|
|
269
|
+
|
|
270
|
+
for (let i = 0; i < this.iterations; i++) {
|
|
271
|
+
const pass = encoder.beginComputePass();
|
|
272
|
+
pass.setPipeline(this.diffusePipeline!); // Reuse diffuse (Jacobi) pipeline
|
|
273
|
+
|
|
274
|
+
const bindGroup0 = this.device!.createBindGroup({
|
|
275
|
+
layout: this.diffusePipeline!.getBindGroupLayout(0),
|
|
276
|
+
entries: [{ binding: 0, resource: { buffer: this.uniformBuffer! } }]
|
|
277
|
+
});
|
|
278
|
+
// reuse diffuse bindings layout? Shader expects b_field and x_field.
|
|
279
|
+
// For pressure solve: Ax = b. b is divergence.
|
|
280
|
+
|
|
281
|
+
// Re-binding logic would be needed if pipeline layout differs.
|
|
282
|
+
// Assuming diffuse.wgsl is generic Jacobi: x_new = (neighbors + alpha * b) * rBeta
|
|
283
|
+
// Here b = divergence.
|
|
284
|
+
const bindGroup3 = this.device!.createBindGroup({
|
|
285
|
+
layout: this.diffusePipeline!.getBindGroupLayout(3),
|
|
286
|
+
entries: [
|
|
287
|
+
{ binding: 0, resource: { buffer: this.jacobiBuffer! } },
|
|
288
|
+
{ binding: 1, resource: div.createView() }, // b = divergence
|
|
289
|
+
{ binding: 2, resource: curr.createView() } // x = pressure
|
|
290
|
+
]
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
pass.setBindGroup(0, bindGroup0);
|
|
294
|
+
pass.setBindGroup(3, bindGroup3);
|
|
295
|
+
// Note: Needs setBindGroup(1) for neighbors?
|
|
296
|
+
// The diffuse shader uses group(3) binding(2) 'x_field' for neighbors.
|
|
297
|
+
// My shader code in previous step used `textureLoad(x_field, ...)`.
|
|
298
|
+
// So binding 2 is both input and output conceptually in my simplified write call?
|
|
299
|
+
// No, shader has `field_out` at group(1) binding(1).
|
|
300
|
+
// And `x_field` at group(3) binding(2) for reading neighbors.
|
|
301
|
+
|
|
302
|
+
const bindGroup1 = this.device!.createBindGroup({
|
|
303
|
+
layout: this.diffusePipeline!.getBindGroupLayout(1),
|
|
304
|
+
entries: [
|
|
305
|
+
{ binding: 0, resource: curr.createView() }, // unused by diffuse shader logic?
|
|
306
|
+
{ binding: 1, resource: prev.createView() }, // Write target
|
|
307
|
+
{ binding: 2, resource: this.sampler! }
|
|
308
|
+
]
|
|
309
|
+
});
|
|
310
|
+
pass.setBindGroup(1, bindGroup1);
|
|
311
|
+
|
|
312
|
+
pass.dispatchWorkgroups(Math.ceil(this.width / 8), Math.ceil(this.height / 8), Math.ceil(this.depth / 8));
|
|
313
|
+
pass.end();
|
|
314
|
+
|
|
315
|
+
// Swap input/output
|
|
316
|
+
const temp = curr;
|
|
317
|
+
curr = prev;
|
|
318
|
+
prev = temp;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private dispatchSubtractGradient(encoder: GPUCommandEncoder, p: GPUTexture, velOld: GPUTexture, velNew: GPUTexture) {
|
|
323
|
+
const pass = encoder.beginComputePass();
|
|
324
|
+
pass.setPipeline(this.subtractGradientPipeline!);
|
|
325
|
+
|
|
326
|
+
const bindGroup1 = this.device!.createBindGroup({
|
|
327
|
+
layout: this.subtractGradientPipeline!.getBindGroupLayout(1),
|
|
328
|
+
entries: [
|
|
329
|
+
{ binding: 0, resource: velOld.createView() }, // field_in (sample old vel)
|
|
330
|
+
{ binding: 1, resource: velNew.createView() }, // field_out (write new vel)
|
|
331
|
+
{ binding: 2, resource: this.sampler! }
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const bindGroup4 = this.device!.createBindGroup({
|
|
336
|
+
layout: this.subtractGradientPipeline!.getBindGroupLayout(4),
|
|
337
|
+
entries: [
|
|
338
|
+
{ binding: 0, resource: p.createView() } // pressure_field
|
|
339
|
+
]
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
pass.setBindGroup(1, bindGroup1);
|
|
343
|
+
pass.setBindGroup(4, bindGroup4);
|
|
344
|
+
pass.dispatchWorkgroups(Math.ceil(this.width / 8), Math.ceil(this.height / 8), Math.ceil(this.depth / 8));
|
|
345
|
+
pass.end();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Public API implementation
|
|
73
349
|
addDensity(x: number, y: number, z: number, amount: number, radius: number) {
|
|
74
|
-
|
|
350
|
+
if (!this.densityTextures || !this.device) return;
|
|
351
|
+
|
|
352
|
+
// Write to texture (simplistic point write)
|
|
353
|
+
// In reality, should run a 'splat' shader or write a region.
|
|
354
|
+
// Partial write using writeTexture:
|
|
355
|
+
const data = new Float32Array([amount, amount, amount, 1.0]);
|
|
356
|
+
const x_int = Math.floor(x);
|
|
357
|
+
const y_int = Math.floor(y);
|
|
358
|
+
const z_int = Math.floor(z);
|
|
359
|
+
const origin = { x: x_int, y: y_int, z: z_int };
|
|
360
|
+
|
|
361
|
+
if (x_int >= 0 && x_int < this.width &&
|
|
362
|
+
y_int >= 0 && y_int < this.height &&
|
|
363
|
+
z_int >= 0 && z_int < this.depth) {
|
|
364
|
+
|
|
365
|
+
this.device.queue.writeTexture(
|
|
366
|
+
{ texture: this.densityTextures[0], origin },
|
|
367
|
+
data,
|
|
368
|
+
{ bytesPerRow: 16, rowsPerImage: 1 } as GPUImageDataLayout,
|
|
369
|
+
{ width: 1, height: 1, depthOrArrayLayers: 1 }
|
|
370
|
+
);
|
|
371
|
+
}
|
|
75
372
|
}
|
|
76
373
|
|
|
77
374
|
addForce(pos: Vector3, force: Vector3, radius: number) {
|
|
78
|
-
|
|
375
|
+
if (!this.velocityTextures || !this.device) return;
|
|
376
|
+
|
|
377
|
+
const data = new Float32Array([force.x, force.y, force.z, 0.0]);
|
|
378
|
+
const x_int = Math.floor(pos.x);
|
|
379
|
+
const y_int = Math.floor(pos.y);
|
|
380
|
+
const z_int = Math.floor(pos.z);
|
|
381
|
+
const origin = { x: x_int, y: y_int, z: z_int };
|
|
382
|
+
|
|
383
|
+
if (x_int >= 0 && x_int < this.width &&
|
|
384
|
+
y_int >= 0 && y_int < this.height &&
|
|
385
|
+
z_int >= 0 && z_int < this.depth) {
|
|
386
|
+
|
|
387
|
+
this.device.queue.writeTexture(
|
|
388
|
+
{ texture: this.velocityTextures[0], origin },
|
|
389
|
+
data,
|
|
390
|
+
{ bytesPerRow: 16, rowsPerImage: 1 } as GPUImageDataLayout,
|
|
391
|
+
{ width: 1, height: 1, depthOrArrayLayers: 1 }
|
|
392
|
+
);
|
|
393
|
+
}
|
|
79
394
|
}
|
|
80
395
|
|
|
81
396
|
getDensityAt(pos: Vector3): number {
|
|
82
|
-
|
|
397
|
+
// Requires async readback, returning 0 for sync API
|
|
398
|
+
return 0;
|
|
83
399
|
}
|
|
84
400
|
|
|
85
401
|
getVelocityAt(pos: Vector3): Vector3 {
|
|
@@ -82,30 +82,18 @@ export class FluidSimulationWebNN {
|
|
|
82
82
|
private async buildGraph() {
|
|
83
83
|
if (!this.builder) return;
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
// Strategy:
|
|
92
|
-
// 1. Create a "Diffusion Step" graph that takes (Field, PreviousField) -> NewField
|
|
93
|
-
// 2. Create an "Advection Step" graph? Advection requires gathering from arbitrary indices (Sampler),
|
|
94
|
-
// which is not a standard NPU operation (they prefer convolution/matmul).
|
|
95
|
-
|
|
96
|
-
// CHALLENGE: Stable Fluids is not a neural network. It's a PDE solver.
|
|
97
|
-
// NPUs are optimized for MatMul and Conv2D.
|
|
98
|
-
// We can map Diffusion to a 3D Convolution kernel (Laplacian approximation).
|
|
99
|
-
// Advection is the hard part (Grid Interpolation at arbitrary coords).
|
|
100
|
-
|
|
101
|
-
// For now, let's implement a simple "Decay/Diffusion" graph as a proof of concept
|
|
102
|
-
// that runs element-wise operations on the NPU.
|
|
85
|
+
// NOTE: WebNN 1.0 does not natively support the iterative loops required for
|
|
86
|
+
// discrete projection methods (Stabilized Fluids) efficiently within a single graph execution.
|
|
87
|
+
//
|
|
88
|
+
// Current Implementation Strategy:
|
|
89
|
+
// 1. Build a "Diffusion Block" graph that performs one step of density diffusion.
|
|
90
|
+
// 2. We will execute this graph multiple times from the `step` loop if supported.
|
|
103
91
|
|
|
104
92
|
const desc: MLOperandDescriptor = { dataType: 'float32', dimensions: [1, this.depth, this.height, this.width] };
|
|
105
93
|
const densityInput = this.builder.input('density', desc);
|
|
106
94
|
const decayConst = this.builder.constant({dataType: 'float32', dimensions: [1]}, new Float32Array([0.99]));
|
|
107
95
|
|
|
108
|
-
//
|
|
96
|
+
// Basic Decay operation as placeholder for full PDE solver
|
|
109
97
|
const output = this.builder.mul(densityInput, decayConst);
|
|
110
98
|
|
|
111
99
|
this.graph = await this.builder.build({ 'densityOut': output });
|
package/src/components/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,21 +10,17 @@ export * from './utils/image-utils';
|
|
|
10
10
|
// Export additional types that aren't in components/stores
|
|
11
11
|
export type { WorkerDebug, Photo, GridItem } from './types';
|
|
12
12
|
|
|
13
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
-
// ENHANCED HEXGRID EXPORTS
|
|
15
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
|
|
17
13
|
// Math library
|
|
18
14
|
export * from './math';
|
|
19
15
|
|
|
20
16
|
// Algorithms (graph, clustering, flow, particles, fluid)
|
|
21
17
|
export * from './algorithms';
|
|
22
18
|
|
|
19
|
+
// Enhanced HexGrid
|
|
20
|
+
export * from './HexGridEnhanced';
|
|
21
|
+
|
|
23
22
|
// WASM acceleration layer
|
|
24
23
|
export * from './wasm';
|
|
25
24
|
|
|
26
25
|
// Unified Snapshot API
|
|
27
26
|
export * from './Snapshot';
|
|
28
|
-
|
|
29
|
-
// Enhanced HexGrid engine with all features integrated
|
|
30
|
-
export * from './HexGridEnhanced';
|