Este artigo foi traduzido por máquina.

Fator DirectX

Manipulando triângulos em um espaço 3D

Charles Petzold

Baixar o código de exemplo

Charles PetzoldA artista gráfico holandês M. C. Escher sempre foi um favorito particular entre programadores, engenheiros e outros técnicos. Seus desenhos espirituosos de estruturas impossíveis jogar com precisa a mente impor ordem na informação visual, enquanto o uso de padrões de malha matematicamente inspirados parece sugerir uma familiaridade com técnicas de recursão de software.

Gosto particular Escher'da mistura bidimensional e tridimensional, imagens, como "Mãos Desenho" de de 1948, neste Par de 3D mãos resultam 2D desenho é si esboçou 3D mãos (ver Figura 1). Mas ESTA justaposição Das imagens 2D e 3D enfatiza Como Mãos 3D Só parecem ter profundidade Como resultado de detalhe e sombreamento. Rõ ràng, tất cả mọi thứ Trong bản vẽ được thực hiện trên giấy phẳng.

M.C. Escher’s “Drawing Hands”
Figura 1 M.C. "Mãos desenho" de Escher

Eu quero fazer algo semelhante no presente artigo: Eu quero fazer 2D objetos gráficos parecem adquirir profundidade e corpo como eles surgem a partir da tela e flutuam no espaço 3D e retirem-se então volta para a tela plana.

Esses objetos gráficos não ser retratos de mãos humanas, no entanto. Em vez disso, eu vou ficar com talvez os objetos 3D mais simples — os cinco sólidos platónicos. Os sólidos platônicos são os única possíveis poliedros convexos cujas faces são polígonos regulares idênticos convexos, com o mesmo número de faces em cada vértice. Eles são o tetraedro (quatro triângulos), Octaedro (oito triângulos), icosaedro (20 triângulos), cubo (seis praças) e o dodecaedro (12 pentágonos).

Sólidos platônicos são populares em gráficos 3D rudimentares programação porque eles são geralmente fáceis de definir e montar. Fórmulas para os vértices podem ser encontradas na Wikipédia, por exemplo.

Para fazer este exercício pedagogicamente tão suave quanto possível, eu vou estar usando Direct2D, ao invés de Direct3D. No entanto, você precisará se familiarizar com alguns conceitos, tipos de dados, funções e estruturas, muitas vezes usadas em conexão com o Direct3D.

Minha estratégia é definir esses objetos sólidos usando triângulos no espaço 3D, e em seguida aplicar transformações 3D para girá-las. As coordenadas do triângulo transformado então são achatadas no espaço 2D, ignorando a coordenada Z, onde eles são usados para criar objetos de ID2D1Mesh, que então são processados usando o método FillMesh do objeto ID2D1DeviceContext.

Como você verá, não é suficiente simplesmente definir coordenadas para objetos em 3D. Somente quando o sombreamento é aplicado para imitar o reflexo da luz os objetos parecem escapar o nivelamento da tela.

Pontos 3D e transformações

Este exercício requer que transformações 3D matrix aplicado a pontos 3D para rotacionar objetos no espaço. Quais são os melhores tipos de dados e funções para este trabalho?

Curiosamente, Direct2D tem uma estrutura de D2D1_MATRIX_4X4_F e um Matrix4x4F classe no namespace D2D1 que são apropriados para representar as matrizes de transformação 3D. No entanto, estes dados tipos são projetados somente para usam com os métodos de DrawBitmap definidos por ID2D1DeviceContext, demonstradas na edição de abril nesta coluna. Em particular, Matrix4x4F nem sequer tem um método chamado de transformação que pode aplicar a transformação de um ponto 3D. Você precisa implementar essa multiplicação de matrizes com seu próprio código.

Um lugar melhor para tipos de dados 3D é a biblioteca de matemática do DirectX, que é usada por programas de Direct3D, também. Esta biblioteca define mais de 500 funções — todos os que começam com as letras XM — e vários tipos de dados. Estes são todos declarados no arquivo de cabeçalho DirectXMath.h e associados a um namespace do DirectX.

