| @@ -92,6 +92,7 @@ To make changes and run the example, click on the CodePen button on the top righ | |||
| - [TonemapPlugin](#tonemapplugin) - Add tonemap to the final screen pass | |||
| - [DropzonePlugin](#dropzoneplugin) - Drag and drop local files to import and load | |||
| - [ProgressivePlugin](#progressiveplugin) - Post-render pass to blend the last frame with the current frame | |||
| - [SSAAPlugin](#ssaaplugin) - Add Super Sample Anti-Aliasing by applying jitter to the camera. | |||
| - [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer | |||
| - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer | |||
| - [GBufferPlugin](#gbufferplugin) - Pre-rendering of depth-normal and flags buffers in a single pass | |||
| @@ -2198,6 +2199,29 @@ Progressive Plugin adds a post-render pass to blend the last frame with the curr | |||
| This is used as a dependency in other plugins for progressive rendering effect which is useful for progressive shadows, gi, denoising, baking, anti-aliasing, and many other effects. The helper function `convergedPromise` returns a new promise that can be used to wait for the progressive rendering to converge. | |||
| ## SSAAPlugin | |||
| [//]: # (todo: image) | |||
| [Example](https://threepipe.org/examples/#ssaa-plugin/) — | |||
| [Source Code](./src/plugins/pipeline/SSAAPlugin.ts) — | |||
| [API Reference](https://threepipe.org/docs/classes/SSAAPlugin.html) | |||
| SSAA Plugin adds support for [Super Sampling Anti-Aliasing](https://en.wikipedia.org/wiki/Supersampling) to the viewer. Simply add the plugin to the viewer to use it. | |||
| It jitters the camera view offset over multiple frames, which are then blended by the [ProgressivePlugin](#progressiveplugin) to create a higher quality image. This is useful for reducing aliasing artifacts in the scene. | |||
| By default, the pipeline only renders once per request animation frame. So we don't get any anti-aliasing while moving. For that, either use the TAA(Temporal Anti-aliasing) plugin or for the case of simple scenes - render multiple times per frame which can be done by setting `plugin.rendersPerFrame` or `viewer.rendersPerFrame`. Check out the [example](https://threepipe.org/examples/#ssaa-plugin/) to see the effect on frame rate. | |||
| ```typescript | |||
| const ssaa = viewer.addPluginSync(new SSAAPlugin()) | |||
| ssaa.enabled = true // toggle jittering(if you want to set custom view offset) | |||
| ssaa.rendersPerFrame = 4 // render 4 times per frame (max 32 is useful) | |||
| ``` | |||
| ## DepthBufferPlugin | |||
| [//]: # (todo: image) | |||
| @@ -323,6 +323,7 @@ | |||
| <ul> | |||
| <li><a href="./progressive-plugin/">Progressive Plugin </a></li> | |||
| <li><a href="./custom-pipeline/">Custom Pipeline specification </a></li> | |||
| <li><a href="./ssaa-plugin/">SSAA Plugin </a></li> | |||
| <li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li> | |||
| <li><a href="./normal-buffer-plugin/">Normal Buffer Plugin </a></li> | |||
| <li><a href="./gbuffer-plugin/">GBuffer Plugin <br/>(NormalDepth+Flags) </a></li> | |||
| @@ -10,6 +10,7 @@ import { | |||
| ProgressivePlugin, | |||
| ShaderChunk, | |||
| shaderReplaceString, | |||
| SSAAPlugin, | |||
| ThreeViewer, | |||
| Vector3, | |||
| } from 'threepipe' | |||
| @@ -39,7 +40,14 @@ async function init() { | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: false, | |||
| rgbm: false, | |||
| plugins: [new ProgressivePlugin((window as any).TESTING ? 20 : 200)], | |||
| plugins: [new ProgressivePlugin((window as any).TESTING ? 20 : 200), SSAAPlugin], | |||
| dropzone: { | |||
| addOptions: { | |||
| disposeSceneObjects: true, | |||
| autoSetEnvironment: true, | |||
| autoSetBackground: true, | |||
| }, | |||
| }, | |||
| }) | |||
| const directionalLight = createDirLight(viewer) | |||
| @@ -9,17 +9,26 @@ import { | |||
| PlaneGeometry, | |||
| ProgressivePlugin, | |||
| RenderTargetPreviewPlugin, | |||
| SSAAPlugin, | |||
| ThreeViewer, | |||
| Vector3, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| rgbm: false, | |||
| dropzone: { | |||
| addOptions: { | |||
| disposeSceneObjects: true, | |||
| importConfig: true, | |||
| }, | |||
| }, | |||
| }) | |||
| viewer.addPluginSync(new ProgressivePlugin((window as any).TESTING ? 20 : 200)) | |||
| viewer.addPluginSync(new SSAAPlugin()) | |||
| // viewer.scene.addObject(new HemisphereLight(0xffffff, 0x444444, 10)) | |||
| const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/fbx/Samba Dancing.fbx', { | |||
| @@ -60,7 +69,6 @@ async function init() { | |||
| const rt = viewer.addPluginSync(RenderTargetPreviewPlugin) | |||
| rt.addTarget(()=>directionalLight.shadow.map || undefined, 'shadow', true, true, true) | |||
| viewer.addPluginSync(new ProgressivePlugin((window as any).TESTING ? 20 : 200)) | |||
| viewer.addEventListener('postFrame', ()=>{ | |||
| if (viewer.renderManager.frameCount < 1) return | |||
| @@ -0,0 +1,36 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>SSAA Plugin</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- Import maps polyfill --> | |||
| <!-- Remove this when import maps will be widely supported --> | |||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||
| <script type="importmap"> | |||
| { | |||
| "imports": { | |||
| "threepipe": "./../../dist/index.mjs", | |||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,58 @@ | |||
| import {_testFinish, IObject3D, PhysicalMaterial, ProgressivePlugin, SSAAPlugin, ThreeViewer} from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| debug: true, | |||
| msaa: false, | |||
| rgbm: true, | |||
| renderScale: 1, | |||
| dropzone: { | |||
| addOptions: { | |||
| disposeSceneObjects: true, | |||
| importConfig: true, | |||
| }, | |||
| }, | |||
| }) | |||
| viewer.addPluginSync(new SSAAPlugin()) | |||
| await Promise.all([ | |||
| viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr'), | |||
| // viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/IridescenceLamp.glb', { | |||
| // autoCenter: true, | |||
| // autoScale: true, | |||
| // }), | |||
| viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/LittlestTokyo.glb', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }), | |||
| ]) | |||
| viewer.scene.overrideMaterial = new PhysicalMaterial({ | |||
| color: 'white', | |||
| roughness: 1, | |||
| metalness: 0, | |||
| wireframe: true, | |||
| }) | |||
| viewer.scene.mainCamera.position.set(0, 0, 3.5) | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| ui.appendChild({ | |||
| type: 'toggle', | |||
| label: 'Auto Rotate', | |||
| property: [viewer.scene.mainCamera.controls, 'autoRotate'], | |||
| }) | |||
| ui.setupPluginUi(SSAAPlugin, { | |||
| expanded: true, | |||
| }) | |||
| ui.setupPlugins(ProgressivePlugin) | |||
| await viewer.getPlugin(ProgressivePlugin)?.convergedPromise | |||
| console.log('converged') | |||
| } | |||
| init().finally(_testFinish) | |||
| @@ -1,17 +1,10 @@ | |||
| import { | |||
| _testFinish, | |||
| ProgressivePlugin, | |||
| RenderTargetPreviewPlugin, | |||
| SSAOPlugin, | |||
| ThreeViewer, | |||
| UnsignedByteType, | |||
| } from 'threepipe' | |||
| import {_testFinish, RenderTargetPreviewPlugin, SSAAPlugin, SSAOPlugin, ThreeViewer, UnsignedByteType} from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| plugins: [ProgressivePlugin], | |||
| plugins: [SSAAPlugin], | |||
| tonemap: false, | |||
| }) | |||
| @@ -34,6 +34,7 @@ import { | |||
| RenderTargetPreviewPlugin, | |||
| Rhino3dmLoadPlugin, | |||
| SceneUiConfigPlugin, | |||
| SSAAPlugin, | |||
| SSAOPlugin, | |||
| STLLoadPlugin, | |||
| ThreeFirstPersonControlsPlugin, | |||
| @@ -74,6 +75,7 @@ async function init() { | |||
| await viewer.addPlugins([ | |||
| new ProgressivePlugin(), | |||
| new SSAAPlugin(), | |||
| GLTFAnimationPlugin, | |||
| PickingPlugin, | |||
| new TransformControlsPlugin(false), | |||
| @@ -126,7 +128,7 @@ async function init() { | |||
| editor.loadPlugins({ | |||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin, TweakpaneUiPlugin], | |||
| ['Scene']: [ContactShadowGroundPlugin], | |||
| ['Scene']: [SSAAPlugin, ContactShadowGroundPlugin], | |||
| ['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, Object3DGeneratorPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin, Object3DWidgetsPlugin, MeshOptSimplifyModifierPlugin], | |||
| ['GBuffer']: [GBufferPlugin, DepthBufferPlugin, NormalBufferPlugin], | |||
| ['Post-processing']: [TonemapPlugin, ProgressivePlugin, SSAOPlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin], | |||
| @@ -5,6 +5,7 @@ import { | |||
| PerspectiveCamera2, | |||
| ProgressivePlugin, | |||
| RenderTargetPreviewPlugin, | |||
| SSAAPlugin, | |||
| ThreeViewer, | |||
| Vector3, | |||
| VirtualCamerasPlugin, | |||
| @@ -16,7 +17,7 @@ async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| debug: true, | |||
| plugins: [new ProgressivePlugin(16)], | |||
| plugins: [new ProgressivePlugin(16), SSAAPlugin], | |||
| }) | |||
| const virtualCameras = viewer.addPluginSync(VirtualCamerasPlugin) | |||
| @@ -14,6 +14,7 @@ export type {ProgressivePluginEventTypes, ProgressivePluginTarget} from './pipel | |||
| export type {GBufferPluginEventTypes, GBufferPluginPass, GBufferUpdater, GBufferUpdaterContext} from './pipeline/GBufferPlugin' | |||
| export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | |||
| export type {NormalBufferPluginEventTypes, NormalBufferPluginPass, NormalBufferPluginTarget} from './pipeline/NormalBufferPlugin' | |||
| export {SSAAPlugin, type SSAAPluginEventTypes} from './pipeline/SSAAPlugin' | |||
| export {SSAOPlugin, SSAOPluginPass, type SSAOPluginEventTypes, type SSAOPluginTarget} from './pipeline/SSAOPlugin' | |||
| // ui | |||
| @@ -1,12 +1,14 @@ | |||
| import {IUniform, Texture, TextureDataType, UnsignedByteType, WebGLRenderTarget} from 'three' | |||
| import {IPassID, IPipelinePass} from '../../postprocessing' | |||
| import {ThreeViewer} from '../../viewer' | |||
| import {ISerializedConfig, ThreeViewer} from '../../viewer' | |||
| import {PipelinePassPlugin} from '../base/PipelinePassPlugin' | |||
| import {uiFolderContainer, uiImage, uiInput} from 'uiconfig.js' | |||
| import {ICamera, IRenderManager, IScene, IWebGLRenderer} from '../../core' | |||
| import {AddBlendTexturePass} from '../../postprocessing/AddBlendTexturePass' | |||
| import {getOrCall, serialize, ValOrFunc} from 'ts-browser-helpers' | |||
| import {IShaderPropertiesUpdater} from '../../materials' | |||
| import {SerializationMetaType} from '../../utils' | |||
| import {SSAAPlugin} from './SSAAPlugin' | |||
| export type ProgressivePluginEventTypes = '' | |||
| export type ProgressivePluginTarget = WebGLRenderTarget | |||
| @@ -24,6 +26,7 @@ export class ProgressivePlugin | |||
| readonly passId = 'progressive' | |||
| public static readonly PluginType = 'ProgressivePlugin' | |||
| public static readonly OldPluginType = 'Progressive' | |||
| /** | |||
| * Different targets for different render cameras. | |||
| @@ -33,7 +36,6 @@ export class ProgressivePlugin | |||
| protected _targets = new Map<string, ProgressivePluginTarget>() | |||
| @serialize() @uiInput('Frame count') maxFrameCount: number | |||
| // todo: deserialize jitter | |||
| // @uiImage('Last Texture' /* {readOnly: true}*/) texture?: Texture | |||
| @@ -58,13 +60,16 @@ export class ProgressivePlugin | |||
| return this._viewer ? this.getTarget(this._viewer.scene.mainCamera)?.texture : undefined | |||
| } | |||
| /** | |||
| * Note - this is not used right now | |||
| */ | |||
| // @onChange2(ProgressivePlugin.prototype._createTarget) | |||
| // @uiDropdown('Buffer Type', threeConstMappings.TextureDataType.uiConfig) | |||
| readonly bufferType: TextureDataType // cannot be changed after creation (for now) | |||
| constructor( | |||
| maxFrameCount = 32, | |||
| bufferType: TextureDataType = UnsignedByteType, | |||
| bufferType: TextureDataType = UnsignedByteType, // this is not used. todo use halffloat when rgbm = false | |||
| enabled = true, | |||
| ) { | |||
| super() | |||
| @@ -154,6 +159,21 @@ export class ProgressivePlugin | |||
| }) | |||
| } | |||
| fromJSON(data: ISerializedConfig&{pass?: any}, meta?: SerializationMetaType): this|null|Promise<this|null> { | |||
| console.log(data) | |||
| if (data.jitter !== undefined) { | |||
| const ssaa = this._viewer?.getPlugin(SSAAPlugin) | |||
| if (!ssaa) { | |||
| console.warn('Loading old webgi v0 file, add SSAAPlugin to get anti-aliasing') | |||
| } else { | |||
| data = {...data} | |||
| ssaa.enabled = data.jitter | |||
| delete data.jitter | |||
| } | |||
| } | |||
| return super.fromJSON(data, meta) | |||
| } | |||
| } | |||
| class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass { | |||
| @@ -0,0 +1,155 @@ | |||
| import {OrthographicCamera, PerspectiveCamera} from 'three' | |||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||
| import {uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | |||
| import {IEvent, onChange, serialize} from 'ts-browser-helpers' | |||
| import {ICamera, ILight} from '../../core' | |||
| import {ProgressivePlugin} from './ProgressivePlugin' | |||
| export type SSAAPluginEventTypes = '' | |||
| export type TCamera = ICamera & (PerspectiveCamera|OrthographicCamera) | |||
| /** | |||
| * SSAA Plugin | |||
| * | |||
| * Jitters the render camera and optionally other cameras in the scene | |||
| * to create a super-sampled anti-aliasing effect. | |||
| * This is done across multiple frames by integrating with the ProgressivePlugin | |||
| * @category Plugins | |||
| */ | |||
| @uiFolderContainer('SSAA Plugin') | |||
| export class SSAAPlugin extends AViewerPluginSync<SSAAPluginEventTypes> { | |||
| public static readonly PluginType = 'SSAAPlugin' | |||
| @serialize() @uiToggle('Enabled') | |||
| @onChange(SSAAPlugin.prototype.setDirty) | |||
| enabled = true | |||
| @serialize() @uiSlider('Renders/Frame', [1, 32], 1) | |||
| @onChange(SSAAPlugin.prototype.setDirty) | |||
| rendersPerFrame = 1 | |||
| @serialize() @uiToggle('Render Camera') | |||
| @onChange(SSAAPlugin.prototype.setDirty) | |||
| jitterRenderCamera = true | |||
| @serialize() @uiToggle('Light Cameras') | |||
| @onChange(SSAAPlugin.prototype.setDirty) | |||
| jitterLightCameras = true | |||
| private _hasSetOffsetRC = false | |||
| private _hasSetOffsetLC = false | |||
| public trackedJitterCameras = new Set<[TCamera, {width: number, height: number}]>() // todo register other cameras and light shadows cameras when added to the scene and changed. | |||
| dependencies = [ProgressivePlugin] | |||
| onAdded(viewer: ThreeViewer) { | |||
| super.onAdded(viewer) | |||
| viewer.addEventListener('preRender', this._preRender) | |||
| viewer.addEventListener('postRender', this._postRender) | |||
| viewer.scene.addEventListener('addSceneObject', this._addSceneObject) | |||
| } | |||
| onRemove(viewer: ThreeViewer): void { | |||
| viewer.removeEventListener('preRender', this._preRender) | |||
| viewer.removeEventListener('postRender', this._postRender) | |||
| viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) | |||
| return super.onRemove(viewer) | |||
| } | |||
| setDirty() { | |||
| if (!this._viewer) return | |||
| this._viewer.rendersPerFrame = this.rendersPerFrame | |||
| this._viewer.setDirty() | |||
| this.uiConfig?.uiRefresh?.(true, 'postFrame') | |||
| } | |||
| private _addSceneObject = (event: IEvent<string>)=> { | |||
| event.object?.traverse((o: ILight)=>{ | |||
| if (o && o.shadow && o.shadow.camera && o.shadow.mapSize) { | |||
| this.trackedJitterCameras.add([o.shadow.camera as TCamera, o.shadow.mapSize]) | |||
| } | |||
| // if (o?.material) { | |||
| // if (o.material.alphaMap) console.log(o.material) //todo why? | |||
| // } | |||
| }) | |||
| } | |||
| private _jitter(camera: TCamera, size: { | |||
| width: number, | |||
| height: number | |||
| }, frameCount: number) { | |||
| if (camera.userData.disableJitter) return | |||
| if (camera.userData.__jittered) { | |||
| this._viewer?.console.warn('SSAAPlugin: Camera already jittered') | |||
| return | |||
| } | |||
| const sample = {...this.jitterOffsets[frameCount % this.jitterOffsets.length]} | |||
| // const sample = {...offsets[Math.floor(Math.random() * (offsets.length - 0.001))]} | |||
| // { | |||
| // sample.x += 1 * (Math.random() - 0.5) | |||
| // sample.y += 1 * (Math.random() - 0.5) | |||
| // } | |||
| camera.setViewOffset(size.width, size.height, sample.x, sample.y, size.width, size.height) | |||
| camera.userData.__jittered = true | |||
| } | |||
| private _clearJitter(camera: TCamera) { | |||
| if (!camera.userData.__jittered) return | |||
| camera.clearViewOffset() | |||
| delete camera.userData.__jittered | |||
| } | |||
| private _preRender = ()=> { | |||
| const v = this._viewer | |||
| if (!v || !this.enabled || v.renderManager.frameCount <= 1) return | |||
| this.rendersPerFrame = v.rendersPerFrame // just to sync. todo: should this be here?. ideally there should be a event fired from the viewer when the prop changes | |||
| const cam = v.scene.renderCamera as TCamera | |||
| if (this.jitterRenderCamera) this._jitter(cam, { | |||
| width: v.renderManager.renderSize.x * v.renderManager.renderScale, | |||
| height: v.renderManager.renderSize.y * v.renderManager.renderScale, | |||
| }, v.renderManager.frameCount) | |||
| if (this.jitterLightCameras) | |||
| this.trackedJitterCameras.forEach((a) => this._jitter(...a, v.renderManager.frameCount)) | |||
| this._hasSetOffsetRC = this.jitterRenderCamera | |||
| this._hasSetOffsetLC = this.jitterLightCameras | |||
| v.renderManager.resetShadows() | |||
| } | |||
| private _postRender = ()=> { | |||
| const v = this._viewer | |||
| if (!v) return | |||
| if (this._hasSetOffsetRC) { | |||
| this._clearJitter(v.scene.renderCamera as TCamera) | |||
| this._hasSetOffsetRC = false | |||
| } | |||
| if (this._hasSetOffsetLC) { | |||
| this.trackedJitterCameras.forEach(([camera]) => this._clearJitter(camera)) | |||
| this._hasSetOffsetLC = false | |||
| } | |||
| } | |||
| jitterOffsets = [ | |||
| {x: 0, y: 0}, | |||
| {x: -0.5, y: 0}, | |||
| {x: -0.375, y: -0.25}, | |||
| {x: -0.1875, y: -0.125}, | |||
| {x: -0.125, y: -0.375}, | |||
| {x: 0.0625, y: -0.0625}, | |||
| {x: 0.125, y: -0.3125}, | |||
| {x: 0.375, y: -0.4375}, | |||
| {x: 0.3125, y: -0.1875}, | |||
| {x: 0.25, y: 0.0625}, | |||
| {x: 0.4375, y: 0.25}, | |||
| {x: 0.1875, y: 0.3125}, | |||
| {x: 0, y: 0.4375}, | |||
| {x: -0.0625, y: 0.1875}, | |||
| {x: -0.25, y: 0.375}, | |||
| {x: -0.4375, y: 0.5}, | |||
| {x: -0.3125, y: 0.125}, | |||
| ] | |||
| } | |||