Este artículo proviene de un motor de traducción automática.

Factor DirectX

Manipulación de triángulos en espacio 3D

Charles Petzold

Descargar el código de muestra

Charles PetzoldEl artista gráfico holandés M. C. Escher siempre ha sido un favorito entre los programadores, ingenieros y otros técnicos particular. Sus dibujos ingeniosos de estructuras imposibles jugarcon con la necesidad de la mente para imponer el orden en la información visual, mientras que su uso de patrones endentados matemáticamente inspirados parece sugerir una familiaridad con técnicas de recursividad de software.

Soy un fan especial de mezcla de Escher de imágenes bidimensionales y tridimensionales, tales como "Manos dibujo" de 1948, en la que un par de manos 3D surgen de un dibujo es sí mismo ser bosquejado por las manos 3D 2D (ver figura 1). Pero esta yuxtaposición de las imágenes 2D y 3D hace hincapié en cómo las manos 3D sólo parecen tener profundidad como resultado de detalle y el sombreado. Obviamente, todo en el dibujo se procesa en plana de papel.

M.C. Escher’s “Drawing Hands”
Figura 1 M.C. Escher "Drawing Hands"

Quiero hacer algo similar en este artículo: Quiero hacer 2D objetos gráficos parecen adquirir cuerpo y profundidad ya que surgen de la pantalla y flotan en el espacio 3D y luego replegarse hacia la pantalla plana.

Estos objetos gráficos no ser representaciones de manos humanas, sin embargo. En cambio, yo me quedo con tal vez los objetos 3D más simples — los cinco sólidos platónicos. Los sólidos platónicos son los solamente posibles poliedros convexos cuyas caras son idéntico polígono regular convexo, con el mismo número de caras en cada vértice. Ellos son el tetraedro (cuatro triángulos), octaedro (ocho triángulos), icosaedro (20 triángulos), cubo (seis plazas) y dodecaedro (12 pentágonos).

Sólidos platónicos son populares en gráficos 3D rudimentarios programación porque son generalmente fáciles de definir y montar. Las fórmulas de los vértices pueden encontrarse en Wikipedia, por ejemplo.

Para realizar este ejercicio pedagógico tan suave como sea posible, va a utilizar Direct2D en lugar de Direct3D. Sin embargo, usted necesitará familiarizarse con algunos conceptos, tipos de datos, funciones y estructuras utilizadas a menudo en relación con Direct3D.

Mi estrategia es definir estos objetos sólidos con triángulos en el espacio 3D y luego aplicar transformaciones 3D para rotarlas. Las coordenadas del triángulo transformada luego se aplanan en espacio 2D haciendo caso omiso de la coordenada Z, donde están acostumbrados a crear los objetos ID2D1Mesh, que luego se procesan utilizando el método FillMesh del objeto ID2D1DeviceContext.

Como verás, no basta simplemente definir las coordenadas de objetos 3D. Sólo cuando se aplica sombreado para imitar la reflexión de la luz los objetos parecen escapar de la monotonía de la pantalla.

Puntos 3D y se transforma

Este ejercicio requiere que se apliquen matriz 3D transforma a puntos 3D para rotar objetos en el espacio. ¿Cuáles son los mejores tipos de datos y funciones para este trabajo?

Curiosamente, Direct2D tiene una estructura D2D1_MATRIX_4X4_F y una clase Matrix4x4F en el espacio de nombres D2D1 que son adecuados para la representación de las matrices de transformación 3D. Sin embargo, estos datos tipos están diseñados sólo para usan con los métodos DibujarBitmap definidos por ID2D1DeviceContext, como he demostrado en la entrega de abril de esta columna. En particular, Matrix4x4F no tiene un método denominado transformación que puede aplicar la transformación a un punto 3D. Necesitaría implementar esa multiplicación de matrices con su propio código.

Un mejor lugar para buscar los tipos de datos 3D es la biblioteca de matemáticas DirectX, que es utilizada por programas de Direct3D, así. Esta biblioteca define más de 500 funciones — todos los cuales comienzan con las letras XM — y varios tipos de datos. Estos son todos declarados en el archivo de cabecera DirectXMath.h y asociados a un espacio de nombres de DirectX.

