GPU скиннинг для WebGL

Раздел, посвящённый самому важному - скорости.

GPU скиннинг для WebGL

Сообщение IDoNotExist 18 авг 2016, 15:44

Кто-нибудь в курсе с чем именно связано отсутствие данной опции для WebGL платформы? И появится ли она вообще?
Данная проблема особенно критична для и без того многострадального WebGL, так как даже небольшое количество заскиненных лоу поли моделей сжирает около 50% CPU.

Есть ли варианты сделать обсчитывание скиннинга, в шейдере? Насколько я понял отсюда, основная проблема в том, что в вертексном шейдере нет доступа к весам и индексам костей.
Аватара пользователя
IDoNotExist
Адепт
 
Сообщения: 1432
Зарегистрирован: 23 мар 2011, 09:18
Skype: iamnoexist

Re: GPU скиннинг для WebGL

Сообщение IDoNotExist 22 авг 2016, 19:06

В общем, для того чтобы впихнуть расчет скиннинга в шейдер, я впихнул индексы костей в Mesh.colors, а веса костей в Mesh.uv3 и Mesh.uv4.
Синтаксис:
Используется csharp
public static Mesh BoneWeightsToColorAndTextcoord(Mesh o)
    {
        Mesh r = Instantiate(o);
               
        List<Color> bColors = new List<Color>();
        List<Vector2> wUV3 = new List<Vector2>();
        List<Vector2> wUV4 = new List<Vector2>();
               
        foreach (BoneWeight bw in o.boneWeights)
        {
            bColors.Add(new Color(bw.boneIndex0 / 255.0f, bw.boneIndex1 / 255.0f, bw.boneIndex2 / 255.0f, bw.weight0));            
            wUV3.Add(new Vector2(bw.weight0, bw.weight1));
            wUV4.Add(new Vector2(bw.weight2, bw.weight3));
        }

        r.colors = bColors.ToArray();
        r.uv3 = wUV3.ToArray();
        r.uv4 = wUV4.ToArray();
       
        return r;
    }
 


Далее при инициализации скрипта я передаю в шейдер Mesh.bindposes и уже в апдейте передаю матрицы костей.
Синтаксис:
Используется csharp
    public static void SetMatricesToMaterial(Material mat, Matrix4x4[] matrices, string property)
    {
        int i = 0;
        foreach (Matrix4x4 m in matrices)
        {
            mat.SetMatrix(string.Concat(property, i), m);
            i++;
        }
    }

    public static void SetBonesToMaterial(Material mat, Transform[] bones, string property)
    {
        int i = 0;
        foreach (Transform t in bones)
        {
            mat.SetMatrix(string.Concat(property, i), t.localToWorldMatrix);
            i++;
        }
    }
 


