Files
2026-07-16 21:49:59 +01:00

234 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
using Vector2 = UnityEngine.Vector2;
// This RendererFeature demonstrates how to integrate a Compute Shader with RenderGraph.
// In this example, the output of the Compute Shader is used to modify the CameraColor texture.
// Additionally, once the CameraColor texture is updated, it is used as input for another Compute Shader pass.
// This sample is based on this video https://www.youtube.com/watch?v=v_WkGKn601M by Git-Amend, who is part of
// the Unity Insider Program (https://unity.com/unity-insiders). In the original sample the output image of the
// compute shader is applied to a RenderTexture instead of to the CameraColor texture.
public class ComputeShaderScreenInOutRenderFeature : ScriptableRendererFeature
{
class HeatmapPass : ScriptableRenderPass
{
// Compute Shader programs.
ComputeShader m_HeatmapComputeShader;
ComputeShader m_HeatmapBrightnessComputeShader;
// Kernel of each computeShader shader.
int m_KernelHeatMapComputeShader;
int m_KernelHeatmapBrightnessComputeShader;
// Heatmap computeShader shader (uses a computeShader shader to simulate a group of enemies moving around).
BufferHandle m_EnemyBuffer;
Vector2[] m_EnemyPositions;
const int k_EnemyCount = 64;
// Texture Handles intended for later use by the render graph.
TextureHandle m_HeatmapTextureHandle;
TextureHandle m_HeatmapBrightnessTextureHandle;
public void Setup(ComputeShader heatmapCS, ComputeShader heatmapBrightnessCS)
{
// Both computeShader shaders are defined here.
// The first computeShader shader generates an output that is stored in the CameraColor.
// The second computeShader shader then takes this CameraColor as its input, processes it further,
// and produces the final result.
m_HeatmapComputeShader = heatmapCS;
m_HeatmapBrightnessComputeShader = heatmapBrightnessCS;
m_KernelHeatMapComputeShader = heatmapCS.FindKernel("CSMain");
m_KernelHeatmapBrightnessComputeShader = heatmapBrightnessCS.FindKernel("CSMain");
// The enemy positions are initialized
m_EnemyPositions = new Vector2[k_EnemyCount];
}
// Compute Pass Data.
// This will be used for both Compute Shaders.
class ComputePassData
{
public ComputeShader computeShader;
public int kernel;
public int enemyCount;
public Vector2[] positions;//This allows us to use the position inside the pass.
public BufferHandle enemyHandle;
public TextureHandle input;
public TextureHandle output;
public int width;
public int height;
}
void UpdateEnemyPositions(int width, int height)
{
for (int i = 0; i < k_EnemyCount; i++) {
float t = Time.time * 0.5f + i * 0.1f;
float x = Mathf.PerlinNoise(t, i * 1.31f) * width;
float y = Mathf.PerlinNoise(i * 0.91f, t) * height;
m_EnemyPositions[i] = new Vector2(x, y);
}
}
// This is the core of the RenderGraph system, where the computeShader passes are executed every frame.
// The purpose of the computeShader pass can be summarized in three steps:
// 1- Update enemy positions using Perlin noise, then upload them to a GPU buffer.
// 2- Set up two computeShader passes in the render graph: the first generates a heatmap texture
// based on enemy positions, while the second further processes the resulting texture
// adding a bit of brightness with another computeShader shader.
// 3- Assign the resulting texture from one computeShader pass to the next, and finally to the camera's color buffer for rendering.
public override void RecordRenderGraph(RenderGraph graph, ContextContainer context)
{
// Retrieving the Universal Resource Data, which contains all texture resources,
// such as the active color texture, depth texture, and more.
var resourceData = context.Get<UniversalResourceData>();
// Getting the dimensions from the camera Color.
var width = resourceData.cameraColor.GetDescriptor(graph).width;
var height = resourceData.cameraColor.GetDescriptor(graph).height;
// Update the enemy positions.
UpdateEnemyPositions(width, height);
// Creating a texture descriptor based on the activeColorTexture's descriptor values.
// This texture descriptor will be used by both texture handlers:
// m_HeatmapTextureHandle and m_HeatmapBrightnessTextureHandle.
var heatmapDesc = resourceData.activeColorTexture.GetDescriptor(graph);
// Defining some attributes of the descriptor
heatmapDesc.name = "HeatmapHandle";
heatmapDesc.enableRandomWrite = true; // Use this to write to the texture efficiently
// with a compute shader, enabling random tile
// access instead of sequential tile writing.
heatmapDesc.msaaSamples = MSAASamples.None;
// Creating the texture for the m_HeatmapTextureHandle texture handle
// based on the camera color descriptor.
m_HeatmapTextureHandle = graph.CreateTexture(heatmapDesc);
// Reusing the previously created heatmapDesc, but this time changing only the name.
heatmapDesc.name = "BrightnessHeatmapHandle";
// Creating the texture for the m_HeatmapBrightnessTextureHandle texture handle based on the camera color descriptor.
m_HeatmapBrightnessTextureHandle = graph.CreateTexture(heatmapDesc);
// Creating the buffer
var bufferDesc = new BufferDesc()
{
name = "EnemyBuffer",
stride = sizeof(float) * 2,
count = k_EnemyCount,
target = GraphicsBuffer.Target.Structured
};
// Now adding it to the RenderGraph.
m_EnemyBuffer = graph.CreateBuffer(bufferDesc);
// This is the definition of the computeShader render pass,
// where the data to be processed by the computeShader shader pass is assigned.
using (var builder = graph.AddComputePass<ComputePassData>("ComputeHeatmapPass", out var passData))
{
// Assign data to the computeShader shader data
passData.computeShader = m_HeatmapComputeShader;
passData.kernel = m_KernelHeatMapComputeShader;
passData.output = m_HeatmapTextureHandle;
passData.enemyHandle = m_EnemyBuffer;
passData.enemyCount = k_EnemyCount;
passData.positions = m_EnemyPositions;
passData.width = width;
passData.height = height;
// Declare resource usage within this pass using the builder.
builder.UseTexture(passData.output, AccessFlags.Write);
builder.UseBuffer(passData.enemyHandle, AccessFlags.Read);
// Set the function to execute the computeShader pass (using static to improve the performance).
builder.SetRenderFunc(static(ComputePassData data, ComputeGraphContext ctx) =>
{
// The SetBufferData use a command buffer to send the enemy position data
// from the passData.enemyHandle to the passData.positions.
ctx.cmd.SetBufferData(data.enemyHandle, data.positions); // Use data.enemyPositions
// to ensure it remains scoped to the render function.
ctx.cmd.SetComputeIntParam(data.computeShader, "k_EnemyCount", data.enemyCount);
ctx.cmd.SetComputeBufferParam(data.computeShader, data.kernel, "m_EnemyPositions", data.enemyHandle);
ctx.cmd.SetComputeTextureParam(data.computeShader, data.kernel, "heatmapTexture", data.output);
ctx.cmd.DispatchCompute(data.computeShader, data.kernel, Mathf.CeilToInt(data.width / 8f), Mathf.CeilToInt(data.height / 8f), 1);
});
}
// Here if you set resourceData.cameraColor = m_HeatmapTextureHandle and comment out the second pass, you
// will get the result of the compute pass directly instead of reusing it in a second pass.
// This is the second computeShader render pass.
// In this pass, the input is the current `m_HeatmapTextureHandle`,
// and the output, after being processed by the brightness computeShader shader,
// will be stored in `m_HeatmapBrightnessTextureHandle`.
using (var builder = graph.AddComputePass<ComputePassData>("ComputeCameraColorFromHeatmapPass", out var passData))
{
// Assign data to the computeShader shader data.
passData.computeShader = m_HeatmapBrightnessComputeShader;
passData.kernel = m_KernelHeatmapBrightnessComputeShader;
passData.input = m_HeatmapTextureHandle;
passData.output = m_HeatmapBrightnessTextureHandle;
passData.width = width;
passData.height = height;
// Declare resource usage within this pass using the builder.
builder.UseTexture(passData.input, AccessFlags.Read);
builder.UseTexture(passData.output, AccessFlags.Write);
// Set the function to execute the computeShader pass.
builder.SetRenderFunc(static(ComputePassData data, ComputeGraphContext ctx) =>
{
ctx.cmd.SetComputeTextureParam(data.computeShader, data.kernel, "heatmapTexture", data.input);
ctx.cmd.SetComputeTextureParam(data.computeShader, data.kernel, "result", data.output);
ctx.cmd.DispatchCompute(data.computeShader, data.kernel, Mathf.CeilToInt(data.width / 8f), Mathf.CeilToInt(data.height / 8f), 1);
});
}
// The resulted texture of the last computeShader pass is assigned to the current Camera Color.
resourceData.cameraColor = m_HeatmapBrightnessTextureHandle;
}
}
// The inspector fields of the Renderer Feature.
[SerializeField] ComputeShader HeatmapComputeShader;
[SerializeField] ComputeShader HeatmapBrightnessComputeShader;
// The HeatmapPass instance.
HeatmapPass heatmapPass;
public override void Create()
{
heatmapPass = new HeatmapPass
{
renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing
};
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (HeatmapComputeShader == null || HeatmapBrightnessComputeShader == null)
{
Debug.Log("Set both shaders for the ComputeShaderRendererFeature.");
return;
}
if (!SystemInfo.supportsComputeShaders)
{
Debug.Log(
"The ComputeShaderRendererFeature cannot be added because this system doesn't support compute shaders.");
}
if (renderingData.cameraData.cameraType == CameraType.Game)
{
heatmapPass.Setup(HeatmapComputeShader, HeatmapBrightnessComputeShader);
renderer.EnqueuePass(heatmapPass);
}
}
}