赞
踩
实现毛发着色器的方法有多种,其中最常用的是shell方法和fin方法。
Fin方法通过在多边形表面上添加垂直于表面的许多细长几何体,然后在这些几何体上贴上毛发纹理来表现毛发的外观。这个方法的概念就像在多边形上“种植”许多“鱼鳍”一样。
通过将许多细长的几何体放置在多边形表面上,可以模拟出毛发的外观,并使用毛发纹理来增加细节和真实感。这种方法允许毛发与光线进行交互,并在每个几何体上进行光照和阴影计算,从而实现更加逼真的毛发效果。
关于Fin方法中毛发表现的光照处理,有许多不同的方法被提出。其中最简单的方法是在毛鳍上施加简易的顶点级光照处理,这种方法相对较轻,被广泛使用。
这种方法只需在毛鳍上从光源方向近的角度施加明暗的渐变效果。毛发根部处于其他毛发的遮挡位置,应该没有光线直接照射到根部。然而,由于这种简易方法无法很好地处理这种情况,因此需要进行一些调整,以确保高光效果不过于集中在毛发根部。
与之前一样,本次我们将继续考虑使用几何着色器在不直接修改原始多边形的情况下动态生成毛发的方法。关于使用几何着色器实现Fin的方式有许多种(例如,使用线条在所有边上生成,使用lineadj来比较两个面的法线和相机方向以检测边缘等),但由于我们也想绘制原始多边形,因此让我们尝试使用三角形作为几何体,并在输出时同时输出原始多边形的顶点和动态生成的毛发。
ShaderLab
Shader "Fur/Fin/Unlit" { Properties { [Header(Basic)][Space] [Toggle(DRAW_ORIG_POLYGON)]_DrawOrigPolygon("Draw Original Polygon", Float) = 0 [MainColor] _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1) _BaseMap("Base Map", 2D) = "white" {} [Header(Fur)][Space] _FurMap("Fur Map", 2D) = "white" {} _AlphaCutout("Alpha Cutout", Range(0.0, 1.0)) = 0.2 _FinLength("Fin Length", Range(0.0, 1.0)) = 0.1 _FaceViewProdThresh("Fin Direction Threshold", Range(0.0, 1.0)) = 0.1 _Occlusion("Occlusion", Range(0.0, 1.0)) = 0.3 } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" } ZWrite On Cull Off Pass { Name "Unlit" HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma multi_compile_fog #pragma multi_compile _ DRAW_ORIG_POLYGON #pragma vertex vert #pragma require geometry #pragma geometry geom #pragma fragment frag #include "./FurFinUnlit.hlsl" ENDHLSL } } }
除了控制Fin长度和纹理的参数外,我们还可以将是否绘制原始多边形的标志(DRAW_ORIG_POLYGON
)以及在相机垂直方向时是否跳过生成的阈值(_FaceViewProdThresh
)作为参数进行设置。
HLSL
#ifndef FUR_FIN_UNLIT_HLSL #define FUR_FIN_UNLIT_HLSL #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" float _FaceViewProdThresh;//相机视线与面法线的内积阈值,用于决定是否跳过生成Fin的部分 float _FinLength;//Fin的长度 float _AlphaCutout;//Alpha剪裁的阈值,用于剔除不透明度低于该阈值的像素 float _Occlusion;//遮挡因子,用于控制Fin的遮挡效果 TEXTURE2D(_FurMap); SAMPLER(sampler_FurMap); float4 _FurMap_ST; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float fogCoord : TEXCOORD1; float3 finUv : TEXCOORD2; }; Attributes vert(Attributes input) { return input; } inline float3 GetViewDirectionOS(float3 posOS) { float3 cameraOS = TransformWorldToObject(GetCameraPositionWS()); return normalize(posOS - cameraOS); } void AppendFinVertex(inout TriangleStream<Varyings> stream, in Attributes input, float3 posOS, float3 normalOS, float2 finUv) { Varyings output; posOS += normalOS * (finUv.y * _FinLength); output.vertex = TransformObjectToHClip(posOS); output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.fogCoord = ComputeFogFactor(output.vertex.z); output.finUv = finUv; stream.Append(output); } [maxvertexcount(7)] void geom(triangle Attributes input[3], inout TriangleStream<Varyings> stream) { #ifdef DRAW_ORIG_POLYGON for (int i = 0; i < 3; ++i) { Varyings output; output.vertex = TransformObjectToHClip(input[i].positionOS.xyz); output.uv = TRANSFORM_TEX(input[i].uv, _BaseMap); output.fogCoord = ComputeFogFactor(output.vertex.z); output.finUv = float2(-1.0, -1.0); stream.Append(output); } stream.RestartStrip(); #endif float3 posOS0 = input[0].positionOS.xyz; float3 posOS1 = input[1].positionOS.xyz; float3 posOS2 = input[2].positionOS.xyz; float3 lineOS01 = posOS1 - posOS0; float3 lineOS02 = posOS2 - posOS0; float3 normalOS = normalize(cross(lineOS01, lineOS02)); float3 centerOS = (posOS0 + posOS1 + posOS2) / 3; float3 viewDirOS = GetViewDirectionOS(centerOS); float eyeDotN = dot(viewDirOS, normalOS); if (abs(eyeDotN) > _FaceViewProdThresh) return; float3 lineCenterOS = (lineOS01 + lineOS02) / 2; float3 posOS3 = posOS0 + lineCenterOS; AppendFinVertex(stream, input[0], posOS0, normalOS, float2(0.0, 0.0)); AppendFinVertex(stream, input[1], posOS3, normalOS, float2(1.0, 0.0)); AppendFinVertex(stream, input[0], posOS0, normalOS, float2(0.0, 1.0)); AppendFinVertex(stream, input[1], posOS3, normalOS, float2(1.0, 1.0)); stream.RestartStrip(); } float4 frag(Varyings input) : SV_Target { float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.finUv); if (input.finUv.z == 0 && furColor.a < _AlphaCutout) discard; float4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); color *= _BaseColor; color *= furColor; color.rgb *= lerp(1.0 - _Occlusion, 1.0, input.finUv.y); color.rgb = MixFog(color.rgb, input.fogCoord); return color; } #endif
我们可以按照下图的方式,在三角形多边形的中间绘制一条线,并将其沿着面法线方向推出,形成Fin的形状。
此外,对于毛发纹理,可以用以下样式的图案。为了考虑到Mipmap和采样,最好不要使用黑白图像,而是只使用Alpha通道。
点击上图可以查看正确的图案
结果如下
下面简单地设置了多个材质进行测试
问题点
事实上,它过多地取决于多边形的形状。如果尝试将它应用到立方体或胶囊上,它会看起来像这样。
在动态植鳍的方法中,多边形的数量是关键,所以如果多边形不够,就需要增加多边形的数量或者使用镶嵌来增加。另外,当前毛皮的 UV 对于每个鳍来说是 (0.0, 0.0) ~ (1.0, 1.0),这是一个问题(纹理被拉伸),但这也可以通过计算在一定程度上修复。
统一 UV 坐标
在UV空间中考虑距离,可以大致调整毛发的密度,前提是贴图相对均匀(例如,将网格贴图应用于模型时,每个网格的面积大致相同)。将在几何着色器中对为Fin指定UV的部分进行以下修改。添加了一个名为_Density
的参数来控制密度。
float _Density; void geom(...) { ... float2 uv1 = (TRANSFORM_TEX(input[1].uv, _BaseMap) + TRANSFORM_TEX(input[2].uv, _BaseMap)) / 2; float uvLen = length(uv0 - uv1); float uvOffset = length(uv0); float uvXScale = uvLen * _Density; AppendFinVertex(stream, input[0], posOS0, normalOS, float2(uvOffset, 0.0)); AppendFinVertex(stream, input[1], posOS3, normalOS, float2(uvOffset + uvXScale, 0.0)); AppendFinVertex(stream, input[0], posOS0, normalOS, float2(uvOffset, 1.0)); AppendFinVertex(stream, input[1], posOS3, normalOS, float2(uvOffset + uvXScale, 1.0)); }
结果如下
可以看到,胶囊,圆柱部分和半球部分的长度变得均匀。
在尝试使用细分之前,可以注意到生成的Fin方式不够均匀,因此尝试为每个三角形添加3个鳍,以使其更加对称。我们可以将几何着色器进行以下修改:
void AppendFinVertex(inout TriangleStream<Varyings> stream, float2 uv, float3 posOS, float3 normalOS, float2 finUv) { Varyings output; posOS += normalOS * (finUv.y * _FinLength); output.vertex = TransformObjectToHClip(posOS); output.uv = uv; output.fogCoord = ComputeFogFactor(output.vertex.z); output.finUv = finUv; stream.Append(output); } void AppendFinVertices( inout TriangleStream<Varyings> stream, Attributes input0, Attributes input1, Attributes input2, float3 normalOS) { float3 posOS0 = input0.positionOS.xyz; float3 lineOS01 = input1.positionOS.xyz - posOS0; float3 lineOS02 = input2.positionOS.xyz - posOS0; float3 posOS3 = posOS0 + (lineOS01 + lineOS02) / 2; float2 uv0 = TRANSFORM_TEX(input0.uv, _BaseMap); float2 uv12 = (TRANSFORM_TEX(input1.uv, _BaseMap) + TRANSFORM_TEX(input2.uv, _BaseMap)) / 2; float uvOffset = length(uv0); float uvXScale = length(uv0 - uv12) * _Density; AppendFinVertex(stream, uv0, posOS0, normalOS, float2(uvOffset, 0.0)); AppendFinVertex(stream, uv12, posOS3, normalOS, float2(uvOffset + uvXScale, 0.0)); AppendFinVertex(stream, uv0, posOS0, normalOS, float2(uvOffset, 1.0)); AppendFinVertex(stream, uv12, posOS3, normalOS, float2(uvOffset + uvXScale, 1.0)); stream.RestartStrip(); } [maxvertexcount(15)] void geom(triangle Attributes input[3], inout TriangleStream<Varyings> stream) { #ifdef DRAW_ORIG_POLYGON for (int i = 0; i < 3; ++i) { Varyings output; output.vertex = TransformObjectToHClip(input[i].positionOS.xyz); output.uv = TRANSFORM_TEX(input[i].uv, _BaseMap); output.fogCoord = ComputeFogFactor(output.vertex.z); output.finUv = float2(-1.0, -1.0); stream.Append(output); } stream.RestartStrip(); #endif float3 lineOS01 = (input[1].positionOS - input[0].positionOS).xyz; float3 lineOS02 = (input[2].positionOS - input[0].positionOS).xyz; float3 normalOS = normalize(cross(lineOS01, lineOS02)); float3 centerOS = (input[0].positionOS + input[1].positionOS + input[2].positionOS).xyz / 3; float3 viewDirOS = GetViewDirectionOS(centerOS); float eyeDotN = dot(viewDirOS, normalOS); if (abs(eyeDotN) > _FaceViewProdThresh) return; AppendFinVertices(stream, input[0], input[1], input[2], normalOS); AppendFinVertices(stream, input[2], input[0], input[1], normalOS); AppendFinVertices(stream, input[1], input[2], input[0], normalOS); }
结果如下
增加密度
我们使用PN Triangles方法来实现细分曲面。我们可以根据距离调整细分因子,使得远处的细分效果较弱,而靠近时逐渐增加细分级别。我们将在几何着色器中实现这个功能。
PN Triangles方法原理可以参考这篇文章文章链接
请注意,曲面细分的每个阶段都插入在几何着色器和顶点着色器之间。
ShaderLab
Shader "Fur/Fin/Unlit" { Properties { ... [Header(Tesselation)][Space] _TessMinDist("Tesselation Min Distance", Range(0.1, 50)) = 1.0 _TessMaxDist("Tesselation Max Distance", Range(0.1, 50)) = 10.0 _TessFactor("Tessellation Factor", Range(1, 50)) = 10 } SubShader { ... Pass { Name "Unlit" HLSLPROGRAM ... #pragma require tessellation tessHW #pragma hull hull #pragma domain domain ... ENDHLSL } } }
HLSL
float _TessMinDist; float _TessMaxDist; float _TessFactor; struct HsConstantOutput { float fTessFactor[3] : SV_TessFactor; float fInsideTessFactor : SV_InsideTessFactor; float3 f3B210 : POS3; float3 f3B120 : POS4; float3 f3B021 : POS5; float3 f3B012 : POS6; float3 f3B102 : POS7; float3 f3B201 : POS8; float3 f3B111 : CENTER; float3 f3N110 : NORMAL3; float3 f3N011 : NORMAL4; float3 f3N101 : NORMAL5; }; [domain("tri")] [partitioning("integer")] [outputtopology("triangle_cw")] [patchconstantfunc("hullConst")] [outputcontrolpoints(3)] Attributes hull(InputPatch<Attributes, 3> input, uint id : SV_OutputControlPointID) { return input[id]; } HsConstantOutput hullConst(InputPatch<Attributes, 3> i) { HsConstantOutput o = (HsConstantOutput)0; float distance = length(float3(UNITY_MATRIX_MV[0][3], UNITY_MATRIX_MV[1][3], UNITY_MATRIX_MV[2][3])); float factor = (_TessMaxDist - _TessMinDist) / max(distance - _TessMinDist, 0.01); factor = min(factor, 1.0); factor *= _TessFactor; o.fTessFactor[0] = o.fTessFactor[1] = o.fTessFactor[2] = factor; o.fInsideTessFactor = factor; float3 f3B003 = i[0].positionOS.xyz; float3 f3B030 = i[1].positionOS.xyz; float3 f3B300 = i[2].positionOS.xyz; float3 f3N002 = i[0].normalOS; float3 f3N020 = i[1].normalOS; float3 f3N200 = i[2].normalOS; o.f3B210 = ((2.0 * f3B003) + f3B030 - (dot((f3B030 - f3B003), f3N002) * f3N002)) / 3.0; o.f3B120 = ((2.0 * f3B030) + f3B003 - (dot((f3B003 - f3B030), f3N020) * f3N020)) / 3.0; o.f3B021 = ((2.0 * f3B030) + f3B300 - (dot((f3B300 - f3B030), f3N020) * f3N020)) / 3.0; o.f3B012 = ((2.0 * f3B300) + f3B030 - (dot((f3B030 - f3B300), f3N200) * f3N200)) / 3.0; o.f3B102 = ((2.0 * f3B300) + f3B003 - (dot((f3B003 - f3B300), f3N200) * f3N200)) / 3.0; o.f3B201 = ((2.0 * f3B003) + f3B300 - (dot((f3B300 - f3B003), f3N002) * f3N002)) / 3.0; float3 f3E = (o.f3B210 + o.f3B120 + o.f3B021 + o.f3B012 + o.f3B102 + o.f3B201) / 6.0; float3 f3V = (f3B003 + f3B030 + f3B300) / 3.0; o.f3B111 = f3E + ((f3E - f3V) / 2.0); float fV12 = 2.0 * dot(f3B030 - f3B003, f3N002 + f3N020) / dot(f3B030 - f3B003, f3B030 - f3B003); float fV23 = 2.0 * dot(f3B300 - f3B030, f3N020 + f3N200) / dot(f3B300 - f3B030, f3B300 - f3B030); float fV31 = 2.0 * dot(f3B003 - f3B300, f3N200 + f3N002) / dot(f3B003 - f3B300, f3B003 - f3B300); o.f3N110 = normalize(f3N002 + f3N020 - fV12 * (f3B030 - f3B003)); o.f3N011 = normalize(f3N020 + f3N200 - fV23 * (f3B300 - f3B030)); o.f3N101 = normalize(f3N200 + f3N002 - fV31 * (f3B003 - f3B300)); return o; } [domain("tri")] Attributes domain( HsConstantOutput hsConst, const OutputPatch<Attributes, 3> i, float3 bary : SV_DomainLocation) { Attributes o = (Attributes)0; float fU = bary.x; float fV = bary.y; float fW = bary.z; float fUU = fU * fU; float fVV = fV * fV; float fWW = fW * fW; float fUU3 = fUU * 3.0f; float fVV3 = fVV * 3.0f; float fWW3 = fWW * 3.0f; o.positionOS.xyz = float4( i[0].positionOS.xyz * fWW * fW + i[1].positionOS.xyz * fUU * fU + i[2].positionOS.xyz * fVV * fV + hsConst.f3B210 * fWW3 * fU + hsConst.f3B120 * fW * fUU3 + hsConst.f3B201 * fWW3 * fV + hsConst.f3B021 * fUU3 * fV + hsConst.f3B102 * fW * fVV3 + hsConst.f3B012 * fU * fVV3 + hsConst.f3B111 * 6.0f * fW * fU * fV, 1.0); o.normalOS = normalize( i[0].normalOS * fWW + i[1].normalOS * fUU + i[2].normalOS * fVV + hsConst.f3N110 * fW * fU + hsConst.f3N011 * fU * fV + hsConst.f3N101 * fW * fV); o.uv = i[0].uv * fW + i[1].uv * fU + i[2].uv * fV; return o; }
结果如下
方向随机性
我们在方向上添加一点随机性。
float _RandomDirection; inline float rand(float2 seed) { return frac(sin(dot(seed.xy, float2(12.9898, 78.233))) * 43758.5453); } inline float3 rand3(float2 seed) { return 2.0 * (float3(rand(seed * 1), rand(seed * 2), rand(seed * 3)) - 0.5); } void geom(...) { ... normalOS += rand3(input[0].uv) * _RandomDirection; normalOS = normalize(normalOS); ... }
结果如下
觉得发束感太多了,我们可以通过镶嵌因子稍微调整一下。也可以用噪声纹理随机地分散一下,不过用纹理控制似乎更通用一些。
续集:如何长出鳍
之前为了增加密度,我们让一个三角形面长出了三个鳍,但如果用细分曲面来增加鳍可能会更好看一些
Shader "Fur/Fin/Unlit" { Properties { ... [Toggle(APPEND_MORE_FINS)]_AppendMoreFins("Append More Fins", Float) = 1 ... } } void geom(...) { ... AppendFinVertices(stream, input[0], input[1], input[2], normalOS); #ifdef APPEND_MORE_FINS AppendFinVertices(stream, input[2], input[0], input[1], normalOS); AppendFinVertices(stream, input[1], input[2], input[0], normalOS); #endif }
结果如下
让我们像以前一样用风和重力移扭动毛发。当前创建的鳍片的多边形是方形的,因此它们不会弯曲。在中间添加一个多边形并尝试弯曲它。几何图形是按如下方式添加的锯齿形图像。
风的计算与上次几乎相同,但改进为在世界空间中给出,以便于调整。
void AppendFinVertex( inout TriangleStream<Varyings> stream, float2 uv, float3 posOS, float2 finUv) { Varyings output; output.vertex = TransformObjectToHClip(posOS); output.uv = uv; output.fogCoord = ComputeFogFactor(output.vertex.z); output.finUv = finUv; stream.Append(output); } void AppendFinVertices( inout TriangleStream<Varyings> stream, Attributes input0, Attributes input1, Attributes input2, float3 normalOS) { float3 posOS0 = input0.positionOS.xyz; float3 lineOS01 = input1.positionOS.xyz - posOS0; float3 lineOS02 = input2.positionOS.xyz - posOS0; float3 posOS3 = posOS0 + (lineOS01 + lineOS02) / 2; float2 uv0 = TRANSFORM_TEX(input0.uv, _BaseMap); float2 uv12 = (TRANSFORM_TEX(input1.uv, _BaseMap) + TRANSFORM_TEX(input2.uv, _BaseMap)) / 2; float uvOffset = length(uv0); float uvXScale = length(uv0 - uv12) * _Density; AppendFinVertex(stream, uv0, posOS0, float2(uvOffset, 0.0)); AppendFinVertex(stream, uv12, posOS3, float2(uvOffset + uvXScale, 0.0)); float3 normalWS = TransformObjectToWorldNormal(normalOS); float3 posWS = TransformObjectToWorld(posOS0); float finStep = _FinLength / _FinJointNum; float3 windAngle = _Time.w * _WindFreq.xyz; float3 windMoveWS = _WindMove.xyz * sin(windAngle + posWS * _WindMove.w); float3 baseMoveWS = _BaseMove.xyz; [loop] for (int i = 1; i <= _FinJointNum; ++i) { float finFactor = (float)i / _FinJointNum; float moveFactor = pow(abs(finFactor), _BaseMove.w); float3 moveWS = SafeNormalize(normalWS + (baseMoveWS + windMoveWS) * moveFactor) * finStep; float3 moveOS = TransformWorldToObjectDir(moveWS, false); posOS0 += moveOS; posOS3 += moveOS; AppendFinVertex(stream, uv0, posOS0, float2(uvOffset, finFactor)); AppendFinVertex(stream, uv12, posOS3, float2(uvOffset + uvXScale, finFactor)); } stream.RestartStrip(); } // 增加最大顶点数 [maxvertexcount(75)] void geom(...) { ...
结果如下
当头发动起来的时候,头发的感觉就大大增加了,另外,与Shell法不同的是,即使头发被拉伸也不会塌陷,这一点很有趣。
添加一些光照参数。还要添加 URP 和光照关键字。并且添加ShadowCaster
和 Depth
Pass。
Shader "Fur/Fin/Lit" { Properties { ... _AmbientColor("AmbientColor", Color) = (0.0, 0.0, 0.0, 1) [Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.5 _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5 _ShadowExtraBias("Shadow Extra Bias", Range(-1.0, 1.0)) = 0.0 ... } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "UniversalMaterialType" = "Lit" "IgnoreProjector" = "True" } ... Pass { Name "ForwardLit" Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM // URP #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile _ _SHADOWS_SOFT #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE // Unity #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_fog #pragma exclude_renderers gles gles3 glcore #pragma multi_compile _ DRAW_ORIG_POLYGON #pragma vertex vert #pragma require tessellation tessHW #pragma hull hull #pragma domain domain #pragma require geometry #pragma geometry geom #pragma fragment frag #include "./FurFinLit.hlsl" ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } ... #include "./FurFinShadow.hlsl" #include "./FurFinUnlitTessellation.hlsl" ENDHLSL } Pass { Name "ShadowCaster" Tags {"LightMode" = "ShadowCaster" } ... #define SHADOW_CASTER_PASS #include "./FurFinShadow.hlsl" #include "./FurFinUnlitTessellation.hlsl" ENDHLSL } } }
HLSL
虽然很长,但随着参数数量的增加,我们会按照URP方法编写曲面细分阶段和光照处理中必要的参数传递。此外,由于要传递的信息量(大小)Varyings
增加,因此从一个多边形生长的鳍的数量固定为一个,以减少要生成的几何形状。
此时,需要注意的是,在这个阶段,法线不是针对每根头发的,而是使用原始多边形的法线,因为它们用于光照。由于从多边形的两侧都可以看到,因此如果要很好地计算鳍的法线,则需要绘制正面和背面。我们在下面尝试这个。
#ifndef FUR_FIN_LIT_HLSL #define FUR_FIN_LIT_HLSL #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" ... struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 lightmapUV : TEXCOORD1; }; struct Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float3 normalWS : TEXCOORD1; float2 uv : TEXCOORD2; DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 3); float4 fogFactorAndVertexLight : TEXCOORD4; // x: fogFactor, yzw: vertex light float2 finUv : TEXCOORD5; }; Attributes vert(Attributes input) { return input; } ... Attributes hull(...) { return input[id]; } HsConstantOutput hullConst(...) { ... HsConstantOutput o = (HsConstantOutput)0; } ... Attributes domain(...) { ... o.tangentOS = float4(normalize( i[0].tangentOS.xyz * fWW + i[1].tangentOS.xyz * fUU + i[2].tangentOS.xyz * fVV + hsConst.f3N110 * fW * fU + hsConst.f3N011 * fU * fV + hsConst.f3N101 * fW * fV), i[0].tangentOS.w); o.texcoord = i[0].texcoord * fW + i[1].texcoord * fU + i[2].texcoord * fV; o.lightmapUV = i[0].lightmapUV * fW + i[1].lightmapUV * fU + i[2].lightmapUV * fV; return o; } void AppendFinVertex( inout TriangleStream<Varyings> stream, float2 uv, float3 posOS, float3 normalOS, float4 tangentOS, float2 finUv) { Varyings output = (Varyings)0; VertexPositionInputs vertexInput = GetVertexPositionInputs(posOS); VertexNormalInputs normalInput = GetVertexNormalInputs(normalOS, tangentOS); output.positionCS = vertexInput.positionCS; output.positionWS = vertexInput.positionWS; output.normalWS = normalInput.normalWS; output.uv = uv; output.finUv = finUv; float3 vertexLight = VertexLighting(output.positionWS, normalInput.normalWS); float fogFactor = ComputeFogFactor(output.positionCS.z); output.fogFactorAndVertexLight = half4(fogFactor, vertexLight); OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV); OUTPUT_SH(output.normalWS, output.vertexSH); stream.Append(output); } void AppendFinVertices( inout TriangleStream<Varyings> stream, Attributes input0, Attributes input1, Attributes input2) { float3 normalOS0 = input0.normalOS; float4 tangentOS0 = input0.tangentOS; float3 posOS0 = input0.positionOS.xyz; float3 lineOS01 = input1.positionOS.xyz - posOS0; float3 lineOS02 = input2.positionOS.xyz - posOS0; float3 posOS3 = posOS0 + (lineOS01 + lineOS02) / 2; float2 uv0 = TRANSFORM_TEX(input0.texcoord, _BaseMap); float2 uv12 = (TRANSFORM_TEX(input1.texcoord, _BaseMap) + TRANSFORM_TEX(input2.texcoord, _BaseMap)) / 2; float uvOffset = length(uv0); float uvXScale = length(uv0 - uv12) * _Density; AppendFinVertex(stream, uv0, posOS0, normalOS0, tangentOS0, float2(uvOffset, 0.0)); AppendFinVertex(stream, uv12, posOS3, normalOS0, tangentOS0, float2(uvOffset + uvXScale, 0.0)); float3 dir = normalOS0; dir += rand3(input0.texcoord) * _RandomDirection; dir = normalize(dir); float3 dirWS = TransformObjectToWorldNormal(dir); float3 posWS = TransformObjectToWorld(posOS0); float finStep = _FinLength / _FinJointNum; float3 windAngle = _Time.w * _WindFreq.xyz; float3 windMoveWS = _WindMove.xyz * sin(windAngle + posWS * _WindMove.w); float3 baseMoveWS = _BaseMove.xyz; [loop] for (int i = 1; i <= _FinJointNum; ++i) { float finFactor = (float)i / _FinJointNum; float moveFactor = pow(abs(finFactor), _BaseMove.w); float3 moveWS = SafeNormalize(dirWS + (baseMoveWS + windMoveWS) * moveFactor) * finStep; float3 moveOS = TransformWorldToObjectDir(moveWS, false); posOS0 += moveOS; posOS3 += moveOS; AppendFinVertex(stream, uv0, posOS0, normalOS0, tangentOS0, float2(uvOffset, finFactor)); AppendFinVertex(stream, uv12, posOS3, normalOS0, tangentOS0, float2(uvOffset + uvXScale, finFactor)); } stream.RestartStrip(); } [maxvertexcount(23)] void geom(triangle Attributes input[3], inout TriangleStream<Varyings> stream) { #ifdef DRAW_ORIG_POLYGON for (int i = 0; i < 3; ++i) { Varyings output = (Varyings)0; VertexPositionInputs vertexInput = GetVertexPositionInputs(input[i].positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(input[i].normalOS, input[i].tangentOS); output.positionCS = vertexInput.positionCS; output.positionWS = vertexInput.positionWS; output.normalWS = normalInput.normalWS; output.uv = TRANSFORM_TEX(input[i].texcoord, _BaseMap); output.finUv = float2(-1.0, -1.0); float3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS); float fogFactor = ComputeFogFactor(vertexInput.positionCS.z); output.fogFactorAndVertexLight = half4(fogFactor, vertexLight); OUTPUT_LIGHTMAP_UV(input[i].lightmapUV, unity_LightmapST, output.lightmapUV); OUTPUT_SH(output.normalWS, output.vertexSH); stream.Append(output); } stream.RestartStrip(); #endif float3 lineOS01 = (input[1].positionOS - input[0].positionOS).xyz; float3 lineOS02 = (input[2].positionOS - input[0].positionOS).xyz; float3 normalOS = normalize(cross(lineOS01, lineOS02)); float3 centerOS = (input[0].positionOS + input[1].positionOS + input[2].positionOS).xyz / 3; float3 viewDirOS = GetViewDirectionOS(centerOS); float eyeDotN = dot(viewDirOS, normalOS); if (abs(eyeDotN) > _FaceViewProdThresh) return; AppendFinVertices(stream, input[0], input[1], input[2]); } float4 frag(Varyings input) : SV_Target { float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.finUv); if (input.finUv.x >= 0.0 && furColor.a < _AlphaCutout) discard; SurfaceData surfaceData = (SurfaceData)0; InitializeStandardLitSurfaceData(input.uv, surfaceData); surfaceData.occlusion = lerp(1.0 - _Occlusion, 1.0, max(input.finUv.y, 0.0)); surfaceData.albedo *= surfaceData.occlusion; InputData inputData = (InputData)0; inputData.positionWS = input.positionWS; inputData.normalWS = input.normalWS; inputData.viewDirectionWS = SafeNormalize(GetCameraPositionWS() - input.positionWS); #if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF) inputData.shadowCoord = TransformWorldToShadowCoord(input.positionWS); #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif inputData.fogCoord = input.fogFactorAndVertexLight.x; inputData.vertexLighting = input.fogFactorAndVertexLight.yzw; inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS); float4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb += _AmbientColor.rgb; color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; } #endif
ShadowCaster / Depth
编写与 Unlit 几乎相同的代码。顶点和几何着色器代码可以共享,但就像上次一样,只会修改阴影偏差计算,所以这次我们将简单地复制并粘贴代码。
... inline float3 CustomApplyShadowBias(float3 positionWS, float3 normalWS) { positionWS += _LightDirection * (_ShadowBias.x + _ShadowExtraBias); float invNdotL = 1.0 - saturate(dot(_LightDirection, normalWS)); float scale = invNdotL * _ShadowBias.y; positionWS += normalWS * scale.xxx; return positionWS; } inline float4 GetShadowPositionHClip(float3 positionWS, float3 normalWS) { positionWS = CustomApplyShadowBias(positionWS, normalWS); float4 positionCS = TransformWorldToHClip(positionWS); #if UNITY_REVERSED_Z positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #else positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #endif return positionCS; } void AppendFinVertex( ... float3 normalWS, ...) { ... #ifdef SHADOW_CASTER_PASS float3 posWS = TransformObjectToWorld(posOS); output.vertex = GetShadowPositionHClip(posWS, normalWS); #else output.vertex = TransformObjectToHClip(posOS); #endif ... } ... void frag( Varyings input, out float4 outColor : SV_Target, out float outDepth : SV_Depth) { float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.finUv); float alpha = furColor.a; if (alpha < _AlphaCutout) discard; outColor = outDepth = input.vertex.z / input.vertex.w; }
通过关键字SHADOW_CASTER_PASS
的定义,确定是否应用阴影。
结果如下
_ShadowExtraBias
参数用于调整阴影的偏移量,通过增加偏移量可以避免自阴影的产生。在这里,通过调整 _ShadowExtraBias
的值来控制自阴影的落差,使其不会过于明显。
另外,_Occlusion
参数用于控制颜色的深浅程度,根部的颜色会随着 _Occlusion
的增加而变得更加浓重。通过调整 _Occlusion
的值,可以使毛发的根部颜色更加饱和,增强了整体的毛发效果。
通过这样的调整,可以获得更加逼真和自然的毛发效果,使毛发看起来更加蓬松丰富。
之前,我们使用了原始多边形(球体)的法线,但是如果我们保持原样,它看起来仍然有点球形,所以让我们调整它。我想介绍两种方法:
前/后正常混合
为了表达物体的正反面,我们需要将之前使用的Cull Off
更改为Cull Back
,这样可以单独处理每一面。为了实现这一点,我们将在几何着色器中生成两面,并翻转法线。另外,由于目前未使用切线信息,我们可以从Attributes
和Varyings
中删除相关代码。
float _FaceNormalFactor; void AppendFinVertices( inout TriangleStream<Varyings> stream, Attributes input0, Attributes input1, Attributes input2) { ... [unroll] for (int j = 0; j < 2; ++j) { float3 posBeginOS = posOS0; float3 posEndOS = posOS3; float uvX1 = uvOffset; float uvX2 = uvOffset + uvXScale; [loop] for (int i = 0; i <= _FinJointNum; ++i) { float finFactor = (float) i / _FinJointNum; float moveFactor = pow(abs(finFactor), _BaseMove.w); float3 moveWS = SafeNormalize(dirWS + (baseMoveWS + windMoveWS) * moveFactor) * finStep; float3 moveOS = TransformWorldToObjectDir(moveWS, false); posBeginOS += moveOS; posEndOS += moveOS; float3 dirOS03 = normalize(posEndOS - posBeginOS); float3 faceNormalOS = normalize(cross(dirOS03, moveOS)); if (j == 0) { float3 finNormalOS = normalize(lerp(normalOS0, faceNormalOS, _FaceNormalFactor)); AppendFinVertex(stream, uv0, posBeginOS, finNormalOS, float2(uvX1, finFactor)); AppendFinVertex(stream, uv12, posEndOS, finNormalOS, float2(uvX2, finFactor)); } else { faceNormalOS *= -1.0; float3 finNormalOS = normalize(lerp(normalOS0, faceNormalOS, _FaceNormalFactor)); AppendFinVertex(stream, uv12, posEndOS, finNormalOS, float2(uvX2, finFactor)); AppendFinVertex(stream, uv0, posBeginOS, finNormalOS, float2(uvX1, finFactor)); } } stream.RestartStrip(); } }
我们将在stream
追加生成的顺序中进行反向操作,以创建反面。此外,我们使用_FaceNormalFactor
对球体的法线和生成的面的法线进行混合,从而修改法线。注意,如果手动在代码中进行unroll操作,可以避免if语句的分支,因此在优化时最好将其删除。
稍微调整一下 _Smoothness
,以使镜面反射更加明显。通过改变 _FaceNormalFactor
,可以在阴影区域中产生明亮的区域,并且会扩散高光的位置。此外,由于表面正反两面都存在,高光部分只会在正面反射并变亮(尽管可能缺乏一些透明感)。
法线贴图
由于我们自己设置了鳍的 UV,因此还需要计算并设置了切线。 尝试选择鳍片的方向,使它们垂直于刚刚混合的法线。
Properties { ... [NoScaleOffset] [Normal] _NormalMap("Normal", 2D) = "bump" {} _NormalScale("Normal Scale", Range(0.0, 2.0)) = 0.0 ... } struct Varyings { ... float3 finTangentWS : TEXCOORD6; }; void AppendFinVertex( ... float3 finSideDirWS) { ... output.normalWS = TransformObjectToWorldNormal(normalOS); ... output.finTangentWS = SafeNormalize(cross(output.normalWS, finSideDirWS)); } void AppendFinVertices(...) { ... float3 finSideDirOS = normalize(posOS3 - posOS0); float3 finSideDirWS = TransformObjectToWorldDir(finSideDirOS); [unroll] for (int j = 0; j < 2; ++j) { ... [loop] for (int i = 0; i <= _FinJointNum; ++i) { ... AppendFinVertex(stream, uv0, posBeginOS, finNormalOS, float2(uvX1, finFactor), finSideDirWS); AppendFinVertex(stream, uv12, posEndOS, finNormalOS, float2(uvX2, finFactor), finSideDirWS); ... } ... } } ... float4 frag(Varyings input) : SV_Target { ... float3 viewDirWS = SafeNormalize(GetCameraPositionWS() - input.positionWS);; float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.finUv), _NormalScale); float3 bitangent = SafeNormalize(viewDirWS.y * cross(input.normalWS, input.finTangentWS)); float3 normalWS = SafeNormalize(TransformTangentToWorld(normalTS, float3x3(input.finTangentWS, bitangent, input.normalWS))); ... }
输入法线如下图所示。
结果如下
个别毛发有些突出,但幅度不大。
Rimlight 可以使用与上次相同的代码。
毛发渲染中的Fin方法是一种常见的技术,用于模拟毛发的外观和光照效果。这种方法通过在模型表面附近生成垂直于表面的一系列小片(称为Fin),然后将毛发纹理映射到这些Fin上来实现。以下是一般的Fin方法的步骤:
1、模型准备:首先,需要为模型创建合适的UV坐标和法线信息。这些信息将用于在Fin上正确映射纹理和计算光照。
2、Fin生成:使用几何着色器或其他技术,在模型的表面附近生成一系列Fin。Fin通常是一组三角形或四边形,并且垂直于模型表面。
3、UV映射:将毛发纹理映射到每个Fin上。根据Fin的UV坐标,可以从纹理中采样出正确的颜色和透明度值。
4、光照计算:对每个Fin进行光照计算,以使其与周围环境和光源保持一致。这通常涉及法线和切线的计算,以及根据光照方程和材质属性来计算最终的颜色值。
5、阴影计算:根据场景中的阴影信息,对Fin进行阴影计算,以确保其与周围对象的阴影正确交互。
6、物理效果:可以根据需要应用其他物理效果,如发丝之间的碰撞和互相遮挡。
通过这些步骤,Fin方法能够以逼真的方式模拟毛发的外观和光照效果。它在游戏和电影产业中广泛应用于创建逼真的动物角色和毛发效果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。