[Unity] Voxel terrain generation - 4 - UVs and DatabasesteemCreated with Sketch.

in indiedev •  6 years ago 

 What we have now is chunks with the same texture applied everywhere. Why don't we add more blocks? 

 

Let's create a basic Database!

In my project folder, i created 2 other folders next to my scripts :

- "Type", where i created a script called "Block" : Here, i will store the type of objects that can exists like Blocks, Items, Mobs, ...

- "Databases", where i created a script called "DB_Blocks" : Here i will create databases for my objects, where i store every existing Block, Item, Mob, ... 

 Here is my Script folder : 

 My Constructors folder : 

 My Databases folder : 

 My Types folder : 

 

- Open the "Block" script :

This is where we will setup a Block type object with it's properties

First let's add some variables :

Notice that the script won't depend on MonoBehaviour. We will create instances of it from another script.

- First, we need a name for our block.

- Then two booleans :

- The block is  solid
- The block is transparent

- Then we will give the block it's textures positions in the atlas :

If it is a full block with the same texture on each side, we will give it to the variable "texture".
If it is a multitexture block, each texture will be given in the other variables.

- Finally, to let the others scripts know if it is a multitexture block, we have a boolean.


Now, we will need constructors to create new blocks. We will use multiple constructors.

Let me explain :

When we create an instance of a class, here a block, we will give values to it.

Meaning that when a new block is created in the database it will give a name, a texture, booleans.

Using multiple constructors will help having different types of blocks generated but still called "blocks".

- There will be a constructor for a block with the same texture for each side.
- A constructor for a block with multiple textures.
- A constructor for an empty block (Air).

So, when we create a new block in the database, we give values to it. It's the basic constructor.

If we add more values to the block, the other texture values, it becomes the second constructor.

If we don't give values, it is air.

Here are the three constructors. We give values when they are called and those values are stored into the block instance we created in the database. The air constructor already has values.


-  Now you can go to the "DB_Block" script. We will add blocks to the database. 

 First, we will need a List to contain all the blocks of the database. It will contain Instances of the Block class, meaning each block we add with it's values. 


-  In the Start method, we will generate our block instances and store them in the List : 

 

- First, we create the air block. It is an empty constructor.

- Then, i created a Dirt block :

We give the name : "Dirt"
isSolid : true
isTransparent : false
texture (full) : position 0,0 in the atlas.

 

- Next, the Grass block which has multiple textures :

Name : "Grass"
isSolid : true
isTransparent : false

here, we give three more values, it will automatically switch to the second constructor with 3 texture values :

textureUp : position 1,0
textureSide : position 2,0
textureBot : position 0,0

And we do the same for the Stone block and the Bedrock.

So, when the game starts, all those blocks will be generated in the Database.

Now we have a Database. But what if we need to find a specific block in it?

We will create a new method :

This method will return a Block :

We give it the Block name. It will loop in the list of the database and if the block with the same name exists, it will give the whole Block with it's values. If it doesn't exists or the name is not the same, it will return nothing

 - Save and go to the Block_Cube script, used to generate a cube : 

 

- First, we add a block variable of type Block. It will be the block contained at the new cube position, the one we will work with and get values.

- In the Block_Cube constructor, we will also ask for the Block which will be at this position, the one we will store in the new variable. And give it to the variable from the constructor.

- Now we need a new UV system. I will explain how it works, this won't be the project implementation but just an example :

-  How are the textures calculated? 

 Let me show you with a 4x4 texture atlas : 

 

What we need is one texture in the 4x4 atlas, the texture offset is 1/4, giving the size of one texture in the atlas. 

 Let's use the 0,0 :

This is how UVs are defined :


We start by giving the 1,1 coordinates (with a Vector2, the block texture is a 2D texture pasted onto the quad) and then

0,1

0,0

1,0

So, to calculate the coordinates, let's use the offset we had, 1/4 (One texture on a 4x4)

1,1 will be : (1/4 = 0.25 and 1/4 = 0.25)

0,1 : (0/4 = 0 and 1/4 = 0.25)

0,0 : (0/4 = 0 and 0/4 = 0)

1,0 : (1/4 = 0.25 and 0/4 = 0)

Giving the values :

1,1 = new Vector2(0.25, 0.25)

0,1 = new Vector2(0, 0.25)

0,0 = new Vector2(0, 0)

1,0 = new Vector2(0.25, 0)


- Now that we know how to calculate the values, we will let Unity do it automatically : 

 Create 4 UV possibilities (4 vertices/corners) : 

-  Then, we store the texture we will use for the new quad inside the variables we just created  :

To apply the corresponding texture to the quad, we will check which block it is. When this method is called, when a quad is created, we ask for a block type and give the requested side of the cube it will be.

Here,

- If it is a Grass block and we are creating the Top side, we apply the Grass Top texture.

- If it is the left/right/front/back, we use the Grass Side texture.

- If it is none of them we use the bottom texture. And if it is a full block, we use only the "Texture" variable on each side.

The idea is to look inside the database and get the texture atlas position of the block texture (Example : Dirt texture is 0,0, or Vector2(0,0)).


 I will use this 4x4 texture atlas  :

 Then we can create a method to get the texture we need. We can make a method which needs to know if it has multiple sides or just a method with every block inside and their sides, replacing the database (if we don't have one, like here in this general explanation), like that :

 To explain :

public Vector2 GetTexture(BlockType btype, Cubeside side)

When called, this method will give a Vector2. We give the block and which side of the block and it gives back the corresponding texture.

if(bType == BlockType.GRASS )

If the block called is "Grass" from the enum "BlockType"

if(side == Cubeside.TOP)

In this block, if the side we are looking at is TOP

return new Vector2(1,0);

We answer back "The texture in the atlas, for the top side of the grass block is at 1,0", Vector2(1,0)
After adding every block possibilities, we have a last line :

return new Vector2(0,0);

This is the default texture used if none of the requirements are in the conditions. If the side doesn't exist or the block called doesn't exist.
 

- Finally, we call the method automatically while calculating the texture position inside the atlas : 

 To explain :

float TextureOffset = 1f/4f;

We tell Unity the texture offset, 1 texture in a 4x4 atlas.

Vector2 TextureAtlasPos = GetTexture(btype, side);

Then we find and store the texture of the quad we are creating in a variable.

Vector2 UL = new Vector2(TextureOffset * TextureAtlasPos.x, (TextureOffset * TextureAtlasPos.y) + TextureOffset);

Exactly like i did earlier :1,1 = 1/4 = 0.25 * number of textures on x, (0.25 * nb of textures on y) + 1/4 on y to have the up left vertex

Vector2 uv01 = UL;

And then we store it and the code already existing will take care of the rest.
 

- Now let's do the same in our project :

 Just after we have set the vertices for each face, in the "CreateCubeFace" method, we will add the UVs system. There are a few lines to add here : 

 

- I will use a 4x4 texture, we create an offset of 1/4.

- Then, we will need the texture stored in the block we are creating. Remember that the block is given by the chunk. The chunk finds the block in the Database, gives it to the block generator, here, and now we have the block stored with it's texture values. Also, when this "CreateCubeSide" is called, it doesn't generate the whole cube at once. It is called for each side of it.

We look at the multitexture boolean of the block. If it is a multitexture, we look at the side we are making and for each side, we find the position we have in the atlas.

If the block is not multitexture, we will look at the texture we have in the block.

- Finally, we add UVs to the list again but this time we won't have "1,1", "0,1", ..., instead we have calculated values i explained. Unity will calculate them.


- Now we will change things in the Chunk script : 

 We won't store IDs in the chunk anymore, now it will be blocks with values. 

 But don't forget to change the array initialization : 

 - Now let's change the GenerateVirtualMap method : 

 

Let me explain how this will work :

Now, for each chunk, if we are at World.chunkSize - 1 in the y loop (the top of the chunk),

we find the Grass block in the database and then we store it int the chunk map array.

If we are not at the top of the chunk, we will do the same with the Dirt block.

- Go to the GenerateBlocksMap method :

Remember, here we had a line which was "If the id of the block i am looking at is 1, we make a block".

Now we will have "If the block i am looking at is not air, we generate the block supposed to be here".

if the block is not air, make a new cube at this position which is the cube we previously stored in the chunk map array.

- The last thing to do is in the BlockExistsAtPos method, this is where we checked if there was a block at a position :

Remember, we had a line which said "If the block here is 0, we return false. Because there is air".

 Now we look at the block stored at the position and at it's boolean "isTransparent". If it is transparent, we return False, like "There is no block here, you can draw".

- Go back to Unity and give the Database script to the World object :

/!\ Remember to change your terrain material to use a texture atlas. /!\

Now you can test your game :

 You'll see that each chunk is the same. We will fix this in the next part, when we add noise to the chunks.

It is possible that nothing appears. This is because the chunks are created before the database, so we automatically have no block returned. You should fix it even if it already works because any lag can make a lot of issues here.

- Go to your DB_Blocks script :

 We replace the start method with a public "GenerateDB" method. It will manually build the Database. 

 - Now go to the World script : 

 In the Start method, just after we initialize the chunk dictionary, we generate the Database attached to the world object.

- Now it should work. 

_____________________

BONUS : Some of you may have noticed holes between blocks in the chunk.

 To fix this, go to your camera and disable "MSAA" : 

 This is the MultiSampling Anti Aliasing used by the camera renderer. 

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!