wgpu-kit 0.9.10 → 0.9.11
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 +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1026 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm i wgpu-kit
|
|
|
16
16
|
**100,000 particles in 5 lines:**
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
|
-
import { particles } from 'wgpu-kit';
|
|
19
|
+
import { particles } from 'wgpu-kit'; // 或 'wgpu-kit/particles'
|
|
20
20
|
|
|
21
21
|
const sim = await particles({ count: 100_000, forces: 'cells' });
|
|
22
22
|
await sim.attach(canvas);
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { elementKernel, type ElementKernel, type ElementKernelSpec } from './cor
|
|
|
4
4
|
import { PingPong } from './core/pingpong.ts';
|
|
5
5
|
import { rawKernel } from './core/raw.ts';
|
|
6
6
|
export { GpuContext, Buffer, elementKernel, PingPong, rawKernel };
|
|
7
|
+
export { particles, type ParticlesSim } from './packs/particles/index.ts';
|
|
7
8
|
export type { ElementKernel, ElementKernelSpec };
|
|
8
9
|
export { TYPES, planUniform, packUniform, type ScalarKind } from './core/layout.ts';
|
|
9
10
|
export { WgpuKitError, WebGPUUnavailableError, CompileError, UsageError } from './core/errors.ts';
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,16 @@ ${mapped}`);
|
|
|
27
27
|
};
|
|
28
28
|
var UsageError = class extends WgpuKitError {
|
|
29
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
|
+
}
|
|
30
40
|
|
|
31
41
|
// src/core/context.ts
|
|
32
42
|
var GpuContext = class _GpuContext {
|
|
@@ -468,6 +478,1021 @@ function rawKernel(code, entryPoint = "main", label = "rawKernel") {
|
|
|
468
478
|
}
|
|
469
479
|
};
|
|
470
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
|
+
}
|
|
471
1496
|
export {
|
|
472
1497
|
Buffer,
|
|
473
1498
|
CompileError,
|
|
@@ -479,6 +1504,7 @@ export {
|
|
|
479
1504
|
WgpuKitError,
|
|
480
1505
|
elementKernel,
|
|
481
1506
|
packUniform,
|
|
1507
|
+
particles,
|
|
482
1508
|
planUniform,
|
|
483
1509
|
rawKernel
|
|
484
1510
|
};
|