화면 방향 지원(DirectX 및 C++)

많은 Windows 8 장치는 여러 화면 방향을 지원합니다. DirectX 및 C++로 작성한 Windows 스토어 앱은 CoreWindow::SizeChanged 이벤트를 처리할 때도 이 기능을 지원할 수 있습니다. 여기서는 Windows 8 장치의 그래픽 하드웨어가 효율적이면서 효과적으로 사용되도록 DirectX로 작성된 Windows 스토어 앱에서 화면 회전을 처리하는 모범 사례에 대해 설명합니다.

참고  이러한 모범 사례는 Microsoft Visual Studio 및 DXGI 스왑 체인 회전 샘플Direct2D 앱 프로젝트 템플릿에 완전히 설명되어 있습니다.

시작하기 전에 그래픽 하드웨어는 장치 방향에 관계없이 항상 같은 방향으로 픽셀 데이터를 출력한다는 점을 기억해 두세요. Windows 8 장치는 현재 디스플레이 방향을 확인하고(특정 유형의 센서 또는 소프트웨어 토글 사용) 사용자가 디스플레이 설정을 변경할 수 있도록 합니다. 이 때문에 Windows 8은 자체적으로 이미지 회전을 처리하여 장치 방향을 기준으로 "직립" 상태가 되도록 합니다. 기본적으로 앱은 방향에서 달라진 점(예: 창 크기)이 있다는 알림을 받게 됩니다. 이 경우 Windows 8은 최종 디스플레이를 위해 이미지를 즉시 회전합니다. 네 개의 특정 화면 방향(나중에 설명) 중 세 가지에서 Windows 8은 추가적인 그래픽 리소스와 계산을 사용하여 최종 이미지를 표시합니다.

DirectX로 작성된 Windows용 Windows 스토어 앱에서 DisplayProperties 개체는 앱이 쿼리할 수 있는 기본 디스플레이 방향 데이터를 제공합니다. 기본 방향은 디스플레이의 픽셀 너비가 높이보다 큰 가로이고, 대체 방향은 디스플레이가 한쪽 방향으로 90도 회전하여 너비가 높이보다 작아지는 세로입니다.

Windows 8에서는 다음의 네 가지 특정 디스플레이 방향 모드를 정의합니다.

  • 가로—Windows 8의 기본 디스플레이 방향으로, 회전의 기준 또는 동일 각도(0도)로 간주됩니다.
  • 세로—디스플레이가 시계 방향으로 270도(또는 시계 반대 방향으로 90도) 회전된 상태입니다.
  • 가로, 대칭 이동—디스플레이가 180도 회전된 상태입니다(위아래가 뒤집힘)
  • 세로, 대칭 이동—디스플레이가 시계 방향으로 90도(또는 시계 반대 방향으로 270도) 회전된 상태입니다.

디스플레이가 한 방향에서 다른 방향으로 회전하면 Windows 8은 내부적으로 회전 작업을 수행하여 그려진 이미지를 새 방향에 맞추며 화면에는 직립 이미지가 표시됩니다.

또한 Windows 8은 자동 전환 애니메이션을 표시하여 한 방향에서 다른 방향으로 전환할 때 매끄러운 사용자 환경을 제공합니다. 디스플레이 방향이 전환될 때 사용자에게는 이러한 전환이 표시된 화면 이미지의 고정된 확대/축소 및 회전 애니메이션으로 표시됩니다. Windows 8에서는 앱에 새 방향으로 레이아웃을 맞추는 시간을 할당합니다. 그렇지만 DirectX로 작성된 Windows 스토어 앱에서는 다른 프레임워크에 더 많은 시간을 할당해야 할 수 있으므로 새 레이아웃에 필요한 시간이 허용된 시간보다 짧은 것이 일반적입니다. 앱은 할당된 전체 시간을 사용하는 경우가 거의 없으므로 앱이 새 디스플레이 방향을 처리하기 위해 필요한 모든 작업을 완료했으며 새 이미지를 표시하기 위해 IDXGISwapChain::Present를 호출했음을 Windows 8에 직접 알릴 수 있습니다. CoreWindowResizeManager::NotifyLayoutCompleted를 사용하여 앱이 레이아웃을 미리 완료했음을 지정합니다.

화면 방향의 변화를 처리하는 일반적인 프로세스는 다음과 같습니다.

  1. 창 경계 값과 디스플레이 방향 데이터를 조합해서 사용하여 장치의 기본 디스플레이 방향에 스왑 체인을 맞춥니다.
  2. IDXGISwapChain1::SetRotation을 사용하여 스왑 체인의 방향을 Windows 8에 알립니다.
  3. 렌더링 코드를 변경하여 장치의 사용자 방향에 맞는 이미지를 생성합니다.
  4. CoreWindowResizeManager::NotifyLayoutCompleted를 사용하여 앱이 새 방향을 유지할 수 있음을 Windows 8에 알립니다.

Direct2D 및 Direct3D 렌더링 둘 다에 적용되는 이 프로세스의 전체 코드를 보려면 DXGI 스왑 체인 회전 샘플을 다운로드하세요.

DirectX로 작성된 Windows 스토어 앱에서 방향 변화를 처리하는 방법에는 다음 두 가지가 있습니다.

이러한 두 가지 방법을 살펴보겠습니다.

방법 1: 스왑 체인 크기 조정

회전을 처리하는 가장 쉬운 방법은 Windows 8에서 회전을 처리하도록 하는 것입니다. 물론 디스플레이가 1:1 가로 세로 비율이 아니므로 늘어나거나 찌그러져 보일 것입니다. 이러한 문제는 어떻게 해결할 수 있을까요? 창 경계의 높이 및 너비 값을 스왑하여 스왑 체인 크기를 조정하고 콘텐츠를 표시하면 됩니다.

DirectX로 작성된 Windows 스토어 앱에서 기본 디스플레이 크기를 조정하려면 다음 단계를 구현하세요.

  1. CoreWindow::SizeChanged 이벤트를 처리합니다.
  2. 스왑 체인의 크기를 창의 새 크기로 조정입니다.
  3. 렌더링 대상 및 기타 픽셀 데이터 버퍼와 같은 창 크기 종속 리소스를 다시 만듭니다.

이제 각 단계를 좀더 자세히 살펴보겠습니다.

첫 번째 단계는 CoreWindow::SizeChanged 이벤트에 대한 처리기를 등록하는 것입니다. 이 이벤트는 화면 크기가 변경될 때마다(예: 디스플레이가 회전될 때) 앱의 CoreWindow에 대해 발생됩니다. 앱의 package.appxmanifest 파일에 앱이 화면 회전을 지원한다고 지정한 경우 SizeChanged 이벤트를 처리해야 합니다. (그렇지 않은 경우 방향이 바뀌면 앱 디스플레이 크기가 자동으로 조정되며 스왑 체인 콘텐츠는 새 영역에 맞게 늘어나거나 줄어듭니다.)

SizeChanged 이벤트를 처리하려면 뷰 공급자가 구현해야 하는 IFrameworkView 인터페이스의 메서드 중 하나인 필수 SetWindow 메서드에서 CoreWindow::SizeChanged에 대한 처리기를 연결합니다.

이 코드 예제에서 CoreWindow::SizeChanged에 대한 이벤트 처리기는 OnWindowSizeChanged라는 메서드로, 뷰 공급자 개체에도 정의되어 있습니다. CoreWindow::SizeChanged가 발생하면 렌더러 개체에 정의된 UpdateForWindowSizeChange라는 메서드가 호출됩니다.


void MyDirectXApp::SetWindow(
    _In_ CoreWindow^ window
    )
{
  // ... Other UI event handlers assigned here ...

  window->SizeChanged += ref new TypedEventHandler<CoreWindow^, WindowSizeChangedEventArgs^>(
                                 this,
                                 &MyDirectXApp::OnWindowSizeChanged
                                 );
  // ...

}


void MyDirectXApp::OnWindowSizeChanged(CoreWindow^ sender, WindowSizeChangedEventArgs^ args)
{
  m_renderer->UpdateForWindowSizeChange(); // m_renderer is an object that inherits from DirectXBase
}

이제 콜백에서 스왑 체인 크기를 조정합니다. (코드 예제에서 이 작업은 DirectXBase::UpdateForWindowSizeChange에서 수행됩니다.)



// This routine is called in the event handler for the view SizeChanged event.
void DirectXBase::UpdateForWindowSizeChange()
{
  if (m_window->Bounds.Width  != m_windowBounds.Width ||
      m_window->Bounds.Height != m_windowBounds.Height ||
      m_orientation != DisplayProperties::CurrentOrientation)
  {
    m_d2dContext->SetTarget(nullptr);
    m_d2dTargetBitmap = nullptr;
    m_d3dRenderTargetView = nullptr;
    m_d3dDepthStencilView = nullptr;
    m_windowSizeChangeInProgress = true;
    CreateWindowSizeDependentResources();
  }
}




// Allocate all memory resources that change on a window SizeChanged event.
void DirectXBase::CreateWindowSizeDependentResources()
{
    // Store the window bounds so the next time we get a SizeChanged event we can
    // avoid rebuilding everything if the size is identical.
    m_windowBounds = m_window->Bounds;

    if (m_swapChain != nullptr)
    {
        // If the swap chain already exists, resize it.
        HRESULT hr = m_swapChain->ResizeBuffers(
            2,
            static_cast<UINT>(m_renderTargetSize.Width),
            static_cast<UINT>(m_renderTargetSize.Height),
            DXGI_FORMAT_B8G8R8A8_UNORM,
            0
            );

        if (hr == DXGI_ERROR_DEVICE_REMOVED)
        {
            // If the device was removed for any reason, a new device and swapchain will need to be created.
            HandleDeviceLost();

            // Everything is set up now. Don't continue to execute this method.
            return;
        }
        else
        {
            DX::ThrowIfFailed(hr);
        }
    }
    else
    {
        // ...
        // Otherwise, create a new one using the same adapter as the existing Direct3D device.
        // ...       
    }

    // Create a Direct3D render target view of the swap chain back buffer.
    ComPtr<ID3D11Texture2D> backBuffer;
    DX::ThrowIfFailed(
        m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer))
        );

    DX::ThrowIfFailed(
        m_d3dDevice->CreateRenderTargetView(
            backBuffer.Get(),
            nullptr,
            &m_d3dRenderTargetView
            )
        );

    // ...
    // Set up other window size dependent resources like Direct2D contexts, depth stencils, 
    // and 3D viewports here...
    // ...

}