Методом гугления алгоритмов скиннинга и методом тыка получился следующий шейдер.
Синтаксис:
Используется csharp
Shader "Skin Vertex Shader" {
   SubShader {
      Pass {                      
          CGPROGRAM
          #pragma vertex vert
          #pragma fragment frag
          #include "UnityCG.cginc"
                                                 
           struct v2f {
                fixed4 color : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

           struct input {
                float2 tex2 : TEXCOORD2;
                float2 tex3 : TEXCOORD3;
                float4 pos : POSITION;
                float4 col : COLOR;
            };

            float4x4 _BoneMatrices[255];
            float4x4 _Bindposes[255];

            v2f vert (input i)
            {
                v2f o;

                int boneIndex = (int)(i.col.r * 255.0f);

                float4x4 bone = _BoneMatrices[boneIndex];
                float4x4 bindpose = _Bindposes[boneIndex];
                float4 bindPos = mul (bindpose, i.pos);                                                        

                float4 resultPos = mul (bone, bindPos);

                o.pos = mul (UNITY_MATRIX_VP, resultPos);                                                        
                o.color = i.col;
                return o;
            }


            fixed4 frag (v2f i) : SV_Target
             {
                        return i.color;
             }
             ENDCG
        }              
    }
    Fallback "Diffuse"      
}
 


И в общем то все работает, но шейдер считает для каждой вершины одну кость (то есть то же самое что поставить SkinnedMeshRenderer.Quality = 1 Bone).

Вопрос состоит в том, как сблендить для вершины 2 и более костей? В принципе я предполагаю что тут должен быть некий лерп по весу вершин между конечными позициями рассчитанными для каждой кости по индексу, но для лерпа допустим между двумя костями достаточно одного веса кости, куда девать второй вес в этом случае?
Аватара пользователя
IDoNotExist
Адепт
 
Сообщения: 1432
Зарегистрирован: 23 мар 2011, 09:18
Skype: iamnoexist

Re: GPU скиннинг для WebGL

Сообщение jetyb 23 авг 2016, 13:47

0. Зачем вам 2 группы матриц float4x4 _BoneMatrices и _BindPoses? Тут достаточно одной группы, да и у вас же результат умножается на их произведение.
Введите одну группу _BindPoses1 : _BindPoses1[i] = _BoneMatrices[i] * _BindPoses[i];
1. В UV - канал можно передавать трехмерные и четырехмерные вектора через перегрузку Mesh.SetUvs(). Так удобнее.
2. По вопросу. Вводите еще канал данных boneWeight считаете позицию для всех 4 костей и лерпите результат по весу каждой кости.
Как-то так:
Синтаксис:
Используется csharp
         
               struct input {
                   float4 boneIndex : TEXCOORD2;
                   float4 boneWeight  : TEXCOORD3;
                   float4 pos : POSITION;
                };
                float4x4 _Bindposes[255];

                v2f vert (input in)
                 {
                float4 resultPos = 0;
                for(int i = 0 ; i < 4; i++)
                {
                     int boneIndex = (int)round(in.boneIndex[i]);
                     resultPos +=  in.boneWeight [i] *  mul (_Bindposes[boneIndex], in.pos);
                }

                 v2f o;
                 o.pos = mul (UNITY_MATRIX_VP, resultPos);
                return o;
                }
 

Можно еще чуток ужать входной канал данных, если хранить индекс кости (целая часть числа) и вес кости (дробная часть числа) в одном float числе.
jetyb
Адепт
 
Сообщения: 1486
Зарегистрирован: 31 окт 2011, 17:21

Re: GPU скиннинг для WebGL

Сообщение IDoNotExist 23 авг 2016, 14:50

jetyb писал(а):0. Зачем вам 2 группы матриц float4x4 _BoneMatrices и _BindPoses? Тут достаточно одной группы, да и у вас же результат умножается на их произведение.
Введите одну группу _BindPoses1 : _BindPoses1[i] = _BoneMatrices[i] * _BindPoses[i];

Либо я вас не понял, либо вы не понимаете зачем нужны bindposes ), без них не обойтись, поэтому я что то не вижу тут смысла упразднять матрицы.

jetyb писал(а):1. В UV - канал можно передавать трехмерные и четырехмерные вектора через перегрузку Mesh.SetUvs(). Так удобнее.

Вот за это спасибо, я знал что в шейдере можно получить их как float4, но не знал как передать.

jetyb писал(а):2. По вопросу. Вводите еще канал данных boneWeight считаете позицию для всех 4 костей и лерпите результат по весу каждой кости.
Как-то так:
Синтаксис:
Используется csharp
         
               struct input {
                   float4 boneIndex : TEXCOORD2;
                   float4 boneWeight  : TEXCOORD3;
                   float4 pos : POSITION;
                };
                float4x4 _Bindposes[255];

                v2f vert (input in)
                 {
                float4 resultPos = 0;
                for(int i = 0 ; i < 4; i++)
                {
                     int boneIndex = (int)round(in.boneIndex[i]);
                     resultPos +=  in.boneWeight [i] *  mul (_Bindposes[boneIndex], in.pos);
                }

                 v2f o;
                 o.pos = mul (UNITY_MATRIX_VP, resultPos);
                return o;
                }
 


Тут вы зачем то лерпите между позициями умноженными на Bindposes, что не имеет никакого смысла ) может вы все-таки не совсем понимаете чем массив bone матриц отличается от bindpose матриц? )
Аватара пользователя
IDoNotExist
Адепт
 
