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_Worldnode and assign a new script calledVoxel_World.gd. Save it in theVoxel_Terrain_Systemfolder, 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 with voxel_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 the voxel_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 the Chunks node and assign it to the chunk_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 divide 1.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 fill voxel_list will the names of each voxel. We do this so we can use voxel_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 the make_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 a 4 by 1 by 4 world) and the second arguments is how big we want each chunk to be (in this example, each chunk will hold 16 by 16 by 16 voxels.)
### Going Through make_voxel_world
First we go through all of the children nodes in chunk_holder_node and delete/free them using queue_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 three for loops, each going through world_size, which is a parameter passed into the function, on one axis. By making three for loops like this, we are in essence making a three dimensional for loop.
For each position within the world_size we instance/create a new chunk_scene chunk and then instance/add it as a child to chunk_holder_node.
Next we set the position of the newly created chunk using the x, y, and z coordinates from the for 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 to Voxel_World.gd by setting its voxel_world variable to this node, self. Finally, we tell the chunk to set itself up by calling the setup 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 Through get_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 is get_voxel_data_from_string, which we will use to get voxel data stored within voxel_dictionary from a string.
First we check to see if voxel_dictionary has a key with the name passed in, voxel_name, in the dictionary using the has function. If it does, then we return the value stored within the dictionary. If it does not, then we return null.
### Going Through get_voxel_data_from_int
This is a helper function that will get voxel data using a integer. It gets the data using voxel_list to get the name of the key we need so we can get the voxel data from voxel_dictionary.
All we are doing here is using get_voxel_data_from_string and passing in the string, the voxel name, stored in voxel_list at the index position voxel_integer points to.
### Going Through get_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 the find function. We pass in voxel_name to the find function so that if a string with the same name as voxel_name is found within voxel_list, then it will return it's index, while if it cannot find it, it will return -1.
### Going Through set_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 called result so we know whether we have successfully placed the voxel. We set result to false initially since we have not yet placed the voxel.
Next we go through each chunk by going through each of the children nodes in chunk_holder_node.
Then we call set_voxel_at_position and pass in both position and voxel, and assign the result to the result variable.
The set_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, the set_voxel_at_position function will return true, while if it cannot place the voxel it will return false.
Next we check to see if result is now equal to true. If it is, then the voxel has been successfully placed and we can call break to stop the for loop so we do not keep going through the other chunks in chunk_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 whether result was true or false, 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 up Voxel_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 everything Voxel_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 the Voxel_Chunk node and assign a new script called Voxel_Chunk.gd. Save it in the Voxel_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_meshA 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_readyAll we are doing in_readyis getting the MeshInstance node and the CollisionShape node from the chunk scene and assigned them to the proper variables,mesh_instanceandcollision_shaperespectively. We also make a new SurfaceTool and assign it tosurface_tool. ### Going ThroughsetupThis 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_readyis because_readyis called as soon as a node is added to the scene, but inVoxel_World.gdwe need to pass information to the chunk *after* it has been added to the scene. To get around this, we'll usesetupto 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 setvoxelsto an empty list. This will clear any data invoxels, giving us a blank slate to work with. Next, make aforloop going from0tochunk_size_x. We make a new variable calledrowand assign it to a empty list. We need to makerowso we can populate it before adding it tovoxels. Then we make anotherforloop, this time going from0tochunk_size_y. This time there is a new variable calledcolumnand it is assigned to an empty list. The last thing we do in theseforloops is make one more loop going from0tochunk_size_z, and then for everyzposition, we appendnulltocolumn. In this tutorial, a value ofnullor-1will be a empty/blank voxel. Once we have addednullfor eachzposition, we appendcolumntorow. Once we have added each of the columns for that row, we appendrowtovoxels. 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 theforloops, we call a function calledmake_starter_terrain, which will make a flat surface of voxels in the chunk. ### Going Throughmake_starter_terrainFirst we make aforloop that will go from0tochunk_size_x. We then make anotherforloop, this time going from0tochunk_size_y/2, so it will only go halfway through the chunk's size on the Y axis. Finally, we make anotherforloop, going from0tochunk_size_z. Using these threeforloops, we will go through the majority of the voxels in the chunk at each position. First we check to see if theyposition of the voxel is at the top half of the chunk. If it is, then we set the voxel at thex,y,zposition to aGrassvoxel usingget_voxel_int_from_stringinvoxel_world. if theyposition 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,zposition to aDirtvoxel usingget_voxel_int_from_stringinvoxel_world. Finally, if theyposition of the voxel is at the very bottom of the chunk, then we set the voxel at thex,y,zposition to aBedrockvoxel usingget_voxel_int_from_stringinvoxel_world. Finally, after we have gone through all three of theforloops, we call theupdate_meshfunction, which will (re)make the meshes needed for the chunk and update the MeshInstance and CollisionShape nodes. ### Going Throughupdate_meshFirst 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 threeforloops, going from0to the chunk size variable for each coordinate. For each voxel in the chunk, we call themake_voxelfunction, and pass in thex,y, andzcoordinates of the voxel we are wanting to make.make_voxelwill populate the render mesh and collision mesh data with everything that voxel needs if it needs to be rendered. **We will go throughmake_voxeland the other functions in just a bit!** Next, we call theclearfunction insurface_toolto remove any old data, and then we start making a Mesh inPRIMITIVE_TRIANGLESmode by calling thebeginfunction. After that we make aforloop going from0to the size of therender_mesh_verticeslist. For each vertex inrender_mesh_vertices, we add the normal vector for that vertex stored inrender_mesh_normalsusing theadd_normalfunction insurface_tool, the UV map vector/position for that vertex stored inrender_mesh_uvsusing theadd_uvfunction insurface_tool, and then finally we add the vertex itself stored inrender_mesh_verticesusing theadd_vertexfunction insurface_tool. This will add all of the vertices stored withinrender_mesh_verticesand all of the data we have stored for each vertex in the other class variables. Next we go through every index stored withinrender_mesh_indicesby making aforloop going from0to the size of therender_mesh_indiceslist. All we are doing in thisforloop is adding each index using theadd_indexfunction 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_tangentsfunction insurface_tool. The last thing we need to do to make the mesh we'll use for rendering the chunk is call thecommitfunction insurface_tool, which will return the Mesh the surface tool created. We store this mesh in therender_meshclass variable, and then we assign themeshvariable in the MeshInstance node stored inmesh_instancetorender_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 theclearfunction insurface_toolto erase any old data, and then we start making a new Mesh inPRIMITIVE_TRIANGLESmode by calling thebeginfunction. Next we make aforloop to go through every vertex stored incollision_mesh_vertices. For each vertex, we call theadd_vertexfunction 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 anotherforloop to go through each index stored withincollision_mesh_indices. For each index, we just add it tosurface_toolusing theadd_indexfunction. Finally, we get the Mesh from thesurface_toolby calling thecommitfunction. We assign the mesh tocollision_mesh, and then assign theshapeproperty in the CollisionShape node, stored incollision_shape, to a TriMesh collision shape we create by calling thecreate_trimesh_shapefunction 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_shapefunction to get the collision shape we can use inCollisionShapenode. ### Going Throughmake_voxelThis 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,zposition is a null/air voxel. We do this by checking to see if the ID of the voxel at the position is equal tonullor-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,zcoordinates. 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_boundsfunction and passing in the position of the voxel with1added to theyaxis. 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_renderfunction and passing in the position of the voxel with1added to theyaxis. If the voxel above the current voxel will cause this voxel to be rendered, then we call themake_voxel_facefunction 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_boundsreturnedfalse, then we callmake_voxel_faceand pass in theTOPface 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/orSOUTHfaces of the voxel at the passed in position using the exact same process that we used for theTOPface, just with some minor changes so it checks for the proper face of the voxel. ### Going Through_check_if_voxel_cause_renderThis 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 invoxelsis equal tonullor-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_intfunction invoxel_world. We then check to see if the voxel is transparent or if the voxel is not solid by checking thetransparentandsolidvariables in the returned dictionary. If the voxel is transparent or not solid, then we returntrue. If none of the other checks have returnedtrueand we get to the end of the function, we returnfalse, meaning the voxel will not cause nearby voxels to be rendered. ### Going Throughmake_voxel_faceFirst we get the voxel data using the voxel ID stored invoxelsat 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 itstextureproperty. We assign the UV position to a variable calleduv_position. Next we multiply thex,y, andzpositions byvoxel_sizeso 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_positionto the special texture stored invoxel_data. Then, based on which voxelfacewas 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_uvsis **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_worldand assign it to a new variable calledv_texture_unit. Then we append four positions torender_mesh_uvs. With each of these positions, we takeuv_positionand multiply it byv_texture_unitto get the position of the texture tile we want. We then addv_texture_unitto 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_indicesso we get two triangles for every face of the voxel. We do this by taking the size of therender_mesh_verticeslist 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_indicesis 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_indicesso 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_topFirst we add four vertex positions that will make up the top face of the voxel. We are usingvoxel_sizeto 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 positiveyaxis. 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_verticesand 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_westSee_make_voxel_face_topfor 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_positionThis 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_boundsfunction. If it is, then we convert the position from global space to a position relative to the chunk using thexform_invfunction. This will make the passed in position be relative to the origin of the chunk. Next, we divide the position byvoxel_sizeto account for large and small voxels. We thenfloorthe position so that it is a whole number. Finally, we return the voxel ID at the position in thevoxelslist. If the passed in global position is not within the chunk's bounds, in other wordsposition_within_chunk_boundsreturnedfalse, then we returnnull. ### Going Throughset_voxel_at_positionThis 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 returnfalseif it cannot. First we check to see if the passed in global position is within the chunk's bounds using theposition_within_chunk_boundsfunction. 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_invfunction. We then divide the position byvoxel_sizeto account for large and small voxels. We thenfloorthe position so that it is awholenumber. Next we set the voxel at the passed in position to the passed in voxel ID. After that we call theupdate_meshfunction so the new voxel is rendered. Finally, we returntruesince 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_boundsThis 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 thexcoordinate of the passed in position is within the bounds of this chunk. We do this by making sure thexposition 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 theyandzcoordinates. If all three coordinates are within bounds, we returntrue. If any of the coordinate checks fail, we returnfalse. ### Going Through_get_voxel_in_boundsThis is a helper function that checks if the position is within the bounds of thevoxelsthree dimensional list. First we check to see if thexposition is too small, less than0, or too large, more thanchunk_size_x-1. If it is, we returnfalse. We repeat this process for both theyandzaxis. If we have not returnedfalseby the time we reach the end of the function, we returntrue, as the position is within the dimensions of thevoxelsthree 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.htmlorLICENSE.pdffor details on attribution. **You must include attributions to use some/all of the assets provided**. See theLICENSEfiles for details.© RandomMomentania 2019

