Skip to content

Instantly share code, notes, and snippets.

@unitycoder
Forked from adammyhre/HeatmapCompute.compute
Created April 13, 2025 18:14
Show Gist options
  • Save unitycoder/30243b2da6c787029e47402ae6ebd420 to your computer and use it in GitHub Desktop.
Save unitycoder/30243b2da6c787029e47402ae6ebd420 to your computer and use it in GitHub Desktop.

Revisions

  1. @adammyhre adammyhre revised this gist Apr 13, 2025. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion HeatmapVisualizer.cs
    Original file line number Diff line number Diff line change
    @@ -18,7 +18,6 @@ void Update() {
    material.SetTexture("_MainTex", texture);
    }

    Image heatmapImage = GetComponent<Image>();
    if (heatmapImage && texture != null) {
    Texture2D texture2D = new Texture2D(texture.rt.width, texture.rt.height, TextureFormat.RFloat, false);

  2. @adammyhre adammyhre revised this gist Apr 13, 2025. 1 changed file with 16 additions and 0 deletions.
    16 changes: 16 additions & 0 deletions HeatmapVisualizer.cs
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,13 @@
    using UnityEngine;
    using UnityEngine.UI;

    public class HeatmapVisualizer : MonoBehaviour {
    public Material material;
    Image heatmapImage;

    void Start() {
    heatmapImage = GetComponent<Image>();
    }

    void Update() {
    var feature = HeatmapRendererFeature.Instance;
    @@ -11,5 +17,15 @@ void Update() {
    if (texture != null) {
    material.SetTexture("_MainTex", texture);
    }

    Image heatmapImage = GetComponent<Image>();
    if (heatmapImage && texture != null) {
    Texture2D texture2D = new Texture2D(texture.rt.width, texture.rt.height, TextureFormat.RFloat, false);

    RenderTexture.active = texture;
    texture2D.ReadPixels(new Rect(0, 0, texture.rt.width, texture.rt.height), 0, 0);
    texture2D.Apply();
    heatmapImage.sprite = Sprite.Create(texture2D, new Rect(0, 0, texture.rt.width, texture.rt.height), new Vector2(0.5f, 0.5f));
    }
    }
    }
  3. @adammyhre adammyhre created this gist Apr 12, 2025.
    25 changes: 25 additions & 0 deletions HeatmapCompute.compute
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,25 @@
    #pragma kernel CSMain

    RWTexture2D<float> heatmapTexture;
    float2 texSize;

    StructuredBuffer<float2> enemyPositions;
    int enemyCount;

    [numthreads(8, 8, 1)]
    void CSMain(uint3 id : SV_DispatchThreadID)
    {
    int2 pixel = int2(id.xy);
    float2 uv = pixel;
    float heat = 0;

    for (int i = 0; i < enemyCount; i++) {
    float2 enemyPos = enemyPositions[i];
    float dist = distance(uv, enemyPos);
    float radius = 20.0;
    heat += saturate(1.0 - dist / radius); // linear falloff
    }

    heat = saturate(heat);
    heatmapTexture[pixel] = heat;
    }
    118 changes: 118 additions & 0 deletions HeatmapRendererFeature.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,118 @@
    using UnityEngine;
    using UnityEngine.Rendering;
    using UnityEngine.Rendering.RenderGraphModule;
    using UnityEngine.Rendering.Universal;
    using UnityEngine.Experimental.Rendering;

    public class HeatmapRendererFeature : ScriptableRendererFeature {
    public static HeatmapRendererFeature Instance { get; private set; }

    class HeatmapPass : ScriptableRenderPass {
    ComputeShader computeShader;
    int kernel;

    GraphicsBuffer enemyBuffer;
    Vector2[] enemyPositions;
    int enemyCount = 64;

    RTHandle heatmapHandle;
    int width = 256, height = 256;

    public RTHandle Heatmap => heatmapHandle;

    public void Setup(ComputeShader cs) {
    computeShader = cs;
    kernel = cs.FindKernel("CSMain");

    if (heatmapHandle == null || heatmapHandle.rt.width != width || heatmapHandle.rt.height != height) {
    heatmapHandle?.Release();
    var desc = new RenderTextureDescriptor(width, height, GraphicsFormat.R32_SFloat, 0) {
    enableRandomWrite = true,
    msaaSamples = 1,
    sRGB = false,
    useMipMap = false
    };
    heatmapHandle = RTHandles.Alloc(desc, name: "_HeatmapRT");
    }

    if (enemyBuffer == null || enemyBuffer.count != enemyCount) {
    enemyBuffer?.Release();
    enemyBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, enemyCount, sizeof(float) * 2);
    enemyPositions = new Vector2[enemyCount];
    }
    }

    class PassData {
    public ComputeShader compute;
    public int kernel;
    public TextureHandle output;
    public Vector2 texSize;
    public BufferHandle enemyHandle;
    public int enemyCount;
    }

    public override void RecordRenderGraph(RenderGraph graph, ContextContainer context) {
    for (int i = 0; i < 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;
    enemyPositions[i] = new Vector2(x, y);
    }

    enemyBuffer.SetData(enemyPositions);

    TextureHandle texHandle = graph.ImportTexture(heatmapHandle);
    BufferHandle enemyHandle = graph.ImportBuffer(enemyBuffer);

    using IComputeRenderGraphBuilder builder = graph.AddComputePass("HeatmapPass", out PassData data);
    data.compute = computeShader;
    data.kernel = kernel;
    data.output = texHandle;
    data.enemyHandle = enemyHandle;
    data.enemyCount = enemyCount;

    builder.UseTexture(texHandle, AccessFlags.Write);
    builder.UseBuffer(enemyHandle, AccessFlags.Read);

    builder.SetRenderFunc((PassData d, ComputeGraphContext ctx) => {
    ctx.cmd.SetComputeIntParam(d.compute, "enemyCount", d.enemyCount);
    ctx.cmd.SetComputeBufferParam(d.compute, d.kernel, "enemyPositions", d.enemyHandle);
    ctx.cmd.SetComputeTextureParam(d.compute, d.kernel, "heatmapTexture", d.output);
    ctx.cmd.DispatchCompute(d.compute, d.kernel, Mathf.CeilToInt(width / 8f), Mathf.CeilToInt(height / 8f), 1);
    });
    }


    public void Cleanup() {
    heatmapHandle?.Release();
    heatmapHandle = null;

    enemyBuffer?.Release();
    enemyBuffer = null;
    }
    }

    [SerializeField] ComputeShader computeShader;
    HeatmapPass pass;

    public override void Create() {
    pass = new HeatmapPass {
    renderPassEvent = RenderPassEvent.BeforeRendering
    };
    Instance = this;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
    if (!SystemInfo.supportsComputeShaders || computeShader == null)
    return;

    pass.Setup(computeShader);
    renderer.EnqueuePass(pass);
    }

    protected override void Dispose(bool disposing) {
    pass?.Cleanup();
    }

    public RTHandle GetHeatmapTexture() => pass?.Heatmap;
    }
    15 changes: 15 additions & 0 deletions HeatmapVisualizer.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    using UnityEngine;

    public class HeatmapVisualizer : MonoBehaviour {
    public Material material;

    void Update() {
    var feature = HeatmapRendererFeature.Instance;
    if (feature == null) return;

    var texture = feature.GetHeatmapTexture();
    if (texture != null) {
    material.SetTexture("_MainTex", texture);
    }
    }
    }