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.

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.

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

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
  }
}