| @@ -220,6 +220,7 @@ | |||
| <h2 class="category">Post-Processing</h2> | |||
| <ul> | |||
| <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> | |||
| </ul> | |||
| <h2 class="category">Rendering</h2> | |||
| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,21 @@ | |||
| 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) | |||
| @@ -31,7 +31,9 @@ export {KTXLoadPlugin} from './import/KTXLoadPlugin' | |||
| export {KTX2LoadPlugin} from './import/KTX2LoadPlugin' | |||
| // postprocessing | |||
| export {AScreenPassExtensionPlugin} from './postprocessing/AScreenPassExtensionPlugin' | |||
| export {TonemapPlugin} from './postprocessing/TonemapPlugin' | |||
| export {VignettePlugin} from './postprocessing/VignettePlugin' | |||
| // animation | |||
| export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | |||
| @@ -0,0 +1,109 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -1,7 +1,4 @@ | |||
| // 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 { | |||
| ACESFilmicToneMapping, | |||
| @@ -10,7 +7,6 @@ import { | |||
| LinearToneMapping, | |||
| Object3D, | |||
| ReinhardToneMapping, | |||
| Shader, | |||
| ShaderChunk, | |||
| ToneMapping, | |||
| Vector4, | |||
| @@ -18,16 +14,12 @@ import { | |||
| } from 'three' | |||
| import {glsl, onChange, serialize} from 'ts-browser-helpers' | |||
| import {IMaterial} from '../../core' | |||
| import {shaderReplaceString, updateBit} from '../../utils' | |||
| import {updateBit} from '../../utils' | |||
| 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 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 | |||
| export const Uncharted2Tonemapping: ToneMapping = CustomToneMapping | |||
| @@ -42,9 +34,18 @@ export const Uncharted2Tonemapping: ToneMapping = CustomToneMapping | |||
| * @category Plugins | |||
| */ | |||
| @uiFolderContainer('Tonemapping') | |||
| export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExtension, GBufferUpdater { | |||
| export class TonemapPlugin extends AScreenPassExtensionPlugin<''> { | |||
| 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 | |||
| @uiDropdown('Mode', ([ | |||
| @@ -57,7 +58,6 @@ export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExte | |||
| label: value[0], | |||
| value: value[1], | |||
| }))) | |||
| @onChange(TonemapPlugin.prototype.setDirty) | |||
| @serialize() toneMapping: ToneMapping = ACESFilmicToneMapping | |||
| @@ -82,16 +82,13 @@ export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExte | |||
| @uniform({propKey: 'toneMappingContrast'}) | |||
| @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 '' | |||
| return glsl` | |||
| @@ -101,30 +98,7 @@ export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExte | |||
| ` | |||
| } | |||
| 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 = {} | |||
| @@ -145,33 +119,9 @@ export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExte | |||
| 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> { | |||
| // really pld 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 | |||
| if (data.clipBackground !== undefined) { | |||
| if (this._viewer) this._viewer.renderManager.screenPass.clipBackground = data.clipBackground | |||
| else console.warn('TonemapPlugin: no viewer attached, clipBackground ignored') | |||
| @@ -181,30 +131,15 @@ export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExte | |||
| 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 | |||
| super.updateGBufferFlags(material, data) | |||
| } | |||
| static { | |||
| // 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 | |||
| } | |||
| @@ -0,0 +1,65 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| 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 ); | |||
| } | |||