Implementing a Camera and View Matrix in WebGL
This article explains how to create a Camera class in WebGL, compute view and rotation matrices using lookAt, apply them to render a cube from arbitrary positions, and discusses why parts of the cube may be clipped when the camera is placed at [0.5,0.5,0.5].
In the previous article we covered matrix‑based 3D transformations, but the WebGL code was hard to visualize. This article introduces the concept of a camera, showing how moving a virtual camera around a scene determines what is finally rendered on the screen.
We first create a Camera instance, set its position, and make it look at the origin:
const camera = new Camera();
camera.position.x = 0.5
camera.position.y = 0.5
camera.position.z = 0.5 // set camera position
camera.lookAt([0, 0, 0]) // look at the origin
const matLoc = gl.getUniformLocation(program, 'uMat')
gl.uniformMatrix4fv(matLoc, false, camera.viewMatrix)The camera.viewMatrix is the view matrix generated from the camera’s position and target; applying this matrix to objects yields the camera’s view.
To align the camera with the OpenGL convention (camera at the origin looking down the –Z axis), we perform two steps: translate the camera to the origin and rotate it to face –Z. The translation matrix is built by negating the camera position, and the rotation matrix is derived from the camera’s own axes.
We compute the camera axes as follows:
Z = normalize(cameraPosition - targetPosition)Using an up vector (usually [0, 1, 0]) we obtain the X axis via cross product, then the Y axis via another cross product:
X = normalize(cross(up, Z))
Y = cross(Z, X)These axes form the rotation matrix. Because rotation matrices are orthogonal, their inverse is simply the transpose.
The final view matrix is the product of the rotation matrix R and the translation matrix T (i.e., viewMatrix = R * T).
class Mat4 {
static identity(out = []) {
return Object.assign(out, [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
])
}
static lookAt(eye, target, up, out = []) {
const eyeX = eye[0], eyeY = eye[1], eyeZ = eye[2]
const upX = up[0], upY = up[1], upZ = up[2]
const targetX = target[0], targetY = target[1], targetZ = target[2]
let x0, x1, x2, y0, y1, y2, z0, z1, z2, len
// Z axis (camera direction)
z0 = eyeX - targetX
z1 = eyeY - targetY
z2 = eyeZ - targetZ
len = z0*z0 + z1*z1 + z2*z2
if (len > 0) {
len = 1 / Math.sqrt(len)
z0 *= len; z1 *= len; z2 *= len
} else {
return Mat4.identity(out)
}
// X axis = up × Z
x0 = upY * z2 - upZ * z1
x1 = upZ * z0 - upX * z2
x2 = upX * z1 - upY * z0
len = x0*x0 + x1*x1 + x2*x2
if (len > 0) {
len = 1 / Math.sqrt(len)
x0 *= len; x1 *= len; x2 *= len
} else {
return Mat4.identity(out)
}
// Y axis = Z × X
y0 = z1 * x2 - z2 * x1
y1 = z2 * x0 - z0 * x2
y2 = z0 * x1 - z1 * x0
// Column‑major order for OpenGL
out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0
out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0
out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0
out[12] = -(x0*eyeX + x1*eyeY + x2*eyeZ)
out[13] = -(y0*eyeX + y1*eyeY + y2*eyeZ)
out[14] = -(z0*eyeX + z1*eyeY + z2*eyeZ)
out[15] = 1
return out
}
}
class Vec3 extends Array {
constructor(x = 0, y = x, z = x) { super(x, y, z) }
get x() { return this[0] }
get y() { return this[1] }
get z() { return this[2] }
set x(v) { this[0] = v }
set y(v) { this[1] = v }
set z(v) { this[2] = v }
}
class Camera {
constructor() {
this.position = new Vec3()
this.up = new Vec3(0, 1, 0)
this.viewMatrix = Mat4.identity()
}
lookAt(target) {
Mat4.lookAt(this.position, target, this.up, this.viewMatrix)
}
}Using the Camera class, we render a unit cube centered at the origin. The cube vertices are generated by createBox(), and the rendering pipeline is set up with shaders, attribute buffers, and depth testing:
const gl = createGl()
const program = createProgramFromSource(gl, `
attribute vec4 aPos;
uniform mat4 uMat;
void main() {
gl_Position = uMat * aPos;
}
`, `
precision highp float;
void main() {
gl_FragColor = vec4(gl_FragCoord.zzz, 1);
}
`)
const box = createBox()
const indexBuffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, box.index.value, gl.STATIC_DRAW)
const [posLoc] = createAttrBuffer(gl, program, 'aPos', box.position.value)
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(posLoc)
const camera = new Camera()
camera.position.x = camera.position.y = camera.position.z = 0.5
camera.lookAt([0, 0, 0])
const matLoc = gl.getUniformLocation(program, 'uMat')
gl.uniformMatrix4fv(matLoc, false, camera.viewMatrix)
gl.enable(gl.DEPTH_TEST)
gl.enable(gl.CULL_FACE)
gl.clearColor(0, 0, 0, 0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.drawElements(gl.TRIANGLES, box.index.value.length, gl.UNSIGNED_SHORT, 0)
function createShader(gl, type, source) {
const shader = gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
return shader
}
function createProgramFromSource(gl, vertex, fragment) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertex)
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragment)
const program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
gl.useProgram(program)
return program
}
function createAttrBuffer(gl, program, attr, data) {
const location = gl.getAttribLocation(program, attr)
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
return [location, buffer]
}When the camera is placed at [0.5, 0.5, 0.5], part of the cube disappears because the cube is translated in the opposite direction by 0.5 units, moving some vertices to the edge of the clip space (‑1 to 1). Vertices outside this range are clipped, which explains the missing corners.
The article concludes by noting that the next tutorial will cover extending the view frustum, handling clipping, and implementing perspective projection.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
