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.
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.
}
dependencies array when the plugin is added to the viewer, are created and attached to the viewer in super.onAddedthis.dispatchEvents, and subscribed to with addEventListener. The event type must be described in the class signature for typescript autocomplete to work.onAdded and onRemove functions for the viewer and other plugins, check the sample above for an example.viewer.setDirty() can be called, or set this.dirty = true in preFrame and reset in postFrame to stop the rendering. (Note that rendering may continue if some other plugin sets the viewer dirty)AViewerPlugin support serialisation.
plugin.toJSON() and plugin.fromJSON() can be used to get custom properties.@serialize('label') decorator can be used to mark any public/private variable as serialisable. label (optional) corresponds to the key in JSON.@serialize supports instances of ITexture, IMaterial, all primitive types, simple JS objects, three.js math classes(Vector2, Vector3, Matrix3…), and some more.uiDecorators can be used to mark properties and functions that will be shown in the Ui. The Ui shows up automatically when TweakpaneUiPlugin is added to the viewer.onDispose.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