Files
chameleon/Assets/Scripts/Components/MeshPainter/MeshPainter.Backend.cs
T
adminandClaude Opus 4.8 0d14980a76 Fix maps never binding due to partial-class static init order
MapPropIds (in MeshPainter.Backend.cs) referenced the *Id fields from the
MeshPainter.cs partial in its field initializer. Static field-initializer
order across partial-class files is unspecified in C#; after the script
reorg the composite array initialized before the *Id fields and captured 0
for every property id. HasProperty(0) is always false, so AssignMapTexture
bound no maps at all -> the shader fell back to its white default textures,
making every fresh avatar fully glossy (smoothness 1) and emissive.

Build MapPropIds lazily on first access (runtime, after all static
initializers) so the file order no longer matters. The per-stroke allocation
savings are preserved; only a single null-check per call is added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:23:37 +02:00

115 lines
4.6 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>
/// Tears down the active backend and rebuilds it from externally supplied canonical maps (load /
/// late-join snapshot / undo). Runs the island/dilation seeding path so edge padding is rebuilt for
/// the new content. The input arrays are never mutated (SetupBackend clones its seed).
/// </summary>
private void ReseedCanonical(Color32[][] canonical) => 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();
}
private static bool MapIsLinear(int mi) => mi == MAP_MSO;
// 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
};
private void AssignMapTexture(int mi, Texture tex)
{
if (!_materialInstance) return;
foreach (int id in MapPropIds[mi])
if (_materialInstance.HasProperty(id)) _materialInstance.SetTexture(id, tex);
}
}