pta简单实现x的n次方_毛发材质实现

本文详细介绍了3D毛发材质的实现过程,从简单的顶点着色器和片元着色器开始,逐步使用几何着色器和细分曲面着色器进行优化。内容包括生成shell外壳、控制透明度随机排布、添加逐层渐变效果、应用几何着色器减少pass数量、实现鳍状体增加细节以及添加动力学效果。文章提供了代码示例和最终效果展示。
摘要由CSDN通过智能技术生成

准备工作做得差不多了,下面我们来一步步实现毛发材质。
我们首先用简单的顶点着色器和片元着色器实现,之后使用几何着色器与细分曲面着色器进行优化。
首先是生成shell外壳,之前介绍过,方法就是将顶点沿其法线方向扩展一段距离。由于可能要多次使用相同的函数和结构体,我们将其定义在一个头文件中。

// FurShader.cginc

#include "UnityCG.cginc"

struct vertexInput
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
};

struct vertexOutput
{
    float3 worldNormal : TEXCOORD1;
    float3 worldPos : TEXCOORD2;
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
};

float _FurLength;
sampler2D _MainTex;
float4 _MainTex_ST;

// BaseVertexShader
vertexOutput baseVert(vertexInput i)
{
    vertexOutput o;
    o.vertex = UnityObjectToClipPos(i.vertex);
    o.uv = TRANSFORM_TEX(i.uv, _MainTex);
    o.worldNormal = UnityObjectToWorldNormal(i.normal);
    o.worldPos = mul(unity_ObjectToWorld, i.vertex).xyz;
    return o;
}

// BaseFragmentShader
fixed4 baseFrag(vertexOutput i) : SV_Target
{
    // sample the texture
    fixed4 col = tex2D(_MainTex, i.uv);

    return col;
}

// FurVertexShader
vertexOutput furVert(vertexInput i)
{
    vertexOutput o;
    float3 pos = i.vertex.xyz + i.normal * _FurLength * FURSTEP;
    o.vertex = UnityObjectToClipPos(float4(pos, 1.0));

    o.uv = TRANSFORM_TEX(i.uv, _MainTex);
    o.worldNormal = UnityObjectToWorldNormal(i.normal);
    o.worldPos = mul(unity_ObjectToWorld, i.vertex).xyz;
    return o;
}


主要在于毛发的顶点着色器,我们将其的顶点沿法线方向挤出一定距离。Shader文件中:

Shader "Unlit/Fur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _FurLength("Fur Length", Range(0,1)) = 0.2
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #define FURSTEP 0.00
            #pragma vertex baseVert
            #pragma fragment baseFrag
            #include "FurShader.cginc"

            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #define FURSTEP 0.1
            #pragma vertex furVert
            #pragma fragment baseFrag
            #include "FurShader.cginc"
            ENDCG
        }
    }
}


我们定义了两个Pass,第一个正常渲染,第二个扩展一定距离。请看一下下面的对比图:

c89c2f0117ac5afbd536eb83cd8f887e.png

993510f6df4eaa4dd9f25c0804f73ca2.png


上面两张图是同一摄像机位置下的毛发长度分别为0和1的效果,可以看到相当于是往外扩张了一段距离。
不过这并不是我们想要的效果,我们可以将渲染的shell外壳的透明度进行一定的随机排布,使用一张噪声图:

7bd6ec3fa68d3c3df9d6e79b795405a3.png


然后,添加噪声纹理变量,编写新的毛发片元着色器:

fixed4 furFrag(vertexOutput i) : SV_Target
{
    fixed alpha = tex2D(_FurNoiseTex, i.uv).r;
    fixed3 col = tex2D(_MainTex, i.uv).rgb;

    return fixed4(col, alpha);
}

注意用噪声图控制shell外壳的透明度。

在shader文件中,我们要开启对应的Tags,方便渲染透明物体:

Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" } 

接着,在第二个Pass中,我们要关闭深度写入,以及设置混合操作:

        Pass
        {
            ZWrite off
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #define FURSTEP 0.1
            #pragma vertex furVert
            #pragma fragment furFrag
            #include "FurShader.cginc"
            ENDCG
        }


效果如下:

c8d6ef7aeed9de9b8d989d037ee1dd10.png


上图中毛发长度设为1。
可以看到有一点毛发的效果,但不够真实,我们可以添加一点逐层渐变的效果。我们可以声明一个密度属性来逐Pass递减透明度值:
我们的主要操作是让shell外壳的颜色随扩展距离三次方递减,同时透明度值由密度控制,随距离二次方递减,并限制在0-1内。
效果:

2d841ab24d55d0d5b5faf0aaa954b4a8.png


上图中毛发长度设为1。
可以看到有一点毛发的效果,但不够真实,我们可以添加一点逐层渐变的效果。我们可以声明一个密度属性来逐Pass递减透明度值:

fixed4 furFrag(vertexOutput i) : SV_Target
{
    fixed alpha = tex2D(_FurNoiseTex, i.uv).r;
    fixed3 col = tex2D(_MainTex, i.uv).rgb - pow(1 - FURSTEP, 3) * 0.1;
    alpha = clamp(alpha - pow(FURSTEP, 2) * _FurDensity, 0, 1);

    return fixed4(col, alpha);
}


我们的主要操作是让shell外壳的颜色随扩展距离三次方递减,同时透明度值由密度控制,随距离二次方递减,并限制在0-1内。
效果:

326b8e4a51cb9eb479fc069bf0a49b85.png


然后我们可以添加一个变量来控制透明度噪声:

fixed alpha = tex2D(_FurNoiseTex, i.uv * _NoiseMultiplier).r; 

同时还可以添加一个变量来为毛发添加重力:

    float4 gravity = float4(0, -1, 0, 0) * (1 - _Rigidness);
    pos += clamp(mul(unity_ObjectToWorld, gravity).xyz, -1, 1) 
           * pow(FURSTEP, 3) * _FurLength;

9e70ca12006398eb2ce75b497b74685a.png


接下来解决一下光照问题,我们可以使用边缘光照来改善毛发显示:

fixed4 furFrag(vertexOutput i) : SV_Target
{
    fixed alpha = tex2D(_FurNoiseTex, i.uv * _NoiseMultiplier).r;
    fixed3 col = tex2D(_MainTex, i.uv).rgb - pow(1 - FURSTEP, 3) * 0.1;

    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
    half rim = 1.0 - saturate(dot(viewDir, i.worldNormal));
    col += pow(rim, _RimPower);

    alpha = clamp(alpha - pow(FURSTEP, 2) * _FurDensity, 0, 1);

    return fixed4(col, alpha);
}

3416473e15997c0323a845421e4c79a3.png


显然,暴力求解会耗费许多的性能,上图中我使用了大概10个pass,可以发现效果会有分层感,提高到20多个Pass可能会有改善,不过这过于繁杂了,我们可以尝试使用几何着色器来进行优化。
这里的思路是,几何着色器输入一个三角形,针对每个顶点生成多层三角形,同时为了保证能够逐层衰减透明度值,这里我添加了一个索引属性来代表三角形所在层数,下面是几何着色器实现:

[maxvertexcount(MAXCOUNT)]
void furGeom(triangle vertexOutput IN[3], inout TriangleStream<geometryOutput> triStream)
{
    geometryOutput o;

    for (int i = 1; i <= _Iteration; i++)
    {
        for(int j = 0; j < 3; j++)
        {
            float3 pos = IN[j].vertex.xyz + IN[j].normal * _FurLength * i * 0.1;
            float4 gravity = float4(0,-1,0,0) * (1 -_Rigidness);
            pos += clamp(mul(unity_ObjectToWorld,gravity).xyz, -1, 1) 
                   * pow(i * 0.1, 3) * _FurLength;
            
            o.vertex = UnityObjectToClipPos(float4(pos, 1.0));
            o.uv = TRANSFORM_TEX(IN[j].uv, _MainTex);
            o.worldNormal = UnityObjectToWorldNormal(IN[j].normal);
            o.worldPos = mul(unity_ObjectToWorld, IN[j].vertex).xyz;
            o.index = i;
            triStream.Append(o);
       }
       triStream.RestartStrip();
    }
}

这样的话,就可以减少shader中pass的数量:

