????????鰭片法是一種在多邊形表面垂直添加許多多邊形,并在其上粘貼毛發紋理以營造毛茸茸的感覺的技術。這就像種植許多鰭(就像魚身上的鰭一樣)。本期我將在Unity6中實現一下基礎的Fin毛發,并不涉及光照著色。后面我會出一篇加上著色效果的最終版本,推薦先收藏一手喲,直接先上效果展示。
使用的Unity版本:6000.0.43f1
?
一.前置準備
本期將主要使用到曲面細分著色器和幾何著色器。
關于這兩種著色器的詳細介紹可以通過搜索了解或看我的兩篇博客:
[學習記錄]Unity-Shader-曲面細分著色器-CSDN博客
[學習記錄]Unity-Shader-幾何著色器-CSDN博客
使用到的貼圖資源:
1.FurTex(PNG):用于發片的透明遮罩采樣
2.MainTex:用于基礎著色采樣
二.Fin基本原理
? ? ? ? 鰭片法簡而言之就是在基本幾何體的表面上種植一些幾何體(小發片,或者說鰭狀體),對這些小發片應用光照模型,實現逼真的毛發效果。
三.實現思路
1.基礎Fin生長效果
? ? ? ? 在Unity中,基本思路就是拿幾何體網格上的三角形做文章,在每個三角形里找到一條線段并嘗試擴展成一個帶狀物體,由于其中涉及到動態生成和操作幾何體圖元。所以我覺得使用幾何著色器是很合適的。
1.技術要點
主要是圍繞Geom幾何著色器展開的工作。
#pragma geometry Geom
1.創建鰭狀體前
主要進行了兩步操作:
(1)將基礎幾何體網格的頂點追加入圖元流,以便能正常渲染基礎的幾何體網格。
(2)設置一個角度閾值確認鰭狀體的生成范圍,將只渲染視線閾值內的鰭狀體。
[maxvertexcount(39)]void Geom(triangle Attributes input[3],inout TriangleStream<Varyings> stream){//渲染原始幾何體for (int i=0;i<3;++i){Varyings output=(Varyings)0;//得到各個坐標系下頂點位置VertexPositionInputs vertexInput=GetVertexPositionInputs(input[i].positionOS.xyz);output.positionCS=vertexInput.positionCS;output.positionWS=vertexInput.positionWS;output.normalWS=TransformObjectToWorld( input[i].normalOS);output.uv = TRANSFORM_TEX(input[i].uv, _MainTex);output.finUv=float2(-1.0,-1.0);//標記為-1,是為了標記原始幾何體的像素,直接跳過在frag中和渲染的毛發片剔除有關的操作stream.Append(output);}stream.RestartStrip();//渲染毛發片(每個三角形圖元內渲染一個發片)//計算出輸入三角形的面法線和中心float3 line1=(input[1].positionOS-input[0].positionOS).xyz;float3 line2=(input[2].positionOS-input[0].positionOS).xyz;float3 normalOS=normalize(cross(line1,line2));//輸入三角形的面法線float3 centerOS=(input[0].positionOS+input[1].positionOS+input[2].positionOS)/3;//三角形的重心//計算視線與面法線近似度,剔除大于一定角度的發片float3 viewDir=GetViewDirectionOS(centerOS);float eyeDotN=dot(viewDir,normalOS);if(abs(eyeDotN)>_FaceViewThresh) return;//流中追加所有頂點AppendFinVertices(stream,input[0],input[1],input[2]);}
2.創建鰭狀體
?基本思路:在三角形多邊形的中間繪制一條線(可以取三角形的一個頂點作為鰭邊的起點,三角形對邊中點作為鰭邊的終點),之后將這條線段將其沿著面法線方向推出,就形成了Fin的形狀。
用到了兩個函數,AppendFinVertices()和AppendFinVertex()
1.?AppendFinVertices()
功能:負責在幾何著色器中向圖元流追加所有鰭狀體的頂點。
接收參數:
(1)可寫入的圖元流:inout TriangleStream<Varyings> stream
(2)輸入圖元的三個頂點:?
????????Attributes input0,//三角形頂點1
? ? ? ? Attributes input1,//三角形頂點2
? ? ? ? Attributes input2)//三角形頂點3
//向流中追加所有頂點void AppendFinVertices(inout TriangleStream<Varyings> stream,Attributes input0,//三角形頂點1Attributes input1,//三角形頂點2Attributes input2)//三角形頂點3
{}
(1)定義鰭邊起點與終點
//先在對象空間進行所有方向計算
//將第一個頂點作為鰭邊的起點
//對邊中點作為鰭邊的終點
float3 line_start=input0.positionOS;//鰭邊起點
float3 line1=input1.positionOS-input0.positionOS;
float3 line2=input2.positionOS-input0.positionOS;
float3 line_end= input0.positionOS+ (line1+line2)/2;//鰭邊終點
(2)計算鰭邊起點與終點的uv
????????這里鰭邊的終點實際是不存在的,需要重新計算uv,加入屬性_FurDensity便于控制uv的Tilling。
float2 uv_start=TRANSFORM_TEX(input0.uv,_MainTex);
float2 uv_end=(TRANSFORM_TEX(input1.uv,_MainTex)+TRANSFORM_TEX(input2.uv,_MainTex))/2;float uv_offset=length(uv_start);
float uv_scale=length(uv_start-uv_end) * _FurDensity;
隨機化函數rand&rand3
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);
}
(3)定義鰭的生長方向
_FinRandomDirIntensity:鰭狀體法線隨機度
float3 finDirOS=input0.normalOS;finDirOS+= rand3(input0.uv) *_FinRandomDirIntensity;//rand3隨機數函數finDirOS=normalize(finDirOS);//生長方向的單位向量
(4)循環創建鰭狀體
定義屬性
?_FinJointNum:鰭狀體的總長度。
_FinJointNum:鰭狀體的段數(可以理解為縱向細分),可以實現更順滑的頂點擾動效果。
_FaceNormalFactor:鰭狀體面法線對生長方向的貢獻度。
外部是2次循環,用于渲染鰭狀體的正面和反面。
內部是_FinJointNum次循環,生成N個毛發段和N+1對頂點(共2(N+1)個頂點)。
float finStep = _FinLength / _FinJointNum;//每個分段的長度[unroll]
for (int j=0;j<2;++j)
{float3 finLine_startPos= line_start;float3 finLine_endPos= line_end;float uvX1=uv_offset;float uvX2=uv_offset+uv_scale;[loop]for ( int i=0;i<=_FinJointNum;++i){float finFactor = (float) i / _FinJointNum;//描述 當前的毛發段在整個毛發片上的位置float3 dirOS03 = normalize(finLine_endPos - finLine_startPos);float3 faceNormalOS=normalize( cross(dirOS03,OffsetOS));//發片的法線方向if(j<1)//渲染正面{float3 finNormalOS = normalize(lerp(finDirOS, faceNormalOS, _FaceNormalFactor));//向流中追加一條鰭邊的起點AppendFinVertex(stream, uv_start, finLine_startPos, finNormalOS, float2(uvX1, finFactor), finSideDirWS);//向流中追加一條鰭邊的終點AppendFinVertex(stream, uv_end, finLine_endPos, finNormalOS, float2(uvX2, finFactor), finSideDirWS);}else//渲染反面{faceNormalOS*=-1;//拿發片面法線再對鰭的生長方向進行混合,得到最終生長方向float3 finNormalOS = normalize(lerp(finDirOS, faceNormalOS, _FaceNormalFactor));//向流中追加一條鰭邊的終點AppendFinVertex(stream, uv_end, finLine_endPos, finNormalOS, float2(uvX2, finFactor), finSideDirWS);//向流中追加一條鰭邊的起點AppendFinVertex(stream, uv_start, finLine_startPos, finNormalOS, float2(uvX1, finFactor), finSideDirWS);}}stream.RestartStrip();
}
2.AppendFinVertex()
功能:計算單個頂點在裁剪空間、世界空間、切線空間等下的位置、法線、UV 等信息,并將其添加到幾何著色器的輸出流中。
//向流中追加1個頂點
void AppendFinVertex(inout TriangleStream<Varyings> stream, float2 uv, float3 posOS, float3 normalOS, float2 finUv,float3 finSideDirWS)
{Varyings output = (Varyings)0;VertexPositionInputs vertexInput = GetVertexPositionInputs(posOS);output.positionCS = vertexInput.positionCS ;output.positionWS = vertexInput.positionWS;output.normalWS = TransformObjectToWorldNormal(normalOS);output.uv = uv;output.finUv = finUv;output.finTangentWS = SafeNormalize(cross(output.normalWS, finSideDirWS));//其他相關操作stream.Append(output);
}
?2.效果展示
通過調整_FurLength得到的效果:
2.添加風力擾動
下面繼續加上頂點擾動的風力效果。
1.效果展示
2.技術要點
1.世界空間下計算風力擾動
定義屬性
_WindFreq:描述風速。
_WindMove:風力及強度。
//將風力在世界坐標下計算
float3 posWS_root=TransformObjectToWorld(line_start);
float3 windAngle = _Time.w * _WindFreq.xyz;//計算了風力動畫的當前相位
float3 windMoveWS = _WindMove.xyz * sin(windAngle + posWS_root * _WindMove.w);
//將風力偏再移轉到物體空間
float3 windMoveOS=TransformWorldToObjectDir(windMoveWS);// 這里用 Dir 因為是相對位移
2.增加網格細分
????????由于網格體細分有限,網格體上的三角形數量有限,導致目前的鰭片相對稀疏,為了獲得更稠密的的毛發,我嘗試使用了曲面著色器對原始網格體進行細分。
關于曲面著色器的使用這里主要涉及三個函數和1個結構體。詳見我之前的一篇博客:
[學習記錄]Unity-Shader-曲面細分著色器-CSDN博客
1.技術要點
1.補丁常量結構體:描述處理補丁的細分信息。
struct HsConstantOutput{float fTessFactor[3] : SV_TessFactor;//必須有的語義,定義補丁三條邊的細分因子float fInsideTessFactor : SV_InsideTessFactor;//定義了補丁內部區域的細分因子。它控制了補丁內部的三角形網格的密度//PN_三角形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;};
2.主外殼著色器
//主外殼著色器函數[domain("tri")][partitioning("integer")][outputtopology("triangle_cw")][patchconstantfunc("hullConst")][outputcontrolpoints(3)]Attributes hull(InputPatch<Attributes, 3> input, uint id : SV_OutputControlPointID){return input[id];}
3.補丁常量著色器
定義屬性
?_TessFactor:描述三角形邊上的細分。
?_InsideTessFactorIntensity:描述補丁內部網格的細分密度。
這里使用的是PN_Triangle(Point-Normal Triangles)細分算法。在補丁常量著色器中得到了所需要的10個控制點,在后續的域著色器中會使用這些控制點去插值得到細分后的頂點的位置,法線等屬性。
//補丁常量函數HsConstantOutput hullConst(InputPatch<Attributes, 3> i){HsConstantOutput o = (HsConstantOutput)0;o.fTessFactor[0] = o.fTessFactor[1] = o.fTessFactor[2] = _TessFactor;o.fInsideTessFactor = _InsideTessFactorIntensity;float3 f3B003 = i[2].positionOS.xyz;//P2=B003float3 f3B030 = i[1].positionOS.xyz;//P1=B030float3 f3B300 = i[0].positionOS.xyz;//P0=B300float3 f3N002 = i[2].normalOS;float3 f3N020 = i[1].normalOS;float3 f3N200 = i[0].normalOS;//P0-P1邊控制點o.f3B210 = ((2.0 * f3B300) + f3B030 - (dot((f3B030 - f3B300), f3N200) * f3N200)) / 3.0;o.f3B120 = ((2.0 * f3B030) + f3B300 - (dot((f3B300 - f3B030), f3N020) * f3N020)) / 3.0;//P1-P2邊控制點o.f3B021 = ((2.0 * f3B030) + f3B003 - (dot((f3B003 - f3B030), f3N020) * f3N020)) / 3.0;o.f3B012 = ((2.0 * f3B003) + f3B030 - (dot((f3B030 - f3B003), f3N002) * f3N002)) / 3.0;//P0-P2邊控制點o.f3B102 = ((2.0 * f3B003) + f3B300 - (dot((f3B300 - f3B003), f3N002) * f3N002)) / 3.0;o.f3B201 = ((2.0 * f3B300) + f3B003 - (dot((f3B003 - f3B300), f3N200) * f3N200)) / 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 - f3B300, f3N200 + f3N020) / dot(f3B030 - f3B300, f3B030 - f3B300);float fV23 = 2.0 * dot(f3B003 - f3B030, f3N020 + f3N002) / dot(f3B003 - f3B030, f3B003 - f3B030);float fV31 = 2.0 * dot(f3B300 - f3B003, f3N002 + f3N200) / dot(f3B300 - f3B003, f3B300 - f3B003);o.f3N110 = normalize(f3N200 + f3N020 - fV12 * (f3B030 - f3B300));o.f3N011 = normalize(f3N020 + f3N002 - fV23 * (f3B003 - f3B030));o.f3N101 = normalize(f3N002 + f3N200 - fV31 * (f3B300 - f3B003));return o;}
4.域著色器
接收輸入: 接收來自 Hull Shader 的常量輸出,包括 PN-三角形的控制點和法線,以及原始補丁的三個控制點i。同時接收當前評估點的重心坐標(bary的x,y,z分別對應 U, V, W)。
計算加權系數: 根據重心坐標fU,fV,fW,計算出用于插值的各種多項式項(fUU,fVV,fWW,fUU3等)。這些系數是 PN-三角形曲面細分算法中的權值。
插值生成新的頂點位置(positionOS):
使用 PN-三角形算法的公式,結合原始補丁的三個頂點位置(i[0].positionOS.xyz,i[1].positionOS.xyz,i[2].positionOS.xyz,) 以及hsConst中計算出的中間控制點 (f3B210
到 f3B111
),通過加權插值計算出新的對象空間頂點位置o.positionOS。使得細分后的曲面更加平滑。
插值生成新的頂點法線(normalOS):
類似地,它使用 PN-三角形算法的法線插值公式,結合原始頂點的法線和 hsConst
中計算出的法線控制點 (f3N110
, f3N011
, f3N101
),加權插值生成新的對象空間頂點法線o.normalOS。最后對法線進行歸一化。
插值生成新的 UV 坐標(uv):
對于 UV 坐標,它進行簡單的重心坐標插值,將原始頂點的 UV 坐標(i[0].uv,i[1].uv,i[2].uv)按照fW,fU,fV的比例進行混合,生成新的o.uv。
返回結果: 將包含新生成的頂點位置、法線和UV的Attributes結構體返回。
[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 = 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;}
?2.效果展示
由此,我們實現了對基礎幾何體網格的細分控制效果。
四.完整源碼
Shader "Unlit/Base_Fin_Fur_NonGpuIns"
{Properties{_MainTex ("BaseMap", 2D) = "white" {}_FurTex ("FurTex", 2D) = "white" {}_FaceViewThresh("FaceView Thresh",Range(0,1))=0.5_FurDensity("FurDensity ",Range(10,40))=15_AlphaCutout("AlphaCutout ",Range(0,1))=0_FinJointNum("_FinJointNum",Int)=1_BaseMove("BaseMove",Vector)=(0,0,0,0)_FinLength("FinLength",Float)=0.5//發片總長度_MoveFactor("MoveFactor",Float)=1_FaceNormalFactor("FaceNormalFactor",Range(0,1))=0_FinRandomDirIntensity("FinRandomDirIntensity",Range(0,1))=0_WindFreq("WindFreq",Vector)=(1,1,1,1)_WindMove("WindMove",Vector)=(1,1,1,1)[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, 20))=1_InsideTessFactorIntensity("Tessellation Factor", Range(1, 20))=1}SubShader{// 設置渲染隊列和混合模式,確保透明度效果正確Tags { "RenderType"="Opacity" "RenderPipeline"="UniversalPipeline" "Queue"="Opacity" }LOD 100 // 簡單的LOD,通常在游戲中使用更復雜的LOD系統Pass{Blend SrcAlpha OneMinusSrcAlpha ZWrite OnCull offHLSLPROGRAM#pragma vertex Vert#pragma fragment Frag#pragma hull hull#pragma domain domain#pragma geometry Geom// 引入URP Shader Library,提供常用函數和宏#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"// 定義從C#傳入的頂點屬性struct Attributes{float4 positionOS : POSITION;float2 uv : TEXCOORD0; float3 normalOS : NORMAL;};// 定義從頂點著色器傳遞到片段著色器的數據struct Varyings{float4 positionCS : SV_POSITION; // 裁剪頂點位置float3 positionWS : TEXCOORD0; // 世界頂點位置float3 normalWS : TEXCOORD1; // 世界法線方向float2 uv : TEXCOORD2; // UV坐標float2 finUv : TEXCOORD5; // 從根部到尖端的因子 (0=根, 1=尖)float3 finTangentWS : TEXCOORD6;};CBUFFER_START(UnityPerMaterial)float4 _MainTex_ST; // 紋理的縮放平移 (由TRANSFORM_TEX自動使用)float4 _FurTex_ST; // 紋理的縮放平移 (由TRANSFORM_TEX自動使用)half _AlphaCutout; // Alpha裁剪閾值float _FaceViewThresh;//視角剔除float _FurDensity;//發片細節密度float _FinRandomDirIntensity;//發片法線隨機強度float _FaceNormalFactor;//發片面法線方向偏移貢獻int _FinJointNum;//發片段數float4 _BaseMove;float _FinLength;//發片總長度float _MoveFactor;//發片移動強度float4 _WindMove;float4 _WindFreq;float _TessMinDist;float _TessMaxDist;float _TessFactor;float _InsideTessFactorIntensity;CBUFFER_END// 紋理和采樣器TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex);TEXTURE2D(_FurTex); SAMPLER(sampler_FurTex);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);}struct HsConstantOutput{float fTessFactor[3] : SV_TessFactor;//必須有的語義,定義補丁三條邊的細分因子float fInsideTessFactor : SV_InsideTessFactor;//定義了補丁內部區域的細分因子。它控制了補丁內部的三角形網格的密度//PN_三角形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;o.fTessFactor[0] = o.fTessFactor[1] = o.fTessFactor[2] = _TessFactor;o.fInsideTessFactor = _InsideTessFactorIntensity;float3 f3B003 = i[2].positionOS.xyz;//P2=B003float3 f3B030 = i[1].positionOS.xyz;//P1=B030float3 f3B300 = i[0].positionOS.xyz;//P0=B300float3 f3N002 = i[2].normalOS;float3 f3N020 = i[1].normalOS;float3 f3N200 = i[0].normalOS;//P0-P1邊控制點o.f3B210 = ((2.0 * f3B300) + f3B030 - (dot((f3B030 - f3B300), f3N200) * f3N200)) / 3.0;o.f3B120 = ((2.0 * f3B030) + f3B300 - (dot((f3B300 - f3B030), f3N020) * f3N020)) / 3.0;//P1-P2邊控制點o.f3B021 = ((2.0 * f3B030) + f3B003 - (dot((f3B003 - f3B030), f3N020) * f3N020)) / 3.0;o.f3B012 = ((2.0 * f3B003) + f3B030 - (dot((f3B030 - f3B003), f3N002) * f3N002)) / 3.0;//P0-P2邊控制點o.f3B102 = ((2.0 * f3B003) + f3B300 - (dot((f3B300 - f3B003), f3N002) * f3N002)) / 3.0;o.f3B201 = ((2.0 * f3B300) + f3B003 - (dot((f3B003 - f3B300), f3N200) * f3N200)) / 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 - f3B300, f3N200 + f3N020) / dot(f3B030 - f3B300, f3B030 - f3B300);float fV23 = 2.0 * dot(f3B003 - f3B030, f3N020 + f3N002) / dot(f3B003 - f3B030, f3B003 - f3B030);float fV31 = 2.0 * dot(f3B300 - f3B003, f3N002 + f3N200) / dot(f3B300 - f3B003, f3B300 - f3B003);o.f3N110 = normalize(f3N200 + f3N020 - fV12 * (f3B030 - f3B300));o.f3N011 = normalize(f3N020 + f3N002 - fV23 * (f3B003 - f3B030));o.f3N101 = normalize(f3N002 + f3N200 - fV31 * (f3B300 - f3B003));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 = 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;}// 頂點著色器Attributes Vert(Attributes input){return input;}//向流中追加1個頂點void AppendFinVertex(inout TriangleStream<Varyings> stream, float2 uv, float3 posOS, float3 normalOS, float2 finUv,float3 finSideDirWS){Varyings output = (Varyings)0;VertexPositionInputs vertexInput = GetVertexPositionInputs(posOS);output.positionCS = vertexInput.positionCS ;output.positionWS = vertexInput.positionWS;output.normalWS = TransformObjectToWorldNormal(normalOS);output.uv = uv;output.finUv = finUv;output.finTangentWS = SafeNormalize(cross(output.normalWS, finSideDirWS));stream.Append(output);}//向流中追加所有頂點void AppendFinVertices(inout TriangleStream<Varyings> stream,Attributes input0,//三角形頂點1Attributes input1,//三角形頂點2Attributes input2)//三角形頂點3{//在對象空間進行所有方向計算//將第一個頂點作為鰭邊的起點//對邊中點作為鰭邊的終點float3 line_start=input0.positionOS;//鰭邊起點float3 line1=input1.positionOS-input0.positionOS;float3 line2=input2.positionOS-input0.positionOS;float3 line_end= input0.positionOS+ (line1+line2)/2;//鰭邊終點float2 uv_start=TRANSFORM_TEX(input0.uv,_MainTex);float2 uv_end=(TRANSFORM_TEX(input1.uv,_MainTex)+TRANSFORM_TEX(input2.uv,_MainTex))/2;float uv_offset=length(uv_start);float uv_scale=length(uv_start-uv_end) * _FurDensity;float3 finDirOS=input0.normalOS;finDirOS+= rand3(input0.uv) *_FinRandomDirIntensity;finDirOS=normalize(finDirOS);//生長方向的單位向量float finStep = _FinLength / _FinJointNum;//每個分段有多長float3 finSideDir=normalize(line_end-line_start);//寬方向的單位向量float3 finSideDirWS = TransformObjectToWorldDir(finSideDir);//將風力在世界坐標下計算float3 posWS_root=TransformObjectToWorld(line_start);float3 windAngle = _Time.w * _WindFreq.xyz;//計算了風力動畫的當前相位float3 windMoveWS = _WindMove.xyz * sin(windAngle + posWS_root * _WindMove.w);//將風力偏再移轉到物體空間float3 windMoveOS=TransformWorldToObjectDir(windMoveWS);// 這里用 Dir 因為是相對位移[unroll]for (int j=0;j<2;++j){float3 finLine_startPos= line_start;float3 finLine_endPos= line_end;float uvX1=uv_offset;float uvX2=uv_offset+uv_scale;[loop]for ( int i=0;i<=_FinJointNum;++i){float finFactor = (float) i / _FinJointNum;//描述 當前的毛發段在整個毛發片上的位置float moveFactor = pow(_MoveFactor,abs(finFactor) );//描述 風力和基礎擺動對毛發當前分段的影響強度float3 OffsetOS = SafeNormalize(finDirOS + (windMoveOS+_BaseMove) * moveFactor) * finStep;//根據毛發的當前進度 (finFactor),將風力和基礎偏移疊加到毛發的正常生長方向上,并計算出當前分段的實際位移finLine_startPos += OffsetOS;finLine_endPos += OffsetOS;//得到一邊的起點終點在世界空間的位置float3 dirOS03 = normalize(finLine_endPos - finLine_startPos);float3 faceNormalOS=normalize( cross(dirOS03,OffsetOS));//發片的法線方向if(j<1)//渲染正面{//拿發片面法線再對鰭的生長方向進行混合,得到最終生長方向float3 finNormalOS = normalize(lerp(finDirOS, faceNormalOS, _FaceNormalFactor));//向流中追加一條鰭邊的起點AppendFinVertex(stream, uv_start, finLine_startPos, finNormalOS, float2(uvX1, finFactor), finSideDirWS);//向流中追加一條鰭邊的終點AppendFinVertex(stream, uv_end, finLine_endPos, finNormalOS, float2(uvX2, finFactor), finSideDirWS);}else//渲染反面{faceNormalOS*=-1;//拿發片面法線再對鰭的生長方向進行混合,得到最終生長方向float3 finNormalOS = normalize(lerp(finDirOS, faceNormalOS, _FaceNormalFactor));//向流中追加一條鰭邊的終點AppendFinVertex(stream, uv_end, finLine_endPos, finNormalOS, float2(uvX2, finFactor), finSideDirWS);//向流中追加一條鰭邊的起點AppendFinVertex(stream, uv_start, finLine_startPos, finNormalOS, float2(uvX1, finFactor), finSideDirWS);}}stream.RestartStrip();}}inline float3 GetViewDirectionOS(float3 posOS){float3 cameraOS = TransformWorldToObject(GetCameraPositionWS());return normalize(posOS - cameraOS);}[maxvertexcount(39)]void Geom(triangle Attributes input[3],inout TriangleStream<Varyings> stream){//渲染原始幾何體for (int i=0;i<3;++i){Varyings output=(Varyings)0;//得到各個坐標系下頂點位置VertexPositionInputs vertexInput=GetVertexPositionInputs(input[i].positionOS.xyz);output.positionCS=vertexInput.positionCS;output.positionWS=vertexInput.positionWS;output.normalWS=TransformObjectToWorld( input[i].normalOS);output.uv = TRANSFORM_TEX(input[i].uv, _MainTex);output.finUv=float2(-1.0,-1.0);//標記為-1,是為了標記原始幾何體的像素,直接跳過在frag中和渲染的毛發片剔除有關的操作stream.Append(output);}stream.RestartStrip();//渲染毛發片(每個三角形圖元內渲染一個發片)//計算出輸入三角形的面法線和中心float3 line1=(input[1].positionOS-input[0].positionOS).xyz;float3 line2=(input[2].positionOS-input[0].positionOS).xyz;float3 normalOS=normalize(cross(line1,line2));//輸入三角形的面法線float3 centerOS=(input[0].positionOS+input[1].positionOS+input[2].positionOS)/3;//三角形的重心//計算視線與面法線近似度,剔除大于一定角度的發片float3 viewDir=GetViewDirectionOS(centerOS);float eyeDotN=dot(viewDir,normalOS);if(abs(eyeDotN)>_FaceViewThresh) return;//流中追加所有頂點AppendFinVertices(stream,input[0],input[1],input[2]);}// 片段著色器half4 Frag(Varyings input) : SV_Target{// 從紋理圖集采樣顏色half4 furColor = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, input.finUv);if (input.finUv.x >= 0.0 && furColor.a < _AlphaCutout) discard;half4 baseColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);// --- 基礎顏色 (無光照,只顯示紋理顏色) ---half4 finalColor =baseColor; return finalColor;}ENDHLSL}}
}
五.Fin方法與Shell方法比較
Shell 方法 (Layered Shells / Opacity Maps)
1.實現難度
相對較低。 實現 Shell 方法主要是基于基礎模型,通過多次偏移其頂點并應用不同的不透明度紋理層。這涉及到幾何體的復制、頂點偏移的計算以及透明度混合的渲染設置。
2.性能
相對較好。 Shell 方法的性能開銷主要取決于渲染的層數(殼體數量)以及每層使用的紋理分辨率。由于它不涉及渲染大量獨立的細小發絲,幾何體數量相對可控,因此在實時渲染,尤其是游戲等對性能要求較高的場景中,效率較高。
3.表現效果
體積感有限: 毛發看起來像一層層堆疊的半透明卡片,尤其是在頭發稀疏或從側面觀察時,容易缺乏真實的體積感和蓬松度,顯得比較扁平。
各向異性高光模擬較弱: 雖然可以通過法線紋理和各向異性高光紋理進行近似,但由于是面片渲染,難以完美模擬真實頭發絲特有的各向異性光照反射效果。
穿插問題: 當模型或頭發運動幅度較大時,不同殼層之間可能會發生穿插,導致視覺上的不自然。
4.適用場景
適合對毛發細節要求不高、性能預算緊張的場景,如移動端游戲、低配 PC 游戲中的角色毛發或背景毛發。
Fin 方法 (Edge Fin / Edge Planes)
1.實現難度
中等,比 Shell 方法復雜。
它需要額外的邏輯來:識別或生成頭發的邊緣(通常在幾何著色器中完成)。
在這些邊緣處生成額外的幾何體(“鰭片”),這些鰭片通常需要根據攝像機方向進行調整,以確保它們始終面向視圖。正確計算這些 Fin 片的法線和 UV 坐標,以填充邊緣空隙并增強體積感。在一些實現中,可能還需要處理 Fin 片與 Shell 片之間的過渡和融合。
2.性能
略高于純 Shell 方法。 Fin 方法增加了額外的幾何體(Fin 片),因此會增加一些頂點處理和 Draw Call(如果不是高效合批)。但是,由于 Fin 片通常數量相對有限且較窄,其性能開銷通常仍在可接受范圍內,遠低于渲染大量真實發絲的方法。
幾何著色器開銷: 如果在幾何著色器中生成 Fin 片,會帶來幾何著色器的處理開銷。
3.表現效果
顯著改善邊緣體積感: Fin 方法的核心優勢在于增強頭發輪廓的飽滿度和立體感,有效減少 Shell 方法帶來的“紙片”感。通過在邊緣處“立起”額外的面片,毛發看起來更蓬松、更具深度。
減少鋸齒: 額外的邊緣幾何體和半透明混合有助于平滑頭發輪廓,減少視覺上的鋸齒感。
更自然的過渡: 可以更好地處理頭發邊緣與背景的過渡,使其看起來更自然。
各向異性光照潛力: 結合正確的法線和切線計算,Fin 片可以更好地支持各向異性光照,從而進一步提升毛發的真實感。
4.適用場景
????????通常與 Shell 方法結合使用,以提供更優質、更具立體感的實時毛發渲染效果。廣泛應用于中高畫質的 PC 游戲和主機游戲中的角色毛發。
? ? ? ? 后面打算繼續出一期加上完整著色效果的Fin毛發渲染,如果感興趣的話請多多關注哦!
本篇完!