生草计划

🌿 | 风吹草低低低低 - 植被动画Ⅱ:hdrp&speedtree实现

by ERIN.Z, 2022-04-21


信息储存

首首首先,SpeedTree文件直接输出至Unity是以.st格式存储的,其中关于风信息的储存在4个uv coordinate中: uvCoordinate 具体解释用途如下:

  • uv0.xy - 纹理采样坐标。也参与Palm类型动画的动画计算。
  • uv0.z - WindWeight 用于树枝动画,影响动画强度。
  • uv0.w - WindOffset 用于树枝动画,unpacked后作为风向的差异化信息。 (uv1.xy 好像没用到?)
  • uv1.zw,uv2.w - 储存有关anchor的位置信息:

    anchor anchor是与树叶和较小层级的树枝相关的参数,记录了叶片生长位置的坐标信息,与position对比可以看出,anchor的信息是不连续的。在顶点运算时,一般会先从坐标中减去anchor,进行动画运算,然后再加上anchor,相当于一个Object Space的offset定位信息。 anchor

  • uv2.xyz - 储存有关LOD的位置信息,smoothLOD中会在顶点位置和该向量中用unity_LODFade.x插值;(但目前还没看懂这个具体跟顶点位置有什么差别。debug shader目测是一样的...)
  • uv3.x - Scale 叶片动画强度,从叶片根部到叶片尖端渐变,根部最小。
  • uv3.y - PackedGrowthDir 在best质量下用于LeafTumble()函数。unpack后得到表示GrowthDir的向量。

    PackedGrowthDir

  • uv3.z - PackedRippleDir 用于LeafRipple()函数。
  • uv3.w - geometryType 区分枝干和树叶;

HDRP Speedtree Shader源码分析

HDRP中已内置了针对.st文件的shader graph,可以读取材质信息并计算定点动画。 hdrp 其中顶点动画的实现如下: hdrp SpeedTree8Wind的内容是一个hlsl书写的custom function,以下挑选了比较重要的部分进行解释:

工具性函数

包含一些对于运算、储存的优化算法。

//  UnpackNormalFromFloat
float3 UnpackNormalFromFloat(float fValue)
{
    float3 vDecodeKey = float3(16.0, 1.0, 0.0625);

    // decode into [0,1] range
    float3 vDecodedValue = frac(fValue / vDecodeKey);

    // move back into [-1,1] range & normalize
    return (vDecodedValue * 2.0 - 1.0);
}

对于精度需求不高且不要求连续性的向量,可以将其压缩到一个通道储存,需要使用时再通过unpack()还原成三个值。节省空间的同时必然会牺牲精度,这里x、y通道的精度只有16/1。 pack

//  CubicSmooth
float4 CubicSmooth(float4 vData)
{
    return vData * vData * (3.0 - 2.0 * vData);
}
//  TriangleWave
float4 TriangleWave(float4 vData)
{
    return abs((frac(vData + 0.5) * 2.0) - 1.0);
}
//  TrigApproximate
float4 TrigApproximate(float4 vData)
{
    return (CubicSmooth(TriangleWave(vData)) - 0.5) * 2.0;
}

这里我理解是一个比较cheap的类正弦波实现。 TriangleWave TrigApproximate (靠 好烂的AA..是谁写的💩) (是我)

风的运算

Shader中将不同的变形动画分解成了多个函数,这里直接从最核心的函数开始说起: 这里的输入函数对应costume function的各个输入。

