ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스파르타) The Last Rollback (D-13, Node.js 게임 서버 최종 프로젝트) - DotRecast
    TIL 2024. 8. 14. 20:49


     > 과제 진행 간 완료한 사항 및 문제점과 해결 과정을 정리해보았다.

     

    학습 키워드: Unity, C#, NavMesh, Recast & Detour, DotRecast

     

    1. Progress

    1) Unity NavMesh 정보를 추출하여 obj 파일 생성하기:

    using System.IO;
    using System.Text;
    using UnityEditor;
    using UnityEngine;
    
    // Obj exporter component based on: http://wiki.unity3d.com/index.php?title=ObjExporter
    
    public class ExportNavMeshToObj : MonoBehaviour {
    
        [MenuItem("Custom/Export NavMesh to mesh")]
        static void Export() {
            UnityEngine.AI.NavMeshTriangulation triangulatedNavMesh = UnityEngine.AI.NavMesh.CalculateTriangulation();
    
            Mesh mesh = new Mesh();
            mesh.name = "ExportedNavMesh";
            mesh.vertices = triangulatedNavMesh.vertices;
            mesh.triangles = triangulatedNavMesh.indices;
            string filename = Application.dataPath +"/" + Path.GetFileNameWithoutExtension(EditorApplication.currentScene) + " Exported NavMesh.obj";
            MeshToFile(mesh, filename);
            print("NavMesh exported as '" + filename + "'");
            AssetDatabase.Refresh();
        }
    
        static string MeshToString(Mesh mesh) {
            StringBuilder sb = new StringBuilder();
    
            sb.Append("g ").Append(mesh.name).Append("\n");
            foreach (Vector3 v in mesh.vertices) {
                sb.Append(string.Format("v {0} {1} {2}\n",v.x,v.y,v.z));
            }
            sb.Append("\n");
            foreach (Vector3 v in mesh.normals) {
                sb.Append(string.Format("vn {0} {1} {2}\n",v.x,v.y,v.z));
            }
            sb.Append("\n");
            foreach (Vector3 v in mesh.uv) {
                sb.Append(string.Format("vt {0} {1}\n",v.x,v.y));
            }
            for (int material = 0; material < mesh.subMeshCount; material++) {
                sb.Append("\n");
                //sb.Append("usemtl ").Append(mats[material].name).Append("\n");
                //sb.Append("usemap ").Append(mats[material].name).Append("\n");
    
                int[] triangles = mesh.GetTriangles(material);
                for (int i=0;i<triangles.Length;i+=3) {
                    sb.Append(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n", triangles[i]+1, triangles[i+1]+1, triangles[i+2]+1));
                }
            }
            return sb.ToString();
        }
    
        static void MeshToFile(Mesh mesh, string filename) {
            using (StreamWriter sw = new StreamWriter(filename)) {
                sw.Write(MeshToString(mesh));
            }
        }
    }

     (출처: https://discussions.unity.com/t/accessing-navmesh-vertices/472880/12)

     

     유니티에서 간단하게 현재 Scene의 NavMesh 정보를 추출하여 obj 파일로 변환해주는 코드다. 여기서 obj 파일은 Geometry 정보를 담고있는 Wavefront .obj 파일을 말한다. 프로젝트에 추가만 해두면 상단 메뉴에 Custom이라는 탭이 생기고, NavMesh를 추출할 Scene 에서 해당 탭의 'Export NavMesh to mesh' 항목을 선택하면 export가 완료된다.

     

    Figure 1. Unity로 생성된 NavMesh를 export 할 수 있다

     

    dotnet run --project src/DotRecast.Recast.Demo --framework net8.0 -c Release

     DotRecast 리포지토리를 clone한 뒤 프로젝트 root에서 위 명령어를 실행하면 Demo 프로그램이 실행된다. 유니티에서 생성된 obj 파일을 Input Mesh란의 'Load Source Geom...' 버튼으로 불러온 뒤 여러가지 테스트를 해볼 수 있다. 현재 프로젝트에서 사용중인 Dungeon Scene에서 생성한 NavMesh로부터 추출한 obj 파일을 사용하여 테스트 해봤다.

     

    Figure 2. Dungeon Scene의 NavMesh를 기반으로 생성된 obj로 진행한 테스트

     

     좌측 탭에서 Mesh 관련 설정 후 아래 Build NavMesh 버튼을 누르면 하늘색으로 NavMesh가 생성된다(Save도 가능하다). 이후 우측의 Crowd Agent Profiling에서 Agents 수를 조정하고 하단의 Start Crowd Profiling 버튼을 누르면 위와 같이 Agent 들이 생성되어 NavMesh 위를 돌아다니게 된다.

     

     몇 가지 문제점이 있는데, 첫째로 이 obj 파일이 이미 Unity의 NavMesh를 기반으로 생성되었기 때문에, 여기서 한 번 더 NavMesh를 생성하는 것은 NavMesh를 두 번 적용하는 것이 되어 맵이 약간 좁아진다. 둘째로, 자세히 보면 맵 mash 구조가 Unity에서 생성된 NavMesh와 좌우 반전된 형태라는 문제가 있다.

     

    Figure 3. clockwise winding (Unity Docs)
    Figure 4. counter-clockwise winding (Wikipedia)

     

     일단 두 번째 문제의 경우 Unity가 vertices를 clockwise로 winding하여 face를 읽는 반면에 Wavefront .obj 파일은 counter-clockwise기 때문에 좌우 반전 현상이 일어나는 것으로 파악했다. 이 현상을 해결하기 위해 앞서 가져온 export 코드를 수정하기로 했다.

     

    using System.IO;
    using System.Text;
    using UnityEditor;
    using UnityEngine;
    
    public class ExportNavMeshToObj : MonoBehaviour {
    
        [MenuItem("Custom/Export NavMesh to mesh")]
        static void Export() {
            UnityEngine.AI.NavMeshTriangulation triangulatedNavMesh = UnityEngine.AI.NavMesh.CalculateTriangulation();
    
            Mesh mesh = new Mesh();
            mesh.name = "ExportedNavMesh";
            mesh.vertices = triangulatedNavMesh.vertices;
            
            // 중심부 정의
            Vector3 center = new Vector3(-4.2f, 0f, 122.8f);
            
            // 새 vertices 배열 생성
            Vector3[] vertices = mesh.vertices;
            // vertices의 x 값 반전
            for (int i = 0; i < vertices.Length; i++)
            {
                vertices[i] = new Vector3(
                    center.x - (vertices[i].x - center.x),
                    vertices[i].y,
                    vertices[i].z
                );
            }
            // 배열 재할당
            mesh.vertices = vertices;
            mesh.triangles = triangulatedNavMesh.indices;
            string filename = Application.dataPath +"/" + Path.GetFileNameWithoutExtension(EditorApplication.currentScene) + " Exported NavMesh.obj";
            MeshToFile(mesh, filename);
            print("NavMesh exported as '" + filename + "'");
            AssetDatabase.Refresh();
        }
    
        static string MeshToString(Mesh mesh) {
            StringBuilder sb = new StringBuilder();
    
            sb.Append("g ").Append(mesh.name).Append("\n");
            foreach (Vector3 v in mesh.vertices) {
                sb.Append(string.Format("v {0} {1} {2}\n",v.x,v.y,v.z));
            }
            
            for (int material = 0; material < mesh.subMeshCount; material++) {
                sb.Append("\n");
    
                int[] triangles = mesh.GetTriangles(material);
                for (int i=0;i<triangles.Length;i+=3) {
                    sb.Append(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n",
                        triangles[i+2]+1, // i와 i+2 순서 변경
                        triangles[i+1]+1,
                        triangles[i]+1
                    ));
                }
            }
            return sb.ToString();
        }
    
        static void MeshToFile(Mesh mesh, string filename) {
            using (StreamWriter sw = new StreamWriter(filename)) {
                sw.Write(MeshToString(mesh));
            }
        }
    }

     

    Figure 5. Unity 원본 NavMesh
    Figure 6. 원본과 동일하게 좌우 반전된 obj 파일

     

     

    + 24-08-24 추가: 실제 게임에 적용하고 알게 된 사실은 이렇게 좌우로 뒤집어 놓으면 데모 앱에서는 정상처럼 보이지만 실제 좌표가 좌우로 뒤집혀서 적용되어 뒤집힌 NavMesh 위에서 게임을 플레이하게 된다. 보기에만 뒤집혀 있고 실제로는 원래의 좌표가 정상 적용되기 때문에 기존 코드를 사용하는 것이 옳다.

     

    --


    REFERENCES:

     

     

    GitHub - eliotjang/the-last-rollback-server: MORPG + Defense

    MORPG + Defense. Contribute to eliotjang/the-last-rollback-server development by creating an account on GitHub.

    github.com

     > 프로젝트 repo

    https://github.com/donkim1212/PathfindingDediServer

     

    GitHub - donkim1212/PathfindingDediServer: (WIP) 내배캠 스파르타 Node.js 5기 Team 12 최종 프로젝트 The Last Rollba

    (WIP) 내배캠 스파르타 Node.js 5기 Team 12 최종 프로젝트 The Last Rollback에서 사용될 길찾기 전용 서버 입니다. - donkim1212/PathfindingDediServer

    github.com

     > Pathfinding Dedicated Server repo

     

    https://discussions.unity.com/t/accessing-navmesh-vertices/472880/17

     > "Accessing NavMesh Vertices?", 답변 코드 참고

    https://en.wikipedia.org/wiki/Wavefront_.obj_file

     > Wikipedia, "Wavefront .obj file"

    https://docs.unity3d.com/2019.4/Documentation/Manual/AnatomyofaMesh.html

     > Unity Documentation, "Mesh Data"

     

     

    --

     

    추후 참고

     

    https://makga.tistory.com/288

     > RcNavMeshBuildSettings 값 설명

    https://www.slideshare.net/slideshow/0903-recast/10655153

     > Recast & Detour 관련 슬라이드

     

     

    728x90
Designed by Tistory.