实现毛发着色器的方法有多种,其中最常用的是shell方法和fin方法。
毛发 Shell 技术是一种用于模拟毛发效果的方法,常用于实时渲染和计算机图形学中。它通过在基础模型表面附加一系列薄片或壳来模拟毛发的外观和动态效果。每个薄片都代表着一束毛发,可以根据需要进行调整和渲染。
1、简单的开始
首先,我们将以简单的Unlit方式进行尝试。顶点、几何和片段着色器的实现将存储在单独的文件(Fur.hlsl),而ShaderLab的设置将在Fur.shader文件中进行描述。现在,让我们先看一下Fur.shader文件。
ShaderLab
Shader "Unlit/Fur"
{
Properties
{
_BaseMap("Base Map", 2D) = "white" {}
_FurMap("Fur Map", 2D) = "white" {}
[IntRange] _ShellAmount("Shell Amount", Range(1, 100)) = 16
_ShellStep("Shell Step", Range(0.0, 0.01)) = 0.001
_AlphaCutout("Alpha Cutout", Range(0.0, 1.0)) = 0.1
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"IgnoreProjector" = "True"
}
LOD 100
ZWrite On
Cull Back
Pass
{
Name "Unlit"
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma multi_compile_fog
#include "./Fur.hlsl"
#pragma vertex vert
#pragma require geometry
#pragma geometry geom
#pragma fragment frag
ENDHLSL
}
Pass
{
Name "DepthOnly"
Tags { "LightMode" = "DepthOnly" }
ZWrite On
ColorMask 0
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma vertex DepthOnlyVertex
#pragma fragment DepthOnlyFragment
#include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
ENDHLSL
}
Pass
{
Name "ShadowCaster"
Tags {"LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
ColorMask 0
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
ENDHLSL
}
}
我们将创建一个带有阴影和深度的Unlit着色器,包括DepthOnly
和ShadowCaster
的Pass。这里的阴影暂时不会反映在毛发上,而是输出原始网格的阴影。在Unlit Pass
中,我们使用#include
指令引入Fur.hlsl
文件,并在那里编写实现代码。接下来,让我们来看看Fur.hlsl
文件的内容。
顶点/几何/片段着色器
#ifndef FUR_HLSL
#define FUR_HLSL
#include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
int _ShellAmount;
float _ShellStep;
float _AlphaCutout;
TEXTURE2D(_FurMap);
SAMPLER(sampler_FurMap);
float4 _FurMap_ST;
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float2 uv2 : TEXCOORD1;
float fogCoord : TEXCOORD2;
float layer : TEXCOORD3;
};
Attributes vert(Attributes input)
{
return input;
}
void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index)
{
Varyings output = (Varyings)0;
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
float3 posWS = vertexInput.positionWS + normalInput.normalWS * (_ShellStep * index);
float4 posCS = TransformWorldToHClip(posWS);
output.vertex = posCS;
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
output.uv2 = TRANSFORM_TEX(input.uv, _FurMap);
output.fogCoord = ComputeFogFactor(posCS.z);
output.layer = (float)index / _ShellAmount;
stream.Append(output);
}
[maxvertexcount(96)]
void geom(triangle Attributes input[3], inout TriangleStream<Varyings> stream)
{
[loop] for (float i = 0; i < _ShellAmount; ++i)
{
[unroll] for (float j = 0; j < 3; ++j)
{
AppendShellVertex(stream, input[j], i);
}
stream.RestartStrip();
}
}
float4 frag(Varyings input) : SV_Target
{
float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv2);
if (input.layer > 0.0 && furColor.r < _AlphaCutout) discard;
float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
float3 color = baseColor;
color = MixFog(color, input.fogCoord);
return float4(color, 1.0);
}
#endif
在几何着色器中,我们根据输入的三角形多边形,在法线方向上稍微膨胀并增加层数,数量由_ShellAmount
参数控制。同时,我们还输出层级信息。
在片段着色器中,我们使用输入的噪音纹理进行剪裁(Clipping),通过_AlphaCutout
参数将黑色部分丢弃,只保留白色部分。不过,对于层级0,我们直接将其作为普通的多边形输出,省略了此处理步骤。然后,按照常规的方式输出颜色,使噪音部分看起来像毛发一样。
这是一个大致的概念,虚线代表噪音纹理。通过缩小步长和增加层数,可以增加毛发效果的外观。
重叠层的数量取决于几何着色器中指定的maxvertexcount
,该数量是可变的,取决于输出结构体的大小,并且需要确保输出的值的总数不超过1024。在这里,由于Varyings
输出了10个浮点数,因此最大数量为1024/10 = 102,每个层使用3个顶点,因此最大层数为34层。
如果将毛发密度等信息打包到一张纹理中(例如alpha通道),则可以减少2个层,从而最大可达128个顶点和42层。另外,如果将UV纹理坐标合并为一个,例如使用_BaseMap
和_FurMap
,再加上_FurScale
之类的参数,可以再次减少2个层,从而最大可达56层。
这样做可以实现最多56层的效果。
结果如下
虽然质量还是那样,但是可以看出层的重叠
先用以下噪音图进行测试
2、效果调整
我们稍微修改一下片段着色器,使其看起来像这样。
float _Occlusion;
float4 frag(Varyings input) : SV_Target
{
float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv2);
// 越往前,噪音纹理越暗,越细。
float alpha = furColor.r * (1.0 - input.layer);
if (input.layer > 0.0 && alpha < _AlphaCutout) discard;
float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
// 使根部变暗!
float occlusion = lerp(1.0 - _Occlusion, 1.0, input.layer);
float3 color = baseColor * occlusion;
color = MixFog(color, input.fogCoord);
return float4(color, alpha);
}
结果如下
通过增加噪音纹理的细节,你可以获得更细微的毛发效果。然而,你可能会发现在边缘处,层级变得更加明显。这是Shell法的一个缺点。为了解决这个问题,你可以尝试使用Fin法或者采用Fin法与Shell法相结合的方法。
如果使噪音纹理更精细,则还可以制作更细的毛发。
另外,通过在alpha
通道上应用适当的纹理,也可以实现毛发的长度控制。例如,可以使用_BaseMap
纹理并根据颜色的亮暗程度来控制毛发的长度。较暗的区域可以使毛发变短。
float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, Sampler_BaseMap, input.uv.xy);
float alpha = FurColor.r * ( 1.0 - input.layer) * baseColor.r;
...
3、扭动毛发
通过在几何着色器中逐渐偏移生成的每个层级的多边形,可以实现毛发在风中飘动或受重力影响下垂下的效果表现。
从外部传入偏移量,越到发梢偏移程度越大。
// xyz: 运动方向 w: 弯曲程度
float4 _BaseMove;
// xyz: 晃动的周期
float4 _WindFreq;
// xyz:震动幅度 w:局部坐标的相移
float4 _WindMove;
void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index)
{
Varyings output = (Varyings)0;
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
// 层数越大,弯度越大
float moveFactor = pow((float)index / _ShellAmount, _BaseMove.w);
// 有规律的摇晃
float3 posOS = input.positionOS;
float3 windAngle = _Time.w * _WindFreq.xyz;
float3 windMove = moveFactor * _WindMove.xyz * sin(windAngle + posOS * _WindMove.w);
// 不断移动
float3 move = moveFactor * _BaseMove.xyz;
// 改变壳的方向(对其进行单位化,使其不会被拉伸)
float3 shellDir = normalize(normalInput.normalWS + move + windMove);
float3 posWS = vertexInput.positionWS + shellDir * (_ShellStep * index);
float4 posCS = TransformWorldToHClip(posWS);
output.vertex = posCS;
output.uv = float4(TRANSFORM_TEX(input.uv, _BaseMap), TRANSFORM_TEX(input.uv, _FurMap));
output.fogCoord = ComputeFogFactor(posCS.z);
output.layer = (float)index / _ShellAmount;
stream.Append(output);
}
结果如下
4、支持阴影
虽然原始形状保持不变,但有时候我们可能愿意牺牲一些性能以获得更漂亮的阴影效果。通过大致重用先前的代码,我们可以实现影的渲染。
ShaderLab
Shader "Unlit/Fur"
{
...
SubShader
{
...
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
ColorMask 0
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#include "./Fur.hlsl"
#pragma vertex vert
#pragma require geometry
#pragma geometry geom
#pragma fragment fragShadow
ENDHLSL
}
}
}
片段着色器
void fragShadow(Varyings input, out float4 outColor : SV_Target, out float outDepth : SV_Depth)
{
float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv.zw);
float alpha = furColor.r * (1.0 - input.layer);
if (input.layer > 0.0 && alpha < _AlphaCutout) discard;
outColor = outDepth = input.vertex.z / input.vertex.w;
}
同样的代码也可以在DepthOnly
Pass中使用。这对于启用深度预处理的情况或需要使用深度进行后期处理的情况非常有用。
结果如下
5、支持光照
到目前为止,我们一直在使用Unlit着色器进行演示,现在让我们转向使用基于Lit着色器的着色方式。基本原理与之前相同,我们需要在片段着色器中传递与光照相关的参数,并在调用UniversalFragmentPBR()
函数后进行渲染。
ShaderLab
Shader "Lit/Fur"
{
Properties
{
[MainColor] _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1)
_BaseMap("Base Map", 2D) = "white" {}
[Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.5
_Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5
_FurMap("Fur Map", 2D) = "white" {}
[IntRange] _ShellAmount("Shell Amount", Range(1, 14)) = 14
_ShellStep("Shell Step", Range(0.0, 0.01)) = 0.001
_AlphaCutout("Alpha Cutout", Range(0.0, 1.0)) = 0.2
_Occlusion("Occlusion", Range(0.0, 1.0)) = 0.5
_BaseMove("Base Move", Vector) = (0.0, -0.0, 0.0, 3.0)
_WindFreq("Wind Freq", Vector) = (0.5, 0.7, 0.9, 1.0)
_WindMove("Wind Move", Vector) = (0.2, 0.3, 0.2, 1.0)
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"UniversalMaterialType" = "Lit"
"IgnoreProjector" = "True"
}
LOD 100
ZWrite On
Cull Back
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 2.0
// 材质关键字
#pragma shader_feature _NORMALMAP
#pragma shader_feature _ALPHATEST_ON
#pragma shader_feature _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICSPECGLOSSMAP
#pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
#pragma shader_feature _OCCLUSIONMAP
#pragma shader_feature _SPECULARHIGHLIGHTS_OFF
#pragma shader_feature _ENVIRONMENTREFLECTIONS_OFF
#pragma shader_feature _SPECULAR_SETUP
#pragma shader_feature _RECEIVE_SHADOWS_OFF
// 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
#include "./FurLit.hlsl"
#pragma vertex vert
#pragma require geometry
#pragma geometry geom
#pragma fragment frag
ENDHLSL
}
Pass
{
Name "DepthOnly"
...
}
Pass
{
Name "ShadowCaster"
...
}
}
}
直接把Lit着色器的设置拿来。Properties中添加_Metallic
和_Smoothness
。着色器本身是写在FurLit.hlsl
里的,下面我们来看一下。
顶点/几何/片段着色器
#ifndef FUR_LIT_HLSL
#define FUR_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"
int _ShellAmount;
float _ShellStep;
float _AlphaCutout;
float _Occlusion;
float4 _BaseMove;
float4 _WindFreq;
float4 _WindMove;
TEXTURE2D(_FurMap);
SAMPLER(sampler_FurMap);
float4 _FurMap_ST;
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 texcoord : TEXCOORD0;
float2 lightmapUV : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float4 uv : TEXCOORD2;
DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 3);
float4 fogFactorAndVertexLight : TEXCOORD4; // x: fogFactor, yzw: vertex light
float layer : TEXCOORD5;
};
Attributes vert(Attributes input)
{
return input;
}
void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index)
{
Varyings output = (Varyings)0;
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
float moveFactor = pow(abs((float)index / _ShellAmount), _BaseMove.w);
float3 posOS = input.positionOS.xyz;
float3 windAngle = _Time.w * _WindFreq.xyz;
float3 windMove = moveFactor * _WindMove.xyz * sin(windAngle + posOS * _WindMove.w);
float3 move = moveFactor * _BaseMove.xyz;
float3 shellDir = SafeNormalize(normalInput.normalWS + move + windMove);
output.positionWS = vertexInput.positionWS + shellDir * (_ShellStep * index);
output.positionCS = TransformWorldToHClip(output.positionWS);
output.uv = float4(TRANSFORM_TEX(input.texcoord, _BaseMap), TRANSFORM_TEX(input.texcoord, _FurMap));
output.normalWS = TransformObjectToWorldNormal(input.normalOS);
output.layer = (float)index / _ShellAmount;
float3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS);
float fogFactor = ComputeFogFactor(vertexInput.positionCS.z);
output.fogFactorAndVertexLight = half4(fogFactor, vertexLight);
OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV);
OUTPUT_SH(output.normalWS.xyz, output.vertexSH);
stream.Append(output);
}
[maxvertexcount(42)]
void geom(triangle Attributes input[3], inout TriangleStream<Varyings> stream)
{
[loop] for (float i = 0; i < _ShellAmount; ++i)
{
[unroll] for (float j = 0; j < 3; ++j)
{
AppendShellVertex(stream, input[j], i);
}
stream.RestartStrip();
}
}
float3 TransformHClipToWorld(float4 positionCS)
{
return mul(UNITY_MATRIX_I_VP, positionCS).xyz;
}
float4 frag(Varyings input) : SV_Target
{
float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv.zw);
float alpha = furColor.r * (1.0 - input.layer);
if (input.layer > 0.0 && alpha < _AlphaCutout) discard;
SurfaceData surfaceData = (SurfaceData)0;
InitializeStandardLitSurfaceData(input.uv.xy, surfaceData);
surfaceData.occlusion = lerp(1.0 - _Occlusion, 1.0, input.layer);
InputData inputData = (InputData)0;
inputData.positionWS = input.positionWS;
inputData.normalWS = input.normalWS;
inputData.viewDirectionWS = SafeNormalize(GetCameraPositionWS() - inputData.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 = MixFog(color.rgb, inputData.fogCoord);
return color;
}
#endif
查看几何着色器后,可以看到Varyings
中增加了用于光照的法线和顶点光照等参数。由于这个原因,正如之前所述,maxvertexcount
减少了,因此在1个Pass中最多只能进行14个层次的渲染。
如果希望进行更多的Shell
层次渲染,需要考虑增加2个Pass、3个Pass等来实现。
在片段着色器中,首先调用InitializeStandardLitSurfaceData()
初始化,将通过属性设置的参数存储在SurfaceData
中,然后将信息填充到InputData
的各个成员中,并通过UniversalFragmentPBR()
进行光照和阴影计算。在SurfaceData
的occlusion
中,对于Unlit情况下,可以直接将颜色乘以遮挡值(occlusion
),也可以乘以albedo
(漫反射颜色)。
结果如下
在观察结果时,我们可能会发现根部的颜色过于暗淡。这是由于阴影过于强烈造成的。要修正这个问题,我们需要对ShadowCaster
Pass进行调整。
偏移调整
之前在Unlit着色器中看到的ShadowCaster代码没有考虑偏移值(bias),这可能导致在不渲染毛发时出现阴影瑕疵(shadow acne)的问题。
void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index)
{
...
// float4 posCS = TransformWorldToHClip(posWS);
// 修改为应用偏移
float4 posCS = TransformWorldToHClip(ApplyShadowBias(posWS, normalInput.normalWS, _LightDirection));
#if UNITY_REVERSED_Z
posCS.z = min(posCS.z, posCS.w * UNITY_NEAR_CLIP_VALUE);
#else
posCS.z = max(posCS.z, posCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
...
}
结果如下
不再出现过多的阴影。
进一步的偏置调整
如果你不希望这里出现阴影,可以进一步修改这个偏差计算。具体来说,_ShadowBias
它允许你将每个对象的附加值添加到全局设置的内容中。
float _ShadowExtraBias;
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 AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index)
{
...
float4 posCS = GetShadowPositionHClip(posWS, normalInput.normalWS);
...
}
_ShadowExtraBias
如果为负值,效果如下:
根据这个外观,将遮挡值(occlusion
)乘以漫反射颜色(albedo
)似乎会让效果更好。
6、添加法线
即使如此,仍然缺乏逼真感。仔细观察可以发现,每根毛发上并没有进行逐个的光照计算。这是因为用于毛发光照的法线仍然使用原始多边形(在这种情况下是球体)的法线。因此,我们需要考虑法线来进行适当的光照计算。
法线贴图
作为法线信息,_FurMap
我们将从外部给出相应的法线纹理。
更改着色器
首先,在 ShaderLab 的 Properties 块中为法线纹理添加一个属性:
Properties
{
...
_NormalMap("Normal", 2D) = "bump" {}
_NormalScale("Normal Scale", Range(0.0, 2.0)) = 1.0
...
_FurScale("Fur Scale", Range(0.0, 10.0)) = 1.0
...
}
为了在片段着色器中读取法线贴图,我们需要在Varyings
中添加切线(tangent
)。因此,几何着色器的输出数量将进一步减少。另外,为了保持远端使用的噪音纹理和法线贴图具有相同的UV缩放比例,我们需要将之前使用的float4
类型的UV更改为float2
类型,并添加_FurScale
参数。最终,Varyings
的定义如下:
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float3 tangentWS : TEXCOORD2;
float2 uv : TEXCOORD4;
DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 5); // float3
float4 fogFactorAndVertexLight : TEXCOORD6; // x: fogFactor, yzw: vertex light
float layer : TEXCOORD7;
};
这样一来,结构体的大小为23,因此maxvertexcount
可以设置为1024 / 23约等于44,最大层数为44 / 3约等于14层。这个数目足够满足超过10层的要求,看起来是足够的。
接下来,我们需要在几何着色器中传递tangentWS
。因此,我们需要对几何着色器进行修改,以传递tangentWS
。
void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index)
{
...
output.tangentWS = normalInput.tangentWS;
...
}
在片段着色器中使用它从法线贴图中提取法线信息。
float _FurScale;
TEXTURE2D(_NormalMap);
SAMPLER(sampler_NormalMap);
float4 _NormalMap_ST;
float _NormalScale;
float4 frag(Varyings input) : SV_Target
{
float2 furUv = input.uv / _BaseMap_ST.xy * _FurScale;
float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, furUv);
float alpha = furColor.r * (1.0 - input.layer);
if (input.layer > 0.0 && alpha < _AlphaCutout) discard;
float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, furUv), _NormalScale);
float sgn = input.tangentWS.w; // viewDirWS.y
float3 bitangent = sgn * cross(input.normalWS.xyz, input.tangentWS.xyz);
float3 normalWS = TransformTangentToWorld( normalTS, float3x3(input.tangentWS.xyz, bitangent.xyz,input.normalWS.xyz));
SurfaceData surfaceData = (SurfaceData)0;
...
inputData.normalWS = normalWS;
inputData.viewDirectionWS = viewDirWS;
...
}
结果如下
通过以上的修改,现在的效果如下所示。非常适应于光照,看起来非常自然。
将地板也添加了毛发效果,这样一来确实与旁边的非光照Unlit毛发效果有了很大的区别。
7、添加边缘光
实际的头发感觉又薄又透明。尤其是当背景是背光的时候,光线似乎会透过来。接下来添加这样一个边缘光效果。
ShaderLab
Properties
{
...
_RimLightPower("Rim Light Power", Range(0.0, 20.0)) = 6.0
_RimLightIntensity("Rim Light Intensity", Range(0.0, 1.0)) = 0.5
...
}
为了控制Rim光照效果应用的范围,我们可以使用视线方向与法线方向或光线方向的点积来决定。为了调整应用范围,可以添加一个名为_RimLightPower
的系数,作为pow()函数的参数。
片元着色器
在进行PBR计算之后,将ApplyRimLight()
放置在下面的位置。在URP中,GetMainLight()
的真正价值在于GetAdditionalPerObjectLight()
,它可以轻松获取各种灯光信息,如颜色、方向、衰减和阴影。因此,您可以创建考虑阴影和距离的边缘光效果。
void ApplyRimLight(inout float3 color, float3 posWS, float3 viewDirWS, float3 normalWS)
{
float viewDotNormal = abs(dot(viewDirWS, normalWS));
float normalFactor = pow(abs(1.0 - viewDotNormal), _RimLightPower);
Light light = GetMainLight();
float lightDirDotView = dot(light.direction, viewDirWS);
float intensity = pow(max(-lightDirDotView, 0.0), _RimLightPower);
intensity *= _RimLightIntensity * normalFactor;
#ifdef _MAIN_LIGHT_SHADOWS
float4 shadowCoord = TransformWorldToShadowCoord(posWS);
intensity *= MainLightRealtimeShadow(shadowCoord);
#endif
color += intensity * light.color;
#ifdef _ADDITIONAL_LIGHTS
int additionalLightsCount = GetAdditionalLightsCount();
for (int i = 0; i < additionalLightsCount; ++i)
{
int index = GetPerObjectLightIndex(i);
Light light = GetAdditionalPerObjectLight(index, posWS);
float lightDirDotView = dot(light.direction, viewDirWS);
float intensity = max(-lightDirDotView, 0.0);
intensity *= _RimLightIntensity * normalFactor;
intensity *= light.distanceAttenuation;
#ifdef _MAIN_LIGHT_SHADOWS
intensity *= AdditionalLightRealtimeShadow(index, posWS);
#endif
color += intensity * light.color;
}
#endif
}
float4 frag(Varyings input) : SV_Target
{
...
float4 color = UniversalFragmentPBR(inputData, surfaceData);
ApplyRimLight(color.rgb, input.positionWS, viewDirWS, input.normalWS);
...
return color;
}
结果如下
不仅限于定向光,包括聚光灯等其他附加光源,都可以很好地应用Rim光照效果。
8、其他效果
改变金属度和光泽度
豹纹
9、总结
毛发 Shell 技术是一种用于模拟毛发效果的方法,常用于实时渲染和计算机图形学中。它通过在基础模型表面附加一系列薄片或壳来模拟毛发的外观和动态效果。每个薄片都代表着一束毛发,可以根据需要进行调整和渲染。
以下是毛发 Shell 技术的一般工作流程:
-
- 基础模型创建:首先创建一个基础模型,该模型通常是人物或动物的头部、身体或其他需要模拟毛发的部分。
-
- 薄片创建:基于基础模型的表面,使用特定的算法和技术创建一系列薄片。这些薄片是一种沿着基础模型曲面分布的几何体,形状类似于薄片或管状结构。
-
- 薄片调整:根据毛发的形状、密度和分布需求,对每个薄片进行调整。这包括调整薄片的长度、曲率、方向和密度等参数。
-
- 着色和渲染:为每个薄片分配适当的材质和纹理,以模拟毛发的外观。常见的技术包括使用纹理贴图进行着色、实时光照计算和透明效果等。
-
- 动态效果:为了实现毛发的动态效果,可以使用物理模拟或动力学模拟技术。这些技术可以模拟毛发的运动、重力和碰撞等效果,增加逼真感。
毛发 Shell 技术的优点是可以在实时渲染中实现比较逼真的毛发效果,并且对于性能要求相对较低。它在游戏、动画和虚拟现实等领域得到广泛应用。然而,毛发 Shell 技术也存在一些挑战,例如复杂模型的创建和调整、物理模拟的计算开销以及处理遮挡和碰撞等问题。因此,在实际应用中需要综合考虑技术要求和性能限制。