I rattled through a load of stuff today, the brain was working well despite no sleep!
Player laser hit detection
Bizarrely one morning in the week I woke up with the approach to this already in my head. I guess it had been percolating for a while as I haven’t had much time to code this week. My solution is essentially to treat it as a two dimensional problem.
I’m already tracking rotated bounding boxes of the objects in the game world which I implemented to help with the docking process. That being the case the solution I took was to “project” them on to a 2D plane (left most point of the box, top most, etc.) to form a quad:
function createLaserCollisionQuad(ship: ShipInstance) {
const xy = (v: vec3) => vec2.fromValues(v[0], v[1])
const translatedBoundingBox = ship.boundingBox.map((v) => vec3.add(vec3.create(), v, ship.position))
return translatedBoundingBox.reduce(
([leftMost, rightMost, topMost, bottomMost], v) => [
v[0] < leftMost[0] ? xy(v) : leftMost,
v[0] > rightMost[0] ? xy(v) : rightMost,
v[1] > topMost[1] ? xy(v) : topMost,
v[1] < bottomMost[1] ? xy(v) : bottomMost,
],
[vec2.fromValues(10000, 0), vec2.fromValues(-10000, 0), vec2.fromValues(0, -10000), vec2.fromValues(0, 10000)],
)
}
The players laser can be treated as a point - it fires down an axis and depth isn’t relly relavant (I may add a basic distance check) and so we can simply check to see if the “point” is inside the quad.
To do this take the quad and break it into two triangles:
function createTrianglesFromQuad(quad: vec2[]) {
return [
[quad[0], quad[1], quad[2]],
[quad[2], quad[3], quad[0]],
]
}
And now I have two triangles I use the barycentric approach I took in Wolfenstein to determine if the point is in the rectangle:
function isPointInTriangle(point: vec2, [p1, p2, p3]: vec2[]) {
let a =
((p2[1] - p3[1]) * (point[0] - p3[0]) + (p3[0] - p2[0]) * (point[1] - p3[1])) /
((p2[1] - p3[1]) * (p1[0] - p3[0]) + (p3[0] - p2[0]) * (p1[1] - p3[1]))
let b =
((p3[1] - p1[1]) * (point[0] - p3[0]) + (p1[0] - p3[0]) * (point[1] - p3[1])) /
((p2[1] - p3[1]) * (p1[0] - p3[0]) + (p3[0] - p2[0]) * (p1[1] - p3[1]))
let c = 1.0 - a - b
return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1
}
Finally all brought together in a fairly simple piece of code that looks for the nearest hit:
function processLaserHits(game: Game) {
// all we are really interested in for deciding if a player has hit a ship is the intersection of the bounding
// box of the ship onto a 2d plane. That results in a quad that we can then split into two triangles and use
// barycentric co-ordinates to determine if the laser has hit the ship
// this isn't how the original did it - it used some bit tricks basically
const hit = game.localBubble.ships.reduce((hit: ShipInstance | null, ship) => {
if (ship.position[2] > 0) return hit
if (hit !== null && hit.position[2] > ship.position[2]) return hit
const quad = createLaserCollisionQuad(ship)
const triangles = createTrianglesFromQuad(quad)
//const testPoint = game.player.laserOffset
const testPoint = vec2.fromValues(0, 0)
if (isPointInTriangle(testPoint, triangles[0]) || isPointInTriangle(testPoint, triangles[1])) {
return ship
}
return hit
}, null)
if (hit !== null) {
hit.isDestroyed = true
}
}
At the moment ships are immediately blown up by a laser hit - its letting me test things easily.
Enemy lasers
In the original game the lasers are simple lines and I wanted to keep that aesthetic. Lines are simple to draw in 2D but what, exactly, is a line in a 3D world. They tend to be a bit weird if you use them in WebGL (or OpenGL for that matter). I did experiment with both rendering a LINE_STRIP and also tried using triangles to form a shape but that all looked very weird.
In the end I took a 2D approach to the rendering using an orthographic projection. For the “source” end of the laser I calculated, in the TypeScript code (as opposed to the GPU), the location of the ship firing the laser:
export function projectPosition(p: vec3, projectionMatrix: mat4) {
const position = vec4.fromValues(p[0], p[1], p[2], 0)
const projectedPosition = vec4.transformMat4(vec4.create(), position, projectionMatrix)
const x = projectedPosition[0] / projectedPosition[3]
const y = projectedPosition[1] / projectedPosition[3]
const viewportX = ((x + 1) / 2) * dimensions.width
const viewportY = ((1 - y) / 2) * dimensions.mainViewHeight
return vec2.fromValues(viewportX, viewportY)
}
const sourcePosition = projectPosition(ship.position, projectionMatrix)
Its fairly standard 3D to 2D maths. With this done I tried a couple of approaches with the lasers target end. Initially I tried to aim them at the player but that just looked weird and odd - you end up with a line ending in the middle of the screen. Next I tried basically drawing the laser along the nose orientation of the source ship and force it off the screen. This works pretty nicely:
const target3DPosition = vec3.add(
vec3.create(),
ship.position,
vec3.multiply(vec3.create(), vec3.normalize(vec3.create(), ship.noseOrientation), [100, 100, 100]),
)
const targetPosition = projectPosition(target3DPosition, projectionMatrix)
const xGrad = (sourcePosition[0] - targetPosition[0]) / dimensions.width
const yGrad = (sourcePosition[1] - targetPosition[1]) / dimensions.mainViewHeight
const endPosition = vec2.fromValues(sourcePosition[0] - xGrad * 100000, sourcePosition[1] - yGrad * 100000)
I then just render this as a line strip using the orthographic projection I mentioned earlier.
Quite happy with it and it looks pretty faithful to the original game. I will probably swing back and apply a little bit of randomisation to the direction.
Explosions
For the explosions I wanted to try and show the ships breaking apart. To do this I updated the ship model loading code to also load the ship model as a set of separately positional and renderable faces.
To trigger the explosion I replace the model of the ship with the separate set of faces. It should look exactly the same to begin with but I’ve got a bit of a problem with that at the moment however the effect already looks pretty good:
There’s still a bit of work to do on this and I’ll probably come back and add some particle effects too.