El factor DirectX

Pintar con los dedos en geometrías Direct2D

Charles Petzold

Descargar el ejemplo de código

Como sistemas operativos han evolucionado con los años, así que tienen las aplicaciones arquetípicas básicas que cada desarrollador debe saber cómo código.Para entornos de línea de comandos viejos, un ejercicio común era un volcado hexadecimal — un programa que muestra el contenido de un archivo en bytes hexadecimales.Para interfaces gráficas de ratón y teclado, calculadoras y libretas eran populares.

En un ambiente multi-touch como Windows 8, iba a nominar a dos aplicaciones arquetípicas: Foto de dispersión y pintar con los dedos.La dispersión de la foto es una buena manera de aprender cómo utilizar dos dedos para escalar y rotar objetos visuales, mientras que consiste en pintar con los dedos los dedos individuales para dibujar líneas en la pantalla de seguimiento.

Exploré distintas aproximaciones a la pintura de dedo de Windows 8 en el capítulo 13 de mi libro, "Programación Windows, 6ª edición" (Microsoft Press, 2012).Esos programas utilizan sólo el Runtime de Windows (WinRT) para la representación de las líneas, pero ahora me gustaría retomar el ejercicio y usar DirectX en su lugar.Esta será una buena manera de familiarizarse con algunos aspectos importantes de DirectX, pero sospecho que también eventualmente nos permitirá cierta flexibilidad adicional no está disponible en el tiempo de ejecución de Windows.

La plantilla de Visual Studio

Como Doug Erickson discutida en su artículo de marzo de 2013, "Usando XAML con DirectX y C++ en tienda de aplicaciones de Windows" (msdn.microsoft.com/­revista/jj991975), hay tres maneras de combinar XAML y DirectX dentro de una aplicación Windows Store.Va a utilizar el enfoque que involucra a un SwapChainBackgroundPanel como el elemento raíz de niño de un derivado de la página XAML.Este objeto sirve como una superficie de dibujo para gráficos Direct2D y Direct3D, pero también puede superponerse con controles WinRT, tales como barras de aplicación.

Visual Studio 2012 incluye una plantilla de proyecto de un programa.En el cuadro de diálogo nuevo proyecto, seleccione Visual C++ y Windows Store en la izquierda y luego la plantilla llamada Direct2D App (XAML).  La otra plantilla de DirectX se llama App Direct3D y crea un programa sólo DirectX sin controles de WinRT o gráficos.Sin embargo, estas dos plantillas son mal algo llamadas porque puedes hacer gráficos 2D o 3D con uno de ellos.

La plantilla de Direct2D App (XAML) crea una sencilla aplicación Windows Store con la lógica del programa dividido entre una base de XAML UI y DirectX salida de gráficos.La interfaz de usuario consiste en una clase denominada DirectXPage que se deriva de la página (igual que en una aplicación de Windows Store normal) y consta de un archivo XAML, archivo de encabezado y archivo de código.Utilice DirectXPage para usuario de manejo de entrada, con controles de WinRT y visualización de texto y gráficos basados en XAML.El elemento raíz de DirectXPage es el SwapChainBackgroundPanel, que se puede tratar como un elemento de cuadrícula regular en XAML y una superficie de renderizado de DirectX.

La plantilla de proyecto también crea una clase denominada DirectXBase que controla la mayor parte de la sobrecarga de DirectX y una clase denominada SimpleTextRenderer que se deriva de DirectXBase y realiza la salida de gráficos DirectX application-specific.El nombre SimpleTextRenderer se refiere a lo que hace esta clase dentro de la aplicación creada a partir de la plantilla de proyecto.Usted querrá cambiar el nombre de esta clase, o reemplazarlo con algo que tiene un nombre más apropiado.

De la plantilla de aplicación

Entre el código descargable para esta columna es un proyecto de Visual Studio llamado BasicFingerPaint que he creado utilizando la plantilla de Direct2D (XAML).Renombrado SimpleTextRenderer a FingerPaint­Renderer y añadido algunas otras clases también.

