| <div class="sidebar" data-selected-example="GLTF Load"> | <div class="sidebar" data-selected-example="GLTF Load"> | ||||
| <button class="hamburger"> ☰</button> | <button class="hamburger"> ☰</button> | ||||
| <h1><a href="https://github.com/repalash/threepipe">ThreePipe</a> Examples</h1> | <h1><a href="https://github.com/repalash/threepipe">ThreePipe</a> Examples</h1> | ||||
| <h2 class="category">Import/Export</h2> | |||||
| <h2 class="category">Import</h2> | |||||
| <ul> | <ul> | ||||
| <li><a href="./fbx-load/">FBX Load </a></li> | <li><a href="./fbx-load/">FBX Load </a></li> | ||||
| <li><a href="./obj-mtl-load/">OBJ MTL Load </a></li> | <li><a href="./obj-mtl-load/">OBJ MTL Load </a></li> | ||||
| <li><a href="./drc-load/">DRACO(DRC) Load </a></li> | <li><a href="./drc-load/">DRACO(DRC) Load </a></li> | ||||
| <li><a href="./hdr-load/">HDR Load </a></li> | <li><a href="./hdr-load/">HDR Load </a></li> | ||||
| <li><a href="./exr-load/">EXR Load </a></li> | <li><a href="./exr-load/">EXR Load </a></li> | ||||
| </ul> | |||||
| <h2 class="category">Export</h2> | |||||
| <ul> | |||||
| <li><a href="./image-snapshot-export/">PNG, JPEG, WEBP Export<br/>(Image Snapshot) </a></li> | <li><a href="./image-snapshot-export/">PNG, JPEG, WEBP Export<br/>(Image Snapshot) </a></li> | ||||
| <li><a href="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li> | <li><a href="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li> | ||||
| <li><a href="./glb-export/">GLB Export </a></li> | <li><a href="./glb-export/">GLB Export </a></li> | ||||
| <li><a href="./pmat-material-export/">PMAT Material export </a></li> | <li><a href="./pmat-material-export/">PMAT Material export </a></li> | ||||
| </ul> | </ul> | ||||
| <h2 class="category">Post-Processing</h2> | |||||
| <ul> | |||||
| <li><a href="./tonemap-plugin/">Tonemap Plugin </a></li> | |||||
| </ul> | |||||
| <h2 class="category">Rendering</h2> | <h2 class="category">Rendering</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li> | <li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li> |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Tonemap Plugin</title> | |||||
| <!-- 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, DepthBufferPlugin, IObject3D, ThreeViewer, TonemapPlugin, UnsignedByteType} 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(new DepthBufferPlugin(UnsignedByteType, true)) | |||||
| viewer.addPluginSync(new TonemapPlugin()) | |||||
| 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(TonemapPlugin) | |||||
| } | |||||
| init().then(_testFinish) |
| import { | import { | ||||
| _testFinish, | _testFinish, | ||||
| AViewerPluginSync, | |||||
| DepthBufferPlugin, | DepthBufferPlugin, | ||||
| DropzonePlugin, | DropzonePlugin, | ||||
| FullScreenPlugin, | FullScreenPlugin, | ||||
| IObject3D, | IObject3D, | ||||
| NormalBufferPlugin, | NormalBufferPlugin, | ||||
| RenderTargetPreviewPlugin, | RenderTargetPreviewPlugin, | ||||
| SceneUiConfigPlugin, | |||||
| ThreeViewer, | ThreeViewer, | ||||
| UnsignedByteType, | |||||
| TonemapPlugin, | |||||
| ViewerUiConfigPlugin, | |||||
| } from 'threepipe' | } from 'threepipe' | ||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | ||||
| import {TweakpaneEditorPlugin} from '@threepipe/plugin-tweakpane-editor' | import {TweakpaneEditorPlugin} from '@threepipe/plugin-tweakpane-editor' | ||||
| class ViewerUiConfig extends AViewerPluginSync<''> { | |||||
| static readonly PluginType = 'ViewerUiConfig' | |||||
| enabled = true | |||||
| toJSON: any = undefined | |||||
| constructor(viewer: ThreeViewer) { | |||||
| super() | |||||
| this._viewer = viewer | |||||
| this.uiConfig = viewer.uiConfig | |||||
| } | |||||
| } | |||||
| async function init() { | async function init() { | ||||
| const viewer = new ThreeViewer({ | const viewer = new ThreeViewer({ | ||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | ||||
| msaa: false, | |||||
| msaa: true, | |||||
| rgbm: true, | |||||
| dropzone: { | dropzone: { | ||||
| addOptions: { | addOptions: { | ||||
| clearSceneObjects: false, | clearSceneObjects: false, | ||||
| const editor = viewer.addPluginSync(new TweakpaneEditorPlugin()) | const editor = viewer.addPluginSync(new TweakpaneEditorPlugin()) | ||||
| await viewer.addPlugins([ | await viewer.addPlugins([ | ||||
| new ViewerUiConfig(viewer), | |||||
| new DepthBufferPlugin(UnsignedByteType, false, false), | |||||
| new ViewerUiConfigPlugin(), | |||||
| // new SceneUiConfigPlugin(), | |||||
| new DepthBufferPlugin(HalfFloatType, true, true), | |||||
| new NormalBufferPlugin(HalfFloatType, false), | new NormalBufferPlugin(HalfFloatType, false), | ||||
| new RenderTargetPreviewPlugin(false), | new RenderTargetPreviewPlugin(false), | ||||
| new TonemapPlugin(), | |||||
| ]) | ]) | ||||
| const rt = viewer.getOrAddPluginSync(RenderTargetPreviewPlugin) | const rt = viewer.getOrAddPluginSync(RenderTargetPreviewPlugin) | ||||
| rt.addTarget(viewer.getPlugin(NormalBufferPlugin)?.target, 'normal', false, true, false) | rt.addTarget(viewer.getPlugin(NormalBufferPlugin)?.target, 'normal', false, true, false) | ||||
| editor.loadPlugins({ | editor.loadPlugins({ | ||||
| ['Viewer']: [ViewerUiConfig, DropzonePlugin, FullScreenPlugin], | |||||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | |||||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | ||||
| ['Post-processing']: [TonemapPlugin], | |||||
| ['Debug']: [RenderTargetPreviewPlugin], | ['Debug']: [RenderTargetPreviewPlugin], | ||||
| }) | }) | ||||
| } | } | ||||
| init().then(_testFinish) | init().then(_testFinish) | ||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {MaterialExtension} from '../../materials' | |||||
| import {uiDropdown, uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js' | |||||
| import { | |||||
| ACESFilmicToneMapping, | |||||
| CineonToneMapping, | |||||
| CustomToneMapping, | |||||
| LinearToneMapping, | |||||
| Object3D, | |||||
| ReinhardToneMapping, | |||||
| Shader, | |||||
| ShaderChunk, | |||||
| SRGBColorSpace, | |||||
| ToneMapping, | |||||
| Vector4, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| import {glsl, onChange, serialize} from 'ts-browser-helpers' | |||||
| import {IMaterial} from '../../core' | |||||
| import {shaderReplaceString, updateBit} from '../../utils' | |||||
| import {matDefine, uniform} from '../../three' | |||||
| import Uncharted2ToneMapping 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 | |||||
| } | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| export const Uncharted2Tonemapping: ToneMapping = CustomToneMapping | |||||
| @uiFolderContainer('Tonemapping') | |||||
| export class TonemapPlugin extends AViewerPluginSync<''> implements MaterialExtension, GBufferUpdater { | |||||
| static readonly PluginType = 'Tonemap' | |||||
| @serialize() @uiToggle('Enabled') enabled = true | |||||
| @uiDropdown('Mode', ([ | |||||
| ['Linear', LinearToneMapping], | |||||
| ['Reinhard', ReinhardToneMapping], | |||||
| ['Cineon', CineonToneMapping], | |||||
| ['ACESFilmic', ACESFilmicToneMapping], | |||||
| ['Uncharted2', Uncharted2Tonemapping], | |||||
| ] as [string, ToneMapping][]).map(value => ({ | |||||
| label: value[0], | |||||
| value: value[1], | |||||
| }))) | |||||
| @onChange(TonemapPlugin.prototype.setDirty) | |||||
| @serialize() toneMapping: ToneMapping = ACESFilmicToneMapping | |||||
| @uiToggle('Tonemap Background', (t: TonemapPlugin)=>({hidden: ()=>!t._viewer?.renderManager.gbufferTarget})) | |||||
| @matDefine('TONEMAP_BACKGROUND', undefined, true, TonemapPlugin.prototype.setDirty, (v)=>v ? '1' : '0', (v) => v !== '0') | |||||
| @serialize() tonemapBackground = true | |||||
| // todo handle legacy deserialize | |||||
| // @onChange(TonemapPlugin.prototype.setDirty) | |||||
| // @uiToggle('Clip Background') | |||||
| // @serialize() clipBackground = false | |||||
| @onChange(TonemapPlugin.prototype.setDirty) | |||||
| @uiSlider('Exposure', [0, 2 * Math.PI], 0.01) | |||||
| @serialize() exposure = 1 | |||||
| @uiSlider('Saturation', [0, 2], 0.01) | |||||
| @uniform({propKey: 'toneMappingSaturation'}) | |||||
| @serialize() saturation: number | |||||
| @uiSlider('Contrast', [0, 2], 0.01) | |||||
| @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() | |||||
| } | |||||
| parsFragmentSnippet: any = (_: WebGLRenderer, _1: IMaterial) => { | |||||
| if (!this.enabled) return '' | |||||
| return glsl` | |||||
| uniform float toneMappingContrast; | |||||
| uniform float toneMappingSaturation; | |||||
| ${TonemapShader} | |||||
| ` | |||||
| } | |||||
| 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 | |||||
| private _rendererState: any = {} | |||||
| onObjectRender(_: Object3D, material: IMaterial, renderer: WebGLRenderer): void { | |||||
| if (!this.enabled) return | |||||
| const {toneMapping, toneMappingExposure, outputColorSpace} = renderer | |||||
| this._rendererState.toneMapping = toneMapping | |||||
| this._rendererState.toneMappingExposure = toneMappingExposure | |||||
| this._rendererState.outputColorSpace = outputColorSpace | |||||
| renderer.toneMapping = this.toneMapping | |||||
| renderer.toneMappingExposure = this.exposure | |||||
| renderer.outputColorSpace = SRGBColorSpace | |||||
| material.toneMapped = true | |||||
| material.needsUpdate = true | |||||
| } | |||||
| onAfterRender(_: Object3D, _1: IMaterial, renderer: WebGLRenderer): void { | |||||
| renderer.toneMapping = this._rendererState.toneMapping | |||||
| renderer.toneMappingExposure = this._rendererState.toneMappingExposure | |||||
| renderer.outputColorSpace = this._rendererState.outputColorSpace | |||||
| } | |||||
| 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 | |||||
| } | |||||
| if (data.extension) { | |||||
| console.error('TODO: old file') | |||||
| return null | |||||
| } | |||||
| 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 | |||||
| } | |||||
| static { | |||||
| // Add support for Uncharted2 tone mapping | |||||
| ShaderChunk.tonemapping_pars_fragment = ShaderChunk.tonemapping_pars_fragment.replace('vec3 CustomToneMapping( vec3 color ) { return color; }', Uncharted2ToneMapping) | |||||
| } | |||||
| // for typescript | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| __setDirty?: () => void | |||||
| } |
| int getToneMapBit(in int number) { | |||||
| return (number/2) % 2; // 2nd bit | |||||
| } | |||||
| vec3 TonemappingSaturation(vec3 rgb) { | |||||
| const vec3 W = vec3(0.2125, 0.7154, 0.0721); | |||||
| vec3 intensity = vec3(dot(rgb, W)); | |||||
| return mix(intensity, rgb, toneMappingSaturation); | |||||
| } | |||||
| vec3 TonemappingContrast(vec3 color){ | |||||
| return (color - vec3(0.5)) * toneMappingContrast + vec3(0.5); | |||||
| } | |||||
| vec4 ToneMapping(in vec4 color) { | |||||
| vec4 outColor = color; | |||||
| #if defined( TONE_MAPPING ) | |||||
| outColor.rgb = toneMapping(outColor.rgb); | |||||
| outColor.rgb = TonemappingContrast(outColor.rgb); | |||||
| outColor.rgb = TonemappingSaturation(outColor.rgb); | |||||
| #endif | |||||
| return outColor; | |||||
| } |
| bool doTonemap = true; | |||||
| #ifdef HAS_GBUFFER | |||||
| // doTonemap = getToneMapBit(getGBufferFlags(vUv).a) > 0; // todo | |||||
| #if TONEMAP_BACKGROUND < 1 | |||||
| if(isBackground) doTonemap = false; | |||||
| #endif | |||||
| #endif | |||||
| if(doTonemap) diffuseColor = ToneMapping(diffuseColor); |
| // source: http://filmicworlds.com/blog/filmic-tonemapping-operators/ | |||||
| #define Uncharted2Helper( x ) max( ( ( x * ( 0.15 * x + 0.10 * 0.50 ) + 0.20 * 0.02 ) / ( x * ( 0.15 * x + 0.50 ) + 0.20 * 0.30 ) ) - 0.02 / 0.30, vec3( 0.0 ) ) | |||||
| vec3 Uncharted2ToneMapping( vec3 color ) { | |||||
| // John Hable's filmic operator from Uncharted 2 video game | |||||
| color *= toneMappingExposure; | |||||
| return saturate( Uncharted2Helper( color ) / Uncharted2Helper( vec3( 1.0 ) ) ); | |||||
| } | |||||
| vec3 CustomToneMapping( vec3 color ) { return Uncharted2ToneMapping( color ); } |