Сообщения: 1432
Зарегистрирован: 23 мар 2011, 09:18
Skype: iamnoexist

Re: GPU скиннинг для WebGL

Сообщение jetyb 23 авг 2016, 15:01

Еще раз, то что у вас
Синтаксис:
Используется csharp
             
                float4 bindPos = mul (bindpose, i.pos);                                                        
                float4 resultPos = mul (bone, bindPos);
 

можно записать так
Синтаксис:
Используется csharp
float4 resultPos = mul (bone, mul (bindpose, i.pos));
 

или так
Синтаксис:
Используется csharp
float4x4 mat = mul(bone, bindpose);
float4 resultPos = mul (mat , bindPos);
 

По отдельности матрицы bindpose и bone не важны, важно их произведение.
Можно предпросчитать матрицу их произведения mat и передавать в шейдер только ее.
Результат такой же, но более быстрый. У меня выше используется именно такая матрица.
jetyb
Адепт
 
Сообщения: 1486
Зарегистрирован: 31 окт 2011, 17:21

Re: GPU скиннинг для WebGL

Сообщение IDoNotExist 23 авг 2016, 15:11

jetyb писал(а):Еще раз, то что у вас
Синтаксис:
Используется csharp
             
                float4 bindPos = mul (bindpose, i.pos);                                                        
                float4 resultPos = mul (bone, bindPos);
 

можно записать так
Синтаксис:
Используется csharp
float4 resultPos = mul (bone, mul (bindpose, i.pos));
 

или так
Синтаксис:
Используется csharp
float4x4 mat = mul(bone, bindpose);
float4 resultPos = mul (mat , bindPos);
 

По отдельности матрицы bindpose и bone не важны, важно их произведение.
Можно предпросчитать матрицу их произведения mat и передавать в шейдер только ее.
Результат такой же, но более быстрый. У меня выше используется именно такая матрица.

Теперь понятно ), да, действительно, так получится намного оптимальнее, спасибо.
Аватара пользователя
IDoNotExist
Адепт
 
Сообщения: 1432
Зарегистрирован: 23 мар 2011, 09:18
Skype: iamnoexist

Re: GPU скиннинг для WebGL

Сообщение IDoNotExist 24 авг 2016, 15:52

Что получилось в итоге.

Шейдер:
Синтаксис:
Используется csharp
Shader "Skinning/SkinnedSurfaceShader" {
        Properties {
                _MainTex ("Texture", 2D) = "white" {}
                _Color ("Main Color", Color) = (1,1,1,1)
                _BumpMap ("Normalmap", 2D) = "bump" {}
                _SkinQuality ("Skin Quality", Int) = 1
        }
        SubShader {
                Tags { "RenderType" = "Opaque" }
                CGPROGRAM
                #pragma surface surf Lambert
                #pragma vertex vert

                struct Input {
                        float2 uv_MainTex;
                        float2 uv_BumpMap;
                };

                float4x4 _Bones[200];  
                int _SkinQuality;        
                 
                float4 GetBoneVertex(int boneId, float4 vertex){
                        float4x4 bone = _Bones[boneId];                  
                        return mul (bone, vertex);
                }

                float3 Lerp(float3 v1, float3 v2, float t){
                        return v1 += (v2-v1)*t;
                }

                float4 Blend2Bones(int id1, int id2, float w2, float4 vertex){
                        float4 v1 = GetBoneVertex(id1,vertex);
                        float4 v2 = GetBoneVertex(id2,vertex);
                        vertex.xyz = Lerp(v1,v2,w2);                   
                        return vertex;
                }

                float4 BlendMultipleBones(float4 ids, float4 weights, float4 vertex, int q){                   
                        float4 v0 = GetBoneVertex((int)ids[0],vertex);
                        float3 result = v0.xyz;

                        for(int i=1; i < q; i++){
                                float4 v1 = GetBoneVertex((int)ids[i],vertex);
                                result = Lerp(result,v1,weights[i]);
                                v0 = v1;                               
                        }

                        vertex.xyz = result;
                        return vertex;
                }

                float4 Blend4Bones(int id1, int id2, int id3, int id4, float w2, float w3, float w4, float4 vertex){
                        float4 v1 = GetBoneVertex(id1,vertex);
                        float4 v2 = GetBoneVertex(id2,vertex);
                        float4 v3 = GetBoneVertex(id3,vertex);
                        float4 v4 = GetBoneVertex(id4,vertex);

                        float3 l1 = Lerp(v1,v2,w2);
                        float3 l2 = Lerp(l1,v3,w3);
                        vertex.xyz = Lerp(l2,v4,w4);
                        return vertex;
                }
                                 
                void vert (inout appdata_full v) {              
                        float4 vertex = v.vertex;

                        if(_SkinQuality < 2){
                                vertex = GetBoneVertex((int)(v.color.r), vertex);
                        }else if(_SkinQuality < 3){
                                vertex = Blend2Bones((int)(v.color.r),(int)(v.color.g), v.texcoord3.y, vertex);
                        }else{                         
                                vertex = BlendMultipleBones(v.color,v.texcoord3,vertex, _SkinQuality);
                        }                      

                        v.vertex = mul (_World2Object, vertex);                        
                }
               

                fixed4 _Color;
                sampler2D _MainTex;            
                sampler2D _BumpMap;
                void surf (Input IN, inout SurfaceOutput o) {
                        fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
                        o.Albedo = tex.rgb * _Color.rgb;
                        o.Gloss = tex.a;
                        o.Alpha = tex.a * _Color.a;
                                               
                        o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
                }
                ENDCG
        }
        FallBack "Diffuse"
}
 


Скрипт:
Синтаксис:
Используется csharp
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[RequireComponent(typeof(SkinnedMeshRenderer))]
public class ShaderSkinMeshRenderer : MonoBehaviour {

    private static Dictionary<int, Mesh> cachedMeshes = new Dictionary<int, Mesh>();

    private static Shader skinShader = null;
    public static readonly string shaderName = "Skinning/SkinnedSurfaceShader";
    public static readonly string bonesName = "_Bones";
       
    private Transform[] bones = new Transform[0];
    private Matrix4x4[] bindposes = new Matrix4x4[0];
    private Mesh currentMesh = null;
    private MeshRenderer _renderer = null;
    private MeshFilter _meshFilter = null;

    public Mesh CurrentMesh
    {
        get { return currentMesh; }
    }
   
    private void Awake()
    {
        try
        {
            Initialize();
        }
        catch (System.Exception e)
        {
            Debug.LogException(e,this);
            Component.Destroy(this);
        }
    }

   
    private void Initialize()
    {
        SkinnedMeshRenderer skinMesh = GetComponent<SkinnedMeshRenderer>();
        if (!skinMesh) return;

        if (skinMesh.sharedMesh == null) throw new System.Exception("SkinnedMeshRenderer not contains mesh");
        if (skinMesh.bones == null || skinMesh.bones.Length == 0) throw new System.Exception("SkinnedMeshRenderer not contains bones");        
        if (skinShader == null) skinShader = Shader.Find(shaderName);
        if (skinShader == null) throw new System.Exception("Shader \"" + shaderName + "\" not found");
               
        bones = skinMesh.bones;
        currentMesh = ProcessMeshOrGetFromCache(skinMesh.sharedMesh);
        SetBindposes(currentMesh.bindposes);                

        _meshFilter = gameObject.AddComponent<MeshFilter>();
        _meshFilter.sharedMesh = currentMesh;

        _renderer = gameObject.AddComponent<MeshRenderer>();
        _renderer.sharedMaterials = skinMesh.sharedMaterials;
        foreach (Material m in _renderer.materials)
        {
            if (m) m.shader = skinShader;
        }

        Component.Destroy(skinMesh);
    }

    private void SetBindposes(Matrix4x4[] bindposes)
    {
        if (bindposes.Length < bones.Length) throw new System.Exception("Bindposes count must be more or equal to bones count");
        this.bindposes = bindposes;
    }

