Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon

The Third Dimension

Save for later
  • 13 min read
  • 10 Aug 2016

article-image

In this article by Sebastián Di Giuseppe, author of the book, Building a 3D game with LibGDX, describes about how to work in 3 dimensions! For which we require new camera techniques. The third dimension adds a new axis, instead of having just the x and y grid, a slightly different workflow, and lastly new render methods are required to draw our game. We'll learn the very basics of this workflow in this article for you to have a sense of what's coming, like moving, scaling, materials, environment, and some others and we are going to move systematically between them one step at a time.

(For more resources related to this topic, see here.)

The following topics will be covered in this article:

  • Camera techniques
  • Workflow
  • LibGDX's 3D rendering API
  • Math

Camera techniques

The goal of this article is to successfully learn about working with 3D as stated. In order to achieve this we will start at the basics, making a simple first person camera. We will facilitate the functions and math that LibGDX contains.

Since you probably have used LibGDX more than once, you should be familiar with the concepts of the camera in 2D. The way 3D works is more or less the same, except there is a z axis now for the depth . However instead of an OrthographicCamera class, a PerspectiveCamera class is used to set up the 3D environment. Creating a 3D camera is just as easy as creating a 2D camera. The constructor of a PerspectiveCamera class requires three arguments, the field of vision, camera width and camera height. The camera width and height are known from 2D cameras, the field of vision is new.

Initialization of a PerspectiveCamera class looks like this:

float FoV = 67;
PerspectiveCamera camera = new PerspectiveCamera(FoV, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());

The first argument, field of vision, describes the angle the first person camera can see.

third-dimension-img-0

The image above gives a good idea what the field of view is. For first person shooters values up to 100 are used. Higher than 100 confuses the player, and with a lower field of vision the player is bound to see less.

Displaying a texture. We will start by doing something exciting, drawing a cube on the screen!

Drawing a cube

First things first! Let's create a camera. Earlier, we showed the difference between the 2D camera and the 3D camera, so let's put this to use. Start by creating a new class on your main package (ours is com.deeep.spaceglad) and name it as you like.

The following imports are used on our test:

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.VertexAttributes;

import com.badlogic.gdx.graphics.g3d.*;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;

Create a class member called cam of type PerspectiveCamera;

public PerspectiveCamera cam;

Now this camera needs to be initialized and needs to be configured. This will be done in the create method as shown below.

public void create() {
cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
cam.position.set(10f, 10f, 10f);
cam.lookAt(0,0,0);
cam.near = 1f;
cam.far = 300f;
cam.update();
}

In the above code snippet we are setting the position of the camera, and looking towards a point set at 0, 0, 0 . Next up, is getting a cube ready to draw. In 2D it was possible to draw textures, but textures are flat. In 3D, models are used. Later on we will import those models. But we will start with generated models.

LibGDX offers a convenient class to build simple models such as: spheres, cubes, cylinders, and many more to choose from. Let's add two more class members, a Model and a ModelInstance. The Model class contains all the information on what to draw, and the resources that go along with it. The ModelInstance class has information on the whereabouts of the model such as the location rotation and scale of the model.

public Model model;
public ModelInstance instance;

Add those class members. We use the overridden create function to initialize our new class members.

public void create() {
…
ModelBuilder modelBuilder = new ModelBuilder();Material mat = new Material(ColorAttribute.createDiffuse(Color.BLUE));model = modelBuilder.createBox(5, 5, 5, mat, VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal);instance = new ModelInstance(model);
}

We use a ModelBuilder class to create a box. The box will need a material, a color. A material is an object that holds different attributes. You could add as many as you would like. The attributes passed on to the material changes the way models are perceived and shown on the screen. We could, for example, add FloatAttribute.createShininess(8f) after the ColorAttribute class, that will make the box to shine with lights around. There are more complex configurations possible but we will leave that out of the scope for now.

With the ModelBuilder class, we create a box of (5, 5, 5). Then we pass the material in the constructor, and the fifth argument are attributes for the specific box we are creating. We use a bitwise operator to combine a position attribute and a normal attribute. We tell the model that it has a position, because every cube needs a position, and the normal is to make sure the lighting works and the cube is drawn as we want it to be drawn. These attributes are passed down to openGL on which LibGDX is build.

