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.
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.
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.
Y eso es todo!
Tutorial files
Te puede interesar:
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!