| @@ -31,7 +31,9 @@ | |||
| import {_testFinish, _testStart, LoadingScreenPlugin, ThreeViewer} from 'threepipe' | |||
| import {BlendLoadPlugin} from '@threepipe/plugin-blend-importer' | |||
| const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas')}) | |||
| const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas'), dropzone: { | |||
| addOptions: {clearSceneObjects: true} | |||
| }}) | |||
| viewer.addPluginsSync([BlendLoadPlugin, LoadingScreenPlugin]) | |||
| async function init() { | |||
| @@ -521,6 +521,15 @@ | |||
| <li><a href="./multi-render-uv-clip/">Multi-render UV clipping <br/> (Material Extension) </a></li> | |||
| <li><a href="./svg-geometry-playground/">SVG Geometry Playground </a></li> | |||
| </ul> | |||
| <h2 class="category">Shaders</h2> | |||
| <ul> | |||
| <li><a href="./screen-shader/">Basic Screen Shader </a></li> | |||
| <li><a href="./screen-shader-advanced/">Advanced Screen Shader </a></li> | |||
| <li><a href="./screen-shader-material/">Screen Shader Material </a></li> | |||
| <li><a href="./screen-pass-extension/">Screen Pass Extension </a></li> | |||
| <li><a href="./screen-pass-extension-plugin/">Screen Pass Extension Plugin </a></li> | |||
| <li><a href="./shadertoy-player/">ShaderToy Player </a></li> | |||
| </ul> | |||
| <h2 class="category">UI Config</h2> | |||
| <ul> | |||
| <li><a href="./material-uiconfig/">Material UI </a></li> | |||
| @@ -0,0 +1,37 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Screen Pass Extension 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/global-loading.mjs"></script> | |||
| <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,91 @@ | |||
| import { | |||
| _testFinish, | |||
| _testStart, | |||
| AScreenPassExtensionPlugin, | |||
| Color, | |||
| glsl, | |||
| onChange, | |||
| serialize, | |||
| ThreeViewer, | |||
| uiColor, | |||
| uiFolderContainer, | |||
| uiSlider, | |||
| uiToggle, | |||
| uniform, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| // Add a material extension to the screen shader material to modify the final rendered image. | |||
| // Here, AScreenPassExtensionPlugin is used to create a custom screen pass extension plugin with auto generated UI and serialization. | |||
| // Checkout the ScreenPass guide for more details: https://threepipe.org/docs/guides/screen-pass | |||
| @uiFolderContainer('Custom Tint Extension') | |||
| export class CustomScreenPassExtensionPlugin extends AScreenPassExtensionPlugin { | |||
| static readonly PluginType = 'Vignette' | |||
| readonly extraUniforms = { | |||
| intensity: {value: 1}, | |||
| tintColor: {value: new Color(0xff0000)}, | |||
| } as const | |||
| @onChange(CustomScreenPassExtensionPlugin.prototype.setDirty) | |||
| @uiToggle('Enable') | |||
| @serialize() enabled: boolean | |||
| @uiSlider('Intensity', [0.1, 4], 0.01) | |||
| @uniform({propKey: 'tintIntensity'}) | |||
| @serialize() intensity = 1 | |||
| @uiColor<CustomScreenPassExtensionPlugin>('Color', t=>({onChange:()=>t?.setDirty()})) | |||
| @uniform({propKey: 'tintColor'}) | |||
| @serialize('tintColor') color = new Color(0xff0000) | |||
| /** | |||
| * 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.isDisabled()) return '' | |||
| return glsl` | |||
| uniform float tintIntensity; | |||
| uniform vec3 tintColor; | |||
| vec4 ApplyTint(vec4 color) { | |||
| return vec4(color.rgb * tintColor * tintIntensity, color.a); | |||
| } | |||
| ` | |||
| } | |||
| protected _shaderPatch = 'diffuseColor = ApplyTint(diffuseColor);' | |||
| constructor(enabled = true) { | |||
| super() | |||
| this.enabled = enabled | |||
| } | |||
| } | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| tonemap: true, // also tonemap (this is also added as an extension) | |||
| plugins: [CustomScreenPassExtensionPlugin], | |||
| }) | |||
| async function init() { | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| // Add the color to the UI | |||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||
| ui.setupPluginUi(CustomScreenPassExtensionPlugin) | |||
| } | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| @@ -0,0 +1,98 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Screen Pass Extension</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/global-loading.mjs"></script> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module"> | |||
| import {_testFinish, _testStart, ThreeViewer, Color, shaderReplaceString} from 'threepipe' | |||
| import {TweakpaneUiPlugin} from "@threepipe/plugin-tweakpane" | |||
| // Add a material extension to the screen shader material to modify the final rendered image. | |||
| // Here, `#glMarker` patched in the screen shader material will be replaced with a custom function that applies a tint color to the final rendered image. | |||
| // Checkout the ScreenPass guide for more details: https://threepipe.org/docs/guides/screen-pass | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas'), | |||
| tonemap: true, // also tonemap (this is also added as an extension) | |||
| }) | |||
| const extension = { | |||
| extraUniforms: { | |||
| tintColor: {value: new Color(0, 1, 1)} // cyan tint | |||
| }, | |||
| parsFragmentSnippet: ` // this is added before the main function | |||
| uniform vec3 tintColor; | |||
| vec4 applyTint(vec4 color) { | |||
| return vec4(color.rgb * tintColor, color.a); | |||
| } | |||
| `, | |||
| shaderExtender: (shader, material, renderer)=>{ | |||
| console.log('Patching shader') | |||
| shader.fragmentShader = shaderReplaceString(shader.fragmentShader, | |||
| '#glMarker', ` | |||
| diffuseColor = applyTint(diffuseColor); | |||
| `, | |||
| {prepend: true} // (prepend, not replace) | |||
| ) | |||
| }, | |||
| // add other properties like computeCacheKey etc | |||
| } /*as MaterialExtension */ | |||
| viewer.renderManager.screenPass.material.registerMaterialExtensions([extension]) | |||
| async function init() { | |||
| viewer.scene.backgroundColor.set(0) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| // Add the color to the UI | |||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||
| ui.appendChild({ | |||
| type: 'color', | |||
| property: [extension.extraUniforms.tintColor, 'value'], | |||
| label: 'Tint Color', | |||
| description: 'Change the tint color applied to the final rendered image.', | |||
| onChange: ()=>{ | |||
| viewer.setDirty() | |||
| } | |||
| }) | |||
| } | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| </script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,90 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Screen Shader Advanced</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/global-loading.mjs"></script> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module"> | |||
| import {_testFinish, _testStart, ThreeViewer, Color} from 'threepipe' | |||
| import {TweakpaneUiPlugin} from "@threepipe/plugin-tweakpane" | |||
| // Set a custom screen shader snippet to modify the final rendered image. | |||
| // Here, `diffuseColor` is the final color of the pixel which can be modified. | |||
| // This happens before the final post-processing effects(like tonemap, vignette, film-grain etc.) are applied. | |||
| // Checkout the ScreenPass guide for more details: https://threepipe.org/docs/guides/screen-pass | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas'), | |||
| screenShader: { | |||
| pars: ` // this is added before the main function | |||
| uniform vec3 tintColor; | |||
| vec4 applyTint(vec4 color) { | |||
| return vec4(color.rgb * tintColor, color.a); | |||
| } | |||
| `, | |||
| main: ` // this is added inside the main function | |||
| diffuseColor = applyTint(diffuseColor); | |||
| ` | |||
| } | |||
| }) | |||
| // add the uniform js reference to the screen pass material | |||
| viewer.renderManager.screenPass.material.uniforms.tintColor = { | |||
| value: new Color(0, 0, 1) // blue tint | |||
| } | |||
| async function init() { | |||
| viewer.scene.backgroundColor.set(0) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| // Add the color to the UI | |||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||
| ui.appendChild({ | |||
| type: 'color', | |||
| property: [viewer.renderManager.screenPass.material.uniforms.tintColor, 'value'], | |||
| label: 'Tint Color', | |||
| description: 'Change the tint color applied to the final rendered image.', | |||
| onChange: ()=>{ | |||
| viewer.setDirty() | |||
| } | |||
| }) | |||
| } | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| </script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,105 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Screen Shader Material</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/global-loading.mjs"></script> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module"> | |||
| import {_testFinish, _testStart, ThreeViewer, Color, ExtendedShaderMaterial, CopyShader, FrontSide, NoBlending} from 'threepipe' | |||
| import {TweakpaneUiPlugin} from "@threepipe/plugin-tweakpane" | |||
| // Set a custom screen shader to render final image. | |||
| // Here, `#glMarker` is defined for external plugins(like tonemap, vignette) to be able to extend the screen shader. They expect diffuseColor to be present in the context, and modify it accordingly. | |||
| // Checkout the ScreenPass guide for more details: https://threepipe.org/docs/guides/screen-pass | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas'), | |||
| tonemap: true, | |||
| screenShader: new ExtendedShaderMaterial({ | |||
| ...CopyShader, | |||
| // Custom fragment shader. Note that this is not the default full shader, it needs to handle gbuffer, transparent target etc as well, checkout the full default shader - https://github.com/repalash/threepipe/blob/master/src/postprocessing/ScreenPass.glsl | |||
| fragmentShader: ` | |||
| #include <packing> | |||
| varying vec2 vUv; | |||
| uniform vec3 tintColor; | |||
| void main() { | |||
| vec4 diffuseColor = tDiffuseTexelToLinear (texture2D(tDiffuse, vUv)); | |||
| #glMarker | |||
| diffuseColor.rgb *= tintColor; | |||
| gl_FragColor = diffuseColor; | |||
| #include <colorspace_fragment> | |||
| } | |||
| `, | |||
| uniforms: { | |||
| tDiffuse: {value: null}, | |||
| tTransparent: {value: null}, | |||
| tintColor: {value: new Color(0, 1, 0)}, | |||
| }, | |||
| transparent: true, | |||
| blending: NoBlending, | |||
| side: FrontSide, | |||
| } /*as ShaderMaterialParameters*/, ['tDiffuse', 'tTransparent']) | |||
| }) | |||
| async function init() { | |||
| viewer.scene.backgroundColor.set(0) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| // Add the color to the UI | |||
| const ui = viewer.addPluginSync(TweakpaneUiPlugin, true) | |||
| ui.appendChild({ | |||
| type: 'color', | |||
| property: [viewer.renderManager.screenPass.material.uniforms.tintColor, 'value'], | |||
| label: 'Tint Color', | |||
| description: 'Change the tint color applied to the final rendered image.', | |||
| onChange: ()=>{ | |||
| viewer.setDirty() | |||
| } | |||
| }) | |||
| } | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| </script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,64 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Screen Shader</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" | |||
| } | |||
| } | |||
| </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/global-loading.mjs"></script> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module"> | |||
| import {_testFinish, _testStart, ThreeViewer} from 'threepipe' | |||
| // Set a custom screen shader snippet to modify the final rendered image. | |||
| // Here, `diffuseColor` is the final color of the pixel which can be modified. | |||
| // This happens before the final post-processing effects(like tonemap, vignette, film-grain etc.) are applied. | |||
| // Checkout the ScreenPass guide for more details: https://threepipe.org/docs/guides/screen-pass | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas'), | |||
| screenShader: ` | |||
| // add a basic red tint | |||
| diffuseColor *= vec4(1.0, 0.0, 0.0, 1.0); | |||
| ` | |||
| }) | |||
| async function init() { | |||
| viewer.scene.backgroundColor.set(0) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| } | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| </script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,37 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>ShaderToy Player</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/global-loading.mjs"></script> | |||
| <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,478 @@ | |||
| import { | |||
| _testFinish, | |||
| _testStart, | |||
| CanvasSnapshotPlugin, createStyles, css, | |||
| ExtendedShaderMaterial, | |||
| glsl, | |||
| GLSL3, | |||
| LoadingScreenPlugin, MaterialExtension, | |||
| ThreeViewer, | |||
| UiObjectConfig, | |||
| Vector2, | |||
| Vector3, | |||
| Vector4, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| // import {BlueprintJsUiPlugin} from '@threepipe/plugin-blueprintjs' | |||
| async function init() { | |||
| const material = new ExtendedShaderMaterial({ | |||
| uniforms: uniforms, | |||
| defines: { | |||
| ['IS_SCREEN']: isScreen ? '1' : '0', | |||
| ['IS_LINEAR_OUTPUT']: isScreen ? '1' : '0', | |||
| }, | |||
| glslVersion: GLSL3, | |||
| vertexShader: toyVert, | |||
| fragmentShader: toyFrag, | |||
| transparent: true, | |||
| depthTest: false, | |||
| depthWrite: false, | |||
| premultipliedAlpha: false, | |||
| }, channels, false) | |||
| material.registerMaterialExtensions([toyExtension]) | |||
| material.needsUpdate = true | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: false, | |||
| rgbm: false, | |||
| tonemap: false, | |||
| plugins: [LoadingScreenPlugin, CanvasSnapshotPlugin], | |||
| screenShader: material, | |||
| renderScale: 2, | |||
| }) | |||
| // setup css alignment of canvas inside container (for proper viewer size) | |||
| viewer.container.style.position = 'relative' | |||
| viewer.canvas.style.position = 'absolute' | |||
| viewer.canvas.style.top = '50%' | |||
| viewer.canvas.style.left = '50%' | |||
| viewer.canvas.style.transform = 'translate(-50%, -50%)' | |||
| addMouseListeners(viewer.canvas) | |||
| viewer.addEventListener('preFrame', (ev)=>{ | |||
| if (!params.running && !params.stepFrame) return | |||
| // uniforms.iTimeDelta.value = viewer.renderManager.clock.getDelta() | |||
| uniforms.iTimeDelta.value = (ev.deltaTime || 0) / 1000.0 | |||
| const date = new Date() | |||
| uniforms.iDate.value.set(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours() * 60 * 60 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds() / 1000) | |||
| uniforms.iFrameRate.value = 30 // todo: get from clock | |||
| const bufferSize = [viewer.renderManager.renderSize.width * viewer.renderManager.renderScale, viewer.renderManager.renderSize.height * viewer.renderManager.renderScale] | |||
| uniforms.iResolution.value.set(bufferSize[0], bufferSize[1], 1) | |||
| uniforms.iMouse.value.set( // acc to shadertoy | |||
| mouse.position.x * bufferSize[0], | |||
| mouse.position.y * bufferSize[1], | |||
| mouse.clickPosition.x * (mouse.isDown ? 1 : -1) * bufferSize[0], | |||
| mouse.clickPosition.y * (mouse.isClick ? 1 : -1) * bufferSize[1], | |||
| ) | |||
| params.time += uniforms.iTimeDelta.value | |||
| uniforms.iTime.value = params.time | |||
| uniforms.iFrame.value = params.frame++ | |||
| // uniforms.iChannelTime.value = [0, 0, 0, 0] | |||
| // uniforms.iChannelResolution.value = [ | |||
| // new Vector3(uniforms.iChannel0Size.value.x, uniforms.iChannel0Size.value.y, 1), | |||
| // new Vector3(uniforms.iChannel1Size.value.x, uniforms.iChannel1Size.value.y, 1), | |||
| // new Vector3(uniforms.iChannel2Size.value.x, uniforms.iChannel2Size.value.y, 1), | |||
| // new Vector3(uniforms.iChannel3Size.value.x, uniforms.iChannel3Size.value.y, 1), | |||
| // ] | |||
| // for (let i = 0; i < channels.length; i++) { | |||
| // const channel = uniforms[channels[i]] | |||
| // if (channel.value) { | |||
| // channel.value.needsUpdate = true | |||
| // uniforms[channels[i] + 'Size'].value.set(channel.value.image.width, channel.value.image.height) | |||
| // } else { | |||
| // uniforms[channels[i] + 'Size'].value.set(0, 0) | |||
| // } | |||
| // } | |||
| material.uniformsNeedUpdate = true | |||
| viewer.setDirty() | |||
| ui.uiRefresh?.(true) | |||
| params.stepFrame = false | |||
| }) | |||
| viewer.setRenderSize(params.resolution) | |||
| const setShader = (v: string)=>{ | |||
| toyExtension.parsFragmentSnippet = v | |||
| toyExtension.computeCacheKey = Math.random().toString() | |||
| material.setDirty() | |||
| viewer.setDirty() | |||
| ui.uiRefresh?.(true) | |||
| } | |||
| const ui: UiObjectConfig = { | |||
| label: 'Edit Properties', | |||
| type: 'folder', | |||
| expanded: true, | |||
| value: params, | |||
| children: [{ | |||
| type: 'vec', | |||
| path: 'resolution', | |||
| label: 'Resolution', | |||
| bounds: [10, 4096], | |||
| stepSize: 1, | |||
| onChange: ()=>{ | |||
| viewer.setRenderSize(params.resolution, 'contain', 1) | |||
| }, | |||
| }, { | |||
| type: 'number', | |||
| path: 'time', | |||
| label: 'Time', | |||
| readOnly: true, | |||
| }, { | |||
| type: 'number', | |||
| path: 'frame', | |||
| label: 'Frame', | |||
| readOnly: true, | |||
| }, { | |||
| type: 'button', | |||
| baseWidth: '100%', | |||
| label: ()=> 'Step', | |||
| disabled: ()=> params.running, | |||
| onClick: ()=>{ | |||
| params.stepFrame = true | |||
| ui.uiRefresh?.(true) | |||
| }, | |||
| }, { | |||
| type: 'button', | |||
| baseWidth: '100%', | |||
| label: ()=> params.running ? 'Pause' : 'Play', | |||
| onClick: ()=>{ | |||
| params.running = !params.running | |||
| ui.uiRefresh?.(true) | |||
| }, | |||
| }, { | |||
| type: 'button', | |||
| baseWidth: '100%', | |||
| label: ()=> 'Reset', | |||
| onClick: ()=>{ | |||
| params.frame = 0 | |||
| params.time = 0 | |||
| params.stepFrame = true | |||
| ui.uiRefresh?.(true) | |||
| }, | |||
| }, { | |||
| type: 'button', | |||
| baseWidth: '100%', | |||
| label: ()=> 'Edit Shader', | |||
| onClick: ()=>setupShaderEditor(toyExtension.parsFragmentSnippet as string, setShader), | |||
| }, { | |||
| type: 'button', | |||
| label: 'Download png', | |||
| baseWidth: '100%', | |||
| onClick: async()=>{ | |||
| const running = params.running | |||
| params.running = false | |||
| await viewer.getPlugin(CanvasSnapshotPlugin)?.downloadSnapshot('snapshot.png', { | |||
| waitForProgressive: false, | |||
| displayPixelRatio: undefined, | |||
| }) | |||
| params.running = running | |||
| ui.uiRefresh?.(true) | |||
| }, | |||
| }], | |||
| } | |||
| const shaderFile = 'https://asset-samples.threepipe.org/shaders/tunnel-cylinders.glsl' | |||
| const response = await fetch(shaderFile) | |||
| const shaderText = await response.text() | |||
| setShader(shaderText) | |||
| const uiPlugin = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| // const uiPlugin = viewer.addPluginSync(new BlueprintJsUiPlugin()) | |||
| uiPlugin.appendChild(ui) | |||
| // uiPlugin.setupPluginUi(CanvasSnapshotPlugin, {expanded: true}) | |||
| } | |||
| // region variables | |||
| const params = { | |||
| resolution: new Vector2(1280, 720), | |||
| time: 0, | |||
| frame: 0, | |||
| stepFrame: false, | |||
| running: true, | |||
| } | |||
| const mouse = { | |||
| position: new Vector2(), | |||
| clickPosition: new Vector2(), | |||
| isDown: false, | |||
| isClick: false, | |||
| clientX: 0, | |||
| clientY: 0, | |||
| } | |||
| const isScreen = true | |||
| const channels = ['iChannel0', 'iChannel1', 'iChannel2', 'iChannel3'] | |||
| const uniforms = { | |||
| iResolution: {value: new Vector3()}, | |||
| iTime: {value: 0}, | |||
| iFrame: {value: 0}, | |||
| iMouse: {value: new Vector4()}, | |||
| iTimeDelta: {value: 0}, | |||
| iDate: {value: new Vector4()}, | |||
| iFrameRate: {value: 0}, | |||
| iChannel0: {value: null}, | |||
| iChannel1: {value: null}, | |||
| iChannel2: {value: null}, | |||
| iChannel3: {value: null}, | |||
| iChannel0Size: {value: new Vector2()}, | |||
| iChannel1Size: {value: new Vector2()}, | |||
| iChannel2Size: {value: new Vector2()}, | |||
| iChannel3Size: {value: new Vector2()}, | |||
| iChannelTime: {value: [0, 0, 0, 0]}, | |||
| iChannelResolution: {value: [new Vector3(), new Vector3(), new Vector3(), new Vector3()]}, | |||
| } | |||
| // endregion variables | |||
| // region shaders | |||
| const toyDefault = glsl` | |||
| void mainImage( out vec4 fragColor, in vec2 fragCoord ) | |||
| { | |||
| // Normalized pixel coordinates (from 0 to 1) | |||
| vec2 uv = fragCoord/iResolution.xy; | |||
| fragColor = vec4(uv, 0, 1); | |||
| } | |||
| ` | |||
| const toyFrag = glsl` | |||
| precision highp int; | |||
| precision highp sampler2D; | |||
| #define HW_PERFORMANCE 0 | |||
| uniform vec3 iResolution; // viewport resolution (in pixels) | |||
| uniform float iTime; // shader playback time (in seconds) | |||
| //uniform float iGlobalTime; // shader playback time (in seconds) | |||
| uniform vec4 iMouse; // mouse pixel coords | |||
| uniform vec4 iDate; // (year, month, day, time in seconds) | |||
| uniform float iSampleRate; // sound sample rate (i.e., 44100) | |||
| vec3 iChannelResolution[4]; // channel resolution (in pixels) | |||
| //uniform float iChannelTime[4]; // channel playback time (in sec) | |||
| //uniform vec2 ifFragCoordOffsetUniform; // used for tiled based hq rendering | |||
| uniform float iTimeDelta; // render time (in seconds) | |||
| uniform int iFrame; // shader playback frame | |||
| uniform float iFrameRate; | |||
| uniform vec2 iChannel0Size; | |||
| uniform vec2 iChannel1Size; | |||
| uniform vec2 iChannel2Size; | |||
| uniform vec2 iChannel3Size; | |||
| in vec2 vUv; | |||
| #define gl_FragColor glFragColor | |||
| layout(location = 0) out vec4 glFragColor; | |||
| void main() { | |||
| iChannelResolution[0] = vec3(iChannel0Size,1.0); | |||
| iChannelResolution[1] = vec3(iChannel1Size,1.0); | |||
| iChannelResolution[2] = vec3(iChannel2Size,1.0); | |||
| iChannelResolution[3] = vec3(iChannel3Size,1.0); | |||
| // mainImage(glFragColor,iResolution.xy*vUv); // this has issues in windows? | |||
| mainImage(glFragColor,gl_FragCoord.xy); | |||
| vec4 diffuseColor = glFragColor; | |||
| #glMarker | |||
| glFragColor = diffuseColor; | |||
| #if IS_SCREEN == 1 | |||
| glFragColor.a = 1.0; | |||
| #ifdef IS_LINEAR_OUTPUT | |||
| //glFragColor = sRGBToLinear(glFragColor); | |||
| #else | |||
| #include <colorspace_fragment> | |||
| #endif | |||
| #endif | |||
| } | |||
| ` | |||
| const toyVert = glsl` | |||
| out vec2 vUv; | |||
| void main() { | |||
| vUv = uv; | |||
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |||
| } | |||
| ` | |||
| // endregion shaders | |||
| // region mouse | |||
| export function getMouseFromEvent(canvas: HTMLElement, e: PointerEvent|WheelEvent): Vector2 | null { | |||
| const rect = canvas.getBoundingClientRect() | |||
| const x = e.clientX - rect.left | |||
| const y = e.clientY - rect.top | |||
| if (x < 0 || y < 0 || x > rect.width || y > rect.height) return null | |||
| return mouse.position.set(x / rect.width, 1.0 - y / rect.height) | |||
| } | |||
| export function onPointerDown(e: PointerEvent, canvas: HTMLElement) { | |||
| if (e.button !== 0 || !mouse) return | |||
| mouse.isDown = false | |||
| mouse.isClick = false | |||
| const m = getMouseFromEvent(canvas, e) | |||
| if (!m) return | |||
| mouse.isDown = true | |||
| mouse.isClick = true | |||
| mouse.clickPosition.copy(m) | |||
| e.preventDefault() | |||
| e.stopPropagation() | |||
| } | |||
| export function onPointerUp(e: PointerEvent, canvas: HTMLElement) { | |||
| if (e.button !== 0 || !mouse) return | |||
| mouse.isDown = false | |||
| mouse.isClick = false | |||
| getMouseFromEvent(canvas, e) | |||
| } | |||
| export function onPointerMove(e: PointerEvent, canvas: HTMLElement) { | |||
| if (!mouse) return | |||
| mouse.clientX = e.clientX | |||
| mouse.clientY = e.clientY | |||
| if (!mouse.isDown) return | |||
| getMouseFromEvent(canvas, e) | |||
| } | |||
| export function onPointerWheel(e: WheelEvent, canvas: HTMLElement) { | |||
| if (!mouse) return | |||
| mouse.clientX = e.clientX | |||
| mouse.clientY = e.clientY | |||
| const m = getMouseFromEvent(canvas, e) | |||
| if (!m) return | |||
| mouse.position.set(0, 0) | |||
| mouse.clickPosition.set(0, 0) | |||
| } | |||
| export function addMouseListeners(canvas: HTMLElement) { | |||
| canvas.addEventListener('pointerdown', (e) => onPointerDown(e as PointerEvent, canvas), {passive: false}) | |||
| canvas.addEventListener('pointerup', (e) => onPointerUp(e as PointerEvent, canvas), {passive: false}) | |||
| canvas.addEventListener('pointermove', (e) => onPointerMove(e as PointerEvent, canvas), {passive: false}) | |||
| canvas.addEventListener('wheel', (e) => onPointerWheel(e as WheelEvent, canvas), {passive: false}) | |||
| } | |||
| // endregion mouse | |||
| // region shader editor | |||
| const toyExtension: MaterialExtension = { | |||
| parsFragmentSnippet: toyDefault, | |||
| isCompatible: () => true, | |||
| computeCacheKey: Math.random().toString(), | |||
| } | |||
| let editor: HTMLElement | undefined = undefined | |||
| window.addEventListener('keydown', (e: KeyboardEvent) => { | |||
| if (e.key === 'Escape' && editor) { | |||
| editor.remove() | |||
| editor = undefined | |||
| } | |||
| }) | |||
| export function setupShaderEditor(value: string, onChange: (v: string)=>void) { | |||
| if (editor) return | |||
| editor = document.createElement('div') | |||
| editor.classList.add('editor-container') | |||
| document.body.appendChild(editor) | |||
| const textarea = document.createElement('textarea') | |||
| textarea.value = value | |||
| textarea.addEventListener('input', ()=>{ | |||
| onChange(textarea.value) | |||
| }) | |||
| editor.appendChild(textarea) | |||
| const closeButton = document.createElement('div') | |||
| closeButton.classList.add('close-button') | |||
| closeButton.textContent = '×' | |||
| closeButton.addEventListener('click', ()=>{ | |||
| if (!editor) return | |||
| editor.remove() | |||
| editor = undefined | |||
| }) | |||
| editor.appendChild(closeButton) | |||
| } | |||
| // endregion shader editor | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| createStyles(css` | |||
| *:focus { | |||
| outline: none; | |||
| } | |||
| .editor-container { | |||
| width: min(800px, 80%); | |||
| height: min(600px, 80%); | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-50%, -50%); | |||
| z-index: 1000; | |||
| background-color: rgba(0, 0, 0, 0.6); | |||
| backdrop-filter: blur(10px); | |||
| border-radius: 10px; | |||
| overflow: hidden; | |||
| } | |||
| .editor-container textarea { | |||
| height: 100%; | |||
| width: 100%; | |||
| color: rgba(240, 240, 240, 0.9); | |||
| font-family: monospace; | |||
| font-size: 14px; | |||
| white-space: pre; | |||
| overflow: auto; | |||
| background: rgba(255, 255, 255, 0.10); | |||
| border-radius: 10px; | |||
| border: none; | |||
| outline: none; | |||
| padding: 10px; | |||
| backdrop-filter: blur(6px); | |||
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); | |||
| transition: box-shadow 0.2s ease-in-out; | |||
| } | |||
| .editor-container textarea:hover { | |||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), 0 2px 0 rgba(255, 255, 255, 0.15) inset; | |||
| } | |||
| .editor-container .close-button { | |||
| position: absolute; | |||
| top: 10px; | |||
| right: 10px; | |||
| z-index: 1; | |||
| width: 36px; | |||
| height: 36px; | |||
| border-radius: 50%; | |||
| background: rgba(255, 255, 255, 0.15); | |||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), 0 1.5px 0 rgba(255, 255, 255, 0.15) inset; | |||
| border: none; | |||
| color: #fff; | |||
| font-size: 20px; | |||
| font-weight: bold; | |||
| cursor: pointer; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| backdrop-filter: blur(4px); | |||
| transition: background 0.4s, box-shadow 0.4s; | |||
| } | |||
| .editor-container .close-button:hover { | |||
| background: rgba(255, 255, 255, 0.3); | |||
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); | |||
| }`) | |||
| @@ -1,4 +1,4 @@ | |||
| import {_testFinish, _testStart, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe' | |||
| import {_testFinish, _testStart, CanvasSnapshotPlugin, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| async function init() { | |||
| @@ -6,7 +6,7 @@ async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| plugins: [LoadingScreenPlugin], | |||
| plugins: [LoadingScreenPlugin, CanvasSnapshotPlugin], | |||
| }) | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| @@ -83,6 +83,7 @@ async function init() { | |||
| stepSize: 0.01, | |||
| }], | |||
| }) | |||
| ui.setupPluginUi(CanvasSnapshotPlugin, {expanded: true}) | |||
| } | |||
| @@ -52,10 +52,16 @@ export class FilmicGrainPlugin extends AScreenPassExtensionPlugin { | |||
| protected _shaderPatch = 'diffuseColor = FilmicGrain(diffuseColor);' | |||
| /** | |||
| * @deprecated | |||
| */ | |||
| get grainIntensity() { | |||
| console.warn('FilmicGrainPlugin.grainIntensity is deprecated, use FilmicGrainPlugin.intensity instead') | |||
| return this.intensity | |||
| } | |||
| /** | |||
| * @deprecated | |||
| */ | |||
| set grainIntensity(v) { | |||
| console.warn('FilmicGrainPlugin.grainIntensity is deprecated, use FilmicGrainPlugin.intensity instead') | |||
| this.intensity = v | |||
| @@ -20,6 +20,15 @@ import {shaderReplaceString} from '../utils' | |||
| export type TViewerScreenShaderFrag = string | [string, string] | {pars?: string, main: string} | |||
| export type TViewerScreenShader = TViewerScreenShaderFrag | ShaderMaterialParameters | ShaderMaterial2 | |||
| /** | |||
| * Screen Pass | |||
| * | |||
| * This pass renders the final scene to the screen. | |||
| * It can be extended by Screen Pass Extensions to apply post-processing effects, such as tonemapping, color grading, etc. | |||
| * | |||
| * It is used by default in {@link ViewerRenderManager} to render the final scene. | |||
| * A custom material/shader can be passed to the constructor to use a custom base fragment shader. | |||
| */ | |||
| @uiFolderContainer('Screen Pass') | |||
| export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'screen'> { | |||
| declare uiConfig: UiObjectConfig | |||
| @@ -100,12 +109,21 @@ export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'scr | |||
| } | |||
| function makeScreenShader(shader: string | [string, string] | {pars?: string; main: string} | ShaderMaterialParameters | ShaderMaterial2) { | |||
| const baseShader = shaderReplaceString( | |||
| ScreenPassShader, | |||
| 'void main()', | |||
| (Array.isArray(shader) ? shader[0] : (<any>shader)?.pars || '') + '\n', | |||
| {prepend: true} | |||
| ) | |||
| const finalShader = baseShader.includes('#glMarker') ? shaderReplaceString( | |||
| baseShader, | |||
| '#glMarker', | |||
| (Array.isArray(shader) ? shader[1] : typeof shader === 'string' ? shader : (shader as any)?.main || '') + '\n', | |||
| {prepend: true} | |||
| ) : baseShader | |||
| return { | |||
| ...CopyShader, | |||
| fragmentShader: | |||
| shaderReplaceString(shaderReplaceString(ScreenPassShader, | |||
| 'void main()', (Array.isArray(shader) ? shader[0] : (<any>shader)?.pars || '') + '\n', {prepend: true}), | |||
| '#glMarker', (Array.isArray(shader) ? shader[1] : typeof shader === 'string' ? shader : (shader as any)?.main || '') + '\n', {prepend: true}), | |||
| fragmentShader: finalShader, | |||
| uniforms: { | |||
| tDiffuse: {value: null}, | |||
| tTransparent: {value: null}, | |||
| @@ -39,6 +39,7 @@ export default defineConfig({ | |||
| {text: 'UI Configuration', link: 'guide/ui-config'}, | |||
| {text: 'Serialization', link: 'guide/serialization'}, | |||
| {text: 'Plugin System', link: 'guide/plugin-system'}, | |||
| {text: 'Screen Pass Shaders', link: 'guide/screen-pass'}, | |||
| ] | |||
| }, | |||
| { | |||
| @@ -47,6 +48,7 @@ export default defineConfig({ | |||
| {text: 'Mesh Lines (Spiral)', link: 'notes/fat-lines'}, | |||
| {text: 'glTF Mesh Lines', link: 'notes/gltf-mesh-lines'}, | |||
| {text: 'Setting Background', link: 'notes/scene-background'}, | |||
| {text: 'ShaderToy Shader Tutorial', link: 'notes/shadertoy-player'}, | |||
| ] | |||
| }, | |||
| { | |||
| @@ -3,9 +3,9 @@ prev: | |||
| text: 'Serialization' | |||
| link: './serialization' | |||
| #next: | |||
| # text: 'UI Configuration' | |||
| # link: './ui-config' | |||
| next: | |||
| text: 'Screen Pass' | |||
| link: './screen-pass' | |||
| --- | |||
| # Plugin System | |||
| @@ -123,8 +123,8 @@ Notes: | |||
| * To the viewer render the next frame, `viewer.setDirty()` can be called, or set `this.dirty = true` in preFrame and reset in postFrame to stop the rendering. (Note that rendering may continue if some other plugin sets the viewer dirty like `ProgressivePlugin` or any of the animation plugins). Check `isConverged` in `ProgressivePlugin` to check if it's the final frame. | |||
| * All Plugins which inherit from AViewerPlugin support serialisation. Create property `serializeWithViewer = false` to disable serialisation with the viewer in config and glb or `toJSON: any = undefined` to disable serialisation entirely | |||
| * `plugin.toJSON()` and `plugin.fromJSON()` or `ThreeSerialization` can be used to serialize and deserialize plugins. `viewer.exportPluginConfig` and `viewer.importPluginConfig` also exist for this. | |||
| * @serialize('label') decorator can be used to mark any public/private variable as serializable. label (optional) corresponds to the key in JSON. | |||
| * @serialize supports instances of ITexture, IMaterial, all primitive types, simple JS objects, three.js math classes(Vector2, Vector3, Matrix3...), and some more. | |||
| * uiDecorators can be used to mark properties and functions that will be shown in the Ui. The Ui shows up automatically when TweakpaneUiPlugin/BlueprintJsUiPlugin is added to the viewer. Plugins have special features in the UI for download preset and saving state. | |||
| * `@serialize('label')` decorator can be used to mark any public/private variable as serializable. label (optional) corresponds to the key in JSON. | |||
| * `@serialize` supports instances of ITexture, IMaterial, all primitive types, simple JS objects, three.js math classes(`Vector2`, `Vector3`, `Matrix3`...), and some more. | |||
| * `@ui...` decorators can be used to mark properties and functions that will be shown in the Ui. The Ui shows up automatically when `TweakpaneUiPlugin`/`BlueprintJsUiPlugin` is added to the viewer. Plugins have special features in the UI for download preset and saving state. | |||
| Check various plugins in the source code for more examples. | |||
| @@ -70,8 +70,7 @@ Render targets created with a `sizeMultiplier` are automatically resized when th | |||
| ## Passes | |||
| By default, the render pipeline([`ViewerRenderManager`](https://threepipe.org/docs/classes/ViewerRenderManager.html) includes 2 passes - | |||
| [RenderPass](https://threepipe.org/docs/classes/ExtendedRenderPass.html) for rendering the scene hierarchy and [ScreenPass](https://threepipe.org/docs/classes/ShaderPass) | |||
| for rendering the final output on the canvas. | |||
| [RenderPass](https://threepipe.org/docs/classes/ExtendedRenderPass.html) for rendering the scene hierarchy and [ScreenPass](https://threepipe.org/docs/classes/ScreenPass.html) for rendering the final output on the canvas. | |||
| More passes can be added and removed from the pipeline | |||
| using the [registerPass](https://threepipe.org/docs/classes/RenderManager.html#registerPass) and [unregisterPass](https://threepipe.org/docs/classes/RenderManager.html#unregisterPass) methods. | |||
| @@ -0,0 +1,343 @@ | |||
| --- | |||
| prev: | |||
| text: 'Plugin System' | |||
| link: './plugin-system' | |||
| next: | |||
| text: 'Screen Pass' | |||
| link: './screen-pass' | |||
| --- | |||
| # Screen Pass - Extensions and Shaders | |||
| The Screen Pass is the final rendering stage in Threepipe that outputs the rendered scene to the screen or a render target. It provides multiple ways to customize the final image through custom shaders, material extensions, and shader snippets. | |||
| ## Overview | |||
| The Screen Pass renders the final scene by processing the diffuse and transparent render targets. It supports: | |||
| - Custom fragment shaders | |||
| - Shader snippets for simple modifications | |||
| - Material extensions for complex modifications | |||
| - Built-in features like tonemapping, background clipping, and transparency handling | |||
| Check out the [ScreenPass.glsl](https://github.com/repalash/threepipe/blob/master/src/postprocessing/ScreenPass.glsl) for the default fragment shader code used in the screen pass. | |||
| ## Basic Screen Shader | |||
| The simplest way to customize the screen pass is by providing a shader snippet as a string: | |||
| ```typescript | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('canvas'), | |||
| screenShader: ` | |||
| // add a basic red tint | |||
| diffuseColor *= vec4(1.0, 0.0, 0.0, 1.0); | |||
| ` | |||
| }) | |||
| ``` | |||
| This snippet is inserted at the `#glMarker` position in the default screen shader and can modify the `diffuseColor` variable which contains the final pixel color. | |||
| **Live Example:** [Basic Screen Shader](https://threepipe.org/examples/#screen-shader/) | |||
| ## Advanced Screen Shader with Parameters | |||
| For more complex modifications, you can provide shader parameters and functions: | |||
| ```typescript | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('canvas'), | |||
| screenShader: { | |||
| pars: ` // this is added before the main function | |||
| uniform vec3 tintColor; | |||
| vec4 applyTint(vec4 color) { | |||
| return vec4(color.rgb * tintColor, color.a); | |||
| } | |||
| `, | |||
| main: ` // this is added inside the main function | |||
| diffuseColor = applyTint(diffuseColor); | |||
| ` | |||
| } | |||
| }) | |||
| // Add the uniform to the screen pass material | |||
| viewer.renderManager.screenPass.material.uniforms.tintColor = { | |||
| value: new Color(0, 0, 1) // blue tint | |||
| } | |||
| ``` | |||
| **Live Example:** [Advanced Screen Shader](https://threepipe.org/examples/#screen-shader-advanced/) | |||
| ## Custom Screen Shader Material | |||
| For complete control, you can provide a full shader material configuration: | |||
| ```typescript | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('canvas'), | |||
| tonemap: true, | |||
| screenShader: new ExtendedShaderMaterial({ | |||
| ...CopyShader, | |||
| // Custom fragment shader | |||
| fragmentShader: ` | |||
| #include <packing> | |||
| varying vec2 vUv; | |||
| uniform vec3 tintColor; | |||
| void main() { | |||
| vec4 diffuseColor = tDiffuseTexelToLinear (texture2D(tDiffuse, vUv)); | |||
| #glMarker | |||
| diffuseColor.rgb *= tintColor; | |||
| gl_FragColor = diffuseColor; | |||
| #include <colorspace_fragment> | |||
| } | |||
| `, | |||
| uniforms: { | |||
| tDiffuse: {value: null}, | |||
| tTransparent: {value: null}, | |||
| tintColor: {value: new Color(0, 1, 0)}, | |||
| }, | |||
| transparent: true, | |||
| blending: NoBlending, | |||
| side: FrontSide, | |||
| }, ['tDiffuse', 'tTransparent']) | |||
| }) | |||
| ``` | |||
| **Live Example:** [Custom Screen Shader Material](https://threepipe.org/examples/#screen-shader-material/) | |||
| ## The #glMarker System | |||
| The `#glMarker` is a special placeholder in the screen shader that allows plugins and extensions to inject their own code. This enables: | |||
| 1. **Plugin Integration**: Plugins like tonemap, vignette, and film grain can modify the final image | |||
| 2. **Extension Points**: Multiple extensions can modify the same shader without conflicts | |||
| 3. **Shader Composition**: Complex effects can be built by combining multiple extensions | |||
| When using custom screen shaders, include `#glMarker` to ensure compatibility with plugins: | |||
| ```glsl | |||
| void main() { | |||
| vec4 diffuseColor = tDiffuseTexelToLinear (texture2D(tDiffuse, vUv)); | |||
| #glMarker // Plugin injection point | |||
| // Your custom modifications | |||
| diffuseColor.rgb *= tintColor; | |||
| gl_FragColor = diffuseColor; | |||
| } | |||
| ``` | |||
| ## Screen Pass Material Extensions | |||
| Material extensions provide the most flexible way to modify the screen pass. They allow you to: | |||
| - Add custom uniforms | |||
| - Inject shader code | |||
| - Add defines | |||
| - Hook into render events | |||
| ```typescript | |||
| const extension = { | |||
| extraUniforms: { | |||
| tintColor: {value: new Color(0, 1, 1)} // cyan tint | |||
| }, | |||
| parsFragmentSnippet: ` // added before main function | |||
| uniform vec3 tintColor; | |||
| vec4 applyTint(vec4 color) { | |||
| return vec4(color.rgb * tintColor, color.a); | |||
| } | |||
| `, | |||
| shaderExtender: (shader, material, renderer) => { | |||
| console.log('Patching shader') | |||
| shader.fragmentShader = shaderReplaceString( | |||
| shader.fragmentShader, | |||
| '#glMarker', | |||
| `diffuseColor = applyTint(diffuseColor);`, | |||
| {prepend: true} // prepend to existing #glMarker content | |||
| ) | |||
| }, | |||
| priority: 100, // execution order | |||
| isCompatible: (material) => material.isShaderMaterial, | |||
| computeCacheKey: (material) => 'tint-extension' | |||
| } | |||
| // Register the extension | |||
| viewer.renderManager.screenPass.material.registerMaterialExtensions([extension]) | |||
| ``` | |||
| **Live Example:** [Screen Pass Extension](https://threepipe.org/examples/#screen-pass-extension/) | |||
| ## Screen Pass Extension Plugins | |||
| For more complex effects that need UI configuration and serialization, you can create a custom screen pass extension plugin using `AScreenPassExtensionPlugin`. This base class provides automatic UI generation, serialization, and integration with the plugin system. | |||
| ```typescript | |||
| import { | |||
| AScreenPassExtensionPlugin, | |||
| Color, | |||
| glsl, | |||
| onChange, | |||
| serialize, | |||
| uiColor, | |||
| uiFolderContainer, | |||
| uiSlider, | |||
| uiToggle, | |||
| uniform, | |||
| } from 'threepipe' | |||
| @uiFolderContainer('Custom Tint Extension') | |||
| export class CustomScreenPassExtensionPlugin extends AScreenPassExtensionPlugin { | |||
| static readonly PluginType = 'CustomTint' | |||
| // Define uniforms that will be available in the shader | |||
| readonly extraUniforms = { | |||
| tintIntensity: {value: 1}, | |||
| tintColor: {value: new Color(0xff0000)}, | |||
| } as const | |||
| // Plugin properties with UI decorators | |||
| @onChange(CustomScreenPassExtensionPlugin.prototype.setDirty) | |||
| @uiToggle('Enable') | |||
| @serialize() enabled: boolean = true | |||
| @uiSlider('Intensity', [0.1, 4], 0.01) | |||
| @uniform({propKey: 'tintIntensity'}) // Links to extraUniforms | |||
| @serialize() intensity = 1 | |||
| @uiColor('Color') | |||
| @uniform({propKey: 'tintColor'}) | |||
| @serialize('tintColor') color = new Color(0xff0000) | |||
| /** | |||
| * Priority determines the order of extension application | |||
| * Lower values = applied later (after other extensions) | |||
| */ | |||
| priority = -50 | |||
| /** | |||
| * Add shader code before the main function | |||
| * Use glsl`` template literal for syntax highlighting | |||
| */ | |||
| parsFragmentSnippet = () => { | |||
| if (this.isDisabled()) return '' | |||
| return glsl` | |||
| uniform float tintIntensity; | |||
| uniform vec3 tintColor; | |||
| vec4 ApplyTint(vec4 color) { | |||
| return vec4(color.rgb * tintColor * tintIntensity, color.a); | |||
| } | |||
| ` | |||
| } | |||
| /** | |||
| * Shader code to inject at the #glMarker position | |||
| */ | |||
| protected _shaderPatch = 'diffuseColor = ApplyTint(diffuseColor);' | |||
| constructor(enabled = true) { | |||
| super() | |||
| this.enabled = enabled | |||
| } | |||
| } | |||
| // Register the plugin | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('canvas'), | |||
| plugins: [CustomScreenPassExtensionPlugin], | |||
| }) | |||
| ``` | |||
| ### Key Features of Extension Plugins: | |||
| 1. **Automatic UI Generation**: UI decorators create controls automatically | |||
| 2. **Serialization**: Properties are saved/loaded with `@serialize()` | |||
| 3. **Uniform Binding**: `@uniform()` decorator links properties to shader uniforms | |||
| 4. **Change Detection**: `@onChange()` triggers updates when properties change | |||
| 5. **Priority System**: Control the order of extension application | |||
| 6. **Conditional Logic**: Use `isDisabled()` to conditionally apply effects | |||
| ### Extension Plugin Methods: | |||
| - `parsFragmentSnippet()`: Add code before the main function | |||
| - `_shaderPatch`: Code to inject at #glMarker (can also be a function) | |||
| - `isDisabled()`: Check if the extension should be applied | |||
| - `setDirty()`: Mark the material for recompilation | |||
| **Live Example:** [Screen Pass Extension Plugin](https://threepipe.org/examples/#screen-pass-extension-plugin/) | |||
| ## Built-in Features | |||
| ### Background Clipping | |||
| Control background rendering with the `clipBackground` option: | |||
| ```typescript | |||
| // Enable background clipping | |||
| viewer.renderManager.screenPass.clipBackground = true | |||
| // Force background clipping (overrides the above which is also in the UI) | |||
| viewer.renderManager.screenPass.clipBackgroundForce = true | |||
| ``` | |||
| ### Output Color Space | |||
| Configure the output color space for the final render: | |||
| ```typescript | |||
| import { SRGBColorSpace, LinearSRGBColorSpace } from 'threepipe' | |||
| viewer.renderManager.screenPass.outputColorSpace = SRGBColorSpace | |||
| ``` | |||
| ## Available Variables | |||
| When writing custom screen shaders, these variables are available: | |||
| - `diffuseColor`: The final pixel color (vec4) | |||
| - `tDiffuse`: Main render target texture (sampler2D) | |||
| - `vUv`: UV coordinates (vec2) | |||
| - `transparentColor`: Transparent objects color (vec4) | |||
| - `tTransparent`: Transparent render target texture (sampler2D) | |||
| ### Working with G-Buffer | |||
| When using the GBufferPlugin, additional variables become available: | |||
| ::: details GBuffer Snippet | |||
| ```glsl | |||
| #ifdef HAS_GBUFFER | |||
| float depth = getDepth(vUv); | |||
| bool isBackground = depth > 0.99 && transparentColor.a < 0.001; | |||
| #endif | |||
| ``` | |||
| ::: | |||
| ```glsl | |||
| // Use depth information for effects | |||
| diffuseColor.rgb = mix(diffuseColor.rgb, fogColor.rgb, depth); | |||
| ``` | |||
| ## Best Practices | |||
| 1. **Always include #glMarker** in custom shaders to maintain plugin compatibility | |||
| 2. **Use material extensions** for complex modifications that need to interact with other plugins | |||
| 3. **Test with different plugins** to ensure compatibility | |||
| 4. **Consider performance** when adding complex shader operations | |||
| 5. **Use appropriate uniforms** instead of hardcoded values for dynamic effects | |||
| ## Integration with Plugins | |||
| Many built-in plugins extend the screen pass: | |||
| - **TonemapPlugin**: Adds tone mapping to the final image | |||
| - **VignettePlugin**: Adds vignette effect | |||
| - **FilmGrainPlugin**: Adds film grain texture | |||
| - **ChromaticAberrationPlugin**: Adds chromatic aberration | |||
| These plugins use the material extension system to inject their effects at the `#glMarker` position, allowing them to work together seamlessly. | |||
| @@ -3,7 +3,9 @@ prev: | |||
| text: 'GLTF Fat/Mesh Lines' | |||
| link: './gltf-mesh-lines' | |||
| next: false | |||
| next: | |||
| text: 'ShaderToy Shaders in Three.js' | |||
| link: './shadertoy-player' | |||
| aside: false | |||
| --- | |||
| @@ -0,0 +1,451 @@ | |||
| --- | |||
| prev: | |||
| text: 'Setting Background Color and Images' | |||
| link: './scene-background' | |||
| next: false | |||
| aside: false | |||
| --- | |||
| # ShaderToy Shaders in Three.js | |||
| <iframe src="https://threepipe.org/examples/shadertoy-player/" style="width:100%;height:600px;border:none;"></iframe> | |||
| This tutorial shows how to use shaders from ShaderToy in a Three.js scene by using them as custom screen shaders. You'll learn how to run ShaderToy shaders in a Three.js context, pass uniforms, and create interactive controls. | |||
| ## Overview | |||
| ShaderToy is a popular online shader editor that uses a specific format for fragment shaders. To use these shaders in Three.js, we need to: | |||
| 1. Set up the proper uniforms that ShaderToy expects | |||
| 2. Create a custom material that wraps the ShaderToy shader | |||
| 3. Use it as a screen shader in threepipe | |||
| 4. Handle mouse input and time updates | |||
| ## Step 1: Setting Up the Basic Structure | |||
| First, import the necessary modules from threepipe: | |||
| ```typescript | |||
| import { | |||
| ExtendedShaderMaterial, | |||
| glsl, | |||
| GLSL3, | |||
| LoadingScreenPlugin, | |||
| MaterialExtension, | |||
| ThreeViewer, | |||
| Vector2, | |||
| Vector3, | |||
| Vector4, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| ``` | |||
| ## Step 2: Define ShaderToy Uniforms | |||
| ShaderToy shaders expect specific uniforms. Create these to match the ShaderToy specification: | |||
| ```typescript | |||
| const uniforms = { | |||
| iResolution: {value: new Vector3()}, // viewport resolution | |||
| iTime: {value: 0}, // shader playback time | |||
| iFrame: {value: 0}, // current frame number | |||
| iMouse: {value: new Vector4()}, // mouse pixel coords | |||
| iTimeDelta: {value: 0}, // render time delta | |||
| iDate: {value: new Vector4()}, // current date | |||
| iFrameRate: {value: 0}, // frame rate | |||
| iChannel0: {value: null}, // texture channels | |||
| iChannel1: {value: null}, | |||
| iChannel2: {value: null}, | |||
| iChannel3: {value: null}, | |||
| // Additional uniforms for channel sizes | |||
| iChannel0Size: {value: new Vector2()}, | |||
| iChannel1Size: {value: new Vector2()}, | |||
| iChannel2Size: {value: new Vector2()}, | |||
| iChannel3Size: {value: new Vector2()}, | |||
| // Custom uniforms for shader parameters | |||
| customFloat: {value: 0.5}, // Sample float parameter | |||
| customColor: {value: new Vector3(1.0, 0.5, 0.2)}, // Sample color parameter | |||
| customIntensity: {value: 1.0}, // Sample intensity parameter | |||
| } | |||
| ``` | |||
| ### Adding Custom Parameters | |||
| You can extend the uniforms object with any custom parameters your shader needs. These will be available in your fragment shader and can be controlled through the UI. Common types include: | |||
| - **Float values**: For controlling intensity, speed, scale, etc. | |||
| - **Vector3 colors**: For color parameters | |||
| - **Vector2/Vector3/Vector4**: For vectors | |||
| - **Boolean flags**: For enabling/disabling effects (passed as floats: 0.0 or 1.0) | |||
| ## Step 3: Create the Shader Material | |||
| The fragment shader needs to be adapted to work with Three.js. Here's the wrapper that makes ShaderToy shaders compatible: | |||
| ```glsl | |||
| precision highp int; | |||
| precision highp sampler2D; | |||
| uniform vec3 iResolution; | |||
| uniform float iTime; | |||
| uniform vec4 iMouse; | |||
| uniform vec4 iDate; | |||
| uniform float iTimeDelta; | |||
| uniform int iFrame; | |||
| uniform float iFrameRate; | |||
| // Channel uniforms | |||
| uniform vec2 iChannel0Size; | |||
| uniform vec2 iChannel1Size; | |||
| uniform vec2 iChannel2Size; | |||
| uniform vec2 iChannel3Size; | |||
| in vec2 vUv; | |||
| layout(location = 0) out vec4 glFragColor; | |||
| void main() { | |||
| // Set up channel resolutions | |||
| vec3 iChannelResolution[4]; | |||
| iChannelResolution[0] = vec3(iChannel0Size, 1.0); | |||
| iChannelResolution[1] = vec3(iChannel1Size, 1.0); | |||
| iChannelResolution[2] = vec3(iChannel2Size, 1.0); | |||
| iChannelResolution[3] = vec3(iChannel3Size, 1.0); | |||
| // Call the ShaderToy main function | |||
| mainImage(glFragColor, gl_FragCoord.xy); | |||
| // Apply screen shader processing | |||
| vec4 diffuseColor = glFragColor; | |||
| #glMarker | |||
| glFragColor = diffuseColor; | |||
| // Ensure alpha is 1.0 for screen shaders | |||
| glFragColor.a = 1.0; | |||
| } | |||
| ``` | |||
| The shader calls the `mainImage` function, which is where your ShaderToy code will go. This function should be defined in your ShaderToy code and will receive the `fragColor` and `fragCoord` parameters. | |||
| Since this is not defined in the shader itself, it will not compile without a material extension that injects the `mainImage` function. | |||
| ::: note `glMarker` | |||
| The `#glMarker` directive is a placeholder for the `ScreenPass` in threepipe that indicates where the screen shader extensions should be added. These include extensions by plugins like `TonemapPlugin`, `VignettePlugin`, etc. | |||
| You can remove it if you don't need these extensions. | |||
| `diffuseColor` is the final color output of the shader, which will be modified by the screen shader extensions. | |||
| Checkout the [Screen Pass guide](./../guide/screen-pass) for more information on how screen shaders work in threepipe. | |||
| ::: | |||
| ## Step 4: Create the Material Extension | |||
| Use a `MaterialExtension` to inject your ShaderToy code: | |||
| ```typescript | |||
| const toyExtension: MaterialExtension = { | |||
| parsFragmentSnippet: ` | |||
| unfiform float customFloat; | |||
| uniform vec3 customColor; | |||
| uniform float customIntensity; | |||
| void mainImage(out vec4 fragColor, in vec2 fragCoord) { | |||
| vec2 uv = fragCoord / iResolution.xy; | |||
| fragColor = vec4(uv * customIntensity, 0, 1); | |||
| } | |||
| `, | |||
| isCompatible: () => true, | |||
| computeCacheKey: Math.random().toString(), | |||
| } | |||
| ``` | |||
| Here, `parsFragmentSnippet` is added to the material's fragment shader just before the main function. | |||
| You can replace it with your ShaderToy code with any custom uniforms you need. | |||
| Checkout the [Material Extension guide](./../guide/material-extension) for more information. | |||
| This has a default shader, but you can dynamically change the `parsFragmentSnippet` to load different ShaderToy shaders at runtime. | |||
| ```typescript | |||
| const response = await fetch('https://asset-samples.threepipe.org/shaders/tunnel-cylinders.glsl') | |||
| const shaderText = await response.text() | |||
| toyExtension.parsFragmentSnippet = v | |||
| toyExtension.computeCacheKey = Math.random().toString() | |||
| material.setDirty() | |||
| ``` | |||
| ## Step 5: Set Up the Material and Viewer | |||
| Create the [`ExtendedShaderMaterial`](https://threepipe.org/docs/classes/ExtendedShaderMaterial.html) and configure the viewer: | |||
| ```typescript | |||
| const material = new ExtendedShaderMaterial({ | |||
| uniforms: uniforms, | |||
| defines: { | |||
| IS_SCREEN: '1', | |||
| IS_LINEAR_OUTPUT: '1', | |||
| }, | |||
| glslVersion: GLSL3, | |||
| vertexShader: toyVert, | |||
| fragmentShader: toyFrag, | |||
| transparent: true, | |||
| depthTest: false, | |||
| depthWrite: false, | |||
| premultipliedAlpha: false, | |||
| }) | |||
| material.registerMaterialExtensions([toyExtension]) | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas'), | |||
| msaa: false, | |||
| rgbm: false, | |||
| tonemap: false, | |||
| screenShader: material, // Use as screen shader/material | |||
| renderScale: 2, | |||
| }) | |||
| ``` | |||
| The material is set as the `screenShader` in the viewer configuration, which sets it as material in the `ScreenPass`. Check out the [Screen Pass guide](./../guide/screen-pass) for more details on how custom screen shaders/materials work in threepipe. | |||
| ::: note `ExtendedShaderMaterial` | |||
| [`ExtendedShaderMaterial`](https://threepipe.org/docs/classes/ExtendedShaderMaterial.html) is a custom material that allows dynamic shader code injection and supports the `MaterialExtension` system. | |||
| It extends the standard `ShaderMaterial` to provide additional features like automatic uniform management, shader code injection, compatibility with the threepipe material extension system, and automatic texture encoding and size support. | |||
| It is used here to apply the ShaderToy shader as a screen shader in the viewer. | |||
| ::: | |||
| ## Step 6: Handle Time and Frame Updates | |||
| Update the uniforms each frame to animate the shader: | |||
| ```typescript | |||
| viewer.addEventListener('preFrame', (ev) => { | |||
| if (!params.running && !params.stepFrame) return | |||
| // Update time uniforms | |||
| uniforms.iTimeDelta.value = (ev.deltaTime || 0) / 1000.0 | |||
| params.time += uniforms.iTimeDelta.value | |||
| uniforms.iTime.value = params.time | |||
| uniforms.iFrame.value = params.frame++ | |||
| // Update date | |||
| const date = new Date() | |||
| uniforms.iDate.value.set( | |||
| date.getFullYear(), | |||
| date.getMonth(), | |||
| date.getDate(), | |||
| date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() | |||
| ) | |||
| // Update resolution | |||
| const bufferSize = [ | |||
| viewer.renderManager.renderSize.width * viewer.renderManager.renderScale, | |||
| viewer.renderManager.renderSize.height * viewer.renderManager.renderScale | |||
| ] | |||
| uniforms.iResolution.value.set(bufferSize[0], bufferSize[1], 1) | |||
| material.uniformsNeedUpdate = true | |||
| viewer.setDirty() | |||
| }) | |||
| ``` | |||
| ## Step 7: Handle Mouse Input | |||
| Implement mouse tracking to match ShaderToy's mouse behavior: | |||
| ```typescript | |||
| const mouse = { | |||
| position: new Vector2(), | |||
| clickPosition: new Vector2(), | |||
| isDown: false, | |||
| isClick: false, | |||
| } | |||
| function getMouseFromEvent(canvas, e) { | |||
| const rect = canvas.getBoundingClientRect() | |||
| const x = e.clientX - rect.left | |||
| const y = e.clientY - rect.top | |||
| if (x < 0 || y < 0 || x > rect.width || y > rect.height) return null | |||
| return mouse.position.set(x / rect.width, 1.0 - y / rect.height) | |||
| } | |||
| // Update mouse uniform in preFrame event | |||
| uniforms.iMouse.value.set( | |||
| mouse.position.x * bufferSize[0], | |||
| mouse.position.y * bufferSize[1], | |||
| mouse.clickPosition.x * (mouse.isDown ? 1 : -1) * bufferSize[0], | |||
| mouse.clickPosition.y * (mouse.isClick ? 1 : -1) * bufferSize[1] | |||
| ) | |||
| ``` | |||
| Checkout the example code for the full boilderplate for mouse handling, including adding event listeners for `mousedown`, `mouseup`, and `mousemove` to update the `mouse` object. | |||
| ## Step 8: Add Interactive Controls | |||
| Create a UI to control the shader, including custom uniform parameters: | |||
| ```typescript | |||
| // Define parameters object to store UI-controlled values | |||
| const params = { | |||
| resolution: new Vector2(1280, 720), | |||
| time: 0, | |||
| frame: 0, | |||
| running: true, | |||
| stepFrame: false, | |||
| // Custom parameters | |||
| customFloat: 0.5, | |||
| customColor: new Color(), | |||
| customIntensity: 1.0, | |||
| } | |||
| const ui = { | |||
| label: 'Shader Controls', | |||
| type: 'folder', | |||
| expanded: true, | |||
| value: params, | |||
| children: [{ | |||
| type: 'button', | |||
| label: () => params.running ? 'Pause' : 'Play', | |||
| onClick: () => { | |||
| params.running = !params.running | |||
| }, | |||
| }, { | |||
| type: 'button', | |||
| label: 'Reset', | |||
| onClick: () => { | |||
| params.frame = 0 | |||
| params.time = 0 | |||
| }, | |||
| }, { | |||
| type: 'folder', | |||
| label: 'Custom Parameters', | |||
| expanded: true, | |||
| children: [{ | |||
| type: 'slider', | |||
| path: 'customFloat', | |||
| label: 'Sample Float', | |||
| bounds: [0, 1], | |||
| stepSize: 0.01, | |||
| onChange: () => { | |||
| uniforms.customFloat.value = params.customFloat | |||
| material.uniformsNeedUpdate = true | |||
| viewer.setDirty() | |||
| }, | |||
| }, { | |||
| type: 'color', | |||
| path: 'customColor', | |||
| label: 'Sample Color', | |||
| onChange: () => { | |||
| uniforms.customColor.value.set( | |||
| params.customColor.r, | |||
| params.customColor.g, | |||
| params.customColor.b | |||
| ) | |||
| material.uniformsNeedUpdate = true | |||
| viewer.setDirty() | |||
| }, | |||
| }, { | |||
| type: 'slider', | |||
| path: 'customIntensity', | |||
| label: 'Intensity', | |||
| bounds: [0, 3], | |||
| stepSize: 0.1, | |||
| onChange: () => { | |||
| uniforms.customIntensity.value = params.customIntensity | |||
| material.uniformsNeedUpdate = true | |||
| viewer.setDirty() | |||
| }, | |||
| }], | |||
| }, { | |||
| type: 'button', | |||
| label: 'Edit Shader', | |||
| onClick: () => setupShaderEditor(toyExtension.parsFragmentSnippet, setShader), | |||
| }], | |||
| } | |||
| const uiPlugin = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| uiPlugin.appendChild(ui) | |||
| ``` | |||
| ### UI Control Types | |||
| The TweakpaneUiPlugin supports various control types for different uniform parameters: | |||
| - **slider**: For numeric values with min/max bounds | |||
| - **color**: For RGB color values (automatically converts to Vector3) | |||
| - **button**: For triggering actions | |||
| - **checkbox**: For boolean values | |||
| - **folder**: For grouping related controls | |||
| - **vec**: For Vector2/Vector3/Vector4 values (like resolution) | |||
| ### Connecting UI to Uniforms | |||
| Each UI control should have an `onChange` callback that: | |||
| 1. Updates the corresponding uniform value | |||
| 2. Sets `material.uniformsNeedUpdate = true` | |||
| 3. Calls `viewer.setDirty()` to trigger a re-render | |||
| ## Step 9: Dynamic Shader Loading | |||
| Implement a function to update the shader dynamically: | |||
| ```typescript | |||
| const setShader = (shaderCode) => { | |||
| toyExtension.parsFragmentSnippet = shaderCode | |||
| toyExtension.computeCacheKey = Math.random().toString() | |||
| material.setDirty() | |||
| viewer.setDirty() | |||
| } | |||
| // Load a shader from URL | |||
| const response = await fetch('path/to/shader.glsl') | |||
| const shaderText = await response.text() | |||
| setShader(shaderText) | |||
| ``` | |||
| ## Texture Channels | |||
| Textures can be set in the uniforms for the shader channels, or can be added to the UI config to configure dynamically: | |||
| ```typescript | |||
| uniforms.iChannel0.value = await viewer.load('path/to/texture0.png') | |||
| // ... | |||
| // sample to add to the UI | |||
| const uiConfig = { | |||
| type: 'image', | |||
| property: [uniforms.iChannel0, 'value'], | |||
| label: 'iChannel0', | |||
| onChange: ()=>{ | |||
| material.uniformsNeedUpdate = true | |||
| material.setDirty() | |||
| } | |||
| } | |||
| ui.appendChild(uiConfig) | |||
| ``` | |||
| ## ShaderToy post-processing | |||
| Since the material is added as the `screenShader`, it is rendered in `ScreenPass` after other passes like `RenderPass`, etc. | |||
| Output of these can be used to in the shader toy shader to blend the 3d scene with a custom shadertoy effect. | |||
| This can be done by defining and accessing the `tDiffuse` and `tTransparent` uniforms in the material and shader code. | |||
| Check out the [ScreenPass.glsl](https://github.com/repalash/threepipe/blob/master/src/postprocessing/ScreenPass.glsl) for a sample of how to access these textures in the shader code, as well as interfacing with the gbuffer. | |||
| Check the [Screen Pass guide](./../guide/screen-pass) for more details and an example. | |||
| ## Key Points | |||
| 1. **Screen Shader Usage**: The material is used as a `screenShader` in the viewer configuration, which applies it as a post-processing effect. | |||
| 2. **Uniform Management**: All ShaderToy uniforms must be properly updated each frame for the shader to work correctly. | |||
| 3. **Coordinate Systems**: Pay attention to coordinate system differences between ShaderToy and Three.js, especially for mouse coordinates. | |||
| 4. **Performance**: Screen shaders run on every pixel, so complex shaders can impact performance significantly. | |||
| 5. **Material Extensions**: Using MaterialExtension allows dynamic shader code injection without recreating the entire material. | |||
| This setup provides a complete ShaderToy player that can run most ShaderToy shaders with proper time, mouse, and resolution handling, plus interactive controls for experimentation. | |||
| Check out the [live example](https://threepipe.org/examples/shadertoy-player/) to see it in action along with the source code on [GitHub](https://github.com/repalash/threepipe/tree/master/examples/shadertoy-player/script.ts). | |||