`wmo.rs` built its submesh texture URLs by hand — backslashes folded,
lowercased, `mpq://` prefixed — which is exactly what `texture_url` does, minus
the one thing that matters: `texture_url` appends `.blp` to a name that has no
extension, as the reference's `CImageManager::CreateTexture` does. The M2 loader
has gone through it all along.
The realm's WMOs ship bare material names too, so that submesh drew untextured
and Bevy could not even pick a loader for it. From grom's 2026-10-28 log, the
only two lines in the whole session above WARN:
WARN Multiple AssetLoaders found for Asset … Extension: None
ERROR Path not found: textures/wetlands_grimbatol/mm_irnfg_trim_02flip
Repeat/repeat is passed, which is what the hand-rolled path was: `texture_url`
returns the plain `mpq://…` for that pair and marks the stem only for a clamped
mode, and a WMO submesh carries no wrap flags here to ask about. That one name is
now a case in `texture_url`'s own test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The starburst was three defects, and none of them was the oblique
projection I blamed it on in the last commit. What found them was looking
at the mirror's own render target instead of reasoning about the
composited frame — the water reads it through a fresnel weight, a rippled
UV, a plane-match band and an SSR march that paints over it, so from the
outside a broken mirror and a broken water shader look identical.
WOW_MIRROR_DUMP=1 now writes that target beside the capture. (Bevy's own
save_to_disk cannot: try_into_dynamic knows four 8-bit formats and answers
UnsupportedFormat for Rgba16Float, logging and writing nothing.)
- The oblique row's scale. Replacing the near plane with the water plane
costs the FAR plane: `z >= 0` stops meaning "nearer than CAM_FAR" and
becomes `depth >= a*d`, a plane through the eye tilted by a. Lengyel's
own scale puts that plane exactly through the frustum's far corner, and
the sky dome is pinned just inside the far plane, so it straddled the
boundary and was sliced along its own tessellation. Scale by the
supremum of d/depth over the frustum instead, with 2% of margin.
- The water surface lay ON its own clip plane. This was the starburst and
it was fatal rather than untidy: depth now means "distance above the
plane", so a fragment at d=0 reads z/w=1, the nearest value reverse-Z
has, and beats every other surface in the frame. The mesh is
wave-displaced, so its triangles straddled the plane instead of lying in
it, and each was clipped to a sliver that kept the winning depth.
SURFACE_CLEARANCE lifts the plane a yard: the swell sums to 0.34 and the
probed height answers for the surface under the eye, not this fragment.
- The mirror was stored upside down. A true reflection is what a camera on
the far side sees with its up vector reflected too — an improper basis
no Transform can hold. The honest right-handed camera sees the same
surfaces through the same frustum and stores the image flipped, so the
shader samples 1 - v; that costs no winding flip, so backface culling
stays correct. It read as a lake reflecting the ridge the RIGHT way up.
Measured: Lake Everstill 2.87 -> 3.36 ms mean over 600 frames (+17%), the
Stormwind canal inside the noise, CPU +0.2 to +1.1 ms. It is the first
lane in docs/GRAPHICS.md that spends main thread, so it is a rung of its
own above High and the default stays 2.
Seated as a setting, per the standing rule that no graphics lane hides
behind an env var: waterQuality gains tier 3, the Ultra preset takes it,
and the Advanced Graphics dropdown gains a fourth entry in both locales.
With the lane off, both scenarios are byte-identical to the build before
it — which took two findings of its own:
- The system could not live in Update at all. `&mut Transform` on a camera
is shared with everything that moves anything; declaring it put 59
undeclared orders into Update and tripped the schedule ratchet, which is
what kept this behind an env var. It belongs in PostUpdate with the
skybox and the billboards, after propagation and before visibility,
where it also reads the eye's settled pose instead of a frame-old one.
(The comment blaming ResMut<Assets<Image>> for those 59 was wrong —
dropping the asset handle changed the count by zero.)
- A system that runs and returns on its first line is still not free.
Holding &mut Camera and &mut GlobalTransform for a frame is enough to
change which undeclared orders the executor picks: the canal moved 0.75%
of its pixels by up to 96/255 with the lane switched off. So the whole
system sits behind a run condition rather than an early return — and the
same effect killed a .before(UpdateDirectionalLightCascades) order that
was correct in itself.
PostUpdate's ambiguity ceiling goes 466 -> 495 with the reason (the pairs
are structurally impossible: nothing else in the schedule names the mirror
camera); Update's ratchets down 4973 -> 4952.
Also restores scripts/gates.sh's `where` example, which had gone missing.
Its absence was silent — cargo's error went to /dev/null and `|| true`
made it read as "no install" — so the memo key and the player-tests gate
saw none even when WOW_DATA was set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
A second camera on the far side of the water plane, rendering into a
half-viewport target that `enhanced_water.wgsl` reads BENEATH the
screen-space march — so SSR still wins wherever it finds something, and
the mirror only fills the hole SSR cannot reach: the ray leaving the
screen, and the top-edge fade, which is exactly where a shallow-angle lake
wants its far bank.
**It does not work yet, and it is not offered to anyone.** The mirrored
eye sits as far below the surface as the real eye sits above it — 26 yd
down on Lake Everstill — which is inside the lake bed, so the pass must
clip at the water plane or it renders sand. The clip is implemented
(Lengyel, adapted to this client's reverse-Z, around a finite far plane
because the oblique row makes depth affine and an infinite far collapses
every distant fragment onto one value) and it is WRONG: the mirror comes
back a radial smear. So this is `WOW_WATER_MIRROR=1`, a dev lever — no
CVar, no row in Options. It graduates to both, like every other lane,
once a capture shows a reflection instead of a starburst.
What IS established, and is why this lands rather than waiting:
- **With the lane off the water is byte-identical** to before the pass
existed, verified against a `git stash`ed build of the same tree.
- With it on it reaches the water and moves 63% of a lake frame — the
plumbing, the binding, the uniform and the composite all carry.
- Off, the plugin **registers nothing at all**, the posture
`static_gx::enabled` takes. That is not tidiness: `drive_mirror`
declares `ResMut<Assets<Image>>`, and merely registering it dormant
added 59 undeclared orders to `Update` and tripped the schedule ratchet.
Three things this cost, all of them now written into `docs/GRAPHICS.md`
and all of them my own:
1. **The pipeline failed to compile for most of a session while every A/B
read a clean zero.** The field is `clip_position`; `WaterFragment` has
no `position`. Naga refused the shader, the water fell back to another
path, and I measured the fallback over and over — including twice
mistaking the lake bed seen through clear water for a working mirror.
The client logs pipeline errors rather than panicking, by design, so
the answer was in the log the whole time. **Grep the capture log for
`error:` before believing any shader A/B.**
2. **A disabled lane is not automatically free.** Starting SSR's march
from the fogged `reflection` instead of `sky` looked equivalent and
moved 10.1% of a lake frame with the mirror switched off. The control
that catches that is diffing lane-off against a stashed build — not
against an older capture, which is what I reached for first.
3. **A flag colour has to be one the scene cannot produce.** My first
three "every gate passes" measurements were counting the orange lake
bed as my red flag, which sent the hunt in the wrong direction for an
hour.
The pass's own cost is deliberately unmeasured: it draws the scene a
second time, so it is the first item on the programme that spends main
thread, and pricing a broken render against item 1's view-distance table
would mean nothing. That happens when the projection does.
Verified: ALL GATES GREEN (591 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`docs/GRAPHICS.md` said we had refraction and foam but no mirror. That was
wrong. `waterQuality` 2 has carried a depth-marched screen-space
reflection of the scenery all along (`enhanced_water.wgsl`, "HIGH"), with
bisection refinement against the real scene depth rather than the fixed
probe its WarcraftXL source took. What was missing was the switch: the
registered default was Enhanced (sky only), and of the five lighting rungs
only Ultra reached High.
The line holding it back said exactly what it was waiting for —
"Mirror reflections stay opt-in until their cost is measured." So I
measured it, `WOW_FPS_PROBE=300`:
Classic Enhanced High
Lake Everstill 1.91 ms 1.84 ms 1.90 ms
open ocean 1.89 ms 1.88 ms 1.89 ms
Every tier is inside the run-to-run noise — Enhanced lands FASTER than
Classic on the lake, which is how you know you are reading noise rather
than a cost. What it buys is not noise: the far bank and the mountains
appear in the water, 18.5% of the lake frame and 35.1% of the ocean frame.
So High becomes the default and the High rung. That is a weld across four
places the tests hold together, and each one bit in turn and was fixed as
it asked: `REGISTERED`, `VideoConfig`'s literal, `WaterQuality::default`,
and the rung itself — the High rung IS the registered defaults, which
`the_high_preset_is_the_registered_defaults` states and enforces.
Low and Medium keep Enhanced deliberately, and that needed saying in the
one test that assumed every rung between Off and Ultra sits AT its
default: free on the machine it was measured on is an argument for the
default, not for the rung whose whole job is hardware that cannot afford
what this one can. `water_and_lava_observers_clamp_and_water_high_is_opt_in`
asserted the old decision in its name and its body, so it is renamed and
now pins the pairing the other way: High reflects, Medium does not.
The honest remaining gap is written into the doc rather than claimed as
done: SSR gives up when the ray leaves the screen and fades toward the top
edge, which is where a shallow-angle lake wants its far bank. A planar
pass would fill that — and it is the first item on the list that draws the
scene twice, so it costs main thread and gets priced against item 1's
view-distance table before anyone builds it.
Verified: ALL GATES GREEN (492 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The Options row shipped last commit said the setting needed a relaunch.
It does not have to. The reference's "enabled upon restart" is about its
TEXTURES, not its CVars — both register with `flags = 1`, live — because
its sampler state was baked inside each `TextureCreate` (`0x449ae0`) and
it kept no list of live textures. Ours can be re-stamped in place.
Two things make it possible, and neither is new work:
- A governed sampler now wears `POLICY_SAMPLER_LABEL`, written by the one
constructor `TexFilterSetting::descriptor`. That is the `applyGlobal`
split the reference draws, made legible at runtime: terrain, WMO/M2 and
the liquid sheets take the policy; weather sprites, the minimap, glue,
Lua widgets, the sky and decals keep their own mode. All four governed
lanes went through that constructor, so a lane cannot forget the label
and silently opt itself out of every later change.
- Extracting a `RENDER_WORLD`-only `Image` takes its *pixels* and leaves
the asset behind (`take_gpu_data` is an `Option::take` on `image.data`).
So every resident texture's own descriptor is still readable in the main
world, for nothing — there is no ledger to keep and none to go stale.
The policy itself stops being a `OnceLock` and becomes one atomic word
plus a revision, so it can be published again on every write and both
halves of the refresh can key off the same number in the same frame:
`refresh_policy_samplers` (render world, `ExtractSchedule`, before every
Prepare) swaps `GpuImage::sampler`, and `invalidate_materials::<M>` raises
`AssetEvent::Modified` so bind groups that captured the OLD sampler are
built again. `static_gx` carries two samplers of its own and rebuilds its
cached cell bind groups in place rather than dropping them — clearing the
cache would be one line, but a region with no GPU state does not draw, so
every building in view would blink out and trickle back under the bake
budget.
Measured, not asserted. `WOW_CVAR_LATE=<name>=<value>` is new here: it
writes CVars *after* the scene has settled, then re-settles the image
around them. A latched lever and a live one give the same boot picture, so
no boot-time A/B can tell them apart. On the Elwynn vista:
anisotropic 16 at boot vs 16 written late:
0 of 1,440,000 pixels differ
and the control that proves the harness is not doing the work (boot 16,
written 16 again late, so no refresh runs but the re-settle is still paid)
is byte-identical to a plain boot. The cost of the change is 873 samplers,
75 material bind groups and 264 static_gx cell bind groups, once; nothing
is re-uploaded, since the pixels never move.
The first run of that A/B was NOT identical — 6.7% of the frame, a dapple
of stale tree shadow on a road that was otherwise correct. That is
`CutoutShadowCasterMaterial`, which deliberately reuses the forward pass's
own leaf handle and so holds a governed sampler, and which lives in
`benilla-app` rather than the engine. Hence the shape the invalidation
took: a public `invalidate_materials::<M>` each crate registers beside its
own `MaterialPlugin`, published through the world-API wall with the record
that says why (the engine naming every material type in the program is the
inversion that wall exists to stop). Forgetting one is silent in exactly
that way — a bind group built after the change is right and one already
cached is wrong — so the capture A/B is written into `docs/GRAPHICS.md` as
the check for this feature, not a unit test pretending to be one.
The Options row keeps its deferral to Apply, now for the real reason: the
commit is cheap once and silly sixteen times, which is what a drag across
the groove would ask for.
Verified: ALL GATES GREEN (520 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
Same fork policy: upstream structure wins, local-unique re-instated.
Cargo.toml unions both measured dev-profile philosophies (codegen-units=1 +
lto=off from samwhosung, debug=line-tables-only + split-debuginfo from the
fork's disk measurements).
glue_booth.rs takes samwhosung's typed GlueScene (token/of_race/from_model_path)
and re-adds the fork's TurtleWoW races (Goblin=9, BloodElf=10) as variants;
their glue fog stays on Human's row, exactly as the fork's scene_fog fallback did.
ui script takes samwhosung's mouseover/hyperlink rework (world_mouseover,
minimap_blip, hyperlink.rs) over the fork's inline hover_link/span_at/pointer
path — the rework covers the same OnHyperlinkEnter/Leave behaviour.
game_plugins.rs keeps the fork's re-measured ceilings (466 / 5,509); mail.rs
keeps the explicit 13-tuple with a float daysLeft; MAP.md takes the superset.
Fork policy (docs/METHOD.md): upstream structure wins; local-unique work
re-applied on top. This commit is the upstream half of the reconciliation.
Upstream is the same program at a rewritten history (884dff77 == 4e0e89cb
tree), so the merge base was replaced to keep a real three-way diff. The
large conflicts are upstream moving first: the UI manifest (benilla.toc ->
the binary LAYER_MANIFEST/layer.toc split), the CVar table (cvars.rs ->
cvars/{mod,table}.rs, +24 video rows), the editbox flush model, the
host-side GetTime, the Options* -> BenillaOptions* rename, expanded
ui_unit.rs, and a rebuilt volumetric_light.rs.
Local-unique pieces re-instated on the upstream structure:
- OptionsFrame.xml: the 1.12 OptionsFrameSliders table restored under the
Benilla rename, with the TurtleWoW deviation note (patch-4 replaces
OptionsFrame.lua).
- layer.toc: CharacterFrameAdapters.xml (the sixth TurtleWoW character tab).
- ui script: wipe/sizeof, BenillaGetMerchantItemStats, and the attach/mount
teardown re-walk all survive.
- game_plugins.rs keeps UPDATE_ACTIONABLE_CEILING=5_509 (re-measure pending).
The LFT/Turtle_TransmogUI rows benilla.toc named off the player's install
now come from the chain's own FrameXML.toc via the new core loader.
Player reports and the trace, in one batch.
- SetFactionInactive/SetFactionActive raise `UPDATE_FACTION`, so the list moves at once.
- NPC greetings: the latch, a dead unit, then the reaction pair with the active mover (`0x60c270`); a dead or ghost player hears none. Only a passing click re-tracks the NPC, and a window hello resets it (`0x60c3d1`).
- "You are now <rank> with <faction>." prints on first contact and rank change (`0x62c670`), in packet order, with the real gender.
- A standing packet prints its "increased by" line for the first entry only, never a hidden one (`0x4d57dd`).
- `WOW_MOVE_TRACE` carries each packet's bytes as hex; chat bodies are opt-in, and dot/bang commands and channel passwords are always redacted.
Tests cover each gate, the line order and the redaction.
Fixes#927Fixes#882Fixes#881Fixes#867Fixes#965Fixes#966Fixes#967🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat bubbles were quads drawn straight into an overlay with no widget behind them, so `WorldFrame:GetChildren()` never returned them and bubble-skinning addons such as pfUI's found nothing.
In the reference, a bubble is an anonymous Frame under WorldFrame from a reused pool: eleven regions (the nine backdrop pieces, the tail, the text in the `NAMEPLATE_FONT` global), the text's bottom on the speaker's projected point, its own fade, and an OnShow on each reuse. It fades out and hides when the speaker is out of range or was not drawn last frame.
Bubbles are now built that way, fed by the app and drawn by the frame pass; the overlay path is gone. Item links show their bare names, and a speaker behind the camera fades out.
Tests cover the child, its regions, a pfUI-style skin, reuse and the fades.
Fixes#884
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An addon's Lua and XML files were read with the exact case its `.toc` or `<Script file>` spelt. On a case-sensitive filesystem, Linux's, a file named in another case never loaded: Questie 3.7.1 lists `Astrolabe\Astrolabe.lua` from a folder shipped as `astrolabe`, so `Astrolabe` stayed nil and its OnUpdate raised every frame.
The reference is a Windows client, and every path it opens matches without regard to case. Addon textures and sounds already resolved that way through `loose_addon_file`; addon scripts, XML and `Bindings.xml` now go through the same resolver, and the second, case-sensitive one is gone. The shared resolver also refuses a component the host reads as more than a plain name (a Windows `C:`), so it still cannot leave the AddOns root.
Test: a file asked for in three spellings resolves to the one on disk; it fails without the change on a case-sensitive volume.
Fixes#851
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seam W: InstanceSurfaceParams (whole_wet, lower_wet, waterline_y, feather) per rig
slot, a 2048-row table in the light buffer, applied in wow_model.wgsl through the
existing wet_surface/wet_sheen (max with rain wetness, opaque/alpha-test + fade
twins only). Zero row = stock image. Self-contained: cherry-pickable to lighting.
Seam F: WaterFoamSpawn message; plugin foam records live in their own 96-record
pool so they never evict or throttle the stock unit wake/ring records.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner: planar is cheap enough (~1 ms CPU, GPU about even) to replace SSR outright. High water
always reflects with the planar mirror; the waterReflections cvar, its preset rows, the options
row (EN/RU) and the debug knob are removed. The screen-space march stays only as the mirror's
fallback for water at another height than the mirror plane; WOW_WATER_REFLECT=0 remains a dev
A/B switch.
SSR: the march accepted rays up to 2 + 0.1*dist yd behind the scene depth and returned the
bisected pixel unchecked, so rays passing BEHIND a thin dock deck converged on its front
silhouette - a vertical smear. After bisection a hit may now sit at most
0.25 + 1.5%*dist + the last interval behind; deeper = passing behind an occluder, march on.
Planar: liquid/mirror.rs (WaterMirrorPlugin) renders a half-res mirrored camera (oblique
near plane at the water) before the world camera into binding 107; no water, no sun
shadows, no post. The plane is voted by a 7x6 ray fan with hysteresis; water off that
plane fades back to SSR. Cost ~+1 ms CPU, GPU about even (SSR march skipped) -> its own
cvar waterReflections (0 Screen Space / 1 Planar, EN+RU option row), on for High/Ultra.
The High preset (= registered defaults) now carries High water (waterQuality 2); Low and
Medium keep Enhanced. Classic and Enhanced captures byte-identical.
The enhanced exterior WMO point-light term evaluated falloff per fragment but used the
flat lamp selection of the provoking vertex (snapped to its 33 yd MCNK cell), so a large
wall or paving triangle took one vertex's whole lamp pool and neighbours differed: long
straight-edged dark wedges on Stormwind's gate ramp and yard walls at dusk/night. Ranks
the closest exterior lamps at the fragment (static_gx + fallback wow_model), shared by
the shadowed and unshadowed evaluations. Classic (all light gains 0) keeps the vertex
pick and its packed frame bits; capture scenarios stormwind-training-yard-*/gate-ramp-*.
A doodad standing on its own baked terrain shadow (Lakeshire gryphon roosts, most forest
trees) was dimmed twice by day: by the MCSH 0.5 sun band AND by the realtime shadow map,
on ground that no longer shows the bake. wow_model.wgsl and static_gx.wgsl now use
terrain.wgsl's own gate (sh_c16.w, weighted by the sun lane so the bake returns at dusk).
Lane off = reference bits. Capture scenarios lakeshire-statue*/lakeshire-gryphons*,
WOW_CAPTURE_EYE/LOOK/MINUTE re-aim overrides.
World and entity receivers now choose the nearest ready torch map within the existing tolerance. Previously an earlier neighbouring fixture won even when the light's own map was present at its exact position. Add an opt-in attached spell-light capture fixture and correlation trace.
Verified on Windows with the app library gate, play build and inn/blacksmith captures. The inn trace reproduces first slot 3 versus exact-position slot 11. Visual 60056 movement and doorway transitions still require an in-game retest.
Co-Authored-By: GPT-6 Astra (Codex)
Move the foliage wind marker from bit 14 to bit 15 in its Rust producer and WGSL consumer so distance-fade twins cannot trigger a downstream fragment effect. Keep bit 14 reserved for those extensions.
Verified on Windows: cargo build -p benilla --profile play; cargo test -p benilla-app --lib (4313 passed, 0 failed, 10 ignored; no-install run). Public-base and fixed captures at the reported Elwynn position are pixel-identical; downstream integration still needs its in-game retest.
Co-Authored-By: GPT-6 Astra (Codex)
Volumetric light: the shadow rig's directional light (sun by day, moon at
night) is ray-marched through its cascades at half resolution (16 Medium /
32 High jittered samples from 0.35 yd, height-thinned air, HG g 0.6), then a
depth-aware blur and upsample composite it over the fog pass. Density and
colour follow the zone fog; no shafts in WMO interiors. The volumetric fog
pass retires its own midpoint shafts while this is on. Cvars volumetricLight
0/1/2 and volumetricLightStrength, env WOW_VOLLIGHT; Graphics Preset Medium 1,
High/Ultra 2.
Moonlight: a cool, dim directional term from the visible moon added to the
stock night on terrain, models, WMO exteriors and doodads, shadowed by the
moon's map (shadow_hook::realtime_shadow_moonlit .z), ramping with the moon's
elevation and handing back to the sun at dawn. MonkeyFrame grows one row
(per-frame blob 8800 B). Cvar moonLight 0..2 (0 = the reference night).
Advanced Graphics rows (EN + RU), capture scenarios vol-*, docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Volumetric light: the shadow rig's directional light (sun by day, moon at
night) is ray-marched through its cascades at half resolution (16 Medium /
32 High jittered samples from 0.35 yd, height-thinned air, HG g 0.6), then a
depth-aware blur and upsample composite it over the fog pass. Density and
colour follow the zone fog; no shafts in WMO interiors. The volumetric fog
pass retires its own midpoint shafts while this is on. Cvars volumetricLight
0/1/2 and volumetricLightStrength, env WOW_VOLLIGHT; Graphics Preset Medium 1,
High/Ultra 2.
Moonlight: a cool, dim directional term from the visible moon added to the
stock night on terrain, models, WMO exteriors and doodads, shadowed by the
moon's map (shadow_hook::realtime_shadow_moonlit .z), ramping with the moon's
elevation and handing back to the sun at dawn. MonkeyFrame grows one row
(per-frame blob 8800 B). Cvar moonLight 0..2 (0 = the reference night).
Advanced Graphics rows (EN + RU), capture scenarios vol-*, docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's "wind does not work" was the motion being too small to see (WXL's
0.06 yd grass tips on a 6 s wave, 0.1 yd tree crowns). Retuned at the 9 yd/s
base speed:
- grass: tips about +-0.2-0.3 yd, 2.5 s main wave (182 cycles/wrap), 1.1 s
cross flutter (410); 55 yd gust bands roll downwind (68), swelling the sway
to 1.2x and lulling it to 0.55x; a bent tip dips (no stretched blades).
Parting around units unchanged.
- trees/bushes: 4 s crown sway (114), 5.5 s side wave (82), 0.8 s leaf
flutter (569); crown weight grows with height (0.43x at 1.5 yd, 1x at 6 yd,
1.3x cap), so canopies sway about +-0.3 yd while bushes nod and bases stay
planted.
- new cvar foliageWindStrength 0.25..3 (default 1, not on the preset ladder):
Advanced Graphics slider under Foliage Wind (EN+RU label/tooltip), greyed
while Foliage Wind is Off; carried in sway_strength, so the parting ignores
it; a storm adds up to +60 % sway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's "wind does not work" was the motion being too small to see (WXL's
0.06 yd grass tips on a 6 s wave, 0.1 yd tree crowns). Retuned at the 9 yd/s
base speed:
- grass: tips about +-0.2-0.3 yd, 2.5 s main wave (182 cycles/wrap), 1.1 s
cross flutter (410); 55 yd gust bands roll downwind (68), swelling the sway
to 1.2x and lulling it to 0.55x; a bent tip dips (no stretched blades).
Parting around units unchanged.
- trees/bushes: 4 s crown sway (114), 5.5 s side wave (82), 0.8 s leaf
flutter (569); crown weight grows with height (0.43x at 1.5 yd, 1x at 6 yd,
1.3x cap), so canopies sway about +-0.3 yd while bushes nod and bases stay
planted.
- new cvar foliageWindStrength 0.25..3 (default 1, not on the preset ladder):
Advanced Graphics slider under Foliage Wind (EN+RU label/tooltip), greyed
while Foliage Wind is Off; carried in sway_strength, so the parting ignores
it; a storm adds up to +60 % sway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fold mask came from the two long wave bands only, which cross in a regular diamond lattice:
every crest broke into the same elongated patch. The mask adds the next two bands' compression,
a slow domain-warped gust field moves the break threshold and calms patches, and ~6 yd warped
segments break each crest into short runs. Geometry is unchanged; crest foam stays on steep waves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fold mask came from the two long wave bands only, which cross in a regular diamond lattice:
every crest broke into the same elongated patch. The mask adds the next two bands' compression,
a slow domain-warped gust field moves the break threshold and calms patches, and ~6 yd warped
segments break each crest into short runs. Geometry is unchanged; crest foam stays on steep waves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wet_hook gains shelter_taps/shelter_cell/shelter_amount (bilinear over
four cells; sheltered when a covered top is >= ~1 yd above the fragment,
the roof's own surface stays open). terrain, wow_model and static_gx
mirror the region after monkey and scale wet exposure by 1 - shelter
(wet_puddles takes exposure, so no puddles under cover); enhanced_water
suppresses rain rings under bridges/roofs via its row view. The lookup
runs only while wetness > 0 and the header is active, so a dry frame is
unchanged.
Verified: the wet_hook + receiver/water lookup snippets composed and
validated with naga_oil 0.20 / naga 27 (scratch harness; unpack2x16float
avoided - it needs a float16 capability). Full receiver shaders were not
compiled here (no GPU, no game data).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8t89gSUuo1uvbXKdh1mJt
wet_hook gains shelter_taps/shelter_cell/shelter_amount (bilinear over
four cells; sheltered when a covered top is >= ~1 yd above the fragment,
the roof's own surface stays open). terrain, wow_model and static_gx
mirror the region after monkey and scale wet exposure by 1 - shelter
(wet_puddles takes exposure, so no puddles under cover); enhanced_water
suppresses rain rings under bridges/roofs via its row view. The lookup
runs only while wetness > 0 and the header is active, so a dry frame is
unchanged.
Verified: the wet_hook + receiver/water lookup snippets composed and
validated with naga_oil 0.20 / naga 27 (scratch harness; unpack2x16float
avoided - it needs a float16 capability). Full receiver shaders were not
compiled here (no GPU, no game data).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8t89gSUuo1uvbXKdh1mJt
Zone Skyboxes row moved to the end of the Advanced Graphics page and added to
OPTIONS_PAGE_ROWS (the lane had left it out of the list).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone Skyboxes row moved to the end of the Advanced Graphics page and added to
OPTIONS_PAGE_ROWS (the lane had left it out of the list).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderSubmesh carries stage 1 (texture, UV set, transform, Mod/Mod2x) for
textureCount 2 batches; M2Vertex reads UV1. WowModelExt gains a stage1
uniform + texture binding 94/95 and the WOW_STAGE1 shader def, so every
one-texture batch compiles the old code. The skybox lane binds stage 1 on
its own material copies and drives its transform rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderSubmesh carries stage 1 (texture, UV set, transform, Mod/Mod2x) for
textureCount 2 batches; M2Vertex reads UV1. WowModelExt gains a stage1
uniform + texture binding 94/95 and the WOW_STAGE1 shader def, so every
one-texture batch compiles the old code. The skybox lane binds stage 1 on
its own material copies and drives its transform rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review B3: floor(cycle) % 50 keeps the ring roll continuous across the 1000 s
wrap. B8: publish the ripple clock only while it rains. B7: nudge the wet
sheen half-vector off zero. B6: WORD_WINDOW batches are excluded from wet
darkening.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review B3: floor(cycle) % 50 keeps the ring roll continuous across the 1000 s
wrap. B8: publish the ripple clock only while it rains. B7: nudge the wet
sheen half-vector off zero. B6: WORD_WINDOW batches are excluded from wet
darkening.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review P4: emissive_boost now lifts only above a 0.6 knee (equal to the old
gain at 1.0), so additive spells and lava keep their look and the excess feeds
the bloom. Review P3: the window gain applies to static_gx's SIDN light term
before fog, not to the whole fogged batch. Review P6: Low-tier extract taps
scale with the quarter-size target. Review P7: excess capped at 64 (no NaN).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review P4: emissive_boost now lifts only above a 0.6 knee (equal to the old
gain at 1.0), so additive spells and lava keep their look and the excess feeds
the bloom. Review P3: the window gain applies to static_gx's SIDN light term
before fog, not to the whole fogged batch. Review P6: Low-tier extract taps
scale with the quarter-size target. Review P7: excess capped at 64 (no NaN).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bit 18 aliases the top lane-weight bit and bit 12 of the interior probe slot,
so doorway units and probe-lit interior props swayed like trees. A classified
leaf batch's fade-band exile now gets marked copies of its cutout/blend
materials (clutter_fade.z bit 14, FOLIAGE_WIND_MARKER, not a key axis) and
wow_model.wgsl sways only those.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>