It’s been a few weeks since my last post, though work on StaticRectangle has been steady.

The pin I put in the renderer last post didn’t even last the full length of that post. Since then, I have made good headway with transparency and dielectrics, the latter on par with shadow-level annoying.

Here is a (broken) teaser

Here is a (broken) teaser

I’ll leave the specifics of that for another post.

This post I’ll talk a bit about updates to the engine and the Entity Component System (ECS).

In The Beninging

Before splitting the SceneGraph and moving on to an ECS, the engine design was (kind of) this1

while app.is_running:
    animation.update(scene, time)       <- update animated scene elements
    renderer.render(scene)              <- render the scene

Even as a first pass this had… problems. The AnimationManager was given the SceneGraph to traverse which meant it was not only responsible for knowing the contents of the scene (which it shoudn’t need to know), keep track of objects and object state (which it shouldn’t need to do), and update them every frame.

Just as an example this is the AnimationManager implementation for the scene with the axis’ pointing towards the cube bobbing up and down in “Lights Camera Action”

export class AnimationManager {

    update(scene: SceneGraph, frame: Frame): void {

        let cube = scene.getTransform("cube");
        cube.setTranslate(0, Math.sin(frame.time * 2) * 20, 0);
        cube.setDirty();
        for (const axis of scene.getTransforms()) {
            if (axis.getName()?.startsWith('axis_')) {
                axis.lookAt(cube);
                axis.setDirty();
            }
        }
    }
    ...
}

While this was easy to understand it meant each scene had to supply entirely new AnimationManager implementations, not very dynamic.

Enter Script Component

The Script component is now responsible for the runtime behavior of an entity. You can implement a bunch of exposed runtime hooks (onUpdate, onCreate, onDestroy etc.) and the ScriptSystem is responsible for managing execution.

So now runtime looks a bit like this

while app.is_running
    frame = tick()
    app.engine.update(frame)
    │
    └─► engine.scriptSystem.update(world, frame)
        │
        └─► entities = world.query('script')
            │
            for entity in entities
            │   script = world.getComponent(entity, 'script')
            │
            │   if not script.initialized
            │   │   script.onCreate(world, entity)
            │   │   script.initialized = true
            │
            │   script.onUpdate(world, entity, frame)
            │
            end for

Here is a real world example of a Script2

export class RotateObject implements Script {
    constructor(private degreesPerSecond = 45) {}

    onUpdate(world: World, entity: number, frame: Frame) {
        const transform = world.getComponent(entity, 'transform')!
        const rotate = transform.getRotate()
        transform.setRotate(
            rotate[0],
            rotate[1] + this.degreesPerSecond * frame.delta,
            rotate[2]
        )
    }
}

and the scene description would have something like this

        {
            "name": "Rotating Object",
            "components": {
                "transform": {
                    "position": [0, 0, 0]
                },
                "script": {
                    "name": "rotate",
                    "args": [60]  // degrees per second
                }
            }
        }

We get from 'rotate' in the scene description to an instance of RotateObject with the ScriptRegistry that registers a script with a ’nice’ name then the SceneLoader creates instances of it.

The ScriptRegistry will house a bunch of built-ins, like this RotateObject and register custom asset specific ones too.

Just Show Me Already

You’ve got three proceedurally generated scenes you can look at today.

They are generated with a Script component that implements the onSceneLoad hook. In these examples when the hook runs, it creates entities on the fly and attaches their own Script components to them. Neat.

  • h toggles the render options
  • ~ opens the terminal
  • Mouse look with left/right mouse buttons
  • w, a, s, d to move
  • Ctrl and Spacebar to fly vertical

Now scenes can be generated with n-number of objects in them it’s possible to stress test the various systems, so I’ll need to do an optimisation pass.

I’ve disabled SSGI for these scenes though you’re free to enable it and see why. When things are moving around quickly the ghosting can make the frame rate seem lower than it is, SSGI isn’t great for moving objects. Another research topic.

The render/pass options are now exposed to the scene description so SSGI parameters can be tuned per scene (though I haven’t done this, I just turned it off). Same goes for SSAO bias/radius etc and any other render settings that make sense to be scene specific.


  1. I described this way back in “A Conscious Decoupling” ↩︎

  2. You may notice I’m still using the Transform directly, instead of just updating a component. This is just temporary until I have multiple systems interacting with entities. ↩︎