Darren David, Fluid
Karsten Januszewski, Microsoft Corporation
September 2005
요약:Windows Presentation Foundation (코드명
"Avalon")을 사용하여 세일즈 아이템으로서, The North Face의 브랜드와 카탈로그를 어떻게 매력적인 제품으로 만들었는지
살펴봅니다.
이 기사에 관련된 샘플 코드
TNF_Samples.msi 를 다운로드하여 주세요.
목차
시작하며
응용 프로그램 모델
상태 관리
이미지 몽타쥬
비디오 슬라이드
사례
시작하며
「Avalon으로 Fluid의 온라인 고객이 요구하는 고도의 인터랙티브 시스템이 실현됩니다. e커머스가 단순한 거래 툴에서 설득력
있는 고도의 시스템 진화를 위해 Avalon은 필수 구성요소입니다.」
-Tamir Scheinok, CEO, Fluid
PDC 2005에서 발표하기 위해, 온라인 판매 시스템의 개척자인 Fluid는 전문 등산가, 한계에 도전하는 스키어나 탐험가 대상의
시장에 최첨단 기술을 사용한 제품을 제공하는 아웃도어 제품 메이커인 The North Face와 협력하여 WPF(Windows
Presentation Foundation) 플랫폼 (코드명 "Avalon")을 사용한 실험적인 전자 카탈로그를 개발했습니다. 프로젝트의
목표는 WPF를 사용하여 소매 환경에서 The North Face 의 브랜드와 카탈로그를 매력적으로 만드는 흥미로운 시스템을 만드는
방법을 보여주는 것입니다. 테스트 프로젝트는「PDC
2005 keynotes」(영문)에서 보실 수 있습니다. .
Fluid에 의해 구축된 The North Face In-Store Explorer는 WPF가 제공하는 다음 기능들이
사용하였습니다.
- 하드웨어 가속기를 사용한 렌더링 : 비디오 소재가 애니메이션 이미지 몽타주에 합성된 애니메이션 3D 매시,
서브 픽셀 클리어 타입 텍스트, 벡터 베이스 형상의 2D 애니메이션 등 이 모든 것이 WPF 엔진에 의해 실현됩니다. WPF
엔진은 하드웨어 가속기를 사용하여 다른 미디어를 공통의 사용자 인터페이스에 통합합니다.
- 모든 미디어 타입에 통일된 프로그래밍 모델 : 테스트 프로젝트는 2D 벡터 형상, 2D
애니메이션, 이미지, 3D 기하 도형, 3D 애니메이션, 비디오 및 텍스트를 단일 공통 플랫폼에 통합된 공통 프로그래밍 모델을
WPF가 어떻게 제공하는지 보여주는 뛰어난 예입니다.
- 3D 지원 : 정교하고 강력한 방법으로 3D가 사용되어 사용자 인터페이스가 개선되어 3차원을
사용해 직감적인 사용자 인터페이스 개념이 제공됩니다.
- .NET Framework 의 완전 액세스 : 이 테스트 프로젝트에서는 WPF 이외의 기술도
사용하고 있습니다. XML 역직렬화(deserialize) 기능등의 .NET 플랫폼의 기능을 사용하여 WPF가 .NET 기능을
모두 자유롭게 사용할 수 있다는 것을 보여줍니다.
이 기사에서는 테스트 프로젝트가 어떻게 구축되었는지 순서대로 설명하고 디자인의 결정 및 테스트 프로젝트의 작성으로 사용된 성능
최적화에 대해 설명합니다. 첫째로 응용 프로그램 모델의 아키텍쳐가 전체적으로 어떻게 디자인 되었는지 설명합니다. 특히, 응용
프로그램용으로 구축된 커스텀 상태 매니저에 대해 상세히 설명합니다. 둘째로 이미지 몽타쥬가 어떻게 작성되었는지 설명합니다. 마지막으로
응용 프로그램의 성능을 향상시키기 위해 불가결한 3D힌트 및 비디오 슬라이드 작성에 대해 상세하게 검토합니다. 설명하는 코드 샘플은
다운로드하여 볼 수 있습니다. .
응용 프로그램 모델
WPF 응용 프로그램의 작성시에는 먼저 응용 프로그램이 다른 부분의 변환을 설계하는 방법을 결정합니다. WPF 에는 페이지간의
네비게이션이 매우 일반적인 개념에 근거하여, 변환을 처리하는 내재된 방법이 있습니다. The North Face In-Store
Explorer 테스트 프로젝트에서는 System.Windows.NavigationApplication 을 작성하여, 네비게이션 인프라를
사용해 화면간을 이동하는 선택사항이 있었습니다. 그러나 The North Face In-Store Explorer 테스트 프로젝트에는
화면간의 응용 프로그램 크기 조절이나 페이딩 등 화면간의 동적인 변환이라는 요건이 있습니다. NavigationApplication
에서는 이 시나리오를 적용할 수 없기 때문에 WPF 기능을 이용해 복수의 층을 합성하는 방법을 선택하였습니다.
이 때문에 응용 프로그램은 WPF의 네비게이션 기능을 전혀 사용하지 않고, 단일 윈도우로 구성됩니다. 여기서 이 단일 윈도우내에서
다른 응용 프로그램 화면간을 이동하기 위한 메커니즘 작성이 문제됩니다. 각 응용 프로그램 화면은 캔버스 입니다. The North
Face In-Store Explorer 테스트 프로젝트는 항상 전 화면에서 표시되어 사용자가 사이즈 변경 옵션은 없기 때문에 Grid
가 아닌 캔버스를 사용하기로 결정하였습니다. 캔버스를 사용하면 위치를 재설정 할 수 없지만, 아이템을 절대 위치에서 설정할 수 있습니다.
화면들을 바꾸려면 단일 윈도우 내에서 캔버스를 조작합니다.
다른 캔버스 요소를 인스턴스화하여 조작하는 방법에는 두 가지의 선택사항이 있습니다. 첫번째 선택사항은 캔버스를 "필요에 따라서"
인스턴스화 하여, 필요할 경우 비주얼 트리에 삽입하고, 불필요한 경우에는 캔버스를 삭제합니다. 두번째 선택사항은 응용 프로그램의 시작
시에 모든 캔버스를 인스턴스화하여, 필요할 경우 표시/비표시 조작 방법입니다. 화면간의 동적 변환이 가능하여, 성능 목표 달성을 위한
두번째 접근 방식이 선택됩니다. 응용 프로그램의 시작시에 모든 캔버스를 읽기 위해 성능이 저하되지만, 실행시에 비주얼 트리로 캔버스를
추가/삭제하기 위한 성능 소모량이 경감됩니다.
모든 캔버스를 단일 윈도우에 읽어 들여, 윈도우의 XAML과 코드를 취급하기 어려울 수도 있었습니다. 이것에 대비하여 각 화면을
컨트롤로서 작성했습니다. 이러한 컨트롤은 이 응용 프로그램의 외부에서 다시 이용할 필요가 없기 때문에 개별의 DLL로 작성하지 않습니다.
각 화면 및 그 구성요소를 응용 프로그램 안의 컨트롤로서 작성했습니다. 이와 같은 방법이 이 기사에 부속의 "Architecture"
라고 하는 샘플 프로젝트에서도 사용됩니다.
XAML 로 컨트롤 작성
다음의 XAML 파일은 컨트롤세트를 인스턴스화하여, 캔버스에 표시하지 않는 방법을 보여줍니다.
"Architecture" 프로젝트의 Window1.xaml 를 살펴보세요.
<?Mapping XmlNamespace="UI" ClrNamespace="Architecture.UI" ?>
<?Mapping XmlNamespace="Local" ClrNamespace="Architecture" ?>
<Window x:Class="Architecture.Window1"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
Title="Architecture"
xmlns:ui="UI"
xmlns:l="Local"
Loaded="WindowLoaded"
>
<Canvas Background="BurlyWood" Width="1024" Height="768" x:Name="MainCanvas">
<ui:Screen3 x:Name="Screen3Canvas" Visibility="Collapsed"/>
<ui:Screen2 x:Name="Screen2Canvas" Visibility="Collapsed" />
<ui:Screen1 x:Name="Screen1Canvas" Visibility="Collapsed"/>
<ui:Logo x:Name="LogoCanvas" Canvas.Left="{x:Static l:Constants.LOGOPANEL_POS_LEFT_OFFSCREEN}" Canvas.Top="300"/>
</Canvas>
</Window>
먼저 XAML 파일의 맨 위에 있는 매핑 처리 명령을 봅시다.
<?Mapping XmlNamespace="UI" ClrNamespace="Architecture.UI" ?>
<?Mapping XmlNamespace="Local" ClrNamespace="Architecture" ?>
이러한 매핑에 의해 이름 공간Architecture 및 Architecture.UI 에 속하는 다른 클래스를 응용 프로그램 안에서
참조 및 인스턴스화할 수 있습니다. 이것은 C#로 다음과 같이 이름 공간의 별명을 작성하는 것과 비슷합니다.
using UI = Architecture.UI;
using Local = Architecture;
다만, 매핑만으로는 충분하지 않습니다. XAML 파일로 이러한 클래스를 참조하려면, xmlns:ui="UI" 와 같이, 루트
요소에서의xmlns 참조도 필요합니다. 이 참조를 추가하여 XAML로 프로젝트내의 그 이름 공간에서 임의의 클래스를 인스턴스화 할 수
있습니다.
클래스Logo를 살펴 보겠습니다. 프로젝트내에는 캔버스의 루트 요소를 포함한 Screen1.xaml 이라는 XAML 파일과
캔버스에서 파생한 파셜 클래스를 포함한 Screen1.xaml.cs 내의 분리 코드 클래스가 있습니다. 이 클래스를 인스턴스화 하려면,
<ui:Screen1 />를 선언합니다. 요소는 기존의 WPF 이름 공간과는 다른 이름 공간에 존재하기 때문에 다음에 코드를 통해서 이
요소에 액세스 하는 경우는 단지 Name 속성을 사용하지 않고, x:Name을 요소를 지정해야 합니다. 따라서 XAML는 다음과 같이
됩니다.
<ui:Screen1 x:Name="Screen1Canvas" Visibility="Collapsed"/>
사용자 인터페이스 컨트롤을 "비표시" 로 하는 방법
많은 UIElements 는 window1.xaml로 인스턴스화 되지만, 그 모든 것이 바로 표시되지는 않습니다. 응용 프로그램의
다양한 화면을 "비표시" 로 하기 위해 두 가지 방법이 있습니다.
첫째는 위의 Screen1 XAML에서 본 것처럼, Property의 visibility 속성을 collapsed 로 설정합니다.
Property의 visibility 를 collapsed 또는 hidden 로 설정하여, 다음에 표시할 수 있습니다. Property는
이미 인스턴스화 되어 비주얼 트리에 추가되어 있기 때문에 비주얼 트리에 수동으로 삽입하거나 안내하는 경우에 비해, 컨트롤을 표시하기 위한
성능 코스트는 억제됩니다. Visibility 속성에는 Visible, Hidden, Collapsed 라는 세가지 상태가 있습니다.
Visibility.Hidden는 요소가 레이아웃 공간을 차지하지만 표시되지 않습니다. 한편 Visibility.Collapsed 는
Property가 레이아웃 공간을 차지하지 않습니다.
메인 윈도우로 Property를 비표시로 하는 두번째 방법은 화면 외에 배치하는 것입니다. 이것은 화면에서 Property를
애니메이션화할 때에 편리한 방법으로 상하 좌우에서 Property가 나타나는 효과를 연출할 수 있습니다.
앞서 말한 XAML에서는 <ui:Logo />에서 캔버스.Left 속성이 다음과 같이 설정되어 있습니다..
"{x:Static l:Constants.LOGOPANEL_POS_LEFT_OFFSCREEN}".이 정수의 값은 -300 이어,
화면에서 왼쪽으로 배치합니다. 다음에 캔버스.Left 속성이 애니메이션화 되면, 캔버스가 좌측에서 화면으로 나타나는 효과가 있습니다.
XAML의 정적 정수의 사용에 대해서도 말해 둘 필요가 있습니다. Constants.cs라는 이름의 프로젝트 파일에는 다음과 같은
클래스가 있습니다.
public static class Constants
{
public const double LOGOPANEL_POS_LEFT_OFFSCREEN = -300;
public const double LOGOPANEL_POS_LEFT_ONSCREEN = 50;
public const double LOGOPANEL_POS_RIGHT_OFFSCREEN = 800;
}
이 클래스는 구문 {x:Static l:Constants.LOGOPANEL_POS_LEFT_OFFSCREEN}을 사용해 XAML로
참조합니다. 물론 정수는 코드 내에서도 참조 가능하고, 이와 같이 선언된 마크 업과 코드는 동등합니다. 여기에서는 xmlns:l이
사용되는 것을 주의해 주세요. 이것은 Window1.xaml 내의 다른 매핑 처리 명령을 참조합니다.
Z 인덱스
마지막으로 "z 인덱스" 도 단일 윈도우내의 UI 요소의 가시성에 영향을 줍니다. 이 인덱스는 3D 공간의 Z 좌표와는 관계
없습니다. "z 인덱스" 는 같은 공간 좌표를 공유할 수 있는 요소의 상대적인 순서를 WPF 가 추적하는 방법입니다. WPF 레이아웃
엔진에 의해 그리드 또는 캔버스의 자식 요소가 같은 좌표를 공유할 수 있습니다. 복수의 요소가 같은 좌표를 공유해, 요소가 불투명한
경우, "z 인덱스"에 근거하고, 다른 요소의 뒤에 가려집니다. WPF 요소의 "z 인덱스" 는 XAML 내 또는 코드 내에서의 순서에
의해서 정해지며, 요소가 전후의 레이아웃 방법을 결정합니다. 최초로 정의된 요소가 제일 뒤가 됩니다. 이 때문에 위의 XAML 에서는
XAML 내의 마지막 요소인 Logo 가 사실상 윈도우의 다른 요소의 "가장 위에" 표시됩니다.
실행 시에 "z 인덱스"를 조작해 그 순서를 조작할 필요가 있는 경우는 그 요소를 삭제하고,
위치를 재설정할 수 있습니다.
상태 관리
단일 윈도우 접근 방식(응용 프로그램의 모든 화면이 하나의 윈도우에 존재하는 방법)을 선택한 결과적으로, 응용 프로그램 상태를
추적할 필요성이 높아집니다. 이 경우 사용자가 현재 표시하고 있는 화면이나 다른 화면간에서 필요한 변환 등의 정보를 의미합니다. 응용
프로그램에서는 몇몇 종류 상태 관리 인프라를 포함할 필요가 있습니다.
The North Face In-Store Explorer 테스트 프로젝트로 사용하는 것과 같은 상태 매니저가
Architecture 코드 샘플에 포함되어 있습니다. 응용 프로그램 상태 매니저는 메인 윈도우가 정적 인스턴스를 취득하는 단일 클래스에
캡슐화됩니다. statemanager 클래스내에는 응용 프로그램의 모든 상태를 나타내는 열거가 있습니다.
public enum ScreenStates
{
AppIntro,
Screen1,
Screen2,
Screen3
}
이러한 상태를 관리하기 위해서 클래스에는 현재 상태를 보관 유지하기 위해서 사용하는 CurrentState 변수가 있습니다. 또
클래스에는 상태 매니저가 전환 처리 중인지 어떤지를 추적하기 위한 부울 변수” InTransition “도 있습니다. 이 변수는 상태의
변환 도중에의 응용 프로그램에 의한 상태 변환의 실행 (이것은 복잡한 전환이 동시에 발생 원인이 됩니다)을 저지하기 위해 사용되는
semaphore입니다.
또 상태 매니저에는 메인 윈도우를 나타내는 멤버 변수와 앞서 얘기한 것과 같이 컨트롤로 표현되는 응용 프로그램의 다양한 화면의 모든
것이 있습니다. 메인 윈도우의 Loaded 이벤트에는 각 "라이브" 컨트롤을 정적 상태 매니저에 관련 짓는 코드가 있어, 상태 매니저는
화면을 조작해 화면간의 전환을 작성할 수 있습니다. 다름은 로드 된 이벤트의 코드의 일부를 나타냅니다.
private StateManager _StateManager;
private void WindowLoaded( object sender, EventArgs e )
{
// . . .
_StateManager = StateManager.GetInstance();
_StateManager.MainWindow = this;
_StateManager.Screen1 = Screen1Canvas;
_StateManager.Screen2 = Screen2Canvas;
// . . .
}
샘플 상태 전환
상태 전환의 예를 봅시다. Architecture 실행 가능 파일을 컴파일 해 시작했을 경우는 마우스의 왼쪽 버튼을 클릭해 상태
변경을 개시할 수 있습니다. 마우스의 오른쪽 버튼을 클릭하고, 언제라도 응용 프로그램 시작 상태에 되돌릴 수 있습니다.
이 때문에 전환을 시작하기 위해서 응용 프로그램은 메인 윈도우로 마우스 클릭을 대기합니다. 다음에 OnLeftMouseClicked
코드 및 SetState 코드의 일부를 나타냅니다.
private void OnLeftMouseClicked(object o, EventArgs e)
{
switch (_StateManager.CurrentState)
{
case (int)StateManager.ScreenStates.AppIntro:
_StateManager.SetState(StateManager.ScreenStates.Screen1);
break;
// . . .
}
}
public void SetState(ScreenStates state)
{
if (_InTransition) return;
switch (state)
{
case ScreenStates.AppIntro:
AppIntro();
break;
// . . .
}
}
상태 매니저는 SetState 메소드를 호출하여 시작됩니다. AppIntro 메소드를 호출하도록 먼저 클릭을 하면 무엇을 할지
생각해 봅시다. 다음의 메소드를 보세요.
private void AppIntro()
{
_InTransition = true;
Screen3.Visibility = Visibility.Collapsed;
_Logo.AnimateIn();
HandleStateChangeComplete(ScreenStates.AppIntro);
}
먼저 InTransition 플래그가 true 로 설정되어 있는 것에 주목해 주세요. 이렇게 하면 전환 실행 중에 다른 전환이
시행되지 않습니다. 다음에 Screen3 이 접힙니다. 이것에 의해 상태를 리셋 했을 경우에 화면이 다소 정리됩니다. 다음에 로고
애니메이션이 나옵니다.. 각 컨트롤의 인스턴스를 위해 상태 매니저에서 다양한 컨트롤을 조작할 수 있습니다. 이 경우는 Logo의
AnimateIn 메소드에 의해 로고가 화면 밖에서 줌을 하는 애니메이션이 작성됩니다. 마지막으로 상태 매니저내의
HandleStateChanged 메소드가 전환의 완료를 나타냅니다.
한층 더 흥미로운 상태 변경
양쪽 모두에 관련된 더욱 흥미 깊은 상태 변경을 다음에 나타냅니다. 이 상태 변경에 의해 로고가 화면 외에 애니메이션화 되어 현재
접힌 Screen1 캔버스가 표시됩니다. 기대 효과는 Screen1 캔버스가 표시되기 전에 로고의 애니메이션이 완료 되도록,
애니메이션에서는 애니메이션의 완료 시에 콜백 메소드를 설정할 필요가 있습니다.
private void Screen1Intro()
{
_InTransition = true;
_Logo.AnimateOut(OnLogoOffScreen);
}
private void OnLogoOffScreen(object sender, EventArgs e)
{
Clock clock = sender as Clock;
if ( clock == null ) return;
if (clock.CurrentState != ClockState.Active)
{
_Screen1.Visibility = Visibility.Visible;
HandleStateChangeComplete(ScreenStates.Screen1);
}
}
logo 클래스의 AnimateOut 메소드 코드입니다.
public void AnimateOut(EventHandler callback)
{
DoubleAnimation da = new DoubleAnimation(Canvas.GetLeft(this),
Constants.LOGOPANEL_POS_RIGHT_OFFSCREEN, TimeSpan.FromSeconds(2));
da.BeginTime = null;
AnimationClock ac = da.CreateClock();
ac.CurrentStateInvalidated += new EventHandler(callback);
this.ApplyAnimationClock(Canvas.LeftProperty, ac);
ac.Controller.Begin();
}
이 메소드는 상태 매니저내의OnLogoOffScreen 메소드 EventHandler 콜백을 받습니다. 애니메이션이 개시하기 전에
애니메이션의CurrentStateInvalidated 이벤트가 코드내에서 정의되기 때문에 애니메이션의 완료 후에 콜백이 실행되어
Screen1 캔버스의 Visibility가 비주얼화 됩니다.
이 기사에서는 다른 전환 코드에 대해 자세히 설명하지 않지만, 모든 전환으로 같은 방법을 사용합니다. 메인 윈도우가 상태 매니저
SetState 메소드를 호출하여 건네줍니다. 다음에 상태 매니저는 다양한 컨트롤에 대해서 메소드를 호출하고, 초기화나 애니메이션화를
실시할 수 있습니다.
각 전환의 완료 후에 호출된 HandleStateChangeComplete 메소드입니다.
private void HandleStateChangeComplete( KioskStates state )
{
_InTransition = false;
_CurrentState = ( int ) state;
}
우선 이 코드는_InTransition 플래그를 false 로 되돌리고, 새로운 전환을 발생할 수 있도록 합니다. 응용 프로그램이
현재 상태를 확인할 수 있도록_CurrentState 변수를 설정합니다.
코드 샘플의 실제의 전환은 비교적 단순하지만, The North Face In-Store Explorer 테스트 프로젝트에서는 이
화면 전환을 설정하는 기본적인 인프라를 사용한 매우 재미있는 상태 전환을 볼 수 있습니다.
이미지 몽타쥬
응용 프로그램이 최초로 실행할 경우에 일련의 이미지가 배경으로 표시됩니다. 각 이미지가 두 번째의 이미지에 페이드아웃 합니다.
이것은 "Ken Burns" 효과로 불리고 있습니다. Ken Burns는 정지화면상에 움직임과 줌을 사용해 documentary를
생생하게 표현하는 방법을 개척한 영화 제작자입니다. The North Face 에서는 이 효과를 이용하여 응용 프로그램에 "역동감" 을
주는 섬세하고 일정한 움직임이 추가되었습니다.
이 기사의 ImageMontage 라는 코드 샘플로 그 효과를 나타냅니다. 이 샘플은 ImageMontage 가 어떻게 구축되었는지
개요를 보여줍니다. 이미지 몽타쥬는 이미지를 로드하여, 순환 및 애니메이션의 기능을 포함한 System.Windows.Controls.캔버스에서
파생한 단일 클래스ImageMontage캔버스를 사용하여 구축되었습니다. 이미지 자체는 모든 이미지를
포함한는System.Collections.ObjectModel.ObservableCollection 에 포함되어 있습니다.
ObservableCollection 클래스는 WPF의 특수한 콜렉션 클래스이며, 특히 데이타바인드의 경우에 WPF 리스트 사용에
최적화되어 있습니다. 이 클래스는 데이터 바인드로 선택할 수 있는 변경 통지를 실행합니다. ObservableCollection의 사용
방법의 상세한 내용은「Optimizing Performance in Windows Presentation Foundation」데이터
바인드의 섹션을 참조해 주세요.
이미지 몽타쥬에 이미지 삽입
이미지의 ObservableCollection 에 데이터를 설정하기 위해서 ImageMontage캔버스 클래스의 생성자에
LoadImages 라는 메소드가 있습니다.
LoadImage 메소드에서는 이미지를 파일에서 실제로 추출해, WPFSystem.Windows.Controls.Image 클래스로
설정하는 코드에 주목해 주세요. Image 의 Source 속성은System.Windows.Media.Imaging.BitmapImage
입니다. BitmapImage 의 생성자 내에서 이미지에의 URI 를 건네주어, 파일 시스템의 이미지를 설정할 수 있습니다.
public void LoadImages()
{
DirectoryInfo dir = new DirectoryInfo(@"images");
foreach (FileInfo f in dir.GetFiles("*.jpg"))
{
Image newImage = new Image();
newImage.Source = new BitmapImage(new Uri(f.FullName, UriKind.Relative)); ;
this.Images.Add(newImage);
}
}
이미지가 로드 되면, 이미지 몽타쥬를 개시할 수 있습니다. 그 코드의 일부를 보겠습니다. ImageMontage 에는 XAML 가
없고, 순수하게 코드내에 작성된 WPF 클래스입니다. 이것은 캔버스에서 만들어집니다. 캔버스를 사용하는 근거는 레이아웃이나 사이즈 변경이
불필요한 것으로 캔버스상에 있는 것은 고정 사이즈의 이미지만입니다.
화상을 표시하여 페이드
이미지 클래스의 각 구현의 상세한 내용은 생략하고, 클래스내의 주요 메소드에 주목합니다. Init() 메소드는 몽타쥬를 개시하기
위해 메인 윈도우에서 호출되는 퍼블릭 메소드입니다.
public void Init()
{
DisplayImage(this.Images[_CurrentImageIndex]);
DoFade(this.Images[_CurrentImageIndex], 0, 1);
PanAndScale(this.Images[_CurrentImageIndex]);
Start();
}
Init() 에서 우선 콜렉션내의 이미지를 건네주어DisplayImage를 호출합니다. DisplayImage() 메소드는 이미지의
불투명도 캔버스상에서의 위치를 설정하여 이미지를 비주얼 트리에 추가합니다.
protected void DisplayImage( Image img )
{
img.Opacity = 0;
Canvas.SetTop( img, 0 );
Canvas.SetLeft( img, 0 );
this.Children.Add( img );
}
DisplayImage가 호출된 후에 Init() 메소드는 이미지에 대해서 DoFade 를 호출하여 이미지를 불투명하게 페이드
인하는 애니메이션이 개시합니다.
protected void DoFade(Image img, double startOpacity, double endOpacity)
{
DoubleAnimation anim = new DoubleAnimation(startOpacity, endOpacity, TimeSpan.FromSeconds(3));
AnimationClock ac = anim.CreateClock();
ac.CurrentStateInvalidated +=
delegate(object sender, EventArgs e)
{
if (sender == null)
return;
Clock clock = sender as Clock;
if (clock == null)
return;
if (clock.CurrentState == ClockState.Filling)
{
if (img.Opacity > .1)
{
this.HandleImageFadeInComplete();
}
else
{
this.Children.Remove(img);
}
}
};
img.BeginAnimation(Image.OpacityProperty, anim);
}
이 애니메이션 코드를 차례로 봅시다. 불투명한 이미지의 애니메이션을 해 불투명도는 엄밀하게 형태 지정된 애니메이션
DoubleAnimation 가 작성됩니다. From, To, BeginTime, Duration 등의 몇 가지 속성을 애니메이션으로
설정 한 후 AnimationClock 가 애니메이션에서 작성된 후 CurrentStateInvalidated 이벤트가 정의됩니다.
이미지가 제로에 페이드 한 뒤, ImageMontage 캔버스에서 그 이미지를 삭제하기 위해 필요합니다. 마지막으로 클락을 시작하고,
클락을 이미지 자체에 적용합니다.
C# 익명 메소드 사용
코드의 가장 흥미로운 부분은 다음 행입니다. 여기에서는 어느 특정의 메소드가 AnimationClock의
CurrentStateInvalidated 이벤트에 관련 지을 수 있습니다. C# 의 새로운 기능의 하나인 익명 메소드를 사용하여,
삭제한 이미지에 액세스 합니다. 메소드를 CurrentStateInvalidated 이벤트에만 할당했을 경우, 응용 프로그램은 그
애니메이션에 관련 지을 수 있고 있는 AnimationClock 의 인스턴스를 송신 측 오브젝트로서 받지만, 애니메이션화 된 이미지에는
액세스 할 수 없습니다. 이 때문에 클락은 조작할 수 있지만, 이미지 자체에 직접 액세스 할 수 없습니다. 이 응용 프로그램의 목적에서는
이미지의 불투명도가 0 이 된 후 이미지를 캔버스에서 삭제하기 위해, 애니메이션화 된 이미지를 참조할 필요가 있습니다.
이미지를 이벤트 처리기에 건네주기 위해서 사용되는 방법으로서 C# 익명 메소드 기능을 사용합니다 (상세한 내용은Juval Lowry
에 의한 「Create
Elegant Code with Anonymous Methods, Iterators, and Partial Classes」(영어)을
참조해 주세요). 이벤트 처리기의 작성시에 로컬 매개 변수 (이미지와 캔버스 자체의 양쪽 모두)를 얻을 수 있습니다.
불투명도의 변화에 따라, 다른 이벤트를 실행하여, 페이드를 한 것을 응용 프로그램에 통지할 수 있습니다. 이미지가 표시되지 않으면
(Opacity 가 0.1 미만)는 ImageMontage 자체에서 Children.Remove 를 호출하는 것으로, 이미지를 트리에서
삭제할 수 있습니다.
익명 메소드를 사용하는 이 방법은 요소의 애니메이션 클락 이벤트 처리기의 애니메이션의 요소를 취득할 때에 효과적으로 많은 상황에
적용할 수 있습니다.
이동 & 이미지 크기 조절 (Panning and Scaling the Images)
이미지의 이동과 그 크기 조절의 효과를 실현하기 위해서 캔버스 좌상의 위치를 애니메이션화하면, 이미지의 폭이 애니메이션화 됩니다.
protected void PanAndScale(Image img)
{
double startW = 800;
double endW = 1500;
double startX = 0;
double endX = -40;
double startY = 0;
double endY = -50;
DoubleAnimation anim = new DoubleAnimation(startW, endW, TimeSpan.FromSeconds(13));
img.BeginAnimation(Image.WidthProperty, anim);
// 위치를 애니메이션화해 화상을 중앙에 유지합니다.
DoubleAnimation anim1 = new DoubleAnimation(startY, endY, TimeSpan.FromSeconds(13));
img.BeginAnimation(Canvas.TopProperty, anim1);
DoubleAnimation anim2 = new DoubleAnimation(startX, endX, TimeSpan.FromSeconds(13));
img.BeginAnimation(Canvas.LeftProperty, anim2);
}
이미지의 폭의 값과 캔버스의 X 및 Y 의 이동거리를 여러 가지로 시험하여, 이동(panning) 과 줌(Zooming)의 외관을
변경할 수 있습니다.
이미지에서 이미지로 변경
Init() 메소드내의 마지막 메소드는 Start()입니다. 이 메소드는 10 초 마다 실행하는 DispatcherTimer 를
시작하여, 이미지를 연속적으로 루프 시킵니다.
public void Start()
{
if (_ImageChangeTimer == null)
{
_ImageChangeTimer = new DispatcherTimer(TimeSpan.FromSeconds(10), //대기 시간
DispatcherPriority.Background, // 우선 순위
new EventHandler(OnImageChangeTimer), //처리기
this.Dispatcher); // 현재의 디스팟체
}
_ImageChangeTimer.Start();
}
마지막에 설명하는 메소드는 10초 간격으로 실행하는 메소드 OnImageChangeTimer 입니다. 그 기능은 Init()
메소드와 같습니다. DoFade 메소드를 호출하여 현재의 이미지를 페이드아웃 하고, 새로운 이미지로 DoFade 를 호출하여 마지막에 _CurrentImageIndex
를 하나 더 늘립니다.
소개
The North Face In-Store Explorer 테스트 프로젝트에서는 첫번째 브랜드 텍스트를 사용자를 향해서 줌 아웃 한
다음에 패널의 슬라이드가 3D 공간에 출현하고 회전을 계속해 비디오가 각 패널에 매핑 됩니다. viewer의 의도는 사용자가 보고 싶은
비디오를 선택할 수 있도록 하는 것입니다. 슬라이드는 WPF 의 3D 엔진을 이용하는 주목해야 할 사용자 인터페이스의 개념인 것을 알 수
있습니다
WPF 로 슬라이드를 구현 하는 방법은 많이 있습니다. The North Face In-Store Explorer 테스트
프로젝트에서는 슬라이드는 System.Windows.Controls.ListBox 에서 만들어진 ListBox3D 라고 하는 컨트롤을
사용해 구축되었습니다. 처음엔 다소 이해하기 어려울 수도 있지만, 최종적으로 리스트 박스는 다음과 같습니다.

