A partir do Unity 2018, a interface AsyncGPUReadback foi introduzida, permitindo operações assíncronas para transferir dados de RenderTexture para a CPU ou para obter dados de um ComputeBuffer. Essa funcionalidade é crucial para cenários onde a performance é crítica, como a leitura de resultados de ComputeShader sem bloquear o thread principal, resultando em uma experiência de jogo mais fluida e responsiva.
1. Transferindo de RenderTexture para Texture2D
A leitura de uma RenderTexture para uma Texture2D é uma operação comum, mas pode ser dispendiosa se feita de forma síncrona. O AsyncGPUReadback oferece uma alternativa não bloqueante.
Abordagem Assíncrona:
Utilizando o mecanismo assíncrono, a solicitação de leitura é enviada para a GPU e o sistema aguarda a conclusão da operação sem congelar a execução do jogo.
using System.Collections;
using UnityEngine;
using UnityEngine.Rendering;
public class AsyncTextureReadback : MonoBehaviour
{
public int textureResolution = 256; // Smaller resolution for example
IEnumerator Start()
{
// 1. Criar uma RenderTexture de origem e renderizar algo para ela.
RenderTexture sourceRT = new RenderTexture(textureResolution, textureResolution, 0);
Texture2D tempSourceTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
Color[] fillColors = new Color[textureResolution * textureResolution];
for (int i = 0; i < fillColors.Length; i++)
{
fillColors[i] = new Color(
(float)(i % textureResolution) / textureResolution,
(float)(i / textureResolution) / textureResolution,
0.5f, 1.0f);
}
tempSourceTexture.SetPixels(fillColors);
tempSourceTexture.Apply();
Graphics.Blit(tempSourceTexture, sourceRT); // Renderiza a textura para a RenderTexture
// 2. Solicitar a leitura assíncrona da GPU.
AsyncGPUReadbackRequest readbackRequest = AsyncGPUReadback.Request(sourceRT);
// 3. Aguardar até que a solicitação seja concluída.
yield return new WaitUntil(() => readbackRequest.done);
// 4. Verificar se a solicitação foi bem-sucedida.
if (readbackRequest.hasError)
{
Debug.LogError("Erro durante a leitura assíncrona da GPU!");
yield break;
}
// 5. Criar uma Texture2D para armazenar os dados e copiar os pixels.
Texture2D destinationTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
Color32[] pixelData = readbackRequest.GetData<Color32>().ToArray();
destinationTexture.SetPixels32(pixelData);
destinationTexture.Apply();
// Limpeza
sourceRT.Release();
Destroy(tempSourceTexture);
Debug.Log("Leitura assíncrona da RenderTexture concluída. Textura criada!");
// Em um cenário real, você atribuiria destinationTexture a um material/UI.
}
}
Abordagem Síncrona (para comparação):
Esta é a maneira tradicional, que bloqueia o thread principal do CPU até que a GPU conclua a leitura. Pode causar gargalos visíveis, especialmente com texturas grandes.
using UnityEngine;
public class SyncTextureReadback : MonoBehaviour
{
public int textureResolution = 256;
void Start()
{
RenderTexture sourceRT = new RenderTexture(textureResolution, textureResolution, 0);
// Preencher a RenderTexture com algum conteúdo (similar ao exemplo assíncrono)
Texture2D tempSourceTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
Color[] fillColors = new Color[textureResolution * textureResolution];
for (int i = 0; i < fillColors.Length; i++)
{
fillColors[i] = new Color(
(float)(i % textureResolution) / textureResolution,
(float)(i / textureResolution) / textureResolution,
0.5f, 1.0f);
}
tempSourceTexture.SetPixels(fillColors);
tempSourceTexture.Apply();
Graphics.Blit(tempSourceTexture, sourceRT);
RenderTexture.active = sourceRT; // Ativar a RenderTexture para leitura
Texture2D outputTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
outputTexture.ReadPixels(new Rect(0, 0, textureResolution, textureResolution), 0, 0, false);
outputTexture.Apply();
RenderTexture.active = null; // Desativar a RenderTexture
sourceRT.Release();
Destroy(tempSourceTexture);
Debug.Log("Leitura síncrona da RenderTexture concluída. Textura criada!");
}
}
2. Lendo Dados de ComputeBuffer com AsyncGPUReadback
O AsyncGPUReadback é particularmente útil para recuperar resultados de cálculos realizados em ComputeShaders de forma eficiente.
Shader de Exemplo (Processamento de Dados):
Este ComputeShader simplesmente multiplica a posição de cada elemento por um fator e adiciona um offset.
// MyComputeProcessor.compute
#pragma kernel ProcessData
struct DataElement
{
float3 position;
};
RWStructuredBuffer<DataElement> ProcessedDataBuffer;
[numthreads(8,1,1)]
void ProcessData (uint3 id : SV_DispatchThreadID)
{
DataElement currentElement = ProcessedDataBuffer[id.x];
currentElement.position = currentElement.position * 1.5f + float3(id.x * 0.1f, 0.0f, 0.0f);
ProcessedDataBuffer[id.x] = currentElement;
}
Abordagem Assíncrona:
Aqui, o ComputeShader é despachado e a solicitação de leitura do ComputeBuffer é feita imediatamente. O script principal aguarda a conclusão da leitura assincronamente.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class ComputeBufferAsyncReader : MonoBehaviour
{
public struct DataElement
{
public Vector3 position;
}
public ComputeShader computeProgram;
public int dataCount = 64; // Número de elementos a processar
private ComputeBuffer gpuBuffer;
private int kernelIndex;
IEnumerator Start()
{
kernelIndex = computeProgram.FindKernel("ProcessData");
List<DataElement> initialElements = new List<DataElement>();
for (int i = 0; i < dataCount; i++)
{
initialElements.Add(new DataElement() { position = new Vector3(i * 1.0f, 0, 0) });
}
// Tamanho da struct em bytes: Vector3 = 3 floats * 4 bytes/float = 12 bytes
gpuBuffer = new ComputeBuffer(dataCount, 12);
gpuBuffer.SetData(initialElements);
computeProgram.SetBuffer(kernelIndex, "ProcessedDataBuffer", gpuBuffer);
computeProgram.Dispatch(kernelIndex, dataCount / 8, 1, 1); // 8 threads por grupo, dataCount/8 grupos
// Registrar o frame atual para observar o atraso
Debug.LogFormat("Início da solicitação assíncrona no frame: {0}", Time.frameCount);
// Enviar a solicitação de leitura assíncrona para o ComputeBuffer
AsyncGPUReadbackRequest bufferReadRequest = AsyncGPUReadback.Request(gpuBuffer);
// Aguardar a conclusão da solicitação
yield return new WaitUntil(() => bufferReadRequest.done);
Debug.LogFormat("Leitura assíncrona concluída no frame: {0}", Time.frameCount);
if (bufferReadRequest.hasError)
{
Debug.LogError("Erro ao ler dados do ComputeBuffer.");
yield break;
}
// Obter os dados processados
DataElement[] processedResults = bufferReadRequest.GetData<DataElement>().ToArray();
for (int i = 0; i < processedResults.Length; i++)
{
Debug.LogFormat("Elemento {0}: Posição = {1}", i, processedResults[i].position);
}
// Limpeza
gpuBuffer.Release();
}
void OnDestroy()
{
if (gpuBuffer != null)
{
gpuBuffer.Release();
gpuBuffer = null;
}
}
}
Abordagem Síncrona (para comparação):
Neste caso, a chamada GetData no ComputeBuffer bloqueia o thread principal, esperando que a GPU termine de processar e transferir os dados.
using System.Collections.Generic;
using UnityEngine;
public class ComputeBufferSyncReader : MonoBehaviour
{
public struct DataElement
{
public Vector3 position;
}
public ComputeShader computeProgram;
public int dataCount = 64;
private ComputeBuffer gpuBuffer;
private int kernelIndex;
void Start()
{
kernelIndex = computeProgram.FindKernel("ProcessData");
List<DataElement> initialElements = new List<DataElement>();
for (int i = 0; i < dataCount; i++)
{
initialElements.Add(new DataElement() { position = new Vector3(i * 1.0f, 0, 0) });
}
gpuBuffer = new ComputeBuffer(dataCount, 12);
gpuBuffer.SetData(initialElements);
computeProgram.SetBuffer(kernelIndex, "ProcessedDataBuffer", gpuBuffer);
computeProgram.Dispatch(kernelIndex, dataCount / 8, 1, 1);
// A chamada GetData() é bloqueante.
Debug.LogFormat("Início da leitura síncrona no frame: {0}", Time.frameCount);
DataElement[] resultsArray = new DataElement[dataCount];
gpuBuffer.GetData(resultsArray); // Bloqueia o thread principal aqui!
Debug.LogFormat("Leitura síncrona concluída no frame: {0}", Time.frameCount);
for (int i = 0; i < resultsArray.Length; i++)
{
Debug.LogFormat("Elemento {0}: Posição = {1}", i, resultsArray[i].position);
}
// Limpeza
gpuBuffer.Release();
}
void OnDestroy()
{
if (gpuBuffer != null)
{
gpuBuffer.Release();
gpuBuffer = null;
}
}
}