<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.btscape.quest/index.php?action=history&amp;feed=atom&amp;title=Visibility</id>
	<title>Visibility - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.btscape.quest/index.php?action=history&amp;feed=atom&amp;title=Visibility"/>
	<link rel="alternate" type="text/html" href="https://wiki.btscape.quest/index.php?title=Visibility&amp;action=history"/>
	<updated>2026-09-18T08:14:26Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://wiki.btscape.quest/index.php?title=Visibility&amp;diff=97&amp;oldid=prev</id>
		<title>Bembi: Created page with &quot;&#039;&#039;&#039;One light level per face&#039;&#039;&#039;, computed server-side once and read by the renderer — and, once Phase 5 lands, by the rule that decides who can be seen at all. This page summarizes &lt;code&gt;docs/initiatives/light-and-visibility-initiative.md&lt;/code&gt;; treat the doc as authoritative if the two ever disagree.  == The one idea == Today&#039;s baked field is named &lt;code&gt;darkness&lt;/code&gt;, but what it actually measures is &#039;&#039;how much sky a face can reach&#039;&#039; — a fact about geometry that...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.btscape.quest/index.php?title=Visibility&amp;diff=97&amp;oldid=prev"/>
		<updated>2026-08-26T23:51:18Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;&amp;#039;&amp;#039;&amp;#039;One light level per face&amp;#039;&amp;#039;&amp;#039;, computed server-side once and read by the renderer — and, once Phase 5 lands, by the rule that decides who can be seen at all. This page summarizes &amp;lt;code&amp;gt;docs/initiatives/light-and-visibility-initiative.md&amp;lt;/code&amp;gt;; treat the doc as authoritative if the two ever disagree.  == The one idea == Today&amp;#039;s baked field is named &amp;lt;code&amp;gt;darkness&amp;lt;/code&amp;gt;, but what it actually measures is &amp;#039;&amp;#039;how much sky a face can reach&amp;#039;&amp;#039; — a fact about geometry that...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;&amp;#039;&amp;#039;&amp;#039;One light level per face&amp;#039;&amp;#039;&amp;#039;, computed server-side once and read by the renderer — and, once Phase 5 lands, by the rule that decides who can be seen at all. This page summarizes &amp;lt;code&amp;gt;docs/initiatives/light-and-visibility-initiative.md&amp;lt;/code&amp;gt;; treat the doc as authoritative if the two ever disagree.&lt;br /&gt;
&lt;br /&gt;
== The one idea ==&lt;br /&gt;
Today&amp;#039;s baked field is named &amp;lt;code&amp;gt;darkness&amp;lt;/code&amp;gt;, but what it actually measures is &amp;#039;&amp;#039;how much sky a face can reach&amp;#039;&amp;#039; — a fact about geometry that only changes when someone edits terrain. Night is a completely different kind of thing: one global number that changes continuously. Keeping the two separate, and adding light sources as their own terms, is the whole design:&amp;lt;pre&amp;gt;&lt;br /&gt;
light(face, t) =   sky_access(face) · sky_light(t)     static field × global clock&lt;br /&gt;
                 + placed_light(face)                   static field&lt;br /&gt;
                 + carried_light(face, t)                dynamic, few sources&lt;br /&gt;
                 + innate_light(viewer, face)             viewer-relative, gating only&lt;br /&gt;
