Skip to content

Instantly share code, notes, and snippets.

@NicolasCaous
Created May 5, 2024 15:44
Show Gist options
  • Save NicolasCaous/5b7621b5f65a6e71b63b4bf6c07ea870 to your computer and use it in GitHub Desktop.
Save NicolasCaous/5b7621b5f65a6e71b63b4bf6c07ea870 to your computer and use it in GitHub Desktop.
#ifndef SHADERGRAPHHAX
#define SHADERGRAPHHAX
StructuredBuffer<float3> _Positions;
float3 _Scale;
float3 _PositionOffset;
sampler2D _Terrain;
void DoHax_float(float vertex_id, out float3 position)
{
position = _Positions[round(vertex_id)];
position.y = tex2Dlod(_Terrain, float4(position.xz, 0, 0)).x;
position *= _Scale;
position += _PositionOffset;
}
#endif
#include "UnityIndirect.cginc"
#pragma kernel ComputeSizes
#pragma kernel ComputeVertices
#pragma kernel ComputeTriangles
#pragma kernel FirstStep
#pragma kernel SecondStep
#pragma kernel ThirdStep
#pragma kernel ForthStep
struct Uniforms
{
uint subdivisions;
uint currentSubdivision;
uint trianglesCount;
uint vertexCount;
float3 cameraPosition;
float threshold;
};
struct LocalUniforms
{
uint selectedTrianglesCount;
float3 position;
float3 scale;
};
RWStructuredBuffer<Uniforms> _Uniforms;
RWStructuredBuffer<LocalUniforms> _LocalUniforms;
RWStructuredBuffer<float3> _Positions;
RWStructuredBuffer<int3> _Triangles;
RWStructuredBuffer<int3> _SelectedTriangles;
RWStructuredBuffer<IndirectDrawIndexedArgs> _IndirectDrawIndexedArgs;
StructuredBuffer<Uniforms> _UniformsReadOnly;
StructuredBuffer<LocalUniforms> _LocalUniformsReadOnly;
StructuredBuffer<float3> _PositionsReadOnly;
StructuredBuffer<int3> _TrianglesReadOnly;
RWStructuredBuffer<uint> _SelectionBufferA;
RWStructuredBuffer<uint> _SelectionBufferB;
RWStructuredBuffer<int3> _SelectionTriangles;
StructuredBuffer<uint> _SelectionBufferReadOnly;
[numthreads(1,1,1)]
void ComputeSizes (uint3 id : SV_DispatchThreadID)
{
const uint subdivisions = _Uniforms[0].subdivisions;
uint side = (1 << (subdivisions + 1)) - (1 << subdivisions) + 1;
_Uniforms[0].vertexCount = side * side;
_Uniforms[0].trianglesCount = 2;
for (uint i = 1; i <= subdivisions; ++i)
{
_Uniforms[0].trianglesCount += 1 << (i * 2 + 1);
}
}
[numthreads(1,1,1)]
void ComputeVertices (uint3 id : SV_DispatchThreadID)
{
const uint subdivisions = _UniformsReadOnly[0].subdivisions;
const uint side = (1 << subdivisions) + 1;
double gap = 1.0L / (side - 1);
for (uint i = 0; i < side; ++i)
{
for (uint j = 0; j < side; ++j)
{
_Positions[i * side + j].x = (float)(gap * j);
_Positions[i * side + j].y = 0;
_Positions[i * side + j].z = (float)(gap * i);
}
}
}
[numthreads(1,1,1)]
void ComputeTriangles (uint3 id : SV_DispatchThreadID)
{
const uint current_subdivision = _UniformsReadOnly[0].currentSubdivision;
const uint subdivisions = _UniformsReadOnly[0].subdivisions;
const uint side = (1 << subdivisions) + 1;
uint offset = 0;
for(uint expo = 0; expo < current_subdivision; ++expo)
{
offset += 1 << (expo * 2 + 1);
}
const uint index_gap = (side - 1) / (1 << current_subdivision);
const uint sections = (side - 1) / index_gap;
for (uint i = 0; i < sections; ++i)
{
for (uint j = 0; j < sections; ++j)
{
const uint bottom_triangle_index = offset + (j * sections + i) * 2;
const uint top_triangle_index = bottom_triangle_index + 1;
{
uint x = j * index_gap;
uint y = i * index_gap;
_Triangles[bottom_triangle_index].x = y * side + x;
x += index_gap;
y += index_gap;
_Triangles[bottom_triangle_index].y = y * side + x;
y -= index_gap;
_Triangles[bottom_triangle_index].z = y * side + x;
}
{
uint x = j * index_gap;
uint y = i * index_gap;
_Triangles[top_triangle_index].x = y * side + x;
y += index_gap;
_Triangles[top_triangle_index].y = y * side + x;
x += index_gap;
_Triangles[top_triangle_index].z = y * side + x;
}
}
}
}
uint ReadBit(StructuredBuffer<uint> buffer, uint id)
{
return buffer[id];
}
void WriteBit(RWStructuredBuffer<uint> buffer, uint id, uint value)
{
buffer[id] = value;
}
// first step -> detect every triangle that is bigger than treshhold (+ apply GPU culling)
[numthreads(1024, 1, 1)]
void FirstStep (uint3 id : SV_DispatchThreadID)
{
if (id.x >= _UniformsReadOnly[0].trianglesCount) return;
float3 firstVertex = _PositionsReadOnly[_TrianglesReadOnly[id.x].x] * _LocalUniformsReadOnly[0].scale + _LocalUniformsReadOnly[0].position - _UniformsReadOnly[0].cameraPosition;
float3 secondVertex = _PositionsReadOnly[_TrianglesReadOnly[id.x].y] * _LocalUniformsReadOnly[0].scale + _LocalUniformsReadOnly[0].position - _UniformsReadOnly[0].cameraPosition;
float3 thirdVertex = _PositionsReadOnly[_TrianglesReadOnly[id.x].z] * _LocalUniformsReadOnly[0].scale + _LocalUniformsReadOnly[0].position - _UniformsReadOnly[0].cameraPosition;
float firstSquaredDistance = dot(firstVertex, firstVertex);
float secondSquaredDistance = dot(secondVertex, secondVertex);
float thirdSquaredDistance = dot(thirdVertex, thirdVertex);
if (firstSquaredDistance < _UniformsReadOnly[0].threshold
|| secondSquaredDistance < _UniformsReadOnly[0].threshold
|| thirdSquaredDistance < _UniformsReadOnly[0].threshold)
{
WriteBit(_SelectionBufferA, id.x, 1);
WriteBit(_SelectionBufferB, id.x, 1);
}
else
{
WriteBit(_SelectionBufferA, id.x, 0);
WriteBit(_SelectionBufferB, id.x, 0);
}
}
// second step -> for every child triangle from the triangle that is going to be rendered, don't render if children are going to be rendered
[numthreads(1024, 1, 1)]
void SecondStep (uint3 id : SV_DispatchThreadID)
{
if (id.x >= _UniformsReadOnly[0].trianglesCount) return;
// skip triangle id.x if it is not selected
if (ReadBit(_SelectionBufferReadOnly, id.x) == 0) return;
uint offset = 0;
uint lastOffset;
uint i = 0;
do
{
lastOffset = 1 << (i * 2 + 1);
offset += lastOffset;
i += 1;
} while(offset <= id.x);
// Last subdivision has no children
if (offset >= _UniformsReadOnly[0].trianglesCount) return;
i -= 1;
offset -= lastOffset;
uint subdivisionOffset = id.x - offset;
bool isTopTriangle = (subdivisionOffset % 2) == 1;
uint side = (lastOffset / 2) >> i;
uint parentRow = (subdivisionOffset / 2) / side;
uint parentColumn = (subdivisionOffset / 2) % side;
offset += lastOffset;
side *= 2;
uint bottomChildrenRow = parentRow * 2;
uint bottomChildrenColumn = parentColumn * 2;
uint childrenRendering = 0;
if (isTopTriangle)
{
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + bottomChildrenRow * 2 * side + bottomChildrenColumn * 2 + 1);
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + (bottomChildrenRow * 2 + 1) * side + bottomChildrenColumn * 2);
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + (bottomChildrenRow * 2 + 1) * side + bottomChildrenColumn * 2 + 1);
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + (bottomChildrenRow * 2 + 1) * side + bottomChildrenColumn * 2 + 3);
}
else
{
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + bottomChildrenRow * 2 * side + bottomChildrenColumn * 2);
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + bottomChildrenRow * 2 * side + bottomChildrenColumn * 2 + 2);
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + bottomChildrenRow * 2 * side + bottomChildrenColumn * 2 + 3);
childrenRendering |= ReadBit(_SelectionBufferReadOnly, offset + (bottomChildrenRow * 2 + 1) * side + bottomChildrenColumn * 2 + 2);
}
if (childrenRendering != 0)
{
WriteBit(_SelectionBufferB, id.x, 0);
}
}
// third step -> for every triangle, copy to buffer to be rendered using atomic add
[numthreads(1024, 1, 1)]
void ThirdStep (uint3 id : SV_DispatchThreadID)
{
if (id.x >= _UniformsReadOnly[0].trianglesCount) return;
// skip triangle id.x if it is not selected
if (ReadBit(_SelectionBufferReadOnly, id.x) == 0) return;
uint triangleId;
InterlockedAdd(_LocalUniforms[0].selectedTrianglesCount, 1, triangleId);
_SelectionTriangles[triangleId] = _TrianglesReadOnly[id.x];
}
[numthreads(1,1,1)]
void ForthStep (uint3 id : SV_DispatchThreadID)
{
_IndirectDrawIndexedArgs[0].indexCountPerInstance = _LocalUniformsReadOnly[0].selectedTrianglesCount * 3;
_IndirectDrawIndexedArgs[0].instanceCount = 1;
_IndirectDrawIndexedArgs[0].startIndex = 0;
_IndirectDrawIndexedArgs[0].baseVertexIndex = 0;
_IndirectDrawIndexedArgs[0].startInstance = 0;
}
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Rendering;
public class TerrainRenderer : MonoBehaviour
{
[StructLayout(LayoutKind.Sequential)]
public struct Uniforms
{
public const int size = sizeof(uint) * 4 + sizeof(float) * 4;
public uint subdivisions;
public uint currentSubdivision;
public uint trianglesCount;
public uint vertexCount;
public Vector3 cameraPosition;
public float threshold;
}
[StructLayout(LayoutKind.Sequential)]
struct LocalUniforms
{
public const int size = sizeof(uint) + sizeof(float) * 3 * 2;
public uint selectedTrianglesCount;
public Vector3 position;
public Vector3 scale;
};
public Material material;
public ComputeShader computeShader;
public Texture terrain;
public Transform cameraTransform;
[Range(0, 12)]
public int subdivisions = 4;
[Range(0f, 1000000f)]
public float threshold = 10000f;
GraphicsBuffer UniformsBuffer;
GraphicsBuffer LocalUniformsBuffer;
GraphicsBuffer TrianglesBuffer;
GraphicsBuffer PositionsBuffer;
GraphicsBuffer SelectionBufferA;
GraphicsBuffer SelectionBufferB;
GraphicsBuffer SelectionTrianglesBuffer;
GraphicsBuffer IndirectDrawIndexedArgsBuffer;
Uniforms[] uniforms;
LocalUniforms[] localUniforms;
CommandBuffer cmd;
RenderParams rp;
static readonly int PositionsID = Shader.PropertyToID("_Positions");
static readonly int ScaleID = Shader.PropertyToID("_Scale");
static readonly int PositionOffsetID = Shader.PropertyToID("_PositionOffset");
static readonly int TerrainID = Shader.PropertyToID("_Terrain");
void InitializeTerrain()
{
UniformsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 1, Uniforms.size);
uniforms = new Uniforms[1];
uniforms[0].subdivisions = (uint) subdivisions;
uniforms[0].currentSubdivision = 0;
uniforms[0].trianglesCount = 0;
uniforms[0].vertexCount = 0;
uniforms[0].cameraPosition = Vector3.zero;
uniforms[0].threshold = threshold;
UniformsBuffer.SetData(uniforms);
LocalUniformsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 1, LocalUniforms.size);
localUniforms = new LocalUniforms[1];
localUniforms[0].selectedTrianglesCount = 0;
LocalUniformsBuffer.SetData(localUniforms);
int kernelId = computeShader.FindKernel("ComputeSizes");
computeShader.SetBuffer(kernelId, "_Uniforms", UniformsBuffer);
computeShader.Dispatch(kernelId, 1, 1, 1);
UniformsBuffer.GetData(uniforms);
TrianglesBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int) uniforms[0].trianglesCount * 3, sizeof(int));
PositionsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int) uniforms[0].vertexCount, 3 * sizeof(float));
SelectionBufferA = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int) uniforms[0].trianglesCount, sizeof(uint));
SelectionBufferB = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int) uniforms[0].trianglesCount, sizeof(uint));
SelectionTrianglesBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (1 << (subdivisions * 2 + 1)) * 3, sizeof(int));
kernelId = computeShader.FindKernel("ComputeVertices");
computeShader.SetBuffer(kernelId, "_UniformsReadOnly", UniformsBuffer);
computeShader.SetBuffer(kernelId, "_Triangles", TrianglesBuffer);
computeShader.SetBuffer(kernelId, "_Positions", PositionsBuffer);
computeShader.Dispatch(kernelId, 1, 1, 1);
kernelId = computeShader.FindKernel("ComputeTriangles");
computeShader.SetBuffer(kernelId, "_UniformsReadOnly", UniformsBuffer);
computeShader.SetBuffer(kernelId, "_Triangles", TrianglesBuffer);
computeShader.SetBuffer(kernelId, "_Positions", PositionsBuffer);
for (int i = 0; i <= subdivisions; i++)
{
uniforms[0].currentSubdivision = (uint) i;
UniformsBuffer.SetData(uniforms);
computeShader.Dispatch(kernelId, 1, 1, 1);
}
rp = new RenderParams(material);
rp.worldBounds = new Bounds(transform.position + transform.localScale * 0.5f, transform.localScale);
rp.matProps = new MaterialPropertyBlock();
rp.matProps.SetBuffer(PositionsID, PositionsBuffer);
rp.matProps.SetVector(ScaleID, transform.localScale);
rp.matProps.SetVector(PositionOffsetID, transform.position);
rp.matProps.SetTexture(TerrainID, terrain);
IndirectDrawIndexedArgsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size);
cmd = new CommandBuffer();
kernelId = computeShader.FindKernel("FirstStep");
cmd.SetComputeBufferParam(computeShader, kernelId, "_UniformsReadOnly", UniformsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_LocalUniformsReadOnly", LocalUniformsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_PositionsReadOnly", PositionsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_TrianglesReadOnly", TrianglesBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_SelectionBufferA", SelectionBufferA);
cmd.SetComputeBufferParam(computeShader, kernelId, "_SelectionBufferB", SelectionBufferB);
cmd.DispatchCompute(computeShader, kernelId, (int) uniforms[0].trianglesCount / 1024 + 1, 1, 1);
kernelId = computeShader.FindKernel("SecondStep");
cmd.SetComputeBufferParam(computeShader, kernelId, "_UniformsReadOnly", UniformsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_SelectionBufferReadOnly", SelectionBufferA);
cmd.SetComputeBufferParam(computeShader, kernelId, "_SelectionBufferB", SelectionBufferB);
cmd.DispatchCompute(computeShader, kernelId, (int) uniforms[0].trianglesCount / 1024 + 1, 1, 1);
kernelId = computeShader.FindKernel("ThirdStep");
cmd.SetComputeBufferParam(computeShader, kernelId, "_UniformsReadOnly", UniformsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_LocalUniforms", LocalUniformsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_TrianglesReadOnly", TrianglesBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_SelectionBufferReadOnly", SelectionBufferB);
cmd.SetComputeBufferParam(computeShader, kernelId, "_SelectionTriangles", SelectionTrianglesBuffer);
cmd.DispatchCompute(computeShader, kernelId, (int) uniforms[0].trianglesCount / 1024 + 1, 1, 1);
kernelId = computeShader.FindKernel("ForthStep");
cmd.SetComputeBufferParam(computeShader, kernelId, "_UniformsReadOnly", UniformsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_LocalUniformsReadOnly", LocalUniformsBuffer);
cmd.SetComputeBufferParam(computeShader, kernelId, "_IndirectDrawIndexedArgs", IndirectDrawIndexedArgsBuffer);
cmd.DispatchCompute(computeShader, kernelId, 1, 1, 1);
}
void UpdateUniforms()
{
uniforms[0].cameraPosition = cameraTransform.position;
uniforms[0].threshold = threshold;
UniformsBuffer.SetData(uniforms);
localUniforms[0].selectedTrianglesCount = 0;
localUniforms[0].position = transform.position;
localUniforms[0].scale = transform.localScale;
LocalUniformsBuffer.SetData(localUniforms);
rp.matProps.SetVector(ScaleID, transform.localScale);
rp.matProps.SetVector(PositionOffsetID, transform.position);
}
void UpdateTerrain()
{
Graphics.ExecuteCommandBuffer(cmd);
Graphics.RenderPrimitivesIndexedIndirect(rp, MeshTopology.Triangles, SelectionTrianglesBuffer, IndirectDrawIndexedArgsBuffer, 1, 0);
}
void Start()
{
InitializeTerrain();
}
void OnDestroy()
{
UniformsBuffer?.Dispose();
UniformsBuffer = null;
LocalUniformsBuffer?.Dispose();
LocalUniformsBuffer = null;
TrianglesBuffer?.Dispose();
TrianglesBuffer = null;
PositionsBuffer?.Dispose();
PositionsBuffer = null;
SelectionBufferA?.Dispose();
SelectionBufferA = null;
SelectionBufferB?.Dispose();
SelectionBufferB = null;
SelectionTrianglesBuffer?.Dispose();
SelectionTrianglesBuffer = null;
IndirectDrawIndexedArgsBuffer?.Dispose();
IndirectDrawIndexedArgsBuffer = null;
}
void Update()
{
UpdateUniforms();
UpdateTerrain();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment