Este artículo proviene de un motor de traducción automática.
El factor DirectX
Triángulos y teselación
Descargar el proyecto de código
El triángulo es la figura de dos dimensiones más básica. No es nada más que tres puntos conectan por tres líneas, y si intentas hacerlo más sencillo, se derrumba en una sola dimensión. Por otro lado, cualquier otro tipo de polígono se puede descomponer en una colección de triángulos.
Incluso en tres dimensiones, un triángulo siempre es plano. De hecho, es una manera de definir un plano en el espacio 3D con tres puntos no colineales, y eso es un triángulo. Una plaza en el espacio 3D no está garantizada que sea plana porque el cuarto punto no podría estar en el mismo plano que los otros tres. Pero ese cuadrado se puede dividir en dos triángulos, cada una de ellas es plana, aunque no necesariamente en el mismo avión.
En la programación de gráficos en 3D, triángulos forman las superficies de figuras sólidas, empezando por el más simple de figuras tridimensionales de todo, la Pirámide triangular o tetraedro. Montaje de una figura aparentemente sólida de triángulo "building blocks" es el proceso más fundamental en gráficos 3D por computadora. Por supuesto, a menudo se curvan las superficies de objetos del mundo real, pero si haces los triángulos suficientemente pequeño, pueden aproximar las superficies curvadas en grado suficiente para engañar al ojo humano.
La ilusión de la curvatura es mejorada mediante la explotación de otra característica de los triángulos: Si los tres vértices de un triángulo se asocian con tres valores diferentes — por ejemplo, tres colores diferentes o tres vectores geométricos distintos, estos valores pueden ser interpolados sobre la superficie del triángulo y utilizados para colorear la superficie. Esto es cómo triángulos están sombreadas para imitar la reflexión de la luz en objetos del mundo real.
Triángulos de Direct2D
Los triángulos son omnipresentes en gráficos 3D por computadora. Gran parte del trabajo realizado por una unidad de procesamiento gráfico moderno (GPU) involucra triángulos de representación, así que por supuesto Direct3D programación implica trabajar con triángulos para definir Figuras sólidas.
En contraste, triángulos no se encuentran en absoluto en gráficos 2D más programación de interfaces, donde los primitivos bidimensionales más comunes son las líneas, curvas, rectángulos y elipses. Así que es sorprendente encontrar triángulos pop-up en una esquina oscura de Direct2D. O tal vez sea realmente no tan sorprendente: Porque Direct2D es construido en la cima de Direct3D, parece razonable para Direct2D aprovechar el apoyo de triángulo en Direct3D y la GPU.
La estructura del triángulo definida en Direct2D es simple:
struct D2D1_TRIANGLE
{
D2D1_POINT_2F point1;
D2D1_POINT_2F point2;
D2D1_POINT_2F point3;
};
En cuanto puedo determinar, esta estructura se utiliza en Direct2D solamente con respecto a una "malla" que es una colección de triángulos almacenados en un objeto de tipo ID2D1Mesh. El ID2D1RenderTarget (de los cuales ID2D1DeviceContext deriva) admite un método denominado CreateMesh que crea un objeto:
ID2D1Mesh * mesh;
deviceContext->CreateMesh(&mesh);
(Para mantener las cosas simples, no muestro el uso de ComPtr o comprobación de valores HRESULT en estos ejemplos de código breve.) La interfaz ID2D1Mesh define un método único llamado abierto. Este método devuelve un objeto de tipo ID2D1TessellationSink:
ID2D1TessellationSink * tessellationSink;
mesh->Open(&tessellationSink);
En general, "teselación" se refiere al proceso de cubrir una superficie con un patrón de mosaico, pero el término se utiliza algo diferentemente en programación Direct2D y Direct3D. En Direct2D, Teselación es el proceso de descomposición de un área de dos dimensiones en triángulos.
La interfaz de ID2D1TessellationSink tiene dos métodos: Añadirtriángulos (que agrega una colección de objetos D2D1_TRIANGLE a la colección) y cierre, que hace que el objeto de malla inmutable.
Aunque el programa puede llamar AddTriangles sí mismo, a menudo pasará el objeto ID2D1TessellationSink al método Tessellate definido por la interfaz de ID2D1Geometry:
geometry->Tessellate(IdentityMatrix(), tessellationSink);
tessellationSink->Close();
El método Tessellate genera triángulos que cubren las áreas delimitadas por la geometría. Después de llamar al método Close, puede descartarse el fregadero y te quedas con un objeto ID2D1Mesh. El proceso de generar el contenido de un objeto ID2D1Mesh con un ID2D1TessellationSink es similar a la definición de un ID2D1Pathgeometría usando un ID2D1GeometrySink.
Entonces puede representar este objeto ID2D1Mesh mediante el método FillMesh de ID2D1RenderTarget. Un cepillo gobierna cómo se colorea la malla:
deviceContext->FillMesh(mesh, brush);
Tenga en cuenta que estos triángulos de malla definir un área y no un esquema de una zona. No hay ningún método de DrawMesh.
FillMesh tiene una limitación: Suavizado no puede activarse cuando se llama a FillMesh. Preceder a la FillMesh con una llamada a SetAntialiasMode:
deviceContext->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
Te preguntarás: ¿Cuál es el punto? ¿Por qué no llamar FillGeometry en el objeto de la geometría original? Las imágenes deben ser iguales (aparte del anti-aliasing). Pero hay en realidad una profunda diferencia entre los objetos ID2D1Geometry y ID2D1Mesh que revela cómo se crean estos dos objetos.
Geometrías son en su mayoría sólo colecciones de puntos coordenadas, geometrías son objetos independientes del dispositivo. Puede crear varios tipos de geometrías mediante llamadas a métodos definidos por ID2D1Factory.
Una malla es una colección de triángulos, los cuales son sólo trillizos de coordinadas puntos, una malla debe ser un objeto independiente del dispositivo. Pero se crea un objeto ID2D1Mesh mediante una llamada a un método definido por ID2D1RenderTarget. Esto significa que la malla es un objeto dependiente del dispositivo, como un pincel.
Esto le dice de los triángulos que componen la malla se almacenan en una forma dependiente del dispositivo, muy probablemente en un formato adecuado para el procesamiento de la GPU, o en realidad en la GPU. Y esto significa que FillMesh debe ejecutar mucho más rápido que FillGeometry para la cifra equivalente.
¿Probamos esa hipótesis?
Entre el código descargable para este artículo es un programa llamado MeshTest que crea una geometría de ruta para una estrella de 201 puntos y haciéndolo girar lentamente mientras que calcular y visualizar la velocidad de fotogramas. Cuando se compila el programa en modo de depuración para x 86 y funciona en mi Surface Pro, me da una velocidad de menos de 30 fotogramas por segundo (FPS) cuando representar la geometría de ruta (incluso si la geometría es esbozada para eliminar zonas superpuestas y aplanada para eliminar las curvas), pero la velocidad de fotogramas salta hasta 60FPS al representar la malla.
CONCLUSIÓN: Para geometrías complejas, tiene sentido para convertirlos a los acoplamientos para la representación. Si la necesidad de desactivar el suavizado para representar esta malla es un ultimátum, tal vez quieras ver ID2D1GeometryRealization, introducido en el punto 8.1 de Windows. Esto combina el rendimiento de ID2D1Mesh pero permite la visualización suavizada. Mantenga en mente mallas y realizaciones de geometría deben crearse si se recrea el dispositivo de visualización, así como con otros recursos dependientes del dispositivo como cepillos.
Examinando los triángulos
Siento curiosidad por los triángulos generados por el proceso de teselación. Podrían en realidad ellos ser visualizadas. El objeto ID2D1Mesh no le permite acceder a los triángulos que componen la malla, pero es posible escribir su propia clase que implementa la interfaz ID2D1TessellationSink y pasar una instancia de esa clase al método Tessellate.
Llamé a mi aplicación ID2D1TessellationSink InterrogableTessellationSink y resultaron para ser vergonzosamente simple. Contiene a un miembro de datos privados para el almacenamiento de objetos del triángulo:
std::vector<D2D1_TRIANGLE> m_triangles;
La mayor parte del código está dedicado a implementar la interfaz IUnknown. Figura 1 muestra el código necesario para implementar los métodos ID2D1TessellationSink dos y obtener los triángulos resultantes.
Figura 1 el código relevante de InterrogableTessellationSink
// ID2D1TessellationSink methods
void InterrogableTessellationSink::AddTriangles(_In_ const D2D1_TRIANGLE *triangles,
UINT trianglesCount)
{
for (UINT i = 0; i < trianglesCount; i++)
{
m_triangles.push_back(triangles[i]);
}
}
HRESULT InterrogableTessellationSink::Close()
{
// Assume the class accessing the tessellation sink knows what it's doing
return S_OK;
}
// Method for this implementation
std::vector<D2D1_TRIANGLE> InterrogableTessellationSink::GetTriangles()
{
return m_triangles;
}
Incorporé esta clase en un proyecto denominado teselaciónvisualización. El programa crea geometrías de diversos tipos — todo, desde una geometría simple rectángulo a geometrías generados a partir de pictogramas de texto — y InterrogableTessellationSink se utiliza para obtener la colección de triángulos creados por el método Tessellate. Cada triángulo se convierte entonces en un objeto ID2D1PathGeometry que consta de tres líneas rectas. Estas geometrías camino entonces se procesan utilizando DrawGeometry.
Como era de esperar, un ID2D1RectangleGeometry es cuadriculada en dos triángulos, pero las otras geometrías son más interesantes. Figura 2 muestra los triángulos que componen un ID2D1RoundedRectangleGeometry.
Figura 2 redondeado rectángulo descompuesto en triángulos
Esto no es que un ser humano podría teselar el rectángulo redondeado. Un ser humano probablemente el rectángulo redondeado se dividen en cuatro trimestre-círculos y cinco rectángulos y cada una de esas figuras teselar por separado. En particular, un ser humano podría cortar los cuatro trimestre-círculos en pastel cuñas.
En otras palabras, un ser humano sería definir varios puntos más en el interior de la geometría para ayudar en el mosaico. Pero el algoritmo de teselación definido por el objeto de la geometría no utiliza ningún punto más allá de los creados por el aplanamiento de la geometría.
Figura 3 muestra dos personajes con la fuente Pescadero descompuestos en triángulos.
Figura 3 texto descompuesto en triángulos
También tenía curiosidad sobre el orden en que se generaron estos triángulos y haga clic en la opción de relleno degradado en la parte inferior izquierda de la ventana, usted puede encontrar hacia fuera. Cuando esta opción está marcada, el programa llama a FillGeometry para cada una de las geometrías de triángulo. Un pincel de color sólido se pasa a FillGeometry pero el color depende índice del triángulo en la colección.
¿Qué encontrarás es que algo parecido a un pincel de degradado de arriba hacia abajo, que significa que los triángulos se almacenan en la colección en un orden descendente visual representa la opción FillGeometry. Al parecer, que el algoritmo de teselación intenta maximizar el ancho de las líneas de exploración horizontal en los triángulos, que probablemente maximiza el rendimiento de procesamiento.
Aunque reconozco claramente la sabiduría de este enfoque, debo confesar que me decepcionó un poco. Esperaba que una curva de Bézier ensanchada (por ejemplo) podría ser teselada comenzando en un extremo de la línea y continuar al otro, entonces los triángulos podrían procesarse con un gradiente de un extremo al otro, que no es un tipo de degradado comúnmente visto en un programa de DirectX. Pero esto no pudo ser.
Curiosamente, necesitaba desactivar el suavizado antes de que llama a la FillGeometry en TessellationVisualization o tenues líneas aparecieron entre los triángulos prestados. Estas líneas débiles resultan del algoritmo de suavizado, que consiste en píxeles parcialmente transparentes que no se vuelvan opacos cuando los comprometidos. Esto me lleva a sospechar que utiliza la visualización suavizada con FillMesh no es un hardware o software limitación, sino una restricción el mandato para evitar anomalías visuales.
Triángulos en 2D y 3D
Después de trabajar sólo un poco mientras que con objetos ID2D1Mesh, comencé a visualizar todas las áreas bidimensionales como mosaicos de triángulos. Esta mentalidad es normal cuando se hace la programación 3D, pero nunca le había extendido una visión centrada en triángulo al mundo 2D.
La documentación del método Tessellate indica que los triángulos generados son "herida hacia la derecha", lo que significa que el point1, point2 y point3 miembros de la estructura D2D1_TRIANGLE se ordenan en sentido horario. Esta es información muy útil cuando se utilizan estos triángulos en la programación de gráficos 2D, pero se torna muy importante en el mundo 3D, donde el orden de los puntos en un triángulo generalmente indica el frente o la parte posterior de la figura.
Por supuesto, estoy muy interesado en utilizar estos triángulos teselados bidimensionales para romperse a través de la tercera dimensión, donde los triángulos son más cómodamente en casa. Pero no quiero ser tan apurado que descuido explorar algunos efectos interesantes con triángulos teselados en dos dimensiones.
Colorear triángulos únicamente
Para mí, la mayor emoción en la programación de gráficos está creando imágenes en la pantalla del ordenador de un tipo que nunca había visto, y no creo que jamás haya visto texto teselado en triángulos cuyos colores cambian de forma aleatoria. Esto sucede en un programa que llamo SparklingText.
Tenga en cuenta que tanto FillGeometry como FillMesh implican sólo un cepillo solo, así que si usted necesita procesar cientos de triángulos con diversos colores, necesitará cientos de llamadas FillGeometry o FillMesh, cada representación un triángulo único. ¿Cuál es más eficiente? Una llamada FillGeometry para rendir un ID2D1PathGeometry que consta de tres líneas rectas. ¿O una llamada FillMesh con un ID2D1Mesh que contiene un solo triángulo?
Supuse que FillMesh sería más eficiente que FillGeometry sólo si la malla contiene múltiples triángulos, y sería más lento para un triángulo, así que escribí originalmente el programa para generar geometrías de ruta de los triángulos teselados. Sólo más tarde agregar una casilla de verificación con la etiqueta "Usar una malla para cada triángulo en lugar de un PathGeometry" e incorporado esa lógica también.
La estrategia de la clase SparklingTextRenderer de SparklingText es utilizar el método GetGlyphRunOutline de ID2D1FontFace para obtener una geometría de ruta para los contornos de carácter. El programa llama al método Tessellate en esta geometría con el InterrogableGeometrySink para obtener una colección de objetos D2D1_TRIANGLE. Luego son convertidos en geometrías de camino o mallas (dependiendo del valor de la casilla de verificación) y almacenadas en una de las colecciones de dos vectores llamados m_triangleGeometries y m_triangleMeshes.
Figura 4 muestra una porción pertinente del Tessellate que llena estas colecciones y el método Render que hace que los triángulos resultantes. Como de costumbre, comprobación de HRESULT ha sido eliminado para simplificar los listados de código.
Figura 4 Teselación y representación de código en SparklingTextRenderer
void SparklingTextRenderer::Tessellate()
{
...
// Tessellate geometry into triangles
ComPtr<InterrogableTessellationSink> tessellationSink =
new InterrogableTessellationSink();
pathGeometry->Tessellate(IdentityMatrix(), tessellationSink.Get());
std::vector<D2D1_TRIANGLE> triangles = tessellationSink->GetTriangles();
if (m_useMeshesNotGeometries)
{
// Generate a separate mesh from each triangle
ID2D1DeviceContext* context = m_deviceResources->GetD2DDeviceContext();
for (D2D1_TRIANGLE triangle : triangles)
{
ComPtr<ID2D1Mesh> triangleMesh;
context->CreateMesh(&triangleMesh);
ComPtr<ID2D1TessellationSink> sink;
triangleMesh->Open(&sink);
sink->AddTriangles(&triangle, 1);
sink->Close();
m_triangleMeshes.push_back(triangleMesh);
}
}
else
{
// Generate a path geometry from each triangle
for (D2D1_TRIANGLE triangle : triangles)
{
ComPtr<ID2D1PathGeometry> triangleGeometry;
d2dFactory->CreatePathGeometry(&triangleGeometry);
ComPtr<ID2D1GeometrySink> geometrySink;
triangleGeometry->Open(&geometrySink);
geometrySink->BeginFigure(triangle.point1, D2D1_FIGURE_BEGIN_FILLED);
geometrySink->AddLine(triangle.point2);
geometrySink->AddLine(triangle.point3);
geometrySink->EndFigure(D2D1_FIGURE_END_CLOSED);
geometrySink->Close();
m_triangleGeometries.push_back(triangleGeometry);
}
}
}
void SparklingTextRenderer::Render()
{
...
Matrix3x2F centerMatrix = D2D1::Matrix3x2F::Translation(
(logicalSize.Width - (m_geometryBounds.right + m_geometryBounds.left)) / 2,
(logicalSize.Height - (m_geometryBounds.bottom + m_geometryBounds.top)) / 2);
context->SetTransform(centerMatrix *
m_deviceResources->GetOrientationTransform2D());
context->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
if (m_useMeshesNotGeometries)
{
for (ComPtr<ID2D1Mesh>& triangleMesh : m_triangleMeshes)
{
float gray = (rand() % 1000) * 0.001f;
m_solidBrush->SetColor(ColorF(gray, gray, gray));
context->FillMesh(triangleMesh.Get(), m_solidBrush.Get());
}
}
else
{
for (ComPtr<ID2D1PathGeometry>& triangleGeometry : m_triangleGeometries)
{
float gray = (rand() % 1000) * 0.001f;
m_solidBrush->SetColor(ColorF(gray, gray, gray));
context->FillGeometry(triangleGeometry.Get(), m_solidBrush.Get());
}
}
...
}
Basado en la tasa de fotogramas de vídeo (que se muestra el programa), mi Surface Pro procesa las mallas más rápido que las geometrías del camino, a pesar de que cada malla contiene sólo un triángulo único.
La animación de los colores es fastidiosamente reminiscente de un aura brillante de migraña, así que tal vez quieras ejercer cierta precaución cuando se lo ve. Figura 5 muestra una imagen fija del programa, que debe ser mucho más seguro.
Figura 5 la pantalla SparklingText
Moviendo los triángulos teselados
Los restantes dos programas utilizan una estrategia similar a la SparklingText para generar una colección de triángulos en contornos de glifo de forma, pero luego se movió los triángulos pequeños alrededor de la pantalla.
Para OutThereAndBackAgain, como lo pronostiqué texto que expandirían en sus triángulos compuestos, que luego volvería a formar el texto nuevo. Figura 6 muestra este proceso en 3 por ciento en la animación de vuelo-apart.
El método CreateWindowSizeDependentResources de la clase OutThereAndBackAgainRenderer reúne información sobre cada triángulo en una estructura de llamar TriangleInfo. Esta estructura contiene un solo triángulo ID2D1Mesh objeto, así como la información necesaria para tomar ese triángulo en un exterior de viaje y de regreso. Este viaje aprovecha de una característica de geometrías que se puede utilizar independientemente de renderizado. El método ComputeLength en ID2D1Geometry devuelve la longitud total de una geometría, mientras que ComputePointAtLength devuelve un punto en la curva y una tangente a la curva en cualquier longitud. De esa información que puede derivar traducir y gire las matrices.
Como se puede ver en figura 6, usé un pincel de degradado para el texto para que se cruzan y se mezclan un poco triángulos de colores ligeramente diferentes. Aunque estoy usando solamente un cepillo, el efecto deseado requiere el método Render para llamar SetTransform y FillMesh para cada malla single-triángulo. El pincel de degradado se aplica como si fuera la malla en su posición original antes de la transformación.
Figura 6 todavía del programa OutThereAndBackAgain
Me preguntaba si sería eficiente para el método Update para transformar todos los triángulos individuales "manualmente" con las llamadas al método TransformPoint de la clase Matrix3x2F y consolidar en un solo objeto ID2D1Mesh, que luego quedarían completamente con una sola llamada FillMesh. He añadido una opción para eso, y efectivamente, era más rápido. Me woudn't imaginado que creando un ID2D1Mesh en cada llamada de actualización funcionaría bien, pero lo hace. Los efectos visuales son ligeramente diferentes, sin embargo: El pincel de degradado se aplica a los triángulos transformados en la malla, así que no hay ninguna mezcla de colores.
¿Texto Morphing?
Supongamos que usted teselar las geometrías de contorno del glifo de dos cadenas de texto — por ejemplo, las palabras "DirectX" y "Factor" que componen el nombre de esta columna — y los triángulos para la interpolación en parejas. Una animación entonces podría definir que una palabra se transforma en el otro. No es exactamente un efecto morphing, pero no sé qué más que llamarlo.
Figura 7 muestra el efecto a mitad de camino entre las dos palabras y con un poco de imaginación puede hacer casi fuera "DirectX" o "Factor" en la imagen.
Figura 7 la pantalla TextMorphing
Óptimamente, cada par de triángulos morphing debe estar espacialmente cerca, pero minimizando las distancias entre todos los pares de triángulos es similar al problema del viajante. Tomé un enfoque relativamente sencillo clasificar las dos colecciones de triángulos por el centro del triángulo las coordenadas X, y luego separando las colecciones en grupos que representan a las gamas de coordenadas X, y clasificar aquellos por el Y coordina. Por supuesto, las colecciones de dos triángulo son de distintos tamaños, para que algunos triángulos en la palabra "Factor" corresponden a dos triángulos en la palabra "DirectX".
Figura 8 muestra la lógica de la interpolación en la actualización y la lógica de representación en Render.
Figura 8 actualización y renderizado en TextMorphing
void TextMorphingRenderer::Update(DX::StepTimer const& timer)
{
...
// Calculate an interpolation factor
float t = (float)fmod(timer.GetTotalSeconds(), 10) / 10;
t = std::cos(t * 2 * 3.14159f); // 1 to 0 to -1 to 0 to 1
t = (1 - t) / 2; // 0 to 1 to 0
// Two functions for interpolation
std::function<D2D1_POINT_2F(D2D1_POINT_2F, D2D1_POINT_2F, float)>
InterpolatePoint =
[](D2D1_POINT_2F pt0, D2D1_POINT_2F pt1, float t)
{
return Point2F((1 - t) * pt0.x + t * pt1.x,
(1 - t) * pt0.y + t * pt1.y);
};
std::function<D2D1_TRIANGLE(D2D1_TRIANGLE, D2D1_TRIANGLE, float)>
InterpolateTriangle =
[InterpolatePoint](D2D1_TRIANGLE tri0, D2D1_TRIANGLE tri1, float t)
{
D2D1_TRIANGLE triangle;
triangle.point1 = InterpolatePoint(tri0.point1, tri1.point1, t);
triangle.point2 = InterpolatePoint(tri0.point2, tri1.point2, t);
triangle.point3 = InterpolatePoint(tri0.point3, tri1.point3, t);
return triangle;
};
// Interpolate the triangles
int count = m_triangleInfos.size();
std::vector<D2D1_TRIANGLE> triangles(count);
for (int index = 0; index < count; index++)
{
triangles.at(index) =
InterpolateTriangle(m_triangleInfos.at(index).triangle[0],
m_triangleInfos.at(index).triangle[1], t);
}
// Create a mesh with the interpolated triangles
m_deviceResources->GetD2DDeviceContext()->CreateMesh(&m_textMesh);
ComPtr<ID2D1TessellationSink> tessellationSink;
m_textMesh->Open(&tessellationSink);
tessellationSink->AddTriangles(triangles.data(), triangles.size());
tessellationSink->Close();
}
// Renders a frame to the screen
void TextMorphingRenderer::Render()
{
...
if (m_textMesh != nullptr)
{
Matrix3x2F centerMatrix = D2D1::Matrix3x2F::Translation(
(logicalSize.Width - (m_geometryBounds.right + m_geometryBounds.left)) / 2,
(logicalSize.Height - (m_geometryBounds.bottom + m_geometryBounds.top)) / 2);
context->SetTransform(centerMatrix *
m_deviceResources->GetOrientationTransform2D());
context->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
context->FillMesh(m_textMesh.Get(), m_blueBrush.Get());
}
...
}
Con eso, creo que he satisfecho mi curiosidad por triángulos 2D y estoy dispuesto a dar esos triángulos una tercera dimensión.
Charles Petzold es desde hace mucho tiempo colaborador de MSDN Magazine y el autor de "Programación Windows, 6ª edición" (Microsoft Press, 2012), un libro acerca de cómo escribir aplicaciones para Windows 8. Su sitio Web es charlespetzold.com.
Gracias a los siguientes expertos técnicos de Microsoft por su ayuda en la revisión de este artículo: Jim Galasyn y Mike riquezas