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.)