The last chapter showed every matrix at once. This one slows down and does one thing: it takes the first triangle you drew and makes it turn. Five small steps, and you run the program after each one. First it turns once, then it spins, then you steer it with the keyboard. Along the way you meet the two things that surprise everyone the first time: the triangle looks stretched while it turns, and it turns around the wrong point.
By the end you can
- Turn a shape with
glm::rotateand a uModel matrix in the vertex shader. - Undo the window's stretch with
glm::ortho, so a turning shape keeps its shape. - Spin it at a fixed speed in degrees per second, then pause and steer it with keys.
- Turn it around any axis and any point, and spin it in place or make it orbit.
This chapter does not touch your main.cpp. That file goes on to the cube and the camera. Here you build a small second program, called spin, next to it. You make it once, and Rotating the Cube, two chapters on, keeps growing it.
Add two lines at the end of CMakeLists.txt, like the mathlab program of the Vectors chapter:
1add_executable(spin src/spin.cpp) # NEW: a third program
2target_link_libraries(spin PRIVATE glad glfw glm::glm) # NEW: the same libraries as appThen create src/spin.cpp as a copy of the complete program from the Drawing the Triangle chapter: the coloured triangle, nothing else. Change only the window title, so you can tell the two windows apart, and add the three GLM includes under the others:
1#include <glm/glm.hpp> // NEW: glm::vec3, glm::mat4
2#include <glm/gtc/matrix_transform.hpp> // NEW: glm::rotate, glm::translate, glm::ortho
3#include <glm/gtc/type_ptr.hpp> // NEW: glm::value_ptr
4
5GLFWwindow* window = glfwCreateWindow(1280, 720, "Spin", nullptr, nullptr); // was "GLApp"The GPU moves vertices in the vertex shader, so that is where the turn goes. Give the shader a mat4 uniform called uModel and multiply every position by it:
1#version 460 core
2layout (location = 0) in vec3 aPos;
3layout (location = 1) in vec3 aColor;
4out vec3 vColor;
5uniform mat4 uModel; // NEW: the turn, the same for every vertex
6
7void main() {
8 gl_Position = uModel * vec4(aPos, 1.0); // NEW: was vec4(aPos, 1.0)
9 vColor = aColor;
10}In step 4, right after the program is made, ask once where the uniform lives:
1GLint modelLoc = glGetUniformLocation(shaderProgram, "uModel");Then, in the render loop, build the matrix and send it, between glUseProgram and the draw call:
1glm::mat4 model = glm::rotate(glm::mat4(1.0f), // start from "change nothing"
2 glm::radians(30.0f), // how far: 30ยฐ
3 glm::vec3(0.0f, 0.0f, 1.0f)); // around which axis: z
4
5glUseProgram(shaderProgram);
6glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
7glBindVertexArray(vao);
8glDrawArrays(GL_TRIANGLES, 0, 3);| Part | What it means |
|---|---|
glm::mat4(1.0f) | the identity matrix: it leaves every vertex where it is. glm::rotate returns this matrix with a turn added to it. |
glm::radians(30.0f) | the angle. GLM wants radians, so glm::radians converts: 30ยฐ is 0.524 radians. A positive angle turns counter-clockwise. |
glm::vec3(0.0f, 0.0f, 1.0f) | the axis, the line the triangle turns around. z points out of the screen toward you, so turning around z keeps the triangle flat on the screen, like a clock hand. |
glUniformMatrix4fv(โฆ) | sends the 16 numbers to uModel. Its four arguments are explained in the Transformations + GLM chapter. |
Run it. The triangle has turned to the left, but look closely: it does not look like the same triangle, turned. It looks bent, a little wider and flatter. Follow the top corner, (0, 0.65). Turned by 30ยฐ, it lands at (โ0.325, 0.563). Now count pixels. The window is 1280 pixels wide and 720 high, and NDC runs from โ1 to 1 both ways. So one unit of x is 640 pixels, but one unit of y is only 360.
| Top corner | NDC | Pixels from the centre (x, y) | Distance |
|---|---|---|---|
| before the turn | (0, 0.65) | (0, 234) | 234 px |
| turned 30ยฐ | (โ0.325, 0.563) | (โ208, 203) | 290 px |
| turned 90ยฐ | (โ0.65, 0) | (โ416, 0) | 416 px |
The corner is always 0.65 units from the centre, but on screen it is 234 pixels away when it points up and 416 when it points sideways. The window stretches everything sideways by 640 / 360 = 1.78. It did so in the Triangle chapter too; you just could not tell while nothing turned. Try it in the figure: press spin, and watch the readout.
glm::mat4 projection(1.0f); // none: x = ยฑ1 are the window's side edges glm::mat4 model = glm::rotate(glm::mat4(1.0f), glm::radians(30.0f), glm::vec3(0, 0, 1));
Spin it with no projection and watch the readout: the top corner is 234 px from the centre when it points up, but 416 px when it points sideways, because one unit of x is 640 pixels and one unit of y only 360. The triangle is turning, but it is also being stretched, by a different amount at every angle. Switch on glm::ortho and the distance stays the same all the way round.
The fix is to make the visible area as wide, in units, as the window is in pixels. If y goes from โ1 to 1, x should go from โ1.78 to 1.78. Then one unit is 360 pixels both ways. glm::ortho builds the matrix that does this. It is an orthographic projection, the simple kind from the Transformations + GLM chapter: no perspective, just a box squeezed into NDC. Add a second uniform to the vertex shader:
1uniform mat4 uModel;
2uniform mat4 uProjection; // NEW
3
4void main() {
5 gl_Position = uProjection * uModel * vec4(aPos, 1.0); // NEW: uProjection on the left
6 vColor = aColor;
7}The order matters. Read it from right to left: uModel turns the vertex first, then uProjection fits the result to the window. Next, a second location in step 4, and in the loop the projection, rebuilt every frame from the window's real size:
1GLint projectionLoc = glGetUniformLocation(shaderProgram, "uProjection"); // step 4
2
3// step 5, before glUseProgram:
4int width, height;
5glfwGetFramebufferSize(window, &width, &height);
6float aspect = height > 0 ? (float)width / (float)height : 1.0f; // 1280 / 720 = 1.78
7glm::mat4 projection = glm::ortho(-aspect, aspect, // left and right edges: x = ยฑ1.78
8 -1.0f, 1.0f, // bottom and top edges: y = ยฑ1
9 -1.0f, 1.0f); // near and far: keep z from โ1 to 1
10
11// step 5, after glUseProgram:
12glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));| Part | What it means |
|---|---|
aspect | width รท height. The (float) casts come first, or 1280 / 720 is divided as integers and gives 1. The height > 0 test avoids dividing by 0 when the window is minimised. |
-aspect, aspect | the x values that land on the left and right edges of the window. A wider window shows more of the world instead of stretching it. |
-1.0f, 1.0f | the y values at the bottom and top edges, the same as before. |
-1.0f, 1.0f (last pair) | the depth range to keep. The triangle sits at z = 0, and in step 5 it will turn out of the screen a little, up to z = ยฑ0.65, still inside. |
With numbers: the turned top corner, (โ0.325, 0.563), now lands at x = โ0.325 / 1.78 = โ0.183 in NDC, which is โ117 pixels from the centre, and y = 203 pixels as before. Its distance is 234 pixels again, the same as before the turn.
A fixed 30ยฐ is a picture. To make it move, the angle has to grow while the program runs. The loop already redraws the triangle about 60 times a second, so the angle only has to be different each time. The simplest source of a growing number is the clock: glfwGetTime() returns the seconds since glfwInit. Replace the 30ยฐ line:
1float time = (float)glfwGetTime(); // 0, 0.016, 0.033, โฆ seconds
2glm::mat4 model = glm::rotate(glm::mat4(1.0f),
3 time * glm::radians(90.0f), // 90ยฐ for every second
4 glm::vec3(0.0f, 0.0f, 1.0f));Read time * glm::radians(90.0f) as "seconds ร degrees per second". After 1 second the angle is 90ยฐ, after 2 seconds 180ยฐ, after 4 seconds 360ยฐ: one full turn. The number in glm::radians is the speed. Change it and see:
| Speed | One full turn takes | What you see |
|---|---|---|
| glm::radians(90.0f) | 360 / 90 = 4 s | a calm spin, counter-clockwise |
| glm::radians(360.0f) | 1 s | fast, one turn per second |
| glm::radians(-90.0f) | 4 s | the same spin, clockwise: a negative angle turns the other way |
| glm::radians(6.0f) | 60 s | the second hand of a clock (going backwards) |
The clock cannot be stopped. If you pause the spin for 3 seconds and let it go, time * speed jumps ahead by 270ยฐ at once. To pause, the program needs its own angle: a variable that remembers where the triangle is, and grows by a small step each frame only while it should.
How big a step? A frame does not always last the same time. At 60 frames per second it lasts 1/60 of a second, on a 144 Hz screen 1/144. So measure it: dt is the time since the last frame, and the step is speed ร dt. At 60 Hz that is 90ยฐ ร 0.0167 = 1.5ยฐ per frame; at 144 Hz it is 0.625ยฐ, smaller steps but more of them, so 90ยฐ per second on every machine. Add the state in step 4, just before the loop:
1float angle = 0.0f; // where the triangle is, in radians
2bool spinning = true; // false while paused
3bool spaceWasDown = false; // was Space held during the last frame?
4float lastFrame = (float)glfwGetTime(); // when the last frame startedThen, at the top of the loop, measure the frame, read the keys and move the angle:
1float time = (float)glfwGetTime();
2float dt = time - lastFrame; // how long the last frame took, in seconds
3lastFrame = time;
4
5bool spaceDown = glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS;
6if (spaceDown && !spaceWasDown) // only on the frame the key goes down
7 spinning = !spinning;
8spaceWasDown = spaceDown;
9
10float speed = glm::radians(90.0f); // 90ยฐ per second
11if (spinning) angle += speed * dt;
12if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS) angle += speed * dt; // counter-clockwise
13if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS) angle -= speed * dt; // clockwiseAnd the model matrix uses the angle instead of the clock:
1glm::mat4 model = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(0.0f, 0.0f, 1.0f));| Line | What it does |
|---|---|
dt = time - lastFrame | the length of the last frame. lastFrame is set before the loop, so the first dt is tiny instead of the whole start-up time. |
spaceDown && !spaceWasDown | true only on the first frame of a press. glfwGetKey says "held", and a short press lasts several frames; flipping spinning on every one of them would leave it on or off at random. |
angle += speed * dt | one frame's worth of turning. Holding Left while it spins adds a second step, so it goes twice as fast. Holding Right cancels the spin, and the triangle stands still. |
angle (no glm::radians) | angle is already in radians, because speed was converted once. Do not wrap it in glm::radians again. |
Turning around z keeps the triangle flat on the screen. The other two axes lie in the screen: x points right, y points up. Turning around them flips the triangle like a playing card. Make the axis a variable, next to angle in step 4, and let the X, Y and Z keys pick it:
1glm::vec3 axis(0.0f, 0.0f, 1.0f); // step 4: start with z
2
3// step 5:
4if (glfwGetKey(window, GLFW_KEY_X) == GLFW_PRESS) axis = glm::vec3(1.0f, 0.0f, 0.0f);
5if (glfwGetKey(window, GLFW_KEY_Y) == GLFW_PRESS) axis = glm::vec3(0.0f, 1.0f, 0.0f);
6if (glfwGetKey(window, GLFW_KEY_Z) == GLFW_PRESS) axis = glm::vec3(0.0f, 0.0f, 1.0f);
7
8glm::mat4 model = glm::rotate(glm::mat4(1.0f), angle, axis); // was the fixed z axis| Axis | What you see |
|---|---|
| z (0, 0, 1) | it turns flat on the screen, like a clock hand. |
| y (0, 1, 0) | it turns like a revolving door. At 90ยฐ you see it edge-on: a thin vertical line. At 180ยฐ it is mirrored, with green on the left and red on the right. |
| x (1, 0, 0) | it tips like a seesaw seen from the front. At 90ยฐ it is a thin horizontal line; at 180ยฐ it is upside down. |
Two things to notice. First, it looks flat even while it turns in 3D: one side never gets bigger as it comes toward you. That is the orthographic projection; perspective comes with the cube. Second, halfway round you see the back of the triangle, and it is still drawn. OpenGL draws both sides of a triangle unless you ask it not to; the Face Winding & Culling chapter shows how and why you would.
glm::rotate always turns around the origin, (0, 0, 0). That is why the triangle wobbled in step 3: its middle is not at the origin. The middle of a triangle is the average of its three corners: ((โ0.6 + 0.65 + 0) / 3, (โ0.55 โ 0.45 + 0.65) / 3) = (0.017, โ0.117). Close to (0, 0), so it almost spins in place. Each variation below replaces the model line of step 5. Try one, run it, then try the next.
Turning around any point
To turn around another point, the pivot, do it in three moves: slide the triangle so the pivot sits on the origin, turn, and slide it back. Here the pivot is the top corner, (0, 0.65):
1glm::vec3 pivot(0.0f, 0.65f, 0.0f); // the top corner
2glm::mat4 model(1.0f);
3model = glm::translate(model, pivot); // 3rd: slide the pivot back
4model = glm::rotate(model, angle, axis); // 2nd: turn around the origin
5model = glm::translate(model, -pivot); // 1st: slide the pivot to the originWhy are the comments numbered upwards? Each GLM call multiplies the new matrix on the right, so the line written last is the first to touch the vertex (the Transformations + GLM chapter explains it with a figure). Follow the red corner, (โ0.6, โ0.55), at 90ยฐ:
| Step | The red corner |
|---|---|
| start | (โ0.6, โ0.55) |
| 1st: minus the pivot (0, 0.65) | (โ0.6, โ1.2) |
| 2nd: turn 90ยฐ, (x, y) becomes (โy, x) | (1.2, โ0.6) |
| 3rd: plus the pivot | (1.2, 0.05) |
The top corner stays put and the triangle swings around it like a flag on a pole. Set the pivot to the middle, (0.017, โ0.117, 0), and the wobble of step 3 is gone.
Spinning in place, or going round in a circle
Now move the triangle away from the centre, to x = 0.6, and shrink it to 0.4 times its size so it fits. Write translate first and rotate second, and it spins in place over there:
1glm::mat4 model(1.0f);
2model = glm::translate(model, glm::vec3(0.6f, 0.0f, 0.0f)); // 3rd: move it right
3model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f)); // 2nd: spin
4model = glm::scale(model, glm::vec3(0.4f)); // 1st: 0.4 times the sizeSwap the first two calls. Now the triangle is moved first and turned after, and a turn is always around the origin, so it travels around the centre of the window on a circle of radius 0.6. It always shows the same side to the centre, like the Moon to the Earth:
1glm::mat4 model(1.0f);
2model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f)); // 3rd: swing around the centre
3model = glm::translate(model, glm::vec3(0.6f, 0.0f, 0.0f)); // 2nd: move it right
4model = glm::scale(model, glm::vec3(0.4f)); // 1st: 0.4 times the sizeBoth at once is one more rotate: an orbit on the outside, a spin on the inside. With a quarter of the speed for the orbit, it spins four times for each trip around the centre:
1glm::mat4 model(1.0f);
2model = glm::rotate(model, angle * 0.25f, glm::vec3(0.0f, 0.0f, 1.0f)); // 4th: orbit, slowly
3model = glm::translate(model, glm::vec3(0.6f, 0.0f, 0.0f)); // 3rd: out to radius 0.6
4model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f)); // 2nd: spin in place
5model = glm::scale(model, glm::vec3(0.4f)); // 1st: 0.4 times the sizeWhen you are done, put back the plain line of step 5, glm::rotate(glm::mat4(1.0f), angle, axis). The checkpoint below and Rotating the Cube start from it.
The whole file after step 5. Compared with the Drawing the Triangle program, the changes are marked NEW: the GLM includes, the two matrices in the vertex shader, two uniform locations and the spin state in step 4, and the time, keys and matrices in the loop. The two shader helpers are shortened because they did not change.
1#include <glad/glad.h> // MUST come before glfw3.h
2#include <GLFW/glfw3.h>
3#include <iostream>
4#include <glm/glm.hpp> // NEW
5#include <glm/gtc/matrix_transform.hpp> // NEW
6#include <glm/gtc/type_ptr.hpp> // NEW
7
8// โโ Shaders โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ NEW: two matrices
9const char* vertexShaderSource = R"(#version 460 core
10layout (location = 0) in vec3 aPos;
11layout (location = 1) in vec3 aColor;
12out vec3 vColor;
13uniform mat4 uModel;
14uniform mat4 uProjection;
15void main() {
16 gl_Position = uProjection * uModel * vec4(aPos, 1.0);
17 vColor = aColor;
18})";
19
20const char* fragmentShaderSource = R"(#version 460 core
21in vec3 vColor;
22out vec4 FragColor;
23void main() {
24 FragColor = vec4(vColor, 1.0);
25})";
26
27// โโ Shader helpers (unchanged since First Shaders) โโโโโโโโโโโโโโโโโโโโโโโโโโ
28GLuint compileShader(GLenum type, const char* src) { /* ... */ }
29GLuint makeProgram(const char* vsSrc, const char* fsSrc) { /* ... */ }
30
31void framebufferSizeCallback(GLFWwindow*, int width, int height) {
32 glViewport(0, 0, width, height);
33}
34
35int main() {
36 // โโ 1โ3. Window, GLAD, viewport (unchanged, except the title) โโโโโโโโโโโ
37 if (!glfwInit()) return 1;
38 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
39 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
40 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
41 GLFWwindow* window = glfwCreateWindow(1280, 720, "Spin", nullptr, nullptr);
42 if (!window) { glfwTerminate(); return 1; }
43 glfwMakeContextCurrent(window);
44 glfwSwapInterval(1);
45 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) return 1;
46 int fbw, fbh;
47 glfwGetFramebufferSize(window, &fbw, &fbh);
48 glViewport(0, 0, fbw, fbh);
49 glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
50
51 // โโ 4. One-time setup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
52 // 4a. Shaders โ program (unchanged)
53 GLuint shaderProgram = makeProgram(vertexShaderSource, fragmentShaderSource);
54 if (shaderProgram == 0) { glfwTerminate(); return 1; }
55
56 // 4b. Vertex data โ VAO + VBO (unchanged: the triangle)
57 float vertices[] = {
58 // x y z r g b
59 -0.6f, -0.55f, 0.0f, 1.0f, 0.27f, 0.27f, // vertex 0: red, bottom left
60 0.65f,-0.45f, 0.0f, 0.13f,0.77f, 0.37f, // vertex 1: green, bottom right
61 0.0f, 0.65f, 0.0f, 0.23f,0.51f, 0.96f, // vertex 2: blue, top
62 };
63 GLuint vao, vbo;
64 glGenVertexArrays(1, &vao);
65 glGenBuffers(1, &vbo);
66
67 glBindVertexArray(vao);
68 glBindBuffer(GL_ARRAY_BUFFER, vbo);
69 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
70 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
71 glEnableVertexAttribArray(0);
72 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
73 glEnableVertexAttribArray(1);
74 glBindVertexArray(0);
75
76 // 4c. Matrix uniforms >>> NEW
77 GLint modelLoc = glGetUniformLocation(shaderProgram, "uModel");
78 GLint projectionLoc = glGetUniformLocation(shaderProgram, "uProjection");
79
80 // 4d. The spin, kept between frames
81 float angle = 0.0f; // radians
82 glm::vec3 axis(0.0f, 0.0f, 1.0f); // z: flat on the screen
83 bool spinning = true;
84 bool spaceWasDown = false;
85 float lastFrame = (float)glfwGetTime();
86 // <<< end of NEW
87
88 // โโ 5. The render loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
89 while (!glfwWindowShouldClose(window)) {
90 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
91 glfwSetWindowShouldClose(window, true);
92
93 // >>> NEW: time, keys and the angle
94 float time = (float)glfwGetTime();
95 float dt = time - lastFrame;
96 lastFrame = time;
97
98 bool spaceDown = glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS;
99 if (spaceDown && !spaceWasDown) spinning = !spinning;
100 spaceWasDown = spaceDown;
101
102 float speed = glm::radians(90.0f);
103 if (spinning) angle += speed * dt;
104 if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS) angle += speed * dt;
105 if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS) angle -= speed * dt;
106
107 if (glfwGetKey(window, GLFW_KEY_X) == GLFW_PRESS) axis = glm::vec3(1.0f, 0.0f, 0.0f);
108 if (glfwGetKey(window, GLFW_KEY_Y) == GLFW_PRESS) axis = glm::vec3(0.0f, 1.0f, 0.0f);
109 if (glfwGetKey(window, GLFW_KEY_Z) == GLFW_PRESS) axis = glm::vec3(0.0f, 0.0f, 1.0f);
110 // <<< end of NEW
111
112 glClearColor(0.06f, 0.07f, 0.10f, 1.0f);
113 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
114
115 // >>> NEW: the two matrices, rebuilt every frame
116 int width, height;
117 glfwGetFramebufferSize(window, &width, &height);
118 float aspect = height > 0 ? (float)width / (float)height : 1.0f;
119 glm::mat4 projection = glm::ortho(-aspect, aspect, -1.0f, 1.0f, -1.0f, 1.0f);
120 glm::mat4 model = glm::rotate(glm::mat4(1.0f), angle, axis);
121 // <<< end of NEW
122
123 glUseProgram(shaderProgram);
124 glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); // NEW
125 glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection)); // NEW
126 glBindVertexArray(vao);
127 glDrawArrays(GL_TRIANGLES, 0, 3);
128
129 glfwSwapBuffers(window);
130 glfwPollEvents();
131 }
132
133 // โโ 6. Clean up (unchanged) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
134 glDeleteVertexArrays(1, &vao);
135 glDeleteBuffers(1, &vbo);
136 glDeleteProgram(shaderProgram);
137 glfwTerminate();
138 return 0;
139}| What you see | Likely cause |
|---|---|
| nothing at all, only the clear colour | a matrix uniform that was never set is all zeros, and every vertex lands on one point. Print modelLoc and projectionLoc: โ1 means a misspelled name, or a uniform the shader declares but never uses. |
| the triangle does not move | the matrix is built once, before the loop, or angle never grows: check that spinning starts true and that dt is not always 0 (lastFrame = time must come after dt is computed). |
| it spins madly, or jumps between a few poses | degrees passed as radians: glm::rotate(โฆ, 90.0f, โฆ) turns by 90 radians, about 14 full turns. Use glm::radians. |
| it changes shape as it turns | no projection, or uProjection is not in the shader's multiplication. That is the stretch of step 1. |
| Space sometimes does nothing | spinning is flipped on every frame the key is held, not once per press. Keep spaceWasDown, as in step 4. |
| it disappears when you press a key | the axis became (0, 0, 0). glm::rotate needs a direction; with none, the matrix fills with NaN and nothing is drawn. |
| Mistake | What happens, and the fix |
|---|---|
| angle += speed, without dt | the step is per frame, so the triangle spins 2.4 times faster on a 144 Hz screen than on a 60 Hz one. Multiply by dt to make the speed "per second". |
| Pausing a time-based angle | angle = time * speed cannot stop; it jumps ahead when you resume. Keep your own angle and add speed * dt. |
| uModel * uProjection in the shader | the projection would act first and the turn after it, in the stretched space: the shape changes as it turns again. The matrix that acts first sits next to the vector: uProjection * uModel * pos. |
| Expecting rotate to turn around the shape's middle | it always turns around the origin. Put the pivot on the origin first (translate by โpivot), turn, and move it back. |
| (float)(width / height) | the division is done on integers first and gives 1, so the stretch comes back. Convert before dividing. |
Next, Drawing a Cube builds the cube in main.cpp. Then Rotating the Cube brings it into spin.cpp, turns it around each axis, combines two turns and lets you spin it with the mouse.
Key ideas
- A turn is a matrix in the vertex shader: gl_Position = uProjection * uModel * vec4(aPos, 1.0), with uModel =
glm::rotate(glm::mat4(1.0f), angle, axis). glm::rotatewants radians and turns counter-clockwise for a positive angle;glm::radiansconverts from degrees.- NDC is stretched to the window, so a turning shape changes shape.
glm::ortho(-aspect, aspect, -1, 1, -1, 1)makes one unit the same number of pixels both ways. - angle = time ร speed spins at a fixed speed. To pause or steer, keep your own angle and add speed ร dt every frame.
glm::rotatealways turns around the origin. To turn around a pivot: translate(pivot) ร rotate ร translate(โpivot).- Translate then rotate (in the order written) spins a shape in place; rotate then translate makes it orbit the origin.