+1 vote

Hi,

I was looking on mesh drawing in godot and I find this while googling for some informations.

From what I understand, we might be able to draw several faces on a cube at once using limited amount of vertexes (the exemple says 8 instead of 36). I didn't figured how to reproduce it. It seems like the definition of the "ARRAY_INDEX" does the trick, but I don't see how it works. The documentation only says that it enables the "index mode", nothing more.

In fact, if someone could give me a good overview on the whole ArrayMesh class and how it is used to render the surfaces on a mesh, I would be grateful, because I can't find more informations online.

Thanks

in Engine by (165 points)

2 Answers

+2 votes

Normally, you would use SurfaceTool to create a new Surface for a MeshArray. MeshDataTool is also available mainly for parsing or editing Meshes.

Just generally:
MeshInstance is a Node and holds a Mesh (and can have optionally have Materials that override the Mesh Surface Materials).

The Mesh in MeshInstance normally is an ArrayMesh. That means it is an Array of Surfaces.

A Surface is the part of a Mesh with a single Material. So all faces in a Surface share the same Color, Shader or Texture.

This means (as far as I know) that vertices are not shared for different Materials. (Although, they can be at the same positions, naturally.)

Inside a Surface vertices can be reused. A Tri (face) references three vertices. This can be done/is done by index.
But: One vertice can only have one set of attributes (UV,UV2, vertex color, normal ...).
Shared vertices can be tricky when using UVs (UVs are x/y coordinates inside the texture as the UVs are shared as well among the faces/tris. This doesn't matter if you use a single albedo color though.

An example for a simple cube texture: (looks like a cut open cube)

....11
22335544
....66

You'll see that the 2 inner UVs of 1/2/6/5 as well as all UVs/vertices of 33 can be shared.
5 and 4 can share 2 UVs/vertices. But all outer vertices should be solitary as their UV positions can't matched.

Back to ArrayMesh:
https://docs.godotengine.org/en/3.1/classes/class_arraymesh.html#enum-arraymesh-arraytype

add_surface_from_arrays( expects an Array of Integer array. The main Array will have 9 array members. Each is either empty or contains an array. Check the example on ArrayMesh for that.

If array[8] is not filled all vertices (and the other arrays with vertice attributes) are simply used one ofter another depending on the primitive type. So PRIMITIVE_TRIANGLES would create a triangle of each three vertices ([0],[1],[2] then [3][4][5] and so on.).

If array[8] is set, then it contains indices/pointers which address the vertices and their other attributes in the other array.

So maybe you have 14 vertices in the vertices and other attributes arrays.
Then the index array will "name" those vertice numbers that are used to create the tris.

Example: For quad face "2" of the cube, you have vertices (clockwise from upper left): 0,1,2,3
0..1
......
3..2
The array_index would now define the two tris for face "2": 0,1,3, (and) 3,1,2.
(I'm adressing the vertices clock-wise so the normal (if it is auto-generated), hopyfully points to "us".)

To be honest, this is all theoretical. Personally I've never used this method. When I wanted to create primitives, I used Surface Tool. It works for me but is certainly not perfect.

Here's some code excerpts:

    var material1 = SpatialMaterial.new()
    material1.albedo_color=meshColor1 
    var st1 = SurfaceTool.new()
    st1.begin(Mesh.PRIMITIVE_TRIANGLES)
    st1.set_material(material1)
    var mesh = Mesh.new()
    buildBox(st1,fPosition,Vector3(fencePostSize.x,fencePostSize.y,fencePostSize.z)) 
    st1.index()
    st1.generate_normals()
    st1.commit(mesh) 
    miNode.set_mesh(mesh)

#add a box to surfacetool at pos (center) with widthX/heightY/depthZ
func buildBox(st,pos,whd):
#clockwise
#uvs are kind of arbitrary (room for improvement)
    var FTL=Vector3(pos.x-whd.x/2, pos.y+whd.y/2, pos.z+whd.z/2)   # Front-top-left
    var FTR=Vector3(pos.x+whd.x/2, pos.y+whd.y/2, pos.z+whd.z/2)   # Front-top-right
    var FBL=Vector3(pos.x-whd.x/2, pos.y-whd.y/2, pos.z+whd.z/2)   # Front-bottom-left
    var FBR=Vector3(pos.x+whd.x/2, pos.y-whd.y/2, pos.z+whd.z/2)   # Front-bottom-right
    var BBR=Vector3(pos.x+whd.x/2, pos.y-whd.y/2, pos.z-whd.z/2)   # Back-bottom-right
    var BTR=Vector3(pos.x+whd.x/2, pos.y+whd.y/2, pos.z-whd.z/2)   # Back-top-right
    var BTL=Vector3(pos.x-whd.x/2, pos.y+whd.y/2, pos.z-whd.z/2)   # Back-top-left
    var BBL=Vector3(pos.x-whd.x/2, pos.y-whd.y/2, pos.z-whd.z/2)   # Back-bottom-left

    #print("boxLR: "+String(pos)+ " FTL:"+String(FTL))

    addTri(st,FTL,FTR,FBR)#front
    addTri(st,FTL,FBR,FBL)
    addTri(st,BTR,BTL,BBL)#back
    addTri(st,BTR,BBL,BBR)
    addTri(st,BTL,FTL,BBL)#left
    addTri(st,BBL,FTL,FBL)
    addTri(st,FTR,BTR,FBR)#right
    addTri(st,FBR,BTR,BBR)
    addTri(st,FTL,BTL,BTR)#top
    addTri(st,FTL,BTR,FTR)
    addTri(st,FBR,BBR,BBL)#bottom
    addTri(st,FBR,BBL,FBL)


func addTri(st,p1,p2,p3): #uvs are improvised, room for improvement
    st.add_uv(Vector2(0,0))
    st.add_vertex(p1)
    st.add_uv(Vector2(0.5,1))
    st.add_vertex(p2)
    st.add_uv(Vector2(1,0))
    st.add_vertex(p3)
by (3,286 points)

Hi and thank you for the answer. As I was leaning over procedural terrain generation, I started first by trying to dertermine the most lightweight method to render meshes. I'm testing it on basic shapes first, like cubes.

The use of the ArrayMesh appears to me relatively optimized comparing to the more traditionnal use of the SurfaceTool. As I can directly define arrays of vectors, give them to the arraymesh who will feed that into the mesh once for all.

When assembling surfaces, some vertices are going to be localted at the exact same position. So when rendering a surface, I thought we could just tell to the mesh to use some vertices on its other surfaces to render the tris instead of explicitely rewrite them into the Vector3Array of the face we're currently working on.

So far this is the code I've came up for the moment. It only displays the construction of two surfaces.

func _ready():
    # face 1
    var vertices1 := PoolVector3Array([
        Vector3(-1,-1,-1),
        Vector3(-1,1,-1),
        Vector3(1,1,-1),
        Vector3(1,-1,-1)
        ])

    # face two
    var vertices2 := PoolVector3Array([
        Vector3(1,1,1),
        Vector3(1,1,-1),
        Vector3(1,-1,-1),
        Vector3(1,-1,1)
        ])

    var arr_mesh := ArrayMesh.new()
    arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_FAN,
            get_arrays(vertices1))
    arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_FAN,
            get_arrays(vertices2))

    var m := MeshInstance.new()
    m.mesh = arr_mesh
    self.add_child(m)

# called to create the array to feed the ArrayMesh
func get_arrays(vertices: PoolVector3Array) -> Array:
    var arrays := []
    arrays.resize(ArrayMesh.ARRAY_MAX)
    arrays[ArrayMesh.ARRAY_VERTEX] = vertices
    return arrays

On this mesh, we can see that two Vector3 are shared by the two faces ((1,1,-1) and (1,-1,-1)). I was wondering if we could simply "merge them" to avoid repetitions.

Indeed you can skip the surface tool. At least that was my experience with Godot 2, and now that I'm delving into meshes with Godot 3 it seems to work much the same way.

If you intend to alter the mesh without rebuilding the arrays, iirc that's where the surface tool might be useful. Although, if your mesh isn't too complex, rebuilding the arrays may not be a performance concern. (I use this stopwatch thingy to benchmark specific parts of my code sometimes.)

As for merging vertices, you can use an array of indices, like you mentioned in the OP. I couldn't wrap my head around it myself back when I tried it, but I think I'm figuring it out now. I just added an answer with my attempt at explaining it. :)

