/ proof-of-concept / cymbalsim
Sign in
proof-of-concept / cymbalsim
cymbalsim / src / sim / shaders / step.wgsl
328 lines · 15.0 KBCodeBlameHistory
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 1// One explicit time step (symplectic Euler) of the von Karman plate with a
2// thickness field, a stress-free profile, the drumstick, and the pads of the
3// stand.
4//
5// State layout (structure of arrays, n = nx * ny nodes):
6// pos: [u | v | w | wLo] wLo carries the rounding error of the w update
7// vel: [du | dv | dw]
8// A workgroup of 16x16 threads advances a 12x12 tile of nodes in three
9// phases separated by barriers. (1) Every thread loads one entry of the tile
10// plus its 2-node halo (w, u, v, w0, thickness, node weights) into shared
11// memory. (2) The threads compute, once each and again into shared memory,
12// the membrane forces of the 13x13 cells around the tile and the weighted
13// bending moments and mixed differences that the bending stencil gathers.
14// (3) Each of the 144 tile nodes evaluates its update from shared memory.
15// On grids of a few thousand nodes the kernel is bound by the latency of one
16// workgroup rather than by throughput, so the shared phases, which replace a
17// tenfold per-node recomputation of the stencil ingredients, are what make a
18// step cheap.
19//
20// Nodes outside the plate (weight 0) are skipped: a second difference exists
21// only where the node and both neighbors belong to the plate, and a cell
22// contributes only when its four corners do. That gives the free edge of the
23// rectangle and a staircase free edge for any other outline, hole included.
25@group(0) @binding(0) var<uniform> P: Params;
26@group(0) @binding(1) var<uniform> SP: StepParams;
27@group(0) @binding(2) var<storage, read> posIn: array<f32>;
28@group(0) @binding(3) var<storage, read> velIn: array<f32>;
29@group(0) @binding(4) var<storage, read_write> posOut: array<f32>;
30@group(0) @binding(5) var<storage, read_write> velOut: array<f32>;
31@group(0) @binding(6) var<storage, read_write> hist: array<f32>; // [slot * n + node]: transverse velocity at the audio sample instants
32// Two 8-float slots of drumstick state: position, velocity, force, max force,
33// time since the contact released, withdrawn flag, and two spare.
34const STICK_RELEASE: f32 = 5e-3; // [s] a release longer than this ends the hit
35@group(0) @binding(7) var<storage, read_write> stick: array<f32>;
37const TILE = 12; // nodes per workgroup along each axis (must match simulation.ts)
38const HALO = 2;
39const TW = 16; // TILE + 2 * HALO: the loaded tile, one thread per entry
40const CT = 13; // TILE + 1: cells adjacent to the tile's nodes
41const MT = 14; // TILE + 2: nodes at which the bending moments are needed
42var<workgroup> tw: array<f32, 256>; // w
43var<workgroup> tu: array<f32, 256>; // u
44var<workgroup> tv: array<f32, 256>; // v
45var<workgroup> tw0: array<f32, 256>; // stress-free shape
46var<workgroup> th: array<f32, 256>; // thickness
47var<workgroup> tn: array<u32, 64>; // node weights in quarters (0..4), four to a word
48var<workgroup> cellF: array<f32, 2028>; // membrane forces of the CT x CT cells, 4 nodes x 3 components
49var<workgroup> ma: array<f32, 196>; // weighted bending moments at the MT x MT inner nodes
50var<workgroup> mb: array<f32, 196>;
51var<workgroup> cc: array<f32, 169>; // weighted mixed differences at the CT x CT cells
53var<private> ox: i32;
54var<private> oy: i32;
55var<private> ihx2: f32;
56var<private> ihy2: f32;
57var<private> ihxy: f32;
59fn id(i: i32, j: i32) -> u32 { return u32(j) * P.nx + u32(i); }
60fn lidx(i: i32, j: i32) -> u32 { return u32(j - oy) * u32(TW) + u32(i - ox); }
61fn midx(i: i32, j: i32) -> u32 { return u32(j - oy - 1) * u32(MT) + u32(i - ox - 1); }
62fn cidx(ci: i32, cj: i32) -> u32 { return u32(cj - oy - 1) * u32(CT) + u32(ci - ox - 1); }
63fn W(i: i32, j: i32) -> f32 { return tw[lidx(i, j)]; }
64fn U(i: i32, j: i32) -> f32 { return tu[lidx(i, j)]; }
65fn V(i: i32, j: i32) -> f32 { return tv[lidx(i, j)]; }
66fn W0(i: i32, j: i32) -> f32 { return tw0[lidx(i, j)]; }
67fn DW(i: i32, j: i32) -> f32 { return velIn[2u * P.n + id(i, j)]; }
68fn TH(i: i32, j: i32) -> f32 { return th[lidx(i, j)]; }
69fn NWq(i: i32, j: i32) -> u32 { let l = lidx(i, j); return (tn[l >> 2u] >> ((l & 3u) * 8u)) & 0xFFu; }
70fn NW(i: i32, j: i32) -> f32 { return 0.25 * f32(NWq(i, j)); }
71fn INS(i: i32, j: i32) -> bool { return NWq(i, j) != 0u; }
72fn insCell(ci: i32, cj: i32) -> bool { return INS(ci, cj) && INS(ci + 1, cj) && INS(ci + 1, cj + 1) && INS(ci, cj + 1); }
73fn Dn(i: i32, j: i32) -> f32 { let h = TH(i, j); return P.Db * h * h * h; }
74fn cellH(ci: i32, cj: i32) -> f32 { return 0.25 * (TH(ci, cj) + TH(ci + 1, cj) + TH(ci + 1, cj + 1) + TH(ci, cj + 1)); }
76// Graph Laplacian of the transverse velocity at node (i, j), over neighbors in the plate.
77fn lapDW(i: i32, j: i32) -> f32 {
78 let c = DW(i, j);
79 var lap = 0.0;
80 if (INS(i - 1, j)) { lap += (DW(i - 1, j) - c) * ihx2; }
81 if (INS(i + 1, j)) { lap += (DW(i + 1, j) - c) * ihx2; }
82 if (INS(i, j - 1)) { lap += (DW(i, j - 1) - c) * ihy2; }
83 if (INS(i, j + 1)) { lap += (DW(i, j + 1) - c) * ihy2; }
84 return lap;
87// Weighted bending moments at nodes; zero where the second difference is undefined.
88fn Ma(i: i32, j: i32) -> f32 {
89 if (!(INS(i - 1, j) && INS(i, j) && INS(i + 1, j))) { return 0.0; }
90 let a = (W(i + 1, j) - 2.0 * W(i, j) + W(i - 1, j)) * ihx2;
91 var b = 0.0;
92 if (INS(i, j - 1) && INS(i, j + 1)) { b = (W(i, j + 1) - 2.0 * W(i, j) + W(i, j - 1)) * ihy2; }
93 return NW(i, j) * Dn(i, j) * (a + P.nu * b);
95fn Mb(i: i32, j: i32) -> f32 {
96 if (!(INS(i, j - 1) && INS(i, j) && INS(i, j + 1))) { return 0.0; }
97 let b = (W(i, j + 1) - 2.0 * W(i, j) + W(i, j - 1)) * ihy2;
98 var a = 0.0;
99 if (INS(i - 1, j) && INS(i + 1, j)) { a = (W(i + 1, j) - 2.0 * W(i, j) + W(i - 1, j)) * ihx2; }
100 return NW(i, j) * Dn(i, j) * (b + P.nu * a);
102fn Cc(ci: i32, cj: i32) -> f32 {
103 if (!insCell(ci, cj)) { return 0.0; }
104 let h = cellH(ci, cj);
105 return P.Db * h * h * h * (W(ci + 1, cj + 1) - W(ci + 1, cj) - W(ci, cj + 1) + W(ci, cj)) * ihxy;
108// Membrane forces of the bilinear cell (ci, cj) on its four nodes (2x2 Gauss), written to cellF at local cell index lc.
109fn cellForces(ci: i32, cj: i32, lc: u32) {
110 let base = lc * 12u;
111 if (!insCell(ci, cj)) {
112 for (var q = 0u; q < 12u; q++) { cellF[base + q] = 0.0; }
113 return;
114 }
115 let hc = cellH(ci, cj);
116 let C = P.Cm * hc;
117 let G = P.Gm * hc;
118 let Un = vec4f(U(ci, cj), U(ci + 1, cj), U(ci + 1, cj + 1), U(ci, cj + 1));
119 let Vn = vec4f(V(ci, cj), V(ci + 1, cj), V(ci + 1, cj + 1), V(ci, cj + 1));
120 let W0n = vec4f(W0(ci, cj), W0(ci + 1, cj), W0(ci + 1, cj + 1), W0(ci, cj + 1));
121 let Wn = vec4f(W(ci, cj), W(ci + 1, cj), W(ci + 1, cj + 1), W(ci, cj + 1)) + W0n; // total shape
122 let q = 0.25 * P.hx * P.hy;
123 let ihx = 1.0 / P.hx; let ihy = 1.0 / P.hy;
124 let g3 = 0.57735026918962576;
125 var Fu = vec4f(0.0); var Fv = vec4f(0.0); var Fw = vec4f(0.0);
126 for (var gp = 0; gp < 4; gp++) {
127 var xi = -g3; if ((gp & 1) != 0) { xi = g3; }
128 var eta = -g3; if ((gp & 2) != 0) { eta = g3; }
129 let dNdx = vec4f(-(1.0 - eta), 1.0 - eta, 1.0 + eta, -(1.0 + eta)) * (0.5 * ihx);
130 let dNdy = vec4f(-(1.0 - xi), -(1.0 + xi), 1.0 + xi, 1.0 - xi) * (0.5 * ihy);
131 let ux = dot(dNdx, Un); let uy = dot(dNdy, Un);
132 let vx = dot(dNdx, Vn); let vy = dot(dNdy, Vn);
133 let wx = dot(dNdx, Wn); let wy = dot(dNdy, Wn);
134 let w0x = dot(dNdx, W0n); let w0y = dot(dNdy, W0n);
135 // Marguerre strains: total slope minus the stress-free slope.
136 let exx = ux + 0.5 * (wx * wx - w0x * w0x);
137 let eyy = vy + 0.5 * (wy * wy - w0y * w0y);
138 let gxy = uy + vx + wx * wy - w0x * w0y;
139 let Nxx = C * (exx + P.nu * eyy);
140 let Nyy = C * (eyy + P.nu * exx);
141 let Nxy = G * gxy;
142 Fu -= q * (Nxx * dNdx + Nxy * dNdy);
143 Fv -= q * (Nyy * dNdy + Nxy * dNdx);
144 Fw -= q * ((Nxx * wx + Nxy * wy) * dNdx + (Nxy * wx + Nyy * wy) * dNdy);
145 }
146 for (var a = 0u; a < 4u; a++) {
147 cellF[base + a * 3u] = Fu[a];
148 cellF[base + a * 3u + 1u] = Fv[a];
149 cellF[base + a * 3u + 2u] = Fw[a];
150 }
153// True for the plate nodes of the drumstick contact patch (same rule as patchAround in params.ts).
154fn inContactPatch(i: i32, j: i32) -> bool {
155 if (nodeWeight(i, j) == 0.0) { return false; }
156 let dxs = nodeX(i) - P.strikeX; let dys = nodeY(j) - P.strikeY;
157 return dxs * dxs + dys * dys <= P.strikeR2 || id(i, j) == P.strikeNode;
160fn cellForce(ci: i32, cj: i32, a: u32) -> vec3f {
161 let base = cidx(ci, cj) * 12u + a * 3u;
162 return vec3f(cellF[base], cellF[base + 1u], cellF[base + 2u]);
165// Force of the pads on a node with bottom-pad rate kb and transverse
166// displacement w (same rule as padForce in reference.ts). The constant part
167// that balances the weight of the plate is subtracted, so w = 0 is rest.
168fn padForce(kb: f32, w: f32) -> f32 {
169 if (P.liftoff == 0u) {
170 if (P.hasTop != 0u) { return -2.0 * kb * w; }
171 return -kb * w;
172 }
173 var f = kb * max(P.preloadBottom - w, 0.0) - kb * P.preloadBottom;
174 if (P.hasTop != 0u) { f -= kb * max(P.preloadTop + w, 0.0) - kb * P.preloadTop; }
175 return f;
178@compute @workgroup_size(16, 16)
179fn main(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) {
180 let n = P.n;
181 ox = i32(wid.x) * TILE - HALO;
182 oy = i32(wid.y) * TILE - HALO;
183 ihx2 = 1.0 / (P.hx * P.hx); ihy2 = 1.0 / (P.hy * P.hy); ihxy = 1.0 / (P.hx * P.hy);
184 let lt = lid.y * 16u + lid.x;
186 // Phase 1: one entry of the tile with halo per thread.
187 {
188 let gi = ox + i32(lid.x); let gj = oy + i32(lid.y);
189 var ww = 0.0; var uu = 0.0; var vv = 0.0; var z0 = 0.0; var hh = 0.0;
190 if (gi >= 0 && gi < i32(P.nx) && gj >= 0 && gj < i32(P.ny)) {
191 let k = id(gi, gj);
192 let st = nd[k];
193 ww = posIn[2u * n + k]; uu = posIn[k]; vv = posIn[n + k]; z0 = st.x; hh = st.z;
194 }
195 tw[lt] = ww; tu[lt] = uu; tv[lt] = vv; tw0[lt] = z0; th[lt] = hh;
196 if (lt < 64u) {
197 var word = 0u;
198 for (var q = 0u; q < 4u; q++) {
199 let e = lt * 4u + q;
200 let ei = ox + i32(e % u32(TW)); let ej = oy + i32(e / u32(TW));
201 var wq = 0u;
202 if (ei >= 0 && ei < i32(P.nx) && ej >= 0 && ej < i32(P.ny)) { wq = u32(round(4.0 * nd[id(ei, ej)].y)); }
203 word |= wq << (q * 8u);
204 }
205 tn[lt] = word;
206 }
207 }
208 workgroupBarrier();
210 // Phase 2: membrane forces of the cells, bending moments and mixed differences.
211 if (lt < 169u) {
212 let ci = ox + 1 + i32(lt % u32(CT)); let cj = oy + 1 + i32(lt / u32(CT));
213 cellForces(ci, cj, lt);
214 cc[lt] = Cc(ci, cj);
215 }
216 if (lt < 196u) {
217 let mi = ox + 1 + i32(lt % u32(MT)); let mj = oy + 1 + i32(lt / u32(MT));
218 ma[lt] = Ma(mi, mj);
219 mb[lt] = Mb(mi, mj);
220 }
221 workgroupBarrier();
223 // Phase 3: the update of the TILE x TILE nodes (no barriers past this point).
224 if (lt >= 144u) { return; }
225 let i = ox + HALO + i32(lt % u32(TILE)); let j = oy + HALO + i32(lt / u32(TILE));
226 if (i >= i32(P.nx) || j >= i32(P.ny)) { return; }
227 let k = id(i, j);
228 let weight = NW(i, j);
229 if (weight == 0.0) {
230 posOut[k] = 0.0; posOut[n + k] = 0.0; posOut[2u * n + k] = 0.0; posOut[3u * n + k] = 0.0;
231 velOut[k] = 0.0; velOut[n + k] = 0.0; velOut[2u * n + k] = 0.0;
232 return;
233 }
235 // Elastic forces: bending from the discrete Kirchhoff energy, membrane from the cells.
236 let bend = (ma[midx(i - 1, j)] - 2.0 * ma[midx(i, j)] + ma[midx(i + 1, j)]) * ihx2
237 + (mb[midx(i, j - 1)] - 2.0 * mb[midx(i, j)] + mb[midx(i, j + 1)]) * ihy2
238 + 2.0 * (1.0 - P.nu) * (cc[cidx(i, j)] - cc[cidx(i - 1, j)] - cc[cidx(i, j - 1)] + cc[cidx(i - 1, j - 1)]) * ihxy;
239 var F = vec3f(0.0, 0.0, -P.hx * P.hy * bend);
240 F += cellForce(i, j, 0u);
241 F += cellForce(i - 1, j, 1u);
242 F += cellForce(i - 1, j - 1, 2u);
243 F += cellForce(i, j - 1, 3u);
244 let pOld = vec3f(posIn[k], posIn[n + k], posIn[2u * n + k]);
245 let vel = vec3f(velIn[k], velIn[n + k], velIn[2u * n + k]);
246 // The pads: a spring to ground on the transverse motion (compression-only
247 // with lift-off), the felt's shear on the in-plane motion, and a dashpot.
248 let kb = nd[k].w;
249 if (P.standOn != 0u && kb > 0.0) {
250 var nPads = 1.0; if (P.hasTop != 0u) { nPads = 2.0; }
251 let ku = nPads * kb * 0.3846153846; // shear: G / E = 1 / (2 (1 + 0.3))
252 F.x -= ku * pOld.x + P.padDamping * ku * vel.x;
253 F.y -= ku * pOld.y + P.padDamping * ku * vel.y;
254 F.z += padForce(kb, pOld.z) - P.padDamping * nPads * kb * vel.z;
255 }
256 let mass = P.rho * TH(i, j) * P.hx * P.hy * weight;
257 var acc = F * (1.0 / mass);
259 // Losses: frequency-independent on all components, the graph Laplacian of the
260 // transverse velocity (~k^2), and an extra frequency-independent in-plane loss.
261 acc -= 2.0 * SP.sigma0 * vel;
262 acc.z += 2.0 * SP.sigma1 * lapDW(i, j);
263 acc.x -= 2.0 * SP.sigmaIn * vel.x;
264 acc.y -= 2.0 * SP.sigmaIn * vel.y;
266 // Drumstick: Hertz contact between a free mass and the contact patch (the
267 // plate nodes within the tip radius, plus the nearest one). The contact
268 // coordinate is the mass-weighted mean displacement of the patch, and the
269 // force is spread over the patch in proportion to the node masses, so the
270 // pair is conjugate and the contact has a potential energy. Once the
271 // contact has been released for STICK_RELEASE the stick is withdrawn.
272 if ((SP.flags & 1u) != 0u) {
273 let sIn = 8u * SP.parity;
274 if (P.standOn == 0u) {
275 // Free plate: inertia relief. The force of the previous step is applied
276 // to the whole plate as the rigid acceleration field that balances the
277 // patch load and its moments about the center, so the plate gains no net
278 // momentum (the thread takes it up in reality). A plate on a stand takes
279 // the reaction through the pads.
280 let Fprev = stick[sIn + 2u];
281 acc.z += Fprev * (1.0 / P.mass + nodeY(j) * P.patchCY / P.Ix + nodeX(i) * P.patchCX / P.Iy);
282 }
283 if (inContactPatch(i, j)) {
284 let sx = stick[sIn]; let sv = stick[sIn + 1u]; let sMax = stick[sIn + 3u];
285 var released = stick[sIn + 4u];
286 var withdrawn = stick[sIn + 5u];
287 var Fc = 0.0;
288 if (withdrawn == 0.0) {
289 var wc = 0.0;
290 for (var pj = i32(P.patchJ0); pj <= i32(P.patchJ1); pj++) {
291 for (var pi = i32(P.patchI0); pi <= i32(P.patchI1); pi++) {
292 if (inContactPatch(pi, pj)) { wc += nodeMass(pi, pj) * posIn[2u * n + id(pi, pj)]; }
293 }
294 }
295 wc /= P.patchMass;
296 let pen = wc - sx; // the stick sits on the +z side and moves toward -z
297 if (pen > 0.0) { Fc = P.K * pow(pen, P.alpha); released = 0.0; }
298 else if (sMax > 0.0) {
299 released += P.dt;
300 if (released > STICK_RELEASE) { withdrawn = 1.0; }
301 }
302 }
303 acc.z -= Fc / P.patchMass;
304 if (k == P.strikeNode) {
305 let sOut = 8u * (1u - SP.parity);
306 let svn = sv + P.dt * Fc / P.stickMass;
307 stick[sOut] = sx + P.dt * svn;
308 stick[sOut + 1u] = svn;
309 stick[sOut + 2u] = Fc;
310 stick[sOut + 3u] = max(sMax, Fc);
311 stick[sOut + 4u] = released;
312 stick[sOut + 5u] = withdrawn;
313 }
314 }
315 }
317 let vNew = vel + P.dt * acc;
318 if (SP.audioSlot >= 0) { hist[u32(SP.audioSlot) * n + k] = vNew.z; }
319 var pNew = pOld + P.dt * vNew;
320 // Compensated update of w: the increment dt * v can be far below the
321 // float32 resolution of w, so the rounding error is carried separately.
322 let y = P.dt * vNew.z + posIn[3u * n + k];
323 let t = pOld.z + y;
324 let lo = (pOld.z - t) + y;
325 pNew.z = t;
326 posOut[k] = pNew.x; posOut[n + k] = pNew.y; posOut[2u * n + k] = pNew.z; posOut[3u * n + k] = lo;
327 velOut[k] = vNew.x; velOut[n + k] = vNew.y; velOut[2u * n + k] = vNew.z;
↑↓ move↵openescclose