Skip to content

Commit

Permalink
perf: Use geometry instancing in new ImageRendererV2 (#3278)
Browse files Browse the repository at this point in the history
This PR roughly doubles the performance in `ex.ImageRendererV2` over `ex.ImageRenderer`.

Flag.useLegacyImageRenderer() will switch back to `ex.ImageRenderer`
  • Loading branch information
eonarheim authored Nov 29, 2024
1 parent c855d89 commit f3a9cda
Show file tree
Hide file tree
Showing 12 changed files with 731 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ are doing mtv adjustments during precollision.

### Updates

- Perf improvement to image rendering! with ImageRendererV2! Roughly doubles the performance of image rendering
- Perf improvement to retrieving components with `ex.Entity.get()` which widely improves engine performance
- Non-breaking parameters that reference `delta` to `elapsedMs` to better communicate intent and units
- Perf improvements to `ex.ParticleEmitter`
Expand Down
7 changes: 5 additions & 2 deletions sandbox/tests/drawcalls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ var loader = new ex.Loader([tex]);

var random = new ex.Random(1337);

var items = [{ pos: ex.Vector.Zero, vel: ex.vec(random.integer(50, 100), random.integer(50, 100)) }];
var items = [
{ pos: ex.Vector.Zero, vel: ex.vec(random.integer(50, 100), random.integer(50, 100)) },
{ pos: ex.Vector.Zero, vel: ex.vec(random.integer(50, 100), random.integer(50, 100)) }
];
var drawCalls$ = document.getElementById('draw-calls');
var drawnItems$ = document.getElementById('drawn-items');
var add$ = document.getElementById('add');
Expand All @@ -39,7 +42,7 @@ game.start(loader).then(() => {
items[i].vel.y *= -1;
}
}
game.graphicsContext.drawCircle(ex.vec(200, 200), width / 4, ex.Color.Blue, ex.Color.Black, 2);
// game.graphicsContext.drawCircle(ex.vec(200, 200), width / 4, ex.Color.Blue, ex.Color.Black, 2);
};

