Fine-Tuning Camera Control and Orientation / Culling issues in JS 3D Rendering without OpenGL

  Kiến thức lập trình

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.

New contributor

Mequ Yo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

LEAVE A COMMENT