| <h2 class="category">Post-Processing</h2> | <h2 class="category">Post-Processing</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./tonemap-plugin/">Tonemap Plugin </a></li> | <li><a href="./tonemap-plugin/">Tonemap Plugin </a></li> | ||||
| <li><a href="./vignette-plugin/">Vignette Plugin </a></li> | |||||
| <li><a href="./frame-fade-plugin/">Frame Fade Plugin </a></li> | <li><a href="./frame-fade-plugin/">Frame Fade Plugin </a></li> | ||||
| </ul> | </ul> | ||||
| <h2 class="category">Rendering</h2> | <h2 class="category">Rendering</h2> |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Vignette 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> |
| import {_testFinish, IObject3D, ThreeViewer, VignettePlugin} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| }) | |||||
| // A GBuffer(depth buffer here) is required for the `tonemapBackground` flag in TonemapPlugin to work | |||||
| viewer.addPluginSync(VignettePlugin) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf') | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| ui.setupPluginUi(VignettePlugin) | |||||
| } | |||||
| init().then(_testFinish) |
| export {KTX2LoadPlugin} from './import/KTX2LoadPlugin' | export {KTX2LoadPlugin} from './import/KTX2LoadPlugin' | ||||
| // postprocessing | // postprocessing | ||||
| export {AScreenPassExtensionPlugin} from './postprocessing/AScreenPassExtensionPlugin' | |||||
| export {TonemapPlugin} from './postprocessing/TonemapPlugin' | export {TonemapPlugin} from './postprocessing/TonemapPlugin' | ||||
| export {VignettePlugin} from './postprocessing/VignettePlugin' | |||||
| // animation | // animation | ||||
| export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' |
| import {AViewerPlugin, AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {MaterialExtension} from '../../materials' | |||||
| import {Shader, Vector4, WebGLRenderer} from 'three' | |||||
| import {IMaterial} from '../../core' | |||||
| import {shaderReplaceString} from '../../utils' | |||||
| // todo move | |||||
| export interface GBufferUpdater { | |||||
| updateGBufferFlags: (material: IMaterial, data: Vector4) => void | |||||
| } | |||||
| /** | |||||
| * Base Screen Pass Extension Plugin | |||||
| * | |||||
| * Extend the class to add an extension to {@link ScreenPass} material. | |||||
| * See {@link TonemapPlugin} and {@link VignettePlugin} for examples. | |||||
| * | |||||
| * | |||||
| * @category Plugins | |||||
| */ | |||||
| export abstract class AScreenPassExtensionPlugin<T extends string> extends AViewerPluginSync<T> implements MaterialExtension, GBufferUpdater { | |||||
| declare ['constructor']: (typeof AScreenPassExtensionPlugin) & (typeof AViewerPluginSync) & (typeof AViewerPlugin) | |||||
| abstract enabled: boolean | |||||
| set uniformsNeedUpdate(v: boolean) { // for @uniform decorator | |||||
| if (v) this.setDirty() | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| this.setDirty = this.setDirty.bind(this) | |||||
| } | |||||
| /** | |||||
| * The priority of the material extension when applied to the material in ScreenPass | |||||
| * set to very low priority, so applied at the end | |||||
| */ | |||||
| priority = -100 | |||||
| protected _shaderPatch = '' | |||||
| shaderExtender(shader: Shader, _: IMaterial, _1: WebGLRenderer): void { | |||||
| if (!this.enabled) return | |||||
| shader.fragmentShader = shaderReplaceString( | |||||
| shader.fragmentShader, | |||||
| '#glMarker', '\n' + this._shaderPatch + '\n', | |||||
| {prepend: true} | |||||
| ) | |||||
| } | |||||
| getUiConfig(): any { | |||||
| return this.uiConfig | |||||
| } | |||||
| computeCacheKey = (_: IMaterial) => this.enabled ? '1' : '0' | |||||
| isCompatible(_: IMaterial): boolean { | |||||
| return true // (material as MeshStandardMaterial2).isMeshStandardMaterial2 | |||||
| } | |||||
| setDirty() { | |||||
| this.__setDirty?.() // this will update version which will set needsUpdate on material | |||||
| this._viewer?.renderManager.screenPass.setDirty() | |||||
| } | |||||
| fromJSON(data: any, meta?: any): this | null | Promise<this | null> { | |||||
| // really old legacy | |||||
| if (data.pass) { | |||||
| data = {...data} | |||||
| data.extension = {...data.pass} | |||||
| delete data.extension.enabled | |||||
| delete data.pass | |||||
| } | |||||
| // legacy | |||||
| if (data.extension) { | |||||
| data = {...data, ...data.extension} | |||||
| delete data.extension | |||||
| } | |||||
| return super.fromJSON(data, meta) | |||||
| } | |||||
| onAdded(viewer: ThreeViewer) { | |||||
| super.onAdded(viewer) | |||||
| // viewer.getPlugin(GBufferPlugin)?.registerGBufferUpdater(this.updateGBufferFlags) // todo | |||||
| viewer.renderManager.screenPass.material.registerMaterialExtensions([this]) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | |||||
| // viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.updateGBufferFlags) | |||||
| viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this]) | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| // updateGBufferFlags(material: IMaterial, data: Vector4): void { | |||||
| // const x = material?.userData.postTonemap === false ? 0 : 1 | |||||
| // data.w = updateBit(data.w, 1, x) // 2nd Bit | |||||
| // } | |||||
| // for typescript | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| __setDirty?: () => void | |||||
| updateGBufferFlags(_: IMaterial, _1: Vector4): void { | |||||
| return | |||||
| } | |||||
| } |
| // noinspection ES6PreferShortImport | // noinspection ES6PreferShortImport | ||||
| import {AViewerPluginSync} from '../../viewer/AViewerPlugin' | |||||
| import type {ThreeViewer} from '../../viewer' | |||||
| import {MaterialExtension} from '../../materials' | |||||
| import {uiDropdown, uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | import {uiDropdown, uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | ||||
| import { | import { | ||||
| ACESFilmicToneMapping, | ACESFilmicToneMapping, | ||||
| LinearToneMapping, | LinearToneMapping, | ||||
| Object3D, | Object3D, | ||||
| ReinhardToneMapping, | ReinhardToneMapping, | ||||
| Shader, | |||||
| ShaderChunk, | ShaderChunk, | ||||
| ToneMapping, | ToneMapping, | ||||
| Vector4, | Vector4, | ||||
| } from 'three' | } from 'three' | ||||
| import {glsl, onChange, serialize} from 'ts-browser-helpers' | import {glsl, onChange, serialize} from 'ts-browser-helpers' | ||||
| import {IMaterial} from '../../core' | import {IMaterial} from '../../core' | ||||
| import {shaderReplaceString, updateBit} from '../../utils' | |||||
| import {updateBit} from '../../utils' | |||||
| import {matDefine, uniform} from '../../three' | import {matDefine, uniform} from '../../three' | ||||
| import Uncharted2ToneMapping from './shaders/Uncharted2ToneMapping.glsl' | |||||
| import Uncharted2ToneMappingShader from './shaders/Uncharted2ToneMapping.glsl' | |||||
| import TonemapShader from './shaders/TonemapPlugin.pars.glsl' | import TonemapShader from './shaders/TonemapPlugin.pars.glsl' | ||||
| import TonemapShaderPatch from './shaders/TonemapPlugin.patch.glsl' | import TonemapShaderPatch from './shaders/TonemapPlugin.patch.glsl' | ||||
| // todo move | |||||
| export interface GBufferUpdater { | |||||
| updateGBufferFlags: (material: IMaterial, data: Vector4) => void | |||||
| } | |||||
| import {AScreenPassExtensionPlugin} from './AScreenPassExtensionPlugin' | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
| export const Uncharted2Tonemapping: ToneMapping = CustomToneMapping | export const Uncharted2Tonemapping: ToneMapping = CustomToneMapping | ||||
| * @category Plugins | * @category Plugins | ||||
| */ | */ | ||||
| @uiFolderContainer('Tonemapping') | @uiFolderContainer('Tonemapping') | ||||
| export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExtension, GBufferUpdater { | |||||
| export class TonemapPlugin extends AScreenPassExtensionPlugin<''> { | |||||
| static readonly PluginType = 'Tonemap' | static readonly PluginType = 'Tonemap' | ||||
| readonly extraUniforms = { | |||||
| toneMappingContrast: {value: 1}, | |||||
| toneMappingSaturation: {value: 1}, | |||||
| } as const | |||||
| readonly extraDefines = { | |||||
| ['TONEMAP_BACKGROUND']: '1', | |||||
| } as const | |||||
| @serialize() @uiToggle('Enabled') enabled = true | @serialize() @uiToggle('Enabled') enabled = true | ||||
| @uiDropdown('Mode', ([ | @uiDropdown('Mode', ([ | ||||
| label: value[0], | label: value[0], | ||||
| value: value[1], | value: value[1], | ||||
| }))) | }))) | ||||
| @onChange(TonemapPlugin.prototype.setDirty) | @onChange(TonemapPlugin.prototype.setDirty) | ||||
| @serialize() toneMapping: ToneMapping = ACESFilmicToneMapping | @serialize() toneMapping: ToneMapping = ACESFilmicToneMapping | ||||
| @uniform({propKey: 'toneMappingContrast'}) | @uniform({propKey: 'toneMappingContrast'}) | ||||
| @serialize() contrast: number | @serialize() contrast: number | ||||
| readonly extraUniforms = { | |||||
| toneMappingContrast: {value: 1}, | |||||
| toneMappingSaturation: {value: 1}, | |||||
| } as const | |||||
| set uniformsNeedUpdate(v: boolean) { // for @uniform decorator | |||||
| if (v) this.setDirty() | |||||
| } | |||||
| /** | |||||
| * The priority of the material extension when applied to the material in ScreenPass | |||||
| * set to very low priority, so applied at the end | |||||
| */ | |||||
| priority = -100 | |||||
| parsFragmentSnippet: any = (_: WebGLRenderer, _1: IMaterial) => { | |||||
| parsFragmentSnippet = () => { | |||||
| if (!this.enabled) return '' | if (!this.enabled) return '' | ||||
| return glsl` | return glsl` | ||||
| ` | ` | ||||
| } | } | ||||
| constructor() { | |||||
| super() | |||||
| this.setDirty = this.setDirty.bind(this) | |||||
| } | |||||
| /** | |||||
| * The priority of the material extension when applied to the material in ScreenPass | |||||
| * set to very low priority, so applied at the end | |||||
| */ | |||||
| readonly priority = -100 | |||||
| shaderExtender(shader: Shader, _: IMaterial, _1: WebGLRenderer): void { | |||||
| if (!this.enabled) return | |||||
| shader.fragmentShader = shaderReplaceString( | |||||
| shader.fragmentShader, | |||||
| '#glMarker', '\n' + TonemapShaderPatch + '\n', | |||||
| {prepend: true} | |||||
| ) | |||||
| } | |||||
| readonly extraDefines = { | |||||
| ['TONEMAP_BACKGROUND']: '1', | |||||
| } as const | |||||
| protected _shaderPatch = TonemapShaderPatch | |||||
| private _rendererState: any = {} | private _rendererState: any = {} | ||||
| renderer.toneMappingExposure = this._rendererState.toneMappingExposure | renderer.toneMappingExposure = this._rendererState.toneMappingExposure | ||||
| } | } | ||||
| getUiConfig(): any { | |||||
| return this.uiConfig | |||||
| } | |||||
| computeCacheKey = (_: IMaterial) => this.enabled ? '1' : '0' | |||||
| isCompatible(_: IMaterial): boolean { | |||||
| return true // (material as MeshStandardMaterial2).isMeshStandardMaterial2 | |||||
| } | |||||
| setDirty() { | |||||
| this.__setDirty?.() // this will update version which will set needsUpdate on material | |||||
| this._viewer?.renderManager.screenPass.setDirty() | |||||
| } | |||||
| fromJSON(data: any, meta?: any): this|null|Promise<this|null> { | fromJSON(data: any, meta?: any): this|null|Promise<this|null> { | ||||
| // really pld legacy | |||||
| if (data.pass) { | |||||
| data = {...data} | |||||
| data.extension = {...data.pass} | |||||
| delete data.extension.enabled | |||||
| delete data.pass | |||||
| } | |||||
| // legacy | // legacy | ||||
| if (data.extension) { | if (data.extension) { | ||||
| data = {...data, ...data.extension} | |||||
| delete data.extension | |||||
| if (data.clipBackground !== undefined) { | if (data.clipBackground !== undefined) { | ||||
| if (this._viewer) this._viewer.renderManager.screenPass.clipBackground = data.clipBackground | if (this._viewer) this._viewer.renderManager.screenPass.clipBackground = data.clipBackground | ||||
| else console.warn('TonemapPlugin: no viewer attached, clipBackground ignored') | else console.warn('TonemapPlugin: no viewer attached, clipBackground ignored') | ||||
| return super.fromJSON(data, meta) | return super.fromJSON(data, meta) | ||||
| } | } | ||||
| onAdded(viewer: ThreeViewer) { | |||||
| super.onAdded(viewer) | |||||
| // viewer.getPlugin(GBufferPlugin)?.registerGBufferUpdater(this.updateGBufferFlags) // todo | |||||
| viewer.renderManager.screenPass.material.registerMaterialExtensions([this]) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | |||||
| // viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.updateGBufferFlags) | |||||
| viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this]) | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| updateGBufferFlags(material: IMaterial, data: Vector4): void { | updateGBufferFlags(material: IMaterial, data: Vector4): void { | ||||
| const x = material?.userData.postTonemap === false ? 0 : 1 | const x = material?.userData.postTonemap === false ? 0 : 1 | ||||
| data.w = updateBit(data.w, 1, x) // 2nd Bit | data.w = updateBit(data.w, 1, x) // 2nd Bit | ||||
| super.updateGBufferFlags(material, data) | |||||
| } | } | ||||
| static { | static { | ||||
| // Add support for Uncharted2 tone mapping | // Add support for Uncharted2 tone mapping | ||||
| ShaderChunk.tonemapping_pars_fragment = ShaderChunk.tonemapping_pars_fragment.replace('vec3 CustomToneMapping( vec3 color ) { return color; }', Uncharted2ToneMapping) | |||||
| ShaderChunk.tonemapping_pars_fragment = ShaderChunk.tonemapping_pars_fragment.replace('vec3 CustomToneMapping( vec3 color ) { return color; }', Uncharted2ToneMappingShader) | |||||
| } | } | ||||
| // for typescript | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| __setDirty?: () => void | |||||
| } | } |
| import {uiColor, uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | |||||
| import {Color} from 'three' | |||||
| import {glsl, onChange, serialize} from 'ts-browser-helpers' | |||||
| import {uniform} from '../../three' | |||||
| import vignette from './shaders/VignettePlugin.glsl' | |||||
| import {AScreenPassExtensionPlugin} from './AScreenPassExtensionPlugin' | |||||
| /** | |||||
| * Vignette Plugin | |||||
| * | |||||
| * Adds an extension to {@link ScreenPass} material | |||||
| * for applying vignette effect on the final buffer before rendering to screen. | |||||
| * The power of the vignette can be controlled with the `power` property. | |||||
| * The color of the vignette can be controlled with the `color`(previously `bgcolor`) property. | |||||
| * | |||||
| * @category Plugins | |||||
| */ | |||||
| @uiFolderContainer('Vignette') | |||||
| export class VignettePlugin extends AScreenPassExtensionPlugin<''> { | |||||
| static readonly PluginType = 'Vignette' | |||||
| readonly extraUniforms = { | |||||
| power: {value: 1}, | |||||
| bgcolor: {value: new Color()}, | |||||
| } as const | |||||
| @onChange(VignettePlugin.prototype.setDirty) | |||||
| @uiToggle('Enable') | |||||
| @serialize() enabled = false | |||||
| @uiSlider('Power', [0.1, 4], 0.01) | |||||
| @uniform({propKey: 'power'}) | |||||
| @serialize() power = 0.5 | |||||
| @uiColor<VignettePlugin>('Color', t=>({onChange:()=>t?.setDirty()})) | |||||
| @uniform({propKey: 'bgcolor'}) | |||||
| @serialize('bgcolor') color = new Color(0x000000) | |||||
| /** | |||||
| * The priority of the material extension when applied to the material in ScreenPass | |||||
| * set to very low priority, so applied at the end | |||||
| */ | |||||
| priority = -50 | |||||
| parsFragmentSnippet = () => { | |||||
| if (!this.enabled) return '' | |||||
| return glsl` | |||||
| uniform float power; | |||||
| uniform vec3 bgcolor; | |||||
| ${vignette} | |||||
| ` | |||||
| } | |||||
| protected _shaderPatch = 'diffuseColor = Vignette(diffuseColor);' | |||||
| get bgcolor() { | |||||
| console.warn('VignettePlugin.bgcolor is deprecated, use VignettePlugin.color instead') | |||||
| return this.color | |||||
| } | |||||
| set bgcolor(v) { | |||||
| console.warn('VignettePlugin.bgcolor is deprecated, use VignettePlugin.color instead') | |||||
| this.color = v | |||||
| } | |||||
| } |
| vec4 Vignette(in vec4 color) { | |||||
| vec2 uv = vUv * (1.0 - vUv); | |||||
| float vig = uv.x * uv.y * 16.0; // max value of this function is 1/16 at the centre(0.5, 0.5) | |||||
| vig = pow(vig, power); | |||||
| return vec4( mix( color.rgb, vec3( bgcolor ), 1. - vig ), color.a ); | |||||
| } |