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
package/dist/index.js
CHANGED
|
@@ -1,1510 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
\u6392\u67E5:\u2460 \u6D4F\u89C8\u5668\u9700 Chrome/Edge 113+ \u6216 Safari 18+;\u2461 \u65E0\u5934\u73AF\u5883\u9700\u5F00\u542F WebGPU;\u2462 \u68C0\u67E5 GPU \u9A71\u52A8\u4E0E\u786C\u4EF6\u52A0\u901F\u8BBE\u7F6E\u3002
|
|
13
|
-
\u53EF\u7528 navigator.gpu \u662F\u5426\u5B58\u5728\u5FEB\u901F\u5224\u65AD\u3002`
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
var CompileError = class extends WgpuKitError {
|
|
18
|
-
constructor(kernelName, messages, userCodeOffset) {
|
|
19
|
-
const mapped = messages.map((m) => {
|
|
20
|
-
const userLine = m.line - userCodeOffset;
|
|
21
|
-
const where = userLine > 0 ? `\u7528\u6237\u4EE3\u7801\u7B2C ${userLine} \u884C` : `\u751F\u6210\u4EE3\u7801\u7B2C ${m.line} \u884C(\u5E93\u7684\u95EE\u9898,\u6B22\u8FCE\u62A5 issue)`;
|
|
22
|
-
return ` ${where}: ${m.msg}`;
|
|
23
|
-
}).join("\n");
|
|
24
|
-
super(`kernel "${kernelName}" WGSL \u7F16\u8BD1\u5931\u8D25:
|
|
25
|
-
${mapped}`);
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
var UsageError = class extends WgpuKitError {
|
|
29
|
-
};
|
|
30
|
-
async function createComputePipelineChecked(device, module, label, entryPoint = "main") {
|
|
31
|
-
device.pushErrorScope("validation");
|
|
32
|
-
const pipeline = device.createComputePipeline({ layout: "auto", compute: { module, entryPoint } });
|
|
33
|
-
const err = await device.popErrorScope();
|
|
34
|
-
if (err) {
|
|
35
|
-
throw new WgpuKitError(`compute \u7BA1\u7EBF "${label}" \u521B\u5EFA\u5931\u8D25: ${err.message}
|
|
36
|
-
\u5E38\u89C1\u539F\u56E0:storage buffer \u6570\u8D85\u8FC7\u6BCF\u9636\u6BB5\u4E0A\u9650(\u53EF\u5411\u672C\u5E93\u63D0 issue \u7533\u8BF7 limits \u652F\u6301)`);
|
|
37
|
-
}
|
|
38
|
-
return pipeline;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// src/core/context.ts
|
|
42
|
-
var GpuContext = class _GpuContext {
|
|
43
|
-
device;
|
|
44
|
-
adapterInfo;
|
|
45
|
-
constructor(device, adapterInfo) {
|
|
46
|
-
this.device = device;
|
|
47
|
-
this.adapterInfo = adapterInfo;
|
|
48
|
-
}
|
|
49
|
-
static #singleton = null;
|
|
50
|
-
static get() {
|
|
51
|
-
if (!_GpuContext.#singleton) {
|
|
52
|
-
_GpuContext.#singleton = _GpuContext.#create().catch((e) => {
|
|
53
|
-
_GpuContext.#singleton = null;
|
|
54
|
-
throw e;
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return _GpuContext.#singleton;
|
|
58
|
-
}
|
|
59
|
-
static async #create() {
|
|
60
|
-
if (typeof navigator === "undefined" || !("gpu" in navigator) || !navigator.gpu) {
|
|
61
|
-
throw new WebGPUUnavailableError("navigator.gpu \u4E0D\u5B58\u5728");
|
|
62
|
-
}
|
|
63
|
-
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
|
64
|
-
if (!adapter) throw new WebGPUUnavailableError("requestAdapter() \u8FD4\u56DE null");
|
|
65
|
-
const info = adapter.info;
|
|
66
|
-
const label = info ? [info.vendor, info.architecture, info.description].filter(Boolean).join(" / ") || "unknown" : "unknown";
|
|
67
|
-
const requiredLimits = {};
|
|
68
|
-
const want = [
|
|
69
|
-
"maxStorageBuffersPerShaderStage",
|
|
70
|
-
"maxStorageBuffersInVertexStage",
|
|
71
|
-
"maxStorageBufferBindingSize"
|
|
72
|
-
];
|
|
73
|
-
for (const key of want) {
|
|
74
|
-
const supported = adapter.limits[key];
|
|
75
|
-
if (typeof supported === "number") requiredLimits[key] = supported;
|
|
76
|
-
}
|
|
77
|
-
const device = await adapter.requestDevice({ label: "wgpu-kit", requiredLimits });
|
|
78
|
-
return new _GpuContext(device, label);
|
|
79
|
-
}
|
|
80
|
-
/** device lost 时 reject;调用方可 await 做清理/提示 */
|
|
81
|
-
get lost() {
|
|
82
|
-
return this.device.lost;
|
|
83
|
-
}
|
|
84
|
-
/** 等待队列中已提交的全部 GPU 工作完成(测试/读回前同步用) */
|
|
85
|
-
async sync() {
|
|
86
|
-
await this.device.queue.onSubmittedWorkDone();
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// src/core/layout.ts
|
|
91
|
-
var TYPES = {
|
|
92
|
-
f32: { size: 4, align: 4, comps: 1, typed: "Float32Array", wgsl: "f32" },
|
|
93
|
-
i32: { size: 4, align: 4, comps: 1, typed: "Int32Array", wgsl: "i32" },
|
|
94
|
-
u32: { size: 4, align: 4, comps: 1, typed: "Uint32Array", wgsl: "u32" },
|
|
95
|
-
vec2f: { size: 8, align: 8, comps: 2, typed: "Float32Array", wgsl: "vec2f" },
|
|
96
|
-
vec2i: { size: 8, align: 8, comps: 2, typed: "Int32Array", wgsl: "vec2i" },
|
|
97
|
-
vec2u: { size: 8, align: 8, comps: 2, typed: "Uint32Array", wgsl: "vec2u" },
|
|
98
|
-
vec3f: { size: 12, align: 16, comps: 3, typed: "Float32Array", wgsl: "vec3f" },
|
|
99
|
-
vec4f: { size: 16, align: 16, comps: 4, typed: "Float32Array", wgsl: "vec4f" }
|
|
100
|
-
};
|
|
101
|
-
function alignTo(offset, align) {
|
|
102
|
-
return Math.ceil(offset / align) * align;
|
|
103
|
-
}
|
|
104
|
-
function planUniform(entries) {
|
|
105
|
-
const fields = [];
|
|
106
|
-
let cursor = 0;
|
|
107
|
-
for (const [name, kind] of entries) {
|
|
108
|
-
const def = TYPES[kind];
|
|
109
|
-
cursor = alignTo(cursor, def.align);
|
|
110
|
-
fields.push({ name, kind, offset: cursor });
|
|
111
|
-
cursor += def.size;
|
|
112
|
-
}
|
|
113
|
-
return { fields, size: alignTo(Math.max(cursor, 1), 16) };
|
|
114
|
-
}
|
|
115
|
-
var PACKERS = {
|
|
116
|
-
f32: (v, o, x) => v.setFloat32(o, x, true),
|
|
117
|
-
i32: (v, o, x) => v.setInt32(o, x, true),
|
|
118
|
-
u32: (v, o, x) => v.setUint32(o, x, true)
|
|
119
|
-
};
|
|
120
|
-
function packUniform(layout, values) {
|
|
121
|
-
const buf = new ArrayBuffer(layout.size);
|
|
122
|
-
const view = new DataView(buf);
|
|
123
|
-
for (const f of layout.fields) {
|
|
124
|
-
const pack = PACKERS[f.kind];
|
|
125
|
-
if (!pack) {
|
|
126
|
-
throw new Error(`uniform \u5B57\u6BB5 ${f.name} \u7684\u7C7B\u578B ${f.kind} \u6682\u4E0D\u652F\u6301(\u5F53\u524D\u4EC5\u652F\u6301\u6807\u91CF)`);
|
|
127
|
-
}
|
|
128
|
-
const v = values[f.name];
|
|
129
|
-
if (v === void 0) throw new Error(`\u7F3A\u5C11 uniform \u503C: ${f.name}`);
|
|
130
|
-
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
131
|
-
throw new Error(`uniform \u503C ${f.name} \u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57,\u6536\u5230: ${String(v)}`);
|
|
132
|
-
}
|
|
133
|
-
pack(view, f.offset, v);
|
|
134
|
-
}
|
|
135
|
-
return buf;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// src/core/buffer.ts
|
|
139
|
-
var TYPED_CTORS = {
|
|
140
|
-
Float32Array,
|
|
141
|
-
Int32Array,
|
|
142
|
-
Uint32Array
|
|
143
|
-
};
|
|
144
|
-
var Buffer = class _Buffer {
|
|
145
|
-
kind;
|
|
146
|
-
length;
|
|
147
|
-
gpuBuffer;
|
|
148
|
-
#ctx;
|
|
149
|
-
#byteLength;
|
|
150
|
-
#staging = null;
|
|
151
|
-
constructor(ctx, kind, length, gpuBuffer) {
|
|
152
|
-
this.#ctx = ctx;
|
|
153
|
-
this.kind = kind;
|
|
154
|
-
this.length = length;
|
|
155
|
-
this.gpuBuffer = gpuBuffer;
|
|
156
|
-
this.#byteLength = length * TYPES[kind].size;
|
|
157
|
-
}
|
|
158
|
-
static async create(kind, length) {
|
|
159
|
-
if (!Number.isInteger(length) || length <= 0) {
|
|
160
|
-
throw new UsageError(`Buffer \u957F\u5EA6\u5FC5\u987B\u662F\u6B63\u6574\u6570,\u6536\u5230: ${String(length)}`);
|
|
161
|
-
}
|
|
162
|
-
const def = TYPES[kind];
|
|
163
|
-
if (!def) throw new UsageError(`\u672A\u77E5\u7C7B\u578B "${String(kind)}",\u53EF\u7528: ${Object.keys(TYPES).join(", ")}`);
|
|
164
|
-
const ctx = await GpuContext.get();
|
|
165
|
-
const gpuBuffer = ctx.device.createBuffer({
|
|
166
|
-
size: length * def.size,
|
|
167
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
168
|
-
label: `wgpu-kit Buffer<${kind}>[${length}]`
|
|
169
|
-
});
|
|
170
|
-
return new _Buffer(ctx, kind, length, gpuBuffer);
|
|
171
|
-
}
|
|
172
|
-
/** 校验并写入(CPU → GPU) */
|
|
173
|
-
write(data) {
|
|
174
|
-
const ctor = TYPED_CTORS[TYPES[this.kind].typed];
|
|
175
|
-
if (!(data instanceof ctor)) {
|
|
176
|
-
throw new UsageError(`Buffer<${this.kind}>.write \u9700\u8981 ${TYPES[this.kind].typed},\u6536\u5230 ${data.constructor?.name ?? typeof data}`);
|
|
177
|
-
}
|
|
178
|
-
const expected = this.length * TYPES[this.kind].comps;
|
|
179
|
-
if (data.length !== expected) {
|
|
180
|
-
throw new UsageError(`Buffer<${this.kind}>[${this.length}].write \u9700\u8981 ${expected} \u4E2A\u5206\u91CF,\u6536\u5230 ${data.length}`);
|
|
181
|
-
}
|
|
182
|
-
this.#ctx.device.queue.writeBuffer(this.gpuBuffer, 0, data);
|
|
183
|
-
}
|
|
184
|
-
/** GPU → CPU:内部 staging buffer + mapAsync,mapAsync 的异步陷阱由库承担 */
|
|
185
|
-
async read() {
|
|
186
|
-
const def = TYPES[this.kind];
|
|
187
|
-
if (!this.#staging) {
|
|
188
|
-
this.#staging = this.#ctx.device.createBuffer({
|
|
189
|
-
size: this.#byteLength,
|
|
190
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
191
|
-
label: `wgpu-kit staging[${this.length}]`
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
const enc = this.#ctx.device.createCommandEncoder();
|
|
195
|
-
enc.copyBufferToBuffer(this.gpuBuffer, 0, this.#staging, 0, this.#byteLength);
|
|
196
|
-
this.#ctx.device.queue.submit([enc.finish()]);
|
|
197
|
-
await this.#staging.mapAsync(GPUMapMode.READ);
|
|
198
|
-
const ab = this.#staging.getMappedRange().slice(0);
|
|
199
|
-
this.#staging.unmap();
|
|
200
|
-
if (def.typed === "Float32Array") return new Float32Array(ab);
|
|
201
|
-
if (def.typed === "Int32Array") return new Int32Array(ab);
|
|
202
|
-
return new Uint32Array(ab);
|
|
203
|
-
}
|
|
204
|
-
destroy() {
|
|
205
|
-
if (this.#staging) {
|
|
206
|
-
this.#staging.destroy();
|
|
207
|
-
this.#staging = null;
|
|
208
|
-
}
|
|
209
|
-
this.gpuBuffer.destroy();
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// src/core/kernel.ts
|
|
214
|
-
var RESERVED = /* @__PURE__ */ new Set(["count"]);
|
|
215
|
-
var nextBufferId = 0;
|
|
216
|
-
var bufferIds = /* @__PURE__ */ new WeakMap();
|
|
217
|
-
function bufId(b) {
|
|
218
|
-
let id = bufferIds.get(b);
|
|
219
|
-
if (id === void 0) {
|
|
220
|
-
id = ++nextBufferId;
|
|
221
|
-
bufferIds.set(b, id);
|
|
222
|
-
}
|
|
223
|
-
return id;
|
|
224
|
-
}
|
|
225
|
-
function generateElementKernel(spec) {
|
|
226
|
-
const name = spec.name ?? "kernel";
|
|
227
|
-
const workgroupSize = spec.workgroupSize ?? 64;
|
|
228
|
-
if (!Number.isInteger(workgroupSize) || workgroupSize < 1 || workgroupSize > 512) {
|
|
229
|
-
throw new UsageError(`workgroupSize \u5FC5\u987B\u5728 1..512,\u6536\u5230: ${String(workgroupSize)}`);
|
|
230
|
-
}
|
|
231
|
-
const state = Object.entries(spec.state ?? {});
|
|
232
|
-
const inputs = Object.entries(spec.inputs ?? {});
|
|
233
|
-
const uniforms = Object.entries(spec.uniforms ?? {});
|
|
234
|
-
if (state.length + inputs.length === 0) {
|
|
235
|
-
throw new UsageError(`elementKernel "${name}" \u81F3\u5C11\u9700\u8981\u4E00\u4E2A state \u6216 inputs \u5B57\u6BB5`);
|
|
236
|
-
}
|
|
237
|
-
for (const [uName] of uniforms) {
|
|
238
|
-
if (RESERVED.has(uName)) throw new UsageError(`uniform \u540D "${uName}" \u662F\u4FDD\u7559\u540D(count \u7531\u5E93\u81EA\u52A8\u6CE8\u5165)`);
|
|
239
|
-
}
|
|
240
|
-
const seen = new Set([...state, ...inputs, ...uniforms].map(([n]) => n));
|
|
241
|
-
if (seen.size !== state.length + inputs.length + uniforms.length) {
|
|
242
|
-
throw new UsageError(`elementKernel "${name}" \u7684 state/inputs/uniforms \u5B58\u5728\u91CD\u540D\u5B57\u6BB5`);
|
|
243
|
-
}
|
|
244
|
-
if (typeof spec.code !== "string" || spec.code.trim().length === 0) {
|
|
245
|
-
throw new UsageError(`elementKernel "${name}" \u7F3A\u5C11 code(\u7528\u6237 WGSL \u51FD\u6570)`);
|
|
246
|
-
}
|
|
247
|
-
const uniformEntries = [...uniforms, ["count", "u32"]];
|
|
248
|
-
const uniformLayout = planUniform(uniformEntries);
|
|
249
|
-
const header = [];
|
|
250
|
-
header.push("// \u7531 wgpu-kit elementKernel \u751F\u6210");
|
|
251
|
-
header.push("struct Params {");
|
|
252
|
-
for (const [n, k] of uniformEntries) header.push(` ${n}: ${TYPES[k].wgsl},`);
|
|
253
|
-
header.push("};");
|
|
254
|
-
header.push("@group(0) @binding(0) var<uniform> params: Params;");
|
|
255
|
-
let binding = 1;
|
|
256
|
-
for (const [n, k] of state) header.push(`@group(0) @binding(${binding++}) var<storage, read_write> ${n}: array<${TYPES[k].wgsl}>;`);
|
|
257
|
-
for (const [n, k] of inputs) header.push(`@group(0) @binding(${binding++}) var<storage, read> ${n}: array<${TYPES[k].wgsl}>;`);
|
|
258
|
-
header.push("");
|
|
259
|
-
header.push(`@compute @workgroup_size(${workgroupSize})`);
|
|
260
|
-
header.push("fn main(@builtin(global_invocation_id) gid: vec3u) {");
|
|
261
|
-
header.push(" let idx = gid.x;");
|
|
262
|
-
header.push(" if (idx >= params.count) { return; }");
|
|
263
|
-
const uniformArgs = uniforms.map(([n]) => `params.${n}`).join(", ");
|
|
264
|
-
header.push(` userFn(idx${uniformArgs ? ", " + uniformArgs : ""});`);
|
|
265
|
-
header.push("}");
|
|
266
|
-
const userCodeLineOffset = header.length;
|
|
267
|
-
const source = [...header, spec.code].join("\n");
|
|
268
|
-
return {
|
|
269
|
-
normalized: { name, workgroupSize, state, inputs, uniforms, code: spec.code },
|
|
270
|
-
source,
|
|
271
|
-
uniformLayout,
|
|
272
|
-
userCodeLineOffset
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
function elementKernel(spec) {
|
|
276
|
-
const first = generateElementKernel(spec);
|
|
277
|
-
const uniformLayout = first.uniformLayout;
|
|
278
|
-
let normalized = first.normalized;
|
|
279
|
-
let source = first.source;
|
|
280
|
-
let userCodeLineOffset = first.userCodeLineOffset;
|
|
281
|
-
const uniformBufferName = `${normalized.name}:uniform`;
|
|
282
|
-
let pipelinePromise = null;
|
|
283
|
-
const bindGroupCache = /* @__PURE__ */ new Map();
|
|
284
|
-
let uniformBuffer = null;
|
|
285
|
-
const compilePipeline = async () => {
|
|
286
|
-
const ctx = await GpuContext.get();
|
|
287
|
-
const device = ctx.device;
|
|
288
|
-
const module = device.createShaderModule({ code: source, label: normalized.name });
|
|
289
|
-
const info = await module.getCompilationInfo();
|
|
290
|
-
const errors = info.messages.filter((m) => m.type === "error");
|
|
291
|
-
if (errors.length > 0) {
|
|
292
|
-
throw new CompileError(
|
|
293
|
-
normalized.name,
|
|
294
|
-
errors.map((m) => ({ line: m.lineNum, msg: m.message })),
|
|
295
|
-
userCodeLineOffset
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
return device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
|
|
299
|
-
};
|
|
300
|
-
async function getPipeline() {
|
|
301
|
-
if (!pipelinePromise) {
|
|
302
|
-
pipelinePromise = compilePipeline().catch((e) => {
|
|
303
|
-
pipelinePromise = null;
|
|
304
|
-
throw e;
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
return pipelinePromise;
|
|
308
|
-
}
|
|
309
|
-
return {
|
|
310
|
-
get name() {
|
|
311
|
-
return normalized.name;
|
|
312
|
-
},
|
|
313
|
-
get source() {
|
|
314
|
-
return source;
|
|
315
|
-
},
|
|
316
|
-
get uniformLayout() {
|
|
317
|
-
return first.uniformLayout;
|
|
318
|
-
},
|
|
319
|
-
get workgroupSize() {
|
|
320
|
-
return normalized.workgroupSize;
|
|
321
|
-
},
|
|
322
|
-
async replace(code) {
|
|
323
|
-
const regen = generateElementKernel({ ...spec, code });
|
|
324
|
-
const savedSource = source;
|
|
325
|
-
const savedOffset = userCodeLineOffset;
|
|
326
|
-
source = regen.source;
|
|
327
|
-
userCodeLineOffset = regen.userCodeLineOffset;
|
|
328
|
-
try {
|
|
329
|
-
const p = await compilePipeline();
|
|
330
|
-
pipelinePromise = Promise.resolve(p);
|
|
331
|
-
bindGroupCache.clear();
|
|
332
|
-
} catch (e) {
|
|
333
|
-
source = savedSource;
|
|
334
|
-
userCodeLineOffset = savedOffset;
|
|
335
|
-
throw e;
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
async run(resources, uniforms = {}) {
|
|
339
|
-
const ctx = await GpuContext.get();
|
|
340
|
-
const device = ctx.device;
|
|
341
|
-
const pipeline = await getPipeline();
|
|
342
|
-
const ordered = [];
|
|
343
|
-
for (const [key] of [...normalized.state, ...normalized.inputs]) {
|
|
344
|
-
const buf = resources[key];
|
|
345
|
-
if (!buf) throw new UsageError(`kernel "${normalized.name}".run \u7F3A\u5C11\u8D44\u6E90 "${key}"`);
|
|
346
|
-
const want = [...normalized.state, ...normalized.inputs].find(([n]) => n === key)?.[1];
|
|
347
|
-
if (buf.kind !== want) {
|
|
348
|
-
throw new UsageError(`\u8D44\u6E90 "${key}" \u7C7B\u578B\u4E0D\u5339\u914D: \u9700\u8981 ${want},\u6536\u5230 ${buf.kind}`);
|
|
349
|
-
}
|
|
350
|
-
ordered.push({ key, buffer: buf });
|
|
351
|
-
}
|
|
352
|
-
const count = ordered[0]?.buffer.length ?? 0;
|
|
353
|
-
for (const { key, buffer } of ordered) {
|
|
354
|
-
if (buffer.length !== count) {
|
|
355
|
-
throw new UsageError(`\u8D44\u6E90 "${key}" \u957F\u5EA6 ${buffer.length} \u4E0E "${ordered[0].key}" \u7684 ${count} \u4E0D\u4E00\u81F4`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
const uBytes = packUniform(uniformLayout, { ...uniforms, count });
|
|
359
|
-
if (!uniformBuffer) {
|
|
360
|
-
uniformBuffer = device.createBuffer({
|
|
361
|
-
size: uniformLayout.size,
|
|
362
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
363
|
-
label: uniformBufferName
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
device.queue.writeBuffer(uniformBuffer, 0, uBytes);
|
|
367
|
-
const cacheKey = ordered.map(({ key, buffer }) => `${key}:${bufId(buffer.gpuBuffer)}`).join("|");
|
|
368
|
-
let bg = bindGroupCache.get(cacheKey);
|
|
369
|
-
if (!bg) {
|
|
370
|
-
const entries = [{ binding: 0, resource: { buffer: uniformBuffer } }];
|
|
371
|
-
ordered.forEach(({ buffer }, i) => entries.push({ binding: i + 1, resource: { buffer: buffer.gpuBuffer } }));
|
|
372
|
-
bg = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
373
|
-
bindGroupCache.set(cacheKey, bg);
|
|
374
|
-
}
|
|
375
|
-
const enc = device.createCommandEncoder();
|
|
376
|
-
const pass = enc.beginComputePass();
|
|
377
|
-
pass.setPipeline(pipeline);
|
|
378
|
-
pass.setBindGroup(0, bg);
|
|
379
|
-
pass.dispatchWorkgroups(Math.ceil(count / normalized.workgroupSize));
|
|
380
|
-
pass.end();
|
|
381
|
-
device.queue.submit([enc.finish()]);
|
|
382
|
-
},
|
|
383
|
-
destroy() {
|
|
384
|
-
pipelinePromise = null;
|
|
385
|
-
bindGroupCache.clear();
|
|
386
|
-
uniformBuffer?.destroy();
|
|
387
|
-
uniformBuffer = null;
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// src/core/pingpong.ts
|
|
393
|
-
var PingPong = class _PingPong {
|
|
394
|
-
#sides;
|
|
395
|
-
#names;
|
|
396
|
-
#length;
|
|
397
|
-
#kinds;
|
|
398
|
-
#index = 0;
|
|
399
|
-
constructor(names, kinds, length, a, b) {
|
|
400
|
-
this.#names = names;
|
|
401
|
-
this.#kinds = kinds;
|
|
402
|
-
this.#length = length;
|
|
403
|
-
this.#sides = [a, b];
|
|
404
|
-
}
|
|
405
|
-
static async create(kinds, length) {
|
|
406
|
-
const names = Object.keys(kinds);
|
|
407
|
-
if (names.length === 0) throw new Error("PingPong \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
408
|
-
const make = async () => {
|
|
409
|
-
const side = {};
|
|
410
|
-
for (const name of names) side[name] = await Buffer.create(kinds[name], length);
|
|
411
|
-
return side;
|
|
412
|
-
};
|
|
413
|
-
return new _PingPong(names, kinds, length, await make(), await make());
|
|
414
|
-
}
|
|
415
|
-
/** 当前帧的数据侧(渲染/读回用) */
|
|
416
|
-
get current() {
|
|
417
|
-
return this.#sides[this.#index];
|
|
418
|
-
}
|
|
419
|
-
/** 另一侧(kernel 写入目标) */
|
|
420
|
-
get other() {
|
|
421
|
-
return this.#sides[1 - this.#index];
|
|
422
|
-
}
|
|
423
|
-
/** 帧末翻转 */
|
|
424
|
-
swap() {
|
|
425
|
-
this.#index = 1 - this.#index;
|
|
426
|
-
}
|
|
427
|
-
/** 以 (写侧, 读侧) 调用 fn 后自动 swap 的语法糖 */
|
|
428
|
-
async runWith(fn) {
|
|
429
|
-
await fn(this.other, this.current);
|
|
430
|
-
this.swap();
|
|
431
|
-
}
|
|
432
|
-
destroy() {
|
|
433
|
-
for (const side of this.#sides) for (const b of Object.values(side)) b.destroy();
|
|
434
|
-
}
|
|
435
|
-
/** 克隆一份同构 PingPong(同字段同长度) */
|
|
436
|
-
async clone() {
|
|
437
|
-
return _PingPong.create(this.#kinds, this.#length);
|
|
438
|
-
}
|
|
439
|
-
get names() {
|
|
440
|
-
return this.#names;
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
// src/core/raw.ts
|
|
445
|
-
function rawKernel(code, entryPoint = "main", label = "rawKernel") {
|
|
446
|
-
if (typeof code !== "string" || code.trim().length === 0) throw new UsageError("rawKernel \u9700\u8981 WGSL \u4EE3\u7801");
|
|
447
|
-
let pipelinePromise = null;
|
|
448
|
-
return {
|
|
449
|
-
async run(entries, workgroups) {
|
|
450
|
-
if (!Number.isInteger(workgroups) || workgroups < 1) {
|
|
451
|
-
throw new UsageError(`rawKernel.run \u7684 workgroups \u5FC5\u987B\u662F\u6B63\u6574\u6570,\u6536\u5230 ${String(workgroups)}`);
|
|
452
|
-
}
|
|
453
|
-
const ctx = await GpuContext.get();
|
|
454
|
-
if (!pipelinePromise) {
|
|
455
|
-
pipelinePromise = (async () => {
|
|
456
|
-
const module = ctx.device.createShaderModule({ code, label });
|
|
457
|
-
const info = await module.getCompilationInfo();
|
|
458
|
-
const errors = info.messages.filter((m) => m.type === "error");
|
|
459
|
-
if (errors.length > 0) {
|
|
460
|
-
pipelinePromise = null;
|
|
461
|
-
throw new CompileError(label, errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
462
|
-
}
|
|
463
|
-
return ctx.device.createComputePipeline({ layout: "auto", compute: { module, entryPoint } });
|
|
464
|
-
})();
|
|
465
|
-
}
|
|
466
|
-
const pipeline = await pipelinePromise;
|
|
467
|
-
const bg = ctx.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries });
|
|
468
|
-
const enc = ctx.device.createCommandEncoder();
|
|
469
|
-
const pass = enc.beginComputePass();
|
|
470
|
-
pass.setPipeline(pipeline);
|
|
471
|
-
pass.setBindGroup(0, bg);
|
|
472
|
-
pass.dispatchWorkgroups(workgroups);
|
|
473
|
-
pass.end();
|
|
474
|
-
ctx.device.queue.submit([enc.finish()]);
|
|
475
|
-
},
|
|
476
|
-
destroy() {
|
|
477
|
-
pipelinePromise = null;
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// src/packs/particles/presets.ts
|
|
483
|
-
var FORCE_PRESETS = {
|
|
484
|
-
/** 经典细胞:小团簇 + 缓慢迁移(spike 验证过的矩阵) */
|
|
485
|
-
cells: [
|
|
486
|
-
0,
|
|
487
|
-
0.6,
|
|
488
|
-
-0.4,
|
|
489
|
-
0,
|
|
490
|
-
-0.4,
|
|
491
|
-
0,
|
|
492
|
-
0.7,
|
|
493
|
-
-0.2,
|
|
494
|
-
0.5,
|
|
495
|
-
-0.5,
|
|
496
|
-
0,
|
|
497
|
-
0.6,
|
|
498
|
-
-0.3,
|
|
499
|
-
0.4,
|
|
500
|
-
-0.6,
|
|
501
|
-
0
|
|
502
|
-
],
|
|
503
|
-
/** 蛇形:链状结构与游动 */
|
|
504
|
-
snakes: [
|
|
505
|
-
0,
|
|
506
|
-
0.7,
|
|
507
|
-
0.1,
|
|
508
|
-
-0.5,
|
|
509
|
-
-0.3,
|
|
510
|
-
0,
|
|
511
|
-
0.8,
|
|
512
|
-
-0.1,
|
|
513
|
-
0.2,
|
|
514
|
-
-0.4,
|
|
515
|
-
0,
|
|
516
|
-
0.7,
|
|
517
|
-
-0.6,
|
|
518
|
-
0.2,
|
|
519
|
-
-0.3,
|
|
520
|
-
0
|
|
521
|
-
],
|
|
522
|
-
/** 轨道:环带与漩涡感 */
|
|
523
|
-
orbitals: [
|
|
524
|
-
0,
|
|
525
|
-
-0.5,
|
|
526
|
-
0.4,
|
|
527
|
-
0.2,
|
|
528
|
-
0.5,
|
|
529
|
-
0,
|
|
530
|
-
-0.6,
|
|
531
|
-
0.1,
|
|
532
|
-
-0.3,
|
|
533
|
-
0.6,
|
|
534
|
-
0,
|
|
535
|
-
-0.4,
|
|
536
|
-
0.1,
|
|
537
|
-
-0.2,
|
|
538
|
-
0.5,
|
|
539
|
-
0
|
|
540
|
-
],
|
|
541
|
-
/** 病毒:捕食结构,红吃绿 */
|
|
542
|
-
viruses: [
|
|
543
|
-
0,
|
|
544
|
-
0.9,
|
|
545
|
-
-0.6,
|
|
546
|
-
0.1,
|
|
547
|
-
-0.5,
|
|
548
|
-
0,
|
|
549
|
-
0.3,
|
|
550
|
-
-0.8,
|
|
551
|
-
0.7,
|
|
552
|
-
0.2,
|
|
553
|
-
0,
|
|
554
|
-
-0.3,
|
|
555
|
-
-0.2,
|
|
556
|
-
0.8,
|
|
557
|
-
0.4,
|
|
558
|
-
0
|
|
559
|
-
]
|
|
560
|
-
};
|
|
561
|
-
function mulberry32(seed) {
|
|
562
|
-
let s = seed | 0;
|
|
563
|
-
return () => {
|
|
564
|
-
s = s + 1831565813 | 0;
|
|
565
|
-
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
566
|
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
567
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
function hashSeed(seed) {
|
|
571
|
-
let h = 2166136261;
|
|
572
|
-
for (let i = 0; i < seed.length; i++) {
|
|
573
|
-
h ^= seed.charCodeAt(i);
|
|
574
|
-
h = Math.imul(h, 16777619);
|
|
575
|
-
}
|
|
576
|
-
return h >>> 0;
|
|
577
|
-
}
|
|
578
|
-
function randomMatrix(seed) {
|
|
579
|
-
const rand = mulberry32(seed ^ 2654435769);
|
|
580
|
-
const m = Array.from({ length: 16 }, () => Math.round((rand() * 2 - 1) * 100) / 100);
|
|
581
|
-
for (let i = 0; i < 4; i++) m[i * 4 + i] = 0;
|
|
582
|
-
return m;
|
|
583
|
-
}
|
|
584
|
-
function resolveMatrix(forces, seed) {
|
|
585
|
-
if (forces === "random") return randomMatrix(seed);
|
|
586
|
-
if (typeof forces === "string") {
|
|
587
|
-
const preset = FORCE_PRESETS[forces];
|
|
588
|
-
if (!preset) {
|
|
589
|
-
throw new Error(`\u672A\u77E5\u529B\u77E9\u9635\u9884\u8BBE "${forces}",\u53EF\u7528: ${Object.keys(FORCE_PRESETS).join(", ")}, random`);
|
|
590
|
-
}
|
|
591
|
-
return preset;
|
|
592
|
-
}
|
|
593
|
-
return forces;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// src/packs/particles/config.ts
|
|
597
|
-
var MODES = ["n2", "tiled", "grid"];
|
|
598
|
-
function resolveConfig(config = {}) {
|
|
599
|
-
const {
|
|
600
|
-
count = 8192,
|
|
601
|
-
forces = "cells",
|
|
602
|
-
mode = "grid",
|
|
603
|
-
// 基准数据驱动:v0.4 起 grid 全面优于 tiled(0.54ms vs 3.62ms @16k),见 benchmarks.md
|
|
604
|
-
color = "species",
|
|
605
|
-
bounds = "wrap",
|
|
606
|
-
seed = "wgpu-kit",
|
|
607
|
-
rMax = 0.12,
|
|
608
|
-
beta = 0.3,
|
|
609
|
-
forceFactor = 10,
|
|
610
|
-
frictionHalfLife = 0.04,
|
|
611
|
-
dt = 0.02,
|
|
612
|
-
pointSize = 4e-3,
|
|
613
|
-
maxNeighbors = 32768
|
|
614
|
-
} = config;
|
|
615
|
-
if (!Number.isInteger(count) || count <= 0 || count > 1e6) {
|
|
616
|
-
throw new UsageError(`count \u5FC5\u987B\u662F 1..1_000_000 \u7684\u6574\u6570,\u6536\u5230: ${String(count)}`);
|
|
617
|
-
}
|
|
618
|
-
if (!MODES.includes(mode)) {
|
|
619
|
-
throw new UsageError(`mode \u5FC5\u987B\u662F ${MODES.join(" | ")},\u6536\u5230: "${String(mode)}"`);
|
|
620
|
-
}
|
|
621
|
-
if (mode === "n2" && count > 32e3) {
|
|
622
|
-
throw new UsageError(`mode='n2' \u5EFA\u8BAE count \u2264 20000(\u5F53\u524D ${count});\u5927\u89C4\u6A21\u8BF7\u7528 mode='tiled' \u6216 'grid'`);
|
|
623
|
-
}
|
|
624
|
-
const seedStr = String(seed);
|
|
625
|
-
return {
|
|
626
|
-
count,
|
|
627
|
-
forces: resolveMatrix(forces, hashSeed(seedStr)),
|
|
628
|
-
forcesName: typeof forces === "string" ? forces : "custom",
|
|
629
|
-
mode,
|
|
630
|
-
color,
|
|
631
|
-
bounds,
|
|
632
|
-
seed: seedStr,
|
|
633
|
-
seedHash: hashSeed(seedStr),
|
|
634
|
-
rMax,
|
|
635
|
-
beta,
|
|
636
|
-
forceFactor,
|
|
637
|
-
friction: Math.exp(-dt / frictionHalfLife),
|
|
638
|
-
dt,
|
|
639
|
-
pointSize,
|
|
640
|
-
maxNeighbors
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// src/packs/particles/wgsl.ts
|
|
645
|
-
var WORKGROUP = 64;
|
|
646
|
-
function simWgsl(mode, speciesCount) {
|
|
647
|
-
const tiled = mode === "tiled";
|
|
648
|
-
const common = (
|
|
649
|
-
/* wgsl */
|
|
650
|
-
`
|
|
651
|
-
struct Params {
|
|
652
|
-
count: u32,
|
|
653
|
-
_pad0: u32,
|
|
654
|
-
dt: f32,
|
|
655
|
-
rMax: f32,
|
|
656
|
-
beta: f32,
|
|
657
|
-
forceFactor: f32,
|
|
658
|
-
friction: f32,
|
|
659
|
-
worldHalf: f32,
|
|
660
|
-
wrapEdge: f32,
|
|
661
|
-
gridSize: u32,
|
|
662
|
-
cells: u32,
|
|
663
|
-
maxCand: u32,
|
|
664
|
-
};
|
|
665
|
-
@group(0) @binding(0) var<uniform> params: Params;
|
|
666
|
-
@group(0) @binding(1) var<storage, read> matrix: array<f32>;
|
|
667
|
-
@group(0) @binding(2) var<storage, read> species: array<u32>;
|
|
668
|
-
@group(0) @binding(3) var<storage, read> posIn: array<vec2f>;
|
|
669
|
-
@group(0) @binding(4) var<storage, read> velIn: array<vec2f>;
|
|
670
|
-
@group(0) @binding(5) var<storage, read_write> posOut: array<vec2f>;
|
|
671
|
-
@group(0) @binding(6) var<storage, read_write> velOut: array<vec2f>;
|
|
672
|
-
|
|
673
|
-
fn force(r: f32, a: f32) -> f32 {
|
|
674
|
-
if (r < params.beta) { return a / params.beta - 1.0; }
|
|
675
|
-
if (r < 1.0) { return a * (1.0 - abs(2.0 * r - 1.0 - params.beta) / (1.0 - params.beta)); }
|
|
676
|
-
return 0.0;
|
|
677
|
-
}
|
|
678
|
-
`
|
|
679
|
-
);
|
|
680
|
-
const interactSig = tiled ? "fn interact(myIdx: u32, mySp: u32, myPos: vec2f, lid: u32) -> vec2f {" : "fn interact(myIdx: u32, mySp: u32, myPos: vec2f) -> vec2f {";
|
|
681
|
-
const body = tiled ? tiledBody(speciesCount) : n2Body(speciesCount);
|
|
682
|
-
const main = (
|
|
683
|
-
/* wgsl */
|
|
684
|
-
`
|
|
685
|
-
@compute @workgroup_size(${WORKGROUP})
|
|
686
|
-
fn main(@builtin(global_invocation_id) gid: vec3u${tiled ? ", @builtin(local_invocation_id) lid: vec3u" : ""}) {
|
|
687
|
-
let i = gid.x;
|
|
688
|
-
// \u6CE8\u610F:workgroupBarrier \u8981\u6C42 uniform control flow\u2014\u2014
|
|
689
|
-
// \u8D8A\u754C\u7EBF\u7A0B\u4E5F\u5FC5\u987B\u53C2\u4E0E barrier \u5FAA\u73AF,\u53EA\u80FD\u5728\u6700\u7EC8\u5199\u5165\u5904 guard(tiled \u5C3E\u90E8 workgroup \u7684\u7ECF\u5178\u5751)
|
|
690
|
-
let ok = i < params.count;
|
|
691
|
-
let myIdx = min(i, params.count - 1u);
|
|
692
|
-
let mySp = species[myIdx];
|
|
693
|
-
let myPos = posIn[myIdx];
|
|
694
|
-
var accel = interact(myIdx, mySp, myPos${tiled ? ", lid.x" : ""});
|
|
695
|
-
accel = accel * params.forceFactor * params.rMax;
|
|
696
|
-
if (ok) {
|
|
697
|
-
var vel = (velIn[i] + accel * params.dt) * params.friction;
|
|
698
|
-
var pos = myPos + vel * params.dt;
|
|
699
|
-
let span = params.worldHalf * 2.0;
|
|
700
|
-
if (params.wrapEdge > 0.5) {
|
|
701
|
-
pos = ((pos + params.worldHalf) % span + span) % span - params.worldHalf;
|
|
702
|
-
} else {
|
|
703
|
-
pos = clamp(pos, vec2f(-params.worldHalf), vec2f(params.worldHalf));
|
|
704
|
-
}
|
|
705
|
-
posOut[i] = pos;
|
|
706
|
-
velOut[i] = vel;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
`
|
|
710
|
-
);
|
|
711
|
-
return `${common}${tiled ? TILE_DECLS : ""}
|
|
712
|
-
${interactSig}${body}
|
|
713
|
-
}
|
|
714
|
-
${main}`;
|
|
715
|
-
}
|
|
716
|
-
function n2Body(speciesCount) {
|
|
717
|
-
return (
|
|
718
|
-
/* wgsl */
|
|
719
|
-
`
|
|
720
|
-
var accel = vec2f(0.0, 0.0);
|
|
721
|
-
let rMax2 = params.rMax * params.rMax;
|
|
722
|
-
for (var j = 0u; j < params.count; j++) {
|
|
723
|
-
if (j == myIdx) { continue; }
|
|
724
|
-
let rel = posIn[j] - myPos;
|
|
725
|
-
let d2 = dot(rel, rel);
|
|
726
|
-
if (d2 > rMax2) { continue; } // \u8DDD\u79BB\u5E73\u65B9 early-out:\u7EDD\u5927\u591A\u6570\u5BF9\u514D\u5F00\u65B9
|
|
727
|
-
let d = sqrt(d2);
|
|
728
|
-
let r = d / params.rMax;
|
|
729
|
-
if (r > 0.0 && r < 1.0) {
|
|
730
|
-
let f = force(r, matrix[mySp * ${speciesCount}u + species[j]]);
|
|
731
|
-
accel = accel + rel / d * f;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return accel;
|
|
735
|
-
`
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
function tiledBody(speciesCount) {
|
|
739
|
-
return (
|
|
740
|
-
/* wgsl */
|
|
741
|
-
`
|
|
742
|
-
var accel = vec2f(0.0, 0.0);
|
|
743
|
-
let rMax2 = params.rMax * params.rMax;
|
|
744
|
-
let tiles = (params.count + ${WORKGROUP}u - 1u) / ${WORKGROUP}u;
|
|
745
|
-
for (var t = 0u; t < tiles; t++) {
|
|
746
|
-
let loadIdx = t * ${WORKGROUP}u + lid;
|
|
747
|
-
tilePos[lid] = posIn[min(loadIdx, params.count - 1u)];
|
|
748
|
-
tileSp[lid] = species[min(loadIdx, params.count - 1u)];
|
|
749
|
-
workgroupBarrier();
|
|
750
|
-
let tileLen = min(${WORKGROUP}u, params.count - t * ${WORKGROUP}u);
|
|
751
|
-
for (var k = 0u; k < ${WORKGROUP}u; k++) {
|
|
752
|
-
if (k >= tileLen) { break; }
|
|
753
|
-
let j = t * ${WORKGROUP}u + k;
|
|
754
|
-
if (j == myIdx) { continue; }
|
|
755
|
-
let rel = tilePos[k] - myPos;
|
|
756
|
-
let d2 = dot(rel, rel);
|
|
757
|
-
if (d2 > rMax2) { continue; } // \u8DDD\u79BB\u5E73\u65B9 early-out
|
|
758
|
-
let d = sqrt(d2);
|
|
759
|
-
let r = d / params.rMax;
|
|
760
|
-
if (r > 0.0 && r < 1.0) {
|
|
761
|
-
let f = force(r, matrix[mySp * ${speciesCount}u + tileSp[k]]);
|
|
762
|
-
accel = accel + rel / d * f;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
workgroupBarrier();
|
|
766
|
-
}
|
|
767
|
-
return accel;
|
|
768
|
-
`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
var TILE_DECLS = (
|
|
772
|
-
/* wgsl */
|
|
773
|
-
`
|
|
774
|
-
var<workgroup> tilePos: array<vec2f, ${WORKGROUP}>;
|
|
775
|
-
var<workgroup> tileSp: array<u32, ${WORKGROUP}>;
|
|
776
|
-
`
|
|
777
|
-
);
|
|
778
|
-
function renderWgsl(speciesCount, colorMode, pointSize) {
|
|
779
|
-
const palette = colorMode === "species" ? `const PALETTE = array<vec3f, ${speciesCount}>(
|
|
780
|
-
vec3f(1.00, 0.42, 0.24),
|
|
781
|
-
vec3f(0.36, 0.86, 0.56),
|
|
782
|
-
vec3f(0.36, 0.58, 1.00),
|
|
783
|
-
vec3f(0.98, 0.80, 0.30),
|
|
784
|
-
);` : "";
|
|
785
|
-
const colorExpr = colorMode === "velocity" ? (
|
|
786
|
-
/* wgsl */
|
|
787
|
-
`
|
|
788
|
-
let speed = length(vel[inst]);
|
|
789
|
-
let t = 1.0 - exp(-speed * 40.0);
|
|
790
|
-
out.color = mix(vec3f(0.20, 0.32, 0.55), vec3f(1.0, 0.85, 0.45), t);
|
|
791
|
-
out.color = mix(out.color, vec3f(1.0, 0.95, 0.9), smoothstep(0.6, 1.0, t));`
|
|
792
|
-
) : (
|
|
793
|
-
/* wgsl */
|
|
794
|
-
`
|
|
795
|
-
let sp = min(species[inst], ${speciesCount - 1}u);
|
|
796
|
-
out.color = PALETTE[sp];`
|
|
797
|
-
);
|
|
798
|
-
const velBinding = colorMode === "velocity" ? "\n@group(0) @binding(2) var<storage, read> vel: array<vec2f>;" : "";
|
|
799
|
-
return (
|
|
800
|
-
/* wgsl */
|
|
801
|
-
`
|
|
802
|
-
struct VsOut {
|
|
803
|
-
@builtin(position) clip: vec4f,
|
|
804
|
-
@location(0) uv: vec2f,
|
|
805
|
-
@location(1) color: vec3f,
|
|
806
|
-
};
|
|
807
|
-
${palette}
|
|
808
|
-
@group(0) @binding(0) var<storage, read> pos: array<vec2f>;
|
|
809
|
-
@group(0) @binding(1) var<storage, read> species: array<u32>;${velBinding}
|
|
810
|
-
@group(0) @binding(3) var<uniform> rs: vec4f; // x = 1/worldHalf(\u76F8\u673A\u7F29\u653E;binding 2 \u7559\u7ED9 velocity \u6A21\u5F0F\u7684 vel)
|
|
811
|
-
|
|
812
|
-
@vertex
|
|
813
|
-
fn vs(@location(0) corner: vec2f, @builtin(instance_index) inst: u32) -> VsOut {
|
|
814
|
-
var out: VsOut;
|
|
815
|
-
out.clip = vec4f((pos[inst] + corner * ${pointSize.toFixed(4)}) * rs.x, 0.0, 1.0);
|
|
816
|
-
out.uv = corner;
|
|
817
|
-
${colorExpr}
|
|
818
|
-
return out;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
@fragment
|
|
822
|
-
fn fs(in: VsOut) -> @location(0) vec4f {
|
|
823
|
-
let d = length(in.uv);
|
|
824
|
-
if (d > 1.0) { discard; }
|
|
825
|
-
let alpha = smoothstep(1.0, 0.35, d);
|
|
826
|
-
return vec4f(in.color * alpha, alpha);
|
|
827
|
-
}
|
|
828
|
-
`
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// src/packs/particles/grid.ts
|
|
833
|
-
var GRID_WORKGROUP = 64;
|
|
834
|
-
var SCAN_WORKGROUP = 256;
|
|
835
|
-
var CELL_OF = (
|
|
836
|
-
/* wgsl */
|
|
837
|
-
`
|
|
838
|
-
fn cellOf(p: vec2f) -> u32 {
|
|
839
|
-
let g = i32(params.gridSize);
|
|
840
|
-
let span = params.worldHalf * 2.0;
|
|
841
|
-
let cx = clamp(i32(floor((p.x + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
842
|
-
let cy = clamp(i32(floor((p.y + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
843
|
-
return u32(cy) * params.gridSize + u32(cx);
|
|
844
|
-
}
|
|
845
|
-
`
|
|
846
|
-
);
|
|
847
|
-
function gridCountsWgsl() {
|
|
848
|
-
return (
|
|
849
|
-
/* wgsl */
|
|
850
|
-
`
|
|
851
|
-
struct Params {
|
|
852
|
-
count: u32, _pad0: u32,
|
|
853
|
-
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
854
|
-
worldHalf: f32, wrapEdge: f32,
|
|
855
|
-
gridSize: u32, cells: u32, maxCand: u32,
|
|
856
|
-
};
|
|
857
|
-
@group(0) @binding(0) var<uniform> params: Params;
|
|
858
|
-
@group(0) @binding(1) var<storage, read> posIn: array<vec2f>;
|
|
859
|
-
@group(0) @binding(2) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
860
|
-
${CELL_OF}
|
|
861
|
-
@compute @workgroup_size(${GRID_WORKGROUP})
|
|
862
|
-
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
863
|
-
let i = gid.x;
|
|
864
|
-
if (i >= params.count) { return; }
|
|
865
|
-
atomicAdd(&cellCount[cellOf(posIn[i])], 1u);
|
|
866
|
-
}
|
|
867
|
-
`
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
function gridScanWgsl() {
|
|
871
|
-
return (
|
|
872
|
-
/* wgsl */
|
|
873
|
-
`
|
|
874
|
-
struct Params {
|
|
875
|
-
count: u32, _pad0: u32,
|
|
876
|
-
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
877
|
-
worldHalf: f32, wrapEdge: f32,
|
|
878
|
-
gridSize: u32, cells: u32, maxCand: u32,
|
|
879
|
-
};
|
|
880
|
-
@group(0) @binding(0) var<uniform> params: Params;
|
|
881
|
-
@group(0) @binding(1) var<storage, read_write> cellCount: array<atomic<u32>>;
|
|
882
|
-
@group(0) @binding(2) var<storage, read_write> cellStart: array<u32>;
|
|
883
|
-
@group(0) @binding(3) var<storage, read_write> cellFill: array<u32>;
|
|
884
|
-
var<workgroup> partial: array<u32, ${SCAN_WORKGROUP}>;
|
|
885
|
-
|
|
886
|
-
@compute @workgroup_size(${SCAN_WORKGROUP})
|
|
887
|
-
fn main(@builtin(local_invocation_id) lid: vec3u) {
|
|
888
|
-
let tid = lid.x;
|
|
889
|
-
let cells = params.cells;
|
|
890
|
-
let chunks = (cells + ${SCAN_WORKGROUP}u - 1u) / ${SCAN_WORKGROUP}u;
|
|
891
|
-
|
|
892
|
-
// \u6BCF\u7EBF\u7A0B\u4E32\u884C\u6C42\u81EA\u5DF1 chunk \u7684\u5C40\u90E8\u548C
|
|
893
|
-
var local = 0u;
|
|
894
|
-
for (var c = 0u; c < chunks; c++) {
|
|
895
|
-
let idx = c * ${SCAN_WORKGROUP}u + tid;
|
|
896
|
-
if (idx < cells) { local = local + atomicLoad(&cellCount[idx]); }
|
|
897
|
-
}
|
|
898
|
-
partial[tid] = local;
|
|
899
|
-
workgroupBarrier();
|
|
900
|
-
|
|
901
|
-
// 256 \u4E2A\u5C40\u90E8\u548C\u505A Hillis-Steele \u542B\u524D\u7F00\u626B\u63CF
|
|
902
|
-
var offset = 1u;
|
|
903
|
-
loop {
|
|
904
|
-
if (offset >= ${SCAN_WORKGROUP}u) { break; }
|
|
905
|
-
var v = 0u;
|
|
906
|
-
if (tid >= offset) { v = partial[tid - offset]; }
|
|
907
|
-
workgroupBarrier();
|
|
908
|
-
if (tid >= offset) { partial[tid] = partial[tid] + v; }
|
|
909
|
-
workgroupBarrier();
|
|
910
|
-
offset = offset << 1u;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// chunk \u57FA\u5740 = \u524D\u9762\u6240\u6709 chunk \u7684\u603B\u548C;\u91CD\u8D70 chunk \u5199 start/fill,\u5E76\u5F52\u96F6 count \u7ED9\u4E0B\u4E00\u5E27
|
|
914
|
-
var run = 0u;
|
|
915
|
-
if (tid > 0u) { run = partial[tid - 1u]; }
|
|
916
|
-
for (var c = 0u; c < chunks; c++) {
|
|
917
|
-
let idx = c * ${SCAN_WORKGROUP}u + tid;
|
|
918
|
-
if (idx < cells) {
|
|
919
|
-
cellStart[idx] = run;
|
|
920
|
-
cellFill[idx] = run;
|
|
921
|
-
run = run + atomicLoad(&cellCount[idx]);
|
|
922
|
-
atomicStore(&cellCount[idx], 0u);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
`
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
function gridScatterWgsl() {
|
|
930
|
-
return (
|
|
931
|
-
/* wgsl */
|
|
932
|
-
`
|
|
933
|
-
struct Params {
|
|
934
|
-
count: u32, _pad0: u32,
|
|
935
|
-
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
936
|
-
worldHalf: f32, wrapEdge: f32,
|
|
937
|
-
gridSize: u32, cells: u32, maxCand: u32,
|
|
938
|
-
};
|
|
939
|
-
@group(0) @binding(0) var<uniform> params: Params;
|
|
940
|
-
@group(0) @binding(1) var<storage, read> posIn: array<vec2f>;
|
|
941
|
-
@group(0) @binding(2) var<storage, read> species: array<u32>;
|
|
942
|
-
@group(0) @binding(3) var<storage, read_write> cellFill: array<atomic<u32>>;
|
|
943
|
-
@group(0) @binding(4) var<storage, read_write> order: array<u32>;
|
|
944
|
-
@group(0) @binding(5) var<storage, read_write> sortedPos: array<vec2f>;
|
|
945
|
-
@group(0) @binding(6) var<storage, read_write> sortedSp: array<u32>;
|
|
946
|
-
${CELL_OF}
|
|
947
|
-
@compute @workgroup_size(${GRID_WORKGROUP})
|
|
948
|
-
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
949
|
-
let i = gid.x;
|
|
950
|
-
if (i >= params.count) { return; }
|
|
951
|
-
let slot = atomicAdd(&cellFill[cellOf(posIn[i])], 1u);
|
|
952
|
-
order[slot] = i;
|
|
953
|
-
sortedPos[slot] = posIn[i];
|
|
954
|
-
sortedSp[slot] = species[i];
|
|
955
|
-
}
|
|
956
|
-
`
|
|
957
|
-
);
|
|
958
|
-
}
|
|
959
|
-
function gridForceWgsl(speciesCount) {
|
|
960
|
-
return (
|
|
961
|
-
/* wgsl */
|
|
962
|
-
`
|
|
963
|
-
struct Params {
|
|
964
|
-
count: u32, _pad0: u32,
|
|
965
|
-
dt: f32, rMax: f32, beta: f32, forceFactor: f32, friction: f32,
|
|
966
|
-
worldHalf: f32, wrapEdge: f32,
|
|
967
|
-
gridSize: u32, cells: u32, maxCand: u32,
|
|
968
|
-
};
|
|
969
|
-
@group(0) @binding(0) var<uniform> params: Params;
|
|
970
|
-
@group(0) @binding(1) var<storage, read> matrix: array<f32>;
|
|
971
|
-
@group(0) @binding(2) var<storage, read> species: array<u32>;
|
|
972
|
-
@group(0) @binding(3) var<storage, read> posIn: array<vec2f>;
|
|
973
|
-
@group(0) @binding(4) var<storage, read> sortedPos: array<vec2f>;
|
|
974
|
-
@group(0) @binding(5) var<storage, read> sortedSp: array<u32>;
|
|
975
|
-
@group(0) @binding(6) var<storage, read> cellStart: array<u32>;
|
|
976
|
-
@group(0) @binding(7) var<storage, read> cellFill: array<u32>;
|
|
977
|
-
@group(0) @binding(8) var<storage, read> order: array<u32>;
|
|
978
|
-
@group(0) @binding(9) var<storage, read_write> partial: array<vec2f>;
|
|
979
|
-
|
|
980
|
-
fn force(r: f32, a: f32) -> f32 {
|
|
981
|
-
if (r < params.beta) { return a / params.beta - 1.0; }
|
|
982
|
-
if (r < 1.0) { return a * (1.0 - abs(2.0 * r - 1.0 - params.beta) / (1.0 - params.beta)); }
|
|
983
|
-
return 0.0;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
@compute @workgroup_size(64)
|
|
987
|
-
fn main_force_cell(@builtin(global_invocation_id) gid: vec3u) {
|
|
988
|
-
let tid = gid.x;
|
|
989
|
-
let i = tid / 9u;
|
|
990
|
-
if (i >= params.count) { return; }
|
|
991
|
-
let c = tid % 9u;
|
|
992
|
-
let g = i32(params.gridSize);
|
|
993
|
-
// \u4E0E CELL_OF \u5B8C\u5168\u76F8\u540C\u7684\u6D6E\u70B9\u5E8F\u5217(\u9664\u4EE5 span \u518D\u4E58 g)\u2014\u2014\u8DEF\u5F84\u4E0D\u4E00\u81F4\u4F1A\u8BA9\u8D34\u683C\u7C92\u5B50\u67E5\u8BE2\u9519\u4F4D\u4E00\u683C
|
|
994
|
-
let span = params.worldHalf * 2.0;
|
|
995
|
-
var cx = clamp(i32(floor((posIn[i].x + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
996
|
-
var cy = clamp(i32(floor((posIn[i].y + params.worldHalf) / span * f32(g))), 0, g - 1);
|
|
997
|
-
let dx = i32(c % 3u) - 1;
|
|
998
|
-
let dy = i32(c / 3u) - 1;
|
|
999
|
-
let nx = cx + dx;
|
|
1000
|
-
let ny = cy + dy;
|
|
1001
|
-
let out = i * 9u + c;
|
|
1002
|
-
if (nx < 0 || ny < 0 || nx >= g || ny >= g) { partial[out] = vec2f(0.0); return; }
|
|
1003
|
-
let cc = u32(ny) * u32(g) + u32(nx);
|
|
1004
|
-
let s = cellStart[cc];
|
|
1005
|
-
let e = cellFill[cc];
|
|
1006
|
-
let myPos = posIn[i];
|
|
1007
|
-
let mySp = species[i];
|
|
1008
|
-
let rMax2 = params.rMax * params.rMax;
|
|
1009
|
-
var accel = vec2f(0.0, 0.0);
|
|
1010
|
-
var checked = 0u;
|
|
1011
|
-
for (var k = s; k < e; k++) {
|
|
1012
|
-
checked = checked + 1u;
|
|
1013
|
-
if (checked > params.maxCand) { break; }
|
|
1014
|
-
if (order[k] == i) { continue; }
|
|
1015
|
-
let rel = sortedPos[k] - myPos;
|
|
1016
|
-
let d2 = dot(rel, rel);
|
|
1017
|
-
if (d2 > rMax2) { continue; }
|
|
1018
|
-
let d = sqrt(d2);
|
|
1019
|
-
let r = d / params.rMax;
|
|
1020
|
-
if (r > 0.0 && r < 1.0) {
|
|
1021
|
-
let f = force(r, matrix[mySp * ${speciesCount}u + sortedSp[k]]);
|
|
1022
|
-
accel = accel + rel / d * f;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
partial[out] = accel;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
@group(0) @binding(0) var<uniform> params2: Params;
|
|
1029
|
-
@group(0) @binding(1) var<storage, read> velIn: array<vec2f>;
|
|
1030
|
-
@group(0) @binding(2) var<storage, read> posIn2: array<vec2f>;
|
|
1031
|
-
@group(0) @binding(3) var<storage, read> partialR: array<vec2f>;
|
|
1032
|
-
@group(0) @binding(4) var<storage, read_write> posOut: array<vec2f>;
|
|
1033
|
-
@group(0) @binding(5) var<storage, read_write> velOut: array<vec2f>;
|
|
1034
|
-
|
|
1035
|
-
@compute @workgroup_size(64)
|
|
1036
|
-
fn main_force_integrate(@builtin(global_invocation_id) gid: vec3u) {
|
|
1037
|
-
let i = gid.x;
|
|
1038
|
-
if (i >= params2.count) { return; }
|
|
1039
|
-
var accel = vec2f(0.0, 0.0);
|
|
1040
|
-
for (var c = 0u; c < 9u; c++) {
|
|
1041
|
-
accel = accel + partialR[i * 9u + c];
|
|
1042
|
-
}
|
|
1043
|
-
accel = accel * params2.forceFactor * params2.rMax;
|
|
1044
|
-
var vel = (velIn[i] + accel * params2.dt) * params2.friction;
|
|
1045
|
-
var pos = posIn2[i] + vel * params2.dt;
|
|
1046
|
-
let span = params2.worldHalf * 2.0;
|
|
1047
|
-
if (params2.wrapEdge > 0.5) {
|
|
1048
|
-
pos = ((pos + params2.worldHalf) % span + span) % span - params2.worldHalf;
|
|
1049
|
-
} else {
|
|
1050
|
-
pos = clamp(pos, vec2f(-params2.worldHalf), vec2f(params2.worldHalf));
|
|
1051
|
-
}
|
|
1052
|
-
posOut[i] = pos;
|
|
1053
|
-
velOut[i] = vel;
|
|
1054
|
-
}
|
|
1055
|
-
`
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// src/packs/particles/render.ts
|
|
1060
|
-
var nextId = 0;
|
|
1061
|
-
var ids = /* @__PURE__ */ new WeakMap();
|
|
1062
|
-
var bufKey = (b) => {
|
|
1063
|
-
let id = ids.get(b);
|
|
1064
|
-
if (id === void 0) {
|
|
1065
|
-
id = ++nextId;
|
|
1066
|
-
ids.set(b, id);
|
|
1067
|
-
}
|
|
1068
|
-
return id;
|
|
1069
|
-
};
|
|
1070
|
-
var ParticlesRenderer = class _ParticlesRenderer {
|
|
1071
|
-
#ctx;
|
|
1072
|
-
#gpuCtx;
|
|
1073
|
-
#format;
|
|
1074
|
-
#pipeline;
|
|
1075
|
-
#quad;
|
|
1076
|
-
#idx;
|
|
1077
|
-
#species;
|
|
1078
|
-
#vel;
|
|
1079
|
-
#colorMode;
|
|
1080
|
-
#rs;
|
|
1081
|
-
#bgCache = /* @__PURE__ */ new Map();
|
|
1082
|
-
#count;
|
|
1083
|
-
constructor(ctx, gpuCtx, format, pipeline, quad, idx, species, count, rs, vel, colorMode) {
|
|
1084
|
-
this.#ctx = ctx;
|
|
1085
|
-
this.#gpuCtx = gpuCtx;
|
|
1086
|
-
this.#format = format;
|
|
1087
|
-
this.#pipeline = pipeline;
|
|
1088
|
-
this.#quad = quad;
|
|
1089
|
-
this.#idx = idx;
|
|
1090
|
-
this.#species = species;
|
|
1091
|
-
this.#count = count;
|
|
1092
|
-
this.#rs = rs;
|
|
1093
|
-
this.#vel = vel;
|
|
1094
|
-
this.#colorMode = colorMode;
|
|
1095
|
-
}
|
|
1096
|
-
static async create(canvas, opts) {
|
|
1097
|
-
const ctx = await GpuContext.get();
|
|
1098
|
-
const gpuCtx = canvas.getContext("webgpu");
|
|
1099
|
-
if (!gpuCtx) throw new Error('canvas.getContext("webgpu") \u8FD4\u56DE\u7A7A:\u8BE5 canvas \u5DF2\u88AB\u5176\u4ED6\u540E\u7AEF\u5360\u7528?');
|
|
1100
|
-
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
1101
|
-
gpuCtx.configure({ device: ctx.device, format, alphaMode: "opaque" });
|
|
1102
|
-
const module = ctx.device.createShaderModule({ code: renderWgsl(4, opts.color, opts.pointSize), label: "particles-render" });
|
|
1103
|
-
const rsUniform = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
1104
|
-
ctx.device.queue.writeBuffer(rsUniform, 0, new Float32Array([1 / opts.worldHalf, 0, 0, 0]));
|
|
1105
|
-
const info = await module.getCompilationInfo();
|
|
1106
|
-
const errors = info.messages.filter((m) => m.type === "error");
|
|
1107
|
-
if (errors.length > 0) throw new CompileError("particles-render", errors.map((m) => ({ line: m.lineNum + 1, msg: m.message })), 0);
|
|
1108
|
-
const pipeline = ctx.device.createRenderPipeline({
|
|
1109
|
-
layout: "auto",
|
|
1110
|
-
vertex: {
|
|
1111
|
-
module,
|
|
1112
|
-
entryPoint: "vs",
|
|
1113
|
-
buffers: [{ arrayStride: 8, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x2" }] }]
|
|
1114
|
-
},
|
|
1115
|
-
fragment: {
|
|
1116
|
-
module,
|
|
1117
|
-
entryPoint: "fs",
|
|
1118
|
-
targets: [{ format, blend: { color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }, alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" } } }]
|
|
1119
|
-
},
|
|
1120
|
-
primitive: { topology: "triangle-list" }
|
|
1121
|
-
});
|
|
1122
|
-
const quad = ctx.device.createBuffer({ size: 8 * 4, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
1123
|
-
ctx.device.queue.writeBuffer(quad, 0, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]));
|
|
1124
|
-
const idx = ctx.device.createBuffer({ size: 6 * 2, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
|
|
1125
|
-
ctx.device.queue.writeBuffer(idx, 0, new Uint16Array([0, 1, 2, 2, 1, 3]));
|
|
1126
|
-
return new _ParticlesRenderer(ctx, gpuCtx, format, pipeline, quad, idx, opts.species, opts.count, rsUniform, opts.vel ?? null, opts.color);
|
|
1127
|
-
}
|
|
1128
|
-
/** 渲染一帧(pos 来自 PingPong 当前侧;可传入覆盖数量) */
|
|
1129
|
-
render(pos, vel = null, count = this.#count) {
|
|
1130
|
-
const key = `${bufKey(pos.gpuBuffer)}:${vel ? bufKey(vel.gpuBuffer) : 0}`;
|
|
1131
|
-
let bg = this.#bgCache.get(key);
|
|
1132
|
-
if (!bg) {
|
|
1133
|
-
bg = this.#ctx.device.createBindGroup({
|
|
1134
|
-
layout: this.#pipeline.getBindGroupLayout(0),
|
|
1135
|
-
entries: [
|
|
1136
|
-
{ binding: 0, resource: { buffer: pos.gpuBuffer } },
|
|
1137
|
-
// 各 binding 按管线 layout 裁剪:velocity 模式的着色器不读 species,
|
|
1138
|
-
// 物种模式不读 vel——auto layout 会剔除未使用的绑定,bind group 必须同步
|
|
1139
|
-
...this.#colorMode === "species" ? [{ binding: 1, resource: { buffer: this.#species.gpuBuffer } }] : [],
|
|
1140
|
-
...this.#colorMode === "velocity" && this.#vel && vel ? [{ binding: 2, resource: { buffer: vel.gpuBuffer } }] : [],
|
|
1141
|
-
{ binding: 3, resource: { buffer: this.#rs } }
|
|
1142
|
-
]
|
|
1143
|
-
});
|
|
1144
|
-
this.#bgCache.set(key, bg);
|
|
1145
|
-
}
|
|
1146
|
-
const enc = this.#ctx.device.createCommandEncoder();
|
|
1147
|
-
const pass = enc.beginRenderPass({
|
|
1148
|
-
colorAttachments: [{
|
|
1149
|
-
view: this.#gpuCtx.getCurrentTexture().createView(),
|
|
1150
|
-
clearValue: { r: 0.012, g: 0.014, b: 0.024, a: 1 },
|
|
1151
|
-
loadOp: "clear",
|
|
1152
|
-
storeOp: "store"
|
|
1153
|
-
}]
|
|
1154
|
-
});
|
|
1155
|
-
pass.setPipeline(this.#pipeline);
|
|
1156
|
-
pass.setBindGroup(0, bg);
|
|
1157
|
-
pass.setVertexBuffer(0, this.#quad);
|
|
1158
|
-
pass.setIndexBuffer(this.#idx, "uint16");
|
|
1159
|
-
pass.drawIndexed(6, count);
|
|
1160
|
-
pass.end();
|
|
1161
|
-
this.#ctx.device.queue.submit([enc.finish()]);
|
|
1162
|
-
}
|
|
1163
|
-
destroy() {
|
|
1164
|
-
this.#quad.destroy();
|
|
1165
|
-
this.#idx.destroy();
|
|
1166
|
-
this.#bgCache.clear();
|
|
1167
|
-
}
|
|
1168
|
-
};
|
|
1169
|
-
|
|
1170
|
-
// src/packs/particles/index.ts
|
|
1171
|
-
var USIZE = 48;
|
|
1172
|
-
async function particles(config = {}) {
|
|
1173
|
-
const cfg = resolveConfig(config);
|
|
1174
|
-
const ctx = await GpuContext.get();
|
|
1175
|
-
const device = ctx.device;
|
|
1176
|
-
const worldHalf = 1 * Math.sqrt(cfg.count / 16e3);
|
|
1177
|
-
const pp = await PingPong.create({ pos: "vec2f", vel: "vec2f" }, cfg.count);
|
|
1178
|
-
const sideA = { pos: pp.current.pos, vel: pp.current.vel };
|
|
1179
|
-
const sideB = { pos: pp.other.pos, vel: pp.other.vel };
|
|
1180
|
-
const species = await Buffer.create("u32", cfg.count);
|
|
1181
|
-
const matrix = await Buffer.create("f32", 16);
|
|
1182
|
-
{
|
|
1183
|
-
const rand = mulberry32(cfg.seedHash);
|
|
1184
|
-
const pos0 = new Float32Array(cfg.count * 2);
|
|
1185
|
-
for (let i = 0; i < pos0.length; i++) pos0[i] = (rand() * 1.6 - 0.8) * worldHalf;
|
|
1186
|
-
const vel0 = new Float32Array(cfg.count * 2);
|
|
1187
|
-
const sp0 = new Uint32Array(cfg.count);
|
|
1188
|
-
for (let i = 0; i < cfg.count; i++) sp0[i] = Math.floor(rand() * 4);
|
|
1189
|
-
sideA.pos.write(pos0);
|
|
1190
|
-
sideA.vel.write(vel0);
|
|
1191
|
-
species.write(sp0);
|
|
1192
|
-
matrix.write(new Float32Array(cfg.forces));
|
|
1193
|
-
}
|
|
1194
|
-
const phys = { rMax: cfg.rMax, beta: cfg.beta, forceFactor: cfg.forceFactor, frictionHalfLife: 0.04, dt: cfg.dt };
|
|
1195
|
-
const uniform = device.createBuffer({ size: USIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "particles-params" });
|
|
1196
|
-
const gridSizeOf = (rMax, half = worldHalf) => Math.max(4, Math.ceil(2 * half / Math.max(rMax, 1e-3)));
|
|
1197
|
-
let gridSize = gridSizeOf(phys.rMax, worldHalf);
|
|
1198
|
-
const writeUniform = (dt) => {
|
|
1199
|
-
const buf = new ArrayBuffer(USIZE);
|
|
1200
|
-
const v = new DataView(buf);
|
|
1201
|
-
v.setUint32(0, cfg.count, true);
|
|
1202
|
-
v.setUint32(4, 0, true);
|
|
1203
|
-
v.setFloat32(8, dt, true);
|
|
1204
|
-
v.setFloat32(12, phys.rMax, true);
|
|
1205
|
-
v.setFloat32(16, phys.beta, true);
|
|
1206
|
-
v.setFloat32(20, phys.forceFactor, true);
|
|
1207
|
-
v.setFloat32(24, Math.exp(-dt / phys.frictionHalfLife), true);
|
|
1208
|
-
v.setFloat32(28, worldHalf, true);
|
|
1209
|
-
v.setFloat32(32, cfg.bounds === "wrap" ? 1 : 0, true);
|
|
1210
|
-
v.setUint32(36, gridSize, true);
|
|
1211
|
-
v.setUint32(40, gridSize * gridSize, true);
|
|
1212
|
-
v.setUint32(44, Math.ceil(cfg.maxNeighbors / 9), true);
|
|
1213
|
-
device.queue.writeBuffer(uniform, 0, buf);
|
|
1214
|
-
};
|
|
1215
|
-
writeUniform(cfg.dt);
|
|
1216
|
-
const compile = async (code, label) => {
|
|
1217
|
-
const module = device.createShaderModule({ code, label });
|
|
1218
|
-
const info = await module.getCompilationInfo();
|
|
1219
|
-
const errors = info.messages.filter((m) => m.type === "error");
|
|
1220
|
-
if (errors.length > 0) throw new CompileError(label, errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
|
|
1221
|
-
return module;
|
|
1222
|
-
};
|
|
1223
|
-
const makePipeline = async (module, entryPoint, label) => createComputePipelineChecked(device, module, `${label}(${entryPoint})`, entryPoint);
|
|
1224
|
-
let simPipeline = null;
|
|
1225
|
-
let bgAB = null;
|
|
1226
|
-
let bgBA = null;
|
|
1227
|
-
let grid = null;
|
|
1228
|
-
const buildGrid = async (size) => {
|
|
1229
|
-
const cells = size * size;
|
|
1230
|
-
const count = await Buffer.create("u32", cells);
|
|
1231
|
-
const start = await Buffer.create("u32", cells);
|
|
1232
|
-
const fill = await Buffer.create("u32", cells);
|
|
1233
|
-
const order = await Buffer.create("u32", cfg.count);
|
|
1234
|
-
count.write(new Uint32Array(cells));
|
|
1235
|
-
const mCounts = await compile(gridCountsWgsl(), "grid-counts");
|
|
1236
|
-
const mScan = await compile(gridScanWgsl(), "grid-scan");
|
|
1237
|
-
const mScatter = await compile(gridScatterWgsl(), "grid-scatter");
|
|
1238
|
-
const mForce = await compile(gridForceWgsl(4), "grid-force");
|
|
1239
|
-
const pCounts = await makePipeline(mCounts, "main", "grid-counts");
|
|
1240
|
-
const pScan = await makePipeline(mScan, "main", "grid-scan");
|
|
1241
|
-
const pScatter = await makePipeline(mScatter, "main", "grid-scatter");
|
|
1242
|
-
const pForceCell = await makePipeline(mForce, "main_force_cell", "grid-force-cell");
|
|
1243
|
-
const pForceInt = await makePipeline(mForce, "main_force_integrate", "grid-force-integrate");
|
|
1244
|
-
const partial = await Buffer.create("vec2f", cfg.count * 9);
|
|
1245
|
-
const sortedPos = await Buffer.create("vec2f", cfg.count);
|
|
1246
|
-
const sortedSp = await Buffer.create("u32", cfg.count);
|
|
1247
|
-
const bgCounts = (readPos) => device.createBindGroup({
|
|
1248
|
-
layout: pCounts.getBindGroupLayout(0),
|
|
1249
|
-
entries: [
|
|
1250
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
1251
|
-
{ binding: 1, resource: { buffer: readPos.gpuBuffer } },
|
|
1252
|
-
{ binding: 2, resource: { buffer: count.gpuBuffer } }
|
|
1253
|
-
]
|
|
1254
|
-
});
|
|
1255
|
-
const bgScan = device.createBindGroup({
|
|
1256
|
-
layout: pScan.getBindGroupLayout(0),
|
|
1257
|
-
entries: [
|
|
1258
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
1259
|
-
{ binding: 1, resource: { buffer: count.gpuBuffer } },
|
|
1260
|
-
{ binding: 2, resource: { buffer: start.gpuBuffer } },
|
|
1261
|
-
{ binding: 3, resource: { buffer: fill.gpuBuffer } }
|
|
1262
|
-
]
|
|
1263
|
-
});
|
|
1264
|
-
const bgScatter = (readPos) => device.createBindGroup({
|
|
1265
|
-
layout: pScatter.getBindGroupLayout(0),
|
|
1266
|
-
entries: [
|
|
1267
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
1268
|
-
{ binding: 1, resource: { buffer: readPos.gpuBuffer } },
|
|
1269
|
-
{ binding: 2, resource: { buffer: species.gpuBuffer } },
|
|
1270
|
-
{ binding: 3, resource: { buffer: fill.gpuBuffer } },
|
|
1271
|
-
{ binding: 4, resource: { buffer: order.gpuBuffer } },
|
|
1272
|
-
{ binding: 5, resource: { buffer: sortedPos.gpuBuffer } },
|
|
1273
|
-
{ binding: 6, resource: { buffer: sortedSp.gpuBuffer } }
|
|
1274
|
-
]
|
|
1275
|
-
});
|
|
1276
|
-
const bgForceCell = (readPos) => device.createBindGroup({
|
|
1277
|
-
layout: pForceCell.getBindGroupLayout(0),
|
|
1278
|
-
entries: [
|
|
1279
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
1280
|
-
{ binding: 1, resource: { buffer: matrix.gpuBuffer } },
|
|
1281
|
-
{ binding: 2, resource: { buffer: species.gpuBuffer } },
|
|
1282
|
-
{ binding: 3, resource: { buffer: readPos.gpuBuffer } },
|
|
1283
|
-
{ binding: 4, resource: { buffer: sortedPos.gpuBuffer } },
|
|
1284
|
-
{ binding: 5, resource: { buffer: sortedSp.gpuBuffer } },
|
|
1285
|
-
{ binding: 6, resource: { buffer: start.gpuBuffer } },
|
|
1286
|
-
{ binding: 7, resource: { buffer: fill.gpuBuffer } },
|
|
1287
|
-
{ binding: 8, resource: { buffer: order.gpuBuffer } },
|
|
1288
|
-
{ binding: 9, resource: { buffer: partial.gpuBuffer } }
|
|
1289
|
-
]
|
|
1290
|
-
});
|
|
1291
|
-
const bgIntegrate = (read, write) => device.createBindGroup({
|
|
1292
|
-
layout: pForceInt.getBindGroupLayout(0),
|
|
1293
|
-
entries: [
|
|
1294
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
1295
|
-
{ binding: 1, resource: { buffer: read.vel.gpuBuffer } },
|
|
1296
|
-
{ binding: 2, resource: { buffer: read.pos.gpuBuffer } },
|
|
1297
|
-
{ binding: 3, resource: { buffer: partial.gpuBuffer } },
|
|
1298
|
-
{ binding: 4, resource: { buffer: write.pos.gpuBuffer } },
|
|
1299
|
-
{ binding: 5, resource: { buffer: write.vel.gpuBuffer } }
|
|
1300
|
-
]
|
|
1301
|
-
});
|
|
1302
|
-
return {
|
|
1303
|
-
size,
|
|
1304
|
-
count,
|
|
1305
|
-
start,
|
|
1306
|
-
fill,
|
|
1307
|
-
order,
|
|
1308
|
-
partial,
|
|
1309
|
-
sortedPos,
|
|
1310
|
-
sortedSp,
|
|
1311
|
-
pCounts,
|
|
1312
|
-
pScan,
|
|
1313
|
-
pScatter,
|
|
1314
|
-
pForceCell,
|
|
1315
|
-
pForceInt,
|
|
1316
|
-
bgCountsA: bgCounts(sideA.pos),
|
|
1317
|
-
bgCountsB: bgCounts(sideB.pos),
|
|
1318
|
-
bgScan,
|
|
1319
|
-
bgScatterA: bgScatter(sideA.pos),
|
|
1320
|
-
bgScatterB: bgScatter(sideB.pos),
|
|
1321
|
-
bgForceCellAB: bgForceCell(sideA.pos),
|
|
1322
|
-
bgForceCellBA: bgForceCell(sideB.pos),
|
|
1323
|
-
bgIntegrateAB: bgIntegrate(sideA, sideB),
|
|
1324
|
-
bgIntegrateBA: bgIntegrate(sideB, sideA)
|
|
1325
|
-
};
|
|
1326
|
-
};
|
|
1327
|
-
if (cfg.mode === "grid") grid = await buildGrid(gridSize);
|
|
1328
|
-
else {
|
|
1329
|
-
const module = await compile(simWgsl(cfg.mode, 4), `particles-sim(${cfg.mode})`);
|
|
1330
|
-
simPipeline = await makePipeline(module, "main", `particles-sim(${cfg.mode})`);
|
|
1331
|
-
const bg = (read, write) => device.createBindGroup({
|
|
1332
|
-
layout: simPipeline.getBindGroupLayout(0),
|
|
1333
|
-
entries: [
|
|
1334
|
-
{ binding: 0, resource: { buffer: uniform } },
|
|
1335
|
-
{ binding: 1, resource: { buffer: matrix.gpuBuffer } },
|
|
1336
|
-
{ binding: 2, resource: { buffer: species.gpuBuffer } },
|
|
1337
|
-
{ binding: 3, resource: { buffer: read.pos.gpuBuffer } },
|
|
1338
|
-
{ binding: 4, resource: { buffer: read.vel.gpuBuffer } },
|
|
1339
|
-
{ binding: 5, resource: { buffer: write.pos.gpuBuffer } },
|
|
1340
|
-
{ binding: 6, resource: { buffer: write.vel.gpuBuffer } }
|
|
1341
|
-
]
|
|
1342
|
-
});
|
|
1343
|
-
bgAB = bg(sideA, sideB);
|
|
1344
|
-
bgBA = bg(sideB, sideA);
|
|
1345
|
-
}
|
|
1346
|
-
let renderer = null;
|
|
1347
|
-
let frame = 0;
|
|
1348
|
-
let lastFps = 0;
|
|
1349
|
-
let fpsFrames = 0;
|
|
1350
|
-
let fpsAcc = 0;
|
|
1351
|
-
let fpsLast = performance.now();
|
|
1352
|
-
let gpuErrorCount = 0;
|
|
1353
|
-
device.addEventListener?.("uncapturederror", (e) => {
|
|
1354
|
-
gpuErrorCount++;
|
|
1355
|
-
const msg = e.error?.message ?? String(e);
|
|
1356
|
-
const g = globalThis;
|
|
1357
|
-
g.__firstGpuError ??= msg.slice(0, 400);
|
|
1358
|
-
g.__lastGpuError = msg.slice(0, 300);
|
|
1359
|
-
console.error("[wgpu-kit particles] GPU \u9519\u8BEF:", msg);
|
|
1360
|
-
});
|
|
1361
|
-
return {
|
|
1362
|
-
config: cfg,
|
|
1363
|
-
async attach(canvas) {
|
|
1364
|
-
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
1365
|
-
canvas.width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
|
1366
|
-
canvas.height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
|
1367
|
-
renderer = await ParticlesRenderer.create(canvas, {
|
|
1368
|
-
count: cfg.count,
|
|
1369
|
-
species,
|
|
1370
|
-
vel: sideA.vel,
|
|
1371
|
-
color: cfg.color,
|
|
1372
|
-
pointSize: cfg.pointSize * worldHalf,
|
|
1373
|
-
worldHalf
|
|
1374
|
-
});
|
|
1375
|
-
},
|
|
1376
|
-
tick(dtMultiplier = 1) {
|
|
1377
|
-
const dt = cfg.dt * dtMultiplier;
|
|
1378
|
-
writeUniform(dt);
|
|
1379
|
-
const useAB = frame % 2 === 0;
|
|
1380
|
-
const read = useAB ? sideA : sideB;
|
|
1381
|
-
const enc = device.createCommandEncoder();
|
|
1382
|
-
const pass = enc.beginComputePass();
|
|
1383
|
-
if (grid) {
|
|
1384
|
-
pass.setPipeline(grid.pCounts);
|
|
1385
|
-
pass.setBindGroup(0, useAB ? grid.bgCountsA : grid.bgCountsB);
|
|
1386
|
-
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
1387
|
-
pass.setPipeline(grid.pScan);
|
|
1388
|
-
pass.setBindGroup(0, grid.bgScan);
|
|
1389
|
-
pass.dispatchWorkgroups(1);
|
|
1390
|
-
pass.setPipeline(grid.pScatter);
|
|
1391
|
-
pass.setBindGroup(0, useAB ? grid.bgScatterA : grid.bgScatterB);
|
|
1392
|
-
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
1393
|
-
pass.end();
|
|
1394
|
-
device.queue.submit([enc.finish()]);
|
|
1395
|
-
const enc2 = device.createCommandEncoder();
|
|
1396
|
-
const pass2 = enc2.beginComputePass();
|
|
1397
|
-
pass2.setPipeline(grid.pForceCell);
|
|
1398
|
-
pass2.setBindGroup(0, useAB ? grid.bgForceCellAB : grid.bgForceCellBA);
|
|
1399
|
-
pass2.dispatchWorkgroups(Math.ceil(cfg.count * 9 / WORKGROUP));
|
|
1400
|
-
pass2.end();
|
|
1401
|
-
device.queue.submit([enc2.finish()]);
|
|
1402
|
-
const enc3 = device.createCommandEncoder();
|
|
1403
|
-
const pass3 = enc3.beginComputePass();
|
|
1404
|
-
pass3.setPipeline(grid.pForceInt);
|
|
1405
|
-
pass3.setBindGroup(0, useAB ? grid.bgIntegrateAB : grid.bgIntegrateBA);
|
|
1406
|
-
pass3.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
1407
|
-
pass3.end();
|
|
1408
|
-
device.queue.submit([enc3.finish()]);
|
|
1409
|
-
} else {
|
|
1410
|
-
pass.setPipeline(simPipeline);
|
|
1411
|
-
pass.setBindGroup(0, useAB ? bgAB : bgBA);
|
|
1412
|
-
pass.dispatchWorkgroups(Math.ceil(cfg.count / WORKGROUP));
|
|
1413
|
-
pass.end();
|
|
1414
|
-
device.queue.submit([enc.finish()]);
|
|
1415
|
-
}
|
|
1416
|
-
renderer?.render((useAB ? pp.other : pp.current).pos, (useAB ? pp.other : pp.current).vel);
|
|
1417
|
-
pp.swap();
|
|
1418
|
-
frame++;
|
|
1419
|
-
fpsFrames++;
|
|
1420
|
-
const now = performance.now();
|
|
1421
|
-
fpsAcc += now - fpsLast;
|
|
1422
|
-
fpsLast = now;
|
|
1423
|
-
if (fpsAcc >= 500) {
|
|
1424
|
-
lastFps = fpsFrames / (fpsAcc / 1e3);
|
|
1425
|
-
fpsFrames = 0;
|
|
1426
|
-
fpsAcc = 0;
|
|
1427
|
-
}
|
|
1428
|
-
},
|
|
1429
|
-
setForces(forces) {
|
|
1430
|
-
const m = resolveMatrix(forces, hashSeed(cfg.seed));
|
|
1431
|
-
matrix.write(new Float32Array(m));
|
|
1432
|
-
cfg.forces = m;
|
|
1433
|
-
cfg.forcesName = typeof forces === "string" ? forces : "custom";
|
|
1434
|
-
},
|
|
1435
|
-
setParams(p) {
|
|
1436
|
-
Object.assign(phys, p);
|
|
1437
|
-
if (grid && p.rMax !== void 0) {
|
|
1438
|
-
const g = gridSizeOf(phys.rMax);
|
|
1439
|
-
if (g !== gridSize) {
|
|
1440
|
-
gridSize = g;
|
|
1441
|
-
void (async () => {
|
|
1442
|
-
const old = grid;
|
|
1443
|
-
grid = await buildGrid(gridSize);
|
|
1444
|
-
old.count.destroy();
|
|
1445
|
-
old.start.destroy();
|
|
1446
|
-
old.fill.destroy();
|
|
1447
|
-
old.order.destroy();
|
|
1448
|
-
})();
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
},
|
|
1452
|
-
snapshot() {
|
|
1453
|
-
return JSON.stringify({
|
|
1454
|
-
count: cfg.count,
|
|
1455
|
-
forces: cfg.forcesName,
|
|
1456
|
-
mode: cfg.mode,
|
|
1457
|
-
color: cfg.color,
|
|
1458
|
-
bounds: cfg.bounds,
|
|
1459
|
-
seed: cfg.seed,
|
|
1460
|
-
rMax: phys.rMax,
|
|
1461
|
-
beta: phys.beta,
|
|
1462
|
-
forceFactor: phys.forceFactor,
|
|
1463
|
-
frictionHalfLife: phys.frictionHalfLife,
|
|
1464
|
-
dt: phys.dt,
|
|
1465
|
-
pointSize: cfg.pointSize
|
|
1466
|
-
});
|
|
1467
|
-
},
|
|
1468
|
-
stats() {
|
|
1469
|
-
return { fps: lastFps, gpuErrors: gpuErrorCount };
|
|
1470
|
-
},
|
|
1471
|
-
debugGrid: grid ? () => {
|
|
1472
|
-
const g = grid;
|
|
1473
|
-
return { partial: g.partial, start: g.start, fill: g.fill, sortedPos: g.sortedPos, sortedSp: g.sortedSp, order: g.order };
|
|
1474
|
-
} : void 0,
|
|
1475
|
-
buffers() {
|
|
1476
|
-
return { pos: pp.current.pos, vel: pp.current.vel, species };
|
|
1477
|
-
},
|
|
1478
|
-
destroy() {
|
|
1479
|
-
renderer?.destroy();
|
|
1480
|
-
pp.destroy();
|
|
1481
|
-
species.destroy();
|
|
1482
|
-
matrix.destroy();
|
|
1483
|
-
uniform.destroy();
|
|
1484
|
-
if (grid) {
|
|
1485
|
-
grid.count.destroy();
|
|
1486
|
-
grid.start.destroy();
|
|
1487
|
-
grid.fill.destroy();
|
|
1488
|
-
grid.order.destroy();
|
|
1489
|
-
grid.partial.destroy();
|
|
1490
|
-
grid.sortedPos.destroy();
|
|
1491
|
-
grid.sortedSp.destroy();
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
export {
|
|
1497
|
-
Buffer,
|
|
1498
|
-
CompileError,
|
|
1499
|
-
GpuContext,
|
|
1500
|
-
PingPong,
|
|
1501
|
-
TYPES,
|
|
1502
|
-
UsageError,
|
|
1503
|
-
WebGPUUnavailableError,
|
|
1504
|
-
WgpuKitError,
|
|
1505
|
-
elementKernel,
|
|
1506
|
-
packUniform,
|
|
1507
|
-
particles,
|
|
1508
|
-
planUniform,
|
|
1509
|
-
rawKernel
|
|
1510
|
-
};
|
|
1
|
+
import { GpuContext } from "./core/context.js";
|
|
2
|
+
import { Buffer } from "./core/buffer.js";
|
|
3
|
+
import { elementKernel } from "./core/kernel.js";
|
|
4
|
+
import { PingPong } from "./core/pingpong.js";
|
|
5
|
+
import { rawKernel } from "./core/raw.js";
|
|
6
|
+
import { UsageError } from "./core/errors.js";
|
|
7
|
+
export { GpuContext, Buffer, elementKernel, PingPong, rawKernel };
|
|
8
|
+
// 主入口直达旗舰包:import { particles } from 'wgpu-kit' 开箱即用
|
|
9
|
+
export { particles } from "./packs/particles/index.js";
|
|
10
|
+
export { TYPES, planUniform, packUniform, packUniformInto } from "./core/layout.js";
|
|
11
|
+
export { WgpuKitError, WebGPUUnavailableError, CompileError, UsageError } from "./core/errors.js";
|