Shader "Unlit/Fur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _FurNoiseTex ("Texture", 2D) = "white" {}
        _FurLength("Fur Length", Range(0,1)) = 0.2
        _FurDensity("Fur Density", Range(0.1,10)) = 0.2
        _NoiseMultiplier("Multiplier", Range(0,10)) = 1
        _RimPower("Rim Power", Range(1, 256)) = 16
        [IntRange]_Iteration("Fur Iteretion", Range(5, 20)) = 10
    }
    SubShader
    {
        Tags { "Queue" = "Transparent" }

        Pass
        {
            CGPROGRAM
            #define MAXCOUNT 0
            #pragma vertex baseVert
            #pragma fragment baseFrag
            #include "FurShader.cginc"
            ENDCG
        }

        Pass
        {
            ZWrite off
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #define MAXCOUNT 60
            #pragma vertex furVert
            #pragma geometry furGeom
            #pragma fragment furFrag
            #include "FurShader.cginc"
            ENDCG
        }
    }
}


效果:

35a30692abbca13a17745bd0ccbc06b4.png


使用了shell外壳后,我们可以尝试添加鳍状体来增加毛发的细节,鳍状体的生成在一个额外的pass中。
生成鳍状体基本的思路是在每个输入三角形的基础上,在某一条边或中间挤出一个四边形,我们可以使用细分曲面着色器控制鳍状体的密度。对于鳍状体,为了模拟毛发,我们也会为其添加细分,同时会让其有弯曲的效果。最后,我们还会研究如何为鳍状体与shell外壳添加跟随物体的动力学效果。
按照之前英伟达白皮书和相关论文中的解释,需要挤出鳍状体的边是剪影边,即一个背朝观察者的三角形和一个朝向观察者的三角形的重合边。我们在几何着色器中判断这一点,然后挤出四边形。
几何着色器实现如下:

