青青草原——弯曲、震动、旋转


一、模型预处理处理

为什么需要对模型进行预处理?

草地渲染的主要困难点在于数量庞大,经常以万为单位,因为本人知识有限,暂时只能想到如下几点优化方式:

  1. 对草进行场景划分管理,例如四叉树或八叉树,然后进行视锥体裁剪剔除,还可以利用 Compute Shader,Jobs 等进行加速处理
  2. 将部分草进行模型合并,大约在一万面左右,做成不同形状的预制体,然后进行摆放(本次学习使用该方案
  3. 针对合并的草模型,可以使用LOD技术,优化性能

合并草模型

在 3ds Max 中对草进行模型合并,也可以使用 Unity 的其他插件(如:Mesh Baker)直接在Unity中合并

草合并

烘焙模型锚点

  • 为了能够更精细的控制每颗草的弯曲,我们可以将原本草的锚点位置坐标烘焙在模型的第四套UV通道中。
  • 思路为:因为模型是合并的,每个草之间并不是连续的,都是独立的个体,所以模型面数之间必定存在断层,不连续的地方,由此入手,进行查找区分。
  • 实现方式为从任意个面开始,向外扩散,直到出现不连续停止,被选中的面作为一个Element,然后进行该Element的顶点坐标相加去平均值。
伪代码如下
// 计算元素
var grassElements = new List<int>(vertexCount);
for (int i = 0; i < trianglesCount; i += 3)
{
    var index1 = triangles[i + 0];
    var index2 = triangles[i + 1];
    var index3 = triangles[i + 2];

    int element = 0;

    if (grassElements[index1] != -1)
    {
        element = grassElements[index1];
    }
    else if (grassElements[index2] != -1)
    {
        element = grassElements[index2];
    }
    else if (grassElements[index3] != -1)
    {
        element = grassElements[index3];
    }
    else
    {
        element = elementCount;
        elementCount++;
    }

    grassElements[index1] = element;
    grassElements[index2] = element;
    grassElements[index3] = element;
}
// 设置锚点
for (int e = 0; e < elementCount; e++)
{
    var positions = new List<Vector3>();

    for (int i = 0; i < grassElements.Count; i++)
    {
        if (grassElements[i] == e)
        {
            positions.Add(vertices[i]);
        }
    }

    float x = 0;
    float z = 0;

    for (int p = 0; p < positions.Count; p++)
    {
        x = x + positions[p].x;
        z = z + positions[p].z;
    }

    for (int i = 0; i < grassElements.Count; i++)
    {
        if (grassElements[i] == e)
        {
            anchors[i] = new Vector4(x / positions.Count,0, z / positions.Count,0);
        }
    }
    mesh.SetUVs(3, anchors);
    
}

使用顶点色

  • R通道:使用顶点色R通道控制运动的 Offset,这样可以使用顶点刷工具,来打破运动的规律性
  • G通道:使用G通道来控制弯曲程度
  • B通道:使用B通道来控制旋转程度
  • A通道:使用A通道来控制震动程度

二、XZ轴弯曲、Y轴旋转

计算绕轴旋转

假设给定草的顶点坐标为Pos,旋转的角度为Angle,旋转后的坐标为ResultPos

绕着X轴计算方式为:

half3 rotationAxis = half3(Pos.x,0,0);
half3 otherAxis = half3(0,Pos.yz);
ResultPos = rotationAxis + otherAxis * Cos(Angle) + Cross(half3(1,0,0),otherAxis) * Sin(Angle);

绕着Y轴计算方式为:

half3 rotationAxis = half3(0,Pos.y,0);
half3 otherAxis = half3(Pos.x,0,Pos.z);
ResultPos = rotationAxis + otherAxis * Cos(Angle) + Cross(half3(0,1,0),otherAxis) * Sin(Angle);

绕着Z轴计算方式为:

half3 rotationAxis = half3(0,0,Pos.z);
half3 otherAxis = half3(Pos.xy,0);
ResultPos = rotationAxis + otherAxis * Cos(Angle) + Cross(half3(0,0,1),otherAxis) * Sin(Angle);

计算旋转的角度 AngleXZ

读取UV3的锚点信息并转换到世界空间下为AnchorsPosWS

half variationValue = Fract(AnchorsPosWS.x + AnchorsPosWS.z + VertexColor.r);
half2 angleXZ = Sin(PositionWS.xz * _Scale + variationValue + _Speed * _Time );
half2 finalAngleXZ = angleXZ * _Amplitude * _MaxBendValue * VertexColor.g;
//finalAngleXZ.x 为绕着Z轴旋转的角度
//finalAngleXZ.Z 为绕着X轴旋转的角度

计算旋转的角度 AngleY

half variationValue = Fract(AnchorsPosWS.x + AnchorsPosWS.z + VertexColor.r);
half2 angleY = Sin(PositionWS.x * _Scale + variationValue + _Speed * _Time );
half2 finalAngleY = angleY * _Amplitude * _MaxRollingValue * VertexColor.b;
//finalAngleY 为绕着Y轴旋转的角度

三、震动

震动的计算方式

half variationValue = Fract(AnchorsPosWS.x + AnchorsPosWS.z + VertexColor.r) * _FlutterValue;
half positionSum = PositionWS.x + PositionWS.y + PositionWS.z;
half FlutterWave = Sin(positionSum * _Scale * 10 + variationValue + _Speed * _Time );
half3 FinalFlutter = half3(FlutterWave,0,FlutterWave);

四、最终结果

为了较好的效果,将上述结算的结果作为模型空间中的变化。 使用模型空间坐标减去模型空间的锚点Anchor(因为旋转的计算,没有加上锚点,默认是在原点进行计算的),使得还原到原点,然后应用变化量,最后加加上锚点坐标记得最终模型空间顶点坐标。


文章作者: 血魂S
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 血魂S !
  目录