夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

UnityでMeshクラスを使用して3Dモデルの原点調整を行う その③ メッシュのエクスポート

本日は機能に引き続きUnity枠です。

 UnityではBlenderやMayaなどのDCCツールと異なり、あくまでゲームエンジンであるため3Dメッシュの編集等のモデリング機能はデフォルトでサポートされていません。

 そのためメッシュ等に不備がある場合はDCCツールに戻り、作業をすることがありますが、特によくあるシチュエーションとして3Dオブジェクトの原点がずれていることでゲームエンジン側で扱いにくいことがあります。

 今回はUnityでMeshクラスを用いて3Dモデルの原点を任意に変更する実装を行います。

 先日、回転といいながら実質位置・回転、スケールの適応を行いました。

redhologerbera.hatenablog.com

redhologerbera.hatenablog.com

 これによってトランスフォームの適応が完了しましたが、このままではランタイムのみの実装になっており、Editorモードに戻ると破棄されてしまいます。

 今回は処理で生成した新しいメッシュをオブジェクトとして出力していきます。

 

〇環境

・Window11PC

〇メッシュのエクスポート

今回はMeshを特定のパスにエクスポートするためimport System.IOを使用します。

StreamWriterを使用してパスにデータを書き込みます。

今回は最も基本形となるOBJ形式でエクスポートします。

処理としては以下のようになります。

    using (StreamWriter writer = new StreamWriter(_path))
        {
            // OBJファイルのヘッダー
            writer.WriteLine("# Exported by AdjustPivotPoint");
            writer.WriteLine("o " + gameObject.name);

            // 頂点を書き込む
            foreach (Vector3 vertex in mesh.vertices)
            {
                writer.WriteLine($"v {vertex.x} {vertex.y} {vertex.z}");
            }

            // 法線を書き込む
            foreach (Vector3 normal in mesh.normals)
            {
                writer.WriteLine($"vn {normal.x} {normal.y} {normal.z}");
            }

            // テクスチャ座標を書き込む(もしあれば)
            if (mesh.uv.Length > 0)
            {
                foreach (Vector2 uv in mesh.uv)
                {
                    writer.WriteLine($"vt {uv.x} {uv.y}");
                }
            }

            // フェイスを書き込む
            for (int i = 0; i < mesh.triangles.Length; i += 3)
            {
                int vertexIndex1 = mesh.triangles[i] + 1; // OBJは1-indexed
                int vertexIndex2 = mesh.triangles[i + 1] + 1;
                int vertexIndex3 = mesh.triangles[i + 2] + 1;

                writer.WriteLine($"f {vertexIndex1} {vertexIndex2} {vertexIndex3}");
            }
        }

処理内容としてはOBJのフォーマットに合わせて各データを書き込んでいる形になります。

 これを実行することで任意のパスにobj形式のデータとしてエクスポートすることができます。

これによって原点調整を行ったモデルを多用途で使用できるようになりました。

〇コード全文

using System.IO;
using JetBrains.Annotations;
using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
public class AdjustPivotPoint : MonoBehaviour
{
    [SerializeField] [NotNull] private Transform pivot; // ピボットを設定するTransform
    [SerializeField] private bool adjustPosition = true; // 位置を調整するか
    [SerializeField] private bool adjustRotation = true; // 回転を調整するか
    [SerializeField] private bool adjustScale = true; // スケールを調整するか
    [SerializeField] private string _path; // エクスポート先のパス

    private void Start()
    {
        if (pivot == null)
        {
            Debug.LogWarning("Pivot transform is not assigned.");
            return;
        }

        AdjustMeshPivot();
    }

    private void AdjustMeshPivot()
    {
        // 元のメッシュを取得
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        Mesh originalMesh = meshFilter.sharedMesh;

        if (originalMesh == null)
        {
            Debug.LogWarning("No mesh found on this object.");
            return;
        }

        // 新しいメッシュを作成し、元のメッシュの情報をコピー
        Mesh newMesh = Instantiate(originalMesh);

        // ピボットのワールド座標を取得
        Vector3 pivotWorldPosition = pivot.position;

        // 頂点を取得して調整
        Vector3[] vertices = newMesh.vertices;
        for (int i = 0; i < vertices.Length; i++)
        {
            // 元の頂点をワールド座標に変換
            Vector3 vertexWorldPosition = transform.TransformPoint(vertices[i]);

            // ピボットの位置を考慮して頂点の位置を調整
            vertexWorldPosition -= pivotWorldPosition; // ピボットを新しい原点とする

            // ワールド座標での位置を新しいメッシュに適用
            vertices[i] = vertexWorldPosition;
        }

        // 調整した頂点を新しいメッシュに適用し、境界を再計算
        newMesh.vertices = vertices;
        newMesh.RecalculateBounds();
        newMesh.RecalculateNormals();

        // メッシュをオブジェクトに適用
        meshFilter.mesh = newMesh;

        // オブジェクトの位置と回転をリセット
        if (adjustPosition)
        {
            transform.position = pivotWorldPosition; // ピボットの位置に合わせる
        }
        if (adjustRotation)
        {
            transform.rotation = Quaternion.identity; // 回転をリセット
        }
        if (adjustScale)
        {
            transform.localScale = Vector3.one; // スケールをリセット   
        }

        // メッシュを.obj形式でエクスポート
        ExportMeshToObj(newMesh);
    }

    private void ExportMeshToObj(Mesh mesh)
    {
        if (string.IsNullOrEmpty(_path))
        {
            Debug.LogWarning("Export path is not set.");
            return;
        }

        using (StreamWriter writer = new StreamWriter(_path))
        {
            // OBJファイルのヘッダー
            writer.WriteLine("# Exported by AdjustPivotPoint");
            writer.WriteLine("o " + gameObject.name);

            // 頂点を書き込む
            foreach (Vector3 vertex in mesh.vertices)
            {
                writer.WriteLine($"v {vertex.x} {vertex.y} {vertex.z}");
            }

            // 法線を書き込む
            foreach (Vector3 normal in mesh.normals)
            {
                writer.WriteLine($"vn {normal.x} {normal.y} {normal.z}");
            }

            // テクスチャ座標を書き込む(もしあれば)
            if (mesh.uv.Length > 0)
            {
                foreach (Vector2 uv in mesh.uv)
                {
                    writer.WriteLine($"vt {uv.x} {uv.y}");
                }
            }

            // フェイスを書き込む
            for (int i = 0; i < mesh.triangles.Length; i += 3)
            {
                int vertexIndex1 = mesh.triangles[i] + 1; // OBJは1-indexed
                int vertexIndex2 = mesh.triangles[i + 1] + 1;
                int vertexIndex3 = mesh.triangles[i + 2] + 1;

                writer.WriteLine($"f {vertexIndex1} {vertexIndex2} {vertexIndex3}");
            }
        }

        Debug.Log("Mesh exported to: " + _path);
    }
}