Add PictureTessellationLit shader (tessellation + height displacement)

Hull/domain tessellation with height-map displacement, occlusion map with
strength/contrast controls, height-weighted brightness multiplier, standard
PBR (base/normal/metallic/smoothness). ComplexLit pattern (UniversalForwardOnly)
so custom shading survives Deferred+; ShadowCaster/DepthOnly/DepthNormalsOnly
all run through the same displacement domain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-11 00:25:38 +02:00
1 parent 8a5f5fd832
commit d0d7028ebb
2 files changed
+390

No files matched your search

@@ -0,0 +1,381 @@
// Forward-only URP PBR shader with hardware tessellation + height-map displacement and an occlusion
// map, built for the PictureFrameBuilder picture surface (works on any mesh with sane UVs).
//
// Deferred+ compatible via the ComplexLit pattern (same as PlayerCamoLit): "UniversalMaterialType" =
// "ComplexLit" stencils these pixels out of the deferred lighting and URP shades them in the
// UniversalForwardOnly pass instead - the only way a custom vertex pipeline (tessellation) can run
// on the deferred path. ALL depth-relevant passes (ShadowCaster / DepthOnly / DepthNormalsOnly) run
// the same hull/domain displacement so shadows, SSAO and depth match the displaced surface.
//
// Displacement = (height - 0.5) * _Displacement * _TessellationStrength along the vertex normal;
// _Displacement is the authored amplitude in meters, _TessellationStrength is the runtime knob the
// PictureFrameBuilder drives per frame (like _BumpScale / _OcclusionStrength).
Shader "Chameleon/PictureTessellationLit"
{
Properties
{
_BaseMap("Albedo", 2D) = "white" {}
_BaseColor("Color", Color) = (1, 1, 1, 1)
_Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5
_Metallic("Metallic", Range(0.0, 1.0)) = 0.0
[Normal] _BumpMap("Normal Map", 2D) = "bump" {}
_BumpScale("Normal Strength", Range(0.0, 3.0)) = 1.0
_HeightMap("Height Map", 2D) = "gray" {}
_Displacement("Displacement (m)", Range(0.0, 0.2)) = 0.02
_TessellationStrength("Tessellation Strength", Range(0.0, 3.0)) = 1.0
_TessellationFactor("Tessellation Factor", Range(1.0, 32.0)) = 8.0
_OcclusionMap("Occlusion Map", 2D) = "white" {}
_OcclusionStrength("Occlusion Strength", Range(0.0, 3.0)) = 1.0
_OcclusionContrast("Occlusion Contrast", Range(-1.0, 3.0)) = 0.0
_Brightness("Brightness", Range(0.0, 3.0)) = 1.0
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"UniversalMaterialType" = "ComplexLit"
"Queue" = "Geometry"
}
LOD 300
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
TEXTURE2D(_HeightMap); SAMPLER(sampler_HeightMap);
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _Smoothness;
half _Metallic;
half _BumpScale;
half _Displacement;
half _TessellationStrength;
half _TessellationFactor;
half _OcclusionStrength;
half _OcclusionContrast;
half _Brightness;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 uv : TEXCOORD0;
};
// Hull/domain control point: everything stays in object space until the domain stage.
struct ControlPoint
{
float4 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 uv : TEXCOORD0;
};
struct TessFactors
{
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
ControlPoint TessVertex(Attributes input)
{
ControlPoint p;
p.positionOS = input.positionOS;
p.normalOS = input.normalOS;
p.tangentOS = input.tangentOS;
p.uv = input.uv;
return p;
}
TessFactors PatchConstants(InputPatch<ControlPoint, 3> patch)
{
TessFactors f;
f.edge[0] = _TessellationFactor;
f.edge[1] = _TessellationFactor;
f.edge[2] = _TessellationFactor;
f.inside = _TessellationFactor;
return f;
}
[domain("tri")]
[outputcontrolpoints(3)]
[outputtopology("triangle_cw")]
[partitioning("fractional_odd")]
[patchconstantfunc("PatchConstants")]
ControlPoint TessHull(InputPatch<ControlPoint, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];
}
#define TESS_INTERPOLATE(field) (patch[0].field * bary.x + patch[1].field * bary.y + patch[2].field * bary.z)
// Barycentric interpolation + height displacement along the vertex normal; each pass turns
// the resulting object-space vertex into its own varyings in its domain function.
Attributes TessInterpolate(OutputPatch<ControlPoint, 3> patch, float3 bary)
{
Attributes v;
v.positionOS = TESS_INTERPOLATE(positionOS);
v.normalOS = normalize(TESS_INTERPOLATE(normalOS));
v.tangentOS = TESS_INTERPOLATE(tangentOS);
v.uv = TESS_INTERPOLATE(uv);
float height = SAMPLE_TEXTURE2D_LOD(_HeightMap, sampler_HeightMap, TRANSFORM_TEX(v.uv, _BaseMap), 0).r;
v.positionOS.xyz += v.normalOS * ((height - 0.5) * _Displacement * _TessellationStrength);
return v;
}
ENDHLSL
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForwardOnly" }
ZWrite On
Cull Back
HLSLPROGRAM
#pragma target 4.6
#pragma vertex TessVertex
#pragma hull TessHull
#pragma domain ForwardDomain
#pragma fragment ForwardFragment
// Keyword set mirrored from PlayerCamoLit's working Deferred+ forward-only pass,
// minus lightmap/detail/emission features this shader does not have.
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION
#pragma multi_compile_fragment _ _REFLECTION_PROBE_ATLAS
#pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile_fragment _ _LIGHT_COOKIES
#pragma multi_compile _ _LIGHT_LAYERS
#pragma multi_compile _ _CLUSTER_LIGHT_LOOP
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
TEXTURE2D(_BumpMap); SAMPLER(sampler_BumpMap);
TEXTURE2D(_OcclusionMap); SAMPLER(sampler_OcclusionMap);
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
float4 tangentWS : TEXCOORD3; // w = bitangent sign
float fogCoord : TEXCOORD4;
};
[domain("tri")]
Varyings ForwardDomain(TessFactors factors, OutputPatch<ControlPoint, 3> patch, float3 bary : SV_DomainLocation)
{
Attributes v = TessInterpolate(patch, bary);
VertexPositionInputs pos = GetVertexPositionInputs(v.positionOS.xyz);
VertexNormalInputs nrm = GetVertexNormalInputs(v.normalOS, v.tangentOS);
Varyings o;
o.positionCS = pos.positionCS;
o.positionWS = pos.positionWS;
o.normalWS = nrm.normalWS;
o.tangentWS = float4(nrm.tangentWS, v.tangentOS.w * GetOddNegativeScale());
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
o.fogCoord = ComputeFogFactor(pos.positionCS.z);
return o;
}
half4 ForwardFragment(Varyings input) : SV_Target
{
half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
// Height-weighted brightness: raised areas get the full multiplier, deep areas stay
// untouched (height 0.5 = mid-gray default when no map is bound -> half effect).
half heightBr = SAMPLE_TEXTURE2D(_HeightMap, sampler_HeightMap, input.uv).r;
albedo.rgb *= lerp(1.0h, _Brightness, heightBr);
half3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, input.uv), _BumpScale);
half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, input.uv).g;
// Contrast around mid-gray: 0 = as sampled, -1 = flat (no occlusion detail), 3 = 4x.
occ = saturate((occ - 0.5h) * (1.0h + _OcclusionContrast) + 0.5h);
float sgn = input.tangentWS.w;
float3 bitangentWS = sgn * cross(input.normalWS, input.tangentWS.xyz);
half3x3 tbn = half3x3(input.tangentWS.xyz, bitangentWS, input.normalWS);
InputData inputData = (InputData)0;
inputData.positionWS = input.positionWS;
inputData.positionCS = input.positionCS;
inputData.normalWS = NormalizeNormalPerPixel(TransformTangentToWorld(normalTS, tbn));
inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(input.positionWS);
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);
#if defined(_MAIN_LIGHT_SHADOWS_SCREEN)
inputData.shadowCoord = float4(inputData.normalizedScreenSpaceUV, 0, 1);
#else
inputData.shadowCoord = TransformWorldToShadowCoord(input.positionWS);
#endif
inputData.fogCoord = input.fogCoord;
inputData.bakedGI = SampleSH(inputData.normalWS);
inputData.shadowMask = half4(1, 1, 1, 1);
SurfaceData surfaceData = (SurfaceData)0;
surfaceData.albedo = albedo.rgb;
surfaceData.alpha = 1;
surfaceData.metallic = _Metallic;
surfaceData.smoothness = _Smoothness;
surfaceData.normalTS = normalTS;
// Strength > 1 keeps darkening past the authored map (saturate stops negatives).
surfaceData.occlusion = saturate(LerpWhiteTo(occ, _OcclusionStrength));
half4 color = UniversalFragmentPBR(inputData, surfaceData);
color.rgb = MixFog(color.rgb, input.fogCoord);
return color;
}
ENDHLSL
}
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
ColorMask 0
Cull Back
HLSLPROGRAM
#pragma target 4.6
#pragma vertex TessVertex
#pragma hull TessHull
#pragma domain ShadowDomain
#pragma fragment ShadowFragment
// NOT multi_compile_vertex: the keyword is consumed in the DOMAIN stage here.
#pragma multi_compile _ _CASTING_PUNCTUAL_LIGHT_SHADOW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
float3 _LightDirection;
float3 _LightPosition;
struct ShadowVaryings
{
float4 positionCS : SV_POSITION;
};
[domain("tri")]
ShadowVaryings ShadowDomain(TessFactors factors, OutputPatch<ControlPoint, 3> patch, float3 bary : SV_DomainLocation)
{
Attributes v = TessInterpolate(patch, bary);
float3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
float3 normalWS = TransformObjectToWorldNormal(v.normalOS);
#if defined(_CASTING_PUNCTUAL_LIGHT_SHADOW)
float3 lightDirectionWS = normalize(_LightPosition - positionWS);
#else
float3 lightDirectionWS = _LightDirection;
#endif
float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS));
#if UNITY_REVERSED_Z
positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#else
positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#endif
ShadowVaryings o;
o.positionCS = positionCS;
return o;
}
half4 ShadowFragment() : SV_Target
{
return 0;
}
ENDHLSL
}
Pass
{
Name "DepthOnly"
Tags { "LightMode" = "DepthOnly" }
ZWrite On
ColorMask R
Cull Back
HLSLPROGRAM
#pragma target 4.6
#pragma vertex TessVertex
#pragma hull TessHull
#pragma domain DepthDomain
#pragma fragment DepthFragment
struct DepthVaryings
{
float4 positionCS : SV_POSITION;
};
[domain("tri")]
DepthVaryings DepthDomain(TessFactors factors, OutputPatch<ControlPoint, 3> patch, float3 bary : SV_DomainLocation)
{
Attributes v = TessInterpolate(patch, bary);
DepthVaryings o;
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
return o;
}
half DepthFragment(DepthVaryings input) : SV_Target
{
return input.positionCS.z;
}
ENDHLSL
}
Pass
{
// Deferred+'s normals prepass draws ONLY this tag (the forward path draws both), see the
// PlayerCamoLit fix - geometric (displaced) normal is enough for SSAO/HTrace.
Name "DepthNormalsOnly"
Tags { "LightMode" = "DepthNormalsOnly" }
ZWrite On
Cull Back
HLSLPROGRAM
#pragma target 4.6
#pragma vertex TessVertex
#pragma hull TessHull
#pragma domain DepthNormalsDomain
#pragma fragment DepthNormalsFragment
struct DepthNormalsVaryings
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
};
[domain("tri")]
DepthNormalsVaryings DepthNormalsDomain(TessFactors factors, OutputPatch<ControlPoint, 3> patch, float3 bary : SV_DomainLocation)
{
Attributes v = TessInterpolate(patch, bary);
DepthNormalsVaryings o;
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.normalWS = TransformObjectToWorldNormal(v.normalOS);
return o;
}
half4 DepthNormalsFragment(DepthNormalsVaryings input) : SV_Target
{
return half4(NormalizeNormalPerPixel(input.normalWS), 0);
}
ENDHLSL
}
}
FallBack "Universal Render Pipeline/Lit"
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 19307ba9bc21d4949851f5ce569a86fe
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant: