Writing custom plugins

All Viewer Plugins must implement a IViewerPlugin to be attached to the viewer:

interface IViewerPlugin extends IEventDispatcher<string>, IUiConfigContainer, Partial<IJSONSerializable> {  
  // all classes must have this static property with a unique identifier value for this plugin
  static readonly PluginType: string
  
  // these plugins will be added automatically(with default settings), if they are not added yet.
  dependencies?: Class<IViewerPlugin<any>>[]  
  
  // the viewer will render the next frame if this is set to true
  dirty?: boolean;  
  
  // Called when this plug-in is added to the viewer  
  onAdded(viewer: TViewer): Promise<void>;  
  
  // Called when this plug-in is removed from the viewer  
  onRemove(viewer: TViewer): Promise<void>;  
  
  // Called when the viewer is disposed  
  onDispose(viewer: TViewer): Promise<void>;  
}

To make it easier and remove boilerplate, the abstract class AViewerPlugin, GenericFilterPlugin, MultiFilterPlugin can be used.

Sample plugin - simple

Here is a sample plugin that extends from AViewerPlugin and adds some basic functionalities to the viewer. Check the inline comments for various explanation.

import {AViewerPlugin, IEvent, reflHelpers, serialize, ViewerApp, uiFolder, uiToggle, uiSlider, uiInput, uiButton, onChange} from 'webgi'

@uiFolder('Sample Plugin') // This creates a folder in the Ui. (Supported by TweakpaneUiPlugin)
export class SamplePlugin extends AViewerPlugin<'sample-1'|'sample-2'> { // These are the list of events that this plugin can dispatch.
    static readonly PluginType = 'SamplePlugin' // This is required for serialization and handling plugins. Also used in viewer.getPluginByType()

    @uiToggle() // This creates a checkbox in the Ui. (Supported by TweakpaneUiPlugin)
    @serialize() // Adds this property to the list of serializable. This is also used when serializing to glb in AssetExporter.
    enabled = true

    // A plugin can have custom properties.

    @uiSlider('Some Number', [0, 100], 1) // Adds a slider to the Ui, with custom bounds and step size (Supported by TweakpaneUiPlugin)
    @serialize('someNumber')
    @onChange(SamplePlugin.prototype._updateParams) // this function will be called whenevr this value changes.
    val1 = 0

    // A plugin can have custom properties.
    @uiInput('Some Text') // Adds a slider to the Ui, with custom bounds and step size (Supported by TweakpaneUiPlugin)
    @onChange(SamplePlugin.prototype._updateParams) // this function will be called whenevr this value changes.
    @serialize()
    val2 = 'Hello'

    @uiButton('Print Counters') // Adds a button to the Ui. (Supported by TweakpaneUiPlugin)
    public printValues() {
        console.log(this.val1, this.val2)
        this.dispatchEvent({type: 'sample-1', detail: {sample: this.val1}}) // This will dispatch an event.
    }

    constructor() {
        super()
        this._updateParams = this._updateParams.bind(this)
    }

    private _updateParams() {
        console.log('Parameters updated.')
        this.dispatchEvent({type: 'sample-2'}) // This will dispatch an event.
    }

    async onAdded(v: ViewerApp): Promise<void> {
        await super.onAdded(v)

        // Do some initialization here.
        this.val1 = 0
        this.val2 = 'Hello'

        v.addEventListener('preRender', this._preRender)
        v.addEventListener('postRender', this._postRender)
        v.addEventListener('preFrame', this._preFrame)
        v.addEventListener('postFrame', this._postFrame)

        this._viewer!.scene.addEventListener('addSceneObject', this._objectAdded) // this._viewer can also be used while this plugin is attached.

    }


    async onRemove(v: ViewerApp): Promise<void> {
        // remove dispose objects

        v.removeEventListener('preRender', this._preRender)
        v.removeEventListener('postRender', this._postRender)
        v.removeEventListener('preFrame', this._preFrame)
        v.removeEventListener('postFrame', this._postFrame)

        this._viewer!.scene.removeEventListener('addSceneObject', this._objectAdded) // this._viewer can also be used while this plugin is attached.

        return super.onRemove(v)
    }


