Okay, back home. i’m going to post my code and provide some explanation ws we go, but don’t have a ton of time at the moment to dive deep into it. Hopefully with the progress you’ve made so far it should be fairly self-explanatory. I’ve tried to remove as much of the ‘non-relevant’ code as possible. Also note that I’m in the midst of a somewhat poor refactoring job, attempting to ‘modularize’ certain aspects of the patch generation. I somehow failed to make a backup of the code prior to this
so it’s not in a compilable state at the moment. That shouldn’t matter - the core code that drives Unity is unchanged, but the organization of it is going to be wrong, so if you see random custom classes referenced in some places but not others, don’t think much of it.
First off, creation of the prototype patch/mesh (dummy mesh). I do this just once, in a class “PatchManager” that is a ‘singleton’ MonoBehaviour.
private void setupDummyMesh()
{
int nVerts = structuralConfiguration.nVerts;
int nVertsPerEdge = structuralConfiguration.nVertsPerEdge;
Vector3[] dummyVerts = new Vector3[nVerts];
Vector2[] uv0 = new Vector2[nVerts];
int[] triangles = new int[(nVertsPerEdge - 1) * (nVertsPerEdge - 1) * 2 * 3];
float height = 0;
for (int r = 0; r < nVertsPerEdge; r++)
{
int rowStartID = r * nVertsPerEdge;
for (int c = 0; c < nVertsPerEdge; c++)
{
int vertID = rowStartID + c;
dummyVerts[vertID] = new Vector3(c, height, r);
Vector2 uv = new Vector2();
uv.x = r / (float)(nVertsPerEdge - 1);
uv.y = c / (float)(nVertsPerEdge - 1);
uv0[vertID] = uv;
}
}
int triangleIndex = 0;
for (int r = 0; r < nVertsPerEdge - 1; r++)
{
int rowStartID = r * nVertsPerEdge;
int rowAboveStartID = (r + 1) * nVertsPerEdge;
for (int c = 0; c < nVertsPerEdge - 1; c++)
{
int vertID = rowStartID + c;
int vertAboveID = rowAboveStartID + c;
triangles[triangleIndex++] = vertID;
triangles[triangleIndex++] = vertAboveID;
triangles[triangleIndex++] = vertAboveID + 1;
triangles[triangleIndex++] = vertID;
triangles[triangleIndex++] = vertAboveID + 1;
triangles[triangleIndex++] = vertID + 1;
}
}
dummyMesh = new Mesh();
dummyMesh.vertices = dummyVerts;
dummyMesh.uv = uv0;
dummyMesh.SetTriangles(triangles, 0);
}
I also go ahead and set up the material which will be used for later rendering. This is also done only once, in a ‘singleton’ monobehavior. Note that the “ProceduralMeshVertSurf” file must be located in the project’s Resources folder.
material = new Material(Shader.Find("ProceduralMeshVertSurf"));
material.SetFloat("_Metallic", 0);
material.SetFloat("_Glossiness", 0);
Texture2D texture = (Texture2D)UnityEngine.Resources.Load("GrassRockyAlbedo");
material.SetTexture("_MainTex", texture);
Now, for each patch I create two ComputeBuffers.
generationConstants, which is where I’ll put the “inputs” to the ComputeShader - i.e. properties required to build the patch like vertex spacing, patch world center, etc.
computeShader.setBuffer(kernel, "terrainGenerationConstants", generationConstants);
and
patchGeneratedDataBuffer, which the ComputeShader will fill with vertex data (position, normal, etc).
computeShader.setBuffer(kernel, "patchOutput", patchGeneratedDataBuffer);
Note that the strings above must match those found in the ComputeShader below (I actually do this procedurally but have used plain strings in this example for clarity).
After filling the generationConstants buffer with the appropriate data, I call ComputeShader.setBuffer for the above two buffers, and then dispatch the Compute Shader with
public void dispatch()
{
computeShader.Dispatch(kernel,
THREADGROUP_SIZE_X,
THREADGROUP_SIZE_Y,
THREADGROUP_SIZE_Z);
}
These constants are defined as:
public static int nVertsPerEdge { get { return 224; } } //Should be multiple of 32
public static int nVerts { get { return nVertsPerEdge * nVertsPerEdge; } }
public int THREADS_PER_GROUP_X { get { return 32; } }
public int THREADS_PER_GROUP_Y { get { return 32; } }
public int THREADGROUP_SIZE_X { get { return nVertsPerEdge / THREADS_PER_GROUP_X; } }
public int THREADGROUP_SIZE_Y { get { return nVertsPerEdge / THREADS_PER_GROUP_Y; } }
public int THREADGROUP_SIZE_Z { get { return 1; } }
Here’s the basic version of Compute Shader itself. I’ve left the preprocessor stuff in just to demonstrate a cool technique, but it’s definitely not essential:
#pragma kernel CSMain
#define threadsPerGroup_X 32
#define threadsPerGroup_Y 32
#define nVerticesPerSide 224
#define nVerticesPerSideFloat 224.0
#define TWO_PI 6.283185
#include "noiseSimplex.cginc"
#include "2DNoiseFunctions.cginc"
//#define RIDGID
#define HYBRID
#ifdef HYBRID
//Good hybridMultifractal values
#define NoiseFrequency 0.001
#define OneMinusFractalIncrement 0.3
#define Lacunarity 1.918
#define nOctaves 14
#define MultifractalOffset 0.9
#elif defined RIDGID
//Good ridgedMulti values
#define NoiseFrequency 0.015
#define OneMinusFractalIncrement 0.8
#define Lacunarity 1.918
#define nOctaves 8
#define MultifractalOffset 0.95
#define RidgedGain 1.3
#endif
struct GenerationConstants
{
float scale;
float noiseSeaLevel;
float spacing;
float4 patchCenter;
};
struct OutputStruct
{
float4 pos;
};
StructuredBuffer<GenerationConstants> terrainGenerationConstants;
RWStructuredBuffer<OutputStruct> patchOutput;
[numthreads(threadsPerGroup_X,threadsPerGroup_Y,1)]
//We lookup the the index into the flat array by using x + y * x_stride
void CSMain (uint3 id : SV_DispatchThreadID)
{
GenerationConstants constants = terrainGenerationConstants[0];
float2 sampleCoord = float2((id.x + constants .patchCenter.x),(id.y + constants .patchCenter.y));
#ifdef HYBRID
float noise = hybridMultifractal(NoiseFrequency*sampleCoord, OneMinusFractalIncrement, Lacunarity, nOctaves, MultifractalOffset);
#elif defined RIDGID
float noise = ridgedMultifractal(NoiseFrequency*sampleCoord, OneMinusFractalIncrement, Lacunarity, nOctaves, MultifractalOffset, RidgedGain);
#endif
float height = constants .scale*max(noise, constants .noiseSeaLevel);
float4 output = float4(id.x*constants .spacing, height, id.y*constants .spacing, 1);
int outBuffOffset = id.x + id.y * nVerticesPerSide;
patchOutput[outBuffOffset].pos = output;
}
So once the ComputeShader completes, outputBuffer will contain the vertex position data. Not depicted here is normals generation, UV coordinates generation, color generation, etc.
Important - I do not try to draw a patch during the same frame in which it was generated. This has caused stalls for me in the past, and I don’t mind waiting a frame. Granted, this was awhile ago when I was using OpenGL (not with Unity), so it might not be a factor here, but I don’t mind waiting a frame to use the patch.
Now that the patch has been generated, on each subsequent frame, and for each and every patch, I do the following:
material.SetBuffer("patchData", patchGeneratedDataBuffer);
Graphics.DrawMesh(dummyMesh, transform.localToWorldMatrix, material, 0, null, 0, null, true, true);
By calling SetBuffer we’re basically hooking-up this patch’s vertex data (generated by the ComputeShader) as an input to the rendering shader. That data will be used to modify the dummyMesh, seen below.
Note that it’s not efficient to call material.SetBuffer like this, because it forces a new Batch to be created (Unity innerworkings). If Unity’s MaterialPropertyBlocks class supported a setBuffer method, that wouldn’t be a problem, but it currently doesn’t. So you’re stuck with 1 patch per batch, which just adds a bit of GPU driver overhead / state thrashing. It’s probably not a huge deal, but definitely not as efficient as it should be.
Inside the "ProceduralMeshVertSurf" shader (loaded and set earlier), we have the following:
Shader "ProceduralMeshVertSurf" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_colorDeepWater ("Deep Water", Color) = (0.03, 0.16, 0.35, 1.0)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#define nVerticesPerSide 224.0
#define SHOW_GRIDLINES
#include "UnityCG.cginc"
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
#pragma vertex vert
struct appdata_full_compute {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API_XBOX360)
half4 texcoord4 : TEXCOORD4;
half4 texcoord5 : TEXCOORD5;
#endif
fixed4 color : COLOR;
#ifdef SHADER_API_D3D11
uint id: SV_VertexID;
#endif
};
#pragma target 5.0
sampler2D _MainTex;
#ifdef SHADER_API_D3D11
StructuredBuffer<float4> patchData;
#endif
struct Input {
float2 uv_MainTex;
};
void vert (inout appdata_full_compute v, out Input o) {
#ifdef SHADER_API_D3D11
float4 position = patchData[v.id];
v.vertex = position;
o.uv_MainTex = v.texcoord.xy;
#endif
}
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o)
{
#ifdef SHOW_GRIDLINES
float2 fract = fmod(IN.uv_MainTex*nVerticesPerSide, float2(1,1));
fixed4 gridLine = any(step(float2(0.9,0.9), fract));
#else
fixed4 gridLine = 0;
#endif
#ifdef SHOW_PATCH_BORDER
fixed4 patchBorder = IN.onBorder;
#else
fixed4 patchBorder = 0;
#endif
// Terrain color comes from a texture tinted by color
fixed4 terrainColor = tex2D (_MainTex, IN.uv_MainTex) * _Color;
fixed4 c = terrainColor + gridLine + patchBorder;
o.Albedo = clamp(c.rgb, fixed3(0,0,0), fixed3(1,1,1));
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack Off
}
So the real magic happens under void vert (inout appdata_full_compute v, out Input o) {...}, where the data stored in the patchGeneratedDataBuffer is used to modify the position of the dummyMesh’s vertices.
So there you have it! Or at least the basic process. Happy to answer questions as they come up.