Replace the fixed additive brush-size step with a fraction of the current
size, so small brushes resize a texel at a time (fine control) while large
ones jump in bigger increments. Always moves at least one texel per notch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brush radius now derives from the mesh-wide average UV density instead of
the hit triangle's own density. Per-triangle density varied up to ~7.6x
across the mesh (spiking at UV seams and stretched islands), so the same
brushSize painted a visibly larger patch on seam triangles than on
evenly-mapped ones. A single global scale keeps the brush the same physical
size everywhere; the stamp remains a 3D box, so seams are still bridged.
Also decouple the hover preview opacity from the brush albedo alpha: the
preview now stays fully opaque and is scaled only by the Preview Opacity
property, since the ring is a size/shape indicator, not a paint-strength
preview.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a second, additive HDR emission color (_XRayColor) that reveals a
player through walls as a pulsing energy glow, driven during matches:
- Shader: XRayOverlay pass (ZTest Greater, additive) for occluded parts
plus a matching additive term in the forward fragment for visible
parts, so the glow shows everywhere and never double-blends. Exposes
_XRayColor, _XRayPulseSpeed and _XRayPulseStrength for a breathing pulse.
- Render it after opaques via a RenderObjects feature (custom XRayReveal
light-mode tag) on both PC and Mobile renderers, so the through-wall
test is depth-sort independent and no longer drops out at some camera
angles/distances.
- MeshPainter drives _XRayColor alpha through a framerate-independent
fade (xrayFadeDuration) rather than a hard toggle.
- Reveal is driven per avatar on every client: all remaining un-found
hiders glow during the Resolution phase, and a hider whose
ConcealmentMeter EmbeddedPercent exceeds MatchManager.embeddedRevealPercent
glows via an owner-authoritative NetworkVariable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire three material-backed look sliders (occlusion AO shift, relief normal
intensity, parallax intensity) through the paint HUD, its layout/state
persistence, the .paint slot format (bumped to v4), owner-authoritative
network sync and the runtime reset, mirroring the existing camo-knob path so
they save, restore, replicate and reset alongside the brush values.
Change the occlusion AO mapping from a division threshold to a linear shift:
AO fades from 1 down to (1 - shift) at the deepest painted hole, so a 0 shift
no longer leaves black spots. Enable _NORMALMAP alongside _PARALLAXMAP so URP
transforms the painted relief normal into world space (otherwise it discards
normalTS and the relief is invisible), and pin the added keyword in the build
keep-alive material.
Stop the pipette from picking the occlusion/height value and turn its toggle
off after a pick. Default the knobs to 0.2 shift / 1.4 normal / 1.0 parallax
across the shader, painter, network and HUD fallbacks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reuse the existing occlusion paint channel (no new map, MAP_COUNT stays 3):
0.5 is neutral, above 0.5 raises a hill, below 0.5 sinks a hole and also
fades ambient occlusion (saturate(2*occ)). Bind the MSO map to _ParallaxMap,
enable _PARALLAXMAP, derive a relief normal from the height gradient, and
reseed unpainted occlusion to 0.5 so untouched surfaces stay visually neutral.
Pin the _PARALLAXMAP runtime variant in the build keep-alive material.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a geometry-based gate in PaintAtScreen that estimates how much of the
brush footprint lands on faces whose normals deviate sharply from the hit
surface. When that fraction exceeds the threshold the whole dab is
suppressed (no draw, emit, or undo mark) instead of smearing around the
edge. Backend-independent (no GPU readback) and winding-robust. Tunable via
cornerGuard / cornerGuardAngle / cornerGuardMaxFraction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the PlayerCamoLit material knobs (HalfLambert, HalfLambert brightness, specular dampen) as material-backed properties on MeshPainter, guarded by an IsReady gate since the HUD binds before the material instance exists. Wire the three HUD sliders to them and seed the sliders from the live material once the painter is ready. Persist the values into the same HUD JSON as the brush settings (with a hasCamoValues flag so pre-camo save files never clobber the authored material) and carry them across a scene-switch respawn. Replicate them owner-authoritatively via a NetworkVariable in PlayerPaintSync, exactly like emission intensity, so remotes and late joiners show the same camo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a foreign object the pipette now reads the data channels
(metallic/smoothness/occlusion/emission) texel-exact from the material
while taking albedo from the rendered, fully-lit on-screen pixel - so the
picked colour honours baked lightmaps/ambient/SSAO while keeping the
material's PBR fidelity. Falls back to a pure screen pick when no readable
UV is available. Pull the render+readback out of PickFromScreen into a
shared TrySampleScreenColor.
The pipette now also arms on Ctrl (in addition to Shift), and Ctrl+wheel
resizes the brush; the orbit camera ignores the wheel while either modifier
is held so it does not zoom and resize at once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hold Shift to momentarily enable the pipette (restores prior state on
release) and Shift+Scroll to resize the brush. The orbit zoom ignores the
wheel while Shift is held so it does not zoom and resize at once. The HUD
brush-size slider follows live via a BrushSizeChanged event, and the brush
preview now stays visible while Shift is held (drawn before the pipette
early-return) so the new size is visible while scrubbing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the connection-approval role selection with a server-authoritative
match flow built on NGO additive scene management:
- Scenes: Main is the persistent foundation (NetworkManager, GameModeManager,
cameras, HUD); Lobby and Test Map load/unload additively. Add Lobby + Test Map
to the build settings; remove the obsolete SampleScene.
- MatchManager (plain MonoBehaviour singleton): loads Lobby on server start,
StartMatchTrigger lets the host start the match, rolls one random Hunter (rest
Hider), swaps Lobby -> Test Map (strictly serialized per NGO), respawns each
player as its role variant. Forces additive client sync mode so clients keep
Main instead of single-loading it (fixes "Destination scene is not valid").
- Server-driven spawning: null out NetworkConfig.PlayerPrefab at runtime to
disable auto-spawn, then spawn players after the lobby floor exists (fixes
falling through the floor) with double-spawn and transition guards.
- StartMatchTrigger: server-gated OnTriggerEnter, only the host's own player
starts the match.
- NetworkBootstrap: drop role selection / connection approval; everyone spawns
as the neutral Player prefab. Host/Client/Server/Shutdown buttons stay.
- NetworkPlayerSetup.ScopedModes flag (baked on Hunter/Hider prefabs): lobby
avatar gets the full mode set, match variants get the role-scoped set.
- GameModeManager.BindLocalPlayer reads ScopedModes and rebinds the painter
unconditionally (fixes stale painter binding on respawn -> brush painting in
every mode).
- MeshPainter.PaintingEnabled defaults to false (unbound/pre-spawn/remote never
paints); PaintHud visibility now also gates on a bound painter (fixes the HUD
showing at startup before any avatar exists).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Persistence rework:
- Drop auto-load on setup and auto-save on teardown; the painter now always
seeds fresh from the material.
- Named slots under MeshPaint/Saved/<name>.paint via the existing CHMP
machinery: SavePaintAs / LoadPaintByName / DeletePaint / ListSavedPaintings /
PaintExists. Slot files carry the display name in the header.
- Remove the obsolete persistence partials (PlayerCharacterController,
PlayerPoseController, ThirdPersonOrbitInput) and their references.
Paintings HUD panel:
- Authored Panel_Paintings + Save/Load/Delete dialogs into PaintHudPanel.prefab
(TMP throughout); PaintHud.Library.cs is now a pure binder that clones the
entry template per slot.
- Save dialog blocks empty/existing names; Load and Delete confirm first.
- Fix dialog z-order: open dialogs SetAsLastSibling so a dragged panel can no
longer cover them.
- Lib_ListContent horizontal offsets corrected to 0 (was clipping entry labels
out of the masked viewport).
Multiplayer:
- Loading a painting re-syncs to all peers via the snapshot broadcast path
(BroadcastLoadedPaint), covering later late-joiners for free.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replicate brush strokes as self-contained commands instead of texture
pixels: the owner emits each committed stamp (local placement plus brush
state, byte-quantized) and remotes replay it through the same draw path,
reproducing identical maps without streaming megabytes.
- MeshPainter.Net.cs: PaintStampNet serializable stamp, OnStampCommitted
owner-capture event (no-op without listener), ApplyNetworkStamp remote
replay, seedable spray for deterministic speckle.
- MeshPainter.cs: emit stamp after drawing, seed spray only when synced.
- PlayerPaintSync.cs: NetworkBehaviour batching owner stamps reliably
(ServerRpc -> ClientRpc) with echo guard; remote disables local painting.
- Player/Hider/Hunter prefabs: PlayerPaintSync plus NGO GlobalObjectIdHash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ConcealmentMeter: standalone component measuring the percentage of the
character volume embedded inside environment geometry (bake -> voxelize via
column ray-stabbing parity -> classify interior voxels against box/sphere/
capsule/convex colliders and terrain). Always-on, throttled, gizmo debug.
- SkinnedColliderBaker: sole per-frame collider bake owner for skinned rigs.
MeshPainter now delegates baking and only borrows BakedMesh for the GPU
raster and cache refresh, so the collider stays fresh outside paint mode.
- Move pose assets to Assets/Poses, retire unused Insect scripts to
Assets/Unused, restore Paintman.controller; mode scripts and scene updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add mask selector with previews to the brush card (data-driven from
painter.brushMasks); persist activeBrushIndex with HUD layout.
- Fix alpha/coverage artifacts when overpainting after load or backend
switch: derive the dilation source marker from a deterministic UV island
mask instead of resetting it to "unpainted" on every seed.
- Import the two final brush mask textures (Brushes/).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add MeshPainter.Persistence.cs: auto-save the three canonical paint maps
(albedo, MSO, emission) as PNGs under persistentDataPath on quit/play-stop
and auto-load them as the backend seed on start, so painting survives
sessions. Reuses the existing ReadCanonical/SetupBackend seed round-trip;
no new color-space code.
Add PaintHud.Persistence.cs: auto-save draggable panel positions and the
widget-bound painter values to a small JSON layout file, restored on start
via the painter -> SyncFromPainter -> widgets path. Complete SyncFromPainter
to also cover the spray sliders.
The pipette now raycasts the nearest collider of any object, not just the
paint mesh. On a foreign hit it reads all channels texel-accurately from the
object's material maps (BaseMap/MetallicGloss/Occlusion/Emission) combined
with the material scalars per URP/Lit conventions, falling back to scalar
properties where a map is missing or the collider yields no UV. Includes
scene test props and materials for verifying foreign-object picking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pipette samples all painted channel values (albedo, metallic, smoothness,
occlusion, emission) at the clicked texel and adopts them into the brush
with every opacity forced to 1, instead of painting. The screen raycast is
extracted into a shared TryRaycastPaintMesh helper used by painting, preview
and the pipette. A pipette toggle is added to the HUD panel prefab and wired
in PaintHud.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Spray brush mode: bakes a procedural scattered-dot mask per stamp (random
position, size, alpha within the brush disc) and paints it through the
existing single-stamp pipeline; dot centers clamped to (0.5 - dotR) so the
spray footprint matches the solid brush radius.
Paint interval: float on MeshPainter; 0 keeps the continuous interpolated
stroke, >0 emits a single timed dab every x.xx seconds.
Color picker alpha: horizontal alpha bar on both HSV pickers, runtime-built
in Rebind (no prefab re-bake), passed through to brushColor.a / emissionColor.a.
Metallic/smoothness + occlusion paint opacity: new opacity fields applied in
both CPU and GPU backends (GPU MSO draw split into MS and occlusion sub-stamps
for independent opacity), with cloned alpha sliders wired into the HUD.
Also includes the prefab layout tweaks and scene adjustments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a code-driven HUD that appears in paint mode and binds live to the
active MeshPainter: four channel toggles, HSV pickers for albedo and
emission, sliders for metallic/smoothness/occlusion, emission intensity
and brush size. The HUD self-assembles its own overlay canvas and event
system at runtime.
Add a ModeChanged event to GameModeManager so the HUD can follow the
active mode, and guard MeshPainter against painting through the UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Split MeshPainter into partial classes (Setup/Backend/Gpu/Cpu) for maintainability
- Add ambient occlusion painting, packed with metallic/smoothness into one
MSO mask map (R=metallic, G=occlusion, A=smoothness) bound to both
_MetallicGlossMap and _OcclusionMap, kept independently paintable via
per-channel ColorMask
- Gate painting from a single IsChannelActive source so AO-only strokes work
- Blend albedo/emission in linear space on CPU to match the GPU framebuffer
- Pre-compensate data-channel writes for sRGB->linear so GPU matches CPU
- Harden GPU shader fallback to reflect real backend state
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the painter from a single albedo map to three paint maps:
albedo (_BaseMap), packed metallic+smoothness (_MetallicGlossMap,
R=metallic A=smoothness), and emission (_EmissionMap/_EmissionColor).
- Per-channel toggles (paint albedo / metallic+smoothness / emission)
- Metallic/Smoothness sliders and emission color + intensity brush params
- One brush shader drives all maps; metallic-smoothness uses a work
layout (R=metallic G=smoothness A=coverage) and a pack pass
(Hidden/MeshPaintPackMS) to the URP layout after each stroke
- GPU and CPU backends mirror all three maps with the same mask and
seam-dilation logic
- Material keywords (_METALLICSPECGLOSSMAP, _EMISSION) enabled and the
new maps seeded from the original material values
- Add hover preview (MeshPaintPreview.shader) with a Preview Mode enum
(Off / Flat Decal / On Surface) sharing the paint placement path so
size, mask, alignment and rotation match the actual stroke.
- Preview Opacity now ranges up to 4.0 so faint/grey masks can be shown
more strongly (saturates to full opacity before the hardware blend).
- Add Paint On Move Only: stamp once on mouse-down, then only while the
cursor moves (GPU and CPU paths alike).
- Remove brushOpacity; brushColor.a is now the global opacity multiplier
over the mask in both backends (the alpha channel was previously unused
by the brush shader).
Replace the 3D sphere brush with a square stamp in the surface tangent
plane whose shape and opacity come from a grayscale/alpha mask. Masks
are assignable via an inspector list with a live-switchable active index;
an empty list falls back to a procedurally generated circle mask that
reproduces the previous round brush.
- Mask drives opacity (luma * alpha * brushOpacity) via source-over blend
instead of a hard threshold cut; gray 0.5 = 50%.
- GPU and CPU paths stay identical; non-readable textures are supported on
the CPU via GPU blit-readback.
- Dilate pass treats any alpha > 0 as painted so soft edges survive seam
padding.
- Brush alignment enum (Camera/World) plus manual rotation; tangent frame
handedness fixed so stamps read upright and unmirrored in both modes.
Reduce comments to non-obvious "why" notes and convert UnityEngine.Object
null checks to implicit bool (if (obj) / if (!obj)). Input System devices
(Mouse.current, Keyboard.current) and plain arrays keep explicit != null.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce an extensible game mode system toggled with Tab:
- Player Control: WASD movement with always-on mouse camera orbit
- Paint: player frozen, left-click paints, right-click holds to orbit
Add GameMode base class, GameModeManager, and the two concrete modes.
Refactor controlled scripts to expose clean state flags instead of
handling input themselves: ThirdPersonOrbitInput.LookMode,
PlayerCharacterController.ControlEnabled, MeshPainter.PaintingEnabled.
Also fix camera zoom: remove the obsolete /120 wheel normalization so
one scroll notch maps to one zoomSpeed step under the new Input System.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Install Cinemachine 3.1.7 and set up an orbital third-person camera rig
(CinemachineBrain, OrbitalFollow, RotationComposer, Deoccluder).
- Add ThirdPersonOrbitInput: mouse-driven orbit and wheel zoom with a Tab
toggle that unlocks the cursor and pauses rotation.
- Fix MeshPainter so the paint ray skips the player's CharacterController
capsule (and any other collider) and always targets the mesh collider,
so painting works from every camera angle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unity 6000.3 (URP) project with live mesh painting in play mode.
- MeshPainter.cs: paint a SkinnedMesh with the mouse, seamless across
UV islands via 3D-space painting, GPU and CPU backends (switchable),
edge-padding/dilation against bilinear seam bleed
- FrameRateLimiter.cs: simple frame rate limiting component
- MeshPaintBrush / MeshPaintDilate shaders for the GPU paint path
- Unity-specific .gitignore and .gitattributes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>