Fix FOV/vision system: hard black mask, real occlusion, subsystem wiring - #11
Merged
Merged
Conversation
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
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Documents/, or create one if needed.Testing
IsServerOnlyserver).LocalPlayerObjectChangedevent.VisionSubSystem'sobstacleMaskInspector field controls which layers raycasts can hit; hits are then filtered down to walls/doors only (seeIsVisionOccluder) so tall furniture doesn't incorrectly occlude and open airlock trigger volumes don't darken FOV.Changes
Vision.hlsl):VisionClipToWorldrebuilt world position from the fullscreen triangle's texture UV without correcting forUNITY_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 inAtmosCommon.hlsl'sAtmosUnproject; mirrored the same guard here.VisionSubSystemwas 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 aFindObjectOfTypelookup, it doesn't auto-create). Added aRuntimeInitializeOnLoadMethodself-bootstrap mirroringScreenEffectsSubSystem's existing pattern; a scene instance was also added directly toGame.unity._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.VisionGridCaster/VisionOcclusionProviderwalked 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 (mirroringAtmosCommon.hlsl'sAtmosIsSampleOccludedfix for the same class of bug in atmos), but landed on aRaycastCommand/Physics.Raycastbatch fromEntity.ViewPointinstead - 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 newWallsphysics layer plus similar-depth neighbor dilation address angular-resolution stripe artifacts.Documents/architecture/systems/rendering.md.Related issues/PRs
Continues the vision-system port from RE-SS3D#1491.
Generated by Claude Code