201 lines
9.3 KiB
HLSL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<Varyings> 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<Varyings> 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();
}