Skip to main content

Custom Renderer Plugins

Excalibur knows how to draw many types graphics to the screen by default comes with those pre-installed into the ExcaliburGraphicsContext. However, you may have a unique requirement to provide custom WebGL commands into excalibur, this can be done with a custom renderer plugin.

warning

This is an advanced API it is recommended you use built in graphics unless you are comfortable with building WebGL geometry and shaders.

Registering a renderer plugin

Registering a renderer with the graphics context will allow you to call it's draw method during your game.

typescript
const game = new ex.Engine({...});
game.start().then(() => {
// register
game.graphicsContext.register(new MyCustomRenderer());
});
// call from a graphics callback or event
const actor = new ex.Actor({...});
actor.graphics.onPostDraw = (graphicsContext) => {
graphicsContext.draw<MyCustomRenderer>('myrenderer', ...);
}
typescript
const game = new ex.Engine({...});
game.start().then(() => {
// register
game.graphicsContext.register(new MyCustomRenderer());
});
// call from a graphics callback or event
const actor = new ex.Actor({...});
actor.graphics.onPostDraw = (graphicsContext) => {
graphicsContext.draw<MyCustomRenderer>('myrenderer', ...);
}

Writing a custom render plugin

In order to build a custom renderer extend the RendererPlugin interface

typescript
export class MyCustomRenderer extends ex.RendererPlugin {
/**
* Unique type name for this renderer plugin
*/
readonly type: string = 'myrenderer';
/**
* Render priority tie breaker when drawings are at the same z index
* @warning Not yet used by excalibur
*/
priority: number = 0;
/**
* Initialize your renderer
*
* @param gl
* @param context
*/
initialize(gl: WebGLRenderingContext, context: ExcaliburGraphicsContextWebGL): void {
// initialize and compile shader
}
/**
* Issue a draw command to draw something to the screen
* @param args
*/
draw(some: ex.Vector, args: ex.Color): void {
// update internal state with draw command with the args
}
/**
* @returns if there are any pending draws in the renderer
*/
hasPendingDraws(): boolean {
// if there are any un-flushed drawings
return false;
}
/**
* Flush any pending graphics draws to the screen
*/
flush(): void {
// flush all pending draws to the screen
}
}
typescript
export class MyCustomRenderer extends ex.RendererPlugin {
/**
* Unique type name for this renderer plugin
*/
readonly type: string = 'myrenderer';
/**
* Render priority tie breaker when drawings are at the same z index
* @warning Not yet used by excalibur
*/
priority: number = 0;
/**
* Initialize your renderer
*
* @param gl
* @param context
*/
initialize(gl: WebGLRenderingContext, context: ExcaliburGraphicsContextWebGL): void {
// initialize and compile shader
}
/**
* Issue a draw command to draw something to the screen
* @param args
*/
draw(some: ex.Vector, args: ex.Color): void {
// update internal state with draw command with the args
}
/**
* @returns if there are any pending draws in the renderer
*/
hasPendingDraws(): boolean {
// if there are any un-flushed drawings
return false;
}
/**
* Flush any pending graphics draws to the screen
*/
flush(): void {
// flush all pending draws to the screen
}
}

For example this is the PointRenderer implementation

