James Randall Musings on software development, business and technology.
Elite: laser collision detection, enemy lasers and explosions

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.