/ proof-of-concept / fmm3dbie-webgpu-poc
Sign in
proof-of-concept / fmm3dbie-webgpu-poc
fmm3dbie-webgpu-poc / src / view / field.ts
67 lines · 3.0 KBBlameHistoryRaw
1/**
2 * The solved field as the views need it, and how a complex value becomes a
3 * color: the pressure at phase t, Re(u exp(-i omega t)) = Re u cos wt + Im u sin wt,
4 * on a symmetric diverging scale, or the level 20 log10(|u| / ref) in dB. The
5 * reference of the level is one number for the room (the median of |u|) and the
6 * free field |G| at each point for the cymbal, so that 0 dB means no effect of
7 * the cymbal.
8 */
9import type { Slice } from '../problem.ts';
11export type Mode = 'pressure' | 'level';
13export interface SliceField {
14 slice: Slice;
15 /** interleaved complex, NaN where the field is not computed */
16 u: Float64Array;
17 /** reference of the level at each point; when absent, Field.ref */
18 ref?: Float64Array;
21export interface Field {
22 k: number;
23 /** the source (the listener, for the cymbal) */
24 x0: [number, number, number];
25 ngrid: number;
26 slices: SliceField[];
27 /** surface field at the nodes, interleaved complex */
28 uWall: Float64Array;
29 /** the level's reference on the surface is the free field |G| = 1 / (4 pi |x - x0|) rather than ref */
30 freeFieldLevel: boolean;
31 /** color limit for the pressure: 99th percentile of |u| more than half a wavelength from the source */
32 umax: number;
33 /** reference for the level when there is no per-point reference: the median of |u| */
34 ref: number;
35 /** the level scale in dB, and whether it is centered (diverging colormap) */
36 level: { min: number; max: number; diverging: boolean; label: string };
37 /** the 3D view: what to look at, how far, and whether surfaces facing the camera are cut away */
38 scene: { center: [number, number, number]; radius: number; cutaway: boolean };
39 /** the cymbal's cross section (r, z) and radius, drawn on the slices */
40 outline?: { r: Float64Array; z: Float64Array; radius: number };
43/** Color limits from the slices, as plot.py computes them. */
44export function colorLimits(slices: SliceField[], x0: ArrayLike<number>, k: number): { umax: number; ref: number } {
45 const vals: number[] = [];
46 const rmin = Math.PI / k;
47 for (const { slice, u } of slices) {
48 const n = slice.mask.length;
49 for (let q = 0; q < n; q++) {
50 if (!slice.mask[q]) continue;
51 const r = Math.hypot(slice.pts[q] - x0[0], slice.pts[n + q] - x0[1], slice.pts[2 * n + q] - x0[2]);
52 if (r <= rmin) continue;
53 const v = Math.hypot(u[2 * q], u[2 * q + 1]);
54 if (Number.isFinite(v)) vals.push(v);
55 }
56 }
57 if (!vals.length) return { umax: 1, ref: 1 };
58 vals.sort((x, y) => x - y);
59 return { umax: vals[Math.floor(0.99 * (vals.length - 1))], ref: vals[Math.floor(0.5 * (vals.length - 1))] };
62/** Position in [0, 1] on the colormap of a complex value, or NaN. */
63export function colorParam(re: number, im: number, mode: Mode, c: number, s: number, umax: number, ref: number, level: Field['level']): number {
64 if (mode === 'pressure') return 0.5 + (0.5 * (re * c + im * s)) / umax;
65 const db = 20 * Math.log10(Math.hypot(re, im) / ref);
66 return (db - level.min) / (level.max - level.min);
↑↓ move↵openescclose