스타일 그리고 템플릿(Style, Template) – {2/3}

이제 앞에서 작성한 기본 코드에 스타일과 템플릿을 적용해 보도록 하자. 가장 먼저 할 일은 두개의 TextBlock의 스타일을 개선해보도록 하자. 즉, My Photos라는 문자열과 Check out my ew photos!라는 문자열의 모양을 개선해 보자.

먼저 ~ 안에 다음 스타일 지정 태그를 추가하고 실행해보자.

실행 결과를 보면 알겠지만, 먼저 모든 글자에 대한 폰트가 Comic Sans MS로, 크기는 14로 변경되었다. 또한 TextBlock의 경우 수평정렬이 중앙으로 되어 있다. 그런데 의아한 것은 ListBox의 글자까지 변경되었다는 것이다. 이것은 ListBox를 구성하고 아이템들이 TextBlock으로 되어져 있기 때문에 그 영향을 받는 것이다.

이런 결과가 나오게 된 이유는 앞에 추가한 XAML 코드에서 <Style> 태그의 영향 때문이다. Style 태그의 TargetType에 TextBlock로 되어 있으므로 XAML 코드의 해당 TextBlock은 모두 지정한 스타일에 맞춰 그려지게 되는 것이다.

이제 “My Photos”라는 TextBlock의 스타일만을 다르게 지정해보자. 마찬가지로 태크 안에 아래의 코드를 추가한다.

이 <Style> 태크의 속성 중 BaseOn은 앞에서 정의한 모든 TextBlock에 해당하는 스타일의 속성을 기반으로 한다는 의미이다. 그리고 이 스타일의 식별 Key를 “TitleText”라고 지정해 둠으로써 원하는 TextBlock이 이 Key 값을 적용해서 스타일을 바꿀수 있는 것이다. 즉, 기존의 “MyPhotos” 값을 갖는 를 아래처럼 변경해 준 뒤에 실행해보자.

My Photos

자, 이제는 ListBox에 있는 jpg 파일을 파일 경로명이 아닌 이미지로 표현해보도록 하자. 역시 태그 안에 아래의 코드를 추가한다.


  
    
  

이번엔 <Style>이 아닌 <DataTemplate>이다. 데이터 템플릿은 속성인 DataType으로 지정된 데이터에 대해서 어떤식으로 표현할 것인지에 대한 템플릿을 지정하는 것이다. 이 경우 우리가 처음에 만들어 놓은 Photo 클래스에 대한 데이터 템플릿으로 Photo의 ToString으로 얻어온 문자열(jpg 파일명)을 Image로 표현하라는 XAML 코드이다. 실행 결과는 아래와 같다.

이쯤이면 대체로 만족할 만한 결과가 나타나기 시작하는 것이 보이는데, 이제 좀더 세련되게 꾸며 보도록 하자. 이러한 과정이 WPF가 제공하는 매력적인 요소중에 하나이니 말이다.

스타일 그리고 템플릿(Style, Template) – {1/3}

WPF의 스타일과 템플릿 기능을 이용하면 어플리케이션의 외향을 고급스럽고 세련되게 바꿀 수 있고, 사용자에게 좀더 효과적으로 시스템을 이해하고 활용할 수 있는 환경을 제공할수 있다.

먼저 스타일과 템플릿이 전혀 적용되지 않는 것부터 시작해서 단계적으로 하나 하나 적용해 가면서 어떻게 어플리케이션의 외향이 고급스럽게 바뀌어 가는지를 살펴봄으로써 WPF의 스타일과 템플릿을 이해해 보도록 하겠다.

아래의 코드가 처음 단계에 대한 코드이며 이어지는 이미지가 실행결과이다.


  
  
    
  
  
  
    My Photos
    Check out my new photos!
    
    

흔이 우리가 많이 봐왔던 UI인데, 상단에 2개의 TextBlock이 있고 바로 아래에 있는 ListBox에 jpg 이미지에 대한 경로가 나타나 있다.

스타일과 템플릿을 적용해보기에 앞서, 한가지 궁금증을 풀어보도록 하자.