+4 votes

To merge vertices you need to build an index array, where each index points to a vertex in the vertex array.

To exemplify I'll start with a simple face with vertices A, B, C and D. (The vertex order is arbitrary. Any order will work, as long as you stay consistent with it. Just be mindful of clockwise/counter-clockwise, because it's important.)

#       o ---- X
#      /
#     Z   A ------------ B
#         /            /
#       D ------------ C

That face is made of two triangles, which can be described in any of multiple ways, like ACD | ABC or DAC | CAB, etc. As long as those vertex orders are clockwise, that face will be facing up. (You can specify vertices differently, for using clockwise orders that face down -- see the cube below).

So for that face, using indices, we only need vertices at 4 locations (in the X and Z axes):

verts = PoolVector3Array([
    Vector3(0, 0, 0),  # a | 0
    Vector3(1, 0, 0),  # b | 1
    Vector3(1, 0, 1),  # c | 2
    Vector3(0, 0, 1)   # d | 3
])

# And also the uvs if you're usign textures.
# UVs go from 0 to 1, with 1 being equivalent to the entire texture. 
# If vertices move (in this case in X or Z) you may need to correct 
# the UVs for deformations, vertex offsets, etc. It can be tricky.
uvs = PoolVector2Array([
    Vector2(0,0)    # a
    Vector2(1,0)    # b
    Vector2(1,1)    # c
    Vector2(0,1)    # d
])

And then we describe the two triangles with indices, laid out to point to the specific vertices in the vertex array:

