| |
|
|
|
|
|
|
새로운 시작을 알리는 2011년 1월에.. 진행하게된 프로젝트에 WPF 3D가 적용됩니다. WPF 3D를 학습한지 상당한 시간이 흘러.. 이번 기회에 다시 정리하고자 가장 먼저 WPF 3D에서 가장 핵심이 된다고 생각되는 클래스 몇가지에 대해 관계도를 그려보았습니다.
제 눈에 비친 이 관계도는 한없이 "아름답다"라는 생각이 듭니다. 여러분은 어떠신가요? 사람들간의 관계와 유사하게.. 클래스들도 서로간에 관계를 맺습니다. 강하게.. 때론 약하게.. 그리고 직접.. 때론 간접적으로.. 말입니다.
|
김형준(Dip2K)
2011/01/07 12:51
2011/01/07 12:51
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/657 |
|
|
|
|
|
|
|
마우스를 이용한 카메라 회전 WPF에서 트랙볼(Trackball) 구현하기
개요 보통 3D 모델을 화면에 표시하면, 그 다음으로 할 작업은 마우스로 모델을 회전해 보는 것이다. 마우스를 통해 3D 오브젝트를 회전하기 위한 가장 일반적인 기술은 트랙볼 기능이라고 알려져 있다. 이 글은 트랙볼이란 무엇이며, 이를 구현하기 위한 방 법을 살펴본다. 이 글의 마지막에 언급한 링크는 WPF 어플리케이션에서 마우스를 이용하여 카메라를 회전할 수 있는 샘플 코드이다.
그림1a) 기본 구성을 가지는 호랑이 모델
그림1b) 마우스를 눌러 왼쪽 아래로 조금 드래그하여 회전된 호랑이 모델 1. 소개 트랙볼은 마우스의 이동을 3D 회전으로 변환한 것이다. 이는 마우스의 위치를 그림2에서 보여지는 것처럼 Viewport3D 전면에 존재하는 가상의 구면으로 마우스의 위치를 투영한 것이다. 마우스를 움직임으로써 카메라(또는 장면)은 마우스 포인터 아래의 구면 위의 동일한 위치를 유지하면서 회전된다.
그럼2a) 정육면체 모델을 가지고 있는 Viewport3D와 사용자 시점에서 본 트랙볼
그림2b) 마우스 위치가 맵핑된 구면 상의 위치를 설명하기 위한 측면에서 본 그림
마우스가 수평으로 이동될 때, Y 축에 대한 회전은 마우스 포인터 아래에 동일한 위치가 유지되어야한다.
그림3) 수평으로 마우스를 움직이는 것은 Y축에 대해 장면을 회전시킨다.
이와 유사하게 마우스 위치를 수직으로 변경시키는 것은 X 축에 대한 회전을 발생시킨다.