&amp;lt;/pre&amp;gt;Composed in &amp;#039;&amp;#039;&amp;#039;light-space&amp;#039;&amp;#039;&amp;#039; — every term adds. The renderer still works in the darkness-space its shaders already multiply by, so the wire format doesn&amp;#039;t change: it converts once, &amp;lt;code&amp;gt;1 − light&amp;lt;/code&amp;gt;, at the very end.&lt;br /&gt;
&lt;br /&gt;
== What each term is ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Term&lt;br /&gt;
!Kind&lt;br /&gt;
!What it is&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;sky_access(face)&amp;lt;/code&amp;gt;&lt;br /&gt;
|static · baked · server&lt;br /&gt;
|A multi-source BFS seeded at every open-roofed face (nothing above it in its own column), ramping 0→1 over 5 tiles and saturating. A face no wave reaches — a sealed room, a cave whose mouth hasn&amp;#039;t loaded — stays fully dark. Rebuilt whenever terrain changes; sent on the wire unchanged as &amp;lt;code&amp;gt;SheetData.darkness_q&amp;lt;/code&amp;gt; (the old name — it&amp;#039;s really the inverse of this).&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;sky_light(t)&amp;lt;/code&amp;gt;&lt;br /&gt;
|global scalar · both sides&lt;br /&gt;
|Day is a flat 1.0, night floors at 0.04 (moonlit, not black), and dawn/evening ramp linearly between. One clock, computed identically client-side (&amp;lt;code&amp;gt;WorldDaylight.cs&amp;lt;/code&amp;gt;) and server-side (&amp;lt;code&amp;gt;bts_content::world_clock::sky_light&amp;lt;/code&amp;gt;), so nothing ever needs to restream as the sun moves.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;placed_light(face)&amp;lt;/code&amp;gt;&lt;br /&gt;
|static · baked · server&lt;br /&gt;
|A second BFS, seeded at every placed light-source object&amp;#039;s footprint, each capped at its own authored radius with a smooth cubic falloff. Multiple lights reaching one face take the brightest, never the sum. Not scaled by the sun — a brazier is as bright at midnight as at noon.&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;carried_light(face, t)&amp;lt;/code&amp;gt;&lt;br /&gt;
|dynamic · client-rendered&lt;br /&gt;
|Equipped torches near a face. Genuinely dynamic, so it isn&amp;#039;t baked; it rides a 4-slot shader array published fresh every frame, nearest sources winning a slot. An interim measure — a chunk with more than four concurrent lights in view needs a real per-chunk light list (Phase 3, unbuilt).&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;code&amp;gt;innate_light(viewer, face)&amp;lt;/code&amp;gt;&lt;br /&gt;
|viewer-relative&lt;br /&gt;
|Not really summed into the field — a proximity override. Every viewer sees a small radius with nothing equipped, and an equipped light replaces it with a much larger one. Rendered as one more additive glow so moonlight reads as navigable up close, not just as a number the visibility rule checks.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== How a value gets from terrain edit to pixel ==&lt;br /&gt;
The static half is computed once and streamed; the dynamic half is republished every frame. Nothing about the sun&amp;#039;s position is ever sent over the wire at all.&lt;br /&gt;
&lt;br /&gt;
# &amp;#039;&amp;#039;&amp;#039;Bake&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;structure.rs&amp;lt;/code&amp;gt; reruns both BFS passes whenever a chunk rebuilds: a terrain edit, a placed light added or removed.&lt;br /&gt;
# &amp;#039;&amp;#039;&amp;#039;Wire&amp;#039;&amp;#039;&amp;#039; — quantised onto &amp;lt;code&amp;gt;SheetData&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;darkness_q&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;placed_light_q&amp;lt;/code&amp;gt;, sent with the rest of a chunk at load time.&lt;br /&gt;
# &amp;#039;&amp;#039;&amp;#039;Cache&amp;#039;&amp;#039;&amp;#039; — client-side &amp;lt;code&amp;gt;SurfaceComponents&amp;lt;/code&amp;gt; holds both fields per face, exposed through &amp;lt;code&amp;gt;FaceLight.TotalLight&amp;lt;/code&amp;gt;.&lt;br /&gt;
# &amp;#039;&amp;#039;&amp;#039;Compose&amp;#039;&amp;#039;&amp;#039; — every frame: cached fields × the clock&amp;#039;s live &amp;lt;code&amp;gt;sky_light(t)&amp;lt;/code&amp;gt;, plus whatever &amp;lt;code&amp;gt;CutawayController&amp;lt;/code&amp;gt; is publishing that frame.&lt;br /&gt;
# &amp;#039;&amp;#039;&amp;#039;Shade&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;Cutaway.hlsl&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;BtsLighting.hlsl&amp;lt;/code&amp;gt; scale scene lighting by darkness and add the lantern, carried and innate terms on top.&lt;br /&gt;
&lt;br /&gt;
== The visibility rule ==&lt;br /&gt;
A face&amp;#039;s light is also what decides whether an &amp;#039;&amp;#039;&amp;#039;NPC, player, or ground item&amp;#039;&amp;#039;&amp;#039; can be seen at all — terrain, walls and objects never hide, only shade dark. Two independent routes to visible, whichever is met first:&lt;br /&gt;
&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Proximity&amp;#039;&amp;#039;&amp;#039; — distance(viewer, target) ≤ viewerRadius → visible, regardless of light.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Illumination&amp;#039;&amp;#039;&amp;#039; — light(face, t) ≥ threshold (0.5) → visible, at any distance.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Hysteresis&amp;#039;&amp;#039;&amp;#039; — the threshold shifts ±0.05 toward whatever the viewer already believed, so a value sitting right on the line can&amp;#039;t flicker every frame it&amp;#039;s re-evaluated.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;A light gives you away.&amp;#039;&amp;#039;&amp;#039; Your own torch lights your own tile, which is exactly the face the illumination route reads — so carrying one lifts you above the threshold for everyone else. Nothing special-cased; it falls straight out of the model, which is what makes carrying a torch a real trade-off instead of a strict upgrade.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;The one deliberate asymmetry:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;sees_in_dark&amp;lt;/code&amp;gt; (an NPC-type flag, off by default) will govern only whether a monster can notice a player in darkness — never the reverse. A player&amp;#039;s sight of an NPC is always the same uniform rule. A predator that aggroes you in pitch dark while you can&amp;#039;t see it back is the horror beat this buys for free; slipping past a guard without it is the stealth beat, from the same field. The flag exists; nothing reads it yet — see Phase 5 below.&lt;br /&gt;
&lt;br /&gt;
== Where each phase stands ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!#&lt;br /&gt;
!Phase&lt;br /&gt;
!Status&lt;br /&gt;
!Notes&lt;br /&gt;
|-&lt;br /&gt;
|1&lt;br /&gt;
|Sky access × clock&lt;br /&gt;
|&amp;#039;&amp;#039;&amp;#039;Shipped&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|Ambient light composes the baked field against the shared clock instead of a flat day/night multiplier. A cave is finally dark at noon.&lt;br /&gt;
|-&lt;br /&gt;
|2&lt;br /&gt;
|Placed lights, baked&lt;br /&gt;
|&amp;#039;&amp;#039;&amp;#039;Shipped&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|Standing torches and braziers bake into the field and actually light something — rendered through the same 4-slot array carried lights use, as an interim measure ahead of a real per-vertex channel.&lt;br /&gt;
|-&lt;br /&gt;
|3&lt;br /&gt;
|Many lights&lt;br /&gt;
|&amp;#039;&amp;#039;&amp;#039;Skipped&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|Uncapping the 4-slot array into a real per-chunk light list. Needs a measurement — a realistic crowd of concurrent carried lights — nobody has taken yet.&lt;br /&gt;
|-&lt;br /&gt;
|4&lt;br /&gt;
|Visibility, client-side&lt;br /&gt;
|&amp;#039;&amp;#039;&amp;#039;Shipped &amp;amp;#x26; kept&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|NPCs, players and ground items fade below the threshold. Played, felt fun, cleared its own checkpoint. Deliberately cheatable — client-only — until Phase 5.&lt;br /&gt;
|-&lt;br /&gt;
|5&lt;br /&gt;
|Visibility, authoritative&lt;br /&gt;
|&amp;#039;&amp;#039;&amp;#039;Foundation only&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|Schema (&amp;lt;code&amp;gt;sees_in_dark&amp;lt;/code&amp;gt;), the light math ported to Rust, and the visibility predicate all exist and are unit tested — but nothing yet decides what a viewer is actually &amp;#039;&amp;#039;sent&amp;#039;&amp;#039;, or rejects an attack against something unseen. Today&amp;#039;s spawn/despawn delivery only reacts to movement; light changes continuously with the clock even when nobody moves, which that system has never had to notice.&lt;br /&gt;
|-&lt;br /&gt;
|6&lt;br /&gt;
|Tuning + editor pass&lt;br /&gt;
|&amp;#039;&amp;#039;&amp;#039;Not started&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|Threshold, innate radius, moonlight floor, ramp shape and falloff sharpness all want tuning together, in the editor, rather than as scattered constants.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Where it lives ==&lt;br /&gt;
&lt;br /&gt;
=== Server ===&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;bts-map/src/structure.rs&amp;lt;/code&amp;gt; — both BFS bakes: sky access, placed light&lt;br /&gt;
* &amp;lt;code&amp;gt;bts-content/src/world_clock.rs&amp;lt;/code&amp;gt; — &amp;lt;code&amp;gt;sky_light(t)&amp;lt;/code&amp;gt;, ported to Rust&lt;br /&gt;
* &amp;lt;code&amp;gt;bts-map/src/query.rs&amp;lt;/code&amp;gt; — &amp;lt;code&amp;gt;Map::sky_access_at&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;placed_light_at&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;bts-server/src/world/visibility.rs&amp;lt;/code&amp;gt; — &amp;lt;code&amp;gt;is_visible&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;distance_tiles&amp;lt;/code&amp;gt; — Phase 5, unwired&lt;br /&gt;
* &amp;lt;code&amp;gt;bts-content/src/schema.rs&amp;lt;/code&amp;gt; — &amp;lt;code&amp;gt;is_light_source&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;light_radius&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;sees_in_dark&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Client ===&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Core/World/FaceLight.cs&amp;lt;/code&amp;gt; — the composition formula, engine-free&lt;br /&gt;
* &amp;lt;code&amp;gt;Core/World/Visibility.cs&amp;lt;/code&amp;gt; — &amp;lt;code&amp;gt;IsVisible&amp;lt;/code&amp;gt; + the fade state machine&lt;br /&gt;
* &amp;lt;code&amp;gt;Core/World/WorldDaylight.cs&amp;lt;/code&amp;gt; — &amp;lt;code&amp;gt;sky_light(t)&amp;lt;/code&amp;gt; curve, the original&lt;br /&gt;
* &amp;lt;code&amp;gt;World/Rendering/CutawayController.cs&amp;lt;/code&amp;gt; — shader globals, lantern, flicker&lt;br /&gt;
* &amp;lt;code&amp;gt;Shaders/BtsLighting.hlsl&amp;lt;/code&amp;gt; — GPU-side composition and falloff&lt;br /&gt;
&lt;br /&gt;
== Decisions worth knowing ==&lt;br /&gt;
&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Full fog of war is the destination, not a client trick.&amp;#039;&amp;#039;&amp;#039; Phase 5&amp;#039;s whole point is that the server withholds what a viewer can&amp;#039;t see — not cheatable by turning your own brightness up. Phase 4, today, still is: a modified client could ignore the fade entirely.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Only living things and loot hide.&amp;#039;&amp;#039;&amp;#039; Terrain, walls and placed objects always stream and always draw, shaded dark. Hiding architecture would reproduce the exact &amp;quot;world is missing&amp;quot; failure this system exists to fix.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Losing sight fades, it doesn&amp;#039;t pop.&amp;#039;&amp;#039;&amp;#039; The client eases an entity out over a third of a second rather than snapping it away — and there&amp;#039;s no last-known-position ghost. Nothing false is ever drawn.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Falloff is one shared shape, authored per light.&amp;#039;&amp;#039;&amp;#039; The same smoothstep curve is mirrored in C#, HLSL and Rust; an authored &amp;lt;code&amp;gt;light_falloff_sharpness&amp;lt;/code&amp;gt; reshapes it per torch type rather than forking the curve itself.&lt;br /&gt;
&lt;br /&gt;
----&amp;#039;&amp;#039;Source: &amp;lt;code&amp;gt;docs/initiatives/light-and-visibility-initiative.md&amp;lt;/code&amp;gt;&amp;#039;&amp;#039;&lt;/div&gt;</summary>
		<author><name>Bembi</name></author>
	</entry>
</feed>