Now we are almost ready for drawing our first cube. Two things are missing, first of all: A batch to draw to. When designing 2D games in LibGDX a SpriteBatch class is used. However since we are not using sprites anymore, but rather models, we will use a ModelBatch class. Which is the equivalent for models. And lastly, we will have to create an environment and add lights to it. For that we will need two more class members:

public ModelBatchmodelBatch;
public Environment environment;

And they are to be initialized, just like the other class members:

public void create() {
    ....
    modelBatch = new ModelBatch();
    environment = new Environment();
    environment.set(new     ColorAttribute(ColorAttribute.AmbientLight,     0.4f, 0.4f, 0.4f, 1f));    environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -    1f, -0.8f, -0.2f));
}

Here we add two lights, an ambient light, which lights up everything that is being drawn (a general light source for all the environment), and a directional light, which has a direction (most similar to a "sun" type of source). In general, for lights, you can experiment directions, colors, and different types. Another type of light would be PointLight and it can be compared to a flashlight.

Both lights start with 3 arguments, for the color, which won't make a difference yet as we don't have any textures. The directional lights constructor is followed by a direction. This direction can be seen as a vector.

Now we are all set to draw our environment and the model in it

@Override
public void render() {
    Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(),     Gdx.graphics.getHeight());
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT |     GL20.GL_DEPTH_BUFFER_BIT);

    modelBatch.begin(cam);
    modelBatch.render(instance, environment);
    modelBatch.end();
}

It directly renders our cube. The ModelBatch catch behaves just like a SpriteBatch, as can be seen if we run it, it has to be started (begin), then ask for it to render and give them the parameters (models and environment in our case), and then make it stop.

We should not forget to release any resources that our game allocated. The model we created allocates memory that should be disposed of.

@Override 
public void dispose() {
    model.dispose();
}

third-dimension-img-1

Now we can look at our beautiful cube! It's only very static and empty. We will add some movement to it in our next subsection!

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $19.99/month. Cancel anytime

Translation

Translating rotating and scaling are a bit different to that of a 2D game. It's slightly more mathematical. The easier part are vectors, instead of a vector2D, we can now use a vector3D, which is essentially the same, just that, it adds another dimension.

Let's look at some basic operations of 3D models. We will use the cube that we previously created.

With translation we are able to move the model along all three the axis.

third-dimension-img-2

Let's create a function that moves our cube along the x axis. We add a member variable to our class to store the position in for now. A Vector3 class.

Vector3 position = new Vector3();
private void movement() {
    instance.transform.getTranslation(position);
    position.x += Gdx.graphics.getDeltaTime();
    instance.transform.setTranslation(position);
}

The above code snippet retrieves the translation, adds the delta time to the x attribute of the translation. Then we set the translation of the ModelInstance. The 3D library returns the translation a little bit different than normally. We pass a vector, and that vector gets adjusted to the current state of the object. We have to call this function every time the game updates. So therefore we put it in our render loop before we start drawing.

@Override
public void render() {
    movement();
    ...
}

It might seem like the cube is moving diagonally, but that's because of the angle of our camera. In fact it's' moving towards one face of the cube. That was easy! It's only slightly annoying that it moves out of bounds after a short while. Therefor we will change the movement function to contain some user input handling.

private void movement() {
    instance.transform.getTranslation(position);
    if(Gdx.input.isKeyPressed(Input.Keys.W)){
        position.x+=Gdx.graphics.getDeltaTime();
    }
    if(Gdx.input.isKeyPressed(Input.Keys.D)){
        position.z+=Gdx.graphics.getDeltaTime();
    }
    if(Gdx.input.isKeyPressed(Input.Keys.A)){
        position.z-=Gdx.graphics.getDeltaTime();
    }
    if(Gdx.input.isKeyPressed(Input.Keys.S)){
        position.x-=Gdx.graphics.getDeltaTime();
    }
    instance.transform.setTranslation(position);
}

The rewritten movement function retrieves our position, updates it based on the keys that are pressed, and sets the translation of our model instance.

Rotation

Rotation is slightly different from 2D. Since there are multiple axes on which we can rotate, namely the x, y, and z axis. We will now create a function to showcase the rotation of the model. First off let us create a function in which  we can rotate an object on all axis