그림4) 마우스를 수직으로 움직이는 것은 X 축에 대해 장면을 회전시키는 것이다. 이러한 인터페이스는 X와 Y축에 대한 회전의 조합을 적용하여 사용자가 원하는 방향에서 모델을 살펴볼 수 있는 매우 직관적인 방법이다.
2. 회전 계산 각각 마우스 이동 이벤트에서 마우스 포인터 아래의 동일한 위치를 유지하는 회전을 계산할 필요가 있으며 이를 수행하기 위한 2가지 단계가 필요하다. 첫번째는 마우스 포인터가 구면의 어느 위치에 있는지 계산하는 것이다. 두번째는 예전 위치를 새로운 위치로 변환하기 위해 필요한 회전을 계산하는 것이다.
그림5a) 마우스 위치는 좌상단에 (0,0)을 가지는 UIElement의 좌표공간 상에서 알 수 있다.
그림5b) 2차원 마우스 위치를 Viewport3D의 구면 상의 위치로 투영하며 이 위치는 3차원이다.
우리는 회전을 계산하는 것만을 생각할 것이므로 우리에게 가장 편리한 구를 선택할 수 있다. 반지름이 1이며 중심이 (0,0,0)인 구를 사용하는 것이 가장 간단하다. 그림6에서 보는 것처럼 두개의 2D 좌표계 사이의 변환의 예에서 X, Y 요소를 찾는다.
그림6a) UIElement의 좌표계
그림6b) 우리가 정한 트랙볼의 좌표계
이를 수행하기 위해 Viewport3D의 경계를 [0,0]-[2,2] 범위로 맵핑되도록 크기 변환을 한다. 다음으로 좌상단 코너에서 중심 위치를 원점으로 움직이도록 변환한다. 위치를 범위 [-1,1]-[1,-1]로 놓는다. 마지막으로 2D 좌표계에서 Y축이 위 방향 대신 아래방향으로 향하도록 한다. 이제 z값을 가진 x와 y 위치에 대한 구면 상의 위치를 알수 있게 되었다. 구의 반지름이 1이므로 z는 다음의 공식으로 구할 수 있다. 이제 마우스 포인터 아래의 구면 상의 위치 (x, y, z)를 알게 되었다. 2.2 포인트 간의 회전
우리는 마우스 이동시에 마우스 포인트 아래에 대한 구면 상의 동일한 위치를 유지하는 모델의 회전을 원한다. 마지막으로 호출된 마우스 이동 이벤트에서 구면상의 이전 위치를 기억하고 마우스 포인터 아래의 현재 위치로 변환될 회전을 만들어 이를 수행할 수 있다.
이 회전을 계산하기 위해 2가지가 필요하다.
- 회전 축
- 회전 각도
그림7) v1에서 v2로 변환될 각도와 회전축을 계산할 필요가 있다.
구가 원점을 중심으로 하고 있으므로 위치를 백터로 해석할 수 있다. 회전 축과 회전 각도는 각각 백터의 내적과 외적을 사용하여 쉽게 구할 수 있다:
일단 축과 회전 각도 모두를 알게 되었다면 남은 것은 새로운 회전을 현재 방향에 적용하는 것이다:
3. 기타 세부사항 2절에서 간과한 몇가지 세부사항이 있다. 첫번째는 Viewport3D가 정사각형이라는 가정하고 구면 상에 마우스 포인터의 투영을 계산했다는 것이다. 만약 Viewport3D가 정사각형이 아니라면 트랙볼은 실제로 타원체의 모습일 것이다.
그림8) 만약 Viewport3D가 정사각형이 아니라면 트랙볼은 실제로 타원체 모양일 것이다. 이러한 효과는 실제로 주목할 만한 사실은 아니지만 정사각형이 아닌 사각형의 너비와 높이에 대한 비율이 크다면 짧은 축을 따라 더 빠르게 회전하는 현상이 발생한다. 이러한 현상을 막고자 한다면 2D 포인트를 (width, height) 구 대신에 가로와 세로의 길이가 동일한 구로 맵핑하면 된다. 한가지 좋은 예는 가로와 세로 길이를 모두 min(width, height)이다.
또 다른 이슈는 트랙볼 상의 위치에 맵핑되지 않는 마우스 포인터가 발생할 경우에 대한 처리이다.
그림9) 회색 지역은 트랙볼 상의 위치로 맵핑되지 않는다.
한가지 해결법은 이런 경우에 z를 0으로 한정하는 것으로 2.1절의 끝에서 보였다:
기술적으로 x와 y를 정규화해야 하는데, 이는 Z=0인 평면에서 트랙볼 상에 가장 가까운 위치를 찾기 위함이다. 정규화하지 않는다면 반환된 위치는 구면상에 있지 않게 된다:
그러나, 2.2절에서 우리는 정규화된 벡터에 해당하는 Vector3D.AngleBetween(v1, v2)를 사용했다. 이는 위에서 처럼 정규화된 x와 y와 동일한 결과이다.
우리는 또한 모델과 카메라의 초기 위치에 대해 이야기 하지 않았다. 이 구현은 모델이 원점에 존재하며 카메라는 원점을 보고 있고 모델이 보이는 위치에 놓여졌다고 가정한다.
마지막으로 이 글은 확대/축소에 대해 언급하지 않았지만 샘플 코드에서 이에 대한 구현을 포함하고 있으므로 살펴보기 바란다.
4. 샘플 코드 샘플 코드는 재사용이 가능한 3개 파일로 구성된다.
Trackball.cs : 유틸리티 클래스 파일이며 FrameworkElement에 대한 마우스 이벤트를 처리한다. 또한 결과로써 회전과 크기변환을 가지는 Transform3D를 업데이트 한다.
Trackport.proj : loose.xaml 로부터 Model3D을 읽고 표시하는 UserControl이며 트랙볼 기능(Trackball.cs)이 적용되었다.
ModelViewer.proj : 그림 1의 모델 뷰어 어플리케이션(Trackport.proj를 사용하는 예)
감사의 말 모델 뷰 샘플을 제공해준 나의 아내, Bonnie에게 감사한다.
|
김형준(Dip2K)
2010/01/04 16:38
2010/01/04 16:38
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/536 |
|
|
|
|
|
|
|
이 놈의 책은 예제 코드에 대한 소스가 없는 관계로.. 그 결과를 꼭 보고 싶은 예제가 있어, 코드를 입력하고서 저와 같은 노고(?) 없이 예제의 결과를 살펴보시라는 의미(?)에서 올려봅니다. 사실, 찰스 아저씨께서는 저와 같은 노고를 가져보라고 소스 코드를 제공하고 있지 않은건데... 뭐 여튼, 저작권에 걸릴지도 모르겠습니다. 찰스페즐드의 WPF, 짱이예요~ 다들 한권쯤 장만하셔도 후회 No 랍니다. =_= 이 정도면.. 저작권에 걸려도 봐주실레나....
결과 화면 나갑니다. Win32에서는 절대 불가능한 표준 컨트롤 뱅뱅 돌리기...
실행환경은 Windows XP입니다. 비스타에서 실행했다면, 좀더 멋진 모습일테지만 말입니다. 12장의 커스텀 패널 323페이지에 있는 예제입니다. 이 예제의 이해 목표의 관건은 MeasureOverride와 ArrangeOverride에 있습니다. 소스 코드 나갑니다. Visual Studio 2008에서 작성했습니다.
제가 이해한 내용에 대한 설명은 다음으로... 시간이 너무 늦었습니다. 저 낼 칼같이 출근해야합니다. ^^;
|
김형준(Dip2K)
2008/03/14 00:22
2008/03/14 00:22
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/376 |
|
|
|
|
|
|
|
| 이제 우리가 진행해야할 것들은 ListBox에 그려진 이미지들의 정렬상태와 크기를 보기 좋게 하는 일과 사용자가 ListBox의 이미지를 선택하는 조작에서 효과를 넣는 일로써, 이 두가지 일을 하나 하나 해보도록 하겠다.
먼저 첫번째 것을 해결해보자. <Window.Resources> 태크안에 아래의 코드를 추가하자. <Style TargetType="ListBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border CornerRadius="6"
Background="{TemplateBinding ListBox.Background}">
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
HorizontalAlignment="Center"
IsItemsHost="True"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ListBoxItem">
<Setter Property="MaxHeight" Value="90" />
</Style> 두개의 스타일이 지정되어져 있다. 하나는 ListBox에 대한 스타일을 수정하고 있는데, 이미지 항목을 가로로 정렬하고 수평과 수직에 대해서 가운데 정렬을 지정하고 있다. 또한 ListBox의 모서리 부분을 반지름이 6으로 해서 둥그렇게 나타내도록 한다. 그리고 또 하나의 스타일은 ListBox의 항목의 높이 값을 90 픽셀로 지정하고 있어서, 이미지의 크기를 작게 나타내도록 한다. 이렇게 설정된 스타일에 어울리는 ListBox의 높이와 폭을 아래의 코드를 참고해서 기존의 <ListBox>의 코드를 수정하길 바란다.
<ListBox ItemsSource="{Binding Source={StaticResource MyPhotos}}"
Background="DarkGray" Width="630" Height="110"
Margin="10" SelectedIndex="0"/> 실행 결과는 아래와 같다.
자, 이제 남은 것은 사용자가 ListBox의 이미지 항목을 선택했을 경우, 그리고 마우스 커서가 이미지 항목에 놓일 때, 떠날 때에 어떤 효과를 줄것이다. 기본적으로 프로그램이 실행되면 모든 이미지 항목에 투명도를 0.4값을 지정해서 흐릿하게 보일 것이다. 이 상태에서 항목을 선택했을때는 선택된 이미지가 선명하게 되면서 커지게 된다. 또한 마우스 커서를 이미지 항목에 놓게 되면 서서히 이미지가 선명하게되고 마우스 커서가 이미지 항목을 떠나게 되면 항목은 다시 서서히 흐릿하게 보이도록 한다. 코드는 바로 앞에서 추가한 <Style TargetType="ListBoxItem"> ~ </Style>부분을 아래로 수정하면 된다. 기본적으로 어떤 이벤트가 발생시에 적절한 효과를 추가하는 것이므로 <Trigger> 태그를 사용하고 <DoubleAnimation>과 <StoryBoard>를 써서 정해진 시간에 맞는 에니메이션 기능을 추가했다. 스타일과 템플릿보다는 에니메이션 효과의 내용이 크므로 자세한 설명은 생략하고 코드만 보이도록 하겠다.
<Style TargetType="ListBoxItem">
<Setter Property="MaxHeight" Value="90" />
<Setter Property="Opacity" Value="0.4" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.Setters>
<Setter Property="Opacity" Value="1.0" />
<Setter Property="MaxHeight" Value="100" />
</Trigger.Setters>
</Trigger>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:1.2"
Storyboard.TargetProperty="Opacity"
To="1.0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:1.2"
Storyboard.TargetProperty="Opacity" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style> 최종 실행 결과를 살펴보기에 앞서, 가장 처음 모습을 살짝 다시 보면..
이랬던 아이가 어느덧 커서.. 아래와 같은 美人으로 탄생했다. 스타일과 템플릿의 힘으로 말이다.
|
김형준(Dip2K)
2007/06/28 02:23
2007/06/28 02:23
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/247 |
|
|
|
|
|
|
|
이제 앞에서 작성한 기본 코드에 스타일과 템플릿을 적용해 보도록 하자. 가장 먼저 할 일은 두개의 TextBlock의 스타일을 개선해보도록 하자. 즉, My Photos라는 문자열과 Check out my ew photos!라는 문자열의 모양을 개선해 보자.
먼저 <Window.Resources> ~ </Window.Resources>안에 다음 스타일 지정 태그를 추가하고 실행해보자.
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="Comic Sans MS"/>
<Setter Property="FontSize" Value="14"/>
</Style> 실행 결과를 보면 알겠지만, 먼저 모든 글자에 대한 폰트가 Comic Sans MS로, 크기는 14로 변경되었다. 또한 TextBlock의 경우 수평정렬이 중앙으로 되어 있다. 그런데 의아한 것은 ListBox의 글자까지 변경되었다는 것이다. 이것은 ListBox를 구성하고 아이템들이 TextBlock으로 되어져 있기 때문에 그 영향을 받는 것이다.
이런 결과가 나오게 된 이유는 앞에 추가한 XAML 코드에서 <Style> 태그의 영향 때문이다. Style 태그의 TargetType에 TextBlock로 되어 있으므로 XAML 코드의 해당 TextBlock은 모두 지정한 스타일에 맞춰 그려지게 되는 것이다.
이제 "My Photos"라는 TextBlock의 스타일만을 다르게 지정해보자. 마찬가지로 <Window.Resources> 태크 안에 아래의 코드를 추가한다.
<Style BasedOn="{StaticResource {x:Type TextBlock}}"
TargetType="TextBlock"
x:Key="TitleText">
<Setter Property="FontSize" Value="26"/>
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#0000FF" />
<GradientStop Offset="1.0" Color="#000000" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style> 이 <Style> 태크의 속성 중 BaseOn은 앞에서 정의한 모든 TextBlock에 해당하는 스타일의 속성을 기반으로 한다는 의미이다. 그리고 이 스타일의 식별 Key를 "TitleText"라고 지정해 둠으로써 원하는 TextBlock이 이 Key 값을 적용해서 스타일을 바꿀수 있는 것이다. 즉, 기존의 "MyPhotos" 값을 갖는 <TextBlock>를 아래처럼 변경해 준 뒤에 실행해보자.
<TextBlock Name="textblock1"
Style="{StaticResource TitleText}">My Photos
</TextBlock> 자, 이제는 ListBox에 있는 jpg 파일을 파일 경로명이 아닌 이미지로 표현해보도록 하자. 역시 <Window.Resources> 태그 안에 아래의 코드를 추가한다.
<DataTemplate DataType="{x:Type local:Photo}">
<Border Margin="10">
<Image Source="{Binding Source}"/>
</Border>
</DataTemplate> 이번엔 <Style>이 아닌 <DataTemplate>이다. 데이터 템플릿은 속성인 DataType으로 지정된 데이터에 대해서 어떤식으로 표현할 것인지에 대한 템플릿을 지정하는 것이다. 이 경우 우리가 처음에 만들어 놓은 Photo 클래스에 대한 데이터 템플릿으로 Photo의 ToString으로 얻어온 문자열(jpg 파일명)을 Image로 표현하라는 XAML 코드이다. 실행 결과는 아래와 같다.
이쯤이면 대체로 만족할 만한 결과가 나타나기 시작하는 것이 보이는데, 이제 좀더 세련되게 꾸며 보도록 하자. 이러한 과정이 WPF가 제공하는 매력적인 요소중에 하나이니 말이다.
|
김형준(Dip2K)
2007/06/27 01:29
2007/06/27 01:29
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/246 |
|
|
|
|
|
|
|
WPF의 스타일과 템플릿 기능을 이용하면 어플리케이션의 외향을 고급스럽고 세련되게 바꿀 수 있고, 사용자에게 좀더 효과적으로 시스템을 이해하고 활용할 수 있는 환경을 제공할수 있다.
먼저 스타일과 템플릿이 전혀 적용되지 않는 것부터 시작해서 단계적으로 하나 하나 적용해 가면서 어떻게 어플리케이션의 외향이 고급스럽게 바뀌어 가는지를 살펴봄으로써 WPF의 스타일과 템플릿을 이해해 보도록 하겠다.
아래의 코드가 처음 단계에 대한 코드이며 이어지는 이미지가 실행결과이다.
<Window x:Class="StylingIntroSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StylingIntroSample"
Loaded="WindowLoaded"
Title="StylingIntroSample"
SizeToContent="WidthAndHeight">
<Window.Resources>
<ObjectDataProvider x:Key="MyPhotos"
ObjectType="{x:Type local:PhotoList}"/>
</Window.Resources>
<StackPanel Margin="25">
<TextBlock Name="textblock1">My Photos</TextBlock>
<TextBlock>Check out my new photos!</TextBlock>
<ListBox ItemsSource="{Binding Source={StaticResource MyPhotos}}"
Background="DarkGray" Width="900" Height="200"
Margin="10" SelectedIndex="0"/>
</StackPanel>
</Window>
 흔이 우리가 많이 봐왔던 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<Photo>
{
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";
} 이제 지금까지의 코드를 기본으로 다양한 스타일과 템플릿을 적용할 준비가 끝났다.
|
김형준(Dip2K)
2007/06/26 01:59
2007/06/26 01:59
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/242 |
|
|
|
|
|
|
|
| WPF는 기본적으로 렌더링을 전담하는 스레드를 가지고 있음으로해서 자체적인 에니메이션 프레임워크를 가진다. 이번에는 WPF의 에니메이션 프레임워크를 기반으로 한 작은 에니메이션 샘플을 만들어 보고자 한다. 만들고자 하는 에니메이션은 이렇다. 화면상에 Windows Vista 문장을 렌더링하고, 'W'문자부터 시작해서 한문자 한문자씩 움직이는데 움직이는 형태는 하나의 문자가 아래방향으로 이동하고 다시 원래 위치로 돌아오는 것이다. 여기에 더해서 <VisualBrush>를 이용해서 마치 물위에 비치는 효과를 넣어 마무리하고자 한다. WPF Window Application 기반으로 기본적으로 XAML은 <Window>로 시작한다. 이 Element안을 작성하는 것이 시작이자 종착점이다. 먼저 Window의 배경을 설정해보자. <Window.Background>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="Black" />
<GradientStop Offset="1.0" Color="#666666" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Window.Background>
배경을 위에서 아래방향으로 검정색에서 회색으로 그라디언트 채움 효과를 지정한 것이다. 다음으로 주인공이 위치할 컨테이너로써 <Border>을 사용하며 주인공은 <TextBlock>이다. 다시 <Border>는 <StackPanel>을 컨테이너로 사용된다. <StackPanel>은 추가하는 Item을 순서대로 차곡차곡 옆으로, 또는 아래에 위치하도록 한다. <StackPanel Margin="40">
<Border Name="TextBorder" HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock
Name="RealText"
FontFamily="Segoe UI"
FontSize="60 px"
Margin="40"
Foreground="White">
Windows Vista
</TextBlock>
</Border>
</StackPanel>
<Border> 역시 배경 효과를 지정할 수 있는데, 체크 무늬를 넣어보도록 하자. 정적 리소스에 바인딩시키는 방법을 사용하고자 하는데 정적 리소스를 정의하기에 앞서 <Border>의 배경을 지정하도록 수정하자. <Border Name="TextBorder" HorizontalAlignment="Left"
VerticalAlignment="Top" Background="{StaticResource MyWireBrushResource}">
우리가 정의할 정적 리소스의 이름이 MyWireBrushResource 라는 것을 알 수 있다. 이제 이 리소스를 정의해보자. <Window.Resources>
<DrawingBrush x:Key="MyWireBrushResource"
Viewport="0,0,10,10" ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M0,0 L1,0 1,0.1, 0,0.1Z" Brush="#66CCCCFF" />
<GeometryDrawing Geometry="M0,0 L0,1 0.1,1, 0.1,0Z" Brush="#66CCCCFF" />
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Window.Resources>
이렇게 정적 리소스를 만들어 놓으면 다른 여러곳에서 x:Key 속성값을 참조함으로써 재활용이 가능함으로 적극 활용하길 바란다. 이제 주인공과 그 무대가 거의 완성되어져 간다. 무지막 무대 효과로써 수면에 반사되는 효과를 넣어보자. 여기서 사용한 방법은 이렇다. 이미 앞서 만들어 놓은 화면을 <VisualBrush>를 이용해서 또 하나의 <Rectangle>에 채움으로써 구현할 수 있다. <VisualBrush>의 Transform 중 ScaleTransform의 ScaleY의 값을 -1로 주어 위와 아래가 뒤집히게 하면 되는 것이다. 여기에 몇가지 많은 데이타 바인딩 개념이 사용되었는데, 특히 주목해야할 곳은 <VisualBrush>의 Visual 속성에 대한 데이터 바인딩이다. 바인딩의 ElementName의 값이 TextBorder인데, 이 값은 위에서 만든 <Border>의 Name이다. 즉, 앞서 만들어 놓은 <Border>의 모양이 그대로 브러시가 되어 <Rectangle>의 채움 속성으로 사용되는 것이다. <Rectangle Name="ReflectedText"
Height="{Binding ElementName=TextBorder, Path=ActualHeight}"
Width="{Binding ElementName=TextBorder, Path=ActualWidth}"
HorizontalAlignment="Left">
<Rectangle.OpacityMask>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#66000000" />
<GradientStop Offset="1.0" Color="#00000000" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.OpacityMask>
<Rectangle.Fill>
<VisualBrush
Visual="{Binding ElementName=TextBorder}">
<VisualBrush.RelativeTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="-1" />
<TranslateTransform Y="1" />
</TransformGroup>
</VisualBrush.RelativeTransform>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
이제, 에니메이션을 위한 주인공과 배경에 대한 정의가 모두 마무리 되었다. 이제 에니메이션을 지정하는 것만 남았다. 에니메이션의 대상이 되는 것은 Windows Vista이고 이 문자열은 <TextBlock>의 내용이므로 <TextBlock>가 에니메이션의 대상이 된다. 즉, <TextBlock>안에 <TextBlock.TextEffects>와 <TextBlock.Triggers>를 추가하고 <DoubleAnimation>, <StoryBoard>, <Int32AnimationUsingKeyFrames>를 이용해 우리가 원하는 에니메이션을 지정하게 된다. 세세하게 살펴보도록 하자. <TextBlock ... >
Windows Vista
<TextBlock.TextEffects> </TextBlock.TextEffects> <TextBlock.Triggers> </TextBlock.Triggers>
</TextBlock>
기존의 <TextBlock>에 <TextBlock.TextEffects>와 <TextBlock.Triggers>가 새롭게 추가되었다. <TextBlock.TextEffects>는 문자에 대해 여러가지 효과를 줄 수 있는 것으로, 이동 효과에는 회전, 이동, Skew, 늘리기가 있다. <TextBlock.Triggers>는 에니메이션에 대한 정의와 시작 시점을 지정하는 것이다. <TextBlock.TextEffects>의 정의는 다음과 같은데, 먼저 효과를 받을 문자의 수를 지정하기 위해 <TextEffect>의 PositionCount 속성값으로 1을 사용했으며 이 효과에 대한 이름을 MyTextEffect로 지정했다. 또한 우리가 원하는 에니메이션이 하나의 문자가 아래에서 다시 원래 자리와 에니메이션되는 이동 효과이므로 <TextEffect.Transform>의 <TranslateTransform>을 추가하였고 이름을 TextEffectTranslateTransform으로 주었다. <TextBlock.TextEffects>
<TextEffect PositionCount="1" x:Name="MyTextEffect">
<TextEffect.Transform>
<TranslateTransform x:Name="TextEffectTranslateTransform"/>
</TextEffect.Transform>
</TextEffect>
</TextBlock.TextEffects>
<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>
<EventTrigger RoutedEvent="TextBlock.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation /> <Int32AnimationUsingKeyFrames>
</Int32AnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
위는 <TextBlock.Triggers>의 아직은 완전하지 않은 시작 단계 코드이다. 여기에서 에니메이션이 시작할 시점을 <TextBlock>의 Loaded 이벤트가 발생할때 시작하도록 지정하고 있다. 여기서 필요한 추가 코드는 <DoubleAnimation>와 <Int32AnimationUsingKeyFrames>의 속성을 지정하는 것이다. <DoubleAnimation
Storyboard.TargetName="TextEffectTranslateTransform"
Storyboard.TargetProperty="Y"
From="0"
To="20"
Duration="00:00:0.25"
RepeatBehavior="Forever"
AutoReverse="True" />
먼저 <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
Storyboard.TargetName="MyTextEffect"
Storyboard.TargetProperty="PositionStart"
Duration="0:0:6.5"
AutoReverse="True"
RepeatBehavior="Forever">
<Int32AnimationUsingKeyFrames.KeyFrames>
<DiscreteInt32KeyFrame Value="0" KeyTime="0:0:0" />
<DiscreteInt32KeyFrame Value="1" KeyTime="0:0:0.5" />
<DiscreteInt32KeyFrame Value="2" KeyTime="0:0:1" />
<DiscreteInt32KeyFrame Value="3" KeyTime="0:0:1.5" />
<DiscreteInt32KeyFrame Value="4" KeyTime="0:0:2" />
<DiscreteInt32KeyFrame Value="5" KeyTime="0:0:2.5" />
<DiscreteInt32KeyFrame Value="6" KeyTime="0:0:3" />
<DiscreteInt32KeyFrame Value="7" KeyTime="0:0:3.5" />
<DiscreteInt32KeyFrame Value="8" KeyTime="0:0:4" />
<DiscreteInt32KeyFrame Value="9" KeyTime="0:0:4.5" />
<DiscreteInt32KeyFrame Value="10" KeyTime="0:0:5" />
<DiscreteInt32KeyFrame Value="11" KeyTime="0:0:5.5" />
<DiscreteInt32KeyFrame Value="12" KeyTime="0:0:6" />
</Int32AnimationUsingKeyFrames.KeyFrames>
</Int32AnimationUsingKeyFrames>
다음으로 <Int32AnimationUsingKeyFrames>은 하나의 정수형 값을 변경하는 에니메이션이면서 정확한 시간별로 프레임을 지정한다. <Int32AnimationUsingKeyFrames>의 대상은 TargetName과 TargetProperty로 지정해준다. 즉 앞서 이동 변환 효과로 설정했던 <TextEffect>의 Name과 <TextEffect>의 효과를 받을 문자의 인덱스 프로퍼티로써 PositionStart 값을 지정했다. 바로 이 PositionStart 프로퍼티가 Int32 형이고 <Int32AnimationUsingKeyFrames>가 에니메이션을 위해 변경시킬 값이다. 총 6.5초 동안 한 사이클을 도는 이 에니메이션의 시간을 0.5초 간격으로 Frame을 나누어주고 있다. 즉, <DiscreteInt32KeyFrame>을 통해 속성 KeyTime 시간에 Value 속성의 값으로 PositionStart 값을 설정하고 있다. WPF에서 제공하는 에니메이션 기능은 유연하고 막강하다고 생각한다. WPF가 나오기 이전의 개발환경에서 에니메이션을 구현하려면 무척 많은 것을을 고민하고 기존의 것을 대폭적으로 수정해야했으나 WPF는 이미 모든 요소에 대해 에니메이션을 적용받을 수 있도록 되어있다. 이제는 정적인 컨텐츠가 아니라 항상 사용자가 교감하는 동적인 컨텐츠를 만들기가 어려운 것이 아니다. 개발자에게 있어 기술보다는 창의력으로 개발할 수 있는 기반을 제공하고 있다.
|
김형준(Dip2K)
2007/06/06 23:36
2007/06/06 23:36
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/240 |
|
|
|
|
|
|
|
친절한 금자씨랑 상관없는 WPF는 매우 불친절하게도 3D에서 구, 원통, 원뿔 등과 같은 기본적인 Geometry를 쉽게 나타낼 수가 없다. 오직 WPF는 세개의 Point로 구성된 삼각형(Triangle) Geometry만을 나타낼 수 있다. 그런즉, 구, 원, 원뿔을 나타내기 위해서는 개발자 직접 삼각형 요소를 조합하는 코드를 작성하여야만 한다. 바로 이 글이 이러한 코드를 위한 것이다. 특히 XAML을 이용하여 구, 원통 등의 위치나 재질 지정 등과 같은 속성을 지정하고 실제 구, 원통 등에 대한 Geometry의 논리적인 구성 정보는 Code-Behind에서, (우리는 C# 코드로..) 처리해주는 WPF의 매력적인 코드 구조로 작성되었다.
먼저 간단이 구 등과 같은 Geometry에 대한 논리적인 구성에 대한 Code-Behind 코드가 작성되어졌다는 가정하에 XAML을 이용하여 화면상에 렌더링 시키는 XAML을 살펴보면 다음과 같다.
<Window x:Class="Visual3DSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:Primitive3DSurfaces;assembly=Primitive3DSurfaces"
Title="tst WPF3D">
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="1,1" EndPoint="0,0">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="DarkBlue" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
<Viewport3D Grid.Column="0" Grid.Row="0">
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0,-8"
UpDirection="0,1,0" LookDirection="0,0,1"
FieldOfView="30" NearPlaneDistance="0.125"/>
</Viewport3D.Camera>
<Viewport3D.Children>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="White" Direction="0,0,1" />
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<my:Sphere3D>
<ModelVisual3D.Transform>
<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0" />
</ModelVisual3D.Transform>
<my:Sphere3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="Azul.jpg" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</my:Sphere3D.Material>
</my:Sphere3D>
</ModelVisual3D>
</Viewport3D.Children>
</Viewport3D>
</Grid>
</Window> 이미 이 블로그를 통해 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)
2007/05/25 01:18
2007/05/25 01:18
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/236 |
|
|
|
|
|
|
|
| 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개의 브러쉬에 대해 하나 하나 살펴 보기로 하자.  <StackPanel Orientation="Horizontal"
VerticalAlignment="Top" Margin="10,10,10,10">
<Rectangle Width="50" Height="50" Stroke="White" StrokeThickness="1">
<Rectangle.Fill>
<SolidColorBrush Color="Blue" Opacity="1.0" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="50" Height="50" Stroke="White" StrokeThickness="1">
<Rectangle.Fill>
<SolidColorBrush Color="Blue" Opacity="0.8" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="50" Height="50" Stroke="White" StrokeThickness="1">
<Rectangle.Fill>
<SolidColorBrush Color="Blue" Opacity="0.6" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="50" Height="50" Stroke="White" StrokeThickness="1">
<Rectangle.Fill>
<SolidColorBrush Color="Blue" Opacity="0.4" />
</Rectangle.Fill>
</Rectangle>
</StackPanel>
Geometry는 Rectangle이고 총 4개를 화면상에 렌더링했으며 Geometry의 Fill을 위해 SolidColorBrush를 사용하였다. 채움색의 지정을 위해 Color 속성을 사용하였고 투명도를 위해 Opacity를 사용하였다.  <StackPanel Orientation="Vertical" VerticalAlignment="Top" Margin="10,10,10,10">
<Rectangle Width="150" Height="150" Grid.Row="0" Margin="2,2,2,2">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Yellow" Offset="0.0" />
<GradientStop Color="Orange" Offset="0.5" />
<GradientStop Color="Red" Offset="1.0" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Purple" Offset="1.0" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Purple" Offset="0.0" />
<GradientStop Color="BlueViolet" Offset="0.5" />
<GradientStop Color="White" Offset="1.0" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Gold" Offset="0.0" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Orange" Offset="1.0" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</StackPanel>
마찬가지로 Rectangle Geometry를 이용하여 화면상에 총 4개를 그렸다. 여러개의 GradientStop Element를 사용해서 선형으로 생성할 Gradient 색상을 단계적으로 지정할 수 있다.  <StackPanel Orientation="Horizontal"
VerticalAlignment="Top" Margin="10,10,10,10">
<Rectangle Width="150" Height="150" Grid.Row="0" Margin="2,2,2,2">
<Rectangle.Fill>
<RadialGradientBrush GradientOrigin="0.75,0.25">
<GradientStop Color="Yellow" Offset="0.0" />
<GradientStop Color="Orange" Offset="0.5" />
<GradientStop Color="Red" Offset="1.0" />
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Ellipse Width="150" Height="150" Margin="2,2,2,2">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.75,0.25">
<GradientStop Color="White" Offset="0.0" />
<GradientStop Color="MediumBlue" Offset="0.5" />
<GradientStop Color="Black" Offset="1.0" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="150" Height="150" Margin="2,2,2,2">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.75,0.25">
<GradientStop Color="AliceBlue" Offset="0.0" />
<GradientStop Color="Purple" Offset="0.5" />
<GradientStop Color="#330033" Offset="1.0" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="150" Height="150" Margin="2,2,2,2">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.75,0.25">
<GradientStop Color="Yellow" Offset="0.0" />
<GradientStop Color="Orange" Offset="0.5" />
<GradientStop Color="Red" Offset="1.0" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</StackPanel>
방사형의 Gradient 색상을 생성하는 것으로 RadialGradientBrush Element를 사용하였고 방사형의 중심을 지정하기 위해 GradientOrigin을 사용하였다. GradientOrigin의 밤위는 0~1 사이의 실수값이다. LinearGradientBrush와 마찬가지로 다수의 GradientStop을 사용하여 색상과 Offset 위치를 지정할 수 있다. 효과적인 방사형의 Gradient 적용을 살펴보기 위해 Geometry로 Ellipse를 사용해보았다.  <StackPanel Orientation="Horizontal"
VerticalAlignment="Top" Margin="10,10,10,10">
<Rectangle Width="140" Height="140" Grid.Row="0"
Margin="2,2,2,2" Stroke="Black">
<Rectangle.Fill>
<ImageBrush ImageSource="pinkcherries.jpg" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="140" Height="140"
Margin="2,2,2,2" Stroke="Black">
<Rectangle.Fill>
<ImageBrush Stretch="None" ImageSource="pinkcherries.jpg" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="70" Height="70"
Margin="2,2,2,2" Stroke="Black">
<Rectangle.Fill>
<ImageBrush Viewport="0,0,25,25"
ViewportUnits="Absolute" TileMode="Tile"
ImageSource="pinkcherries.jpg" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="70" Height="70"
Margin="2,2,2,2" Stroke="Black">
<Rectangle.Fill>
<ImageBrush Viewport="0,0,10,10"
ViewportUnits="Absolute" TileMode="Tile"
ImageSource="pinkcherries.jpg" />
</Rectangle.Fill>
</Rectangle>
이미지를 이용하여 Geometry를 채우는 것으로써 Geometry의 크기에 맞게 이미지를 키울 수 도 있고, 키우지 않고 원래 크기대로 채워 그릴 수도 있으며, 타일형식으로 채워 넣을 수도 있다. 사용하는 Element는 ImageBrush이다.
 <StackPanel Orientation="Horizontal" VerticalAlignment="Top" Margin="10,10,10,10">
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<DrawingBrush Viewport="0,0,0.25,0.25" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="Black"
Geometry="M 0,0 L0,0.5 0.5,0.5 0.5,1 1,1 1,0.5 0.5,0.5 0.5,0" />
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<DrawingBrush Viewport="0,0,1,1" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="Pink">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<ImageBrush ImageSource="cherries.jpg" />
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,0.5,0.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<ImageBrush ImageSource="cherries.jpg" />
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0.5,0.5,0.5,0.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<DrawingBrush Viewport="0,0,10,10" ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="Blue"
Geometry="M 0,0 L 0,1 0.1,1 0.1,0.1 1,0.1 1,0 Z" />
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<DrawingBrush Viewport="0,0,1,1" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1" />
</GeometryDrawing.Geometry>
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="#9966CC" Offset="0.5" />
<GradientStop Color="MediumBlue" Offset="1.0" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1" />
</GeometryDrawing.Geometry>
<GeometryDrawing.Brush>
<RadialGradientBrush GradientOrigin="0.75,0.25">
<RadialGradientBrush.GradientStops>
<GradientStop Color="White" Offset="0.0" />
<GradientStop Color="Transparent" Offset="0.5" />
<GradientStop Color="Transparent" Offset="0.9" />
<GradientStop Color="Black" Offset="1.0" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
</StackPanel>
Effect가 적용된 Geometry 자체를 채움을 위한 브러쉬로써 사용할 수 있는 DrawingBrush Element의 사용예이다. 앞서 사용했던 다양한 Brsuh들이 적용된 Geometry가 다시 Brush로써 사용되는 것을 알 수 있다.  <StackPanel Orientation="Horizontal" VerticalAlignment="Top" Margin="10,10,10,10">
<Rectangle Width="150" Height="150" Grid.Row="0" Margin="2,2,2,2">
<Rectangle.Fill>
<VisualBrush TileMode="Tile">
<VisualBrush.Visual>
<StackPanel>
<StackPanel.Background>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color="MediumBlue" Offset="0.0" />
<GradientStop Color="White" Offset="1.0" />
</RadialGradientBrush>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,0.5,0.5" />
<RectangleGeometry Rect="0.5,0.5,0.5,0.5" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</StackPanel.Background>
<TextBlock FontSize="10pt" Margin="10">Hello, World!</TextBlock>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<VisualBrush Viewport="0,0,1,0.25" TileMode="Tile" Stretch="Uniform">
<VisualBrush.Visual>
<StackPanel Background="White">
<TextBlock FontSize="10pt" Margin="1">Hello, World!</TextBlock>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<VisualBrush Viewport="0,0,1,0.25" TileMode="Tile" Stretch="Uniform">
<VisualBrush.Visual>
<StackPanel Background="White">
<TextBlock FontSize="10pt" Margin="1">Hello, World!</TextBlock>
</StackPanel>
</VisualBrush.Visual>
<VisualBrush.RelativeTransform>
<RotateTransform Angle="-45" CenterX="0.5" CenterY="0.5" />
</VisualBrush.RelativeTransform>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="150" Height="150" Margin="2,2,2,2">
<Rectangle.Fill>
<VisualBrush>
<VisualBrush.Visual>
<StackPanel Background="White">
<Button Margin="1">Button Control</Button>
<Button Margin="1">Another Button</Button>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
</StackPanel>
마지막으로 가장 융통성이 뛰어난 VisualBrush이다. Control은 물론이거니와 DrawingBrush의 기능까지도 포함할 수 있는 브러쉬로써 3차원으로 렌더링된 장면까지도 담을 수 있는 Brush이다. 또한 이 브러쉬를 이용하면 3차원 장면에서 2D GUI를 활용할 수 있게 하는 가장 화려한 브러쉬이다.
|
김형준(Dip2K)
2007/05/15 23:26
2007/05/15 23:26
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/235 |
|
|
|
|
|
|
|
<Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<RectangleGeometry Rect="30,55 100 30" />
</Path.Data>
</Path>
Path를 이용한 Geometry를 렌더링 하는 가장 간단한 코드이다. Rectangle Geometry를 나타내고 있다. Path Element의 속성으로써 Stroke와 Fill을 줄 수 있는데, Stroke는 외곽선의 색상을 의미하고 Fill은 채움색을 의미한다. StrokeThickness는 외곽선의 굵기값이다.
<Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<GeometryGroup FillRule="Nonzero">
<LineGeometry StartPoint="10,10" EndPoint="50,30" />
<EllipseGeometry Center="40,70" RadiusX="30" RadiusY="30" />
<RectangleGeometry Rect="30,55 100 30" />
</GeometryGroup>
</Path.Data>
</Path>
Path를 이용한 Geometry를 렌더링하는 코드로써 여러개의 Geometry를 하나의 Geometry로 묶기 위해 GeometryGroup Element를 사용하였다. GemoetryGroup는 FillRule 속성을 가지고 있으며 EvenOdd와 Nonzero 값을 가질 수 있다. FillRule는 Geometry가 서로 겹치는 부분을 어떻게 처리할 것인지를 결정하는 것으로써 Nonzero는 그냥 채움이고 EvenOdd는 빈공간으로 채우지 않는다.
<Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<GeometryGroup FillRule="EvenOdd">
<LineGeometry StartPoint="10,10" EndPoint="50,30" />
<EllipseGeometry Center="40,70" RadiusX="30" RadiusY="30" />
<RectangleGeometry Rect="30,55 100 30" />
</GeometryGroup>
</Path.Data>
</Path>
FillRule를 EvenOdd인 경우이다.
<Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure IsClosed="True" StartPoint="10,100">
<PathFigure.Segments>
<PathSegmentCollection>
<LineSegment Point="100,100" />
<LineSegment Point="100,50" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
<PathFigure IsClosed="True" StartPoint="10,10">
<PathFigure.Segments>
<PathSegmentCollection>
<LineSegment Point="100,10" />
<LineSegment Point="100,40" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
Path를 생성하는 방법으로써 직접 직선이나 다양한 곡선을 이용해 구성하는 방법이다. 직선을 이용한 예로써 LineSegment를 사용하였다. <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure StartPoint="10,10">
<PathFigure.Segments>
<PathSegmentCollection>
<ArcSegment Size="100,50" RotationAngle="45"
IsLargeArc="True"
SweepDirection="CounterClockwise"
Point="200,100" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
곡선을 이용한 예로써 ArcSegment를 이용하였다. Stroke를 완전하게 폐합하고자 한다면 PathFigure Element의 IsClosed 속성을 True로 주면 된다. 기본값은 False이다. <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure StartPoint="10,10" IsClosed="True">
<PathFigure.Segments>
<PathSegmentCollection>
<QuadraticBezierSegment Point1="200,200" Point2="300,100" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
곡선을 이용한 또 다른 예로써 QuadraticBezierSegment를 사용하였다. <Path Stroke="Black" StrokeThickness="1">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure StartPoint="10,10">
<PathFigure.Segments>
<PathSegmentCollection>
<BezierSegment Point1="100,0"
Point2="200,200"
Point3="300,100" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
곡선을 이용한 또 다른 예로써 BezierSegment를 사용하였다. <Image Source="Waterlilies.jpg" Width="200" Height="200"
HorizontalAlignment="Left">
<Image.Clip>
<GeometryGroup>
<RectangleGeometry Rect="50,5 100,10" />
<RectangleGeometry Rect="5,5 95,180" />
<EllipseGeometry Center="100, 100" RadiusX="20" RadiusY="30"/>
<RectangleGeometry Rect="50,175 100,10" />
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure IsClosed="true" StartPoint="50,50">
<PathFigure.Segments>
<PathSegmentCollection>
<BezierSegment Point1="75,300"
Point2="125,100"
Point3="150,50"/>
<BezierSegment Point1="125,300"
Point2="75,100"
Point3="50,50"/>
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</GeometryGroup>
</Image.Clip>
</Image>
Geometry를 이용하여 Image를 Clip하는 코드이다. 이미지를 화면에 나타내기 위해 가장 먼저 Image Element를 사용하였고 Source로 이미지 파일을 지정하였다. 이후에 Clip하기 위한 Geometry를 지정하기 위하는데, 두개 이상의 Geometry를 지정하기 위해 GeometryGroup Element를 사용하였으며 Geometry로써 단순 Geometry와 Path를 이용한 복합 Geometry를 사용하여 Clip을 위한 Geometry를 생성한다. <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<GeometryGroup>
<RectangleGeometry Rect="50,5 100,10" />
<RectangleGeometry Rect="5,5 95,180" />
<EllipseGeometry Center="100, 100" RadiusX="20" RadiusY="30"/>
<RectangleGeometry Rect="50,175 100,10" />
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure IsClosed="true" StartPoint="50,50">
<PathFigure.Segments>
<PathSegmentCollection>
<BezierSegment Point1="75,300"
Point2="125,100"
Point3="150,50"/>
<BezierSegment Point1="125,300"
Point2="75,100"
Point3="50,50"/>
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</GeometryGroup>
</Path.Data>
</Path>
Path를 생성하기 위해 두개 이상의 Geometry를 사용할 경우 GeometryGroup로 묶게 되는데, 하나의 Geometry로써 PathGeometry를 사용하여, 이 PathGeometry Element 안쪽에 여러개의 Geometry 격인 PathFigure를 넣는 경우에 대한 코드이다.  <Rectangle Height="200" Width="200" Stroke="Black" StrokeThickness="1"
HorizontalAlignment="Left">
<Rectangle.Fill>
<DrawingBrush Viewbox="0,0,200,200" ViewboxUnits="Absolute"
Viewport="0,0,0.5,0.5" TileMode="FlipXY">
<DrawingBrush.Drawing>
<GeometryDrawing Brush="#CCCCFF">
<GeometryDrawing.Pen>
<Pen Thickness="1" Brush="Black" />
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="50,5 100,10" />
<RectangleGeometry Rect="5,5 95,180" />
<EllipseGeometry Center="100, 100" RadiusX="20" RadiusY="30"/>
<RectangleGeometry Rect="50,175 100,10" />
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure IsClosed="true" StartPoint="50,50">
<PathFigure.Segments>
<PathSegmentCollection>
<BezierSegment Point1="75,300"
Point2="125,100"
Point3="150,50"/>
<BezierSegment Point1="125,300"
Point2="75,100"
Point3="50,50"/>
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
Path 자체를 채움을 위한 Brush의 Pattern으로 사용하는 코드이다. <Path Stroke="Black" StrokeThickness="1"
Fill="#CCCCFF"
Data="M 10,100 L 100,100 100,50 Z M 10,10 100,10 100,40 Z" />
Path를 생성하는 세번째 경우로써 가장 최적화되고 함축적인 방법이다. Path Element의 Data 속성을 이용하여 좌표를 직접 하나 하나 지정한다. M은 Move, L은 Line, Z는 폐합을 의미한다. Data로써 Data="M 10,10 A 100,50,45,1,0,200,100"와 Data="M 10,100 C35,0 135,0 160,100 S285,200 310,100", Data="M 10,100 Q 200,200 300,100", Data="M 10,100 C 100,0 200,200 300,100", Data="M 10,50 V 200", Data="M 10,50 H 200"를 사용하여 그 결과를 확인해보기 바란다. <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Xor">
<CombinedGeometry.Geometry1>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="75,75" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="125,75" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
Path를 구상하는 Geometry에 대한 결합규칙을 지정하는 방법 중 Xor 규칙에 대한 코드와 그 결과이다. 겹치는 부분에 대해서 채움을 무시한다. <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Union">
<CombinedGeometry.Geometry1>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="75,75" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="125,75" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
Path를 구성하는 Geometry를 하나의 Geometry로 합하는 것으로 겹합규칙을 Union을 사용하였다. <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Intersect">
<CombinedGeometry.Geometry1>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="75,75" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="125,75" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
Path를 구성하는 Geometry들이 교차하는 부분만을 남기고 나머지는 무시하는 것으로 결합규칙으로써 Intersect를 사용하였다. <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="75,75" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry RadiusX="50" RadiusY="50" Center="125,75" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
Path를 구성하는 Geometry들에서 첫번째에서 두번째 Geometry를 뺀 부분만을 남기는 것으로 결합규칙은 Exclude이다.
|
김형준(Dip2K)
2007/05/09 23:59
2007/05/09 23:59
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/234 |
|
|
|
|
|
|
|
오늘 작성해본 WPF에서 직사각형이 아닌 창을 나타내는 코드이다. UI 부분이므로 모든 주요 코드는 XAML에서 작성되며 드레그했을시에 창이 이동되는 이벤트 처리는 Behind Code(.CS 파일)에서 작성하였다. 먼저 XAML 코드는 아래와 같다.
<Window
x:Class="NonRectangularWindowSample.NonRectangularWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Kim Hyoung Jun"
MouseLeftButtonDown="eventMouseLeftButtonDown"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent">
<Canvas Width="200" Height="100" >
<Path Stroke="LightBlue" StrokeThickness="2">
<Path.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1" >
<GradientStop Color="White" Offset="0" />
<GradientStop Color="White" Offset="0.2" />
<GradientStop Color="LightBlue" Offset="0.9" />
<GradientStop Color="Black" Offset="1" />
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="40,20" IsClosed="True">
<LineSegment Point="160,20" />
<ArcSegment Point="180,40" Size="20,20"
SweepDirection="Clockwise" />
<LineSegment Point="180,80" />
<ArcSegment Point="160,100" Size="20,20"
SweepDirection="Clockwise" />
<LineSegment Point="40,100" />
<ArcSegment Point="20,80" Size="20,20"
SweepDirection="Clockwise" />
<LineSegment Point="20,40" />
<ArcSegment Point="40,20" Size="20,20"
SweepDirection="Clockwise" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<Label Width="200"
Height="120"
FontSize="15"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Content="끌면 이동됩니다" />
</Canvas>
</Window> 창을 원하는 모양으로 만들기 위해서 반드시 설정해야할 Window의 속성은 위의 코드에서 분홍색으로 나타낸 WindowStyle, AllowTransparency, Background 속성이다. 그리고 Path Element를 이용해 원하는 모양을 그려주면 끝이다. 참으로 간단하면서도 명확하다. 실행 결과는 다음과 같다.
 드레그를 하면 창이 이동을 하는데 그와 관련된 코드는 다음과 같다. 마우스의 왼쪽 버턴을 눌렸을 경우 발생하는 이벤트이다.
void eventMouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
DragMove();
}
|
김형준(Dip2K)
2007/04/30 01:23
2007/04/30 01:23
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/232 |
|
|
|
|
|
|
|
MSDN에 있는 WPF의 라이프 사이클에 대한 그림이다. 가운데 Application Object 상자 안이 코어 부분인데, 하나의 Application은 Run 매서드로 시작해서 Shutdown 매서드의 호출로 끝나게 된다. Shoutdown 매서드의 호출은 ShutdownMode의 값에 따라서 Application이 자동으로 호출해주는 경우와 사용자가 반드시 호출해주는 경우로 구분된다. 그리고 Activated, Deactivated, DispatcherUnhandledException, SessionEnding, Exit는 Application에 발생하는 이벤트이다. SessionEnding의 경우는 사용자가 OS를 Shutdown하거나 Logoff 시에 호출되는데 이 이벤트 안에서 OS의 종료를 취소시킬 수 있다. 또한 DispatcherUnhandledException 이벤트는 Application에서 처리되지 않는 예외가 발생했을 경우에 발생하는 범용 예외 처리가 가능한 곳으로 지정하지 않았을 경우 예외가 발생하면 Application은 자동으로 종료된다.
위의 그림은 WPF Window Application에 대한 또 다른 Life-Cycle이다. 모두 이벤트 명이다.
|
김형준(Dip2K)
2007/04/30 00:23
2007/04/30 00:23
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/231 |
|
|
|
|
|
|
|
| 한때 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)
2007/04/18 20:28
2007/04/18 20:28
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/227 |
|
|
|
|
|
|
|
이제 앞에서 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)
2007/04/06 22:59
2007/04/06 22:59
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/226 |
|
|
|
|
|
|
|
.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의 개발환경의 지원은 완벽하지 않다.
<Window x:Class="tstWPF3D.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="tstWPF3D" Height="300" Width="300"
>
<Grid>
<DockPanel
Width="Auto"
VerticalAlignment="Stretch"
Height="Auto"
HorizontalAlignment="Stretch"
Grid.ColumnSpan="1"
Grid.Column="0"
Grid.Row="0"
Margin="0,0,0,0"
Grid.RowSpan="1">
<StackPanel Margin="5" Width="70">
<Button Name="Cube" Click="ClickCubeButton">Cube</Button>
<Button Name="RotateX" Click="ClickRotateXButton">RotateX</Button>
<Button Name="RotateY" Click="ClickRotateYButton">RotateY</Button>
<Button Name="RotateZ" Click="ClickRotateZButton">RotateZ</Button>
</StackPanel>
<Viewport3D Name="mainViewport" ClipToBounds="True">
<Viewport3D.Camera>
<PerspectiveCamera
NearPlaneDistance="1"
FarPlaneDistance="100"
LookDirection="0,0,-1"
UpDirection="0,1,0"
Position="0,0,5"
FieldOfView="45" />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight
Color="White"
Direction="1.5,-2.5,-2" />
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</DockPanel>
</Grid>
</Window> 청색 코드가 실제로 새롭게 추가된 코드이다. 먼저 폼에 DockPanel 컨트롤을 올려 놓았고 그 DockPanel의 좌측에 StackPanel와 우측에 Viewport3D 컨트롤을 올려놓았다. StackPanel에는 Cube, RotateX, RotateY, RotateZ 버튼이 놓여있다. 위의 코드를 보면 각 버튼을 클릭했을때 수행해야할 이벤트 매서드가 Click이라는 속성의 값으로 지정 되어져 있다. 이 이벤트 지정에 관련된 부분만 뽑아내 보면 아래와 같다.
<StackPanel Margin="5" Width="70">
<Button Name="Cube" Click="ClickCubeButton">Cube</Button>
<Button Name="RotateX" Click="ClickRotateXButton">RotateX</Button>
<Button Name="RotateY" Click="ClickRotateYButton">RotateY</Button>
<Button Name="RotateZ" Click="ClickRotateZButton">RotateZ</Button>
</StackPanel> 이제 Viewport3D를 살펴보자. OpenGL이나 DirectX와 같은 3D 그래픽 개발을 해본 사람이라면 알겠지만 이 XAML 의 Viewport3D 요소 안에 카메라의 설정과 빛이 정의되어져 있다. 마찬가지로 3D Model에 대한 좌표데이터와 같은 Data 부분도 이 XAML 에 정의할 수 있다는 것은 앞서 언급을 했다. Viewport3D를 로직 구현부분(CS 파일)에서 접근하기 위해 Viewport3D의 Name 속성의 값으로 "mainViewport"로 지정해 놓았다. 그리고 카메라와 빛에 대한 정의가 되어 있다. 3D 개발을 해본 사람이라면 이 부분(카메라와 빛의 속성)은 직관적으로 알 수 있기에 자세히 설명하지 않겠다. (궁금하면 방명록에 질문을 하시길..) Viewport3D에 대한 부분만을 다시 나타내보면 아래와 같다.
<Viewport3D Name="mainViewport" ClipToBounds="True">
<Viewport3D.Camera>
<PerspectiveCamera
NearPlaneDistance="1"
FarPlaneDistance="100"
LookDirection="0,0,-1"
UpDirection="0,1,0"
Position="0,0,5"
FieldOfView="45" />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight
Color="White"
Direction="1.5,-2.5,-2" />
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D> 여기까지가 UI에대한 XAML 부분이고 UI의 이벤트 등과 같은 로직에 대한 부분은 다음에 살펴보기로 하겠다.
|
김형준(Dip2K)
2007/04/05 00:21
2007/04/05 00:21
|
|
| Track this back : http://www.gisdeveloper.co.kr/trackback/225 |
|
|
|
|
«
2012/02
»
| 일 |
월 |
화 |
수 |
목 |
금 |
토 |
| |
|
|
1 |
2 |
3 |
4 |
| 5 |
6 |
7 |
8 |
9 |
10 |
11 |
| 12 |
13 |
14 |
15 |
16 |
17 |
18 |
| 19 |
20 |
21 |
22 |
23 |
24 |
25 |
| 26 |
27 |
28 |
29 |
|
|
|
|
Total : 930598
Today : 67
Yesterday : 645 |
|
|
|