Skip to content

会社ロゴなどのimg2model

会社のロゴなどの2D画像を3Dモデルにして、LKGで展示できるようにするのをAIツールなどを使ってできないかを模索します。

↓記事でやっていたことを自動化するイメージ。

↓検証用画像

xseeds.png

既存のimg2modelAIを使ってみる。

AppleDepthProは当たり前にできませんでした。

Figure_1.png

DepthAnythingも無理でした。

tmp9fk4emy2.png

↑深度推定系はまず使えないと判断します。

Meshyは期待していたものと違いました

Meshy

スクリーンショット 2025-02-26 165909.png

ロゴは基本平面で、ロゴが存在しているピクセルのみ平面的に押し出しをするイメージで、今出ているimg2modelのものは基本的に丸みを帯びているものを扱うことを前提にしていそう。


コードベースでの実装

unityC#でコードベースにモデルへの変換はやってみることにした。

pngやjpgからだと輪郭線ががたついてモデルがうまく作れないと思うのでベクター画像から作るようにしたい。既存のpng,jpgsvg化ツールだとグラデーションがあった時にうまくいかないことを知っているので輪郭線のみ抽出してベクター画像を作るようにしたい。

輪郭線抽出から画像のベクタ化までopencvでできるらしいのでそれでやっていきたい。

元画像からUVも作りたいので元画像も取っておくようにする。

①png、jpgから輪郭線のみのベクタ画像を出力

②輪郭領域内の塗りつぶし→メッシュ化

③そのまま指定分の厚さ押し出し

④元画像のUV化、貼り付け


opencvsharpの環境構築

OpenCVSharp

このドキュメントを参考にしました。

まず、nugetを入れます。nugetはgitにパッケージが転がっているのでそれをインポートして導入しました。

opencvsharpはunityエディタのnugetタブのManageNuGetpackagesウィンドウからopencvsharpと検索して、OpenCvSharp4.runtime.win.4.10.0.20241108をインストールしました。

ここから参考にしたドキュメントに書いてあったように、opencvsharpのgitからdllをダウンロードして配置しようとしたのですが、フォルダの構造が参考にしたものと変わっていたため、dllは適当に配置しておきました。ただ、opencvsharp4/runtimes/win-x64/native以下の階層がないうえに、同名のdllファイルが最下層に置いてあったため、必要かは不明です。

この状態でスクリプトからusing OpenCvSharpと書いてみましたが、参照できなかったのでnugetを使わずにopencvを使う方法を探ります。

OpenCV plus Unity 入門 (1) - 事始め|npaka

↑この記事のOpenCVplusUnityが無料で使えるopencvらしいのでこれも試してみます。

上手く参照できました。

ドーナツ型のような穴が開いているものの処理が難しい。

とりあえず今は無視してやってみます。

  • 今日書いたコード

    csharp
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using OpenCvSharp;
    using System.IO;
    using System.Text;
    using UnityEngine.UIElements;
    using UnityEditor.Build;
    using System;
    
    [RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
    public class LogoModelGenerater : MonoBehaviour
    {
        [Header("入力画像")]
        [SerializeField]
        public Texture2D inputLogo;
    
        [Header("モデルの厚さ")]
        [SerializeField, Range(0f, 5f)]
        public float Depth = 0.2f;
    
        [Header("無視して生成する面積の閾値")]
        public double minAreaThreshold = 0.005;
    
        //出力先パス
        public string outputPah = "Assets/GeneratedLogo.obj";
    
        private Mesh generatedMesh;
    
        void Start()
        {
            //輪郭線抽出
            List<List<Vector2>> allOutLines = ExtractOutLine(inputLogo);
    
            if (allOutLines == null || allOutLines.Count == 0)
            {
                Debug.LogError("輪郭線が作れんかった");
                return;
            }
    
            Debug.Log("輪郭線が作れた");
    
            //輪郭内メッシュ作成
            List<Mesh> meshs = new List<Mesh>();
            foreach (List<Vector2> outlinePoints in allOutLines)
            {
                //三角形分割
                List<int> triangles;
                List<Vector2> triangulatedVertices;
                MakeTrianglePolygon(outlinePoints, out triangulatedVertices, out triangles);
    
                if (triangulatedVertices == null || triangles == null || triangulatedVertices.Count < 3)
                    continue;
    
                //メッシュ統合と押し出し
                Mesh mesh = CreateExtrudedMesh(triangulatedVertices, triangles, Depth);
    
                //UV生成
                Vector2[] uvs = new Vector2[mesh.vertexCount];
                for (int i = 0; i < mesh.vertexCount; i++)
                {
                    Vector3 v = mesh.vertices[i];
                    uvs[i] = new Vector2(v.x / inputLogo.width, v.y / inputLogo.height);
                }
                mesh.uv = uvs;
    
                meshs.Add(mesh);
    
               
            }
    
            if(meshs.Count == 0)
            {
                Debug.LogError("メッシュが作成できんかった");
            }
            Debug.Log("メッシュが作成できた");
    
            //メッシュの統合
            generatedMesh = CombineMeshes(meshs);
    
            //生成したメッシュを確認
            MeshFilter mf = GetComponent<MeshFilter>();
    
            mf.mesh = generatedMesh;
    
            MeshRenderer mr = GetComponent<MeshRenderer>();
    
            Material mat = new Material(Shader.Find("Standard"));
            mat.mainTexture = inputLogo;
            mr.material = mat;
    
            //出力
            ExportMeshToOBJ(generatedMesh, outputPah);
            Debug.Log("出力できた!");
    
        }
    
        /// <summary>
        /// 輪郭線抽出
        /// </summary>
        /// <param name="texture"></param>
        /// <returns></returns>
        List<List<Vector2>> ExtractOutLine(Texture2D texture)
        {
            //test
            Debug.Log($"Texture readable? {texture.isReadable}");
            Debug.Log($"Format: {texture.format}, width: {texture.width}, height: {texture.height}");
    
            byte[] imageData = texture.EncodeToPNG();
    
            //test
            Debug.Log($"imageData length: {imageData.Length}");
    
            Mat src = Mat.ImDecode(imageData, ImreadModes.Color);
            if (src.Empty())
            {
                Debug.LogError("画像の読み込みに失敗した");
                return null;
            }
    
            //グレースケールに変換
            Mat gray = new Mat();
            Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
    
            //グラデーションがある場合無効化したいので二値化
            Mat binary = new Mat();
            Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
    
            //輪郭線の抽出
            OpenCvSharp.Point[][] contours;
            HierarchyIndex[] hierarchy;
            Cv2.FindContours(binary, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
    
            List<List<Vector2>> contourList = new List<List<Vector2>>();
            double minArea = (src.Width * src.Height) * minAreaThreshold;
    
            //輪郭線配列への追加
            for (int i = 0; i < contours.Length; i++)
            {
                double area = Cv2.ContourArea(contours[i]);
                if (area < minArea)
                    continue;
    
                List<Vector2> points = new List<Vector2>();
                foreach (var p in contours[i])
                {
                    points.Add(new Vector2(p.X, p.Y));
                }
                contourList.Add(points);
            }
    
            return contourList;
        }
    
        /// <summary>
        /// 三角形分割
        /// </summary>
        /// <param name="polygon"></param>
        /// <param name="verticesOut"></param>
        /// <param name="trianglesOut"></param>
        void MakeTrianglePolygon(List<Vector2> polygon, out List<Vector2> verticesOut, out List<int> trianglesOut)
        {
            verticesOut = new List<Vector2>(polygon);
            trianglesOut = new List<int>();
    
            List<int> index = new List<int>();
    
            for (int i = 0; i < polygon.Count; i++)
            {
                index.Add(i);
            }
    
            while (index.Count > 3)
            {
                bool earFound = false;
                for (int i = 0; i < index.Count; i++)
                {
                    int prev = index[(i - 1 + index.Count) % index.Count];
                    int current = index[i];
                    int next = index[(i + 1) % index.Count];
    
                    Vector2 a = polygon[prev];
                    Vector2 b = polygon[current];
                    Vector2 c = polygon[next];
    
                    //ポリゴンが裏面にならないように内角が凸かを確認
                    if (!IsConvex(a, b, c))
                        continue;
    
                    //ほかの頂点を含んでしまわないかをチェック
                    bool hasPointInside = false;
                    for (int j = 0; j < index.Count; j++)
                    {
                        int idx = index[j];
    
                        if (idx == prev || idx == current || idx == next)
                            continue;
    
                        if (PointInTriangle(polygon[idx], a, b, c))
                        {
                            hasPointInside = true;
                            break;
                        }
                    }
    
                    if (hasPointInside)
                        continue;
    
                    trianglesOut.Add(prev);
                    trianglesOut.Add(current);
                    trianglesOut.Add(next);
    
                    earFound = true;
    
                    break;
                }
    
                if (!earFound)
                {
                    Debug.Log("三角形分割に失敗した");
                    break;
                }
            }
        }
    
        bool IsConvex(Vector2 a, Vector2 b, Vector2 c)
        {
            return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) < 0;
        }
        bool PointInTriangle(Vector2 pt, Vector2 a, Vector2 b, Vector2 c)
        {
            float s = a.y * c.x - a.x * c.y + (c.y - a.y) * pt.x + (a.x - c.x) * pt.y;
            float t = a.x * b.y - a.y * b.x + (a.y - b.y) * pt.x + (b.x - a.x) * pt.y;
    
            if ((s < 0) != (t < 0))
                return false;
    
            float A = -b.y * c.x + a.y * (c.x - b.x) + a.x * (b.y - c.y) + b.x * c.y;
            return A < 0 ? (s <= 0 && s + t >= A) : (s >= 0 && s + t <= A);
        }
    
        /// <summary>
        /// 三角形から表裏側面のメッシュを作成
        /// </summary>
        /// <param name="vertices2D"></param>
        /// <param name="triangles2D"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        Mesh CreateExtrudedMesh(List<Vector2> vertices2D, List<int> triangles2D, float depth)
        {
            Mesh mesh = new Mesh();
    
            int n = vertices2D.Count;
    
            List<Vector3> vertices = new List<Vector3>();
            List<int> triangles = new List<int>();
    
            //前面の頂点
            foreach (var v in vertices2D)
            {
                vertices.Add(new Vector3(v.x, v.y, depth / 2f));
            }
    
            //背面の頂点
            foreach (var v in vertices2D)
            {
                vertices.Add(new Vector3(v.x, v.y, -depth / 2f));
            }
    
            //前面の三角形
            for (int i = 0; i < triangles2D.Count; i += 3)
            {
                triangles.Add(triangles2D[i]);
                triangles.Add(triangles2D[i + 1]);
                triangles.Add(triangles2D[i + 2]);
            }
    
            //背面の三角形
            for (int i = 0; i < triangles2D.Count; i += 3)
            {
                triangles.Add(triangles2D[i + 2] + n);
                triangles.Add(triangles2D[i + 1] + n);
                triangles.Add(triangles2D[i] + n);
            }
    
            //側面の生成
            for (int i = 0; i < n; i++)
            {
                int next = (i + 1) % n;
    
                int frontA = i;
                int frontB = next;
                int backA = i + n;
                int backB = next + n;
    
                //△これ
                triangles.Add(frontA);
                triangles.Add(frontB);
                triangles.Add(backB);
    
                //▽これ
                triangles.Add(frontA);
                triangles.Add(backB);
                triangles.Add(backA);
            }
    
            mesh.vertices = vertices.ToArray();
            mesh.triangles = triangles.ToArray();
            mesh.RecalculateNormals();
            mesh.RecalculateBounds();
    
            return mesh;
        }
    
        /// <summary>
        /// メッシュの統合
        /// </summary>
        /// <param name="meshs"></param>
        /// <returns></returns>
        Mesh CombineMeshes(List<Mesh> meshs)
        {
            Mesh combinedMesh = new Mesh();
            List<Vector3> combinedVertices = new List<Vector3>();
            List<int> combinedTriangles = new List<int>();
            List<Vector2> combinedUV = new List<Vector2>();
    
            int vertexOffset = 0;
    
            //頂点とUVの結合
            foreach (Mesh m in meshs)
            {
                combinedVertices.AddRange(m.vertices);
                combinedUV.AddRange(m.uv);
                int[] tris = m.triangles;
                for (int i = 0; i < tris.Length; i++)
                {
                    combinedTriangles.Add(tris[i] + vertexOffset);
                }
                vertexOffset += m.vertexCount;
            }
    
            combinedMesh.vertices = combinedVertices.ToArray();
            combinedMesh.triangles = combinedTriangles.ToArray();
            combinedMesh.uv = combinedUV.ToArray();
            combinedMesh.RecalculateNormals();
            combinedMesh.RecalculateBounds();
    
            return combinedMesh;
    
        }
    
        /// <summary>
        /// OBJの書き出し
        /// </summary>
        /// <param name="mesh"></param>
        /// <param name="filePath"></param>
        void ExportMeshToOBJ(Mesh mesh, string filePath)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(" # Exported from Unity ");
    
            foreach (Vector3 v in mesh.vertices)
            {
                sb.AppendLine(string.Format("v {0} {1} {2}", v.x, v.y, v.z));
            }
            
            // UV情報
            foreach (Vector2 uv in mesh.uv)
            {
                sb.AppendLine(string.Format("vt {0} {1}", uv.x, uv.y));
            }
            
            // 法線情報
            foreach (Vector3 n in mesh.normals)
            {
                sb.AppendLine(string.Format("vn {0} {1} {2}", n.x, n.y, n.z));
            }
           
            // 面情報(OBJは1-indexed)
            for (int i = 0; i < mesh.triangles.Length; i += 3)
            {
                int idx0 = mesh.triangles[i] + 1;
                int idx1 = mesh.triangles[i + 1] + 1;
                int idx2 = mesh.triangles[i + 2] + 1;
                sb.AppendLine(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}", idx0, idx1, idx2));
            }
    
            File.WriteAllText(filePath, sb.ToString());
        }
    }

