Add see-through X-ray reveal overlay for players
Add a second, additive HDR emission color (_XRayColor) that reveals a player through walls as a pulsing energy glow, driven during matches: - Shader: XRayOverlay pass (ZTest Greater, additive) for occluded parts plus a matching additive term in the forward fragment for visible parts, so the glow shows everywhere and never double-blends. Exposes _XRayColor, _XRayPulseSpeed and _XRayPulseStrength for a breathing pulse. - Render it after opaques via a RenderObjects feature (custom XRayReveal light-mode tag) on both PC and Mobile renderers, so the through-wall test is depth-sort independent and no longer drops out at some camera angles/distances. - MeshPainter drives _XRayColor alpha through a framerate-independent fade (xrayFadeDuration) rather than a hard toggle. - Reveal is driven per avatar on every client: all remaining un-found hiders glow during the Resolution phase, and a hider whose ConcealmentMeter EmbeddedPercent exceeds MatchManager.embeddedRevealPercent glows via an owner-authoritative NetworkVariable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7104f9314
commit
c54e0cf502
8 files changed
+338
-13
No files matched your search
@@ -145,11 +145,23 @@ public class MatchManager : MonoBehaviour
|
||||
[Min(0f)]
|
||||
[SerializeField] private float shootCooldown = 0.4f;
|
||||
|
||||
[Header("Concealment reveal")]
|
||||
[Tooltip("A hider whose ConcealmentMeter EmbeddedPercent reaches this value (while a match is running) is " +
|
||||
"revealed through walls with the material's X-ray glow, so nobody can hide by burying itself deep " +
|
||||
"inside geometry. 0..100; 100 effectively disables it (never reached). Read owner-side per avatar.")]
|
||||
[Range(0f, 100f)]
|
||||
[SerializeField] private float embeddedRevealPercent = 80f;
|
||||
|
||||
/// <summary>Range of the hunter's shoot raycast (read by <see cref="FirstPersonMode"/>).</summary>
|
||||
public float ShootRange => shootRange;
|
||||
/// <summary>Hunter shoot cooldown in seconds (read by <see cref="FirstPersonMode"/>).</summary>
|
||||
public float ShootCooldown => shootCooldown;
|
||||
|
||||
/// <summary>EmbeddedPercent [0..100] a hider must reach for the through-walls X-ray reveal to switch on
|
||||
/// (only while a match is running). Read by each owner's <see cref="NetworkPlayerSetup"/> against its own
|
||||
/// concealment meter; baked identically on every client, so it needs no replication.</summary>
|
||||
public float EmbeddedRevealPercent => embeddedRevealPercent;
|
||||
|
||||
/// <summary>Server-local phase guard. Not replicated - see class summary.</summary>
|
||||
public MatchPhase Phase { get; private set; } = MatchPhase.Lobby;
|
||||
|
||||
|
||||
@@ -260,4 +260,19 @@ public partial class MeshPainter
|
||||
if (_materialInstance && _materialInstance.HasProperty(EmissionColorId))
|
||||
_materialInstance.SetColor(EmissionColorId, Color.white * Mathf.Max(0f, emissionIntensity));
|
||||
}
|
||||
|
||||
/// <summary>Eases the see-through X-ray glow's alpha toward its on/off target (<see cref="XRayRevealed"/>)
|
||||
/// at a constant rate, so the reveal fades in/out over <c>xrayFadeDuration</c> seconds instead of popping.
|
||||
/// Rgb is left untouched (the authored glow colour). Only writes the material on an actual change, and
|
||||
/// snaps instantly when the duration is 0.</summary>
|
||||
private void TickXRayFade()
|
||||
{
|
||||
if (!_materialInstance || !_materialInstance.HasProperty(XRayColorId)) return;
|
||||
Color c = _materialInstance.GetColor(XRayColorId);
|
||||
float step = xrayFadeDuration > 0f ? Time.deltaTime / xrayFadeDuration : 1f;
|
||||
float a = Mathf.MoveTowards(c.a, _xrayTarget, step);
|
||||
if (a == c.a) return; // already at target -> no material write
|
||||
c.a = a;
|
||||
_materialInstance.SetColor(XRayColorId, c);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,25 @@ public partial class MeshPainter : MonoBehaviour
|
||||
set { if (_materialInstance && _materialInstance.HasProperty(ParallaxId)) _materialInstance.SetFloat(ParallaxId, Mathf.Max(0f, value) * ParallaxBase); }
|
||||
}
|
||||
|
||||
/// <summary>Drives the see-through-walls X-ray overlay (see <c>PlayerCamoLit</c>): sets the TARGET for
|
||||
/// the ALPHA of the authored <c>_XRayColor</c>, so the rgb glow colour set on the material is preserved
|
||||
/// (target 0 = off/no effect, 1 = full glow). The material alpha eases toward this target over
|
||||
/// <see cref="xrayFadeDuration"/> seconds in <c>Update</c> (a smooth fade instead of a hard pop); set the
|
||||
/// duration to 0 for an instant toggle. This is a per-avatar runtime reveal, NOT a painted channel: it
|
||||
/// neither persists with the painting nor round-trips through the HUD. <see cref="NetworkPlayerSetup"/>
|
||||
/// raises it for the remaining hiders during the resolution phase and for a hider that buries itself
|
||||
/// too deep in geometry (over the concealment threshold).</summary>
|
||||
public bool XRayRevealed
|
||||
{
|
||||
get => _xrayTarget > 0f;
|
||||
set => _xrayTarget = value ? 1f : 0f;
|
||||
}
|
||||
|
||||
[Tooltip("Seconds for the see-through X-ray reveal glow to fade fully in or out when toggled (0 = instant snap).")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float xrayFadeDuration = 0.3f;
|
||||
private float _xrayTarget; // desired _XRayColor alpha (0/1); the material eases toward it each frame
|
||||
|
||||
[Header("Backend")]
|
||||
[Tooltip("ON = GPU painting (fast), OFF = CPU painting. Can be toggled live in Play mode.")]
|
||||
public bool useGpu = true;
|
||||
@@ -361,6 +380,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private static readonly int HalfLambertId = Shader.PropertyToID("_HalfLambert");
|
||||
private static readonly int HalfLambertBrightnessId = Shader.PropertyToID("_HalfLambertBrightness");
|
||||
private static readonly int SpecularDampenId = Shader.PropertyToID("_SpecularDampen");
|
||||
private static readonly int XRayColorId = Shader.PropertyToID("_XRayColor");
|
||||
private static readonly int BrushColorId = Shader.PropertyToID("_BrushColor");
|
||||
private static readonly int ColorWriteMaskId = Shader.PropertyToID("_ColorWriteMask");
|
||||
private static readonly int BrushCenterId = Shader.PropertyToID("_BrushCenter");
|
||||
@@ -404,6 +424,10 @@ public partial class MeshPainter : MonoBehaviour
|
||||
if (_materialInstance.HasProperty(BaseColorId)) _materialInstance.SetColor(BaseColorId, Color.white);
|
||||
else if (_materialInstance.HasProperty(ColorId)) _materialInstance.SetColor(ColorId, Color.white);
|
||||
|
||||
// Seed the X-ray reveal target from the authored alpha so the fade baseline matches the material
|
||||
// (authored 0 = off). NetworkPlayerSetup drives this target at runtime; Update eases the alpha to it.
|
||||
if (_materialInstance.HasProperty(XRayColorId)) _xrayTarget = _materialInstance.GetColor(XRayColorId).a;
|
||||
|
||||
BuildSeedColors();
|
||||
// Always start fresh from the material seed. Saved paintings are loaded explicitly from the
|
||||
// HUD library (MeshPainter.Persistence), never auto-loaded on start.
|
||||
@@ -695,6 +719,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
if (useGpu != _gpuActive) SwitchBackend(useGpu);
|
||||
|
||||
RefreshEmissionColor(); // live-tunable emission intensity
|
||||
TickXRayFade(); // ease the see-through X-ray glow toward its on/off target (runs on every avatar)
|
||||
|
||||
// Painting disabled by the game mode (e.g. player-control mode): do nothing.
|
||||
if (!PaintingEnabled)
|
||||
|
||||
@@ -81,6 +81,13 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
/// target that is already out of play.</summary>
|
||||
public bool IsFound => _isFound.Value;
|
||||
|
||||
// Owner-authoritative "this hider is buried too deep" flag. Only the OWNER runs the ConcealmentMeter
|
||||
// (it is disabled on remotes), so the owner compares its EmbeddedPercent against MatchManager's
|
||||
// threshold and writes this; every client reads it to switch the through-walls X-ray glow on/off for
|
||||
// this avatar. Stays false on hunters / the lobby avatar and whenever no match is running.
|
||||
private readonly NetworkVariable<bool> _embeddedReveal =
|
||||
new NetworkVariable<bool>(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
||||
|
||||
// Conversion-mode in-place respawn position. The owner holds authority over the NetworkTransform, so the
|
||||
// server's Instantiate position is NOT honoured on the owner (it would otherwise snap to the prefab origin,
|
||||
// 0,0,0). The server writes the tagged world position here in its OnNetworkSpawn so it rides the initial
|
||||
@@ -104,9 +111,16 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
private bool _lastVisible = true;
|
||||
private bool _renderGate; // conversion: body force-hidden until the fresh hunter has settled into place
|
||||
|
||||
private MeshPainter _painter; // owns the per-avatar material instance carrying _XRayColor
|
||||
private ConcealmentMeter _concealment; // owner-only embedded-percent source (disabled on remotes)
|
||||
private bool _xrayApplied;
|
||||
private bool _lastXRay;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
_bodyRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||
_painter = GetComponentInChildren<MeshPainter>(true);
|
||||
_concealment = GetComponentInChildren<ConcealmentMeter>(true);
|
||||
_isFound.OnValueChanged += OnFoundChanged;
|
||||
if (_isFound.Value) ApplyFound(); // defensive: an avatar is never normally spawned already-found
|
||||
|
||||
@@ -142,10 +156,10 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
if (cc) cc.enabled = false;
|
||||
|
||||
// Owner cleanup: the concealment meter is a per-frame skinned-mesh bake + voxelization +
|
||||
// physics overlap whose result only feeds the LOCAL player (HUD / gameplay). A remote avatar's
|
||||
// concealment is computed on its own owner's machine, so running it here is pure wasted CPU.
|
||||
var concealment = GetComponentInChildren<ConcealmentMeter>(true);
|
||||
if (concealment) concealment.enabled = false;
|
||||
// physics overlap whose result only feeds the LOCAL player (HUD / gameplay + the embedded-reveal
|
||||
// flag). A remote avatar's concealment is computed on its own owner's machine, so running it here
|
||||
// is pure wasted CPU.
|
||||
if (_concealment) _concealment.enabled = false;
|
||||
|
||||
// NOTE: SkinnedColliderBaker deliberately stays ENABLED on remotes. MeshPainter's stamp replay
|
||||
// (ApplyNetworkStamp -> DrawBrushGpu) rasterizes the baker's live, per-frame BakedMesh to map a
|
||||
@@ -219,7 +233,12 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void Update() => UpdateVisibility();
|
||||
private void Update()
|
||||
{
|
||||
UpdateVisibility();
|
||||
UpdateEmbeddedReveal();
|
||||
UpdateXRayReveal();
|
||||
}
|
||||
|
||||
// Per-client visibility: each machine decides whether to render THIS avatar's body from the LOCAL
|
||||
// viewer's perspective (hunter bodies are always visible; only hider bodies are ever hidden). A found
|
||||
@@ -286,6 +305,50 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
return true;
|
||||
}
|
||||
|
||||
// Owner-only. Publishes whether this hider is buried too deep to the replicated flag every client reads.
|
||||
// Only a Hider avatar during a running match qualifies (the anti-burrow reveal targets hiders); the meter
|
||||
// runs owner-side only, so this is where EmbeddedPercent is available. Cheap: a bool compare, one network
|
||||
// write only on an actual edge (NetworkVariable dirties on assignment, so guard against no-op writes).
|
||||
private void UpdateEmbeddedReveal()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
|
||||
bool over = false;
|
||||
var match = MatchManager.Instance;
|
||||
if (match && match.MatchActive && role == PlayerRole.Hider && !_isFound.Value &&
|
||||
_concealment && _concealment.HasResult)
|
||||
over = _concealment.EmbeddedPercent >= match.EmbeddedRevealPercent;
|
||||
|
||||
if (over != _embeddedReveal.Value) _embeddedReveal.Value = over;
|
||||
}
|
||||
|
||||
// Per-client. Switches this avatar's see-through-walls X-ray glow on/off by driving the material's
|
||||
// _XRayColor alpha (via MeshPainter). Two independent triggers, OR-combined: the replicated embedded
|
||||
// flag (a buried hider, from its owner) and the local resolution phase (every un-found hider is revealed
|
||||
// to everyone at the end). Change-guarded so the material is only touched on an actual transition.
|
||||
private void UpdateXRayReveal()
|
||||
{
|
||||
if (!_painter) return;
|
||||
bool reveal = ComputeXRay();
|
||||
if (_xrayApplied && reveal == _lastXRay) return;
|
||||
_painter.XRayRevealed = reveal;
|
||||
_lastXRay = reveal;
|
||||
_xrayApplied = true;
|
||||
}
|
||||
|
||||
// The X-ray reveal predicate for this avatar on the local machine.
|
||||
private bool ComputeXRay()
|
||||
{
|
||||
// Buried-hider reveal: already gated to hider + running match on the owner before it is published.
|
||||
if (_embeddedReveal.Value) return true;
|
||||
|
||||
// Resolution reveals every remaining (un-found) hider through walls, on every client.
|
||||
var match = MatchManager.Instance;
|
||||
return match && match.MatchActive &&
|
||||
match.LocalSubPhase == MatchManager.MatchSubPhase.Resolution &&
|
||||
role == PlayerRole.Hider && !_isFound.Value;
|
||||
}
|
||||
|
||||
// True if THIS machine's local player is a Hunter role variant (so it must not see hiders during hide).
|
||||
private static bool LocalViewerIsHunter()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user