성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>.NET으로 구현하는 OpenGL (9), (10) - OBJ File Format, Loading 3D Models</h1> <p> 아래의 글에 이어,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > .NET으로 구현하는 OpenGL (7), (8) - Matrices and Uniform Variables, Model, View & Projection Matrices ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11784'>http://www.sysnet.pe.kr/2/0/11784</a> </pre> <br /> 9회 강좌는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > OpenGL 3D Game Tutorial 9: OBJ File Format ; <a target='tab' href='https://www.youtube.com/watch?v=KMWUjNE0fYI'>https://www.youtube.com/watch?v=KMWUjNE0fYI</a> </pre> <br /> 블렌더(또는 OBJ 파일로 export하는 3D 프로그램)와 같은 도구에서 만들어준 모델을 로드하는 방법에 대해 나옵니다. (혹시나 해서 하는 말인데, 여기서 말하는 OBJ는 컴파일 시에 나오는 목적 파일이 아닙니다. ^^) 일단, 강좌에서는 블렌더를 다루니 이를 다운로드하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Download - blender.org ; <a target='tab' href='https://www.blender.org/download/'>https://www.blender.org/download/</a> </pre> <br /> 강좌에서 공개한 블렌더 예제를 함께 받아둡니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Download the stall model ; <a target='tab' href='https://www.dropbox.com/sh/7l598pr7b4zx63j/AAB_PzQl3zU2WS5hhlKgMb1Wa?dl=0'>https://www.dropbox.com/sh/7l598pr7b4zx63j/AAB_PzQl3zU2WS5hhlKgMb1Wa?dl=0</a> </pre> <br /> 압축을 풀면 다음의 파일들이 나오는데,<br /> <br /> <ul> <li>stall.blend</li> <li>stall.obj</li> <li>stallTexture.png</li> </ul> <br /> 이 중에서 stall.blend와 stallTexture.png 파일이 실제로 블렌더 프로그램으로 3D 모델을 작업할 때 다루는 파일들입니다. 그리고 이렇게 디자인한 3D 모델을 OpenGL에서 가져다 쓰기 위해 obj (텍스트) 파일로 변환을 하는 것입니다. 그러니까 사실 ^^; 블렌더를 설치할 필요 없이 그냥 저 stall.obj와 stallTexture.png 파일만 있으면 이후의 강좌를 따라 하는데 영향이 없습니다.<br /> <br /> 그래도 ^^ 그냥 강좌에 따라 stall.blend 로드한 다음에 "File" / "Export" / "Wavefront (.obj)" 메뉴를 선택하고, "Write Normals", "Include UVs", "Triangluate Faces" 옵션 체크 및 "Forward: -Z forward", "Up: Y Up" 설정 확인을 하고 Export를 해봅니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> obj 파일을 비주얼 스튜디오로 열면 자동으로 3D 렌더링 모델을 보여줍니다. 반면 텍스트 내용을 확인하기 위해 Visual Studio Code로 열어 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # Blender v2.78 (sub 0) OBJ File: 'stall.blend' # www.blender.org mtllib stall.mtl o Cube v 3.227124 -0.065127 -1.000000 v 3.227124 -0.065127 1.000000 v -3.227125 -0.065127 1.000000 ...[생략]... v 4.401308 5.051156 -0.716959 v 4.401308 4.593092 -1.289192 vt 0.1772 0.8931 vt 0.0134 0.9069 ...[생략]... vt 0.0580 0.2351 vt 0.1015 0.2351 vn 0.6673 0.1394 0.7316 vn 1.0000 0.0000 0.0000 vn 0.0000 -0.1415 -0.9899 ...[생략]... vn -0.1087 0.9935 -0.0334 vn -0.0605 0.9785 -0.1971 usemtl Material s 1 f 80/1/1 75/2/2 504/3/2 f 77/7/6 76/8/7 80/1/1 ...[생략]... f 90/24/21 94/27/24 509/28/25 f 508/29/2 95/25/22 96/26/23 usemtl Material_stall.png f 38/31/27 45/32/27 41/33/27 f 23/49/30 9/50/30 10/51/30 ...[생략]... f 492/536/124 498/627/129 497/537/128 f 493/543/125 491/629/123 497/544/128 </pre> <br /> 각 행마다 v, vt, vn,... 과 같은 식별자가 있는데 이를 통해 각각의 데이터가 어떤 데이터인지 알 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > v: vertex vt: UV texture Coordinates vn: Normal vector f: 연관이 있는 v, vt, vn에 대한 index 조합 (f position/texture-coordinates/normal position/texture-coordinates/normal position/texture-coordinates/normal) </pre> <br /> 참고로 보다 구체적인 포맷 열람은 다음의 자료에서 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Wavefront OBJ File Format Summary ; <a target='tab' href='http://www.fileformat.info/format/wavefrontobj/egff.htm'>http://www.fileformat.info/format/wavefrontobj/egff.htm</a> </pre> <br /> 이렇게 export한 파일을 아마도 Unity의 경우 다음과 같은 방법으로 import해서 쓸 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 3D 응용 프로그램에서 모델을 어떻게 임포트합니까? ; <a target='tab' href='https://docs.unity3d.com/kr/530/Manual/HOWTO-importObject.html'>https://docs.unity3d.com/kr/530/Manual/HOWTO-importObject.html</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그럼, 이것을 OpenGL에서 사용하려면 어떻게 해야 할까요? 이에 대해서 10회 강의에서 다루고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > OpenGL 3D Game Tutorial 10: Loading 3D Models ; <a target='tab' href='https://youtu.be/YKFYtekgnP8'>https://youtu.be/YKFYtekgnP8</a> </pre> <br /> 당연히 해당 텍스트 파일을 로드해서 의미에 맞게 변수 구성을 하면 됩니다. 사실, 지난 OpenGL 예제들에서 v는 vertices 변수, vt는 textureCoords 변수, f는 indices 변수로 사용했던 것들이므로 그대로 로드를 해주면 됩니다. (vn은 11회 강좌에서 다룹니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // OBJLoader.cs namespace GameApp.Helper { public class OBJLoader { public static RawModel loadObjModel(string fileName, Loader loader) { List<Vertex3f> vertices = new List<Vertex3f>(); List<Vertex2f> textures = new List<Vertex2f>(); List<Vertex3f> normals = new List<Vertex3f>(); List<int> indices = new List<int>(); float[] verticesArray = null; float[] normalsArray = null; float[] textureArray = null; string filePath = $".\\res\\{fileName}.obj"; using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) using (StreamReader sr = new StreamReader(fs)) { string line; while (true) { line = sr.ReadLine(); string[] currentLine = line.Split(' '); if (line.StartsWith("v ")) { Vertex3f vertex = new Vertex3f(float.Parse(currentLine[1]), float.Parse(currentLine[2]), float.Parse(currentLine[3])); vertices.Add(vertex); } else if (line.StartsWith("vt ")) { Vertex2f texture = new Vertex2f(float.Parse(currentLine[1]), float.Parse(currentLine[2])); textures.Add(texture); } else if (line.StartsWith("vn ")) { Vertex3f normal = new Vertex3f(float.Parse(currentLine[1]), float.Parse(currentLine[2]), float.Parse(currentLine[3])); normals.Add(normal); } else if (line.StartsWith("f ")) { textureArray = new float[vertices.Count * 2]; normalsArray = new float[vertices.Count * 3]; break; } } while (line != null) { if (line.StartsWith("f ") == false) { line = sr.ReadLine(); continue; } string[] currentLine = line.Split(' '); string[] vertex1 = currentLine[1].Split('/'); string[] vertex2 = currentLine[2].Split('/'); string[] vertex3 = currentLine[3].Split('/'); processVertex(vertex1, indices, textures, normals, textureArray, normalsArray); processVertex(vertex2, indices, textures, normals, textureArray, normalsArray); processVertex(vertex3, indices, textures, normals, textureArray, normalsArray); line = sr.ReadLine(); } } verticesArray = new float[vertices.Count * 3]; int vertexPointer = 0; foreach (Vertex3f vertex in vertices) { verticesArray[vertexPointer++] = vertex.x; verticesArray[vertexPointer++] = vertex.y; verticesArray[vertexPointer++] = vertex.z; } int [] indicesArray = indices.ToArray(); <span style='color: blue; font-weight: bold'>return loader.loadToVAO(verticesArray, textureArray, indicesArray);</span> } static void processVertex(string[] vertexData, List<int> indices, List<Vertex2f> textures, List<Vertex3f> normals, float[] textureArray, float[] normalsArray) { int currentVertexPointer = int.Parse(vertexData[0]) - 1; indices.Add(currentVertexPointer); Vertex2f currentTex = textures[int.Parse(vertexData[1]) - 1]; textureArray[currentVertexPointer * 2] = currentTex.x; textureArray[currentVertexPointer * 2 + 1] = 1 - currentTex.y; Vertex3f currentNorm = normals[int.Parse(vertexData[2]) - 1]; normalsArray[currentVertexPointer * 3] = currentNorm.x; normalsArray[currentVertexPointer * 3 + 1] = currentNorm.y; normalsArray[currentVertexPointer * 3 + 2] = currentNorm.z; } } } </pre> <br /> 이제부터는 다른 사람이 만들어 준 3D 모델을 렌더링하는 것이므로 코드에 포함한 _vertices, _textureCoords, _indices 변수는 삭제하고 다음과 같이 변경해 줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // MainForm.cs private void glControl_ContextCreated(object sender, OpenGL.GlControlEventArgs e) { // ...[생략]... // _model = _loader.loadToVAO(_vertices, _textureCoords, _indices); // _texture = new ModelTexture(_loader.loadTexture("image")); <span style='color: blue; font-weight: bold'>_model = OBJLoader.loadObjModel("stall", _loader); // stall.obj 로드 _texture = new ModelTexture(_loader.loadTexture("stallTexture")); // stallTexture.png 로드</span> // ...[생략]... _entity = new Entity(_staticModel, new Vertex3f(0, 0, -50), 0, 0, 0, 1); } private void glControl_ContextUpdate(object sender, GlControlEventArgs e) { // ...[생략]... _entity.increaseRotation(0, 1, 0); // ...[생략]... } </pre> <br /> 그런데, 이상하군요. 아무리 소스 코드를 점검해도 정상적으로 한 것 같은데 실제로 저렇게 로드를 하면 지붕이 없고 컵의 경우 일부 폴리곤 영역이 비어 있는 체로, 다시 말해 구멍이 숭숭 뚫린 모양으로 렌더링됩니다.<br /> <br /> <img alt='opengl_tutorial_10_1.png' src='/SysWebRes/bbs/opengl_tutorial_10_1.png' /><br /> <br /> 음... 저 코드에 뭐가 문제일까요? ^^; (혹시 아시는 분은 덧글 부탁드립니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그래도 어떻게든 결과를 보고 싶어서, OBJ 파일을 로드하는 라이브러리를 검색해 봤습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > The Open-Asset-Importer-Lib ; <a target='tab' href='http://www.assimp.org/'>http://www.assimp.org/</a> ; <a target='tab' href='https://github.com/assimp/assimp'>https://github.com/assimp/assimp</a> </pre> <br /> Asset-Importer가 나름 유명한 것 같은데 다행히 닷넷 용으로 포팅한 것이 있어,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > AssimpNet ; <a target='tab' href='https://bitbucket.org/Starnick/assimpnet'>https://bitbucket.org/Starnick/assimpnet</a> ; <a target='tab' href='https://www.nuget.org/packages/AssimpNet/'>https://www.nuget.org/packages/AssimpNet/</a> 또는, dotnet/Silk.NET - high-speed .NET multimedia, graphics, and compute; providing bindings to popular low-level APIs such as OpenGL, OpenCL, OpenAL, OpenXR, GLFW, SDL, Vulkan, Assimp, and DirectX. ; <a target='tab' href='https://github.com/dotnet/Silk.NET'>https://github.com/dotnet/Silk.NET</a> </pre> <br /> 이를 참조 추가해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Install-Package AssimpNet -Version 4.1.0 </pre> <br /> OBJLoader 클래스에 loadObjModel2 메서드를 추가해 봤습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // OBJLoader.cs public static RawModel loadObjModel2(string fileName, Loader loader) { string filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "res", fileName + ".obj"); AssimpContext importer = new AssimpContext(); importer.SetConfig(new NormalSmoothingAngleConfig(66.0f)); Scene scene = importer.ImportFile(filePath, PostProcessPreset.TargetRealTimeQuality | PostProcessSteps.FlipWindingOrder); if (scene == null || scene.HasMeshes == false) { return null; } float[] verticesArray = VerticesFromMesh(scene.Meshes[0]); float[] textureArray = TextureFromMesh(scene.Meshes[0]); int[] indicesArray = IndicesFromMesh(scene.Meshes[0]); float[] normalsArray = NormalsFromMesh(scene.Meshes[0]); // normalsArray는 11회 강좌에서 사용. return loader.loadToVAO(verticesArray, textureArray, indicesArray); } static float[] TextureFromMesh(Mesh mesh) { List<float> list = new List<float>(); foreach (Vector3D vertex in mesh.TextureCoordinateChannels[0]) { list.Add(vertex.X); list.Add(1 - vertex.Y); } return list.ToArray(); } static float[] VerticesFromMesh(Mesh mesh) { List<float> list = new List<float>(); foreach (Vector3D vertex in mesh.Vertices) { list.Add(vertex.X); list.Add(vertex.Y); list.Add(vertex.Z); } return list.ToArray(); } static int[] IndicesFromMesh(Mesh mesh) { List<int> list = new List<int>(); foreach (Face face in mesh.Faces) { list.AddRange(face.Indices); } return list.ToArray(); } private static float[] NormalsFromMesh(Mesh mesh) { List<float> list = new List<float>(); foreach (Vector3D vector in mesh.Normals) { list.Add(vector.X); list.Add(vector.Y); list.Add(vector.Z); } return list.ToArray(); } </pre> <br /> 오호... 이렇게 해서 실행하니 ^^ 다음과 같이 렌더링이 잘 됩니다.<br /> <br /> <img alt='opengl_tutorial_10_2.png' src='/SysWebRes/bbs/opengl_tutorial_10_2.png' /><br /> <br /> 해석된 vertex의 수를 보면 Asset-Importer의 경우 870개인 반면 강좌에서 했던 것은 (OBJ 파일이 가진 그대로) 509개입니다. 그래도 해당 강좌에 달린 덧글들을 보면 아무런 문제 제기가 없는 것을 보면 분명 강좌에서 제시한 코드로도 잘 동작해야 하는 것 같은데... 암튼 제가 이 분야 초보자다 보니 ^^ 더 할 말이 없습니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1407&boardid=331301885'>첨부 파일은 이 글의 예제 프로젝트를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 어쨌든 남이 만든 멋진 3D 객체를 렌더링할 수 있다는 것에 신나서 ^^ 다음의 파일을 내려받고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Watch Tower Made Of Wood 3d model ; <a target='tab' href='https://free3d.com/3d-model/watch-tower-made-of-wood-94934.html'>https://free3d.com/3d-model/watch-tower-made-of-wood-94934.html</a> ; <a target='tab' href='https://static.free3d.com/models/1/d0udkqkrjj0g-Wooden-Watch-Tower.zip'>https://static.free3d.com/models/1/d0udkqkrjj0g-Wooden-Watch-Tower.zip</a> </pre> <br /> 로드했는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > _model = OBJLoader.loadObjModel2("wooden watch tower2", _loader); _texture = new ModelTexture(_loader.loadTexture("Wood_Tower_Col")); </pre> <br /> 그냥 아무런 렌더링이 안 됩니다. ^^; 아직 제가 모르는 것들이 많은 것 같습니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1661
(왼쪽의 숫자를 입력해야 합니다.)