Netcode: receiver-side paint reveal gate + fur pipette gating
Hide remote avatars until this client has actually applied a snapshot reaching the owner-published baseline cutoff, preventing the default grey/white material from flashing before a player's painting has loaded. - PlayerPaintSync: owner-authoritative _publishedCutoff NetworkVariable, receiver tracks _appliedCutoff; explicit blank publish for never-painted avatars so they still reveal. - NetworkPlayerSetup: cache _paintSync, hide body renderers on spawn, gate UpdateVisibility on PaintRevealReady. - MeshPainter.Pick: fur toggle now follows pickedHeight (own paint-mesh only), matching the Occlusion/Height pipette behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
49704448e9
commit
ed09d6a1b0
3 files changed
+47
-6
No files matched your search
@@ -8,8 +8,9 @@ public partial class MeshPainter
|
||||
[Header("Brush - pipette (eyedropper)")]
|
||||
[Tooltip("Pipette mode: a click samples the channel values at the hit point on ANY surface " +
|
||||
"(albedo, metallic, smoothness, emission) and adopts them into the brush (opacities/alphas " +
|
||||
"left untouched) instead of painting. Height (occlusion) is adopted and its toggle enabled " +
|
||||
"only when our own paint mesh is picked; any other surface disables the height toggle. On a " +
|
||||
"left untouched) instead of painting. Height (occlusion) and fur length are adopted and their " +
|
||||
"toggles enabled only when our own paint mesh is picked; any other surface disables those " +
|
||||
"toggles and keeps their values. On a " +
|
||||
"foreign object the data channels are read texel-exact from the material; albedo follows " +
|
||||
"'Pipette Albedo From Screen'. Held button picks once.")]
|
||||
public bool pipette = false;
|
||||
@@ -84,9 +85,10 @@ public partial class MeshPainter
|
||||
paintMetallicSmoothness = true;
|
||||
paintOcclusion = pickedHeight;
|
||||
paintEmission = true;
|
||||
// Fur toggle always turns on with a pick; the length is only adopted off our own paint mesh
|
||||
// (pickedHeight), so a foreign-surface pick enables fur but leaves furLength at its current value.
|
||||
fur = true;
|
||||
// Fur mirrors height/occlusion: adopted and its toggle enabled only when our OWN paint mesh was
|
||||
// picked (pickedHeight, where furLength was sampled); any other surface disables the fur toggle
|
||||
// and keeps furLength.
|
||||
fur = pickedHeight;
|
||||
|
||||
pipette = false;
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
private float _renderGateUntil; // Time.time deadline that lifts _renderGate; frame-driven (see UpdateVisibility) so a mid-settle disable/despawn can't strand the body hidden forever
|
||||
|
||||
private MeshPainter _painter; // owns the per-avatar material instance carrying _XRayColor
|
||||
private PlayerPaintSync _paintSync; // receiver-side reveal gate: body hidden until this client applied the painting
|
||||
private ConcealmentMeter _concealment; // owner-only embedded-percent source (disabled on remotes)
|
||||
private PlayerCharacterController _controller; // sibling, cached (the freeze re-asserts per frame)
|
||||
private bool _xrayApplied;
|
||||
@@ -120,6 +121,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
_active.Add(this); // scoreboard registry (paired with OnNetworkDespawn)
|
||||
_bodyRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||
_painter = GetComponentInChildren<MeshPainter>(true);
|
||||
_paintSync = GetComponentInChildren<PlayerPaintSync>(true);
|
||||
_concealment = GetComponentInChildren<ConcealmentMeter>(true);
|
||||
_isFound.OnValueChanged += OnFoundChanged;
|
||||
if (_isFound.Value) ApplyFound(); // defensive: an avatar is never normally spawned already-found
|
||||
@@ -150,6 +152,11 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
// simulation (input/movement) and the CharacterController's own depenetration.
|
||||
DisableLocalSimulation();
|
||||
|
||||
// Receiver-side reveal gate: hide the body from the very first frame until this client has applied
|
||||
// the owner's painting baseline (see PlayerPaintSync.PaintRevealReady). Without this a freshly
|
||||
// spawned remote flashes the default grey/white material before its painting arrives.
|
||||
SetBodyRenderers(false);
|
||||
|
||||
// The concealment meter (per-frame bake + voxelization + overlap) only feeds the LOCAL player;
|
||||
// a remote's concealment is computed on its own owner's machine, so here it is wasted CPU.
|
||||
if (_concealment) _concealment.enabled = false;
|
||||
@@ -274,7 +281,10 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
if (IsOwner && !found) return;
|
||||
|
||||
// While the conversion render gate is up, the body stays hidden no matter what ComputeVisible says.
|
||||
bool visible = !_renderGate && ComputeVisible(found);
|
||||
// The paint gate is an additional hard hide: a remote body never shows before this client has
|
||||
// applied its painting (owner is always ready; a null sync - offline/single-player - never gates).
|
||||
bool paintReady = !_paintSync || _paintSync.PaintRevealReady;
|
||||
bool visible = paintReady && !_renderGate && ComputeVisible(found);
|
||||
if (_visibilityApplied && visible == _lastVisible) return;
|
||||
SetBodyRenderers(visible);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,26 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
new(new Vector3(0.2f, 1.4f, 1f), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
||||
private bool _heightApplyPending; // remote: apply _height.Value to the material once IsReady / on change
|
||||
|
||||
// Receiver-side reveal gate (owner-authoritative): the cutoff sequence of the owner's FIRST published
|
||||
// baseline - carry-loaded, manually loaded, or (for a never-painted avatar) an explicit blank publish.
|
||||
// -1 until the owner has established and published its painting. A remote body stays HIDDEN until this
|
||||
// client has actually APPLIED a snapshot whose cutoff reaches this value (see PaintRevealReady), so a
|
||||
// peer never flashes the default grey/white material, and a match-start carry never briefly shows the
|
||||
// pre-load blank maps (the flag can't outrun the chunked snapshot - the applied cutoff must catch up).
|
||||
private readonly NetworkVariable<int> _publishedCutoff =
|
||||
new(-1, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
||||
|
||||
// Highest snapshot cutoff this replica has actually APPLIED (not merely begun receiving). Compared
|
||||
// against _publishedCutoff to decide the reveal; only ever raised.
|
||||
private int _appliedCutoff = int.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// True once THIS client may render the avatar's body: the owner is always ready (it holds its own
|
||||
/// painting locally); a remote replica is ready only after it has applied a snapshot reaching the
|
||||
/// owner's published baseline. Read by NetworkPlayerSetup's per-client visibility pass.
|
||||
/// </summary>
|
||||
public bool PaintRevealReady => IsOwner || (_publishedCutoff.Value >= 0 && _appliedCutoff >= _publishedCutoff.Value);
|
||||
|
||||
// Monotonic message counter on the owner (one per stamp batch / checkpoint / restore); on every peer,
|
||||
// the highest sequence accounted for (applied to a remote replica / sent by the owner). Used as the
|
||||
// snapshot's cutoff marker so the join window neither loses nor double-applies events.
|
||||
@@ -225,6 +245,7 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
{
|
||||
_snapReadyToApply = false;
|
||||
_painter.ApplyPaintSnapshot(_snapBuffer, _snapBaselineId);
|
||||
if (_snapCapturedSeq > _appliedCutoff) _appliedCutoff = _snapCapturedSeq; // receiver-side reveal gate
|
||||
if (logSync) LogSync($"applied snapshot baseline {_snapBaselineId}; replaying buffered events");
|
||||
FlushBuffered();
|
||||
_snapBuffer = null;
|
||||
@@ -262,6 +283,13 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Establish + publish the reveal baseline once even when nothing was carried/loaded: a never-painted
|
||||
// avatar must still hand every peer an explicit (blank) snapshot, otherwise its body stays hidden
|
||||
// forever behind the receiver-side reveal gate. Carry/manual-load already publish (and set the
|
||||
// cutoff), so this only fires for the no-carry case.
|
||||
if (_publishedCutoff.Value < 0 && _pendingCarry == null && _painter && _painter.IsPaintReady)
|
||||
BroadcastLoadedPaint();
|
||||
|
||||
// Delta-only publish: the NetworkVariable dirties only on an actual change.
|
||||
if (_painter && _emissionIntensity.Value != _painter.emissionIntensity)
|
||||
_emissionIntensity.Value = _painter.emissionIntensity;
|
||||
@@ -605,6 +633,7 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
int cutoff = ++_seq; // every event up to here is now superseded by the loaded baseline
|
||||
_seqHorizon = cutoff;
|
||||
int baselineId = _painter.CurrentCheckpointId; // the load's fresh baseline id
|
||||
if (_publishedCutoff.Value < 0) _publishedCutoff.Value = cutoff; // first publish = the receiver-side reveal baseline
|
||||
if (logSync) LogSync($"broadcasting loaded painting to all peers (cutoff {cutoff}, baseline {baselineId}, {data.Length} bytes)");
|
||||
StartCoroutine(UploadSnapshot(data, cutoff, baselineId, broadcast: true, target: 0));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user