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
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

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.
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.
I described this way back in “A Conscious Decoupling” ↩︎
You may notice I’m still using the
Transformdirectly, instead of just updating a component. This is just temporary until I have multiple systems interacting with entities. ↩︎