Godot Voxel Terrain Tutorial Part 1
Pre-tutorial note
This tutorial uses Markdeep! Markdeep renders to HTML locally on your web browser and this may take a second, so please be patient! 🙂
**Godot Voxel Terrain Tutorial Part 1** # Godot Voxel Terrain Tutorial OverviewIn this tutorial, we'll be going over how to make a voxel terrain system akin to what you can find in popular games like Minecraft. !!! TIP: Tip This tutorial is loosely based off this [Unity tutorial](https://forum.unity.com/threads/tutorial-procedural-meshes-and-voxel-terrain-c.198651/) I went through a long time ago. **Everything here was written from scratch**, but those tutorials were my first experience making voxel terrains and so they have almost certainly influenced the voxel terrain system I built. We'll be using Godot's SurfaceTool extensively for this tutorial, so you are not familiar with the SurfaceTool, I'd highly suggest checking out my [introduction tutorial on using the SurfaceTool](https://randommomentania.com/2018/11/godot-surface-tool-tutorial/) in Godot. **This tutorial is not aimed at showing you how to make a efficient or performance friendly voxel terrain system**, but rather it is designed to be approachable if you have never made a voxel system before. Because of this, the voxel system is written in GDScript and is not fast enough to be used in complex games. In other words, **I would not suggest using the voxel system in this tutorial to try and recreate your own version of Minecraft**. The GDScript voxel terrain system is simply too slow to be usable at that scale. I would instead suggest checking out [Godot Voxel](https://github.com/Zylann/godot_voxel) on GitHub, as its much faster, stable, and uses C++. This tutorial is really only meant to introduce how to use the SurfaceTool in a more complex way and to show how to make a voxel terrain system in Godot using GDScript. !!! NOTE: Note While this tutorial can be completed by beginners, it is recommended to have some Godot experience before tackling this tutorial as it is fairly complex. This tutorial was made using **Godot 3.0.6!** With all that out of the way, let's start making a voxel terrain system in Godot! # Part Overview ## What are we making? Before we start working on making a voxel terrain system, we first need to know what we are trying to create. If you are experienced with voxels, feel free to skip ahead a bit to the explanation of how the system will be setup. ## What a voxel is As [Wikipedia](https://en.wikipedia.org/wiki/Voxel) puts it: > "A voxel represents a value on a regular grid in three dimensional space." > > -- Wikipedia But what does that really mean? Think about how 2D images are stored on a computer. They are composed of tiny colored pixels, that when zoomed out amount to a picture. For example, here's what it looks like if you zoom WAY in to a cobble stone texture: As you can see, each pixel is just a colored square. When zoomed out, these pixels work together to make image that actually looks like something beyond just some colored squares. A image is in essence a voxel but squished into two dimensional space. In fact, if you have ever used a 2D tile map before in your games, you have already used a voxel system, just in two dimensions instead of in three dimensions. We call them tiles instead of voxels, and instead of solid colors like pixels, it is instead a small section of a texture: However, if it is 2D we do not call them voxels. Voxels refers to data stored in three dimensions, not two. For example, this image from MagicaVoxel shows what solid colored voxels look like: And likewise, you can also have textured voxels, which is what we will be using: And just like a image editor edits the values stored in the pixels to get different looks, the same principle applies to voxels. In the case of solid colored voxels, you can get different looks by coloring the voxels with different colors. For the textured voxels, you can change the textures used to get different looks. Another way of thinking about voxels that may be helpful is to think of them like Lego pieces. Individual pieces can be connected to other pieces and these combined creations makes objects and shape that would have otherwise been very hard, if not impossible, to create using the individual Lego pieces. !!! NOTE: Note I would highly suggest reading the [Wikipedia](https://en.wikipedia.org/wiki/Voxel) article! It almost certainly explains voxels better than I could! !!! TIP: Tip Not all voxels have to be cubes either! There is also smooth voxels, marching cube voxels, and more! We'll just be using cube voxels as they are easier to work with, but there are other forms of voxels that give different looks and styles! ## How our system will function Now that we know what a individual voxel is, let's talk about how we are going to make it work in Godot before we actually start working with Godot. The first thing we need to do is figure out how we are going to store our voxel and the information within each voxel. **For this tutorial, we are going to store the voxels in a three dimensional list of integers.** Each of these integers are going to be the voxel's ID, which we will use to get information (like what texture to use) for each type of voxel. The reason we are going to be using integers is two fold: * Using integers uses less bytes than some other methods, like saving each voxel as a class. Because voxels are stored as integers, we will need to make a few functions to get voxel data, like which textures to use, from a integer. This slows things down a little in comparison to storing the voxels as classes, but not too much that it is really noticeable. Depending on your project, you may want to store your voxels differently. * The other reason is that it is much easier to save and load a three dimensional list of integers to a file. In Godot this is not as much of an issue, as Godot has lots of really great ways to save data to a file using the File class, but in other game engines saving voxel data can be much harder. Thankfully, saving a three dimensional list of integers is one of the easier things to save in most every game engine. **We will not be implementing saving/loading in this tutorial**, but if there is interest, I can make a follow up tutorial showing how to save/load voxel data in Godot. ________ The second thing we need to figure out is how we are going to define the voxel world. There are two popular ways of doing this: one single big voxel world or multiple parts of the world stitched together to make a world. Both of these methods have their own strengths and weaknesses. One reason for using a single big voxel world is that it is easier to handle adding and removing voxels, as you do not need to worry about what chunk of the world it is at. However, many times this method will require going through every voxel in the world to update the visuals and physics, which is not really good for performance. There are tricks you can use to get around this, or you can just use a smaller world, but personally I am not a fan of this method simply because it is not too hard to use chunks, and the performance boost (in my opinion) makes using a single big world seem slow in comparison. Chunks on the other hand have the advantage of only needing to update a single chunk when a voxel changes. This means you only need to go through all of the voxels in a small portion of the world to update that portion's visuals and physics. This has the advantage of being better for performance depending on the size of your chunks. Too small of chunks will actually not increase performance any, so the key to using chunks is finding the ideal size for what your project needs. The downside with using chunks is that it makes it harder to calculate where to add/remove voxels to the terrain. Personally, I would suggest using chunks due to it being better for performance, especially on lower end devices. Either way, both solutions above will work just fine because of how we will handle our terrain system. !!! NOTE: Note Minecraft uses voxels and chunks to render their worlds. Due to the popularity of Minecraft, there are now lots of resources on how to make chunk based voxel worlds, both single big worlds and worlds broken down into chunks. Minecraft uses chunks for their terrain, probably for performance reasons, and so we're going to use chunks too for this tutorial. # Making the voxel terrain in Godot Okay, let's jump right in to working on the voxel terrain in Godot. Download the [starter assets HERE](https://drive.google.com/open?id=1tnKMyTCpkzSfeUM6WR2hQZwJ2UQ8Yvar), extract the ZIP file, and open the project up in Godot. !!! TIP: Asset Credits Credits for the assets included in the project are as follows: * 001.hdr
-> [CG Tuts OceanHDRIs Freebie](https://cgi.tutsplus.com/articles/freebie-8-awesome-ocean-hdris--cg-5684) Everything else, unless otherwise noted, was created by TwistedTwigleg specifically for this tutorial! The starter assets includes some textures we can use for the voxels, along with a few already setup scenes. We'll look at each of these scenes in a bit, but first let's take a look at how the everything in the project is laid out. *HDRI_Map
: A folder to hold the HDRI Map used for this tutorial. This just gives the skybox some better visuals. *UI_Assets
: A folder to hold all of the assets (scenes, textures, etc) that we will use for the UI in this tutorial. *Voxel_Terrain_System
: A folder to hold all of the assets (scenes, textures, etc) that we will use for the voxel terrain system. *Voxel_Chunk.tscn
: A scene to hold all of the nodes needed in a single voxel chunk. *2D_Assets
: A folder to hold all of the 2D assets we will need for the voxel terrain system. *Main_Scene.tscn
: The main scene for the Godot project for this tutorial. This is where everything will come together to form the complete project. *Player_Camera.gd
: The script to control the player camera. We will briefly go over this script, as it is not really the focus of the tutorial! *Rigid_Sphere.tscn
: A RigidBody sphere that we will use to demonstrate that the voxel world's collision mesh is setup correctly. We will briefly go over this scene, as it is not really the focus of the tutorial! Alright, now that we have looked at how the project is setup, and roughly how we plan on creating the parts of the voxel terrain, let's actually start creating! ## Making The Voxel World First we need to make the voxel world. The voxel world will store all of the data we need for each of the voxels, and will provide a interface to add and remove voxels using positions within the scene. Open upMain_Scene.tscn
(res://
->Main_Scene.tscn
) if it is not already open. Select theVoxel_World
node and assign a new script calledVoxel_World.gd
. Save it in theVoxel_Terrain_System
folder, and then add the following code toVoxel_World.gd
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
extends Spatial var voxel_dictionary = { "Stone": {"transparent":false, "solid":true, "texture":Vector2(0, 0)}, "Bedrock": {"transparent":false, "solid":true, "texture":Vector2(2, 0)}, "Cobble": {"transparent":false, "solid":true, "texture":Vector2(1, 0)}, "Dirt": {"transparent":false, "solid":true, "texture":Vector2(0, 1)}, "Grass": {"transparent":false, "solid":true, "texture":Vector2(0, 1), "texture_TOP":Vector2(2, 1), "texture_NORTH":Vector2(1, 1), "texture_SOUTH":Vector2(1, 1), "texture_EAST":Vector2(1, 1), "texture_WEST":Vector2(1, 1)}, } var voxel_list = []; export (int) var voxel_texture_size = 96; export (int) var voxel_texture_tile_size = 32; var voxel_texture_unit; var chunk_scene = preload("res://Voxel_Terrain_System/Voxel_Chunk.tscn"); var chunk_holder_node; var VOXEL_UNIT_SIZE = 1; func _ready(): chunk_holder_node = get_node("Chunks"); voxel_texture_unit = 1.0 / (voxel_texture_size / voxel_texture_tile_size); for voxel_name in voxel_dictionary.keys(): voxel_list.append(voxel_name); make_voxel_world(Vector3(4, 1, 4), Vector3(16, 16, 16)); func make_voxel_world(world_size, chunk_size): for child in chunk_holder_node.get_children(): child.queue_free(); for x in range(0, world_size.x): for y in range(0, world_size.y): for z in range(0, world_size.z): var new_chunk = chunk_scene.instance(); chunk_holder_node.add_child(new_chunk); new_chunk.global_transform.origin = Vector3( x * (chunk_size.x * VOXEL_UNIT_SIZE), y * (chunk_size.y * VOXEL_UNIT_SIZE), z * (chunk_size.z * VOXEL_UNIT_SIZE)); new_chunk.voxel_world = self; new_chunk.setup(chunk_size.x, chunk_size.y, chunk_size.z, VOXEL_UNIT_SIZE); print ("Done making voxel world!"); func get_voxel_data_from_string(voxel_name): if (voxel_dictionary.has(voxel_name) == true): return voxel_dictionary[voxel_name]; return null; func get_voxel_data_from_int(voxel_integer): return voxel_dictionary[voxel_list[voxel_integer]]; func get_voxel_int_from_string(voxel_name): return voxel_list.find(voxel_name); func set_world_voxel(position, voxel): var result = false; for chunk in chunk_holder_node.get_children(): result = chunk.set_voxel_at_position(position, voxel); if (result == true): break; # If you want, you can check to see if a voxel was placed or not using the code bellow: """ if (result == true): print ("Voxel successfully placed"); else: print ("Could not place voxel!"); """ |
!!! NOTE: Note Feel free to download the finished project below and work through the code alongside the tutorial if you want. If you have any problems, feedback, or questions, feel free to ask in the comments below! Let's go through how this script works, starting with the class variables. !!! TIP: Tip When I am refer to class variables in Godot, I am referring to variables outside of any/all functions. *voxel_dictionary
: A dictionary that holds all possible voxels. **The dictionary is structured as follows**: * Name of Voxel (for example:Stone
) *transparent
: A boolean for determining whether this voxel is transparent or not. *solid
: A boolean for determining whether this voxel is solid or not. (in other words, whether you can collide with this voxel or not) *texture
: The coordinates of the tile that will be the main texture for this voxel. *texture_NORTH
,texture_SOUTH
,texture_EAST
,texture_WEST
,texture_TOP
,texture_BOTTOM
: If added, these coordinates will override the main texture facing that direction. *voxel_list
: A list that we will use to store the voxels as integers instead of dictionary data. This makes it easier to save/load chunk data. *(note: we are not adding saving/loading this tutorial)* *voxel_texture_size
: The size of the texture that contains the voxel textures. We are assuming that every texture for the voxel terrain system will be a square. (For example, a value of 96 means it will assume the texture size is 96x96 pixels in size) *voxel_texture_tile_size
: The size of the texture tile/face in the voxel texture. As withvoxel_texture_size
, it is assumed that each voxel texture tile/face will be a square. (For example, a value of 32 means it will assume the tile/face size is 32x32 pixels in size) *voxel_texture_unit
: A variable to hold the amount of space each tile/face in the voxel texture will take. This is because UV maps are positioned in a range from 0 -> 1 instead of using pixel measurements. *chunk_scene
: A variable to hold a reference to the chunk scene so we can instance chunks as needed. *chunk_holder_node
: A variable that will hold all of the instanced chunks. This is just for better organization in the remote debugger. *VOXEL_UNIT_SIZE
: The size of each voxel in 3D space. A bigger value will lead to bigger voxels, while a smaller value will lead to smaller voxels. As of right now, this value has to be a integer for the math we are using in the voxel system to work. As you can see, this is quite a few class variables. The most complicated variable is thevoxel_dictionary
variable, and that is mainly due to how we are storing the information within it. Now that we have looked at the class variables, let's start going through the functions, starting with_ready
. ### Going Through_ready
First we get theChunks
node and assign it to thechunk_holder_node
variable. Then we calculate how much space each voxel texture face/tile takes in UV space. To do this, we first figure out how many tiles/faces there are in the texture (voxel_texture_size / voxel_texture_tile_size
). Then we divide1.0
by the amount of tiles/faces in the texture, and that will give us the amount of UV space each tile/face in the texture takes. Next we fillvoxel_list
will the names of each voxel. We do this so we can usevoxel_list
to store voxel data as integers instead of as voxel dictionary data. As mentioned before, this makes it easier to save/load chunks to/from files. Finally, we call themake_voxel_world
function and pass in the size of the world we want to create. The first argument is the amount of chunks we want to create on each axis (in this example, we're making a4
by1
by4
world) and the second arguments is how big we want each chunk to be (in this example, each chunk will hold16
by16
by16
voxels.) ### Going Throughmake_voxel_world
First we go through all of the children nodes inchunk_holder_node
and delete/free them usingqueue_free
. This is because we will be making new chunk nodes, and so we want to remove any old chunks that may have already been spawned. Next we make threefor
loops, each going throughworld_size
, which is a parameter passed into the function, on one axis. By making threefor
loops like this, we are in essence making a three dimensionalfor
loop. For each position within theworld_size
we instance/create a newchunk_scene
chunk and then instance/add it as a child tochunk_holder_node
. Next we set the position of the newly created chunk using thex
,y
, andz
coordinates from thefor
loop multiplied by the size of the voxels multiplied by the amount of voxels in each chunk. This will position the chunks where they are side by side in a grid. Next we give the newly created chunk a reference toVoxel_World.gd
by setting itsvoxel_world
variable to this node,self
. Finally, we tell the chunk to set itself up by calling thesetup
function on the newly created chunk. We pass in the size we want the chunk to be, along with the size of the voxels it needs to create. !!! NOTE: Note Don't worry, we'll go through making the chunks in just a bit! Both the world script and the chunk script are pretty heavily connected and dependent on each other, so we'll need to do both before we'll be able to see any results. ### Going Throughget_voxel_data_from_string
**All that is left really is making some helper functions that will make our lives a lot easier when we are working with the chunks.** These functions are not necessarily needed, but they'll help make our code easier to read and use later. The first of these functions isget_voxel_data_from_string
, which we will use to get voxel data stored withinvoxel_dictionary
from a string. First we check to see ifvoxel_dictionary
has a key with the name passed in,voxel_name
, in the dictionary using thehas
function. If it does, then we return the value stored within the dictionary. If it does not, then we returnnull
. ### Going Throughget_voxel_data_from_int
This is a helper function that will get voxel data using a integer. It gets the data usingvoxel_list
to get the name of the key we need so we can get the voxel data fromvoxel_dictionary
. All we are doing here is usingget_voxel_data_from_string
and passing in the string, the voxel name, stored invoxel_list
at the index positionvoxel_integer
points to. ### Going Throughget_voxel_int_from_string
This is a helper function to get the voxel integer, or ID, using a voxel's name, a string. All we are doing here is returning the result from calling thefind
function. We pass invoxel_name
to thefind
function so that if a string with the same name asvoxel_name
is found withinvoxel_list
, then it will return it's index, while if it cannot find it, it will return-1
. ### Going Throughset_world_voxel
This function will *attempt* to place the passed in voxel,voxel
, at the passed in world coordinates,position
. First, we make a variable calledresult
so we know whether we have successfully placed the voxel. We setresult
tofalse
initially since we have not yet placed the voxel. Next we go through each chunk by going through each of the children nodes inchunk_holder_node
. Then we callset_voxel_at_position
and pass in bothposition
andvoxel
, and assign the result to theresult
variable. Theset_voxel_at_position
function will attempt to place the voxel at the chunk only if the position is within the chunk's boundaries. If the chunk successfully placed the voxel, theset_voxel_at_position
function will returntrue
, while if it cannot place the voxel it will returnfalse
. Next we check to see ifresult
is now equal totrue
. If it is, then the voxel has been successfully placed and we can callbreak
to stop thefor
loop so we do not keep going through the other chunks inchunk_holder_node
. I also included some code that I have enclosed within a comment block. This code will print a message to the console based on whetherresult
wastrue
orfalse
, letting you look at the console to see whether the voxel was able to be placed or not. I have left it enclosed in comments simply because I find it can be a tad distracting and slows down performance a bit when placing many voxels. ## Making The Voxel Chunk So, now we have written all of the code we'll need for the voxel world. Next we need to make the code that will manage everything we need for each individual chunk. This script will need to make the mesh we'll render, the collision mesh we'll need for the physics engine, and will need to handle placing/removing voxels. To handle the creation of both the collision mesh and the mesh we'll render, we are going to use the SurfaceTool. The SurfaceTool is not necessarily the fastest choice, but it gives us control in how we create the meshes for the chunks and is tightly integrated with Godot and GDScript. !!! NOTE: Note If we want better speed and performance, we'd be better off writing the code in C++ and using the ImmediateGeometry node, or something similar, instead of the SurfaceTool. The SurfaceTool is create for creating static meshes that are not intended to change very often, but unfortunately it is not quite fast enough for rapidly changing geometry. First, let's open up the scene we will use for every chunk. Open upVoxel_Chunk.tscn
(res://
->Voxel_Terrain_System
->Voxel_Chunk.tscn
).By default, there is nothing actually to see, as none of the nodes in Voxel_Chunk.tscn
have any default visuals. This is fine, as we will be creating everythingVoxel_Chunk.tscn
needs entirely from Code. Let's take a look at how the scene is setup: *Voxel_Chunk
: A Spatial node to hold all of the nodes needed for each chunk. *MeshInstance
: The MeshInstance node that will render the chunk's mesh, which we will create using the SurfaceTool. *StaticBody
: A StaticBody node that will tell Godot to treat this chunk as a solid physics object. *CollisionShape
The CollisionShape node that will define the physics shape for this chunk, which we will create using the SurfaceTool. Alright, now that we have looked at the scene, let's jump right in to making the code. Select theVoxel_Chunk
node and assign a new script calledVoxel_Chunk.gd
. Save it in theVoxel_Terrain_System
folder, and then add the following: !!! TIP: Tip While normally I would not necessarily suggest copy-pasting code from a tutorial, as you can learn quite a bit by manually typing it in, feel free to copy-paste the code below if you want. It is quite a bit of code, around 360+ lines. We will still be going through the code piece by piece, so you'll not miss anything by copy-pasting the code!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
extends Spatial var voxel_world; var voxels; var chunk_size_x; var chunk_size_y; var chunk_size_z; var voxel_size = 1; var render_mesh; var render_mesh_vertices; var render_mesh_normals; var render_mesh_indices; var render_mesh_uvs; var collision_mesh; var collision_mesh_vertices; var collision_mesh_indices; var mesh_instance; var collision_shape; var surface_tool; func _ready(): mesh_instance = get_node("MeshInstance"); collision_shape = get_node("StaticBody/CollisionShape"); surface_tool = SurfaceTool.new(); func setup(p_chunk_size_x, p_chunk_size_y, p_chunk_size_z, p_voxel_size): chunk_size_x = p_chunk_size_x; chunk_size_y = p_chunk_size_y; chunk_size_z = p_chunk_size_z; voxel_size = p_voxel_size; voxels = []; for x in range(0, chunk_size_x): var row = []; for y in range(0, chunk_size_y): var column = []; for z in range(0, chunk_size_z): column.append(null); row.append(column); voxels.append(row); make_starter_terrain(); func make_starter_terrain(): for x in range(0, chunk_size_x): for y in range(0, chunk_size_y/2): for z in range(0, chunk_size_z): if (y + 1 == chunk_size_y/2): voxels[x][y][z] = voxel_world.get_voxel_int_from_string("Grass"); elif (y >= chunk_size_y/4): voxels[x][y][z] = voxel_world.get_voxel_int_from_string("Dirt"); elif (y == 0): voxels[x][y][z] = voxel_world.get_voxel_int_from_string("Bedrock"); else: voxels[x][y][z] = voxel_world.get_voxel_int_from_string("Stone"); update_mesh(); func update_mesh(): render_mesh_vertices = []; render_mesh_normals = []; render_mesh_indices = []; render_mesh_uvs = []; collision_mesh_vertices = []; collision_mesh_indices = []; for x in range(0, chunk_size_x): for y in range(0, chunk_size_y): for z in range(0, chunk_size_z): make_voxel(x, y, z); # Make the render mesh # ******************** surface_tool.clear(); surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES); for i in range(0, render_mesh_vertices.size()): surface_tool.add_normal(render_mesh_normals[i]); surface_tool.add_uv(render_mesh_uvs[i]); surface_tool.add_vertex(render_mesh_vertices[i]); for i in range(0, render_mesh_indices.size()): surface_tool.add_index(render_mesh_indices[i]); surface_tool.generate_tangents(); render_mesh = surface_tool.commit(); mesh_instance.mesh = render_mesh; # ******************** # Make the collision mesh # ******************** surface_tool.clear(); surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES); for i in range(0, collision_mesh_vertices.size()): surface_tool.add_vertex(collision_mesh_vertices[i]); for i in range(0, collision_mesh_indices.size()): surface_tool.add_index(collision_mesh_indices[i]); collision_mesh = surface_tool.commit(); collision_shape.shape = collision_mesh.create_trimesh_shape(); # ******************** func make_voxel(x, y, z): if (voxels[x][y][z] == null or voxels[x][y][z] == -1): return; if (_get_voxel_in_bounds(x, y+1, z)): if (_check_if_voxel_cause_render(x, y+1, z)): make_voxel_face(x, y, z, "TOP"); else: make_voxel_face(x, y, z, "TOP"); if (_get_voxel_in_bounds(x, y-1, z)): if (_check_if_voxel_cause_render(x, y-1, z)): make_voxel_face(x, y, z, "BOTTOM"); else: make_voxel_face(x, y, z, "BOTTOM"); if (_get_voxel_in_bounds(x+1, y, z)): if (_check_if_voxel_cause_render(x+1, y, z)): make_voxel_face(x, y, z, "EAST"); else: make_voxel_face(x, y, z, "EAST"); if (_get_voxel_in_bounds(x-1, y, z)): if (_check_if_voxel_cause_render(x-1, y, z)): make_voxel_face(x, y, z, "WEST"); else: make_voxel_face(x, y, z, "WEST"); if (_get_voxel_in_bounds(x, y, z+1)): if (_check_if_voxel_cause_render(x, y, z+1)): make_voxel_face(x, y, z, "NORTH"); else: make_voxel_face(x, y, z, "NORTH"); if (_get_voxel_in_bounds(x, y, z-1)): if (_check_if_voxel_cause_render(x, y, z-1)): make_voxel_face(x, y, z, "SOUTH"); else: make_voxel_face(x, y, z, "SOUTH"); func _check_if_voxel_cause_render(x, y, z): if (voxels[x][y][z] == null or voxels[x][y][z] == -1): return true; else: var tmp_voxel_data = voxel_world.get_voxel_data_from_int(voxels[x][y][z]); if (tmp_voxel_data.transparent == true or tmp_voxel_data.solid == false): return true; return false; func make_voxel_face(x, y, z, face): var voxel_data = voxel_world.get_voxel_data_from_int(voxels[x][y][z]); var uv_position = voxel_data.texture; x = x * voxel_size; y = y * voxel_size; z = z * voxel_size; if (voxel_data.has("texture_" + face) == true): uv_position = voxel_data["texture_" + face]; if (face == "TOP"): _make_voxel_face_top(x, y, z, voxel_data); elif (face == "BOTTOM"): _make_voxel_face_bottom(x, y, z, voxel_data); elif (face == "EAST"): _make_voxel_face_east(x, y, z, voxel_data); elif (face == "WEST"): _make_voxel_face_west(x, y, z, voxel_data); elif (face == "NORTH"): _make_voxel_face_north(x, y, z, voxel_data); elif (face == "SOUTH"): _make_voxel_face_south(x, y, z, voxel_data); else: print ("ERROR: Unknown face: " + face); return; var v_texture_unit = voxel_world.voxel_texture_unit; render_mesh_uvs.append(Vector2( (v_texture_unit * uv_position.x), (v_texture_unit * uv_position.y) + v_texture_unit)); render_mesh_uvs.append(Vector2( (v_texture_unit * uv_position.x) + v_texture_unit, (v_texture_unit * uv_position.y) + v_texture_unit)); render_mesh_uvs.append(Vector2( (v_texture_unit * uv_position.x) + v_texture_unit, (v_texture_unit * uv_position.y)) ); render_mesh_uvs.append(Vector2( (v_texture_unit * uv_position.x), (v_texture_unit * uv_position.y) )); render_mesh_indices.append(render_mesh_vertices.size() - 4); render_mesh_indices.append(render_mesh_vertices.size() - 3); render_mesh_indices.append(render_mesh_vertices.size() - 1); render_mesh_indices.append(render_mesh_vertices.size() - 3); render_mesh_indices.append(render_mesh_vertices.size() - 2); render_mesh_indices.append(render_mesh_vertices.size() - 1); if (voxel_data.solid == true): collision_mesh_indices.append(render_mesh_vertices.size() - 4); collision_mesh_indices.append(render_mesh_vertices.size() - 3); collision_mesh_indices.append(render_mesh_vertices.size() - 1); collision_mesh_indices.append(render_mesh_vertices.size() - 3); collision_mesh_indices.append(render_mesh_vertices.size() - 2); collision_mesh_indices.append(render_mesh_vertices.size() - 1); func _make_voxel_face_top(x, y, z, voxel_data): render_mesh_vertices.append(Vector3(x, y + voxel_size, z)); render_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z)); render_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z + voxel_size)); render_mesh_vertices.append(Vector3(x, y + voxel_size, z + voxel_size)); render_mesh_normals.append(Vector3(0, 1, 0)); render_mesh_normals.append(Vector3(0, 1, 0)); render_mesh_normals.append(Vector3(0, 1, 0)); render_mesh_normals.append(Vector3(0, 1, 0)); if (voxel_data.solid == true): collision_mesh_vertices.append(Vector3(x, y + voxel_size, z + voxel_size)); collision_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z + voxel_size)); collision_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z)); collision_mesh_vertices.append(Vector3(x, y + voxel_size, z)); func _make_voxel_face_bottom(x, y, z, voxel_data): render_mesh_vertices.append(Vector3(x, y, z + voxel_size)); render_mesh_vertices.append(Vector3(x + voxel_size, y, z + voxel_size)); render_mesh_vertices.append(Vector3(x + voxel_size, y, z)); render_mesh_vertices.append(Vector3(x, y, z)); render_mesh_normals.append(Vector3(0, -1, 0)); render_mesh_normals.append(Vector3(0, -1, 0)); render_mesh_normals.append(Vector3(0, -1, 0)); render_mesh_normals.append(Vector3(0, -1, 0)); if (voxel_data.solid == true): collision_mesh_vertices.append(Vector3(x, y, z + voxel_size)); collision_mesh_vertices.append(Vector3(x + voxel_size, y, z + voxel_size)); collision_mesh_vertices.append(Vector3(x + voxel_size, y, z)); collision_mesh_vertices.append(Vector3(x, y, z)); func _make_voxel_face_north(x, y, z, voxel_data): render_mesh_vertices.append(Vector3(x + voxel_size, y, z + voxel_size)); render_mesh_vertices.append(Vector3(x, y, z + voxel_size)); render_mesh_vertices.append(Vector3(x, y + voxel_size, z + voxel_size)); render_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z + voxel_size)); render_mesh_normals.append(Vector3(0, 0, 1)); render_mesh_normals.append(Vector3(0, 0, 1)); render_mesh_normals.append(Vector3(0, 0, 1)); render_mesh_normals.append(Vector3(0, 0, 1)); if (voxel_data.solid == true): collision_mesh_vertices.append(Vector3(x, y, z + voxel_size)); collision_mesh_vertices.append(Vector3(x + voxel_size, y, z + voxel_size)); collision_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z + voxel_size)); collision_mesh_vertices.append(Vector3(x, y + voxel_size, z + voxel_size)); func _make_voxel_face_south(x, y, z, voxel_data): render_mesh_vertices.append(Vector3(x, y, z)); render_mesh_vertices.append(Vector3(x + voxel_size, y, z)); render_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z)); render_mesh_vertices.append(Vector3(x, y + voxel_size, z)); render_mesh_normals.append(Vector3(0, 0, -1)); render_mesh_normals.append(Vector3(0, 0, -1)); render_mesh_normals.append(Vector3(0, 0, -1)); render_mesh_normals.append(Vector3(0, 0, -1)); if (voxel_data.solid == true): collision_mesh_vertices.append(Vector3(x, y, z)); collision_mesh_vertices.append(Vector3(x + voxel_size, y, z)); collision_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z)); collision_mesh_vertices.append(Vector3(x, y + voxel_size, z)); func _make_voxel_face_east(x, y, z, voxel_data): render_mesh_vertices.append(Vector3(x + voxel_size, y, z)); render_mesh_vertices.append(Vector3(x + voxel_size, y, z + voxel_size)); render_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z + voxel_size)); render_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z)); render_mesh_normals.append(Vector3(1, 0, 0)); render_mesh_normals.append(Vector3(1, 0, 0)); render_mesh_normals.append(Vector3(1, 0, 0)); render_mesh_normals.append(Vector3(1, 0, 0)); if (voxel_data.solid == true): collision_mesh_vertices.append(Vector3(x + voxel_size, y, z + voxel_size)); collision_mesh_vertices.append(Vector3(x + voxel_size, y, z)); collision_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z)); collision_mesh_vertices.append(Vector3(x + voxel_size, y + voxel_size, z + voxel_size)); func _make_voxel_face_west(x, y, z, voxel_data): render_mesh_vertices.append(Vector3(x, y, z + voxel_size)); render_mesh_vertices.append(Vector3(x, y, z)); render_mesh_vertices.append(Vector3(x, y + voxel_size, z)); render_mesh_vertices.append(Vector3(x, y + voxel_size, z + voxel_size)); render_mesh_normals.append(Vector3(-1, 0, 0)); render_mesh_normals.append(Vector3(-1, 0, 0)); render_mesh_normals.append(Vector3(-1, 0, 0)); render_mesh_normals.append(Vector3(-1, 0, 0)); if (voxel_data.solid == true): collision_mesh_vertices.append(Vector3(x, y, z + voxel_size)); collision_mesh_vertices.append(Vector3(x, y, z)); collision_mesh_vertices.append(Vector3(x, y + voxel_size, z)); collision_mesh_vertices.append(Vector3(x, y + voxel_size, z + voxel_size)); func get_voxel_at_position(position): if (position_within_chunk_bounds(position) == true): position = global_transform.xform_inv(position); position.x = floor(position.x / voxel_size); position.y = floor(position.y / voxel_size); position.z = floor(position.z / voxel_size); return voxels[position.x][position.y][position.z]; return null; func set_voxel_at_position(position, voxel): if (position_within_chunk_bounds(position) == true): position = global_transform.xform_inv(position); position.x = floor(position.x / voxel_size); position.y = floor(position.y / voxel_size); position.z = floor(position.z / voxel_size); voxels[position.x][position.y][position.z] = voxel; update_mesh(); return true; return false; func position_within_chunk_bounds(position): if (position.x < global_transform.origin.x + (chunk_size_x * voxel_size) and position.x > global_transform.origin.x): if (position.y < global_transform.origin.y + (chunk_size_y * voxel_size) and position.y > global_transform.origin.y): if (position.z < global_transform.origin.z + (chunk_size_z * voxel_size) and position.z > global_transform.origin.z): return true; return false; func _get_voxel_in_bounds(x,y,z): if (x < 0 || x > chunk_size_x-1): return false; elif (y < 0 || y > chunk_size_y-1): return false; elif (z < 0 || z > chunk_size_z-1): return false; return true; |
This is quiet a bit to go through! Let's start with the class variables first: *voxel_world
: A variable to hold a reference to the voxel world this chunk is a part of. *voxels
: A variable to hold all of the voxels in this chunk. This will be a three dimensional list of integers, where each integer is the ID for a voxel. *chunk_size_x
,chunk_size_y
,chunk_size_z
: Three variables to store the size of the chunk on each of the three dimensions. *voxel_size
: A variable to hold the size of the voxels in this chunk. *render_mesh
A variable to hold the Mesh that will be used to render the visible part of the chunk. *render_mesh_vertices
: A variable to hold all of the vertices that will be used inrender_mesh
. *render_mesh_normals
: A variable to hold all of the normal vectors that will be used inrender_mesh
. *render_mesh_indices
: A variable to hold all of the indices that will be used inrender_mesh
. *render_mesh_uvs
: A variable to hold all of the UV vectors that will be used inrender_mesh
. *collision_mesh
: A variable to hold the Mesh that will be used for the collision geometry. *collision_mesh_vertices
: A variable to hold the vertices that will be used incollision_mesh
. *collision_mesh_indices
: A variable to hold the indices that will be used incollision_mesh
. *mesh_instance
: A variable to hold the MeshInstance node. *collision_shape
: A variable to hold the CollisionShape node. *surface_tool
: A variable to hold the SurfaceTool we will use to make both the mesh for the MeshInstance, and the mesh for the CollisionShape. As you can see, the majority of these variables are used to store information about how we want to create the 3D mesh. This is because we need to store the mesh data in such a way that we can add to it as we need in various functions. We need to do it this way because we do not necessarily know where we need to add geometry, as it can change based on the voxel information invoxels
. Alright, now that we have looked at the class variables, let's take a look at all of the functions! ### Going Through_ready
All we are doing in_ready
is getting the MeshInstance node and the CollisionShape node from the chunk scene and assigned them to the proper variables,mesh_instance
andcollision_shape
respectively. We also make a new SurfaceTool and assign it tosurface_tool
. ### Going Throughsetup
This is the function where we actually setup the chunk so it is ready to be used. The reason we are not setting up the chunk in_ready
is because_ready
is called as soon as a node is added to the scene, but inVoxel_World.gd
we need to pass information to the chunk *after* it has been added to the scene. To get around this, we'll usesetup
to get initialize the chunk. First, we take the passed in arguments,p_chunk_size_x
,p_chunk_size_y
,p_chunk_size_z
, andp_voxel_size
, to their respective class variables,chunk_size_x
,chunk_size_y
,chunk_size_z
, andvoxel_size
. Next, we setvoxels
to an empty list. This will clear any data invoxels
, giving us a blank slate to work with. Next, make afor
loop going from0
tochunk_size_x
. We make a new variable calledrow
and assign it to a empty list. We need to makerow
so we can populate it before adding it tovoxels
. Then we make anotherfor
loop, this time going from0
tochunk_size_y
. This time there is a new variable calledcolumn
and it is assigned to an empty list. The last thing we do in thesefor
loops is make one more loop going from0
tochunk_size_z
, and then for everyz
position, we appendnull
tocolumn
. In this tutorial, a value ofnull
or-1
will be a empty/blank voxel. Once we have addednull
for eachz
position, we appendcolumn
torow
. Once we have added each of the columns for that row, we appendrow
tovoxels
. This effectively makes a three dimensional list with the sizes defined inchunk_size_x
,chunk_size_y
, andchunk_size_z
, that we can access using the following syntax:voxels[x][y][z]
. Finally, after all of thefor
loops, we call a function calledmake_starter_terrain
, which will make a flat surface of voxels in the chunk. ### Going Throughmake_starter_terrain
First we make afor
loop that will go from0
tochunk_size_x
. We then make anotherfor
loop, this time going from0
tochunk_size_y/2
, so it will only go halfway through the chunk's size on the Y axis. Finally, we make anotherfor
loop, going from0
tochunk_size_z
. Using these threefor
loops, we will go through the majority of the voxels in the chunk at each position. First we check to see if they
position of the voxel is at the top half of the chunk. If it is, then we set the voxel at thex
,y
,z
position to aGrass
voxel usingget_voxel_int_from_string
invoxel_world
. if they
position of the voxel is not at the top half, but is still on the top quarter of the voxel, then we set the voxel at thex
,y
,z
position to aDirt
voxel usingget_voxel_int_from_string
invoxel_world
. Finally, if they
position of the voxel is at the very bottom of the chunk, then we set the voxel at thex
,y
,z
position to aBedrock
voxel usingget_voxel_int_from_string
invoxel_world
. Finally, after we have gone through all three of thefor
loops, we call theupdate_mesh
function, which will (re)make the meshes needed for the chunk and update the MeshInstance and CollisionShape nodes. ### Going Throughupdate_mesh
First we clear all of the old render mesh and collision mesh data. We do this by setting all of the class variables related to making either mesh to a empty list. Next we make threefor
loops, going from0
to the chunk size variable for each coordinate. For each voxel in the chunk, we call themake_voxel
function, and pass in thex
,y
, andz
coordinates of the voxel we are wanting to make.make_voxel
will populate the render mesh and collision mesh data with everything that voxel needs if it needs to be rendered. **We will go throughmake_voxel
and the other functions in just a bit!** Next, we call theclear
function insurface_tool
to remove any old data, and then we start making a Mesh inPRIMITIVE_TRIANGLES
mode by calling thebegin
function. After that we make afor
loop going from0
to the size of therender_mesh_vertices
list. For each vertex inrender_mesh_vertices
, we add the normal vector for that vertex stored inrender_mesh_normals
using theadd_normal
function insurface_tool
, the UV map vector/position for that vertex stored inrender_mesh_uvs
using theadd_uv
function insurface_tool
, and then finally we add the vertex itself stored inrender_mesh_vertices
using theadd_vertex
function insurface_tool
. This will add all of the vertices stored withinrender_mesh_vertices
and all of the data we have stored for each vertex in the other class variables. Next we go through every index stored withinrender_mesh_indices
by making afor
loop going from0
to the size of therender_mesh_indices
list. All we are doing in thisfor
loop is adding each index using theadd_index
function insurface_tool
. After that, we tell the surface tool to generate the tangent vectors for each face/quad in the surface tool by calling thegenerate_tangents
function insurface_tool
. The last thing we need to do to make the mesh we'll use for rendering the chunk is call thecommit
function insurface_tool
, which will return the Mesh the surface tool created. We store this mesh in therender_mesh
class variable, and then we assign themesh
variable in the MeshInstance node stored inmesh_instance
torender_mesh
. !!! TIP: Tip If you are wondering why we did not assign the material for the mesh, it is because we will be using the material override property in the MeshInstance node to apply the material. This is not necessarily ideal, but it makes the code a little smaller and since there is already so much going on, I decided to leave it like this. All that is left is making the mesh for the CollisionShape. First call theclear
function insurface_tool
to erase any old data, and then we start making a new Mesh inPRIMITIVE_TRIANGLES
mode by calling thebegin
function. Next we make afor
loop to go through every vertex stored incollision_mesh_vertices
. For each vertex, we call theadd_vertex
function and pass in the vertex position stored incollision_mesh_vertices
. !!! NOTE: Note Unlike withrender_mesh
, we do not need to worry about the normal, UV position, or anything else vertex related for the collision mesh. This is because collision geometry in Godot only cares about the vertices and indices. Then we make anotherfor
loop to go through each index stored withincollision_mesh_indices
. For each index, we just add it tosurface_tool
using theadd_index
function. Finally, we get the Mesh from thesurface_tool
by calling thecommit
function. We assign the mesh tocollision_mesh
, and then assign theshape
property in the CollisionShape node, stored incollision_shape
, to a TriMesh collision shape we create by calling thecreate_trimesh_shape
function on the mesh stored incollision_mesh
. !!! TIP: Tip Unfortunately right now, this is really the only way to make a collision mesh in Godot from code. Perhaps in the future there will be a way to make a TriMesh collision shape directly from code, but until then we have to make a normal Mesh, and then call thecreate_trimesh_shape
function to get the collision shape we can use inCollisionShape
node. ### Going Throughmake_voxel
This function will add all of the data to the mesh, and collision mesh, variables as needed for each individual voxel. First, we check to see if the voxel at the passed inx
,y
,z
position is a null/air voxel. We do this by checking to see if the ID of the voxel at the position is equal tonull
or-1
. If it is, then we justreturn
, as there is nothing to add/create with a null/air voxel. !!! NOTE: Note There are six faces in a cube. We will be defining them as follows: * The face/quad that faces the positive Y axis is the TOP face. * The face/quad that faces the negative Y axis is the BOTTOM face. * The face/quad that faces the positive X axis is the EAST face. * The face/quad that faces the negative X axis is the WEST face. * The face/quad that faces the positive Z axis is the NORTH face. * The face/quad that faces the negative Z axis is the SOUTH face. The following picture hopefully shows what I mean. Each of the letters represents a direction, and the colors are respective to the colors of the handles in the Godot Spatial gizmo.Next we check to see if we need to make the top face of the voxel at the passed in x
,y
,z
coordinates. We first check to see if there is a voxel within the chunk's bounds is above this voxel by calling the_get_voxel_in_bounds
function and passing in the position of the voxel with1
added to they
axis. If there is a voxel within the bounds of the chunk above the voxel at the passed in coordinates, we then check to see if the voxel above this voxel will cause this voxel to be rendered by calling the_check_if_voxel_cause_render
function and passing in the position of the voxel with1
added to they
axis. If the voxel above the current voxel will cause this voxel to be rendered, then we call themake_voxel_face
function and pass in the position of the voxel, along with which face we want to render. In this case, we want to render the top face of the voxel, so we pass inTOP
. If the voxel above this voxel is out of bounds in this chunk,_get_voxel_in_bounds
returnedfalse
, then we callmake_voxel_face
and pass in theTOP
face of the voxel. !!! NOTE: Note This is not ideal for performance, as we will be adding faces at the edges where chunks meet, but checking for surrounding chunks adds complexities that I'd rather avoid in this tutorial, and the few added faces do not amount to much in the long run. We repeat this process for the other five faces in the voxel, making changes as needed. Next we check to see if we need to make theBOTTOM
,EAST
,WEST
,NORTH
, and/orSOUTH
faces of the voxel at the passed in position using the exact same process that we used for theTOP
face, just with some minor changes so it checks for the proper face of the voxel. ### Going Through_check_if_voxel_cause_render
This function will check to see if the voxel at the passed in position would cause nearby voxels to be rendered or not. First, we check to see if the voxel at the passed in position is a null/air voxel by checking to see if the voxel's ID invoxels
is equal tonull
or-1
. If the voxel is a null/air voxel, then we returntrue
. If the voxel is not a null/air voxel, then we get the voxel's data using theget_voxel_data_from_int
function invoxel_world
. We then check to see if the voxel is transparent or if the voxel is not solid by checking thetransparent
andsolid
variables in the returned dictionary. If the voxel is transparent or not solid, then we returntrue
. If none of the other checks have returnedtrue
and we get to the end of the function, we returnfalse
, meaning the voxel will not cause nearby voxels to be rendered. ### Going Throughmake_voxel_face
First we get the voxel data using the voxel ID stored invoxels
at the passed in position. We assign the voxel data for the voxel at the passed in position tovoxel_data
. Next, we get the UV position for the main texture for the voxel by accessing itstexture
property. We assign the UV position to a variable calleduv_position
. Next we multiply thex
,y
, andz
positions byvoxel_size
so the mesh face(s) we create are scaled according tovoxel_size
. After that, we check to see if the voxel at the passe in position has a special texture for the passed inface
. If it does, then we assignuv_position
to the special texture stored invoxel_data
. Then, based on which voxelface
was passed in, we call the function that will make the vertices and indices to make the correct voxel face. We pass in the position of the voxel, and the voxel data. Then we add the UV mapping for the voxel's face. !!! WARNING: Warning The order which you add torender_mesh_uvs
is **important**! When we add vertices torender_mesh_vertices
, we are adding them in the following order: * top-left, top-right, bottom-right, bottom-left. This means we need to add the UV coordinates for the vertices in the same order, otherwise the texture will not be mapped correctly. Next we get the voxel texture unit fromvoxel_world
and assign it to a new variable calledv_texture_unit
. Then we append four positions torender_mesh_uvs
. With each of these positions, we takeuv_position
and multiply it byv_texture_unit
to get the position of the texture tile we want. We then addv_texture_unit
to position the vertices at the proper corners of the texture tile we want to map the face/quad to. After we've added the UV positions, we need to add the indices torender_mesh_indices
so we get two triangles for every face of the voxel. We do this by taking the size of therender_mesh_vertices
list and subtract as needed to get the proper index order that will result in two triangles. !!! WARNING: Warning As with adding UV vectors for the vertices, the order in which we add torender_mesh_indices
is important! The wrong order will either result in no triangles, or triangles not in the order we are expecting and so things like the UV coordinates will need to be adjusted. Finally, if the voxel whose geometry we are adding is solid, then we add the indices tocollision_mesh_indices
so we make solid triangles for the collision geometry. !!! NOTE: Note notice how we are adding the UVs and indices here instead of in the _make_voxel_face functions. This is because regardless of the voxel, we will need to add the UVs and indices. It is easier to add it here outside of those functions, as adding it to the _make_voxel_face functions will add duplicate code and the process is the same for all voxel faces so we can just do it here to save space and time. ### Going Through_make_voxel_face_top
First we add four vertex positions that will make up the top face of the voxel. We are usingvoxel_size
to offset the vertices from left -> right and bottom -> top, so the voxels are the same size asvoxel_size
. !!! NOTE: Note Remember! We are adding vertices in the following order: * top-left, top-right, bottom-right, bottom-left. Next, we add the normal vectors for each of the four vertices. Because we are making the top face of the voxel, we make the normal vectors face the positivey
axis. Finally, if the voxel is solid, we add the four vertices that make up the top face of the voxel tocollision_mesh_vertices
. This is exactly the same process as adding vertices torender_mesh_vertices
and they are in the exact same order. ### Going through_make_voxel_face_bottom
,_make_voxel_face_north
,_make_voxel_face_south
,_make_voxel_face_east
,_make_voxel_face_west
See_make_voxel_face_top
for more information on what is going on here. The process is more or less the same, with just some minor changes to make a different voxel face, really it is just different coordinate vectors and normal vectors. If you have any questions, feel free to ask in the comments below! ### Going Throughget_voxel_at_position
This function will return the voxel at the passed in global position, if it exists. If no voxel exists at the passed in position, it will returnnull
. First, we check to see if the global position is within the chunk's bounds using theposition_within_chunk_bounds
function. If it is, then we convert the position from global space to a position relative to the chunk using thexform_inv
function. This will make the passed in position be relative to the origin of the chunk. Next, we divide the position byvoxel_size
to account for large and small voxels. We thenfloor
the position so that it is a whole number. Finally, we return the voxel ID at the position in thevoxels
list. If the passed in global position is not within the chunk's bounds, in other wordsposition_within_chunk_bounds
returnedfalse
, then we returnnull
. ### Going Throughset_voxel_at_position
This function will try to set the voxel ID at the passed in global position to the passed in voxel ID. If it sets the voxel, it will returntrue
, while it will returnfalse
if it cannot. First we check to see if the passed in global position is within the chunk's bounds using theposition_within_chunk_bounds
function. If the position is within the chunk's bounds, then we convert it so that it is relative to the chunk's position using thexform_inv
function. We then divide the position byvoxel_size
to account for large and small voxels. We thenfloor
the position so that it is awhole
number. Next we set the voxel at the passed in position to the passed in voxel ID. After that we call theupdate_mesh
function so the new voxel is rendered. Finally, we returntrue
since the voxel was placed successfully. If the passed in global position is not within the bounds of the chunk, we returnfalse
. ### Going Throughposition_within_chunk_bounds
This is a helper function that will check if the passed in global position is within the boundaries of this chunk. It will return true if the position is within bounds, and false if it is not. First, we check to see if thex
coordinate of the passed in position is within the bounds of this chunk. We do this by making sure thex
position is more than the global position of the chunk, and less than the global position of the chunk plus the size of the chunk multiplied by the size of the voxels. We repeat this process for they
andz
coordinates. If all three coordinates are within bounds, we returntrue
. If any of the coordinate checks fail, we returnfalse
. ### Going Through_get_voxel_in_bounds
This is a helper function that checks if the position is within the bounds of thevoxels
three dimensional list. First we check to see if thex
position is too small, less than0
, or too large, more thanchunk_size_x-1
. If it is, we returnfalse
. We repeat this process for both they
andz
axis. If we have not returnedfalse
by the time we reach the end of the function, we returntrue
, as the position is within the dimensions of thevoxels
three dimensional list. # Final NotesAlright! Unfortunately, this tutorial is getting very long, and so I'll have to break it out into two parts. Thankfully, we have actually already finished the voxel terrain part of the project! If you run the project right now, you'll find that you have a big flat surface of grass. This is not terribly exciting though, as we cannot interact with our voxel world! However, you can see that indeed the chunks are being rendered correctly, and if you place a physics object, like a RigidBody, in the scene, you'll find that the physics are working correctly as well. In the next part, we will make some scripts that will allow us to explore the voxel world we have created! **[Part 2 HERE!](https://randommomentania.com/2019/01/godot-voxel-terrain-tutorial-part-2/)** !!! NOTE: Note In case you were wondering about what I was saying when I said voxels on the sides of chunks get rendered, if you go into a chunk at look at nearby chunks, you'll find the following sight: These extra faces/quads will never be seen by the player, and so they are technically wasted geometry. However, adding the code to check nearby chunks adds both a lot of complexities to making each voxel, and makes performance not quite as good as all voxels on the edges of chunks have to check other chunks. For this tutorial, the extra faces/quads will not make a huge difference, so we're just going to ignore them! !!! WARNING: Warning If you ever get lost, be sure to read over the code again! If you have any questions, feel free to ask in the comments below! You can access the [finished project for this part here](https://drive.google.com/open?id=1NYK6-V8LGYMAwiE6f17PHj-2xzOZUhD6)! Please read LICENSE.html
orLICENSE.pdf
for details on attribution. **You must include attributions to use some/all of the assets provided**. See theLICENSE
files for details.© RandomMomentania 2019