Files
chameleon/Assets/Scripts/MeshPainter.Backend.cs
T
adminandClaude Opus 4.8 301f7e64cb Add Ctrl+Z/Ctrl+Y undo/redo with command-based network sync
Pointer-based per-painter history of the canonical maps with copy-on-write
sharing (only maps touched by a stroke are cloned). A stroke (press..release)
is one step; a Load resets the history to the loaded maps as the sole baseline.

Network sync replicates undo/redo as tiny checkpoint/restore commands instead
of broadcasting full map snapshots: every peer runs the same COW history keyed
by owner-assigned checkpoint ids and reproduces restores locally. A peer that
cannot reproduce a target (undo across its join boundary) requests a targeted
snapshot from the owner as fallback.

Hardening: ordered send FIFO keeps stamps + checkpoints + restores in
production order (a marker never overtakes the stamps it finalizes); stamp
overflow carries over to the next frame instead of being decimated, so remote
content matches the owner; remote always inserts the checkpoint id to stay
1:1 aligned. Owner-authoritative snapshot fallback replaces the silent
host no-op. Adds a logSync toggle (default on) tracing when each
checkpoint/restore/snapshot is sent, applied, or falls back.

Also includes paintings-panel layout tuning in PaintHudPanel.prefab
(inspector edits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:19:36 +02:00

88 lines
2.9 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);
if (_gpuActive) TeardownGpu();
else TeardownCpu();
SetupBackend(toGpu, canonical);
_hasLastScreen = false;
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)
{
if (_gpuActive) TeardownGpu();
else TeardownCpu();
SetupBackend(useGpu, canonical);
_hasLastScreen = false;
}
/// <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;
private string[] MapProps(int mi)
{
switch (mi)
{
case MAP_ALBEDO: return new[] { "_BaseMap", "_MainTex" };
// One mask map drives both URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G).
case MAP_MSO: return new[] { "_MetallicGlossMap", "_OcclusionMap" };
default: return new[] { "_EmissionMap" };
}
}
private void AssignMapTexture(int mi, Texture tex)
{
if (!_materialInstance) return;
foreach (var p in MapProps(mi))
{
int id = Shader.PropertyToID(p);
if (_materialInstance.HasProperty(id)) _materialInstance.SetTexture(id, tex);
}
}
}