Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld db2c16ac-a…

asc_tornado

ASCENSION's aspect 3 — the fire tornado. The hero, and the largest single line in the part's frame budget.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 3mo ago
What it does

asc_tornado

ASCENSION's aspect 3 — the fire tornado. The hero, and the largest single line in the part's frame budget.

Six states, all of them on the clock bus and none of them in this module's own memory: arrival, ignition, approach, spin peak, tear, gone. Three things carry them.

The volume. A 128 × 256 × 128 emissive box spanning 180 × 280 × 180 u, sited on torX/torZ and standing on the deformed sand — the base height comes from DESERT's published duneGroundAt, not from y = 0. It is filled by asc_pyre and raymarched by preseed.volume into its composite at post priority 5, which is what puts the column inside the storm: STORM's medium composites at 6, so its extinction and inscatter apply over the fire rather than under it.

The sparks. One camera-facing quad swarm on asc_spark, flown entirely in the vertex stage off bus scalars. No per-frame CPU.

The lights. One base point light on fireKey plus aux.emberLights of eight along the column, carried around the axis by the same accumulated angle the fill turns by. Nine, and not a tenth; none of them casts a shadow.

The three calibrations

EMISSION_GAIN compensates the marcher, which delivers Σ trans·emission·(1−e^−στ) rather than the emission itself, so a ray crossing the core at alpha 0.93 lands 93 % of the authored radiance.

DISPLAY_CEIL is the brightest byte this post chain actually delivers, measured rather than derived: the vignette and grade between the tonemap and the framebuffer cost about 14 %, so asking for a red channel at full display clamps it and clamps green with it, and the core comes back a neutral grey. The whole accent row is scaled by the real ceiling before the tonemap is inverted, which is what keeps the hue when the magnitude has nowhere left to go.

KEY_GAIN and BASE_RANGE turn the bus's author-facing fireKey curve into a point light's intensity and reach. They are calibrated at the P4/P5 stations, where fireKey is at its highest and the near sand is closest — a calibration taken at the P6 crane, where the curve is already falling, leaves the near dunes lit to a neutral cream in exactly the beats the focal hierarchy makes them texture.

The two bus amendments

aux.torSpinAccum — the integral of torSpin in revolutions, the rotational twin of windAccum. A fill shader handed an angular rate has to integrate it to get an angle, and integrating per frame would make every seek a different tornado.

aux.torTear — the collapse's loss of axis, which ignite does not carry. ignite falling is the fire going out top-down; this is the three ropes drifting apart. ignite alone cannot tell t = 12 (rising through 0.1) from t = 27.8 (falling through 0.1), and those are not the same picture.

The cut ladder

preseed/volume marches into ONE shared 1600 × 900 render target for the whole demo, and its size is a module constant that five other parts also march into. Rung ① is therefore spent on the other factor of the same product — samples per ray — rather than on pixels: 2.00 u × 172 steps drops to 3.10 × 112, which is about 40 % of the march's work for a resolution the composite does not have to be taught about. Rung ③ takes the step count down again, drops the fill's finest noise band, and takes the volume to 96 × 192 × 96, which needs a re-spawn and gets one, exactly on the crossing.

Switching itself off

Outside t ∈ [10, 28.2] the fill is dispatched exactly once more, to clear the texture — a 3D texture holds whatever it was last filled with, and a seek out of the column's window would otherwise leave a frozen tornado standing in the desert — and the march is then handed a degenerate AABB, so every ray's box intersection is empty and the marcher early-outs on its first branch. That is how this aspect disables its own composite without reaching into preseed/volume's private pass key.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

