How to do multiple Graphics.DrawMesh calls in a C# script attached to one GO to draw procedural meshes

Hi,

Situation:
I am trying to move from my CPU based procedural planet generation approach to a GPU based (when it comes to plane calculation and rendering).
Being fairly new new to shader programing at all, I am right now at the stage that I can hand over generation constants in a buffer to a compute shader, precalulcate the plane in a compute shader (vertice positions, norrmals, based on noise) and hand them over via the buffer for rendering (replacing the vertex positions of a prototype mesh with the ones from the buffer).

Question:
I havent been able to render more than one plane at once. Is there some dependency in using Graphics.DrawMesh (maybe the order of creating the buffers, materials, DrawMesh calls) for multiple draw calls in a script attached to a gameobject? Or in the end is there the need of one gameobject per DrawMesh call?!.

Any hint on what I might be doing wrong would be very helpfull and appreciated.
My (not working) approach for in this example to create a sphere out of six planes is:

  • Create six buffers (located in the QuadtreeTerrain class (a quadtree node)) which will hold the vertex positions computed in a compute shader.
  • Create six materials (one per QuadtreeTerrain).
  • Dispatch the six buffers to the compute shader to fill them with noise data (vertex position data)
  • Create six meshes using Graphics.DrawMesh. The applied vertex shader receives the buffers with vertex positions and replaces the vertex positions of a dummy mesh with the positions in the buffer.

See below the code. Right now only one mesh (the first one in the order) is drawn. Is there any dependency in using DrawMesh?

class QuadtreeTerrain {
    // Quadtree classes
    public QuadtreeTerrain parentNode; // The parent quadtree node
    public QuadtreeTerrain childNode1; // A children quadtree node
    public QuadtreeTerrain childNode2; // A children quadtree node
    public QuadtreeTerrain childNode3; // A children quadtree node
    public QuadtreeTerrain childNode4; // A children quadtree node
    // Buffer
    public ComputeBuffer generationConstantsBuffer;
    public ComputeBuffer patchGeneratedDataBuffer;
    // Material
    public Material material;
        ....
}

In the SpaceObjectProceduralPlanet script, applied to a single game object, I hold six instances of quadtrees [=QuadtreeTerrain] then.

public class SpaceObjectProceduralPlanet : MonoBehaviour {
        ....
        // QuadtreeTerrain
        private QuadtreeTerrain quadtreeTerrain1;
        private QuadtreeTerrain quadtreeTerrain2;
        private QuadtreeTerrain quadtreeTerrain3;
        private QuadtreeTerrain quadtreeTerrain4;
        private QuadtreeTerrain quadtreeTerrain5;
        private QuadtreeTerrain quadtreeTerrain6;
  
        // We initialize the buffers and the material used to draw.
        void Start()
        {
            ...
            // QuadtreeTerrain
            this.quadtreeTerrain1 = new QuadtreeTerrain(0, edgeVector1, edgeVector2, edgeVector3, edgeVector4, quadtreeTerrainParameter1);
            this.quadtreeTerrain2 = new QuadtreeTerrain(0, edgeVector2, edgeVector5, edgeVector4, edgeVector7, quadtreeTerrainParameter2);
            this.quadtreeTerrain3 = new QuadtreeTerrain(0, edgeVector5, edgeVector6, edgeVector7, edgeVector8, quadtreeTerrainParameter3);
            this.quadtreeTerrain4 = new QuadtreeTerrain(0, edgeVector6, edgeVector1, edgeVector8, edgeVector3, quadtreeTerrainParameter4);
            this.quadtreeTerrain5 = new QuadtreeTerrain(0, edgeVector6, edgeVector5, edgeVector1, edgeVector2, quadtreeTerrainParameter5);
            this.quadtreeTerrain6 = new QuadtreeTerrain(0, edgeVector3, edgeVector4, edgeVector8, edgeVector7, quadtreeTerrainParameter6);
            CreateBuffers(this.quadtreeTerrain1);
            CreateBuffers(this.quadtreeTerrain2);
            CreateBuffers(this.quadtreeTerrain3);
            CreateBuffers(this.quadtreeTerrain4);
            CreateBuffers(this.quadtreeTerrain5);
            CreateBuffers(this.quadtreeTerrain6);
            CreateMaterial(this.quadtreeTerrain1);
            CreateMaterial(this.quadtreeTerrain2);
            CreateMaterial(this.quadtreeTerrain3);
            CreateMaterial(this.quadtreeTerrain4);
            CreateMaterial(this.quadtreeTerrain5);
            CreateMaterial(this.quadtreeTerrain6);
            Dispatch(this.quadtreeTerrain1);
            Dispatch(this.quadtreeTerrain2);
            Dispatch(this.quadtreeTerrain3);
            Dispatch(this.quadtreeTerrain4);
            Dispatch(this.quadtreeTerrain5);
            Dispatch(this.quadtreeTerrain6);
    }
  
