动机和想要实现的效果

最直接的动机是看了顽皮狗在Siggraph 2016上的PPT,里面介绍了顽皮狗在神秘海域中是如何让植被随风飘荡的。他们介绍了一种将植被的每一部分的pivot的物体空间坐标写到顶点色里,然后在shader中使用这个坐标进行风的效果的计算的方法。较为震撼在风吹过草原时,植被进行弯曲后,草表面的高光会有一种时空上的起伏感(也就是说神秘海域的植被的法线也会被风影响)。所以我也想要借助写pivot的方法来制作植被受到风吹的效果,通过这个方法计算出正确的风吹之后的植被的法线(同时由于法线贴图的存在,还要计算正确的切线)。

稍微翻了一下网上的资料(也没仔细地去搜索),大部分的就是一个普通的顶点动画,有的是用的sin,有的就直接平移。这就产生了第二个需求,植被在顶点动画中应该保持差不多的长度,不然会发现很明显的拉伸的效果。

当然最好还能投射出正常的影子了,这一步只需要把顶点着色器复制一份到投射影子的pass里就可以了。

这里使用的植被模型是MegaScans上的CORDYLINE模型中的var12这个小模型。

难点和相对应的应对方法

Unity的顶点色限制

稍微测试一下就能发现,Unity的顶点色是UNorm8的格式,也就是说无论你在Maya或是3ds Max里导出的模型的顶点色信息是什么样的,导入到Unity中就会变成只有256精度的UNorm8。顽皮狗使用的是自己的引擎,所以它们能够使用全精度的顶点色,但是由于Unity的引擎限制,我们可以考虑到导出pivot的顶点坐标到模型的UV中。

但是很不幸的是,fbx导入到Unity时,即使UV是float4的类型(也就是16bytes),在Unity中只会识别UV的前两位。所以只能无奈的将pivot的顶点坐标(float3的数据)储存到两个UV的三个通道里,同时将pivot的层级存到剩下的一个通道里。我不知道顽皮狗具体是怎么计算pivot的层级关系的,他在PPT中写的是无需计算,但我在实际操作中只能一层一层的算(而且只能算两层),也希望知道具体怎么操作的人告知一下方法。

所以接下来要做的是在Maya中把pivot的物体空间坐标和pivot的层级写到对应顶点的某两套UV中,本文是写到第二套和第三套UV中(也就是TEXCOORD1TEXCOORD2)。于是我恶补了一下maya的python脚本的写法,不过在写数值到UV中时,又遇到了一个小问题。Maya的cmds.polyEditUV这个方法,明明能传入uvSetName这个参数,用于操作对应的UV,但我实际使用时只能写数值到当前的UV中,导致最后写的脚本只能僵硬的操作当前UV,每次切换UV时需要重新修改脚本再运行一次。

最终的脚本是这样的:

VertexPivotWriteTool.py

import maya.cmds as cmds

targetVertexStr = "Select any vertex to start."
vertexColorStr = "Select any vertex to start."
pivotPosition = [0.0, 0.0, 0.0]

def ui():
    if cmds.window("VertexPivotWriteTool", exists = True):
        cmds.deleteUI("VertexPivotWriteTool")

    global targetVertexStr
    global targetVertexField
    global vertexColorStr
    global vertexColorField
    global pivotLayer

    vertexPivotWindow = cmds.window("VertexPivotWriteTool", widthHeight = [500, 400])
    form = cmds.formLayout(numberOfDivisions = 100)

    pivotLayerLable = cmds.text("Pivot Layer (0 for root pivot)")
    pivotLayer = cmds.intField()
    cmds.intField(pivotLayer, e = True, minValue = 0, maxValue = 5, step = 1, value = 0)

    targetVertexButton = cmds.button("Target Vertex", command = 'GetTargetVertex()')
    targetVertexField = cmds.textField(text=targetVertexStr, width = 300)
    #writeVertexButton = cmds.button("Write to Vertex Color", command = 'WriteToVertexColor()')
    writeVertexButton = cmds.button("Write to Vertex Texcoord", command = 'WriteToVertexTexcoord()')
    
    targetVertexColorButton = cmds.button("Show Vertex Color", command = 'GetTargetVertexColor()')
    vertexColorField = cmds.textField(text=vertexColorStr, width = 300)
    
    cmds.showWindow(vertexPivotWindow)
    cmds.formLayout(form, e=True, attachForm = (
    
        [pivotLayerLable, 'left', 25],
        [pivotLayerLable, 'top', 20],
        [pivotLayer, 'right', 25],
        [pivotLayer, 'top', 20],
    
        [targetVertexButton, 'left', 25],
        [targetVertexButton, 'top', 60],
        [targetVertexField, 'right', 25],
        [targetVertexField, 'top', 60],

        [writeVertexButton, 'left', 25],
        [writeVertexButton, 'top', 100],

        [targetVertexColorButton, 'left', 25],
        [targetVertexColorButton, 'bottom', 20],
        [vertexColorField, 'right', 25],
        [vertexColorField, 'bottom', 20],
        
    ))

def GetPivotLayer():
    value = cmds.intField(pivotLayer, q=True, value=True)
    print("pivotLayerValue is: " + str(value))
    return value
    

def GetTargetVertex():
    print("Get Target Vertex...")
    selVertices = cmds.ls(selection = True)

    global targetVertexStr
    global pivotPosition

    if len(selVertices) == 0:
        targetVertexStr = "No vetex selected!"
    elif len(selVertices) >= 2:
        targetVertexStr = "Too many vertices selected! Expected 1, got " + str(len(selVertices))
    else:
        pivotPosition = cmds.pointPosition(selVertices[0])
        tempStr = "("
        for axis in range(len(pivotPosition)):
            if axis >= 1:
                tempStr += ", "
            tempStr += "{:.2f}".format(pivotPosition[axis])
        tempStr += ")"    
        targetVertexStr = tempStr
    
    cmds.textField(targetVertexField, e= True, text = targetVertexStr)

def GetTargetVertexColor():
    print("Get Target Vertex...")
    selVertices = cmds.ls(selection = True)

    global vertexColorStr

    if len(selVertices) == 0:
        vertexColorStr = "No vetex selected!"
    elif len(selVertices) >= 2:
        vertexColorStr = "Too many vertices selected! Expected 1, got " + str(len(selVertices))
    else:
        vertexColor = cmds.polyColorPerVertex(query=True, rgb=True)
        tempStr = "("
        for axis in range(len(vertexColor)):
            if axis >= 1:
                tempStr += ", "                
            tempStr += "{:.2f}".format(vertexColor[axis])
        tempStr += ")"    
        vertexColorStr = tempStr
    
    cmds.textField(vertexColorField, e= True, text = vertexColorStr)

def WriteToVertexColor():
    print("Write To Vertex Color...")
    selVertices = cmds.ls(selection = True)
    for vertex in selVertices:
        cmds.polyColorPerVertex(vertex, rgb=(pivotPosition[0], pivotPosition[1], pivotPosition[2]))

def WriteToVertexTexcoord():
    print("Write To Vertex Coord...")
    pivotLayerValue = GetPivotLayer()
    allUVSets = cmds.polyUVSet( query=True, allUVSets=True )
    uvSetCount = len(allUVSets)
    cmds.polyEditUV(relative = False, uValue = pivotPosition[0], vValue = pivotPosition[1])
    #cmds.polyEditUV(relative = False, uValue = pivotPosition[2], vValue = pivotLayerValue)

ui()

Vertex Pivot Write Tool

因为种种限制,使用时较为复杂,如果有更好的脚本的话,也很感谢分享出来告诉我。首先是要在UV集编辑器中,为模型新增两套UV,由于使用的MegaScans模型本身有两套不同的UV,操作是把原来的第二套UV移动到第四套UV中,然后把第一套UV复制到第二第三套UV中,然后在UV编辑器中定位当前UV到第二套UV。在脚本编辑器中打开或者复制上面的VertexPivotWriteTool.py,通过Crtl + Enter可以生成该脚本的一个窗口。然后执行下述操作:首先是将每个pivot的前两个坐标写到第二套UV中,对茎来说,其pivot是最底下的顶点,对叶片来说,其pivot是最接近茎的顶点,选中这个顶点然后点击Target Vertex,在窗口中可以看到这个顶点的物体空间的坐标;然后在UV编辑器选中该茎或者叶片的UV壳,点击Write to Vertex Texcoord,在UV编辑器中可以看到UV坍缩成了一个点(往往找不到);对第二套UV中的所有的UV壳执行上述操作;然后将当前UV切换到第三套UV,同时注释掉脚本的第117行,取消注释脚本的第118行,然后输入Crtl + Enter重新生成一遍工具;这时我们将要把每个pivot的最后一个坐标和pivot的层级写到第三套UV中;对植被的每一片叶子和枝干,判断其pivot的层级(以现在使用的MegaScans模型为例,茎的层级是0,其他叶片的层级是1),在Pivot Layer中输入层级;然后重复判断层级,选择顶点,写入UV;最后最后,不要忘记把脚本还原成最开始的样子。这样就把每个顶点对应的pivot坐标写入到第二和第三套UV了!导出到Unity就可以了。

如何计算拉伸较小的风的效果,并且计算对应的法线

首先来看这样一张图:

Circle Transform Diagram

这张图表现了在Bend Space中把红色的线段弯曲到绿色线段的算法,X轴是风的方向,可以看到风的强度越高,Radius的大小就越小。同时为了计算出正确的法线和切线,需要同样的计算出AxisX和AxisZ在Bend Space中的向量。使用这个算法,当模型处在Bend Space的Z轴上时,不会受到扭曲,当其X轴大于0时,会受到压缩,当X轴小于0时,会受到拉伸。同样的,这种算法可以推广到三维空间中,同时扭曲Y轴和Z轴,我特地写了一个C#脚本来对变换的结果进行可视化。

WindDebugger.cs

using UnityEngine;
using Unity.Mathematics;

public class WindDebugger : MonoBehaviour
{
    public bool draw = true;

    public float debugRadius = 0.01f;
    public float debugLength = 0.2f;

    public Color pivotColor;
    public Color sphereColor;
    public float radius;
    public float3 originalPosition;

    private void DrawAxes(Color color, Vector3 pos, Vector3 tangent, Vector3 bitangent, Vector3 normal)
    {
        Gizmos.color = color;
        Gizmos.DrawSphere(pos, debugRadius);
        Gizmos.color = Color.red;
        Gizmos.DrawLine(pos, pos + tangent * debugLength);
        Gizmos.color = Color.green;
        Gizmos.DrawLine(pos, pos + bitangent * debugLength);
        Gizmos.color = Color.blue;
        Gizmos.DrawLine(pos, pos + normal * debugLength);
    }

    private float3 CircleTransform(float3 positionBS, float radius, out float3 axisX, out float3 axisY, out float3 axisZ)
    {
        float radVal = math.length(positionBS.xy) / radius;
        float sinVal = math.sin(radVal);
        float cosVal = math.cos(radVal);
        float2 normalizeDir = math.normalize(positionBS.xy);

        float3 targetPosBS = new float3((radius * sinVal) * normalizeDir, radius - radius * cosVal);

        float3 tempAxisX = new float3(-sinVal * normalizeDir, cosVal);
        float3 tempAxisY = new float3(normalizeDir.y, -normalizeDir.x, 0.0f);
        float3 tempAxisZ = new float3(cosVal * normalizeDir, sinVal);

        axisX = tempAxisX;
        axisY = normalizeDir.y * tempAxisY + normalizeDir.x * tempAxisZ;
        axisZ = -normalizeDir.x * tempAxisY + normalizeDir.y * tempAxisZ;

        float3 newPositionBS = targetPosBS + axisX * positionBS.z;
        return newPositionBS;
    }

    private void OnDrawGizmos()
    {
        if (!draw)
        {
            return;
        }

        Color originalColor = Gizmos.color;
        Gizmos.color = sphereColor;
        Gizmos.DrawSphere(new float3(0.0f, 0.0f, radius), radius);

        float3 axisX, axisY, axisZ;
        float3 newPosition = CircleTransform(originalPosition, radius, out axisX, out axisY, out axisZ);
        DrawAxes(pivotColor, newPosition, axisY, axisZ, axisX);

        DrawAxes(pivotColor, originalPosition, new float3(1.0f, 0.0f, 0.0f), new float3(0.0f, 1.0f, 0.0f), new float3(0.0f, 0.0f, 1.0f));

        Gizmos.color = Color.black;
        Gizmos.DrawLine(float3.zero, originalPosition);
        Gizmos.color = Color.white;
        Gizmos.DrawLine(float3.zero, newPosition);

        Gizmos.color = originalColor;
    }
}

其他的一些问题

由于整个计算过程中用到了很多的坐标变换,需要特别的注意每一次变换是从什么空间变换到什么空间。首先是物体空间到风的弯曲空间,由于我们CircleTransform方法是认为风是吹向X轴正方向的,所以需要先对所有的坐标、向量进行一个变换,由于只涉及到旋转,所以可以用一个float3x3的矩阵来表示从物体空间到弯曲空间的变换矩阵。

然后分两种情况:一种是Pivot Layer为0的顶点,也就是所使用的模型的茎上的顶点。这种相对简单,将顶点在物体Bend Space中进行CircleTransform后,就能获得新的Bend Space的坐标和新的三个轴的向量(新的三个轴可以组合出顶点的Bend Space到物体的Bend Space的变换矩阵),可以计算出顶点、法线和切线在物体Bend Space的坐标和向量。最后再从物体Bend Space转换到物体空间就可以了。

第二种是Pivot Layer为1的顶点,要先计算出Pivot的新的物体Bend Space坐标,然后在其基础上计算出每一个顶点相对于Pivot Bend Space的新的坐标,然后一层套一层的算回顶点及其法线切线在物体Bend Space的坐标。最后再从物体Bend Space转换到物体空间就可以了。值得一提的是,我在计算pivot的Bend Space时,所使用的空间和之前图上不太一样,是AxisZ, AxisY和-AxisX对应新的Bend Space的XYZ轴,这样能让垂直于枝干的叶片有更好的风吹的效果。

为了让随风摆动的效果看上去更自然,除了按照圆形来变换顶点之外,参考顽皮狗的演讲,还要给枝干的摇晃添加一个和距离相关的延迟,这样不会显得生硬。至于随风飘动的频率,就随便找一个sin函数的组合就可以了。

具体代码和相关的思考

顶点着色器就按照之前介绍的来做就可以了。由于较好看的植被都是双面渲染的,在Cull的参数里面选择Off。这样同样的会遇到一个问题,就是模型背面的法线和正面的法线是相同的,这里需要使用HLSL片元着色器的VFACE语义,来判断当前面是正面还是背面,如果是背面的话需要反转一下法线。这里写的Shader也同时写了阴影、深度图和烘焙所需要的pass。

VertexAnimationPlantShader.shader

Shader "zznewclear13/VertexAnimationPlantShader"
{
    Properties
    {
        _BaseColor ("Base Color", color) = (1, 1, 1, 1)
        _BaseMap("Base Map", 2D) = "white" {}

        _BumpMap ("Bump Map", 2D) = "bump" {}
        _BumpIntensity ("Bump Intensity", range(0, 1)) = 1
        _RoughnessMap("Roughness Map", 2D) = "white" {}
        _RoughnessIntensity ("Roughness Intensity", range(0, 1)) = 1
        _MetallicMap ("Metallic Map", 2D) = "black" {}
        _MetallicIntensity ("Metallic Intensity", range(0, 1)) = 1

        _WindDirection ("Wind Direction", vector) = (1.0, 0.0, 0.0, 0.0)
        _WindIntensity ("Wind Intensity", float) = 1
        _WindVariety ("Wind Variety", range(0, 10)) = 0.5
        _BranchDelay ("Branch Delay", range(0, 10)) = 2

        _WindVarietyLeaves ("Wind Variety Leaves", range(0, 10)) = 1
        _BranchDelayLeaves ("Branch Delay Leaves", range(0, 10)) = 3

        _WindVaryFrequency ("Wind Vary Frequency", float) = 5
    }

    HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    
    sampler2D _BaseMap;
    sampler2D _BumpMap;
    sampler2D _RoughnessMap;
    sampler2D _MetallicMap;
    CBUFFER_START(UnityPerMaterial)
    float4 _BaseColor;
    float4 _BaseMap_ST;
    float _BumpIntensity;
    float _RoughnessIntensity;
    float _MetallicIntensity;

    float4 _WindDirection;
    float _WindIntensity;
    float _WindVariety;
    float _BranchDelay;
    float _WindVarietyLeaves;
    float _BranchDelayLeaves;
    float _WindVaryFrequency;
    CBUFFER_END

    //Apply wind variety
    float GetVariety(float timeFunction)
    {
        return sin(timeFunction) + 0.25 * sin(timeFunction * 1.5) + 0.1 * sin(timeFunction * 0.33);
    }

    //CircleTransform transforms a current bend space point to a new position in bend space,
    //and output three axes of next bend space.
    //New position is in current bend space and ready for use.
    //Normal and tangent in current bend space can be calculated by axes.
    float3 CircleTransform(float3 positionBS, float windIntensity, out float3 axisX, out float3 axisY, out float3 axisZ)
    {
        float intensity = windIntensity;

        if(intensity == 0.0 || length(positionBS.yz) == 0.0)
        {
            axisX = float3(1.0, 0.0, 0.0);
            axisY = float3(0.0, 1.0, 0.0);
            axisZ = float3(0.0, 0.0, 1.0);

            return positionBS;
        }

        float radius = rcp(intensity);
        float radVal = length(positionBS.yz) * intensity;
        float sinVal = sin(radVal);
        float cosVal = cos(radVal);
        float2 normalizeDir = normalize(positionBS.yz);

        float3 targetPosBS = float3(radius - radius * cosVal, (radius * sinVal) * normalizeDir);

        float3 tempAxisX = float3(cosVal, -sinVal * normalizeDir);
        float3 tempAxisY = float3(0.0, normalizeDir.y, -normalizeDir.x);
        float3 tempAxisZ = float3(sinVal, cosVal * normalizeDir);

        axisX = tempAxisX;
        axisY = normalizeDir.y * tempAxisY + normalizeDir.x * tempAxisZ;
        axisZ = -normalizeDir.x * tempAxisY + normalizeDir.y * tempAxisZ;

        float3 newPositionBS = targetPosBS + axisX * positionBS.x;
        return newPositionBS;
    }

    //windDirection: object space, upVec: world upVec in object space
    void InitBendSpace(float3 windDirection, float3 upVec, out float3x3 objectToBend, out float3x3 bendToObject)
    {
        float3 u = windDirection;
        float3 v = normalize(cross(upVec, u));
        float3 w = cross(u, v);
        //Object space to bend space       
        objectToBend = float3x3(u, v, w);
        //Bend space to object space
        bendToObject =  float3x3(u.x, v.x, w.x, u.y, v.y, w.y, u.z, v.z, w.z);
    }

    ENDHLSL

    SubShader
    {
        Tags{ "RenderType" = "Transparent" "Queue" = "Transparent"}

        Pass
        {
            Name "ForwardLit"
            Tags{"LightMode" = "UniversalForward"}

            Cull Off
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite On

            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            #pragma shader_feature_local _NORMALMAP

            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
            #pragma multi_compile _ SHADOWS_SHADOWMASK       
            #pragma multi_compile_fragment _ _SHADOWS_SOFT

            #pragma multi_compile _ LIGHTMAP_ON

            #pragma vertex LitPassVert
            #pragma fragment LitPassFrag

            struct Attributes
            {
                float4 positionOS           : POSITION;
                float3 normalOS             : NORMAL;
                float4 tangentOS            : TANGENT;
                float2 texcoord0            : TEXCOORD0;
                float2 texcoord1            : TEXCOORD1;
                float2 texcoord2            : TEXCOORD2;
                float2 staticLightmapUV     : TEXCOORD3;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionCS               : SV_POSITION;
                float2 uv                       : TEXCOORD0;
                float3 positionWS               : TEXCOORD1;
                float3 normalWS                 : TEXCOORD2;
                float4 tangentWS                : TEXCOORD3;
                float4 shadowCoord              : TEXCOORD4;
                DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 5);

                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            //////////////////////////////////
            //GGX BRDF and related functions//
            //////////////////////////////////
            float D(float ndoth, float roughness)
            {
                float a = ndoth * roughness;
                float k = roughness / (1.0 - ndoth * ndoth + a * a);
                return k * k;
            }

            float G(float ndotl, float ndotv, float roughness)
            {
                float a2 = roughness * roughness;
                float gv = ndotv * sqrt((1.0 - a2) * ndotl * ndotl + a2);
                float gl = ndotl * sqrt((1.0 - a2) * ndotv * ndotv + a2);
                return 0.5 * rcp(gv + gl);
            }

            float3 F(float3 specular, float hdotl)
            {
                return specular + (1 - specular) * pow(1 - hdotl, 5);
            }

            float3 GGXBRDF(float3 wi, float3 wo, float3 normal, float3 specular, float roughness)
            {
                float3 h = normalize(wi + wo);
                float ndotv = max(dot(normal, wo), 1e-5);
                float ndoth = max(dot(normal, h), 0.0);
                float ndotl = max(dot(normal, wi), 0.0);
                float hdotl = max(dot(h, wi), 0.0);

                float d = D(ndoth, roughness);
                float g = G(ndotl, ndotv, roughness);
                float3 f = F(specular, hdotl);

                return d * g * f;
            }

            Varyings LitPassVert(Attributes input)
            {
                Varyings output = (Varyings)0;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_TRANSFER_INSTANCE_ID(input, output);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                //Pivot positions are stored in TEXCOORD1.xy and TEXCOORD2.x.
                float3 pivotPosition = float3(input.texcoord1.xy, input.texcoord2.x) * 0.01;
                float3 pointOffset = input.positionOS.xyz - pivotPosition;

                //Initialize Bend Space.
                float3 windDirectionOS = mul((float3x3)UNITY_MATRIX_I_M, _WindDirection.xyz);
                float3 upVec = mul((float3x3)UNITY_MATRIX_I_M, float3(0.0, 1.0, 0.0));
                windDirectionOS = normalize(windDirectionOS);
                upVec = normalize(upVec);
                float3x3 objectToBend, bendToObject;
                InitBendSpace(windDirectionOS, upVec, objectToBend, bendToObject);

                //Initialize vertex data, transform from object space to bend space.
                float3 pivotPositionBS = mul(objectToBend, pivotPosition);
                float3 pointOffsetBS = mul(objectToBend, pointOffset);
                float3 originalTangentBS = mul(objectToBend, input.tangentOS.xyz);
                float3 originalNormalBS = mul(objectToBend, input.normalOS);
                
                float3 windPointBS;
                float3 windTangentBS;
                float3 windNormalBS;

                //TEXCOORD2.y is used to check pivot layers. 0 is root layer.
                if(input.texcoord2.y > 0.5)
                {
                    //////////////////////////////////
                    //Calculate pivot root transform//
                    //////////////////////////////////

                    //Get pivot wind intensity.
                    float intensity = _WindIntensity;
                    float magnitude = length(pivotPositionBS);
                    intensity += _WindVariety * GetVariety(_Time.y * _WindVaryFrequency - magnitude * _BranchDelay);
                    
                    //Calculate new position bent by wind in bend space,
                    //and save the transform matrix.
                    float3 axisX, axisY, axisZ;
                    float3 windPivotPositionBS = CircleTransform(pivotPositionBS, intensity, axisX, axisY, axisZ);
                    float4x4 pivotBSToObjBS = float4x4(float4(axisX.x, axisY.x, axisZ.x, windPivotPositionBS.x), float4(axisX.y, axisY.y, axisZ.y, windPivotPositionBS.y), float4(axisX.z, axisY.z, axisZ.z, windPivotPositionBS.z), float4(0.0, 0.0, 0.0, 1.0));

                    /////////////////////////////
                    //Calculate point transform//
                    /////////////////////////////

                    //Switch axes, transform to next bend space (point bend space).
                    pointOffsetBS = float3(pointOffsetBS.z, pointOffsetBS.y, -pointOffsetBS.x);

                    //Get point wind intensity.
                    intensity = abs(axisZ.x) * _WindIntensity;//_WindIntensityLeaves;
                    magnitude = length(pointOffsetBS);
                    intensity += _WindVarietyLeaves * GetVariety(_Time.y * _WindVaryFrequency - magnitude * _BranchDelayLeaves);
                    
                    //Calculate new position bent by wind in bend space,
                    //and save the transform matrix from point bend space to pivot bend space.
                    //This transform matrix can be used to calculate normal and tangent.
                    float3 windPointPositionBS = CircleTransform(pointOffsetBS, intensity, axisX, axisY, axisZ);
                    float4x4 pointBSToPivotBS = float4x4(float4(axisX.x, axisY.x, axisZ.x, windPointPositionBS.x), float4(axisX.y, axisY.y, axisZ.y, windPointPositionBS.y), float4(axisX.z, axisY.z, axisZ.z, windPointPositionBS.z), float4(0.0, 0.0, 0.0, 1.0));
                    
                    //Switch axes, transform to pivot bend space.
                    windPointPositionBS = float3(-windPointPositionBS.z, windPointPositionBS.y, windPointPositionBS.x);

                    //Calculate position, normal and tangent in pivot bend space.
                    float3 windPointPS = windPointPositionBS;
                    float3 windTangentPS = mul((float3x3)pointBSToPivotBS, originalTangentBS);
                    float3 windNormalPS = mul((float3x3)pointBSToPivotBS, originalNormalBS);

                    //Calculate position, normal and tangent in object bend space.
                    windPointBS = mul(pivotBSToObjBS, float4(windPointPS, 1.0)).xyz;
                    windTangentBS = mul((float3x3)pivotBSToObjBS, windTangentPS);
                    windNormalBS = mul((float3x3)pivotBSToObjBS, windNormalPS);

                }
                else
                {
                    //////////////////////////////////
                    //Calculate point transform only//
                    //////////////////////////////////

                    //Get point wind intensity.
                    float intensity = _WindIntensity;
                    float magnitude = length(pointOffsetBS);
                    intensity += _WindVariety * GetVariety(_Time.y * _WindVaryFrequency - magnitude * _BranchDelay);
                    
                    float3 axisX, axisY, axisZ;
                    float3 windPointOffsetBS = CircleTransform(pointOffsetBS, intensity, axisX, axisY, axisZ);
                    float4x4 pointBSToObjBS = float4x4(float4(axisX.x, axisY.x, axisZ.x, windPointOffsetBS.x), float4(axisX.y, axisY.y, axisZ.y, windPointOffsetBS.y), float4(axisX.z, axisY.z, axisZ.z, windPointOffsetBS.z), float4(0.0, 0.0, 0.0, 1.0));

                    windPointBS = windPointOffsetBS + pivotPositionBS;
                    windTangentBS = mul((float3x3)pointBSToObjBS, originalTangentBS);
                    windNormalBS = mul((float3x3)pointBSToObjBS, originalNormalBS);
                }

                //Transform from bend space to object space
                float3 pivotPositionOS = mul(bendToObject, windPointBS);
                float3 pivotTangentOS = mul(bendToObject, windTangentBS);
                float3 pivotNormalOS = mul(bendToObject, windNormalBS);

                VertexPositionInputs vertexInput = GetVertexPositionInputs(pivotPositionOS);
                VertexNormalInputs normalInput = GetVertexNormalInputs(pivotNormalOS, float4(pivotTangentOS, input.tangentOS.w));

                output.positionCS = vertexInput.positionCS;
                output.uv = TRANSFORM_TEX(input.texcoord0, _BaseMap);
                output.positionWS = vertexInput.positionWS;
                output.normalWS = normalInput.normalWS;
                output.tangentWS = float4(normalInput.tangentWS, input.tangentOS.w);
                output.shadowCoord = TransformWorldToShadowCoord(vertexInput.positionWS);
                
                OUTPUT_LIGHTMAP_UV(input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV);
                OUTPUT_SH(normalInput.normalWS.xyz, output.vertexSH);

                output.normalWS = normalInput.normalWS;
                
                return output;
            }

            float4 LitPassFrag(Varyings input, float vFace : VFACE) : SV_TARGET
            {
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                //wo
                float3 positionWS = input.positionWS;
                float3 viewDirWS = GetWorldSpaceNormalizeViewDir(positionWS);
                
                //wi
                float4 shadowCoord = TransformWorldToShadowCoord(positionWS);
                float4 shadowMask = SAMPLE_SHADOWMASK(input.staticLightmapUV);
                Light mainLight = GetMainLight(shadowCoord, positionWS, shadowMask);

                //normal
                float3 normalMap = UnpackNormal(tex2D(_BumpMap, input.uv));
                normalMap.xy *= _BumpIntensity;
                float3 bitangentWS = cross(input.normalWS, input.tangentWS.xyz) * input.tangentWS.w;
                float3x3 tbn = float3x3(input.tangentWS.xyz, bitangentWS, input.normalWS);
                float3 normalWS = mul(normalMap, tbn);
                normalWS = normalize(input.normalWS);
                //If we are looking and back faces, revert the normal.
                normalWS = vFace > 0.5 ? normalWS: -normalWS;

                //material properties
                float4 baseMap = tex2D(_BaseMap, input.uv) * _BaseColor;
                clip(baseMap.a - 0.5);
                float roughnessMap = tex2D(_RoughnessMap, input.uv).r;
                float roughness = max(roughnessMap * _RoughnessIntensity, 1e-2);
                float metallicMap = tex2D(_MetallicMap, input.uv).r;
                float metallic = metallicMap * _MetallicIntensity;

                float oneMinusReflectivity = kDieletricSpec.a * (1 - metallic);
                float reflectivity = 1.0 - oneMinusReflectivity;
                float3 diffuse = baseMap.rgb * oneMinusReflectivity;
                float3 specular = lerp(kDieletricSpec.rgb, baseMap.rgb, metallic);     
                
                //gi
                float3 bakedGI = SAMPLE_GI(input.staticLightmapUV, input.vertexSH, normalWS);
                MixRealtimeAndBakedGI(mainLight, normalWS, bakedGI);
                float3 giDiffuse = bakedGI;
                float3 reflectVector = reflect(-viewDirWS, normalWS);
                float3 giSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, roughness, 1.0);

                //directional lights
                float3 directDiffuse = diffuse;
                float3 directSpecular = GGXBRDF(mainLight.direction, viewDirWS, normalWS, specular, roughness);
                float ndotl = saturate(dot(mainLight.direction, normalWS));
                float atten = mainLight.shadowAttenuation;

                //indirectional lights
                float3 indirectDiffse = giDiffuse * diffuse;
                float surfaceReduction = rcp(roughness * roughness + 1.0);
                float grazingTerm = saturate(1.0 - roughness + reflectivity);
                float ndotv = saturate(dot(normalWS, viewDirWS));
                float fresnelTerm = pow(1.0 - ndotv, 5.0);
                float3 indirectSpecular = giSpecular * surfaceReduction * lerp(specular, grazingTerm, fresnelTerm);

                //final compose
                float3 directBRDF = (directDiffuse + directSpecular) * mainLight.color * atten * ndotl;
                float3 indirectBRDF = indirectDiffse + indirectSpecular;

                float3 finalColor = directBRDF + indirectBRDF;
                return float4(finalColor, baseMap.a);
            }

            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment

            float3 _LightDirection;

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
                float4 tangentOS    : TANGENT;
                float2 texcoord0    : TEXCOORD0;
                float2 texcoord1    : TEXCOORD1;
                float2 texcoord2    : TEXCOORD2;
                float4 color        : COLOR;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float2 uv           : TEXCOORD0;
                float4 positionCS   : SV_POSITION;
            };

            Varyings ShadowPassVertex(Attributes input)
            {
                Varyings output = (Varyings)0;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_TRANSFER_INSTANCE_ID(input, output);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                float3 pivotPosition = float3(input.texcoord1.xy, input.texcoord2.x) * 0.01;
                float3 pointOffset = input.positionOS.xyz - pivotPosition;

                float3 windDirectionOS = mul((float3x3)UNITY_MATRIX_I_M, _WindDirection.xyz);
                float3 upVec = mul((float3x3)UNITY_MATRIX_I_M, float3(0.0, 1.0, 0.0));
                windDirectionOS = normalize(windDirectionOS);
                upVec = normalize(upVec);
                float3x3 objectToBend, bendToObject;
                InitBendSpace(windDirectionOS, upVec, objectToBend, bendToObject);

                float3 pivotPositionBS = mul(objectToBend, pivotPosition);
                float3 pointOffsetBS = mul(objectToBend, pointOffset);
                float3 originalTangentBS = mul(objectToBend, input.tangentOS.xyz);
                float3 originalNormalBS = mul(objectToBend, input.normalOS);
                
                float3 windPointBS;
                float3 windTangentBS;
                float3 windNormalBS;

                if(input.texcoord2.y > 0.5)
                {
                    float intensity = _WindIntensity;
                    float magnitude = length(pivotPositionBS);
                    intensity += _WindVariety * GetVariety(_Time.y * _WindVaryFrequency - magnitude * _BranchDelay);
                    
                    float3 axisX, axisY, axisZ;
                    float3 windPivotPositionBS = CircleTransform(pivotPositionBS, intensity, axisX, axisY, axisZ);
                    float4x4 pivotBSToObjBS = float4x4(float4(axisX.x, axisY.x, axisZ.x, windPivotPositionBS.x), float4(axisX.y, axisY.y, axisZ.y, windPivotPositionBS.y), float4(axisX.z, axisY.z, axisZ.z, windPivotPositionBS.z), float4(0.0, 0.0, 0.0, 1.0));

                    pointOffsetBS = float3(pointOffsetBS.z, pointOffsetBS.y, -pointOffsetBS.x);

                    intensity = abs(axisZ.x) * _WindIntensity;//_WindIntensityLeaves;
                    magnitude = length(pointOffsetBS);
                    intensity += _WindVarietyLeaves * GetVariety(_Time.y * _WindVaryFrequency - magnitude * _BranchDelayLeaves);
                    
                    float3 windPointPositionBS = CircleTransform(pointOffsetBS, intensity, axisX, axisY, axisZ);
                    float4x4 pointBSToPivotBS = float4x4(float4(axisX.x, axisY.x, axisZ.x, windPointPositionBS.x), float4(axisX.y, axisY.y, axisZ.y, windPointPositionBS.y), float4(axisX.z, axisY.z, axisZ.z, windPointPositionBS.z), float4(0.0, 0.0, 0.0, 1.0));
                    
                    windPointPositionBS = float3(-windPointPositionBS.z, windPointPositionBS.y, windPointPositionBS.x);

                    float3 windPointPS = windPointPositionBS;
                    float3 windTangentPS = mul((float3x3)pointBSToPivotBS, originalTangentBS);
                    float3 windNormalPS = mul((float3x3)pointBSToPivotBS, originalNormalBS);

                    windPointBS = mul(pivotBSToObjBS, float4(windPointPS, 1.0)).xyz;
                    windTangentBS = mul((float3x3)pivotBSToObjBS, windTangentPS);
                    windNormalBS = mul((float3x3)pivotBSToObjBS, windNormalPS);
                }
                else
                {
                    float intensity = _WindIntensity;
                    float magnitude = length(pointOffsetBS);
                    intensity += _WindVariety * GetVariety(_Time.y * _WindVaryFrequency - magnitude * _BranchDelay);
                    
                    float3 axisX, axisY, axisZ;
                    float3 windPointOffsetBS = CircleTransform(pointOffsetBS, intensity, axisX, axisY, axisZ);
                    float4x4 pointBSToObjBS = float4x4(float4(axisX.x, axisY.x, axisZ.x, windPointOffsetBS.x), float4(axisX.y, axisY.y, axisZ.y, windPointOffsetBS.y), float4(axisX.z, axisY.z, axisZ.z, windPointOffsetBS.z), float4(0.0, 0.0, 0.0, 1.0));

                    windPointBS = windPointOffsetBS + pivotPositionBS;
                    windTangentBS = mul((float3x3)pointBSToObjBS, originalTangentBS);
                    windNormalBS = mul((float3x3)pointBSToObjBS, originalNormalBS);
                }

                float3 pivotPositionOS = mul(bendToObject, windPointBS);
                float3 pivotTangentOS = mul(bendToObject, windTangentBS);
                float3 pivotNormalOS = mul(bendToObject, windNormalBS);

                VertexPositionInputs vertexInput = GetVertexPositionInputs(pivotPositionOS);
                VertexNormalInputs normalInput = GetVertexNormalInputs(pivotNormalOS, float4(pivotTangentOS, input.tangentOS.w));

                output.uv = TRANSFORM_TEX(input.texcoord0, _BaseMap);
                output.positionCS = TransformWorldToHClip(ApplyShadowBias(vertexInput.positionWS, normalInput.normalWS, _LightDirection));
                return output;
            }

            half4 ShadowPassFragment(Varyings input) : SV_TARGET
            {
                return 0.0;
            }

            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags{"LightMode" = "DepthOnly"}

            HLSLPROGRAM
            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 texcoord     : TEXCOORD0;
                float4 color        : COLOR;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float2 uv           : TEXCOORD0;
                float4 positionCS   : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings DepthOnlyVertex(Attributes input)
            {
                Varyings output = (Varyings)0;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS);
                
                output.uv = input.texcoord;
                output.positionCS = vertexInput.positionCS;
                
                return output;
            }

            half4 DepthOnlyFragment(Varyings input) : SV_TARGET
            {
                return 0.0;
            }

            ENDHLSL
        }

        Pass
        {
            Name "Meta"
            Tags{"LightMode" = "Meta"}

            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/MetaInput.hlsl"

            #pragma vertex MetaVertex
            #pragma fragment MetaFragment

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
                float2 uv0          : TEXCOORD0;
                float2 uv1          : TEXCOORD1;
                float2 uv2          : TEXCOORD2;
            };

            struct Varyings
            {
                float4 positionCS   : SV_POSITION;
                float2 uv           : TEXCOORD0;
            };

            Varyings MetaVertex(Attributes input)
            {
                Varyings output;
                output.positionCS = MetaVertexPosition(input.positionOS, input.uv1, input.uv2, unity_LightmapST, unity_DynamicLightmapST);
                output.uv = TRANSFORM_TEX(input.uv0, _BaseMap);
                return output;
            }

            half4 MetaFragment(Varyings input) : SV_Target
            {
                //material properties
                float4 baseMap = tex2D(_BaseMap, input.uv);
                float roughnessMap = tex2D(_RoughnessMap, input.uv).r;
                float roughness = max(roughnessMap * _RoughnessIntensity, 1e-2);
                float metallicMap = tex2D(_MetallicMap, input.uv).r;
                float metallic = metallicMap * _MetallicIntensity;

                float oneMinusReflectivity = kDieletricSpec.a * (1 - metallic);
                float reflectivity = 1.0 - oneMinusReflectivity;
                float3 diffuse = baseMap.rgb * oneMinusReflectivity;
                float3 specular = lerp(kDieletricSpec.rgb, baseMap.rgb, metallic);

                MetaInput metaInput;
                metaInput.Albedo = diffuse;
                metaInput.SpecularColor = specular;
                metaInput.Emission = 0;
                return MetaFragment(metaInput);
            }

            ENDHLSL
        }
    }
}

后续的思考

首先先讲好的方面,风吹动的效果确实十分自然,同样的计算出的正确的法线在PBR的渲染中也十分重要(顶点动画中正确的法线尤其不易!)。但是不足之处是矩阵运算过多了,不过矩阵运算全都在顶点着色器中,消耗也不是特别大。太多的矩阵运算也导致了这种算法的扩展性不是很好,如果想要有Pivot Layer为3的顶点,在目前的算法里面是没办法计算的,也不知道顽皮狗是怎么做的了。在模型的形状不是特别好的时候,比如一个quad来渲染草,或者是模型叶子歪歪扭扭的,会有比较大的变形。不过仔细设置每一个顶点的Pivot的话,应该还是能够解决一部分问题的,当然拙劣的Maya脚本又成了一个痛点。

总的来说我还是比较满意的,也算是解决了一个比较复杂的问题,当然了,我对hugo能够支持gif图片更加满意!