typescript
export class PointRenderer implements RendererPlugin {
public readonly type = 'ex.point';
public priority: number = 0;
private _shader: Shader;
private _maxPoints: number = 10922;
private _buffer: VertexBuffer;
private _layout: VertexLayout;
private _gl: WebGLRenderingContext;
private _context: ExcaliburGraphicsContextWebGL;
private _pointCount: number = 0;
private _vertexIndex: number = 0;
initialize(gl: WebGLRenderingContext, context: ExcaliburGraphicsContextWebGL): void {
this._gl = gl;
this._context = context;
this._shader = new Shader({
vertexSource: pointVertexSource,
fragmentSource: pointFragmentSource
});
this._shader.compile();
this._shader.use();
this._shader.setUniformMatrix('u_matrix', this._context.ortho);
this._buffer = new VertexBuffer({
size: 7 * this._maxPoints,
type: 'dynamic'
});
this._layout = new VertexLayout({
shader: this._shader,
vertexBuffer: this._buffer,
attributes: [
['a_position', 2],
['a_color', 4],
['a_size', 1]
]
});
}
draw(point: Vector, color: Color, size: number): void {
// Force a render if the batch is full
if (this._isFull()) {
this.flush();
}
this._pointCount++;
const transform = this._context.getTransform();
const opacity = this._context.opacity;
const finalPoint = transform.multv(point);
const vertexBuffer = this._buffer.bufferData;
vertexBuffer[this._vertexIndex++] = finalPoint.x;
vertexBuffer[this._vertexIndex++] = finalPoint.y;
vertexBuffer[this._vertexIndex++] = color.r / 255;
vertexBuffer[this._vertexIndex++] = color.g / 255;
vertexBuffer[this._vertexIndex++] = color.b / 255;
vertexBuffer[this._vertexIndex++] = color.a * opacity;
vertexBuffer[this._vertexIndex++] = size * Math.max(transform.getScaleX(), transform.getScaleY());
}
private _isFull() {
if (this._pointCount >= this._maxPoints) {
return true;
}
return false;
}
hasPendingDraws(): boolean {
return this._pointCount !== 0;
}
flush(): void {
// nothing to draw early exit
if (this._pointCount === 0) {
return;
}
const gl = this._gl;
this._shader.use();
this._layout.use(true);
this._shader.setUniformMatrix('u_matrix', this._context.ortho);
gl.drawArrays(gl.POINTS, 0, this._pointCount);
GraphicsDiagnostics.DrawnImagesCount += this._pointCount;
GraphicsDiagnostics.DrawCallCount++;
this._pointCount = 0;
this._vertexIndex = 0;
}
}
typescript
export class PointRenderer implements RendererPlugin {
public readonly type = 'ex.point';
public priority: number = 0;
private _shader: Shader;
private _maxPoints: number = 10922;
private _buffer: VertexBuffer;
private _layout: VertexLayout;
private _gl: WebGLRenderingContext;
private _context: ExcaliburGraphicsContextWebGL;
private _pointCount: number = 0;
private _vertexIndex: number = 0;
initialize(gl: WebGLRenderingContext, context: ExcaliburGraphicsContextWebGL): void {
this._gl = gl;
this._context = context;
this._shader = new Shader({
vertexSource: pointVertexSource,
fragmentSource: pointFragmentSource
});
this._shader.compile();
this._shader.use();
this._shader.setUniformMatrix('u_matrix', this._context.ortho);
this._buffer = new VertexBuffer({
size: 7 * this._maxPoints,
type: 'dynamic'
});
this._layout = new VertexLayout({
shader: this._shader,
vertexBuffer: this._buffer,
attributes: [
['a_position', 2],
['a_color', 4],
['a_size', 1]
]
});
}
draw(point: Vector, color: Color, size: number): void {
// Force a render if the batch is full
if (this._isFull()) {
this.flush();
}
this._pointCount++;
const transform = this._context.getTransform();
const opacity = this._context.opacity;
const finalPoint = transform.multv(point);
const vertexBuffer = this._buffer.bufferData;
vertexBuffer[this._vertexIndex++] = finalPoint.x;
vertexBuffer[this._vertexIndex++] = finalPoint.y;
vertexBuffer[this._vertexIndex++] = color.r / 255;
vertexBuffer[this._vertexIndex++] = color.g / 255;
vertexBuffer[this._vertexIndex++] = color.b / 255;
vertexBuffer[this._vertexIndex++] = color.a * opacity;
vertexBuffer[this._vertexIndex++] = size * Math.max(transform.getScaleX(), transform.getScaleY());
}
private _isFull() {
if (this._pointCount >= this._maxPoints) {
return true;
}
return false;
}
hasPendingDraws(): boolean {
return this._pointCount !== 0;
}
flush(): void {
// nothing to draw early exit
if (this._pointCount === 0) {
return;
}
const gl = this._gl;
this._shader.use();
this._layout.use(true);
this._shader.setUniformMatrix('u_matrix', this._context.ortho);
gl.drawArrays(gl.POINTS, 0, this._pointCount);
GraphicsDiagnostics.DrawnImagesCount += this._pointCount;
GraphicsDiagnostics.DrawCallCount++;
this._pointCount = 0;
this._vertexIndex = 0;
}
}