private void rotate() {
    if (Gdx.input.isKeyPressed(Input.Keys.NUM_1))         instance.transform.rotate(Vector3.X,         Gdx.graphics.getDeltaTime() * 100);
    if (Gdx.input.isKeyPressed(Input.Keys.NUM_2))         instance.transform.rotate(Vector3.Y,          Gdx.graphics.getDeltaTime() * 100);     if (Gdx.input.isKeyPressed(Input.Keys.NUM_3))
        instance.transform.rotate(Vector3.Z,         Gdx.graphics.getDeltaTime() * 100); }

And let's not forget to call this function from the render loop, after we call the movement function

@Override
public void render() {
    ...
    rotate();
}

If we press the number keys 1, 2 or 3, we can rotate our model. The first argument of the rotate function is the axis to rotate on. The second argument is the amount to rotate. These functions are to add a rotation. We can also set the value of an axis, instead of add a rotation, with the following function:

instance.transform.setToRotation(Vector3.Z, Gdx.graphics.getDeltaTime() * 100);

However say, we want to set all three axis rotations at the same time, we can't simply call setToRotation function three times in a row for each axis, as they eliminate any other rotation done before that. Luckily LibGDX has us covered with a function that is able to take all three axis.

float rotation;
private void rotate() {
    rotation = (rotation + Gdx.graphics.getDeltaTime() * 100) %     360;
    instance.transform.setFromEulerAngles(0, 0, rotation);
}

The above function will continuously rotate our cube. We face one last problem. We can't seem to move the cube! The setFromEulerAngles function clears all the translation and rotation properties. Lucky for us the setFromEulerAngles returns a Matrix4 type, so we can chain and call another function from it. A function which translates the matrix for example. For that we use the trn(x,y,z) function. Short for translate. Now we can update our rotation function, although it also translates.

instance.transform.setFromEulerAngles(0, 0, rotation).trn(position.x, position.y, position.z);

Now we can set our cube to a rotation, and translate it! These are the most basic operations which we will use a lot throughout the book. As you can see this function does both the rotation and the translation. So we can remove the last line in our movement function

instance.transform.setTranslation(position);

Our latest rotate function looks like the following:

private void rotate() {
    rotation = (rotation + Gdx.graphics.getDeltaTime() * 100) %     360;
    instance.transform.setFromEulerAngles(0, 0,     rotation).trn(position.x, position.y, position.z);
}

The setFromEulerAngles function will be extracted to a function of its own, as it serves multiple purposes now and is not solely bound to our rotate function.

private void updateTransformation(){     instance.transform.setFromEulerAngles(0, 0,     rotation).trn(position.x, position.y,     position.z).scale(scale,scale,scale); }

This function should be called after we've calculated our rotation and translation

public void render() {
        rotate();
        movement();
        updateTransformation();
        ...
}

Scaling

We've almost had all of the transformations we can apply to models. The last one being described in this book is the scaling of a model. LibGDX luckily contains all the required functions and methods for this. Let's extend our previous example and make our box growing and shrinking over time.

We first create a function that increments and subtracts from a scale variable.

boolean increment;float scale = 1;
void scale(){     if(increment) {        scale = (scale + Gdx.graphics.getDeltaTime()/5);        if (scale >= 1.5f)  {            increment = false;        } else {            scale = (scale - Gdx.graphics.getDeltaTime()/5);         if(scale <= 0.5f)             increment = true;        }    }

Now to apply this scaling we can adjust our updateTransformation function to include the scaling.

private void updateTransformation(){
    instance.transform.setFromEulerAngles(0, 0,
    rotation).trn(position.x, position.y,     position.z).scale(scale,scale,scale);
}

Our render method should now include the scaling function as well

public void render() {         rotate();         movement();         scale();         updateTransformation();         ... }

And there you go, we can now successfully move, rotate and scale our cube!

Summary

In this article we learned about the workflow of LibGDX 3D API. We are now able to apply multiple kinds of transformations to a model, and understand the differences between 2D and 3D. We also learned how to apply materials to models, which will change the appearance of the model and lets us create cool effects.

Note that there's plenty more information that you can learn about 3D and a lot of practice to go with it to fully understand it. There's also subjects not covered here, like how to create your own materials, and how to make and use of shaders. There's plenty room for learning and experimenting.

In the next article we will start on applying the theory that's learned in this article, and start working towards an actual game! We will also go more in depth on the environment and lights, as well as collision detection. So plenty to look forward to.

Resources for Article:


Further resources on this subject: