Godot MultiMesh SetAsBulkArray Method

This article tries to bridge the gaps and ambiguities in the official Godot documentation for SetAsBulkArray(). We don’t go over everything. But armed with this information you should be able to figure it out.

MultiMeshInstance‘s MultiMesh resource has a SetAsBulkArray() method which allows you to set all the data for your MultiMesh directly from a float[] array in one swift operation.

The great thing about this is that it’s as efficient as it can get. And you get to generate the data yourself in any way you want.

But how to generate the data?

The documentation for SetAsBulkArray() at the time of writing this is pretty terrible, so let’s go over the essentials.

The data for the array can be composed of three different parts:

  • Transform data
  • Color data
  • Custom data

Transform data is mandatory, but the color data and custom data are optional.

Transform data

What the transform data does is it tells MultiMeshInstance where your objects instances should be located, what their scale should be, and what their shears should be.

The transform data is a matrix of 12 float values:

// Transform data for a single mesh instance.  
float scaleX = 1f, shearXalongY = 0f, shearXalongZ = 0f, translateX = xOffset, // 3 Basis and Origin X.  
      shearYalongX = 0f, scaleY = 1f, shearYalongZ = 0f, translateY = yOffset, // 3 Basis and Origin Y.  
      shearZalongX = 0f, shearZalongY = 0f, scaleZ = 1f, translateZ = zOffset; // 3 Basis and Origin Z.

It is noteworthy that the offsets translateX, translateY, and translateZ are placed always right after the 3 basis values (eg. scaleX, shearXalongY, shearXalongZ in the above example) interleaved in the data instead of having them sitting at the end grouped together like you might expect.

The scale values change the scale of the mesh like you would expect. The shear values deform the mesh in different ways. And the translate values move the mesh around along the x, y, and z axis.

Color data

Vertex color data can be passed to SetAsBulkArray() in two formats.

You can pass it either as four floats (more accurate colors) representing the color data as rgba (red, green, blue, alpha) or as a single float (faster, smaller) which has four bytes crammed into it forcibly as Color8. These four bytes represent the rgba values but with less data and less accuracy (you’re never going to notice).

You can also disable the color information if you don’t need it.

When we create the MultiMesh we need to declare which format were are going to use.

NOTE: Which ever ColorFormat you choose the data you generate yourself will always has to match the selected ColorFormat or you will get errors and nothing will work.

Let’s look at practical examples where we declare the format when we create a new MultiMesh. I have removed all other values so we can focus on the color format:

Example 1: Declaring we use the binary format

MultiMesh multiMesh = new () {  
    ColorFormat = MultiMesh.ColorFormatEnum.Color8bit;  
};

Above we declare we are going to use the Color8 byte format where we pass a single float.

Example 2: Declaring we use the float format

MultiMesh multiMesh = new () {  
    ColorFormat = MultiMesh.ColorFormatEnum.Float;  
};

Above we declare we are going to use the format with four float values representing the rgba values.

Example 3: Declaring we have no color information

MultiMesh multiMesh = new () {  
    ColorFormat = MultiMesh.ColorFormatEnum.None;  
};

Creating the Color8bit color data

Let’s start right off by diving into some code.

// Our colors as raw values (between 0 and 1).
float red = 0.1f;
float green = 0.2f;
float blue = 0.3f;
float alpha = 1f;

// Convert to RGBA from 0 to 255.
red = red * 255;
green = green * 255;
blue = blue * 255;
alpha = alpha * 255;

// We need to pass an array, so we create one.
byte[] myColors = new byte[4];

// Assign colors to the array we need to input.
myColors[0] = (byte) red;
myColors[1] = (byte) green;
myColors[2] = (byte) blue;
myColors[3] = (byte) alpha;

// This is how we cram 4 bytes into a float.
float color = BitConverter.ToSingle(myColors, 0);

Keep in mind that this is a contrived example and you will have to think for yourself how to solve your particular loops, functions, and such for constructing the practical color data for your meshes.

The example above shows you how to get a float which some of you might find to be a rather strange float. It has 4 bytes crammed into it and it might be a negative number or otherwise rather curious in it’s value range.

The example above is all you really need. But for a deeper understanding, let’s go over the code in detail.

float red = 0.1f;
float green = 0.2f;
float blue = 0.3f;
float alpha = 1f;

First, we need to have some color values to work with. So we create some in the raw format which ranges from 0 to 1 so we can better understand how to convert raw values to the format we actually need. Feel free to skip this stage if you’re not using raw values and your color data is already in the 0 to 255 range.

Just note that you will get trouble if you try to use the wrong color format.

You would most likely have a function which gives you the colors you want. But for the sake of this example it doesn’t really matter where your colors come from, or in what format, as long as you have your colors ready at hand and in the correct format by the time you’re ready to pass them on.

red = red * 255;
green = green * 255;
blue = blue * 255;
alpha = alpha * 255;

By multiplying the raw colors that are in the scale of 0 to 1 with 255 we transform them from the scale of 0 to 255. It is required for the Color8bit binary format.

byte[] myColors = new byte[4];

BitConverter uses byte[] as input, so we need to have a byte array in which to pass our data. The size of the array has to be to be 4 as we have 4 colors: red, green, blue, and alpha.

NOTE: You most likely do not want to declare this array inside of your color processing loop. You might be generating a lot of mesh instance data and creating a lot of new arrays which become useless right after might not be a good idea. So I recommend declaring the array outside of your loop and reusing the array.

myColors[0] = (byte) red;
myColors[1] = (byte) green;
myColors[2] = (byte) blue;
myColors[3] = (byte) alpha;

Next we simply assign the color values to the array and cast them as bytes since a byte array is required and our color values are still floats.

float color = BitConverter.ToSingle(myColors, 0);

Finally we use BitConverter.ToSingle() to get our weird-ass float which has 4 colors packed as a single float.

What BitConverter does is it basically uses unsafe methods to copy the data as-is really fast and efficiently from myColors array directly to the 4 bytes in the target float.

That’s it. You can now use the single float in your MultiMesh data array.

If you feel the urge to understand on a deeper level why this weird dance is necessary you have to look into how float values work.

Floating point values quite complex and clever under the hood and that’s why you need special considerations when putting them together from bytes. But we won’t get deeper into that here as it’s a whole subject unto itself.

NOTE: There might be a simpler way of doing this. But I’m not aware if it at the time of writing this.

Creating the float color data

float red = 0.5f
float green = 0.5f
float blue = 0.5f
float alpha = 1f

And you’re done. Just replace the values with the color values you want using the raw color format with values between 0 and 1.

Accessing the data from a shader

The color information can be accessed via COLOR in the shader automatically.

Example:

void fragment() {
    ALBEDO = COLOR.rgb;
}

Custom data

The process for custom data is largely identical to the color data (except for the color specific parts). Read the color data section above for the details.

MultiMesh custom data can be accessed automatically in the shader via INSTANCE_CUSTOM and the id number with INSTANCE_ID.

Putting it all together

You have your transforms, you have your color data, and you have your custom data. Now we need to put it into an array that represents all the data for all the meshes in your MultiMesh.

Create and pass your MultiMesh to your MultiMeshInstance

MultiMesh multiMesh = new () {  
    TransformFormat = MultiMesh.TransformFormatEnum.Transform3d,  
    ColorFormat = MultiMesh.ColorFormatEnum.Color8bit,  
    CustomDataFormat = MultiMesh.CustomDataFormatEnum.Float,  
    InstanceCount = totalCount,  
    VisibleInstanceCount = -1,  
    Mesh = mesh,  
};  
  
Multimesh = multiMesh;

You would do this inside of your MultiMeshInstance Make sure all the data is set correctly and to correct values for your use case.

Generate MultiMeshData full example

internal float[] GenerateMultiMeshData() {  
      
    int transformDataSize = 12;  
    int colorDataSize = 1;  
    int customDataSize = 4;  
    int dataSize = transformDataSize + colorDataSize + customDataSize;  
    int maxIterations = 10;  
    int maxDataSize = maxIterations * dataSize;  
    int rgbaDataSize = 4;  
    byte[] randomColorsArray = new byte[rgbaDataSize];  
    float[] multiMeshData = new float[maxDataSize];  
    Random random = new Random();  
  
    int dataIteration = 0;  
    for (; dataIteration < maxDataSize; dataIteration += dataSize) {  
  
        int xOffset = dataIteration / dataSize;  
        int yOffset = 0;  
        int zOffset = 0;  
  
        // Generate transforms data.      
float scaleX = 1f, shearXalongY = 0f, shearXalongZ = 0f, translateX = xOffset,  
              shearYalongX = 0f, scaleY = 1f, shearYalongZ = 0f, translateY = yOffset,  
              shearZalongX = 0f, shearZalongY = 0f, scaleZ = 1f, translateZ = zOffset;  
  
        // Generate random color data.      
float red = random.Next(0, 256);  
        float green = random.Next(0, 256);  
        float blue = random.Next(0, 256);  
        float alpha = 255f;  
        randomColorsArray[0] = (byte) red;  
        randomColorsArray[1] = (byte) green;  
        randomColorsArray[2] = (byte) blue;  
        randomColorsArray[3] = (byte) alpha;  
        float color = BitConverter.ToSingle(randomColorsArray, 0);  
  
        // Generate custom data.  
        float dataA = 0f, dataB = 0f, dataC = 0f, dataD = 0f;  
  
        // Assign the data of a single mesh to the array one mesh at a time per loop iteration.  
        multiMeshData[dataIteration] = scaleX;  
        multiMeshData[dataIteration + 1] = shearXalongY;  
        multiMeshData[dataIteration + 2] = shearXalongZ;  
        multiMeshData[dataIteration + 3] = translateX;  
        multiMeshData[dataIteration + 4] = shearYalongX;  
        multiMeshData[dataIteration + 5] = scaleY;  
        multiMeshData[dataIteration + 6] = shearYalongZ;  
        multiMeshData[dataIteration + 7] = translateY;  
        multiMeshData[dataIteration + 8] = shearZalongX;  
        multiMeshData[dataIteration + 9] = shearZalongY;  
        multiMeshData[dataIteration + 10] = scaleZ;  
        multiMeshData[dataIteration + 11] = translateZ;  
        multiMeshData[dataIteration + 12] = color;  
        multiMeshData[dataIteration + 13] = dataA;  
        multiMeshData[dataIteration + 14] = dataB;  
        multiMeshData[dataIteration + 15] = dataC;  
        multiMeshData[dataIteration + 16] = dataD;  
    }  
    return multiMeshData;  
}

ERROR: Condition “dsize != p_array.size()” is true.

This error is caused by your data not having the length you told MultiMesh it would have.

  1. Make sure your ColorFormat, CustomDataFormat, and InstanceCount are set to the correct values reflecting what your data has.

  2. Make sure your generated data is of correct length. Make sure you are not accidentally missing data fields.

  3. Make sure you are iterating through the data correctly in your loops. Check your index values and how they are used and incremented in code.

  4. It didn’t fix the issue? Go through the checklist again, carefully.