- Laser.shader + HunterLaserBeam: additive HDR beam, fade in/out over opacity and thickness, code-driven LineRenderer, tunable emission multiplier. - FirstPersonMode: TryShoot computes world endpoint, fires beam with lateral muzzle offset (right) so it isn't dead-center from hunter view. - NetworkPlayerSetup: shot relay RPCs (owner->server->all) + hit-flash RPC (server->all), flash OR-branch in ComputeXRay. - MatchManager: hit reveal broadcasts X-ray flash first, delays hider removal by hitRevealDelay so the glow is visible before the body vanishes. - MeshPainter reseed: in-place fast path avoids full GPU backend teardown on snapshot apply (no ~70MB RT churn), fixes spawn/paint-load hitch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
165 lines
7.0 KiB
C#
165 lines
7.0 KiB
C#
using UnityEngine;
|
|
|
|
public partial class MeshPainter
|
|
{
|
|
// --- Backend selection / switching ---
|
|
|
|
private void SetupBackend(bool gpu, Color32[][] canonical)
|
|
{
|
|
// SetupGpu returns false if the shaders are missing and it fell back to CPU; honor
|
|
// the real outcome so _gpuActive never claims GPU while _gpu is null (would NRE on
|
|
// the next SwitchBackend / paint).
|
|
if (gpu)
|
|
{
|
|
_gpuActive = SetupGpu(canonical);
|
|
}
|
|
else
|
|
{
|
|
SetupCpu(canonical);
|
|
_gpuActive = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Switches the backend at runtime, carrying over all painted maps.</summary>
|
|
private void SwitchBackend(bool toGpu)
|
|
{
|
|
var canonical = new Color32[MAP_COUNT][];
|
|
for (int mi = 0; mi < MAP_COUNT; mi++) canonical[mi] = ReadCanonical(mi);
|
|
|
|
RebuildBackend(toGpu, canonical);
|
|
|
|
Debug.Log($"[MeshPainter] Backend switched -> {(toGpu ? "GPU" : "CPU")} painting.", this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Re-seeds the backend from externally supplied canonical maps (load / late-join snapshot / undo),
|
|
/// rebuilding the island/dilation edge padding for the new content. The input arrays are never mutated
|
|
/// (both seed paths clone before touching a map).
|
|
///
|
|
/// Fast path: when the backend is already built on this same type + size - the common runtime reseed
|
|
/// (remote snapshot apply, carry/library load, undo/redo) - the existing RTs/textures are re-filled in
|
|
/// place instead of a full teardown+rebuild. At 1024² a rebuild frees and re-creates nine ARGBHalf RTs
|
|
/// (~70 MB of GPU allocation) every call, which stalls the frame and shows up as a hitch on every
|
|
/// observer when a (freshly painted) avatar spawns. Re-seeding reuses the RTs (identical size/format)
|
|
/// for a bit-identical result. A pending GPU<->CPU switch or an un-built backend falls through to the
|
|
/// full rebuild.
|
|
/// </summary>
|
|
private void ReseedCanonical(Color32[][] canonical)
|
|
{
|
|
if (_setupValid)
|
|
{
|
|
if (useGpu && _gpuActive && _gpu != null) { ReseedGpuInPlace(canonical); _hasLastScreen = false; return; }
|
|
if (!useGpu && !_gpuActive && _cpu != null) { ReseedCpuInPlace(canonical); _hasLastScreen = false; return; }
|
|
}
|
|
RebuildBackend(useGpu, canonical);
|
|
}
|
|
|
|
/// <summary>Tears down the active backend and sets up the requested one on the given maps.</summary>
|
|
private void RebuildBackend(bool gpu, Color32[][] canonical)
|
|
{
|
|
if (_gpuActive) TeardownGpu();
|
|
else TeardownCpu();
|
|
SetupBackend(gpu, canonical);
|
|
_hasLastScreen = false;
|
|
}
|
|
|
|
/// <summary>Seed source for a map: the supplied canonical data when valid, else the material seed.</summary>
|
|
private Color32[] ResolveSeed(int mi, Color32[][] canonical)
|
|
{
|
|
return (canonical != null && canonical[mi] != null && canonical[mi].Length == textureSize * textureSize)
|
|
? canonical[mi] : _seedCanonical[mi];
|
|
}
|
|
|
|
// Backend-agnostic stroke dispatch (GPU vs CPU), shared by local painting and replayed net stamps.
|
|
|
|
private void BeginBackendStroke()
|
|
{
|
|
if (_gpuActive) BeginGpuStroke();
|
|
else ResetCpuDirty();
|
|
}
|
|
|
|
private void DrawBackendStamp(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
|
|
{
|
|
if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal, u, v, n);
|
|
else PaintStamp3D(pLocal, radiusLocal, u, v, n);
|
|
}
|
|
|
|
private void EndBackendStroke()
|
|
{
|
|
if (_gpuActive) EndGpuStroke();
|
|
else EndCpuStroke();
|
|
}
|
|
|
|
/// <summary>Reads a map from the active backend into canonical (material) layout.</summary>
|
|
private Color32[] ReadCanonical(int mi)
|
|
{
|
|
if (_gpuActive)
|
|
{
|
|
var gm = _gpu[mi];
|
|
// MSO: read the packed final mask RT (R=metallic, G=occlusion, A=smoothness).
|
|
// Others: the display RT. The mask map is linear data; color maps read sRGB.
|
|
if (mi == MAP_MSO) return ReadRtPixels(gm.final, linear: true);
|
|
return ReadRtPixels(gm.display, linear: MapIsLinear(mi));
|
|
}
|
|
return (Color32[])_cpu[mi].pixels.Clone();
|
|
}
|
|
|
|
// MSO and fur are DATA maps (raw values, no sRGB): MSO packs metallic/occlusion/smoothness, fur's R is
|
|
// painted strand length. Storing them linearly keeps the values undistorted for the shaders that read them.
|
|
private static bool MapIsLinear(int mi) => mi == MAP_MSO || mi == MAP_FUR;
|
|
|
|
// Material slots per map, as pre-hashed IDs (this runs per stroke end - no per-call allocations).
|
|
// MSO: one mask map drives three URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G) +
|
|
// _ParallaxMap (G = occlusion reused as a 0.5-centred height for parallax/relief).
|
|
// Built lazily on first use (runtime): the *Id fields live in the MeshPainter.cs partial and the
|
|
// static field-initializer order ACROSS partial files is unspecified - a plain field initializer
|
|
// here can run first and capture 0 for every id, so HasProperty(0) fails and no map ever binds
|
|
// (white defaults -> full-glossy + emissive). Deferring to first access sidesteps that ordering.
|
|
private static int[][] _mapPropIds;
|
|
private static int[][] MapPropIds => _mapPropIds ??= new[]
|
|
{
|
|
new[] { BaseMapId, MainTexId }, // MAP_ALBEDO
|
|
new[] { MetallicGlossMapId, OcclusionMapId, ParallaxMapId }, // MAP_MSO
|
|
new[] { EmissionMapId }, // MAP_EM
|
|
System.Array.Empty<int>(), // MAP_FUR: read by FurShellRenderer, not the base material
|
|
};
|
|
|
|
private void AssignMapTexture(int mi, Texture tex)
|
|
{
|
|
if (!_materialInstance) return;
|
|
foreach (int id in MapPropIds[mi])
|
|
if (_materialInstance.HasProperty(id)) _materialInstance.SetTexture(id, tex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The live fur map (R = painted strand length, linear data), for a sibling FurShellRenderer to sample
|
|
/// as _FurMap. GPU backend: the display RT (stable - fur skips the dilation ping-pong swap); CPU backend:
|
|
/// the Texture2D mirror. Null until the backend is set up.
|
|
/// </summary>
|
|
public Texture FurMapTexture
|
|
{
|
|
get
|
|
{
|
|
if (!_setupValid) return null;
|
|
if (_gpuActive) return _gpu != null ? _gpu[MAP_FUR]?.display : null;
|
|
return _cpu != null ? _cpu[MAP_FUR]?.tex : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The live albedo map (the painted camo), for a sibling FurShellRenderer to sample as the fur strand
|
|
/// colour so the fur wears the camo per texel. GPU backend: the current display RT (ping-pongs on the
|
|
/// dilation swap, so callers must re-fetch each frame); CPU backend: the Texture2D mirror. Null until the
|
|
/// backend is set up.
|
|
/// </summary>
|
|
public Texture AlbedoMapTexture
|
|
{
|
|
get
|
|
{
|
|
if (!_setupValid) return null;
|
|
if (_gpuActive) return _gpu != null ? _gpu[MAP_ALBEDO]?.display : null;
|
|
return _cpu != null ? _cpu[MAP_ALBEDO]?.tex : null;
|
|
}
|
|
}
|
|
}
|