Fator DirectX

Pintura a dedo com geometrias do Direct2D

Charles Petzold

Baixar o código de exemplo

Charles PetzoldComo sistemas operacionais têm evoluído ao longo dos anos, então tem os aplicativos arquetípicos básicos que todo desenvolvedor deve saber como código. Para ambientes de linha de comando antigos, um exercício comum foi um dump hexadecimal — um programa que lista o conteúdo de um arquivo em bytes hexadecimais. Para interfaces gráficas de mouse e teclado, calculadoras e blocos de notas eram populares.

Em um ambiente multi-touch como Windows 8, eu iria nomear dois aplicativos arquetípicos: dispersão de foto e pintar com os dedos. A dispersão da foto é uma boa maneira de aprender a usar dois dedos para escalar e rotacionar objetos visuais, enquanto pintura a dedo envolve acompanhamento individuais dedos para desenhar linhas na tela.

Eu explorei várias abordagens para pintura a dedo Windows 8 no 13º capítulo do meu livro, "Programação Windows, 6ª edição" (Microsoft Press, 2012). Esses programas usado apenas o Runtime do Windows (WinRT) para processar as linhas, mas agora eu gostaria de rever o exercício e usar DirectX em vez disso. Esta será uma boa maneira de se familiarizar com alguns aspectos importantes do DirectX, mas eu suspeito que também eventualmente permitir-nos alguma flexibilidade adicional não está disponível em tempo de execução do Windows.

O modelo de Visual Studio

Como Doug Erickson discutido em seu artigo de março de 2013, "Usando XAML com DirectX e C++ em Windows Apps da loja" (msdn.microsoft.com/­revista/jj991975), existem três maneiras de combinar o XAML e DirectX dentro de um aplicativo Windows Store. Eu vou estar usando a abordagem que envolve um SwapChainBackgroundPanel como o elemento filho de raiz de um derivativo da página XAML. Este objeto serve como uma superfície de desenho para gráficos Direct2D e Direct3D, mas também pode ser coberta com o WinRT controles, tais como barras de aplicação.

Visual Studio 2012 inclui um modelo de projeto para um programa desse tipo. Na caixa de diálogo novo projeto, escolha Visual C++ e Windows Store na esquerda e em seguida o modelo chamado Direct2D App (XAML).  O outro modelo de DirectX é chamado App Direct3D e cria um programa somente DirectX sem qualquer WinRT controles ou elementos gráficos. No entanto, esses dois modelos são um pouco misnamed porque você pode fazer gráficos 2D ou 3D com qualquer um deles.

O modelo de Direct2D App (XAML) cria um aplicativo simples do Windows Store com dividido entre um XAML DirectX e interface do usuário gráfica saída baseada em lógica do programa. A interface do usuário consiste de uma classe chamada DirectXPage que deriva da página (tal como um aplicativo Windows loja normal) e consiste de um arquivo XAML, arquivo de cabeçalho e arquivo de código. Você pode usar DirectXPage para manipulação de entrada usuário, interfaceando com controles WinRT e exibição de texto e elementos gráficos baseados em XAML. O elemento raiz do DirectXPage é o SwapChainBackgroundPanel, que você pode tratar como um elemento Grid regular em XAML e uma superfície de renderização DirectX.

O modelo de projeto também cria uma classe chamada DirectXBase que lida com a maioria de sobrecarga a DirectX e uma classe chamada SimpleTextRenderer que deriva de DirectXBase e realiza a saída de elementos gráficos de DirectX application-specific. O nome SimpleTextRenderer refere-se a o que essa classe dentro do aplicativo criado a partir do modelo de projeto. Você vai querer mudar o nome dessa classe, ou substituí-lo com algo que tem um nome mais apropriado.

Do modelo de aplicativo

Entre o código para download para esta coluna é um projeto de Visual Studio chamado BasicFingerPaint que eu criei usando o modelo de Direct2D (XAML). Renomeei o SimpleTextRenderer para pintura a dedo­Renderer e acrescentou algumas outras classes também.

O modelo de Direct2D (XAML) implica uma arquitetura que separa os XAML e DirectX partes do aplicativo: Todo o código do aplicativo DirectX deve ser restrito a DirectXBase (que você não deve precisar de alterar), a classe de processador que deriva de DirectXBase (no caso FingerPaintRenderer) e quaisquer outras classes ou estruturas, que essas duas classes podem precisar. Apesar do nome, DirectXPage não deve precisar de conter qualquer código de DirectX. Em vez disso, DirectXPage instancia a classe do processador, que ele salva como um membro de dados privados, chamado m_renderer. DirectXPage faz muitas chamadas para a classe de processador (e, indiretamente, de DirectXBase) para exibir a saída gráfica e notificar o DirectX de alterações de tamanho de janela e outros eventos importantes. A classe de processador não liga em DirectXPage.

No arquivo DirectXPage.xaml, acrescentei as caixas de combinação para a barra de aplicativos que permitem que você selecionavam uma cor de desenho e largura de linha, botões para salvar, carregar e limpar desenhos. (O arquivo lógica I/O é extremamente rudimentar e não incluem amenidades, tais como avisá-lo se você está prestes a limpar um desenho que você ainda não salvou).

Como tocar um dedo na tela, movê-lo e levantá-lo, PointerPressed, PointerMoved e PointerReleased eventos são gerados para indicar progresso do dedo. Cada evento é acompanhado por um número de identificação que permite rastrear os dedos individuais e um valor de ponto indicando a atual posição do dedo. Reter e conectar esses pontos, e se tornou um único curso. Processar vários golpes, e tem um desenho completo. Figura 1 mostra um desenho BasicFingerPaint, consistindo de nove traços.

A BasicFingerPaint Drawing
Figura 1 desenho de BasicFingerPaint

O arquivo code-behind DirectXPage, adicionei substitui os métodos de evento do ponteiro. Esses métodos chamam métodos correspondentes no FingerPaintRenderer que eu chamado BeginStroke, ContinueStroke, EndStroke e CancelStroke, como mostrado na Figura 2.

Figura 2 métodos de evento ponteiro fazendo chamadas para a classe de processador

void DirectXPage::OnPointerPressed(PointerRoutedEventArgs^ args)
{
  NamedColor^ namedColor = 
    dynamic_cast<NamedColor^>(colorComboBox->SelectedItem);
  Color color = 
    namedColor != nullptr ?
namedColor->Color : Colors::Black;
  int width = widthComboBox->SelectedIndex !=
    -1 ?
(int)widthComboBox->SelectedItem : 5;
  m_renderer->BeginStroke(args->Pointer->PointerId,
                          args->GetCurrentPoint(this)->Position,
                          float(width), color);
  CapturePointer(args->Pointer);
}
void DirectXPage::OnPointerMoved(PointerRoutedEventArgs^ args)
{
  IVector<PointerPoint^>^ pointerPoints = 
    args->GetIntermediatePoints(this);
  // Loop backward through intermediate points
  for (int i = pointerPoints->Size - 1; i >= 0; i--)
    m_renderer->ContinueStroke(args->Pointer->PointerId,
                               pointerPoints->GetAt(i)->Position);
}
void DirectXPage::OnPointerReleased(PointerRoutedEventArgs^ args)
{
  m_renderer->EndStroke(args->Pointer->PointerId,
                        args->GetCurrentPoint(this)->Position);
}
void DirectXPage::OnPointerCaptureLost(PointerRoutedEventArgs^ args)
{
  m_renderer->CancelStroke(args->Pointer->PointerId);
}
void DirectXPage::OnKeyDown(KeyRoutedEventArgs^ args)
{
  if (args->Key == VirtualKey::Escape)
      ReleasePointerCaptures();
}

O objeto PointerId é um inteiro exclusivo que diferencia os dedos, mouse e caneta. Os ponto e cor valores passados para esses métodos são tipos básicos de WinRT, mas não são tipos de DirectX. DirectX tem suas próprias estruturas de ponto e cor chamadas D2D1_POINT_2F e D2D1::ColorF. DirectXPage não sabe de nada sobre o DirectX, então a classe FingerPaintRenderer tem a responsabilidade de executar todas as conversões entre os tipos de dados WinRT e tipos de dados do DirectX.

Construção de geometrias de caminho

Em BasicFingerPaint, cada traço é uma coleção de linhas conectadas curtas, construída a partir de uma série de eventos de ponteiro de rastreamento. Normalmente, um pintar aplicativo processará essas linhas em um bitmap, que então podem ser salvos em um arquivo. Eu decidi não fazer isso. Os arquivos que você salvar e carrega de BasicFingerPaint são coleções de traços, que são coleções de pontos.

Como você usa Direct2D para processar esses traços na tela? Se você olhar através de métodos de desenho definidos por ID2D1DeviceContext (que são principalmente os métodos definidos pelo ID2D1RenderTarget), três candidatos saltam para fora: DrawLine, DrawGeometry e FillGeometry.

DrawLine desenha uma linha reta entre dois pontos com uma largura específica, escova e estilo. É razoável processar um stroke com uma série de chamadas DrawLine, mas é provavelmente mais eficiente para consolidar as linhas individuais em uma única polilinha. Para isso, você precisa DrawGeometry.

Em Direct2D, uma geometria é basicamente uma coleção de pontos que definem as linhas retas, curvas de Bezier e arcos. Não há nenhum conceito de largura de linha, cor ou estilo em uma geometria. Embora Direct2D suporta diversos tipos de geometrias simples (retângulo, retângulo arredondado, elipse), o mais versátil geometria é representada pelo objeto ID2D1PathGeometry.

Uma geometria de caminho consiste em um ou mais "figuras". Cada figura é uma série de linhas conectadas e curvas. Os componentes individuais da figura são conhecidos como "segmentos". Uma figura pode ser fechada — isto é, o último ponto pode se conectar com o primeiro ponto — mas não precisa ser.

Para processar uma geometria, você chama DrawGeometry no contexto de dispositivo com uma largura de linha especial, pincel e estilo. O FillGeometry método preenche o interior de áreas fechadas da geometria com um pincel.

Para encapsular um derrame, FingerPaintRenderer define uma estrutura particular chamada StrokeInfo, como mostrado na Figura 3.

Figura 3 do processador StrokeInfo estrutura e duas coleções

struct StrokeInfo
{
  StrokeInfo() : Color(0, 0, 0),
                 Geometry(nullptr)
  {
  };
  std::vector<D2D1_POINT_2F> Points;
  Microsoft::WRL::ComPtr<ID2D1PathGeometry> Geometry;
  float Width;
  D2D1::ColorF Color;
};
std::vector<StrokeInfo> completedStrokes;
std::map<unsigned int, StrokeInfo> strokesInProgress;

Figura 3 também mostra duas coleções usadas para salvar objetos de StrokeInfo: A coleção completedStrokes é uma coleção de vetor, enquanto strokesInProgress é uma coleção de mapa usando o ponteiro ID como chave.

O membro de pontos da estrutura StrokeInfo acumula todos os pontos que compõem um stroke. Partir destes pontos, pode ser construído um objeto ID2D1PathGeometry. Figura 4 mostra o método que executa este trabalho. (Para maior clareza, a listagem não mostra o código que verifica se há valores HRESULT errantes).

Figura 4 Criando uma geometria de caminho de pontos

ComPtr<ID2D1PathGeometry>
  FingerPaintRenderer::CreatePolylinePathGeometry
    (std::vector<D2D1_POINT_2F> points)
{
  // Create the PathGeometry
  ComPtr<ID2D1PathGeometry> pathGeometry;
  HRESULT hresult = 
    m_d2dFactory->CreatePathGeometry(&pathGeometry);
  // Get the GeometrySink of the PathGeometry
  ComPtr<ID2D1GeometrySink> geometrySink;
  hresult = pathGeometry->Open(&geometrySink);
  // Begin figure, add lines, end figure, and close
  geometrySink->BeginFigure(points.at(0), D2D1_FIGURE_BEGIN_HOLLOW);
  geometrySink->AddLines(points.data() + 1, points.size() - 1);
  geometrySink->EndFigure(D2D1_FIGURE_END_OPEN);
  hresult = geometrySink->Close();
  return pathGeometry;
}

Um objeto ID2D1PathGeometry é uma coleção de figuras e segmentos. Para definir o conteúdo de uma geometria de caminho, você deve primeiro chamar aberto sobre o objeto para obter um ID2D1GeometrySink. Este coletor de geometria, você chamar BeginFigure e EndFigure para delimitar cada figura e entre essas chamadas, AddLines, AddArc, AddBezier e outros para adicionar segmentos para aquela figura. (As geometrias de caminho criadas por FingerPaintRenderer tem apenas uma única figura contendo vários segmentos de linha reta). Depois de chamar Close no coletor de geometria, a geometria do percurso está pronta para usar, mas tornou-se imutável. Não é possível reabri-lo ou mudar alguma coisa nele.

Por esta razão, como seus dedos se movem através da tela e o programa acumula pontos e mostra de cursos em andamento, novas geometrias de caminho devem ser continuamente construídos e velhos os abandonou.

Quando estas novas geometrias de caminho devem ser criadas? Tenha em mente que um aplicativo pode receber eventos de PointerMoved mais rápido do que a taxa de atualização de vídeo, então não faz sentido criar a geometria de caminho no manipulador de PointerMoved. Em vez disso, o programa lida com este evento, só salvando o novo ponto, mas não se é uma duplicata do ponto anterior (que às vezes acontece).

Figura 5 mostra os três principais métodos em FingerPaintRenderer envolvidos no acúmulo de pontos que compõem um stroke. Um novo StrokeInfo é adicionado à coleção strokeInProgress durante a BeginStroke; tem atualizado durante a ContinueStroke e transferido para a coleção de completedStrokes em EndStroke.

Figura 5 acumulando traços em FingerPaintRenderer

void FingerPaintRenderer::BeginStroke(unsigned int id, Point point,
                                      float width, Color color)
{
  // Save stroke information in StrokeInfo structure
  StrokeInfo strokeInfo;
  strokeInfo.Points.push_back(Point2F(point.X, point.Y));
  strokeInfo.Color = ColorF(color.R / 255.0f, color.G / 255.0f,
                            color.B / 255.0f, color.A / 255.0f);
  strokeInfo.Width = width;
  // Store in map with ID number
  strokesInProgress.insert(std::pair<unsigned int, 
    StrokeInfo>(id, strokeInfo));
  this->IsRenderNeeded = true;
}
void FingerPaintRenderer::ContinueStroke(unsigned int id, Point point)
{
  // Never started a stroke, so skip
  if (strokesInProgress.count(id) == 0)
      return;
  // Get the StrokeInfo object for this finger
  StrokeInfo strokeInfo = strokesInProgress.at(id);
  D2D1_POINT_2F previousPoint = strokeInfo.Points.back();
  // Skip duplicate points
  if (point.X != previousPoint.x || point.Y != previousPoint.y)
  {
    strokeInfo.Points.push_back(Point2F(point.X, point.Y));
    strokeInfo.Geometry = nullptr;          // Because now invalid
    strokesInProgress[id] = strokeInfo;
    this->IsRenderNeeded = true;
  }
}
void FingerPaintRenderer::EndStroke(unsigned int id, Point point)
{
  if (strokesInProgress.count(id) == 0)
      return;
  // Get the StrokeInfo object for this finger
  StrokeInfo strokeInfo = strokesInProgress.at(id);
  // Add the final point and create final PathGeometry
  strokeInfo.Points.push_back(Point2F(point.X, point.Y));
  strokeInfo.Geometry = CreatePolylinePathGeometry(strokeInfo.Points);
  // Remove from map, save in vector
  strokesInProgress.erase(id);
  completedStrokes.push_back(strokeInfo);
  this->IsRenderNeeded = true;
}

Observe que cada um desses métodos define IsRenderNeeded como true, indicando que a tela precisa ser redesenhada. Isto representa uma das mudanças estruturais que eu tive que fazer para o projeto. Em um projeto recentemente criado baseado no modelo Direct2D (XAML), ambos DirectXPage e SimpleTextRenderer declaram um membro de dados privados booleano chamado m_renderNeeded. No entanto, somente em DirectXPage é o membro de dados utilizado na verdade. Isso não é exatamente como deveria ser: Muitas vezes o código de renderização precisa determinar quando a tela deve ser redesenhada. Troquei os dois membros de dados de m_renderNeeded com uma única propriedade pública em FingerPaintRenderer chamado IsRender­necessário. O IsRenderNeeded pode ser definida de DirectXPage e FingerPaintRenderer, mas ele é usado somente pelo DirectXPage.

O Loop de processamento

No caso geral, um programa do DirectX pode redesenhar sua tela inteira com a taxa de atualização de vídeo, que é muitas vezes a 60 quadros por segundo, ou por aí. Esta facilidade oferece a flexibilidade máxima de programa em exibindo gráficos envolvendo animação ou transparência. Ao invés de descobrir que parte da tela precisa ser atualizado e como evitar estragar gráficos existentes, a tela inteira é simplesmente redesenhada.

Em um programa como o BasicFingerPaint, a tela só precisa ser redesenhado quando algo muda, que é indicado por uma configuração true da propriedade IsRenderNeeded. Além disso, redesenho pode concebivelmente ser limitado a determinadas áreas da tela, mas isso não é tão fácil com um aplicativo criado com base no modelo Direct2D (XAML).

Para atualizar a tela, o DirectXPage usa a composição à mão­Target::Rendering o evento, que é acionado em sincronização com a atualização do hardware de vídeo. Em um programa do DirectX, o manipulador para este evento é às vezes conhecido como o loop de processamento e é mostrado na Figura 6.

Figura 6 o Loop de processamento em DirectXPage

void DirectXPage::OnRendering(Object^ sender, Object^ args)
{
  if (m_renderer->IsRenderNeeded)
  {
    m_timer->Update();
    m_renderer->Update(m_timer->Total, m_timer->Delta);
    m_renderer->Render();
    m_renderer->Present();
    m_renderer->IsRenderNeeded = false;
  }
}

O método de atualização é definido pelo renderizador. Isto é onde os objetos visuais são preparados para renderização, particularmente se eles exigem informações de tempo fornecidas por um timer classe criado pelo modelo de projeto. FingerPaintRenderer usa o método Update para criar geometrias de caminho do ponto de coleções, se necessário. O Render método é declarado pelo DirectXBase mas definido por FingerPaintRenderer e é responsável pelo processamento de todos os gráficos. O método chamado presente — é um verbo, não substantivo — é definido por DirectXBase e transfere os visuais compostos para o hardware de vídeo.

O método Render começa chamando BeginDraw ID3D11DeviceContext objeto do programa e conclui chamando EndDraw. Nesse meio tempo, ele pode chamar funções de desenho. O processamento de cada traço durante o Render método é simplesmente:

m_solidColorBrush->SetColor(strokeInfo.Color);
m_d2dContext->DrawGeometry(strokeInfo.Geometry.Get(),
                           m_solidColorBrush.Get(),
                           strokeInfo.Width,
                           m_strokeStyle.Get());

Os objetos m_solidColorBrush e m_strokeStyle são membros de dados.

Qual é o próximo passo?

Como o nome implica, o BasicFingerPaint é uma aplicação muito simples. Porque ele não processar traçados para um bitmap, um pintor de dedo ansioso e persistente pode causar o programa gerar e processar milhares de geometrias. Em algum momento, poderá sofrer atualização da tela.

No entanto, porque o programa mantém discretas geometrias, ao invés de misturar tudo junto em um bitmap, o programa poderia permitir individual traçados para mais tarde eliminado ou editado, talvez mudando a cor ou largura ou mesmo mudou-se para um local diferente na tela.

Porque cada curso é uma geometria de caminho único, aplicar estilos diferentes é bastante fácil. Por exemplo, tente alterar uma linha no criar­DeviceIndependentResources método no FingerPaintRenderer:

strokeStyleProps.dashStyle = D2D1_DASH_STYLE_DOT;

Agora o programa desenha linhas pontilhadas, ao invés de linhas sólidas, com o resultado mostrado no Figura 7. Esta técnica só funciona porque cada curso é uma geometria única; Não funcionaria se os segmentos individuais, compreendendo os traços foram todas linhas separadas.

Rendering a Path Geometry with a Dotted Line
Figura 7-renderização de uma geometria de caminho com uma linha pontilhada

Outro possível reforço é um pincel de gradiente. O programa GradientFingerPaint é muito semelhante ao BasicFingerPaint, exceto que tem duas caixas de combinação de cores e usa um pincel linear de gradiente para processar a geometria do percurso. O resultado é mostrado na Figura 8.

The GradientFingerPaint Program
Figura 8 o programa de GradientFingerPaint

Embora cada curso tem seu próprio pincel linear de gradiente, o ponto inicial do gradiente é sempre definido para o canto superior esquerdo dos limites do curso e o ponto final para o canto inferior direito. Como desenhar um stroke com um dedo, muitas vezes vê o gradiente mudando como o traço fica mais tempo. Mas dependendo de como o curso é puxado, às vezes, o gradiente vai ao longo do comprimento do traço, e às vezes você quase não vê um gradiente em tudo, como é óbvio, com os dois traços do X em Figura 8.

Não seria melhor se você poderia definir um gradiente que se estendia ao longo de toda a extensão do traçado, independentemente da forma do traço ou orientação? Ou que tal um gradiente, que é sempre perpendicular ao traço, independentemente de como o acidente vascular cerebral voltas e mais voltas?

Como eles pedem nos filmes de ficção científica: Como é que tal coisa é possível?

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 pela revisão deste artigo: James McNellis (Microsoft)
James McNellis é um aficionado de C++ e um desenvolvedor de software na equipe do Visual C++ da Microsoft, onde ele onde ele constrói bibliotecas C++ e mantém as bibliotecas C Runtime (CRT).  Ele tweets no @JamesMcNellise pode ser encontrada em outro lugar on-line via http://jamesmcnellis.com/.