첨부파일 제가 월간 마이크로소프트웨어 2007년 1월호에 기고한 글에 나오는 WPF 예제입니다.

소개
몇 일전에 WPF에서 제공하는 3D 기능을 재미있게 다루는 방법에 대해 생각해 보았는데, 2D 그래픽과 애니메이션 자료 밖에 없어서 MSDN을 뒤져봤다. MSDN에서 3D 그래픽을 소개하는 내용을 찾아냈고, 코드프로젝트에서는 XAML로 3D를 구현하는데 도움이 될만한 3D in XAML이란 아티클을 찾아냈다. 이 아티클에서는 카메라 기초상식, 메시, 빛 등에 대한 설명은 하고 있지 않다.
MSDN에서 "지금은 스피어와 큐빅 같은 미리 정의된 3-D 도형은 지원하지 않는다 "라는 내용을 읽었을 때 적잖이 실망했다. WPF에서 제공하는 MeshGeometry3D 클래스는 삼각형 목록과 같은 도형을 빌드하는데 그칠 뿐이어서, 첫 WPF 3D 미니프로젝트에서는 스피어를 보여주는 메시를 생성하는 알고리즘을 구현하기록 결심했다.
아쉽게도 필자는 3D 그래픽을 구현하는데 능숙하지 못하다. 그래서, 삼각형 메시에서 스피어를 생성하는 비교적 간단한 알고리즘을 구현하려고 하며, 이를 위해 오픈소스 3D 모델러 블렌더(Blender)에서 제공하는 UVSphere 메시를 사용하려고 한다.

(소스: 위키: Grundkörper)
이 그림에서 보이는 대로, 스피어를 선분(segment)과 링(ring)으로 나누었다. 그 결과 위 부분과 아래 부분에는 (삼각형 두 개로 나눌 수 있는) 정사각형과 삼각형 모형이 보이게 된다. 블렌더의 Icosphere(더 자세한 내용은 위키iki: Ikosaeder를 참조)는 XAML 메시에 적합하긴 하지만, 필자는 UVSphere로 시작하려고 한다.
스피어는 단지 라운드 메시(round mesh)가 아니며, 원을 선분으로 나눌 때 생성되기도 한다. 그래서 3D 공간에서 원을 구현하는데 추상 기본 클래스(abstract base class)를 사용하려고 한다.
Collapse
using System;
using System.Windows.Media;
using System.Windows.Media.Media3D;
namespace Sphere3D
{
abstract class RoundMesh3D
{
protected int n = 10;
protected int r = 20;
protected Point3DCollection points;
protected Int32Collection triangleIndices;
public virtual int Radius
{
get { return r; }
set { r = value; CalculateGeometry(); }
}
public virtual int Separators
{
get { return n; }
set { n = value; CalculateGeometry(); }
}
public Point3DCollection Points
{
get { return points; }
}
public Int32Collection TriangleIndices
{
get { return triangleIndices; }
}
protected abstract void CalculateGeometry();
}
}
r은 그물 모양(mesh)의 반지름을 나타내고, n은 원을 몇 개의 선분으로 나눌 건지를 나타낸다(4*n+4는 원을 동일하게 나누는데 사용하는 점의 수).
첫 번째 테스트는 원반 구현으로, 아래에 코드가 있다. 삼각함수에 대한 내용일 뿐이라 그다지 어렵지는 않다.
Collapse
using System;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Diagnostics;
namespace Sphere3D
{
class DiscGeometry3D : RoundMesh3D
{
protected override void CalculateGeometry()
{
int numberOfSeparators = 4 * n + 4;
points = new Point3DCollection(numberOfSeparators + 1);
triangleIndices = new Int32Collection((numberOfSeparators + 1) * 3);
points.Add(new Point3D(0, 0, 0));
for (int divider = 0; divider < numberOfSeparators; divider++)
{
double alpha = Math.PI / 2 / (n + 1) * divider;
points.Add(new Point3D(r * Math.Cos(alpha),
0, -1 * r * Math.Sin(alpha)));
triangleIndices.Add(0);
triangleIndices.Add(divider + 1);
triangleIndices.Add((divider ==
(numberOfSeparators-1)) ? 1 : (divider + 2));
}
}
public DiscGeometry3D()
{ }
}
}
스피어를 생성하는 코드는 조금 길지만, 스피어를 점으로 나누는 건 정말 간단하다. 삼각형 생성이 좀 어렵긴 하지만 말이다.
Collapse
using System;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Diagnostics;
namespace Sphere3D
{
class SphereGeometry3D : RoundMesh3D
{
protected override void CalculateGeometry()
{
int e;
double segmentRad = Math.PI / 2 / (n + 1);
int numberOfSeparators = 4 * n + 4;
points = new Point3DCollection();
triangleIndices = new Int32Collection();
for (e = -n; e <= n; e++)
{
double r_e = r * Math.Cos(segmentRad * e);
double y_e = r * Math.Sin(segmentRad * e);
for (int s = 0; s <= (numberOfSeparators - 1); s++)
{
double z_s = r_e * Math.Sin(segmentRad * s) * (-1);
double x_s = r_e * Math.Cos(segmentRad * s);
points.Add(new Point3D(x_s, y_e, z_s));
}
}
points.Add(new Point3D(0, r, 0));
points.Add(new Point3D(0, -1 * r, 0));
for (e = 0; e < 2 * n; e++)
{
for (int i = 0; i < numberOfSeparators; i++)
{
triangleIndices.Add(e * numberOfSeparators + i);
triangleIndices.Add(e * numberOfSeparators + i +
numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators + (i + 1) %
numberOfSeparators + numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators + (i + 1) %
numberOfSeparators + numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators +
(i + 1) % numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators + i);
}
}
for (int i = 0; i < numberOfSeparators; i++)
{
triangleIndices.Add(e * numberOfSeparators + i);
triangleIndices.Add(e * numberOfSeparators + (i + 1) %
numberOfSeparators);
triangleIndices.Add(numberOfSeparators * (2 * n + 1));
}
for (int i = 0; i < numberOfSeparators; i++)
{
triangleIndices.Add(i);
triangleIndices.Add((i + 1) % numberOfSeparators);
triangleIndices.Add(numberOfSeparators * (2 * n + 1) + 1);
}
}
public SphereGeometry3D()
{ }
}
}
이 예제에서는 아티클 맨 위에 있는 그림처럼 스피어 두 개와 배경의 멋진 그림을 보여주려고 한다. 그래서 SphereGeometry3D에서 상속 받는 클래스 두 개를 생성하기로 했다.
namespace Sphere3D
{
class BigPlanet : SphereGeometry3D
{
BigPlanet()
{
Radius = 30;
Separators = 5;
}
}
class SmallPlanet : SphereGeometry3D
{
SmallPlanet()
{
Radius = 5;
Separators = 5;
}
}
}
마지막으로, 위에서 본 그림처럼 실행되도록 하기 위해서는 MeshGeometry3D의 Positions 과 TriangleIndices를 바인딩 해야 한다. 여기에서는 XAML 데이터 바인딩 메커니즘을 사용하여 처리하도록 한다.
Collapse
<Window x:Class="Sphere3D.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Sphere3D"
Title="Labyrinth3d" Height="600" Width="600"
>
<Window.Background>
<ImageBrush Stretch="UniformToFill"
ImageSource="Images/Pleiades.jpg"/>
</Window.Background>
<Grid VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" x:Name="Grid1">
<Grid.Resources>
<local:BigPlanet x:Key="SphereGeometrySource1"/>
<local:SmallPlanet x:Key="SphereGeometrySource2"/>
<MeshGeometry3D x:Key="SphereGeometry1"
Positions="{Binding Source={StaticResource
SphereGeometrySource1}, Path=Points}"
TriangleIndices="{Binding Source={StaticResource
SphereGeometrySource1},
Path=TriangleIndices}"/>
<MeshGeometry3D x:Key="SphereGeometry2"
Positions="{Binding Source={StaticResource
SphereGeometrySource2}, Path=Points}"
TriangleIndices="{Binding Source={StaticResource
SphereGeometrySource2},
Path=TriangleIndices}"/>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Viewport3D Grid.Column="1" Grid.Row="1"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" Name="Viewport1">
<Viewport3D.Camera>
<PerspectiveCamera x:Name="myCamera" Position="100 30 0"
LookDirection="-50 -33 0"
UpDirection="0,1,0" FieldOfView="90"/>
<!--<OrthographicCamera x:Name="myCamera"
Position="200 0 0" LookDirection="-1 0 0"
Width="180" UpDirection="0,1,0"/>-->
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight Color="#FFFFFF"
Direction="0 -30 0" />
<DirectionalLight Color="#FFFFFF"
Direction="0 +30 0" />
<GeometryModel3D
Geometry="{StaticResource SphereGeometry1}">
<GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="Orange"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</MaterialGroup>
</GeometryModel3D.Material>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource SphereGeometry2}">
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="Yellow"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Transform>
<TranslateTransform3D
x:Name="Sphere2Translation" OffsetZ="50" />
</GeometryModel3D.Transform>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
</Window>
출처 : http://www.codeproject.com/WPF/XamlUVSphere.asp
이 문서는 날개달기에 의해 번역되었습니다.