preseed/asc/asc_tornado.module — ASCENSION's aspect 3: THE FIRE TORNADO. The hero. Six states, all of them on the bus and none of them in this file's own memory: arrival (t 10.0 → 15.0, a warm smear that acquires an axis), ignition (15.000, a 0.30 s root-to-tip sweep), approach (15 → 20, full body), spin peak (22.50, plus the base debris torus), tear (25.0 → 28.2, three ropes that lose the axis and go out top-down), gone. `torX` `torZ` `torR` `torH` `torSpin` `ignite` `fireKey` carry all six, and `aux.torSpinAccum` and `aux.torTear` are this aspect's two amendments to the bus. WHAT IS IN THIS ASPECT the volume a 128 × 256 × 128 emissive box, 180 × 280 × 180 u, sited on `torX`/`torZ` and standing on the DEFORMED sand — the base y is read from DESERT's published `duneGroundAt`, not from 0. Filled by `asc_pyre` and raymarched by `preseed.volume` into its composite at post priority 5, so STORM's medium at 6 applies its extinction OVER the fire and the column sits IN the storm rather than in front of it. the sparks one camera-facing quad swarm on `asc_spark`, flown entirely in the vertex stage off the bus. Zero per-frame CPU. the lights ONE base point light on `fireKey` plus `aux.emberLights` of eight along the column. Nine, and not a tenth. None casts. WHY THE ROTATION IS THE WHOLE THING. A column that flickers without turning is a fire pillar. `aux.torSpinAccum` is the integral of `torSpin` in revolutions; the fill shader carries its sample frame around the axis by that angle, turns the inner radii faster than the outer, and TWISTS the frame with height so every feature is a helix even in a frozen frame. Because the accumulator is a cumulative table sampled at t rather than a per-frame integration, the picture is a pure function of t and the director's seeks are repeatable. WHY THE COLOUR IS AUTHORED THROUGH `invAces`. The fill's ramp is handed SCENE-LINEAR RADIANCE, not §3's hex triples: `Palette.invAces(hexD(hex))` divided by the frame's `sceneExposure` is the value the engine's tonemap maps ONTO that hex. #FF7A22 is 1 : 0.048 : 0.015 as radiance where its hex reads 1 : 0.42 : 0.10, and feeding the hex ratios in as radiance is precisely how a white-hot core comes back as neutral grey — measured at rgb(219, 219, 216), saturation 0.014, against §3's floor of 0.10. WHAT THIS ASPECT DOES NOT DRAW, deliberately: the storm's dust medium, the airborne sand on the ground, the ambient inscatter. It LIGHTS them, through `fireKey` and `aux.dustLitByFire`, and STORM renders them.

lin(hex: string) → void

argtypedescription
hexstring

sparkQuads( ) → void

Four coincident vertices per spark, spanned into a camera-facing quad by the vertex stage. `uv.x` is the seed; `uv.y` packs the corner index in its integer part and the spark's size multiplier in its fraction, because a mesh vertex carries exactly two uv channels and a billboarded sprite needs three numbers.

addLight(ctx: ?, name: string, colour: { number }, intensity: number, range: number) → void

argtypedescription
ctx?
namestring
colour{ number }
intensitynumber
rangenumber

build(ctx: ?, st: ?, ck: ?) → void

argtypedescription
ctx?
st?
ck?

enter(ctx: ?, st: ?, ck: ?) → void

argtypedescription
ctx?
st?
ck?

setLight(id: string?, pos: { number }?, intensity: number, colour: { number }?, range: number?) → void

argtypedescription
idstring?
pos{ number }?
intensitynumber
colour{ number }?
rangenumber?

update(ctx: ?, st: ?, ck: ?, dt: ?, sun: ?, cam: ?) → void

argtypedescription
ctx?
st?
ck?
dt?
sun?
cam?

ramp(k: string) → void

argtypedescription
kstring

check(st: ?) → void

What the fill pass actually did, for a check that does not depend on a screenshot: a compute dispatch returning true means QUEUED, and `status()` is where the outcome lands.

argtypedescription
st?

exit(ctx: ?, st: ?) → void

argtypedescription
ctx?
st?

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

