Version: 1.3
语言 : 中文
绘制优化
图形渲染优化

GPU Skinning支持

目前微信小游戏平台已经能够支持多线程和SIMD,但受限于CPU性能(只有原生App的三分之一),以及额外的内存占用,使得CPU Skinning仍然可能存在较大开销。 因此GPU Skinning仍然是一个重要选项。 Unity当前的GPU Skinning都是基于Compute Shader实现的,但WebGL 1 和 2 都不支持Compute Shader。 用户为了使用GPU Skinning,需要自行修改Vertex Shader或者使用一些第三方插件,增大了适配平台的成本。 针对于此我们在团结微信小游戏平台实现了基于Transform Feedback 和 Vertex Shader 的GPU Skinning方案

设置选项位于ProjectSettings->Player->Other Settings,下拉Mesh Skinning选择即可,默认使用CPU Skinning。

CPU Skinning

顶点蒙皮运算在CPU上执行。每帧动画开始时,先按FK/IK计算出所有骨骼的平移和旋转变换矩阵。 然后读取模型在内存中的顶点数据,依据每个顶点的位置和绑定的骨骼权重计算出变换后的顶点位置。 变换后的顶点数据需要每帧重新上传到GPU,同时供Shader的多个pass(阴影,光照)使用。 CPU Skinning是WebGL平台默认使用的动画方案。

Compute Shader Skinning

顶点蒙皮运算在GPU上的计算着色器执行。原始顶点数据、骨骼权重等通过rawbuffer或者structurebuffer的方式上传到GPU, 不需要每帧重复上传。骨骼变换矩阵计算在CPU上计算时需要每帧更新到GPU。计算着色器完成顶点蒙皮运算后,将变换后的顶点数据写入到GPU buffer中。 该buffer作为渲染阶段的顶点输入,可以同时给shadow/light等多个renderpass使用。WebGL 1 和 2 都不支持Compute Shader, 因此Compute Shader Skinning无法在WebGL和微信小游戏平台使用

Transform Feedback

新增一个TF Pass,在vertex shader中根据骨骼变换矩阵计算新的顶点位置(local space),并把经过vertex shader处理后的位置输出到绑定GL_TRANSFORM_FEEDBACK_BUFFER的缓冲, 作为后续绘制的输入数据。整个流程和compute shader skinning类似,只是用vertex shader代替compute shader计算顶点位置。

使用Transform Feedback功能需要打开WebGL 2.0。 目前Transform Feedback最多支持单个动画64根骨骼,超出数量后则默认切换为CPU Skinning。 需要注意,在某些场景下(角色数量多,每个角色顶点数少),Transform Feedback可能出现负优化的情况。

Vertex Shader Skinning

相比于Transform Feedback,该方案不需要增加新的TF pass。 计算顶点位置的代码(同transform feedback skinning)位于正常绘制的vertex shader中,处理后的数据直接用于后续MVP坐标转换。 但对于多Pass的渲染,比如ShadowCaster + Normal Pass, 每个Pass都需要重新计算顶点位置。 部分引擎内置的shader已支持Vertex Shader Skinning(详见下文)。

使用方法

在Player Settings中勾选“GPU - Vertex Shader”开启此功能,并在Shader中添加keyword ENABLE_VS_SKINNING,例如

CGPROGRAM
    ...
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"

    #pragma multi_compile _ ENABLE_VS_SKINNING
    ...
    struct appdata_t {
        float4 vertex : POSITION;
        float2 texcoord : TEXCOORD0;
        ...
    };
    
    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        ...
    };
            
    v2f vert (appdata_t v)
    {
        v2f o;
        ...
        o.vertex = UnityObjectToClipPos(v.vertex);
        ...
    }
    ...
ENDCG

通过multi_compile添加keyword时,SkinnedMeshRenderer会根据Player Settings自动启用keyword。如果确定该shader只会使用VS Skinning,且不希望生成禁用此keyword的变体,可以通过shader_feature添加keyword。但此时需要添加Material Inspector,并确保keyword处于启用状态,详情可参见MaterialPropertyDrawer。示例:

Shader "Custom/MyShader"
{
    Properties
    {
        // Display a toggle in the Material's Inspector window
        [Toggle(ENABLE_VS_SKINNING)] EnableVSSkinning("Enable VS Skinnnig", Float) = 1.0
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }
        
            CGPROGRAM
            ...
            #pragma shader_feature ENABLE_VS_SKINNING
            ....
            ENDCG
        }
        ...
    }
}
在场景中引用的材质检查器中开启此选项
在场景中引用的材质检查器中开启此选项

对于启用VS Skinning的SkinnedMeshRenderer,如果其mesh数据没有在其他地方需要访问,可以禁用Read/Write,并勾选释放mesh数据选项(默认值是已勾选),以减少运行时内存占用。同一个Mesh在不同renderer中分别以VS和CPU Skinning方式被使用时,mesh数据不会被释放,类似的,在PlayerSetting中切换skinning方式时,无需再改动Read/Write或释放mesh数据的选项。

实现原理

ShaderCompiler识别到keyword后,会自动在vertex shader的函数入口注入skinning计算代码,新的顶点位置仍然在Object Space,保存在输入的POSITION通道,例如v.vertex,因此无需额外修改shader函数。

自动注入的代码可以勾选Propress Only选项查看
自动注入的代码可以勾选Propress Only选项查看

自动注入后的代码示例:

