perf: Better realistic collision perf + CI benches (#3842)
This PR basically takes as much allocation out of the realistic collision hot loop as possible. This is done by inlining the vector/matrix/line clipping etc so far less readible.
CI runners will be underwhelming in general, but relative movement is still good.
<img width="720" height="320" alt="edge-floor-realistic-200" src="https://github.com/user-attachments/assets/0ca9a9c3-7e56-4783-ab39-695b43399dcd" />
<img width="720" height="320" alt="sleeping-pile-wake" src="https://github.com/user-attachments/assets/6730b959-70c3-4618-a6a5-46c686412ca6" />
<img width="720" height="320" alt="stack-realistic-100-settle" src="https://github.com/user-attachments/assets/fd7976e7-655e-4ba4-9af5-ded3a480ed12" />
<img width="720" height="320" alt="stack-realistic-300-rest" src="https://github.com/user-attachments/assets/d6e26984-1628-4d18-b428-9fa55d5f2b7f" />
<img width="720" height="320" alt="stack-realistic-300-settle" src="https://github.com/user-attachments/assets/859ee921-1783-4c6d-b7c5-2110ad2c38f1" />
<img width="1240" height="360" alt="summary" src="https://github.com/user-attachments/assets/099f0886-1540-44da-9eed-7373f94a25e7" />
Final report
### Excalibur benchmark
Baseline: `npm:excalibur@latest` (0.32.0) · Candidate: `../build/dist/excalibur.js` (0.33.0-alpha.14+816d29e) · 2 interleaved run(s), best run shown with the min–max of run medians · ms/frame
| Test | Baseline median | Baseline p95 | Candidate median | Candidate p95 | Δ median | Δ p95 | Extras (candidate) | |
|---|---:|---:|---:|---:|---:|---:|---|---|
| actors-1000 | 4.90 <sub>(4.90–6.15)</sub> | 11.62 | 4.90 <sub>(4.90–5.50)</sub> | 10.60 | +0.0% | -8.7% | actors=1000 | |
| actors-4000 | 39.05 <sub>(39.05–39.05)</sub> | 64.41 | 33.35 <sub>(33.35–40.25)</sub> | 66.93 | -14.6% | +3.9% | actors=4000 | |
| stack-realistic-100-settle | 8.20 <sub>(8.20–8.30)</sub> | 19.80 | 6.00 <sub>(6.00–7.05)</sub> | 14.03 | -26.8% | -29.2% | sleepingAtEnd=0, firstFrameAllAsleep=-1 | 🚀 |
| stack-realistic-300-settle | ERROR | | 18.10 | | | | page errors: [Fatal] : RangeError: Invalid array length | ❌ |
| stack-realistic-300-rest | ERROR | | 3.30 | | | | page errors: [Fatal] : RangeError: Invalid array length | ❌ |
| arcade-1000-bouncing | 28.50 <sub>(28.50–28.70)</sub> | 45.70 | 23.40 <sub>(23.40–24.35)</sub> | 40.51 | -17.9% | -11.4% | outside=14 | |
| edge-floor-realistic-200 | 9.70 <sub>(9.70–10.25)</sub> | 18.10 | 3.80 <sub>(3.80–3.95)</sub> | 12.80 | -60.8% | -29.3% | sleepingAtEnd=200, fellThrough=0 | 🚀 |
| sleeping-pile-wake | 26.40 <sub>(26.40–27.45)</sub> | 48.70 | 14.70 <sub>(14.70–14.70)</sub> | 31.70 | -44.3% | -34.9% | sleepingAtEnd=0, spawned=5 | 🚀 |
| bunnymark-2000 | 99.20 <sub>(99.20–99.65)</sub> | 102.30 | 99.20 <sub>(99.20–100.10)</sub> | 102.03 | -0.0% | -0.3% | bunnies=2000, drawCalls=1 | |
| bunnymark-5000 | 231.80 <sub>(231.80–232.70)</sub> | 238.51 | 231.90 <sub>(231.90–233.10)</sub> | 238.55 | +0.0% | +0.0% | bunnies=5000, drawCalls=1 | |
No test is more than 20% slower than the baseline. Results are informational, shared CI runners are noisy.
Also ironed out some correctness issues with stale geometry that would cause simulation instability (already present in v0.32 sadly, see the stack-realistic-300 boxes row). Roughly speaking the solver would blow up impulse on subsequent substeps because the geometry was out of date and it'd try harder and harder to correct.
Additionally there was more savings by skipping sleeping contacts from the solver, we still wake them when necessary but we don't want to apply impulse to things that are asleep. The contact islands now make sure there are now mixed states. Either all awake or all asleep.
Bonus points! Updated and added our legacy benchmarking tool so we can apples to apples compare with older releases yay science!
feat: Shader Pipelines (#3828)
This PR adds Shader pipeline capabilities to Materials/PostProcessors!
https://github.com/user-attachments/assets/256bdf2c-c09d-40db-b5cd-0e0e1776729c
1. You can stack fragment shaders on each other and re-use them to mix/match effects
- Built in uniforms are passed through each pass
- We support lists of glsl fragment strings or ShaderPasses, and ShaderPipelines that can wrap up multipass effects into a convient package
3. This enables effects like Blur/Bloom/Glow (built in ShaderPipelines) that are only easily possible with multipass workflows
Example using multiple strings
```typescript
// 3. Custom 2-pass pipeline from bare glsl strings
var redOnly = ex.glsl`
in vec2 v_uv;
uniform sampler2D u_image;
out vec4 fragColor;
void main() {
vec4 color = texture(u_image, v_uv);
fragColor = vec4(color.r + color.g + color.b, 0.0, 0.0, color.a);
}`;
var pulseBlue = ex.glsl`
in vec2 v_uv;
uniform sampler2D u_image;
uniform float u_time_ms;
out vec4 fragColor;
void main() {
vec4 color = texture(u_image, v_uv);
float pulse = (sin(u_time_ms / 300.0) + 1.0) / 2.0;
fragColor = vec4(color.r, 0.0, pulse * color.r, color.a);
}`;
var tinted = new ex.Actor({ pos: ex.vec(480, 150) });
tinted.graphics.use(swordImage.toSprite());
tinted.graphics.material = new ex.Material({
name: 'tint-pipeline',
graphicsContext,
passes: [redOnly, pulseBlue]
});
```
Built in effects!
```typescript
var bloomEffect = new ex.BloomEffect({ graphicsContext, threshold: 0.3, intensity: 1.5 });
var blooming = new ex.Actor({ pos: ex.vec(780, 150) });
blooming.graphics.use(swordImage.toSprite());
blooming.graphics.material = new ex.Material({
name: 'bloom-material',
graphicsContext,
passes: bloomEffect,
padding: 32
});
game.add(blooming);
```
Accidental bug fixes
- The time based uniforms in post processors were counting N times based on the number of postprocessors
This was re-implemented resurrected from my old branch back onto main
* https://github.com/excaliburjs/Excalibur/compare/main...feat/shader-pipelines
Huge thanks to Andrew Adamson's tutorials and code samples that provided the inspiration for the framebuffer technique
* https://youtu.be/bAq9Zk7BAWo?si=f4h0rBBR9T5yaS2W
* https://github.com/scriptfoundry/WebGL2-Videos-Materials/blob/main/25.framebuffer.basics.js
feat: Add SoundManager for managing music/fx/audio in a friendly way (#3472)
Closes #590
Related #1161
## Changes:
- Added a new `ex.Sound({...})` option back constructor to set all the props you could set on sound at constructor time
- Added new `ex.SoundManager({...}` api to manage volume and muting on large groups of sounds at once
```typescript
var soundManager = new ex.SoundManger({
channels: ['fx', 'music', 'background'],
sounds: {
jumpSnd: { sound: jumpSnd, volume: 0.4, channels: ['fx'] },
forestSnd: { sound: forestSnd, volume: 0.2, channels: ['music', 'background'] },
challengeMusic: { sound: challengeMusic, volume: 0.2, channels: ['music'] },
guitarLoop: { sound: guitarLoop, volume: 0.2, channels: ['music'] }
}
});
toggleMusic.on('pointerdown', () => {
soundManager.channel.toggle('music');
});
game.add(toggleMusic);
game.input.keyboard.on('press', (evt) => {
if (evt.key === ex.Keys.J) {
soundManager.play('jumpSnd');
}
if (evt.key === ex.Keys.M) {
soundManager.channel.mute('music');
}
if (evt.key === ex.Keys.A) {
soundManager.mute();
}
if (evt.key === ex.Keys.S) {
soundManager.unmute();
}
if (evt.key === ex.Keys.U) {
soundManager.channel.unmute('music');
}
if (evt.key === ex.Keys.P) {
soundManager.channel.play('music');
}
if (evt.key === ex.Keys.V) {
soundManager.channel.setVolume('music', 0.9);
}
});
```