1struct RU {
2 mvp: mat4x4f,
3 nx: u32,
4 ny: u32,
5 hx: f32,
6 hy: f32,
7 wScale: f32, // vertical exaggeration in the 3D view
8 colorRange: f32, // |w| mapped to full color [m]
9 aspect: f32, // canvas width / height
10 mode: u32, // 0: 2D, 1: 3D
11 holeX: f32,
12 holeY: f32,
13 pushX: f32,
14 pushY: f32,
15 markerR: f32,
16 lightX: f32,
17 lightY: f32,
18 lightZ: f32,
19};
21@group(0) @binding(0) var<uniform> R: RU;
22@group(0) @binding(1) var<storage, read> w: array<f32>;
24fn colormap(t: f32) -> vec3f {
25 let s = clamp(t, -1.0, 1.0);
26 let mid = vec3f(0.93, 0.93, 0.93);
27 let blue = vec3f(0.10, 0.35, 0.85);
28 let red = vec3f(0.85, 0.20, 0.12);
29 if (s < 0.0) { return mix(mid, blue, -s); }
30 return mix(mid, red, s);
31}
33fn wAt(i: i32, j: i32) -> f32 {
34 let ii = clamp(i, 0, i32(R.nx) - 1);
35 let jj = clamp(j, 0, i32(R.ny) - 1);
36 return w[u32(jj) * R.nx + u32(ii)];
37}
39// ---------- 2D view: full-screen triangle, bilinear lookup in the fragment shader ----------
41struct V2 { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
43@vertex
44fn vs2d(@builtin(vertex_index) vi: u32) -> V2 {
45 var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
46 var o: V2;
47 o.pos = vec4f(p[vi], 0.0, 1.0);
48 o.uv = p[vi];
49 return o;
50}
52@fragment
53fn fs2d(in: V2) -> @location(0) vec4f {
54 let Lx = f32(R.nx - 1u) * R.hx;
55 let Ly = f32(R.ny - 1u) * R.hy;
56 // Fit the plate into the canvas with a margin, preserving its aspect ratio.
57 let margin = 0.92;
58 var sx = margin; var sy = margin;
59 let plateAspect = Lx / Ly;
60 if (plateAspect > R.aspect) { sy = margin * R.aspect / plateAspect; } else { sx = margin * plateAspect / R.aspect; }
61 let px = in.uv.x / sx * 0.5 * Lx;
62 let py = in.uv.y / sy * 0.5 * Ly;
63 let bg = vec4f(0.11, 0.12, 0.14, 1.0);
64 if (abs(px) > 0.5 * Lx || abs(py) > 0.5 * Ly) { return bg; }
65 let fi = (px + 0.5 * Lx) / R.hx;
66 let fj = (py + 0.5 * Ly) / R.hy;
67 let i0 = i32(floor(fi)); let j0 = i32(floor(fj));
68 let a = fi - f32(i0); let b = fj - f32(j0);
69 let val = mix(mix(wAt(i0, j0), wAt(i0 + 1, j0), a), mix(wAt(i0, j0 + 1), wAt(i0 + 1, j0 + 1), a), b);
70 var col = colormap(val / R.colorRange);
71 // Markers: hole (ring) and pushed patch (dashed-looking ring).
72 let dh = distance(vec2f(px, py), vec2f(R.holeX, R.holeY));
73 if (abs(dh - R.markerR) < 0.35 * R.markerR) { col = vec3f(0.1, 0.1, 0.1); }
74 if (dh < 0.4 * R.markerR) { col = vec3f(0.11, 0.12, 0.14); }
75 let dp = distance(vec2f(px, py), vec2f(R.pushX, R.pushY));
76 if (abs(dp - R.markerR * 1.6) < 0.25 * R.markerR) { col = vec3f(0.95, 0.75, 0.1); }
77 return vec4f(col, 1.0);
78}
80// ---------- 3D view: height field ----------
82struct V3 { @builtin(position) pos: vec4f, @location(0) val: f32, @location(1) normal: vec3f };
84@vertex
85fn vs3d(@builtin(vertex_index) vi: u32) -> V3 {
86 let i = i32(vi % R.nx); let j = i32(vi / R.nx);
87 let x = (f32(i) - 0.5 * f32(R.nx - 1u)) * R.hx;
88 let y = (f32(j) - 0.5 * f32(R.ny - 1u)) * R.hy;
89 let val = wAt(i, j);
90 let z = val * R.wScale;
91 let dzdx = (wAt(i + 1, j) - wAt(i - 1, j)) * R.wScale / (2.0 * R.hx);
92 let dzdy = (wAt(i, j + 1) - wAt(i, j - 1)) * R.wScale / (2.0 * R.hy);
93 var o: V3;
94 o.pos = R.mvp * vec4f(x, y, z, 1.0);
95 o.val = val;
96 o.normal = normalize(vec3f(-dzdx, -dzdy, 1.0));
97 return o;
98}
100@fragment
101fn fs3d(in: V3) -> @location(0) vec4f {
102 let l = normalize(vec3f(R.lightX, R.lightY, R.lightZ));
103 let n = normalize(in.normal);
104 let diff = abs(dot(n, l));
105 let base = colormap(in.val / R.colorRange);
106 let col = base * (0.35 + 0.65 * diff) + vec3f(0.15) * pow(diff, 24.0);
107 return vec4f(col, 1.0);
108}