ListBox에 나타난 jpg 이미지 파일의 경로 문자열은 어디서 왔는가? <ListBox>의 속성 중에 ItemsSource가 그 해답으로 가는 길의 시작점이다. ItemSource 속성에 MyPhotos라는 StaticResource를 바인딩하고 있다. 그렇다면 MyPhotos는 무엇인가? <Window.Resources>에 보면 <ObjectDataProvider>를 이용해서 PhotoList라는 클래스를 MyPhotos라는 Key로 생성하고 있다.

이제 PhotoList 클래스에 대해서 살펴보도록 하자. PhotoList는 Photo라는 클래스를 관리해주는 Collection으로써 다음과 같이 정의되어 있다.

public class PhotoList : ObservableCollection
{
    public PhotoList() { }

    public PhotoList(string path) : this(new DirectoryInfo(path)) { }

    public PhotoList(DirectoryInfo directory)
    {
        _directory = directory;
        Update();
    }

    public string Path
    {
        set {
            _directory = new DirectoryInfo(value);
            Update();
        }

        get { return _directory.FullName; }
    }

    public DirectoryInfo Directory
    {
        set
        {
            _directory = value;
            Update();
        }

        get { return _directory; }
    }

    private void Update()
    {
        foreach (FileInfo f in _directory.GetFiles("*.jpg"))
        {
            Add(new Photo(f.FullName));
        }
    }

    DirectoryInfo _directory;
}

먼저, PhotoList는 ObservableCollection<Photo>에서 파생되었는데, ObservableCollection은 콜렉션에서 자신이 관리하고 있는 데이터(여기서는 Photo 클래스의 인스턴스)가 삭제, 추가등과 같은 변경이 있을 경우 통지를 해주는 클래스이며, ListBox 컨트롤의 ItemSource가 될 수 있는 클래스이다. 기본적으로 PhotoList가 하는 일은 특정 폴더경로를 받아서 그 경로에 있는 확장자가 jpg인 파일명을 통해 Photo라는 클래스의 인스턴스를 만들어 준다. 이제 Photo 클래스에 대해서 살펴보자.

public class Photo
{
    public Photo(string path)
    {
        _source = path;
    }

    public override string ToString()
    {
        return Source;
    }

    private string _source;

    public string Source { get { return _source; } }
}

Photo는 무척 간단한 클래스인데, 하는 일은 단지 문자열(여기서는 jpg 파일명)을 속성으로 가지고 있고 ToString을 통해 반환하는 일을 한다.

이제 다시 XAML에서 살펴본 PhotoList 클래스 타입으로써 생성된 MyPhotos라는 Key를 가진 객체에 PhotoList 클래스가 jpg 파일명을 수집할 경로명을 지정해야 하는데, 그것은 해당 XAML에 대한 Code-Behind 코드 안의 WindowLoaded 이벤트에서 이루어지게 된다.

private void WindowLoaded(object sender, RoutedEventArgs e)
{
    Photos = (PhotoList)(this.Resources["MyPhotos"] 
        as ObjectDataProvider).Data;

    Photos.Path = "...\\...\\Images";
}

이제 지금까지의 코드를 기본으로 다양한 스타일과 템플릿을 적용할 준비가 끝났다.

스토리보드 에니메이션

WPF는 기본적으로 렌더링을 전담하는 스레드를 가지고 있음으로해서 자체적인 에니메이션 프레임워크를 가진다. 이번에는 WPF의 에니메이션 프레임워크를 기반으로 한 작은 에니메이션 샘플을 만들어 보고자 한다.

만들고자 하는 에니메이션은 이렇다. 화면상에 Windows Vista 문장을 렌더링하고, ‘W’문자부터 시작해서 한문자 한문자씩 움직이는데 움직이는 형태는 하나의 문자가 아래방향으로 이동하고 다시 원래 위치로 돌아오는 것이다. 여기에 더해서 <VisualBrush>를 이용해서 마치 물위에 비치는 효과를 넣어 마무리하고자 한다.

WPF Window Application 기반으로 기본적으로 XAML은 <Window>로 시작한다. 이 Element안을 작성하는 것이 시작이자 종착점이다. 먼저 Window의 배경을 설정해보자.

   
   
      
        
        
      
   
  