Cada única função na biblioteca DirectX matemática envolve o uso de um tipo de dados chamado XMVECTOR, que é uma coleção de quatro números. XMVECTOR é adequada para a representação 2D ou 3D pontos (com ou sem uma coordenada W) ou uma cor (com ou sem um canal alfa). Aqui está como você define um objeto do tipo XMVECTOR:

XMVECTOR vector;

Repare que eu disse que XMVECTOR é uma coleção de "quatro números" ao invés de "quatro valores de ponto flutuante" ou "quatro inteiros." Não posso ser mais específico, como o formato real dos quatro números em um objeto XMVECTOR é dependente do hardware.

XMVECTOR não é um tipo de dados normal! É na verdade um proxy para quatro registradores de hardware do chip do processador, especificamente única instrução registra vários dados (SIMD) usada com streaming SIMD extensions (SSE) que implementam processamento paralelo. Em x86 hardware esses registos são na verdade simples-precisão flutuante -­apontar valores, mas em processadores ARM (encontrados em dispositivos Windows RT) estão definidos para terem componentes fracionários de inteiros.

Por este motivo, você não deveria tentar acessar os campos de um objeto XMVECTOR diretamente (se você não sabe o que está fazendo). Em vez disso, a biblioteca de matemática do DirectX inclui inúmeras funções para definir os campos de número inteiro ou valores de ponto flutuante. Aqui está um comum:

XMVECTOR vector = XMVectorSet(x, y, z, w);

Funções também existem para obter os valores de campo individuais:

float x = XMVectorGetX(vector);

Porque este tipo de dados é um proxy para registradores de hardware, certas restrições governam seu uso. Ler o guia on-line"DirectXMath de programação" (bit.ly/1d4L7Gk) para obter detalhes sobre como definir membros de estrutura do tipo XMVECTOR e passando XMVECTOR argumentos para funções.

Em geral, no entanto, você vai a probabilidade­bly usar XMVECTOR principalmente no código que é local para um método. Para o armazenamento de uso geral de pontos 3D e vetores, a biblioteca de matemática DirectX define outros tipos de dados que são simples estruturas normais, tais como XMFLOAT3 (que tem três membros de dados do tipo float chamado x, y e z) e XMFLOAT4 (que tem quatro membros de dados para incluir w). Em particular, você vai querer usar XMFLOAT3 ou XMFLOAT4 para armazenar conjuntos de pontos.

É fácil transferir entre XMVECTOR e XMFLOAT3 ou XMFLOAT4. Suponha que você usar o XMFLOAT3 para armazenar um ponto 3D:

XMFLOAT3 point;

Quando você precisa usar uma das funções que requerem uma XMVECTOR matemática do DirectX, você pode carregar o valor em um XMVECTOR usando a função XMLoadFloat3:

XMVECTOR vector = XMLoadFloat3(&point);

O valor de w no XMVECTOR é inicializado para 0. Você pode usar o objeto XMVECTOR em várias funções de matemática de DirectX. Para armazenar o valor XMVECTOR volta no objeto XMFLOAT3, ligue:

XMStoreFloat3(&point, vector);

Da mesma forma, XMLoadFloat4 e XMStoreFloat4 a transferência de valores entre objetos XMVECTOR e XMFLOAT4 de objetos, e estas são muitas vezes preferidas se a coordenada W é importante.

Em geral, você estará trabalhando com vários objetos XMVECTOR no mesmo bloco de código, algumas das quais correspondem aos objetos subjacentes de XMFLOAT3 ou XMFLOAT4, e alguns dos quais são apenas transitórios. Você verá exemplos em breve.

Eu disse anteriormente que cada função na biblioteca DirectX matemática envolve XMVECTOR. Se você explorou a biblioteca, você pode encontrar algumas funções que na verdade não exigem um XMVECTOR, mas envolve um objeto do tipo XMMATRIX.

O tipo de dados XMMATRIX é uma matriz 4 × 4 apropriado para transformações 3D, mas é na verdade quatro XMVECTOR objetos, um para cada linha:

