struct RU { mvp: mat4x4f, nx: u32, ny: u32, hx: f32, hy: f32, wScale: f32, // vertical exaggeration in the 3D view colorRange: f32, // |w| mapped to full color [m] aspect: f32, // canvas width / height mode: u32, // 0: 2D, 1: 3D holeX: f32, holeY: f32, pushX: f32, pushY: f32, markerR: f32, lightX: f32, lightY: f32, lightZ: f32, showShape: u32, // add the stress-free initial shape to the displayed field (both views) shape: u32, // 0: rectangle, 1: ellipse inscribed in the bounding box pad1: u32, pad2: u32, }; @group(0) @binding(0) var R: RU; @group(0) @binding(1) var w: array; @group(0) @binding(2) var w0: array; @group(0) @binding(3) var nw: array; // node weights, 0 outside the sheet fn colormap(t: f32) -> vec3f { let s = clamp(t, -1.0, 1.0); let mid = vec3f(0.93, 0.93, 0.93); let blue = vec3f(0.10, 0.35, 0.85); let red = vec3f(0.85, 0.20, 0.12); if (s < 0.0) { return mix(mid, blue, -s); } return mix(mid, red, s); } // Displayed field: the deviation w, plus the initial shape when requested (the total shape). fn wAt(i: i32, j: i32) -> f32 { let ii = clamp(i, 0, i32(R.nx) - 1); let jj = clamp(j, 0, i32(R.ny) - 1); var z = w[u32(jj) * R.nx + u32(ii)]; if (R.showShape != 0u) { z += w0[u32(jj) * R.nx + u32(ii)]; } return z; } fn nwAt(i: i32, j: i32) -> f32 { if (i < 0 || i >= i32(R.nx) || j < 0 || j >= i32(R.ny)) { return 0.0; } return nw[u32(j) * R.nx + u32(i)]; } // Height used by the 3D view: the same field as the color. fn zAt(i: i32, j: i32) -> f32 { return wAt(i, j); } // Slope along one axis, one-sided where a neighbor lies outside the sheet. fn slope(i: i32, j: i32, di: i32, dj: i32, h: f32) -> f32 { let hasP = nwAt(i + di, j + dj) > 0.0; let hasM = nwAt(i - di, j - dj) > 0.0; if (hasP && hasM) { return (zAt(i + di, j + dj) - zAt(i - di, j - dj)) / (2.0 * h); } if (hasP) { return (zAt(i + di, j + dj) - zAt(i, j)) / h; } if (hasM) { return (zAt(i, j) - zAt(i - di, j - dj)) / h; } return 0.0; } // ---------- 2D view: full-screen triangle, bilinear lookup in the fragment shader ---------- struct V2 { @builtin(position) pos: vec4f, @location(0) uv: vec2f }; @vertex fn vs2d(@builtin(vertex_index) vi: u32) -> V2 { var p = array(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0)); var o: V2; o.pos = vec4f(p[vi], 0.0, 1.0); o.uv = p[vi]; return o; } @fragment fn fs2d(in: V2) -> @location(0) vec4f { let Lx = f32(R.nx - 1u) * R.hx; let Ly = f32(R.ny - 1u) * R.hy; // Fit the plate into the canvas with a margin, preserving its aspect ratio. let margin = 0.92; var sx = margin; var sy = margin; let plateAspect = Lx / Ly; if (plateAspect > R.aspect) { sy = margin * R.aspect / plateAspect; } else { sx = margin * plateAspect / R.aspect; } let px = in.uv.x / sx * 0.5 * Lx; let py = in.uv.y / sy * 0.5 * Ly; let bg = vec4f(0.11, 0.12, 0.14, 1.0); if (abs(px) > 0.5 * Lx || abs(py) > 0.5 * Ly) { return bg; } if (R.shape == 1u) { let ex = px / (0.5 * Lx); let ey = py / (0.5 * Ly); if (ex * ex + ey * ey > 1.0) { return bg; } } let fi = (px + 0.5 * Lx) / R.hx; let fj = (py + 0.5 * Ly) / R.hy; let i0 = i32(floor(fi)); let j0 = i32(floor(fj)); let a = fi - f32(i0); let b = fj - f32(j0); // Bilinear interpolation over the nodes of the sheet only (outside nodes carry no value). let c = vec4f((1.0 - a) * (1.0 - b), a * (1.0 - b), (1.0 - a) * b, a * b); let m = vec4f(nwAt(i0, j0), nwAt(i0 + 1, j0), nwAt(i0, j0 + 1), nwAt(i0 + 1, j0 + 1)); let wgt = c * step(vec4f(1e-6), m); let den = wgt.x + wgt.y + wgt.z + wgt.w; if (den <= 0.0) { return bg; } let val = dot(wgt, vec4f(wAt(i0, j0), wAt(i0 + 1, j0), wAt(i0, j0 + 1), wAt(i0 + 1, j0 + 1))) / den; var col = colormap(val / R.colorRange); // Markers: hole (ring) and pushed patch (dashed-looking ring). let dh = distance(vec2f(px, py), vec2f(R.holeX, R.holeY)); if (abs(dh - R.markerR) < 0.35 * R.markerR) { col = vec3f(0.1, 0.1, 0.1); } if (dh < 0.4 * R.markerR) { col = vec3f(0.11, 0.12, 0.14); } let dp = distance(vec2f(px, py), vec2f(R.pushX, R.pushY)); if (abs(dp - R.markerR * 1.6) < 0.25 * R.markerR) { col = vec3f(0.95, 0.75, 0.1); } return vec4f(col, 1.0); } // ---------- 3D view: height field ---------- struct V3 { @builtin(position) pos: vec4f, @location(0) val: f32, @location(1) normal: vec3f }; @vertex fn vs3d(@builtin(vertex_index) vi: u32) -> V3 { let i = i32(vi % R.nx); let j = i32(vi / R.nx); let x = (f32(i) - 0.5 * f32(R.nx - 1u)) * R.hx; let y = (f32(j) - 0.5 * f32(R.ny - 1u)) * R.hy; let val = wAt(i, j); let z = zAt(i, j) * R.wScale; let dzdx = slope(i, j, 1, 0, R.hx) * R.wScale; let dzdy = slope(i, j, 0, 1, R.hy) * R.wScale; var o: V3; o.pos = R.mvp * vec4f(x, y, z, 1.0); o.val = val; o.normal = normalize(vec3f(-dzdx, -dzdy, 1.0)); return o; } @fragment fn fs3d(in: V3) -> @location(0) vec4f { let l = normalize(vec3f(R.lightX, R.lightY, R.lightZ)); let n = normalize(in.normal); let diff = abs(dot(n, l)); let base = colormap(in.val / R.colorRange); let col = base * (0.35 + 0.65 * diff) + vec3f(0.15) * pow(diff, 24.0); return vec4f(col, 1.0); }