developer notes

scene

If like me you haven't done any 3D modelling in a while, you might find something like this topology guide a useful refresher course. And there are some pretty pictures in this article about modular design.

reflection

Make the floor a little more interesting by making it shiny. There are several different approaches to do this.

Currently, we are using a Mirror surface. Which creates the reflection by creating a camera and rendering an image that we then use a a texture to apply to the floor. Other than potential performance issues that can arise when we start pre-render passes, the problem is that we get a perfect mirror rather than a shiny reflection. this guy had the same problem.

Screen Space Reflections

On a side note, there is this raytracing renderer for Three, which is amazing.

textures

To be more precise, decals. this reddit post outlines what we're going to be attempting (also the same source of the face-weighted normal explanation above).

this tombstone decal example is awesome too!

physics

Physics engines are great fun, but they do take up a lot of processing power and sometimes getting them to behave the way you want is more trouble than it's worth.

There are quite a few javascript engines out there, and each one has it's quirks. Here is a handful of the more high-profile ones I looked at while researching this :

The people behind babylonjs did a lot of research on the subject, and I found that this page in their documentation was an useful round-up of the engines they use. There's even a cool demo at games-matter.com that compares them all.

Kudos to the third person game tutorial that uses Cannon.js, Mozilla also use it in their collision detection examples.

The most important functionality I was looking for was collision detection for static objects, just to stop us from falling through the floor or walking through walls. We'll be using simplifed meshes so the physics engine will have to be able to use abritrary objects

Typically, what a physics engine does is create a invisible parallel world representing your 3D scene. You feed in objects with properties and forces that the physics engine then processes so that you can then apply the changes to your rendered scene. This means that you're computing your scene twice, every frame.

For the sake of performance and load times, and considering that the short-term objective was simple wall collision, I ended up removing the physics engine entirely in favour of simple Raycaster collision

due credit to nick janssen for this elegantly simple collision function. Turns out that using the direction vector is sufficiantly effective to avoid clipping issues, you have to really slide along a wall to make a difference, definately worth the performance boost from using only one Raycaster instead of cardinal directions or vertex/face testing.

input

Due to the large differences between input controllers, mouse/keyboard, gamepad or VR, this has been divided up into modules wrapped into a small controller object. Notably because gamepad implementation was more complicated than I expected (see below).

mouse / keyboard

Event handlers for mouse movement and WASD or ←↑→↓ keyboard input. This is pretty straightforward thanks to some really useful functions like requestPointerLock() for mouse events and event.code for keyboard input that lets us use WASD controls on non-azerty layouts. Only caveat is that mouse movement don't vary between -1 / 1 like other input controls so they have to be handled slightly differently.

gamepad

This was a bit of a nightmare. Getting gamepads to work consistently across different devices and browsers required an extra layer of abstraction and a user-configurable way of customising layouts. If you're interested in exploring the Gamepad API, this blog post is a great starting point, and is better written than my ramblings.

the source code for this game jam is an interesting read.

