구(Sphere), 원통(Cylinder), 원뿔(Cone) 렌더링

친절한 금자씨랑 상관없는 WPF는 매우 불친절하게도 3D에서 구, 원통, 원뿔 등과 같은 기본적인 Geometry를 쉽게 나타낼 수가 없다. 오직 WPF는 세개의 Point로 구성된 삼각형(Triangle) Geometry만을 나타낼 수 있다. 그런즉, 구, 원, 원뿔을 나타내기 위해서는 개발자 직접 삼각형 요소를 조합하는 코드를 작성하여야만 한다. 바로 이 글이 이러한 코드를 위한 것이다. 특히 XAML을 이용하여 구, 원통 등의 위치나 재질 지정 등과 같은 속성을 지정하고 실제 구, 원통 등에 대한 Geometry의 논리적인 구성 정보는 Code-Behind에서, (우리는 C# 코드로..) 처리해주는 WPF의 매력적인 코드 구조로 작성되었다.

먼저 간단이 구 등과 같은 Geometry에 대한 논리적인 구성에 대한 Code-Behind 코드가 작성되어졌다는 가정하에 XAML을 이용하여 화면상에 렌더링 시키는 XAML을 살펴보면 다음과 같다.

 
 
   
     
       
         
           
           
         
       
     
     
     
       
         
       
 
       
         
           
             
           
         
         
           
         
           
             
           
           
             
               
                 
               
             
           
         

       
     
   
   

이미 이 블로그를 통해 WPF에서 기본적인 3D 장면을 렌더링하기 위한 코드는 살펴보았으므로, 여기서는 새로운 것(오렌지색상의 코드)만을 짚고 넘어가겠다.

먼저 Window 요소의 xmlns:my 속성은 Code-Behind에서 우리가 나중에 작성할 구, 원통 등과 같은 Geometry의 실제 구성 코드가 담겨 있는 Namespace와 Assembly(DLL)에 대한 참조이다. 즉, 우리는 또 하나의 프로젝트에 구, 원통 등의 구성 코드를 작성하여 어셈블리를 만들고 이를 사용하는 사용하는 것이다. 이렇게 참조를 한후에 우리는 my:Sphere3D 요소의 형태로 원하는 위치와 재질 등을 지정해서 화면상에 쉽게 렌더링 할 수 있는 것이다. my:Sphere3D의 Sphere3D는 앞서 참조한 Assembly DLL 안에 만든 Public Class 이름이다.

이제 결과를 살펴본 후에 Sphere3D가 어떻게 구현되었는지 코드를 살펴보기로 하자.

Sphere3D에 대한 코드를 살펴보기에 앞서 먼저 WPF 3D에서 Geometry와 연관된 클래스의 구조를 살펴보자.


여기서 Primitive3D와 Sphere3D, Cylinder3D, Cone3D는 새롭게 정의한 클래스이고 나머지는 모두 .NET에서 제공하는 클래스이다. ModelVisual3D는 WPF에서 최종적으로 화면상에 렌더링될 대상이 되는 클래스로써 렌더링할 Geometry 정보 저장을 위해 Model3D Type의 GeometryModel3D 인스턴스를 맴버로 갖는다. 바로 이 ModelVisual3D로부터 파생된 새로운 Primitive3D를 통해 우리가 원하는 구, 원통 등과 같은 3D 요소를 렌더링할 수 있는 Geometry를 구성하는 것이다. 이제 Primitive3D 클래스를 살펴보기로 하자.

using System; 
using System.Windows; 
using System.Windows.Media; 
using System.Windows.Media.Media3D; 
 
namespace Primitive3DSurfaces 
{ 
    public abstract class Primitive3D : ModelVisual3D 
    { 
        //<1> 
        internal abstract Geometry3D Tessellate(); 
        //<2> 
        internal readonly GeometryModel3D _content  
            = new GeometryModel3D(); 
 
        //<3>
        public Primitive3D() 
        { 
            Content = _content; 
            _content.Geometry = Tessellate(); 
        } 
 
        //<4-1> 
        public static DependencyProperty MaterialProperty = 
            DependencyProperty.Register( 
                "Material", 
                typeof(Material), 
                typeof(Primitive3D),  
                new PropertyMetadata( null,  
                    new PropertyChangedCallback(OnMaterialChanged))); 
        //<4-2> 
        public Material Material 
        { 
            get { return (Material)GetValue(MaterialProperty); } 
            set { SetValue(MaterialProperty, value); } 
        } 
 
        //<5> 
        internal static void OnMaterialChanged(Object sender,  
            DependencyPropertyChangedEventArgs e) 
        { 
            Primitive3D p = ((Primitive3D)sender); 
            p._content.Material = p.Material; 
        } 
 
        //<6> 
        internal static void OnGeometryChanged(DependencyObject d) 
        { 
            Primitive3D p = ((Primitive3D)d); 
            p._content.Geometry = p.Tessellate(); 
        } 
 
        //<7> 
        internal double DegToRad(double degrees) 
        { 
            return (degrees / 180.0) * Math.PI; 
        } 
    } 
}

먼저, <1>번 코드의 목적은 구, 원통 등을 구성하는 Vertex Point와 Point Index, Texture 좌표를 계산하여 이 계산된 정보를 담을 수 있는 Geometry3D를 반환해주는 추상함수로써 Primitive3D의 가장 핵심이 되는 매서드이다. 즉, 구, 원통 등은 각각 이 Tessellate 함수를 자신에 맞게 구현하여 자신의 모양을 구성하는 것이다.

<2>번 코드는 <1>에서 소개한 Tessellate 함수에서 반환된 좌표 데이터를 저장하기 위한 GeometryModel3D를 생성하는 것이다. 보다 적확히 말한다면 GeometryModel3D의 Geometry 멤버 변수에 Tessellate의 반환 정보가 담기게 된다.

<3>번 코드는 생성자로써 Primitive3D가 상속받은 ModelVisual3D의 멤버변수인 Content를 설정하고 계산되어질 좌표를 구한후 설정하고 있다.

<4-1>과 <4-2>는 보다 많은 설명이 필요한데, 여기서는 3D에 대한 설명이므로 간단히 설명하도록 하겠다. 이 부분을 이해하기 위해서는 Dependency Property이라는 WPF의 개념을 알아야 하는데, Dependency Property은 데이터바인딩이나 트리거 처리등에서 해당 속성이 그 대상이 될 수 있도록 하는 개념이다. 좀더 자세한 내용은 추후에 Dependency Property에 대해 중점적으로 살펴볼 기회를 갖겠다.

<5>는 XAML이나 Code-Behind의 코드를 통해서 재질에 대한 속성이 변경되었을때 발생하는 이벤트 코드이다.

<6>은 <5>와 마찬가지로 Geometry 구성정보(좌표, TextureMapping 좌표, 좌표 Index)가 변경되었을때 발생되는 코드이다.

마지막으로 <7>은 간단한 보조 Utility 함수이다.

이제 이 Primitive3D에서 상속받은 Sphere3D 클래스에 대해서 살펴보도록 하자. 그 코드는 다음과 같다.

using System; 
using System.Windows; 
using System.Windows.Media; 
using System.Windows.Media.Media3D; 
 
namespace Primitive3DSurfaces 
{ 
    public sealed class Sphere3D : Primitive3D 
    { 
        internal Point3D GetPosition(double t, double y) 
        { 
            double r = Math.Sqrt(1 - y * y); 
            double x = r * Math.Cos(t); 
            double z = r * Math.Sin(t); 
 
            return new Point3D(x, y, z); 
        } 
 
        private Vector3D GetNormal(double t, double y) 
        { 
            return (Vector3D) GetPosition(t, y); 
        } 
 
        private Point GetTextureCoordinate(double t, double y) 
        { 
            Matrix TYtoUV = new Matrix(); 
            TYtoUV.Scale(1 / (2 * Math.PI), -0.5); 
 
            Point p = new Point(t, y); 
            p = p * TYtoUV; 
 
            return p; 
        } 
 
        internal override Geometry3D Tessellate() 
        { 
            int tDiv = 32; 
            int yDiv = 32; 
            double maxTheta = DegToRad(360.0); 
            double minY = -1.0; 
            double maxY = 1.0; 
 
            double dt = maxTheta / tDiv; 
            double dy = (maxY - minY) / yDiv; 
 
            MeshGeometry3D mesh = new MeshGeometry3D(); 
 
            for (int yi = 0; yi <= yDiv; yi++) 
            { 
                double y = minY + yi * dy; 
 
                for (int ti = 0; ti <= tDiv; ti++) 
                { 
                    double t = ti * dt; 
 
                    mesh.Positions.Add(GetPosition(t, y)); 
                    mesh.Normals.Add(GetNormal(t, y)); 
                    mesh.TextureCoordinates.Add(GetTextureCoordinate(t, y)); 
                } 
            } 
 
            for (int yi = 0; yi < yDiv; yi++) 
            { 
                for (int ti = 0; ti < tDiv; ti++) 
                { 
                    int x0 = ti; 
                    int x1 = (ti + 1); 
                    int y0 = yi * (tDiv + 1); 
                    int y1 = (yi + 1) * (tDiv + 1); 
 
                    mesh.TriangleIndices.Add(x0 + y0); 
                    mesh.TriangleIndices.Add(x0 + y1); 
                    mesh.TriangleIndices.Add(x1 + y0); 
 
                    mesh.TriangleIndices.Add(x1 + y0); 
                    mesh.TriangleIndices.Add(x0 + y1); 
                    mesh.TriangleIndices.Add(x1 + y1); 
                } 
            } 
 
            mesh.Freeze(); 
            return mesh; 
        } 
    } 
}

가장 핵심적이고 유일하게 집중해야하는 코드는 역시 Override된 Tessellate 매서드이다. 코드를 보면 반환할 Geometry3D에서 상속된 MeshGeometry3D를 생성한 후, 이 생성된 인스턴스에 위치 좌표, 삼각형 Index, TextureMapping 좌표를 계산하여 그 값들을 추가하고 있음을 알 수 있다.

끝으로 원통과 원뿔에 대한 코드를 제시한다. 서로 비교하며 분석해 보길바란다.

using System; 
using System.Windows; 
using System.Windows.Media; 
using System.Windows.Media.Media3D; 
 
namespace Primitive3DSurfaces 
{ 
    public sealed class Cylinder3D : Primitive3D 
    { 
        internal Point3D GetPosition(double t, double y) 
        { 
            double x = Math.Cos(t); 
            double z = Math.Sin(t); 
 
            return new Point3D(x, y, z); 
        } 
 
        private Vector3D GetNormal(double t, double y) 
        { 
            double x = Math.Cos(t); 
            double z = Math.Sin(t); 
 
            return new Vector3D(x, 0, z); 
        } 
 
        private Point GetTextureCoordinate(double t, double y) 
        { 
            Matrix m = new Matrix(); 
            m.Scale(1 / (2 * Math.PI), -0.5); 
 
            Point p = new Point(t, y); 
            p = p * m; 
 
            return p; 
        } 
 
        internal override Geometry3D Tessellate() 
        { 
            int tDiv = 32; 
            int yDiv = 32; 
            double maxTheta = DegToRad(360.0); 
            double minY = -1.0; 
            double maxY = 1.0; 
 
            double dt = maxTheta / tDiv; 
            double dy = (maxY - minY) / yDiv; 
 
            MeshGeometry3D mesh = new MeshGeometry3D(); 
 
            for (int yi = 0; yi <= yDiv; yi++) 
            { 
                double y = minY + yi * dy; 
 
                for (int ti = 0; ti <= tDiv; ti++) 
                { 
                    double t = ti * dt; 
 
                    mesh.Positions.Add(GetPosition(t, y)); 
                    mesh.Normals.Add(GetNormal(t, y)); 
                    mesh.TextureCoordinates.Add(GetTextureCoordinate(t, y)); 
                } 
            } 
 
            for (int yi = 0; yi < yDiv; yi++) 
            { 
                for (int ti = 0; ti < tDiv; ti++) 
                { 
                    int x0 = ti; 
                    int x1 = (ti + 1); 
                    int y0 = yi * (tDiv + 1); 
                    int y1 = (yi + 1) * (tDiv + 1); 
 
                    mesh.TriangleIndices.Add(x0 + y0); 
                    mesh.TriangleIndices.Add(x0 + y1); 
                    mesh.TriangleIndices.Add(x1 + y0); 
 
                    mesh.TriangleIndices.Add(x1 + y0); 
                    mesh.TriangleIndices.Add(x0 + y1); 
                    mesh.TriangleIndices.Add(x1 + y1); 
                } 
            } 
 
            mesh.Freeze(); 
            return mesh; 
        } 
    } 
}

using System; 
using System.Windows; 
using System.Windows.Media; 
using System.Windows.Media.Media3D; 
 
namespace Primitive3DSurfaces 
{ 
    public sealed class Cone3D : Primitive3D 
    { 
        internal Point3D GetPosition(double t, double y) 
        { 
            double r = (1 - y) / 2; 
            double x = r * Math.Cos(t); 
            double z = r * Math.Sin(t); 
 
            return new Point3D(x, y, z); 
        } 
 
        private Vector3D GetNormal(double t, double y) 
        { 
            double x = 2 * Math.Cos(t); 
            double z = 2 * Math.Sin(t); 
 
            return new Vector3D(x, 1, z); 
        } 
 
        private Point GetTextureCoordinate(double t, double y) 
        { 
            Matrix m = new Matrix(); 
            m.Scale(1 / (2 * Math.PI), -0.5); 
 
            Point p = new Point(t, y); 
            p = p * m; 
 
            return p; 
        } 
 
        internal override Geometry3D Tessellate() 
        { 
            int tDiv = 32; 
            int yDiv = 32; 
            double maxTheta = DegToRad(360.0); 
            double minY = -1.0; 
            double maxY = 1.0; 
 
            double dt = maxTheta / tDiv; 
            double dy = (maxY - minY) / yDiv; 
 
            MeshGeometry3D mesh = new MeshGeometry3D(); 
 
            for (int yi = 0; yi <= yDiv; yi++) 
            { 
                double y = minY + yi * dy; 
 
                for (int ti = 0; ti <= tDiv; ti++) 
                { 
                    double t = ti * dt; 
 
                    mesh.Positions.Add(GetPosition(t, y)); 
                    mesh.Normals.Add(GetNormal(t, y)); 
                    mesh.TextureCoordinates.Add(GetTextureCoordinate(t, y)); 
                } 
            } 
 
            for (int yi = 0; yi < yDiv; yi++) 
            { 
                for (int ti = 0; ti < tDiv; ti++) 
                { 
                    int x0 = ti; 
                    int x1 = (ti + 1); 
                    int y0 = yi * (tDiv + 1); 
                    int y1 = (yi + 1) * (tDiv + 1); 
 
                    mesh.TriangleIndices.Add(x0 + y0); 
                    mesh.TriangleIndices.Add(x0 + y1); 
                    mesh.TriangleIndices.Add(x1 + y0); 
 
                    mesh.TriangleIndices.Add(x1 + y0); 
                    mesh.TriangleIndices.Add(x0 + y1); 
                    mesh.TriangleIndices.Add(x1 + y1); 
                } 
            } 
 
            mesh.Freeze(); 
            return mesh; 
        } 
    } 
}

Dip2K’s WPF 3D 입문 (3/3)

한때 3D의 꽃을 뽑으라면 Texture Mapping이였다. 물론 한때다. 지금 다시 뽑으라면 Shader가 되겠지만… 여하튼 오늘 잠시 WPF 3D 쪽으로 눈을 돌리면서 WPF 3D 입문에서 다소 부족했던 부분인, Texutre Mapping 부분을 정리하기로 하겠다.

먼저 Texture Mapping을 하기 위해서는 Texture Mapping 좌표가 필요하다. 이전에 했던 부분은 Texture Mapping 좌표를 제외한 Mesh의 정점과 정점 인덱스 그리고 법선 벡터 만을 지정하였다. 이제 Texture Mapping 좌표를 지정해보는 것을 살펴보자.

우리가 지금까지 구축해 왔던 것에서 시작해보자. Mesh의 정점과 인덱스 그리고 법선 벡터를 지정해 주는 부분을 포함하는 함수가 Windows1.xaml.cs 파일 안의 Window1 클래스에 대한 CreateTriangleModel 함수였다. 이제 이것이 아래와 같이 바뀐다. 파랑색의 코드 부분이 변경이나 추가된 부분이다.

private GeometryModel3D CreateTriangleModel(Point3D p0, Point3D p1, 
    Point3D p2, Point p0t, Point p1t, Point p2t)
{
    MeshGeometry3D mesh = new MeshGeometry3D();
            
    mesh.Positions.Add(p0);
    mesh.Positions.Add(p1);
    mesh.Positions.Add(p2);

    mesh.TriangleIndices.Add(0);
    mesh.TriangleIndices.Add(1);
    mesh.TriangleIndices.Add(2);

    mesh.TextureCoordinates.Add(p0t);
    mesh.TextureCoordinates.Add(p1t);
    mesh.TextureCoordinates.Add(p2t);

    Vector3D normal = CalculateNormal(p0, p1, p2);
            
    mesh.Normals.Add(normal);
    mesh.Normals.Add(normal);
    mesh.Normals.Add(normal);

    BitmapImage bi = new BitmapImage();
    bi.BeginInit();
    bi.UriSource = new Uri(@"y:/face.PNG", UriKind.RelativeOrAbsolute);
    bi.EndInit();

    Brush brush = new ImageBrush(bi);
    Material material = new DiffuseMaterial(brush);
    GeometryModel3D model = new GeometryModel3D(mesh, material);

    return model;
}

Texture Mapping 좌표와 Texture Image로 사용될 그림을 재질(Material)로 지정하였다.  CreateTriangleModel 함수가 변경되었으니 이 함수를 사용하는 부분도 변경되어야 하지 않겠는가? 그 부분에 대한 코드는 ClickCubeButton 함수 안이며 변경된 코드를 파란색으로 나타내면 다음과 같다.

private void ClickCubeButton(object Sender, RoutedEventArgs e)
{
    if (model != null) return;

    Model3DGroup cube = new Model3DGroup();

    Point3D p0 = new Point3D(-1, -1, -1);
    Point3D p1 = new Point3D(1, -1, -1);
    Point3D p2 = new Point3D(1, -1, 1);
    Point3D p3 = new Point3D(-1, -1, 1);
    Point3D p4 = new Point3D(-1, 1, -1);
    Point3D p5 = new Point3D(1, 1, -1);
    Point3D p6 = new Point3D(1, 1, 1);
    Point3D p7 = new Point3D(-1, 1, 1);

    Point t00 = new Point(0, 0);
    Point t01 = new Point(0, 1);
    Point t10 = new Point(1, 0);
    Point t11 = new Point(1, 1);

    //front side triangles
    cube.Children.Add(CreateTriangleModel(p3, p2, p6, t00, t10, t11));
    cube.Children.Add(CreateTriangleModel(p3, p6, p7, t00, t11, t01));

    //right side triangles
    cube.Children.Add(CreateTriangleModel(p2, p1, p5, t00, t10, t11));
    cube.Children.Add(CreateTriangleModel(p2, p5, p6, t00, t11, t01));

    //back side triangles
    cube.Children.Add(CreateTriangleModel(p1, p0, p4, t10, t00, t01));
    cube.Children.Add(CreateTriangleModel(p1, p4, p5, t10, t01, t11));

    //left side triangles
    cube.Children.Add(CreateTriangleModel(p0, p3, p7, t10, t00, t01));
    cube.Children.Add(CreateTriangleModel(p0, p7, p4, t10, t01, t11));

    //top side triangles
    cube.Children.Add(CreateTriangleModel(p7, p6, p5, t00, t10, t11));
    cube.Children.Add(CreateTriangleModel(p7, p5, p4, t00, t11, t01));

    //bottom side triangles
    cube.Children.Add(CreateTriangleModel(p2, p3, p0, t10, t00, t01));
    cube.Children.Add(CreateTriangleModel(p2, p0, p1, t10, t01, t11));

    model = new ModelVisual3D();
    model.Content = cube;

    mainViewport.Children.Add(model);
}

Texture Mapping 좌표에 대한 설명은 이곳 OpenGL의 Texture Mapping에 대한 강좌에 동일하니 그곳을 참고하길 바란다. 그 실행 결과는 다음과 같다.
참고로 텍스쳐 이미지의 크기는 과거의 2의 자승이여야 한다는 제약이 더 이상 적용하지 않으며 동영상(AVI, 동영상 GIF 등)도 쉽게 지원하며 이미지의 경로를 http 프로토콜을 통해서도 쉽게 받아들일 수 있다.

이상으로 WPF를 이용한 간단한 3D 그래픽에 대한 글의 정리를 끝내겠다. 추후에 지속적으로 WPF에 대한 Article을 실제 업무에 적용하면서 정리하는 예가 많아지길 스스로에게 기대한다.

Dip2K’s WPF 3D 입문 (2/3)

이제 앞에서 XAML를 통해 만들어 놓은 UI에 대한 로직을 CS(C# 소스) 코드로 작성해 보는 것을 정리해보자. 코드를 작성하기에 앞서 이해하고 넘어가야 할 것은 WPF의 3D 부분을 구성하고 있는 클래스이다. 이 클래스는 System.Windows.Media.Media3D 네임스페이스에 위치하며 이 글에서 사용하는 주요 클래스의 관계도는 다음과 같다.

각 클래스의 목적(용도)을 간단이 정리하면 다음과 같다.

MeshGeometry3D는 Mesh의 Vertex, Normal, Vertex Index, Textture Coordnate 정보를 가지고 있으며, Material은 Mesh에 대한 재질 정보를, GeometryModel3D는 MeshGeometry3D와 Material 정보를 하나로 묶어 주는 역활을 한다. Model3DGroup는 여러개의 GeometryModel3D을 묶어 마치 하나의 GeometryModel3D 처럼 사용할 수 있도록 하며, ModelVisual3D는 최종적으로 화면에 렌더링하기 위한 목적을 갖는다.

우리는 최종적으로 다음과 같은 결과을 얻고자 한다. 화면상에 정육면체 Mesh를 렌더링하고 사용자가 버튼을 눌러 이 Mesh를 회전시켜 보는 것이다.

Window1.xaml.cs 소스 파일을 보면 기본적으로 Window1 클래스가 있는데, 이 Window1 클래스의 맴버 변수로 아래의 항목을 추가한다.

private ModelVisual3D model = null;
private Transform3DGroup transformGroup = new Transform3DGroup();

model은 최종적으로 화면상에 렌더링할 Mesh로 사용되며 transformGroup은 이동, 회전, 크기조정과 같은 Transform을 위해서 필요한데, model의 Transform 속성에 바로 이 transformGroup를 대입해주면 우리가 원하는 회전이 이루어진다.

이제 앞에서 구성한 UI의 이벤트를 하나 하나 구현해 보도록 하자. 먼저 Cube 버튼을 눌렀을 경우 실행되는 코드는 다음과 같다.

private void ClickCubeButton(object Sender, RoutedEventArgs e)
{
     if (model != null) return;

     Model3DGroup cube = new Model3DGroup();

     Point3D p0 = new Point3D(-1, -1, -1);
     Point3D p1 = new Point3D(1, -1, -1);
     Point3D p2 = new Point3D(1, -1, 1);
     Point3D p3 = new Point3D(-1, -1, 1);
     Point3D p4 = new Point3D(-1, 1, -1);
     Point3D p5 = new Point3D(1, 1, -1);
     Point3D p6 = new Point3D(1, 1, 1);
     Point3D p7 = new Point3D(-1, 1, 1);

     //front side triangles
     cube.Children.Add(CreateTriangleModel(p3, p2, p6));
     cube.Children.Add(CreateTriangleModel(p3, p6, p7));
     //right side triangles
     cube.Children.Add(CreateTriangleModel(p2, p1, p5));
     cube.Children.Add(CreateTriangleModel(p2, p5, p6));
     //back side triangles
     cube.Children.Add(CreateTriangleModel(p1, p0, p4));
     cube.Children.Add(CreateTriangleModel(p1, p4, p5));
     //left side triangles
     cube.Children.Add(CreateTriangleModel(p0, p3, p7));
     cube.Children.Add(CreateTriangleModel(p0, p7, p4));
     //top side triangles
     cube.Children.Add(CreateTriangleModel(p7, p6, p5));
     cube.Children.Add(CreateTriangleModel(p7, p5, p4));
     //bottom side triangles
     cube.Children.Add(CreateTriangleModel(p2, p3, p0));
     cube.Children.Add(CreateTriangleModel(p2, p0, p1));

     model = new ModelVisual3D();
     model.Content = cube;

     mainViewport.Children.Add(model);
}

정육면체는 모두 8개의 Vertex로 이루어져 있으며 총 6개의 사각형의 면으로 이루어져 있다. 3D에서는 면을 삼각형으로 표현하므로, 결과적으로 총 12개의 삼각형의 면으로 이루어진다. 위의 코드에서 Point3D를 이용해 총 8개의 Vertex를 구성하고 Model3DGroup 클래스의 변수인 cube에 삼각형 면을 구성해서 cube의 Children 속성에 넣어준다. 삼각형 면을 구성하기 위해서는 3개의 Vertex가 필요한데, 이렇게 삼각형 면을 구성하는 함수를 따로 만들었다. 그 함수는 아래의 CreateTriangleModel이다.

private Model3DGroup CreateTriangleModel(Point3D p0, Point3D p1, Point3D p2)
{
    MeshGeometry3D mesh = new MeshGeometry3D();
        
    mesh.Positions.Add(p0);
    mesh.Positions.Add(p1);
    mesh.Positions.Add(p2);

    mesh.TriangleIndices.Add(0);
    mesh.TriangleIndices.Add(1);
    mesh.TriangleIndices.Add(2);

    Vector3D normal = CalculateNormal(p0, p1, p2);
            
    mesh.Normals.Add(normal);
    mesh.Normals.Add(normal);
    mesh.Normals.Add(normal);

    Material material = new DiffuseMaterial(new SolidColorBrush(Colors.Blue));
    GeometryModel3D model = new GeometryModel3D(mesh, material);
            
    Model3DGroup group = new Model3DGroup();
    group.Children.Add(model);
            
    return group;
}

CreateTriangleModel은 세개의 Vertex를 받아서 MeshGeometry3D를 만들어주게 되는데, 이 MeshGeometry3D는 앞서 설명했던 것처럼 Vertex와 이 Vertex의 인덱스로부터 삼각형의 면을 구성하기 위한 Vertex Inddex 지정, 그리고 빛에 대한 사실적인 재질 렌더링을 위한 법선 벡터를 갖는다. 그리고 파랑색의 재질을 만들기 위해 Material 클래스를 사용하였고, 이렇게 만들어진 두개의 MeshGeometry3D와 Material을 묶어서 GeometryModel3D 클래스의 인스턴스를 만들어었다. 그리고 최종적으로 Model3DGroup을 생성해 GeometryModel3D의 인스턴스를 자식으로 추가해준후 반환해주게 되면 파랑색의 삼각형면이 하나 만들어지게된다. 여기서 빛에 대한 사실적인 렌더링을 위한 법선 벡터를 만들기 위해 또 하나의 함수를 만들었는데 아래와 같다.

private Vector3D CalculateNormal(Point3D p0, Point3D p1, Point3D p2)
{
    Vector3D v0 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z);
    Vector3D v1 = new Vector3D(p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z);

    return Vector3D.CrossProduct(v0, v1);
}

법선벡터는 면에 대한 수직벡터이다. 벡터의 외적을 이용하여 구할 수 있으며 위의 코드가 그 외적을 구현하고 있다.

여기가지 코딩을 하고 실행한후, Cube 버튼을 눌러보면 화면상에 Mesh가 나타나게 된다. 이제 X, Y, Z 축에 대한 회전 버튼을 눌렀을 경우에 대한 이벤트를 구현해보자. 먼저 RotateX 버튼에 대한 구현부는 아래와 같다.

private void ClickRotateXButton(object Sender, RoutedEventArgs e) 
{
    AxisAngleRotation3D rotation = new AxisAngleRotation3D(
        new Vector3D(1, 0, 0), 5);

    RotateTransform3D rt = new RotateTransform3D(rotation);
            
    transformGroup.Children.Add(rt);

    model.Transform = transformGroup;
}

X축을 기준으로 하는 회전에 대한  정보를 만들기 위해서 AxisAngleRotation3D 클래스를 사용하였다. X축이므로 (1,0,0)와 5도 만큼의 회전값을 인자로 주어 생성을 하였다. (참고로 회전은 축에 대한 회전과 쿼터니언에 의한 회전이 있으며 WPF는 둘 모두를 지원한다) 이제 AxisAngleRotation3D를 이용해 실제 회전 Matrix(행렬)을 만들기 위해 RotateTransform3D 클래스를 생상하며, 이렇게 생성된 RotateTransform3D를 앞서 Window1 클래스의 맴버로 추가한 Transform3DGroup 클래스 타입인 transformGroup의 Children으로 추가한다. 자식으로써 추가하는 이유는 회전뿐만이 아니라 이동이나 크기조정 등과 같은 여러개의 Transform을 다중으로 적용할 수 있도록 하기 위해서이다. 결국 이렇게 설정된 transformGroup를 model의 Transform 속성에 넣어주게 되면 버튼을 누를때마다 X축으로 회전이 일어나게 된다. Y축과 Z축에 대한 회전은 그 축만 다르고 나머지는 동일하므로 설명은 생략한다.

Dip2K’s WPF 3D 입문 (1/3)

.NET 3.0의 기초화장(파운데이션) 중에 하나인 WPF에 대한 자료를 찾아 살펴보았다. 2D에 앞서 바로 3D를 살펴보았는데.. 이를 정리해 보고자한다.

먼저 개발을 위한 요구사항은 먼저 .NET 3.0 Framework를 설치해야 한다. 그리고 2개를 더 설치해줘야 하는데..  Microsoft Windows SDK for .NET Framework 3.0와  Visual Studio 2005 extensions for .NET Framework 3.0 (WCF & WPF)를 순서대로 설치해야 한다. 물론 이 모두에 앞서 OS(Win2000, XP, Vista)와 Visual Studio 2005가 설치되어져 있어야 한다. 참고로 필자의 OS는 .NET 3.0이 이미 설치된 윈도우즈 비스타이다.

일단 필요한것을 모두 실치했다면 VS2005를 실행해보라. 그리고 새로운 프로젝트 생성을 실행시키면 다음과 같은 WPF와 WCF 관련 프로젝트 생성을 위한 템플릿이 추가된다. (아래 이미지 캡춰는 비스타의 기본 프로그램인 “캡처도구”를 사용하였다) 참고로 WWF, 즉 지금의 WF는 또 다른 Extensions를 설치해줘야 한다.

여기서 Windows Application (WPF)를 선택하자. WPF에서 2D와 3D가 따로 있는것이 아니라 2D든 3D든 똑같이 WPF인데, 새로운 프로젝트를 생성하게 되면 자동으로 솔루션 탐색기에 아래와 같은 파일들이 자동으로 구성된다.

여기서 실제 우리가 관심을 집중시켜 코딩할 소스 파일은 Window1.xaml과 Windows1.xaml.cs이다. 확장자 XAML은 eXtansible Application Markup Language로써 개발단계에서 UI와 Data 부분을 정의할 수 있는 XML 포멧이다. UI와 Data가 아닌 실행에 관련된 로직은 확장자가 CS인 XAML 까지 포함한 동일한 파일명이다. 여기서 한가지 더 언급하면 Data의 경우 XAML 에도 넣을 수 있지만 CS 에서도 정의할 수 있다는 점이다. 여하튼 바로 이러한 UI와 Logic의 분리.. 즉, UI는 XAML 에 작성하고 로직은 CS 에 작성한다는 것이 디자이너와 개발자간의 분리된 효율적인 협업이 손쉬워졌다는 것이다. 아직 이러한 방식으로 프로젝트에서 디자이너와 협업을 해보지 않아서 모르겠지만, 적용했을시에 개발자는 편해지겠지만 디자이너는 머리가 좀 아플것같다. 디자이너가 XAML 에 익숙해져야 하는데, 이 XAML 이 꽤나 많은 내용을 담고 있기 때문인데… 이러한 문제점에 대해서 MS에서는 디자이너가 코딩보다는 미적인 감각을 더욱 잘살리길 바라는 바, XAML을 좀더 쉽게 만들 수 있는 툴을 MS에서 제공하고 있다. (참고로 XAML은 발음은 “제믈” 이다)

이제 UI를 작성해보자. 즉, 아래와 같은 폼을 구성할 것이다.

도구모음을 통해 해당 컨트롤을 가져다 디자인을 하든, 아니면 필자처럼 직접 xaml을 막코딩하든…. 위의 폼 구성에 대한 xaml의 내용은 다음과 같다. 참고로 여전이 WPF를 위한 VS2005의 개발환경의 지원은 완벽하지 않다.


  
    
      
        
        
        
        
      
      
      
        
          
        
        
          
            
          
        
      
    
  

청색 코드가 실제로 새롭게 추가된 코드이다. 먼저 폼에 DockPanel 컨트롤을 올려 놓았고 그 DockPanel의 좌측에 StackPanel와 우측에 Viewport3D 컨트롤을 올려놓았다. StackPanel에는 Cube, RotateX, RotateY, RotateZ 버튼이 놓여있다. 위의 코드를 보면 각 버튼을 클릭했을때 수행해야할 이벤트 매서드가 Click이라는 속성의 값으로 지정 되어져 있다. 이 이벤트 지정에 관련된 부분만 뽑아내 보면 아래와 같다.


    
    
    
    

이제 Viewport3D를 살펴보자. OpenGL이나 DirectX와 같은 3D 그래픽 개발을 해본 사람이라면 알겠지만 이 XAML 의 Viewport3D 요소 안에 카메라의 설정과 빛이 정의되어져 있다. 마찬가지로 3D Model에 대한 좌표데이터와 같은 Data 부분도 이 XAML 에 정의할 수 있다는 것은 앞서 언급을 했다. Viewport3D를 로직 구현부분(CS 파일)에서 접근하기 위해 Viewport3D의 Name 속성의 값으로 “mainViewport”로 지정해 놓았다. 그리고 카메라와 빛에 대한 정의가 되어 있다. 3D 개발을 해본 사람이라면 이 부분(카메라와 빛의 속성)은 직관적으로 알 수 있기에 자세히 설명하지 않겠다. (궁금하면 방명록에 질문을 하시길..) Viewport3D에 대한 부분만을 다시 나타내보면 아래와 같다.


    
        
    
    
        
            
        
    

여기까지가 UI에대한 XAML 부분이고 UI의 이벤트 등과 같은 로직에 대한 부분은 다음에 살펴보기로 하겠다.