Apply camo brightness/saturation grade in GBuffer pass for HTrace GI
HTrace WSGI composites indirect lighting after post-processing using the GBuffer albedo/occlusion of the player's stock URP GBuffer pass. That pass knew neither the painted-height AO semantics nor the brightness/saturation grade, so desaturated paint reappeared fully saturated in shadow/indirect light and painted height modulated GI with the raw 0.5-centered value. - PlayerCamoShared.hlsl (new): single source for camo knobs, ApplyPaintedHeightSurface and ApplyCamoBodyGrade shared by both passes - PlayerCamoGBuffer.hlsl (new): CamoGBufferFragment = URP 17.3 Lit GBuffer fragment + height-AO remap on occlusion + grade applied to albedo - PlayerCamoLit.shader: GBuffer pass now uses CamoGBufferFragment - PlayerCamo.hlsl: consumes the shared helpers (forward path unchanged) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78ab6e2dd6
commit
6db78397da
6 files changed
+186
-66
No files matched your search
@@ -34,31 +34,14 @@
|
||||
// swaps the per-light shading call for the camo variant below.
|
||||
//
|
||||
// Declared OUTSIDE UnityPerMaterial (LitInput.hlsl owns that CBUFFER). The player is a handful of
|
||||
// instances, so the resulting SRP-batcher break is irrelevant.
|
||||
// instances, so the resulting SRP-batcher break is irrelevant. Brightness/saturation and the painted
|
||||
// height (knobs + ApplyPaintedHeightSurface) live in PlayerCamoShared.hlsl - the UniversalGBuffer
|
||||
// pass (PlayerCamoGBuffer.hlsl, HTrace WSGI's GI input) mirrors them so injected indirect light
|
||||
// matches the graded forward look.
|
||||
#include "PlayerCamoShared.hlsl"
|
||||
half _HalfLambert;
|
||||
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) ---
|
||||
// The painted occlusion value (G of the MSO mask map, bound to _OcclusionMap AND _ParallaxMap) is
|
||||
// reinterpreted as a 0.5-centred HEIGHT field:
|
||||
// 0.5 -> neutral: no displacement, flat relief normal, AO = 1 (identical to the unpainted look)
|
||||
// <0.5 -> recessed (hole) >0.5 -> raised (hill)
|
||||
// The UV displacement is URP's stock single-step parallax (ParallaxOffset1Step already does
|
||||
// height*amp - amp/2, i.e. the 0.5 centring) under the _PARALLAXMAP keyword. Here we add the
|
||||
// matching per-pixel relief NORMAL from the height gradient (the dominant cue) and remap the ambient
|
||||
// occlusion so only the LOWER half darkens, with the darkening DEPTH driven by _OcclusionThreshold
|
||||
// (a shift, see below). Declared outside UnityPerMaterial like the camo knobs above (the player is a
|
||||
// handful of instances, so the SRP-batcher break is irrelevant).
|
||||
half _HeightNormalStrength;
|
||||
// Runtime intensities tuned from the paint HUD. _HeightNormalIntensity scales the relief normal
|
||||
// (default 1). _OcclusionThreshold is a SHIFT (0..1, default 0.5): the AO darkening depth at the
|
||||
// deepest painted hole - 0 = no darkening at all, 1 = AO reaches full black at occ=0.
|
||||
half _HeightNormalIntensity;
|
||||
half _OcclusionThreshold;
|
||||
float4 _OcclusionMap_TexelSize;
|
||||
|
||||
// X-Ray reveal glow (rgb = HDR color, a = fade/opacity master). Consumed here for the VISIBLE parts;
|
||||
// the separate XRayOverlay pass (SRPDefaultUnlit) covers the OCCLUDED parts. See CamoForwardFragment.
|
||||
half4 _XRayColor;
|
||||
@@ -66,37 +49,6 @@ half _XRayPulseSpeed; // 0 = steady; otherwise breathing pulse via _Time (see
|
||||
half _XRayPulseStrength; // modulation depth: how far the trough dims below the full-intensity peak
|
||||
half _XRayPulseShape; // >1 bends the pulse: longer near-off plateau, rising to a brief full peak
|
||||
|
||||
// Remaps the painted occlusion/height value into surface AO + a relief normal. Runs after URP's
|
||||
// InitializeStandardLitSurfaceData (uv is already parallax-displaced) and before InitializeInputData,
|
||||
// so the relief normal flows into inputData.normalWS through the tangent frame.
|
||||
void ApplyPaintedHeightSurface(float2 uv, inout SurfaceData surfaceData)
|
||||
{
|
||||
#if defined(_OCCLUSIONMAP)
|
||||
// Only the lower (recessed) half of the painted height darkens. _OcclusionThreshold is a SHIFT =
|
||||
// how DEEP the darkening reaches at the deepest hole: AO fades from 1 (occ>=0.5, neutral/hill)
|
||||
// down to (1 - shift) at occ=0. shift=0 -> AO stays 1 everywhere (no darkening, no black spots),
|
||||
// shift=0.5 -> AO in [0.5,1], shift=1 -> AO in [0,1] (full darkening). Linear over occ in [0,0.5].
|
||||
half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
|
||||
half depth = saturate(1.0h - occ * 2.0h); // 0 at occ>=0.5 (neutral/hill), 1 at occ=0 (deepest)
|
||||
half ao = 1.0h - depth * _OcclusionThreshold; // darken toward (1 - shift) as the hole deepens
|
||||
surfaceData.occlusion = LerpWhiteTo(ao, _OcclusionStrength);
|
||||
#endif
|
||||
|
||||
#if defined(_PARALLAXMAP)
|
||||
// Per-pixel relief normal from the height (occlusion) gradient via central differences over one
|
||||
// texel. A flat field (occ=0.5 everywhere) yields zero deltas -> (0,0,1), so neutral stays unbent.
|
||||
float2 texel = _OcclusionMap_TexelSize.xy;
|
||||
half hL = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv - float2(texel.x, 0.0)).g;
|
||||
half hR = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv + float2(texel.x, 0.0)).g;
|
||||
half hD = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv - float2(0.0, texel.y)).g;
|
||||
half hU = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv + float2(0.0, texel.y)).g;
|
||||
half strength = _HeightNormalStrength * _HeightNormalIntensity;
|
||||
half3 reliefTS = normalize(half3((hL - hR) * strength,
|
||||
(hD - hU) * strength, 1.0h));
|
||||
surfaceData.normalTS = BlendNormalRNM(surfaceData.normalTS, reliefTS);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Per-light shading: copy of URP's LightingPhysicallyBased with the diffuse term wrapped
|
||||
// (half-Lambert) and the specular term dampened. Diffuse and specular use SEPARATE radiance so the
|
||||
// wrap lifts only the diffuse shadow side and never smears specular onto back faces.
|
||||
@@ -298,14 +250,12 @@ void CamoForwardFragment(
|
||||
|
||||
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);
|
||||
// Camo body-tone grade on the WHOLE shaded body (direct + ambient/GI + specular). 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 => byte-identical to stock when the
|
||||
// camo knobs are off. Mirrored onto the GBuffer albedo (PlayerCamoGBuffer.hlsl) so HTrace WSGI's
|
||||
// injected indirect light follows the grade too - otherwise paint keeps its raw look in shadows.
|
||||
ApplyCamoBodyGrade(color.rgb);
|
||||
|
||||
color.rgb = MixFog(color.rgb, inputData.fogCoord);
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef PLAYER_CAMO_GBUFFER_INCLUDED
|
||||
#define PLAYER_CAMO_GBUFFER_INCLUDED
|
||||
|
||||
// Camo variant of URP's LitGBufferPassFragment. Include AFTER LitGBufferPass.hlsl (reuses its
|
||||
// Varyings / InitializeInputData / InitializeBakedGIData).
|
||||
//
|
||||
// The project renders FORWARD, so this GBuffer pass has exactly ONE consumer: HTrace WSGI renders
|
||||
// its thin GBuffer through the "UniversalGBuffer" tag and its GI composite multiplies the traced
|
||||
// irradiance by GBuffer0.rgb (diffuse albedo) and GBuffer1.a (occlusion), ADDING the result after
|
||||
// post-processing - i.e. after the forward pass's camo grade already ran. Two camo hooks keep that
|
||||
// injected indirect light consistent with the graded forward look:
|
||||
// 1. ApplyPaintedHeightSurface: forward AO semantics for the painted height (neutral 0.5 -> AO 1)
|
||||
// instead of the raw 0.5-centred height value in GBuffer1.a, + the relief normal.
|
||||
// 2. ApplyCamoBodyGrade on the albedo: brightness/saturation follow into the injected GI, so
|
||||
// desaturated paint stays desaturated in shadows / under indirect light.
|
||||
// Everything else is a faithful copy of URP 17.3 LitGBufferPass.hlsl's fragment.
|
||||
|
||||
#include "PlayerCamoShared.hlsl"
|
||||
|
||||
GBufferFragOutput CamoGBufferFragment(Varyings input)
|
||||
{
|
||||
UNITY_SETUP_INSTANCE_ID(input);
|
||||
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
|
||||
|
||||
#if defined(_PARALLAXMAP)
|
||||
#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
|
||||
half3 viewDirTS = input.viewDirTS;
|
||||
#else
|
||||
half3 viewDirWS = GetWorldSpaceNormalizeViewDir(input.positionWS);
|
||||
half3 viewDirTS = GetViewDirectionTangentSpace(input.tangentWS, input.normalWS, viewDirWS);
|
||||
#endif
|
||||
ApplyPerPixelDisplacement(viewDirTS, input.uv);
|
||||
#endif
|
||||
|
||||
SurfaceData surfaceData;
|
||||
InitializeStandardLitSurfaceData(input.uv, surfaceData);
|
||||
|
||||
// Camo hooks (see header). The albedo grade is clamped: GBuffer0 is UNORM, and HTrace multiplies
|
||||
// its GI by the stored value, so out-of-range brightness would clip there anyway.
|
||||
ApplyPaintedHeightSurface(input.uv, surfaceData);
|
||||
ApplyCamoBodyGrade(surfaceData.albedo);
|
||||
surfaceData.albedo = saturate(surfaceData.albedo);
|
||||
|
||||
#ifdef LOD_FADE_CROSSFADE
|
||||
LODFadeCrossFade(input.positionCS);
|
||||
#endif
|
||||
|
||||
InputData inputData;
|
||||
InitializeInputData(input, surfaceData.normalTS, inputData);
|
||||
SETUP_DEBUG_TEXTURE_DATA(inputData, UNDO_TRANSFORM_TEX(input.uv, _BaseMap));
|
||||
|
||||
#if defined(_DBUFFER)
|
||||
ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData);
|
||||
#endif
|
||||
|
||||
InitializeBakedGIData(input, inputData);
|
||||
|
||||
BRDFData brdfData;
|
||||
InitializeBRDFData(surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData);
|
||||
|
||||
Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, inputData.shadowMask);
|
||||
MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, inputData.shadowMask);
|
||||
|
||||
half3 color = GlobalIllumination(brdfData, (BRDFData)0, 0,
|
||||
inputData.bakedGI, surfaceData.occlusion, inputData.positionWS,
|
||||
inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);
|
||||
|
||||
return PackGBuffersBRDFData(brdfData, inputData, surfaceData.smoothness, surfaceData.emission + color, surfaceData.occlusion);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa6746f5d4263894aa14dfc135bc9410
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -4,10 +4,14 @@
|
||||
//
|
||||
// Faithful copy of URP's Lit.shader - same properties, keywords and includes, so material values
|
||||
// transfer 1:1, the Lit inspector keeps working, and the runtime MeshPainter (which only sets
|
||||
// _BaseColor / PBR keywords on the material instance) is unaffected. The ONLY change is the
|
||||
// ForwardLit pass: its fragment is CamoForwardFragment, which reimplements URP's forward shading
|
||||
// while delegating GI / shadows / clustered lights to URP and applying the camo knobs. All
|
||||
// other passes (ShadowCaster, GBuffer, DepthOnly, DepthNormals, Meta, ...) are stock URP/Lit.
|
||||
// _BaseColor / PBR keywords on the material instance) is unaffected. TWO passes deviate from stock:
|
||||
// - ForwardLit: fragment is CamoForwardFragment, which reimplements URP's forward shading while
|
||||
// delegating GI / shadows / clustered lights to URP and applying the camo knobs.
|
||||
// - GBuffer: fragment is CamoGBufferFragment. The project renders forward, so this pass only feeds
|
||||
// HTrace WSGI's thin GBuffer - it mirrors the camo body grade (brightness/saturation) onto the
|
||||
// albedo and the painted-height AO remap onto the occlusion, so HTrace's injected indirect light
|
||||
// matches the graded forward look (see PlayerCamoGBuffer.hlsl).
|
||||
// All other passes (ShadowCaster, DepthOnly, DepthNormals, Meta, ...) are stock URP/Lit.
|
||||
Shader "Custom/PlayerCamoLit"
|
||||
{
|
||||
Properties
|
||||
@@ -379,7 +383,7 @@ Shader "Custom/PlayerCamoLit"
|
||||
// -------------------------------------
|
||||
// Shader Stages
|
||||
#pragma vertex LitGBufferPassVertex
|
||||
#pragma fragment LitGBufferPassFragment
|
||||
#pragma fragment CamoGBufferFragment
|
||||
|
||||
// -------------------------------------
|
||||
// Material Keywords
|
||||
@@ -438,6 +442,7 @@ Shader "Custom/PlayerCamoLit"
|
||||
// Includes
|
||||
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
|
||||
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitGBufferPass.hlsl"
|
||||
#include "PlayerCamoGBuffer.hlsl"
|
||||
ENDHLSL
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef PLAYER_CAMO_SHARED_INCLUDED
|
||||
#define PLAYER_CAMO_SHARED_INCLUDED
|
||||
|
||||
// Camo knobs + painted-height surface remap SHARED by the ForwardLit pass (PlayerCamo.hlsl) and the
|
||||
// UniversalGBuffer pass (PlayerCamoGBuffer.hlsl).
|
||||
//
|
||||
// Why the GBuffer pass needs this at all: the project renders FORWARD, so the only consumer of the
|
||||
// UniversalGBuffer pass is HTrace WSGI. Its screen-space GI composite (ColorComposeURP) multiplies
|
||||
// the traced irradiance by THIS pass's diffuse albedo (GBuffer0.rgb) and occlusion (GBuffer1.a) and
|
||||
// adds the result AFTER post-processing - i.e. after the forward pass's camo grade already ran.
|
||||
// Anything that grades the body's diffuse look (brightness/saturation, painted-height AO) must
|
||||
// therefore be mirrored into the GBuffer, or HTrace re-injects the UNGRADED look wherever indirect
|
||||
// light dominates (paint keeps its full saturation in shadows, painted height swings the GI).
|
||||
//
|
||||
// Declared OUTSIDE UnityPerMaterial (LitInput.hlsl owns that CBUFFER). The player is a handful of
|
||||
// instances, so the resulting SRP-batcher break is irrelevant.
|
||||
half _HalfLambertBrightness; // legacy name: a GENERAL brightness/exposure (see PlayerCamo.hlsl docs)
|
||||
half _Saturation; // 0 = neutral, -1 = greyscale, >0 = punchier (see PlayerCamo.hlsl docs)
|
||||
|
||||
// --- Painted height (from the occlusion channel) ---
|
||||
// The painted occlusion value (G of the MSO mask map, bound to _OcclusionMap AND _ParallaxMap) is
|
||||
// reinterpreted as a 0.5-centred HEIGHT field:
|
||||
// 0.5 -> neutral: no displacement, flat relief normal, AO = 1 (identical to the unpainted look)
|
||||
// <0.5 -> recessed (hole) >0.5 -> raised (hill)
|
||||
// The UV displacement is URP's stock single-step parallax (ParallaxOffset1Step already does
|
||||
// height*amp - amp/2, i.e. the 0.5 centring) under the _PARALLAXMAP keyword. Here we add the
|
||||
// matching per-pixel relief NORMAL from the height gradient (the dominant cue) and remap the ambient
|
||||
// occlusion so only the LOWER half darkens, with the darkening DEPTH driven by _OcclusionThreshold
|
||||
// (a shift, see below).
|
||||
half _HeightNormalStrength;
|
||||
// Runtime intensities tuned from the paint HUD. _HeightNormalIntensity scales the relief normal
|
||||
// (default 1). _OcclusionThreshold is a SHIFT (0..1, default 0.5): the AO darkening depth at the
|
||||
// deepest painted hole - 0 = no darkening at all, 1 = AO reaches full black at occ=0.
|
||||
half _HeightNormalIntensity;
|
||||
half _OcclusionThreshold;
|
||||
float4 _OcclusionMap_TexelSize;
|
||||
|
||||
// Remaps the painted occlusion/height value into surface AO + a relief normal. Runs after URP's
|
||||
// InitializeStandardLitSurfaceData (uv is already parallax-displaced) and before InitializeInputData,
|
||||
// so the relief normal flows into inputData.normalWS through the tangent frame.
|
||||
void ApplyPaintedHeightSurface(float2 uv, inout SurfaceData surfaceData)
|
||||
{
|
||||
#if defined(_OCCLUSIONMAP)
|
||||
// Only the lower (recessed) half of the painted height darkens. _OcclusionThreshold is a SHIFT =
|
||||
// how DEEP the darkening reaches at the deepest hole: AO fades from 1 (occ>=0.5, neutral/hill)
|
||||
// down to (1 - shift) at occ=0. shift=0 -> AO stays 1 everywhere (no darkening, no black spots),
|
||||
// shift=0.5 -> AO in [0.5,1], shift=1 -> AO in [0,1] (full darkening). Linear over occ in [0,0.5].
|
||||
half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
|
||||
half depth = saturate(1.0h - occ * 2.0h); // 0 at occ>=0.5 (neutral/hill), 1 at occ=0 (deepest)
|
||||
half ao = 1.0h - depth * _OcclusionThreshold; // darken toward (1 - shift) as the hole deepens
|
||||
surfaceData.occlusion = LerpWhiteTo(ao, _OcclusionStrength);
|
||||
#endif
|
||||
|
||||
#if defined(_PARALLAXMAP)
|
||||
// Per-pixel relief normal from the height (occlusion) gradient via central differences over one
|
||||
// texel. A flat field (occ=0.5 everywhere) yields zero deltas -> (0,0,1), so neutral stays unbent.
|
||||
float2 texel = _OcclusionMap_TexelSize.xy;
|
||||
half hL = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv - float2(texel.x, 0.0)).g;
|
||||
half hR = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv + float2(texel.x, 0.0)).g;
|
||||
half hD = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv - float2(0.0, texel.y)).g;
|
||||
half hU = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv + float2(0.0, texel.y)).g;
|
||||
half strength = _HeightNormalStrength * _HeightNormalIntensity;
|
||||
half3 reliefTS = normalize(half3((hL - hR) * strength,
|
||||
(hD - hU) * strength, 1.0h));
|
||||
surfaceData.normalTS = BlendNormalRNM(surfaceData.normalTS, reliefTS);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Camo body-tone grade: 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. Neutral at brightness 0 /
|
||||
// saturation 0 (x1 multiply, no-op lerp). Linear in the colour, so applying it to the ALBEDO (GBuffer
|
||||
// path) matches applying it to the lit result (forward path) for the diffuse contribution.
|
||||
void ApplyCamoBodyGrade(inout half3 color)
|
||||
{
|
||||
color *= 1.0h + _HalfLambertBrightness;
|
||||
half camoLuma = dot(color, half3(0.2126h, 0.7152h, 0.0722h)); // Rec.709 luma
|
||||
color = lerp(camoLuma.xxx, color, 1.0h + _Saturation);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 014a631929fff3b44b0edf8e54e2c373
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user