Este artigo foi traduzido por máquina.

Fator de DirectX

Um portal 2D para um mundo 3D

Charles Petzold

Baixe o código de exemplo

Se você é bem versado em gráficos 2D, você pode assumir que o 3D é semelhante, excepto a dimensão extra. Não é bem assim! Quem já se envolveu em programação gráfica 3D sabe como é difícil. Programação de gráficos 3D requer a mestre conceitos novos e exóticos, além de tudo o que encontrou no mundo 2D convencional. Muitas preliminares são necessárias para obter um pouco 3D na tela, e mesmo assim um pequeno erro de cálculo pode torná-lo invisível. Consequentemente, o feedback visual tão importante para aprender programação de gráficos é atrasado até que todas as peças de programação estão no lugar e trabalhando em harmonia.

DirectX reconhece a profunda diferença entre programação de gráficos 2D e 3D com a divisão entre o Direct2D e Direct3D. Embora você pode misturar conteúdo 2D e 3D no mesmo dispositivo de saída, estas são muito distinto e diferente interfaces de programação, e não existe meio termo. DirectX não permite que você ser o país um pouco mais, um pouco rock and roll.

Ou não é?

Curiosamente, Direct2D inclui alguns conceitos e instalações que se originou no universo de programação 3D. Através de recursos como mosaico de geometria (a decomposição de geometrias complexas em triângulos) e efeitos 2D usando shaders (que consistem em especial código que é executado sobre o GPU ou unidade de processamento gráfico), é possível explorar alguns conceitos 3D poderosos enquanto ainda permanecem dentro do contexto do Direct2D.

Além disso, esses conceitos 3D podem ser encontrados e explorados gradualmente, e você tem a satisfação de ver os resultados na tela. Você pode obter seus pés 3D molhado em Direct2D para que um mergulho mais tarde na programação Direct3D é um pouco menos chocante.

Acho que não deveria ser tão surpreendente que o Direct2D incorpora algumas características 3D. Arquitetonicamente, Direct2D é construída em cima de Direct3D, que permite que o Direct2D aproveitar também a aceleração de hardware da GPU. Esta relação entre Direct2D e Direct3D torna-se mais aparente como você começa a explorar as partes baixas do Direct2D.

Eu vou começar essa exploração com uma revisão de coordenadas 3D e sistemas de coordenadas.

O grande salto para o exterior

Se você estiver seguindo essa coluna nos últimos meses, sabe que é possível chamar o método GetGlyphRunOutline de um objeto que implementa a interface IDWriteFontFace para obter uma instância de ID2D1PathGeometry que descreve os contornos de caracteres de texto em termos de linhas retas e curvas de Bézier. Em seguida, você pode manipular as coordenadas dessas linhas e curvas para distorcer os caracteres de texto de várias maneiras.

Também é possível converter as coordenadas 2D de uma geometria de caminho em coordenadas 3D e em seguida manip­ulate estas coordenadas 3D antes de convertê-los de volta em 2D para exibir a geometria de caminho normalmente. Isso parece divertido?

Coordenadas no espaço bidimensional são expressos como pares de números (X, Y), que correspondem a um local na tela; Coordenadas 3D estão na forma (X, Y, Z) e, conceitualmente, o eixo Z é ortogonal à tela. A menos que você está lidando com um display holográfico ou uma impressora 3D, estas coordenadas Z não são quase tão reais como as coordenadas X e Y.

Existem outras diferenças entre os sistemas de coordenadas 2D e 3D. Convencionalmente a origem 2D — o ponto (0, 0) — é o canto superior esquerdo do dispositivo de vídeo. O X coordenadas aumento para a direita e Y coordenadas descendo. Em 3D, muitas vezes a origem está no centro da tela, e é mais parecido com um sistema de coordenadas cartesiano padrão: O X ainda coordena aumento indo para a direita, mas o aumento de coordenadas Y subindo, e existem coordenadas negativas também. (Claro, a origem, escala e orientação destes eixos podem ser alterados com transformações de matriz e normalmente são.)

Conceitualmente, o eixo Z positivo pode apontar fora da tela ou ponto na tela. Estas duas convenções são conhecidas como "braço direito" e "esquerdo" sistemas de coordenadas, referindo-se a uma técnica para distingui-los: Com um sistema de coordenadas à direita, se você apontar o dedo indicador da mão direita na direção do X linha central e o dedo médio na direção de Y positivo, positivo o polegar aponta para Z positivo. Além disso, se você curve os dedos da mão direita do positivo X eixo para o eixo Y positivo, seus pontos de polegar para Z positivo. Com um sistema de coordenadas da esquerda, é o mesmo, exceto usando a mão esquerda.

Meu objetivo aqui é obter uma geometria 2D caminho de uma seqüência de caracteres de texto curto e depois transformam em torno da origem um anel 3D então início encontra o fim, semelhante à ilustração mostrada na Figura 1. Porque eu vou ser converter 2D coordenadas para coordenadas 3D e depois volta ao 2D, eu escolhi para usar um sistema de coordenadas 3D com coordenadas Y aumentando a descer, assim como em 2D. O eixo Z positivo sai da tela, mas é realmente um sistema de coordenadas da esquerda.


Figura 1 o sistema de coordenadas usado para os programas neste artigo

Para tornar este trabalho mais fácil possível, eu usei um arquivo de fonte armazenado como um recurso de programa e criado um objeto IDWriteFontFile para obter o objeto IDWriteFontFace. Alternativamente, você poderia obter um IDWriteFontFace através de um método mais rotunda da coleção de fonte do sistema.

O objeto de ID2D1PathGeometry gerado a partir do método GetGlyphRunOutline é então passado através do método de simplificar usando o argumento D2D1_GEOMETRY_SIMPLIFICATION_OPTION_LINES para nivelar todas as curvas de Bézier em sequências de linhas curtas. Que a geometria simplificada é passada em uma implementação personalizada de ID2D1GeometrySink chamado FlattenedGeometrySink para decompor mais todas as linhas retas em muito mais curtas linhas retas. O resultado é uma geometria completamente maleável, consistindo apenas em linhas.

Para facilitar a manipulação destas coordenadas, FlattenedGeometry­pia gera uma coleção de objetos de polígono. Figura 2 mostra a definição da estrutura de polígono. É basicamente apenas uma coleção de pontos 2D conectados. Cada objeto de polígono corresponde a uma figura fechada na geometria de caminho. Nem todas as figuras em geometrias de caminho estão fechadas, mas aqueles em glifos de texto estão sempre fechados, então essa estrutura está bem para essa finalidade. Alguns caracteres (como C, E e X) são descritos por um polígono; alguns (A, D e O) consistem em dois objetos de polígono para interior e exterior; alguns (B, por exemplo) consiste de três; e alguns caracteres de símbolo podem ter muitos mais.

Figuras Figura 2 Classe Polygon para armazenar o caminho fechado

struct Polygon
{
  // Constructors
  Polygon()
  {
  }
  Polygon(size_t pointCount)
  {
    Points = std::vector<D2D1_POINT_2F>(pointCount);
  }
  // Move constructor
  Polygon(Polygon && other) : Points(std::move(other.Points))
  {
  }
  std::vector<D2D1_POINT_2F> Points;
  static HRESULT CreateGeometry(ID2D1Factory* factory,
                                const std::vector<Polygon>& polygons,
                                ID2D1PathGeometry** pathGeometry);
};
HRESULT Polygon::CreateGeometry(ID2D1Factory* factory,
                                const std::vector<Polygon>& polygons,
                                ID2D1PathGeometry** pathGeometry)
{
  HRESULT hr;
  if (FAILED(hr = factory->CreatePathGeometry(pathGeometry)))
    return hr;
  Microsoft::WRL::ComPtr<ID2D1GeometrySink> geometrySink;
  if (FAILED(hr = (*pathGeometry)->Open(&geometrySink)))
    return hr;
  for (const Polygon& polygon : polygons)
  {
    if (polygon.Points.size() > 0)
    {
      geometrySink->BeginFigure(polygon.Points[0],
                                D2D1_FIGURE_BEGIN_FILLED);
      if (polygon.Points.size() > 1)
      {
        geometrySink->AddLines(polygon.Points.data() + 1,
                               polygon.Points.size() - 1);
      }
      geometrySink->EndFigure(D2D1_FIGURE_END_CLOSED);
    }
  }
  return geometrySink->Close();
}

Entre o código para download desta coluna é um programa de armazenamento Windows chamado CircularText que cria uma coleção de objetos de polígono baseado no texto "Texto em um infinito círculo de," onde o final destina-se a ligar para o início de um círculo. A seqüência de caracteres de texto na verdade é especificada no programa como "ext em um círculo infinito de T" evitar um espaço no início ou final que iria desaparecer quando uma geometria de caminho é gerada a partir de glifos.

A classe CircularTextRenderer no CircularText projeto contém dois objetos de std:: vector do tipo polígono chamado m_srcPolygons (os originais objetos polígono gerados a partir da geometria de caminho) e m_dstPolygons (os objetos de polígono usados para gerar a geometria renderizada caminho). Figura 3 mostra o método CreateWindowSizeDependentResources que converte os polígonos de fonte para os polígonos de destino com base no tamanho da tela.

Figura 3 de 2D para 3D para 2D no programa CircularText

void CircularTextRenderer::CreateWindowSizeDependentResources()
{
  // Get window size and geometry size
  Windows::Foundation::Size logicalSize = m_deviceResources->GetLogicalSize();
  float geometryWidth = m_geometryBounds.right - m_geometryBounds.left;
  float geometryHeight = m_geometryBounds.bottom - m_geometryBounds.top;
  // Calculate a few factors for converting 2D to 3D
  float radius = logicalSize.Width / 2 - 50;
  float circumference = 2 * 3.14159f * radius;
  float scale = circumference / geometryWidth;
  float height = scale * geometryHeight;
  for (size_t polygonIndex = 0; polygonIndex < m_srcPolygons.size(); polygonIndex++)
  {
    const Polygon& srcPolygon = m_srcPolygons.at(polygonIndex);
    Polygon& dstPolygon = m_dstPolygons.at(polygonIndex);
    for (size_t pointIndex = 0; pointIndex < srcPolygon.Points.size(); pointIndex++)
    {
      const D2D1_POINT_2F pt = srcPolygon.Points.at(pointIndex);
      float radians = 2 * 3.14159f * (pt.x - m_geometryBounds.left) / geometryWidth;
      float x = radius * sin(radians);
      float z = radius * cos(radians);
      float y = height * ((pt.y - m_geometryBounds.top) / geometryHeight - 0.5f);
      dstPolygon.Points.at(pointIndex) = Point2F(x, y);
    }
  }
  // Create path geometry from Polygon collection
  DX::ThrowIfFailed(
    Polygon::CreateGeometry(m_deviceResources->GetD2DFactory(),
                            m_dstPolygons,
                            &m_pathGeometry)
    );
}

No loop interno, você verá x, y e z os valores calculados. Esta é uma coordenada 3D, mas ele nem sequer é salvo. Em vez disso, ele é imediatamente recolhido volta em 2D por simplesmente ignorar o valor de z. Para eaocálculo­tarde essas coordenadas 3D, o código primeiro converte numa posição horizontal sobre a geometria original de caminho para um ângulo em radianos, de 0 a 2 π. As funções sin e cos calculam uma posição em um círculo unitário no plano XZ. O valor de y é uma conversão mais direta das coordenadas verticais da geometria do caminho original.

O método CreateWindowSizeDependentResources conclui-se, obtendo um novo objeto de ID2D1PathGeometry do destino coleção Polygon. O método Render, em seguida, define uma transformação de matriz para colocar a origem no centro da tela, e ambos preenche e descreve essa geometria de caminho, com o resultado mostrado no Figura 4.


Figura 4 o visor CircularText

O programa está funcionando? É difícil dizer! Olhar de perto e você pode ver alguns caracteres de largura no centro e mais estreitos caracteres à esquerda e à direita. Mas o grande problema é que eu comecei com uma geometria de caminho com linhas não se cruzam, e agora a geometria é exibida volta sobre si mesmo, com o resultado que as áreas sobrepostas não são preenchidas. Este efeito é característica de geometrias, e isso acontece se a geometria do percurso criada pela estrutura polígono tem um modo de preenchimento de alternativo ou enrolamento.

Ficando um pouco de perspectiva

Programação de gráficos tridimensionais não é apenas sobre coordenadas de pontos. Indicações visuais são necessárias para o espectador a interpretar uma imagem em uma tela 2D como representando um objeto no espaço 3D. No mundo real, você raramente Ver os objetos do ponto de vista constante. Você poderia ter uma melhor visão do texto 3D no Figura 4 se você pode incliná-lo um pouco para que mais parece o anel no Figura 1.

Para obter uma perspectiva sobre o texto tridimensional, as coordenadas precisam ser girada no espaço. Como você sabe, Direct2D suporta uma estrutura de transformação de matriz chamada D2D1_MATRIX_3x2_F que você pode usar para definir transformações 2D, que você pode aplicar a sua saída de gráficos 2D, chamando o método SetTransform da ID2D1RenderTarget.

Mais comumente, você usaria uma classe chamada Matrix3x2F do namespace D2D1 para esta finalidade. Essa classe deriva de D2D1_MATRIX_3x2F_F e fornece métodos para definir vários tipos de padrões para a tradução, escala, rotação e inclinação.

A classe Matrix3x2F também define um método chamado TransformPoint que permite que você aplique a transformação "manualmente" para objetos individuais de D2D1_POINT_2F. Isso é útil para manipular pontos antes que eles são processados.

Você pode pensar que eu preciso de uma matriz de rotação 3D para inclinar o texto exibido. Eu certamente estaremos explorando transformações 3D matrix em colunas futuras, mas por enquanto eu consigo fazer com rotação 2D. Imagine-se situado em algum lugar no negativo eixo de X Figura 1, olhando em direção a origem. Os eixos Z e Y positivos situam-se apenas como o X e Y eixos em um 2D convencional coordenam sistema, portanto, parece plausível que, através da aplicação de uma matriz de rotação 2D para os valores de Z e Y, pode girar todas as coordenadas em torno do três -­dimensional eixo X.

Você pode experimentar com isso com o programa CircularText. Criar uma matriz de rotação 2D em CreateWindowSizeDependent o­método de recursos, antes as coordenadas do polígono são manipuladas:

Matrix3x2F tiltMatrix = Matrix3x2F::Rotation(-8);

Isso é uma rotação de-8 graus, e o sinal negativo indica uma rotação no sentido anti-horário. No loop interno, depois de x, y e z foram calculados, aplicar essa transformação para os valores de z e y, como se fossem x e y valores:

 

D2D1_POINT_2F tiltedPoint =
     tiltMatrix.TransformPoint(Point2F(z, y));
z = tiltedPoint.x;
y = tiltedPoint.y;

Figura 5 mostra o que você verá.


Figura 5 a exibição CircularText inclinado

Isto é muito melhor, mas ainda tem problemas. Coisas horríveis acontecem quando se sobrepõe a geometria, e não há nada que sugira que parte da geometria é mais perto de você e que está mais longe. Olhar para ele, e pode haver alguma mudança de perspectiva.

Ainda assim, a capacidade de aplicar transformações 3D para este objeto sugere que também pode ser fácil girar o objeto em torno do eixo Y — e é. Se você imagine ver a origem do eixo Y positivo, você verá que os eixos X e Z são orientados da mesma maneira que os eixos X e Y em um sistema de coordenadas 2D.

O projeto SpinningCircularText implementa duas transformações de rotação para girar o texto e incliná-lo. Toda a lógica computacional anteriormente no CreateWindowSizeDependentResources foi movida para o método Update. Os pontos 3D são girados duas vezes: uma vez em torno do eixo X com base no tempo decorrido e em seguida, em torno do eixo Y baseia o usuário varrendo um dedo acima e abaixo da tela. Este método de atualização é mostrado na Figura 6.

Figura 6 o método Update do SpinningCircularText

void SpinningCircularTextRenderer::Update(DX::StepTimer const& timer)
{
  // Get window size and geometry size
  Windows::Foundation::Size logicalSize = m_deviceResources->GetLogicalSize();
  float geometryWidth = m_geometryBounds.right - m_geometryBounds.left;
  float geometryHeight = m_geometryBounds.bottom - m_geometryBounds.top;
  // Calculate a few factors for converting 2D to 3D
  float radius = logicalSize.Width / 2 - 50;
  float circumference = 2 * 3.14159f * radius;
  float scale = circumference / geometryWidth;
  float height = scale * geometryHeight;
  // Calculate rotation matrix
  float rotateAngle = -360 * float(fmod(timer.GetTotalSeconds(), 10)) / 10;
  Matrix3x2F rotateMatrix = Matrix3x2F::Rotation(rotateAngle);
  // Calculate tilt matrix
  Matrix3x2F tiltMatrix = Matrix3x2F::Rotation(m_tiltAngle);
  for (size_t polygonIndex = 0; polygonIndex < m_srcPolygons.size(); polygonIndex++)
  {
    const Polygon& srcPolygon = m_srcPolygons.at(polygonIndex);
    Polygon& dstPolygon = m_dstPolygons.at(polygonIndex);
    for (size_t pointIndex = 0; pointIndex < srcPolygon.Points.size(); pointIndex++)
    {
      const D2D1_POINT_2F pt = srcPolygon.Points.at(pointIndex);
      float radians = 2 * 3.14159f * (pt.x - m_geometryBounds.left) / geometryWidth;
      float x = radius * sin(radians);
      float z = radius * cos(radians);
      float y = height * ((pt.y - m_geometryBounds.top) / geometryHeight - 0.5f);
      // Apply rotation to X and Z
      D2D1_POINT_2F rotatedPoint = rotateMatrix.TransformPoint(Point2F(x, z));
      x = rotatedPoint.x;
      z = rotatedPoint.y;
      // Apply tilt to Y and Z
      D2D1_POINT_2F tiltedPoint = tiltMatrix.TransformPoint(Point2F(y, z));
      y = tiltedPoint.x;
      z = tiltedPoint.y;
      dstPolygon.Points.at(pointIndex) = Point2F(x, y);
    }
  }
  // Create path geometry from Polygon collection
  DX::ThrowIfFailed(
    Polygon::CreateGeometry(m_deviceResources->GetD2DFactory(),
    m_dstPolygons,
    &m_pathGeometry)
    );
    // Update FPS display text
    uint32 fps = timer.GetFramesPerSecond();
    m_text = (fps > 0) ?
std::to_wstring(fps) + L" FPS" : L" - FPS";
}

É sabido que transformações de compósitos de matriz são equivalentes a matriz multiplicações e porque multiplicações de matriz não não comutativas, nem são compostas de transformações. Tente trocar em torno da aplicação da inclinação e girar transformações para um efeito diferente (que na verdade talvez você prefira).

Ao criar o programa de SpinningCircularText, adaptei-a classe de SampleFpsTextRenderer criada pelo modelo de Visual Studio para criar a classe SpinningCircularTextRenderer, mas deixei a exibição da taxa de processamento. Isso permite que você veja como está o desempenho. Na minha Surface Pro, eu vejo um frames por segunda figura (FPS) de 25 em modo de depuração, o que indica que o código não é mantendo-se com a taxa de atualização da tela do vídeo.

Se você não gosta que o desempenho, temo que eu tenho más notícias: Vou torná-lo ainda pior.

Separando o primeiro plano de fundo

O maior problema com a abordagem de geometria de caminho para o 3D é o efeito de áreas sobrepostas. É possível evitar as sobreposições? Este programa está a tentar desenhar a imagem não é tão complexa. A qualquer momento, há uma vista frontal da parte do texto e uma vista traseira do resto do texto, e a vista frontal deve ser sempre exibida no topo a vista traseira. Se fosse possível separar a geometria do percurso em duas geometrias de caminho — uma para o fundo e outra para o primeiro plano — você poderia fazer essas geometrias de caminho com chamadas de FillGeometry separadas para o primeiro plano seria em cima de plano de fundo. Essas geometrias de dois caminho até poderiam ser processadas com pincéis diferentes.

Considere a geometria original do caminho criada pelo método GetGlyphRunOutline. Isso é só uma geometria plana 2D caminho ocupando uma área retangular. Eventualmente, metade do que geometria é exibida em primeiro plano, e a outra metade é exibida em segundo plano. Mas quando que os objetos do polígono são obtidos, é tarde demais para fazer essa divisão com nada igual facilidade computacional.

Em vez disso, a geometria original do caminho precisa ser quebrada no meio, antes que os objetos do polígono são obtidos. Este intervalo é dependente do ângulo de rotação, o que significa que muito mais lógica deve ser movida para o método de atualização.

A geometria original do caminho pode ser dividida ao meio com duas chamadas ao método CombineWithGeometry. Este método combina duas geometrias de várias maneiras para fazer uma terceira geometria. As duas geometrias que podem ser combinadas são a geometria original do caminho que descreve os contornos de texto e uma geometria de retângulo que define um subconjunto da geometria do caminho. Esse subconjunto aparece em primeiro plano ou no fundo, dependendo do ângulo de rotação.

Por exemplo, se o ângulo de rotação for 0, a geometria retangular deve cobrir a parte central da geometria do caminho dos contornos de texto. Esta é a parte da geometria original que aparece em primeiro plano. Chamar CombineWithGeometry com o modo de D2D1_COMBINE_MODE_INTERSECT retorna uma geometria de caminho, consistindo apenas em zona central, enquanto que chamar CombineWithGeometry com D2D1_COMBINE_MODE_EXCLUDE Obtém uma geometria de caminho do restante — as partes à esquerda e à direita. Essas geometrias de dois caminho podem ser então convertidas a objetos de polígono separadamente para manipulação das coordenadas, seguido de uma conversão de volta para geometrias de caminho separado para renderização.

Essa lógica é parte de um projeto chamado OccludedCircularText, que implementa o método Render, preenchendo as duas geometrias com pincéis diferentes, conforme mostrado no Figura 7.


Figura 7 o Display OccludedCircularText

Agora é muito mais óbvio o que está em primeiro plano e o que está em segundo plano. Ainda, tanto cálculo foi movido para o método de atualização que o desempenho é muito pobre.

Em um ambiente de programação 2D convencional, gostaria ter esgotado todas as ferramentas de programação 2D à minha disposição e agora ser preso com este desempenho terrível. Direct2D, no entanto, oferece uma abordagem alternativa para a geometria que simplifica a lógica e imensamente melhora o desempenho de renderização. Esta solução faz uso da mais básica 2D polígono — que é um polígono que também desempenha um papel importante na programação 3D.

Falo, claro, do triângulo humilde.

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

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Jim Galasyn (Microsoft)