出たバグ

スクリーンショット 2025-03-07 172112.png

読み込んだ画像は読めてて、書き込める状態にあって、実態としてあることは確認できるけど、png化に失敗しているし、それに伴って輪郭線も描けない。

chatgptは入れたopencvが間違っている可能性を指摘しているけどそんなわけない。usingもできてるしvisualstudio上ではエラーは出ていないから。


3/10

以下のコードを参考にしてpng→Matのコンバートの仕方を書き換えました。

  • 参考にしたコード

    csharp
    using UnityEngine;
    using UnityEngine.UI;
    using OpenCvSharp;
    
    public class Sample : MonoBehaviour
    {
        // 初期化
        void Start()
        {
            // Texture2Dの読み込み
            Texture2D srcTexture = (Texture2D)Resources.Load("Textures/twitter") as Texture2D;
    
            // Texture2D → Mat
            Mat srcMat = OpenCvSharp.Unity.TextureToMat(srcTexture);
    
            // グレースケールへの変換
            Mat grayMat = new Mat();
            Cv2.CvtColor(srcMat, grayMat, ColorConversionCodes.RGBA2GRAY);
    
            // Mat → Texture2D
            Texture2D dstTexture = OpenCvSharp.Unity.MatToTexture(grayMat);
    
            // 表示
            GetComponent<RawImage>().texture = dstTexture;
        }
    }

texture2Dから一度PNGを通ってMatにしていたのを直接Matに行くように書き換えました。

書き換えたらエラーは出ずに出力まで行きました。でも期待していた出力結果じゃないです。

入力画像

入力画像

出力

出力

輪郭線の抽出が絶対うまくいってない。

