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,228 @@
|
|
|
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 { MapRenderer } from "./map.js";
|
|
6
|
+
import { mulberry32 } from "../particles/presets.js";
|
|
7
|
+
const AWG = 32; // count(u32) pad(u32) + sensorAngle, sensorDist, turnAngle, step, deposit, worldHalf (f32×6) = 32
|
|
8
|
+
export async function physarum(config = {}) {
|
|
9
|
+
const { agents: N = 100_000, mapSize = 1024, sensorAngle = 0.5, sensorDist = 0.012, turnAngle = 0.45, step = 0.003, decay = 0.06, seed = 'physarum', colormap = 'amber', } = config;
|
|
10
|
+
const seedHash = typeof seed === 'string' ? hashStr(seed) : (seed ?? 7);
|
|
11
|
+
const ctx = await GpuContext.get();
|
|
12
|
+
const device = ctx.device;
|
|
13
|
+
// —— 智能体:中心圆盘出发,朝外 ——
|
|
14
|
+
const posBuf = await Buffer.create('vec2f', N);
|
|
15
|
+
const angBuf = await Buffer.create('f32', N);
|
|
16
|
+
{
|
|
17
|
+
const rand = mulberry32(seedHash);
|
|
18
|
+
const p = new Float32Array(N * 2);
|
|
19
|
+
const a = new Float32Array(N);
|
|
20
|
+
for (let i = 0; i < N; i++) {
|
|
21
|
+
const t = rand() * Math.PI * 2;
|
|
22
|
+
const r = Math.sqrt(rand()) * 0.08;
|
|
23
|
+
p[i * 2] = Math.cos(t) * r;
|
|
24
|
+
p[i * 2 + 1] = Math.sin(t) * r;
|
|
25
|
+
a[i] = t;
|
|
26
|
+
}
|
|
27
|
+
posBuf.write(p);
|
|
28
|
+
angBuf.write(a);
|
|
29
|
+
}
|
|
30
|
+
// —— 信息素图(双缓冲) ——
|
|
31
|
+
const trail = await PingPong.create({ t: 'f32' }, mapSize * mapSize);
|
|
32
|
+
const trailA = trail.current.t;
|
|
33
|
+
const trailB = trail.other.t;
|
|
34
|
+
const uniform = device.createBuffer({ size: AWG, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
35
|
+
const writeUniform = () => {
|
|
36
|
+
const buf = new ArrayBuffer(AWG);
|
|
37
|
+
const v = new DataView(buf);
|
|
38
|
+
v.setUint32(0, N, true);
|
|
39
|
+
v.setUint32(4, 0, true);
|
|
40
|
+
v.setFloat32(8, sensorAngle, true);
|
|
41
|
+
v.setFloat32(12, sensorDist, true);
|
|
42
|
+
v.setFloat32(16, turnAngle, true);
|
|
43
|
+
v.setFloat32(20, step, true);
|
|
44
|
+
v.setFloat32(24, 1.0, true); // deposit
|
|
45
|
+
v.setFloat32(28, 1.0, true); // worldHalf
|
|
46
|
+
device.queue.writeBuffer(uniform, 0, buf);
|
|
47
|
+
};
|
|
48
|
+
writeUniform();
|
|
49
|
+
const diffuseUniform = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
50
|
+
device.queue.writeBuffer(diffuseUniform, 0, new Uint32Array([mapSize, mapSize]));
|
|
51
|
+
// decay 写进 diffuse kernel 的第二个 uniform?并成 16B:w,h,decayFrac,pad
|
|
52
|
+
device.queue.writeBuffer(diffuseUniform, 8, new Float32Array([1 - decay, 0]));
|
|
53
|
+
const compile = async (code, label) => {
|
|
54
|
+
const m = device.createShaderModule({ code, label });
|
|
55
|
+
const info = await m.getCompilationInfo();
|
|
56
|
+
const errors = info.messages.filter((x) => x.type === 'error');
|
|
57
|
+
if (errors.length > 0)
|
|
58
|
+
throw new CompileError(label, errors.map((x) => ({ line: x.lineNum, msg: x.message })), 0);
|
|
59
|
+
return m;
|
|
60
|
+
};
|
|
61
|
+
const mAgent = await compile(agentWgsl(mapSize), 'physarum-agent');
|
|
62
|
+
const mDiffuse = await compile(diffuseWgsl(), 'physarum-diffuse');
|
|
63
|
+
const pAgent = device.createComputePipeline({ layout: 'auto', compute: { module: mAgent, entryPoint: 'main' } });
|
|
64
|
+
const pDiffuse = device.createComputePipeline({ layout: 'auto', compute: { module: mDiffuse, entryPoint: 'main' } });
|
|
65
|
+
const bgAgent = (t) => device.createBindGroup({
|
|
66
|
+
layout: pAgent.getBindGroupLayout(0),
|
|
67
|
+
entries: [
|
|
68
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
69
|
+
{ binding: 1, resource: { buffer: posBuf.gpuBuffer } },
|
|
70
|
+
{ binding: 2, resource: { buffer: angBuf.gpuBuffer } },
|
|
71
|
+
{ binding: 3, resource: { buffer: t.gpuBuffer } },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
const bgDiffuse = (read, write) => device.createBindGroup({
|
|
75
|
+
layout: pDiffuse.getBindGroupLayout(0),
|
|
76
|
+
entries: [
|
|
77
|
+
{ binding: 0, resource: { buffer: diffuseUniform } },
|
|
78
|
+
{ binding: 1, resource: { buffer: read.gpuBuffer } },
|
|
79
|
+
{ binding: 2, resource: { buffer: write.gpuBuffer } },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
let renderer = null;
|
|
83
|
+
let frame = 0;
|
|
84
|
+
let lastFps = 0;
|
|
85
|
+
let fFrames = 0;
|
|
86
|
+
let fAcc = 0;
|
|
87
|
+
let fLast = performance.now();
|
|
88
|
+
return {
|
|
89
|
+
async attach(canvas) {
|
|
90
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
91
|
+
canvas.width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
|
92
|
+
canvas.height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
|
93
|
+
renderer = await MapRenderer.create(canvas, { width: mapSize, height: mapSize, maxV: 6.0, gamma: 0.6, colormap });
|
|
94
|
+
},
|
|
95
|
+
tick() {
|
|
96
|
+
const readT = trail.current.t;
|
|
97
|
+
const writeT = trail.other.t;
|
|
98
|
+
const enc = device.createCommandEncoder();
|
|
99
|
+
const pass = enc.beginComputePass();
|
|
100
|
+
// 智能体:感知/转向/移动/沉积(在当前 trail 上就地沉积)
|
|
101
|
+
pass.setPipeline(pAgent);
|
|
102
|
+
pass.setBindGroup(0, bgAgent(readT));
|
|
103
|
+
pass.dispatchWorkgroups(Math.ceil(N / 64));
|
|
104
|
+
// 扩散 + 衰减 → 另一侧
|
|
105
|
+
pass.setPipeline(pDiffuse);
|
|
106
|
+
pass.setBindGroup(0, bgDiffuse(readT, writeT));
|
|
107
|
+
pass.dispatchWorkgroups(Math.ceil((mapSize * mapSize) / 64));
|
|
108
|
+
pass.end();
|
|
109
|
+
device.queue.submit([enc.finish()]);
|
|
110
|
+
renderer?.render(writeT);
|
|
111
|
+
trail.swap();
|
|
112
|
+
frame++;
|
|
113
|
+
fFrames++;
|
|
114
|
+
const now = performance.now();
|
|
115
|
+
fAcc += now - fLast;
|
|
116
|
+
fLast = now;
|
|
117
|
+
if (fAcc >= 500) {
|
|
118
|
+
lastFps = fFrames / (fAcc / 1000);
|
|
119
|
+
fFrames = 0;
|
|
120
|
+
fAcc = 0;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
stats() { return { fps: lastFps }; },
|
|
124
|
+
async sampleTrail() {
|
|
125
|
+
return (await trail.current.t.read());
|
|
126
|
+
},
|
|
127
|
+
destroy() {
|
|
128
|
+
posBuf.destroy();
|
|
129
|
+
angBuf.destroy();
|
|
130
|
+
trail.destroy();
|
|
131
|
+
uniform.destroy();
|
|
132
|
+
diffuseUniform.destroy();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function agentWgsl(trailW) {
|
|
137
|
+
return /* wgsl */ `
|
|
138
|
+
const TRAIL_W: u32 = ${trailW}u;
|
|
139
|
+
const TRAIL_MASK: u32 = ${trailW - 1}u;
|
|
140
|
+
struct Params {
|
|
141
|
+
count: u32, _pad: u32,
|
|
142
|
+
sensorAngle: f32, sensorDist: f32, turnAngle: f32, step: f32,
|
|
143
|
+
deposit: f32, worldHalf: f32,
|
|
144
|
+
};
|
|
145
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
146
|
+
@group(0) @binding(1) var<storage, read_write> pos: array<vec2f>;
|
|
147
|
+
@group(0) @binding(2) var<storage, read_write> ang: array<f32>;
|
|
148
|
+
@group(0) @binding(3) var<storage, read_write> trail: array<f32>;
|
|
149
|
+
|
|
150
|
+
fn texel(p: vec2f) -> u32 {
|
|
151
|
+
let x = min(u32((p.x * 0.5 + 0.5) * f32(TRAIL_W)), TRAIL_MASK);
|
|
152
|
+
let y = min(u32((p.y * 0.5 + 0.5) * f32(TRAIL_W)), TRAIL_MASK);
|
|
153
|
+
return y * TRAIL_W + x;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn sense(p: vec2f, a: f32) -> f32 {
|
|
157
|
+
return trail[texel(p + vec2f(cos(a), sin(a)) * params.sensorDist)];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@compute @workgroup_size(64)
|
|
161
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
162
|
+
let i = gid.x;
|
|
163
|
+
if (i >= params.count) { return; }
|
|
164
|
+
let p = pos[i];
|
|
165
|
+
var a = ang[i];
|
|
166
|
+
|
|
167
|
+
let fwd = sense(p, a);
|
|
168
|
+
let left = sense(p, a - params.sensorAngle);
|
|
169
|
+
let right = sense(p, a + params.sensorAngle);
|
|
170
|
+
|
|
171
|
+
let jitter = (f32((i * 2654435761u + 1u) % 100u) / 100.0 - 0.5) * 0.1;
|
|
172
|
+
if (fwd > left && fwd > right) {
|
|
173
|
+
// 直行
|
|
174
|
+
} else if (left > right) {
|
|
175
|
+
a = a - params.turnAngle;
|
|
176
|
+
} else if (right > left) {
|
|
177
|
+
a = a + params.turnAngle;
|
|
178
|
+
}
|
|
179
|
+
a = a + jitter * 0.1;
|
|
180
|
+
|
|
181
|
+
var np = p + vec2f(cos(a), sin(a)) * params.step;
|
|
182
|
+
let h = params.worldHalf;
|
|
183
|
+
if (abs(np.x) > h || abs(np.y) > h) {
|
|
184
|
+
np = clamp(np, vec2f(-h), vec2f(h));
|
|
185
|
+
a = a + 3.14159265; // 撞墙掉头
|
|
186
|
+
}
|
|
187
|
+
pos[i] = np;
|
|
188
|
+
ang[i] = a;
|
|
189
|
+
let t = texel(np);
|
|
190
|
+
trail[t] = trail[t] + params.deposit;
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
function diffuseWgsl() {
|
|
195
|
+
return /* wgsl */ `
|
|
196
|
+
@group(0) @binding(0) var<uniform> vp: vec4f; // w, h, keep(1-decay), pad
|
|
197
|
+
@group(0) @binding(1) var<storage, read> src: array<f32>;
|
|
198
|
+
@group(0) @binding(2) var<storage, read_write> dst: array<f32>;
|
|
199
|
+
|
|
200
|
+
@compute @workgroup_size(64)
|
|
201
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
202
|
+
let i = gid.x;
|
|
203
|
+
let w = u32(vp.x);
|
|
204
|
+
let h = u32(vp.y);
|
|
205
|
+
if (i >= w * h) { return; }
|
|
206
|
+
let x = i32(i % w);
|
|
207
|
+
let y = i32(i / w);
|
|
208
|
+
|
|
209
|
+
// 4 邻域 + 自身 混合(扩散),再乘 keep(衰减)
|
|
210
|
+
let c = src[i];
|
|
211
|
+
var s = c * 2.0;
|
|
212
|
+
s = s + src[u32(clamp(y - 1, 0, i32(h) - 1)) * w + u32(clamp(x, 0, i32(w) - 1))];
|
|
213
|
+
s = s + src[u32(clamp(y + 1, 0, i32(h) - 1)) * w + u32(clamp(x, 0, i32(w) - 1))];
|
|
214
|
+
s = s + src[u32(clamp(y, 0, i32(h) - 1)) * w + u32(clamp(x - 1, 0, i32(w) - 1))];
|
|
215
|
+
s = s + src[u32(clamp(y, 0, i32(h) - 1)) * w + u32(clamp(x + 1, 0, i32(w) - 1))];
|
|
216
|
+
let blurred = s / 6.0;
|
|
217
|
+
dst[i] = mix(blurred, c, 0.5) * vp.z;
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
function hashStr(s) {
|
|
222
|
+
let h = 2166136261;
|
|
223
|
+
for (let i = 0; i < s.length; i++) {
|
|
224
|
+
h ^= s.charCodeAt(i);
|
|
225
|
+
h = Math.imul(h, 16777619);
|
|
226
|
+
}
|
|
227
|
+
return h >>> 0;
|
|
228
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
const USIZE = 32;
|
|
6
|
+
export async function tentacles(config = {}) {
|
|
7
|
+
const { chains = 48, segments = 64, segLen = 0.018, gravity = 0.00035, damping = 0.985, iterations = 10, thickness = 0.006, colorCycle = 0.35, } = config;
|
|
8
|
+
const N = chains * segments;
|
|
9
|
+
const ctx = await GpuContext.get();
|
|
10
|
+
const device = ctx.device;
|
|
11
|
+
const pp = await PingPong.create({ pos: 'vec2f' }, N);
|
|
12
|
+
const prev = await Buffer.create('vec2f', N);
|
|
13
|
+
{
|
|
14
|
+
const p = new Float32Array(N * 2);
|
|
15
|
+
pp.current.pos.write(p);
|
|
16
|
+
prev.write(p);
|
|
17
|
+
}
|
|
18
|
+
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
19
|
+
let time = 0;
|
|
20
|
+
const writeUniform = () => {
|
|
21
|
+
const b = new ArrayBuffer(USIZE);
|
|
22
|
+
const v = new DataView(b);
|
|
23
|
+
v.setUint32(0, N, true);
|
|
24
|
+
v.setUint32(4, segments, true);
|
|
25
|
+
v.setFloat32(8, segLen, true);
|
|
26
|
+
v.setFloat32(12, gravity, true);
|
|
27
|
+
v.setFloat32(16, damping, true);
|
|
28
|
+
v.setFloat32(20, time, true);
|
|
29
|
+
v.setFloat32(24, 0.45, true); // anchorR
|
|
30
|
+
v.setUint32(28, iterations, true);
|
|
31
|
+
device.queue.writeBuffer(uniform, 0, b);
|
|
32
|
+
};
|
|
33
|
+
const module = device.createShaderModule({ code: tentaclesWgsl(thickness, colorCycle), label: 'tentacles' });
|
|
34
|
+
const info = await module.getCompilationInfo();
|
|
35
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
36
|
+
if (errors.length > 0)
|
|
37
|
+
throw new CompileError('tentacles', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
38
|
+
const pIntegrate = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_integrate' } });
|
|
39
|
+
const pConstraint = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main_constraint' } });
|
|
40
|
+
const pRender = device.createRenderPipeline({
|
|
41
|
+
layout: 'auto',
|
|
42
|
+
vertex: { module, entryPoint: 'vs' },
|
|
43
|
+
fragment: { module, entryPoint: 'fs', targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }] },
|
|
44
|
+
primitive: { topology: 'triangle-list' },
|
|
45
|
+
});
|
|
46
|
+
const bgIntegrate = (read, write) => device.createBindGroup({
|
|
47
|
+
layout: pIntegrate.getBindGroupLayout(0),
|
|
48
|
+
entries: [
|
|
49
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
50
|
+
{ binding: 1, resource: { buffer: read.gpuBuffer } },
|
|
51
|
+
{ binding: 2, resource: { buffer: prev.gpuBuffer } },
|
|
52
|
+
{ binding: 3, resource: { buffer: write.gpuBuffer } },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
const bgConstraint = (read, write) => device.createBindGroup({
|
|
56
|
+
layout: pConstraint.getBindGroupLayout(0),
|
|
57
|
+
entries: [
|
|
58
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
59
|
+
{ binding: 1, resource: { buffer: read.gpuBuffer } },
|
|
60
|
+
{ binding: 2, resource: { buffer: write.gpuBuffer } },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
const bgRenderCache = new Map();
|
|
64
|
+
const ids = new WeakMap();
|
|
65
|
+
const bgRender = (pos) => {
|
|
66
|
+
let id = ids.get(pos.gpuBuffer);
|
|
67
|
+
if (id === undefined) {
|
|
68
|
+
id = bgRenderCache.size + 1;
|
|
69
|
+
ids.set(pos.gpuBuffer, id);
|
|
70
|
+
}
|
|
71
|
+
let bg = bgRenderCache.get(id);
|
|
72
|
+
if (!bg) {
|
|
73
|
+
bg = device.createBindGroup({
|
|
74
|
+
layout: pRender.getBindGroupLayout(0),
|
|
75
|
+
entries: [
|
|
76
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
77
|
+
{ binding: 1, resource: { buffer: pos.gpuBuffer } },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
bgRenderCache.set(id, bg);
|
|
81
|
+
}
|
|
82
|
+
return bg;
|
|
83
|
+
};
|
|
84
|
+
let gpuCtx = null;
|
|
85
|
+
let frame = 0;
|
|
86
|
+
let lastFps = 0;
|
|
87
|
+
let fFrames = 0;
|
|
88
|
+
let fAcc = 0;
|
|
89
|
+
let fLast = performance.now();
|
|
90
|
+
return {
|
|
91
|
+
async attach(canvas) {
|
|
92
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
93
|
+
canvas.width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
|
94
|
+
canvas.height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
|
95
|
+
const c = canvas.getContext('webgpu');
|
|
96
|
+
if (!c)
|
|
97
|
+
throw new Error('canvas.getContext("webgpu") 返回空');
|
|
98
|
+
c.configure({ device, format: navigator.gpu.getPreferredCanvasFormat(), alphaMode: 'opaque' });
|
|
99
|
+
gpuCtx = c;
|
|
100
|
+
},
|
|
101
|
+
tick() {
|
|
102
|
+
time += 1 / 60;
|
|
103
|
+
writeUniform();
|
|
104
|
+
// 积分:读 current 写 other,swap 后 current = 最新
|
|
105
|
+
const enc = device.createCommandEncoder();
|
|
106
|
+
const pass = enc.beginComputePass();
|
|
107
|
+
pass.setPipeline(pIntegrate);
|
|
108
|
+
pass.setBindGroup(0, bgIntegrate(pp.current.pos, pp.other.pos));
|
|
109
|
+
pass.dispatchWorkgroups(Math.ceil(N / 64));
|
|
110
|
+
pass.end();
|
|
111
|
+
device.queue.submit([enc.finish()]);
|
|
112
|
+
pp.swap();
|
|
113
|
+
// 约束:每轮 ping-pong,杜绝同 dispatch 内相邻节点竞态
|
|
114
|
+
const enc2 = device.createCommandEncoder();
|
|
115
|
+
const pass2 = enc2.beginComputePass();
|
|
116
|
+
pass2.setPipeline(pConstraint);
|
|
117
|
+
for (let r = 0; r < iterations; r++) {
|
|
118
|
+
pass2.setBindGroup(0, bgConstraint(pp.current.pos, pp.other.pos));
|
|
119
|
+
pass2.dispatchWorkgroups(Math.ceil(N / 64));
|
|
120
|
+
pp.swap();
|
|
121
|
+
}
|
|
122
|
+
pass2.end();
|
|
123
|
+
device.queue.submit([enc2.finish()]);
|
|
124
|
+
if (gpuCtx) {
|
|
125
|
+
const re = device.createCommandEncoder();
|
|
126
|
+
const rp = re.beginRenderPass({
|
|
127
|
+
colorAttachments: [{
|
|
128
|
+
view: gpuCtx.getCurrentTexture().createView(),
|
|
129
|
+
clearValue: { r: 0.012, g: 0.016, b: 0.03, a: 1 },
|
|
130
|
+
loadOp: 'clear', storeOp: 'store',
|
|
131
|
+
}],
|
|
132
|
+
});
|
|
133
|
+
rp.setPipeline(pRender);
|
|
134
|
+
rp.setBindGroup(0, bgRender(pp.current.pos));
|
|
135
|
+
rp.draw(6, N);
|
|
136
|
+
rp.end();
|
|
137
|
+
device.queue.submit([re.finish()]);
|
|
138
|
+
}
|
|
139
|
+
frame++;
|
|
140
|
+
fFrames++;
|
|
141
|
+
const now = performance.now();
|
|
142
|
+
fAcc += now - fLast;
|
|
143
|
+
fLast = now;
|
|
144
|
+
if (fAcc >= 500) {
|
|
145
|
+
lastFps = fFrames / (fAcc / 1000);
|
|
146
|
+
fFrames = 0;
|
|
147
|
+
fAcc = 0;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
stats() { return { fps: lastFps }; },
|
|
151
|
+
buffers() { return { pos: pp.current.pos }; },
|
|
152
|
+
destroy() {
|
|
153
|
+
pp.destroy();
|
|
154
|
+
prev.destroy();
|
|
155
|
+
uniform.destroy();
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function tentaclesWgsl(thickness, colorCycle) {
|
|
160
|
+
return /* wgsl */ `
|
|
161
|
+
struct Params {
|
|
162
|
+
count: u32, segments: u32,
|
|
163
|
+
segLen: f32, gravity: f32, damping: f32, time: f32,
|
|
164
|
+
anchorR: f32, iterations: u32,
|
|
165
|
+
};
|
|
166
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
167
|
+
@group(0) @binding(1) var<storage, read> posIn: array<vec2f>;
|
|
168
|
+
@group(0) @binding(2) var<storage, read_write> posOut: array<vec2f>;
|
|
169
|
+
@group(0) @binding(3) var<storage, read_write> prev: array<vec2f>;
|
|
170
|
+
|
|
171
|
+
fn anchorPos(chain: u32, t: f32) -> vec2f {
|
|
172
|
+
let phase = f32(chain) * 2.399963; // 黄金角:链与链永不重叠排布
|
|
173
|
+
let x = cos(t * 0.7 + phase) * params.anchorR;
|
|
174
|
+
let y = sin(t * 1.13 + phase * 1.7) * params.anchorR * 0.55;
|
|
175
|
+
return vec2f(x, y);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@compute @workgroup_size(64)
|
|
179
|
+
fn main_integrate(@builtin(global_invocation_id) gid: vec3u) {
|
|
180
|
+
let i = gid.x;
|
|
181
|
+
if (i >= params.count) { return; }
|
|
182
|
+
let segs = params.segments;
|
|
183
|
+
let k = i % segs;
|
|
184
|
+
let chain = i / segs;
|
|
185
|
+
if (k == 0u) {
|
|
186
|
+
let anchor = anchorPos(chain, params.time);
|
|
187
|
+
posOut[i] = anchor;
|
|
188
|
+
prev[i] = anchor;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
let vel = (posIn[i] - prev[i]) * params.damping;
|
|
192
|
+
prev[i] = posIn[i];
|
|
193
|
+
posOut[i] = posIn[i] + vel + vec2f(0.0, -params.gravity);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 跟随约束:读 posIn 写 posOut(单线程单数据,零竞态)
|
|
197
|
+
@compute @workgroup_size(64)
|
|
198
|
+
fn main_constraint(@builtin(global_invocation_id) gid: vec3u) {
|
|
199
|
+
let i = gid.x;
|
|
200
|
+
if (i >= params.count) { return; }
|
|
201
|
+
let segs = params.segments;
|
|
202
|
+
let k = i % segs;
|
|
203
|
+
if (k == 0u) { posOut[i] = posIn[i]; return; }
|
|
204
|
+
let d = posIn[i] - posIn[i - 1u];
|
|
205
|
+
let l = length(d) + 1e-6;
|
|
206
|
+
posOut[i] = posIn[i - 1u] + d / l * params.segLen;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
struct VsOut {
|
|
210
|
+
@builtin(position) clip: vec4f,
|
|
211
|
+
@location(0) color: vec3f,
|
|
212
|
+
};
|
|
213
|
+
@group(0) @binding(0) var<uniform> vp: Params;
|
|
214
|
+
@group(0) @binding(1) var<storage, read> posR: array<vec2f>;
|
|
215
|
+
|
|
216
|
+
@vertex
|
|
217
|
+
fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) inst: u32) -> VsOut {
|
|
218
|
+
let segs = vp.segments;
|
|
219
|
+
let k = inst % segs;
|
|
220
|
+
let chain = inst / segs;
|
|
221
|
+
let taper = 1.0 - f32(k) / f32(segs);
|
|
222
|
+
let r = ${(thickness * 1.1).toFixed(5)} * (0.25 + 0.75 * taper);
|
|
223
|
+
var corner = array<vec2f, 6>(
|
|
224
|
+
vec2f(-r, -r), vec2f(r, -r), vec2f(-r, r),
|
|
225
|
+
vec2f(-r, r), vec2f(r, -r), vec2f(r, r)
|
|
226
|
+
);
|
|
227
|
+
var out: VsOut;
|
|
228
|
+
out.clip = vec4f(posR[inst] + corner[v], 0.0, 1.0);
|
|
229
|
+
out.color = 0.5 + 0.5 * cos(f32(chain) * ${colorCycle.toFixed(3)} + vec3f(0.0, 2.1, 4.2)) * (0.4 + 0.6 * taper);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@fragment
|
|
234
|
+
fn fs(in: VsOut) -> @location(0) vec4f {
|
|
235
|
+
return vec4f(in.color, 1.0);
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
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 { MapRenderer } from "./map.js";
|
|
6
|
+
import { mulberry32 } from "../particles/presets.js";
|
|
7
|
+
const PRESETS = {
|
|
8
|
+
coral: { f: 0.0545, k: 0.062 },
|
|
9
|
+
mitosis: { f: 0.0367, k: 0.0649 },
|
|
10
|
+
spots: { f: 0.03, k: 0.062 },
|
|
11
|
+
waves: { f: 0.014, k: 0.045 },
|
|
12
|
+
};
|
|
13
|
+
const USIZE = 32; // w,h (u32×2) + f,k,dA,dB,dt (f32×5) = 28 → 32
|
|
14
|
+
export async function turing(config = {}) {
|
|
15
|
+
const size = config.size ?? 512;
|
|
16
|
+
const preset = config.preset ?? 'coral';
|
|
17
|
+
const p = preset === 'custom' ? { f: config.feed ?? 0.037, k: config.kill ?? 0.06 } : PRESETS[preset];
|
|
18
|
+
const steps = config.steps ?? 12;
|
|
19
|
+
const seedHash = typeof config.seed === 'string' ? hashStr(config.seed) : (config.seed ?? 42);
|
|
20
|
+
const colormap = config.colormap ?? 'duotone';
|
|
21
|
+
const ctx = await GpuContext.get();
|
|
22
|
+
const device = ctx.device;
|
|
23
|
+
const state = await PingPong.create({ st: 'vec2f' }, size * size);
|
|
24
|
+
const sideA = state.current.st;
|
|
25
|
+
const sideB = state.other.st;
|
|
26
|
+
// 初始化:A=1,B 在随机小方块里 =1
|
|
27
|
+
{
|
|
28
|
+
const rand = mulberry32(seedHash);
|
|
29
|
+
const init = new Float32Array(size * size * 2);
|
|
30
|
+
for (let i = 0; i < size * size; i++)
|
|
31
|
+
init[i * 2] = 1;
|
|
32
|
+
for (let s = 0; s < 10; s++) {
|
|
33
|
+
const cx = 40 + Math.floor(rand() * (size - 80));
|
|
34
|
+
const cy = 40 + Math.floor(rand() * (size - 80));
|
|
35
|
+
const r = 3 + Math.floor(rand() * 6);
|
|
36
|
+
for (let y = cy - r; y <= cy + r; y++) {
|
|
37
|
+
for (let x = cx - r; x <= cx + r; x++) {
|
|
38
|
+
const idx = ((y + size) % size) * size + ((x + size) % size);
|
|
39
|
+
init[idx * 2] = 0.5;
|
|
40
|
+
init[idx * 2 + 1] = 0.25;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
sideA.write(init);
|
|
45
|
+
}
|
|
46
|
+
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
47
|
+
const params = { f: p.f, k: p.k, dA: 1.0, dB: 0.5, dt: 1.0 };
|
|
48
|
+
const writeUniform = () => {
|
|
49
|
+
const buf = new ArrayBuffer(USIZE);
|
|
50
|
+
const v = new DataView(buf);
|
|
51
|
+
v.setUint32(0, size, true);
|
|
52
|
+
v.setUint32(4, size, true);
|
|
53
|
+
v.setFloat32(8, params.f, true);
|
|
54
|
+
v.setFloat32(12, params.k, true);
|
|
55
|
+
v.setFloat32(16, params.dA, true);
|
|
56
|
+
v.setFloat32(20, params.dB, true);
|
|
57
|
+
v.setFloat32(24, params.dt, true);
|
|
58
|
+
device.queue.writeBuffer(uniform, 0, buf);
|
|
59
|
+
};
|
|
60
|
+
writeUniform();
|
|
61
|
+
const module = device.createShaderModule({ code: updateWgsl(), label: 'turing-update' });
|
|
62
|
+
const info = await module.getCompilationInfo();
|
|
63
|
+
const errors = info.messages.filter((m) => m.type === 'error');
|
|
64
|
+
if (errors.length > 0)
|
|
65
|
+
throw new CompileError('turing-update', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
66
|
+
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
67
|
+
const bg = (read, write) => device.createBindGroup({
|
|
68
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
69
|
+
entries: [
|
|
70
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
71
|
+
{ binding: 1, resource: { buffer: read.gpuBuffer } },
|
|
72
|
+
{ binding: 2, resource: { buffer: write.gpuBuffer } },
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
const bgAB = bg(sideA, sideB);
|
|
76
|
+
const bgBA = bg(sideB, sideA);
|
|
77
|
+
let renderer = null;
|
|
78
|
+
let frame = 0;
|
|
79
|
+
let lastFps = 0;
|
|
80
|
+
let fFrames = 0;
|
|
81
|
+
let fAcc = 0;
|
|
82
|
+
let fLast = performance.now();
|
|
83
|
+
return {
|
|
84
|
+
config: { size, preset, feed: params.f, kill: params.k, steps },
|
|
85
|
+
async attach(canvas) {
|
|
86
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
87
|
+
canvas.width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
|
88
|
+
canvas.height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
|
89
|
+
renderer = await MapRenderer.create(canvas, { width: size, height: size, maxV: 0.9, gamma: 0.85, colormap });
|
|
90
|
+
},
|
|
91
|
+
tick() {
|
|
92
|
+
const base = frame * steps; // 全局迭代序号决定数据在哪一侧(奇偶必须跨帧连续)
|
|
93
|
+
const enc = device.createCommandEncoder();
|
|
94
|
+
const pass = enc.beginComputePass();
|
|
95
|
+
pass.setPipeline(pipeline);
|
|
96
|
+
for (let s = 0; s < steps; s++) {
|
|
97
|
+
pass.setBindGroup(0, (base + s) % 2 === 0 ? bgAB : bgBA);
|
|
98
|
+
pass.dispatchWorkgroups(Math.ceil((size * size) / 64));
|
|
99
|
+
}
|
|
100
|
+
pass.end();
|
|
101
|
+
device.queue.submit([enc.finish()]);
|
|
102
|
+
// 渲染最后一步写入的一侧
|
|
103
|
+
renderer?.render((base + steps - 1) % 2 === 0 ? sideB : sideA);
|
|
104
|
+
for (let s = 0; s < steps; s++)
|
|
105
|
+
state.swap();
|
|
106
|
+
frame++;
|
|
107
|
+
fFrames++;
|
|
108
|
+
const now = performance.now();
|
|
109
|
+
fAcc += now - fLast;
|
|
110
|
+
fLast = now;
|
|
111
|
+
if (fAcc >= 500) {
|
|
112
|
+
lastFps = fFrames / (fAcc / 1000);
|
|
113
|
+
fFrames = 0;
|
|
114
|
+
fAcc = 0;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
sprinkle(count = 6) {
|
|
118
|
+
const rand = mulberry32((seedHash + frame) >>> 0);
|
|
119
|
+
const patch = new Float32Array(size * size * 2);
|
|
120
|
+
for (let s = 0; s < count; s++) {
|
|
121
|
+
const cx = Math.floor(rand() * size);
|
|
122
|
+
const cy = Math.floor(rand() * size);
|
|
123
|
+
const r = 2 + Math.floor(rand() * 5);
|
|
124
|
+
for (let y = cy - r; y <= cy + r; y++) {
|
|
125
|
+
for (let x = cx - r; x <= cx + r; x++) {
|
|
126
|
+
const idx = ((y + size) % size) * size + ((x + size) % size);
|
|
127
|
+
patch[idx * 2] = 0.5;
|
|
128
|
+
patch[idx * 2 + 1] = 0.25;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
device.queue.writeBuffer(state.current.st.gpuBuffer, 0, patch);
|
|
133
|
+
},
|
|
134
|
+
stats() { return { fps: lastFps }; },
|
|
135
|
+
async sampleB() {
|
|
136
|
+
const data = (await state.current.st.read());
|
|
137
|
+
const out = new Float32Array(size * size);
|
|
138
|
+
for (let i = 0; i < out.length; i++)
|
|
139
|
+
out[i] = data[i * 2 + 1] ?? 0;
|
|
140
|
+
return out;
|
|
141
|
+
},
|
|
142
|
+
destroy() {
|
|
143
|
+
state.destroy();
|
|
144
|
+
uniform.destroy();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function updateWgsl() {
|
|
149
|
+
return /* wgsl */ `
|
|
150
|
+
struct Params {
|
|
151
|
+
w: u32, h: u32,
|
|
152
|
+
f: f32, k: f32, dA: f32, dB: f32, dt: f32,
|
|
153
|
+
};
|
|
154
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
155
|
+
@group(0) @binding(1) var<storage, read> src: array<vec2f>;
|
|
156
|
+
@group(0) @binding(2) var<storage, read_write> dst: array<vec2f>;
|
|
157
|
+
|
|
158
|
+
fn at(x: i32, y: i32) -> vec2f {
|
|
159
|
+
let w = i32(params.w);
|
|
160
|
+
let h = i32(params.h);
|
|
161
|
+
let xi = (x + w) % w;
|
|
162
|
+
let yi = (y + h) % h;
|
|
163
|
+
return src[u32(yi) * u32(w) + u32(xi)];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@compute @workgroup_size(64)
|
|
167
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
168
|
+
let i = gid.x;
|
|
169
|
+
if (i >= params.w * params.h) { return; }
|
|
170
|
+
let x = i32(i % params.w);
|
|
171
|
+
let y = i32(i / params.w);
|
|
172
|
+
|
|
173
|
+
// 9 点 Laplacian:正交 0.2,对角 0.05
|
|
174
|
+
var lapA = -1.0 * at(x, y).x;
|
|
175
|
+
var lapB = -1.0 * at(x, y).y;
|
|
176
|
+
for (var dy = -1; dy <= 1; dy++) {
|
|
177
|
+
for (var dx = -1; dx <= 1; dx++) {
|
|
178
|
+
if (dx == 0 && dy == 0) { continue; }
|
|
179
|
+
let s = at(x + dx, y + dy);
|
|
180
|
+
let wgt = select(0.05, 0.2, dx == 0 || dy == 0);
|
|
181
|
+
lapA = lapA + wgt * s.x;
|
|
182
|
+
lapB = lapB + wgt * s.y;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let a = at(x, y).x;
|
|
187
|
+
let b = at(x, y).y;
|
|
188
|
+
var a2 = a + (params.dA * lapA - a * b * b + params.f * (1.0 - a)) * params.dt;
|
|
189
|
+
var b2 = b + (params.dB * lapB + a * b * b - (params.k + params.f) * b) * params.dt;
|
|
190
|
+
a2 = clamp(a2, 0.0, 1.0);
|
|
191
|
+
b2 = clamp(b2, 0.0, 1.0);
|
|
192
|
+
dst[i] = vec2f(a2, b2);
|
|
193
|
+
}
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
function hashStr(s) {
|
|
197
|
+
let h = 2166136261;
|
|
198
|
+
for (let i = 0; i < s.length; i++) {
|
|
199
|
+
h ^= s.charCodeAt(i);
|
|
200
|
+
h = Math.imul(h, 16777619);
|
|
201
|
+
}
|
|
202
|
+
return h >>> 0;
|
|
203
|
+
}
|