구(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; 
        } 
    } 
}

Brush for WPF

WPF의 2D Graphic의 효과 중에 채움(Fill) 효과에 대한 것이다. WPF의 채움은 Brush라는 개념으로 이루어지며 다음과 같은 종류가 있다.

  • SolidColorBrush
  • LinearGradientBrush
  • RadialGradientBrush
  • ImageBrush
  • DrawingBrush
  • VisualBrush

SolidBrush는 Geometry에 대해 단색으로 칠하는 브러쉬이고 LinearGradientBrush는 Gradient 색상을 선형으로 생성하여 채워준다. 또한 RadialGradientBrush는 방사형으로 Gradient 색상을 생성하여 채워주며 ImageBrush는 Image를 이용해 원하는 Geometry의 안을 채워준다. 그리고 DrawingBrush는 채우기 위한 내용을 사용자가 직접 또 다른 Geometry를 이용하여 만들어 채울 수 있다.  마지막으로 VisualBrush는 Control 등과 같은 내용(Content)를 이용하여 그 UI의 외형을 Geometry에 채울 수 있는 브러쉬이다. 참고로 브러쉬는 2차원뿐만이 아니라 3차원에서도 사용할 수 있다.

그럼 6개의 브러쉬에 대해 하나 하나 살펴 보기로 하자.

  
    
      
        
      
    

    
      
        
      
    

    
      
        
      
    

    
      
        
      
        
  

Geometry는 Rectangle이고 총 4개를 화면상에 렌더링했으며 Geometry의 Fill을 위해 SolidColorBrush를 사용하였다. 채움색의 지정을 위해 Color 속성을 사용하였고 투명도를 위해 Opacity를 사용하였다.

  
   
     
       
         
         
         
       
     
   

   
     
       
         
         
       
     
   

   
     
       
         
         
         
       
     
   

   
     
       
         
         
         
       
     
   
  

마찬가지로 Rectangle Geometry를 이용하여 화면상에 총 4개를 그렸다. 여러개의 GradientStop Element를 사용해서 선형으로 생성할 Gradient 색상을 단계적으로 지정할 수 있다.

  
  
     
       
         
         
         
       
     
  

  
     
       
         
         
         
       
     
  
  
     
       
         
         
         
       
     
  
  
     
       
         
         
         
       
     
  
  

방사형의 Gradient 색상을 생성하는 것으로 RadialGradientBrush Element를 사용하였고 방사형의 중심을 지정하기 위해 GradientOrigin을 사용하였다. GradientOrigin의 밤위는 0~1 사이의 실수값이다. LinearGradientBrush와 마찬가지로 다수의 GradientStop을 사용하여 색상과 Offset 위치를 지정할 수 있다. 효과적인 방사형의 Gradient 적용을 살펴보기 위해 Geometry로 Ellipse를 사용해보았다.

  
  
     
       
     
  

  
     
       
     
  

  
     
       
     
  

  
     
       
     
  

이미지를 이용하여 Geometry를 채우는 것으로써 Geometry의 크기에 맞게 이미지를 키울 수 도 있고, 키우지 않고 원래 크기대로 채워 그릴 수도 있으며, 타일형식으로 채워 넣을 수도 있다. 사용하는 Element는 ImageBrush이다.    
 

  
  
     
       
         
           
             
               
                 
               
             
             
           
         
       
     
  

  
     
       
         
           
             
               
                 
               
             
             
               
                 
               
               
                 
               
             
             
               
                 
               
               
                 
               
             
           
         
       
     
  

  
     
       
         
           
             
               
                 
               
             
             
           
         
       
     
  

  
     
       
         
           
             
               
                 
               
               
                 
                   
                     
                     
                     
                   
                 
               
             
             
               
                 
               
               
                 
                   
                     
                     
                     
                     
                   
                 
               
             
           
         
       
     
  
  

Effect가 적용된 Geometry 자체를 채움을 위한 브러쉬로써 사용할 수 있는 DrawingBrush Element의 사용예이다. 앞서 사용했던 다양한 Brsuh들이 적용된 Geometry가 다시 Brush로써 사용되는 것을 알 수 있다.

  
  
     
       
         
           
             
               
                 
                   
                     
                       
                         
                         
                       
                     
                     
                       
                         
                         
                       
                     
                   
                 
               
             
             Hello, World!
           
         
       
     
  

  
     
       
         
           
             Hello, World!
           
         
       
     
  

  
     
       
         
           
             Hello, World!
           
         
         
           
         
       
     
  

  
     
       
         
           
             
             
           
         
       
     
  
  

마지막으로 가장 융통성이 뛰어난 VisualBrush이다. Control은 물론이거니와 DrawingBrush의 기능까지도 포함할 수 있는 브러쉬로써 3차원으로 렌더링된 장면까지도 담을 수 있는 Brush이다. 또한 이 브러쉬를 이용하면 3차원 장면에서 2D GUI를 활용할 수 있게 하는 가장 화려한 브러쉬이다.