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 strikeX: f32, strikeY: f32, strikeR: f32, mountX: f32, mountY: f32, mountR: f32, lightX: f32, lightY: f32, lightZ: f32, showShape: u32, // add the stress-free initial shape to the displayed field (both views) colorMode: u32, // 0: color by the displayed field, 1: color by the thickness mountOn: u32, // draw the mount marker hMin: f32, // thickness range for colorMode 1 [m] hMax: f32, 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 plate @group(0) @binding(4) var hn: array; // thickness per node @group(0) @binding(5) var cm: array; // cell membership, (nx - 1) * (ny - 1) const BG = vec3f(0.11, 0.12, 0.14); // Diverging map for the displacement: blue, light gray, red. 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); } // Sequential map for the thickness: dark teal through green to pale yellow. fn thickmap(t: f32) -> vec3f { let s = clamp(t, 0.0, 1.0); let a = vec3f(0.13, 0.22, 0.40); let b = vec3f(0.20, 0.60, 0.55); let c = vec3f(0.98, 0.93, 0.55); if (s < 0.5) { return mix(a, b, 2.0 * s); } return mix(b, c, 2.0 * s - 1.0); } fn idx(i: i32, j: i32) -> u32 { let ii = clamp(i, 0, i32(R.nx) - 1); let jj = clamp(j, 0, i32(R.ny) - 1); return u32(jj) * R.nx + u32(ii); } // Displayed field: the deviation w, plus the initial shape when requested (the total shape). fn wAt(i: i32, j: i32) -> f32 { let k = idx(i, j); var z = w[k]; if (R.showShape != 0u) { z += w0[k]; } return z; } fn hAt(i: i32, j: i32) -> f32 { return hn[idx(i, j)]; } 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)]; } fn cellAt(ci: i32, cj: i32) -> f32 { if (ci < 0 || ci >= i32(R.nx) - 1 || cj < 0 || cj >= i32(R.ny) - 1) { return 0.0; } return cm[u32(cj) * (R.nx - 1u) + u32(ci)]; } // Color of a node from the displayed field value and the thickness. fn colorOf(val: f32, h: f32) -> vec3f { if (R.colorMode == 1u) { return thickmap((h - R.hMin) / max(R.hMax - R.hMin, 1e-9)); } return colormap(val / R.colorRange); } // 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 plate. 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(BG, 1.0); if (abs(px) > 0.5 * Lx || abs(py) > 0.5 * Ly) { return bg; } let fi = (px + 0.5 * Lx) / R.hx; let fj = (py + 0.5 * Ly) / R.hy; let i0 = min(i32(floor(fi)), i32(R.nx) - 2); let j0 = min(i32(floor(fj)), i32(R.ny) - 2); // Only the cells of the plate are drawn (staircase edge, hole included). if (cellAt(i0, j0) == 0.0) { return bg; } let a = fi - f32(i0); let b = fj - f32(j0); let c = vec4f((1.0 - a) * (1.0 - b), a * (1.0 - b), (1.0 - a) * b, a * b); let val = dot(c, vec4f(wAt(i0, j0), wAt(i0 + 1, j0), wAt(i0, j0 + 1), wAt(i0 + 1, j0 + 1))); let h = dot(c, vec4f(hAt(i0, j0), hAt(i0 + 1, j0), hAt(i0, j0 + 1), hAt(i0 + 1, j0 + 1))); var col = colorOf(val, h); // Markers: the mount (dark ring) and the strike point (yellow ring). if (R.mountOn != 0u) { let dm = distance(vec2f(px, py), vec2f(R.mountX, R.mountY)); if (abs(dm - R.mountR) < 0.12 * R.mountR + 0.0015) { col = vec3f(0.1, 0.1, 0.1); } } let ds = distance(vec2f(px, py), vec2f(R.strikeX, R.strikeY)); if (abs(ds - R.strikeR * 1.6) < 0.25 * R.strikeR) { 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) color: vec3f, @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.color = colorOf(val, hAt(i, j)); 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 col = in.color * (0.35 + 0.65 * diff) + vec3f(0.15) * pow(diff, 24.0); return vec4f(col, 1.0); }