static const int max_bone_count = 64;
uniform int BonesPerVertex;
uniform float4 Bones [max_bone_count * 3];
bool GetLocalToWorldMatrix (in float4 boneWeights_in, in uint4 boneIndices_in, out float4x4 localToWorldMatrix)
{
    if (BonesPerVertex == 1)
    {
        localToWorldMatrix = float4x4 (Bones [int (boneIndices_in . x) * 3 + 0], Bones [int (boneIndices_in . x) * 3 + 1], Bones [int (boneIndices_in . x) * 3 + 2], float4 (0.0, 0.0, 0.0, 1.0));
    }
    else if (BonesPerVertex == 2)
    {
        localToWorldMatrix = float4x4 (Bones [int (boneIndices_in . x) * 3 + 0], Bones [int (boneIndices_in . x) * 3 + 1], Bones [int (boneIndices_in . x) * 3 + 2], float4 (0.0, 0.0, 0.0, 1.0)) * boneWeights_in . x;
        localToWorldMatrix += float4x4 (Bones [int (boneIndices_in . y) * 3 + 0], Bones [int (boneIndices_in . y) * 3 + 1], Bones [int (boneIndices_in . y) * 3 + 2], float4 (0.0, 0.0, 0.0, 1.0)) * boneWeights_in . y;
    }
    else if (BonesPerVertex == 4)
    {
        localToWorldMatrix = float4x4 (Bones [int (boneIndices_in . x) * 3 + 0], Bones [int (boneIndices_in . x) * 3 + 1], Bones [int (boneIndices_in . x) * 3 + 2], float4 (0.0, 0.0, 0.0, 1.0)) * boneWeights_in . x;
        localToWorldMatrix += float4x4 (Bones [int (boneIndices_in . y) * 3 + 0], Bones [int (boneIndices_in . y) * 3 + 1], Bones [int (boneIndices_in . y) * 3 + 2], float4 (0.0, 0.0, 0.0, 1.0)) * boneWeights_in . y;
        localToWorldMatrix += float4x4 (Bones [int (boneIndices_in . z) * 3 + 0], Bones [int (boneIndices_in . z) * 3 + 1], Bones [int (boneIndices_in . z) * 3 + 2], float4 (0.0, 0.0, 0.0, 1.0)) * boneWeights_in . z;
        localToWorldMatrix += float4x4 (Bones [int (boneIndices_in . w) * 3 + 0], Bones [int (boneIndices_in . w) * 3 + 1], Bones [int (boneIndices_in . w) * 3 + 2], float4 (0.0, 0.0, 0.0, 1.0)) * boneWeights_in . w;
    }
    else
    {
        localToWorldMatrix = float4x4 (float4 (1.0, 0.0, 0.0, 0.0),
        float4 (0.0, 1.0, 0.0, 0.0),
        float4 (0.0, 0.0, 1.0, 0.0),
        float4 (0.0, 0.0, 0.0, 1.0));
        return false;
    }
    return true;
}

void VertexShaderSkinning_float (in float3 vertex_in, in float4 boneWeights_in, in uint4 boneIndices_in, out float3 vertex_out)
{
    float4 inPos = float4 (vertex_in, 1.0);
    float4x4 localToWorldMatrix;
    if (GetLocalToWorldMatrix (boneWeights_in, boneIndices_in, localToWorldMatrix))
    {
        vertex_out = mul (localToWorldMatrix, inPos) . xyz;
    }
}

void vs_skinning (inout float3 vertex, in float4 boneWeights, in float4 boneIndices)
{
    VertexShaderSkinning_float (vertex, boneWeights, uint4 ((uint) boneIndices . x, (uint) boneIndices . y, (uint) boneIndices . z, (uint) boneIndices . w), vertex);
}

v2f vert (appdata_t v, float4 boneIndices : BLENDINDICES, float4 boneWeights : BLENDWEIGHTS)
{
    vs_skinning(v.vertex, boneWeights, boneIndices);
    
    v2f o;
    o = (v2f) 0;
    o.vertex = UnityObjectToClipPos (v.vertex);
    ...
}

已经支持的内置Shader

Built-in RP:

  1. Mobile/Bumped Diffuse
  2. Mobile/Bumped Specular (1 Directional Realtime Light)
  3. Mobile/Bumped Specular
  4. Mobile/Diffuse
  5. Mobile/VertexLit (Only Directional Lights)
  6. Unlit/Transparent
  7. Unlit/Transparent Cutout
  8. Unlit/Color
  9. Unlit/Texture

URP:

  1. Universal Render PipelineA series of operations that take the contents of a Scene, and displays them on a screen. Unity lets you choose from pre-built render pipelines, or write your own. More info
    See in Glossary
    /Simple Lit
  2. Universal Render Pipeline/Unlit
  3. Universal Render Pipeline/Lit
  4. Universal Render Pipeline/ComplexLit
  5. Universal Render Pipeline/BakedLit

注意

  1. 同一个Renderer使用多个Material时,例如有多个SubMesh,所有Materials必须都添加ENABLE_VS_SKINNING Keyword才能开启VS Skinning
  2. 当PlayerSetting中选择了VS Skinning,对于没有添加keyword的shaders,会Fallback到CPU Skinning
  3. 暂不支持运行时切换Material
  4. 暂不支持运行时启用或禁用ENABLE_VS_SKINNING Keyword
  5. 暂不支持BlendShape和MotionVector
  6. 不支持使用Legacy shaderlab command的Pass,如Mobile/VertexLit中的Vertex LightMode
  7. 如果遇到动画的顶点位置错误,可尝试reimport mesh和shader资源

性能对比

场景1:21,488个面,15,172个顶点,212个骨骼,(接近一般MMO副本需求)

场景2:257,856个面,182,064个顶点,2544 个骨骼,(接近多同屏人数需求)

其他设置: Shader: URP Simple Lit, SRP batcher: on

CPU耗时
CPU耗时
CPU耗时
CPU耗时
绘制优化
图形渲染优化