tga, 01/15/2023
performance gain: 1.2x
Due to the batched renderer kaboom is using, all vertex transformation are done on the CPU. Kaboom was using this style of matrix math:
class Mat4 {
static translate(x, y) {
return new Mat4([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, 0, 1,
])
}
translate(x, y) {
return this.mult(Mat4.translate(x, y))
}
}
This is inefficient because:
Mat4
on every transformwhile in fact we only need to in-place change few fields while doing these types of transforms, which gets rid of the extra allocation and unused calculations.
class Mat4 {
translate(x, y) {
this.m[12] += this.m[0] * x + this.m[4] * y
this.m[13] += this.m[1] * x + this.m[5] * y
this.m[14] += this.m[2] * x + this.m[6] * y
this.m[15] += this.m[3] * x + this.m[7] * y
return this
}
}
performance gain: 2x
Kaboom uses a lot of spread operators to forward drawing options to other draw functions, for example
function drawSprite(opts) {
// doing some calculations above, then forward
drawTexture({
...opts,
tex: spr.data.tex,
quad: q.scale(opt.quad || new Quad(0, 0, 1, 1)),
}))
}
However we found spread operators are extremely slow if called thousands times per frame, changing every spread operator to Object.assign()
ups the performance by 2x. Easiest performance gain ever.
function drawSprite(opts) {
drawTexture(Object.assign(opt, {
tex: spr.data.tex,
quad: q.scale(opt.quad ?? new Quad(0, 0, 1, 1)),
}))
}
performance gain: up to 50x
It's expensive to initiate a draw call (gl.drawElements()
, gl.drawArrays()
etc.) in WebGL. Kaboom uses a batched renderer that keeps all shape vertices data in a buffer and only initiates a draw call at frame end or when texture changes. This approach makes it fast to draw a lot of 2d shapes with the same texture, however when texture changes a lot it can be slower than the naive render approach.
Consider this example:
kaboom()
loadSprite("bean", "sprites/bean.png")
for (let i = 0; i < 5000; i++) {
add([
sprite("bean"),
pos(rand(0, width()), rand(0, height())),
anchor("center"),
])
}
It renders 5000 sprites. After
However, if you draw the same amount of sprites but alternate between 2 sprites, this example will crash your browser tab:
kaboom()
loadSprite("bean", "sprites/bean.png")
loadSprite("bag", "sprites/bag.png")
for (let i = 0; i < 5000; i++) {
add([
sprite(i % 2 === 0 ? "bean" : "bag"),
pos(rand(0, width()), rand(0, height())),
anchor("center"),
])
}
In v3000 Kaboom introduced a machanism to automatically batch all sprites to a large texture atlas when loaded. As a result, the later example have the exact same performance with the former one.