| @@ -213,7 +213,7 @@ | |||
| <div class="sidebar" data-selected-example="GLTF Load"> | |||
| <button class="hamburger"> ☰</button> | |||
| <h1><a href="https://github.com/repalash/threepipe">ThreePipe</a> Examples</h1> | |||
| <h2 class="category">Import/Export</h2> | |||
| <h2 class="category">Import</h2> | |||
| <ul> | |||
| <li><a href="./fbx-load/">FBX Load </a></li> | |||
| <li><a href="./obj-mtl-load/">OBJ MTL Load </a></li> | |||
| @@ -222,11 +222,18 @@ | |||
| <li><a href="./drc-load/">DRACO(DRC) Load </a></li> | |||
| <li><a href="./hdr-load/">HDR 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="./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="./pmat-material-export/">PMAT Material export </a></li> | |||
| </ul> | |||
| <h2 class="category">Post-Processing</h2> | |||
| <ul> | |||
| <li><a href="./tonemap-plugin/">Tonemap Plugin </a></li> | |||
| </ul> | |||
| <h2 class="category">Rendering</h2> | |||
| <ul> | |||
| <li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li> | |||
| @@ -0,0 +1,35 @@ | |||
| <!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> | |||
| @@ -0,0 +1,22 @@ | |||
| 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) | |||
| @@ -1,6 +1,5 @@ | |||
| import { | |||
| _testFinish, | |||
| AViewerPluginSync, | |||
| DepthBufferPlugin, | |||
| DropzonePlugin, | |||
| FullScreenPlugin, | |||
| @@ -8,27 +7,20 @@ import { | |||
| IObject3D, | |||
| NormalBufferPlugin, | |||
| RenderTargetPreviewPlugin, | |||
| SceneUiConfigPlugin, | |||
| ThreeViewer, | |||
| UnsignedByteType, | |||
| TonemapPlugin, | |||
| ViewerUiConfigPlugin, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| 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() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: false, | |||
| msaa: true, | |||
| rgbm: true, | |||
| dropzone: { | |||
| addOptions: { | |||
| clearSceneObjects: false, | |||
| @@ -40,10 +32,12 @@ async function init() { | |||
| const editor = viewer.addPluginSync(new TweakpaneEditorPlugin()) | |||
| 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 RenderTargetPreviewPlugin(false), | |||
| new TonemapPlugin(), | |||
| ]) | |||
| const rt = viewer.getOrAddPluginSync(RenderTargetPreviewPlugin) | |||
| @@ -51,8 +45,9 @@ async function init() { | |||
| rt.addTarget(viewer.getPlugin(NormalBufferPlugin)?.target, 'normal', false, true, false) | |||
| editor.loadPlugins({ | |||
| ['Viewer']: [ViewerUiConfig, DropzonePlugin, FullScreenPlugin], | |||
| ['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin], | |||
| ['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin], | |||
| ['Post-processing']: [TonemapPlugin], | |||
| ['Debug']: [RenderTargetPreviewPlugin], | |||
| }) | |||
| @@ -71,3 +66,4 @@ async function init() { | |||
| } | |||
| init().then(_testFinish) | |||
| @@ -0,0 +1,197 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| 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; | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| 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); | |||
| @@ -0,0 +1,8 @@ | |||
| // 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 ); } | |||