輪郭線抽出だけのコードを作って動かした。加えて、今のコードではsvgを使った処理ができておらず、svgでないと輪郭周りががたがたすると考えているのでsvgに変換して処理できるようにプロトタイピングした。

  • png→輪郭線抽出→塗りつぶし→svg変換、出力

    csharp
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using OpenCvSharp;
    using System.IO;
    using System.Text;
    using UnityEngine.UIElements;
    using UnityEditor.Build;
    using System;
    using UnityEngine.UI;
    
    public class TestMakeConour : MonoBehaviour
    {
        [Header("入力画像")]
        [SerializeField]
        public Texture2D inputLogo;
    
        private void Start()
        {
    
            Mat input = OpenCvSharp.Unity.TextureToMat(inputLogo);
            Mat gray =  new Mat();
            Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
    
            //グラデーションがある場合無効化したいので二値化
            Mat binary = new Mat();
            Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
    
    				//色反転してマスク画像にする
            Mat Mask = new Mat();
            Cv2.BitwiseNot(binary, Mask);
    
            Point[][] contours;
            HierarchyIndex[] hierarchy;
            Cv2.FindContours(Mask, out contours, out hierarchy, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
    
            double areaThreshold = input.Rows * input.Cols * 0.005;
    
            int width = input.Cols;
            int height = input.Rows;
    
            StringBuilder svgBuilder = new StringBuilder();
    
            svgBuilder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            svgBuilder.AppendLine($"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" viewBox=\"0 0 {width} {height}\">");
    
            foreach (var contour in contours)
            {
                double area = Cv2.ContourArea(contour);
                if (area > areaThreshold)
                {
                    StringBuilder pointsStr = new StringBuilder();
                    foreach (var pt in contour)
                    {
                        pointsStr.AppendFormat("{0},{1} ", pt.X, pt.Y);
                    }
                    svgBuilder.AppendLine($"  <polygon points=\"{pointsStr.ToString().Trim()}\" style=\"fill:rgb(0,255,0);stroke:rgb(0,255,0);stroke-width:2\" />");
                }
                
            }
            svgBuilder.AppendLine("</svg>");
    
            string savePath = "Assets/Contour.svg";
            File.WriteAllText(savePath, svgBuilder.ToString(), Encoding.UTF8);
    
        }
    }

↓出力

スクリーンショット 2025-03-10 113323.pngスクリーンショット 2025-03-10 114327.png

この輪郭線の取り方を組み込みたい。

でもopencvではsvgの取り扱いはそのままできないらしい。unityもそれ用のassetを使わないとsvgの表示などはできない。

輪郭線抽出から先(ポリゴン製作、モデル製作)はopencv使わないから大丈夫と思う(?)

メッシュの生成まで書き直してみたが、重くてうまく走らない。。。

三角形分割の際のindex処理を軽量化することで走るようになった。

でも三角形分割ができず、エラーコードのほうに行く。

輪郭線の点同士が近すぎるのが原因かもしれない。先に近似化処理をしないと動かなさそう。


3/11

輪郭線のみ出すスクリプトをもう一度見直してみたり、輪郭線の近似化処理など試していたらなんかできてた(!?)

スクリーンショット 2025-03-11 093311.pngスクリーンショット 2025-03-11 093624.png
  • モデルを出力するコード

    csharp
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using OpenCvSharp;
    using System.IO;
    using System.Text;
    using UnityEngine.UIElements;
    using UnityEditor.Build;
    using System;
    using UnityEngine.UI;
    
    [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
    public class TestMakeConour : MonoBehaviour
    {
        [Header("入力画像")]
        [SerializeField]
        public Texture2D inputLogo;
    
        [Header("生成する親オブジェクト")]
        [SerializeField]
        public GameObject Objectparent;
    
        [Header("モデルの厚さ")]
        [SerializeField, Range(0f, 20f)]
        public float Depth = 10f;
    
        public double approximateValue = 0.05;
    
        private void Start()
        {
    
            Mat input = OpenCvSharp.Unity.TextureToMat(inputLogo);
            Mat gray =  new Mat();
            Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
    
            //グラデーションがある場合無効化したいので二値化
            Mat binary = new Mat();
            Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
    
            Mat Mask = new Mat();
            Cv2.BitwiseNot(binary, Mask);
    
            Point[][] contours;
            HierarchyIndex[] hierarchy;
            Cv2.FindContours
                (Mask, out contours, out hierarchy, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
    
            double areaThreshold = input.Rows * input.Cols * 0.005;
    
            //輪郭処理
            foreach (var contour in contours)
            {
                double area = Cv2.ContourArea(contour);
    
                if (area > areaThreshold)
                {
                    //輪郭の近似処理   
                    double arcLength = Cv2.ArcLength(contour, true);
                    double epsilon = approximateValue * arcLength;
    
                    Point[] aproxCurve = Cv2.ApproxPolyDP(contour, epsilon, true);
    
                    //輪郭をlistに変換
                    List<Vector2> polygon = new List<Vector2>();
                    foreach (var pt in contour)
                    {
                        polygon.Add(new Vector2(pt.X, pt.Y));
                    }
    
                    Debug.Log($"近似処理完了{polygon.Count}");
    
                    List<Vector2> vertices;
                    List<int> triangles;
                    //三角形分割
                    MakeTrianglePolygon(polygon, out vertices, out triangles);
    
                    Mesh mesh = CreateExtrudedMesh(vertices, triangles, Depth);
    
                    Vector3[] meshVertices = new Vector3[vertices.Count];
                    for (int i = 0; i < vertices.Count; i++)
                    {
                        meshVertices[i] = new Vector3(vertices[i].x, vertices[i].y, 0);
                    }
    
                    
    
                    //作ったメッシュを生成する
                    GameObject contourObj = new GameObject("childMesh");
                    contourObj.transform.parent = Objectparent.transform;
    
                    MeshFilter mf = contourObj.AddComponent<MeshFilter>();
                    mf.mesh = mesh;
    
                    MeshRenderer mr = contourObj.AddComponent<MeshRenderer>();
                    mr.material = new Material(Shader.Find("Standard"));
    
                }
            }
            //ここまで出たら何個かメッシュが作られているはず。
    
    #region
            //--------svg変換---------
            /*
            int width = input.Cols;
            int height = input.Rows;
            StringBuilder svgBuilder = new StringBuilder();
    
            svgBuilder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            svgBuilder.AppendLine
            ($"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" viewBox=\"0 0 {width} {height}\">");
    
            foreach (var contour in contours)
            {
                double area = Cv2.ContourArea(contour);
                if (area > areaThreshold)
                {
                    StringBuilder pointsStr = new StringBuilder();
                    foreach (var pt in contour)
                    {
                        pointsStr.AppendFormat("{0},{1} ", pt.X, pt.Y);
                    }
                    svgBuilder.AppendLine
                    ($"  <polygon points=\"{pointsStr.ToString().Trim()}\" style=\"fill:rgb(0,255,0);stroke:rgb(0,255,0);stroke-width:2\" />");
                }
                
            }
            svgBuilder.AppendLine("</svg>");
    
            string savePath = "Assets/Contour.svg";
            File.WriteAllText(savePath, svgBuilder.ToString(), Encoding.UTF8);
            */
    #endregion
        }
    
        /// <summary>
        /// 三角形分割S
        /// </summary>
        /// <param name="polygon"></param>
        /// <param name="verticesOut"></param>
        /// <param name="trianglesOut"></param>
        void MakeTrianglePolygon(List<Vector2> polygon, out List<Vector2> verticesOut, out List<int> trianglesOut)
        {
            verticesOut = new List<Vector2>(polygon);
            trianglesOut = new List<int>();
    
            List<int> index = new List<int>();
    
            for (int i = 0; i < polygon.Count; i++)
            {
                index.Add(i);
            }
    
            //三角形になるように頂点検索
            while (index.Count > 3)
            {
                bool earFound = false;
                for (int i = 0; i < index.Count; i++)
                {
                    int prev = index[(i - 1 + index.Count) % index.Count];
                    int current = index[i];
                    int next = index[(i + 1) % index.Count];
    
                    Vector2 a = polygon[prev];
                    Vector2 b = polygon[current];
                    Vector2 c = polygon[next];
    
                    //ポリゴンが裏面にならないように内角が凸かを確認
                    if (!IsConvex(a, b, c))
                        continue;
    
                    //ほかの頂点を含んでしまわないかをチェック
                    bool hasPointInside = false;
                    for (int j = 0; j < index.Count; j++)
                    {
                        int idx = index[j];
    
                        if (idx == prev || idx == current || idx == next)
                            continue;
    
                        if (PointInTriangle(polygon[idx], a, b, c))
                        {
                            hasPointInside = true;
                            break;
                        }
                    }
    
                    if (hasPointInside)
                        continue;
    
                    trianglesOut.Add(prev);
                    trianglesOut.Add(current);
                    trianglesOut.Add(next);
    
                    index.RemoveAt(i);
                    earFound = true;
    
                    break;
                }
    
                if (!earFound)
                {
                    Debug.Log("三角形分割に失敗した");
                    break;
                }
            }
    
        }
        bool IsConvex(Vector2 a, Vector2 b, Vector2 c)
        {
            return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) < 0;
        }
        bool PointInTriangle(Vector2 pt, Vector2 a, Vector2 b, Vector2 c)
        {
            float s = a.y * c.x - a.x * c.y + (c.y - a.y) * pt.x + (a.x - c.x) * pt.y;
            float t = a.x * b.y - a.y * b.x + (a.y - b.y) * pt.x + (b.x - a.x) * pt.y;
    
            if ((s < 0) != (t < 0))
                return false;
    
            float A = -b.y * c.x + a.y * (c.x - b.x) + a.x * (b.y - c.y) + b.x * c.y;
            return A < 0 ? (s <= 0 && s + t >= A) : (s >= 0 && s + t <= A);
        }
    
        Mesh CreateExtrudedMesh(List<Vector2> vertices2D, List<int> triangles2D, float depth)
        {
            Mesh mesh = new Mesh();
    
            int n = vertices2D.Count;
    
            List<Vector3> vertices = new List<Vector3>();
            List<int> triangles = new List<int>();
    
            //前面の頂点
            foreach (var v in vertices2D)
            {
                vertices.Add(new Vector3(v.x, v.y, depth / 2f));
            }
    
            //背面の頂点
            foreach (var v in vertices2D)
            {
                vertices.Add(new Vector3(v.x, v.y, -depth / 2f));
            }
    
            //前面の三角形
            for (int i = 0; i < triangles2D.Count; i += 3)
            {
                triangles.Add(triangles2D[i]);
                triangles.Add(triangles2D[i + 1]);
                triangles.Add(triangles2D[i + 2]);
            }
    
            //背面の三角形
            for (int i = 0; i < triangles2D.Count; i += 3)
            {
                triangles.Add(triangles2D[i + 2] + n);
                triangles.Add(triangles2D[i + 1] + n);
                triangles.Add(triangles2D[i] + n);
            }
    
            //側面の生成
            for (int i = 0; i < n; i++)
            {
                int next = (i + 1) % n;
    
                int frontA = i;
                int frontB = next;
                int backA = i + n;
                int backB = next + n;
    
                //△これ
                triangles.Add(frontA);
                triangles.Add(frontB);
                triangles.Add(backB);
    
                //▽これ
                triangles.Add(frontA);
                triangles.Add(backB);
                triangles.Add(backA);
            }
    
            mesh.vertices = vertices.ToArray();
            mesh.triangles = triangles.ToArray();
            mesh.RecalculateNormals();
            mesh.RecalculateBounds();
    
            return mesh;
        }
    
       
        void ExportMeshToOBJ(Mesh mesh, string filePath)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(" # Exported from Unity ");
    
            foreach (Vector3 v in mesh.vertices)
            {
                sb.AppendLine(string.Format("v {0} {1} {2}", v.x, v.y, v.z));
            }
    
            // UV情報
            foreach (Vector2 uv in mesh.uv)
            {
                sb.AppendLine(string.Format("vt {0} {1}", uv.x, uv.y));
            }
    
            // 法線情報
            foreach (Vector3 n in mesh.normals)
            {
                sb.AppendLine(string.Format("vn {0} {1} {2}", n.x, n.y, n.z));
            }
    
            // 面情報(OBJは1-indexed)
            for (int i = 0; i < mesh.triangles.Length; i += 3)
            {
                int idx0 = mesh.triangles[i] + 1;
                int idx1 = mesh.triangles[i + 1] + 1;
                int idx2 = mesh.triangles[i + 2] + 1;
                sb.AppendLine(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}", idx0, idx1, idx2));
            }
    
            File.WriteAllText(filePath, sb.ToString());
        }
    }

でもこれ輪郭線ガタガタだし、そもそも輪郭線内のメッシュ作られてないし、なぜか最初から上下反転して出力されてるし、ちょっと意味が分からない。

あとログに三角形分割に失敗したって書いてある。

輪郭線の近似化処理をローカルでするのに限界を感じるので以下のようにやってみようと思う。

  1. [ローカル]画像のマスク画像作成までコードでやって出力、
  2. [外部]外部サイトでベクタ化
  3. [ローカル]頂点読み込み
  4. [ローカル]メッシュ作成
  5. [ローカル]obj出力

マスク画像でベクタ化しないとグラデーションがあった時に輪郭線抽出が難しくなるのでマスク画像が必要。

ベクタ画像の頂点とったってエッジは曲線をそのまま表現ができないから多分カクついたモデルができるな。考え直します。

それと、外部サイトでベクタ化したって、今回使ったみたいな解像度低い画像だと結局ガタガタのものが出力される。エクシーズのロゴもそうなので、実用性に欠ける。

モデラーの友達に相談したら、unityにモデルにマスク画像を適用するマテリアルがあるらしいのでそれも手。でもそれで作ったモデルを外に持ち出した時にどう見えるかはわからない。

マスク画像のpng出力、opencvではできないかもしれない。


現状の問題点

  • 輪郭線がガタガタである
    • 対策
      • ベクタ画像を作って、その頂点から出力する
        • 外部サイトに持っていこうにも、unityで出力したマスク画像のpng化ができない。
        • 元画像の解像度が低い場合(エクシーズのロゴなど)外部サイトに持って行ってもなめらかなベクター曲線は出力されない。
        • ベクタ画像にしたとして頂点出力しても、頂点間の辺の補完用のアルゴリズムを作らないといけない。これが大変そうすぎる。
  • 輪郭線内のメッシュがうまく作れてない

輪郭線ガタガタのほうが解決が難しそうなのでメッシュ分割のほうから手を付けてみたいと思います。

メッシュ分割も難しすぎるかも。三角形分割の方法やライブラリはネット上に転がっているけれど、ロゴなどをメッシュ分割するとなると、

  • 輪郭点同士を一周させた領域内に三角形が作成されたか
    • (ロゴの輪郭線は凹凸があるので、輪郭点同士をつなぐような普通のアルゴリズムでは凹部分を埋めるように三角形が作成される)
  • 領域内がすべて埋め尽くされたか
    • 今やっている耳切り法だとこれが満たされない

という制約が出てくるため、合う方法がなかなかない。

既存のアルゴリズムをオーバーライドして今回用のアルゴリズムに改造する方向で開発してみているけど、時間がかかりすぎる気がします。


三角形分割_メッシュ生成

https://x.com/toygramming/status/1771379376535580782

ツイッターで調べたらドロネー分割でやってる人いた;;とりあえずドロネー分割で作らせて、外周に飛び出てしまった三角形を消せばいいらしい。これが実装できたら一番いいかも;;

ドロネー三角形分割を自前で実装してみる - Qiita

↑この記事が一番参考になりそう、でもこの記事で作ってるのjavascriptのコードだから使えないので自前で作ります

ツイッターのやつは技術系の記事もないし、当たり前にコードも何も公開してくれてない。

長くなりそうなのでこの部分だけ記事を分けます。

会社ロゴのimg2model メッシュ分割編

↑この記事のリザルト

🔖

  • できたこと
    • 輪郭線内のメッシュ分割
  • これから解決すべきこと(優先度順)
    • [x] 裏面と側面のメッシュが作れていないので作る
    • [x] obj形式での出力をする
    • [ ] テクスチャを生成する
    • [ ] 輪郭線がまだガタガタなのでできれば直したい。
      • 輪郭点の抽出処理を調べたりする
スクリーンショット 2025-03-12 140836.png

裏面と側面のメッシュ生成

ドロネーを試す前のコードで生成していた方法だと、背面と前面、両方が内側を向いて生成されていたため、正面から見たときに前面のメッシュが見えずに背面のメッシュが、後ろから見たときに背面のメッシュが見えずに前面のメッシュが見えてしまい、どちらから見ても側面に対して凹んだ形のモデルに見えてしまっていたのですが、これはunityのメッシュの特徴である、時計回りに登録された面が表側になるというもののせいでした。

なので、vector3であらわされる頂点の情報を下記のように登録するように書き直すことで解決しました。

csharp
private Mesh CreateMesh(List<Vector2> vertice2D, List<int> triangleindex, float depth)
{
    Mesh mesh = new Mesh();

    int n = vertice2D.Count;

    List<Vector3> vertice3 = new List<Vector3>();
    List<int> triangles = new List<int>();

    //前面の頂点
    foreach (var v in vertice2D)
    {
        vertice3.Add(new Vector3(v.x,v.y,depth));
        Debug.Log($"前面の頂点の座標{v.x},{v.y},{depth}");
    }

    //背面の頂点
    foreach (var v in vertice2D)
    {
        vertice3.Add(new Vector3(v.x, v.y, 0));
        Debug.Log($"背面の頂点の座標{v.x},{v.y},{0}");
    }

    for (int i = 0; i < vertice3.Count; i++)
    {
        Vector3 worldpos = transform.TransformPoint(vertice3[i]);
        Instantiate(prefab, worldpos,Quaternion.identity,transform);
    }

    //前面の三角形
    for (int i = 0; i < triangleindex.Count; i += 3)
    {
        triangles.Add(triangleindex[i + 2]);
        triangles.Add(triangleindex[i + 1]);
        triangles.Add(triangleindex[i]);
        Debug.Log($"前面:{vertice3[triangleindex[i]]},{vertice3[triangleindex[i+1]]},{vertice3[triangleindex[i]+2]}");
    }

    //背面の三角形
    for (int i = 0; i < triangleindex.Count; i += 3)
    {
        triangles.Add(triangleindex[i] + n);
        triangles.Add(triangleindex[i + 1] + n);
        triangles.Add(triangleindex[i + 2] + n);
        Debug.Log($"背面:{vertice3[triangleindex[i+2] + n]},{vertice3[triangleindex[i + 1] + n]},{vertice3[triangleindex[i] + n]}");
    }

    //側面
    for (int i = 0; i < n; i++)
    {
        int next = (i + 1) % n;

        int frontA = i;
        int frontB = next;
        int backA = i + n;
        int backB = next + n;

        //△これ
        triangles.Add(frontA);
        triangles.Add(frontB);
        triangles.Add(backB);

        //▽これ
        triangles.Add(frontA);
        triangles.Add(backB);
        triangles.Add(backA);
    }

    mesh.vertices = vertice3.ToArray();
    mesh.triangles = triangles.ToArray();
    mesh.RecalculateNormals();
    mesh.RecalculateBounds();

    return mesh;

}

OBJ出力

生成した部品が複数になっても一つのobjで出力されるように、生成されたメッシュを一つの親メッシュに統合する処理を追加してからobj出力処理を追加しました。

統合処理

csharp
/// <summary>
/// 親オブジェクトの子オブジェクトを探索して、そのメッシュフィルターからメッシュをとってきて統合する
/// </summary>
/// <param name="parent"></param>
/// <returns></returns>
Mesh MergeChildMeshes(GameObject parent)
{
    List<Vector3> mergedVertices = new List<Vector3>();
    List<Vector2> mergedUV = new List<Vector2>();
    List<Vector3> mergedNormals = new List<Vector3>();
    List<int> mergedTriangles = new List<int>();

    int vertexOffset = 0;

    foreach (Transform child in parent.transform)
    {
        MeshFilter mf = child.GetComponent<MeshFilter>();
        if (mf == null) continue;

        Mesh mesh = mf.mesh;

        mergedVertices.AddRange(mesh.vertices);
        mergedUV.AddRange(mesh.uv);
        mergedNormals.AddRange(mesh.normals);

        foreach (int index in mesh.triangles)
        {
            mergedTriangles.Add(index + vertexOffset);
        }

        vertexOffset += mesh.vertexCount;
    }

    Mesh mergedMesh = new Mesh();
    mergedMesh.vertices = mergedVertices.ToArray();
    mergedMesh.uv = mergedUV.ToArray();
    mergedMesh.normals = mergedNormals.ToArray();
    mergedMesh.triangles = mergedTriangles.ToArray();
    mergedMesh.RecalculateBounds();

    return mergedMesh;

}

obj出力処理

csharp
void ExportCombinedMeshToOBJ(GameObject parent, string filePath)
{
    Mesh combinedMesh = MergeChildMeshes(parent);
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("# Exported form Unity(combinedMesh)");

    // 頂点情報
    foreach (Vector3 v in combinedMesh.vertices)
    {
        sb.AppendLine($"v {v.x} {v.y} {v.z}");
    }

    // UV 情報(もし存在すれば)
    if (combinedMesh.uv.Length > 0)
    {
        foreach (Vector2 uv in combinedMesh.uv)
        {
            sb.AppendLine($"vt {uv.x} {uv.y}");
        }
    }

    // 法線情報(もし存在すれば)
    if (combinedMesh.normals.Length > 0)
    {
        foreach (Vector3 n in combinedMesh.normals)
        {
            sb.AppendLine($"vn {n.x} {n.y} {n.z}");
        }
    }

    // 面情報(OBJ は 1-indexed なので +1)
    for (int i = 0; i < combinedMesh.triangles.Length; i += 3)
    {
        int idx0 = combinedMesh.triangles[i] + 1;
        int idx1 = combinedMesh.triangles[i + 1] + 1;
        int idx2 = combinedMesh.triangles[i + 2] + 1;
        // UV と法線も同じインデックスで指定(存在しない場合はスキップも可)
        sb.AppendLine($"f {idx0}/{idx0}/{idx0} {idx1}/{idx1}/{idx1} {idx2}/{idx2}/{idx2}");
    }

    File.WriteAllText(filePath, sb.ToString());
}

↓生成されたobj

https://youtu.be/by_FCt-c3t4

こんな感じで部品が複数の場合も一つのobjとして出力ができます。

スクリーンショット 2025-03-12 161022.png

上下逆に生成されるのは謎ですが優先度的に直すのは後でいいと判断しました。


テクスチャを生成する

入力された画像をもとにテクスチャを生成する処理を書きます。

山本さんから頂いたコードでは頂点カラーを使ってマテリアルに色を付けていたので同じように頂点カラーを使ってマテリアルに色を付けようと思います。

私の使っているunity2022.3.45だと頂点カラーを使うシェーダーがないので下記のサイトからコードを拝借してシェーダーを作りました。

【Unity】プログラミングで作ったメッシュを頂点カラーで色を塗る方法 - 渋谷ほととぎす通信

加えて、コードに以下の文を追加しました。

  1. Startメソッド、メッシュレンダラーの適応前
csharp
Material vertexColoredMat = new Material(Shader.Find("Unlit/VertexColorShader"));
  1. CreateMeshメソッド、前面の頂点と背面の頂点の生成中
csharp
Color col = inputLogo.GetPixel(ix, iy);
colors.Add(col);
  1. CreateMeshメソッド、mesh書き出し中
csharp
mesh.colors = colors.ToArray();

最終的なコードはこうなりました。

  • LogoModelManager

    csharp
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using OpenCvSharp;
    using System.IO;
    using System.Text;
    using UnityEngine.UIElements;
    using UnityEditor.Build;
    using System;
    using UnityEngine.UI;
    using UnityEngine.Rendering;
    using System.Linq;
    public class LogoModelManager : MonoBehaviour
    {
        [Header("入力画像")]
        [SerializeField]
        public Texture2D inputLogo;
    
        [Header("モデルの厚さ")]
        [SerializeField, Range(0f, 20f)]
        public float Depth = 5f;
    
        [Header("無視して生成する面積の閾値")]
        public double minAreaThreshold = 0.005;
    
        [Header("生成先の親オブジェクト")]
        [SerializeField]
        public GameObject parentObject;
        public RawImage preview;
    
        [Header("出力先のファイルパス")]
        [SerializeField]
        public string PathToOutput = "Assets/output/Output.obj";
    
        [SerializeField]
        public GameObject prefab;
        private Mesh generatedMesh;
        
    
        // Start is called before the first frame update
        void Start()
        {
            //輪郭線の作成
            List<List<Vector2>> allOutLines = ExtractOutLine(inputLogo);
    
            if (allOutLines == null || allOutLines.Count == 0)
            {
                Debug.LogError("輪郭線が作れんかった");
                return;
            }
            Debug.Log("輪郭線が作れた");
    
            List<Mesh> meshs = new List<Mesh>();
            DelaunayTriangulation triangulation = new DelaunayTriangulation();
    
            //三角形分割,メッシュ生成
            foreach (List<Vector2> outlinePoints in allOutLines)
            {
                List<int> triangles;
                triangulation.Triangulator(outlinePoints, out triangles);
    
                //メッシュ化
                Mesh mesh = CreateMesh(outlinePoints,triangles,Depth);
    
                //メッシュ生成
                GameObject contourObj = new GameObject("childMesh");
                contourObj.transform.parent = parentObject.transform;
    
                MeshFilter mf = contourObj.AddComponent<MeshFilter>();
                mf.mesh = mesh;
    
                Material vertexColoredMat = new Material(Shader.Find("Unlit/VertexColorShader"));
    
                MeshRenderer mr = contourObj.AddComponent<MeshRenderer>();
                mr.material = vertexColoredMat;
            }
            Debug.Log("メッシュ生成できた");
    
            //メッシュのマージ
            Mesh mergeMesh = MergeChildMeshes(parentObject);
            Debug.Log("メッシュのマージが完了した");
    
            //obj出力
            ExportCombinedMeshToOBJ(parentObject,PathToOutput);
            Debug.Log("出力した");
        }
    
        /// <summary>
        /// 輪郭点抽出
        /// </summary>
        /// <param name="_input"></param>
        /// <returns></returns>
        List<List<Vector2>> ExtractOutLine(Texture2D _input)
        {
            Mat src = OpenCvSharp.Unity.TextureToMat(_input);
            //グレースケールに変換
            preview = GetComponent<RawImage>();
            Mat input = OpenCvSharp.Unity.TextureToMat(_input);
            Mat gray = new Mat();
            Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
    
            //二値化
            Mat binary = new Mat();
            Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
    
            //色反転してマスク画像にする
            Mat Mask = new Mat();
            Cv2.BitwiseNot(binary, Mask);
    
            Point[][] contours;
            HierarchyIndex[] hierarchy;
            Cv2.FindContours
                (
                Mask,
                out contours,
                out hierarchy,
                RetrievalModes.List,
                ContourApproximationModes.ApproxSimple
                );
    
            List<List<Vector2>> contourList = new List<List<Vector2>>();
            double minArea = (src.Width * src.Height) * minAreaThreshold;
    
            //輪郭線配列への追加
            for (int i = 0; i < contours.Length; i++)
            {
                double area = Cv2.ContourArea(contours[i]);
                if (area < minArea)
                    continue;
                List<Vector2> points = new List<Vector2>();
    
                foreach (var p in contours[i])
                {
                    points.Add(new Vector2(p.X, p.Y));
                }
                contourList.Add(points);
            }
    
            return contourList;
        }
    
        private Mesh CreateMesh(List<Vector2> vertice2D, List<int> triangleindex, float depth)
        {
            Mesh mesh = new Mesh();
    
            int n = vertice2D.Count;
    
            List<Vector3> vertice3 = new List<Vector3>();
            List<int> triangles = new List<int>();
            List<Color> colors = new List<Color>();
    
            //前面の頂点
            foreach (var v in vertice2D)
            {
                vertice3.Add(new Vector3(v.x,v.y,depth));
                int ix = Mathf.Clamp((int)v.x, 0, inputLogo.width - 1);
                int iy = Mathf.Clamp((int)v.y, 0, inputLogo.height - 1);
                Color col = inputLogo.GetPixel(ix, iy);
                colors.Add(col);
                Debug.Log($"前面の頂点の座標{v.x},{v.y},{depth}");
            }
    
            //背面の頂点
            foreach (var v in vertice2D)
            {
                vertice3.Add(new Vector3(v.x, v.y, 0));
                int ix = Mathf.Clamp((int)v.x, 0, inputLogo.width - 1);
                int iy = Mathf.Clamp((int)v.y, 0, inputLogo.height - 1);
                Color col = inputLogo.GetPixel(ix, iy);
                colors.Add(col);
                Debug.Log($"背面の頂点の座標{v.x},{v.y},{0}");
            }
    
            for (int i = 0; i < vertice3.Count; i++)
            {
                Vector3 worldpos = transform.TransformPoint(vertice3[i]);
                Instantiate(prefab, worldpos,Quaternion.identity,transform);
            }
    
            //前面の三角形
            for (int i = 0; i < triangleindex.Count; i += 3)
            {
                triangles.Add(triangleindex[i + 2]);
                triangles.Add(triangleindex[i + 1]);
                triangles.Add(triangleindex[i]);
                Debug.Log($"前面:{vertice3[triangleindex[i]]},{vertice3[triangleindex[i+1]]},{vertice3[triangleindex[i]+2]}");
            }
    
            //背面の三角形
            for (int i = 0; i < triangleindex.Count; i += 3)
            {
                triangles.Add(triangleindex[i] + n);
                triangles.Add(triangleindex[i + 1] + n);
                triangles.Add(triangleindex[i + 2] + n);
                Debug.Log($"背面:{vertice3[triangleindex[i+2] + n]},{vertice3[triangleindex[i + 1] + n]},{vertice3[triangleindex[i] + n]}");
            }
    
            //側面
            for (int i = 0; i < n; i++)
            {
                int next = (i + 1) % n;
    
                int frontA = i;
                int frontB = next;
                int backA = i + n;
                int backB = next + n;
    
                //△これ
                triangles.Add(frontA);
                triangles.Add(frontB);
                triangles.Add(backB);
    
                //▽これ
                triangles.Add(frontA);
                triangles.Add(backB);
                triangles.Add(backA);
            }
    
            mesh.vertices = vertice3.ToArray();
            mesh.triangles = triangles.ToArray();
            mesh.colors = colors.ToArray();
            mesh.RecalculateNormals();
            mesh.RecalculateBounds();
    
            return mesh;
    
        }
    
        /// <summary>
        /// 親オブジェクトの子オブジェクトを探索して、そのメッシュフィルターからメッシュをとってきて統合する
        /// </summary>
        /// <param name="parent"></param>
        /// <returns></returns>
        Mesh MergeChildMeshes(GameObject parent)
        {
            List<Vector3> mergedVertices = new List<Vector3>();
            List<Vector2> mergedUV = new List<Vector2>();
            List<Vector3> mergedNormals = new List<Vector3>();
            List<int> mergedTriangles = new List<int>();
            List<Color> mergedColors = new List<Color>();
    
            int vertexOffset = 0;
    
            foreach (Transform child in parent.transform)
            {
                MeshFilter mf = child.GetComponent<MeshFilter>();
                if (mf == null) continue;
    
                Mesh mesh = mf.mesh;
    
                mergedVertices.AddRange(mesh.vertices);
                mergedUV.AddRange(mesh.uv);
                mergedNormals.AddRange(mesh.normals);
    
                if (mesh.colors != null && mesh.colors.Length > 0)
                {
                    mergedColors.AddRange(mesh.colors);
                }
                else
                {
                    for (int i = 0; i < mesh.vertexCount; i++)
                    {
                        mergedColors.Add(Color.white);
                    }
                }
    
                foreach (int index in mesh.triangles)
                {
                    mergedTriangles.Add(index + vertexOffset);
                }
    
                vertexOffset += mesh.vertexCount;
            }
    
            Mesh mergedMesh = new Mesh();
            mergedMesh.vertices = mergedVertices.ToArray();
            mergedMesh.uv = mergedUV.ToArray();
            mergedMesh.normals = mergedNormals.ToArray();
            mergedMesh.triangles = mergedTriangles.ToArray();
            mergedMesh.colors = mergedColors.ToArray();
    
            mergedMesh.RecalculateBounds();
    
            return mergedMesh;
    
        }
    
        void ExportCombinedMeshToOBJ(GameObject parent, string filePath)
        {
            Mesh combinedMesh = MergeChildMeshes(parent);
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# Exported form Unity(combinedMesh)");
    
            // 頂点情報
            foreach (Vector3 v in combinedMesh.vertices)
            {
                sb.AppendLine($"v {v.x} {v.y} {v.z}");
            }
    
            // UV 情報(もし存在すれば)
            if (combinedMesh.uv.Length > 0)
            {
                foreach (Vector2 uv in combinedMesh.uv)
                {
                    sb.AppendLine($"vt {uv.x} {uv.y}");
                }
            }
    
            // 法線情報(もし存在すれば)
            if (combinedMesh.normals.Length > 0)
            {
                foreach (Vector3 n in combinedMesh.normals)
                {
                    sb.AppendLine($"vn {n.x} {n.y} {n.z}");
                }
            }
    
            // 面情報(OBJ は 1-indexed なので +1)
            for (int i = 0; i < combinedMesh.triangles.Length; i += 3)
            {
                int idx0 = combinedMesh.triangles[i] + 1;
                int idx1 = combinedMesh.triangles[i + 1] + 1;
                int idx2 = combinedMesh.triangles[i + 2] + 1;
                // UV と法線も同じインデックスで指定(存在しない場合はスキップも可)
                sb.AppendLine($"f {idx0}/{idx0}/{idx0} {idx1}/{idx1}/{idx1} {idx2}/{idx2}/{idx2}");
            }
    
            File.WriteAllText(filePath, sb.ToString());
        }
    
    }

作られたモデルは以下です。

スクリーンショット 2025-03-12 170508.png

上下反転しているせいなのかわかりませんがうまく色がついてないっぽいです。でも元の画像の面影が色くらいしかなく、画像のどの部分から引っ張ってきた色なのかがわからない。

とりあえず上下反転するところを直してみようと思います。

chatgptに聞いたら、opencvのy軸の原点が左上にあるのに対し、unityのy軸の原点は左下にあるかららしいです。

なので、輪郭点を抽出してリストに入れていく際に補正して入れるように変えれば治るかもしれないです。

輪郭線配列への追加部分に以下の処理を組み込みました。

csharp
float correctedY = _input.height - p.Y;
points.Add(new Vector2(p.X, correctedY));
  • ExtractOutLineメソッド全体

    csharp
    List<List<Vector2>> ExtractOutLine(Texture2D _input)
    {
        Mat src = OpenCvSharp.Unity.TextureToMat(_input);
        //グレースケールに変換
        preview = GetComponent<RawImage>();
        Mat input = OpenCvSharp.Unity.TextureToMat(_input);
        Mat gray = new Mat();
        Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
    
        //二値化
        Mat binary = new Mat();
        Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
    
        //色反転してマスク画像にする
        Mat Mask = new Mat();
        Cv2.BitwiseNot(binary, Mask);
    
        Point[][] contours;
        HierarchyIndex[] hierarchy;
        Cv2.FindContours
            (
            Mask,
            out contours,
            out hierarchy,
            RetrievalModes.List,
            ContourApproximationModes.ApproxSimple
            );
    
        List<List<Vector2>> contourList = new List<List<Vector2>>();
        double minArea = (src.Width * src.Height) * minAreaThreshold;
    
        //輪郭線配列への追加
        for (int i = 0; i < contours.Length; i++)
        {
            double area = Cv2.ContourArea(contours[i]);
            if (area < minArea)
                continue;
            List<Vector2> points = new List<Vector2>();
    
            foreach (var p in contours[i])
            {
                float correctedY = _input.height - p.Y;
                points.Add(new Vector2(p.X, correctedY));
            }
            contourList.Add(points);
        }
    
        return contourList;
    }

ほんとに治った。

でも色の付き方はダメ。

Appleのロゴでも試行してみた。ヘタのところにも色を付けようとしているので複数部品があっても色を付けることはできそう。

でもやっぱり色の付き方がダメ。

スクリーンショット 2025-03-12 171450.pngスクリーンショット 2025-03-12 171548.png

よく見たら側面のメッシュが裏側になっている!y座標を補正したからと思う。

側面のメッシュ生成も見直しました。

csharp
//△これ
    triangles.Add(frontA);
    triangles.Add(frontB);
    triangles.Add(backB);

    //▽これ
    triangles.Add(frontA);
    triangles.Add(backB);
    triangles.Add(backA);

直した

csharp
//△これ
triangles.Add(frontA);
triangles.Add(backB);
triangles.Add(frontB);

//▽これ
triangles.Add(frontA);
triangles.Add(backA);
triangles.Add(backB);

ここ直しただけで側面のメッシュ生成も元通りうまくいくようになりました!

頂点カラーを利用したテクスチャだと、画像の解像度が悪いと境界線上のあいまいな色をとってきてしまってこういうことになってそう、それと、グラデーションがあった時にこのやり方だと対応ができなさそう。

マスクテクスチャという方法があるっぽいのでそれを調べてみることにします。

メッシュの生成の仕方を変えなければならないらしく。変えてみた。

csharp
private Mesh CreateMesh(List<Vector2> vertice2D, List<int> triangleindex, float depth)
{
    Mesh mesh = new Mesh();

    int n = vertice2D.Count;

    List<Vector3> vertice3 = new List<Vector3>();
    List<Vector2> uvs = new List<Vector2>();
    List<int> triangles = new List<int>();

    //前面の頂点
    for (int i = 0; i < n; i++)
    {
        Vector2 v = vertice2D[i];
        vertice3.Add(new Vector3(v.x, v.y, depth));
        uvs.Add(new Vector2(v.x / inputLogo.width, v.y / inputLogo.height));
    }

    //背面の頂点
    for (int i = 0; i < n; i++)
    {
        Vector2 v = vertice2D[i];
        vertice3.Add(new Vector3(v.x, v.y, 0));
        uvs.Add(new Vector2(1 - (v.x / inputLogo.width), v.y / inputLogo.height));
    }

    for (int i = 0; i < vertice3.Count; i++)
    {
        Vector3 worldpos = transform.TransformPoint(vertice3[i]);
        Instantiate(prefab, worldpos, Quaternion.identity, transform);
    }

    //前面の三角形
    for (int i = 0; i < triangleindex.Count; i += 3)
    {
        triangles.Add(triangleindex[i + 2]);
        triangles.Add(triangleindex[i + 1]);
        triangles.Add(triangleindex[i]);
        Debug.Log($"前面:{vertice3[triangleindex[i]]},{vertice3[triangleindex[i + 1]]},{vertice3[triangleindex[i] + 2]}");
    }

    //背面の三角形
    for (int i = 0; i < triangleindex.Count; i += 3)
    {
        triangles.Add(triangleindex[i] + n);
        triangles.Add(triangleindex[i + 1] + n);
        triangles.Add(triangleindex[i + 2] + n);
        Debug.Log($"背面:{vertice3[triangleindex[i + 2] + n]},{vertice3[triangleindex[i + 1] + n]},{vertice3[triangleindex[i] + n]}");
    }

    //側面
    
    float totalLength = 0f;
    float[] cumLength = new float[n];
    cumLength[0] = 0;

    for (int i = 1; i < n; i++)
    {
        totalLength += Vector2.Distance(vertice2D[i - 1], vertice2D[i]);
        cumLength[i] = totalLength;
    }
    totalLength += Vector2.Distance(vertice2D[n - 1], vertice2D[0]);

    int sideStartIndex = vertice3.Count;

    for(int i = 0; i < n; i++)
    {
        int next = (i + 1) % n;
        float u0 = cumLength[i] / totalLength;
        float u1 = (i == n - 1) ? 1.0f : cumLength[next] / totalLength;

        //前面
        Vector2 frontCurrent = vertice2D[i];
        vertice3.Add(new Vector3(frontCurrent.x, frontCurrent.y, depth));
        uvs.Add(new Vector2(u0, 1));

        Vector2 frontNext = vertice2D[next];
        vertice3.Add(new Vector3(frontNext.x, frontNext.y, depth));
        uvs.Add(new Vector2(u1, 1));

        //背面
        Vector2 backCurrent = vertice2D[i];
        vertice3.Add(new Vector3(backCurrent.x, backCurrent.y, 0));
        uvs.Add(new Vector2(u0, 0));

        Vector2 backNext = vertice2D[next];
        vertice3.Add(new Vector3(backNext.x, backNext.y, 0));
        uvs.Add(new Vector2(u1, 0));

        int baseIndex = sideStartIndex + i * 4;

        triangles.Add(baseIndex);
        triangles.Add(baseIndex + 3);
        triangles.Add(baseIndex + 1);

        triangles.Add(baseIndex);
        triangles.Add(baseIndex + 2);
        triangles.Add(baseIndex + 3);
    }

    //vertices=> vertice3 contour => vertice2D
    mesh.vertices = vertice3.ToArray();
    mesh.triangles = triangles.ToArray();
    mesh.uv = uvs.ToArray();
    mesh.RecalculateNormals();
    mesh.RecalculateBounds();

    return mesh;
}
スクリーンショット 2025-03-13 164718.png

頂点カラーではなくて、uv座標から算出してマテリアルを作れるように書き換えましたが、色がつかない。元の画像(白黒でないもの)を使って出力しているはずなのにマスク画像のほうで出力がされてるように見える。

しかもなぜかモデルが左右逆。

シェーダーが違うのかもしれない。。。

ゲームシーンではマテリアルがはげたまっピンクだけど正しい向きで生成されたものが表示されているのにobjにエクスポートすると向きが逆になっててマテリアルも逆になってるのが本当に意味が分からない。


3/28

前回作成したスクリプトを思い出すために読み解きと整理します。

できた!?!?!!!

側面の色は付いていないです!

それと裏面のテクスチャは左右反転して貼られています。

スクリーンショット 2025-03-28 105109.png

背面と前面のuv参照の仕方、出力の際の左右反転を解消できたスクリプトです。

  • 統合したスクリプト

    csharp
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using OpenCvSharp;
    using System.IO;
    using System.Text;
    using UnityEngine.UIElements;
    using UnityEditor.Build;
    using System;
    using UnityEngine.UI;
    using UnityEngine.Rendering;
    using System.Linq;
    public class LogoModelManager : MonoBehaviour
    {
        [Header("入力画像")]
        [SerializeField]
        public Texture2D inputLogo;
    
        [Header("モデルの厚さ")]
        [SerializeField, Range(0f, 20f)]
        public float Depth = 5f;
    
        [Header("無視して生成する面積の閾値")]
        public double minAreaThreshold = 0.005;
    
        [Header("生成先の親オブジェクト")]
        [SerializeField]
        public GameObject parentObject;
        public RawImage preview;
    
        [Header("出力先のファイルパス")]
        [SerializeField]
        public string PathToOutput = "Assets/output/Output.obj";
    
        [SerializeField]
        public GameObject prefab;
        private Mesh generatedMesh;
        
        void Start()
        {
            //出力先の確認
            string outputDir = Path.GetDirectoryName(PathToOutput);
            if (!Directory.Exists(outputDir))
            {
                Directory.CreateDirectory(outputDir);
            }
    
            string textureFileName = "InputLogo.png";
            Texture2D tempTex = new Texture2D(inputLogo.width, inputLogo.height, TextureFormat.RGBA32, false);
            tempTex.SetPixels(inputLogo.GetPixels());
            tempTex.Apply();
    
            string texturePath = Path.Combine(outputDir, textureFileName);
            File.WriteAllBytes(texturePath, tempTex.EncodeToPNG());
    
            //輪郭線の作成
            List<List<Vector2>> allOutLines = ExtractOutLine(inputLogo);
    
            if (allOutLines == null || allOutLines.Count == 0)
            {
                Debug.LogError("輪郭線が作れんかった");
                return;
            }
            Debug.Log("輪郭線が作れた");
    
            List<Mesh> meshs = new List<Mesh>();
            DelaunayTriangulation triangulation = new DelaunayTriangulation();
    
            //三角形分割,メッシュ生成
            foreach (List<Vector2> outlinePoints in allOutLines)
            {
                List<int> triangles;
                triangulation.Triangulator(outlinePoints, out triangles);
    
                //メッシュ化
                Mesh mesh = CreateMesh(outlinePoints,triangles,Depth);
    
                //メッシュ生成
                GameObject contourObj = new GameObject("childMesh");
                contourObj.transform.parent = parentObject.transform;
    
                MeshFilter mf = contourObj.AddComponent<MeshFilter>();
                mf.mesh = mesh;
    
                //Material vertexColoredMat = new Material(Shader.Find("Unlit/VertexColorShader"));
    
                MeshRenderer mr = contourObj.AddComponent<MeshRenderer>();
                //mr.material = vertexColoredMat;
            }
            Debug.Log("メッシュ生成できた");
    
            //メッシュのマージ
            Mesh mergeMesh = MergeChildMeshes(parentObject);
            Debug.Log("メッシュのマージが完了した");
    
            //MTLファイルの出力(OBJと同じ出力フォルダに出力、参照テクスチャ名を "InputLogo.png" に変更)
            string mtlPath = Path.Combine(outputDir, "Output.mtl");
            ExportMTL(mtlPath, textureFileName);
    
            //obj出力
            ExportCombinedMeshToOBJ(parentObject,PathToOutput);
            Debug.Log("出力した");
        }
    
        /// <summary>
        /// 輪郭点抽出
        /// </summary>
        /// <param name="_input"></param>
        /// <returns></returns>
        List<List<Vector2>> ExtractOutLine(Texture2D _input)
        {
            Mat src = OpenCvSharp.Unity.TextureToMat(_input);
            //グレースケールに変換
            preview = GetComponent<RawImage>();
            Mat input = OpenCvSharp.Unity.TextureToMat(_input);
            Mat gray = new Mat();
            Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
    
            //二値化
            Mat binary = new Mat();
            Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
    
            //色反転してマスク画像にする
            Mat Mask = new Mat();
            Cv2.BitwiseNot(binary, Mask);
    
            //previewにマスク画像を表示
            if (preview != null)
            {
                Texture2D previewTex = new Texture2D(Mask.Width, Mask.Height, TextureFormat.R8, false);
                OpenCvSharp.Unity.MatToTexture(Mask, previewTex);
                preview.texture = previewTex;
            }
    
            Point[][] contours;
            HierarchyIndex[] hierarchy;
            Cv2.FindContours
                (
                Mask,
                out contours,
                out hierarchy,
                RetrievalModes.List,
                ContourApproximationModes.ApproxSimple
                );
    
            List<List<Vector2>> contourList = new List<List<Vector2>>();
            double minArea = (src.Width * src.Height) * minAreaThreshold;
    
            //輪郭線配列への追加
            for (int i = 0; i < contours.Length; i++)
            {
                double area = Cv2.ContourArea(contours[i]);
                if (area < minArea)
                    continue;
                List<Vector2> points = new List<Vector2>();
    
                foreach (var p in contours[i])
                {
                    float correctedY = _input.height - p.Y;
                    points.Add(new Vector2(p.X, correctedY));
                }
                contourList.Add(points);
            }
    
            return contourList;
        }
    
        private Mesh CreateMesh(List<Vector2> vertice2D, List<int> triangleindex, float depth)
        {
            Mesh mesh = new Mesh();
    
            int n = vertice2D.Count;
    
            List<Vector3> vertice3 = new List<Vector3>();
            List<Vector2> uvs = new List<Vector2>();
            List<int> triangles = new List<int>();
    
            // 前面の頂点
            for (int i = 0; i < n; i++)
            {
                Vector2 v = vertice2D[i];
                vertice3.Add(new Vector3(v.x, v.y, depth));
                uvs.Add(new Vector2(v.x / inputLogo.width, v.y / inputLogo.height));
            }
    
            // 背面の頂点
            for (int i = 0; i < n; i++)
            {
                Vector2 v = vertice2D[i];
                vertice3.Add(new Vector3(v.x, v.y, 0));
                uvs.Add(new Vector2(v.x / inputLogo.width, v.y / inputLogo.height));
            }
    
            // 各頂点にプレハブを配置(デバッグ用)
            for (int i = 0; i < vertice3.Count; i++)
            {
                Vector3 worldpos = transform.TransformPoint(vertice3[i]);
                Instantiate(prefab, worldpos, Quaternion.identity, transform);
            }
    
            // 前面の三角形(頂点の順番は逆順で法線を外向きに)
            for (int i = 0; i < triangleindex.Count; i += 3)
            {
                triangles.Add(triangleindex[i + 2]);
                triangles.Add(triangleindex[i + 1]);
                triangles.Add(triangleindex[i]);
            }
    
            // 背面の三角形
            for (int i = 0; i < triangleindex.Count; i += 3)
            {
                triangles.Add(triangleindex[i] + n);
                triangles.Add(triangleindex[i + 1] + n);
                triangles.Add(triangleindex[i + 2] + n);
            }
    
            // 側面生成
            float totalLength = 0f;
            float[] cumLength = new float[n];
            cumLength[0] = 0;
            for (int i = 1; i < n; i++)
            {
                totalLength += Vector2.Distance(vertice2D[i - 1], vertice2D[i]);
                cumLength[i] = totalLength;
            }
            totalLength += Vector2.Distance(vertice2D[n - 1], vertice2D[0]);
    
            int sideStartIndex = vertice3.Count;
            for (int i = 0; i < n; i++)
            {
                int next = (i + 1) % n;
                float u0 = cumLength[i] / totalLength;
                float u1 = (i == n - 1) ? 1.0f : cumLength[next] / totalLength;
    
                // 前面側
                Vector2 frontCurrent = vertice2D[i];
                vertice3.Add(new Vector3(frontCurrent.x, frontCurrent.y, depth));
                uvs.Add(new Vector2(u0, 1));
    
                Vector2 frontNext = vertice2D[next];
                vertice3.Add(new Vector3(frontNext.x, frontNext.y, depth));
                uvs.Add(new Vector2(u1, 1));
    
                // 背面側
                Vector2 backCurrent = vertice2D[i];
                vertice3.Add(new Vector3(backCurrent.x, backCurrent.y, 0));
                uvs.Add(new Vector2(u0, 0));
    
                Vector2 backNext = vertice2D[next];
                vertice3.Add(new Vector3(backNext.x, backNext.y, 0));
                uvs.Add(new Vector2(u1, 0));
    
                int baseIndex = sideStartIndex + i * 4;
                triangles.Add(baseIndex);
                triangles.Add(baseIndex + 3);
                triangles.Add(baseIndex + 1);
    
                triangles.Add(baseIndex);
                triangles.Add(baseIndex + 2);
                triangles.Add(baseIndex + 3);
            }
    
            mesh.vertices = vertice3.ToArray();
            mesh.triangles = triangles.ToArray();
            mesh.uv = uvs.ToArray();
            mesh.RecalculateNormals();
            mesh.RecalculateBounds();
    
            return mesh;
        }
    
        /// <summary>
        /// 親オブジェクトの子オブジェクトを探索して、そのメッシュフィルターからメッシュをとってきて統合する
        /// </summary>
        /// <param name="parent"></param>
        /// <returns></returns>
        Mesh MergeChildMeshes(GameObject parent)
        {
            List<Vector3> mergedVertices = new List<Vector3>();
            List<Vector2> mergedUV = new List<Vector2>();
            List<Vector3> mergedNormals = new List<Vector3>();
            List<int> mergedTriangles = new List<int>();
            List<Color> mergedColors = new List<Color>();
    
            int vertexOffset = 0;
    
            foreach (Transform child in parent.transform)
            {
                MeshFilter mf = child.GetComponent<MeshFilter>();
                if (mf == null) continue;
    
                Mesh mesh = mf.mesh;
    
                mergedVertices.AddRange(mesh.vertices);
                mergedUV.AddRange(mesh.uv);
                mergedNormals.AddRange(mesh.normals);
    
                if (mesh.colors != null && mesh.colors.Length > 0)
                {
                    mergedColors.AddRange(mesh.colors);
                }
                else
                {
                    for (int i = 0; i < mesh.vertexCount; i++)
                    {
                        mergedColors.Add(Color.white);
                    }
                }
    
                foreach (int index in mesh.triangles)
                {
                    mergedTriangles.Add(index + vertexOffset);
                }
    
                vertexOffset += mesh.vertexCount;
            }
    
            Mesh mergedMesh = new Mesh();
            mergedMesh.vertices = mergedVertices.ToArray();
            mergedMesh.uv = mergedUV.ToArray();
            mergedMesh.normals = mergedNormals.ToArray();
            mergedMesh.triangles = mergedTriangles.ToArray();
            mergedMesh.colors = mergedColors.ToArray();
    
            mergedMesh.RecalculateBounds();
    
            return mergedMesh;
    
        }
    
        void ExportMTL(string mtlFilePath, string textureFilename)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("newmtl material0");
            sb.AppendLine("Ka 1.000 1.000 1.000");
            sb.AppendLine("Kd 1.000 1.000 1.000");
            sb.AppendLine("Ks 0.000 0.000 0.000");
            sb.AppendLine("d 1.0");
            sb.AppendLine("illum 2");
            sb.AppendLine($"map_Kd {textureFilename}");
            File.WriteAllText(mtlFilePath, sb.ToString());
            Debug.Log("MTLファイル出力:" + mtlFilePath);
        }
    
        /// <summary>
        /// obj出力
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="filePath"></param>
        void ExportCombinedMeshToOBJ(GameObject parent, string filePath)
        {
            Mesh combinedMesh = MergeChildMeshes(parent);
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# Exported from Unity (combinedMesh)");
            // MTLファイル参照の記述
            sb.AppendLine("mtllib Output.mtl");
            // マテリアル指定
            sb.AppendLine("usemtl material0");
    
            // 頂点情報(X座標を反転)
            foreach (Vector3 v in combinedMesh.vertices)
            {
                sb.AppendLine($"v {-v.x} {v.y} {v.z}");
            }
    
            // UV情報
            if (combinedMesh.uv.Length > 0)
            {
                foreach (Vector2 uv in combinedMesh.uv)
                {
                    sb.AppendLine($"vt {uv.x} {uv.y}");
                }
            }
    
            // 法線情報(X成分を反転)
            if (combinedMesh.normals.Length > 0)
            {
                foreach (Vector3 n in combinedMesh.normals)
                {
                    sb.AppendLine($"vn {-n.x} {n.y} {n.z}");
                }
            }
    
            // 面情報(各面の頂点順を反転して出力、OBJは1-indexedのため+1)
            for (int i = 0; i < combinedMesh.triangles.Length; i += 3)
            {
                int idx0 = combinedMesh.triangles[i] + 1;
                int idx1 = combinedMesh.triangles[i + 1] + 1;
                int idx2 = combinedMesh.triangles[i + 2] + 1;
                sb.AppendLine($"f {idx0}/{idx0}/{idx0} {idx2}/{idx2}/{idx2} {idx1}/{idx1}/{idx1}");
            }
    
            File.WriteAllText(filePath, sb.ToString());
            Debug.Log("OBJファイル出力:" + filePath);
        }
    
    }