struct XMMATRIX
{
  XMVECTOR r[4];
};

Então o que eu disse foi correto, pois todas as funções de DirectX matemática que requerem objetos XMMATRIX realmente envolvem objetos XMVECTOR, bem como e XMMATRIX tem as mesmas restrições como XMVECTOR.

Assim como XMFLOAT4 é uma estrutura normal, que você pode usar para transferir valores para e de um objeto XMVECTOR, você pode usar uma estrutura normal chamada XMFLOAT4X4 para armazenar uma matriz de 4 × 4 e transferir isso para e de um XMMATRIX usando as funções XMLoadFloat4x4 e XMStoreFloat4x4.

Se você tiver carregado um ponto 3D em um objeto XMVECTOR (chamado de vetor, por exemplo), e você tiver carregado uma matriz de transformar em um objeto XMMATRIX, chamado de matriz, você pode aplicar essa transformação ao ponto usando:

XMVECTOR result = XMVector3Transform(vector, matrix);

Ou, você pode usar:

XMVECTOR result = XMVector4Transform(vector, matrix);

A única diferença é que o XMVector4Transform usa o valor real w o XMVECTOR enquanto XMVector3Transform assume que é 1, o que é correto para a aplicação 3D tradução.

No entanto, se você tiver uma matriz de XMFLOAT3 ou XMFLOAT4 valores e você deseja aplicar a transformação de todo o conjunto, há uma solução muito melhor: As funções XMVector3TransformStream e XMVector4TransformStream aplicam o XMMATRIX para uma matriz de valores e armazenam os resultados em uma matriz de valores de XMFLOAT4 (independentemente do tipo de entrada).

O bônus: Porque XMMATRIX é na verdade nos registradores SIMD em uma CPU que implementa SSE, a CPU pode usar processamento paralelo para aplicar que transforme para a matriz de pontos e acelerar um dos maiores gargalos em renderização em 3D.

Definição de sólidos platônicos

O código para download desta coluna é um único 8.1 do Windows projeto chamado PlatonicSolids. O programa usa Direct2D para renderizar imagens 3D dos cinco sólidos platónicos.

Como todas as figuras 3D, estes sólidos podem ser descritos como uma coleção de triângulos no espaço 3D. Eu sabia que eu iria querer usar XMVector3­TransformStream ou XMVector4TransformStream para transformar uma matriz de triângulos 3D, e eu sabia que a matriz de saída dessas duas funções é sempre uma matriz de objetos XMFLOAT4, então eu decidi usar o XMFLOAT4 para a matriz de entrada, bem como, e isso é como defini minha estrutura 3D do triângulo:

struct Triangle3D
{
  DirectX::XMFLOAT4 point1;
  DirectX::XMFLOAT4 point2;
  DirectX::XMFLOAT4 point3;
};

Figura 2 mostra algumas estruturas de dados privados adicionais definidas no PlatonicSolidsRenderer.h que armazenam as informações necessárias para descrever e processar uma figura 3D. Cada um dos cinco sólidos platónicos é um objeto do tipo FigureInfo. As coleções srcTriangles e dstTriangles armazenam os triângulos "fonte" original e os triângulos de "destino" depois de dimensionamento e aplicaram-se transformações de rotação.  Ambas as coleções têm um tamanho igual ao produto da faceCount e trianglesPerFace. Observe que srcTriangles.data e dstTriangles.data são efetivamente ponteiros para estruturas XMFLOAT4 e, portanto, podem ser argumentos para a função de XMVector4TransformStream. Como você verá, isto acontece durante o Update método na classe PlatonicSolidRenderer.

Figura 2 as estruturas de dados usadas para armazenar figuras 3D

struct RenderInfo
{
  Microsoft::WRL::ComPtr<ID2D1Mesh> mesh;
  Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
};
struct FigureInfo
{
  // Constructor
  FigureInfo()
  {
  }
  // Move constructor
  FigureInfo(FigureInfo && other) :
    srcTriangles(std::move(other.srcTriangles)),
    dstTriangles(std::move(other.dstTriangles)),
    renderInfo(std::move(other.renderInfo))
  {
  }
  int faceCount;
  int trianglesPerFace;
  std::vector<Triangle3D> srcTriangles;
  std::vector<Triangle3D> dstTriangles;
  D2D1_COLOR_F color;
  std::vector<RenderInfo> renderInfo;
};
std::vector<FigureInfo> m_figureInfos;

O campo renderInfo é uma coleção de objetos RenderInfo, um para cada face da figura. Os dois membros dessa estrutura são também determinados durante o método de atualização, e eles simplesmente são passados para o método FillMesh do objeto ID2D1DeviceContext durante o Render método.

O construtor da classe PlatonicSolidsRenderer inicializa cada um dos cinco objetos FigureInfo. Figura 3 mostra o processo para o mais simples dos cinco, o tetraedro.

Figura 3 definindo o tetraedro

FigureInfo tetrahedron;
tetrahedron.faceCount = 4;
tetrahedron.trianglesPerFace = 1;
tetrahedron.srcTriangles =
{
  Triangle3D { XMFLOAT4(-1,  1, -1, 1),
               XMFLOAT4(-1, -1,  1, 1),
               XMFLOAT4( 1,  1,  1, 1) },
  Triangle3D { XMFLOAT4( 1, -1, -1, 1),
               XMFLOAT4( 1,  1,  1, 1),
               XMFLOAT4(-1, -1,  1, 1) },
  Triangle3D { XMFLOAT4( 1,  1,  1, 1),
               XMFLOAT4( 1, -1, -1, 1),
               XMFLOAT4(-1,  1, -1, 1) },
  Triangle3D { XMFLOAT4(-1, -1,  1, 1),
               XMFLOAT4(-1,  1, -1, 1),
               XMFLOAT4( 1, -1, -1, 1) }
};
tetrahedron.srcTriangles.shrink_to_fit();
tetrahedron.dstTriangles.resize(tetrahedron.srcTriangles.size());
tetrahedron.color = ColorF(ColorF::Magenta);
tetrahedron.renderInfo.resize(tetrahedron.faceCount);
m_figureInfos.at(0) = tetrahedron;

A inicialização do octaedro e icosaedro são semelhantes. Nos três casos, cada face consiste de um triângulo. Em termos de pixels, as coordenadas são muito pequenas, mas o código mais tarde no programa dimensiona-los para um tamanho adequado.

O cubo e o dodecaedro são diferentes, no entanto. O cubo tem seis faces, cada uma das quais é um quadrado, e o dodecaedro é 12 pentágonos. Para essas duas figuras, eu usei uma estrutura de dados diferentes para armazenar os vértices de cada rosto e um método comum que converteu cada face em triângulos — dois triângulos para cada face do cubo e três triângulos para cada face do dodecaedro.

Para a facilidade em converter as coordenadas 3D em coordenadas 2D, eu já com base estes números em um sistema de coordenadas no qual positivo X aumento de coordenadas para o direito positivo Y coordenadas aumento e descendo. (É mais comum na programação 3D para coordenadas de Y positivas aumentar a subir). Eu também achei que positivo Z coordenadas saiam da tela. Portanto, este é um sistema de coordenadas da esquerda. Se você apontar o dedo indicador da mão esquerda no sentido de X positivo e o dedo médio na direção de Y positivo, o polegar aponta para Z positivo.

Presume-se o espectador da tela do computador para ser localizado em um ponto no eixo Z positivo, olhando em direção a origem.

Rotações em 3D

O método de atualização em PlatonicSolidsRenderer executa uma animação que consiste de várias seções. Quando o programa começa a correr, os cinco sólidos platónicos são exibidos, mas eles parecem ser plana, conforme mostrado no Figura 4.

The PlatonicSolids Program As It Begins Running
Figura 4 o programa de PlatonicSolids como ele começa a correr

Estes não são reconhecíveis como objetos 3D!

