diff --git a/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs b/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs index ad7d61d..d150c4e 100644 --- a/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs +++ b/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs @@ -30,7 +30,8 @@ public partial class MeshPainter // is), so saves without a fur.png load fine - the fur map stays seeded (bald). NOTE: the fur.png layout // changed to R = length (was RGB colour + A length); pre-change fur.png files load stale length data and // should be repainted. - private const int PaintMetaVersion = 6; + // v8: camoSaturation semantics changed to an offset (0 = neutral, -1 = greyscale; was 1 = neutral in v7). + private const int PaintMetaVersion = 8; private const string MetaFileName = "painting.json"; private const string AlbedoFileName = "albedo.png"; @@ -57,6 +58,7 @@ public partial class MeshPainter public float camoHalfLambert; public float camoHalfLambertBrightness; public float camoSpecularDampen; + public float camoSaturation; // v7+; remapped on load per meta.version (semantics changed in v8) public float occlusionThreshold; public float heightNormalIntensity; public float parallaxIntensity; @@ -149,6 +151,7 @@ public partial class MeshPainter camoHalfLambert = CamoHalfLambert, camoHalfLambertBrightness = CamoHalfLambertBrightness, camoSpecularDampen = CamoSpecularDampen, + camoSaturation = CamoSaturation, occlusionThreshold = OcclusionThreshold, heightNormalIntensity = HeightNormalIntensity, parallaxIntensity = ParallaxIntensity, @@ -200,6 +203,12 @@ public partial class MeshPainter CamoHalfLambert = meta.camoHalfLambert; CamoHalfLambertBrightness = meta.camoHalfLambertBrightness; CamoSpecularDampen = meta.camoSpecularDampen; + // Saturation semantics changed in v8 (0 = neutral, -1 = greyscale; was 1 = neutral in v7). + // v7 stored the old scheme -> shift by -1 to preserve its look. Pre-v7 has no field (deserializes + // to 0), which is already neutral under the new scheme. + CamoSaturation = meta.version >= 8 ? meta.camoSaturation + : meta.version == 7 ? meta.camoSaturation - 1f + : 0f; OcclusionThreshold = meta.occlusionThreshold; HeightNormalIntensity = meta.heightNormalIntensity; ParallaxIntensity = meta.parallaxIntensity; diff --git a/Assets/Scripts/Components/MeshPainter/MeshPainter.cs b/Assets/Scripts/Components/MeshPainter/MeshPainter.cs index ebe727c..3549c1d 100644 --- a/Assets/Scripts/Components/MeshPainter/MeshPainter.cs +++ b/Assets/Scripts/Components/MeshPainter/MeshPainter.cs @@ -78,6 +78,16 @@ public partial class MeshPainter : MonoBehaviour set => SetMatFloat(SpecularDampenId, value); } + /// General colour saturation of the shaded body (0 = neutral, -1 = greyscale, >0 = punchier). + /// A camo body-tone knob alongside ; drives the material's + /// _Saturation (offset: shader lerps to grey by 1 + value). Persisted + replicated with the + /// other camo knobs (see ). + public float CamoSaturation + { + get => GetMatFloat(SaturationId, 0f); + set => SetMatFloat(SaturationId, value); + } + // Painted-height look knobs on the same material instance, surfaced/guarded/persisted/replicated // like the camo knobs above. Defaults match the shader so the unpainted look is unchanged. @@ -104,11 +114,12 @@ public partial class MeshPainter : MonoBehaviour set => SetMatFloat(ParallaxId, Mathf.Max(0f, value) * ParallaxBase); } - /// The three camo knobs as one vector (x = half-Lambert, y = its brightness, z = specular dampen). - public Vector3 CamoKnobs + /// The four camo knobs as one vector (x = half-Lambert, y = brightness, z = specular dampen, + /// w = saturation) - one payload for persistence + network replication. + public Vector4 CamoKnobs { - get => new(CamoHalfLambert, CamoHalfLambertBrightness, CamoSpecularDampen); - set { CamoHalfLambert = value.x; CamoHalfLambertBrightness = value.y; CamoSpecularDampen = value.z; } + get => new(CamoHalfLambert, CamoHalfLambertBrightness, CamoSpecularDampen, CamoSaturation); + set { CamoHalfLambert = value.x; CamoHalfLambertBrightness = value.y; CamoSpecularDampen = value.z; CamoSaturation = value.w; } } /// The three painted-height knobs as one vector (x = occlusion threshold, y = relief normal intensity, z = parallax intensity). @@ -454,6 +465,7 @@ public partial class MeshPainter : MonoBehaviour private const float ParallaxBase = 0.02f; private static readonly int HalfLambertId = Shader.PropertyToID("_HalfLambert"); private static readonly int HalfLambertBrightnessId = Shader.PropertyToID("_HalfLambertBrightness"); + private static readonly int SaturationId = Shader.PropertyToID("_Saturation"); private static readonly int SpecularDampenId = Shader.PropertyToID("_SpecularDampen"); private static readonly int XRayColorId = Shader.PropertyToID("_XRayColor"); private static readonly int XRayPulseSpeedId = Shader.PropertyToID("_XRayPulseSpeed"); diff --git a/Assets/Scripts/Components/UI/PaintHud/PaintHud.Persistence.cs b/Assets/Scripts/Components/UI/PaintHud/PaintHud.Persistence.cs index 2fda77f..8fa2d79 100644 --- a/Assets/Scripts/Components/UI/PaintHud/PaintHud.Persistence.cs +++ b/Assets/Scripts/Components/UI/PaintHud/PaintHud.Persistence.cs @@ -20,9 +20,11 @@ public partial class PaintHud "defaults every session.")] public bool persistHud = true; - // v2: HudValues gained fur (toggle) + furLength. Bumping discards pre-v2 saves so the new fur fields - // fall back to the painter's prefab defaults instead of deserializing as 0/false. - private const int HudFileVersion = 2; + // v2: HudValues gained fur (toggle) + furLength. v3: gained camoSaturation. v4: camoSaturation + // semantics changed to an offset (0 = neutral, was 1). Bumping discards older saves so the fields + // fall back to the painter's prefab/material defaults instead of stale/old-scheme values (a v3 save + // stored saturation 1 = old-neutral, which would read as punchier now). + private const int HudFileVersion = 4; // Auto-save bookkeeping. Brush edits are debounced (sliders fire every frame while dragged): the due // time is pushed forward and written once they settle. @@ -41,7 +43,7 @@ public partial class PaintHud // Camo knobs live on the material instance, which only exists after the painter's Start: a restored // (disk) or carried (respawn) value waits here and is applied in the LateUpdate ready-gate. // _camoEverReady gates carry/save so the not-yet-ready 0 never overwrites the authored value. - private Vector3 _pendingCamo; + private Vector4 _pendingCamo; // (x,y,z,w) = CamoKnobs (halfLambert, brightness, specularDampen, saturation) private bool _hasPendingCamo; private bool _camoEverReady; @@ -95,7 +97,7 @@ public partial class PaintHud public int activeBrushIndex; public int sprayDots; public float sprayDotSizeMin, sprayDotSizeMax, sprayDotAlphaMin, sprayDotAlphaMax; - public float camoHalfLambert, camoHalfLambertBrightness, camoSpecularDampen; // PlayerCamoLit material knobs + public float camoHalfLambert, camoHalfLambertBrightness, camoSpecularDampen, camoSaturation; // PlayerCamoLit material knobs public float occlusionThreshold, heightNormalIntensity, parallaxIntensity; // painted-height knobs (material-backed) } @@ -170,9 +172,10 @@ public partial class PaintHud // save files (applying their 0 would clobber the authored camo). if (state.hasCamoValues && state.values != null) { - _pendingCamo = new Vector3(state.values.camoHalfLambert, + _pendingCamo = new Vector4(state.values.camoHalfLambert, state.values.camoHalfLambertBrightness, - state.values.camoSpecularDampen); + state.values.camoSpecularDampen, + state.values.camoSaturation); _hasPendingCamo = true; } @@ -228,6 +231,7 @@ public partial class PaintHud _savedValues.camoHalfLambert = _lastSeenValues.camoHalfLambert = painter.CamoHalfLambert; _savedValues.camoHalfLambertBrightness = _lastSeenValues.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness; _savedValues.camoSpecularDampen = _lastSeenValues.camoSpecularDampen = painter.CamoSpecularDampen; + _savedValues.camoSaturation = _lastSeenValues.camoSaturation = painter.CamoSaturation; _savedValues.occlusionThreshold = _lastSeenValues.occlusionThreshold = painter.OcclusionThreshold; _savedValues.heightNormalIntensity = _lastSeenValues.heightNormalIntensity = painter.HeightNormalIntensity; _savedValues.parallaxIntensity = _lastSeenValues.parallaxIntensity = painter.ParallaxIntensity; @@ -298,6 +302,7 @@ public partial class PaintHud && p.CamoHalfLambert == v.camoHalfLambert && p.CamoHalfLambertBrightness == v.camoHalfLambertBrightness && p.CamoSpecularDampen == v.camoSpecularDampen + && p.CamoSaturation == v.camoSaturation && p.OcclusionThreshold == v.occlusionThreshold && p.HeightNormalIntensity == v.heightNormalIntensity && p.ParallaxIntensity == v.parallaxIntensity; @@ -322,9 +327,10 @@ public partial class PaintHud // by _camoEverReady so a carry taken before any material was ready can't apply a stale 0. if (_camoEverReady) { - _pendingCamo = new Vector3(_carry.camoHalfLambert, + _pendingCamo = new Vector4(_carry.camoHalfLambert, _carry.camoHalfLambertBrightness, - _carry.camoSpecularDampen); + _carry.camoSpecularDampen, + _carry.camoSaturation); _hasPendingCamo = true; _pendingHeight = new Vector3(_carry.occlusionThreshold, _carry.heightNormalIntensity, @@ -365,6 +371,7 @@ public partial class PaintHud v.camoHalfLambert = from.CamoHalfLambert; v.camoHalfLambertBrightness = from.CamoHalfLambertBrightness; v.camoSpecularDampen = from.CamoSpecularDampen; + v.camoSaturation = from.CamoSaturation; v.occlusionThreshold = from.OcclusionThreshold; v.heightNormalIntensity = from.HeightNormalIntensity; v.parallaxIntensity = from.ParallaxIntensity; @@ -401,6 +408,7 @@ public partial class PaintHud to.CamoHalfLambert = v.camoHalfLambert; to.CamoHalfLambertBrightness = v.camoHalfLambertBrightness; to.CamoSpecularDampen = v.camoSpecularDampen; + to.CamoSaturation = v.camoSaturation; to.OcclusionThreshold = v.occlusionThreshold; to.HeightNormalIntensity = v.heightNormalIntensity; to.ParallaxIntensity = v.parallaxIntensity; @@ -459,6 +467,7 @@ public partial class PaintHud _defaults.camoHalfLambert = painter.CamoHalfLambert; _defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness; _defaults.camoSpecularDampen = painter.CamoSpecularDampen; + _defaults.camoSaturation = painter.CamoSaturation; _defaults.occlusionThreshold = painter.OcclusionThreshold; _defaults.heightNormalIntensity = painter.HeightNormalIntensity; _defaults.parallaxIntensity = painter.ParallaxIntensity; diff --git a/Assets/Scripts/Components/UI/PaintHud/PaintHud.cs b/Assets/Scripts/Components/UI/PaintHud/PaintHud.cs index c56d0d3..5793510 100644 --- a/Assets/Scripts/Components/UI/PaintHud/PaintHud.cs +++ b/Assets/Scripts/Components/UI/PaintHud/PaintHud.cs @@ -59,7 +59,8 @@ public partial class PaintHud : MonoBehaviour private const string SliderSprayDotAlphaMin = "Slider_SprayDotAlphaMin"; private const string SliderSprayDotAlphaMax = "Slider_SprayDotAlphaMax"; private const string SliderHalfLambert = "Slider_HalfLambert"; - private const string SliderHalfLambertBrightness = "Slider_HalfLambertBrightness"; + private const string SliderBrightness = "Slider_Brightness"; + private const string SliderSaturation = "Slider_Saturation"; private const string SliderSpecularDamp = "Slider_SpecularDamp"; private const string SliderOcclusionThreshold = "Slider_OcclusionThreshold"; private const string SliderNormalIntensity = "Slider_NormalIntensity"; @@ -253,7 +254,8 @@ public partial class PaintHud : MonoBehaviour // Camo knobs are material-backed and not readable yet (the HUD binds before the painter's Start); // SyncCamoSliders re-seeds them once IsReady. Setting a slider before then is a safe no-op. WireSlider(panel, SliderHalfLambert, painter ? painter.CamoHalfLambert : 0f, v => { if (painter) painter.CamoHalfLambert = v; }); - WireSlider(panel, SliderHalfLambertBrightness, painter ? painter.CamoHalfLambertBrightness : 0f, v => { if (painter) painter.CamoHalfLambertBrightness = v; }); + WireSlider(panel, SliderBrightness, painter ? painter.CamoHalfLambertBrightness : 0f, v => { if (painter) painter.CamoHalfLambertBrightness = v; }); + WireSlider(panel, SliderSaturation, painter ? painter.CamoSaturation : 0f, v => { if (painter) painter.CamoSaturation = v; }); WireSlider(panel, SliderSpecularDamp, painter ? painter.CamoSpecularDampen : 0f, v => { if (painter) painter.CamoSpecularDampen = v; }); // Painted-height knobs: material-backed like the camo sliders above. @@ -333,7 +335,8 @@ public partial class PaintHud : MonoBehaviour if (!_panel || !painter) return; var panel = _panel.transform; SetSlider(panel, SliderHalfLambert, painter.CamoHalfLambert); - SetSlider(panel, SliderHalfLambertBrightness, painter.CamoHalfLambertBrightness); + SetSlider(panel, SliderBrightness, painter.CamoHalfLambertBrightness); + SetSlider(panel, SliderSaturation, painter.CamoSaturation); SetSlider(panel, SliderSpecularDamp, painter.CamoSpecularDampen); // Painted-height knobs share the same material-ready timing as the camo sliders. SetSlider(panel, SliderOcclusionThreshold, painter.OcclusionThreshold); diff --git a/Assets/Scripts/Network/PlayerPaintSync.cs b/Assets/Scripts/Network/PlayerPaintSync.cs index ba8fc79..b39a1ba 100644 --- a/Assets/Scripts/Network/PlayerPaintSync.cs +++ b/Assets/Scripts/Network/PlayerPaintSync.cs @@ -69,12 +69,12 @@ public class PlayerPaintSync : NetworkBehaviour private readonly NetworkVariable _emissionIntensity = new(1f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); - // Per-player camo knobs on the PlayerCamoLit material (x = HalfLambert, y = its brightness, - // z = specular dampen) - material values like emission intensity, applied once the remote painter - // is ready and on every change. The default mirrors Player.mat's authored camo so the brief - // pre-publish spawn frame shows no flash. - private readonly NetworkVariable _camo = - new(new Vector3(0.5f, 0f, 0.1f), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + // Per-player camo knobs on the PlayerCamoLit material (x = HalfLambert, y = brightness, + // z = specular dampen, w = saturation) - material values like emission intensity, applied once the + // remote painter is ready and on every change. The default mirrors Player.mat's authored camo (with + // neutral saturation 0) so the brief pre-publish spawn frame shows no flash. + private readonly NetworkVariable _camo = + new(new Vector4(0.5f, 0f, 0.1f, 0f), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); private bool _camoApplyPending; // remote: apply _camo.Value to the material once IsReady / on change // Painted-height knobs (x = occlusion AO threshold, y = relief normal intensity, z = parallax @@ -246,7 +246,7 @@ public class PlayerPaintSync : NetworkBehaviour // Apply on the next LateUpdate once the material is ready (the camo setters are a no-op before that, // and on a fresh spawn the OnValueChanged may arrive before the painter's Start has built the instance). - private void OnCamoChanged(Vector3 previous, Vector3 current) => _camoApplyPending = true; + private void OnCamoChanged(Vector4 previous, Vector4 current) => _camoApplyPending = true; private void OnHeightChanged(Vector3 previous, Vector3 current) => _heightApplyPending = true; // ---- Live stamp stream (owner -> server -> remotes) ---- @@ -315,7 +315,7 @@ public class PlayerPaintSync : NetworkBehaviour // published (it would briefly flatten remotes' lighting until the real value lands). if (_painter && _painter.IsReady) { - Vector3 camo = _painter.CamoKnobs; + Vector4 camo = _painter.CamoKnobs; if (_camo.Value != camo) _camo.Value = camo; diff --git a/Assets/Shaders/PlayerCamo.hlsl b/Assets/Shaders/PlayerCamo.hlsl index 53fc855..95fd91d 100644 --- a/Assets/Shaders/PlayerCamo.hlsl +++ b/Assets/Shaders/PlayerCamo.hlsl @@ -3,7 +3,7 @@ // Camouflage lighting for the player avatar. Goal: keep the player painted to match its // surroundings while de-emphasising the 3D FORM, so a correctly painted avatar is harder to spot. -// Knobs - _HalfLambert at 0 and _HalfLambertBrightness at 0 => byte-identical URP/Lit: +// Knobs - _HalfLambert 0, _HalfLambertBrightness 0 and _Saturation 0 => byte-identical URP/Lit: // // _HalfLambert (0..1): form-flattening diffuse over two segments - // 0 -> saturate(N.L) plain Lambert (no-op) @@ -14,9 +14,16 @@ // (operates on the shading term, not a view-/light-bent normal, so // there is no bright/dark flip when orbiting). The DIRECT specular // highlight (a strong shape cue) is faded out with it, gone at 1. -// _HalfLambertBrightness (-1..1): additive shift of the (flattened) diffuse term, default 0. Tunes -// the body brighter/darker to match the scene tone (e.g. flatten to -// 1, then dial this down to sink into a dark scene). Clamped at 0. +// _HalfLambertBrightness (-1..1): GENERAL brightness (exposure) on the WHOLE shaded body - diffuse, +// ambient/GI AND specular - as a final multiply (1 + value), default +// 0 = neutral. NO LONGER tied to the half-Lambert diffuse term (the +// "HalfLambert" in the name is legacy): dial down to sink the body +// into a dark scene, up to lift it into a bright one. Applied in +// CamoForwardFragment, before fog and the X-ray glow. +// _Saturation (-1..1): colour saturation of the shaded body, default 0 = neutral. -1 = +// greyscale, <0 = muted (blends into muted scenes), >0 = punchier. +// Luma-preserving lerp(grey, colour, 1 + _Saturation) toward Rec.709 +// grey, same final stage as brightness. // _SpecularDampen (0..1): scales down the DIRECT specular highlight (a strong shape cue) on // top of the _HalfLambert fade. // @@ -29,7 +36,8 @@ // Declared OUTSIDE UnityPerMaterial (LitInput.hlsl owns that CBUFFER). The player is a handful of // instances, so the resulting SRP-batcher break is irrelevant. half _HalfLambert; -half _HalfLambertBrightness; +half _HalfLambertBrightness; // legacy name: now a GENERAL brightness/exposure (CamoForwardFragment), not a half-Lambert-only shift +half _Saturation; half _SpecularDampen; // --- Painted height (from the occlusion channel) --- @@ -103,13 +111,11 @@ half3 LightingPhysicallyBasedCamo(BRDFData brdfData, BRDFData brdfDataClearCoat, // 0 -> saturate(N.L) plain Lambert (no-op, byte-identical URP/Lit) // 0.5 -> N.L * 0.5 + 0.5 classic half-Lambert (soft terminator, no hard shadow line) // 1 -> 1 fully flat: the direct diffuse no longer reveals the 3D form. - // _HalfLambertBrightness then shifts the whole (flattened) term up/down to tune the body tone; - // clamped at 0 so it never produces negative light. + // (Brightness is NOT applied here anymore - it is a general final-color exposure in CamoForwardFragment.) half classicHL = NdotL * 0.5h + 0.5h; // already in [0,1] for N.L in [-1,1] half t1 = saturate(_HalfLambert * 2.0h); // 0..1 across _HalfLambert in [0, 0.5] half t2 = saturate(_HalfLambert * 2.0h - 1.0h); // 0..1 across _HalfLambert in [0.5, 1] - half diffuseTerm = lerp(lerp(ndlSat, classicHL, t1), 1.0h, t2); - diffuseTerm = max(0.0h, diffuseTerm + _HalfLambertBrightness); + half diffuseTerm = lerp(lerp(ndlSat, classicHL, t1), 1.0h, t2); // in [0,1], no negative light half3 atten = lightColor * lightAttenuation; half3 diffuseRadiance = atten * diffuseTerm; // flattened + brightness-shifted @@ -291,6 +297,16 @@ void CamoForwardFragment( InitializeBakedGIData(input, inputData); half4 color = UniversalFragmentCamo(inputData, surfaceData); + + // Camo body-tone grade on the WHOLE shaded body (direct + ambient/GI + specular): a GENERAL + // brightness/exposure and a saturation control - camo tools to sink/lift the body tone and to + // mute/punch its colour to match the scene. Applied before fog (part of the body look) and before + // the X-ray reveal glow below, so the reveal keeps its authored intensity. Neutral at brightness 0 + // / saturation 0 (x1 multiply, no-op lerp) => byte-identical to stock when the camo knobs are off. + color.rgb *= 1.0h + _HalfLambertBrightness; + half camoLuma = dot(color.rgb, half3(0.2126h, 0.7152h, 0.0722h)); // Rec.709 luma + color.rgb = lerp(camoLuma.xxx, color.rgb, 1.0h + _Saturation); + color.rgb = MixFog(color.rgb, inputData.fogCoord); // X-Ray reveal glow on the VISIBLE parts. URP draws the SRPDefaultUnlit XRayOverlay pass BEFORE diff --git a/Assets/Shaders/PlayerCamoLit.shader b/Assets/Shaders/PlayerCamoLit.shader index deb75f4..05634e9 100644 --- a/Assets/Shaders/PlayerCamoLit.shader +++ b/Assets/Shaders/PlayerCamoLit.shader @@ -12,10 +12,12 @@ Shader "Custom/PlayerCamoLit" { Properties { - // --- Camouflage knobs (both 0 => identical to URP/Lit) --- + // --- Camouflage knobs (HalfLambert 0, Brightness 0, Saturation 0 => identical to URP/Lit) --- [Header(Camouflage)] _HalfLambert("Half Lambert (0 none, 0.5 classic, 1 flat)", Range(0.0, 1.0)) = 0.5 - _HalfLambertBrightness("Half Lambert Brightness Shift", Range(-1.0, 1.0)) = 0.0 + // Legacy property name; now a GENERAL brightness/exposure on the whole shaded body (see PlayerCamo.hlsl). + _HalfLambertBrightness("Brightness (general exposure, 0 = neutral)", Range(-1.0, 1.0)) = 0.0 + _Saturation("Saturation (0 = neutral, -1 = greyscale, >0 = punchier)", Range(-1.0, 1.0)) = 0.0 _SpecularDampen("Specular Dampen", Range(0.0, 1.0)) = 0.5 // Specular vs Metallic workflow