Este artigo foi traduzido por máquina.

Fator DirectX

Transformações 3D em bitmaps 2D

Charles Petzold

Baixar o código de exemplo

Programação de gráficos tridimensionais é principalmente uma questão de criar ilusões de ótica. Imagens são processadas em uma tela plana consistindo de uma matriz bidimensional de pixels, mas esses objetos devem aparecer ter uma terceira dimensão com profundidade.

Provavelmente o maior contribuinte para a ilusão de 3D é sombreamento — a arte e a ciência da coloração pixels para que as superfícies se assemelham a texturas reais com iluminação e sombras.

Tudo isso, no entanto, é uma infra-estrutura de objetos virtuais, descrito por coordenadas 3D em baixo.  Em última análise, estas coordenadas 3D são achatadas em um espaço 2D, mas até essa etapa final, coordenadas 3D são frequentemente sistematicamente modificadas de várias formas com o uso de transformações. Matematicamente, transformações são operações em álgebra matricial. Alcançar a fluência nessas transformações 3D é crucial para quem quer se tornar um programador de gráficos 3D.

Recentemente, estive explorando as formas que 3D é suportado no componente Direct2D do DirectX. Explorar 3D dentro da relativa familiaridade e conforto do Direct2D permite que você se familiarizar com conceitos 3D antes do mergulho profundo muito assustador em Direct3D.

Bitmaps em 3D?

Uma das várias formas que 3D é suportado em Direct2D está escondida como o último argumento para DrawBitmap métodos definidos pelo ID2D1DeviceContext. Esse argumento permite que você aplicar uma transformação 3D para um bitmap 2D. (Esse recurso é especial para ID2D1DeviceContext. -Não é suportado pelos métodos definidos pelo ID2D1RenderTarget ou as outras interfaces que derivam de que DrawBitmap.)

DrawBitmap métodos definidos pelo ID2D1DeviceContext tem um argumento final do tipo D2D1_MATRIX_4X4_F, que é uma matriz de transformar de 4 × 4 que executa uma transformação 3D no bitmap como ela é processada para a tela:

void DrawBitmap(ID2D1Bitmap *bitmap,
                D2D1_RECT_F *destinationRectangle,
                FLOAT opacity,
                D2D1_INTERPOLATION_MODE interpolationMode,
                const D2D1_RECT_F *sourceRectangle,
                const D2D1_MATRIX_4X4_F *perspectiveTransform)

Esta parece ser a única finalidade de D2D1_MATRIX_4X4_F. Ele não é usado em outro lugar no DirectX. Na programação Direct3D, a biblioteca de matemática do DirectX é usada em vez disso representam transformações 3D.

DD2D1_MATRIX_4X4_F é um typedef para D2D_MATRIX_4X4_F, que é definido em Figura 1. É basicamente uma coleção de 16 valores float, dispostos em quatro fileiras de quatro colunas. Você pode referenciar o valor na terceira linha e segunda coluna usando o _32 de membro de dados, ou você pode obter o mesmo valor como o elemento de matriz baseada em zero m [2] [1].

Figura 1 o 3D transformar Matrix aplicado a Bitmaps

typedef struct D2D_MATRIX_4X4_F
{
  union
  {
    struct
    {
      FLOAT _11, _12, _13, _14;
      FLOAT _21, _22, _23, _24;
      FLOAT _31, _32, _33, _34;
      FLOAT _41, _42, _43, _44;
    } ;
    FLOAT m[4][4];
  };
} D2D_MATRIX_4X4_F;

No entanto, quando mostrando a matemática de transform, vou em vez disso me referir o elemento na terceira linha e segunda coluna da matriz como m32. Toda a matriz pode ser representado na notação de matriz tradicional, da seguinte forma:

A transformação de bitmap 3D em Direct2D está subjacente uma instalação similar no Windows Runtime, onde ele aparece como a estrutura Matrix3D e a propriedade de projeção definida por UIElement. Os dois mecanismos são tão semelhantes que você pode multifaces seu conhecimento e experiência entre os dois ambientes.

A transformação Linear

Muitas pessoas encontrando transformações 3D pela primeira vez perguntam-se: Por que é uma matriz 4 × 4? Uma matriz 3 × 3 não deve ser adequada para 3D?

Para responder a esta pergunta enquanto explora a transformação 3D na DrawBitmap, eu criei um programa chamado BitmapTransformExperiment que está incluído com o código para download deste artigo. Este programa contém controles de spinner caseiro que permitem que você selecione os valores para os 16 elementos da matriz de transformação e ver como a matriz afeta a exibição de um bitmap. A Figura 2 mostra uma exibição típica.


Figura 2 o programa de BitmapTransformExperiment

Para suas experimentações iniciais, restringir-se a atenção para as top três linhas e três colunas mais à esquerda da matriz. Estas compõem uma matriz 3 × 3 que executa a seguinte transformação:

A 1 matriz × 3 no canto esquerdo representa uma coordenada 3D. Para o bitmap, o valor de x varia de 0 a largura do bitmap; y varia de 0 a altura do bitmap; e z é 0.

Quando o 1 × 3 matriz é multiplicada pela matriz de transformação de 3 × 3, a multiplicação de matrizes padrão resulta em coordenadas transformadas:

A matriz de identidade — em que os elementos na diagonal da m11, m22 e m33 são todos 1 e tudo o resto é 0 — resulta em nenhuma transformação.

Porque eu estou começando com um bitmap plano, a coordenada z é 0, portanto, m31, m32 e m33 não têm efeito sobre o resultado. Quando o bitmap transformado é renderizado na tela, a (x*', y', z') resultado é recolhido em um sistema de coordenadas 2D plana, ignorando a z'* coordenar, o que significa que m13 e m23 m33 não têm efeito. É por isso que a terceira linha e terceira coluna são cinza fora no programa BitmapTransformExperiment. Você pode definir valores para esses elementos de matriz, mas eles não afetam como o bitmap é processado.

O que você descobrirá é que m11 é um fator de escala horizontal com um valor padrão de 1. Torná-lo maior ou menor para aumentar ou diminuir a largura de bitmap, ou torná-lo negativo para inverter o bitmap em torno do eixo vertical. Da mesma forma, m22 é um fator de escala vertical.

O valor de m21 é um fator de inclinação vertical: Valores diferentes de 0 transformar o bitmap retangular em um paralelogramo como a borda direita é deslocada de cima ou para baixo. Da mesma forma, m12 é um fator de inclinação horizontal.

Uma combinação de inclinação horizontal e inclinação vertical pode resultar em rotação. Para girar o bitmap no sentido horário por um ângulo específico, conjunto m11 e m22 para o cosseno desse ângulo, conjunto m21 para o seno do ângulo e conjunto m12 para o seno negativo. Figura 2 mostra a rotação de 30 graus.

No contexto do 3D, a rotação mostrada na Figura 2 é na verdade uma rotação em torno do eixo Z, que conceitualmente se estende para fora da tela. Para um ângulo α, a matriz de transformar esta aparência:

Também é possível girar o bitmap em torno do eixo Y ou eixo X. Rotação em torno do eixo Y não afeta a coordenada de y, então a matriz de transformação é o seguinte:

Se você tentar fazer isso com o programa BitmapTransformExperiment, você vai descobrir que somente o valor de m11 tem um efeito. Defini-lo como o cosseno de um ângulo de rotação apenas diminui a largura do bitmap renderizado. Essa diminuição na largura é consistente com a rotação em torno do eixo Y.

Da mesma forma, esta é a rotação em torno do eixo X:

No programa BitmapTransformExperiment, isso resulta em redução da altura do bitmap.

Os sinais dos fatores dois seno em matrizes de transformação regem o sentido de rotação. Conceitualmente, presume-se o eixo Z positivo para estender a partir da tela, e as rotações seguem a regra da esquerda: Alinhe o polegar da mão esquerda com o eixo de rotação e aponte-o para valores positivos; a curva de seus outros dedos indica o sentido de rotação para ângulos positivos.

O tipo de transformação 3D representado por essa matriz de transformação de 3 × 3 é conhecido como uma transformação linear. A transformação envolve apenas constantes multiplicados por x, y e z as coordenadas. Não importa o que você entrar no primeiro três linhas e colunas da matriz de transformação de números, o bitmap nunca é transformado em algo mais exótico do que um paralelogramo; em três dimensões, um cubo é sempre transformado em um paralelepípedo.

Você já viu como o bitmap pode ser escalado, distorcida e girado, mas durante todo este exercício, o canto superior esquerdo do bitmap manteve-se fixo em um único local. (Esse local é regido por uma transformação 2D definida no método Render antes para a chamada de DrawBitmap). A incapacidade de mover o canto superior esquerdo do bitmap resultante da matemática de transformação linear. Não há nada na fórmula de transformação que pode deslocar uma (0, 0, 0) aponte para outro local, que é um tipo de transformação conhecida como tradução.

Realização de tradução

Para entender como obter a tradução em 3D, vamos brevemente pensar transformações 2D.

Em duas dimensões, uma transformação linear é uma matriz 2 x 2, e é capaz de escalar, inclinação e rotação. Para obter a tradução também, os gráficos 2D são assumidos como existe no espaço 3D, mas em um plano 2D onde a coordenada z sempre é igual a 1. Para acomodar a dimensão adicional, a matriz de transformação linear 2D é expandida em uma matriz 3 × 3, mas geralmente a última linha é fixa:

Os resultados de multiplicação de matriz em estas transformam as fórmulas:

Os fatores de m31 e m32 são os fatores de conversão. O segredo por trás desse processo é que a tradução em duas dimensões é equivalente a inclinação em três dimensões.

Um processo análogo é usado para gráficos 3D: A coordenada 3D presume-se que existem no espaço de 4D, onde a coordenada da quarta dimensão é 1. Mas para representar um ponto de coordenada 4D, você tem um bobo problema pouco prático: É um ponto 3D (x, y, z) e nenhuma carta vem depois do z, então a carta que você usa para a quarta dimensão? A letra disponível mais próxima é w, então a coordenada 4D é (x, y, z, w).

A matriz de transformação linear 3D é expandida para 4 × 4 para acomodar a dimensão extra:

As fórmulas de transformação são agora:

Você agora tem tradução ao longo dos eixos X, Y e Z com m41, m42 e m43. O cálculo parece ocorrer no espaço de 4D, mas na verdade é restrito a uma secção 3D do espaço de 4D, onde a coordenada w é sempre 1.

Coordenadas homogêneas

Se você brincar com valores m41 e m42 em BitmapTransformExperiment, você verá que eles de fato resultar em tradução horizontal e vertical.

Mas aquela última linha da matriz? O que acontece se você não restringir essa última linha de 0s e 1s? Aqui é a transformação completa 4 × 4, aplicada a um ponto 3D:

As fórmulas para x*', y'* e z*'* permanecem os mesmos, mas w*'* agora é calculado assim:

E isso é um problema real. Anteriormente, um pequeno truque foi empregado para usar uma secção 3D do espaço de 4D, onde a coordenada w sempre é igual a 1. Mas agora a coordenada W já não é 1, e você já foi movido fora aquela secção 3D. Você está perdido no espaço 4D, e você precisa voltar para aquela secção 3D onde w é igual a 1.

Não há nenhuma necessidade de construir uma máquina do tempo-espaço trans-dimensional, no entanto. Felizmente, você pode fazer o salto matematicamente dividindo todas as coordenadas transformadas por w*'*:

Agora w*'* é igual a 1 e está de volta em casa!

Mas a que custo? Você agora tem uma divisão nas fórmulas de transformação, e é fácil de ver como o denominador pode ser 0 em algumas circunstâncias. Isso resultaria em coordenadas infinitas.

Bem, talvez isso é uma coisa boa.

Quando o matemático alemão August Ferdinand Möbius (1790-1868) inventou o sistema que acabei de descrever (chamado "coordenadas homogéneas" ou "coordenadas projetivas"), um de seus objetivos era representar infinitas coordenadas usando números finitos.

A matriz com os 0s e 1s na última linha chama-se uma transformação afim, significando que ele não resulta em infinito, portanto, uma transformação capaz do infinito é chamada uma transformação afim.

Em gráficos 3D, não-affine transformações são extremamente importantes, pois este é como perspectiva é alcançada. Todo mundo sabe que na vida real, objetos mais longe seu olho parecem ser menor. Em gráficos 3D, você pode obter esse efeito com um denominador que não é uma constante 1.

Experimentá-lo em BitmapTransformExperiment: Se fizer um pequeno número positivo m14 — apenas pequenos valores são necessários para resultados interessantes — então os valores de x e y são diminuídos proporcionalmente como x fica maior. Fazer m14, um pequeno número negativo e os maiores valores de x e y são aumentadas. Figura 3 mostra que o efeito combinado com um valor diferente de zero m12. O bitmap renderizado já não é um paralelogramo, e a perspectiva sugere que a borda direita tem balançou mais perto de seus olhos.


Figura 3 perspectiva em BitmapTransformExperiment

Da mesma forma, valores zero de m24 podem fazer a parte superior ou inferior do bitmap aparentemente balançar em sua direção ou um pouco mais longe. Programação 3D real, é o valor de m34 que é comumente usado, para que permite que objetos aumentar ou diminuir de tamanho com base em suas coordenadas z — sua distância dos olhos do espectador.

Quando uma transformação 3D é aplicada a objetos 2D, o valor de m44 geralmente é deixado em 1, mas pode funcionar como um fator de escala global. Em programação real 3D, m44 é normalmente definida como 0 quando perspectiva está envolvida porque conceitualmente a câmera está na origem. Um valor 0 de m44 só funciona se objetos 3D não tem coordenadas z igual a 0, mas quando se trabalha com objetos 2D, a coordenada z é sempre 0.

Qualquer quadrilátero convexo

Na aplicação desta matriz de transformar de 4 × 4 para um bitmap plano, você está apenas fazendo uso de metade os elementos da matriz. Mesmo assim, Figura 3 mostra algo você não pode fazer com a normal bidimensional Direct2D transformação matriz, que é aplicar uma transformação que transforma um retângulo em algo que não seja um paralelogramo. Com efeito, os transform objetos usados na maioria de Direct2D são chamados D2D1_MATRIX_3X2_F e Matrix3x2F, enfatizando a inacessibilidade da terceira linha e a incapacidade de executar não-affine transformações.

Com D2D1_MATRIX_4X4_F, é possível derivar uma transformação que mapeia um bitmap em qualquer quadrilátero convexo — ou seja, qualquer arbitrária figura de quatro lados onde os lados não Cruz e onde os ângulos internos nos vértices são menos de 180 graus.

Se não acredita, tente brincar com o programa NonAffineStretch. Observe que este programa é adaptado a partir de um programa de Windows Runtime, também chamado de NonAffineStretch, no capítulo 10 do meu livro, "Programação Windows, 6ª edição" (Microsoft Press, 2013).

Figura 4 mostra NonAffineStretch em uso. Você pode usar o mouse ou os dedos para arrastar os pontos verdes em qualquer local na tela. Enquanto você manter a figura de um quadrilátero convexo, uma transformação de 4 × 4 pode ser derivada com base sobre os locais de ponto. Essa transformação é usada para desenhar o bitmap e também é exibida no canto inferior direito. Apenas oito valores envolvidos; os elementos na terceira linha e terceira coluna são sempre valores padrão, e m44 é sempre 1.


Figura 4 o programa de NonAffineStretch

A matemática por trás disso está um pouco peludo, mas a derivação do algoritmo é mostrada no capítulo 10 do meu livro.

A classe Matrix4x4F

Para facilitar um pouco a trabalhar com a estrutura de D2D1_MATRIX_4X4_F, o Matrix4x4F classe no namespace D2D1 deriva dessa estrutura. Essa classe define um construtor e um operador de multiplicação (que eu usei no algoritmo NonAffineStretch), e vários útil métodos estáticos para criar comuns transformam as matrizes. Por exemplo, o método Matrix4x4F::RotationZ aceita um argumento que é um ângulo em graus e retorna uma matriz que representa a rotação por esse ângulo, em torno do eixo Z:

Outras funções de Matrix4x4F criam matrizes para a rotação em torno dos eixos X e Y e a rotação em torno de um eixo arbitrário, que é uma matriz muito mais difícil.

Uma função chamada Matrix4x4F::PerspectiveProjection tem um argumento chamado de profundidade. A matriz retorna é:

Isto significa que a fórmula de transformação para x' é:

E da mesma forma para y*'* e z*'*, que significa que sempre que a coordenada z é igual a profundidade, o denominador é 0, e todas as coordenadas tornam-se infinitas.

Conceitualmente, isso significa que você está vendo a tela do computador a uma distância de unidades de profundidade, onde as unidades são as mesmas que a tela em si, significado pixels ou unidades independentes de dispositivo. Um objeto gráfico tem uma coordenada z igual a profundidade, se as unidades de profundidade na frente da tela, que está no teu olho! Esse objeto deve aparecer muito grande para você — matematicamente infinito.

Mas espere um minuto: O único propósito da D2D1_MATRIX_4X4_Fstructure e a classe Matrix4x4F é para chamadas de DrawBitmap, e bitmaps sempre têm coordenadas z igual a 0. Então como é que esse valor de m34 – 1/profundidade tem qualquer efeito em tudo?

Se a matriz PerspectiveProjection é usada sozinho na chamada DrawBitmap, certamente não terá nenhum efeito. Mas é destinado a ser usado em conjunto com outras transformações de matriz. Transformações de matriz podem ser agravadas pela multiplicação-los. Embora o bitmap original tem sem coordenadas z e z coordenadas são ignoradas para renderização, z coordenadas certamente podem desempenhar um papel na composição de transformações.

Vamos examinar um exemplo. O programa de RotatingText cria um bitmap com o texto "ROTATE", com uma largura que é quase a metade da largura da tela. Muito dos métodos de atualização e renderização são mostrados em Figura 5.

Figura 5 código de RotatingTextRenderer.cpp

void RotatingTextRenderer::Update(DX::StepTimer const& timer)
{
  ...
// Begin with the identity transform
  m_matrix = Matrix4x4F();
  // Rotate around the Y axis
  double seconds = timer.GetTotalSeconds();
  float angle = 360 * float(fmod(seconds, 7) / 7);
  m_matrix = m_matrix * Matrix4x4F::RotationY(angle);
  // Apply perspective based on the bitmap width
  D2D1_SIZE_F bitmapSize = m_bitmap->GetSize();
  m_matrix = m_matrix * 
    Matrix4x4F::PerspectiveProjection(bitmapSize.width);
}
void RotatingTextRenderer::Render()
{
  ...
ID2D1DeviceContext* context = 
    m_deviceResources->GetD2DDeviceContext();
  Windows::Foundation::Size logicalSize = 
    m_deviceResources->GetLogicalSize();
  context->SaveDrawingState(m_stateBlock.Get());
  context->BeginDraw();
  context->Clear(ColorF(ColorF::DarkMagenta));
  // Move origin to top center of screen
  Matrix3x2F centerTranslation =
    Matrix3x2F::Translation(logicalSize.Width / 2, 0);
  context->SetTransform(centerTranslation *
    m_deviceResources->GetOrientationTransform2D());
  // Draw the bitmap
  context->DrawBitmap(m_bitmap.Get(),
                      nullptr,
                      1.0f,
                      D2D1_INTERPOLATION_MODE_LINEAR,
                      nullptr,
                      &m_matrix);
  ...
}

O método de atualização, o método Matrix4x4F::RotationY cria a seguinte transformação:

Multiplique isso pela matriz mostrada anteriormente retornado do método Matrix4x4F::PerspectiveProjection, e você terá:

As fórmulas de transformação são:

Estes envolvem definitivamente a perspectiva, e você pode ver o resultado em Figura 6.


Na Figura 6, a exposição de RotatingText

Cuidado: O argumento de profundidade para Matrix4x4F::Perspective­projeção é definida como a largura do bitmap, assim como o bitmap rotativo move-se, ele pode vir muito perto de seu nariz.

Charles Petzold é um colaborador de longa data de MSDN Magazine e autor de "Programação Windows, 6ª edição" (Microsoft Press, 2013), um livro sobre como escrever aplicativos para Windows 8. Seu site é charlespetzold.com.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Jim Galasyn e Mike riquezas