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 입문 (2/3)”에 대한 12개의 댓글

답글 남기기

이메일 주소는 공개되지 않습니다.