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.
Files changed (42) hide show
  1. package/README.md +7 -0
  2. package/README.zh-CN.md +3 -3
  3. package/dist/core/buffer.d.ts +1 -1
  4. package/dist/core/buffer.js +109 -0
  5. package/dist/core/context.d.ts +2 -0
  6. package/dist/core/context.js +62 -0
  7. package/dist/core/errors.js +42 -0
  8. package/dist/core/kernel.js +197 -0
  9. package/dist/core/layout.d.ts +5 -1
  10. package/dist/core/layout.js +69 -0
  11. package/dist/core/pingpong.js +61 -0
  12. package/dist/core/raw.js +38 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +11 -1510
  15. package/dist/interop/three.js +29 -0
  16. package/dist/media.js +70 -0
  17. package/dist/observe.d.ts +19 -0
  18. package/dist/observe.js +76 -0
  19. package/dist/packs/fields/index.js +230 -0
  20. package/dist/packs/image/index.js +250 -0
  21. package/dist/packs/life/boids.js +367 -0
  22. package/dist/packs/life/index.js +8 -0
  23. package/dist/packs/life/map.js +110 -0
  24. package/dist/packs/life/physarum.js +228 -0
  25. package/dist/packs/life/tentacles.js +238 -0
  26. package/dist/packs/life/turing.js +203 -0
  27. package/dist/packs/particles/config.d.ts +1 -0
  28. package/dist/packs/particles/config.js +36 -0
  29. package/dist/packs/particles/grid.js +248 -0
  30. package/dist/packs/particles/index.js +361 -0
  31. package/dist/packs/particles/presets.js +75 -0
  32. package/dist/packs/particles/render.js +116 -0
  33. package/dist/packs/particles/wgsl.js +175 -0
  34. package/dist/react/index.js +36 -0
  35. package/dist/vite.js +27 -28
  36. package/package.json +1 -1
  37. package/dist/fields.js +0 -585
  38. package/dist/image.js +0 -318
  39. package/dist/life.js +0 -1407
  40. package/dist/particles.js +0 -1245
  41. package/dist/react.js +0 -1289
  42. 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,8 @@
1
+ /**
2
+ * life 包 —— 人工生命合集。每个模拟都是"简单规则 → 涌现复杂",
3
+ * 引擎与渲染管线与 particles 包同源,每个模拟都是一条可发布的视频素材。
4
+ */
5
+ export { turing } from "./turing.js";
6
+ export { physarum } from "./physarum.js";
7
+ export { boids } from "./boids.js";
8
+ export { tentacles } from "./tentacles.js";
@@ -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
+ }