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 float
s (more accurate colors) representing the color data as rgba (red, green, blue, alpha) or as a single float
(faster, smaller) which has four byte
s 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.
Make sure your
ColorFormat
,CustomDataFormat
, andInstanceCount
are set to the correct values reflecting what your data has.Make sure your generated data is of correct length. Make sure you are not accidentally missing data fields.
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.
It didn’t fix the issue? Go through the checklist again, carefully.