// SPDX-License-Identifier: (Not available for this version, you are only allowed to use this software if you have express permission from the copyright holder and agreed to the latest NiloToonURP EULA) // Copyright (c) 2021 Kuroneko ShaderLab Limited // Fur geometry shader for NiloToonCharacter — lilToon-style barycentric fur shell generation. // Adapted from lilToon's lil_common_vert_fur.hlsl (AppendFur / geom functions). // // This file is included by the fur pass blocks in NiloToonCharacter.shader, // AFTER NiloToonCharacter_Shared.hlsl (so Varyings struct and all helpers are available). #pragma once // NiloFurUtil.hlsl is already included via NiloAllUtilIncludes.hlsl -> Shared.hlsl. // Do NOT include it again here — duplicate include causes "redefinition of NiloLerp3" // because Unity may resolve relative vs absolute paths differently, bypassing #pragma once. //============================================================================= // InterpolateVaryings — single source of truth for Varyings struct interpolation. // // ** MAINTENANCE NOTE ** // When a field is added to or removed from the Varyings struct in // NiloToonCharacter_Shared.hlsl, this function MUST be updated to match. // A corresponding comment exists at the Varyings struct definition. //============================================================================= Varyings InterpolateVaryings(Varyings a, Varyings b, Varyings c, float3 f) { Varyings r = (Varyings)0; // positionCS will be recomputed from interpolated positionWS by the caller // r.positionCS is left as 0 here r.uv01 = NiloLerp3(a.uv01, b.uv01, c.uv01, f); r.uv23 = NiloLerp3(a.uv23, b.uv23, c.uv23, f); r.positionWS_ZOffsetFinalSum = NiloLerp3(a.positionWS_ZOffsetFinalSum, b.positionWS_ZOffsetFinalSum, c.positionWS_ZOffsetFinalSum, f); r.SH_fogFactor = NiloLerp3(a.SH_fogFactor, b.SH_fogFactor, c.SH_fogFactor, f); r.normalWS_averageShadowAttenuation = NiloLerp3(a.normalWS_averageShadowAttenuation, b.normalWS_averageShadowAttenuation, c.normalWS_averageShadowAttenuation, f); r.smoothedNormalWS = NiloLerp3(a.smoothedNormalWS, b.smoothedNormalWS, c.smoothedNormalWS, f); #if VaryingsHasViewDirTS r.viewDirTS = NiloLerp3(a.viewDirTS, b.viewDirTS, c.viewDirTS, f); #endif #if VaryingsHasTangentWS r.tangentWS = NiloLerp3(a.tangentWS, b.tangentWS, c.tangentWS, f); #endif r.color = NiloLerp3(a.color, b.color, c.color, f); #if _NILOTOON_DEBUG_SHADING r.uv8 = NiloLerp3(a.uv8, b.uv8, c.uv8, f); #endif // Fur-specific fields r.furVector = NiloLerp3(a.furVector, b.furVector, c.furVector, f); r.furLayer = 0; // caller sets this r.furVertexID = a.furVertexID; // not consumed after geometry; keep the nointerpolation field initialized // GPU instancing / stereo #if !NiloToonCharSelfShadowCasterPass UNITY_TRANSFER_INSTANCE_ID(a, r); UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(a, r); #endif return r; } void NiloApplyFurDepthPrepassZBias(inout float4 positionCS, float bias) { #if UNITY_REVERSED_Z positionCS.z -= bias; #else positionCS.z += bias; #endif } //============================================================================= // AppendFurQuad — emit 2 vertices (base + tip) forming one fur quad edge. // Adapted from lilToon's AppendFur() in lil_common_vert_fur.hlsl lines 256-371. //============================================================================= void AppendFurQuad( inout TriangleStream outStream, Varyings input[3], float3 furVectors[3], float3 factor) { Varyings output = InterpolateVaryings(input[0], input[1], input[2], factor); float3 posWS = output.positionWS_ZOffsetFinalSum.xyz; // Base vertex (furLayer = 0) — at original mesh surface output.furLayer = 0; output.positionCS = TransformWorldToHClip(posWS); #if NiloToonFurDepthPrepassPass || NiloToonFurPrepassBufferDepthPass NiloApplyFurDepthPrepassZBias(output.positionCS, 0.0000001); #endif outStream.Append(output); // Tip vertex (furLayer = 1) — displaced by fur vector float3 mixVector = NiloLerp3(furVectors[0], furVectors[1], furVectors[2], factor); posWS += mixVector; output.positionWS_ZOffsetFinalSum.xyz = posWS; output.furLayer = 1; output.positionCS = TransformWorldToHClip(posWS); #if NiloToonFurDepthPrepassPass || NiloToonFurPrepassBufferDepthPass NiloApplyFurDepthPrepassZBias(output.positionCS, 0.000001); #endif outStream.Append(output); } //============================================================================= // FurShellGeom — geometry shader entry point. // Receives one triangle, emits fur shell quads using lilToon's barycentric layout. // Adapted from lilToon's geom() in lil_common_vert_fur.hlsl lines 375-503. // // D3D11 geometry shader limit: maxvertexcount × output_scalars ≤ 1024. // NiloToon's Varyings struct is large (40-48 scalars depending on keywords) // because fur reuses the full toon lighting pipeline (0% fragment duplication). // This limits us to layer 1-2 (max 14 verts). 14 × 48 = 672, safely under 1024. // (lilToon supports layer 3 because its v2f struct is much smaller.) // // maxvertexcount breakdown: // LayerNum == 1: 3 AppendFurQuad calls × 2 verts = 6 + closing 2 = 8 // LayerNum == 2: 6 AppendFurQuad calls × 2 verts = 12 + closing 2 = 14 //============================================================================= [maxvertexcount(14)] void FurShellGeom(triangle Varyings input[3], inout TriangleStream outStream) { // When _NILOTOON_FUR keyword is not enabled on this material, emit nothing. // The renderer feature still enqueues the fur pass for all materials, // but only materials with the keyword active should actually produce fur shells. #if !_NILOTOON_FUR return; #endif // Per-vertex fur vectors (already computed in vertex shader) float3 furVectors[3]; furVectors[0] = input[0].furVector; furVectors[1] = input[1].furVector; furVectors[2] = input[2].furVector; // Per-vertex noise randomization using stable vertex ID hashing (from lilToon lines 461-469). // Do not hash positionWS here: object rotation changes world positions and makes randomize flicker. uint h0 = input[0].furVertexID; uint h1 = input[1].furVertexID; uint h2 = input[2].furVertexID; uint3 n0 = (h0 * 3u + h1 * 1u + h2 * 1u) * uint3(1597334677u, 3812015801u, 2912667907u); uint3 n1 = (h0 * 1u + h1 * 3u + h2 * 1u) * uint3(1597334677u, 3812015801u, 2912667907u); uint3 n2 = (h0 * 1u + h1 * 1u + h2 * 3u) * uint3(1597334677u, 3812015801u, 2912667907u); float3 noise0 = normalize(float3(n0) * (2.0 / float(0xFFFFFFFFu)) - 1.0); float3 noise1 = normalize(float3(n1) * (2.0 / float(0xFFFFFFFFu)) - 1.0); float3 noise2 = normalize(float3(n2) * (2.0 / float(0xFFFFFFFFu)) - 1.0); float randomizedFurLength = _NiloFurLength * _NiloFurStrength * _NiloFurRandomize; furVectors[0] += noise0 * randomizedFurLength; furVectors[1] += noise1 * randomizedFurLength; furVectors[2] += noise2 * randomizedFurLength; // Per-vertex fur length mask (from lilToon lines 471-475) // Sample in geometry shader via tex2Dlod (explicit mip 0) to scale fur length per-vertex float2 uv0 = input[0].uv01.xy; float2 uv1 = input[1].uv01.xy; float2 uv2 = input[2].uv01.xy; furVectors[0] *= SAMPLE_TEXTURE2D_LOD(_NiloFurLengthMask, sampler_linear_repeat, uv0, 0).r; furVectors[1] *= SAMPLE_TEXTURE2D_LOD(_NiloFurLengthMask, sampler_linear_repeat, uv1, 0).r; furVectors[2] *= SAMPLE_TEXTURE2D_LOD(_NiloFurLengthMask, sampler_linear_repeat, uv2, 0).r; // _FurCutoutLength is now applied in the vertex shader (only for depth prepass). // Color pass gets full-length fur — the outer portion blends softly (lilToon FurTwoPass design). // Emit fur quads using lilToon's barycentric layout (lines 477-502). // Layer count limited to 1-2 due to D3D11 geometry shader output limit // (maxvertexcount × scalars_per_vertex ≤ 1024). NiloToon's full Varyings // struct is too large for layer 3 (would need 26 verts × 39+ scalars > 1024). float layerNum = _NiloFurLayerNum; if (layerNum < 1.5) // LayerNum == 1: vertices only (3 quads = 6 verts) { AppendFurQuad(outStream, input, furVectors, float3(1.0, 0.0, 0.0)); AppendFurQuad(outStream, input, furVectors, float3(0.0, 1.0, 0.0)); AppendFurQuad(outStream, input, furVectors, float3(0.0, 0.0, 1.0)); } else // LayerNum == 2: interleaved vertices + midpoints (6 quads = 12 verts) { // lilToon interleaves: vertex A, midpoint BC, vertex B, midpoint AC, vertex C, midpoint AB // This creates a valid continuous triangle strip. AppendFurQuad(outStream, input, furVectors, float3(1.0, 0.0, 0.0)); // vertex A AppendFurQuad(outStream, input, furVectors, float3(0.0, 1.0, 1.0) / 2.0); // midpoint BC AppendFurQuad(outStream, input, furVectors, float3(0.0, 1.0, 0.0)); // vertex B AppendFurQuad(outStream, input, furVectors, float3(1.0, 0.0, 1.0) / 2.0); // midpoint AC AppendFurQuad(outStream, input, furVectors, float3(0.0, 0.0, 1.0)); // vertex C AppendFurQuad(outStream, input, furVectors, float3(1.0, 1.0, 0.0) / 2.0); // midpoint AB } // Closing vertex + restart strip (matches lilToon line 502) AppendFurQuad(outStream, input, furVectors, float3(1.0, 0.0, 0.0)); outStream.RestartStrip(); }