Este artigo foi traduzido por máquina.

Fator DirectX

Triângulos e mosaicos

Charles Petzold

Download do projeto de código

O triângulo é a figura mais básica bidimensional. Não é nada mais do que três pontos conectados por três linhas, e se você tentar para torná-lo mais simples, recolhe em uma única dimensão. Por outro lado, qualquer outro tipo de polígono pode ser decomposto em uma coleção de triângulos.

Mesmo em três dimensões, um triângulo sempre é plano. Na verdade, uma maneira de definir um plano no espaço 3D é com três pontos não-colineares, e que é um triângulo. Um quadrado no espaço 3D não é garantido para ser plana, pois o quarto ponto pode não estar no mesmo plano que os outros três. Mas que o quadrado pode ser dividido em dois triângulos, cada um deles é liso, embora não necessariamente no mesmo avião.

Na programação de gráficos 3D, triângulos formam as superfícies das figuras sólidas, começando com o mais simples dos números tudo tridimensionais, a pirâmide triangular ou tetraedro. Montar uma figura aparentemente sólida de triângulo "blocos de construção" é o processo mais fundamental em gráficos de computador 3D. Claro, as superfícies dos objetos do mundo real, muitas vezes são curvos, mas se você fizer os triângulos pequenos o suficiente, eles podem aproximar superfícies curvadas em grau suficiente para enganar o olho humano.

A ilusão da curvatura é reforçada através da exploração de outra característica dos triângulos: Se os três vértices de um triângulo são associados com três valores diferentes — por exemplo, três cores diferentes ou três vetores geométricos diferentes — estes valores podem ser interpolados sobre a superfície do triângulo e usados para colorir a superfície. Isto é como triângulos são sombreados para imitar o reflexo da luz em objetos do mundo real.

Triângulos em Direct2D

Triângulos são onipresentes em gráficos de computador 3D. Grande parte do trabalho realizado por uma unidade de processamento de gráficos modernos (GPU) envolve triângulos de renderização, assim claro programação Direct3D envolve trabalhar com triângulos definir valores sólidos.

Em contraste, os triângulos não são encontrados em todos os na maioria dos gráficos 2D, interfaces de programação, onde os primitivos bidimensionais mais comuns são linhas, curvas, retângulos e elipses. Então é um pouco surpreendente encontrar triângulos pop-up em um canto bastante obscuro do Direct2D. Ou talvez seja realmente não é tão surpreendente: Porque Direct2D é construída em cima de Direct3D, parece razoável para Direct2D aproveitar o suporte triângulo em Direct3D e a GPU.

A estrutura do triângulo definida em Direct2D é simples:

struct D2D1_TRIANGLE
{
  D2D1_POINT_2F point1;
  D2D1_POINT_2F point2;
  D2D1_POINT_2F point3;
};

Tanto quanto eu possa determinar, essa estrutura é usada em Direct2D somente em conexão com uma "malha" que é uma coleção de triângulos armazenados em um objeto do tipo ID2D1Mesh. O ID2D1RenderTarget (do qual ID2D1DeviceContext deriva) suporta um método chamado CreateMesh que cria um objeto:

ID2D1Mesh * mesh;
deviceContext->CreateMesh(&mesh);

(Para manter as coisas simples, eu não estou mostrando o uso de ComPtr ou verificar o HRESULT valores nestes exemplos de código breve.) A interface ID2D1Mesh define um único método chamado aberto. Esse método retorna um objeto do tipo ID2D1TessellationSink:

ID2D1TessellationSink * tessellationSink;
mesh->Open(&tessellationSink);

Em geral, "mosaico" refere-se ao processo de cobrir uma superfície com um padrão de mosaico, mas o termo é usado de forma um pouco diferente na programação Direct2D e Direct3D. Em Direct2D, mosaico é o processo de decomposição de uma área bidimensional em triângulos.

A interface de ID2D1TessellationSink tem apenas dois métodos: Adicionar­triângulos (que adiciona uma coleção de objetos D2D1_TRIANGLE à coleção) e fechar, o que faz com que o objeto de malha imutáveis.

Embora seu programa pode chamar AddTriangles em si, muitas vezes passará o objeto ID2D1TessellationSink para o método Tessellate definido pela interface ID2D1Geometry:

geometry->Tessellate(IdentityMatrix(), tessellationSink);
tessellationSink->Close();

O método Tessellate gera triângulos que cobrem as zonas delimitadas pela geometria. Depois de chamar o método Close, o coletor pode ser descartado e você é deixado com um objeto ID2D1Mesh. O processo de gerar o conteúdo de um objeto ID2D1Mesh usando um ID2D1TessellationSink é semelhante à definição de um ID2D1Path­geometria usando um ID2D1GeometrySink.

Em seguida, você pode processar este objeto de ID2D1Mesh usando o método de FillMesh de ID2D1RenderTarget. Um pincel governa como a malha é colorida:

deviceContext->FillMesh(mesh, brush);

Tenha em mente que estas malha de triângulos definir uma área e não um esboço de uma área. Não há nenhum método de DrawMesh.

FillMesh tem uma limitação: Suavização de serrilhado, não pode ser habilitada quando FillMesh é chamado. Preceda o FillMesh com uma chamada para SetAntialiasMode:

deviceContext->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);

Você pode se perguntar: O que é o ponto? Porque não chamar FillGeometry no objeto original de geometria? O Visual deve ser o mesmo (com exceção da suavização de serrilhado). Mas há realmente uma diferença profunda entre os objetos ID2D1Geometry e ID2D1Mesh que é revelado por como criar esses dois objetos.

Geometrias são na sua maioria apenas coleções de pontos de coordenadas, portanto geometrias são objetos independentes de dispositivo. Você pode criar vários tipos de geometrias, chamando métodos definidos pelo ID2D1Factory.

Uma malha é um conjunto de triângulos, que são apenas trigêmeos de pontos de coordenadas, por uma malha deve ser um objeto de dispositivo independente também. Mas você cria um objeto ID2D1Mesh chamando um método definido pelo ID2D1RenderTarget. Isto significa que a malha é um objeto dependente do dispositivo, como um pincel.

Este diz-lhe que os triângulos que compõem a malha são armazenados em uma maneira dependente do dispositivo, provavelmente em um formulário adequado para ser processado pela GPU, ou na verdade na GPU. E isso significa que FillMesh deve ser executado muito mais rápido do que FillGeometry para a figura equivalente.

Nós deve testar essa hipótese?

Entre o código de download para este artigo é um programa chamado MeshTest que cria uma geometria de caminho para uma estrela 201-ponto e gira-lo lentamente enquanto calcular e exibir a taxa de quadros. Quando o programa é compilado em modo de depuração para o x86 e é executado no meu Surface Pro, eu recebo uma taxa de quadros de menos de 30 frames por segundo (FPS), quando a geometria de caminho (mesmo se a geometria é delineada para eliminar áreas sobrepostas e achatada para eliminar curvas), mas a taxa de quadros de renderização saltos até 60FPS quando o processamento a malha.

Conclusão: Para geometrias complexas, faz sentido para convertê-los em malhas para renderização. Se a necessidade de desativar a suavização de serrilhado para processar este engranzamento é um deal-breaker, você pode querer verificar para fora ID2D1GeometryRealization, introduzida no Windows 8.1. Isto combina o desempenho de ID2D1Mesh, mas permite a suavização de serrilhado. Tenha em mente malhas e realizações de geometria devem ser recriadas se o dispositivo de exibição é recriado, assim como com outros recursos dependentes de dispositivo, tais como escovas.

Examinando os triângulos

Eu estava curioso sobre os triângulos gerados pelo processo de suavização de serrilhado. Eles na verdade poderiam ser visualizados? O objeto ID2D1Mesh não permite que você acesse os triângulos que compõem a malha, mas é possível escrever sua própria classe que implementa a interface ID2D1TessellationSink e passar uma instância dessa classe para o método Tessellate.

Liguei para minha implementação de ID2D1TessellationSink Interrogable­TessellationSink e acabou por ser embaraçosamente simples. Ele contém um membro de dados particulares para armazenar objetos de triângulo:

std::vector<D2D1_TRIANGLE> m_triangles;

Dedica-se a maior parte do código para implementar a interface IUnknown. Figura 1 mostra o código necessário para implementar os dois métodos de ID2D1TessellationSink e obter os triângulos resultantes.

Figura 1 o código relevante do 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;
}

Eu incorporei essa classe em um projeto chamado mosaico­visualização. O programa cria geometrias de vários tipos — tudo a partir de uma geometria retangular simples para geometrias gerados a partir de glifos de texto — e usa o InterrogableTessellationSink para obter a coleção de triângulos criado pelo método Tessellate. Cada triângulo é então convertido em um objeto ID2D1PathGeometry, constituído por três linhas retas. Essas geometrias de caminho então são processadas usando DrawGeometry.

Como você poderia esperar, um ID2D1RectangleGeometry é incluída no mosaico em apenas dois triângulos, mas as outras geometrias são mais interessantes. Figura 2 mostra os triângulos que compõem uma ID2D1Rounded­RectangleGeometry.


Figura 2 arredondado rectângulo decomposto em triângulos

Isto não é o caminho que um ser humano teria suavização de serrilhado o retângulo arredondado. Um ser humano provavelmente divida o retângulo arredondado em cinco retângulos e quatro trimestre-círculos e suavização de serrilhado separadamente cada um desses valores. Em particular, um ser humano iria cortar os quatro trimestre-círculos em fatias de torta.

Em outras palavras, um ser humano poderia definir vários pontos mais no interior da geometria para ajudar a suavização de serrilhado. Mas o algoritmo de triangulação definido pelo objeto de geometria não usa quaisquer pontos além daqueles criados pelo achatamento da geometria.

Figura 3 mostra dois personagens renderizadas com a fonte Pescadero decompostas em triângulos.


Figura 3 texto decomposto em triângulos

Também fiquei curioso sobre a ordem em que foram gerados estes triângulos e clicando na opção de preenchimento de gradiente no canto inferior esquerdo da janela, você pode descobrir. Quando esta opção estiver marcada, o programa chama FillGeometry para cada uma das geometrias triângulo. Um pincel de cor sólida é passado para FillGeometry, mas a cor depende do índice do triângulo na coleção.

O que você vai encontrar é que a opção FillGeometry processa algo parecido com um pincel de gradiente de cima para baixo, o que significa que os triângulos são armazenados na coleção em uma ordem de cima para baixo a visual. Parece que o algoritmo de mosaico tenta maximizar a largura das linhas de varredura horizontal em triângulos, que provavelmente maximiza o desempenho de processamento.

Embora eu reconheça claramente a sabedoria desta abordagem, confesso que fiquei um pouco decepcionado. Esperava-se que uma curva de Bézier ampliada (por exemplo) pode ser incluída no mosaico começa em uma extremidade da linha e continuando para o outro, então os triângulos podem ser processados com um gradiente de uma ponta à outra, que não é um tipo de gradiente comumente visto em um programa de DirectX! Mas isto não era para ser.

Curiosamente, eu precisava para desativar a suavização de serrilhado antes que o FillGeometry chama em TessellationVisualization ou linhas fracas apareceram entre os triângulos renderizados. Estas linhas fracas resultam do algoritmo de suavização de serrilhado, que envolve a pixels parcialmente transparentes que não se torna opacos quando sobrepostas. Isso me leva a suspeitar que usar suavização com FillMesh não é um hardware ou limitação de software, mas uma restrição mandatado para evitar anomalias visuais.

Triângulos em 2D e 3D

Depois de trabalhar apenas um pouco enquanto com objetos ID2D1Mesh, comecei a visualização de todas as áreas bidimensionais como mosaicos de triângulos. Essa mentalidade é normal quando se faz a programação 3D, mas eu nunca tinha estendido uma visão centrada em triângulo para o mundo 2D.

A documentação do método Tessellate indica que os triângulos gerados são "no sentido horário-ferida", que significa que o point1, point2 e point3 membros da estrutura D2D1_TRIANGLE são ordenados no sentido horário. Isso não é informação muito útil quando usando estes triângulos na programação de gráficos 2D, mas torna-se bastante importante no mundo 3D, onde a ordenação dos pontos de um triângulo normalmente indica a frente ou no verso da figura.

Claro, estou muito interessado em usar estes triângulos mosaico bidimensionais para romper a terceira dimensão, onde os triângulos são mais confortavelmente em casa. Mas não quero estar com tanta pressa que eu negligencie a explorar alguns efeitos interessantes com mosaico triângulos em duas dimensões.

Colorir triângulos com exclusividade

Para mim, a maior emoção na programação de gráficos está criando imagens na tela do computador de um tipo que nunca vi antes, e não acho que eu já vi texto incluída no mosaico em triângulos cujas cores alterar de forma aleatória. Isso acontece em um programa que eu chamo de SparklingText.

Tenha em mente que tanto FillGeometry e FillMesh envolvem apenas um único pincel, então se você precisa processar centenas de triângulos com cores diferentes, você precisará de centenas de chamadas de FillGeometry ou FillMesh, cada uma renderização de um único triângulo. Qual é mais eficiente? Uma chamada de FillGeometry para processar um ID2D1PathGeometry que consiste em três linhas retas? Ou uma chamada FillMesh com um ID2D1Mesh que contém um único triângulo?

Presumi que FillMesh seria mais eficiente do que a FillGeometry somente se a malha continha vários triângulos, e seria mais lento para um triângulo, então eu escrevi originalmente o programa para gerar geometrias de caminho de triângulos em xadrez. Só mais tarde para adicionar uma caixa de seleção rotulada "Usar uma malha para cada triângulo em vez de um PathGeometry" e incorporou essa lógica também.

A estratégia na classe de SparklingTextRenderer de SparklingText é usar o método de GetGlyphRunOutline de ID2D1FontFace para obter uma geometria de caminho para os contornos de caracteres. O programa chama o método Tessellate sobre essa geometria com o InterrogableGeometrySink para obter uma coleção de objetos D2D1_TRIANGLE. Estes são então convertidos em geometrias de caminho ou malhas (dependendo do valor de caixa de seleção) e armazenados em uma das coleções de dois vetores denominados m_triangleGeometries e m_triangleMeshes.

Figura 4 mostra uma parte pertinente do método Tessellate que preenche essas coleções e o Render método que processa os triângulos resultantes. Como de costume, verificação de HRESULT foi removido para simplificar as listagens de código.

Figura 4 mosaico e renderizar o código em 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());
    }
  }
  ...
}

Com base na taxa de frame de vídeo (que exibe o programa), meu Surface Pro processa as malhas mais rápido do que as geometrias de caminho, apesar do fato de que cada malha contém apenas um único triângulo.

A animação das cores é irritantemente reminiscente de uma aura de enxaqueca cintilante, então você pode querer ter alguma cautela ao exibi-lo. Figura 5 mostra uma imagem do programa, que deve ser muito mais seguro.


Figura 5 a exibição de SparklingText

Movendo os triângulos mosaico

Os restantes dois programas usam uma estratégia semelhante ao SparklingText para gerar um conjunto de triângulos em contornos de glifo de forma, mas em seguida, mova os pequenos triângulos em torno da tela.

Para OutThereAndBackAgain, que imaginei o texto que se desfazem em seus triângulos compostos, que voltaria em seguida para formar o texto novamente. Figura 6 mostra esse processo em 3 por cento para a animação de vôo-apart.

O método CreateWindowSizeDependentResources na classe OutThereAndBackAgainRenderer reúne informações sobre cada triângulo em uma estrutura eu chamo de TriangleInfo. Essa estrutura contém um single-triângulo ID2D1Mesh objeto, bem como informações necessárias para assumir um passivo de viagem desse triângulo e volta novamente. Esta viagem aproveita-se de uma característica das geometrias que você pode usar independentemente de renderização. O método de ComputeLength em ID2D1Geometry retorna o comprimento total de uma geometria, enquanto ComputePointAtLength retorna um ponto sobre a curva e uma tangente à curva em qualquer comprimento. Do que você pode derivar a informação traduzir e rodar as matrizes.

Como você pode ver na Figura 6, eu usei um pincel de gradiente para o texto para que os triângulos de cores ligeiramente diferentes que cruzam e se misturam um pouco. Mesmo que eu estou usando apenas uma escova, o efeito desejado requer o Render método para chamar SetTransform e FillMesh para cada malha single-triângulo. O pincel de gradiente é aplicado como se a malha estivesse na sua posição original antes da transformar.


Figura 6 ainda do programa OutThereAndBackAgain

Eu me perguntei se seria eficiente para o Update método para transformar todos os triângulos individuais "manualmente" com chamadas para o método TransformPoint da classe Matrix3x2F e para consolidar estas em um único objeto de ID2D1Mesh, que poderia então ser processado com uma única chamada de FillMesh. Eu adicionei uma opção para isso, e com certeza, foi mais rápido. Eu imaginei que criar um ID2D1Mesh em cada chamada de atualização que funcionam bem, mas ele faz woudn ' t. O Visual é um pouco diferente, no entanto: O pincel de gradiente é aplicado para os triângulos transformados na malha, então não há nenhuma mistura de cores.

Morphing texto?

Suponha que você as geometrias de contorno de glifo de duas seqüências de texto de suavização de serrilhado — por exemplo, as palavras "DirectX" e o "Fator" que compõem o nome desta coluna — e um par dos triângulos para interpolação. Uma animação pode então ser definida que transforma uma palavra para o outro. É exatamente um efeito morphing, mas não sei como chamá-lo.

Figura 7 mostra a midway efeito entre as duas palavras e com um pouco de imaginação você pode fazer quase fora "DirectX" ou "Fator" na imagem.


Figura 7 o Display TextMorphing

Idealmente, cada par de triângulos morphing deve ser espacialmente próximo, mas minimizar as distâncias entre todos os pares de triângulos é parecido com o problema do caixeiro. Eu levei uma abordagem relativamente mais simples, classificando os dois conjuntos de triângulos pelas coordenadas X do centro do triângulo, e em seguida separando as coleções em grupos que representam os intervalos de X coordenadas, e classificar-os pelo Y coordenadas. Claro, as coleções de dois triângulo são de tamanhos diferentes, assim que alguns triângulos na palavra "Fator" correspondem aos dois triângulos na palavra "DirectX".

Figura 8 mostra a lógica de interpolação em atualização e a lógica de renderização no Render.

Figura 8 atualização e Render em 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());
  }
  ...
}

Com isso, eu acho que eu já satisfez minha curiosidade sobre triângulos 2D e estou pronto para dar aqueles triângulos uma terceira dimensão.

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

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