// One explicit time step (symplectic Euler) of the von Karman plate with a // thickness field, a stress-free profile, the drumstick, and the pads of the // stand. // // State layout (structure of arrays, n = nx * ny nodes): // pos: [u | v | w | wLo] wLo carries the rounding error of the w update // vel: [du | dv | dw] // A workgroup of 16x16 threads advances a 12x12 tile of nodes in three // phases separated by barriers. (1) Every thread loads one entry of the tile // plus its 2-node halo (w, u, v, w0, thickness, node weights) into shared // memory. (2) The threads compute, once each and again into shared memory, // the membrane forces of the 13x13 cells around the tile and the weighted // bending moments and mixed differences that the bending stencil gathers. // (3) Each of the 144 tile nodes evaluates its update from shared memory. // On grids of a few thousand nodes the kernel is bound by the latency of one // workgroup rather than by throughput, so the shared phases, which replace a // tenfold per-node recomputation of the stencil ingredients, are what make a // step cheap. // // Nodes outside the plate (weight 0) are skipped: a second difference exists // only where the node and both neighbors belong to the plate, and a cell // contributes only when its four corners do. That gives the free edge of the // rectangle and a staircase free edge for any other outline, hole included. @group(0) @binding(0) var P: Params; @group(0) @binding(1) var SP: StepParams; @group(0) @binding(2) var posIn: array; @group(0) @binding(3) var velIn: array; @group(0) @binding(4) var posOut: array; @group(0) @binding(5) var velOut: array; @group(0) @binding(6) var hist: array; // [slot * n + node]: transverse velocity at the audio sample instants // Two 8-float slots of drumstick state: position, velocity, force, max force, // time since the contact released, withdrawn flag, and two spare. const STICK_RELEASE: f32 = 5e-3; // [s] a release longer than this ends the hit @group(0) @binding(7) var stick: array; const TILE = 12; // nodes per workgroup along each axis (must match simulation.ts) const HALO = 2; const TW = 16; // TILE + 2 * HALO: the loaded tile, one thread per entry const CT = 13; // TILE + 1: cells adjacent to the tile's nodes const MT = 14; // TILE + 2: nodes at which the bending moments are needed var tw: array; // w var tu: array; // u var tv: array; // v var tw0: array; // stress-free shape var th: array; // thickness var tn: array; // node weights in quarters (0..4), four to a word var cellF: array; // membrane forces of the CT x CT cells, 4 nodes x 3 components var ma: array; // weighted bending moments at the MT x MT inner nodes var mb: array; var cc: array; // weighted mixed differences at the CT x CT cells var ox: i32; var oy: i32; var ihx2: f32; var ihy2: f32; var ihxy: f32; fn id(i: i32, j: i32) -> u32 { return u32(j) * P.nx + u32(i); } fn lidx(i: i32, j: i32) -> u32 { return u32(j - oy) * u32(TW) + u32(i - ox); } fn midx(i: i32, j: i32) -> u32 { return u32(j - oy - 1) * u32(MT) + u32(i - ox - 1); } fn cidx(ci: i32, cj: i32) -> u32 { return u32(cj - oy - 1) * u32(CT) + u32(ci - ox - 1); } fn W(i: i32, j: i32) -> f32 { return tw[lidx(i, j)]; } fn U(i: i32, j: i32) -> f32 { return tu[lidx(i, j)]; } fn V(i: i32, j: i32) -> f32 { return tv[lidx(i, j)]; } fn W0(i: i32, j: i32) -> f32 { return tw0[lidx(i, j)]; } fn DW(i: i32, j: i32) -> f32 { return velIn[2u * P.n + id(i, j)]; } fn TH(i: i32, j: i32) -> f32 { return th[lidx(i, j)]; } fn NWq(i: i32, j: i32) -> u32 { let l = lidx(i, j); return (tn[l >> 2u] >> ((l & 3u) * 8u)) & 0xFFu; } fn NW(i: i32, j: i32) -> f32 { return 0.25 * f32(NWq(i, j)); } fn INS(i: i32, j: i32) -> bool { return NWq(i, j) != 0u; } fn insCell(ci: i32, cj: i32) -> bool { return INS(ci, cj) && INS(ci + 1, cj) && INS(ci + 1, cj + 1) && INS(ci, cj + 1); } fn Dn(i: i32, j: i32) -> f32 { let h = TH(i, j); return P.Db * h * h * h; } fn 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)); } // Graph Laplacian of the transverse velocity at node (i, j), over neighbors in the plate. fn lapDW(i: i32, j: i32) -> f32 { let c = DW(i, j); var lap = 0.0; if (INS(i - 1, j)) { lap += (DW(i - 1, j) - c) * ihx2; } if (INS(i + 1, j)) { lap += (DW(i + 1, j) - c) * ihx2; } if (INS(i, j - 1)) { lap += (DW(i, j - 1) - c) * ihy2; } if (INS(i, j + 1)) { lap += (DW(i, j + 1) - c) * ihy2; } return lap; } // Weighted bending moments at nodes; zero where the second difference is undefined. fn Ma(i: i32, j: i32) -> f32 { if (!(INS(i - 1, j) && INS(i, j) && INS(i + 1, j))) { return 0.0; } let a = (W(i + 1, j) - 2.0 * W(i, j) + W(i - 1, j)) * ihx2; var b = 0.0; if (INS(i, j - 1) && INS(i, j + 1)) { b = (W(i, j + 1) - 2.0 * W(i, j) + W(i, j - 1)) * ihy2; } return NW(i, j) * Dn(i, j) * (a + P.nu * b); } fn Mb(i: i32, j: i32) -> f32 { if (!(INS(i, j - 1) && INS(i, j) && INS(i, j + 1))) { return 0.0; } let b = (W(i, j + 1) - 2.0 * W(i, j) + W(i, j - 1)) * ihy2; var a = 0.0; if (INS(i - 1, j) && INS(i + 1, j)) { a = (W(i + 1, j) - 2.0 * W(i, j) + W(i - 1, j)) * ihx2; } return NW(i, j) * Dn(i, j) * (b + P.nu * a); } fn Cc(ci: i32, cj: i32) -> f32 { if (!insCell(ci, cj)) { return 0.0; } let h = cellH(ci, cj); return P.Db * h * h * h * (W(ci + 1, cj + 1) - W(ci + 1, cj) - W(ci, cj + 1) + W(ci, cj)) * ihxy; } // Membrane forces of the bilinear cell (ci, cj) on its four nodes (2x2 Gauss), written to cellF at local cell index lc. fn cellForces(ci: i32, cj: i32, lc: u32) { let base = lc * 12u; if (!insCell(ci, cj)) { for (var q = 0u; q < 12u; q++) { cellF[base + q] = 0.0; } return; } let hc = cellH(ci, cj); let C = P.Cm * hc; let G = P.Gm * hc; let Un = vec4f(U(ci, cj), U(ci + 1, cj), U(ci + 1, cj + 1), U(ci, cj + 1)); let Vn = vec4f(V(ci, cj), V(ci + 1, cj), V(ci + 1, cj + 1), V(ci, cj + 1)); let W0n = vec4f(W0(ci, cj), W0(ci + 1, cj), W0(ci + 1, cj + 1), W0(ci, cj + 1)); let Wdn = vec4f(W(ci, cj), W(ci + 1, cj), W(ci + 1, cj + 1), W(ci, cj + 1)); // deviation from the stress-free shape let q = 0.25 * P.hx * P.hy; let ihx = 1.0 / P.hx; let ihy = 1.0 / P.hy; let g3 = 0.57735026918962576; var Fu = vec4f(0.0); var Fv = vec4f(0.0); var Fw = vec4f(0.0); for (var gp = 0; gp < 4; gp++) { var xi = -g3; if ((gp & 1) != 0) { xi = g3; } var eta = -g3; if ((gp & 2) != 0) { eta = g3; } let dNdx = vec4f(-(1.0 - eta), 1.0 - eta, 1.0 + eta, -(1.0 + eta)) * (0.5 * ihx); let dNdy = vec4f(-(1.0 - xi), -(1.0 + xi), 1.0 + xi, 1.0 - xi) * (0.5 * ihy); let ux = dot(dNdx, Un); let uy = dot(dNdy, Un); let vx = dot(dNdx, Vn); let vy = dot(dNdy, Vn); let dwx = dot(dNdx, Wdn); let dwy = dot(dNdy, Wdn); let w0x = dot(dNdx, W0n); let w0y = dot(dNdy, W0n); let wx = w0x + dwx; let wy = w0y + dwy; // total slope // Marguerre strains: the total slope minus the stress-free slope, written // so that every term carries the deviation. The direct form // 0.5 (wx^2 - w0x^2) subtracts two nearly equal numbers, and in single // precision its rounding error is proportional to w0x^2 and so does not // fall with the vibration: on a curved design it acts as a constant // broadband forcing that leaves a noise floor which never decays. let exx = ux + dwx * (w0x + 0.5 * dwx); let eyy = vy + dwy * (w0y + 0.5 * dwy); let gxy = uy + vx + w0x * dwy + w0y * dwx + dwx * dwy; let Nxx = C * (exx + P.nu * eyy); let Nyy = C * (eyy + P.nu * exx); let Nxy = G * gxy; Fu -= q * (Nxx * dNdx + Nxy * dNdy); Fv -= q * (Nyy * dNdy + Nxy * dNdx); Fw -= q * ((Nxx * wx + Nxy * wy) * dNdx + (Nxy * wx + Nyy * wy) * dNdy); } for (var a = 0u; a < 4u; a++) { cellF[base + a * 3u] = Fu[a]; cellF[base + a * 3u + 1u] = Fv[a]; cellF[base + a * 3u + 2u] = Fw[a]; } } // True for the plate nodes of the drumstick contact patch (same rule as patchAround in params.ts). fn inContactPatch(i: i32, j: i32) -> bool { if (nodeWeight(i, j) == 0.0) { return false; } let dxs = nodeX(i) - P.strikeX; let dys = nodeY(j) - P.strikeY; return dxs * dxs + dys * dys <= P.strikeR2 || id(i, j) == P.strikeNode; } fn cellForce(ci: i32, cj: i32, a: u32) -> vec3f { let base = cidx(ci, cj) * 12u + a * 3u; return vec3f(cellF[base], cellF[base + 1u], cellF[base + 2u]); } // Force of the pads on a node with bottom-pad rate kb and transverse // displacement w (same rule as padForce in reference.ts). The constant part // that balances the weight of the plate is subtracted, so w = 0 is rest. fn padForce(kb: f32, w: f32) -> f32 { if (P.liftoff == 0u) { if (P.hasTop != 0u) { return -2.0 * kb * w; } return -kb * w; } var f = kb * max(P.preloadBottom - w, 0.0) - kb * P.preloadBottom; if (P.hasTop != 0u) { f -= kb * max(P.preloadTop + w, 0.0) - kb * P.preloadTop; } return f; } @compute @workgroup_size(16, 16) fn main(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) { let n = P.n; ox = i32(wid.x) * TILE - HALO; oy = i32(wid.y) * TILE - HALO; ihx2 = 1.0 / (P.hx * P.hx); ihy2 = 1.0 / (P.hy * P.hy); ihxy = 1.0 / (P.hx * P.hy); let lt = lid.y * 16u + lid.x; // Phase 1: one entry of the tile with halo per thread. { let gi = ox + i32(lid.x); let gj = oy + i32(lid.y); var ww = 0.0; var uu = 0.0; var vv = 0.0; var z0 = 0.0; var hh = 0.0; if (gi >= 0 && gi < i32(P.nx) && gj >= 0 && gj < i32(P.ny)) { let k = id(gi, gj); let st = nd[k]; ww = posIn[2u * n + k]; uu = posIn[k]; vv = posIn[n + k]; z0 = st.x; hh = st.z; } tw[lt] = ww; tu[lt] = uu; tv[lt] = vv; tw0[lt] = z0; th[lt] = hh; if (lt < 64u) { var word = 0u; for (var q = 0u; q < 4u; q++) { let e = lt * 4u + q; let ei = ox + i32(e % u32(TW)); let ej = oy + i32(e / u32(TW)); var wq = 0u; if (ei >= 0 && ei < i32(P.nx) && ej >= 0 && ej < i32(P.ny)) { wq = u32(round(4.0 * nd[id(ei, ej)].y)); } word |= wq << (q * 8u); } tn[lt] = word; } } workgroupBarrier(); // Phase 2: membrane forces of the cells, bending moments and mixed differences. if (lt < 169u) { let ci = ox + 1 + i32(lt % u32(CT)); let cj = oy + 1 + i32(lt / u32(CT)); cellForces(ci, cj, lt); cc[lt] = Cc(ci, cj); } if (lt < 196u) { let mi = ox + 1 + i32(lt % u32(MT)); let mj = oy + 1 + i32(lt / u32(MT)); ma[lt] = Ma(mi, mj); mb[lt] = Mb(mi, mj); } workgroupBarrier(); // Phase 3: the update of the TILE x TILE nodes (no barriers past this point). if (lt >= 144u) { return; } let i = ox + HALO + i32(lt % u32(TILE)); let j = oy + HALO + i32(lt / u32(TILE)); if (i >= i32(P.nx) || j >= i32(P.ny)) { return; } let k = id(i, j); let weight = NW(i, j); if (weight == 0.0) { posOut[k] = 0.0; posOut[n + k] = 0.0; posOut[2u * n + k] = 0.0; posOut[3u * n + k] = 0.0; velOut[k] = 0.0; velOut[n + k] = 0.0; velOut[2u * n + k] = 0.0; return; } // Elastic forces: bending from the discrete Kirchhoff energy, membrane from the cells. let bend = (ma[midx(i - 1, j)] - 2.0 * ma[midx(i, j)] + ma[midx(i + 1, j)]) * ihx2 + (mb[midx(i, j - 1)] - 2.0 * mb[midx(i, j)] + mb[midx(i, j + 1)]) * ihy2 + 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; var F = vec3f(0.0, 0.0, -P.hx * P.hy * bend); F += cellForce(i, j, 0u); F += cellForce(i - 1, j, 1u); F += cellForce(i - 1, j - 1, 2u); F += cellForce(i, j - 1, 3u); let pOld = vec3f(posIn[k], posIn[n + k], posIn[2u * n + k]); let vel = vec3f(velIn[k], velIn[n + k], velIn[2u * n + k]); // The pads: a spring to ground on the transverse motion (compression-only // with lift-off), the felt's shear on the in-plane motion, and a dashpot. let kb = nd[k].w; if (P.standOn != 0u && kb > 0.0) { var nPads = 1.0; if (P.hasTop != 0u) { nPads = 2.0; } let ku = nPads * kb * 0.3846153846; // shear: G / E = 1 / (2 (1 + 0.3)) F.x -= ku * pOld.x + P.padDamping * ku * vel.x; F.y -= ku * pOld.y + P.padDamping * ku * vel.y; F.z += padForce(kb, pOld.z) - P.padDamping * nPads * kb * vel.z; } let mass = P.rho * TH(i, j) * P.hx * P.hy * weight; var acc = F * (1.0 / mass); // Losses: frequency-independent on all components, the graph Laplacian of the // transverse velocity (~k^2), and an extra frequency-independent in-plane loss. acc -= 2.0 * SP.sigma0 * vel; acc.z += 2.0 * SP.sigma1 * lapDW(i, j); acc.x -= 2.0 * SP.sigmaIn * vel.x; acc.y -= 2.0 * SP.sigmaIn * vel.y; // Drumstick: Hertz contact between a free mass and the contact patch (the // plate nodes within the tip radius, plus the nearest one). The contact // coordinate is the mass-weighted mean displacement of the patch, and the // force is spread over the patch in proportion to the node masses, so the // pair is conjugate and the contact has a potential energy. Once the // contact has been released for STICK_RELEASE the stick is withdrawn. if ((SP.flags & 1u) != 0u) { let sIn = 8u * SP.parity; if (P.standOn == 0u) { // Free plate: inertia relief. The force of the previous step is applied // to the whole plate as the rigid acceleration field that balances the // patch load and its moments about the center, so the plate gains no net // momentum (the thread takes it up in reality). A plate on a stand takes // the reaction through the pads. let Fprev = stick[sIn + 2u]; acc.z += Fprev * (1.0 / P.mass + nodeY(j) * P.patchCY / P.Ix + nodeX(i) * P.patchCX / P.Iy); } if (inContactPatch(i, j)) { let sx = stick[sIn]; let sv = stick[sIn + 1u]; let sMax = stick[sIn + 3u]; var released = stick[sIn + 4u]; var withdrawn = stick[sIn + 5u]; var Fc = 0.0; if (withdrawn == 0.0) { var wc = 0.0; for (var pj = i32(P.patchJ0); pj <= i32(P.patchJ1); pj++) { for (var pi = i32(P.patchI0); pi <= i32(P.patchI1); pi++) { if (inContactPatch(pi, pj)) { wc += nodeMass(pi, pj) * posIn[2u * n + id(pi, pj)]; } } } wc /= P.patchMass; let pen = wc - sx; // the stick sits on the +z side and moves toward -z if (pen > 0.0) { Fc = P.K * pow(pen, P.alpha); released = 0.0; } else if (sMax > 0.0) { released += P.dt; if (released > STICK_RELEASE) { withdrawn = 1.0; } } } acc.z -= Fc / P.patchMass; if (k == P.strikeNode) { let sOut = 8u * (1u - SP.parity); let svn = sv + P.dt * Fc / P.stickMass; stick[sOut] = sx + P.dt * svn; stick[sOut + 1u] = svn; stick[sOut + 2u] = Fc; stick[sOut + 3u] = max(sMax, Fc); stick[sOut + 4u] = released; stick[sOut + 5u] = withdrawn; } } } let vNew = vel + P.dt * acc; if (SP.audioSlot >= 0) { hist[u32(SP.audioSlot) * n + k] = vNew.z; } var pNew = pOld + P.dt * vNew; // Compensated update of w: the increment dt * v can be far below the // float32 resolution of w, so the rounding error is carried separately. let y = P.dt * vNew.z + posIn[3u * n + k]; let t = pOld.z + y; let lo = (pOld.z - t) + y; pNew.z = t; posOut[k] = pNew.x; posOut[n + k] = pNew.y; posOut[2u * n + k] = pNew.z; posOut[3u * n + k] = lo; velOut[k] = vNew.x; velOut[n + k] = vNew.y; velOut[2u * n + k] = vNew.z; }