    private static Mesh ProcessMeshOrGetFromCache(Mesh source)
    {
        Mesh cached = null;
        int id = source.GetInstanceID();
        cachedMeshes.TryGetValue(id, out cached);
        if (cached) return cached;

        cached = BoneWeightsToColorAndTextcoord(source);
        cachedMeshes[id] = cached;

        return cached;        
    }

    private void LateUpdate()
    {
        Matrix4x4[] matrices = CalculateBoneMatrices(bones,bindposes);
        foreach (Material m in _renderer.materials)
        {
            if (m)SetMatricesToMaterial(m, matrices, bonesName);
        }
    }

    public static Mesh BoneWeightsToColorAndTextcoord(Mesh o)
    {
        Mesh r = Instantiate(o);
               
        List<Color> bColors = new List<Color>();        
        List<Vector4> wUV4 = new List<Vector4>();
               
        foreach (BoneWeight bw in o.boneWeights)
        {
            //Debug.Log("w0:" + bw.weight0 + "; w1:" + bw.weight1 + "; w2:" + bw.weight2 + "; w3:" + bw.weight3+"\n"+
                //"i0:"+bw.boneIndex0+"; i1:"+bw.boneIndex1+"; i2:"+bw.boneIndex2+"; i3"+bw.boneIndex3);

            bColors.Add(new Color(bw.boneIndex0, bw.boneIndex1, bw.boneIndex2, bw.boneIndex3));                        
            wUV4.Add(new Vector4(bw.weight0, bw.weight1, bw.weight2, bw.weight3));
        }

        r.colors = bColors.ToArray();                
        r.SetUVs(3, wUV4);
       
        return r;
    }

    public static Matrix4x4[] CalculateBoneMatrices(Transform[] bones, Matrix4x4[] bindposes)
    {
        Matrix4x4[] result = new Matrix4x4[bones.Length];

        int i = 0;
        foreach (Transform t in bones)
        {
            result[i] = t.localToWorldMatrix * bindposes[i];
            i++;
        }
        return result;
    }

    public static void SetMatricesToMaterial(Material mat, Matrix4x4[] matrices, string property)
    {
        int i = 0;
        foreach (Matrix4x4 m in matrices)
        {
            mat.SetMatrix(string.Concat(property, i), m);
            i++;
        }
    }

}
 


И в принципе все работает, но при сравнении с оригинальным скин мешем есть погрешность хотя и не сразу заметная на глаз.
скрин1, скрин2.
Я так полагаю что это может быть в различии погрешности при расчете float на GPU.
Аватара пользователя
IDoNotExist
Адепт
 
Сообщения: 1432
Зарегистрирован: 23 мар 2011, 09:18
Skype: iamnoexist

Re: GPU скиннинг для WebGL

Сообщение jetyb 24 авг 2016, 16:12

У вас странный метод определения общей суммы.
По идее веса это положительные числа w1,w2,w3,w4 в сумме равные 1. Взвешенная сумма (a1,a2,a3,a4) это
w1 * a1 + w2 * a2 + w3 * a3 + w4 * a4
У вас же метод BlendMultipleBones возвращает совсем другой результат.
jetyb
Адепт
 
Сообщения: 1486
Зарегистрирован: 31 окт 2011, 17:21

Re: GPU скиннинг для WebGL

Сообщение IDoNotExist 24 авг 2016, 16:19

jetyb писал(а):У вас странный метод определения общей суммы.
По идее веса это положительные числа w1,w2,w3,w4 в сумме равные 1.

С этим я согласен.

jetyb писал(а): Взвешенная сумма (a1,a2,a3,a4) это
w1 * a1 + w2 * a2 + w3 * a3 + w4 * a4

И как из этого считать итоговую вершину?

jetyb писал(а):У вас же метод BlendMultipleBones возвращает совсем другой результат.

Тем не менее рабочий результат, можете сами проверить, просто существует погрешность, как я понял из дальнейшего анализа она существует только на тех вершинах, которые содержат длинную дробную часть, значит моя теория верна, где то теряется точность float, что в общем то неприятно, но не смертельно.
Аватара пользователя
IDoNotExist
Адепт
 
Сообщения: 1432
Зарегистрирован: 23 мар 2011, 09:18
Skype: iamnoexist


Вернуться в Оптимизация

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 1