Adding a modifier
A modifier is a wrapper component that merges one field into the modifier frame, a bit in the per-shape mask, and a branch in the shader. Twist is the worked example; a new modifier is the same six edits with its own bit.
The bit map
Section titled “The bit map”Each shape carries a modifier block at the end of its record — modA, modB
and modC, slots 52 to 63 — with the mask in modA.x and the parameters
around it:
| Bit | Modifier | Parameter slot |
|---|---|---|
| 0 | onion | modA.y thickness |
| 1 · 2 · 3 | symmetry X · Y · Z | — |
| 4 | twist | modA.z k |
| 5 | bend | modA.w k |
| 6 | displacement | modB.x amount, modB.y frequency |
| 7 | elongation | modC.xyz half-extents |
| 8 | extrude | modB.z half-thickness |
| 9 | revolve | modB.z radius |
The next free bit is 10. modB.w and modC.w are free parameter floats.
Glass shapes carry the block too, so a new modifier applies to glass as well.
1. The component — modifiers/Twist.tsx
Section titled “1. The component — modifiers/Twist.tsx”export type TwistComponentProps = { /** Twist amount in radians per unit Y. */ amount: number | PhotonValue<number> children: JSXChild | (() => JSXChild)}
export function Twist({ amount, children }: TwistComponentProps) { const parent = useModifier() const merged: ModifierState = { ...parent, twist: amount } pushModifier(merged) try { return runChildren(children) as Node } finally { popModifier() }}Read the enclosing frame, merge your field into it, push, render the children,
pop. Merging with the parent is what lets modifiers nest. Export it from
modifiers/index.ts.
2. The state field — ModifierState in core/types.ts
Section titled “2. The state field — ModifierState in core/types.ts”twist?: number | PhotonValue<number>3. The record — updateSDFBuffers in render/sdfEngine.ts
Section titled “3. The record — updateSDFBuffers in render/sdfEngine.ts”Resolve the value, set the bit if it is active, and write the parameter into its slot:
if (mod.twist != null) { const k = resolveValue(mod.twist, 0) if (k !== 0) { modMask |= 0x10; twistK = k }}// …data[idx+52] = modMask // modA.xdata[idx+54] = twistK // modA.zA value of zero leaves the bit clear, so the shader takes no branch for it.
4. The shader — evalObjectSDF in sdf.wgsl
Section titled “4. The shader — evalObjectSDF in sdf.wgsl”A domain modifier rewrites the local point before the primitive runs:
// Twist: rotate XZ by k*y. Spirals the shape around +Y.//#region MODIFIER Twistif ((modMask & 0x10u) != 0u) { let k = o.modA.z; let c = cos(k * pTr.y); let s = sin(k * pTr.y); pTr = vec3f(c * pTr.x - s * pTr.z, pTr.y, s * pTr.x + c * pTr.z);}//#endregion MODIFIER TwistOrder matters and follows IQ’s article: elongate → symmetry → bend → twist →
primitive. A distance modifier — onion, displacement — rewrites d after the
primitive instead, in the block below the dispatch.
5. The envelope — vs in sdf.wgsl
Section titled “5. The envelope — vs in sdf.wgsl”If the modifier makes the shape bigger, the proxy cube has to grow or the shape clips at its silhouette. Twist has no term. The ones that do:
let onionExpand = o.modA.y * f32((modMaskV & 0x1u) != 0u);let displExpand = o.modB.x * f32((modMaskV & 0x40u) != 0u);let bendK = o.modA.w;let bendExpand = max(max(env.x, env.y), env.z) * abs(bendK) * 0.5 * f32((modMaskV & 0x20u) != 0u);let elongExpand = o.modC.xyz * f32((modMaskV & 0x80u) != 0u);Each term is multiplied by its bit, so it contributes zero when the modifier is off. Add a term for yours if it can push the surface past the bare envelope.
6. The shake plugin — MODIFIER_NAME_TO_BITS
Section titled “6. The shake plugin — MODIFIER_NAME_TO_BITS”Twist: [0x10],The keys are the wrapper tags the plugin looks for. When <Twist appears
nowhere in the app’s source, it removes the MODIFIER Twist region from step
4. It strips only the names in this table, so without an entry your branch
always ships.
Say whether it is metric
Section titled “Say whether it is metric”If the modifier distorts space non-uniformly, the distance it returns
overestimates how far a ray can safely step, and the march can pass through a
surface. Twist, bend and displace are non-metric; onion, symmetry and elongate
are not. Document which yours is — see
Modifiers — so users know to reduce the amount
or raise maxSteps.