The API has changed quite a lot over the last couple of years, if you are reading this in the future then probably everything that follows will be obsolete and we will no longer need to manually re-map inputs, or use loops and customEvents in order for them to work, but today in 2018 we're going to use a Playstation™ 4 controller as an example : Here is what a standard gamepad layout looks like compared to what Firefox gives us.

  1. axes[0]
  2. axes[1]
  3. axes[2]
  4. axes[3]
  1. analog_L_axis_X
  2. analog_L_axis_Y
  3. analog_R_axis_X
  4. analog_R_axis_Y
  1. axes[0]
  2. axes[1]
  3. axes[2]
  4. axes[3]
  5. axes[4]
  6. axes[5]
  7. axes[6]
  8. axes[7]
  1. analog_L_axis_X
  2. analog_L_axis_Y
  3. analog_R_axis_X
  4. analog_R_axis_Y
  1. buttons[0]
  2. buttons[1]
  3. buttons[2]
  4. buttons[3]
  5. buttons[4]
  6. buttons[5]
  7. buttons[6]
  8. buttons[7]
  9. buttons[8]
  10. buttons[9]
  11. buttons[10]
  12. buttons[11]
  13. buttons[12]
  14. buttons[13]
  15. buttons[14]
  16. buttons[15]
  17. buttons[16]
  1. button_1
  2. button_2
  3. button_3
  4. button_4
  5. trigger_L1
  6. trigger_R1
  7. trigger_L2
  8. trigger_R2
  9. button_select
  10. button_start
  11. analog_L
  12. analog_R
  13. direction_up
  14. direction_down
  15. direction_left
  16. direction_right
  17. button_brand
  1. buttons[0]
  2. buttons[1]
  3. buttons[2]
  4. buttons[3]
  5. buttons[4]
  6. buttons[5]
  7. buttons[6]
  8. buttons[7]
  9. buttons[8]
  10. buttons[9]
  11. buttons[10]
  12. buttons[11]
  13. buttons[12]
  14. buttons[13]
  15. buttons[14]
  16. buttons[15]
  17. buttons[16]
  18. buttons[17]
  1. button_1
  2. button_2
  3. button_3
  4. button_4
  5. trigger_L1
  6. trigger_R1
  7. trigger_L2
  8. trigger_R2
  9. button_select
  10. button_start
  11. analog_L
  12. analog_R
  13. direction_up
  14. direction_down
  15. direction_left
  16. direction_right
  17. button_brand

Axes 3 and 4 are actually the triggers L2 and R2. They even register the amount the trigger is pressed, although their rest state when untouched is -1 not 0. The extra axes 6 and 7 are a bit of a mystery, and they continuously cycle through values between -1 and 1. Maybe they have something to do with the touchpad. Several other gamepad models I tried have different layouts for the main buttons, it seems to be a common issue. More importantly the D-pad doesn't respond to any input, but since we're using the analog sticks here it's not too much of a problem.

The gamepadconnected event does give us a device id in order to identify specific hardware. A quickfix is to hardcode custom layouts on a case-by-case basis, however even that is nowhere near as straightforward as it seems. Playstation™ controllers have vendor and device codes in their id that has to be cross-referenced with a database like the one here : www.linux-usb.org. Microsoft™ XBox controllers return Xbox 360 Controller (XInput STANDARD GAMEPAD) on Chrome, apparently irrespective of whether they are a 360 or an XBox One controller, and a simple xinput on Firefox.

identify gamepad from id
browser-specific mapping

I gave up this method before testing on Edge or Safari though. It ends up being a long-winded process that will probably require an awful lot of exceptions. Also, browser sniffing.

alternative using getters
create object from array

A more open-ended approach would be to allow abitrary custom mapping options to any gamepad. Unfortunately Firefox again makes this overly complicated because the phantom axes continuously change values which makes them indistiguishable from actual user input. Also, because the trigger axes read a value of -1 when unpressed, the page would have to create rest state variables and work with delta values instead of absolutes. So the usual method of prompting the user to press the right input for each output runs into lots of problems.

customise mapping via user input

This method is also unfinished, filtering out those phantom axes would require a timing function in order to stabilise the input. Theoretically though, all that is missing is something to prevent the same input being assigned to another output on every loop iteration.

The simplest solution was to forego the gamepad entirely and use the mouse to assign inputs to outputs instead.

module

It's written as verbose as possible in the hope of pruning out uneccessary parts when the API becomes more stable.

The identify function got left in since I already had the .svg images for Playstation™ and XBox™ controllers, but it doesn't serve any real purpose other than eye candy.

VR headsets

I only occasionally get access to the equipment in order to test this, implementing the controllers is still work in progress. This project on Github has a lot of information on the subject.

This could potentially be an issue : camera translation

Miscellaneous code snippets

listening to object changes using Proxy()

inspired by this blog post.

increasing array length and populate empty slots
layout.axes.length = event.gamepad.axes.length;
layout.axes = [...layout.axes];