또한 이 방법에서는 깊이 스텐실이나 기타 픽셀 데이터 버퍼와 같은 창 크기 종속형 DirectX 리소스와 Direct3D 뷰포트(표시할 3차원 화면이 있는 경우)를 다시 구성하거나 크기를 조정합니다.

이제 Windows 8이 시스템 회전 애니메이션을 완료하면 크기가 효과적으로 조정된 스왑 체인이 표시되면서 앱 화면 콘텐츠가 새 방향으로 올바르게 표시됩니다. 그러나 Windows 8은 회전 애니메이션을 위해 전체 화면의 픽셀 데이터 복사를 자체적으로 수행하며 화면 버퍼의 스냅숏을 작성하고 앱이 나타내는 스왑 체인을 표시하기 전에 이미지 자체를 회전합니다. 렌더링 파이프라인을 이미 회전했음을 Windows 8에 알림으로써 이러한 중복성을 없애고 GPU를 절약할 수 있습니다(특히 기본 해상도가 높은 경우). 다음 섹션에서는 이러한 잠재적인 최적화 과정을 알아봅니다. 또한 회전된 이미지를 작성할 때 추가적인 기술을 요구하는 스왑 체인의 Direct2D 및 Direct3D 구성 요소 처리 방법도 살펴보겠습니다.

이 프로세스의 작동 방식을 보여 주는 전체 코드를 확인하려면 Visual Studio의 Direct2D App 템플릿을 검토하거나 DXGI 스왑 체인 회전 샘플을 다운로드하세요.

방법 2: 스왑 체인 콘텐츠 미리 회전

Direct3D 11DXGI는 DirectX로 작성된 Windows 스토어 앱에서 스왑 체인의 창 이미지 데이터 방향을 Windows 8에 알리는 데 사용할 수 있는 새로운 API인 IDXGISwapChain1::SetRotation을 제공합니다. 장치의 방향과 스왑 체인의 방향이 일치하는 경우 Windows 8은 회전을 수행하지 않고 직접 창 이미지를 사용할 수 있으며 사용자는 하드웨어 수준에서 추가적인 전체 화면 복사 작업을 수행할 필요가 없으므로 비축한 성능 및 배터리 수명을 앱으로 되돌릴 수 있습니다. 그러나 사용자가 이러한 추가적인 효율성을 누릴 수 있도록 앱은 이미지를 방향에 맞게 스왑 체인으로 렌더링해야 합니다.

앞에서 살펴본 것처럼 적어도 DirectX로 작성된 Windows 스토어 앱은 스왑 체인의 버퍼 크기를 화면의 새 높이와 너비로 조정하여 나타냄으로써 회전 이벤트를 간단히 처리할 수 있습니다.

이 작업은 이러한 프로세스에 따라 진행되지만 다음과 같은 몇 가지 단점이 있습니다.

  • 중복된 전체 화면 복사 작업을 수행합니다.
  • 끌어온 상태 또는 채워진 상태로 전환되는 앱 창에 의해 또는 디스플레이 해상도 변경에 의해 발생될 수 있는 크기 변경 이벤트 유형과 실제로 차이가 없습니다.

다음의 업데이트된 단계를 수행하여 이러한 비효율성을 해결해 보겠습니다.

  1. 앞에 나온 대로 CoreWindow::SizeChanged 이벤트를 처리합니다.
  2. DisplayProperties::CurrentOrientation을 사용하여 해당 창 크기 변경 이벤트가 회전에 대한 것임을 확실히 정의합니다.
  3. 스왑 체인의 크기를 조정하지 않도록 합니다. 대신 현재 방향 데이터를 사용하여 앱이 잠근 Direct2D 및 Direct3D 변환을 업데이트하고 렌더링 파이프라인을 회전합니다.
  4. IDXGISwapChain1::SetRotation을 호출하여 스왑 체인 방향을 설정합니다.
  5. 앞에 나온 대로 창 크기 종속 리소스를 다시 만듭니다.

다음은 렌더링이 수행될 때 새 화면 방향에 대한 스왑 체인 크기를 조정하고 그래픽 파이프라인의 콘텐츠 회전 준비를 진행하는 과정입니다. 이 예제에서 DirectXBase::CreateWindowSizeDependentResources는 렌더러 개체에 대해 구현한 메서드입니다.

먼저 코드를 검토해 보고 계속 진행하는 것이 좋습니다.



