From d0d7028ebb9828c264f351857393cde2960ca288 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Sat, 11 Jul 2026 00:25:38 +0200 Subject: [PATCH] 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 --- Assets/Shaders/PictureTessellationLit.shader | 381 ++++++++++++++++++ .../PictureTessellationLit.shader.meta | 9 + 2 files changed, 390 insertions(+) create mode 100644 Assets/Shaders/PictureTessellationLit.shader create mode 100644 Assets/Shaders/PictureTessellationLit.shader.meta diff --git a/Assets/Shaders/PictureTessellationLit.shader b/Assets/Shaders/PictureTessellationLit.shader new file mode 100644 index 0000000..2d17106 --- /dev/null +++ b/Assets/Shaders/PictureTessellationLit.shader @@ -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 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 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 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 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 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 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 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" +} diff --git a/Assets/Shaders/PictureTessellationLit.shader.meta b/Assets/Shaders/PictureTessellationLit.shader.meta new file mode 100644 index 0000000..6db3cfd --- /dev/null +++ b/Assets/Shaders/PictureTessellationLit.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 19307ba9bc21d4949851f5ce569a86fe +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: