动机和想要实现的效果
最直接的动机是看了顽皮狗在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中(也就是TEXCOORD1
和TEXCOORD2
)。于是我恶补了一下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()
因为种种限制,使用时较为复杂,如果有更好的脚本的话,也很感谢分享出来告诉我。首先是要在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就可以了。
如何计算拉伸较小的风的效果,并且计算对应的法线
首先来看这样一张图:
这张图表现了在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图片更加满意!