drawing_canvas_featured

en Tutoriales, UE4

Texturas Dinámicas: Como crear un lienzo de dibujo

Ya hemos visto como modificar dinámicamente y aplicar efectos a texturas en tutoriales anteriores, pero en ocasiones necesitamos crear esta textura dinámicamente también. Un buen ejemplo de esto es un lienzo de dibujo.

Parte 1: Lienzo de dibujo
Parte 2: Añadiendo calcomanías

Sección C++

Lo primero que vamos a hacer es escribir una clase para guardar la textura dinámica y actualizarla. Necesitamos almacenar los valores de cada uno de los canales de color y también el valor de transparencia o alpha. Por lo tanto cada pixel se representará utilizando 4 celdas de nuestra estructura de datos. Esto lo debemos tener en cuenta cuando iteremos sobre los pixels de nuestro lienzo para realizar los incrementos correctamente.

pixeldata_sctructure

DrawingCanvas.h

#pragma once

#include <memory>
#include "Engine/Texture2D.h"
#include "Object.h"
#include "DrawingCanvas.generated.h"

UCLASS(Blueprintable, BlueprintType)
class TUTORIAL_CANVAS_API UDrawingCanvas : public UObject
{
	GENERATED_BODY()

public:

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Variables)
		UTexture2D* dynamicCanvas;

	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void InitializeCanvas(const int32 pixelsH, const int32 pixelsV);
	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void UpdateCanvas();
	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void ClearCanvas();

	UDrawingCanvas();
	~UDrawingCanvas();

private:

	// canvas
	std::unique_ptr<uint8[]> canvasPixelData;
	int canvasWidth;
	int canvasHeight;
	int bytesPerPixel;
	int bufferPitch;
	int bufferSize;

	std::unique_ptr<FUpdateTextureRegion2D> echoUpdateTextureRegion;
	void setPixelColor(uint8*& pointer, uint8 red, uint8 green, uint8 blue, uint8 alpha);
};

DrawingCanvas.cpp

Para inicializar el lienzo necesitamos crear la textura dinámica (Texture2D) utilizando la función CreateTransient y reservar el espacio para el buffer que contendrá los datos de cada pixel.

void UDrawingCanvas::InitializeCanvas(const int32 pixelsH, const int32 pixelsV)
{
	//dynamic texture initialization
	canvasWidth = pixelsH;
	canvasHeight = pixelsV;

	dynamicCanvas = UTexture2D::CreateTransient(canvasWidth, canvasHeight);
#if WITH_EDITORONLY_DATA
	dynamicCanvas->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
#endif
	dynamicCanvas->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
	dynamicCanvas->SRGB = 1;
	dynamicCanvas->AddToRoot();
	dynamicCanvas->Filter = TextureFilter::TF_Nearest;
	dynamicCanvas->UpdateResource();

	echoUpdateTextureRegion = std::unique_ptr<FUpdateTextureRegion2D>(new FUpdateTextureRegion2D(0, 0, 0, 0, canvasWidth, canvasHeight));	


	// buffers initialization
	bytesPerPixel = 4; // r g b a
	bufferPitch = canvasWidth * bytesPerPixel;
	bufferSize = canvasWidth * canvasHeight * bytesPerPixel;
	canvasPixelData = std::unique_ptr<uint8[]>(new uint8[bufferSize]);

	ClearCanvas();
}

Una función para establecer los valores de los canales del pixel con una sola llamada. Nos fijamos que el orden de los canales dentro de la estructura es diferente del típico RGBA

void UDrawingCanvas::setPixelColor(uint8*& pointer, uint8 red, uint8 green, uint8 blue, uint8 alpha)
{
	*pointer = blue; //b
	*(pointer + 1) = green; //g
	*(pointer + 2) = red; //r
	*(pointer + 3) = alpha; //a
}

Necesitamos mover el puntero a los datos del pixel al siguiente pixel en vez de a la siguiente celda, por lo tanto tenemos que incrementar este puntero en 4 celdas. Por ejemplo la función para limpiar el lienzo podría quedar tal que así:

void UDrawingCanvas::ClearCanvas()
{
	uint8* canvasPixelPtr = canvasPixelData.get();
	for (int i = 0; i < canvasWidth * canvasHeight; ++i)
	{
		setPixelColor(canvasPixelPtr, 255, 255, 255, 255); //white
		canvasPixelPtr += bytesPerPixel;
	}
	UpdateCanvas();
}

Para actualizar la textura podemos usar una llamada a UpdateTextureRegions. Uno de los parámetros de entrada de esta función es nuestra estructura con los datos de cada pixel. Cada vez que modifiquemos esta estructura necesitamos llamar a esta función para hacer los cambios visibles en la textura.

void UDrawingCanvas::UpdateCanvas()
{
	if (echoUpdateTextureRegion)
	{
		dynamicCanvas->UpdateTextureRegions((int32)0, (uint32)1, echoUpdateTextureRegion.get(), (uint32)bufferPitch, (uint32)bytesPerPixel, canvasPixelData.get());
	}
}

La herramienta pincel

También podemos añadir una herramienta básica para pintar sobre el lienzo, una función para inicializar los parámetros del pincel, su máscara de pincel, y una llamada para pintar utilizando este pincel sobre el canvas centrado en las coordenadas del evento táctil.

DrawingCanvas.h

	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void InitializeDrawingTools(const int32 brushRadius);
	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void DrawDot(const int32 pixelCoordX, const int32 pixelCoordY);
private:
	// draw brush tool
	std::unique_ptr<uint8[]> canvasBrushMask;
	int radius;
	int brushBufferSize;

DrawingCanvas.cpp

El buffer del pincel tiene la misma estructura que el buffer del canvas. Utilizaremos 4 celdas para cada pixel. Para este ejemplo utilizaremos un pincel negro por lo que estableceremos un RGB a 000 y un valor de 255 a su alpha. Fuera del límite del pincel solo necesitaremos establecer el valor de alpha a 0.

void UDrawingCanvas::InitializeDrawingTools(const int32 brushRadius)
{
	radius = brushRadius;
	brushBufferSize = radius * radius * 4 * bytesPerPixel; //2r*2r * bpp
	canvasBrushMask = std::unique_ptr<uint8[]>(new uint8[brushBufferSize]);
	uint8* canvasBrushPixelPtr = canvasBrushMask.get();
	for (int px = -radius; px < radius; ++px)
	{
		for (int py = -radius; py < radius; ++py)
		{
			int32 tx = px + radius;
			int32 ty = py + radius;
			canvasBrushPixelPtr = canvasBrushMask.get() + (tx +  + ty * 2 * radius) * bytesPerPixel;
			if (px*px + py*py < radius*radius)
			{
				setPixelColor(canvasBrushPixelPtr, 0, 0, 0, 255); //black alpha 255 - bgra
			}
			else
			{
				setPixelColor(canvasBrushPixelPtr, 0, 0, 0, 0); // alpha 0
			}
		}
	}
}

Para pintar el punto tenemos que centrar la mascara del pincel y aplicar sus valores sobre el lienzo, comprobando el valor del canal alpha podemos saber si necesitamos pintar ese pixel sobre el lienzo o podemos descartarlo. Cada vez que el canvas es modificado necesitamos llamar a la función UpdateCanvas() para actualizar la textura dinámica.

void UDrawingCanvas::DrawDot(const int32 pixelCoordX, const int32 pixelCoordY)
{
	uint8* canvasPixelPtr = canvasPixelData.get();
	const uint8* canvasBrushPixelPtr = canvasBrushMask.get();
	for (int px = -radius; px < radius; ++px)
	{
		for (int py = -radius; py < radius; ++py) 
		{ 
			int32 tbx = px + radius; 
			int32 tby = py + radius; 
			canvasBrushPixelPtr = canvasBrushMask.get() + (tbx + tby * 2* radius) * bytesPerPixel; 
			if (*(canvasBrushPixelPtr + 3) == 255) // check the alpha value of the pixel of the brush mask 
			{ 
				int32 tx = pixelCoordX + px; 
				int32 ty = pixelCoordY + py; 
				if (tx >= 0 && tx < canvasWidth && ty >= 0 && ty < canvasHeight)
				{
					canvasPixelPtr = canvasPixelData.get() + (tx + ty * canvasWidth) * bytesPerPixel;
					setPixelColor(canvasPixelPtr, *(canvasBrushPixelPtr + 2), *(canvasBrushPixelPtr + 1), *(canvasBrushPixelPtr), *(canvasBrushPixelPtr + 3));
				}
			}
		}
	}

	UpdateCanvas();
}

Sección Blueprint

Para ser capaces de utilizar la textura dinámica necesitamos crear un material con un parámetro de entrada de tipo TextureSampleParameter2D..

canvas_material

En la inicializacion del lienzo vamos a pasar la textura dinámica de nuestra clase DrawingCanvas como parámetro de entrada del material anterior utilizando el nodo SetTextureParameterValue

set_material_parameter


Ahora ya podemos crear un blueprint de nuestra clase DrawingCanvas para utilizarla en el proyecto.

drawingclassbp

Vamos a añadir el lienzo de dibujo a un Actor. Este actor tiene un componente Cámara enfocado sobre un Plane Mesh, el material de esta malla ha sido reemplazado por nuestro material de lienzo.

mesh_material

Guardaremos las dimensiones del lienzo, el objeto del lienzo de dibujo, una instancia dinámica del material del Plano y la posición del touch event, para dibujar solo si se ha cambiado de pixel mientras se arrastra la herramienta.

Game_actor_components
Init_drawing_canvas

Echa un vistazo a esta función para inicializar las componentes principales:

Empezemos con el objeto DrawingCanvas, usando el nodo Construct Object from Class para crear la variable y llamando después a las funciones definidas en la clase para establecer las dimensiones y el radio del pincel.

El siguiente paso es inicializar la instancia que contendrá la textura dinámica, el Material.

El ultimo paso es establecer las dimensiones del Plane Mesh y modificar los parámetros de la cámara en relación a estos.

Un plano tiene una dimensiones de 100 x 100 píxeles por lo que si queremos un lienzo de 1000 x 1000 píxeles con una relación 1:1 (1 pixel de Plano : 1 pixel de lienzo) tenemos que escalar el plano por 1000 / 100 = 10. 

Estableciendo la cámara con Proyección Ortográfica y un valor de ancho igual al ancho del plano llenará la pantalla con el lienzo.

camera_params

El evento BeginPlay solo necesita asignar el View Target del Player Controller para activar la cámara y después llamar a la función de inicialización anterior.

actor_event_play

Para gestionar el evento táctil utilizaremos el nodo InputTouch. Mientras el ratón está presionado necesitamos calcular sus coordenadas y transformarlas a coordenadas del lienzo. Si el ratón se arrastra sobre un nuevo pixel podemos pintar un punto utilizando la llamada a la herramienta de pincel del DrawingCanvas centrada en el nuevo pixel.

get_touch_coords
input_touch_event

Para poder utilizar el touch event con un ratón necesitamos activar su emulación en Project Settings > Input > Use Mouse for Touch

enable_mouse_touch

drawingCanvas_Test

Tutorial files

Ayudanos con este blog!

El último año he estado dedicando cada vez más tiempo a la creación de tutoriales, en su mayoria sobre desarrollo de videojuegos. Si crees que estos posts te han ayudado de alguna manera o incluso inspirado, por favor considera ayudarnos a mantener este blog con alguna de estas opciones. Gracias por hacerlo posible!

Escribe un comentario

Comentario