    // async onDispose(viewer: ViewerApp): Promise<void> {
    //     // this is optional
    //     return super.onDispose(viewer);
    // }

    private _objectAdded = (ev: IEvent<any>)=>{
        console.log('A new object, texture or material is added to the scene.', ev.object)
    }
    private _preFrame = (ev: IEvent<any>)=>{
        // THis function will be called before each frame. This is called even if the viewer is not dirty, so it's a good place to do viewer.setDirty()
    }
    private _preRender = (ev: IEvent<any>)=>{
        // This is called before each frame is rendered, only when the viewer is dirty.
    }
    // postFrame and postRender work the same way as preFrame and preRender.

}

Notes:

Plugin with a ShaderPass

Below is a sample plugin that shows implementation for a simple post processing pass responsible for Tonemapping. This inherits the abstract class GenericFilterPlugin which adds support for creating a single shader pass and adding it to the viewer pipeline. Incase multiple passes are required to be added in the sample plugin, see MultiFilterPlugin.
Note that we already provide a TonemapPlugin that should be used, this is just a sample to show the GenericFilterPlugin API

@uiFolder('Sample Plugin 2') // This creates a folder in the Ui. (Supported by TweakpaneUiPlugin)
export class SampleTonemapPlugin extends GenericFilterPlugin<TonemapPass, 'tonemap', '', ViewerApp> implements IViewerPlugin {

    static readonly PluginType = 'SampleTonemap'
    passId: 'tonemap' = 'tonemap' // ID for this pass, to represent in the pipeline.

    dependencies = [GBufferPlugin]

    protected _beforeFilters = ['screen'] // this pass is added before the screen pass..
    protected _afterFilters = ['render'] // this pass is added after the render pass.
    protected _requiredFilters = ['render'] // render pass(RenderPass) is required in the pipeline to render this pass.

    constructor(readonly renderToScreen = true) {
        super()
        this._setDirty = this._setDirty.bind(this)
    }

    async onAdded(viewer: ViewerApp): Promise<void> {
        if (this.renderToScreen)
            safeSetProperty(
                viewer.renderer.passes.find(value => value.passId === 'screen'),
                'enabled', false, true, true
            ) // disable the screen pass from the pipeline so that this can be used instead
        return super.onAdded(viewer)
    }

    passCtor(v: ViewerApp): TonemapPass {
        return new TonemapPass()
    }

    protected _update(v: ViewerApp): boolean {
        if (!super._update(v)) return false
        this._pass!.passObject.updateShaderProperties(this._viewer?.getPlugin(GBufferPlugin)) // Add uniforms from GBufferPlugin to this shader, like the depthNormal texture.
        return true
    }

    @serialize()
    @uiSlider('Exposure', [0, 10], 0.1)
    get exposure(): number {
        return this.pass?.passObject.exposure ?? 1
    }

    set exposure(value: number) {
        const t = this.pass?.passObject
        if (t) {
            t.exposure = value
            this._setDirty()
        }
    }

    @serialize()
    @uiDropdown('Mode', ([
        ['Linear', LinearToneMapping],
        ['Reinhard', ReinhardToneMapping],
        ['Cineon', CineonToneMapping],
        ['ACESFilmic', ACESFilmicToneMapping],
        ['Uncharted2', Uncharted2Tonemapping],
    ] as [string, ToneMapping][]).map(value => ({
        label: value[0],
        value: value[1],
    })))
    get toneMapping(): ToneMapping {
        return this.pass?.passObject.toneMapping ?? LinearToneMapping
    }

    set toneMapping(value: ToneMapping) {
        const t = this.pass?.passObject
        if (t) {
            t.toneMapping = value
            this._setDirty()
        }
    }

    private _setDirty() {
        if (this.pass) this.pass.dirty = true
    }
}

In-Depth tutorials and guides for Post/pre-processing Passes and Filters pipeline is coming soon


Back to home