24+items
▣
module · born here
❒asset
# geo A module script.
▲ 1↑ born
✦
shader · born here
❒asset
# asc_spark ASCENSION's ember swarm — the sparks the fire tornado throws off its own surface. The vertex stage flies each spark off material scalars and there is no per-frame CPU at all. Each spark rides the column's live radius profile at its current height and its azimuth advances with the accumulated spin in revolutions, so the swarm turns with the surface it came off instead of drifting through it, and the radius contracts with height as the inflow. Sparks exist only below the fire front — `ignite` is coverage measured from the root — and during the tear they lose the axis on the wind exactly as the flame ropes do. ## A spark is a soft round dot of a fixed pixel size The mesh is four coincident vertices per spark, spanned into a **camera-facing quad** by the vertex stage on `cam_right` / `cam_up`, which the aspect writes every frame; `uv.x` is the seed and `uv.y` packs the corner index in its integer part and the size multiplier in its fraction, because a mesh vertex carries two uv channels and a billboarded sprite needs three numbers. The fragment fades on `1 − dot(uv, uv)`, so there is no quad edge, no corner and no straight line in the sprite. Its world size is **proportional to its distance from the camera**, so its screen size is constant: `size_k` is the target diameter as a fraction of frame height times the lens half-angle, recomputed from the live fov each frame. Authored as a world size, a spark that reads as three pixels at the 330 u approach arrives at the 84 u climax as a sixteen-pixel slab of geometry. It blends **additively**, premultiplied by its own falloff, so a spark can only ever add light. An ember darker than the fire behind it is a hole, not an ember. The authored centres fill the whole flight volume, so the static AABB covers every animated position. It is a separate shader from `fyr_ember` rather than a reuse of it because that one bakes a 7 u flight height and a sub-metre spiral in as constants, and scaling the entity to reach 240 u would scale the ember's own size by the same factor.
▲ 0↑ born
▣
module · born here
❒asset
# asc_clock — ASCENSION: the shared clock The finale's spine. One pure function of part-local `t` produces every number that crosses between the part's eight aspects. **Nothing else in ASCENSION reads raw `t`, wall time or a frame counter** — the orchestrator evaluates this module once per frame and hands the resulting table to every aspect hook. Eight aspects reading the same numbers resolve into one scene. Eight aspects each keying off their own idea of the beat do not. That is the entire reason this module exists. ## How you use it Your hook is handed the bus. It is the third argument, always: ```lua function M.update(ctx, st, ck, dt) entity(st.id).localPosition = ctx.at(0, ck.seedY, 0) -- yes -- local phase = t / 1.25 -- no. never. end ``` `build(ctx, st, ck)` and `enter(ctx, st, ck)` get it too, evaluated at t = 0. To read it outside the update chain, `asc_clock.current()` returns the last published bus (`nil` before the part's first frame). ## The bus — the brief's §3, and nothing else Top level is exactly the §3 contract. Every value is hard-clamped to its stated range; a curve can be retuned but it cannot escape its bounds. | field | range | what it is | |---|---|---| | `t` | 0 … 35 | part-local seconds | | `bar` | 0 … 28 | `t / 1.25`, float. `floor(bar)` is the bar index | | `phase` | 1 … 7 | `floor(bar/4) + 1` | | `sunElev` | 4 … 12 | degrees above horizon | | `sunAzim` | 118 | degrees. Constant for the whole part | | `sunInt` | 0 … 12 | sun intensity, engine units. Already multiplied by `worldLight` | | `sunColor` | `{r,g,b}` | linear | | `skyMix` | 0 … 1 | 0 = full daylight gradient, 1 = flat void black | | `depth` | 1 … 0 | camera submersion. 1 at y = −4.2, 0 at y ≥ 0, crosses 0 at t = 5.000 exactly | | `alt` | −4.2 … 900 | camera altitude in units | | `rise` | 0 … 1 | column raise envelope. 0 before 2.50, 1 at 12.50 | | `colTopY` | 0 … 900 | column top height, units | | `strike` | 0 … 1 | bolt envelope. Impulses on the resolved cue and at +3 / +6 beats, 12-frame decay each | | `flicker` | 0 … 1 | residual column glow, 11 Hz, live from the strike to 25.0, amplitude 1 → 0.25 | | `ignite` | 0 … 1 | fire coverage on the column's top 200 u. 0 → 1 across 25.000 → the opening | | `apex` | 0 … 1 | seed-opening envelope. 1 on the opening cue, 0 by 27.500 | | `ringR` | 0 … 600 | additive shell radius in units. Passes the locked camera (116.3 u) at t ≈ 26.9 | | `seedY` | −4.2 … 780 | seed world Y, units | | `seedInt` | 0 … 2000 | seed point-light intensity | | `worldLight` | 1 … 0 | **global light authority.** 1.0 until 27.5, 0.0 at 30.0, stays 0 | | `expEV` | −1.4 … +1.2 | exposure offset, EV | | `cue` | integer | index into the composed cue track: how many cues have passed | | `band` | `{sub,low,mid,high}` | STFT band levels, 0 … 1, raw | | `flux` | 0 … 1 | spectral flux, raw | `worldLight` is the most consequential number in the part. **Every light you create multiplies by it**, including yours. It is how the seed's opening takes the daylight with it, and from t = 30.000 nothing in the scene is lit — only emissive. ## `aux` — §5 / §6 values, not the §3 contract Several aspects would otherwise re-derive the palette and the light table by hand and drift apart. Those live under `ck.aux`, which is derived, documented here, and equally a pure function of `t`: `sunDir` · `skyZenith` · `skyHorizon` · `skyMid` · `skyVoid` · `skyCeil` · `horizonTight` · `midUp` · `zenRate` · `antiSun` · `sceneExposure` · `gradeSat` · `gradeBri` · `ambInt` · `ambColor` · `bloomThreshold` · `bloomContribution` · `letterbox` · `phraseMix` · `phraseFrom` · `phraseTo` · `strikeT` · `openT` · `cueKind` · `cueT` · `globalT` · `handoffNdc` · `handoffHex` · `barNum` · `pointAmp` · `quality` · `autoLadder` · `frameMs` · `gpuMs` · `shadowStrength` · `sunDiscScale` · `emberLights` · `dustLitByFire` · `typeReveal` · `typeGlow` · `torTrack` · `torTrackN` · **`torSpinAccum`** · **`torTear`** The last two are TORNADO's amendments, added under §8's append-only right and read by that aspect alone. `torSpinAccum` (0…27.5) is **∫ `torSpin` dt in REVOLUTIONS** — the rotational twin of `windAccum`, and it exists for the same reason. `torSpin` is an angular RATE, and a fill shader handed a rate has to integrate it to get an angle; integrating per frame would make every seek a different tornado, which §5 forbids in as many words. It is a cumulative table built once at module load and sampled at `t`, so the column's rotation is a pure function of `t` and a held frame is byte-identical however often it is captured. `torTear` (0…1) is the collapse's loss of **axis**, which `ignite` does not carry. `ignite` falling 1.00 → 0.25 → 0 across 25.0 → 28.2 is the fire going out TOP-DOWN — the same fire front that swept up on ignition, sweeping back down. `torTear` is the other half: how far the three flame ropes have separated from the axis and drifted downwind. The two are genuinely independent, and `ignite` alone cannot tell t = 12 (rising through 0.1) from t = 27.8 (falling through 0.1), which are not the same picture at all. `skyVoid` is the colour `skyMix` folds the gradient **toward**. §3 calls skyMix's far end "flat void black" without saying what black; §5 rule 2 does: "Every dark value carries blue. Void is `#03080C`, never `#000000`, until t = 33.75." So the void is on a curve — §5 P4's flat `#060A12` while the world still has light in it, `#03080C` once the world is out, real black only across t 33.75 → 34.00 — and any aspect that folds something toward the void folds it toward this, not toward a constant of its own. `skyMid` is the crossover colour §5's two ends need between them, solved by `asc_palette` against rule 1's two failure conditions. **An aspect drawing its own sky-adjacent medium should read this rather than average the two ends** — averaging them is what puts a neutral on screen, and the proof is in `asc_palette`'s README. ### The sky rows are DISPLAY values. Everything else on the bus is linear. `skyZenith` / `skyHorizon` / `skyMid` / `skyVoid` are **sRGB display fractions** — exactly what §5 wrote — and `M.C` / `sunColor` / `ambColor` are **linear**. That split is not an inconsistency, it is the two kinds of surface: **LIT surface → linear.** Use `M.C`, or `Palette.hexL("#…")`. The render and the tonemap decide what the value becomes, which is the point of authoring in linear, and §5's header ("convert to linear at load") is the whole instruction. **EMISSIVE surface that fills frame → display.** A sky dome, a volume's emission term, a full-screen pass: §5's hex is what the pixel must READ AS, and getting there means inverting the post chain. Do not hand-roll that. `asc_palette` owns it: `ceilFor`, `invAces`, `unSaturate`, and the ramp. The dome shader (`preseed/shaders/asc_sky`) is the worked example — it blends in display space and applies the inverse once at the end. Three numbers on the bus exist so a consumer can do that without re-deriving anything: - **`sceneExposure`** — everything multiplicative between a scene value and the tonemap's input (`BASE_EXPOSURE · 2^expEV · gradeBri`). Divide by it. - **`gradeSat`** — the colorGrade pass's saturation, to un-apply. - **`skyCeil`** — the brightest display value an emissive sky-scale element may ask for, derived from §2's bloom threshold and this frame's exposure. §6 lets exactly one element per bar exceed 1.0 linear and the sky is never it. Scale the **whole triple** to fit the ceiling, never each channel — a per-channel clamp pulls the brightest channel toward the others and walks the colour off its hue family. Feeding a dark palette row as plain linear loses it outright: ACES's slope near zero is 0.03/0.14, so `#060A12` arrives at 0.0018 and leaves at 0.0004, a ninth of one 8-bit step. Measured, that came back 82 % literal `#000000`. ### The palette gate `asc_clock.checkPalette()` replays the dome's blend law against the triples the dome actually receives at each of the sixteen judged timestamps and reports §5's two hard rules as numbers. Run it after changing any curve that touches the sky. | detector | budget | now | |---|---|---| | magenta family (green below both others) | 0.00 % | 0.00 % at all 16 | | (max−min)/max < 0.25 | ≤ 2 % | 0.00 % at 11, 1.09–1.39 % at 5 | | (max−min) ≤ 20 | ≤ 2 % | 0.00 % at 11, 0.39–1.80 % at 5 | The magenta budget is zero because that family is exactly avoidable. The low-saturation budgets are not zero and cannot be: §5's two ends have a display-space chord that passes within one unit of the achromatic axis, so some sample on any continuous ramp between them sits near neutral. `asc_palette`'s README carries the arithmetic and the on-screen figures, which are larger than the function's because a level camera compresses many screen rows into a small range of elevation near the horizon. `phraseMix` is the phrase-boundary cross-fade: it runs 0 → 1 across the 0.75 s centred on each boundary (t = 5, 10, 15, 20, 25, 30) and reads 1 everywhere else, so a grade owner can lerp `phraseFrom` → `phraseTo`. `M.C` on the module is §5's palette, authored in sRGB hex and converted to linear once at load. Use it rather than re-typing a hex string. ## The two cue-fired events The strike and the seed's opening fire off the **score**, not a timer. `resolveCues()` snaps their nominal beats (15.000 and 26.250) onto the nearest cue within ±0.30 s, once, at part build, preferring the kinds that carry a downbeat. Against the current score they resolve to a `kick` at 15.000 and a `motif` at 26.250 — exactly on the beat, and now genuinely tied to the audio: if the composer moves the drop, the image moves with it. The envelope around the resolved instant is a pure function of `t`, so a held preview frame stays byte-identical however many times you capture it. ## The ENDCARD handoff `asc_clock.handoff()` returns everything that crosses the part boundary: ```lua { ndc = { 0.0, 0.060 }, hex = "#FFF0C8", color = { … linear … } } ``` Two numbers and a colour. No buffer, no texture, no starfield. ## The debug overlay Off by default. It prints the whole bus on screen, live, so you can check your curves against the spine's. `require` is environment-local, so an `execute` call gets its own copy of this module and cannot reach the running part's — arm it through the engine-global request instead: ```lua require("preseed.asc.asc_clock").requestOverlay(true) -- and (false) to clear ``` The part picks it up within a quarter of a second. Once it is up, `ui.getScreenTree("asc_clock_bus")` reads the printed values back, so the overlay is a readout as well as a picture. It costs no measurable frame time while on, and nothing at all while off. ## §10's cut ladder — read `aux.quality`, never decide for yourself §10 fixes six cuts in a strict order and assigns them to no aspect. There is **one** position, it is on the bus as `ck.aux.quality` (0 = nothing cut, 6 = every rung spent), and the rule is: if your system is named at or at a lower rung than the live position, drop to your cut setting. Nobody negotiates and nobody measures their own cost. ```lua if ck.aux.quality >= 3 then steps = 30 else steps = 40 end -- rung ③ ``` `M.CUT_LADDER` is the ordered list. The position moves **automatically**, and it moves on the only signal that is both correct and affordable. **Not the frame delta.** Wall-clock frame-to-frame time is work plus however long the engine waited to present, and on this machine that is 99 ms of present/idle vsync wait around 4 ms of real work. An earlier pass drove the ladder from it, spent all six rungs within seconds of boot and never gave one back, because the release threshold is a frame time a compositor-throttled window cannot reach. `profiler frame`, the tree that does separate work from pacing, costs 39 ms per sample — twice the budget it would protect. **`profiler gpu`.** It reads resolved GPU **timestamp queries** out of a ring the renderer already fills, is compositor-independent by construction, and measures at **0.21 ms** per call. Sampled once every 30 frames it costs 3.5 µs a frame — 0.02 % of budget. Its header line carries the attributed pass total, which is the number §10's own table is written in. The ladder arms on a five-sample median at or above **15.5 ms**, spends **one** rung at a time in §10's order, and gives a rung back only after four consecutive samples at or under **13.0 ms**, so it cannot oscillate. Both numbers are on the bus: `aux.gpuMs` (GPU timestamps, the ladder's signal) and `aux.frameMs` (wall-clock, including vsync wait, labelled for what it is). `asc_clock.setQuality(n)` still overrides by hand, and `setAutoLadder(false)` pins it — a measurement can be wrong and a director cannot be argued with. `asc_clock.sampleCost()` takes one sample on demand and returns `{ gpuMs, frameMs, quality, auto, ladder }`, for the moment a cost claim has to be backed by an artifact rather than by prose. **No rung on §10's ladder touches §10's never-touch list, and that is checkable rather than asserted.** The list is the column's LIGHT march, camera motion blur, the type's legibility and the sun's shadow cascades. The six rungs are the fire volume's resolution and step count, the ember point lights, the column far shell's VIEW steps, the cloud deck's step count, the SPH particle count and DOF quality. The two column entries are different marches. ## World conventions Published on the module so nobody re-types them: `BAR` `BEAT` `DURATION` `BARS` `SUN_AZIM` `LETTERBOX` `COL_BASE_R` `COL_WAIST_R` `COL_WAIST_Y` `ANVIL_R` `ANVIL_Y0` `ANVIL_Y1` `COL_TOP` `SEED_LEN` `SEED_START_Y` `SEED_TOP_Y` `CAM_TOP_Y` `CUT_T` `MAX_POINT_LIGHTS` `MAX_PARTICLES`. ## What `expEV` is for §3 gives it the range −1.4 … +1.2 and §4 fixes two keys: 0 at t = 27.500 and −1.4 four bars later. What the positive headroom is FOR is fixed by §6 and §2 together, and it is not a look. §5's bright phrases ask the sky for `#FF9A5A` and `#FFC98A`, whose brightest channel is display 1.0, and the scene-linear value that lands on display 1.0 is 7.24 — five times §2's bloom threshold. Exposure is what divides the value the sky has to ask for: at `expEV = +1.2` the horizon arrives at 1.35 linear, under the threshold, and reads within 4/255 of its hex. At `expEV = 0` the ceiling lands 27/255 short and §5's row is unreachable. So `expEV` is high exactly where §5's palette is bright — +1.2 across the breach (t 5–12.5) and the breakout (t 23–26.25), 0 through the blackout, down to −1.4 as the world goes out. **It is also the part's only real exposure move**: the sky cancels it (the sky lands on §5 whatever it does), so from the blackout's 0.0 to the breakout's +1.2, every element that is not the sky gets 1.2 stops brighter. That is §8 H4's "the eye should have to adjust", as a number. ## Owns - every scalar on the bus and the keyframes behind it - the STFT binding and the cue-track resolution - the phrase-boundary cross-fade — both the weights and the values (`aux.gradeSat` / `aux.gradeBri`), because `aux.sceneExposure` is `gradeBri` times the tonemap's exposure and two files cannot own half a product each - the §5 palette → display seam (`asc_palette`) and the gate over it - the ENDCARD handoff - the debug overlay - §10's cut-ladder position and the GPU signal that moves it ## Does not own Any visual element. This module spawns nothing and draws nothing but the overlay.
▲ 0↑ born
▣
module · born here
❒asset
# volume A module script.
▲ 0↑ born
·
computeshader · born here
❒asset
# asc_pyre ASCENSION's fire tornado, filled one invocation per voxel into a 128 × 256 × 128 emissive volume and raymarched by `preseed.volume`. ## The rotation Three things make the column read as turning, and all three have to be present. 1. Every noise field is sampled in a cylindrical frame whose angular coordinate is advanced by the accumulated spin in revolutions, so the surface moves between frames. The advance is **differential** — faster at the inner radii — which is the vortex's own shear. 2. The frame is **twisted with height**, so every feature is a helix in a frozen frame: at the waist radius a feature leans about 50° off vertical, and the lean grows with radius so the wind tightens toward the silhouette edge. 3. The noise is strongly **anisotropic** — five times longer along the axis than around it *before* the twist — because shearing something already long is what produces a long thin diagonal ribbon rather than a slightly tilted lump. On top of those, an **integer number of angular lobes** carries the read through the raymarch. A volume is integrated along a chord that crosses many azimuths, and the average of a helix is not a helix; a `sin(N·a)` term survives it, is continuous across `atan2`'s branch cut, and — evaluated on the outer shell, where a dense medium's near wall carries the radiance — arrives on screen as the banding the eye reads as a wound surface. The spin angle arrives as a cumulative table sampled at `t`, so the whole fill is a pure function of `t` and a held frame is identical however often it is taken. ## The profile The **body** is monotone-narrowing root to tip: a root swell that has decayed by the bottom 30 u (an absolute height, not a fraction of the column's), the waist, then a steady thinning to two thirds of it at the top. The **skirt** is a separate wide, low, ragged, wind-blown term in the bottom 26 u and never enters the body's silhouette. The **anvil** is a separate cap whose radius is capped *below* the root swell, offset downwind by more than half its own radius and stretched along the wind — a sheared anvil rather than a radially symmetric cap. ## Fire, dust and the collapse `ignite` is the fraction of the column height that is burning, measured from the root, so one scalar carries both the 0.30 s root-to-tip ignition and the top-down collapse. Below the front the same volume is emissive **hot dust**, brighter than the murk behind it rather than a hole in it; the anvil is always dust with an amber belly where the fire below reaches it. `tear` throws the three ropes off the axis on 120°-separated bearings, lifts them into their upper reach, and kills everything else; the ropes carry their own fire so they go out as flame rather than as smoke. ## Colour `cLow` / `cMid` / `cUp` / `cCore` / `cDust` / `cSkirt` arrive as **scene-linear radiance** that the engine's ACES fit maps onto the accent row, scaled by the display ceiling the post chain delivers and divided by the frame's own `sceneExposure`. Dimming is a **mix toward a darker authored ember**, never a multiply: the tonemap is compressive in red and near-linear in blue, so a scaled `#FF7A22` comes back a scarlet and a ramp authored in display ratios comes back neutral. Nothing here is white; the hottest value is `#FFF0C8`. ## Density Density is occlusion, and the composite is `scene*(1−a) + rgb`, so a term carrying more density than emission is a hole in the frame. The lanes between the angular bands go to **zero**, not to a floor, so the scene behind shows through and the frame keeps something dark to read the fire against. Density fades out over the last 7 u above the sand, so the column dies *into* the surface instead of ending on the volume's floor slab, and it fades around the camera — tightly while the column is whole, widely during the collapse, when the crane is inside the box. RGB is emission, alpha is density. `U` carries 41 f32s and the index list is in the shader's header.
▲ 0↑ born
backing path · preseed/asc/asc_tornado.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.