1
\$\begingroup\$

I was looking over the API for a game engine a friend was developing in her spare time. She made the decision to make positive angles rotate in a clockwise direction. This immediately struck me as odd. My intuition was that angle should be measured counter-clockwise. I envision the typical unit circle when thinking about 2D rotations: enter image description here

Her reasoning for picking this convention was that she was using a "positive y" = "down" coordinate system. From a mathematical sense, it makes sense to me that rotations would be clockwise if you're using this orientation. sin(90 degrees) would give me y = +1, which would be in a downward direction on my screen. So shouldn't I be thinking about angles in a clockwise way?

Why have we arrived at the convention that rotations should be counter-clockwise then, even in engines where positive y is down?

\$\endgroup\$
2
  • 2
    \$\begingroup\$ "This immediately struck me as odd. My intuition was that angle should be measured counter-clockwise" — that may itself be your answer, if you consider that many developers might have the same first reaction as you. ;) \$\endgroup\$
    – DMGregory
    Jan 23 '20 at 17:24
  • \$\begingroup\$ Can you give examples of two game engines where positive is counterclockwise? \$\endgroup\$ Jan 30 '20 at 23:29
3
\$\begingroup\$

Why have we arrived at the convention that rotations should be counter-clockwise then, even in engines where positive y is down?

Have we? Let us try CSS:

const box = document.getElementById("box");

function step(timestamp)
{
    let deg = timestamp/10;
    box.style.transform = "rotate(" + deg + "deg)"; 
    window.requestAnimationFrame(step);
}

window.requestAnimationFrame(step);
#box
{
  width: 100px;
  height: 100px;
  border: 1px solid black;
}
<div id="box"></div>

Hmm... It look clockwise to me.

Let us try with canvas:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

canvas.width = 250;
canvas.height = 250;

const cx = canvas.width/2;
const cy = canvas.height/2;

const x = -50;
const y = -50;
const w = 100;
const h = 100;

function step(timestamp)
{
    let deg = timestamp/10;
    let rad = deg * Math.PI/180;
    
    // erase
    ctx.save();    
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);    
    ctx.restore();
    
    // draw
    ctx.save();
    ctx.translate(cx, cy); // pivot point
    ctx.rotate(rad); // rotate square in radians   
    ctx.fillRect(x, y, w, h);    
    ctx.restore();
    
    window.requestAnimationFrame(step);
}

window.requestAnimationFrame(step);
<canvas id="canvas"></canvas>

Still clockwise.

WebGL?

const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");

const vsSource = `
    attribute vec4 aVertexPosition;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;

    void main() {
      gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
    }
`;

const fsSource = `
    void main() {
      gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
`;

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  return shader;
}

const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);

const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
  },
};

// ---

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

const positions = [
 -1.0,  1.0,
  1.0,  1.0,
 -1.0, -1.0,
  1.0, -1.0,
];
const numComponents = 2;
const componentType = gl.FLOAT;
const vertexCount = 4;

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

// ---

gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clearDepth(1.0);

// ---

function step(timestamp)
{
    let deg = timestamp/10;
    let rad = deg * Math.PI/180;
    
    //erase
    
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    
    //draw
    
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.vertexAttribPointer(
        programInfo.attribLocations.vertexPosition,
        numComponents,
        componentType,
        false,
        0,
        0);
    gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);    
    gl.useProgram(programInfo.program);    
    gl.uniformMatrix4fv(
      programInfo.uniformLocations.projectionMatrix,
      false,      
      [1.0, 0.0, 0.0, 0.0,
       0.0, 1.0, 0.0, 0.0,
       0.0, 0.0, 1.0, 0.0,
       0.0, 0.0, 0.0, 1.0]);       
    gl.uniformMatrix4fv(
      programInfo.uniformLocations.modelViewMatrix,
      false,
      [Math.cos(rad), -Math.sin(rad), 0.0, 0.0,
       Math.sin(rad),  Math.cos(rad), 0.0, 0.0,
       0.0          , 0.0           , 1.0, 0.0,
       0.0          , 0.0           , 0.0, 1.0]);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
    
    window.requestAnimationFrame(step);
}

window.requestAnimationFrame(step);
#canvas
{
  width: 250px;
  height: 250px;
}
<canvas id="canvas"></canvas>

Clockwise too. And I believe I have wrote that rotation matrix as per math convention. You can compare with Rotation Matrix at Wolfram MathWorld or Rotation Matrix at Wikipedia.

I would remind you that web technologies are standard.

So, if your friend is doing the rotation like this, then it is correct. And you are right, this makes sense with a vertical axis going down. We think of angles going counter-clockwise because we think of the vertical axis going up, which is mathematical convention. However, the vertical axis in screen goes down, because historical reasons.

I could have, of course, flipped the vertical axis in the projection matrix. Then the rotation would be counter-clockwise. In fact, it is more or less common that developers do some axis flipping to make it match their expectations.

\$\endgroup\$
3
  • \$\begingroup\$ I feel this is a little bit like the Coriolis effect. Does Water Swirl the Other Way in the Southern Hemisphere?. The Coriolis Effect Test: two hemispheres, one sink. Do object actually rotate the other way in that game engine? \$\endgroup\$
    – Theraot
    Jan 28 '20 at 9:06
  • \$\begingroup\$ In the WebGL one you are setting your own rotation matrix, so it can be whichever way you like. \$\endgroup\$
    – user253751
    Jan 30 '20 at 16:54
  • \$\begingroup\$ I get why CSS and Canvas are following the 2D convention of Y+ = down, but does WebGL actually not use the same default coordinate system as OpenGL? On OpenGL the default coordinate system with a "default" viewport is Y+ = up. \$\endgroup\$
    – Romen
    Jan 30 '20 at 22:25
-1
\$\begingroup\$

Positive angles of rotation follow the “Right Hand Rule.” Place your right hand with palm at origin and fingers along positive x-axis. Curl your fingers toward the positive y-axis. The direction your fingers curl is a positive angle by convention.

\$\endgroup\$
2
  • \$\begingroup\$ Ok, but... Why are rotations in 2D game engines often counter-clockwise = positive systems? \$\endgroup\$
    – Vaillancourt
    Jan 30 '20 at 12:58
  • 2
    \$\begingroup\$ This is neither right-handed nor left-handed: I can perform the gesture you describe with either hand. To have a handedness, we need to specify the direction your thumb is pointing: into the screen or out of the screen. So 3D coordinate systems with an x, y, and Z axis come in left-handed and right-handed versions, following the left-hand and right-hand rule respectively, but this does not directly apply to 2D coordinate systems. \$\endgroup\$
    – DMGregory
    Jan 30 '20 at 14:12

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .