Search code examples
c#unity-game-enginepoint-clouds

How to get dynamic vertex coordinates from Unity animation?


I have a question about generating point cloud from 3D model mesh in animation.

I try it following the steps below.

  1. I get free assets. (assets url)
  2. I set up an animation that the 3D model walking with "FreeVoxelGril-walk" from "Inspector > Select Motion".
  3. I have the 3D model walk using a C# script. This script has functions that count vertexes on the mesh and get vertex coordinates. In addition, it exports these data to a .csv file.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerControl : MonoBehaviour
{
    // 移動スピード
    public float speed;

    // 前進or後退
    bool FronFlag = true;

    // 保存ファイル名
    public string filename = "sample";

    // 原点にあるオブジェクトの頂点を数えるか?
    bool OriginSaveFlag = false;

    // 時間計測のための変数
    // private float time; //時間計測のための変数

    // Start is called before the first frame update
    void Start() {
        if (OriginSaveFlag) {
            // 頂点の数を数える
            SkinnedMeshRenderer skin = GetComponentInChildren<SkinnedMeshRenderer>();
            Mesh mesh = skin.sharedMesh;
            int vtx_num = 0;
            for (int i = 0; i < mesh.vertices.Length; i++) {
                vtx_num++;
                Vector3 item = mesh.vertices[i];
                Debug.Log("No." + vtx_num + ", " + "Vertex Coordinates: " + item.ToString("F6"));
            }
            Debug.Log("Number of vertexes: " + vtx_num);
            Vector3[] vtx_posi_array = new Vector3[vtx_num];
        
            // 頂点座標を取得
            int count = 0;
            for(int i = 0; i < mesh.vertices.Length; i++) {
                float x = mesh.vertices[i].x;
                float y = mesh.vertices[i].y;
                float z = mesh.vertices[i].z;
                vtx_posi_array[count] = new Vector3(x, y, z);
                count++;
            }

            // csvファイルに書き込む
            try {
                filename = filename + ".csv";
                bool append = false;
                Debug.Log("vtx_posi_array.Length: " + vtx_posi_array.Length);
                using(var sw = new System.IO.StreamWriter(@filename, append)) {
                    for(int i = 0; i < vtx_posi_array.Length; ++i) {
                        sw.WriteLine("{0},{1},{2}", vtx_posi_array[i].x, vtx_posi_array[i].y, vtx_posi_array[i].z);
                    }
                }
            }
            catch(System.Exception e) {
                Debug.Log(e.Message);
            }
        }
    }

    // Update is called once per frame
    void Update() {
        // プレイヤの位置情報
        Transform PlayerTransform = this.transform;
        
        // 座標を取得
        Vector3 pos = PlayerTransform.position;
        // Debug.Log("座標: " + pos);

        if (FronFlag) {
            // 前に移動する
            transform.position += transform.forward * speed * Time.deltaTime;
            if (pos.z <= -10.0){
                FronFlag = false;
            }
        } else {
            // 後ろに移動する
            transform.position -= transform.forward * speed * Time.deltaTime;
                if (pos.z >= 0){
                FronFlag = true;
            }
        }

        SkinnedMeshRenderer skin = GetComponentInChildren<SkinnedMeshRenderer>();
        Mesh mesh = skin.sharedMesh;
        // Debug.Log("法線: " + mesh.uv.Length);
        // Debug.Log("三角形: " + mesh.triangles.Length);

        if (Input.GetKeyDown(KeyCode.S)) { // Game画面にて、キーボードを入力
            Debug.Log("Since you put the S key, vertex coordinates are saved.");

            // 頂点の数を数える
            int vtx_num = 0;
            for (int i = 0; i < mesh.vertices.Length; i++) {
                vtx_num++;
                Vector3 item = mesh.vertices[i];
                Debug.Log("No." + vtx_num + ", " + "Vertex Coordinates: " + item.ToString("F6"));
            }
            Debug.Log("Number of vertexes: " + vtx_num);
            Vector3[] vtx_posi_array = new Vector3[vtx_num];
        
            // 頂点座標を取得
            int count = 0;
            for(int i = 0; i < mesh.vertices.Length; i++) {
                float x = mesh.vertices[i].x;
                float y = mesh.vertices[i].y;
                float z = mesh.vertices[i].z;
                vtx_posi_array[count] = new Vector3(x, y, z);
                count++;
            }

            // csvファイルに書き込む
            try {
                filename = filename + ".csv";
                bool append = false;
                Debug.Log("vtx_posi_array.Length: " + vtx_posi_array.Length);
                using(var sw = new System.IO.StreamWriter(@filename, append)) {
                    for(int i = 0; i < vtx_posi_array.Length; ++i) {
                        sw.WriteLine("{0},{1},{2}", vtx_posi_array[i].x, vtx_posi_array[i].y, vtx_posi_array[i].z);
                    }
                }
            }
            catch(System.Exception e) {
                Debug.Log(e.Message);
            }
        }
    }
}

  1. I convert the .csv file to a .pcd file.

The method can generate static point cloud. In other words, the point cloud does not reflect a walking posture, e.g., movement of hands and feet, head.

In Step 4, I found a problem in that vertex coordinates had not changed when the 3D model is moving using Unity animation.

What should I do to solve the problem?


Solution

  • That is exactly the purpose of a SkinnedMeshRenderer, that you can animate its bones without actually changing its underlying mesh data.

    You could use BakeMesh to create

    a snapshot of SkinnedMeshRenderer and stores it in mesh.

    The vertices are relative to the SkinnedMeshRenderer Transform component.

    like e.g. (simplyfied a lot)

    var targetMesh = new Mesh();
    skin.BakeMesh(targetMesh);
    

    be aware though that of course this is not the most performant method depending on the complexity of your mesh.


    Regarding performance you have some other improvement potentials here like e.g.

    • use private Mesh targetMesh = new Mesh(); so always the same one is re-used
    • cash the skin into a class field only once

    also note that this

     int vtx_num = 0;
     for (int i = 0; i < mesh.vertices.Length; i++) 
     {
         vtx_num++;
         Vector3 item = mesh.vertices[i];
         Debug.Log("No." + vtx_num + ", " + "Vertex Coordinates: " + item.ToString("F6"));
     }
     Debug.Log("Number of vertexes: " + vtx_num);
     Vector3[] vtx_posi_array = new Vector3[vtx_num];
        
     // 頂点座標を取得
     int count = 0;
     for(int i = 0; i < mesh.vertices.Length; i++) {
         float x = mesh.vertices[i].x;
         float y = mesh.vertices[i].y;
         float z = mesh.vertices[i].z;
         vtx_posi_array[count] = new Vector3(x, y, z);
         count++;
     }
    

    can/should be simplified to

    var vtx_posi_array = mesh.vertices;
    Debug.Log("Number of vertexes: " + vtx_posi_array.Length);
    
    for(var i = 0; i < vtx_posi_array.Length; i++)
    {
        var item = vtx_posi_array[i];
        Debug.Log($"No. {i}, Vertex Coordinates: {item.ToString("F6")}");
    }
    

    Reasons:

    • Vector3 is a struct and anyway always a copy by value, it is only more overhead to first store each component in a float and then construct a Vector3 again
    • In both loops i already is your index. You have redundant vtx_num and count variables
    • mesh.vertices is a property that every time returns a new copy of an Vector3[] -> access it only once, then directly work on the returned array since it is just a copy