FeaturedCanvasDecals

en Tutoriales, UE4

Texturas Dinámicas: Calcomanías

En este tutorial vamos a añadir una nueva herramienta a nuestro anterior lienzo de dibujo que nos servirá para imprimir calcomanías. Es un buen ejemplo para explicar como cargar imágenes y extraer la información de sus píxeles usando C++.

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

Sección C++

Vamos a empezar añadiendo dos nuevas funciones nuestra clase, una se utilizará para inicializar la herramienta, y la otra para pintar sobre el lienzo, tendremos que añadir también las estructuras de datos que almacenarán la información de los píxeles de la calcomanía.

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

	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void InitializeDrawingTools(const int32 brushRadius);
	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void DrawDot(const int32 pixelCoordX, const int32 pixelCoordY);

	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void InitializeDecal(const UTexture2D* decalTexture);
	UFUNCTION(BlueprintCallable, Category = DrawingTools)
		void DrawDecal(const int32 pixelCoordX, const int32 pixelCoordY);

	UDrawingCanvas();
	~UDrawingCanvas();

private:

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

	// draw brush tool
	std::unique_ptr<uint8[]> canvasBrushMask;
	int radius;
	int brushBufferSize;

	// decal tool
	std::unique_ptr<uint8[]> canvasDecalImage;
	int decalWidth;
	int decalHeight;
	int decalBufferSize;

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

DrawingCanvas.cpp

Para inicializar esta herramienta tenemos que extraer la información de cada uno de los canales de color de cada pixel y guardarlo en la estructura de nuestra calcomanía. Si la textura esta siendo usada el el hilo de renderizado tenemos que proteger su acceso, utilizando un lock, para asegurarnos un uso exclusivo, de lo contrario podemos obtener desenlaces poco deseados como corrupción de memoria o incluso cierres inexperados.

void UDrawingCanvas::InitializeDecal(const UTexture2D* decalTexture)
{
	//Access to the mipmap 0 of the texture to get the raw data image
	FTexture2DMipMap* MyMipMap = &decalTexture->PlatformData->Mips[0];
	FByteBulkData* RawImageData = &MyMipMap->BulkData;

	//lock image
	uint8* FormatedImageData = static_cast<uint8*>(RawImageData->Lock(LOCK_READ_ONLY));

	//initialize our data structure variables
	decalWidth = decalTexture->GetSurfaceWidth();
	decalHeight = decalTexture->GetSurfaceHeight();

	decalBufferSize = decalWidth * decalHeight * bytesPerPixel;//4
	canvasDecalImage = std::unique_ptr<uint8[]>(new uint8[decalBufferSize]);

	uint8* decalPixelPtr = canvasDecalImage.get();

	//copy channel color values
	memcpy(decalPixelPtr, FormatedImageData, decalBufferSize);

	//unlock image
	RawImageData->Unlock();
}

Como estamos guardando los datos de los canales en el mismo orden que la estructura de origen podemos copiar los datos directamente utilizando una llamada a memcpy, otra forma de acceder a estos valores, o si solo queremos acceder a un valor de cierto canal es utilizar un acceso indexado, como este:

for (int numPixel = 0; numPixel < decalBufferSize; numPixel += 4)
{
	decalPixelPtr[numPixel] = FormatedImageData[numPixel];	//blue
	decalPixelPtr[numPixel + 1] = FormatedImageData[numPixel + 1];	//green
	decalPixelPtr[numPixel + 2] = FormatedImageData[numPixel + 2];	//red
	decalPixelPtr[numPixel + 3] = FormatedImageData[numPixel + 3];	//alpha
}

Para pintar la calcomanía podemos reutilizar el mismo código de la herramienta de pincel, solo tenemos que cambiar la estructura con la máscara de la herramienta por la de la calcomanía y utilizar las dimensiones de la imagen de la calcomanía en vez del parámetro que definía el radio del pincel.

void UDrawingCanvas::DrawDecal(const int32 pixelCoordX, const int32 pixelCoordY)
{
	uint8* canvasPixelPtr = canvasPixelData.get();
	const uint8* canvasDecalPixelPtr = canvasDecalImage.get();
	for (int px = -(decalWidth / 2); px < (decalWidth / 2); ++px)
	{
		for (int py = -(decalHeight / 2); py < (decalHeight / 2); ++py)
		{
			int32 tbx = px + decalWidth / 2;
			int32 tby = py + decalHeight / 2;
			canvasDecalPixelPtr = canvasDecalImage.get() + (tbx + tby * decalWidth) * bytesPerPixel;
			if (*(canvasDecalPixelPtr + 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, *(canvasDecalPixelPtr + 2), *(canvasDecalPixelPtr + 1), *(canvasDecalPixelPtr), *(canvasDecalPixelPtr + 3));
				}
			}
		}
	}

	UpdateCanvas();
}

Sección Blueprint

Para este ejemplo vamos a utilizar la textura de Pingvin, por lo que vamos a importarla al projecto.

pinvin_texture

Para inicializar la herramienta solo tendremos que añadir el nodo InitializeDecal a nuestro flujo de inicialización y establecer la anterior textura como su parámetro de entrada.

init_decal

Para digujar la calcomanía sobre en lienzo se pueden utilizar los mismos parámetros de entrada que en el nodo drawDot pero reemplazando este con el nodo DrawDecal de nuestra clase lienzo.

draw_decal_node

Y eso es todo!

canvas_decal_gif

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