// Allocate all memory resources that change on a window SizeChanged event.
void DirectXBase::CreateWindowSizeDependentResources()
{
    // Store the window bounds so the next time we get a SizeChanged event we can
    // avoid rebuilding everything if the size is identical.
    m_windowBounds = m_window->Bounds;

    // Calculate the necessary swap chain and render target size in pixels.
    auto windowWidth = ConvertDipsToPixels(m_windowBounds.Width);
    auto windowHeight = ConvertDipsToPixels(m_windowBounds.Height);

    // Swap width and height based on orientation.
    m_orientation = DisplayProperties::CurrentOrientation;
    bool swapDimensions = (
        m_orientation == DisplayOrientations::Portrait ||
        m_orientation == DisplayOrientations::PortraitFlipped
        );
    m_renderTargetSize.Width = swapDimensions ? windowHeight : windowWidth;
    m_renderTargetSize.Height = swapDimensions ? windowWidth : windowHeight;

    if (m_swapChain != nullptr)
    {
        // If the swap chain already exists, resize it.
        HRESULT hr = m_swapChain->ResizeBuffers(
            2,
            static_cast<UINT>(m_renderTargetSize.Width),
            static_cast<UINT>(m_renderTargetSize.Height),
            DXGI_FORMAT_B8G8R8A8_UNORM,
            0
            );

        if (hr == DXGI_ERROR_DEVICE_REMOVED)
        {
            // If the device was removed for any reason, a new device and swapchain will need to be created.
            HandleDeviceLost();

            // Everything is set up now. Don't continue to execute this method.
            return;
        }
        else
        {
            DX::ThrowIfFailed(hr);
        }
    }
    else
    {
        // Otherwise, create a new one using the same adapter as the existing Direct3D device.
        DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {0};
        swapChainDesc.Width = static_cast<UINT>(m_renderTargetSize.Width); // Match the size of the window.
        swapChainDesc.Height = static_cast<UINT>(m_renderTargetSize.Height);
        swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;           // This is the most common swap chain format.
        swapChainDesc.Stereo = false;
        swapChainDesc.SampleDesc.Count = 1;                          // Don't use multi-sampling.
        swapChainDesc.SampleDesc.Quality = 0;
        swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        swapChainDesc.BufferCount = 2;                               // Use double-buffering to minimize latency.
        swapChainDesc.Scaling = DXGI_SCALING_NONE;
        swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; // All Windows Store apps must use this SwapEffect.
        swapChainDesc.Flags = 0;

        ComPtr<IDXGIDevice1> dxgiDevice;
        DX::ThrowIfFailed(
            m_d3dDevice.As(&dxgiDevice)
            );

        ComPtr<IDXGIAdapter> dxgiAdapter;
        DX::ThrowIfFailed(
            dxgiDevice->GetAdapter(&dxgiAdapter)
            );

        ComPtr<IDXGIFactory2> dxgiFactory;
        DX::ThrowIfFailed(
            dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory))
            );

        CoreWindow^ window = m_window.Get();
        DX::ThrowIfFailed(
            dxgiFactory->CreateSwapChainForCoreWindow(
                m_d3dDevice.Get(),
                reinterpret_cast<IUnknown*>(window),
                &swapChainDesc,
                nullptr,
                &m_swapChain
                )
            );

        // Ensure that DXGI doesn't queue more than one frame at a time. This both reduces latency and
        // ensures that the application will only render after each VSync, minimizing power consumption.
        DX::ThrowIfFailed(
            dxgiDevice->SetMaximumFrameLatency(1)
            );
    }

    // Set the proper orientation for the swap chain, and generate 2-D and
    // 3-D matrix transformations for rendering to the rotated swap chain.
    // Note the rotation angle for the 2-D and 3-D transforms are different.
    // This is due to the difference in coordinate spaces.  Additionally,
    // the 3-D matrix is specified explicitly to avoid rounding errors.
    DXGI_MODE_ROTATION rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
    switch (m_orientation)
    {
        case DisplayOrientations::Landscape:
            rotation = DXGI_MODE_ROTATION_IDENTITY;
            m_rotationTransform2D = Matrix3x2F::Identity();
            m_rotationTransform3D = identity();
            break;
        case DisplayOrientations::Portrait:
            rotation = DXGI_MODE_ROTATION_ROTATE270;
            m_rotationTransform2D =
                Matrix3x2F::Rotation(270.0f) *
                Matrix3x2F::Translation(0.0f, m_windowBounds.Width);
            m_rotationTransform3D = float4x4( // 90-degree Z-rotation
                0.0f, -1.0f, 0.0f, 0.0f,
                1.0f, 0.0f, 0.0f, 0.0f,
                0.0f, 0.0f, 1.0f, 0.0f,
                0.0f, 0.0f, 0.0f, 1.0f
                );
            break;
        case DisplayOrientations::LandscapeFlipped:
            rotation = DXGI_MODE_ROTATION_ROTATE180;
            m_rotationTransform2D =
                Matrix3x2F::Rotation(180.0f) *
                Matrix3x2F::Translation(m_windowBounds.Width, m_windowBounds.Height);
            m_rotationTransform3D = float4x4( // 180-degree Z-rotation
                -1.0f, 0.0f, 0.0f, 0.0f,
                0.0f, -1.0f, 0.0f, 0.0f,
                0.0f, 0.0f, 1.0f, 0.0f,
                0.0f, 0.0f, 0.0f, 1.0f
                );
            break;
        case DisplayOrientations::PortraitFlipped:
            rotation = DXGI_MODE_ROTATION_ROTATE90;
            m_rotationTransform2D =
                Matrix3x2F::Rotation(90.0f) *
                Matrix3x2F::Translation(m_windowBounds.Height, 0.0f);
            m_rotationTransform3D = float4x4( // 270-degree Z-rotation
                0.0f, 1.0f, 0.0f, 0.0f,
                -1.0f, 0.0f, 0.0f, 0.0f,
                0.0f, 0.0f, 1.0f, 0.0f,
                0.0f, 0.0f, 0.0f, 1.0f
                );
            break;
        default:
            throw ref new Platform::FailureException();
            break;
    }

    DX::ThrowIfFailed(
        m_swapChain->SetRotation(rotation)
        );

    // Create a Direct3D render target view of the swap chain back buffer.
    ComPtr<ID3D11Texture2D> backBuffer;
    DX::ThrowIfFailed(
        m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer))
        );

    DX::ThrowIfFailed(
        m_d3dDevice->CreateRenderTargetView(
            backBuffer.Get(),
            nullptr,
            &m_d3dRenderTargetView
            )
        );

    // Create a depth stencil view for use with 3-D rendering if needed.
    CD3D11_TEXTURE2D_DESC depthStencilDesc(
        DXGI_FORMAT_D24_UNORM_S8_UINT,
        static_cast<UINT>(m_renderTargetSize.Width),
        static_cast<UINT>(m_renderTargetSize.Height),
        1,
        1,
        D3D11_BIND_DEPTH_STENCIL
        );

    ComPtr<ID3D11Texture2D> depthStencil;
    DX::ThrowIfFailed(
        m_d3dDevice->CreateTexture2D(
            &depthStencilDesc,
            nullptr,
            &depthStencil
            )
        );

    auto viewDesc = CD3D11_DEPTH_STENCIL_VIEW_DESC(D3D11_DSV_DIMENSION_TEXTURE2D);
    DX::ThrowIfFailed(
        m_d3dDevice->CreateDepthStencilView(
            depthStencil.Get(),
            &viewDesc,
            &m_d3dDepthStencilView
            )
        );

    // Set the 3-D rendering viewport to target the entire window.
    CD3D11_VIEWPORT viewport(
        0.0f,
        0.0f,
        m_renderTargetSize.Width,
        m_renderTargetSize.Height
        );

    m_d3dContext->RSSetViewports(1, &viewport);

    // Create a Direct2D target bitmap associated with the
    // swap chain back buffer and set it as the current target.
    D2D1_BITMAP_PROPERTIES1 bitmapProperties =
        BitmapProperties1(
            D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
            PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
            m_dpi,
            m_dpi
            );

    ComPtr<IDXGISurface> dxgiBackBuffer;
    DX::ThrowIfFailed(
        m_swapChain->GetBuffer(0, IID_PPV_ARGS(&dxgiBackBuffer))
        );

    DX::ThrowIfFailed(
        m_d2dContext->CreateBitmapFromDxgiSurface(
            dxgiBackBuffer.Get(),
            &bitmapProperties,
            &m_d2dTargetBitmap
            )
        );

    m_d2dContext->SetTarget(m_d2dTargetBitmap.Get());

    // Grayscale text anti-aliasing is recommended for all Windows Store apps.
    m_d2dContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);

}


다음 번에 이 메서드가 호출되는 동안 창의 현재 높이와 너비 값이 저장되면 디스플레이 경계에 대한 DIP(장치 독립적 픽셀) 값을 픽셀로 변환합니다. 이 샘플에서는 다음 코드를 실행하는 간단한 함수인 ConvertDipsToPixels를 호출합니다.

floor((dips * DisplayProperties::LogicalDpi / 96.0f) + 0.5f);

0.5f를 추가하여 가장 가까운 정수 값으로 반올림합니다.

부연하지만 CoreWindow 좌표는 항상 DIP로 정의됩니다. Windows 8 및 이전 버전의 Windows에서 DIP는 1인치의 1/96으로 정의되며 OS의 위쪽 정의에 맞춰집니다. 디스플레이 방향이 세로 모드로 회전되면 앱은 CoreWindow의 너비와 높이를 대칭 이동하며 렌더링 대상 크기(경계)가 그에 따라 변경되어야 합니다. Direct3D의 좌표는 항상 물리적 픽셀로 되어 있으므로 이러한 값을 Direct3D에 전달하여 스왑 체인을 설정하기 전에 CoreWindow의 DIP 값을 정수 픽셀 값으로 변환해야 합니다.

프로세스 측면에서 볼 때 단순히 스왑 체인 크기를 조정하는 경우보다 좀 더 많은 작업을 수행하게 됩니다. 즉, 표시를 위해 이미지를 작성하기 전에 이미지의 Direct2D 및 Direct3D 구성 요소를 실제로 회전하고 결과를 새 방향으로 렌더링했음을 스왑 체인에 알립니다. 다음에서는 DirectXBase::CreateWindowSizeDependentResources에 대한 코드 예제에 표시된 대로 이 프로세스를 보다 자세히 설명합니다.

  • 디스플레이의 새 방향을 결정합니다. 디스플레이가 가로에서 세로로 대칭 이동되었거나 그 반대로 대칭 이동되었으면 디스플레이 경계에 대한 높이 및 너비 값을 바꿉니다(DIP 값이 픽셀로 변경됨).

  • 그런 후 스왑 체인이 만들어졌는지 확인합니다. 아직 만들어지지 않았으면 IDXGIFactory2::CreateSwapChainForCoreWindow을 호출하여 만듭니다. 그렇지 않은 경우 IDXGISwapchain:ResizeBuffers를 호출하여 기존 스왑 체인의 버퍼 크기를 새 디스플레이 크기로 조정합니다. 렌더링 파이프라인에 의해 이미 콘텐츠가 회전되었으므로 회전 이벤트에 대한 스왑 체인 크기는 조정할 필요가 없지만 크기 조정이 필요한 끌기 및 채우기 이벤트와 같은 다른 크기 변경 이벤트가 있을 수 있습니다.

  • 그런 다음 스왑 체인으로 렌더링할 때 그래픽 파이프라인의 픽셀 또는 꼭지점에 각각 적용할 2차원 또는 3차원 행렬 변환을 설정합니다. 회전 행렬은 다음 네 가지가 가능합니다.

    • 가로(DXGI_MODE_ROTATION_IDENTITY)
    • 세로(DXGI_MODE_ROTATION_ROTATE270)
    • 가로, 대칭 이동(DXGI_MODE_ROTATION_ROTATE180)
    • 세로, 대칭 이동(DXGI_MODE_ROTATION_ROTATE90)

    디스플레이 방향을 결정하기 위해 Windows 8이 제공한 데이터(예: DisplayProperties::OrientationChanged 또는 SimpleOrientationSensor::OrientationChanged의 결과)를 토대로 올바른 행렬이 선택된 다음 화면의 각 픽셀((Direct2D) 또는 꼭지점(Direct3D) 좌표를 곱하여 화면 방향에 맞게 효과적으로 회전시킵니다. (Direct2D에서는 화면 원점이 왼쪽 위 구석으로 정의되지만 Direct3D에서는 원점이 창의 논리적 중심으로 정의됩니다.)

참고  회전에 사용되는 2차원 변환과 이러한 변환을 정의하는 방법에 대한 자세한 내용은 화면 회전을 위한 행렬 정의(2차원)를 참조하세요. 회전에 사용되는 3차원 변환에 대한 자세한 내용은 화면 회전을 위한 행렬 정의(3차원)를 참조하세요.

이제 IDXGISwapChain1::SetRotation을 호출한 후 업데이트된 회전 행렬을 제공해야 합니다. 이 과정은 중요합니다.

m_swapChain->SetRotation(rotation);

또한 렌더 메서드가 새 프로젝션을 계산할 때 가져올 수 있는 선택된 회전 행렬을 저장합니다. 이 행렬은 최종 3차원 프로젝션을 렌더링하거나 최종 2차원 레이아웃을 작성할 때 사용합니다. (자동으로 적용되지는 않습니다.)

그런 다음 회전된 3차원 보기에 대한 새로운 렌더링 대상과 보기에 대한 새 깊이 스텐실 버퍼를 만듭니다. ID3D11DeviceContext:RSSetViewports를 호출하여 회전된 화면에 대한 3차원 렌더링 뷰포트를 설정합니다.

마지막으로 회전하거나 배치할 2차원 이미지가 있는 경우 ID2D1DeviceContext::CreateBitmapFromDxgiSurface를 사용하여 2차원 렌더링 대상을 크기 조정된 스왑 체인에 대한 쓰기 가능 비트맵으로 만들고 업데이트된 방향에 대한 새 레이아웃을 작성합니다. 앤티앨리어싱 모드와 같이 렌더링 대상에 대해 필요한 속성을 설정합니다(코드 예제 참조).

이제 스왑 체인을 표시합니다.

CoreWindowResizeManager를 사용하여 회전 지연 단축

기본적으로 Windows 8은 앱 모델이나 언어에 관계없이 이미지 회전을 완료하기 위해 앱에 짧지만 충분히 인식 가능한 시간을 제공합니다. 그러나 앱이 여기에 설명된 기법 중 하나를 사용하여 회전 계산을 수행하면 이 기간이 끝나기 전에 작업을 끝낼 수 있을 것입니다. 여러분은 이 시간을 다시 얻고 회전 애니메이션을 완료하려고 할 것입니다. CoreWindowResizeManager를 통해 이러한 결과를 얻을 수 있습니다.

다음은 CoreWindowResizeManager의 사용 방법입니다. CoreWindow::SizeChanged 이벤트가 발생하면 이벤트에 대한 처리기 내에서 CoreWindowResizeManager::GetForCurrentView를 호출하여 CoreWindowResizeManager의 인스턴스를 획득하고, 새 방향의 레이아웃이 완료되고 표시되면 NotifyLayoutCompleted를 호출하여 회전 애니메이션을 완료하고 앱 화면을 표시할 수 있음을 Windows에 알립니다.

CoreWindow::SizeChanged에 대한 이벤트 처리기의 코드는 다음과 유사할 수 있습니다.


CoreWindowResizeManager^ resizeManager = Windows::UI::Core::CoreWindowResizeManager::GetForCurrentView();

// ... build the layout for the new display orientation ...

resizeManager->NotifyLayoutCompleted();

사용자가 디스플레이 방향을 회전하면 Windows 8은 사용자에게 피드백으로 앱에 독립적인 애니메이션을 표시합니다. 이 애니메이션의 세 부분이 다음 순서로 나타납니다.

  • Windows 8에서 원래 이미지가 축소됩니다.
  • Windows 8은 새 레이아웃을 다시 작성하는 데 소요되는 시간 동안 이미지를 보류합니다. 앱에는 이러한 시간이 필요하지 않으므로 사용자는 이 시간을 단축하려고 할 것입니다.
  • 레이아웃 시간이 만료되거나 레이아웃 완료 알림이 수신되면 Windows는 이미지를 회전한 다음 새 방향으로 크로스페이드하면서 확대/축소합니다.

위의 세 번째 글머리 기호에 설명된 것처럼 앱이 NotifyLayoutCompleted를 호출하면 Windows 8은 제한 시간을 중지하고 회전 애니메이션을 완료한 다음 앱으로 컨트롤을 반환합니다. 그러면 새 디스플레이 방향으로 그리기가 진행됩니다. 전체적으로 앱은 약간 더 유연해지고 더 빠르게 응답하는 것처럼 느껴지고 좀 더 효율적으로 작동하는 효과를 얻게 됩니다.

부록 A: 화면 회전을 위한 행렬 적용(2차원)

회전 프로세스 최적화의 샘플(및 DXGI 스왑 체인 회전 샘플)을 통해 Direct2D 출력과 Direct3D 출력에 대해 다른 회전 행렬을 적용했다는 사실을 알게 되었을 것입니다. 먼저 2차원 행렬을 살펴보겠습니다.

Direct2D 콘텐츠와 Direct3D 콘텐츠에 동일한 회전 행렬을 적용할 수 없는 이유에는 두 가지가 있습니다.

  • 첫째, 두 콘텐츠는 다른 카티전 좌표 모델을 사용합니다. Direct2D는 y 좌표의 양수 값이 원점에서 위로 커지는 오른손잡이 규칙 을 사용합니다. 그러나 Direct3D는 y 좌표의 양수 값이 원점에서 오른쪽으로 커지는 왼손잡이 규칙을 사용합니다. 결과적으로 Direct2D의 경우 화면 좌표의 원점이 왼쪽 위에 있고 Direct3D의 경우 화면의 원점(프로젝션 평명)이 왼쪽 아래에 있게 됩니다. (자세한 내용은 3차원 좌표계를 참조하세요.)

    Direct3D 좌표계Direct2D 좌표계
  • 반올림 오차를 피하려면 2개의 3차원 회전 행렬을 명시적으로 지정해야 합니다.

스왑 체인은 원점이 왼쪽 아래에 있다고 간주하므로 오른손잡이용 Direct2D 좌표계를 스왑 체인에 사용되는 왼손잡이용 Direct2D 좌표계에 맞추기 위해 회전을 수행해야 합니다. 특히 회전 행렬에 회전된 좌표계 원점에 대한 변환 행렬을 곱하여 새로운 왼손잡이용 방향으로 이미지를 다시 배치하고 CoreWindow의 좌표 영역에서 스왑 체인의 좌표 영역으로 이미지를 변환합니다. 또한 앱은 Direct2D 렌더링 대상이 스왑 체인에 연결되어 있을 때 이러한 변환을 일관되게 적용해야 합니다. 그러나 앱이 스왑 체인과 직접적으로 연결되어 있지 않은 중간 화면에 그리는 경우에는 이 좌표 영역 변환을 적용하지 마세요.

네 가지 가능한 회전 중에서 올바른 행렬을 선택하는 코드는 다음과 같습니다(새 좌표계 원점으로 전환).


	// Set the proper orientation for the swap chain, and generate 2-D and
 // 3-D matrix transformations for rendering to the rotated swap chain.

    DXGI_MODE_ROTATION rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
    switch (m_orientation)
    {
        case DisplayOrientations::Landscape:
            rotation = DXGI_MODE_ROTATION_IDENTITY;
            m_rotationTransform2D = Matrix3x2F::Identity();
            break;
        case DisplayOrientations::Portrait:
            rotation = DXGI_MODE_ROTATION_ROTATE270;
            m_rotationTransform2D =
                Matrix3x2F::Rotation(270.0f) *
                Matrix3x2F::Translation(0.0f, m_windowBounds.Width);
            break;
        case DisplayOrientations::LandscapeFlipped:
            rotation = DXGI_MODE_ROTATION_ROTATE180;
            m_rotationTransform2D =
                Matrix3x2F::Rotation(180.0f) *
                Matrix3x2F::Translation(m_windowBounds.Width, m_windowBounds.Height);
            break;
        case DisplayOrientations::PortraitFlipped:
            rotation = DXGI_MODE_ROTATION_ROTATE90;
            m_rotationTransform2D =
                Matrix3x2F::Rotation(90.0f) *
                Matrix3x2F::Translation(m_windowBounds.Height, 0.0f);
             break;
        default:
            throw ref new Platform::FailureException();
            break;
    }

2차원 이미지에 대해 올바른 회전 행렬과 원점이 지정되면 ID2D1DeviceContext::BeginDrawID2D1DeviceContext::EndDraw 호출 중에서 ID2D1DeviceContext::SetTransform 호출을 사용하여 설정합니다.

경고   Direct2D에는 변환 스택이 없습니다. 앱이 그리기 코드의 일부로 ID2D1DeviceContext::SetTransform도 사용하는 경우 나중에 적용한 다른 변환을 이 행렬에 곱해야 합니다.


m_d2dContext->BeginDraw();

// draw reference bitmap at the center of the screen
m_d2dContext->SetTransform(
        Matrix3x2F::Translation(
            m_windowBounds.Width / 2.0f - bitmapWidth / 2.0f,
            m_windowBounds.Height / 2.0f - bitmapHeight / 2.0f
        ) *
     m_rotationTransform2D // apply 2D prerotation transform
);
m_d2dContext->DrawBitmap(m_referenceBitmap.Get());

m_d2dcontext->EndDraw();

앞의 예제에서 회전이 발생한 후에 항상 화면 중앙의 왼쪽 위에 배치되어야 하는 이미지의 왼쪽 위 구석을 지정하는 다른 변환 행렬을 새 회전 행렬에 곱하여 이미지를 중앙에 배치할 수도 있습니다. (이 단계는 선택적입니다.) ID2D1DeviceContext::DrawBitmap 호출을 사용하여 회전한 이미지를 디바이스 컨텍스트로 그립니다.

다음 번에 스왑 체인을 나타낼 때 2차원 이미지는 새 디스플레이 방향에 맞게 회전됩니다.

부록 B: 화면 회전을 위한 행렬 적용(3차원)

회전 프로세스 최적화의 샘플(및 DXGI 스왑 체인 회전 샘플)에서 가능한 각 화면 방향에 대한 특정 변환 행렬을 정의했습니다. 이제 3차원 화면 회전을 위한 행렬을 살펴보겠습니다. 앞에 나온 것처럼 가능한 네 가지 방향 각각에 대해 행렬 집합을 만듭니다. 반올림 오차를 방지하고 시각적으로 불필요한 부분을 최소화하려면 코드에서 명시적으로 행렬을 선언합니다.

이러한 3차원 회전 행렬을 다음과 같이 설정합니다. 다음 코드 예제에 표시되는 행렬은 카메라의 3차원 화면 영역에 있는 점을 정의하는 꼭지점의 0, 90, 180 및 270도 회전에 대한 표준 회전 행렬입니다. 화면의 2차원 프로젝션이 계산될 때 화면의 각 꼭지점 [x, y, z] 좌표 값에 이 회전 행렬을 곱해집니다.


	// Set the proper orientation for the swap chain, and generate the
	// 3-D matrix transformation for rendering to the rotated swap chain.
	DXGI_MODE_ROTATION rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
	switch (m_orientation)
	{
		case DisplayOrientations::Landscape:
			rotation = DXGI_MODE_ROTATION_IDENTITY;
			m_orientationTransform3D = XMFLOAT4X4( // 0-degree Z-rotation
				1.0f, 0.0f, 0.0f, 0.0f,
				0.0f, 1.0f, 0.0f, 0.0f,
				0.0f, 0.0f, 1.0f, 0.0f,
				0.0f, 0.0f, 0.0f, 1.0f
				);
			break;

		case DisplayOrientations::Portrait:
			rotation = DXGI_MODE_ROTATION_ROTATE270;
			m_orientationTransform3D = XMFLOAT4X4( // 90-degree Z-rotation
				0.0f, 1.0f, 0.0f, 0.0f,
				-1.0f, 0.0f, 0.0f, 0.0f,
				0.0f, 0.0f, 1.0f, 0.0f,
				0.0f, 0.0f, 0.0f, 1.0f
				);
			break;

		case DisplayOrientations::LandscapeFlipped:
			rotation = DXGI_MODE_ROTATION_ROTATE180;
			m_orientationTransform3D = XMFLOAT4X4( // 180-degree Z-rotation
				-1.0f, 0.0f, 0.0f, 0.0f,
				0.0f, -1.0f, 0.0f, 0.0f,
				0.0f, 0.0f, 1.0f, 0.0f,
				0.0f, 0.0f, 0.0f, 1.0f
				);
			break;

		case DisplayOrientations::PortraitFlipped:
			rotation = DXGI_MODE_ROTATION_ROTATE90;
			m_orientationTransform3D = XMFLOAT4X4( // 270-degree Z-rotation
				0.0f, -1.0f, 0.0f, 0.0f,
				1.0f, 0.0f, 0.0f, 0.0f,
				0.0f, 0.0f, 1.0f, 0.0f,
				0.0f, 0.0f, 0.0f, 1.0f
				);
			break;

		default:
			throw ref new Platform::FailureException();
	}

다음과 같이 IDXGISwapChain1::SetRotation을 호출하여 스왑 체인에 대한 회전 유형을 설정합니다.

m_swapChain->SetRotation(rotation);

이제 render 메서드에서 다음과 비슷한 코드를 구현합니다.


struct ConstantBuffer // this struct provided for illustration
{
    // other constant buffer matrices and data defined here

    float4x4 projection; // current matrix for projection
} ;
ConstantBuffer  m_constantBufferData;          // constant buffer resource data

// ...

// rotate the projection matrix as it will be used to render to the rotated swap chain
m_constantBufferData.projection = mul(m_constantBufferData.projection, m_rotationTransform3D);

render 메서드를 호출하면 현재 회전 행렬(클래스 변수 m_orientationTransform3D로 지정)에 현재 프로젝션 행렬을 곱한 다음 작업 결과를 렌더러의 새 프로젝션 행렬로 할당합니다. 스왑 체인을 표시하여 업데이트된 디스플레이 방향으로 화면을 나타냅니다.

관련 항목

DXGI 스왑 체인 회전 샘플
회전에 대한 지침
사용자 조작에 응답(DirectX 및 C++)

 

 

표시:
© 2015 Microsoft