Cada función individual en la biblioteca de matemáticas DirectX implica el uso de un tipo de datos denominado XMVECTOR, que es una colección de cuatro números. XMVECTOR es conveniente para la representación 2D o 3D puntos (con o sin una coordenada W) o un color (con o sin un canal alfa). Aquí está Cómo definiría un objeto de tipo XMVECTOR:

XMVECTOR vector;

Aviso que XMVECTOR es una colección de "cuatro cifras" en lugar de "cuatro valores de coma flotante" o "cuatro enteros". No puedo ser más específico porque el actual formato de los cuatro números en un objeto XMVECTOR depende del hardware.

XMVECTOR no es un tipo de datos normal! Es en realidad un proxy para cuatro registros de hardware en el chip del procesador, específicamente única instrucción registra datos múltiples (SIMD) utilizado con streaming extensiones SIMD (SSE) que implementan el procesamiento en paralelo. X 86 hardware estos registros son de hecho precisión simple flotante -­punto de valores, pero en procesadores ARM (encontrados en los dispositivos Windows RT) están definidas para tener componentes fraccionarios enteros.

Por esta razón, usted no debería intentar acceder directamente a los campos de un objeto XMVECTOR (si no sabes lo que estás haciendo). En cambio, la biblioteca de matemáticas DirectX incluye numerosas funciones para definir los campos de número entero o valores de punto flotante. Aquí es una especie común:

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

También existen funciones para obtener los valores de campo individual:

float x = XMVectorGetX(vector);

Debido a este tipo de datos es un servidor proxy para registros de hardware, ciertas restricciones rigen su uso. Lea la guía en línea"DirectXMath programación" (bit.ly/1d4L7Gk) para más detalles sobre la definición de los miembros de la estructura de tipo XMVECTOR y pasar XMVECTOR argumentos a las funciones.

En general, sin embargo, usted proba­bly utilizar XMVECTOR en su mayoría en código local a un método. Para el almacenamiento de propósito general de puntos 3D y vectores, la biblioteca de matemáticas DirectX define otros tipos de datos que son simples estructuras normales, tales como XMFLOAT3 (que tiene tres miembros de datos de tipo float llamado x, y y z) y XMFLOAT4 (que tiene cuatro miembros de datos para incluir w). En particular, usted querrá usar XMFLOAT3 o XMFLOAT4 para el almacenamiento de matrices de puntos.

Es fácil transferir entre XMVECTOR y XMFLOAT3 o XMFLOAT4. Supongamos que utilizas XMFLOAT3 para almacenar un punto 3D:

XMFLOAT3 point;

Cuando usted necesita utilizar una de las funciones de DirectX matemáticas que requieren de un XMVECTOR, puede cargar el valor en un XMVECTOR usando la función XMLoadFloat3:

XMVECTOR vector = XMLoadFloat3(&point);

El valor de w en el XMVECTOR se inicializa en 0. Entonces puede utilizar el objeto XMVECTOR en varias funciones matemáticas de DirectX. Para almacenar el valor XMVECTOR en el objeto XMFLOAT3, llame al:

XMStoreFloat3(&point, vector);

Asimismo, XMLoadFloat4 y XMStoreFloat4 la transferencia de valores entre los objetos XMVECTOR y XMFLOAT4 de objetos, y estos se prefieren a menudo si la coordenada W es importante.

En el caso general, usted trabajará con varios objetos XMVECTOR en el mismo bloque de código, algunas de las cuales corresponden a objetos subyacentes de XMFLOAT3 o XMFLOAT4, y algunos de los cuales son sólo transitorios. Pronto veremos ejemplos.

Ya he dicho que todas las funciones de la biblioteca de matemáticas DirectX implica XMVECTOR. Si ya has explorado la biblioteca, es posible que algunas funciones que en realidad no requieren una XMVECTOR pero implican un objeto de tipo XMMATRIX.

El tipo de datos XMMATRIX es una matriz de 4 × 4 conveniente para las transformaciones 3D, pero es en realidad cuatro objetos XMVECTOR, uno por cada fila:

struct XMMATRIX
{
  XMVECTOR r[4];
};

Así que lo que dije fue correcto porque todas las funciones matemáticas DirectX que requieren los objetos XMMATRIX incluyen objetos XMVECTOR y XMMATRIX tiene las mismas restricciones que XMVECTOR.

XMFLOAT4 es una estructura normal que se puede utilizar para transferir los valores a y desde un objeto XMVECTOR, puede utilizar una estructura normal llamada XMFLOAT4X4 para almacenar una matriz de 4 × 4 y la transferencia desde y hacia un XMMATRIX usando las funciones XMLoadFloat4x4 y XMStoreFloat4x4.

Si has cargado un punto 3D en un objeto XMVECTOR (llamado vector, por ejemplo), y has cargado una matriz de transformación en un objeto XMMATRIX denominado matriz, puede aplicar esa transformación al punto usando:

XMVECTOR result = XMVector3Transform(vector, matrix);

O bien, puede utilizar:

XMVECTOR result = XMVector4Transform(vector, matrix);

La única diferencia es que XMVector4Transform utiliza el valor w real de la XMVECTOR mientras que el XMVector3Transform asume que es 1, que es correcto para la aplicación de traducción en 3D.

Sin embargo, si usted tiene una gran variedad de XMFLOAT3 o XMFLOAT4 los valores y desea aplicar la transformación de la matriz completa, hay una solución mucho mejor: Las funciones XMVector3TransformStream y XMVector4TransformStream el XMMATRIX aplican a una matriz de valores y guardar los resultados en una matriz de valores XMFLOAT4 (sin importar el tipo de entrada).

El bono: Porque XMMATRIX es en realidad en los registros SIMD en una CPU que implementa SSE, la CPU puede utilizar procesamiento en paralelo para aplicar que transforman a la matriz de puntos y uno de los mayores cuellos de botella en Render 3D aceleran.

Definición de sólidos platónicos

El código descargable para esta columna es un solo proyecto 8.1 Windows llamado PlatonicSolids. El programa usa Direct2D para procesar las imágenes 3D de los cinco sólidos platónicos.

Como todas las figuras 3D, estos sólidos pueden describirse como una colección de triángulos en el espacio 3D. Sabía que me gustaría usar XMVector3­TransformStream o XMVector4TransformStream para transformar un conjunto de triángulos 3D, y sabía que la matriz de salida de estas dos funciones siempre es una matriz de objetos XMFLOAT4, así que decidí usar XMFLOAT4 para la matriz de entrada, así como, y eso es lo definí mi estructura del triángulo 3D:

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

Figura 2 muestra algunas estructuras de datos privados adicionales definidos en PlatonicSolidsRenderer.h que almacenan información necesaria para describir y representar una figura 3D. Cada uno de los cinco sólidos platónicos es un objeto de tipo FigureInfo. Las colecciones srcTriangles y dstTriangles almacenan los triángulos "fuente" original y los triángulos "destino" después de escalar y transformaciones de rotación se han aplicado.  Ambas colecciones tienen un tamaño igual al producto de faceCount y trianglesPerFace. Observe que srcTriangles.data y dstTriangles.data son efectivamente punteros a las estructuras XMFLOAT4 y por lo tanto pueden ser argumentos para la función XMVector4TransformStream. Como verás, esto ocurre durante el método Update en la clase PlatonicSolidRenderer.

Figura 2 las estructuras de datos se utiliza para almacenar las 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;

El campo renderInfo es una colección de objetos RenderInfo, uno por cada cara de la figura. Los dos miembros de esta estructura también se determinan durante el método Update, y simplemente se las ignora al método FillMesh del objeto ID2D1DeviceContext en el método Render.

El constructor de la clase PlatonicSolidsRenderer Inicializa cada uno de los cinco objetos de FigureInfo. Figura 3 muestra el proceso para el más simple de los cinco, el tetraedro.

Figura 3 definiendo el 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;

La inicialización del octaedro y el icosaedro son similares. En los tres casos, cada cara consiste en un triángulo. En términos de píxeles, las coordenadas son muy pequeñas, pero código más adelante en el programa de las escalas a un tamaño adecuado.

El cubo y el dodecaedro son diferentes, sin embargo. El cubo tiene seis caras, cada una de ellas es un cuadrado, y el dodecaedro es 12 pentágonos. Para estos dos personajes, he utilizado una estructura de datos diferentes para almacenar los vértices de cada cara y un método común que convierte cada cara en triángulos — dos triángulos para cada cara del cubo y tres triángulos para cada cara del dodecaedro.

Para facilitar la conversión de las coordenadas 3D en coordenadas 2D, he basado en un sistema de coordenadas en que positivo X aumento de coordenadas al correcto y positivo Y coordenadas aumento a bajar estas cifras. (Es más común en la programación 3D para coordenadas positivas Y aumentar subiendo). También he asumido que positivas las coordenadas Z salen de la pantalla. Por lo tanto, este es un sistema de coordenadas de izquierda. Si señalas con el dedo índice de su mano izquierda en la dirección X positiva, y el dedo medio en la dirección positiva Y, el pulgar apunta a Z positivo.

El visor de la pantalla del ordenador se supone que se encuentra en un punto en el eje Z positivo mirando hacia el origen.

Rotaciones en 3D

El método de actualización en PlatonicSolidsRenderer realiza una animación que consta de varias secciones. Cuando inicia el programa en ejecución, los cinco sólidos platónicos se muestran, pero parecen ser plana, como se muestra en la figura 4.

The PlatonicSolids Program As It Begins Running
Figura 4 se inicia el programa PlatonicSolids como Running

Obviamente no son reconocibles como objetos 3D!

Los objetos en 2,5 segundos, comienzan a girar. El método Update calcula ángulos de rotación y un factor de escala basándose en el tamaño de la pantalla, y luego hace uso de funciones matemáticas de DirectX. Funciones tales como XMMatrixRotationX para calcular un objeto XMMATRIX que representa la rotación alrededor del eje X. XMMATRIX también define los operadores multiplicación de la matriz para que los resultados de estas funciones pueden multiplicar juntos.

Figura 5 muestra cómo una transformación de matriz total está calculada y aplicada a la matriz de objetos Triangle3D en cada figura.

Figura 5 rotar las figuras

// 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);
}

Una vez que comienzan a girar las cifras, sin embargo, que aún parecen polígonos planos, aunque están cambiando la forma.

Oclusión y superficies ocultas

Uno de los aspectos cruciales de la programación de gráficos en 3D está haciendo objetos que acerca al espectador de ojos oscuros (u ocluir) objetos más lejos. En escenas complejas, esto no es un problema trivial, y, en general, esto debe realizarse en hardware gráfico sobre una base píxel por píxel.

Con poliedros convexos, sin embargo, es relativamente sencillo. Consideremos un cubo. Como el cubo está girando en el espacio, en su mayoría verá tres caras y a veces sólo uno o dos. ¿No ves cuatro, cinco o las seis caras.

Por una cara del cubo giratorio en particular, ¿cómo puede determinar qué caras ves y se enfrenta a lo que están ocultos? Piensa en vectores (a menudo visualizados como flechas con una dirección determinada) perpendicular a cada cara del cubo y apuntando hacia el exterior del cubo. Éstos se denominan vectores "superficie normal".

Solamente si un vector normal superficial tiene un componente Z positivo será visible a un espectador que observa el objeto desde el eje Z positivo que la superficie.

Matemáticamente, una superficie normal para un triángulo de computación es sencilla: Los tres vértices del triángulo definen dos vectores y dos vectores (V1 y V2) en el espacio 3D definen un avión y un perpendicular a ese plano se obtiene a partir del vector producto cruzado, como se muestra en la figura 6.

The Vector Cross Product
Figura 6 el Vector Cruz producto

La actual dirección de este vector depende del uso de las manos del sistema de coordenadas. Para un sistema de coordenadas de la derecho, por ejemplo, puede determinar la dirección del × V1 V2 producto cruzado curvando los dedos de la mano derecha de V1 a V2. Los puntos del pulgar en la dirección del producto cruz. Para un sistema de coordenadas izquierdo, use su mano izquierda.

Para cualquier triángulo particular que compone estas cifras, el primer paso es cargar los tres vértices en objetos XMVECTOR:

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

Entonces, dos vectores que representan dos lados de un triángulo pueden calcularse restando point2 y point3 de punto1 utilizando que funciones matemáticas DirectX conveniente:

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

Todos los sólidos platónicos en este programa se definen con triángulos cuyos tres puntos están dispuestos hacia la derecha de punto1 a point2 a point3 cuando el triángulo es visto desde fuera de la figura. Una superficie normal hacia fuera de la figura se puede calcular mediante una función matemática de DirectX que obtiene el producto cruzado:

XMVECTOR normal = XMVector3Cross(v1, v2);

Un programa de visualización de estas cifras podría elegir simplemente no muestra cualquier triángulo con una superficie normal que tiene un 0 o un componente Z negativo. En cambio sigue el programa de PlatonicSolids Mostrar esos triángulos, pero con un color transparente.

Se trata de la sombra

Usted ver objetos en el mundo real porque reflejan la luz. Sin luz, no es visible. En muchos entornos del mundo real, la luz viene de muchas direcciones distintas porque rebota en otras superficies y difunde en el aire.

En la programación de gráficos en 3D, esto se conoce como "ambiente" luz, y no es muy adecuada. Si un cubo está flotando en el espacio 3D y la misma luz ambiente golpea todas las seis caras, todas las seis caras podría ser coloreadas el mismo y no se vería como un cubo 3D en absoluto.

Escenas en 3D, por lo tanto, generalmente requieren una luz direccional — luz proveniente de una o más direcciones. Un enfoque común para escenas en 3D simples es definir una fuente de luz direccional como un vector que parece venir por detrás del hombro izquierdo del espectador:

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

Desde la perspectiva del observador, este es uno de muchos vectores que apunta a la derecha y hacia abajo y lejos del espectador en la dirección del eje Z negativo.

En preparación para el siguiente trabajo, quiero normalizar tanto el vector normal de superficie y el vector de luz:

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

La función XMVector3Normalize calcula la magnitud del vector usando la forma 3D del Teorema de Pitágoras y luego divide las tres coordenadas de esa magnitud. El vector resultante tiene una magnitud de 1.

Si el vector normal pasa a ser igual a la negativa de la lightVector, significa que la luz es sorprendente el triángulo perpendicular a la superficie, y eso es la iluminación máxima que puede proporcionar luz direccional. Si la luz direccional no es perpendicular a la superficie del triángulo, la iluminación será menor.

Matemáticamente, la iluminación de una superficie a partir de una fuente de luz direccional es igual con el coseno del ángulo entre el vector de luz y la superficie negativa normal. Si estos dos vectores tienen una magnitud de 1, entonces ese número crucial es proporcionado por el producto escalar de los dos vectores:

XMVECTOR dot = XMVector3Dot(normal, -lightVector);

El producto de punto es un escalar, un número — en lugar de un vector, así todos los campos del objeto XMVECTOR devuelven en esta función espera los mismos valores.

Para hacerlo parecer como si los sólidos platónicos giratorios mágicamente asumen la profundidad 3D como surgen de la pantalla plana, el programa PlatonicSolids anima un valor llamado lightIntensity de 0 a 1 y luego de vuelta a 0. El valor 0 es no sombreado luz direccional y sin efecto 3D, mientras que el 1 valor es máxima en 3D. Este valor de lightIntensity se utiliza en conjunción con el producto de punto para calcular el factor total de la luz:

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

El primer 0.5 en esta fórmula se refiere a la luz ambiental, y el segundo 0.5 permite totalLight al rango de 0 a 1 dependiendo del valor del producto punto. (En teoría, esto no es correcto. Los valores negativos del producto punto deben establecerse a 0 porque resultan en luz total menor que la luz ambiente.)

Este totalLight se utiliza para calcular un color y un cepillo para cada cara:

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

Se muestran los resultados con máxima 3D-ishness en figura 7.

The PlatonicSolids Program with Maximum 3D
Figura 7 el programa PlatonicSolids con máxima 3D

Charles Petzold es desde hace mucho tiempo colaborador de MSDN Magazine y el autor de "Programación Windows, 6ª edición" (Microsoft Press, 2013), un libro acerca de cómo escribir aplicaciones para Windows 8. Su sitio Web es charlespetzold.com.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Doug Erickson