Not sure if this is still an issue for you, but I had a very similar question some months ago.
I was using a “modular” character for building my own avatar…but I wanted to combine the selected meshes into 1 to reduce draw calls once the character was created from the picked parts.
Here is what I came up that worked for me…it’s in C# rather than GDScript but I’m hoping there is something here that will help.
I also ran into another issue I vaguely remember where when reading the surface, the format of one did not match the others. The only reason I knew this was because the mesh would not render.
I can’t recall the details exactly, but I believe it had to with UV2. Once I saw that a specific mesh’s values from SurfaceGetArrays() did not look like any of the others, I opened it in blender and removed the UV2 I think it was.
// Private vars
private static ArrayMesh _arrayMesh;
private static Vector4 _scaleAndOffsetUV;
private static Material _material;
public static MeshInstance3D CombineVisibleChildrenMeshInstances(Node3D container, Node3D[] nodes, bool willIgnorePosition = false, bool willIgnoreRotation = false, bool willIgnoreScale = false, bool willUseDefaultUVs = false) {
// Getting vars prepped
_arrayMesh = new ArrayMesh();
_material = null;
// Collect all the visible children
for (int index = 0; index < nodes.Length; index++) {
GetVisibleChildrenMeshInstances(nodes[index], willIgnorePosition, willIgnoreRotation, willIgnoreScale);
}
// Create a mesh instance to hold the collected meshes
MeshInstance3D combinedMesh = new MeshInstance3D() {
Name = "combined_" + GD.Randi(),
Mesh = _arrayMesh,
};
// Set the material for this mesh instance
for (int index = 0; index < combinedMesh.Mesh.GetSurfaceCount(); index++) {
if (willUseDefaultUVs) {
// Specify that the instance uniforms for UV definitions in the shader should not be used as they were adjusted directly on the mesh
combinedMesh.SetInstanceShaderParameter(RenderConst.WILL_USE_SCALE_AND_OFFSET_UV_SHADER_INSTANCE_PARAMETER_NAME, false);
}
else {
// Asked that the UVs were ignored (not calculated on the mesh) so use the shader instance uniforms instead
combinedMesh.SetInstanceShaderParameter(RenderConst.WILL_USE_SCALE_AND_OFFSET_UV_SHADER_INSTANCE_PARAMETER_NAME, true);
combinedMesh.SetInstanceShaderParameter(RenderConst.SCALE_AND_OFFSET_UV_SHADER_INSTANCE_PARAMETER_NAME, _scaleAndOffsetUV);
}
// Set the material
combinedMesh.Mesh.SurfaceSetMaterial(index, _material);
}
// Finally add the mesh instance to the container as a child
container.AddChild(combinedMesh);
combinedMesh.Owner = container;
GD.Print("All visible mesh instances were combined into a mesh named: " + combinedMesh.Name);
return combinedMesh;
}
private static void GetVisibleChildrenMeshInstances(Node3D node, bool willIgnorePosition = false, bool willIgnoreRotation = false, bool willIgnoreScale = false, bool willIgnoreUVs = false) {
// Enusure we have a node and it is not null
if (node == null) {
return;
}
// Pull out the child count for easier readability
int childCount = node.GetChildCount();
// Enumerate all children, only collecting the ones that are visible
for (int index = 0; index < childCount; index++) {
// Recursively evaluate all children
if (node.GetChild(index).GetChildCount() > 0) {
GetVisibleChildrenMeshInstances((Node3D)node.GetChild(index), willIgnorePosition, willIgnoreRotation, willIgnoreScale, willIgnoreUVs);
}
// Only evaulate MeshInstance3D types
if (node.GetChild(index) is not MeshInstance3D) {
continue;
}
// Create more usable/readable variable
MeshInstance3D child = (MeshInstance3D)node.GetChild(index);
if (child.Visible && child.IsVisibleInTree()) {
GD.Print("'" + child.Name + "' will be combined");
float volume = child.Mesh.GetAabb().Volume; ///// REMOVE THIS LINE, ONLY FOR TESTING
// This mesh is visible, get its surface. Assumption it has only 1
Array surface = child.Mesh.SurfaceGetArrays(0);
// When all of the flags are false, no need to process this section
if (willIgnorePosition == false
&& willIgnoreRotation == false
&& willIgnoreScale == false
&& willIgnoreUVs == false) {
// Capture the UV settings from instance parameter in shader
Vector4 scaleAndOffsetUV = (Vector4)child.GetInstanceShaderParameter(RenderConst.SCALE_AND_OFFSET_UV_SHADER_INSTANCE_PARAMETER_NAME);
// Capture transform information
Node3D logisticsInfo = child;
Vector3 position = Vector3.Zero;
Quaternion quaternion = Quaternion.Identity;
Vector3 scale = Vector3.One;
do {
position += logisticsInfo.Position;
quaternion *= logisticsInfo.Quaternion;
scale *= logisticsInfo.Scale;
if (logisticsInfo.GetParent() is not Node3D verifyParent) {
break;
}
logisticsInfo = verifyParent;
} while (logisticsInfo != null);
// Capture vertices to operate on
Vector3[] vertices = surface[(int)Mesh.ArrayType.Vertex].AsVector3Array();
Vector3[] normals = surface[(int)Mesh.ArrayType.Normal].AsVector3Array();
Vector2[] texUVs = surface[(int)Mesh.ArrayType.TexUV].AsVector2Array();
// Adjust transform of all the vertices according to where the parent is located
int vertexCount = vertices.Length;
for (int vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
// Apply scale
if (willIgnoreScale == false && scale.IsEqualApprox(Vector3.One) == false) {
vertices[vertexIndex] *= scale;
}
// Apply rotation
if (willIgnoreRotation == false && quaternion.IsEqualApprox(Quaternion.Identity) == false) {
// Adjust for any offsets the child may have so rotation will work as expected, child expected to be located at 0, 0, 0
Vector3 offset = child.Position;
// Rotate position of vertex
vertices[vertexIndex] = (vertices[vertexIndex] + offset).Rotated(quaternion.GetAxis().IsNormalized() ? quaternion.GetAxis() : quaternion.GetAxis().Normalized(), quaternion.GetAngle());
// Rotate normal so lighting is correct
normals[vertexIndex] = normals[vertexIndex].Rotated(quaternion.GetAxis().IsNormalized() ? quaternion.GetAxis() : quaternion.GetAxis().Normalized(), quaternion.GetAngle());
}
// Apply translation
if (willIgnorePosition == false && position.IsEqualApprox(Vector3.Zero) == false) {
vertices[vertexIndex] += position;
}
// Apply UV based on shader instance uniforms so texture mapping is correct for each mesh
if (willIgnoreUVs == false) {
texUVs[vertexIndex].X *= scaleAndOffsetUV.X;
texUVs[vertexIndex].Y *= scaleAndOffsetUV.Y;
texUVs[vertexIndex].X += scaleAndOffsetUV.Z;
texUVs[vertexIndex].Y += scaleAndOffsetUV.W;
}
}
//Assign data back to the surface to store any changes
surface[(int)Mesh.ArrayType.Vertex] = vertices;
surface[(int)Mesh.ArrayType.Normal] = normals;
surface[(int)Mesh.ArrayType.TexUV] = texUVs;
}
// Clear out any data in index 5-9
// WAIT UNTIL RAW IMPORT TEST IS COMPLETE
// Add the processed surface to the mesh array
_arrayMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surface);
// Grab the material used if it isn't known yet
if (_material == null) {
_material = child.Mesh.SurfaceGetMaterial(0);
_scaleAndOffsetUV = (Vector4) child.GetInstanceShaderParameter(RenderConst.SCALE_AND_OFFSET_UV_SHADER_INSTANCE_PARAMETER_NAME);
}
// Set the mesh child to invisible since it's been processed
child.Visible = false;
}
}
And this is how I would call it from code. I’d generally tell it to ignore scale when working with a skeleton as it would cause some very weird results. It’s not perfect, but it served the purpose that I needed
ModelProcessing.CombineVisibleChildrenMeshInstances(skeleton, new Node3D[] { skeleton }, false, false, true);