[maxvertexcount(4)]
void finsGeom(lineadj vertexOutput IN[4], inout TriangleStream<geometryOutput> triStream)
{
    geometryOutput o;
    
// triangle's normals
    //float3 N1 = normalize(cross(IN[0].worldPos - IN[1].worldPos, IN[3].worldPos - IN[1].worldPos));
    //float3 N2 = normalize(cross(IN[2].worldPos - IN[1].worldPos, IN[0].worldPos - IN[1].worldPos))
    float3 N1 = normalize(cross(IN[0].vertex.xyz - IN[1].vertex.xyz, 
             IN[3].vertex.xyz - IN[1].vertex.xyz));
    float3 N2 = normalize(cross(IN[2].vertex.xyz - IN[1].vertex.xyz, 
             IN[0].vertex.xyz - IN[1].vertex.xyz));

    N1 = UnityObjectToWorldNormal(N1);
    N2 = UnityObjectToWorldNormal(N2);

    // triangles's barycentric
    float3 barycentric1 = (IN[0].worldPos + IN[1].worldPos + IN[3].worldPos) / 3;
    float3 barycentric2 = (IN[0].worldPos + IN[1].worldPos + IN[2].worldPos) / 3;

    // viewDir
    float3 viewDir1 = normalize(_WorldSpaceCameraPos.xyz - barycentric1);
    float3 viewDir2 = normalize(_WorldSpaceCameraPos.xyz - barycentric2);

    // if silhouette
    float eyeDotN1 = dot(viewDir1, N1);
    float eyeDotN2 = dot(viewDir2, N2);

    if(eyeDotN1 * eyeDotN2 < 0 || abs(eyeDotN1) < _FinThreshold || 
     abs(eyeDotN2) < _FinThreshold)
    {
        o.vertex = UnityObjectToClipPos(IN[0].vertex);
        o.uv = TRANSFORM_TEX(IN[0].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(IN[0].normal);
        o.worldPos = IN[0].worldPos;
        o.index = 0;
        triStream.Append(o);

        o.vertex = UnityObjectToClipPos(IN[1].vertex);
        o.uv = TRANSFORM_TEX(IN[1].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(IN[1].normal);
        o.worldPos = IN[1].worldPos;
        o.index = 1;
        triStream.Append(o);


        fixed3 pos = IN[0].vertex.xyz + IN[0].normal * _FinLength;
        o.vertex = UnityObjectToClipPos(fixed4(pos, 1.0));
        o.uv = TRANSFORM_TEX(IN[0].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(IN[0].normal);
        o.worldPos = mul(unity_ObjectToWorld, float4(pos, 1.0)).xyz;
        o.index = 2;
        triStream.Append(o);

        pos = IN[1].vertex.xyz + IN[1].normal * _FinLength;
        o.vertex = UnityObjectToClipPos(fixed4(pos, 1.0));
        o.uv = TRANSFORM_TEX(IN[1].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(IN[1].normal);
        o.worldPos = mul(unity_ObjectToWorld, float4(pos, 1.0)).xyz;
        o.index = 3;
        triStream.Append(o);

        triStream.RestartStrip();
    }

虽然鳍状体可以设置的很短来避免看出来,但稍长的话就会明显看出是一个一个的四边形,我们可以使用细分曲面着色器来增加鳍状体的数量:

// patchConstantFunction
tessellationFactors patchConstantFunction(InputPatch<vertexInput, 3> patch)
{
    tessellationFactors f;
    f.edge[0] = _TessellationUniform;
    f.edge[1] = _TessellationUniform;
    f.edge[2] = _TessellationUniform;
    f.inside = _TessellationUniform;

    return f;
}

// FinsHullShader
[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("patchConstantFunction")]
vertexInput finsHull(InputPatch<vertexInput, 3> patch, uint id : SV_OUTPUTCONTROLPOINTID)
{
    return patch[id];
}

// FinsDomainShader
[UNITY_domain("tri")]
vertexOutput finsDomain(tessellationFactors factors, OutputPatch<vertexInput, 3> patch, 
float3 barycentricCoordinates : SV_DOMAINLOCATION)
{
    vertexInput i;
    #define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) i.fieldName = 
    patch[0].fieldName * barycentricCoordinates.x + 
    patch[1].fieldName * barycentricCoordinates.y + 
    patch[2].fieldName * barycentricCoordinates.z;

    MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
    MY_DOMAIN_PROGRAM_INTERPOLATE(normal)
    //MY_DOMAIN_PROGRAM_INTERPOLATE(uv)

    return finsVert(i);
}


这是非常常规的写法,通过一个属性来控制细分程度。
最后的效果(这里就不用那个球了)

fd2361b535bfcd1697482269ee9de73c.png


效果不是那么好调。
-----ps----
经过我的调试,发现修改生成鳍状体的顶点的世界法线为某一面法线会有不错的效果。更新的几何着色器:

// FinsGeometryShader
[maxvertexcount(4)]
void finsGeom(lineadj vertexOutput IN[4], inout TriangleStream<geometryOutput> triStream)
{
    geometryOutput o;

    float3 N1 = normalize(cross(IN[0].vertex.xyz - IN[1].vertex.xyz, 
             IN[3].vertex.xyz - IN[1].vertex.xyz));
    float3 N2 = normalize(cross(IN[2].vertex.xyz - IN[1].vertex.xyz, 
             IN[0].vertex.xyz - IN[1].vertex.xyz));

    float3 worldN1 = UnityObjectToWorldNormal(N1);
    float3 worldN2 = UnityObjectToWorldNormal(N2);
 // triangles's barycentric
    float3 barycentric1 = (IN[0].worldPos + IN[1].worldPos + IN[3].worldPos) / 3;
    float3 barycentric2 = (IN[0].worldPos + IN[1].worldPos + IN[2].worldPos) / 3;

    // viewDir
    float3 viewDir1 = normalize(_WorldSpaceCameraPos.xyz - barycentric1);
    float3 viewDir2 = normalize(_WorldSpaceCameraPos.xyz - barycentric2);

    // if silhouette
    float eyeDotN1 = dot(viewDir1, worldN1);
    float eyeDotN2 = dot(viewDir2, worldN2);

    if(eyeDotN1 * eyeDotN2 < 0 || abs(eyeDotN1) < _FinThreshold 
     || abs(eyeDotN2) < _FinThreshold)
    {
        o.vertex = UnityObjectToClipPos(IN[0].vertex);
        o.uv = TRANSFORM_TEX(IN[0].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(N2);
        o.worldPos = IN[0].worldPos;
        o.index = 0;
        triStream.Append(o);

        o.vertex = UnityObjectToClipPos(IN[1].vertex);
        o.uv = TRANSFORM_TEX(IN[1].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(N2);
        o.worldPos = IN[1].worldPos;
        o.index = 0;
        triStream.Append(o);


        fixed3 pos = IN[0].vertex.xyz + N2 * _FinLength;
        o.vertex = UnityObjectToClipPos(fixed4(pos, 1.0));
        o.uv = TRANSFORM_TEX(IN[0].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(N2);
        o.worldPos = mul(unity_ObjectToWorld, float4(pos, 1.0)).xyz;
        o.index = 1;
        triStream.Append(o);

        pos = IN[1].vertex.xyz + N2 * _FinLength;
        o.vertex = UnityObjectToClipPos(fixed4(pos, 1.0));
        o.uv = TRANSFORM_TEX(IN[1].uv, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(N2);
        o.worldPos = mul(unity_ObjectToWorld, float4(pos, 1.0)).xyz;
        o.index = 1;
        triStream.Append(o);

        triStream.RestartStrip();
    }
}


同时,为鳍状体使用和shell外壳类似的边缘光照:

// FinsFragmentShader
fixed4 finsFrag(geometryOutput i) : SV_Target
{
    fixed alpha = tex2D(_FurNoiseTex, i.uv * _NoiseMultiplier).r;
    fixed3 col = tex2D(_MainTex, i.uv).rgb- pow(1 - (i.index +1) * 0.1, 3) * 0.1;

    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
    half rim = 1.0 - saturate(dot(viewDir, i.worldNormal));
    col += pow(rim, _RimPower);

    alpha = clamp(alpha - pow((i.index+1) * 0.1, 2) * _FurDensity, 0, 1);

    return fixed4(col, alpha);
}


效果:

13c6b2d7c5b5304e79198d58c7af5cfe.png

更新
目前找到了一个可行的办法来添加一点毛发的动力学。
主要做法在于获得模型在前一帧的模型变换矩阵,来获得其点在前一帧的世界位置。
接下来我们为相关结构体添加之前世界位置的属性:

struct vertexOutput
{
    float3 worldNormal : TEXCOORD1;
    float3 worldPos : TEXCOORD2;
    float3 previousWorldPos : TEXCOORD3;
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    float3 normal : NORMAL;
};

struct geometryOutput
{
    float3 worldNormal : TEXCOORD1;
    float3 worldPos : TEXCOORD2;
    float3 previousWorldPos : TEXCOORD4;
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    int index : TEXCOORD3;
};

然后我们可以在shell外壳的几何着色器中获得一个顶点运动的方向,将重力方向减去该运动方向就可以得到顶点随物体运动方向的一个模拟,我们可以编写一个C#脚本获得当前模型的模型矩阵,并将其传入材质中:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SendMatrix : MonoBehaviour
{
    Matrix4x4 previousModelMatrix;
    Matrix4x4 currentModelMatrix;
    public Material material;

    void Start()
    {
        currentModelMatrix = transform.localToWorldMatrix;
        previousModelMatrix = currentModelMatrix;
    }
    void FixedUpdate()
    {
        Renderer rend = GetComponent<Renderer>();
        previousModelMatrix = currentModelMatrix;
        currentModelMatrix = transform.localToWorldMatrix;
        
        material.SetMatrix("_PreviousFrameModelMatrix", previousModelMatrix);
        material.SetMatrix("_CurrentFrameInverseModelMatrix", currentModelMatrix.inverse);
        material.SetMatrix("_CurrentFrameModelMatrix", currentModelMatrix);
        Debug.Log(previousModelMatrix);
    }
}


经我的测试,由于某些原因,外部得到的模型矩阵与自动传入shader的模型矩阵好像存在一定的差距,可能是帧率的原因,因此我自己传入了三个矩阵,只用于顶点运动方向的计算,其余时刻仍使用自动传入shader的模型矩阵计算相关位置。几何着色器中:

            float3 pos = IN[j].vertex.xyz + IN[j].normal * _FurLength * i * 0.1;
            float4 gravity = float4(0,-1,0,0) * (1 -_Rigidness);

            float4 vertexMotionDir = float4(mul(_CurrentFrameModelMatrix, 
                    IN[j].vertex).xyz - IN[j].previousWorldPos, 0) * (1 -_Rigidness);

            float4 MotionDir = gravity - vertexMotionDir;

            pos += clamp(mul(_CurrentFrameInverseModelMatrix, MotionDir).xyz, -1, 1) 
                     * pow(i * 0.1, 3) * _FurLength;

当然,这实现的只是最简单的动态,只是让毛能够随物体动,但更真实的效果需要体现其的轻柔材质,这需要一定的运动缓冲,即得到平滑曲线的运动效果,这个目前还在研究中。

项目代码已经更新,欢迎大家查阅。

项目的github地址放这里,大家可以查阅:https://github.com/Dragon-Baby/Hair-Material

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值