배경을 위에서 아래방향으로 검정색에서 회색으로 그라디언트 채움 효과를 지정한 것이다. 다음으로 주인공이 위치할 컨테이너로써 <Border>을 사용하며 주인공은 <TextBlock>이다. 다시 <Border>는 <StackPanel>을 컨테이너로 사용된다. <StackPanel>은 추가하는 Item을 순서대로 차곡차곡 옆으로, 또는 아래에 위치하도록 한다.

   
   
      
       Windows Vista 
      
   
  

<Border> 역시 배경 효과를 지정할 수 있는데, 체크 무늬를 넣어보도록 하자. 정적 리소스에 바인딩시키는 방법을 사용하고자 하는데 정적 리소스를 정의하기에 앞서 <Border>의 배경을 지정하도록 수정하자.

  

우리가 정의할 정적 리소스의 이름이 MyWireBrushResource 라는 것을 알 수 있다. 이제 이 리소스를 정의해보자.

   
   
     
       
         
           
           
         
       
     
   
  

이렇게 정적 리소스를 만들어 놓으면 다른 여러곳에서 x:Key 속성값을 참조함으로써 재활용이 가능함으로 적극 활용하길 바란다.

이제 주인공과 그 무대가 거의 완성되어져 간다. 무지막 무대 효과로써 수면에 반사되는 효과를 넣어보자.

여기서 사용한 방법은 이렇다. 이미 앞서 만들어 놓은 화면을 <VisualBrush>를 이용해서 또 하나의 <Rectangle>에 채움으로써 구현할 수 있다. <VisualBrush>의 Transform 중 ScaleTransform의 ScaleY의 값을 -1로 주어 위와 아래가 뒤집히게 하면 되는 것이다. 여기에 몇가지 많은 데이타 바인딩 개념이 사용되었는데, 특히 주목해야할 곳은 <VisualBrush>의 Visual 속성에 대한 데이터 바인딩이다. 바인딩의 ElementName의 값이 TextBorder인데, 이 값은 위에서 만든 <Border>의 Name이다. 즉, 앞서 만들어 놓은 <Border>의 모양이 그대로 브러시가 되어 <Rectangle>의 채움 속성으로 사용되는 것이다.

   
     
       
         
           
           
         
       
     
     
       
         
           
             
             
           
         
       
     
  

이제, 에니메이션을 위한 주인공과 배경에 대한 정의가 모두 마무리 되었다. 이제 에니메이션을 지정하는 것만 남았다. 에니메이션의 대상이 되는 것은 Windows Vista이고 이 문자열은 <TextBlock>의 내용이므로 <TextBlock>가 에니메이션의 대상이 된다. 즉, <TextBlock>안에 <TextBlock.TextEffects>와 <TextBlock.Triggers>를 추가하고 <DoubleAnimation>, <StoryBoard>, <Int32AnimationUsingKeyFrames>를 이용해 우리가 원하는 에니메이션을 지정하게 된다. 세세하게 살펴보도록 하자.

      
       Windows Vista 
       
       
       
        
     

기존의 <TextBlock>에 <TextBlock.TextEffects>와 <TextBlock.Triggers>가 새롭게 추가되었다. <TextBlock.TextEffects>는 문자에 대해 여러가지 효과를 줄 수 있는 것으로, 이동 효과에는 회전, 이동, Skew, 늘리기가 있다. <TextBlock.Triggers>는 에니메이션에 대한 정의와 시작 시점을 지정하는 것이다.

<TextBlock.TextEffects>의 정의는 다음과 같은데, 먼저 효과를 받을 문자의 수를 지정하기 위해 <TextEffect>의 PositionCount 속성값으로 1을 사용했으며 이 효과에 대한 이름을 MyTextEffect로 지정했다. 또한 우리가 원하는 에니메이션이 하나의 문자가 아래에서 다시 원래 자리와 에니메이션되는 이동 효과이므로 <TextEffect.Transform>의 <TranslateTransform>을 추가하였고 이름을 TextEffectTranslateTransform으로 주었다.

   
      
        
          
        
      
  

<TextEffect>와 <TranslateTransform>에 이름을 준 이유는 <TextBlock.Triggers>에서 이 이름을 통해 <TextEffect>와 <TranslateTransform>의 속성값을 적절한 시간에 변경시켜 에니메이션이 되도록 하기 위함이다. <TextBlock.Triggers>를 작성하기 전에, 먼저 우리는 영화감독이 되어 각 장면, 장면을 면밀하게 고려해야하는 고통이 필요하다. 고통스럽기도 하지만 한편으로는 멋지지 않은가!!? 먼저 생각해야할 것은 Windows Vista라는 문장은 공백문자 하나를 포함해서 총 13자이다. 이 13개의 문자가 0.5초씩 시간을 할당 받는데, 0.5초 동안 하는 액션(Action~~)은 Y축 아래로 20픽셀 이동하고 다시 원래 자리로 이동하는데 쓰인다. 즉, 0.5초의 반인 0.25초는 아래로 이동하고 나머지 0.25초는 원래 자리로 이동하는데 쓰인다. 비록 간단한 에니메이션의 구현이지만 등장인물의 수와 한치의 시간 오차없는 계산이 필요하다. 시간이 1초만 틀려져도 쌩뚱맞는 액션이 나오게 된다.

   
   
      
        
         
          
          
        
      
   
  

위는 <TextBlock.Triggers>의 아직은 완전하지 않은 시작 단계 코드이다. 여기에서 에니메이션이 시작할 시점을 <TextBlock>의 Loaded 이벤트가 발생할때 시작하도록 지정하고 있다. 여기서 필요한 추가 코드는 <DoubleAnimation>와 <Int32AnimationUsingKeyFrames>의 속성을 지정하는 것이다.

  

먼저 <DoubleAnimation>은 하나의 실수형 값만을 변경함으로써 우리가 원하는 에니메이션을 얻을 수 있는 경우 사용한다. <DoubleAnimation>을 살펴보면 앞서 정의한 TextEffectTranslateTransform이라는 이름의 <TextEffect.Transform>의 Y 속성을 0~20(From, To 속성)으로 0.25초 동안(Duration=”00:00:0.25″) 변화시킨다는 내용이다. RepeatBehavior=”Forever”는 모든 문자들에 대해 반복하다는 의미이고 AutoReverse=”True”는 Y 축으로 0~20까지 변경이 완료되면 다시 역으로 변경되도록 하는 것이다.

   
        
          
          
          
          
          
          
          
          
          
          
          
          
          
        
     

다음으로 <Int32AnimationUsingKeyFrames>은 하나의 정수형 값을 변경하는 에니메이션이면서 정확한 시간별로 프레임을 지정한다. <Int32AnimationUsingKeyFrames>의 대상은 TargetName과 TargetProperty로 지정해준다. 즉 앞서 이동 변환 효과로 설정했던 <TextEffect>의 Name과 <TextEffect>의 효과를 받을 문자의 인덱스 프로퍼티로써 PositionStart 값을 지정했다. 바로 이 PositionStart 프로퍼티가 Int32 형이고 <Int32AnimationUsingKeyFrames>가 에니메이션을 위해 변경시킬 값이다. 총 6.5초 동안 한 사이클을 도는 이 에니메이션의 시간을 0.5초 간격으로 Frame을 나누어주고 있다. 즉, <DiscreteInt32KeyFrame>을 통해 속성 KeyTime 시간에 Value 속성의 값으로 PositionStart 값을 설정하고 있다.

WPF에서 제공하는 에니메이션 기능은 유연하고 막강하다고 생각한다. WPF가 나오기 이전의 개발환경에서 에니메이션을 구현하려면 무척 많은 것을을 고민하고 기존의 것을 대폭적으로 수정해야했으나 WPF는 이미 모든 요소에 대해 에니메이션을 적용받을 수 있도록 되어있다. 이제는 정적인 컨텐츠가 아니라 항상 사용자가 교감하는 동적인 컨텐츠를 만들기가 어려운 것이 아니다. 개발자에게 있어 기술보다는 창의력으로 개발할 수 있는 기반을 제공하고 있다.

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