当前位置: 首页 > news >正文

[学习记录]Unity毛发渲染[URP]-Fin基础版

        鳍片法是一种在多边形表面垂直添加许多多边形,并在其上粘贴毛发纹理以营造毛茸茸的感觉的技术。这就像种植许多鳍(就像鱼身上的鳍一样)。本期我将在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中计算出的中间控制点 (f3B210f3B111),通过加权插值计算出新的对象空间顶点位置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毛发渲染,如果感兴趣的话请多多关注哦!

本篇完!

http://www.dtcms.com/a/266377.html

相关文章:

  • Django Channels WebSocket实时通信实战:从聊天功能到消息推送
  • Linux入门篇学习——Linux 帮助手册
  • 八、测试与调试
  • 万勋科技「柔韧机器人玻璃幕墙清洗」全国巡展@上海!引领清洗无人机智能化升级
  • Rovo Dev CLI Windows 安装与使用指南
  • 暑期数据结构第一天
  • CLIP的tokenizer详解
  • 2-jdk8环境下安装Kafka
  • 标签体系设计与管理:从理论基础到智能化实践的综合指南
  • chrome安装AXURE插件后无效
  • uniapp 微信小程序水印
  • c++游戏_小恐龙(开源)
  • Spring Boot + MyBatis/MyBatis Plus:XML中循环处理List参数的终极指南
  • MySQL安装报错解决
  • 解锁阿里云Hologres:开启实时数据分析新时代
  • [论文阅读] 人工智能 + 软件工程 | 需求获取访谈中LLM生成跟进问题研究:来龙去脉与创新突破
  • ODS 系统是什么?企业为什么需要搭建 ODS?
  • .net对象映射框架
  • Response对象
  • Gartner《数据与分析治理的参考架构概述》学习心得
  • electron 打包太大 试试 tauri , tauri 安装打包demo
  • 短剧系统开发定制全流程解析:从需求分析到上线的专业指南
  • 屏幕分辨率修改工具 SwitchResX(Mac电脑)
  • 2025.7.4总结
  • Compose LazyVerticalStaggeredGrid卡顿
  • Excel 如何处理更复杂的嵌套逻辑判断?
  • 【嵌入式电机控制#9】编码器滤波算法
  • 敏捷开发在国际化团队管理中的落地
  • 如何选择合适的工业相机快门种类
  • SpringCloud系列 - OpenFeign 远程调用(三)