import { d } from './index'; import { InferInput } from 'typegpu/data'; import { TgpuRoot, TgpuTexture } from 'typegpu'; import { KitComputePipeline } from '../compute'; import { isMobileGpuViewport } from '../../utilities/device'; import { Expr, GpuFragmentParams, KitTexture, GpuComputeStep } from '../contract'; import { MAX_METABALLS } from '../../utilities/sdf3d'; export { isMobileGpuViewport }; export { MAX_METABALLS }; export declare const SHAPE3D_TYPES: readonly ["sphere3D", "cube3D", "torus3D", "octahedron3D", "cylinder3D", "capsule3D", "cone3D", "pyramid3D", "prism3D", "ellipsoid3D", "diamond3D", "link3D", "gem3D", "helix3D", "metaballs3D", "dodecahedron3D", "hemisphere3D", "ribbon3D", "blob3D", "gyroscope3D"]; export type Shape3DType = typeof SHAPE3D_TYPES[number]; export declare function is3dShapeType(shapeType: string): shapeType is Shape3DType; export declare const SVG3D_TYPES: readonly ["svgExtrude3D"]; export type Svg3DType = typeof SVG3D_TYPES[number]; export declare function isSvg3dShapeType(shapeType: string): shapeType is Svg3DType; /** True when the shape type produces a volumetric (raymarched) field — analytic 3D or SVG 3D. */ export declare function isVolumetricShapeType(shapeType: string): boolean; /** Defaults for the SVG 3D sub-props stored in the shape JSON (shared by sampler, bounds, and UI). */ export declare const SVG3D_DEFAULTS: Record; /** * Per-frame context for resolving shape sub-props: time (auto-animate phase), pointer * (mouse drivers) and a persistent per-key spring store (mouse smoothing/momentum). */ export interface ShapeSubPropFrame { /** Wall-clock seconds since sampler creation (drives auto-animate phase). */ elapsed: number; /** Seconds since the previous frame (drives mouse spring integration). */ deltaTime: number; /** Normalised pointer position in [0,1]. */ pointerX: number; pointerY: number; /** Persistent spring state per sub-prop key (mouse smoothing/momentum). */ springs: Map; } /** * Resolve a shape sub-prop value that may be a number, an auto-animate config, or a * mouse config. Auto base rate matches the renderer's prop drivers (0.2 cycles/sec → * 5s full cycle at speed 1); mouse mapping (axis → spring → curve → output range) * mirrors the renderer's resolveMouseMapForProp so behaviour matches top-level props. */ export declare function resolveShapeSubProp(value: any, fallback: number, frame: ShapeSubPropFrame, key: string): number; /** Static bound for a sub-prop value: the largest magnitude it can take (for bounding boxes). */ export declare function shapeSubPropMax(value: any, fallback: number): number; export declare const SHAPE3D_DEFAULTS: Record>; /** * Conservative bounding radius (field UV units) for a 3D shape config — radius of * the bounding sphere, which contains the silhouette under any rotation. * Sub-prop values may be auto-animate configs; the max output bound is used. */ export declare function shape3dBoundingRadius(cfg: Record): number; /** Fixed winding count for the helix (not user-exposed — `pitch` alone drives the * height). Kept as a constant so the SDF, bounds, and march agree. */ export declare const HELIX_TURNS = 3; /** CPU-value rotation holder (a type alias — the consumer re-assembles it on TypeGPU uniforms; * see CONSUMER WIRES). */ export interface RotUniforms { cx: any; sx: any; cy: any; sy: any; cz: any; sz: any; } /** * How the march fills the surface-locked pattern coords (.g/.b): * - 'none' → not computed (left 0). The cheapest; use when the consumer only * reads .r (field/chord) and .a (depth). Skips ALL pattern work. * - 'raw' → oblique projection of the raw shape-local hit position. Seamless, * rotates with the solid, zero extra SDF evals. * - 'triplanar' → per-face axis pick from the local surface normal. Costs SIX extra * sdf() evals per hit (a full normal probe). Only Crystal needs this. * * Defaulting to 'none' means a shader pays for pattern coords only by opting in — * the 6-eval triplanar probe no longer runs for the effects that ignore .g/.b. */ export type SurfacePatternMode = 'none' | 'raw' | 'triplanar'; /** * How the march measures the in-solid chord (`.r = −chord/2` inside the silhouette): * - 'span' → first-entry → LAST-exit, treating internal air gaps as glass. A rear lobe * passing behind a front lobe steps the field discontinuously at the rear * lobe's silhouette (mid-surface of the front lobe), which gradient-driven * effects render as smeared halos where shapes overlap on screen. * - 'firstLobe' → first-entry → FIRST-exit: the optical thickness of only the front lobe. * The field stays continuous across a rear lobe's silhouette, so overlaps * stay clean; discontinuities remain only at the front lobe's own silhouette, * where an edge is visually expected. Also cheaper for thin/multi-lobe shapes * (the interior march is shorter than the backward trace from the bounding * sphere). Glass uses this. */ export type ChordMode = 'span' | 'firstLobe'; /** CPU mirror of the GPU `rotateVec3` (same ZXY order) for plain JS scalars — used to project * an oriented bounding box onto the field axes for the aspect-fit domain. Returns * the rotated vector as {x, y, z}. */ export declare function rotateVecCpu(vx: number, vy: number, vz: number, s: { cx: number; sx: number; cy: number; sy: number; cz: number; sz: number; }): { x: number; y: number; z: number; }; /** Sphere: `|p| - r`. */ export declare const sdSphere: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32) => d.F32>; /** Rounded box: half-extents (hx,hy,hz), corner rounding. */ export declare const sdRoundBox: import('typegpu').TgpuFn<(p: d.Vec3f, hx: d.F32, hy: d.F32, hz: d.F32, rounding: d.F32) => d.F32>; /** Torus: ring radius `ringR` (in xz), tube radius `tubeR` (about y). */ export declare const sdTorus: import('typegpu').TgpuFn<(p: d.Vec3f, ringR: d.F32, tubeR: d.F32) => d.F32>; /** Octahedron (iq, bound version): scaled L1 distance — Lipschitz-safe with the march's * min-step guard. */ export declare const sdOctahedron: import('typegpu').TgpuFn<(p: d.Vec3f, s: d.F32) => d.F32>; /** Rounded cylinder: radius `r`, half-height `h`, corner rounding. */ export declare const sdRoundCylinder: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32, h: d.F32, rounding: d.F32) => d.F32>; /** Capsule: radius `r`, half-height `h` (about y). */ export declare const sdCapsule: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32, h: d.F32) => d.F32>; export declare const sdCappedCone: import('typegpu').TgpuFn<(p: d.Vec3f, r1: d.F32, r2: d.F32, h: d.F32) => d.F32>; export declare const sdPyramid: import('typegpu').TgpuFn<(p: d.Vec3f, b: d.F32, ht: d.F32) => d.F32>; export declare const sdHexPrism: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32, he: d.F32) => d.F32>; export declare const sdEllipsoid: import('typegpu').TgpuFn<(p: d.Vec3f, rx: d.F32, ry: d.F32, rz: d.F32) => d.F32>; export declare const sdBicone: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32, h: d.F32) => d.F32>; export declare const sdLink: import('typegpu').TgpuFn<(p: d.Vec3f, le: d.F32, r1: d.F32, r2: d.F32) => d.F32>; export declare const smin: import('typegpu').TgpuFn<(a: d.F32, b: d.F32, k: d.F32) => d.F32>; export declare const sdGem: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32, h: d.F32, sides: d.F32) => d.F32>; export declare const sdHelix: import('typegpu').TgpuFn<(p: d.Vec3f, R: d.F32, tube: d.F32, pitch: d.F32, turns: d.F32) => d.F32>; export declare const sdMetaballs: import('typegpu').TgpuFn<(p: d.Vec3f, b0: d.Vec3f, b1: d.Vec3f, b2: d.Vec3f, b3: d.Vec3f, b4: d.Vec3f, b5: d.Vec3f, b6: d.Vec3f, b7: d.Vec3f, r: d.F32, k: d.F32) => d.F32>; export declare const sdDodecahedron: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32) => d.F32>; export declare const sdCutSphere: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32, h: d.F32) => d.F32>; export declare const sdRibbon: import('typegpu').TgpuFn<(p: d.Vec3f, w: d.F32, th: d.F32, len: d.F32, amp: d.F32, freq: d.F32, ph1: d.F32, ph2: d.F32, twRate: d.F32, twPh: d.F32, lip: d.F32) => d.F32>; export declare const sdWobbleBlob: import('typegpu').TgpuFn<(p: d.Vec3f, r: d.F32, amp: d.F32, freq: d.F32, ph1: d.F32, ph2: d.F32, lip: d.F32) => d.F32>; export declare const sdFlatBand: import('typegpu').TgpuFn<(radialDist: d.F32, axialDist: d.F32, R: d.F32, ht: d.F32, hw: d.F32, rnd: d.F32) => d.F32>; export declare const sdGyroscope: import('typegpu').TgpuFn<(p: d.Vec3f, radius: d.F32, width: d.F32, thick: d.F32, rnd: d.F32, core: d.F32, c1: d.F32, s1: d.F32, c2: d.F32, s2: d.F32, c3: d.F32, s3: d.F32) => d.F32>; /** CPU-precomputed sin/cos rotation, passed to the march as one struct argument (ZXY Euler). */ export declare const RotStruct: d.WgslStruct<{ cx: d.F32; sx: d.F32; cy: d.F32; sy: d.F32; cz: d.F32; sz: d.F32; }>; /** Rotate a vector by ZXY Euler angles using CPU-precomputed sin/cos uniforms (GPU). */ export declare const rotateVec3: import('typegpu').TgpuFn<(v: d.Vec3f, rot: d.WgslStruct<{ cx: d.F32; sx: d.F32; cy: d.F32; sy: d.F32; cz: d.F32; sz: d.F32; }>) => d.Vec3f>; /** A shape SDF baked with its size params — the piece the march consumes. */ export type BakedSdf = (p: any) => any; /** * Shared orthographic raymarch core: turns any Lipschitz-safe 3D SDF into a 2D field sampler. * Returns a resolvable `tgpu.fn([d.vec2f, RotStruct, d.f32], d.vec4f)`: * .r = scalar field (silhouette distance outside, −chord/2 inside) * .g/.b = surface-locked pattern coords (per `patternMode`; 0 when 'none') * .a = view-space hit depth (tEnter) along the ray * * `fuv` = field UV, `rot` = the CPU-precomputed rotation struct, `rBound` = a bounding-sphere * radius that always contains the solid (used to skip empty space analytically). * * The shape's size params are baked into `sdfFn` by the caller (a `tgpu.fn([d.vec3f], d.f32)` * reading its size uniforms); `patternMode` is a build-time JS string that selects the emitted * branch (only 'triplanar' emits the 6-eval normal probe; 'none' emits no pattern code). */ export declare function buildRaymarchedFieldFn(sdfFn: BakedSdf, patternMode?: SurfacePatternMode, chordMode?: ChordMode): import('typegpu').TgpuFn<(fuv: d.Vec2f, rot: d.WgslStruct<{ cx: d.F32; sx: d.F32; cy: d.F32; sy: d.F32; cz: d.F32; sz: d.F32; }>, rBound: d.F32) => d.Vec4f>; /** Thin wrapper for `buildRaymarchedFieldFn` (the march fn is the value itself; rotation + * bounding radius are call arguments, not build-time closures). */ export declare function buildRaymarchedField(sdfFn: BakedSdf, patternMode?: SurfacePatternMode, chordMode?: ChordMode): import('typegpu').TgpuFn<(fuv: d.Vec2f, rot: d.WgslStruct<{ cx: d.F32; sx: d.F32; cy: d.F32; sy: d.F32; cz: d.F32; sz: d.F32; }>, rBound: d.F32) => d.Vec4f>; /** Field texture format. rgba32float is required (fp16 quantizes the depth channel → visible * banding in the reconstructed normals) and is NOT filterable in WebGPU, hence the manual * bilinear/Catmull-Rom in the samplers. */ export declare const VOLUMETRIC_FIELD_FORMAT: "rgba32float"; /** Field-texture resolution. The near-binary edge mask of Neon/Holographic/Crystal exposes the * texel lattice as jagged silhouettes once a field texel spans more than ~1 device pixel. 1536 * is the middle ground (1.5× the linear resolution of 1024, 2.25× the march cost, 36 MB fp32 vs * 16 MB) which — together with the aspect-fit domain — gets the strokes below device-pixel * frequency without the 4× hit of 2048. The march only re-runs on shape-state changes. */ export declare const VOLUMETRIC_FIELD_RES = 1536; /** Mobile/tablet field-texture ALLOCATION (VRAM ceiling + activeRes cap). A coarse-pointer device * gets a smaller field than the 1536 desktop default: its GPU can't afford the desktop march * (cost scales with res²) and the texture is heavy (768² is 9 MB fp32 vs 1024²'s 16 MB vs 1536²'s * 36 MB). Why 768 and not 1024: on mobile the active region is sized at HALF the device-pixel * footprint, so a typical phone shape already resolves to ≤640 — well under this cap. */ export declare const VOLUMETRIC_FIELD_RES_MOBILE = 768; /** * Pick the pre-march field resolution for this device, once, at shader-build time. * * The field texture is allocated when the sampler node is built and is NEVER reallocated * afterwards (the renderer keeps resize free of texture reallocation), so we can't track the live * canvas size. The one signal available synchronously is the device class, which is exactly what * separates "needs the full desktop field" from "a phone that shouldn't pay for a 2K texture". */ export declare function resolveVolumetricFieldRes(): number; /** * Pick the active march/sample resolution for a shape from its on-screen footprint. * * The field domain covers the shape's AABB (spanX × spanY in field-UV). The shader maps field-UV * → screen with the `scale` uniform, so matching one field texel to one device pixel along the * tighter axis means activeRes ≈ max(spanX,spanY) · scale · Hdev. We snap up to a bucket and clamp * to the texture allocation. `prevRes` adds a shrink deadband so a shape hovering on a boundary * doesn't re-march every frame. */ export declare function resolveActiveFieldRes(spanXv: number, spanYv: number, scale: number, canvasHeightDevicePx: number, maxRes: number, prevRes: number): number; /** * March-pass uniform. Holds the rotation, bounding radius, aspect-fit domain (span/origin), active * resolution, AND the shape's size superset (pA..pD + MAX_METABALLS metaball centres) so the * caller's baked `sdfFn` reads its params from `layout.$.params.pA` etc. It is a superset; unused * fields are never read by the shape's kernel. (The consumer's per-frame CPU setup writes this * each time the shape state changes — see CONSUMER WIRES.) * * SLOT MAP — the mb0..mb7 vec3 slots are the metaballs3D centres; the other CPU-animated shapes * reuse them positionally as generic parameter carriers. The `update` switch in * `createAnalytic3dSdfSetup` packs them and the matching `sdfFn` branch unpacks them — keep the * two in sync with this table: * metaballs3D: mb0..mb7 = ball centres (unused balls parked at y = 99) * ribbon3D: mb0 = (waveAmp, waveFreq, phase1) · mb1 = (phase2, twistRate, twistPhase) * mb2.x = Lipschitz factor * blob3D: mb0 = (wobbleAmp, wobbliness, phase1) · mb1.x = phase2 · mb2.x = Lipschitz factor * gyroscope3D: mb0/mb1/mb2 = (cos, sin, –) of the three ring angles · mb3.x = bevel radius */ export declare const MarchParams: d.WgslStruct<{ rot: d.Decorated, [d.Align<16>, d.Size<32>]>; rBound: d.F32; spanX: d.F32; spanY: d.F32; originX: d.F32; originY: d.F32; activeRes: d.U32; pA: d.F32; pB: d.F32; pC: d.F32; pD: d.F32; mb0: d.Vec3f; mb1: d.Vec3f; mb2: d.Vec3f; mb3: d.Vec3f; mb4: d.Vec3f; mb5: d.Vec3f; mb6: d.Vec3f; mb7: d.Vec3f; }>; /** * Build the GPU-free compute bind-group layout for the pre-march (kit/blur `build*Graph` pattern): * - `field` → the write-only rgba32float storage texture the march fills. * - `params` → the `MarchParams` uniform. * * The caller threads the returned `layout` through both the baked `sdfFn` (which reads * `layout.$.params.pA` …) and `buildVolumetricFieldKernel`, so everything shares one layout / one * bind group. Returned separately from the kernel so the parent can bake `sdfFn` against * `layout.$.params` before the march + kernel are built. */ export declare function makeVolumetricFieldLayout(): { layout: import('typegpu').TgpuBindGroupLayout<{ field: { storageTexture: d.WgslStorageTexture2d<"rgba32float", string>; }; params: { uniform: d.WgslStruct<{ rot: d.Decorated, [d.Align<16>, d.Size<32>]>; rBound: d.F32; spanX: d.F32; spanY: d.F32; originX: d.F32; originY: d.F32; activeRes: d.U32; pA: d.F32; pB: d.F32; pC: d.F32; pD: d.F32; mb0: d.Vec3f; mb1: d.Vec3f; mb2: d.Vec3f; mb3: d.Vec3f; mb4: d.Vec3f; mb5: d.Vec3f; mb6: d.Vec3f; mb7: d.Vec3f; }>; }; }>; MarchParams: d.WgslStruct<{ rot: d.Decorated, [d.Align<16>, d.Size<32>]>; rBound: d.F32; spanX: d.F32; spanY: d.F32; originX: d.F32; originY: d.F32; activeRes: d.U32; pA: d.F32; pB: d.F32; pC: d.F32; pD: d.F32; mb0: d.Vec3f; mb1: d.Vec3f; mb2: d.Vec3f; mb3: d.Vec3f; mb4: d.Vec3f; mb5: d.Vec3f; mb6: d.Vec3f; mb7: d.Vec3f; }>; }; export type VolumetricFieldLayout = ReturnType['layout']; /** * The compute kernel that pre-marches the field into the storage texture. Dispatched 2D over the * ACTIVE block (see D3 rules: TGSL `/` is always float, so a 1D→2D index reconstruction is unsafe * — the guarded pipeline gives clean `(cx, cy)` u32 + an auto bounds guard, and we * `dispatchThreads(activeRes, activeRes)` each frame). Each active texel maps to its field-UV over * the aspect-fit domain (span/origin) at the active density, marches, and writes the top-left * `activeRes × activeRes` block of the maxRes texture. */ export declare function buildVolumetricFieldKernel(layout: VolumetricFieldLayout, marchFn: ReturnType): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** * Fragment-side sampler uniform: the aspect-fit domain + active resolution + bounding radius. (The * composer binds this + the field texture into the sampler layout — see CONSUMER WIRES.) */ export declare const SampleParams: d.WgslStruct<{ originX: d.F32; originY: d.F32; spanX: d.F32; spanY: d.F32; activeResF: d.F32; rBound: d.F32; }>; /** * Build the field samplers over a kit-owned bind-group layout (kit/blur pattern): * - `field` → the rgba32float texture bound SAMPLED (`d.texture2d(d.f32)`). * - `params` → the `SampleParams` uniform (domain + active res + bounding radius). * * Returns two resolvable `tgpu.fn([d.vec2f], d.vec4f)` samplers: * - `fieldSample` — bicubic Catmull-Rom on .r/.a + inner-2×2 neighbourhood clamp + bilinear * .g/.b + far-field circle blend. Keeps the near-binary silhouette/normal * edges crisp under magnification. * - `fieldSampleFast` — 4-tap bilinear (same domain map + far-field extension). Used by * finite-difference gradient taps, which don't benefit from the bicubic. * * rgba32float is not filterable, so texels are read with an integer `std.textureLoad(field, coords, * 0)` (3-arg SAMPLED read — level 0), NOT `.sample()`; there is no `/maxRes` normalization. * * CONSUMER WIRES: register the compute `fieldTexture` (from `createVolumetricFieldCompute`) as a * `params.registerComputeTexture(...)` KitTexture the fragment samples, bind it + a `SampleParams` * uniform to the returned `sampleLayout`, and drive the domain values each frame. The `texelSpan` * an effect uses for a fast-sampler early-out is `max(spanX, spanY) / activeRes` (CPU-computable * from the same values). */ export declare function buildFieldSampleGraph(): { sampleLayout: import('typegpu').TgpuBindGroupLayout<{ field: { texture: d.WgslTexture2d; sampleType: string; }; params: { uniform: d.WgslStruct<{ originX: d.F32; originY: d.F32; spanX: d.F32; spanY: d.F32; activeResF: d.F32; rBound: d.F32; }>; }; }>; fieldSample: import('typegpu').TgpuFn<(uv: d.Vec2f) => d.Vec4f>; fieldSampleFast: import('typegpu').TgpuFn<(uv: d.Vec2f) => d.Vec4f>; SampleParams: d.WgslStruct<{ originX: d.F32; originY: d.F32; spanX: d.F32; spanY: d.F32; activeResF: d.F32; rBound: d.F32; }>; }; /** Device-side volumetric field pre-march: allocates the field texture + march uniform, builds the * guarded compute pipeline, and exposes a per-frame `writeParams` + variable-count `dispatch`. */ export interface VolumetricFieldCompute { /** The guarded pipeline (already bound); dispatch it with `dispatch(activeRes)`. */ computeStep: KitComputePipeline; /** rgba32float, storage + sampled. The consumer registers this as a compute KitTexture the * fragment samples (CONSUMER WIRES). */ fieldTexture: TgpuTexture; /** Fixed texture allocation (device-class dependent). */ maxRes: number; /** Current active linear resolution (≤ maxRes). */ readonly activeRes: number; /** Write the whole march uniform (the parent's per-frame CPU setup drives this). */ writeParams(value: InferInput): void; /** Dispatch a 2D `activeRes × activeRes` guarded grid, filling the top-left block. */ dispatch(activeRes: number): void; } /** * Allocate + wire the compute pre-march. `layout` + `kernel` come from `makeVolumetricFieldLayout` * / `buildVolumetricFieldKernel` (the caller bakes the shape `sdfFn` against `layout.$.params` * first). The guarded pipeline is built WITHOUT a pre-bound size so each frame can * `dispatch(activeRes)` a clean 2D grid. * * NOTE: this is the device-touching allocator (like kit/blur's `createGaussianBlurCompute`); it is * NOT exercised under vitest (no GPU) — only tsc-checked. The march/kernel/sampler CORRECTNESS is * verified on real GPU by the shape-effect shaders. CONSUMER WIRES: register `fieldTexture` as a * compute KitTexture + bind the fragment sampler layout (see `buildFieldSampleGraph`), and drive * `writeParams` / `dispatch` from the per-frame CPU setup (the createAnalytic3dSdfSetup equivalent). */ /** A MarchParams uniform handle (from `root.createUniform(MarchParams)`) the caller can share with * the compute — so the setup's per-frame `update` writes the SAME buffer the kernel reads. */ export interface MarchParamsUniform { readonly buffer: unknown; write(value: InferInput): void; } /** Extra wiring for `createVolumetricFieldCompute`: * - `paramsUniform` → reuse a caller-owned MarchParams uniform (the shape setup writes it each * frame) instead of creating a fresh one. The kernel's bind group binds it. * - `sdfSourceTexture` → the 2D SDF DATA texture the SVG-extrusion `sdfFn` samples inside the march * (`layout` MUST be a `makeVolumetricSvgFieldLayout()` layout with the * `sdfSource` entry). Bound into the SAME compute bind group. */ export interface VolumetricFieldComputeExtra { paramsUniform?: MarchParamsUniform; sdfSourceTexture?: unknown; /** Additional bind groups the march fn reads (a consumer's OWN param uniform — the voxel * pre-march's `VoxelMarchParams`). Bound after the field/params group. */ extraBindGroups?: unknown[]; } export declare function createVolumetricFieldCompute(root: TgpuRoot, layout: VolumetricFieldLayout, kernel: ReturnType, onCleanup: (cb: () => void) => void, extra?: VolumetricFieldComputeExtra): VolumetricFieldCompute; /** * SVG-extrusion variant of the march bind-group layout: same `field` + `params` as * `makeVolumetricFieldLayout`, PLUS a SAMPLED `sdfSource` entry — the 2D SDF DATA texture the * extrusion `sdfFn` reads (via `textureLoad`) inside the march. Kept as a SEPARATE function (rather * than an optional flag on `makeVolumetricFieldLayout`) so the analytic path's layout TYPE stays * free of the optional entry — the SVG `sdfFn` reads `layout.$.sdfSource` off THIS layout's precise * type, and `createVolumetricFieldCompute`'s `sdfSourceTexture` extra binds it into the same group. */ export declare function makeVolumetricSvgFieldLayout(): { layout: import('typegpu').TgpuBindGroupLayout<{ field: { storageTexture: d.WgslStorageTexture2d<"rgba32float", string>; }; params: { uniform: d.WgslStruct<{ rot: d.Decorated, [d.Align<16>, d.Size<32>]>; rBound: d.F32; spanX: d.F32; spanY: d.F32; originX: d.F32; originY: d.F32; activeRes: d.U32; pA: d.F32; pB: d.F32; pC: d.F32; pD: d.F32; mb0: d.Vec3f; mb1: d.Vec3f; mb2: d.Vec3f; mb3: d.Vec3f; mb4: d.Vec3f; mb5: d.Vec3f; mb6: d.Vec3f; mb7: d.Vec3f; }>; }; sdfSource: { texture: d.WgslTexture2d; }; }>; MarchParams: d.WgslStruct<{ rot: d.Decorated, [d.Align<16>, d.Size<32>]>; rBound: d.F32; spanX: d.F32; spanY: d.F32; originX: d.F32; originY: d.F32; activeRes: d.U32; pA: d.F32; pB: d.F32; pC: d.F32; pD: d.F32; mb0: d.Vec3f; mb1: d.Vec3f; mb2: d.Vec3f; mb3: d.Vec3f; mb4: d.Vec3f; mb5: d.Vec3f; mb6: d.Vec3f; mb7: d.Vec3f; }>; }; export type VolumetricSvgFieldLayout = ReturnType['layout']; /** * FN-ARG field samplers — the CONSUMER-WIREABLE form of `buildFieldSampleGraph`. Same * bicubic/bilinear math, but the field texture + the six SampleParams (origin/span/activeRes/rBound) * arrive as FN ARGUMENTS instead of a captured kit-owned bind-group layout. This is the fn-arg * path: the consuming shape-effect FRAGMENT passes the compute field texture's `.accessor()` (a * group-1 `registerComputeTexture` binding) + its own SampleParams accessors (declared as * `extraFields`, see `VOLUMETRIC_FIELD_EXTRA_FIELDS`). The captured-layout `buildFieldSampleGraph` * cannot be used directly by a fragment because the pass manager only binds the four composer-pinned * groups (uniforms/textures/samplers/external) — it has no mechanism to bind a shader-owned 5th * bind group — so a fragment that referenced `sampleLayout.$.field` would resolve but never get * bound. The fn-arg form routes everything through the standard groups. * * rgba32float is not filterable → texels are read with integer `std.textureLoad(field, coords, 0)`. */ export declare function buildFieldSampleGraphArgs(): { fieldSampleArg: import('typegpu').TgpuFn<(uv: d.Vec2f, field: d.WgslTexture2d, originX: d.F32, originY: d.F32, spanX: d.F32, spanY: d.F32, activeResF: d.F32, rBound: d.F32) => d.Vec4f>; fieldSampleGradArg: import('typegpu').TgpuFn<(uv: d.Vec2f, field: d.WgslTexture2d, originX: d.F32, originY: d.F32, spanX: d.F32, spanY: d.F32, activeResF: d.F32, rBound: d.F32) => d.Vec4f>; fieldSampleFastArg: import('typegpu').TgpuFn<(uv: d.Vec2f, field: d.WgslTexture2d, originX: d.F32, originY: d.F32, spanX: d.F32, spanY: d.F32, activeResF: d.F32, rBound: d.F32) => d.Vec4f>; fieldSampleTexelArg: import('typegpu').TgpuFn<(uv: d.Vec2f, field: d.WgslTexture2d, originX: d.F32, originY: d.F32, spanX: d.F32, spanY: d.F32, activeResF: d.F32, rBound: d.F32) => d.Vec4f>; texelSpanFn: import('typegpu').TgpuFn<(spanX: d.F32, spanY: d.F32, activeResF: d.F32) => d.F32>; }; /** * The six SampleParams a shape-effect shader must declare as `extraFields` so the field SAMPLER * (fn-arg form) can read the compute-written aspect-fit domain in its FRAGMENT. `initial` values are * inert (span 1 / full-res); `createVolumetricFieldComputeNode` writes the live values each frame * via `params.setExtraField`. CONSUMER WIRES: spread these into the shader definition's * `extraFields`, then pass `params.uniforms.` into `buildVolumetricFieldSampler`. */ export declare const VOLUMETRIC_FIELD_EXTRA_FIELDS: Record; /** The six SampleParams accessor Exprs the consumer's fragment reads from `params.uniforms._vf*`. */ export interface VolumetricFieldSampleParams { originX: Expr; originY: Expr; spanX: Expr; spanY: Expr; activeResF: Expr; rBound: Expr; } /** The field-sampler Exprs a shape-effect fragment consumes. */ export interface VolumetricFieldSamplers { /** Bicubic sampler — crisp silhouette/normal edges. `(uv) => vec4(field, gPattern, bPattern, depth)`. */ volumetricFieldSampler: (uv: Expr) => Expr; /** Bicubic value + ANALYTIC gradient in one 16-load tap: `(uv) => vec4(field, ∂field/∂u, * ∂field/∂v, depth)`. Replaces a 3-tap finite-difference stencil (48 loads → 16) with a * C1-smooth, texel-localised gradient — feed an effect's `bakedGradients` path. `.g/.b` are * gradients, NOT pattern coords. */ volumetricFieldSamplerGrad: (uv: Expr) => Expr; /** 4-tap bilinear sampler (finite-difference gradient taps). */ volumetricFieldSamplerFast: (uv: Expr) => Expr; /** Unfiltered nearest-texel read — discrete payload channels (ids, packed cells). */ volumetricFieldSamplerTexel: (uv: Expr) => Expr; /** `max(spanX, spanY) / activeRes` — the fast-sampler early-out threshold. */ volumetricFieldTexelSpan: Expr; } /** * Build the field samplers for a shape-effect FRAGMENT. `field` is the compute-written field * KitTexture (`params.computeOutputs.volumetricFieldTexture` — a `registerComputeTexture` binding); * `p` are the SampleParams accessor Exprs (`params.uniforms._vf*`). Returns the three sampler Exprs * the effect samples. See `createVolumetricFieldComputeNode` for the compute side that fills them. */ export declare function buildVolumetricFieldSampler(field: KitTexture, p: VolumetricFieldSampleParams): VolumetricFieldSamplers; /** How a consumer wants the SDF-field gradient delivered on each of the three shape paths. */ export interface ShapeFieldSamplerOptions { /** Surface-pattern coords baked into the ANALYTIC sampler (`.g/.b`). Match the shader's * `createVolumetricFieldComputeNode` patternMode so both paths agree. */ patternMode?: SurfacePatternMode; /** * The second sampler the consumer takes its finite-difference neighbour taps with: * - `'fast'` (default) — the cheap 4-tap bilinear sampler on the volumetric path, the same * sampler as `sampler` on the flat paths. The metals/Frost/Water contract. * - `'same'` — `gradSampler === sampler` everywhere (a consumer that takes no extra taps, or * wants the bicubic tap for all three). */ gradSampler?: 'fast' | 'same'; /** * Which paths carry gradients baked into the CENTRE tap's `.g/.b` (so the consumer skips its * finite-difference taps — see the returned `bakedGradients`): * - `'none'` (default) — nobody does; the consumer takes its own taps. * - `'volumetric'` — only the compute path (`volumetricFieldSamplerGrad`). ThinFilm. * - `'all'` — the compute path AND the flat-SVG path (`createSvgSdfSamplerWithGradients`); * the analytic path still reports `bakedGradients: false`. Glass. */ bakedGradients?: 'none' | 'volumetric' | 'all'; } /** The resolved shape-field sampling surface a shape-effect fragment builds its taps from. */ export interface ShapeFieldSamplerResult { /** Centre-tap sampler: `(uv) => vec4(field/−chord, patternU|gradU, patternV|gradV, depth)`. */ sampler: (uv: Expr) => Expr; /** Neighbour-tap sampler (see `ShapeFieldSamplerOptions.gradSampler`). */ gradSampler: (uv: Expr) => Expr; /** Unfiltered nearest-texel tap for discrete payload channels (the volumetric path); the flat * paths (analytic / SVG bilinear) alias `sampler`. */ texelSampler: (uv: Expr) => Expr; /** The active shape is a 3D / extruded field (`.r` is −chord/2, not a 2D distance). */ volumetric: boolean; /** The centre tap carries gradients in `.g/.b` — the consumer must skip its own taps. */ bakedGradients: boolean; } /** * Resolve the SDF field sampler for a shape-effect fragment: the volumetric → flat-SVG → analytic * routing every shader in the family (Glass, LiquidMetal, Chrome, Frost, Heatmap, …) used to * hand-write. Call it from `fragment` with the same `patternMode` the shader's * `createVolumetricFieldComputeNode` uses. * * - **volumetric** — `params.computeOutputs.volumetricFieldTexture` exists, so the compute pass * pre-marched a 3D/extruded field; samplers come from `buildVolumetricFieldSampler` against * this node's `_vf*` SampleParams accessors. * - **flat SVG** — a `shapeSdfUrl` prop; the 2D SDF data texture (`kit/sdf`). * - **analytic** — the baked 2D SDF for the resolved shape type, with its animated sub-props * driven per frame by `driveAnalyticSubProps`. * * A shader with no `shapeSdfUrl`/`shapeType` props still works (both read as `''` → analytic). */ export declare function resolveShapeFieldSampler(params: GpuFragmentParams, options?: ShapeFieldSamplerOptions): ShapeFieldSamplerResult; /** A per-frame shape-effect volumetric setup: a baked `sdfFn` (reads its layout's MarchParams) plus * the CPU resolver that writes the March uniform each frame from the shape config (auto-animate / * mouse sub-props). The parent (`createVolumetricFieldComputeNode`) threads `layout` + `sdfFn` * into the march/kernel/compute and drives `update` / `setActiveRes` / `getStateKey`. */ export interface VolumetricFieldSetup { /** The march bind-group layout the `sdfFn` reads its params off (analytic or SVG variant). */ layout: VolumetricFieldLayout; /** The MarchParams uniform `update` writes each frame (shared with the compute bind group). */ marchParamsBuffer: MarchParamsUniform; /** The shape SDF baked against `layout.$.params` — fed to `buildRaymarchedFieldFn`. */ sdfFn: BakedSdf; /** SVG-3D only: the 2D SDF DATA texture bound into the compute kernel (`sdfSource`). */ sdfSourceTexture?: unknown; /** Resolve the shape config + write the March uniform (advances the auto-animate clock). */ update: (frameParams?: ShapeFrameParams) => void; /** Set the active linear resolution + re-write the uniform (no time advance). */ setActiveRes: (res: number) => void; /** Dirty-key: the field re-marches only when this changes. */ getStateKey: () => string; /** The aspect-fit domain footprint the parent feeds `resolveActiveFieldRes` / SampleParams. */ getFootprint: () => { spanX: number; spanY: number; originX: number; originY: number; rBound: number; }; /** The CPU-resolved shape rotation (the `rot` written to MarchParams) — for a consumer that * composes its own view→shape transform on the CPU (the voxel pre-march's camera + light). */ getRotation: () => { cx: number; sx: number; cy: number; sy: number; cz: number; sz: number; }; /** Dirty-key of the shape's GEOMETRY alone (state key minus rotation/domain) — for a consumer * caching something in shape-local space (the voxel occupancy grid) that rotation can't change. */ getGeometryKey?: () => string; } /** Optional knobs shared by the volumetric setups. */ export interface VolumetricSetupOptions { /** Extra margin (field UV) added to the bounding radius AND the aspect-fit domain each update — * for a consumer whose geometry puffs the solid outward (a voxel half-diagonal). Read live. */ extraPad?: () => number; } /** The per-frame params a setup's `update` reads (deltaTime → auto clock, pointer → mouse drivers). */ export interface ShapeFrameParams { deltaTime?: number; pointer?: { x: number; y: number; }; } /** * Compile-time-selected analytic 3D SDF setup. Allocates the march layout + a MarchParams uniform, * bakes `sdfFn` for `shapeType` (one primitive, compile-time * branch, reading its size params off `layout.$.params`), and returns a per-frame `update` that * resolves the shape JSON (incl. auto-animate / mouse sub-props) and writes the uniform. */ export declare function createAnalytic3dSdfSetup(root: TgpuRoot, shapeType: string, initialConfig: Record, getShapeConfig: () => unknown, opts?: VolumetricSetupOptions): VolumetricFieldSetup; /** * SVG-extrusion 3D SDF setup. Lifts the uploaded 2D SDF field into a solid by extrusion (slab + * rounded bevel) and raymarches it. The `sdfFn` SAMPLES the 2D SDF DATA texture (bound as the * layout's `sdfSource`) via a bilinear `textureLoad` INSIDE the march (rgba/r16 float texel reads, * explicit level 0). * * The bounding radius uses the full-field fallback (`hwf = hhf = 0.5`) rather than a content-tight * scan (`getSdfContentBounds`, which lives in `utilities/sdfBounds.ts`). Only march EFFICIENCY * differs, never correctness. */ export declare function createSvg3dSdfSetup(params: GpuFragmentParams, shapeSdfUrl: string, getShapeConfig: () => unknown, opts?: VolumetricSetupOptions): VolumetricFieldSetup; /** * Flat analytic shape lifted into a slab (the 2D primitive extruded along z by `getHalfDepth()`, * no bevel, no rotation — the consumer's camera supplies any tilt). The volumetric counterpart of * `createAnalyticSdfSampler` for a consumer that wants EVERY shape on the compute path (the voxel * pre-march treats a flat circle as a one-layer-deep voxel disc). The eight analytic sub-props ride * the MarchParams slots: pA..pD = radius/sides/rounding/innerRatio · mb0 = (rotation, height, * offset) · mb1 = (aperture, halfDepth, –). */ export declare function createAnalytic2dExtrudeSetup(root: TgpuRoot, shapeType: string, getShapeConfig: () => unknown, getHalfDepth: () => number, opts?: VolumetricSetupOptions): VolumetricFieldSetup; /** * The shared shape-effect compute entry. When the active shape is volumetric (analytic 3D or SVG * extrude), pre-marches its field into a compute texture and * exposes the samplers. Returns null for flat shapes and when no device is present (GPU-free resolve * / WebGL) — the consumer then falls back to the inline analytic sampler. * * CONSUMER WIRES (the shape-effect shader that owns this — a later wave): * 1. Declare `...VOLUMETRIC_FIELD_EXTRA_FIELDS` in the shader definition's `extraFields` (the six * SampleParams uniforms this node writes each frame via `setExtraField`). * 2. `compute: (params) => { const vf = createVolumetricFieldComputeNode(params, () => * params.getCpuValue('shape')); return vf && {outputs: vf.outputs, getComputeNodes: * vf.getComputeNodes} }`. * 3. `fragment: (params) => { const fieldTex = params.computeOutputs?.volumetricFieldTexture; if * (!fieldTex) { …inline analytic fallback… } const s = buildVolumetricFieldSampler(fieldTex, * {originX: params.uniforms._vfOriginX, …}); …applyGlassEffect(s.volumetricFieldSampler, …)… }`. * * The one binding this can't close without a consuming fragment is the sampler↔fragment handoff * (step 3): the samplers are built in the FRAGMENT (a separate builder invocation from `compute`), * from the registered field texture (`outputs.volumetricFieldTexture`) + the fragment's own * `extraFields` accessors. Everything on the compute side (setup, march, kernel, dispatch, dirty-key, * SampleParams writes) is closed here. */ export declare function createVolumetricFieldComputeNode(params: GpuFragmentParams, getShapeConfig: () => unknown, patternMode?: SurfacePatternMode, chordMode?: ChordMode): { outputs: { volumetricFieldTexture: KitTexture; }; /** `frameParams` is typed `unknown` (narrowed to `ShapeFrameParams` inside) so the returned * object satisfies the contract's `GpuComputeNode` shape directly — a shader's `compute` hook * is just `compute: (params) => createVolumetricFieldComputeNode(params, …)`, no adapter. */ getComputeNodes: (frameParams?: unknown) => GpuComputeStep[] | null; /** The raw field TgpuTexture (rgba32float, storage+sampled) — for a consumer whose OWN compute * kernels sample the field (SmokeFill's mask kernel). Bind it as a SAMPLED entry declared * `sampleType: 'unfilterable-float'` (rgba32float is not filterable). */ fieldTexture: TgpuTexture; /** The aspect-fit domain + active res describing the field texture's CURRENT contents — the * same values last published to the `_vf*` extraFields (initials before the first march). * Read AFTER `getComputeNodes` each frame for a compute-side consumer's uniform. */ getFieldDomain: () => { originX: number; originY: number; spanX: number; spanY: number; activeRes: number; rBound: number; }; } | null; /** Euler rotation rotateZ(rotateX(rotateY(p))) from packed cos/sin (rp0 = cx,sx,cy,sy; rp1.xy = cz,sz). Pure. */ export declare const eulerRotatePacked: import('typegpu').TgpuFn<(p: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f) => d.Vec3f>; export declare const paramSphereSdf: import('typegpu').TgpuFn<(p: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, _t: d.F32) => d.F32>; /** Spherical (longitude/latitude) surface parameterization. */ export declare const sphereSurfaceUV: import('typegpu').TgpuFn<(h: d.Vec3f, _n: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, _t: d.F32) => d.Vec2f>; export declare const paramTorusSdf: import('typegpu').TgpuFn<(p: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, _t: d.F32) => d.F32>; /** Toroidal (ring angle / tube angle) surface parameterization. */ export declare const torusSurfaceUV: import('typegpu').TgpuFn<(h: d.Vec3f, _n: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, _t: d.F32) => d.Vec2f>; export declare const paramBoxSdf: import('typegpu').TgpuFn<(p: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, rp2: d.Vec4f, _t: d.F32) => d.F32>; /** Dominant-face cube projection: picks the face by the largest |normal| axis, scaled to 0–1. */ export declare const boxFaceUV: import('typegpu').TgpuFn<(h: d.Vec3f, n: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, rp2: d.Vec4f, _t: d.F32) => d.Vec2f>; export declare const paramCapsuleSdf: import('typegpu').TgpuFn<(p: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, _t: d.F32) => d.F32>; /** Cylindrical (azimuth / axial span) surface parameterization. */ export declare const capsuleSurfaceUV: import('typegpu').TgpuFn<(h: d.Vec3f, _n: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, _t: d.F32) => d.Vec2f>; export declare const paramMobiusSdf: import('typegpu').TgpuFn<(p: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, rp2: d.Vec4f, _t: d.F32) => d.F32>; /** Möbius-band (ring angle / half-twisted width) surface parameterization. */ export declare const mobiusSurfaceUV: import('typegpu').TgpuFn<(h: d.Vec3f, _n: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, _t: d.F32) => d.Vec2f>; export declare const paramTwistedRibbonSdf: import('typegpu').TgpuFn<(p: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, t: d.F32) => d.F32>; /** Twisted-ribbon (along-spine / across-width) surface parameterization. */ export declare const twistedRibbonSurfaceUV: import('typegpu').TgpuFn<(h: d.Vec3f, _n: d.Vec3f, rp0: d.Vec4f, rp1: d.Vec4f, _rp2: d.Vec4f, t: d.F32) => d.Vec2f>; /** * Packed vec4 param-carrier extraFields for a param-threaded trace consumer: `0…N-1`, * one vec4f per initial. The CPU driver writes them per frame via `params.setExtraField`. */ export declare function packedParamExtraFields(prefix: string, initials: number[][]): Record; //# sourceMappingURL=sdf3d.d.ts.map