https://youtu.be/xH3YFP5v1NY

以下テスト

部品が複数に分かれていたり、細かい部品がたくさんあっても一つのオブジェクトとして出力できます。

色が違う場合もきれいに出力ができます。

がたがたしていたり、うまくテクスチャが作れていないのはおそらく入力画像の解像度が低かったり、サイズが小さいとopencvがうまく輪郭点抽出ができないんだと思います。

スクリーンショット 2025-03-28 115557.pngスクリーンショット 2025-03-28 115830.png

現在の課題は側面のテクスチャが真っ白もしくは変なところから取ってきている色になっていることです。

リサーチ中ですが改善方法としては、頂点カラーを利用する、サブメッシュを利用してマテリアルを分ける、カスタムシェーダーを利用するという方法があるみたいなのですが、頂点カラーを利用する方法は一度失敗しているので、いっそのこと側面は一色にする、またはサブメッシュを使ってみる方法を試してみたいと思います。

側面の色を、画像の最頻値をとってきてその色一色にする方法を試してみます。

サブメッシュを使うとマテリアルが増えてしまう。ゲームを作っていた時、キャラクタを実装する際にパーツ分けしてマテリアルが10個くらいになることはよくあったのでそれ自体は問題ないと思うけど、たった一つ平面のロゴを作るだけにマテリアルを分割しないといけないのは汚い気がする。

テクスチャ自体を編集する方法も考えたが、今回はテクスチャ用の画像を用意せず、元画像を引用してテクスチャにしているため、uv展開したときに前面背面側面で参照しているuv座標が重複しているのでテクスチャの編集をすると意図しない場所に影響が出てしまう可能性がある。

実装間に合わなかったです。

メッシュ分けと、マテリアルわけ、それぞれのメッシュに別々のマテリアルを適応させて出力させるところまではできたのですが、色が真っ白になって出力されてしまい、できませんでした。

  • 上手くいかなかったコード

    csharp
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using OpenCvSharp;
    using System.IO;
    using System.Text;
    using UnityEngine.UIElements;
    using UnityEditor.Build;
    using System;
    using UnityEngine.UI;
    using UnityEngine.Rendering;
    using System.Linq;
    public class LogoModelManager : MonoBehaviour
    {
        [Header("入力画像")]
        [SerializeField]
        public Texture2D inputLogo;
    
        [Header("モデルの厚さ")]
        [SerializeField, Range(0f, 20f)]
        public float Depth = 5f;
    
        [Header("無視して生成する面積の閾値")]
        public double minAreaThreshold = 0.005;
    
        [Header("生成先の親オブジェクト")]
        [SerializeField]
        public GameObject parentObject;
        public RawImage preview;
    
        [Header("出力先のファイルパス")]
        [SerializeField]
        public string PathToOutput = "Assets/output/Output.obj";
    
        [SerializeField]
        public GameObject prefab;
        private Mesh generatedMesh;
    
        private Color sideColor;
        
        void Start()
        {
            //出力先の確認
            string outputDir = Path.GetDirectoryName(PathToOutput);
            if (!Directory.Exists(outputDir))
            {
                Directory.CreateDirectory(outputDir);
            }
    
            //元画像をとっておく処理
            string textureFileName = "InputLogo.png";
            Texture2D tempTex = new Texture2D(inputLogo.width, inputLogo.height, TextureFormat.RGBA32, false);
            tempTex.SetPixels(inputLogo.GetPixels());
            tempTex.Apply();
    
            string texturePath = Path.Combine(outputDir, textureFileName);
            File.WriteAllBytes(texturePath, tempTex.EncodeToPNG());
    
            sideColor = GetModeColor(tempTex);
    
            //輪郭線の作成
            List<List<Vector2>> allOutLines = ExtractOutLine(inputLogo);
    
            if (allOutLines == null || allOutLines.Count == 0)
            {
                Debug.LogError("輪郭線が作れんかった");
                return;
            }
            Debug.Log("輪郭線が作れた");
    
            List<Mesh> meshs = new List<Mesh>();
            DelaunayTriangulation triangulation = new DelaunayTriangulation();
    
            //三角形分割,メッシュ生成
            foreach (List<Vector2> outlinePoints in allOutLines)
            {
                List<int> triangles;
                triangulation.Triangulator(outlinePoints, out triangles);
    
                //メッシュ化
                Mesh mesh = CreateMesh(outlinePoints,triangles,Depth);
    
                //メッシュ生成
                GameObject contourObj = new GameObject("childMesh");
                contourObj.transform.parent = parentObject.transform;
    
                MeshFilter mf = contourObj.AddComponent<MeshFilter>();
                mf.mesh = mesh;
    
                //Material vertexColoredMat = new Material(Shader.Find("Unlit/VertexColorShader"));
    
                MeshRenderer mr = contourObj.AddComponent<MeshRenderer>();
                //mr.material = vertexColoredMat;
            }
            Debug.Log("メッシュ生成できた");
    
            //メッシュのマージ
            Mesh mergeMesh = MergeChildMeshes(parentObject);
            Debug.Log("メッシュのマージが完了した");
    
            //MTLファイルの出力(OBJと同じ出力フォルダに出力、参照テクスチャ名を "InputLogo.png" に変更)
            string mtlPath = Path.Combine(outputDir, "Output.mtl");
            ExportMTL(mtlPath, textureFileName);
    
            //obj出力
            theExportCombinedMeshToOBJ(parentObject,PathToOutput);
            Debug.Log("出力した");
        }
    
        /// <summary>
        /// 輪郭点抽出
        /// </summary>
        /// <param name="_input"></param>
        /// <returns></returns>
        List<List<Vector2>> ExtractOutLine(Texture2D _input)
        {
            Mat src = OpenCvSharp.Unity.TextureToMat(_input);
            //グレースケールに変換
            preview = GetComponent<RawImage>();
            Mat input = OpenCvSharp.Unity.TextureToMat(_input);
            Mat gray = new Mat();
            Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
    
            //二値化
            Mat binary = new Mat();
            Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
    
            //色反転してマスク画像にする
            Mat Mask = new Mat();
            Cv2.BitwiseNot(binary, Mask);
    
            //previewにマスク画像を表示
            if (preview != null)
            {
                Texture2D previewTex = new Texture2D(Mask.Width, Mask.Height, TextureFormat.R8, false);
                OpenCvSharp.Unity.MatToTexture(Mask, previewTex);
                preview.texture = previewTex;
            }
    
            Point[][] contours;
            HierarchyIndex[] hierarchy;
            Cv2.FindContours
                (
                Mask,
                out contours,
                out hierarchy,
                RetrievalModes.List,
                ContourApproximationModes.ApproxSimple
                );
    
            List<List<Vector2>> contourList = new List<List<Vector2>>();
            double minArea = (src.Width * src.Height) * minAreaThreshold;
    
            //輪郭線配列への追加
            for (int i = 0; i < contours.Length; i++)
            {
                double area = Cv2.ContourArea(contours[i]);
                if (area < minArea)
                    continue;
                List<Vector2> points = new List<Vector2>();
    
                foreach (var p in contours[i])
                {
                    float correctedY = _input.height - p.Y;
                    points.Add(new Vector2(p.X, correctedY));
                }
                contourList.Add(points);
            }
    
            return contourList;
        }
    
        /// <summary>
        /// 側面ように画像の色の最頻値をとる
        /// </summary>
        /// <param name="tex"></param>
        /// <returns></returns>
        Color GetModeColor(Texture2D tex)
        {
            Color[] pixels = tex.GetPixels();
            Dictionary<Color, int> colorCount = new Dictionary<Color, int>();
            foreach (Color col in pixels)
            {
                if (col.a == 0.5f)
                    continue;
    
                Color rounded = new Color
                    (
                        Mathf.Round(col.r * 10f) / 10f,
                        Mathf.Round(col.g * 10f) / 10f,
                        Mathf.Round(col.b * 10f) / 10f,
                        Mathf.Round(col.a * 10f) / 10f
                    );
    
                if (!colorCount.ContainsKey(rounded))
                {
                    colorCount[rounded] = 0;
                }
    
                colorCount[rounded]++;
            }
    
            Color modeColor = Color.white;
            int maxCount = 0;
    
            foreach(var keyValue in colorCount)
            {
                if (keyValue.Value > maxCount)
                {
                    maxCount = keyValue.Value;
                    modeColor = keyValue.Key;
                }
            }
    
            return modeColor;
    
        }
    
        private Mesh CreateMesh(List<Vector2> vertice2D, List<int> triangleindex, float depth)
        {
            Mesh mesh = new Mesh();
    
            int n = vertice2D.Count;
    
            List<Vector3> vertice3 = new List<Vector3>();
            List<Vector2> uvs = new List<Vector2>();
            List<int> triangles = new List<int>();
    
            List<int> frontTriangles = new List<int>();
            List<int> sideTriangles = new List<int>();
    
            //前面の頂点
            for (int i = 0; i < n; i++)
            {
                Vector2 v = vertice2D[i];
                vertice3.Add(new Vector3(v.x, v.y, depth));
                uvs.Add(new Vector2(v.x / inputLogo.width, v.y / inputLogo.height));
            }
    
            //背面の頂点
            for (int i = 0; i < n; i++)
            {
                Vector2 v = vertice2D[i];
                vertice3.Add(new Vector3(v.x, v.y, 0));
                uvs.Add(new Vector2(v.x / inputLogo.width, v.y / inputLogo.height));
            }
    
            //各頂点にプレハブを配置(デバッグ用)
            for (int i = 0; i < vertice3.Count; i++)
            {
                Vector3 worldpos = transform.TransformPoint(vertice3[i]);
                Instantiate(prefab, worldpos, Quaternion.identity, transform);
            }
    
            //前面の三角形
            for (int i = 0; i < triangleindex.Count; i += 3)
            {
                frontTriangles.Add(triangleindex[i + 2]);
                frontTriangles.Add(triangleindex[i + 1]);
                frontTriangles.Add(triangleindex[i]);
            }
    
            //背面の三角形
            for (int i = 0; i < triangleindex.Count; i += 3)
            {
                frontTriangles.Add(triangleindex[i] + n);
                frontTriangles.Add(triangleindex[i + 1] + n);
                frontTriangles.Add(triangleindex[i + 2] + n);
            }
    
            //-----側面-----
            float totalLength = 0f;
            float[] cumLength = new float[n];
            cumLength[0] = 0;
    
            //厚みだし
            for (int i = 1; i < n; i++)
            {
                totalLength += Vector2.Distance(vertice2D[i - 1], vertice2D[i]);
                cumLength[i] = totalLength;
            }
            totalLength += Vector2.Distance(vertice2D[n - 1], vertice2D[0]);
    
            //ここで生成
            int sideStartIndex = vertice3.Count;
            for (int i = 0; i < n; i++)
            {
                int next = (i + 1) % n;
                float u0 = cumLength[i] / totalLength;
                float u1 = (i == n - 1) ? 1.0f : cumLength[next] / totalLength;
    
                //前面側
                Vector2 frontCurrent = vertice2D[i];
                vertice3.Add(new Vector3(frontCurrent.x, frontCurrent.y, depth));
                uvs.Add(new Vector2(u0, 1));
    
                Vector2 frontNext = vertice2D[next];
                vertice3.Add(new Vector3(frontNext.x, frontNext.y, depth));
                uvs.Add(new Vector2(u1, 1));
    
                //背面側
                Vector2 backCurrent = vertice2D[i];
                vertice3.Add(new Vector3(backCurrent.x, backCurrent.y, 0));
                uvs.Add(new Vector2(u0, 0));
    
                Vector2 backNext = vertice2D[next];
                vertice3.Add(new Vector3(backNext.x, backNext.y, 0));
                uvs.Add(new Vector2(u1, 0));
    
                int baseIndex = sideStartIndex + i * 4;
                sideTriangles.Add(baseIndex);
                sideTriangles.Add(baseIndex + 3);
                sideTriangles.Add(baseIndex + 1);
    
                sideTriangles.Add(baseIndex);
                sideTriangles.Add(baseIndex + 2);
                sideTriangles.Add(baseIndex + 3);
            }
    
            mesh.vertices = vertice3.ToArray();
            mesh.uv = uvs.ToArray();
    
            mesh.subMeshCount = 2;
    
            mesh.SetTriangles(frontTriangles, 0);
            mesh.SetTriangles(sideTriangles, 1);
    
            mesh.RecalculateNormals();
            mesh.RecalculateBounds();
    
            return mesh;
        }
    
        /// <summary>
        /// 親オブジェクトの子オブジェクトを探索して、そのメッシュフィルターからメッシュをとってきて統合する
        /// </summary>
        /// <param name="parent"></param>
        /// <returns></returns>
        Mesh MergeChildMeshes(GameObject parent)
        {
            List<Vector3> mergedVertices = new List<Vector3>();
            List<Vector2> mergedUV = new List<Vector2>();
            List<Vector3> mergedNormals = new List<Vector3>();
            List<int> mergedTriangles = new List<int>();
            List<Color> mergedColors = new List<Color>();
    
            int vertexOffset = 0;
    
            foreach (Transform child in parent.transform)
            {
                MeshFilter mf = child.GetComponent<MeshFilter>();
                if (mf == null) continue;
    
                Mesh mesh = mf.mesh;
    
                mergedVertices.AddRange(mesh.vertices);
                mergedUV.AddRange(mesh.uv);
                mergedNormals.AddRange(mesh.normals);
    
                if (mesh.colors != null && mesh.colors.Length > 0)
                {
                    mergedColors.AddRange(mesh.colors);
                }
                else
                {
                    for (int i = 0; i < mesh.vertexCount; i++)
                    {
                        mergedColors.Add(Color.white);
                    }
                }
    
                foreach (int index in mesh.triangles)
                {
                    mergedTriangles.Add(index + vertexOffset);
                }
    
                vertexOffset += mesh.vertexCount;
            }
    
            Mesh mergedMesh = new Mesh();
            mergedMesh.vertices = mergedVertices.ToArray();
            mergedMesh.uv = mergedUV.ToArray();
            mergedMesh.normals = mergedNormals.ToArray();
            mergedMesh.triangles = mergedTriangles.ToArray();
            mergedMesh.colors = mergedColors.ToArray();
    
            mergedMesh.RecalculateBounds();
    
            return mergedMesh;
    
        }
    
        void ExportMTL(string mtlFilePath, string textureFilename)
        {
            StringBuilder sb = new StringBuilder();
            // material0: 前面・背面用(従来どおり、テクスチャを使用)
            sb.AppendLine("newmtl material0");
            sb.AppendLine("Ka 1.000 1.000 1.000");
            sb.AppendLine("Kd 1.000 1.000 1.000");
            sb.AppendLine("Ks 0.000 0.000 0.000");
            sb.AppendLine("d 1.0");
            sb.AppendLine("illum 2");
            sb.AppendLine($"map_Kd {textureFilename}");
            sb.AppendLine("");
    
            // material1: 側面用。sideColor を反映(RGBは 0~1 の値をそのまま使う)
            sb.AppendLine("newmtl material1");
            // アンビエントもディフューズも sideColor に設定
            sb.AppendLine($"Ka {sideColor.r:F3} {sideColor.g:F3} {sideColor.b:F3}");
            sb.AppendLine($"Kd {sideColor.r:F3} {sideColor.g:F3} {sideColor.b:F3}");
            sb.AppendLine("Ks 0.000 0.000 0.000");
            sb.AppendLine("d 1.0");
            sb.AppendLine("illum 2");
    
            File.WriteAllText(mtlFilePath, sb.ToString());
            Debug.Log("MTLファイル出力:" + mtlFilePath);
        }
    
        /// <summary>
        /// obj出力
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="filePath"></param>
        void ExportCombinedMeshToOBJ(GameObject parent, string filePath)
        {
            Mesh combinedMesh = MergeChildMeshes(parent);
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# Exported from Unity (combinedMesh)");
            // MTLファイル参照の記述
            sb.AppendLine("mtllib Output.mtl");
            // マテリアル指定
            sb.AppendLine("usemtl material0");
    
            // 頂点情報(X座標を反転)
            foreach (Vector3 v in combinedMesh.vertices)
            {
                sb.AppendLine($"v {-v.x} {v.y} {v.z}");
            }
    
            // UV情報(そのまま)
            if (combinedMesh.uv.Length > 0)
            {
                foreach (Vector2 uv in combinedMesh.uv)
                {
                    sb.AppendLine($"vt {uv.x} {uv.y}");
                }
            }
    
            // 法線情報(X成分を反転)
            if (combinedMesh.normals.Length > 0)
            {
                foreach (Vector3 n in combinedMesh.normals)
                {
                    sb.AppendLine($"vn {-n.x} {n.y} {n.z}");
                }
            }
    
            // 面情報(各面の頂点順を反転して出力、OBJは1-indexedのため+1)
            for (int i = 0; i < combinedMesh.triangles.Length; i += 3)
            {
                int idx0 = combinedMesh.triangles[i] + 1;
                int idx1 = combinedMesh.triangles[i + 1] + 1;
                int idx2 = combinedMesh.triangles[i + 2] + 1;
                sb.AppendLine($"f {idx0}/{idx0}/{idx0} {idx2}/{idx2}/{idx2} {idx1}/{idx1}/{idx1}");
            }
    
            File.WriteAllText(filePath, sb.ToString());
            Debug.Log("OBJファイル出力:" + filePath);
        }
    
        void theExportCombinedMeshToOBJ(GameObject parent, string filePath)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# Exported from Unity (combinedMesh with submeshes)");
    
            sb.AppendLine("mtllib Output.mtl");
    
            int vertexOffset = 0;
    
            foreach (Transform child in parent.transform)
            {
                MeshFilter mf = child.GetComponent<MeshFilter>();
                if (mf == null)
                    continue;
    
                Mesh mesh = mf.mesh;
    
                sb.AppendLine("o " + child.name);
    
                for (int i = 0; i < mesh.vertexCount; i++)
                {
                    Vector3 v = child.transform.TransformPoint(mesh.vertices[i]);
                    sb.AppendLine($"v {-v.x} {v.y} {v.z}");
                }
    
                for (int i = 0; i < mesh.uv.Length; i++)
                {
                    Vector2 uv = mesh.uv[i];
                    sb.AppendLine($"vt {uv.x} {uv.y}");
                }
    
                for (int i = 0; i < mesh.normals.Length; i++)
                {
                    Vector3 n = child.transform.TransformDirection(mesh.normals[i]); 
                    sb.AppendLine($"vn {-n.x} {n.y} {n.z}");
                }
    
                for (int sub = 0; sub < mesh.subMeshCount; sub++)
                {
                    string materialName = (sub == 1) ? "material1" : "material0";
                    sb.AppendLine("g " + child.name + "_submesh" + sub);
                    sb.AppendLine("usemtl " + materialName);
    
                    int[] triangles = mesh.GetTriangles(sub);
    
                    for (int i = 0; i < triangles.Length; i += 3)
                    {
                        int idx0 = triangles[i] + 1 + vertexOffset;
                        int idx1 = triangles[i + 1] + 1 + vertexOffset;
                        int idx2 = triangles[i + 2] + 1 + vertexOffset;
                        // 面情報。ここでは、前面側の頂点順を逆順に出力して整合性を取ります。
                        sb.AppendLine($"f {idx0}/{idx0}/{idx0} {idx2}/{idx2}/{idx2} {idx1}/{idx1}/{idx1}");
                    }
                }
    
                vertexOffset += mesh.vertexCount;
            }
    
            File.WriteAllText(filePath, sb.ToString());
            Debug.Log("OBJファイル出力(サブメッシュ対応):" + filePath);
        }
    }