# considering the vertex array has the vertices [a, b, c, d]
# the indices will be their positions in the array: 0, 1, 2 and 3
# I like to use this order:
#                         a c d   a b c 
indices = PoolIntArray( [ 0,2,3,  0,1,2 ] )

Then you can build the mesh.

var mi = MeshInstance.new()
add_child(mi)

var mesh = ArrayMesh.new()

var arrays = []
arrays.resize(Mesh.ARRAY_MAX)

arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_VERTEX] = verts
arrays[Mesh.ARRAY_INDEX] = indices

mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh.surface_set_material(0, some_texture)

mi.mesh = mesh

We can then extend this to a cube. Now, we need to keep in mind that a cube probably needs hard edges, or else the lighting will have a smooth transition from one face to the other, as if the edge was rounded. This is probably good for terrains, but usually not convenient for voxels. That means we shouldn't merge the vertices at the edges, and so, for a voxel you'll need 4 vertices per face (or 6 if you're not using indices), for a total of 24 vertices.

I usually go with the kind of layout below, but again, this is down to personal preference.

You may notice the vertex order ABCD or EFGH are facing inward (clockwise). This is because I was doing inverted cubes for the player to walk inside. But you
can get an outward cube with the same layout by just reversing the order of the vertices. (Though it might make more sense to change the order to more sensibly fit your use case.)

#      Y
#      |
#      o ---- X
#     /
#    Z    A ------------ B
#         /|           /|
#       F ------------ E|
#        | |          | |
#        |D ----------|- C
#        |/           |/
#       G ------------ H

var a = Vector3(0, 1, 0)
var b = Vector3(1, 1, 0)
var c = Vector3(1, 0, 0)
var d = Vector3(0, 0, 0)   # D is at the origin
var e = Vector3(1, 1, 1)   # E is opposite of origin
var f = Vector3(0, 1, 1)
var g = Vector3(0, 0, 1)
var h = Vector3(1, 0, 1)

# lay out all 6 faces
# (for an outward cube (voxel), use the order that's commented below)
verts = PoolVector3Array( [
    a, b, c, d,    # b, a, d, c,
    e, f, g, h,    # f, e, h, g,
    f, a, d, g,    # a, f, g, d,
    b, e, h, c,    # e, b, c, h,
    f, e, b, a,    # a, b, e, f,
    d, c, h, g,    # g, h, c, d,
] )

var uv_a = Vector2(0,0)
var uv_b = Vector2(1,0)
var uv_c = Vector2(1,1)
var uv_d = Vector2(0,1)

# lay out the uvs
uvs = PoolVector2Array( [
    uv_a, uv_b, uv_c, uv_d,    # for the UVs, there's
    uv_a, uv_b, uv_c, uv_d,    # nothing much to it
    uv_a, uv_b, uv_c, uv_d,    # in this case
    uv_a, uv_b, uv_c, uv_d,    # just abcd, abcd...
    uv_a, uv_b, uv_c, uv_d,
    uv_a, uv_b, uv_c, uv_d,
] )

# now describe the triangles for each of the faces
add_indices(  0,  1,  2,  3 ) # North (Z axis)
add_indices(  4,  5,  6,  7 ) # South
add_indices(  8,  9, 10, 11 ) # West  (X axis)
add_indices( 12, 13, 14, 15 ) # East
add_indices( 16, 17, 18, 19 ) # Top   (Y axis)
add_indices( 20, 21, 22, 23 ) # Bottom

# ... now build the mesh as above

I'm using an add_indices() function there just to make my life easier:

func add_indices(a, b, c, d): # these are 'int' indices, not vertices
    # the West face, or example, points to the vertices [8,10,11,  8,9,10]
    indices += PoolIntArray([a,c,d,  a,b,c]) 

If you really want to merge all the vertices in a cube, you could do something like this (I can't make sense of the uvs this way, though):

verts = PoolVector3Array( [
    a,b,c,d,    # b,a,d,c,
    e,f,g,h    # f,e,h,g,
] )

uvs = PoolVector2Array( [
    uv_a, uv_b, uv_c, uv_d,    
    uv_a, uv_b, uv_c, uv_d,    
] )

# now there's only 8 vertices, in the array positions 0 to 7
create_face( 0,1,2,3 ) # North
create_face( 4,5,6,7 ) # South
create_face( 5,0,3,6 ) # West
create_face( 1,4,7,2 ) # East
create_face( 5,4,1,0 ) # Top
create_face( 3,2,7,6 ) # Bottom

I've also used this kind of stuff with ImmediateGeometry to create visual stuff, like representing vertex locations and bounding boxes in a map editor I was trying to make. (And in the case of the editor, I even made the map itself (walls, floors, etc) out of ImmediateGeometry, because there's no need for collisions in the editor. But I don't know which performs better. EDIT: I tested this later on, and actually a mesh performs better than ImmediateGeometry.)

by (196 points)
edited by

how'd you add normals so it reflects light?

Thanks for this incredibly detailed answer. Very nice!

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.