다만 WPF 에서는 ListBox 는 선택 가능한 아이템의 콜렉션에 대한 단순한 사용자 인터페이스의 개념입니다. 그 컨트롤의 스타일
설정은 응용 프로그램 개발자와 디자이너에 전면적으로 맡습니다. ListBox 에는 추상적인 개념으로 ListBox의 인스턴스 수는
무한합니다. 이 경우 ListBox는 3D 컨트롤입니다.
Listbox에서 파생된 ListBox3D 컨트롤은 아이템의 추가, 삭제, 선택등의 ListBox 의 다양한 기능이 있습니다.
ListBox3D의 스타일은 Viewport3D 를 사용해 설정됩니다. 이 때문에 ListBox3D 내의 각 아이템은 3D 기하
도형입니다. 비디오는 각 아이템의 기하 도형으로 소재로서 사용됩니다. ListBox3D 아이템 하나가 클릭되면, 범용 ListBox 와
같이 선택된 이벤트가 실행합니다.
이 동작을 표현하기 위해 VideoCarousel 이라는 샘플이 이 기사에 포함되어 있습니다. 이 샘플은 The North Face
In-Store Explorer 테스트 프로젝트의 기본 기능을 구현 합니다.
슬라이드의 코어는 두개의 클래스로 구성되어 있습니다. 첫번째 클래스는 System.Windows.Controls.ListBox 에서
파생된 새로운 ListBox3D 입니다. 두번째의 클래스는 DispatcherObject 에서 파생되어, 리스트내의 개개의 아이템을
나타냅니다.
public class List3DItem : DispatcherObject
{
}
public class List3D : ListBox
{
}
ListBox3D 를 스타일 설정 및 인스턴스화
이러한 클래스의 내용을 검토하기 전에 위와 같이 ListBox3D 를 스타일 설정할 필요가 있습니다. 스타일 설정은 리소스로서
XAML에서 행해집니다. Template 속성을 설정하는 스타일을 지정하여, 컨트롤의 비주얼 트리를 재이용할 수 있습니다. 이것은 그
컨트롤의 비주얼 트리를 포함한 컨트롤 템플릿입니다. 컨트롤 템플릿은 외부 사용자에는 표시되지 않는 상세 구현을 스스로 가지는 경향이
있습니다. 이 경우는 단일의 Viewport3D 를 작성합니다.
<Style x:Key="ExpeditionCarousel3DStyle" TargetType="{x:Type l:List3D}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type l:List3D}">
<Viewport3D Focusable="true" ClipToBounds="true" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
코드에서는 다음과 같이, 카메라, 라이트, 디폴트의 변환 등 뷰포트의 기본 속성의 설정을 처리합니다.
VideoCarousel의 XAML의 나머지의 부분은 매우 단순합니다. ListBox3D 와 캔버스 전체를 가리는 투과적인 그리드를
인스턴스화합니다.
<Canvas Background="Black" Width="1024" Height="768" x:Name="MainCanvas" Loaded="WindowLoaded">
<l:ListBox3D x:Name="Carousel"
Canvas.Top="0" Canvas.Left="0"
Width="1024" Height="768"
Visibility="Visible"
Style="{DynamicResource Carousel3DStyle}"
SelectionChanged="OnList3DItemSelected"/>
<Grid x:Name="CarouselMouseEventInterceptor"
Canvas.Top="0" Canvas.Left="0"
Width="1024" Height="768"
Background="Transparent"
PreviewMouseLeftButtonUp="CarouselMouseEventInterceptorClicked"></Grid>
</Canvas>
커스텀 리스트 박스가 윈도우의 리소스 섹션으로 선언한 스타일을 어떻게 사용하는지에 주목해 주세요. 또 리스트 박스가
ItemSelected 이벤트를 정의하는 방법도 주목해 주세요. 다만 응용 프로그램은 이벤트를 실행하기 위해 몇 가지 작업을 해야
합니다. 커스텀 리스트 박스에서는 히트 테스트를 자유롭게 실시할 수 없습니다. 실제로 여기에서는 리스트 박스 하위의 그리드가 도움이
됩니다. 계속 리스트 박스의 뒤로 가기 위해, 그리드는 "z 인덱스" 에 관한한 리스트 박스의 "전면" 에 표시됩니다. 이 그리드의
목적은 마우스의 클릭을 가로채서, 3D 히트 테스트 엔진에 건네주는 것입니다. 사용자가 실제로 한 개의 매시를 클릭했을 경우만
ItemSelected 이벤트를 실행합니다. 이 동작을 간단하게 검증해봅니다.
리스트 박스가 어떻게 구축되어 액티브화 되는지 봅시다. 슬라이드를 실행해 실행하기 위해서 일련의 처리를 합니다.
- ListBox3D 초기화 : 커스텀 리스트 박스가 인스턴스화 되어 그 생성자가 불려 갑니다. 생성자는 최상위의
모델 그룹을 뷰포트에 추가해, 카메라와 라이트도 추가하는 코드를 실행합니다. 뷰포트의 모델 그룹 전체에 변환을 작성합니다.
- ListItem3D 초기화 : ListItems 가 리스트 박스에 추가됩니다. 리스트아이템의 생성자에 그
리스트아이템에 관련지을 수 있는 매시가 fetch 됩니다. 또, 각 매시의 비디오 브러쉬가 작성됩니다.
- ListItem3D 레이아웃 : 뷰포트내의 각 매시를 서로 같은 거리에 배치하는 것으로 슬라이드가 구축됩니다.
- ListBox3D 애니메이션 : 슬라이드의 애니메이션이 개시합니다.
- ListItem 의 선택 : ListItem의 선택이 끝난 이벤트의 capther 방법.
- 매시 작성에 대한 보충 : 접속되어 있지 않은 삼각형으로 매시를 작성해 매시의 양면에서 소재를 사용할 수
있도록 하는 방법에 대해 설명합니다.
순서대로 상세하게 검토해 보겠습니다..
ListBox 초기화
ListBox3D의 생성자에서는 Loaded 및 Initialized의 두 가지의 이벤트가 정의되어 있습니다.
public ListBox3D()
{
this.Initialized += new EventHandler(OnInitialized);
this.Loaded += new RoutedEventHandler(OnLoaded);
}
두 가지 다른 이벤트가 있는 이유는ListBox3D에서 속성이 설정되기 전에Initialized 가 불려 가기 때문입니다.
Loaded 이벤트는 속성이 설정된 다음에 불려 갑니다. Initialized 이벤트로 MainGroup의 최상위모델 그룹을 작성합니다.
변환의 표준 세트 (Scale, Rotation, Translation)가 이 최상위그룹에 추가됩니다. 이러한 변환을 사용하고,
Model3DGroup 전체를 다음에 회전할 수 있습니다. 다음에 하나의 흰색 환경빛이 _MainGroup 에 아이로서 추가됩니다._MainGroup
자체에는 GeometryModel3Ds 가 포함되지 않습니다._ModelItems 로 불리는 자식의 ModelGroup3D가 포함됩니다.
다음으로 실제의 3D 기하 도형을 포함한 ListItem3D 오브젝트세트인 매시를 추가합니다. 이 시점에서 이러한
Model3DGroups모두 뷰포트에 추가되어 있지 않은 것에 주의해 주세요. 이것은 뷰포트 OnLoaded 이벤트로 행해집니다.
OnLoaded 이벤트가 실행하면, ModelGroups의 추가와 카메라 등의 뷰포트 외의 속성의 조작을 실시하기 위해서 먼저
뷰포트를 조작할 필요가 있습니다. 뷰포트 자체에의 접근은 예상보다 어렵습니다. 뷰포트는 스타일의 컨트롤 템플릿내에 있기 때문에
ListBox3D 에서는 직접 액세스 할 수 없습니다. 실제로는 다음의 FindViewport3D 메소드에서 보이도록, ListBox3D
비주얼 트리를 찾아 찾아내야 합니다.
private FrameworkElement FindViewport3D(Visual parent)
{
foreach (Visual visual in VisualOperations.GetChildren(parent))
{
if ((visual is FrameworkElement) && (visual is Viewport3D))
return (visual as FrameworkElement);
else
{
FrameworkElement result = FindViewport3D(visual);
if (result != null)
return result;
}
}
return null;
}
스타일 설정된 컨트롤에서 자식 요소를 추출할 필요가 있는 상황이 발생했을 경우에 많은 상황으로 재이용할 수 있는 편리한 메소드입니다.
뷰포트를 참조하면 카메라와 _MainGroup ModelGroup3D 양쪽 모두를 추가할 수 있습니다.
이 모든 처리는 코드로 실시했지만, XAML로 실시할 수도 있습니다. 두가지 방법의 차이는 없습니다. XML 비주얼 트리의 계층적인
형상이 직관적이라고 생각하는 사람도 있고, 오브젝트를 실제로 인스턴스화하여 콜렉션에 수동으로 추가하는 것이 편리하다고 생각하는
사람도 있습니다.
이 시점에서 뷰포트는 완전하게 기능합니다. 라이트, 카메라 및 포함된 모든 모델의 조작에 다음에 사용할 수 있는 디폴트의 변환
세트를 추가할 준비를 할 수 있었습니다. 여기서 ListItem3D 가 필요합니다.
ListItem3D 초기화
ListItem3D 오브젝트를 ListBox3D 에 추가할 준비를 할 수 있었습니다. 커스텀 ListBox3D에 대해서 독자적인
Add 메소드를 호출하는 것으로 추가합니다. 이 메소드는 새로운 ListItem 를 작성하여 Listitem의 VideoSrc 속성을
설정하여, 기본 ListBox 클래스의 protected AddChild 메소드를 호출합니다.
public void Add(string VideoSrc)
{
ListBox3DItem expListItem = new ListBox3DItem();
expListItem.VideoSrc = VideoSrc;
this.AddChild(expListItem);
}
ListItem3D 오브젝트의 콜렉션의 관리 모든 것이 ListBox 기본 클래스에 유래하는 기능으로 처리됩니다.
ListItem3D 작성 시에 그 생성자가 호출되어 _ItemGroup Model3DGroup 멤버 변수로 나타내지는
ListItem3D 자체의 실제의 기하 도형을 fetch 합니다. _ItemGroup 멤버 변수의 값은 각 아이템의 매시를 생성하는
클래스를 인스턴스화하는 GetMainGroup() 메소드에 의해 설정됩니다. 이 기사의 후반으로 3D 기하 도형의 작성 방법을
설명합니다. 여기에서는 각 ListItem3D가 인스턴스화 되면, 생성자가 매시를 구축해, 그것을 ListItem3D 의 _ItemGroup
속성에 관련 짓는 것만 이해하면 충분합니다.
이 시점에서 ListBox3D 내에는 ListItem3D 오브젝트세트가 있지만 이 시점에서 코드를 실행했을 경우는 매시를 뷰포트에
실제로 추가하기 위한 작업을 하지 않기 때문에 아무것도 표시되지 않습니다. ListItem3D 오브젝트를 ListBox3D 에 추가했을
뿐입니다. 뷰포트에 매시를 실제로 추가해, 스케일, 변환, 회전을 포함한 디폴트의 변환 세트로 위치 설정할 필요가 있습니다.
ListItem 레이아웃
ListBox3D 클래스의 Build 메소드가 Window1.cs 코드에서 호출되면, 기하 도형 실제로 뷰포트에 추가됩니다. 또
비디오 소재는 관련 기하 도형상에 있습니다.
기대하는 효과는 각 ListItem3D 를 서로 또 뷰포트의 중심 (0,0,0)으에서도 등거리에 배치하기 위해 뷰포트로의 기하
도형의 레이아웃은 수학적으로 약간 복잡하게 됩니다. 모델이 회전했을 때에 보고 있는 사람에게 기하 도형의 정면이 향하도록 각 기하 도형을
회전할 필요가 있습니다. 몇 가지 아이템이 추가되어도 올바르게 궤도를 주회하도록 ListItem3D 오브젝트를 배치하기 위해
ListBox3D 레이아웃의 디자인은 동적이어야 합니다.
Build 메소드의 코드를 조사하면, 이 실현 방법을 알 수 있습니다. 기본적으로는 최초로 ListItem3D 아이템의 총수를
360으로 나누어 각 아이템의 각도 오프셋을 산출하면 됩니다. 이 때문에 슬라이드에 4 개의 아이템이 있는 경우, 오프셋은 90
도입니다. 이 오프셋 각도를 염두에 두고, 아이템의 콜렉션을 루프하여, 이 오프셋에 근거해 각 회전각도가 설정되면 합계 각도가
증가합니다.
각 아이템의 변환 벡터도 다음의 메소드의 알고리즘을 사용하고, 오프셋 각도의 증가에 근거해 생성됩니다.
private Vector3D GetTranslationOffsetForCarouselAngle(double angle)
{
double radian = Math.PI * angle / 180.0;
double x = _Radius * Math.Cos(radian);
double z = _Radius * Math.Sin(radian);
return new Vector3D(x, 0, z);
}
이러한 변환이 확립되면, SetDefaultPosition 메소드를 호출해, 스케일, 변환, 및 회전 벡터를 건네주는 것으로
ListItem3D Model3DGroup 에 추가할 수 있습니다. 다음에 Model3DGroup 를 실제로 뷰포트의 _ModelItems
콜렉션에 추가합니다.
코드 구축의 마지막 순서로서 각 ListBox3D 오브젝트의 Initialize 메소드를 호출합니다. 이 메소드는 다음과 같이
매시에 비디오를 페인트 합니다.
public void Initialize()
{
if (this.VideoSrc != "")
{
_FrontVideoMediaTimeline = new MediaTimeline(new
Uri(BASE_DATA_DIR + this.VideoSrc, UriKind.Relative));
MediaClock mc = _FrontVideoMediaTimeline.CreateClock();
_FrontVideoMediaTimeline.RepeatBehavior = RepeatBehavior.Forever;
_FrontVideoDrawing.MediaClock = mc;
_FrontVideoDrawing.Rect = new Rect(0, 0, 5, 10);
DrawingBrush db = new DrawingBrush();
db.Drawing = _FrontVideoDrawing;
Brush br = db as Brush;
MaterialGroup mg = new MaterialGroup();
mg.Children.Add(new DiffuseMaterial(br));
GeometryModel3D gm3dFront = (GeometryModel3D) _ItemGroup.Children[MAIN_IMAGE_INDEX];
//2 개소에 표시하려면 1 개소에게만 페인트가 필요합니다.
gm3dFront.Material = mg;
_FrontVideoMediaTimeline.VolumeRatio = 0;
}
}
비디오를 WPF 응용 프로그램에 추가하려면, MediaTimeline 을 작성하여, 비디오의 장소를 타임 라인에 건네줘야합니다.
타임 라인으에서 타임 라인의 제어에 사용할 수 있는 미디어 클락을 작성합니다. 타임 라인에 반복 동작을 설정해 무한 루프시켜, 각
비디오가 마지막에 이르면 자동적으로 재개하도록 합니다. 타임 라인을 VideoDrawing과 관련지어 VideoDrawing 을 사용한
DrawingBrush 를 작성하여, diffuse material 에 브러쉬를 사용하여, 매시에 비디오를 페인트 합니다.
매시의 한편의 면을 페인트 하는 것만으로, 비디오는 어떻게 해 양면에 표시되는 방법은 성능을 최적화하는 방법의 하나로, 다음에
설명하겠습니다.
또, 비디오의 소리를 내지 않게VolumeRatio가 제로로 설정되어 있습니다. 모든 비디오의 소리가 동시에 재생되었을 경우, 귀에
거슬릴수 있기 때문에 슬라이드내의 장소에 따라 각 비디오의 볼륨을 조작하는 응용 프로그램 로직이 필요합니다. 이것을 다음에 설명합니다.
먼저 이러한 매시의 애니메이션화를 실시할 필요가 있습니다.
ListBox 애니메이션
매시가 올바르게 배치되어 비디오가 페인트 되었으므로, 실제로 모델의 회전과 비디오의 재생을 시작할 수 있습니다. 이 처리는
ListBox3D의Activate 메소드로 행해집니다. 이 메소드는 각 비디오의 재생하여, 다음의 두 가지 처리를 실시하는
StartAutoRotation 메소드를 호출합니다..
먼저 볼륨을 조정하는 코드를 초기화합니다. 각 비디오가 사용자의 정면으로 표시된 시간량에 관련된 DispatcherTimer를
설정합니다.
_VolumeAdjustTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(ADJUST_VOLUME_INTERVAL),
//대기하는 시간
DispatcherPriority.Background, // 우선 순위
new EventHandler(this.OnVolumeAdjustTimer), //처리기
this.Dispatcher); // 현재의 디스팟체
타이머OnVolumeAdjustTimer 의 콜백으로 각 비디오가 정면에 있는지에 따라 그 비디오의 볼륨이 설정됩니다. 슬라이드
전체가 동적이기 때문에 사용자의 정면으로 현재 어느 비디오가 있는지를 판단하기 위해 사용되는 알고리즘이 있습니다. 이 알고리즘을
사용하여, 비디오의 위치에 따라 볼륨이 설정됩니다. 이것은OnVolumeAdjustTimer 메소드로 조사할 수 있습니다..
다음은 StartAutoRotation 메소드가 슬라이드의 애니메이션을 개시합니다. 슬라이드에는 두 종류의 주요한 애니메이션이
있습니다. 한쪽은 비디오에서 소개하는 것을 보는 사람의 정면에 있는 저속인 애니메이션입니다. 다른 쪽의 애니메이션은 슬라이드가 회전하는
경우로 보다 고속의 애니메이션입니다. 응용 프로그램에서는 RotationState 열거에 근거하여, 실행하는 애니메이션이 결정됩니다.
if (_AutoRotationState != (int)ExpeditionCarousel3DRotationStates.ActivationRotate)
{
ShowcaseItem();
}
else
{
RotateToNextItem();
}
이러한 각 메소드는 회전의 각도를 계산해, RotateModel 메소드를 호출하여, 새롭게 RotationState를 설정한다는
점이 닮았습니다. 흥미로운 것은 RotateModel 메소드입니다. 이 메소드에서는 실제의 애니메이션이 개시됩니다.
private void RotateModel(double start, double end, int duration)
{
RotateTransform3D rt3D = _GroupRotateTransformY.GetCurrentValue();
Rotation3D r3d = rt3D.Rotation;
DoubleAnimation anim = new DoubleAnimation();
anim.From = start;
anim.To = end;
anim.BeginTime = null;
anim.AccelerationRatio = 0.1;
anim.DecelerationRatio = 0.6;
anim.Duration = new TimeSpan(0, 0, 0, 0, duration);
AnimationClock ac = anim.CreateClock();
ac.CurrentStateInvalidated += new EventHandler(OnRotateEnded);
ac.Controller.Begin();
r3d.ApplyAnimationClock(Rotation3D.AngleProperty, ac);
}
public void OnRotateEnded(object sender, EventArgs args)
{
if (sender == null)
return;
Clock clock = sender as Clock;
if (clock == null)
return;
if (clock.CurrentState == ClockState.Filling)
{
if (this.IsAutoRotating)
{
if (this.AutoRotationState == (int)ExpeditionCarousel3DRotationStates.ShowcaseRotate)
{
this.RotateToNextItem();
}
else
{
this.ShowcaseItem();
}
}
clock.CurrentStateInvalidated -= new EventHandler(this.OnRotateEnded);
clock = null;
}
}
여기에서는 슬라이드 자체의 회전은 Y축의 RotationTransform의 Rotation3D 오브젝트의AngleProperty 에
적용된 DoubleAnimation 애니메이션을 작성하여 실현됩니다. 흥미로운 점은 모델의 회전을 계속하기 위해 사용되는 메커니즘입니다.
CurrentStateInvalidated 이벤트는 애니메이션의 작성시에 정의되기 때문에 애니메이션이 종료되면 이벤트가 실행되어 새로운
애니메이션을 개시할 수 있습니다. 이 패턴은 응용 프로그램이 실행되는 한 계속됩니다.
ListItem 의 선택
The North Face In-Store Explorer 테스트 프로젝트서 ListItem3D 오브젝트가 선택되면 다양한
애니메이션이 개시됩니다. 디버거로 코드를 실행해OnList3DItemSelected 메소드에 breakpoint를 설정하면, 이것이
동작중인 것을 알 수 있습니다.
이 이벤트는 응용 프로그램에 의해서 실행되어 실제로는 뷰포트를 오버레이 하는 투과적인 그리드가 마우스로 클릭하면 이벤트가
실행됩니다. 이것은 PreviewMouseLeftButtonUp 이벤트를 정의한 그리드이며, 다음의 코드를 포함한 ListBox3D의
OnPreviewLeftClick 메소드에 이벤트를 건네줍니다.
public void OnPreviewLeftClick(object sender, MouseButtonEventArgs e)
{
Point p = e.GetPosition(this);
DoHitTest(_MainViewport3D, p);
//선택된 리스트아이템을 여기서 다시 취득해, 처리할 수 있습니다.
if ((_ciHitTest != null))
{
ListBox3DItem[] ListBox3DItemList = { _ciHitTest };
ListBox3DItem[] ListBox3DItemListRemoved = { };
//청취자에 대해서 OnSelectionChanged 이벤트도 실행 합니다.
OnSelectionChanged(new SelectionChangedEventArgs(ListBox3D.SelectionChangedEvent,
ListBox3DItemListRemoved, ListBox3DItemList));
}
}
분명히 마우스의 위치는 이벤트 인수에서 추출되어 히트 테스트 메소드로 건네집니다. 최종적으로 히트가 검색 되었을 경우,
protectedOnSelectionChanged 이벤트가 ListBox 기본 클래스에서 실행하여, 응용 프로그램 코드로 처리할 수
있습니다.
매시 작성에 대한 보충
마지막으로 매시 작성 방법을 설명합니다. 원래 기하 도형은 3D 모델링 툴을 사용해 작성되었습니다. 이러한 기하 도형은 .obj
파일로서 export 되어 Expression Interactive Designer 툴에 가져오도록 되었습니다. XAML가 추출되어
The North Face In-Store Explorer 테스트 프로젝트로 사용되었습니다. 이 워크플로는 성공적이지만, 큰 제한 사항이
있었습니다. export 된 기하 도형은 정면 1,뒷면 1, 측면4, 총6개의 매시로 구성되었습니다.
이것이 이상적이지 않은 솔루션임을 이해하려면, WPF3D의 3D 모델 표시 방법을 이해하는 것이 중요합니다. 일반적으로 디자이너는
각각의 다각형이 적은 다수의 GeometryModel3D와 각각의 다각형이 많은 소수의 GeometryModel3D 중 선택의 필요가
있는 경우에 후자를 선택합니다. 예를 들어 디자이너가 각면이 같은 소재로 페인트 된 큐브를 구축합니다. 큐브는 두 가지의 삼각형에서
구성되기 위해, 디자이너는 각각 한 개의 삼각형만 가지는 매시를 포함한 두 가지의 GeometryModel3D 를 작성하던가 또는 두
가지의 삼각형을 가지는 단일의 매시를 포함한 단일의 GeometryModel3D를 작성할 수 있습니다. 후자가 훨씬 효율적입니다.
비디오 슬라이드의 경우, 테스트 프로젝트의 디자인에서는 기하 도형의 양면에 재생중의 비디오를 표시하는 기능이 필요했습니다. 원래의
기하 도형은 기하 도형의 정면과 뒷면이 다른 매시로 구성되었기 때문에 두개의 다른 비디오 소재가 필요했습니다. 하나만 사용했을 경우,
아이템이 슬라이드의 가장 안쪽에 있을 때, 비디오는 안보이게 됩니다. 그러나 각 매시에 두개의 비디오를 페인트 하면, 하나가 아닌 복수의
GeometryModel3D에서 큐브를 작성하는 경우와 같이, 성능이 큰 폭으로 저하되는 것을 알 수 있습니다.
이 문제의 해결책은 단일의 소재를 사용하여 단일의 GeometryModel3D 에 포함된 단일의 매시에 정면의 매시와 뒷면의 매시의
양쪽 모두를 결합합니다. 매시의 삼각형이 반드시 접속되어 있다는 요건은 없기 때문에 이것은 Avalon 으로 가능합니다. 매시는 단순한
삼각형의 콜렉션이며, 이러한 삼각형은 서로 완전하게 분리되어 있습니다. 매시내의 모든 삼각형은 같은 소재로 페인트 되어 모두 같은 변환의
대상이 되지만, 반드시 연결되지 않습니다. 이렇게 하여 실제로 두개의 평면에서 구성된 단일의 매시를 생성할 수 있습니다. 비디오
슬라이드의 샘플의 경우 두개의 평면은 완전하게 병행으로 서로 어긋나 있습니다. The North Face In-Store Explorer
테스트 프로젝트에서는 평면은 실제로는 휘어져 있어, 구형 셸의 슬라이스를 작성을 위해 서로 일치하지 않습니다.
WPF 의 현재의 빌드에서는 뒷면의 커링(culling)이 항상 유효하기 때문에 매시의 뒷면이 정면과는 반대로 회전하는 것에
주의해야 합니다. 또 소재의 뒷면이 정말로 정면과 같은 인상을 주기 위해 뒷면이 정면의 "밀러 이미지" 가 되도록 수평 texture
구성요소를 반전할 필요가 있습니다.
비디오 슬라이드의 데모로 사용된 Model3DGroup는 다음과 같습니다.
<Model3DGroup x:Key="ListItem3DModel3DGroup" >
<Model3DGroup.Children>
<!--정면 및 배면에 단일의 매시를 사용해,
배면에서는 texture 좌표를 반전해
반대로 회전한다-->
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D
Positions="-0.5,0.5,0.125 -0.5,-0.5,0.125 0.5,-0.5,0.125
0.5,0.5,0.125 0.5,0.5,-0.125 0.5,-0.5,-0.125 -0.5,-0.5,-0.125 -0.5,0.5,-0.125"
TextureCoordinates="0,0 0,1 1,1 1,0 0,0 0,1 1,1 1,0"
TriangleIndices="0 1 2 2 3 0 4 5 6 6 7 4" />
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="#48565E" />
</GeometryModel3D.Material>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<!--4 개의 측면을 작성하는 단일의 매시-->
<MeshGeometry3D
Positions="0.5,0.5,0.125 0.5,-0.5,0.125 0.5,-0.5,-0.125
0.5,0.5,-0.125 -0.5,0.5,-0.125 -0.5,-0.5,-0.125 -0.5,-0.5,0.125 -
0.5,0.5,0.125 -0.5,0.5,-0.125 -0.5,0.5,0.125 0.5,0.5,0.125 0.5,0.5,-
0.125 -0.5,-0.5,0.125 -0.5,-0.5,-0.125 0.5,-0.5,-0.125 0.5,-0.5,0.125"
TextureCoordinates="0,0 0,1 1,1 1,0 0,0 0,1 1,1 1,0 0,0
0,1 1,1 1,0 0,0 0,1 1,1 1,0"
TriangleIndices="0 1 2 2 3 0 4 5 6 6 7 4 8 9 10 10 11 8
12 13 14 14 15 12" />
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="#48565E" />
</GeometryModel3D.Material>
</GeometryModel3D>
</Model3DGroup.Children>
</Model3DGroup>
첫번째 MeshGeometry3D를 잘 보면, 위치 콜렉션의 처음 4개 점의 Z 좌표가 1.25, 마지막 4 개 Z 좌표가
-1.25 인 것을 알 수 있습니다. 사실상 처음 4개의 좌표가 정면을 작성합니다.

마지막 4는 뒷면을 작성합니다.

반전은 TriangleIndices 콜렉션으로 행해집니다. 위치 0~3 으로 구성된 전반의 두 가지의 삼각형은 반시계 방향으로
회전하고, 마지막의 위치 4~7로 구성된 두 가지의 삼각형은 시계방향으로 회전합니다.
(프리 릴리스 버전의 WPF 성능 특성은 출시 전에 큰 폭으로 변경될 가능성이 있으며, 여기서 행해진 최적화가 최종 제품 출시
시에서는 반영되지 않을 수도 있음을 알려드립니다.)