メモ

Python/OpenCVで輪郭検出と輪郭内領域の塗りつぶし - knowwell-livewellの日記

【Unity】実機上で外部ファイルの読み込み書き出しできるフリープラグインStandaloneFileBrowserの使い方 - ゲーム開発備忘録

【Unity】Ear Clipping Triangulation - Qiita

Class Cv2

Douglas-Peucker アルゴリズム(正式名称:Ramer-Douglas-Peucker アルゴリズム)は、曲線や折れ線の点の数を削減し、元の形状を大まかに再現するための手法です。主なポイントは以下の通りです:

  • 目的

    点の数を減らすことで、データ量や計算コストを削減しつつ、重要な形状情報は保持することが目的です。

  • 動作原理

    始点と終点を直線で結び、その直線から各中間点までの距離を計算します。最大の距離が所定の閾値(epsilon)以下であれば、その直線で元の形状を十分に近似できると判断し、中間点を削除します。もし閾値を超える場合は、その点を残し、直線を分割して再帰的に同じ処理を行います。

  • OpenCVでの利用

    OpenCV では、このアルゴリズムを用いた輪郭近似関数 approxPolyDP が提供されています。これを使うと、輪郭の点の数を減らし、よりシンプルな形状に変換することができます。例えば、以下のように使います:

    csharp
    csharp
    コピーする
    // contours は検出された輪郭のリスト
    // epsilon は許容する誤差(例: 輪郭の長さの1%程度)
    double epsilon = 0.01 * Cv2.ArcLength(contour, true);
    Point[] approxCurve = Cv2.ApproxPolyDP(contour, epsilon, true);

このアルゴリズムは、地図データの簡略化やパターン認識、物体検出など、さまざまな分野で利用されています。

↑これ使えそう。

【Unity】マテリアルのテクスチャ画像をスクリプトから取得して他のテクスチャに変更する方法! - umi studio blog


Author: 松崎 | Source: 松崎\会社ロゴなどのimg2model 1a6aba435ee780298ad9d3c59a93c6df.md