float3 SpeedTreeWind(float3 vPos, float3 vNormal, float4 vTexcoord0, float4 vTexcoord1, float4 vTexcoord2, float4 vTexcoord3, int iWindQuality, bool bBillboard, bool bCrossfade)
{
    float3 vReturnPos = vPos;

使用vTexcoord3.w区分mesh是树枝还是树叶,这里的数值对应最前面定义的类型。 #define ST_GEOM_TYPE_BRANCH 0 #define ST_GEOM_TYPE_FROND 1 #define ST_GEOM_TYPE_LEAF 2 #define ST_GEOM_TYPE_FACINGLEAF 3

    // geometry type
    int geometryType = (int)(vTexcoord3.w + 0.25);
    bool leafTwo = false;
    if (geometryType > ST_GEOM_TYPE_FACINGLEAF)
    {
        geometryType -= 2;
        leafTwo = true;
    }

优化LOD的渐变效果,bCrossfade参数应该与LOD Group的选项有关。 lod

CrossFade: Perform cross-fade style blending between the current LOD and the next LOD if the distance to camera falls in the range specified by the LOD.fadeTransitionWidth of each LOD. SpeedTree: By specifying this mode, your LODGroup will perform a SpeedTree-style LOD fading scheme:For all the mesh LODs other than the last (most crude) mesh LOD, the fade factor is calculated as the percentage of the object's current screen height, compared to the whole range of the LOD. It is 1, if the camera is right at the position where the previous LOD switches out and 0, if the next LOD is just about to switch in.For the last mesh LOD and the billboard LOD, the cross-fade mode is used.

(具体的实现原理还不是很懂....)

    // smooth LOD
    if (!bCrossfade && !bBillboard)
    {
        vReturnPos = lerp(vReturnPos, vTexcoord2.xyz, unity_LODFade.x);
    }

对于Facing Leaf类型的网格,对其进行顶点运算使之朝向摄像机。

Facing Leaf: Leaf geometry that always faces the camera (e.g., leaf cards).

    // do leaf facing even when we don't have wind
    if (geometryType == ST_GEOM_TYPE_FACINGLEAF)
    {
        float3 anchor = float3(vTexcoord1.zw, vTexcoord2.w);
        float3 facingPosition = vReturnPos - anchor;

        // face camera-facing leaf to camera
        float offsetLen = length(facingPosition);
        facingPosition = float3(facingPosition.x, -facingPosition.z, facingPosition.y);
        float4x4 itmv = transpose(mul(UNITY_MATRIX_I_M, UNITY_MATRIX_I_V));
        facingPosition = mul(facingPosition.xyz, (float3x3)itmv);
        facingPosition = normalize(facingPosition) * offsetLen; // make sure the offset vector is still scaled
        vReturnPos = facingPosition + anchor;
    }

接下来就正式开始风的计算了,由于.st文件储存了大量可用的细节信息,其对于顶点的操作可以更加灵活。与上一篇我们做shader的思路相反,speedtree是先从最微观的叶片开始添加动画,然后逐级添加树枝、全树的动画信息。 首先准备计算使用的一些变量。_ST_WindVector应该是后台可以直接从WindZone读取的信息。根据风质量等级的不同和植被叶片类型的不同,会选择不同的计算方法。

    // wind
    if ((iWindQuality > 0) && (length(_ST_WindVector) > 0))
    {
        float3 rotatedWindVector = TransformWorldToObjectDir(_ST_WindVector.xyz);
        float windLength = length(rotatedWindVector);
        if (windLength < 1.0e-5)
        {
            // sanity check that wind data is available
            return vReturnPos;
        }
        rotatedWindVector /= windLength;

        float4x4 matObjectToWorld = GetObjectToWorldMatrix();
        float3 treePos = GetAbsolutePositionWS(float3(matObjectToWorld[0].w, matObjectToWorld[1].w, matObjectToWorld[2].w));

仅当fast/better/best质量下进行叶片的动画计算。leafwind是相对于物件空间原点计算的,leaves用anchor储存了叶片根部相对于物件空间远点的位置,所以运算前先减去anchor移至原点,运算后再加上anchor移回原位。 fastest

        if (!bBillboard)
        {
            // leaves
            if (geometryType > ST_GEOM_TYPE_FROND)
            {
                // remove anchor position
                float3 anchor = float3(vTexcoord1.zw, vTexcoord2.w);
                vReturnPos -= anchor;

                // leaf wind
                if ((iWindQuality == ST_WIND_QUALITY_FAST) || (iWindQuality == ST_WIND_QUALITY_BETTER) || (iWindQuality == ST_WIND_QUALITY_BEST))
                {
                    bool bBestWind = (iWindQuality == ST_WIND_QUALITY_BEST);
                    float leafWindTrigOffset = anchor.x + anchor.y;
                    vReturnPos = LeafWind(bBestWind, leafTwo, vReturnPos, vNormal, vTexcoord3.x, float3(0, 0, 0), vTexcoord3.y, vTexcoord3.z, leafWindTrigOffset, rotatedWindVector);
                }

                // move back out to anchor
                vReturnPos += anchor;
            }

fronds类型的叶片生长方式与leaves不同:leaves只有根部与branch相连,而fronds的中轴都与branch相连。

            // frond wind
            bool bPalmWind = false;
            if (iWindQuality == ST_WIND_QUALITY_PALM)
            {
                bPalmWind = true;
                if (geometryType == ST_GEOM_TYPE_FROND)
                {
                    vReturnPos = RippleFrond(vReturnPos, vNormal, vTexcoord0.x, vTexcoord0.y, vTexcoord3.x, vTexcoord3.y, vTexcoord3.z);
                }
            }

仅当better/best质量和palm模式下进行树枝分簇的动画计算。树枝动画会同时作用在branch本身和它们的子物体leaves上,与叶片的动画叠加。 better _ST_WindBranchAnchor记录了树枝旋转的轴向信息,可惜是完全不知道这个信息是由哪里储存读取的...这个shader中只在CBUFFER中声明了这个变量。

            // branch wind (applies to all 3D geometry)
            if ((iWindQuality == ST_WIND_QUALITY_BETTER) || (iWindQuality == ST_WIND_QUALITY_BEST) || (iWindQuality == ST_WIND_QUALITY_PALM))
            {
                float3 rotatedBranchAnchor = TransformWorldToObjectDir(_ST_WindBranchAnchor.xyz) * _ST_WindBranchAnchor.w;
                vReturnPos = BranchWind(bPalmWind, vReturnPos, treePos, float4(vTexcoord0.zw, 0, 0), rotatedWindVector, rotatedBranchAnchor);
            }
        }

最后进行最大规模的动画叠加:

        // global wind
        float globalWindTime = _ST_WindGlobal.x;
        vReturnPos = GlobalWind(vReturnPos, treePos, true, rotatedWindVector, globalWindTime);
    }

    return vReturnPos;
}

在best质量下,叶片动画会在LeafRipple()后再进行LeafTumble()运算,用到了上文说的GrowthDir信息和不知道哪里拿到的_ST_WindLeaf1/2Tumble,_ST_WindLeaf1/2Twitch。 best

总而言之——

本来是想完全读懂之后复现一下这个shader的,读完法线它实现的内容和储存的信息实在是太多了!该老实交给DCC的工作就还是交给DCC吧。 植被的顶点动画是场景制作的基础工作,speedtree的实现方法还是挺有启发的~ 划一下重点——

  • 利用模型本身记录更多信息-存入顶点色/其他的uv;
  • 比sine更快运行的周期函数算法;
  • 如何压缩向量至浮点数;

by ERIN.Z

2024 © typecho & elise