-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorials
In this tutorial, you will start working with the T3D graphics engine, and write code to draw lines and circles.
You should be able to get by with your knowledge of C from previous units, but it would be better to have started Self Study: Maths Refresher.
- Create a new folder called KIT307.
- Navigate to the T3D GitHub page and download or clone the repository into the KIT307 folder.
- Download the repository by pressing the green Code button and select Download ZIP.
- Extract the resulting .zip file into the KIT307 folder.
- Clone the repository by following the guide here.
- Download the repository by pressing the green Code button and select Download ZIP.
- Launch the T3D.sln file from the KIT307/T3D folder using Microsoft Visual Studio.
- Open the Main.cpp file using the Solution Explorer panel.
- This is the primary entry point for the program as it has the main method.
- Currently the main method creates the T3DTest class, calls the run method for T3DTest, deletes T3DTest, and then exits the program.
- This is the primary entry point for the program as it has the main method.
- Run the program by clicking the green play buttton on the top toolbar.
- It can also be run by pressing F5.
- Modify Main.cpp so that it runs Tutorial1 instead of T3DTest.
The application places a 2D overlay over the entire screen, and starts a Task that can be modified to draw onto that overlay.
The overlay is called drawArea and is of type Texture. Texture has a plotPixel method, and that's all we will be using in this tutorial. The screen (and texture) size is currently hard coded at 1024x640, but that may change in the future.
The task is called DrawTask. This class has an init method that currently clears the screen and draws a line using the drawDDALine method. The drawDDALine method works for these parameters, but not for other choices of start and end point. There is also a drawBresLine method that does nothing.
The update method has a commented line to clear the draw area (this can be uncommented for animation), and renderer call to reload the texture so that any new data is uploaded to OpenGL.
Note
This is not a recommended way to do 2D drawing with OpenGL, it just a simple method of testing our drawing algorithms.
Review the Lecture Content
The DDA algorithm was covered in Lecture 02 2D Drawing.pptx.
Make sure that you are familiar with these concepts before continuing. You may also be able use the code in the lecture slides as a starting point.
A great way to start with many programming tasks is to set up a test case. So, let's do that for our drawDDALine algorithm.
- Add some drawDDALine calls to the init method to test all possible configurations of parameters.
- e.g. first point at the bottom left with shallow slope, first point at the bottom left with steep slope, etc.
- There are at least 12 different arrangements that you should test.
- Modify your 12 lines so that they are all draw with a different colour.
- This will help you see which ones are working correctly and which ones are not.
- Confirm that most lines are NOT drawn correctly.
- Using the notes from the lectures, you should be able to work out how to modify the drawDDALine method, so that all tests pass.
- There are two approaches to ensuring that all 12 cases are covered.
- The easiest approach requires selecting between four different for loops.
- But it can be done with just one or two for loops.
- When you have it working, check with your tutor to make sure that you haven't missed anything.
- Change all of your drawDDALine method calls to drawBresLine calls with the same parameters.
- When you run this nothing should be drawn.
- Use your lecture notes (and maybe a bit of Googling) to try and get Bresenham's algorithm working.
Remember!
Bresenham's Algorithm should not use any floating point variables.
Review the Lecture Content
Drawing circles was also covered in Lecture 02 2D Drawing.pptx.
You should be aware of the trigonometry method, and the method that uses Pythagoras.
- Add a drawCircle method that takes a centre, radius and colour as parameters.
- Copy the code from the lectures for drawing a circle using trigonometry.
- Experiment with different radii and different step sizes.
- Is there an ideal step size?
- Modify your code so that you mirror each quadrant to make the algorithm 4x faster.
- If you write a loop to draw many random circles, you should be able to see the speed-up.
- Modify your code so that you mirror each quadrant to make the algorithm 8x faster!
- Modify your code so that you use Pythagoras instead of trigonometry.
- Can you observe any further speedup?
If you still have time, you could try searching for Bresenham's circle algorithm, and try implementing that to see if you can get any further improvement.
For this tutorial, you can continue to use the project you were working on in tutorial 1. If you haven't completed tutorial 1, you should at least get the drawDDALine method working before continuing.
- Start by modifying your code to draw a polygon in the centre of the screen using either of your line drawing methods.
- Also modify your line drawing code so that it checks to make sure pixels are within the draw area before plotting them.
- Just skip pixels that are outside the draw area.
- Move the polygon drawing code from the init method to the update method and un-comment or add the following line at the beginning of update to clear the draw area each frame.
drawArea->clear(Colour(255,255,255,255));
- Create some member variables for the vertices of your polygon and initialise them in the constructor.
- Store these in an array of Vector3's (the Vector3 class is already part of T3D) using homogeneous coordinates.
- Modify the drawing code so that it uses a for loop with the new member variables.
- Test that animation is working by adding a small offset to each x and y coordinate before drawing the lines.
- Now create a Matrix3x3 member variable (Matrix3x3 is also a T3D class) and initialise it in the constructor with values that will achieve the same translation effect.
- Make sure you get the orientation of the matrix correct.
- Test your matrix by using it to transform the coordinates using the Matrix3x3 '*' operator (make sure you get the order right) instead of the manual translation you were using in step 1.
- Change your translation matrix to a scale matrix (or create a new matrix) and confirm that the code still works as expected.
- Try a few different scale matrices to make sure that it works, and maybe try a few random matrices just to confirm that it doesn't work if you mess up.
- Change to a rotation matrix and test your code (use a small angle).
- Notice that the polygon rotates around the origin.
- Perform two translations (using separate matrices) before and after the rotation, to get the polygon to rotate around the centre of the screen.
- Now manually calculate the product of the three matrices and use the calculated matrices to replace the three previous multiplications.
- Skip this if you are running out of time.
- Finally, use the '*' operator for matrices to concatenate your original translate (again, make sure the order is correct), rotate and translate sequence, and use the result instead of your manually calculated matrix.
- You should get the same result.
In this tutorial you will be creating two new Mesh classes for T3D: a pyramid and a cylinder. As usual, you can continue working with your previous T3D project, but you will be creating a new T3D Application.
Note
This is the same process you should follow to set up an application for the assignment!
- Create a new application class called Tutorial3.
- This will be a subclass of WinGLApplication.
- Wrap the class declaration in the header file in a namespace block.
namespace T3D { // ... class declaration ... }
- Add the following line at the beginning of the .cpp file.
- The namespace should be added after the #include section.
using namespace T3D;
- Add an init() method to the class declaration to override the WinGLApplication::init.
bool init();
- Add the following init() definition to the .cpp file.
bool Tutorial3::init(){ WinGLApplication::init(); // more code to come... return true; }
- Add a light to the scene by adding the following code to the init method.
- Ensure it is after the call to WinGLApplication::init
GameObject *lightObj = new GameObject(this); Light *light = new Light(Light::DIRECTIONAL); light->setAmbient(1,1,1); light->setDiffuse(1,1,1); light->setSpecular(1,1,1); lightObj->setLight(light); lightObj->getTransform()-> setLocalRotation(Vector3(-45*Math::DEG2RAD, 70*Math::DEG2RAD, 0)); lightObj->getTransform()->setParent(root);
- Add a camera and a camera controller.
GameObject *camObj = new GameObject(this); renderer->camera = new Camera(Camera::PERSPECTIVE,0.1,500.0,45.0,1.6); camObj->getTransform()->setLocalPosition(Vector3(0,0,20)); camObj->getTransform()->setLocalRotation(Vector3(0,0,0)); camObj->setCamera(renderer->camera); camObj->getTransform()->setParent(root); camObj->addComponent(new KeyboardController());
- And a material.
Material *green = renderer->createMaterial(Renderer::PR_OPAQUE); green->setDiffuse(0,1,0,1);
- Finally, add a cube mesh for testing.
GameObject *cube = new GameObject(this); cube->setMesh(new Cube(1)); cube->setMaterial(green); cube->getTransform()->setLocalPosition(Vector3(0,0,0)); cube->getTransform()->setParent(root); cube->getTransform()->name = "Cube";
- Add any missing #includes at the top of Tutorial3.cpp file to fix compiler errors.
- Modify the Main.cpp file so that it creates and runs your new application.
Note
The application should display the cube without errors at this stage, although you may need to move the camera around using the mouse and WASD on the keyboard to see the cube.
First we will create a very simple shape - a square pyramid.
- Add a new Pyramid class that is a subclass of Mesh.
- You will need to add namespace commands to the header and .cpp file as you did for the Tutorial3 application.
- Add a size parameter to the constructor.
- Copy code from Cube.cpp into Pyramid.cpp.
- The code to be copied is from the beginning of the constructor
(numVerts = 4*6;)
down to, and including, the linecalcNormals();
.
- The code to be copied is from the beginning of the constructor
- In Tutorial3::init(), change your code so that it constructs a pyramid instead of a cube.
- Run the application.
- You should still see a cube.
Now you will modify the Pyramid constructor so that it creates vertices and faces for a Pyramid instead of a cube as described below.
- In the constructor, change the first parameter of initArrays to
4+3*4
.- The pyramid will be using flat shading, which means you will need 4 vertices for the base and 3 each for the other 4 sides.
- The first line of code in the constructor initialises the arrays, with the first parameter being the number of vertices.
- Decide where the vertices should be positioned.
- The point
(0,0,0)
in model space should be the natural origin for the object.- A natural origin for a pyramid could be the centre of the base, the tip of the pyramid, or the centre of the pyramid - you will need to choose the point that seems best to you.
- The point
- Decide how the pyramid should be oriented.
- The best orientation is probably with the base in the x-z plane and pointing in the y-direction.
- Draw a diagram of the pyramid using these decicisons.
- Label each vertex with an id number and its coordinates.
- Using the size parameter.
- Label each vertex with an id number and its coordinates.
- Replace the SetVertex commands in the constructor with appropriate values based on the diagram.
- In the constructor, change the second and third parameters of initArrays to
4
and1
.- The pyarmid will have four faces and one quad.
- Add 5 SetFace commands for the diagram indices.
Note
SetFace is an overloaded function that is used for both tris and quads. In both cases the first parameter is the number of the face that you wish to set.
Remember that we have separate triIndices and quadIndices arrays, so the face numbering for each array is also independent. That means that you will set a quad number 0, and tris 0, 1, 2, 3.
Be Careful!
Make sure that you do NOT delete the following lines from the end of the constructor.// Check vertex and index arrays checkArrays(); // Calculate normals calcNormals();
Running the application should now display a pyramid. If there is a problem with the display then it's most likely an issue with the face indices, in particular their order.
The wireframe view could help with further debugging, it can be toggled with F1 when the program is running.
Hint
If you're having trouble seeing the back or bottom of the pyramid to verify the direction of a face, change the last line of your creation of lightObj to.// Set the light source to come from the camera lightObj->getTransform()->setParent(camObj->getTransform());
Now we will make an object of rotation - a cylinder class. The cylinder will be created with parameters for height, radius and density (this will control the smoothness of the cylinder by creating more or less faces around the circumference)
- Add a new Cylinder class that is a subclass of Mesh.
- Add the namespace as before.
- Add three parameters to the constructor: radius, height, and density.
- In the constructor, change the first parameter of initArrays to
density*2
.- The first parameter is the number of vertices.
- Additional vertices will be needed for the cylinder caps.
- In the constructor, change the third parameter of initArrays to
density
.- The third parameter is the number of quads.
- Add the following code for setting the vertices.
float dTheta = Math::TWO_PI / d; for (int i = 0; i < d; i++) { theta = i * dTheta; float x = r * cos(theta); float z = r * sin(theta); // Top vertex setVertex(i, x, h, z); // Bottom vertex setVertex(d + i, x, -h, z); }
- Add the following code for setting the faces.
for(i = 0; i < d; i++>) { setFace(i, // Face id i, // Current top vertex (i + 1) % d, // Next top vertex (wrapping) d + (i + 1) % d, // Next bottom vertex (wrapping) d + i // Current bottom vertex ); }
- Add the following lines to the end of the method.
// Check vertex and index arrays checkArrays(); // Calculate normals calcNormals();
- Test your code before moving on to the next section.
Try to work out the code to add end caps yourself.
- Draw a diagram of the cylinder.
- The caps should use triangle faces with a center vertex.
- The caps should be flat shaded.
- Change the first parameter of initArrays to add
(density+1)*2
.- This is in addition to the side vertices.
- Change the second parameter of initArrays to
density*2
.- The second parameter is the number of tris.
- Write code for drawing the cylinder caps.
Running the application should now display a cylinder. If there is a problem with the display then it's most likely an issue with the face indices, in particular their order.
The wireframe view could help with further debugging, it can be toggled with F1 when the program is running.
In this tutorial you will be creating a simplified version of the Pixar lamp. This is similar to the type of objects that are required for Assignment 1.
-
Create a new application class called Tutorial4.
- This will be a subclass of WinGLApplication.
-
Follow steps 2 to 6 from 3D Objects - Gettings Started.
-
Modify the Main.cpp file so that it creates and runs your new application.
Note
The Tutorial4 class will need to be updated once the lamp class is created.
First we will create the lamp class as a composite object. This is a useful technique for keeping all the component parts together.
- Add a new Lamp class that is a subclass of GameObject.
- You will need to add the namespace commands to the Lamp.cpp and Lamp.h files.
- Add a T3DApplication* parameter to the constructor.
- Add member variables for the component parts: base, arm1, arm2, baseJoint, elbowJoint, and shadeJoint.
- These should be of the type GameObject*.
Note
Naming a part with the Joint suffix is a good way to remember that the part will be used for animation.
The Lamp.h file should match the following.#pragma once #include "GameObject.h" namespace T3D { class Lamp : public GameObject { public: Lamp(T3DApplication* app); ~Lamp(void); GameObject *base; GameObject *arm1; GameObject *arm2; GameObject *baseJoint; GameObject *elbowJoint; GameObject *shadeJoint; }; }
-
In the constructor, add a cylinder mesh.
- Remember to also include cylinder.h.
Lamp::Lamp(T3DApplication* app):GameObject(app) { setMesh(new Cylinder(.1, .01, 16)); getTransform()->name = "Lamp"; }
-
In Tutorial4.cpp, add a lamp instance to the init() method.
Material *grey = renderer->createMaterial(Renderer::PR_OPAQUE); grey->setDiffuse(0.8, 0.8, 0.9, 1); Lamp* lamp = new Lamp(this); lamp->setMaterial(grey); lamp->getTransform()->setLocalPosition(Vector3(0, 0, 0)); lamp->getTransform()->setParent(root);
-
In Tutorial4.cpp, add a camera controller to the init() method.
// This camera is super fast... you have been warned GameObject* camObj = new GameObject(this); renderer->camera = new Camera(Camera::PERSPECTIVE, 0.1, 500.0, 45.0, 1.6); camObj->getTransform()->setLocalPosition(Vector3(0, 0.5, 3)); camObj->setCamera(renderer->camera); camObj->getTransform()->setParent(root); camObj->addComponent(new KeyboardController());
-
In the constructor, add a cylinder for the base.
base = new GameObject(app); // Note the use of 'app' not 'this' base->setMesh(new Cylinder(.02, .01, 16)); base->getTransform()->setParent(getTransform()); // Attachs to the Lamp transform base->getTransform()->setLocalPosition(Vector3(0, 0.02, 0)); base->getTransform()->name = "Base";
-
In Tutorial4.cpp, add a material for the lamp base component to the init() method.
- Sub-components must have their material separately.
lamp->base->setMaterial(grey);
-
Run the application.
- You should only see the lamp base.
We are going to make the arms of the lamp using the Sweep class. We could use a cube and scale it, but by using the Sweep class we can create a nicely bevelled shape that will look much better.
-
In the constructor, add a sweep profile.
std::vector<Vector3> armProfile; armProfile.push_back(Vector3(0.0f, -0.12f, 0.0f)); armProfile.push_back(Vector3(0.014f, -0.114f, 0.0f)); armProfile.push_back(Vector3(0.02f, -0.1f, 0.0f)); armProfile.push_back(Vector3(0.02f, 0.1f, 0.0f)); armProfile.push_back(Vector3(0.014f, 0.114f, 0.0f)); armProfile.push_back(Vector3(0.0f, 0.12f, 0.0f)); armProfile.push_back(Vector3(-0.014f, 0.114f, 0.0f)); armProfile.push_back(Vector3(-0.02f, 0.1f, 0.0f)); armProfile.push_back(Vector3(-0.02f, -0.1f, 0.0f)); armProfile.push_back(Vector3(-0.014f, -0.114f, 0.0f));
Note
None of the vertices have been duplicated. This creates a smooth shading around the perimeter of the arm profile, giving it a curved appearance.
A sharper appearance could be created by adding some duplicate vertices. -
In Lamp.h, add a SweepPath.
- Remember to also include SweepPath.h.
SweepPath armsp; Transform t;
Note
As the profile is centred around the origin point, scaling the profile to size 0 creates edges that meet at a single point. This closes the sweep similar to the cap of a cylinder.
In this case, the path being created will consist of these steps:- Rotate so that the profile is in YZ plane (from XY plane), scale to 0%, position 1cm in -ve x-direction.
- Scale back up to 90%.
- Scale to 100%, move x-position to -0.75cm.
- Move x-position to +0.75cm.
- Scale to 90%, move to +1cm.
- Scale to 0%.
-
Create the first transform.
t.setLocalPosition(Vector3(-0.01, 0, 0)); t.setLocalRotation(Quaternion(Vector3(0, Math::PI / 2, 0))); t.setLocalScale(Vector3(0, 0, 1.0)); // No need to scale the z-direction as the profile is in the XY plane armsp.addTransform(t);
-
Adjust the scale for the next path instance.
t.setLocalScale(Vector3(0.9, 1, 1.0)); armsp.addTransform(t);
-
Adjust the position and scale for the next path instance.
t.setLocalPosition(Vector3(-0.0075, 0, 0)); t.setLocalScale(Vector3(1, 1, 1.0)); armsp.addTransform(t);
-
Adjust the position for the next path instance.
t.setLocalPosition(Vector3(0.0075, 0, 0)); armsp.addTransform(t);
-
Adjust the position and scale for the next path instance.
t.setLocalPosition(Vector3(0.01, 0, 0)); t.setLocalScale(Vector3(0.9, 1, 1.0)); armsp.addTransform(t);
-
Adjust the scale for the final cap.
t.setLocalScale(Vector3(0, 0, 1.0)); armsp.addTransform(t);
-
Now use the profile and sweep path to create the first arm.
arm1 = new GameObject(app); arm1->setMesh(new Sweep(armProfile, armsp, false)); arm1->getTransform()->setLocalPosition(Vector3(0, 0.2, 0)); // Not correctly positioned yet arm1->getTransform()->setParent(base->getTransform()); // Not correct attachment yet arm1->getTransform()->name = "Arm1";
-
In Tutorial4.cpp, add a material for the first arm component to the init() method.
-
Run the application and see how the arm looks.
- Use the wireframe mode to see how the cap scaling works.
- Note that the shading looks a little off, and lacks a sharp bevel.
-
Correct the vertices by adding duplicate transforms to the sweep path.
- This ensures that each tranform setup (excluding the two caps) will be added to the sweep path twice.
armsp.addTransform(t);
-
Run the application and ensure the arm now looks correct.
Attaching the arms directly to the lamp hierachy would cause them to be centered on the origin point, which would make animating them difficult. Instead we will create two empty game objects to act as joints for the arms.
Note
Any animation will be performed on the joints rather than on the arms.
- In the constructor, create the joints.
baseJoint = new GameObject(app); baseJoint->getTransform()->setParent(base->getTransform()); baseJoint->getTransform()->name = "BaseJoint"; elbowJoint = new GameObject(app); elbowJoint->getTransform()->setLocalPosition(Vector3(0, 0.2, 0)); elbowJoint->getTransform()->setParent(baseJoint->getTransform()); elbowJoint->getTransform()->setLocalRotation(Quaternion(Vector3(Math::PI / 4, 0, 0))); // This rotation just sets a good starting pose elbowJoint->getTransform()->name = "ElbowJoint" shadeJoint = new GameObject(app); shadeJoint->getTransform()->setLocalPosition(Vector3(0, 0.2, 0)); shadeJoint->getTransform()->setParent(elbowJoint->getTransform()); shadeJoint->getTransform()->name = "ShadeJoint";
Note
None of these game objects have meshes attached yet. When meshs have been added they will need their materials set in the init() method. - Attach the first arm to the baseJoint with a 10cm y-offset and create a second arm attached to the elbowJoint.
arm1 = new GameObject(app); arm1->setMesh(new Sweep(armProfile, armsp, false)); arm1->getTransform()->setLocalPosition(Vector3(0, 0.1, 0)); arm1->getTransform()->setParent(baseJoint->getTransform()); arm1->getTransform()->name = "Arm1"; arm2 = new GameObject(app); arm2->setMesh(new Sweep(armProfile, armsp, false)); arm2->getTransform()->setLocalPosition(Vector3(0, 0.1, 0)); arm2->getTransform()->setParent(elbowJoint->getTransform()); arm2->getTransform()->name = "Arm2";
- In Tutorial4.cpp, add a material for the second arm component to the init() method.
Experiment
In the init() method, try adjusting the joint rotations and see what happens.The baseJoint rotates best around the x and y axises, while the elbowJoint looks odd when rotated around any axis other than the x-axis.
lamp->baseJoint->getTransform()->setLocalRotation(Quaternion(Vector3(-Math::PI / > 10, Math::PI / 4, 0))); lamp->elbowJoint->getTransform()->setLocalRotation(Quaternion(Vector(Math::PI / 4, 0, 0)));
The final sweep to be created is the lamp shade. This code is not provided and will need to be created with your understanding of the past sections.
- Define a new sweep profile for the lamp shade.
- The profile should cover half of the cross section and should include both the inside and outside of the shade.
- Some duplicated vertices will be needed where sharp edges are required.
- The diagram is a suggested profile of the lamp shade.
- Orange dots are vertex locations.
- Red dots are vertices that should be duplicated for sharper edges.
- The pivot point of the shade should be at (0, 0);
- Attach the new lamp shade to the shadeJoint.
- In Tutorial4.cpp, add a material for the lamp shade component to the init() method.
TBA
TBA
TBA
TBA