[学习记录]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.pswp.cn/pingmian/87483.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/87483.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

指针篇(7)- 指针运算笔试题(阿里巴巴)

目录 一、指针运算笔试题解析3.1 题目1&#xff1a;3.2 题目2&#xff1a;3.3 指针3&#xff1a;3.4 题目4&#xff1a;3.5 题目5&#xff1a;3.6 题目6&#xff1a;3.7 题目7&#xff1a; 总结 一、指针运算笔试题解析 3.1 题目1&#xff1a; #include<stdio.h> int m…

homebrew的一些常用方法

前言 因本人工作换到mac电脑&#xff0c;对包管理器homebrew的需求增加&#xff0c;因此将一些常用命令做如下记录&#xff0c;本博客主要用作记录用。 官网 macOS&#xff08;或 Linux&#xff09;缺失的软件包的管理器 — Homebrew 常用命令 如果脚本因网络问题无法下载…

【Python面试题】Python面试之基础知识常见面试题3-汇总篇(精选30个)

目录专栏导读前言1. 字典的内存管理机制是什么&#xff1f;2. 列表的内存管理机制是什么&#xff1f;3. 元组和列表的区别4. 字符串插值的方法5. 闭包、装饰器的原理闭包&#xff08;Closure&#xff09;装饰器&#xff08;Decorator&#xff09;6. map、filter的区别7. range(…

【免费.NET方案】CSV到PDF与DataTable的快速转换

CSV作为轻量级数据载体&#xff0c;在数据传输中占比超过70%。但其原生格式存在三大痛点&#xff1a; 可视化缺陷&#xff1a;无法直接生成可打印的报表结构限制&#xff1a;缺乏数据类型定义和关系约束安全风险&#xff1a;易被意外修改导致数据失真 因此&#xff0c;我们常…

connect的断线重连

connect的短线重连 客户端代码的编写服务器代码的编写总结 客户端代码的编写 #include <iostream> #include <string> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h>…

通过观看数百个外科手术视频讲座来学习多模态表征|文献速递-最新论文分享

Title题目Learning multi-modal representations by watching hundreds of surgical video lectures通过观看数百个外科手术视频讲座来学习多模态表征01文献速递介绍外科计算机视觉领域的最新进展&#xff0c;已开始为手术室&#xff08;OR&#xff09;的新一代人工智能辅助支…

微信小程序如何实现再多个页面共享数据

在微信小程序中&#xff0c;实现多个页面共享数据有以下几种常用方式&#xff0c;根据场景选择最适合的方案&#xff1a; 全局变量&#xff08;App.js&#xff09; 适用场景&#xff1a;简单数据共享&#xff08;非响应式&#xff09; 实现方式&#xff1a; javascript // ap…

PCIE5.0 TAG说明(ima回答)

在PCIe 5.0规范中&#xff0c;TLP&#xff08;Transaction Layer Packet&#xff09;报文的Tag字段用于标识和管理事务。以下是关于Tag的生成和使用规则和定义的详细描述&#xff1a; Tag字段的定义 Tag字段&#xff1a;位于TLP报文的Header中&#xff0c;占用8位&#xff08…

Type-C PD快充协议智能芯片S312L详解

1. 芯片概述 S312L 是一款智能Type-C PD协议触发芯片&#xff0c;支持**PD3.0&#xff08;含PPS&#xff09;**及多种A口快充协议&#xff08;如QC/PE等&#xff09;&#xff0c;可自动识别并申请5V/9V/12V电压&#xff0c;适用于快充适配器、移动电源等场景。 核心优势&…

stm32学到什么程度可以找工作?

我重新为你写一篇更加详细深入的回答&#xff1a; STM32学到什么程度可以找工作&#xff1f;一个十年老兵的血泪史 写在前面的话&#xff1a;这些年踩过的坑&#xff0c;都是血淋淋的教训 刚看到这个问题&#xff0c;我就想起了2014年那个炎热的夏天。 当时我刚从厦门某马离…

基于 Elasticsearch 实现地图点聚合

在地图类应用中&#xff0c;当需要展示大量地理兴趣点时&#xff0c;直接将所有点渲染在地图上会导致视觉混乱&#xff0c;影响用户体验。为此&#xff0c;我基于 Elasticsearch 提供的 geotile_grid 和 geo_bounding_box 查询能力&#xff0c;实现了一套高效的 POI 聚合展示方…

【Prometheus 】通过 Pushgateway 上报指标数据

Prometheus 是目前最流行的开源监控系统之一&#xff0c;其拉取&#xff08;pull&#xff09;模型非常适合服务发现和静态目标的监控。然而&#xff0c;在某些场景下&#xff0c;例如短生命周期任务、批处理作业或无法暴露 HTTP 接口的服务&#xff0c;传统的拉取方式并不适用。…

服务器 - - QPS与TPS介绍

1、QPS&#xff08;Queries Per Second 每秒查询数&#xff09; 定义&#xff1a;常用于表示每秒的请求次数&#xff0c;衡量接口请求、数据库查询等动作的吞吐量&#xff08;单位时间内处理的数据量&#xff09; 计算&#xff1a;总请求数/请求时间&#xff0c;如&#xff1…

Cot2:思维链提示激发大型语言模型的推理能力

摘要 我们探讨了生成思维链——一系列中间推理步骤——如何显著提升大型语言模型执行复杂推理的能力。特别地&#xff0c;我们展示了在足够大的语言模型中&#xff0c;这种推理能力如何通过一种简单的方法——思维链提示&#xff08;chain-of-thought prompting&#xff09;自…

go交易数据后端

地址 https://gitee.com/EEPPEE_admin/go-stock-line-trading-datahttps://github.com/jerryshell/midas 需求 为了替代rust后端爬虫端: 爬取东方财富数据到index-data目录server端: 项目主要内容 todo 替代https://github.com/jerryshell/midas的前端量化概念性理解扩展: 存储…

灵巧手概览

第一章 灵巧手的技术演进与核心价值 1.1 技术演进的五个阶段 仿生学启蒙阶段&#xff08;1960-1980&#xff09; 1968年斯坦福大学首台3自由度机械夹爪标志机器人操作技术开端&#xff0c;1973年MIT提出"仿生手"概念&#xff0c;但受限于材料和控制技术&#xff0c;…

在设计提示词(Prompt)时,关于信息位置的安排z怎么 结合模型特性和任务目标

在设计提示词(Prompt)时,关于信息位置的安排z怎么 结合模型特性和任务目标 在设计提示词(Prompt)时,关于信息位置的安排确实需要结合模型特性和任务目标。从自注意力机制的原理及应用场景来看,关键信息的位置选择需遵循以下启示,并结合具体场景灵活调整: 一、核心启示…

七、性能优化

目录 1. 如何检测Flutter应用的性能问题&#xff1f;2. 什么是重绘边界&#xff08;Repaint Boundary&#xff09;&#xff1f;3. 如何避免不必要的重建&#xff1f;4. const 构造函数在优化中起什么作用&#xff1f;5. 如何优化长列表的性能&#xff1f;6. 如何减少应用启动时…

Webpack优化详解

Webpack 5提供了一系列工具和功能,可以在本地开发和线上构建过程中进行优化,以提高开发效率和构建性能。 1. 本地开发优化 1.1. 开启模块热替换(HMR) 模块热替换可以在不刷新整个页面的情况下更新模块,提高开发效率。 const webpack = require(webpack);module.export…

latency 对功耗的影响

文章目录 1、Connection Interval(连接间隔) vs. Latency(从机延迟)2、为什么不能完全依赖 Connection Interval?3、什么时候可以不用 Latency?4、如何正确配置?5、结论调节连接间隔(Connection Interval)确实可以直接影响通信频率和功耗,但 Latency(从机延迟)仍然…