Skip to content

Fix FOV/vision system: hard black mask, real occlusion, subsystem wiring - #11

Merged
henkhooft merged 23 commits into
developfrom
claude/fov-shader-darkness-pc3jai
Jul 17, 2026
Merged

henkhooft merged 23 commits into
developfrom
claude/fov-shader-darkness-pc3jai

Conversation

@henkhooft

@henkhooft henkhooft commented Jul 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

Gets the client FOV/fog-of-war system (ported earlier from RE-SS3D#1491 onto the URP/tilemap rework) actually working: it was rendering nothing at all, and once fixed, was leaking vision through walls and only lightly shadowing hidden areas instead of hiding them.

This branch was also brought up to date with develop (merging in the intervening atmospherics, selection, and tile-system work) before these fixes were made.

PR checklist

  • The game builds properly without errors.
  • No unrelated changes are present.
  • No "trash" files are committed.
  • Relevant code is documented.
  • Update the related design or architecture doc in Documents/, or create one if needed.

Testing

  1. Enter Play mode as host or client (vision is client-side; it no-ops on a dedicated IsServerOnly server).
  2. Spawn/possess a character - vision binds to the local player via the existing LocalPlayerObjectChanged event.
  3. Areas outside the vision cone/range should be fully opaque black, not a partial shadow.
  4. Walking around, walls should correctly hide what's behind them (including another wall behind a nearer one), while doors opening should unblock vision without any extra action.
  5. VisionSubSystem's obstacleMask Inspector field controls which layers raycasts can hit; hits are then filtered down to walls/doors only (see IsVisionOccluder) so tall furniture doesn't incorrectly occlude and open airlock trigger volumes don't darken FOV.

Changes

  • Vertical UV flip in depth reconstruction (Vision.hlsl): VisionClipToWorld rebuilt world position from the fullscreen triangle's texture UV without correcting for UNITY_UV_STARTS_AT_TOP, so on D3D/Vulkan/Metal the reconstructed position was mirrored vertically before being projected onto the XZ polar map - desyncing the vision mask from the actual geometry under each pixel. Same class of bug already fixed once in AtmosCommon.hlsl's AtmosUnproject; mirrored the same guard here.
  • VisionSubSystem was never instantiated anywhere - it existed as a script and the URP renderer feature/shaders were fully wired into the pipeline asset, but nothing created the component (SubSystems.Get<T>() is a FindObjectOfType lookup, it doesn't auto-create). Added a RuntimeInitializeOnLoadMethod self-bootstrap mirroring ScreenEffectsSubSystem's existing pattern; a scene instance was also added directly to Game.unity.
  • Hard black mask instead of a soft fog: the old composite blended unseen pixels toward black at a tunable _VisionFogStrength (0.9) and ran a 25-direction x 5-sample AO-style blur per pixel (also softening the visible side of the boundary). Replaced with a straight binary composite - fully opaque black outside the mask, source color inside, nothing in between - and dropped the now-dead blur/fog-strength knobs.
  • Real occlusion instead of the CPU tile grid: the original VisionGridCaster/VisionOcclusionProvider walked an abstract per-tile boolean grid with no notion of real wall shape, so a wall behind another wall (or anything whose silhouette didn't match the grid abstraction) leaked through as visible. Tried a GPU depth-cubemap capture from the player's position first (mirroring AtmosCommon.hlsl's AtmosIsSampleOccluded fix for the same class of bug in atmos), but landed on a RaycastCommand/Physics.Raycast batch from Entity.ViewPoint instead - each ray iteratively skips furniture/props (TryFindNearestOccluder) until it hits the nearest actual wall/door, capped at 64 skips so a dense prop pile can't exhaust a fixed hit buffer and falsely report a clear line of sight through a wall. Windows stay see-through, trigger volumes (e.g. airlock open triggers) are ignored, and a new Walls physics layer plus similar-depth neighbor dilation address angular-resolution stripe artifacts.
  • Documented the final approach in Documents/architecture/systems/rendering.md.

Related issues/PRs

Continues the vision-system port from RE-SS3D#1491.


Generated by Claude Code

BroodjeKipcorn and others added 22 commits July 7, 2026 10:30
Cherry-pick vision shaders, subsystem, and editor tooling as reference
material; apply client-only guard, detection offset wiring, and null
graphics teardown fix ahead of grid casting and URP port.
Extend tile occupancy for turf walls, doors, and per-edge adjacency
blocking; add VisionOcclusionProvider cache with mutation invalidation,
VisionGridCaster for polar depth samples, and edit-mode coverage.
Add Vision.hlsl, URP mask/blur/space shaders, VisionRendererFeature with
mask and blur passes, wire Forward+ renderer and player camera depth;
remove PPv2 VisionMaskEffect.
Move occupancy evaluation into TileOccupancyEvaluator, restore TileOccupancy
to SS3D.Systems.Tile, and fix URP MSAA enum usage, C# 9 struct init,
Coimbra Dispose, and edit-mode test helpers.
Remove the Blit.hlsl include from the vision mask/blur shaders, which
redefined Varyings/Vert and prevented the shaders from compiling, so the
fullscreen fog rendered garbage over the whole screen.

Reconstruct world position from a CPU-supplied camera inverse
view-projection matrix instead of UNITY_MATRIX_I_VP, since the bound VP in
a blit pass is the blitter matrix rather than the camera's. Add a
diagnostic debug-mask toggle that outputs the raw visibility mask.
Replace the physics raycast depth producer in VisionSubSystem with the
tile-grid shadowcaster bound to the tilemap occlusion provider. Since
TileMap/TileChunk are server-only, build a client-local TileMap populated
by replicated PlacedTileObjects so the grid caster can query occupancy on
remote clients; on a host the authoritative server map is used directly.
Replace the 8-connected Bresenham walk in VisionGridCaster with a float
DDA that marches from the real world origin and only crosses cardinal
cell boundaries. Fixes open plenums rendering a cross shape (diagonal
steps were always treated as blocking edges) and the pulsating effect
when moving between tiles (distances were measured from a tile-snapped
origin). Remove the now-unused distance/line helpers.
Sync airlock open/closed state via IDynamicTileOccupant and refresh
tilemap occupancy when it changes so vision unblocks open doors. Add
OnTileStateChanged observer notifications and a tunable fog strength
(default 0.9) for stronger out-of-vision darkening.
…darkness-pc3jai

# Conflicts:
#	Assets/Scripts/SS3D/Systems/Furniture/AirLockOpener.cs
#	Assets/Scripts/SS3D/Systems/Tile/IDynamicTileOccupant.cs.meta
#	Assets/Scripts/SS3D/Systems/Tile/PlacedObjects/PlacedTileObject.cs
#	Assets/Scripts/SS3D/Systems/Tile/TileOccupancyEvaluator.cs
#	Assets/Scripts/SS3D/Systems/Tile/TileOccupancyEvaluator.cs.meta
#	Assets/Scripts/SS3D/Systems/Tile/TileSubSystem.cs
#	Assets/Settings/URP/SS3D_ForwardPlusRenderer.asset
VisionClipToWorld rebuilt world position from the fullscreen triangle's
texture UV without accounting for UNITY_UV_STARTS_AT_TOP, so on D3D/
Vulkan/Metal the reconstructed position was mirrored vertically before
being projected onto the XZ polar depth map. That desynced the vision
mask from the actual geometry under each pixel, so occluded areas only
picked up a partial/misaligned shadow instead of being fully dark.
AtmosCommon.hlsl's AtmosUnproject already guards against this; mirror
the same fix in Vision.hlsl.

Also drops VisionCG.cginc, a dead built-in-pipeline leftover from
before the URP port that nothing includes.
VisionSubSystem was never placed on any GameObject in any scene or
prefab, so SubSystems.Get<VisionSubSystem>() (a FindObjectOfType
lookup, it doesn't auto-create) always failed silently and
VisionRenderContext.Enabled never flipped on - the renderer feature
was fully wired up but had nothing driving it. Self-bootstrap via
RuntimeInitializeOnLoadMethod, mirroring ScreenEffectsSubSystem's
existing pattern for the same reason: hand-editing scene YAML outside
the Editor isn't safe.
Add the vision subsystem to the scene
Unseen tiles should tell the player nothing about what's there - full
opaque black, not a partial shadow. Replace the tunable _VisionFogStrength
blend and the AO-style multi-directional blur (125 texture samples and a
world-position reconstruction per pixel) with a straight binary composite:
sample the mask, output black or the source color, nothing in between.

Drops the now-dead blur quality/directions/size and fog strength knobs
from VisionRendererFeature and the renderer asset.
The CPU tile-grid raycaster (VisionGridCaster/VisionOcclusionProvider)
only knew "does this tile have BlocksVision", a coarse per-tile boolean
with no notion of real wall shape - so a wall behind another wall, or
any geometry whose silhouette didn't line up with the grid abstraction,
could leak through as visible.

Replace it with a real GPU depth capture: VisionOcclusionCapture renders
a small depth cubemap from the player's position each frame (skipping
the unused top/bottom faces, since vision queries are always horizontal)
using Camera.RenderToCubemap, and Vision.hlsl now samples that cubemap
directly for "distance to nearest occluder in this direction" instead of
the old CPU-built _VisionMap texture. This is the same fix already
applied to atmos's wall-occlusion bleed (AtmosCommon.hlsl's
AtmosIsSampleOccluded) - trust the real rendered depth, not a flattened
world-XZ abstraction. No physics raycasts involved, so this stays cheap.

VisionSubSystem no longer needs any tile-system dependency at all for
occlusion - door open/close now "just works" since the capture reads
whatever geometry is actually there each frame, no explicit
tile-mutation notification needed. Drops VisionGridCaster,
VisionOcclusionProvider, VisionEdgeMask, and their EditMode tests.

Needs Editor verification: the occluderMask default excludes
Characters/BodyParts/Items so the capture (positioned at the player)
doesn't self-occlude, but may need tuning to just wall/structure layers
depending on how this project's layers are laid out.
TextureDimension lives in UnityEngine.Rendering, not UnityEngine.
Camera teardown also needs Coimbra's GameObject.Dispose(bool) instead
of Object.Destroy, per this project's Coimbra lint rule (COIMBRA0008).
Unity forbids LayerMask.GetMask/NameToLayer in a MonoBehaviour field
initializer - it runs as part of the constructor, before the engine
context needed for the layer lookup exists. It threw on every
construction, which also skipped every later field initializer
(_occlusionCapture never got assigned), cascading into null refs in
OnAwake/OnDisabled. Compute the default lazily in OnAwake instead,
applied only when the field is still at its unset default.
The depth-cubemap occluder blacked out nearly all visibility and could not
separate walls from floors on shared layers. Bring back the grid caster
and _VisionMap producer so FOV matches tile BlocksVision again.
Replace tile-grid casting with batched RaycastCommands so only walls/doors
block vision, and tighten polar sampling plus toon depth passes to cut
stripe jitter and plenum-through-wall leaks.
Airlock proximity volumes stay in the doorway when open; only animated
door-leaf colliders should occlude.
Inventory icon RTs were getting the FOV mask; denser polar sampling and
depth dilation keep flat walls from shimmering at glancing angles.
Replace the capped multi-hit batch with iterative casts that skip
furniture until the nearest wall/door, so prop piles no longer punch
vision holes through walls.
…darkness-pc3jai

# Conflicts:
#	Assets/Content/Scenes/Game.unity
#	Assets/Settings/URP/SS3D_ForwardPlusRenderer.asset
@henkhooft
henkhooft merged commit f3b26a2 into develop Jul 17, 2026
1 check failed
@henkhooft
henkhooft deleted the claude/fov-shader-darkness-pc3jai branch July 17, 2026 22:13

This branch had an error being deployed

1 failed deployment
unity_tests — e5e5fd82 Deployed Jul 17, 2026 by henkhooft via Run tests #311
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants