Render Target

Render Target es una textura dinámica que puedes modificar en tiempo de ejecución. En concreto, es una textura obtenida del resultado de algún render.

El render más común es el de la propia escena, ¡pero no el único! Puedes usar cualquier material (shader) para escribir sobre la textura render target.

Para ilustrar el uso de Render Target en UE4 vamos a escribir una textura flowmap en tiempo de ejecución.

¡Vamos a ello!

Preparación del proyecto

Crea un nuevo proyecto. Con una nueva escena (level) vacía.

Añade una esfera. Será sobre la esfera que dibujaremos el flowmap. Puedes usar cualquier otra geometría (cubo, cono o cualquier otra).

En concreto yo he usado la esfera ArcadeEditorSphere de Engine Content / EditorMeshes.

Crea un material M_Flowmap y aplícalo sobre la esfera anterior.

Yo he usado texturas de lava, pero puedes usar las textura water_d y water_n que trae el propio motor.

No olvides usar la opción Show Engine Contents para poder acceder a dichas texturas y a la esfera desde el Content Browser.

Añade un componente RotationComponent sobre la esfera para que gire en yaw.

Añade algunas luces y un fondo y ¡voilá! escena lista.

Pawn

Vamos a crear nuestro Pawn principal.

La clase Pawn forma parte del GameFramework. Aquí tienes un post sobre ello.

Será responsable de:

  1. Colocar una marca sobre la esfera a medida que pasemos el ratón sobre ella.
  2. Mientras mantienes pulsado el botón izquierdo del ratón, pintará el flowmap.
  3. Cuanto más rápido muevas el ratón sobre la zona dónde estás pintando, ¡más veloz irá el flujo!
  4. Cambiará el tamaño del pincel con la ruedecita del ratón.

Vamos a ello. Crea una nueva clase en C++ que herede de Pawn, llámala FlowmapCapture.

Necesitamos un componente cámara, un StaticMeshComponent que usaremos como marca del pincel, un atributo para el tamaño del pincel y métodos para pintar:

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "FlowmapCapture.generated.h"

UCLASS()
class STANGEATTRACTORS_API AFlowmapCapture : public APawn
{
	GENERATED_BODY()
	
public:	
	AFlowmapCapture();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "FlowmapCapture")
	class UCameraComponent* CameraComponent;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "FlowmapCapture")
	class UStaticMeshComponent* BrushMeshComponent;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FlowmapCapture")
	float BrushSize;

	UFUNCTION(BlueprintCallable, Category = "FlowmapCapture")
	void BeginPaint();

	UFUNCTION(BlueprintCallable, Category = "FlowmapCapture")
	void EndPaint();

	UFUNCTION(BlueprintCallable, Category = "FlowmapCapture")
	void ChangeBrushSize(float Val);

	UFUNCTION(BlueprintCallable, Category = "FlowmapCapture")
	void Paint(const FVector2D& UVPos);

	UFUNCTION(BlueprintPure, Category = "FlowmapCapture")
	FORCEINLINE bool IsPainting() const { return bIsPainting; }

	void Tick(float DeltaSeconds) override;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

private:
	bool bIsPainting;
	FVector2D LastUVPos;
	float LastTime;

};
FlowmapCapture.h

Y la implementación:

#include "FlowmapCapture.h"
// aqui añadir los includes correspondientes

#define ECC_FlowmapTraceable ECC_GameTraceChannel1

static const FName PaintActionName = FName(TEXT("Paint"));
static const FName BrushSizeActionName = FName(TEXT("BrushSize"));

// Sets default values
AFlowmapCapture::AFlowmapCapture()
{
	PrimaryActorTick.bCanEverTick = true;
	bIsPainting = false;

	USceneComponent* SceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
	SetRootComponent(SceneComponent);

	CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
	CameraComponent->SetupAttachment(GetRootComponent());

	BrushMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BrushMeshComponent"));
	BrushMeshComponent->SetupAttachment(GetRootComponent());

	AutoPossessPlayer = EAutoReceiveInput::Player0;
}

void AFlowmapCapture::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(BrushSizeActionName, this, &AFlowmapCapture::ChangeBrushSize);

	PlayerInputComponent->BindAction(PaintActionName, IE_Pressed, this, &AFlowmapCapture::BeginPaint);
	PlayerInputComponent->BindAction(PaintActionName, IE_Released, this, &AFlowmapCapture::EndPaint);
}

void AFlowmapCapture::BeginPaint()
{
	bIsPainting = true;
	LastTime = GetWorld()->GetTimeSeconds();	
}

void AFlowmapCapture::Paint(const FVector2D& UVPos)
{
	if (!bIsPainting)
	{
		return;
	}
	
	const float NowTime = GetWorld()->GetTimeSeconds();
	const float DeltaTime = FMath::Max(NowTime - LastTime, 1.0f / 60.0f);
	
	FVector2D Velocity = ((UVPos - LastUVPos) / DeltaTime).ClampAxes(-1.0f, 1.0f);
	
	// Pintar la velocidad en el render target

	LastTime = NowTime;
	LastUVPos = UVPos;
}

void AFlowmapCapture::EndPaint()
{
	bIsPainting = false;
}

void AFlowmapCapture::ChangeBrushSize(float Val)
{
	BrushSize += 0.01f * Val;
	BrushSize = FMath::Clamp(BrushSize, 0.01f, 1.5f);
}

// Called when the game starts or when spawned
void AFlowmapCapture::BeginPlay()
{
	Super::BeginPlay();
	
	if (RenderTarget == NULL)
	{
		UE_LOG(LogTemp, Error, TEXT("Canvas is null"));
		return;
	}

	APlayerController* PC = Cast<APlayerController>(GetController());
	PC->bShowMouseCursor = true;

	// Inicializar el render target con un valor base
}

void AFlowmapCapture::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	FHitResult Hit;

	APlayerController* PC = Cast<APlayerController>(GetController());
	
	FVector2D MouseScreenPos;
	PC->GetMousePosition(MouseScreenPos.X, MouseScreenPos.Y);

	FVector MouseWorldPos, MouseWorldDir;
	UGameplayStatics::DeprojectScreenToWorld(PC, MouseScreenPos, MouseWorldPos, MouseWorldDir);

	const FVector Start = MouseWorldPos;
	const FVector End = Start + 100000.0f * MouseWorldDir;
	
	FCollisionQueryParams QueryParams;
	QueryParams.AddIgnoredActor(this);
	QueryParams.bTraceComplex = true;
	QueryParams.bReturnFaceIndex = true; // required for UV

	BrushMeshComponent->SetWorldScale3D(6.5f * FVector(BrushSize, BrushSize, BrushSize));
	
	bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_FlowmapTraceable, QueryParams);
	if (bHit)
	{
		FVector2D UV;
		bool bFoundUV = UGameplayStatics::FindCollisionUV(Hit, 0, UV);

		Paint(UV);

		BrushMeshComponent->SetWorldLocationAndRotation(Hit.Location, Hit.Normal.Rotation());
	}
}
FlowmapCapture.cpp

Algunas consideraciones:

  1. Si tienes dificultades para seguir el código anterior quizás te sirva el siguiente post acerca de las colisiones.
  2. Usamos UGameplayStatics::DeprojectScreenToWorld para pasar de un sistema de coordenadas en pantalla (es el sistema en el que está expresada la posición del ratón) a coordenadas del mundo en 3D.
  3. Cuando pintemos el RenderTarget pintamos sobre unas coordenadas UV. Usamos UGameplayStatics::FindCollisionUV para convertir la información de una colisión a las coordenadas UV dónde ha impactado.
  4. Usamos un canal de colisión propio, ECC_FlowmapTraceable, para poder filtrar sobre qué objetos vamos colocar la marca y pintar sobre ellos.

Para que el código anterior funcione correctamente necesitamos hacer algunos ajustes al proyecto:

  • En Project Settings > Input añade las entradas correspondientes: Paint como action y BrushSize como axis.
  • También en Project Settings, busca el ajuste Support UV From Hit Collision para que la línea UGameplayStatics::FindCollisionUV funcione correctamente.
  • En Project Settings > Collision añade nuestro nuevo Trace Channel. Yo lo he llamado FlowmapTraceable. Al ser el primero que añades, corresponderá a ECC_GameTraceChannel1 que es así como hemos definido ECC_FlowmapTraceable. Marca para este canal Ignore por defecto. Más info aquí.
  • No olvides, en la escena, seleccionar el actor esfera sobre el que vamos a pintar y en colisiones marcar Block al canal ECC_FlowmapTraceable.

Guarda y compila.

Crea un nuevo Blueprint que herede de la clase FlowmapCapture que acabamos de compilar en C++ y llámalo BP_FlowmapCapture.

Yo he usado como mesh para BrushMestComponent una esfera básica (tomada del propio Engine Content). Y como BrushSize por defecto $0.1$

Añade BP_FlowmapCapture a la escena, colócalo de forma que sea cómodo pintar la esfera.

Si pulsas Play deberías ver la esfera con la textura de agua (o la que hayas usado) y una pequeña esferita actuando como marca que se coloca a medida que pasas el ratón por encima de la esfera.

Prueba a cambiar el material de la marca (la esferita) a uno translucido.

La esferita, actuando como marca, debería cambiar de tamaño a medida que mueves la ruedecita del ratón.

Crear Render Target

Vamos a pintar un RenderTarget.

Primero crea uno en el Content Browser:

Nuevo Render Target

Llámalo RT_Flowmap.

Abre el material M_Flowmap y usa la textura RT_Flowmap como flowmap:

M_Flowmap

¿Por qué multiplicar por $2$ y luego restar $1$ al valor leído en el Flowmap?

El tooltip de la entrada "Flow Vector Map (see tooltip) (V2)" te da una pista:

Tooltip Flowmap Vector Map

Cada pixel del Flowmap codifica una velocidad en 2D. Cada componente de la velocidad puede ir de $-1$ a $1$.

¡Pero en una textura no existe tal cosa como colores negativos! Para ello se hace un remapeo. Cada canal (rojo, verde) de la textura va de $0$ a $1$, para remapearlo a $-1$ - $1$ se usa la fórmula anterior.

Si quieres saber como funciona el nodo FlowMaps aquí tienes un post sobre ello.

Borrar el Render Target

Amplia nuestro Pawn.

Añade la siguiente propiedad pública a FlowmapCapture:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FlowmapCapture")
class UTextureRenderTarget2D* RenderTarget;
Propiedades RenderTarget a Pawn

Es una referencia a un RenderTarget.

Vamos a pintar con color base todo el flowmap.

void AFlowmapCapture::BeginPlay()
{
	Super::BeginPlay();
	
	if (RenderTarget == NULL)
	{
		UE_LOG(LogTemp, Error, TEXT("Canvas is null"));
		return;
	}

	APlayerController* PC = Cast<APlayerController>(GetController());
	PC->bShowMouseCursor = true;

	const FLinearColor ClearColor(0.5f, 0.5f, 0.0f, 1.0f);
	UKismetRenderingLibrary::ClearRenderTarget2D(this, RenderTarget, ClearColor);
}
ClearRenderTarget

¿Por qué el color amarillo (0.5, 0.5)?

Recuerda que estamos hablando que el flowmap es una textura que codifica una velocidad.

¿Qué velocidad codifica (0.5, 0.5)?

Parecería que significa que el flujo iría 0.5 a la derecha y 0.5 hacía arriba, es decir, en diagonal arriba-derecha. Pero nada más lejos de la realidad, recuerda que se hace un remapeo del rango 0 - 1 al rango  (-1) - 1.

El color amarillo (0.5, 0.5) significa, en realidad, la velocidad (0, 0).

Guarda. Compila. En BP_FlowmapCapture no olvides asignar a la propiedad nuestro render target RT_Flowmap.

Si le das a Play verás como el flujo es estático.

Experimenta un poco. Prueba a cambiar el color de borrado por (1.0, 0.5) para que el flujo se mueva a la derecha. ¿Sabrías decir cuál sería moverse hacía arriba? ¿y hacía abajo?.

Recuerda dejarlo en estático en (0.5, 0.5).

Pintar el Render Target

Va siendo hora de pintar el RenderTarget.

Añade la siguiente propiedad pública a FlowmapCapture:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FlowmapCapture")
class UMaterialInterface* BrushMaterialBase;
Materiales para pintar

Esta propiedad es el material que usaremos para pintar el RenderTarget.

Crea un nuevo material. Llámalo M_Brush.

Este material será Unlit y Translucent.

Este material es el que usaremos, a modo de pincel, para pintar el RenderTarget.

Lo primero, la forma. En nuestro caso el pincel tendrá forma de círculo. Lo segundo, ¿qué color pintamos? La velocidad que nos pasen como parámetro.

Tercero, recuerda que la velocidad que nos pasan como parámetro va de -1 a 1 y debemos convertirla a 0 - 1 como color.

M_Brush

Como ves, necesitamos pasarle a nuestro material el parámetro velocidad.

UE4 no permite cambiar dinámicamente los parámetros de un material por temas de rendimiento. Para poder hacerlo, debemos crear un material dinámico a partir de un material base.

Crea una nueva propiedad:

UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "FlowmapCapture")
class UMaterialInstanceDynamic* BrushMaterial;
BrushMaterial

Vamos a instanciar BrushMaterial, material dinámico, a partir de nuestro material base.

void AFlowmapCapture::BeginPlay()
{
	// ...

	BrushMaterial = UMaterialInstanceDynamic::Create(BrushMaterialBase, NULL);
}
Create Dynamic Material

Y ahora, ¡a pintar!

void AFlowmapCapture::Paint(const FVector2D& UVPos)
{
	if (!bIsPainting || RenderTarget == NULL || BrushMaterial == NULL)
	{
		return;
	}
	
	const float NowTime = GetWorld()->GetTimeSeconds();
	const float DeltaTime = FMath::Max(NowTime - LastTime, 1.0f / 60.0f);
	
	FVector2D Velocity = ((UVPos - LastUVPos) / DeltaTime).ClampAxes(-1.0f, 1.0f);
    
    static const FName ParamName = FName(TEXT("Velocity"));
	FLinearColor ParamValue(Velocity.X, -1.0f * Velocity.Y, 0.0f, 0.2f);
	BrushMaterial->SetVectorParameterValue(ParamName, ParamValue);
	
	FDrawToRenderTargetContext Context;
	UCanvas* Canvas;
	FVector2D Size;

	UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(this, RenderTarget, Canvas, Size, Context);

	// Pintar con BrushMaterial
	
	UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(this, Context);

	LastTime = NowTime;
	LastUVPos = UVPos;
}
Paint

Como puedes ver, para pintar un RenderTarget se hace uso de los métodos BeginDrawCanvasToRenderTarget y  EndDrawCanvasToRenderTarget.

La firma de BeginDrawCanvasToRenderTarget es:

static void BeginDrawCanvasToRenderTarget
(
    UObject * WorldContextObject,
    UTextureRenderTarget2D * TextureRenderTarget,
    UCanvas *& Canvas,
    FVector2D & Size,
    FDrawToRenderTargetContext & Context
)
UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget

Dónde los dos primeros parámetros son de entrada, y los tres siguientes de salida.

El último parámetro, Context nos sirve principalmente como identificador que usaremos para, una vez terminado el proceso de pintado, finalizar con EndDrawCanvasToRenderTarget.

El parámetro importante, que estamos buscando, es Canvas.

Puedes pensar en Canvas como una representación en memoria de la textura y, por tanto, podemos hacer operaciones con ella.

Otro modo de pensarlo es que RenderTarget es la textura en modo lectura y Canvas es en modo escritura.

Por último, el parámetro Size indica el tamaño de la textura. Todas las operaciones se hacen en función de este tamaño. Es decir, si queremos pintar en el centro las coordenadas no serán $(0.5, 0.5)$ si no $(0.5 * {BrushSize},  0.5 * {BrushSize})$.

Ya solo nos queda pintar usando Canvas:

// Pintar con BrushMaterial
FVector2D ScreenSize = BrushSize * Size;
FVector2D ScreenPos = UVPos * Size - 0.5f * ScreenSize * FVector2D::UnitVector;
		
Canvas->K2_DrawMaterial(BrushMaterial, ScreenPos, ScreenSize, FVector2D::ZeroVector);
Canvas::DrawMaterial

Con ScreenSize indicamos la porción que vamos a pintar con el material y con ScreenPos la posición.

Siguientes pasos

RenderTarget abre la puerta a multitud de efectos avanzados. En siguientes tutoriales estudiaremos algunos.

Jorge Moreno Aguilera

Jorge Moreno Aguilera