La plantilla de Direct2D (XAML) implica una arquitectura que separa las partes XAML y DirectX de la aplicación: Todo el código de la aplicación DirectX debe restringirse a DirectXBase (que no será necesario alterar), la clase de procesador que se deriva de DirectXBase (en este caso FingerPaintRenderer) y cualquier otras clases o estructuras de estas dos clases puede ser que necesite.A pesar de su nombre, DirectXPage no debería contener cualquier código de DirectX.En cambio, DirectXPage crea una instancia de la clase de procesador, que lo guarda como un miembro de datos privado llamado m_renderer.DirectXPage hace muchas llamadas en la clase de procesador (e indirectamente a DirectXBase) para mostrar la salida gráfica y notificar a DirectX de cambios de tamaño de ventana y otros eventos importantes.La clase de procesador no llama a DirectXPage.

En el archivo DirectXPage.xaml he añadido combo boxes a la barra de aplicación que le permiten seleccionar un color plano y ancho de línea y los botones para guardar, cargan y claro dibujos.(El archivo lógica de I/O es extremadamente rudimentario y no incluye los servicios, tales como aviso si vas a borrar un dibujo que no salvaste).

Como tocar un dedo a la pantalla, moverlo y levantarlo, se generan eventos PointerPressed, PointerMoved y PointerReleased para indicar el progreso de los dedos.Cada evento es acompañado por un número de identificación que permite controlar los dedos individuales y un valor de punto que indica la posición actual del dedo.Retener y conectar estos puntos, y has rendido a un solo golpe.Procesar múltiples golpes, y tienes un dibujo completo.Figura 1 muestra un dibujo de BasicFingerPaint que consta de nueve golpes.

A BasicFingerPaint DrawingDibujo de figura 1 BasicFingerPaint

En el archivo de código subyacente DirectXPage, añadí anulaciones de los métodos de evento puntero.Estos métodos llaman métodos correspondientes en FingerPaintRenderer que nombrado BeginStroke, ContinueStroke, EndStroke y CancelStroke, como se muestra en figura 2.

Figura 2 métodos de evento puntero haciendo llamadas a la clase de procesador

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

El objeto PointerId es un entero único que distingue a los dedos, ratón y pluma. Los valores de punto y Color pasados a estos métodos son tipos básicos de WinRT, pero no son tipos de DirectX. DirectX tiene sus propias estructuras de punto y color llamados D2D1_POINT_2F y D2D1::ColorF. DirectXPage no sabe nada acerca de DirectX, la clase FingerPaintRenderer tiene la responsabilidad de realizar todas las conversiones entre los tipos de datos WinRT y tipos de datos de DirectX.

Construcción de geometrías de camino

En BasicFingerPaint, cada trazo es una colección de líneas cortas conectadas, construido a partir de una serie de eventos de puntero de seguimiento. Típicamente, un pintar con los dedos aplicación hará que estas líneas en un mapa de bits que luego se pueden guardar en un archivo. Decidí no hacerlo. Los archivos que salva y carga de BasicFingerPaint son colecciones de trazos, que son ellos mismos colecciones de puntos.

¿Cómo usas Direct2D para representar estos trazos en la pantalla? Si miras a través de los métodos de dibujo definidos por ID2D1DeviceContext (que son en su mayoría los métodos definidos por ID2D1RenderTarget), saltan tres candidatos: DrawLine, DrawGeometry y FillGeometry.

DrawLine dibuja una línea recta entre dos puntos con una determinada anchura, cepillo y estilo. Es razonable hacer un movimiento con una serie de llamadas DrawLine, pero es probablemente más eficiente para consolidar las líneas individuales en una sola polilínea. Para eso, necesitas DrawGeometry.

En Direct2D, una geometría es básicamente una colección de puntos que definen las líneas rectas, curvas Bezier y arcos. No hay ningún concepto de ancho de línea, color o estilo en una geometría. Aunque Direct2D admite varios tipos de geometrías simples (rectángulo, rectángulo redondeado, elipse), el más versátil geometría está representada por el objeto ID2D1PathGeometry.

Una geometría de ruta consta de uno o más "figuras". Cada figura es una serie de curvas y líneas conectadas. Los componentes individuales de la figura se conocen como "segmentos". Una figura puede ser cerrada — es decir, el último punto podría conectar con el primer punto, pero no tiene que ser.

Para representar una geometría, llamaste a DrawGeometry en el contexto de dispositivo con un ancho de línea particular, cepillo y estilo. El método FillGeometry rellena el interior de las áreas cerradas de la geometría con un cepillo.

Para encapsular un accidente cerebrovascular, FingerPaintRenderer define una estructura privada llamada StrokeInfo, como se muestra en la figura 3.

Figura 3 el renderizador StrokeInfo estructura y dos colecciones

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 también muestra dos colecciones utilizadas para guardar objetos de StrokeInfo: La colección de completedStrokes es una colección de vector, mientras que strokesInProgress es una colección de mapa utilizando el identificador del puntero como una llave.

El miembro de puntos de la estructura StrokeInfo acumula todos los puntos que componen un derrame cerebral. Desde estos puntos, puede construirse un objeto ID2D1PathGeometry. Figura 4 muestra el método que realiza este trabajo. (Para mayor claridad, el listado no aparece el código que comprueba errantes valores HRESULT).

Figura 4 creando una geometría de ruta de puntos

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

Un objeto ID2D1PathGeometry es una colección de figuras y segmentos. Para definir el contenido de una geometría de ruta, primero llamas abiertas en el objeto de obtener un ID2D1GeometrySink. En este lavabo de geometría, llame a BeginFigure y EndFigure para delimitar cada figura y entre esas llamadas, AddLines, AddArc, AddBezier y otros segmentos de agregar a esa cifra. (Las geometrías camino creadas por FingerPaintRenderer tienen solamente una sola figura que contiene múltiples segmentos de línea recta). Después de llamar a cerrar en el fregadero de la geometría, la geometría del camino está lista para usar, pero ha vuelto inmutable. No puedes abrirla o cambiar nada en él.

Por esta razón, como mover los dedos a través de la pantalla y el programa acumula puntos y muestra trazos en progreso, nuevas geometrías de ruta deben ser continuamente construido y viejos abandonados.

¿Cuándo se deben crear estas nuevas geometrías de camino? Tenga en cuenta que una aplicación puede recibir eventos PointerMoved más rápido que la tasa de refresco de vídeo, así que no tiene sentido para crear la geometría de ruta en el controlador PointerMoved. En cambio, el programa encarga de este evento por sólo salvar el punto nuevo, pero no se si duplica el punto anterior (que a veces ocurre).

Figura 5 muestra los tres métodos principales en FingerPaintRenderer implicadas en la acumulación de puntos que conforman un derrame cerebral. Un nuevo StrokeInfo se agrega a la colección de strokeInProgress en BeginStroke; se ha actualizado durante ContinueStroke y transferido a la colección de completedStrokes en EndStroke.

Figura 5 acumulando trazos en 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;
}

Tenga en cuenta que cada uno de estos métodos IsRenderNeeded se establece en true, lo que indica que la pantalla tiene que ser rediseñados. Esto representa uno de los cambios estructurales que debía realizar en el proyecto. En un proyecto recién creado basado en la plantilla de Direct2D (XAML), tanto DirectXPage como SimpleTextRenderer declaran a un miembro de datos booleano privado llamado m_renderNeeded. Sin embargo, sólo en DirectXPage es el miembro de datos utilizado en realidad. Esto no es exactamente como debe ser: A menudo el código de representación debe determinar cuando debe volver a dibujar la pantalla. Reemplacé los dos miembros de datos de m_renderNeeded con una sola propiedad pública en FingerPaintRenderer llamado IsRender­necesarios. La propiedad IsRenderNeeded puede establecerse de tanto DirectXPage y FingerPaintRenderer, pero es usado solamente por DirectXPage.

El bucle de renderizado

En el caso general, un programa de DirectX puede repintar su pantalla entera en la tasa de actualización de vídeo, que es a menudo 60 frames por segundo o menos. Esta instalación da la máxima flexibilidad de programa de visualización de gráficos de animación o transparencia. En lugar de averiguar en qué parte de la pantalla debe ser actualizada y cómo evitar arruinar gráficos existentes, simplemente se redibuja la pantalla completa.

En un programa como BasicFingerPaint, la pantalla sólo necesita redibujar cuando algo cambia, que es indicado por un verdadero valor de la propiedad IsRenderNeeded. Además, redibujando podría ser limitada a ciertas áreas de la pantalla, pero esto no es tan fácil con una aplicación creada a partir de la plantilla de Direct2D (XAML).

Para actualizar la pantalla, DirectXPage utiliza la composición handy­Target::Rendering evento, que se cuece en sincronización con la actualización de vídeo de hardware. En un programa de DirectX, el controlador para este evento es conocido como el bucle de la representación y se muestra en la figura 6.

Figura 6 el bucle de renderizado en 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;
  }
}

El método Update se define por el procesador. Es donde se preparan los objetos visuales para la representación, especialmente si requieren información de tiempo proporcionada por una timer, clase creada por la plantilla de proyecto. FingerPaintRenderer utiliza el método de actualización para crear geometrías camino de colecciones de punto, si es necesario. El método Render es declarado por DirectXBase pero definidos por FingerPaintRenderer y es responsable para la representación de todos los gráficos. El método llamado presente — es un verbo, no Sustantivo — es definida por DirectXBase y transfiere las imágenes composited para el hardware de vídeo.

El método Render comienza llamando a BeginDraw en ID3D11DeviceContext objeto del programa y concluye llamando al EndDraw. Entre tanto, puede llamar funciones de dibujo. La representación de cada trazo durante el método Render es simplemente:

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

Los objetos m_solidColorBrush y m_strokeStyle son miembros de datos.

¿Cuál es el próximo paso?

Como su nombre indica, BasicFingerPaint es una aplicación muy sencilla. Porque no render trazos para un mapa de bits, un pintor de dedo impaciente y persistente podría causar el programa generar y procesar miles de geometrías. En algún momento, podría sufrir el refresco de pantalla.

Sin embargo, porque el programa mantiene discretos geometrías en lugar de mezclar todo junto en un mapa de bits, el programa podría permitir individual trazos para posteriormente eliminan o editado, quizás cambiando el color o la anchura o incluso se trasladaron a una ubicación diferente en la pantalla.

Porque cada movimiento es una geometría única ruta, aplicando diferente peinado es bastante fácil. Por ejemplo, trate de cambiar una línea en el crear­método DeviceIndependentResources en FingerPaintRenderer:

strokeStyleProps.dashStyle = D2D1_DASH_STYLE_DOT;

Ahora el programa dibuja líneas punteadas en lugar de líneas sólidas, con el resultado se muestra en la figura 7. Esta técnica funciona sólo porque cada movimiento es una geometría única; No funcionaría si los segmentos individuales que comprende los trazos fueron todas las líneas separadas.

Rendering a Path Geometry with a Dotted Line
Figura 7 representación una geometría de ruta con una línea punteada

Otra posible mejora es un pincel de degradado. El programa GradientFingerPaint es muy similar a BasicFingerPaint, excepto que tiene dos cuadros combinados para el color y usa un pincel de degradado lineal para representar la geometría del camino. El resultado se muestra en la figura 8.

The GradientFingerPaint Program
Figura 8 el programa GradientFingerPaint

Aunque cada movimiento tiene su propio pincel de degradado lineal, el punto inicial del degradado que se siempre se establece en la esquina superior izquierda de los límites del movimiento y el punto final a la esquina inferior derecha. Mientras dibuja un trazo con un dedo, a menudo vemos el gradiente de cambio como el trazo más largo. Pero a veces dependiendo de cómo se dibuja el trazo, el gradiente va a lo largo de la carrera, y a veces apenas ves un gradiente en absoluto, como es obvio con los dos trazos de la X en figura 8.

¿No sería mejor si se puede definir un degradado que se extendía a lo largo de toda la longitud del trazo, independientemente de la forma o la orientación de la carrera? O ¿qué tal un gradiente que siempre es perpendicular al trazo, independientemente de cómo el movimiento giros y vueltas?

Como piden en las películas de ciencia ficción: ¿Cómo es posible tal cosa?

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 por su ayuda en la revisión de este artículo: James McNellis (Microsoft)
James McNellis es un aficionado de C++ y un desarrollador de software en el equipo de Visual C++ de Microsoft, donde él donde construye bibliotecas de C++ y mantiene las bibliotecas de C Runtime (CRT).  Él tweets en @JamesMcNellisy puede encontrarse en otros lugares en línea via http://jamesmcnellis.com/.