wgpu-kit 0.9.11 → 1.0.1
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/README.md +7 -0
- package/README.zh-CN.md +3 -3
- package/dist/core/buffer.d.ts +1 -1
- package/dist/core/buffer.js +109 -0
- package/dist/core/context.d.ts +2 -0
- package/dist/core/context.js +62 -0
- package/dist/core/errors.js +42 -0
- package/dist/core/kernel.js +197 -0
- package/dist/core/layout.d.ts +5 -1
- package/dist/core/layout.js +69 -0
- package/dist/core/pingpong.js +61 -0
- package/dist/core/raw.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -1510
- package/dist/interop/three.js +29 -0
- package/dist/media.js +70 -0
- package/dist/observe.d.ts +19 -0
- package/dist/observe.js +76 -0
- package/dist/packs/fields/index.js +230 -0
- package/dist/packs/image/index.js +250 -0
- package/dist/packs/life/boids.js +367 -0
- package/dist/packs/life/index.js +8 -0
- package/dist/packs/life/map.js +110 -0
- package/dist/packs/life/physarum.js +228 -0
- package/dist/packs/life/tentacles.js +238 -0
- package/dist/packs/life/turing.js +203 -0
- package/dist/packs/particles/config.d.ts +1 -0
- package/dist/packs/particles/config.js +36 -0
- package/dist/packs/particles/grid.js +248 -0
- package/dist/packs/particles/index.js +361 -0
- package/dist/packs/particles/presets.js +75 -0
- package/dist/packs/particles/render.js +116 -0
- package/dist/packs/particles/wgsl.js +175 -0
- package/dist/react/index.js +36 -0
- package/dist/vite.js +27 -28
- package/package.json +1 -1
- package/dist/fields.js +0 -585
- package/dist/image.js +0 -318
- package/dist/life.js +0 -1407
- package/dist/particles.js +0 -1245
- package/dist/react.js +0 -1289
- package/dist/three.js +0 -33
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { GpuContext } from "../../core/context.js";
|
|
2
|
+
import { Buffer } from "../../core/buffer.js";
|
|
3
|
+
import { PingPong } from "../../core/pingpong.js";
|
|
4
|
+
import { CompileError } from "../../core/errors.js";
|
|
5
|
+
import { mulberry32 } from "../particles/presets.js";
|
|
6
|
+
const WG = 64;
|
|
7
|
+
export async function boids(config = {}) {
|
|
8
|
+
const { count: N = 3000, perception = 0.05, maxSpeed = 0.012, wSep = 1.6, wAli = 1.0, wCoh = 0.8, size = 0.009, seed = 'boids', } = config;
|
|
9
|
+
const seedHash = typeof seed === 'string' ? hashStr(seed) : (seed ?? 3);
|
|
10
|
+
const gridSize = Math.max(4, Math.ceil(2 / perception));
|
|
11
|
+
const ctx = await GpuContext.get();
|
|
12
|
+
const device = ctx.device;
|
|
13
|
+
const pp = await PingPong.create({ pos: 'vec2f', vel: 'vec2f' }, N);
|
|
14
|
+
const sideA = { pos: pp.current.pos, vel: pp.current.vel };
|
|
15
|
+
const sideB = { pos: pp.other.pos, vel: pp.other.vel };
|
|
16
|
+
{
|
|
17
|
+
const rand = mulberry32(seedHash);
|
|
18
|
+
const p = new Float32Array(N * 2);
|
|
19
|
+
const v = new Float32Array(N * 2);
|
|
20
|
+
for (let i = 0; i < N; i++) {
|
|
21
|
+
const t = rand() * Math.PI * 2;
|
|
22
|
+
const r = Math.sqrt(rand()) * 0.6;
|
|
23
|
+
p[i * 2] = Math.cos(t) * r;
|
|
24
|
+
p[i * 2 + 1] = Math.sin(t) * r;
|
|
25
|
+
const va = rand() * Math.PI * 2;
|
|
26
|
+
v[i * 2] = Math.cos(va) * maxSpeed * 0.6;
|
|
27
|
+
v[i * 2 + 1] = Math.sin(va) * maxSpeed * 0.6;
|
|
28
|
+
}
|
|
29
|
+
sideA.pos.write(p);
|
|
30
|
+
sideA.vel.write(v);
|
|
31
|
+
}
|
|
32
|
+
const USIZE = 48;
|
|
33
|
+
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
34
|
+
const writeUniform = () => {
|
|
35
|
+
const b = new ArrayBuffer(USIZE);
|
|
36
|
+
const v = new DataView(b);
|
|
37
|
+
v.setUint32(0, N, true);
|
|
38
|
+
v.setUint32(4, gridSize, true);
|
|
39
|
+
v.setFloat32(8, perception, true);
|
|
40
|
+
v.setFloat32(12, maxSpeed, true);
|
|
41
|
+
v.setFloat32(16, wSep, true);
|
|
42
|
+
v.setFloat32(20, wAli, true);
|
|
43
|
+
v.setFloat32(24, wCoh, true);
|
|
44
|
+
v.setFloat32(28, 1 / 60, true); // dt
|
|
45
|
+
v.setFloat32(32, 1.0, true); // worldHalf
|
|
46
|
+
device.queue.writeBuffer(uniform, 0, b);
|
|
47
|
+
};
|
|
48
|
+
writeUniform();
|
|
49
|
+
const cells = gridSize * gridSize;
|
|
50
|
+
const cellCount = await Buffer.create('u32', cells);
|
|
51
|
+
const cellStart = await Buffer.create('u32', cells);
|
|
52
|
+
const cellFill = await Buffer.create('u32', cells);
|
|
53
|
+
const order = await Buffer.create('u32', N);
|
|
54
|
+
cellCount.write(new Uint32Array(cells));
|
|
55
|
+
const module = device.createShaderModule({ code: boidsWgsl(size), label: 'boids' });
|
|
56
|
+
const info = await module.getCompilationInfo();
|
|
57
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
58
|
+
if (errors.length > 0)
|
|
59
|
+
throw new CompileError('boids', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
60
|
+
const pCounts = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_counts' } });
|
|
61
|
+
const pScan = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_scan' } });
|
|
62
|
+
const pScatter = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_scatter' } });
|
|
63
|
+
const pForce = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_force' } });
|
|
64
|
+
const pRender = device.createRenderPipeline({
|
|
65
|
+
layout: 'auto',
|
|
66
|
+
vertex: { module, entryPoint: 'vs' },
|
|
67
|
+
fragment: { module, entryPoint: 'fs', targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }] },
|
|
68
|
+
primitive: { topology: 'triangle-list' },
|
|
69
|
+
});
|
|
70
|
+
const bgCounts = (read) => device.createBindGroup({
|
|
71
|
+
layout: pCounts.getBindGroupLayout(0),
|
|
72
|
+
entries: [
|
|
73
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
74
|
+
{ binding: 1, resource: { buffer: read.pos.gpuBuffer } },
|
|
75
|
+
{ binding: 2, resource: { buffer: cellCount.gpuBuffer } },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
const bgScan = device.createBindGroup({
|
|
79
|
+
layout: pScan.getBindGroupLayout(0),
|
|
80
|
+
entries: [
|
|
81
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
82
|
+
{ binding: 2, resource: { buffer: cellCount.gpuBuffer } },
|
|
83
|
+
{ binding: 3, resource: { buffer: cellStart.gpuBuffer } },
|
|
84
|
+
{ binding: 4, resource: { buffer: cellFill.gpuBuffer } },
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
const bgScatter = (read) => device.createBindGroup({
|
|
88
|
+
layout: pScatter.getBindGroupLayout(0),
|
|
89
|
+
entries: [
|
|
90
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
91
|
+
{ binding: 1, resource: { buffer: read.pos.gpuBuffer } },
|
|
92
|
+
{ binding: 4, resource: { buffer: cellFill.gpuBuffer } },
|
|
93
|
+
{ binding: 5, resource: { buffer: order.gpuBuffer } },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
const bgForce = (read, write) => device.createBindGroup({
|
|
97
|
+
layout: pForce.getBindGroupLayout(0),
|
|
98
|
+
entries: [
|
|
99
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
100
|
+
{ binding: 1, resource: { buffer: read.pos.gpuBuffer } },
|
|
101
|
+
{ binding: 6, resource: { buffer: read.vel.gpuBuffer } },
|
|
102
|
+
{ binding: 7, resource: { buffer: write.pos.gpuBuffer } },
|
|
103
|
+
{ binding: 8, resource: { buffer: write.vel.gpuBuffer } },
|
|
104
|
+
{ binding: 3, resource: { buffer: cellStart.gpuBuffer } },
|
|
105
|
+
{ binding: 4, resource: { buffer: cellFill.gpuBuffer } },
|
|
106
|
+
{ binding: 5, resource: { buffer: order.gpuBuffer } },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
const bgRender = (read) => device.createBindGroup({
|
|
110
|
+
layout: pRender.getBindGroupLayout(0),
|
|
111
|
+
entries: [
|
|
112
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
113
|
+
{ binding: 1, resource: { buffer: read.pos.gpuBuffer } },
|
|
114
|
+
{ binding: 6, resource: { buffer: read.vel.gpuBuffer } },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
let gpuCtx = null;
|
|
118
|
+
let frame = 0;
|
|
119
|
+
let lastFps = 0;
|
|
120
|
+
let fFrames = 0;
|
|
121
|
+
let fAcc = 0;
|
|
122
|
+
let fLast = performance.now();
|
|
123
|
+
return {
|
|
124
|
+
async attach(canvas) {
|
|
125
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
126
|
+
canvas.width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
|
127
|
+
canvas.height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
|
128
|
+
const c = canvas.getContext('webgpu');
|
|
129
|
+
if (!c)
|
|
130
|
+
throw new Error('canvas.getContext("webgpu") 返回空');
|
|
131
|
+
c.configure({ device, format: navigator.gpu.getPreferredCanvasFormat(), alphaMode: 'opaque' });
|
|
132
|
+
gpuCtx = c;
|
|
133
|
+
},
|
|
134
|
+
tick() {
|
|
135
|
+
const useAB = frame % 2 === 0;
|
|
136
|
+
const read = useAB ? sideA : sideB;
|
|
137
|
+
const write = useAB ? sideB : sideA;
|
|
138
|
+
const enc = device.createCommandEncoder();
|
|
139
|
+
const pass = enc.beginComputePass();
|
|
140
|
+
pass.setPipeline(pCounts);
|
|
141
|
+
pass.setBindGroup(0, bgCounts(read));
|
|
142
|
+
pass.dispatchWorkgroups(Math.ceil(N / WG));
|
|
143
|
+
pass.setPipeline(pScan);
|
|
144
|
+
pass.setBindGroup(0, bgScan);
|
|
145
|
+
pass.dispatchWorkgroups(1);
|
|
146
|
+
pass.setPipeline(pScatter);
|
|
147
|
+
pass.setBindGroup(0, bgScatter(read));
|
|
148
|
+
pass.dispatchWorkgroups(Math.ceil(N / WG));
|
|
149
|
+
pass.setPipeline(pForce);
|
|
150
|
+
pass.setBindGroup(0, bgForce(read, write));
|
|
151
|
+
pass.dispatchWorkgroups(Math.ceil(N / WG));
|
|
152
|
+
pass.end();
|
|
153
|
+
device.queue.submit([enc.finish()]);
|
|
154
|
+
if (gpuCtx) {
|
|
155
|
+
const bg = bgRender(write);
|
|
156
|
+
const re = enc2(device, gpuCtx, pRender, bg, N);
|
|
157
|
+
device.queue.submit([re]);
|
|
158
|
+
}
|
|
159
|
+
pp.swap();
|
|
160
|
+
frame++;
|
|
161
|
+
fFrames++;
|
|
162
|
+
const now = performance.now();
|
|
163
|
+
fAcc += now - fLast;
|
|
164
|
+
fLast = now;
|
|
165
|
+
if (fAcc >= 500) {
|
|
166
|
+
lastFps = fFrames / (fAcc / 1000);
|
|
167
|
+
fFrames = 0;
|
|
168
|
+
fAcc = 0;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
stats() { return { fps: lastFps }; },
|
|
172
|
+
buffers() { return { pos: pp.current.pos, vel: pp.current.vel }; },
|
|
173
|
+
destroy() {
|
|
174
|
+
pp.destroy();
|
|
175
|
+
cellCount.destroy();
|
|
176
|
+
cellStart.destroy();
|
|
177
|
+
cellFill.destroy();
|
|
178
|
+
order.destroy();
|
|
179
|
+
uniform.destroy();
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function enc2(device, gpuCtx, pipeline, bg, n) {
|
|
184
|
+
const enc = device.createCommandEncoder();
|
|
185
|
+
const pass = enc.beginRenderPass({
|
|
186
|
+
colorAttachments: [{
|
|
187
|
+
view: gpuCtx.getCurrentTexture().createView(),
|
|
188
|
+
clearValue: { r: 0.012, g: 0.016, b: 0.03, a: 1 },
|
|
189
|
+
loadOp: 'clear', storeOp: 'store',
|
|
190
|
+
}],
|
|
191
|
+
});
|
|
192
|
+
pass.setPipeline(pipeline);
|
|
193
|
+
pass.setBindGroup(0, bg);
|
|
194
|
+
pass.draw(3, n);
|
|
195
|
+
pass.end();
|
|
196
|
+
return enc.finish();
|
|
197
|
+
}
|
|
198
|
+
function boidsWgsl(pointSize) {
|
|
199
|
+
return /* wgsl */ `
|
|
200
|
+
struct Params {
|
|
201
|
+
count: u32, gridSize: u32,
|
|
202
|
+
perception: f32, maxSpeed: f32, wSep: f32, wAli: f32, wCoh: f32,
|
|
203
|
+
dt: f32, worldHalf: f32, _p: f32,
|
|
204
|
+
};
|
|
205
|
+
// 统一绑定布局(模块级唯一声明,四个入口按需引用):
|
|
206
|
+
// 0 uniform | 1 posIn | 2 cellCount(atomic) | 3 cellStart | 4 cellFill(atomic)
|
|
207
|
+
// 5 order | 6 velIn | 7 posOut | 8 velOut
|
|
208
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
209
|
+
@group(0) @binding(1) var<storage, read> posIn: array<vec2f>;
|
|
210
|
+
@group(0) @binding(2) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
211
|
+
@group(0) @binding(3) var<storage, read_write> cellStart: array<u32>;
|
|
212
|
+
@group(0) @binding(4) var<storage, read_write> cellFill: array<atomic<u32>>;
|
|
213
|
+
@group(0) @binding(5) var<storage, read_write> order: array<u32>;
|
|
214
|
+
@group(0) @binding(6) var<storage, read> velIn: array<vec2f>;
|
|
215
|
+
@group(0) @binding(7) var<storage, read_write> posOut: array<vec2f>;
|
|
216
|
+
@group(0) @binding(8) var<storage, read_write> velOut: array<vec2f>;
|
|
217
|
+
|
|
218
|
+
fn cellOf(p: vec2f) -> u32 {
|
|
219
|
+
let g = i32(params.gridSize);
|
|
220
|
+
let span = params.worldHalf * 2.0;
|
|
221
|
+
let cx = clamp(i32(floor((p.x + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
222
|
+
let cy = clamp(i32(floor((p.y + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
223
|
+
return u32(cy) * u32(g) + u32(cx);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@compute @workgroup_size(${WG})
|
|
227
|
+
fn main_counts(@builtin(global_invocation_id) gid: vec3u) {
|
|
228
|
+
let i = gid.x;
|
|
229
|
+
if (i >= params.count) { return; }
|
|
230
|
+
atomicAdd(&cellCount[cellOf(posIn[i])], 1u);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
var<workgroup> partial: array<u32, 256>;
|
|
234
|
+
@compute @workgroup_size(256)
|
|
235
|
+
fn main_scan(@builtin(local_invocation_id) lid: vec3u) {
|
|
236
|
+
let tid = lid.x;
|
|
237
|
+
let cells = params.gridSize * params.gridSize;
|
|
238
|
+
let chunks = (cells + 255u) / 256u;
|
|
239
|
+
var local = 0u;
|
|
240
|
+
for (var c = 0u; c < chunks; c++) {
|
|
241
|
+
let idx = c * 256u + tid;
|
|
242
|
+
if (idx < cells) { local = local + atomicLoad(&cellCount[idx]); }
|
|
243
|
+
}
|
|
244
|
+
partial[tid] = local;
|
|
245
|
+
workgroupBarrier();
|
|
246
|
+
var offset = 1u;
|
|
247
|
+
loop {
|
|
248
|
+
if (offset >= 256u) { break; }
|
|
249
|
+
var v = 0u;
|
|
250
|
+
if (tid >= offset) { v = partial[tid - offset]; }
|
|
251
|
+
workgroupBarrier();
|
|
252
|
+
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
253
|
+
workgroupBarrier();
|
|
254
|
+
offset = offset << 1u;
|
|
255
|
+
}
|
|
256
|
+
var run = 0u;
|
|
257
|
+
if (tid > 0u) { run = partial[tid - 1u]; }
|
|
258
|
+
for (var c = 0u; c < chunks; c++) {
|
|
259
|
+
let idx = c * 256u + tid;
|
|
260
|
+
if (idx < cells) {
|
|
261
|
+
cellStart[idx] = run;
|
|
262
|
+
atomicStore(&cellFill[idx], run);
|
|
263
|
+
run = run + atomicLoad(&cellCount[idx]);
|
|
264
|
+
atomicStore(&cellCount[idx], 0u);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@compute @workgroup_size(${WG})
|
|
270
|
+
fn main_scatter(@builtin(global_invocation_id) gid: vec3u) {
|
|
271
|
+
let i = gid.x;
|
|
272
|
+
if (i >= params.count) { return; }
|
|
273
|
+
let slot = atomicAdd(&cellFill[cellOf(posIn[i])], 1u);
|
|
274
|
+
order[slot] = i;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@compute @workgroup_size(${WG})
|
|
278
|
+
fn main_force(@builtin(global_invocation_id) gid: vec3u) {
|
|
279
|
+
let i = gid.x;
|
|
280
|
+
if (i >= params.count) { return; }
|
|
281
|
+
let myPos = posIn[i];
|
|
282
|
+
let myVel = velIn[i];
|
|
283
|
+
var sep = vec2f(0.0);
|
|
284
|
+
var ali = vec2f(0.0);
|
|
285
|
+
var coh = vec2f(0.0);
|
|
286
|
+
var n = 0u;
|
|
287
|
+
let g = i32(params.gridSize);
|
|
288
|
+
let cellSize = params.worldHalf * 2.0 / f32(g);
|
|
289
|
+
var cx = clamp(i32(floor((myPos.x + params.worldHalf) / cellSize)), 0, g - 1);
|
|
290
|
+
var cy = clamp(i32(floor((myPos.y + params.worldHalf) / cellSize)), 0, g - 1);
|
|
291
|
+
let p2 = params.perception * params.perception;
|
|
292
|
+
|
|
293
|
+
for (var dy = -1; dy <= 1; dy++) {
|
|
294
|
+
for (var dx = -1; dx <= 1; dx++) {
|
|
295
|
+
let nx = cx + dx;
|
|
296
|
+
let ny = cy + dy;
|
|
297
|
+
if (nx < 0 || ny < 0 || nx >= g || ny >= g) { continue; }
|
|
298
|
+
let c = u32(ny) * u32(g) + u32(nx);
|
|
299
|
+
let s = cellStart[c];
|
|
300
|
+
let e = atomicLoad(&cellFill[c]);
|
|
301
|
+
for (var k = s; k < e; k++) {
|
|
302
|
+
let j = order[k];
|
|
303
|
+
if (j == i) { continue; }
|
|
304
|
+
let rel = posIn[j] - myPos;
|
|
305
|
+
let d2 = dot(rel, rel);
|
|
306
|
+
if (d2 > p2) { continue; }
|
|
307
|
+
let d = sqrt(d2) + 1e-5;
|
|
308
|
+
sep = sep + (myPos - posIn[j]) / d;
|
|
309
|
+
ali = ali + velIn[j];
|
|
310
|
+
coh = coh + posIn[j];
|
|
311
|
+
n = n + 1u;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
var vel = myVel;
|
|
317
|
+
if (n > 0u) {
|
|
318
|
+
let nf = f32(n);
|
|
319
|
+
ali = ali / nf;
|
|
320
|
+
coh = coh / nf - myPos;
|
|
321
|
+
vel = myVel + (sep * params.wSep + ali * params.wAli + coh * params.wCoh) * 0.016;
|
|
322
|
+
}
|
|
323
|
+
let sp = length(vel);
|
|
324
|
+
if (sp > params.maxSpeed) { vel = vel / sp * params.maxSpeed; }
|
|
325
|
+
if (sp < params.maxSpeed * 0.35) { vel = vel / max(sp, 1e-5) * params.maxSpeed * 0.35; }
|
|
326
|
+
|
|
327
|
+
var pos = myPos + vel;
|
|
328
|
+
let h = params.worldHalf;
|
|
329
|
+
pos = ((pos + h) % (2.0 * h) + 2.0 * h) % (2.0 * h) - h;
|
|
330
|
+
posOut[i] = pos;
|
|
331
|
+
velOut[i] = vel;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ---- 渲染:朝向速度方向的三角 ----
|
|
335
|
+
struct VsOut {
|
|
336
|
+
@builtin(position) clip: vec4f,
|
|
337
|
+
@location(0) color: vec3f,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
@vertex
|
|
341
|
+
fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) inst: u32) -> VsOut {
|
|
342
|
+
var shape = array<vec2f, 3>(vec2f(${(pointSize * 1.6).toFixed(4)}, 0.0), vec2f(${(-pointSize).toFixed(4)}, ${(pointSize * 0.45).toFixed(4)}), vec2f(${(-pointSize).toFixed(4)}, ${(-pointSize * 0.45).toFixed(4)}));
|
|
343
|
+
let a = atan2(velIn[inst].y, velIn[inst].x);
|
|
344
|
+
let c = cos(a);
|
|
345
|
+
let s = sin(a);
|
|
346
|
+
let l = shape[v];
|
|
347
|
+
var out: VsOut;
|
|
348
|
+
out.clip = vec4f(posIn[inst] + vec2f(l.x * c - l.y * s, l.x * s + l.y * c), 0.0, 1.0);
|
|
349
|
+
let sp = length(velIn[inst]) / params.maxSpeed;
|
|
350
|
+
out.color = mix(vec3f(0.12, 0.2, 0.45), vec3f(0.4, 0.9, 1.0), clamp(sp, 0.0, 1.0));
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@fragment
|
|
355
|
+
fn fs(in: VsOut) -> @location(0) vec4f {
|
|
356
|
+
return vec4f(in.color, 1.0);
|
|
357
|
+
}
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
function hashStr(s) {
|
|
361
|
+
let h = 2166136261;
|
|
362
|
+
for (let i = 0; i < s.length; i++) {
|
|
363
|
+
h ^= s.charCodeAt(i);
|
|
364
|
+
h = Math.imul(h, 16777619);
|
|
365
|
+
}
|
|
366
|
+
return h >>> 0;
|
|
367
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { GpuContext } from "../../core/context.js";
|
|
2
|
+
import { Buffer } from "../../core/buffer.js";
|
|
3
|
+
import { CompileError } from "../../core/errors.js";
|
|
4
|
+
const COLORMAPS = {
|
|
5
|
+
mono: 'vec3f(v)',
|
|
6
|
+
amber: `vec3f(
|
|
7
|
+
1.35 * v * v,
|
|
8
|
+
0.9 * v * v * v + 0.25 * v * (1.0 - v),
|
|
9
|
+
0.15 * v * v * v
|
|
10
|
+
)`,
|
|
11
|
+
ice: `vec3f(0.15 * v * v, 0.55 * v * v + 0.2 * v, 1.1 * v)`,
|
|
12
|
+
duotone: `mix(vec3f(0.02, 0.03, 0.08), vec3f(0.42, 0.78, 1.0), v) + vec3f(0.9, 0.6, 0.25) * v * v * v * 0.6`,
|
|
13
|
+
};
|
|
14
|
+
export function mapFragment(colormap) {
|
|
15
|
+
return /* wgsl */ `
|
|
16
|
+
struct VsOut {
|
|
17
|
+
@builtin(position) pos: vec4f,
|
|
18
|
+
@location(0) uv: vec2f,
|
|
19
|
+
};
|
|
20
|
+
@group(0) @binding(0) var<uniform> vp: vec4f; // w, h, maxV, gamma
|
|
21
|
+
@group(0) @binding(1) var<storage, read> map: array<f32>;
|
|
22
|
+
|
|
23
|
+
@vertex
|
|
24
|
+
fn vs(@builtin(vertex_index) v: u32) -> VsOut {
|
|
25
|
+
var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
|
|
26
|
+
var out: VsOut;
|
|
27
|
+
out.pos = vec4f(p[v], 0.0, 1.0);
|
|
28
|
+
out.uv = (p[v] + vec2f(1.0)) * 0.5;
|
|
29
|
+
out.uv = vec2f(out.uv.x, 1.0 - out.uv.y); // buffer 行 0 = 画面顶部
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@fragment
|
|
34
|
+
fn fs(in: VsOut) -> @location(0) vec4f {
|
|
35
|
+
let w = u32(vp.x);
|
|
36
|
+
let x = min(u32(in.uv.x * vp.x), w - 1u);
|
|
37
|
+
let y = min(u32(in.uv.y * vp.y), u32(vp.y) - 1u);
|
|
38
|
+
let v = pow(clamp(abs(map[y * w + x]) * vp.z, 0.0, 1.0), vp.w);
|
|
39
|
+
let c = ${COLORMAPS[colormap]};
|
|
40
|
+
return vec4f(c, 1.0);
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
export class MapRenderer {
|
|
45
|
+
#ctx;
|
|
46
|
+
#gpuCtx;
|
|
47
|
+
#pipeline;
|
|
48
|
+
#uniform;
|
|
49
|
+
#bgCache = new Map();
|
|
50
|
+
#ids = new WeakMap();
|
|
51
|
+
constructor(ctx, gpuCtx, pipeline, uniform) {
|
|
52
|
+
this.#ctx = ctx;
|
|
53
|
+
this.#gpuCtx = gpuCtx;
|
|
54
|
+
this.#pipeline = pipeline;
|
|
55
|
+
this.#uniform = uniform;
|
|
56
|
+
}
|
|
57
|
+
static async create(canvas, opts) {
|
|
58
|
+
const ctx = await GpuContext.get();
|
|
59
|
+
const gpuCtx = canvas.getContext('webgpu');
|
|
60
|
+
if (!gpuCtx)
|
|
61
|
+
throw new Error('canvas.getContext("webgpu") 返回空');
|
|
62
|
+
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
63
|
+
gpuCtx.configure({ device: ctx.device, format, alphaMode: 'opaque' });
|
|
64
|
+
const module = ctx.device.createShaderModule({ code: mapFragment(opts.colormap), label: 'life-map-render' });
|
|
65
|
+
const info = await module.getCompilationInfo();
|
|
66
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
67
|
+
if (errors.length > 0)
|
|
68
|
+
throw new CompileError('life-map-render', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
69
|
+
const pipeline = ctx.device.createRenderPipeline({
|
|
70
|
+
layout: 'auto',
|
|
71
|
+
vertex: { module, entryPoint: 'vs' },
|
|
72
|
+
fragment: { module, entryPoint: 'fs', targets: [{ format }] },
|
|
73
|
+
primitive: { topology: 'triangle-list' },
|
|
74
|
+
});
|
|
75
|
+
const uniform = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
76
|
+
ctx.device.queue.writeBuffer(uniform, 0, new Float32Array([opts.width, opts.height, opts.maxV, opts.gamma ?? 1.0]));
|
|
77
|
+
return new MapRenderer(ctx, gpuCtx, pipeline, uniform);
|
|
78
|
+
}
|
|
79
|
+
render(map) {
|
|
80
|
+
let id = this.#ids.get(map.gpuBuffer);
|
|
81
|
+
if (id === undefined) {
|
|
82
|
+
id = this.#bgCache.size + 1;
|
|
83
|
+
this.#ids.set(map.gpuBuffer, id);
|
|
84
|
+
}
|
|
85
|
+
let bg = this.#bgCache.get(id);
|
|
86
|
+
if (!bg) {
|
|
87
|
+
bg = this.#ctx.device.createBindGroup({
|
|
88
|
+
layout: this.#pipeline.getBindGroupLayout(0),
|
|
89
|
+
entries: [
|
|
90
|
+
{ binding: 0, resource: { buffer: this.#uniform } },
|
|
91
|
+
{ binding: 1, resource: { buffer: map.gpuBuffer } },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
this.#bgCache.set(id, bg);
|
|
95
|
+
}
|
|
96
|
+
const enc = this.#ctx.device.createCommandEncoder();
|
|
97
|
+
const pass = enc.beginRenderPass({
|
|
98
|
+
colorAttachments: [{
|
|
99
|
+
view: this.#gpuCtx.getCurrentTexture().createView(),
|
|
100
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
101
|
+
loadOp: 'clear', storeOp: 'store',
|
|
102
|
+
}],
|
|
103
|
+
});
|
|
104
|
+
pass.setPipeline(this.#pipeline);
|
|
105
|
+
pass.setBindGroup(0, bg);
|
|
106
|
+
pass.draw(3);
|
|
107
|
+
pass.end();
|
|
108
|
+
this.#ctx.device.queue.submit([enc.finish()]);
|
|
109
|
+
}
|
|
110
|
+
}
|