Em 2,5 segundos, os objetos começam a girar. O Update método calcula os ângulos de rotação e um fator de escala com base no tamanho da tela, e então faz uso das funções matemáticas do DirectX. Funções como XMMatrixRotationX computar um objeto XMMATRIX que representa a rotação em torno do eixo X. XMMATRIX também define os operadores de multiplicação de matriz, para que os resultados dessas funções podem ser multiplicados juntos.

Figura 5 mostra como uma transformação de matriz total é calculada e aplicada para a matriz de objetos de Triangle3D em cada figura.

Figura 5 as figuras de giro

// Calculate total matrix
XMMATRIX matrix = XMMatrixScaling(scale, scale, scale) *
                  XMMatrixRotationX(xAngle) *
                  XMMatrixRotationY(yAngle) *
                  XMMatrixRotationZ(zAngle);
// Transform source triangles to destination triangles
for (FigureInfo& figureInfo : m_figureInfos)
{
  XMVector4TransformStream(
    (XMFLOAT4 *) figureInfo.dstTriangles.data(),
    sizeof(XMFLOAT4),
    (XMFLOAT4 *) figureInfo.srcTriangles.data(),
    sizeof(XMFLOAT4),
    3 * figureInfo.srcTriangles.size(),
    matrix);
}

Uma vez que os números começam a rodar, no entanto, ainda parecem ser planos polígonos, mesmo que eles estão mudando de forma.

Oclusão e superfícies escondidas

Um dos aspectos cruciais da programação de gráficos 3D está fazendo certo objetos mais perto do telespectador do obscuro do olho (ou ocluir) objetos mais distantes. Em cenas complexas, isto não é um problema trivial, e, geralmente, esta deve ser executada em hardware gráfico em uma base de pixel por pixel.

Com poliedros convexos, no entanto, é relativamente simples. Considere um cubo. Como o cubo está girando no espaço, na maior parte você vê três faces e às vezes apenas um ou dois. Nunca viu quatro, cinco ou todas as seis faces.

Para uma face do cubo girando particular, como você pode determinar que rostos que você vê e o que enfrenta são ocultas? Pense em perpendicular de vetores (freqüentemente visualizadas como setas com uma determinada direção) para cada face do cubo e apontando para fora do cubo. Estes são referidos como vetores "normal da superfície".

Só se um vetor normal de superfície tem um componente de Z positivo essa superfície será visível para um espectador, observando o objeto a partir do eixo Z positivo.

Matematicamente, uma superfície normal para um triângulo de computação é simples: Os três vértices do triângulo definem dois vetores e dois vetores (V1 e V2) no espaço 3D definem um plano, e uma perpendicular a esse plano é obtida a partir do vetor produto cruzado, conforme mostrado no Figura 6.

The Vector Cross Product
Figura 6 o vetor Cross produto

O real sentido deste vetor depende da destreza manual do sistema de coordenadas. Para um sistema de coordenadas à direita, por exemplo, você pode determinar a direção da × V1 V2 produto cruzado, curvando os dedos da mão direita de V1 para V2. Os pontos de polegar no sentido do produto cruzado. Para um sistema de coordenadas da esquerda, use a mão esquerda.

Para qualquer triângulo especial que faz com que estes números, o primeiro passo é carregar os três vértices em objetos XMVECTOR:

XMVECTOR point1 = XMLoadFloat4(&triangle3D.point1);
XMVECTOR point2 = XMLoadFloat4(&triangle3D.point2);
XMVECTOR point3 = XMLoadFloat4(&triangle3D.point3);

Em seguida, dois vetores representando os dois lados do triângulo podem ser calculadas subtraindo point2 e point3 point1 que usa que funções de matemática conveniente do DirectX:

XMVECTOR v1 = XMVectorSubtract(point2, point1);
XMVECTOR v2 = XMVectorSubtract(point3, point1);

Todos os sólidos platônicos neste programa são definidos com triângulos cujos três pontos são dispostos no sentido horário da point1 para point2 para point3 quando o triângulo é visto de fora da figura. Um normal superfície apontando para fora da figura pode ser calculada usando uma função matemática de DirectX que obtém o produto vetorial:

XMVECTOR normal = XMVector3Cross(v1, v2);

Um programa exibindo estes números poderia simplesmente optar por não exibir qualquer triângulo com uma superfície normal que tem um 0 ou componente de Z negativo. O programa PlatonicSolids, em vez disso, continua a exibir esses triângulos, mas com uma cor transparente.

É tudo sobre o sombreamento

Você vê objetos no mundo real porque eles refletem a luz. Sem luz, nada é visível. Em muitos ambientes do mundo real, a luz vem do muitas direções diferentes porque ele salta para fora outras superfícies e difunde-se no ar.

Na programação de gráficos 3D, isto é conhecido como luz "ambiente", e não é muito adequada. Se um cubo está flutuando no espaço 3D e a mesma luz ambiente ataca todas as faces, todas as faces que ser coloridas a mesma e não ficaria como um cubo 3D em tudo.

Cenas em 3D, portanto, normalmente requerem uma luz direcional — luz proveniente de uma ou mais direções. Uma abordagem comum para cenas 3D simples é definir uma fonte de luz direcional como um vetor que parece vir por trás do ombro esquerdo do visualizador:

XMVECTOR lightVector = XMVectorSet(2, 3, -1, 0);

Na perspectiva do espectador, este é um dos muitos vetores que aponta para a direita e para baixo e longe do espectador na direção do eixo Z negativo.

Em preparação para o próximo trabalho, quero normalizar o vetor normal superfície e o vetor de luz:

normal = XMVector3Normalize(normal);
lightVector = XMVector3Normalize(lightVector);

A função XMVector3Normalize calcula a magnitude do vetor usando o formulário em 3D do teorema de Pitágoras e em seguida, divide as três coordenadas por dessa magnitude. O vetor resultante tem uma magnitude de 1.

Se o vetor normal passa a ser igual ao negativo do lightVector, que significa que a luz está golpeando o triângulo perpendicular a sua superfície, e que é a iluminação máxima que pode fornecer luz direcional. Se a luz direcional não é completamente perpendicular à superfície do triângulo, a iluminação será menor.

Matematicamente, a iluminação de uma superfície de uma fonte de luz direcional é igual para o co-seno do ângulo entre o vetor de luz e a normal da superfície negativa. Se estes dois vetores tem uma magnitude de 1, então esse número crucial é fornecido pelo produto escalar de dois vetores:

XMVECTOR dot = XMVector3Dot(normal, -lightVector);

O produto escalar é um escalar — um número — em vez de um vetor, então todos os campos do objeto XMVECTOR retornado de segurar esta função, os mesmos valores.

Para fazê-lo parecer como se os sólidos platônicos rotativos magicamente assumem profundidade 3D que possam surgir a partir da tela plana, o programa de PlatonicSolids anima um valor chamado lightIntensity de 0 para 1 e depois de volta para 0. O valor 0 é sem sombreamento luz direcional e nenhum efeito 3D, enquanto o 1 valor é o máximo 3D. Este valor de lightIntensity é usado em conjunto com o produto de ponto para calcular um fator de luz total:

float totalLight = 0.5f +
  lightIntensity * 0.5f * XMVectorGetX(dot);

O primeiros 0,5 nesta fórmula refere-se a luz ambiente, e a 0,5 segundo permite totalLight a escala de 0 a 1 dependendo do valor do produto dot. (Teoricamente, isso não é correto. Valores negativos do produto dot devem ser definidos como 0 porque eles resultam em luz total menor do que a luz ambiente.)

Este totalLight é usado para calcular uma cor e uma escova para cada rosto:

renderColor = ColorF(totalLight * baseColor.r,
                     totalLight * baseColor.g,
                     totalLight * baseColor.b);

O resultado com 3D-ishness máximo é mostrado no Figura 7.

The PlatonicSolids Program with Maximum 3D
Figura 7 o programa de PlatonicSolids com máximo 3D

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 ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Doug Erickson