LUMENFORGE v1.4
A performance release, built against a real scene from the field. HeroRay costs less than half of what it did, and nothing on screen moved.
In one screen
[TD colspan=2]mean difference 0.000/255 , 99th percentile 0.86/255 (8K per-pixel luminance)[/TD]
v1.3 v1.4 change HeroRay, factory settings (two-person field scene, 8K, over the empty floor) +10.45 ms +4.43 ms β58% HeroRay at 24 steps, old default kept +10.45 ms +7.53 ms β28% HeroRay, Light casts shadows OFF (new opt-in) β β +3.7 ms β65% The whole field scene, all three Forge plugins at factory settings 36.35 ms Β· 27.5 fps 12.53 ms Β· 79.8 fps β65% (with SteamForge v1.2 and GlassForge v1.1) Picture, old 24 steps vs new 12 steps identical
Defaults changed : Beam steps 24 β 12. Scenes that saved their own value keep it. Everything else is either identical or opt-in.
Changelog
Plugin Change What you see HeroRay Beam cone drawn one-sided ( Cull Backinstead ofCull Off)Nothing. The near face's ray ended before the shaft began and contributed nothing; it cost a full march per pixel. HeroRay Analytic cone entry : the march starts where the camera ray enters the frustum, not at the camera Nothing at equal steps; it is what lets 12 steps equal the old 24. HeroRay Per-segment exact in-scatter ( trΒ·(1βexp(βΟΒ·ds))) replaces the left-Riemann sumBrightness no longer depends on the step count. A dense beam at 24 steps reads about 2% darker than v1.3; thin beams unchanged. HeroRay Beam steps default 24 β 12 (slider still 4β48) Same picture, half the cost. HeroRay New switch : Light casts shadows (ships ON) ON is v1.3. OFF : no shadow map, the person casts no shadow from this light, the beam has no silhouette hole. About 0.8 ms in the field scene, ~3 ms in isolation at 8K. HeroRay Shadow capture is a 32-bit float texture ( RFloat) instead of the 8-bit ARGB32 defaultThe beam used to read the light's depth quantised to 256 levels across its whole range, which is where the ring acne at silhouettes came from and why the auto near plane and a large bias were needed. Same 4 bytes per pixel, so nothing about the cost moves; the silhouette hole's edge is cleaner. Field-checked at 8K, camera inside and outside the shaft. HeroRay Shadow tap uses an explicit LOD ( tex2Dlod)Nothing. Removes a d3d11 gradient-in-loop warning that could force the compiler to flatten the out-of-cone branch on some drivers. HeroRay Status text is readable by other plugins ( status, not saved)Nothing. Bench tools can verify which shader build is live. SkyRay Β· EnvRay Β· HeroRay Β· CUARay Capture rig mirror guard uses MirrorReflection.globalEnabledinstead of flipping each component'senabledFixes a hard crash : SkyRay LIVE plus a reflective atom rebuilt the reflection surface every frame until VRAM ran out. Mirror image unchanged. PerfMeter (tools) Five field-scene sweep tables, an ATOM=@on:0/1row switch,read_sweep.pyto fold a run and apply pre-written criteriaNothing in play; the measuring kit that produced this page ships with it.
Where it came from
A creator published a bathroom scene built on SteamForge, LumenForge and GlassForge, and a player reported 30 fps on a Radeon 7900XT with the GPU pinned at 100%. Switching the hero light off alone gave them 50% more frames. That is not a configuration problem; it is a bill, and this release pays it.
Reading the beam shader against that scene found three things, in order of size :
- Two faces for every pixel. The cone mesh was drawn double-sided, so every covered pixel ran the whole march twice, and the near-face copy contributed nothing.
- Steps spent outside the shaft. The 24 samples were spread along the entire camera ray, so on a side view most of them landed in empty air.
- A shadow that could not be switched off. The real spot had Hard or Soft, never None, and its shadowed lighting pass over skin, hair, cloth and room was a third of the light's cost.
QA : the A/B ladder
Two persons, EnvFog, GlassForge pane, water. 8K DSR, RTX 5090, six passes of ten seconds per row with a four-second settle, VAM panel closed, camera pinned by the scene's own spawn point, the music-reactive light reactor switched off by the harness. Every row declares its full state; the first row of every pass re-pins the whole scene.
HeroRay alone, over the empty floor ms vs v1.3 default v1.3 : 24 steps, Cull Off, no clip (the shipped build, 2026-09-11 run) +10.45 β v1.4 build, switches at v1.3 behaviour +10.22 β0.23 (the new code costs nothing when it is off) + Cull Back only +7.27 β2.96 + analytic clip only +13.48 +3.26 : every sample now lands inside the shaft and pays its shadow tap and noise. Kept because of the next rows. Cull Back + clip, 24 steps +7.53 β2.69 Cull Back + clip, 12 steps (v1.4 default) +4.38 (+4.43 in the closing run) β5.84 Β· β58% Cull Back + clip, 8 steps +3.31 β6.91 Cull Back + clip, 4 steps +2.19 β8.03 v1.3 path at 4 steps, for reference +2.83 clip+cull is 0.63 cheaper even there Shadow resolution tier 3 β 1 β 0 +10.45 / +10.48 / +10.47 0.00 : not a performance dial Beam shadow gate off (beam samples no shadow) +9.61 β0.84 Light casts shadows OFF (v1.4, 12 steps) β +3.7 β0.77 measured in the full scene, applied to the isolated figure
The whole scene ms fps 2026-09-11 baseline, all three Forge plugins at their old defaults 36.35 27.5 HeroRay v1.4 alone (steps 12), the other two unchanged 30.10 33.2 All three at new defaults (LumenForge v1.4, SteamForge v1.2, GlassForge v1.1) 12.53 79.8 β¦ plus Light casts shadows OFF 11.77 85.0 Everything off (the floor) 7.0β7.5 β
The picture test. Two 8K screenshots per state, same camera, haze drift frozen, per-pixel luminance difference. The first attempt read the new path 4.6/255 brighter on a side view. That was not the clip : it was the old accumulation's step-size bias (the left-Riemann sum over-reads by about ΟΒ·ds/2, so fewer steps had always looked brighter). Integrating each segment exactly made the two states identical : mean 0.000/255, 99th percentile 0.86/255, both with the camera inside the shaft and outside it.
What did not go to plan, and is recorded :
- The one-sided mode is Back, not Front. The cone mesh is wound inward; Front made the beam vanish on the first try.
- A 0.5-second dry run read the new build 2.5 ms slower than the old one at 24 steps. The six-pass run put them 0.18 ms apart. Dry runs answer "did anything move", not "by how much".
- The SkyRay LIVE crash fix was re-run in the reporter's configuration (SkyRay LIVE plus a reflective atom at render queue 3000, 60 seconds) : no crash, steady VRAM, mirror image unchanged.
The field scene, every sweep table, the raw rows and the A/B screenshots are archived underdev/logs/wet_2026-09-11/in the package. Nothing here was estimated.
Updating scenes you already published
Two things carry over from an old scene and are worth a minute :
- The package reference. A scene saved against
Shadow Venom.LumenForge.4keeps loading v1.3 even after you install v1.4, because VAM pins script references to the exact version. Re-point the plugin toLumenForge.5in the atom's Plugins tab (or re-add it), then save.- An explicit Beam steps value. Scenes that never touched the slider get the new 12 automatically. Scenes where you set it (24, 48 β¦) keep that number.
Every ForgeLab package now shipsForgeLab_Updater.cslist, a temporary tool for the second point : load it on an Empty, press Scan, read what it found, press Apply, save the scene, remove it. It moves EnvFog Quality and HeroRay Beam steps from the old factory values (24 and above) onto the new ones, only on instances that are already running the updated packages, and leaves anything you had set below the old factory value alone. Undo restores the session's values, and a log is written toSaves/PluginData/ForgeLab/.
Known limits
- Light casts shadows OFF changes the picture : the person casts no shadow from the hero light and the beam has no silhouette hole. That is why it ships ON.
- The remaining cost is the real light. About 3 ms of the 4.4 is Unity's own shadowed spot over everything in range; the switch above is the only cheaper version of that.
- Aux cameras. A GlassForge reflection render or a VAM mirror draws the beam again at its own resolution. This release does not gate that; the glass transmission side is covered by GlassForge v1.1.
v1.3 built the outdoors.
This one goes back to the first fixture and makes it cost what it should have.
LUMENFORGE v1.3
A ninth plugin, and it is the outdoors. Full-screen sun and moon shafts, carved by the scene you actually built.
![]()
In one screen
What changed What it gives you New : SkyRay
Volumetric shafts across the whole frame, with your scene's own geometry cutting them. Stand in a forest and the leaves cut the light where the leaves are. Sun or moon
Nothing in it is solar. Set the colour cold and dim and it is moonlight, same code, same cost. Cloud shadows without a sky
A skybox never enters a depth map, so painted clouds cast nothing. Sky mask is an independent layer with coverage, feature size, softness and drift. SkyRay ships its own measurement document
PERF_SkyRay.md, inside the package : conditions in full, every row, the caveats, and a 10.5 hour soak.EnvRay is now named as the indoor half
Nothing about it changed. The page just stops asking it to be two things. Reviewed before release
Two independent code audits, two re-verification rounds, a save-load round trip and five field checks, all before this went out.
Nothing was removed, no existing setting changed its meaning, and your saved scenes are untouched. The only behaviour change outside SkyRay is a one-directional correction inside SkyRay itself, described under Known limits.
Where it came from
v1.2 gave EnvRay a 15 metre beam radius so one shaft could cover an outdoor space. That was the right fix for the wrong shape, and it took a while to see why.
EnvRay is built around an opening. A window, a doorway, a skylight : a volume you place by hand, with a lot of local detail inside it and a defined direction through it. Widening that volume until it covers a courtyard makes it bigger. It does not make it the right thing, because outdoors the light does not arrive through an opening at all. It arrives from everywhere above, and what shapes it is not a frame you positioned but every tree, rock and roofline that happens to be standing there.
That is a different problem, and it wanted a different plugin rather than a longer slider.
SkyRay : the whole sky, and the whole scene shadows it
![]()
What it does
It marches the entire screen and asks, at every step along every ray, whether the sun or the moon actually reaches that point. The answer comes from a depth capture of your scene taken along the light direction, so what carves the shafts is what is really standing there. Not a painted texture, not a noise field, not a cone you aimed.
Add an Empty, load SkyRay.cslist, set an azimuth and an elevation, thicken the air. That is the whole setup.
It aims by eye, and that is a decision rather than a gap
SkyRay binds to no light in your scene and reads nothing from one. That will look like a missing feature until you count how many outdoor scenes on the Hub are lit by baked lighting, or by an ambient colour, or by nothing at all : a plugin that demanded a Directional Light would simply not work in most of them.
So the direction is two sliders, and matching them to whatever is lighting your shot is a creative act. It is also the part nobody else can do for you, because only you can see the shot.
Put it on an Empty, not on a Person
The shadow capture is baked around a core box. Hosted on a figure that box follows her, so every step she takes re-bakes it, even under STATIC. The plugin's own Report warns you when you have done it, in words, rather than leaving you to wonder why a static scene is not static.
Sky mask is not your skybox, and it cannot be
A skybox is not geometry. It never enters a depth map. So the clouds you can see painted above your scene cast nothing at all, and no amount of work on our side changes that.
Sky mask is a separate layer of cloud shadow with its own coverage, feature size, edge softness and drift. It is deliberately not called Clouds, because it cannot match the sky you are looking at : it is its own weather. It also supplies the large-scale haze variation that stops an outdoor scene reading as uniformly foggy, which is most of what makes it worth switching on.
What it costs
Bench, 7680x4320, one RTX 5090, camera on open sky so 100% of rays are unobstructed, three passes of ten seconds per row with the VAM panel closed.
March steps Frame time Above the floor 512 (amplifier, not a setting) 28.43 ms 25.20 ms 256 14.92 ms 11.69 ms 128 8.07 ms 4.84 ms 48 3.44 ms 0.21 ms 24 (factory) 3.23 ms 0.00 ms empty scene, everything off 3.23 ms floor
0.0530 ms per march step. Linear to three decimals from 512 down to 128.
At factory settings it reads at the floor, and that is not the same as free
24 march steps with cloud shadows on measures 3.23 ms against a 3.23 ms empty-scene floor, in all three passes, with the mask verifiably running. It is tempting to print that as zero.
It is about 1.3 ms of GPU work sitting under 1.95 ms of headroom, on a frame that is CPU-bound in this bench. Absorbed, not absent. On a machine that is already GPU-bound, or in a scene heavy enough to have used that headroom already, those 1.3 ms are real and you will feel them. We would rather say that than let you find out.
The 1.95 ms absorption is not assumed. It is the intercept of the step ladder above, and it independently reproduces the 2.22 ms this same bench measured on EnvRay, a completely unrelated subject, two days earlier. Two subjects, one floor.
What is free, and what is not
Measured at 512 steps so the differences are readable at all.
Setting Reading What it means Penumbra 0 / 1 / 3 27.40 / 28.34 / 28.34 ms The second shadow tap costs 0.94 ms. Its width is free. Bake resolution 512 / 1024 / 2048 28.42 / 28.46 / 28.50 ms Free per frame. It buys shadow sharpness with VRAM. No reason to lower it chasing frames. STATIC / LIVE 28.50 / 28.75 ms 0.25 ms in this room. See the warning below. Framing : near / room / open sky 3.23 / 15.12 / 28.46 ms Cost tracks how much of the screen the shafts cover, not how big your world is. Reach 300 / 60 26.32 / 28.71 ms Longer reach is cheaper : bigger cells make transmittance decay faster, so the ray gives up sooner. Haze density 0 / 0.02 / 0.3 27.75 / 28.48 / 28.48 ms Flat. The height falloff thins the air along an upward ray before the early-out can fire.
β The one number on this page that does not transfer
LIVE re-renders your whole scene into the shadow capture every frame. Its cost therefore scales with how many objects you have, not with anything on SkyRay's panel. It measured 0.25 ms in a bench room; a dense forest will be far more than that, and no number we print here would tell you yours.
STATIC is the default for exactly this reason. Use LIVE only when the scenery itself moves.
One finding worth passing on, because it is not only ours
Halfway through pricing the cloud shadows, the same configuration started returning two different answers several milliseconds apart. It was neither.
Toggling a shader uniform that gates a branch makes the GPU driver throw away its specialised version of that shader. The frame gets several milliseconds dearer, and it does not come back when you toggle the switch off again. It clears only when the driver rebuilds its specialisation, which nothing on our side can trigger.
Isolated with identical probes on both sides of a single toggle : 27.99 ms before, 35.17 and 35.22 after, 28.15 once other work had run. It tracks the toggle row rather than the position in the table, at 19 seconds into one run and 4 seconds into another. The feature being toggled costs 0.02 ms by itself, so it is not the work. It is the act of switching.
Measured with that designed out, using two complete ladders with exactly one toggle between them and settling on both sides, the cloud shadows cost 0.00503 ms per step, about 2.57 ms at 512.
Which means the sky mask has two costs, and both of them are true.
What you did this session At 512 steps The mask appears to cost Never touched the switch 28.4 ms baseline Switched it on at some point 36.3 ms +7.9 ms Both halves on an already-generic shader 33.7 to 36.3 ms +2.6 ms
At the factory 24 steps every one of these reads at the floor, so it changes nothing about how you should use it. It is on this page because if you run high step counts and compare notes with somebody else, this is why two people can measure the same setting and honestly disagree.
Ten and a half hours, unattended
634 one-minute buckets, deliberately not at shipping settings : 128 march steps, LIVE re-baking every frame, cloud shadows drifting. The night answers stability. Cost was already settled above.
Frame time drifted +0.018 ms across 10.5 hours. p95 drifted +0.032 ms, so it tracked the mean : no stutter grew in, which is the shape a soak exists to catch.
No leak. The managed heap saw-tooths as any Unity app does, so the number that matters is the floor it returns to after each collection. It was 890 MB at the start of the night and 890 MB at the end. The floor is the thing a leak would move.
3 bytes per frame. Measured against the bench's own 119 B/frame floor : SkyRay at shipping settings reads 122, and at 128 steps 123, so the march itself allocates nothing.
State held. The same source flags, step count and capture size in all 634 buckets, and rendering advanced in every one of them. No re-specialisation step anywhere overnight, so the state we measured is the state it held for ten and a half hours.
The heap collects roughly every 65 minutes and about seven of those ten collections carried a frame near 150 ms. That is VAM's garbage collector, the same one documented on the main page, and SkyRay's contribution to reaching it is 3 bytes a frame.
Known limits, stated rather than claimed fixed
- Under a heavy sky the far distance is slightly darker than perfect. Past the wash-out distance the shafts blend into flat haze at the sky's average brightness, and SkyRay estimates that average rather than measuring it. The estimate runs dark : at 50% cloud cover the distance is about half as bright as a perfect one would be. It is capped so it can never come out brighter than the near field, which is the way round that reads as wrong. If you want more distant glow, raise Reach or push Wash-out starts at further out. The correct fix replaces the estimate outright and moves the picture at every cloud setting, which is not something to do three days after a code review.
- Cloud drift does not survive a save. A reload starts the clouds at their origin rather than where they had drifted to. Clouds are weather, and that is the intended behaviour rather than an oversight. If you need a fixed sky, set drift to 0.
- The Ray plugins share Unity's camera depth texture and hand it back together. If a third-party effect that also needs depth is switched on after them, and they are then the last of the group to be switched off, that effect can lose depth until it next asks for it again. Most re-ask every frame and you will never see it. Line-wide behaviour, not specific to SkyRay.
What we did not change
- EnvRay. Not a line of it. It is described differently on the main page because it is one half of a pair now, and describing something accurately is not the same as changing it.
- CUARay stays experimental. Same reason as v1.2 : an experimental tier should not have its factory values tuned as though it were settled.
- The shared capture rig. Two things about it are known and neither is repaired here : LIVE's cost scales with scene complexity, and a large capture volume degrades what SMART can save. The honest fix is a change inside a class that three shipped products depend on, and making it unreviewed on a release day is a worse risk than the thing it fixes. Filed, not forgotten.
- An optimisation we wrote and then removed. Skipping work for samples whose contribution is about to be multiplied away sounded free : "it costs one compare". Measured, it was a 7% regression, roughly five hundred times more expensive than the work it skipped. A branch on 100% of steps, wrapped around a texture fetch, is not free on a GPU. It is gone.
Reviewed before release
SkyRay went through two independent code audits and two re-verification rounds before this page existed, followed by a save-load round trip and five field checks.
The part worth reporting is not that they found things. It is what kind of things:
- Defects inside fixes. Two of the first round's findings were bugs in repairs for earlier findings, including one where a comment described the intent and the code beneath it did the opposite.
- The far-field limit above was found by writing down the acceptance criterion. A reviewer asked us to check that a heavily clouded distance does not read brighter than the near field. Turning that into a number rather than a look showed the estimate was wrong, and wrong in the direction that reads as a defect. The check found it; the eye had not.
- One check could not have failed as written. Another asked us to verify that a wall a metre away still shows haze rather than black. At factory air density a one-metre path scatters about 2% of the light : the correct frame and the broken one are both nearly black. It was rebuilt as a comparison between two step counts against the same wall, which either matches or does not, and it matched to within one least significant bit across 3.4 million pixels.
Every test that was run, and what it returned
Test What it asked Result Instrument check Can this bench read the differences it is about to print 0.09 ms spread across 9 rows spanning 24 table positions
Step ladder Is cost linear in march steps 0.0530 ms/step, linear to three decimals 512 down to 128
Factory settings Is there a measurable cost at 8K 3.23 ms against a 3.23 ms floor, all three passes
Penumbra Does a softer shadow edge cost more The second tap costs 0.94 ms. Its width is free
Bake resolution Does sharper shadow capture cost frame time 0.08 ms across 512 to 2048. Free per frame
Update strategy What does re-baking every frame cost β +0.25 ms in this room. Scales with your object count, not with the panel Framing Does cost follow screen coverage 3.23 / 15.12 / 28.46 ms for near / room / open sky at one step count
Reach Is a longer march dearer No, cheaper : 26.32 ms at 300 m against 28.71 at 60 m
Haze density Does zero density short-circuit the march Flat, and documented as such rather than gated
Sky mask cost What do cloud shadows cost per step 0.00503 ms/step, two ladders with one toggle between them
Shader de-specialisation Why did one configuration return two answers The toggle, not the work : 27.99 to 35.2 to 28.15 ms
Cloud tile seam Does the cloud field wrap without a visible join Opposite edges agree to 2e-16, proved offline
Soak : drift Does it get slower over a night +0.018 ms across 10.5 hours
Soak : stutter Does a hitch grow in that the mean would hide p95 +0.032 ms, tracking the mean
Soak : memory Does it leak Heap floor 890 MB at both ends of the night
Allocation How much garbage per frame +3 B/frame over the bench floor. The march itself: none
VR Does it match desktop in a headset Identical, on the shipping build, in a moonlit forest
Load the shipping build Do twenty commits of review fixes still load and render Bundle resolved, lazy cloud bake correct, no rebuild
Save and reload Does every parameter come back All 34, floats within 1e-6. Diagnostics correctly not restored
Disabled plugin Can a trigger build a frozen box while it is switched off No, by three routes, and the work resumes on re-enable
Master off Does the Report say it is down Every source flag clear, volume released
Overcast horizon Does the far field read brighter than the near field Defect found at high cloud cover. Capped, and written up under Known limits
Wall one metre away Does a ray that ends inside the first march cell still draw 99.12% of 3.4 million pixels bit-identical to a 512-step reference
Teardown order Does unloading one Ray plugin break another Reference count correct across add, remove, and both orders
Twenty-four tests, one defect, and it is on this page rather than in a changelog next quarter. Two of them are worth reading twice : the wall test could not have failed as first written, and the horizon test found something the eye had not.
Everything they found is either fixed or written down as a limit. Nothing was closed by agreeing with it.
How it was measured, and where it failed
- Three passes of ten seconds per row, with a four second settle, every setting written by the harness rather than by hand. Instrument quality on the run of record : 0.09 ms spread across nine rows spanning 24 table positions, and two identical configurations seventeen rows apart agreeing to 0.09 ms.
- The panel was closed. VAM's own open plugin panel costs roughly 11 ms a frame. Measuring with it open measures the panel.
- The rehearsal is not optional. Every table was dry-run first and read before the real pass was allowed to start. That is what caught the run below.
- A whole 23-row pass was thrown away. The table's first row did not reset one switch, so twenty-three rows were measured with cloud shadows on that were supposed to be off. It was fixed at the class rather than in the table : every control now enrols itself for reset when it is created, so a control added next year cannot be forgotten.
- The cloud shadow cost took three answers before it was right, and that is recorded rather than tidied away. Three answers to one question means the question was under-specified, which is exactly what it turned out to be : see the de-specialisation section above.
- The numbers were taken on the development build. The march changed afterwards, in the fix for a reviewer's finding : the per-cell step count now rounds up rather than down, and a partial last cell is integrated over its true length. Sample positions along the ray are unchanged for any ray not cut short by geometry, which is every ray in these open-sky rows, so the figures stand. Two reviewers checked that argument rather than accepting it.
- One later change only makes LIVE cheaper : the volume no longer marches itself into the shadow capture, once per bake. The 0.25 ms above is therefore an upper bound. It has not been re-measured, so it is printed unchanged rather than adjusted by an estimate.
Nothing here was estimated. Where a number could not be measured honestly it is not on this page, and where the instrument ran out we said so instead of extrapolating past it.
v1.2 made EnvRay's beam wide enough for the outdoors.
This one admits that was the wrong shape, and builds the right one.
LUMENFORGE v1.2
A quality-of-life and performance update. The same picture, a lot faster, and three controls that used to be out of reach.
In one screen
What changed What it gives you Raymarch steps now ships at 24 instead of 48 (EnvRay and HeroRay)
A 46% shorter frame with the shaft filling the screen, and no visible difference. Measured both ways. HeroRay: Aim smoothing is a slider now
Tracking can be as sharp or as soft as the shot needs. 0 = instant, for a light bolted to something that moves. EnvRay: Beam radius to 15 m (was 4)
One shaft can cover an outdoor space instead of a window. EnvRay: Bake resolution (512 / 1024 / 2048)
Keeps a wide beam's shadows sharp. Costs VRAM and nothing per frame. SMART now prints the corridor it is watching
You can see what the strategy is actually doing instead of guessing.
Nothing was removed, no setting changed its meaning, and your saved scenes are untouched. Every slider keeps the range it had; only the value a newly added instance starts at has moved.
Where it came from
A creator working on real scenes asked for two things, and both were the same kind of ask: a value we had chosen for him that he wanted to choose for himself.
HeroRay's tracking carried a fixed amount of smoothing, tuned to read as a follow spot operator. It is a good default and a bad law, and he had a shot that wanted the opposite. EnvRay's beam radius reached 4 m, which is the right range for a window and the wrong one for a beach.
Opening the first of those led somewhere neither of us expected, and that turned into the main item on this page.
Raymarch steps now ships at 24 instead of 48
The problem, stated plainly
Every marching plugin in this line ships its quality knob at the slider's top : EnvRay at 48, HeroRay at 48, CUARay at 128 slices. That is the safe way to ship something whose look has never been priced, and it is the wrong place to leave it once it has been. Maximum quality had been standing in for "correct", and this update replaces the guess with a measurement.
What it costs
Bench, 7680x4320, one RTX 5090, shaft filling the frame, median of six passes:
Raymarch steps Whole frame fps Frame time it added The shaft's own GPU cost* 48 (old default) 10.86 ms 92 7.62 ms 9.84 ms 40 9.18 ms 109 5.95 ms 8.20 ms 32 7.53 ms 133 4.29 ms 6.56 ms 24 (new default) 5.86 ms 171 2.63 ms 4.92 ms 16 4.28 ms 234 1.05 ms 3.28 ms 12 3.55 ms 282 0.32 ms 2.46 ms
* Added frame time plus 2.25 ms. That is what this bench absorbs before anything shows up : the empty room is CPU-bound, so GPU work smaller than the slack changes nothing you can read. The figure is fitted from two completely different subjects measured on the same bench, whose lines came back with the same offset (-2.22 and -2.46 ms), and it is the reason rows at 8 steps and below report nothing at all.
Corrected for that, the shaft's cost is 0.205 ms per step. Exactly linear, with no offset left over.
What it looks like
Stepped through by eye, in several different real scenes, on EnvRay and HeroRay both:
Between 20 and 48 there is no perceptible difference.
Below about 18 it falls off sharply.
So the old default was charging four times over for a difference nobody can see.
What you actually gain
A 46% shorter frame in the bench above, where the shaft fills the screen. Your figure depends on how much of your frame the shaft covers, and that is something only you can see: a shaft crossing the whole view gains the most, and one over in the corner was never costing much to begin with.
Why 24 and not 20
The cliff is around 18. 24 keeps a third of a margin over it; 20 keeps a ninth. The failure modes are not symmetric: too high only spends performance you can see and turn down. Too low means the plugin looks wrong in somebody's scene and they cannot tell why. The scenes it was checked in are not all scenes.
There is also something non-obvious about that slider, worth knowing before you judge a value by eye. The number on it is not the number of samples inside the beam. The steps are spread along the whole camera ray, and only the ones landing inside the shaft do any work, so a framing that views the beam from outside spends a good share of them on empty air. Standing inside the beam is the one framing where the slider does equal the sample count, which makes it the framing that flatters a low value. Judge from outside, with something thin casting a shadow into the beam: fine shadow detail is the first thing to go.
The maximum is still 48 and nothing was taken away. Saved scenes are untouched, because a scene stores the value it was saved with : only newly added instances get the new default. If you preferred 48, drag it back and nothing on this page applies to you.
HeroRay : the damping is a slider now
Aim smoothing (s, 0 = instant), measured in seconds: how far behind the target the beam runs.
- 0.083, the default, is the feel the plugin has always had. Your existing scenes do not change by a single frame.
- 0 snaps every frame. This is the one for a light bolted to something that moves : a vehicle window, a swinging rig, an animation pattern you keep speeding up. There is no operator behind a moving window, so there should be no operator smoothing.
- Anything between is a spring you can dial.
Two things worth knowing about what that damping was actually doing. It only ever applied to the aim, never to the position : the light itself was always instant. That is why the symptom grew the faster the light travelled, because the faster it moves the more angle it has to cover per frame. And the old form drifted with framerate, so the same setting felt different at 60 and at 90. The new one does not, which matters most in VR.
HeroRay's steps default moved to 24 in the same way, and that was checked by eye on HeroRay directly. The millisecond figures on this page are EnvRay's : HeroRay's own rows would not move our bench, so we are not going to print a number for it that we did not measure.
EnvRay : the beam can be an outdoor space now
Beam radius goes to 15 m, up from 4. The default is unchanged at 1.5 m.
What we measured out there is not what we expected, so here it is:
Beam radius The shaft's share of the frame vs the row above 4 m 4.74 ms β 8 m 5.52 ms +16% 15 m 5.46 ms +0%
Cost saturates, and it saturates early. 4 m to 8 m adds 16%. 8 m to 15 m adds nothing at all, because once the shaft fills the frame there is nothing left to fill : the extra metres are free precisely because you are already paying the maximum. The jump you can feel is from a narrow beam to one that covers the view, and it happens well before the slider runs out.
β Past about 7 m our bench room cannot see any further : a 15 m beam does not fit inside a 14 m room, so those last two rows are measuring a beam larger than its container rather than a wider beam. That is where the instrument stops, and we would rather say so than extrapolate past it.
Bake resolution : its companion, and it is free
A wide beam bakes soft. The capture is an orthographic camera as wide as the beam, so texel density is resolution divided by twice the radius: 1024 over a 1.5 m beam is 341 px per metre, and over a 15 m beam it is 34. Bake resolution (512 / 1024 / 2048) buys that back.
Measured at all three: within 0.11 ms of each other. It costs VRAM and nothing else per frame, which is what you would hope for from something the baked engine only does when it re-bakes.
SMART, in a wide beam
SMART wakes while a person is inside the beam corridor, and that corridor is the beam. At 15 m radius the corridor is 30 m across and nobody is ever outside it, so SMART settles into behaving like LIVE.
We deliberately did not clamp it. A narrower corridor would stop carving a person standing off-axis inside a wide shaft, which is a visible defect and a worse trade than SMART saving less. Instead the status line now prints the corridor width it is actually watching, so you can see it rather than guess at it.
And measured at 8K with an empty corridor, STATIC, SMART and LIVE differ by 0.00 ms. The v1.1 page said the strategy dropdown was the wrong knob for framerate. It is measured now rather than asserted.
What we did not change
- CUARay's slice count stays at 128. It is the same "default pinned at the maximum" pattern, and it is staying there on purpose: CUARay still carries an EXPERIMENTAL banner, and an experimental tier should not have its factory values tuned as though it were settled. It is also a slice stack rather than a raymarch, so EnvRay's curve does not transfer to it.
- HeroRay's per-frame timing. There is reason to think the tracking could be up to one frame tighter still, but proving it needs a step-input test we have not run. Shipping a timing change on reasoning alone is how you get a regression nobody can reproduce.
How it was measured, and where it failed
- The same code-generated bench room as v1.1, camera snapped to numbered positions by the harness. Six passes of ten seconds per row, every setting written by the harness rather than by hand. The 48-step row was measured twice in every pass, once at the start and once after the whole ladder: 7.62 and 7.66 ms. That gap is the drift across the run.
- 7680x4320, forced from the command line rather than chosen in the launcher, because the launcher's choice failed to reach the player once before and cost a whole run at the wrong resolution.
- The step ladder ran first on purpose, because it is also the instrument's own calibration. It separates cleanly from 48 down to 12 and then closes completely at 8 and 4, so this bench cannot see anything below roughly 0.2 ms and nothing under that may be called free. That floor is measured rather than assumed, and assuming it is exactly what went wrong earlier the same day.
- It could not measure two of our own plugins. HeroRay's and CUARay's rows came back flat end to end. For CUARay the reason is known: the bench window is one metre square, so its slice volume covers almost no screen. For HeroRay it is not yet known. Those blocks produced no data, and this page prints no numbers for them.
Nothing here was estimated. Where a number could not be measured honestly it is not on this page, and where the instrument ran out we said so instead of extrapolating past it.
Thanks to the creator who asked whether the tracking could be sharper.
One question about a follow spot, and the whole line got faster.
Why this update exists
On the official VaM Discord, @AlphaCentauri reported a noticeable framerate drop from a baked EnvRay shaft in an apartment scene with no Person atoms in it at all, and asked whether he had set it up wrong.
He had not. Our documentation was wrong.
v1.0 described the baked engine as having "zero running cost". What is actually free is the light slot and the re-bake. The shaft itself is a volumetric raymarch and it is re-integrated every frame, from wherever your camera is, whether or not anyone is standing in it. Baking freezes the bake, not the beam.
That report is the reason for everything below. The wording is corrected, and then the thing the wording was wrong about got measured properly and rebuilt around what the measurement said.
What it costs now
Framing v1.0 v1.1 Frame time The shaft's own cost Shaft crossing the frame (typical) 13.51 ms 6.51 ms -51.8% 10.28 β 3.28 ms (3.1x) Standing inside the shaft (worst case) 23.13 ms 13.71 ms -40.7% 19.90 β 10.48 ms (1.9x) Shaft entirely behind a wall nearly full price 0.00 ms free 19.6 β 0.00 ms
The last row is the one that matters most in a real interior. A shaft in the next room used to cost almost as much as one filling your screen, because the volume was still being integrated behind the wall. It now exits before doing any work at all.
What that means for your framerate
The shaft got cheaper by roughly three times. Your framerate gain depends on how much of your frame the shaft was, which is something only you can see:
If you were at 60 fps and the shaft was... expect roughly 30% of your frame time ~75 fps 50% of your frame time ~91 fps 70% of your frame time ~114 fps
β Reference only, and here is what that qualifier actually covers. Every number on this page was measured on a single RTX 5090 at 7680x4320, in a purpose-built empty test room where the shaft is nearly the whole cost. Your GPU, your resolution and your composition all move these figures, and the largest of the three optimisations trades arithmetic for texture lookups, which is a bargain whose value depends on GPU architecture. We have no data from any card other than that one. If your result differs, that is information we want.
What actually changed
1. The noise is now a lookup table (most of the win)
Haze and Dust turned out to be 88.9% of the shaft's entire cost. Both were procedural value noise, evaluated three times per raymarch step, each evaluation eight hashes and seven blends. They are now one texture fetch each from a small tiled 3D table built at load.
v1.0 procedural v1.1 lookup Haze 11.40 ms 2.61 ms 4.4x Dust 6.16 ms 2.22 ms 2.8x
Side effect worth having: the old code fed world position plus elapsed time into the noise, and after a few hours at dust frequencies that coordinate ran out of floating point precision and the grain slowly degraded. The table tiles, so the coordinate can be wrapped exactly. That failure mode is now structurally gone.
If you prefer the original look,Noise qualityon the EnvRay panel has a Legacy hash option. It costs about twice as much and, in a still frame, the two differ by 1.3% in average brightness.
2. The march no longer walks the empty parts of the ray
The shader stepped from the camera to the far side of the beam volume in a fixed number of steps, so a large share of them landed in empty air outside the shaft, doing nothing. It now solves where the ray enters and leaves the volume and visits only the steps in between. Same sample positions, same image : the difference is that the empty ones are skipped.
Worth 8-14% depending on framing, and worth nothing at all when you are standing inside the shaft, because then there is no empty part to skip.
3. A hidden shaft exits immediately
Falls out of the same solve. If the ray meets solid geometry before it reaches the volume, the pixel returns black without entering the loop.
4. StageLightGroup : flare offset fixed
With several fixtures in one group and Flare enabled, the flare billboards collapsed onto a single position. Unity's two merging paths both require the merged renderers to share a material, so each fixture now carries its own flare material, which rules the merge out by construction rather than by hoping.
What we tried and did not ship
- Drawing only the back faces of the beam volume. On paper this halves the work. Measured: no gain at all : the front-face pixels were already exiting almost immediately. It would have carried a small risk to the shaft's silhouette for nothing, so it is not in the build.
- A smaller lookup table for the haze. Faster when Haze runs alone, but its repeat period in metres would then be short enough to see inside a long shaft. Rejected on looks, not on speed.
Both are listed because "we measured this and decided against it" is a more useful thing to know than a list of wins.
If it is still too expensive
In the order that actually helps:
- Haze off, then Dust off. Even after this update they are most of the shaft's cost. Together they are worth more than everything else on this list combined.
- Raymarch steps 48 β 24. Cost is exactly linear in steps, so this is exactly half.
- Make the shaft smaller in frame. Beam radius, throw depth, or simply not composing it across the whole view. Screen coverage is a straight multiplier.
- Fog softening off. Smallest of the four.
Changing STATIC / SMART / LIVE will not help. Measured, the three differ by 0.05 ms, which is 0.2% of the shaft. They decide how often the bake re-runs, and the bake was never the expensive part. This is the same misunderstanding the v1.0 wording encouraged, so it is worth stating twice.
How it was measured
- A purpose-built test room, generated by a plugin rather than hand-assembled, so the fixture is identical every run and on anyone's machine. The camera is snapped to numbered positions by code, because "same seat as last time" is not a measurement.
- 8 passes of 10 seconds per configuration, every setting written by the harness rather than by hand. Scatter per row 0.01 to 0.08 ms; drift across an hour under 0.2%.
- 7680x4320, forced with DSR. An earlier attempt at 4K produced a completely flat table : the shaft was cheaper than the CPU floor, so every row read the same and Dust and Fog looked free when they were not.
- Every configuration was checked both ways. If a change should make one row move and another row stay still, both halves were required before it was believed. Two conclusions were thrown away during this work because the row that was supposed to stay still had never been able to move in the first place.
Nothing here was estimated. Where a number could not be measured honestly it is not on this page.
Thanks to @AlphaCentauri, who reported the issue after wondering if he'd set something up wrong.
He had not, and the question was worth more than a bug report.