game.onPostDraw = (_engine, deltaMs) => {
Expand Down
7 changes: 7 additions & 0 deletions src/engine/Flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export class Flags {
Flags.enable('use-canvas-context');
}

/**
* Force excalibur to use the less optimized image renderer
*/
public static useLegacyImageRenderer() {
Flags.enable('use-legacy-image-renderer');
}

/**
* Freeze all flag modifications making them readonly
*/
Expand Down
47 changes: 32 additions & 15 deletions src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { Material, MaterialOptions } from './material';
import { MaterialRenderer } from './material-renderer/material-renderer';
import { Shader, ShaderOptions } from './shader';
import { GarbageCollector } from '../../GarbageCollector';
import { ImageRendererV2 } from './image-renderer-v2/image-renderer-v2';
import { Flags } from '../../Flags';

export const pixelSnapEpsilon = 0.0001;

Expand Down Expand Up @@ -97,20 +99,11 @@ export interface ExcaliburGraphicsContextWebGLOptions extends ExcaliburGraphicsC
export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
private _logger = Logger.getInstance();
private _renderers: Map<string, RendererPlugin> = new Map<string, RendererPlugin>();
public imageRenderer: 'ex.image' | 'ex.image-v2' = Flags.isEnabled('use-legacy-image-renderer') ? 'ex.image' : 'ex.image-v2';
private _isDrawLifecycle = false;
public useDrawSorting = true;

private _drawCallPool = new Pool<DrawCall>(
() => new DrawCall(),
(instance) => {
instance.priority = 0;
instance.z = 0;
instance.renderer = undefined as any;
instance.args = undefined as any;
return instance;
},
4000
);
private _drawCallPool = new Pool<DrawCall>(() => new DrawCall(), undefined, 4000);

private _drawCallIndex = 0;
private _drawCalls: DrawCall[] = new Array(4000).fill(null);
Expand Down Expand Up @@ -311,7 +304,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

gl.depthMask(false);
// Setup builtin renderers
this.register(
new ImageRenderer({
Expand All @@ -324,6 +317,12 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
this.register(new CircleRenderer());
this.register(new PointRenderer());
this.register(new LineRenderer());
this.register(
new ImageRendererV2({
uvPadding: this.uvPadding,
pixelArtSampler: this.pixelArtSampler
})
);

this.materialScreenTexture = gl.createTexture();
if (!this.materialScreenTexture) {
Expand Down Expand Up @@ -398,6 +397,11 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
}

public draw<TRenderer extends RendererPlugin>(rendererName: TRenderer['type'], ...args: Parameters<TRenderer['draw']>) {
if (process.env.NODE_ENV === 'development') {
if (args.length > 9) {
throw new Error('Only 10 or less renderer arguments are supported!;');
}
}
if (!this._isDrawLifecycle) {
this._logger.warnOnce(
`Attempting to draw outside the the drawing lifecycle (preDraw/postDraw) is not supported and is a source of bugs/errors.\n` +
Expand All @@ -421,7 +425,16 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
drawCall.state.opacity = this._state.current.opacity;
drawCall.state.tint = this._state.current.tint;
drawCall.state.material = this._state.current.material;
drawCall.args = args;
drawCall.args[0] = args[0];
drawCall.args[1] = args[1];
drawCall.args[2] = args[2];
drawCall.args[3] = args[3];
drawCall.args[4] = args[4];
drawCall.args[5] = args[5];
drawCall.args[6] = args[6];
drawCall.args[7] = args[7];
drawCall.args[8] = args[8];
drawCall.args[9] = args[9];
this._drawCalls[this._drawCallIndex++] = drawCall;
} else {
// Set the current renderer if not defined
Expand All @@ -435,7 +448,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
}

// If we are still using the same renderer we can add to the current batch
renderer.draw(...args);
renderer.draw(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);

this._currentRenderer = renderer;
}
Expand Down Expand Up @@ -523,7 +536,11 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
if (this._state.current.material) {
this.draw<MaterialRenderer>('ex.material', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
} else {
this.draw<ImageRenderer>('ex.image', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
if (this.imageRenderer === 'ex.image') {
this.draw<ImageRenderer>(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
} else {
this.draw<ImageRendererV2>(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/engine/Graphics/Context/draw-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export class DrawCall {
tint: Color.White,
material: null
};
public args!: any[];
public args: any[] = new Array(10);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#version 300 es
precision mediump float;

// UV coord
in vec2 v_texcoord;

// Textures in the current draw
uniform sampler2D u_textures[%%count%%];

uniform bool u_pixelart;

in float v_texture_index;

in float v_opacity;

in vec4 v_tint;

// texture resolution
in vec2 v_res;

in vec2 v_uv_min;
in vec2 v_uv_max;

out vec4 fragColor;

// Inigo Quilez pixel art filter https://jorenjoestar.github.io/post/pixel_art_filtering/
vec2 uv_iq(in vec2 uv, in vec2 texture_size) {
vec2 pixel = uv * texture_size;

vec2 seam=floor(pixel+.5);
vec2 dudv=fwidth(pixel);
pixel=seam+clamp((pixel-seam)/dudv,-.5,.5);

return pixel/texture_size;
}

float lerp(float from, float to, float rel){
return ((1. - rel) * from) + (rel * to);
}

float invLerp(float from, float to, float value){
return (value - from) / (to - from);
}

float remap(float origFrom, float origTo, float targetFrom, float targetTo, float value){
float rel = invLerp(origFrom, origTo, value);
return lerp(targetFrom, targetTo, rel);
}

void main(){
// In order to support the most efficient sprite batching, we have multiple
// textures loaded into the gpu (usually 8) this picker logic skips over textures
// that do not apply to a particular sprite.

vec4 color=vec4(1.,0,0,1.);

// GLSL is templated out to pick the right texture and set the vec4 color
vec2 uv = u_pixelart ? uv_iq(v_texcoord, v_res) : v_texcoord;
uv.x = remap(0.,1., v_uv_min.x, v_uv_max.x, uv.x);
uv.y = remap(0.,1., v_uv_min.y, v_uv_max.y, uv.y);

%%texture_picker%%

color.rgb = color.rgb * v_opacity;
color.a = color.a * v_opacity;
fragColor = color * v_tint;
}
Loading

0 comments on commit f3a9cda

Please sign in to comment.