perf(paint): stronger X-ray hit flash + kill spawn/respawn freeze

X-ray hit flash: drive a dedicated flash mode instead of the normal
reveal - pulse off (steady), intensity boost (xrayFlashBoost, default 3x),
snap-on, then restore authored color/pulse exactly. Fixes the weak/dark
flicker where the 0.35s hit window often caught only the pulse's near-off
plateau.

Spawn/respawn freeze: pool GPU RenderTextures across despawn->respawn
(TeardownGpu recycles, CreateRT pops) so a hider->hunter conversion no
longer churns ~70MB of RT alloc/free on every observer; reuse staging
textures to cut per-map GC. Defer the blank base-seed on reveal-gated
remote replicas (DeferInitialSeed) - the real pixels arrive with the
snapshot apply anyway; gate the FurShellRenderer alongside the body so a
deferred replica never shows the pooled RT's stale fur; EnsureBackendSeeded
+ TryCaptureSnapshot guard against capture-before-reseed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-04 15:14:20 +02:00
1 parent 4ceaf85c8f
commit 48975c0f20
9 files changed
+196 -24

No files matched your search

+19 -8
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using Chameleon.Components;
/// <summary>
/// Per-avatar network glue on the spawned player. The local owner binds the scene singletons
@@ -102,6 +103,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
public void MarkConversionSpawn(Vector3 worldPos) => _conversionSpawnPos = worldPos;
private Renderer[] _bodyRenderers; // skinned body mesh(es), toggled by the per-client visibility pass
private FurShellRenderer[] _furRenderers; // fur draws via a custom GPU-instanced pass (NOT a Renderer); gated with the body so the avatar reveals atomically and never shows stale pooled fur
private bool _visibilityApplied;
private bool _lastVisible = true;
private bool _renderGate; // conversion: body force-hidden until the fresh hunter has settled into place
@@ -113,6 +115,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
private PlayerCharacterController _controller; // sibling, cached (the freeze re-asserts per frame)
private bool _xrayApplied;
private bool _lastXRay;
private bool _lastFlash;
private bool _frozenApplied;
private bool _lastFrozen;
private float _xrayFlashUntil; // Time.time deadline for the just-shot X-ray flash (local per client)
@@ -122,6 +125,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
{
_active.Add(this); // scoreboard registry (paired with OnNetworkDespawn)
_bodyRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
_furRenderers = GetComponentsInChildren<FurShellRenderer>(true);
_painter = GetComponentInChildren<MeshPainter>(true);
_paintSync = GetComponentInChildren<PlayerPaintSync>(true);
_concealment = GetComponentInChildren<ConcealmentMeter>(true);
@@ -297,6 +301,12 @@ public class NetworkPlayerSetup : NetworkBehaviour
if (_bodyRenderers != null)
foreach (var r in _bodyRenderers)
if (r) r.enabled = visible;
// Fur is drawn by a custom GPU-instanced pass, NOT a Renderer, so the skinned-mesh toggle above
// misses it; gate it too (enabled=false unsubscribes its camera hook), or a deferred/reveal-gated
// body would show stale pooled fur strands while its own mesh is hidden.
if (_furRenderers != null)
foreach (var f in _furRenderers)
if (f) f.enabled = visible;
_lastVisible = visible;
_visibilityApplied = true;
}
@@ -353,20 +363,21 @@ public class NetworkPlayerSetup : NetworkBehaviour
private void UpdateXRayReveal()
{
if (!_painter) return;
bool reveal = ComputeXRay();
if (_xrayApplied && reveal == _lastXRay) return;
_painter.XRayRevealed = reveal;
// Just-shot flash: a hunter's hit lights this hider up for a brief window (server-broadcast). It drives
// a distinct, much stronger flash mode (steady + boosted), so it is tracked apart from the normal reveal.
bool flash = Time.time < _xrayFlashUntil;
bool reveal = flash || ComputeXRay();
if (_xrayApplied && reveal == _lastXRay && flash == _lastFlash) return;
_painter.SetXRayReveal(reveal, flash);
_lastXRay = reveal;
_lastFlash = flash;
_xrayApplied = true;
}
// The X-ray reveal predicate for this avatar on the local machine.
// The normal (non-flash) X-ray reveal predicate for this avatar on the local machine. The just-shot flash
// is OR-combined by the caller so it can select the stronger flash mode.
private bool ComputeXRay()
{
// Just-shot flash: a hunter's hit lights this hider up for a brief window (server-broadcast), on top
// of the normal reveal triggers, so the moment of the hit reads before the hider leaves play.
if (Time.time < _xrayFlashUntil) return true;
// Buried-hider reveal: already gated to hider + running match on the owner before it is published.
if (_embeddedReveal.Value) return true;
@@ -158,6 +158,12 @@ public class PlayerPaintSync : NetworkBehaviour
// Remote replica: never paint from this machine's mouse; paint only from replicated data.
_painter.PaintingEnabled = false;
// Skip the wasted base seed on the initial backend build: this reveal-gated body stays hidden
// until the painting snapshot is applied (which re-seeds the backend), so seeding grey/white at
// Start only stalls this observer's spawn frame. Set BEFORE the painter's Start (OnNetworkSpawn
// runs first), so the deferral takes effect.
_painter.DeferInitialSeed = true;
// Track the owner's emission intensity; seed the current (late-join) value right away.
_emissionIntensity.OnValueChanged += OnEmissionChanged;
_painter.emissionIntensity = _emissionIntensity.Value;