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.
Registering a renderer with the graphics context will allow you to call it's draw method during your game.
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', ...);
}
In order to build a custom renderer extend the RendererPlugin interface
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
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
}
}