/ proof-of-concept / fmm3dbie-webgpu-poc
Sign in
proof-of-concept / fmm3dbie-webgpu-poc
fmm3dbie-webgpu-poc / src / view / room3d.ts
511 lines · 19.5 KBCodeBlameHistory
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 2 * A WebGPU view of the scene: the surface colored by the surface field, the
3 * three planes, and the source. For the room, walls whose outward normal faces
4 * the camera are cut away in the fragment shader, so the view looks into the
5 * room through the nearer walls; the cymbal is drawn whole. Drag to orbit,
6 * scroll or pinch to zoom.
8import { basis2d, type Surface } from '../surface.ts';
9import { DIVERGING, SEQUENTIAL } from './colormaps.ts';
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 10import type { Field, Mode } from './field.ts';
12/** Multisampling; 1 when the query string has ?nomsaa (SwiftShader's GPU process does not survive the resolve in headless Chrome). */
13const SAMPLES = typeof location !== 'undefined' && location.search.includes('nomsaa') ? 1 : 4;
15const SHARED = /* wgsl */ `
16struct View {
17 viewProj: mat4x4f,
18 eye: vec4f,
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 19 p1: vec4f, // cos wt, sin wt, umax, 0
20 p2: vec4f, // mode (0 pressure, 1 level), db min, db max, level colormap (0 sequential, 1 diverging)
21 p3: vec4f, // cut away surfaces facing the camera (0, 1), 0, 0, 0
23@group(0) @binding(0) var<uniform> V: View;
24@group(0) @binding(1) var cmapD: texture_2d<f32>;
25@group(0) @binding(2) var cmapS: texture_2d<f32>;
26@group(0) @binding(3) var samp: sampler;
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 28// lref: the reference of the level at this point ('ref' is reserved in WGSL)
29fn colorOf(re: f32, im: f32, lref: f32) -> vec3f {
31 let t = clamp(0.5 + 0.5 * (re * V.p1.x + im * V.p1.y) / V.p1.z, 0.0, 1.0);
32 return textureSampleLevel(cmapD, samp, vec2f(t, 0.5), 0.0).rgb;
33 }
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 34 let db = 20.0 * log2(max(length(vec2f(re, im)) / lref, 1e-30)) * 0.30102999566;
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 35 let t = clamp((db - V.p2.y) / (V.p2.z - V.p2.y), 0.0, 1.0);
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 36 if (V.p2.w > 0.5) { return textureSampleLevel(cmapD, samp, vec2f(t, 0.5), 0.0).rgb; }
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 37 return textureSampleLevel(cmapS, samp, vec2f(t, 0.5), 0.0).rgb;
39`;
41const WALL = /* wgsl */ `
42${SHARED}
43struct VOut {
44 @builtin(position) pos: vec4f,
45 @location(0) world: vec3f,
46 @location(1) nrm: vec3f,
47 @location(2) u: vec2f,
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 48 @location(3) lref: f32,
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 50@vertex fn vs(@location(0) p: vec3f, @location(1) n: vec3f, @location(2) u: vec2f, @location(3) lref: f32) -> VOut {
52 o.pos = V.viewProj * vec4f(p, 1.0);
53 o.world = p;
54 o.nrm = n;
55 o.u = u;
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 56 o.lref = lref;
59@fragment fn fs(i: VOut) -> @location(0) vec4f {
60 let toEye = normalize(V.eye.xyz - i.world);
61 let n = normalize(i.nrm);
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 62 if (V.p3.x > 0.5 && dot(n, toEye) > 0.0) { discard; }
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 63 let shade = 0.6 + 0.4 * abs(dot(n, toEye));
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 64 return vec4f(colorOf(i.u.x, i.u.y, i.lref) * shade, 1.0);
66`;
68const PLANE = /* wgsl */ `
69${SHARED}
70@group(0) @binding(4) var field: texture_2d<f32>;
71struct VOut {
72 @builtin(position) pos: vec4f,
73 @location(0) uv: vec2f,
75@vertex fn vs(@location(0) p: vec3f, @location(1) uv: vec2f) -> VOut {
76 var o: VOut;
77 o.pos = V.viewProj * vec4f(p, 1.0);
78 o.uv = uv;
79 return o;
81@fragment fn fs(i: VOut) -> @location(0) vec4f {
82 // bilinear by hand: r32float textures are not filterable everywhere
83 let dim = vec2f(textureDimensions(field));
84 let x = clamp(i.uv * (dim - 1.0), vec2f(0.0), dim - 1.0);
85 let x0 = vec2i(floor(x));
86 let x1 = min(x0 + 1, vec2i(dim) - 1);
87 let f = x - vec2f(x0);
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 88 // (Re u, Im u, level reference)
89 let a = textureLoad(field, x0, 0).xyz;
90 let b = textureLoad(field, vec2i(x1.x, x0.y), 0).xyz;
91 let c = textureLoad(field, vec2i(x0.x, x1.y), 0).xyz;
92 let d = textureLoad(field, x1, 0).xyz;
93 // NaN marks points where the field is not computed
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 94 if (any(a != a) || any(b != b) || any(c != c) || any(d != d)) { discard; }
95 let u = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 96 return vec4f(colorOf(u.x, u.y, u.z), 1.0);
98`;
100const MARKER = /* wgsl */ `
101${SHARED}
102@vertex fn vs(@location(0) p: vec3f) -> @builtin(position) vec4f {
103 return V.viewProj * vec4f(p, 1.0);
105@fragment fn fs() -> @location(0) vec4f {
106 return vec4f(0.05, 0.05, 0.05, 1.0);
108`;
110type Mat4 = Float32Array;
111const BG = { r: 0.043, g: 0.055, b: 0.071, a: 1 };
113function perspective(fovy: number, aspect: number, near: number, far: number): Mat4 {
114 const f = 1 / Math.tan(fovy / 2);
115 const m = new Float32Array(16);
116 m[0] = f / aspect;
117 m[5] = f;
118 m[10] = far / (near - far);
119 m[11] = -1;
120 m[14] = (near * far) / (near - far);
121 return m;
124function lookAt(eye: number[], target: number[], up: number[]): Mat4 {
125 const z = norm(sub(eye, target));
126 const x = norm(cross(up, z));
127 const y = cross(z, x);
128 const m = new Float32Array(16);
129 m.set([x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, eye), -dot(y, eye), -dot(z, eye), 1]);
130 return m;
133function mul(a: Mat4, b: Mat4): Mat4 {
134 const m = new Float32Array(16);
135 for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {
136 let s = 0;
137 for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k];
138 m[c * 4 + r] = s;
139 }
140 return m;
143const sub = (a: number[], b: number[]) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
144const dot = (a: number[], b: number[]) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
145const cross = (a: number[], b: number[]) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
146const norm = (a: number[]) => {
147 const l = Math.hypot(a[0], a[1], a[2]);
148 return [a[0] / l, a[1] / l, a[2] / l];
149};
151export interface Show {
152 walls: boolean;
153 planes: [boolean, boolean, boolean];
156export class RoomView {
157 private ctx: GPUCanvasContext;
158 private format: GPUTextureFormat;
159 private wallPipe: GPURenderPipeline;
160 private planePipe: GPURenderPipeline;
161 private markerPipe: GPURenderPipeline;
162 private ubuf: GPUBuffer;
163 private cmapD: GPUTexture;
164 private cmapS: GPUTexture;
165 private sampler: GPUSampler;
166 private baseGroup: GPUBindGroup;
167 private wall: { vbuf: GPUBuffer; ibuf: GPUBuffer; count: number } | null = null;
168 private planes: { vbuf: GPUBuffer; tex: GPUTexture; group: GPUBindGroup }[] = [];
169 private marker: { vbuf: GPUBuffer; count: number } | null = null;
170 private msaa: GPUTexture | null = null;
171 private depth: GPUTexture | null = null;
172 private theta = 0.75;
173 private phi = 0.95;
174 private dist = 5.2;
175 private field: Field | null = null;
176 private dirty = true;
178 constructor(
179 private device: GPUDevice,
180 private canvas: HTMLCanvasElement,
181 ) {
182 this.ctx = canvas.getContext('webgpu') as GPUCanvasContext;
183 this.format = navigator.gpu.getPreferredCanvasFormat();
184 this.ctx.configure({ device, format: this.format, alphaMode: 'opaque' });
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 185 this.ubuf = device.createBuffer({ size: 128, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 186 const mkmap = (lut: Uint8Array<ArrayBuffer>) => {
187 const t = device.createTexture({ size: [256, 1], format: 'rgba8unorm', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
188 device.queue.writeTexture({ texture: t }, lut, { bytesPerRow: 1024 }, [256, 1]);
189 return t;
190 };
191 this.cmapD = mkmap(DIVERGING);
192 this.cmapS = mkmap(SEQUENTIAL);
193 this.sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear', addressModeU: 'clamp-to-edge' });
195 const baseEntries: GPUBindGroupLayoutEntry[] = [
196 { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
197 { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
198 { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
199 { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
200 ];
201 const baseLayout = device.createBindGroupLayout({ entries: baseEntries });
202 const planeLayout = device.createBindGroupLayout({
203 entries: [...baseEntries, { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'unfilterable-float' } }],
204 });
205 this.baseGroup = device.createBindGroup({ layout: baseLayout, entries: this.baseEntries() });
206 const common = (code: string, layout: GPUBindGroupLayout, buffers: GPUVertexBufferLayout[], topology: GPUPrimitiveTopology = 'triangle-list'): GPURenderPipeline => {
207 const module = device.createShaderModule({ code });
208 return device.createRenderPipeline({
209 layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
210 vertex: { module, entryPoint: 'vs', buffers },
211 fragment: { module, entryPoint: 'fs', targets: [{ format: this.format }] },
212 primitive: { topology, cullMode: 'none' },
213 depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
214 multisample: { count: SAMPLES },
215 });
216 };
217 this.wallPipe = common(WALL, baseLayout, [
218 {
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 219 arrayStride: 36,
221 { shaderLocation: 0, offset: 0, format: 'float32x3' },
222 { shaderLocation: 1, offset: 12, format: 'float32x3' },
223 { shaderLocation: 2, offset: 24, format: 'float32x2' },
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 224 { shaderLocation: 3, offset: 32, format: 'float32' },
226 },
227 ]);
228 this.planePipe = common(PLANE, planeLayout, [
229 {
230 arrayStride: 20,
231 attributes: [
232 { shaderLocation: 0, offset: 0, format: 'float32x3' },
233 { shaderLocation: 1, offset: 12, format: 'float32x2' },
234 ],
235 },
236 ]);
237 this.planeLayout = planeLayout;
238 this.markerPipe = common(MARKER, baseLayout, [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }]);
239 this.attachControls();
240 new ResizeObserver(() => {
241 this.dirty = true;
242 }).observe(canvas);
243 }
245 private planeLayout: GPUBindGroupLayout;
247 private baseEntries(): GPUBindGroupEntry[] {
248 return [
249 { binding: 0, resource: { buffer: this.ubuf } },
250 { binding: 1, resource: this.cmapD.createView() },
251 { binding: 2, resource: this.cmapS.createView() },
252 { binding: 3, resource: this.sampler },
253 ];
254 }
256 private attachControls() {
257 const c = this.canvas;
258 const pointers = new Map<number, { x: number; y: number }>();
259 let pinch = 0;
260 c.addEventListener('pointerdown', (e) => {
261 c.setPointerCapture(e.pointerId);
262 pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
263 if (pointers.size === 2) {
264 const [a, b] = [...pointers.values()];
265 pinch = Math.hypot(a.x - b.x, a.y - b.y);
266 }
267 });
268 c.addEventListener('pointermove', (e) => {
269 const p = pointers.get(e.pointerId);
270 if (!p) return;
271 if (pointers.size === 1) {
272 this.theta -= (e.clientX - p.x) * 0.008;
273 this.phi = Math.min(Math.PI - 0.05, Math.max(0.05, this.phi - (e.clientY - p.y) * 0.008));
274 }
275 p.x = e.clientX;
276 p.y = e.clientY;
277 if (pointers.size === 2) {
278 const [a, b] = [...pointers.values()];
279 const d = Math.hypot(a.x - b.x, a.y - b.y);
280 if (pinch > 0) this.zoom(pinch / d);
281 pinch = d;
282 }
283 this.dirty = true;
284 });
285 const up = (e: PointerEvent) => {
286 pointers.delete(e.pointerId);
287 pinch = 0;
288 };
289 c.addEventListener('pointerup', up);
290 c.addEventListener('pointercancel', up);
291 c.addEventListener(
292 'wheel',
293 (e) => {
294 e.preventDefault();
295 this.zoom(Math.exp(e.deltaY * 0.001));
296 },
297 { passive: false },
298 );
299 }
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 301 /** the scene's radius: 1 for the room */
302 private scale = 1;
303 private center: [number, number, number] = [0, 0, 0];
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 306 this.dist = Math.min(12 * this.scale, Math.max(2.2 * this.scale, this.dist * f));
308 }
310 /** Drop the displayed field, for while a new one is computed. */
311 clear() {
312 this.wall?.vbuf.destroy();
313 this.wall?.ibuf.destroy();
314 this.wall = null;
315 for (const p of this.planes) {
316 p.vbuf.destroy();
317 p.tex.destroy();
318 }
319 this.planes = [];
320 this.marker?.vbuf.destroy();
321 this.marker = null;
322 this.field = null;
323 this.dirty = true;
324 }
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 326 /** The surface mesh: every patch sampled on an nv x nv grid, with the surface field interpolated by the patch's Legendre expansion. */
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 327 setField(surf: Surface, f: Field, nv = 9) {
328 const { device } = this;
329 this.field = f;
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 330 if (f.scene.radius !== this.scale) {
331 this.dist *= f.scene.radius / this.scale;
332 this.scale = f.scene.radius;
333 }
334 this.center = f.scene.center;
336 const npatches = surf.npatches;
337 const npts = surf.npts;
338 const uv = new Float64Array(2 * nv * nv);
339 for (let a = 0; a < nv; a++) for (let b = 0; b < nv; b++) {
340 uv[a * nv + b] = -1 + (2 * b) / (nv - 1);
341 uv[nv * nv + a * nv + b] = -1 + (2 * a) / (nv - 1);
342 }
343 const B = basis2d(uv, surf.norder); // (npols, nv^2)
344 const ng = nv * nv;
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 345 const verts = new Float32Array(9 * ng * npatches);
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 346 const idx = new Uint32Array(6 * (nv - 1) * (nv - 1) * npatches);
347 const cr = new Float64Array(npols);
348 const ci = new Float64Array(npols);
349 let ii = 0;
350 for (let j = 0; j < npatches; j++) {
351 const g = surf.evalGeometry(j, uv);
352 for (let n = 0; n < npols; n++) {
353 let sr = 0;
354 let si = 0;
355 for (let l = 0; l < npols; l++) {
356 const U = surf.umat[n * npols + l];
357 sr += U * f.uWall[2 * (j * npols + l)];
358 si += U * f.uWall[2 * (j * npols + l) + 1];
359 }
360 cr[n] = sr;
361 ci[n] = si;
362 }
363 for (let q = 0; q < ng; q++) {
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 364 const o = 9 * (j * ng + q);
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 365 const ru = [g[3 * ng + q], g[4 * ng + q], g[5 * ng + q]];
366 const rv = [g[6 * ng + q], g[7 * ng + q], g[8 * ng + q]];
367 const nn = norm(cross(ru, rv));
368 let ur = 0;
369 let ui = 0;
370 for (let n = 0; n < npols; n++) {
371 ur += cr[n] * B[n * ng + q];
372 ui += ci[n] * B[n * ng + q];
373 }
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 374 const ref = f.freeFieldLevel ? 1 / (4 * Math.PI * Math.hypot(g[q] - f.x0[0], g[ng + q] - f.x0[1], g[2 * ng + q] - f.x0[2])) : f.ref;
375 verts.set([g[q], g[ng + q], g[2 * ng + q], nn[0], nn[1], nn[2], ur, ui, ref], o);
377 for (let a = 0; a < nv - 1; a++) for (let b = 0; b < nv - 1; b++) {
378 const v0 = j * ng + a * nv + b;
379 idx.set([v0, v0 + 1, v0 + nv + 1, v0, v0 + nv + 1, v0 + nv], ii);
380 ii += 6;
381 }
382 }
383 void npts;
384 this.wall?.vbuf.destroy();
385 this.wall?.ibuf.destroy();
386 const vbuf = device.createBuffer({ size: verts.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
387 device.queue.writeBuffer(vbuf, 0, verts);
388 const ibuf = device.createBuffer({ size: idx.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
389 device.queue.writeBuffer(ibuf, 0, idx);
390 this.wall = { vbuf, ibuf, count: idx.length };
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 392 // the three planes, as textures of (Re u, Im u, level reference) with NaN where there is no field
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 393 for (const p of this.planes) {
394 p.vbuf.destroy();
395 p.tex.destroy();
396 }
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 397 this.planes = f.slices.map(({ slice, u, ref }) => {
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 399 const tex = device.createTexture({ size: [n, n], format: 'rgba32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
400 const data = new Float32Array(4 * n * n);
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 401 for (let q = 0; q < n * n; q++) {
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 402 data[4 * q] = slice.mask[q] ? u[2 * q] : NaN;
403 data[4 * q + 1] = slice.mask[q] ? u[2 * q + 1] : NaN;
404 data[4 * q + 2] = ref ? ref[q] : f.ref;
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 406 device.queue.writeTexture({ texture: tex }, data, { bytesPerRow: 16 * n }, [n, n]);
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 407 const [i, jx] = slice.axes;
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 408 const [ea, eb] = slice.extent;
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 409 const corner = (s: number, t: number) => {
410 const x = [0, 0, 0];
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 411 x[i] = ea[0] + (ea[1] - ea[0]) * s;
412 x[jx] = eb[0] + (eb[1] - eb[0]) * t;
413 x[slice.fixed] = slice.value;
415 };
416 const quad = new Float32Array([...corner(0, 0), ...corner(1, 0), ...corner(1, 1), ...corner(0, 0), ...corner(1, 1), ...corner(0, 1)]);
417 const vb = device.createBuffer({ size: quad.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
418 device.queue.writeBuffer(vb, 0, quad);
419 const group = device.createBindGroup({ layout: this.planeLayout, entries: [...this.baseEntries(), { binding: 4, resource: tex.createView() }] });
420 return { vbuf: vb, tex, group };
421 });
423 // the source: a small octahedron
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 424 const r = 0.035 * this.scale;
426 const P = [
427 [x + r, y, z], [x - r, y, z], [x, y + r, z], [x, y - r, z], [x, y, z + r], [x, y, z - r],
428 ];
429 const faces = [[0, 2, 4], [2, 1, 4], [1, 3, 4], [3, 0, 4], [2, 0, 5], [1, 2, 5], [3, 1, 5], [0, 3, 5]];
430 const mv = new Float32Array(faces.flat().flatMap((k) => P[k]));
431 this.marker?.vbuf.destroy();
432 const mb = device.createBuffer({ size: mv.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
433 device.queue.writeBuffer(mb, 0, mv);
434 this.marker = { vbuf: mb, count: faces.length * 3 };
435 this.dirty = true;
436 }
438 private targets() {
439 const dpr = Math.min(2, window.devicePixelRatio || 1);
440 const w = Math.max(1, Math.round(this.canvas.clientWidth * dpr));
441 const h = Math.max(1, Math.round(this.canvas.clientHeight * dpr));
442 if (this.canvas.width !== w || this.canvas.height !== h || !this.depth) {
443 this.canvas.width = w;
444 this.canvas.height = h;
445 this.msaa?.destroy();
446 this.depth?.destroy();
447 this.msaa = this.device.createTexture({ size: [w, h], format: this.format, sampleCount: SAMPLES, usage: GPUTextureUsage.RENDER_ATTACHMENT });
448 this.depth = this.device.createTexture({ size: [w, h], format: 'depth24plus', sampleCount: SAMPLES, usage: GPUTextureUsage.RENDER_ATTACHMENT });
449 }
450 return { w, h };
451 }
453 draw(mode: Mode, phase: number, show: Show, animating: boolean) {
454 if (!animating && !this.dirty) return;
455 this.dirty = false;
456 const { device } = this;
457 const { w, h } = this.targets();
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 458 const c = this.center;
459 const eye = [
460 c[0] + this.dist * Math.sin(this.phi) * Math.cos(this.theta),
461 c[1] + this.dist * Math.sin(this.phi) * Math.sin(this.theta),
462 c[2] + this.dist * Math.cos(this.phi),
463 ];
464 const vp = mul(perspective((35 * Math.PI) / 180, w / h, 0.01 * this.scale, 50 * this.scale), lookAt(eye, c, [0, 0, 1]));
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 466 const u = new Float32Array(32);
468 u.set([eye[0], eye[1], eye[2], 1], 16);
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 469 u.set([Math.cos(phase), Math.sin(phase), f?.umax ?? 1, 0], 20);
470 u.set([mode === 'pressure' ? 0 : 1, f?.level.min ?? -30, f?.level.max ?? 15, f?.level.diverging ? 1 : 0], 24);
471 u.set([f?.scene.cutaway === false ? 0 : 1, 0, 0, 0], 28);
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 472 device.queue.writeBuffer(this.ubuf, 0, u);
473 const enc = device.createCommandEncoder();
474 const pass = enc.beginRenderPass({
475 colorAttachments: [
476 SAMPLES > 1
477 ? { view: this.msaa!.createView(), resolveTarget: this.ctx.getCurrentTexture().createView(), clearValue: BG, loadOp: 'clear', storeOp: 'discard' }
478 : { view: this.ctx.getCurrentTexture().createView(), clearValue: BG, loadOp: 'clear', storeOp: 'store' },
479 ],
480 depthStencilAttachment: { view: this.depth!.createView(), depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'discard' },
481 });
482 if (this.wall && show.walls) {
483 pass.setPipeline(this.wallPipe);
484 pass.setBindGroup(0, this.baseGroup);
485 pass.setVertexBuffer(0, this.wall.vbuf);
486 pass.setIndexBuffer(this.wall.ibuf, 'uint32');
487 pass.drawIndexed(this.wall.count);
488 }
489 if (this.planes.length) {
490 pass.setPipeline(this.planePipe);
491 this.planes.forEach((p, s) => {
492 if (!show.planes[s]) return;
493 pass.setBindGroup(0, p.group);
494 pass.setVertexBuffer(0, p.vbuf);
495 pass.draw(6);
496 });
497 }
498 if (this.marker) {
499 pass.setPipeline(this.markerPipe);
500 pass.setBindGroup(0, this.baseGroup);
501 pass.setVertexBuffer(0, this.marker.vbuf);
502 pass.draw(this.marker.count);
503 }
504 pass.end();
505 device.queue.submit([enc.finish()]);
506 }
508 invalidate() {
509 this.dirty = true;
510 }
↑↓ move↵openescclose