// One explicit time step (symplectic Euler) of the von Karman plate. // // 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] // Each 16x16 workgroup loads its tile plus a 2-node halo of w and of the node // weights into shared memory once, so the stencils below never touch global // memory. Nodes outside the sheet (weight 0) are skipped: a second difference // exists only where the node and both neighbours belong to the sheet, and a // cell contributes only when its four corners do. That reproduces the free // edge of the rectangle and gives a staircase free edge for an ellipse. @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] velocity of w at the audio sample instants // Constraint reaction (acceleration of w) per node, followed by two 4-float // slots of drumstick state (position, velocity, force, max force). @group(0) @binding(7) var react: array; @group(0) @binding(8) var w0: array; // stress-free initial shape const TILE = 16; const HALO = 2; const TW = 20; // TILE + 2 * HALO var tw: array; // Node weights of the same tile in quarters (0..4), packed four to a word to stay // within the default 16 KB of workgroup storage. var tn: array; // Membrane force contributions of the 17x17 cells around the tile, 4 nodes x 3 components each. const CT = 17; var cellF: array; // Tile origin in global node coordinates (set once per invocation). 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); } // Global indices; the tile covers [ox, ox + TW) x [oy, oy + TW). fn W(i: i32, j: i32) -> f32 { return tw[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 U(i: i32, j: i32) -> f32 { return posIn[id(i, j)]; } fn V(i: i32, j: i32) -> f32 { return posIn[P.n + id(i, j)]; } fn DW(i: i32, j: i32) -> f32 { return velIn[2u * P.n + id(i, j)]; } fn W0(i: i32, j: i32) -> f32 { return w0[id(i, j)]; } // 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) * (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) * (b + P.nu * a); } // Mixed second difference at a cell center; zero outside the sheet. fn Cc(ci: i32, cj: i32) -> f32 { if (!insCell(ci, cj)) { return 0.0; } return (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 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 Wn = vec4f(W(ci, cj), W(ci + 1, cj), W(ci + 1, cj + 1), W(ci, cj + 1)) + W0n; // total 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 wx = dot(dNdx, Wn); let wy = dot(dNdy, Wn); let w0x = dot(dNdx, W0n); let w0y = dot(dNdy, W0n); // Marguerre strains: total slope minus the stress-free slope. let exx = ux + 0.5 * (wx * wx - w0x * w0x); let eyy = vy + 0.5 * (wy * wy - w0y * w0y); let gxy = uy + vx + wx * wy - w0x * w0y; let Nxx = P.C * (exx + P.nu * eyy); let Nyy = P.C * (eyy + P.nu * exx); let Nxy = P.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]; } } // Stored membrane force on local node `a` of cell (ci, cj). fn cellForce(ci: i32, cj: i32, a: u32) -> vec3f { let lc = u32(cj - oy - 1) * u32(CT) + u32(ci - ox - 1); let base = lc * 12u + a * 3u; return vec3f(cellF[base], cellF[base + 1u], cellF[base + 2u]); } @compute @workgroup_size(16, 16) fn main(@builtin(global_invocation_id) gid: vec3u, @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); // Cooperative load of the w and weight tiles (400 entries each, 256 threads). let lt = lid.y * 16u + lid.x; for (var e = lt; e < 400u; e += 256u) { let gi = ox + i32(e % u32(TW)); let gj = oy + i32(e / u32(TW)); var ww = 0.0; if (gi >= 0 && gi < i32(P.nx) && gj >= 0 && gj < i32(P.ny)) { ww = posIn[2u * n + id(gi, gj)]; } tw[e] = ww; } if (lt < 100u) { var word = 0u; for (var q = 0u; q < 4u; q++) { let e = lt * 4u + q; let gi = ox + i32(e % u32(TW)); let gj = oy + i32(e / u32(TW)); var wq = 0u; if (gi >= 0 && gi < i32(P.nx) && gj >= 0 && gj < i32(P.ny)) { wq = u32(round(4.0 * nw[id(gi, gj)])); } word |= wq << (q * 8u); } tn[lt] = word; } workgroupBarrier(); // Membrane forces of the 17x17 cells adjacent to the tile's nodes. for (var e = lt; e < 289u; e += 256u) { let lci = i32(e % u32(CT)); let lcj = i32(e / u32(CT)); cellForces(ox + 1 + lci, oy + 1 + lcj, e); } workgroupBarrier(); let i = i32(gid.x); let j = i32(gid.y); if (i >= i32(P.nx) || j >= i32(P.ny)) { return; } let k = id(i, j); let weight = NW(i, j); if (weight == 0.0) { // Outside the sheet: the node stays at rest. 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; if ((SP.flags & 2u) != 0u) { react[k] = 0.0; } return; } let bend = (Ma(i - 1, j) - 2.0 * Ma(i, j) + Ma(i + 1, j)) * ihx2 + (Mb(i, j - 1) - 2.0 * Mb(i, j) + Mb(i, j + 1)) * ihy2 + 2.0 * (1.0 - P.nu) * (Cc(i, j) - Cc(i - 1, j) - Cc(i, j - 1) + Cc(i - 1, j - 1)) * ihxy; var F = vec3f(0.0, 0.0, -P.D * 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 mass = P.rhoh * P.hx * P.hy * weight; var acc = F * (1.0 / mass); // Loss: frequency-independent on all components, frequency-dependent (graph Laplacian) on w. let vel = vec3f(velIn[k], velIn[n + k], velIn[2u * n + k]); var lap = 0.0; if (INS(i - 1, j)) { lap += (DW(i - 1, j) - vel.z) * ihx2; } if (INS(i + 1, j)) { lap += (DW(i + 1, j) - vel.z) * ihx2; } if (INS(i, j - 1)) { lap += (DW(i, j - 1) - vel.z) * ihy2; } if (INS(i, j + 1)) { lap += (DW(i, j + 1) - vel.z) * ihy2; } acc -= 2.0 * SP.sigma0 * vel; acc.z += 2.0 * SP.sigma1 * lap; // In-plane (stretching) waves lie at several kHz; they get their own loss. acc.x -= 2.0 * SP.sigmaIn * vel.x; acc.y -= 2.0 * SP.sigmaIn * vel.y; // Finger letting go: the reaction recorded at the end of the hold, ramped to zero. if ((SP.flags & 4u) != 0u) { acc.z += react[k] * SP.releaseGain; } // Drumstick: Hertz contact between a free mass and the contact patch. if ((SP.flags & 8u) != 0u) { let sIn = n + 4u * SP.parity; let sx = react[sIn]; let sv = react[sIn + 1u]; let sMax = react[sIn + 3u]; let wc = posIn[2u * n + P.strikeNode]; let pen = wc - sx; // the stick sits on the +z side and moves toward -z var F = 0.0; if (pen > 0.0) { F = P.K * pow(pen, P.alpha); } let dxs = nodeX(i) - P.strikeX; let dys = nodeY(j) - P.strikeY; if (dxs * dxs + dys * dys <= P.strikeR2 || k == P.strikeNode) { acc.z -= F / P.patchMass; } if (k == P.strikeNode) { let sOut = n + 4u * (1u - SP.parity); let svn = sv + P.dt * F / P.stickMass; react[sOut] = sx + P.dt * svn; react[sOut + 1u] = svn; react[sOut + 2u] = F; react[sOut + 3u] = max(sMax, F); } } var vNew = vel + P.dt * acc; if (SP.audioSlot >= 0) { hist[u32(SP.audioSlot) * n + k] = vNew.z; } let pOld = vec3f(posIn[k], posIn[n + k], posIn[2u * n + k]); 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; var lo = (pOld.z - t) + y; pNew.z = t; if ((SP.flags & 1u) != 0u) { let dxp = nodeX(i) - P.pushX; let dyp = nodeY(j) - P.pushY; let inPatch = dxp * dxp + dyp * dyp <= P.pushR2; if ((SP.flags & 2u) != 0u) { // Acceleration the constraint has to supply to hold this node. var r = 0.0; if (inPatch) { r = (SP.pushV - vNew.z) / P.dt; } react[k] = r; } if (inPatch) { pNew.z = SP.pushW; vNew.z = SP.pushV; lo = 0.0; } } 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; }