        // We compute the buffers.
        void CreateBuffers(QuadtreeTerrain quadtreeTerrain)
        {
            .... preparing generation constants
            quadtreeTerrain.generationConstantsBuffer.SetData(generationConstants);
            // Buffer Output
            quadtreeTerrain.patchGeneratedDataBuffer = new ComputeBuffer(nVerts, 16 + 12 + 4 + 12);
        }
  
        //We create the material
        void CreateMaterial(QuadtreeTerrain quadtreeTerrain)
        {
            Material material = new Material(shader);
            material.SetTexture("_MainTex", this.texture);
            material.SetFloat("_Metallic", 0);
            material.SetFloat("_Glossiness", 0);
            quadtreeTerrain.material = material;
        }
  
        //We dispatch threads of our CSMain1 and CSMain2 kernels.
        void Dispatch(QuadtreeTerrain quadtreeTerrain)
        {
            // Set Buffers
            computeShader.SetBuffer(_kernel, "generationConstantsBuffer", quadtreeTerrain.generationConstantsBuffer);
            computeShader.SetBuffer(_kernel, "patchGeneratedDataBuffer", quadtreeTerrain.patchGeneratedDataBuffer);
            // Dispatch first kernel
            _kernel = computeShader.FindKernel("CSMain1");
               computeShader.Dispatch(_kernel, THREADGROUP_SIZE_X, THREADGROUP_SIZE_Y, THREADGROUP_SIZE_Z);
            // Dispatch second kernel
            _kernel = computeShader.FindKernel("CSMain2");
            computeShader.Dispatch(_kernel, THREADGROUP_SIZE_X, THREADGROUP_SIZE_Y, THREADGROUP_SIZE_Z);
        }
  
        // We set the material before drawing and call DrawMesh on OnRenderObject
        void OnRenderObject()
        {
            this.quadtreeTerrain1.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain1.patchGeneratedDataBuffer);
            Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain1.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
  
            this.quadtreeTerrain2.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain2.patchGeneratedDataBuffer);
            Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain2.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
  
            this.quadtreeTerrain3.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain3.patchGeneratedDataBuffer);
            Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain3.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
  
            this.quadtreeTerrain4.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain4.patchGeneratedDataBuffer);
            Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain4.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
  
            this.quadtreeTerrain5.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain5.patchGeneratedDataBuffer);
            Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain5.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
  
            this.quadtreeTerrain6.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain6.patchGeneratedDataBuffer);
            Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain6.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
        }
  
        //When this GameObject is disabled we must release the buffers.
        private void OnDisable()
        {
            ReleaseBuffer();
        }
  
        //Release buffers and destroy the material when play has been stopped.
        void ReleaseBuffer()
        {
            // Destroy everything recursive in the quadtrees.
            this.quadtreeTerrain1.generationConstantsBuffer.Release();
            this.quadtreeTerrain1.patchGeneratedDataBuffer.Release();
            this.quadtreeTerrain2.generationConstantsBuffer.Release();
            this.quadtreeTerrain2.patchGeneratedDataBuffer.Release();
            this.quadtreeTerrain3.generationConstantsBuffer.Release();
            this.quadtreeTerrain3.patchGeneratedDataBuffer.Release();
            this.quadtreeTerrain4.generationConstantsBuffer.Release();
            this.quadtreeTerrain4.patchGeneratedDataBuffer.Release();
            this.quadtreeTerrain5.generationConstantsBuffer.Release();
            this.quadtreeTerrain5.patchGeneratedDataBuffer.Release();
            this.quadtreeTerrain6.generationConstantsBuffer.Release();
            this.quadtreeTerrain6.patchGeneratedDataBuffer.Release();
            DestroyImmediate(this.quadtreeTerrain1.material);
            DestroyImmediate(this.quadtreeTerrain2.material);
            DestroyImmediate(this.quadtreeTerrain3.material);
            DestroyImmediate(this.quadtreeTerrain4.material);
            DestroyImmediate(this.quadtreeTerrain5.material);
            DestroyImmediate(this.quadtreeTerrain6.material);
        }
  
        void Update() {
            // Do nothing
        }
  
    }

Of course this is very bruteforce, but well this should work before I proceed as I need to figure out how to handle the buffers and draw calls and where to put them.

I tried to get closer to the problem. It seems to depend which “Dispatch()” call I do first to precalculate a computebuffer in one computeshader before it is sent to the vertex buffer.
It seems the first ComputeBuffer.Dispatch() call overrules all following ones, as only the results from the first call are drawn. Although from my point of unserstanding I am using different buffers.
Edit: To be more precise: Both meshes ares drawn but they seem to share the same locations and probably so the buffer. I noticed that as the rendered triangles doubled with each Graphics.DrawMesh added.

Each “QuadtreeTerrain” class has two compute buffer references.

class QuadtreeTerrain {
     public ComputeBuffer generationConstantsBuffer;
     public ComputeBuffer patchGeneratedDataBuffer;
}

In SpaceObjectProceduralPlanet I initialize the buffers (call CreateBuffer(QuadtreeTerrain) in Start(), where in the called functions the buffers of each object are initialized (“new ComputeBuffer()”). Afterwards I dispatch each buffer by calling (in Start() ) the function Dispatch(QuadtreeTerrain).
Then, in “OnRenderObject()” I sent the buffers to the renderer. For the ease of read I reduced the number of different buffers to 2. Any hint why the first Dispatch() call overrules all others is very much appreciated.

using UnityEngine;
using System.Collections;
using System.Threading;
using System.Collections.Generic;

[RequireComponent(typeof(GameObject))]
public class SpaceObjectProceduralPlanet : MonoBehaviour {
public int seed;
public Position position;
public string name;
public float radius;
public float diameter;
public Transform m_Transform;
private int LOD;
// Primitive
private AbstractPrimitive primitive;
private enum PrimitiveState { IN_PRECALCULATION, PRECALCULATED, DONE };
private PrimitiveState primitiveState;
// QuadtreeTerrain
private QuadtreeTerrain quadtreeTerrain1;
private QuadtreeTerrain quadtreeTerrain2;
// Plane Template
public Mesh prototypeMesh;
public Mesh prototypeMesh2;
public Plane plane;
public Texture2D texture;
// ComputeShader
public Shader shader;
public ComputeShader computeShader;
private ComputeBuffer generationConstantsBuffer;
private ComputeBuffer patchGeneratedDataBuffer;
private int _kernel;
// Constants
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; } }

struct PatchGenerationConstantsStruct
{
    public int nVertsPerEdge;
    public float scale;
    public float spacing;
    public Vector3 patchCubeCenter;
    public Vector3 cubeFaceEastDirection;
    public Vector3 cubeFaceNorthDirection;
    public float planetRadius;
    public float terrainMaxHeight;
    public float noiseSeaLevel;
    public float noiseSnowLevel;
}

struct patchGeneratedDataStruct
{
    public Vector4 position;
    public Vector3 normal;
    public float noise;
    public Vector3 patchCenter;
}

// Initial call. We setup the shaders and prototype meshes here.
void Awake () {
    // Transform
    m_Transform = transform;

    // Mesh prottype
    this.prototypeMesh = MeshServiceProvider.setupNavyFishDummyMesh(nVertsPerEdge);
    this.prototypeMesh2 = MeshServiceProvider.setupNavyFishDummyMesh(nVertsPerEdge);
    // Plane Template (not used right now as we have the prototype mesh)
    this.plane = new Plane(nVertsPerEdge, Vector3.back);
    // Shader
    this.shader = Shader.Find("Custom/ProceduralPatch3");
    // ComputeShader
    this.computeShader = (ComputeShader)Resources.Load("Shaders/Space/Planet/Custom/ProceduralPatchCompute3");
    // Texture
    this.texture = (Texture2D)Resources.Load("Textures/space/planets/seamless/QuadtreeTerrainTexture.MugDry_1024") as Texture2D;
}

// We initialize the buffers and the material used to draw.
void Start()
{
    // Edge coordinates for initialization
    Vector3 edgeVector1 = new Vector3(-1, +1, -1);
    Vector3 edgeVector2 = new Vector3(+1, +1, -1);
    Vector3 edgeVector3 = new Vector3(-1, -1, -1);
    Vector3 edgeVector4 = new Vector3(+1, -1, -1);
    Vector3 edgeVector5 = new Vector3(+1, +1, +1);
    Vector3 edgeVector6 = new Vector3(-1, +1, +1);
    Vector3 edgeVector7 = new Vector3(+1, -1, +1);
    Vector3 edgeVector8 = new Vector3(-1, -1, +1);
    // Parameters
    QuadtreeTerrainParameter parameter = new QuadtreeTerrainParameter();
    parameter.nVertsPerEdge = nVertsPerEdge;
    parameter.scale = 2.0f / nVertsPerEdge;
    parameter.spacing = 2.0f / nVertsPerEdge;
    parameter.planetRadius = 6371.0f; // 6371000.0f; = earth
    parameter.terrainMaxHeight = 15.0f;
    parameter.noiseSeaLevel = 0.0f;
    parameter.noiseSnowLevel = 0.8f;
    QuadtreeTerrainParameter quadtreeTerrainParameter1 = parameter.clone();
    quadtreeTerrainParameter1.cubeFaceEastDirection = new Vector3(1, 0, 0);
    quadtreeTerrainParameter1.cubeFaceNorthDirection = new Vector3(0, 1, 0);
    QuadtreeTerrainParameter quadtreeTerrainParameter2 = parameter.clone();
    quadtreeTerrainParameter2.cubeFaceEastDirection = new Vector3(0, 0, 1);
    quadtreeTerrainParameter2.cubeFaceNorthDirection = new Vector3(0, 1, 0);
    // QuadtreeTerrain
    this.quadtreeTerrain1 = new QuadtreeTerrain(0, edgeVector1, edgeVector2, edgeVector3, edgeVector4, quadtreeTerrainParameter1);
    this.quadtreeTerrain2 = new QuadtreeTerrain(0, edgeVector2, edgeVector5, edgeVector4, edgeVector7, quadtreeTerrainParameter2);
    CreateBuffers(this.quadtreeTerrain1);
    CreateBuffers(this.quadtreeTerrain2);
    CreateMaterial(this.quadtreeTerrain1);
    CreateMaterial(this.quadtreeTerrain2);

// Only the mesh is drawn where there has been the first Dispatch(..) call. E.g. if the first call is commented out, the second mesh (QuadtreeTerrain2) is drawn.
    //Dispatch(this.quadtreeTerrain1);
    Dispatch(this.quadtreeTerrain2);
}

void Update()
{

}

// We compute the buffers.
void CreateBuffers(QuadtreeTerrain quadtreeTerrain)
{
    // Buffer Patch Generation Constants
    quadtreeTerrain.generationConstantsBuffer = new ComputeBuffer(4, // 1x int (4 bytes) for one index, index = 0
        4 +     // nVertsPerEdge (int = 4 bytes),
        4 +     // scale (float = 4 bytes),
        4 +     // spacing (float = 4 bytes),
        12 +    // patchCubeCenter (float3 = 12 bytes),
        12 +    // cubeFaceEastDirection (float3 = 12 bytes),
        12 +    // cubeFaceNorthDirection (float3 = 12 bytes),
        4 +     // planetRadius (float = 4 bytes),
        4 +     // terrainMaxHeight (float = 4 bytes),
        4 +     // noiseSeaLevel (float = 4 bytes),
        4);     // noiseSnowLevel (float = 4 bytes),
    PatchGenerationConstantsStruct[] generationConstants = new PatchGenerationConstantsStruct[1];
    generationConstants[0].nVertsPerEdge = quadtreeTerrain.parameters.nVertsPerEdge;
    generationConstants[0].scale = quadtreeTerrain.parameters.scale;
    generationConstants[0].spacing = quadtreeTerrain.parameters.spacing;
    generationConstants[0].patchCubeCenter = quadtreeTerrain.centerVector;
    generationConstants[0].cubeFaceEastDirection = quadtreeTerrain.parameters.cubeFaceEastDirection;
    generationConstants[0].cubeFaceNorthDirection = quadtreeTerrain.parameters.cubeFaceNorthDirection;
    generationConstants[0].planetRadius = quadtreeTerrain.parameters.planetRadius;
    generationConstants[0].terrainMaxHeight = quadtreeTerrain.parameters.terrainMaxHeight;
    generationConstants[0].noiseSeaLevel = quadtreeTerrain.parameters.noiseSeaLevel;
    generationConstants[0].noiseSnowLevel = quadtreeTerrain.parameters.noiseSnowLevel;
    quadtreeTerrain.generationConstantsBuffer.SetData(generationConstants);
    // Buffer Output
    quadtreeTerrain.patchGeneratedDataBuffer = new ComputeBuffer(nVerts, 16 + 12 + 4 + 12); // Output buffer contains vertice position (float4 = 16 bytes),
                                                                                            // normals (float3 = 12 bytes),
                                                                                            // noise (float = 4 bytes)
                                                                                            // patchCenter (float3 = 12 bytes)
}

//We create the material
void CreateMaterial(QuadtreeTerrain quadtreeTerrain)
{
    quadtreeTerrain.material = new Material(shader);
    quadtreeTerrain.material.SetTexture("_MainTex", this.texture);
    quadtreeTerrain.material.SetFloat("_Metallic", 0);
    quadtreeTerrain.material.SetFloat("_Glossiness", 0);
}

//The meat of this script, it sets the buffers for the compute shader.
// We then dispatch threads of our CSMain1 and 2 kernels.
void Dispatch(QuadtreeTerrain quadtreeTerrain)
{
    // Set Buffers
    computeShader.SetBuffer(_kernel, "generationConstantsBuffer", quadtreeTerrain.generationConstantsBuffer);
    computeShader.SetBuffer(_kernel, "patchGeneratedDataBuffer", quadtreeTerrain.patchGeneratedDataBuffer);
    // Dispatch first kernel
    _kernel = computeShader.FindKernel("CSMain1");
    computeShader.Dispatch(_kernel, THREADGROUP_SIZE_X, THREADGROUP_SIZE_Y, THREADGROUP_SIZE_Z);
    // Dispatch second kernel
    _kernel = computeShader.FindKernel("CSMain2");
    computeShader.Dispatch(_kernel, THREADGROUP_SIZE_X, THREADGROUP_SIZE_Y, THREADGROUP_SIZE_Z);
}

//After all rendering is complete we dispatch the compute shader and then set the material before drawing.
void OnRenderObject()
{
    this.quadtreeTerrain1.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain1.patchGeneratedDataBuffer);
    Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain1.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
    this.quadtreeTerrain2.material.SetBuffer("patchGeneratedDataBuffer", this.quadtreeTerrain2.patchGeneratedDataBuffer);
    Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, this.quadtreeTerrain2.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
}

//When this GameObject is disabled we must release the buffers.
private void OnDisable()
{
    ReleaseBuffer();
}

//Release buffers and destroy the material when play has been stopped.
void ReleaseBuffer()
{
    // Destroy everything recursive in the quadtrees.
    this.quadtreeTerrain1.generationConstantsBuffer.Release();
    this.quadtreeTerrain1.patchGeneratedDataBuffer.Release();
    this.quadtreeTerrain2.generationConstantsBuffer.Release();
    this.quadtreeTerrain2.patchGeneratedDataBuffer.Release();
    DestroyImmediate(this.quadtreeTerrain1.material);
    DestroyImmediate(this.quadtreeTerrain2.material);
}
}