I’m trying to write a 3D “Engine” in JS and I’ve encountered some issues with rendering. I followed this wikipedia article and this other article on graphics pipelines on how to set up the model, view and projection matrices:
Note that I’m using two-dimensional arrays for vectors for now, so multiplying them with matrices is easier, which means that every value in a vector is an array; that’s annoying, but I’ll change that later on.
const near = 0.1, far = 100, fov = 90
const projection = [
[1 / Math.tan(fov / 2) / aspectratio, 0, 0, 0],
[0, 1 / Math.tan(fov / 2), 0, 0],
[0, 0, -(far+near)/(far-near), 0],
[0, 0, -1, 0]
]
let up = [[0], [1], [0], [1]] // universal up-vector
let camera = [[0], [0], [-4], [1]] // camera position
let cameraUp = [[0], [1], [0], [1]] // perpendicular up-vector for camera in case you're looking up
let target = [[0], [0], [0], [1]] // look-at, the point the camera is looking at
let direction = [[0], [0], [-1], [1]] // supposed to be the direction the camera is looking at
let zaxis = normal(minus(camera, target))
let xaxis = normal(cross(cameraUp, zaxis))
let yaxis = cross(zaxis, xaxis)
viewmatrix = [
[xaxis[x], xaxis[y], xaxis[z], -dot(xaxis, camera)],
[yaxis[x], yaxis[y], yaxis[z], -dot(yaxis, camera)],
[zaxis[x], zaxis[y], zaxis[z], -dot(zaxis, camera)],
[0, 0, 0, 1 ]
]
let size = 1
let model = [
[this.size, 0, 0, this.position[x]],
[0, this.size, 0, this.position[y]],
[0, 0, this.size, this.position[z]],
[0, 0, 0, 1 ]
]
This is what I’m doing with these matrices:
use() takes in an array of vertices and multiplies the vertices with the matrix
let points = use(projection, use(viewmatrix, use(model, this.vertices)))
// delete points outside the frustum
let outside = []
points.forEach(v => {
let planes = getFrustumPlanes(projection)
for (let i = 0; i < planes.length; i++) {
if (!vertexOutside(v, planes[i])) break
}
outside.push(v)
})
let divided = outside.map(v => [[v[x] / v[w]], [v[y] / v[w]], [v[z] / v[w]], [1]])
let transformed = divided.map(v => [[width / 2 * (v[x] + 1)], [height / 2 * (v[y] + 1)], [0.5 * v[z] + 0.5], [1]])
[1] For some reason I have to add (width / 2, height / 2) to all the vertices when drawing, even after applying the viewport-transform, otherwise the cube(s) aren’t in the middle of the screen, even if the cube’s position and center is (0, 0, 0) and the camera is looking down the z-axis.
Those pretty much work; when I load 9 cubes next to each other it looks like this:
9 1×1 cubes
[2] The weird thing is that I can see stuff that should be behind me, even though I’m determining whether a vertex is inside the camera frustum with this code:
function vertexOutside(vertex, plane) {
// Calculate the signed distance from the vertex to the plane
var distance = dot(vertex, plane.normal) - plane.distance;
// If the distance is negative, the vertex is outside the frustum plane
return distance < 0;
}
function getFrustumPlanes(projectionMatrix) {
var planes = [];
// Right plane
planes[0] = {
normal: [projectionMatrix[3][0] - projectionMatrix[0][0], projectionMatrix[3][1] - projectionMatrix[0][1], projectionMatrix[3][2] - projectionMatrix[0][2]],
distance: projectionMatrix[3][3] - projectionMatrix[0][3]
};
// Left plane
planes[1] = {
normal: [projectionMatrix[3][0] + projectionMatrix[0][0], projectionMatrix[3][1] + projectionMatrix[0][1], projectionMatrix[3][2] + projectionMatrix[0][2]],
distance: projectionMatrix[3][3] + projectionMatrix[0][3]
};
// Top plane
planes[2] = {
normal: [projectionMatrix[3][0] - projectionMatrix[1][0], projectionMatrix[3][1] - projectionMatrix[1][1], projectionMatrix[3][2] - projectionMatrix[1][2]],
distance: projectionMatrix[3][3] - projectionMatrix[1][3]
};
// Bottom plane
planes[3] = {
normal: [projectionMatrix[3][0] + projectionMatrix[1][0], projectionMatrix[3][1] + projectionMatrix[1][1], projectionMatrix[3][2] + projectionMatrix[1][2]],
distance: projectionMatrix[3][3] + projectionMatrix[1][3]
};
// Near plane
planes[4] = {
normal: [projectionMatrix[3][0] + projectionMatrix[2][0], projectionMatrix[3][1] + projectionMatrix[2][1], projectionMatrix[3][2] + projectionMatrix[2][2]],
distance: projectionMatrix[3][3] + projectionMatrix[2][3]
};
// Far plane
planes[5] = {
normal: [projectionMatrix[3][0] - projectionMatrix[2][0], projectionMatrix[3][1] - projectionMatrix[2][1], projectionMatrix[3][2] - projectionMatrix[2][2]],
distance: projectionMatrix[3][3] - projectionMatrix[2][3]
};
return planes;
}
[3] I’m trying to get the mouse-movement right such that I can look around with it. Right now I’m doing that like this:
window.addEventListener('mousemove', e => {
mouse.dx = e.x - mouse.x
mouse.dy = e.y - mouse.y
mouse.x = e.x
mouse.y = e.y
var sens = 0.2
var rotX = mouse.dx * sens * 0.1
var rotY = mouse.dy * sens * 0.1
const rotYMat = [
[1, 0, 0, 0],
[0, Math.cos(rotY), -Math.sin(rotY), 0],
[0, Math.sin(rotY), Math.cos(rotY), 0],
[0, 0, 0, 1]
]
const rotXMat = [
[Math.cos(rotX), 0, Math.sin(rotX), 0],
[0, 1, 0, 0],
[-Math.sin(rotX), 0, Math.cos(rotX), 0],
[0, 0, 0, 1]
]
direction = mult(rotXMat, direction)
direction = mult(rotYMat, direction)
// this doesn't work yet
// [1] How do I achieve targetting via mouse movement?
target = add(camera, direction)
//cameraUp = normal(cross(target, up))
})
As you’ll see in the full code, this is super janky but I can’t think of any other way except rotating the direction.
Here’s the full code via pastebin.