Pārlūkot izejas kodu

Docs changes, screen pass guide, shadertoy guide, screen shader examples, screen pass extension examples.

master
Palash Bansal pirms 11 mēnešiem
vecāks
revīzija
9097d94d95
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam

+ 3
- 1
examples/blend-load/index.html Parādīt failu

import {_testFinish, _testStart, LoadingScreenPlugin, ThreeViewer} from 'threepipe' import {_testFinish, _testStart, LoadingScreenPlugin, ThreeViewer} from 'threepipe'
import {BlendLoadPlugin} from '@threepipe/plugin-blend-importer' 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]) viewer.addPluginsSync([BlendLoadPlugin, LoadingScreenPlugin])


async function init() { async function init() {

+ 9
- 0
examples/index.html Parādīt failu

<li><a href="./multi-render-uv-clip/">Multi-render UV clipping <br/> (Material Extension) </a></li> <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> <li><a href="./svg-geometry-playground/">SVG Geometry Playground </a></li>
</ul> </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> <h2 class="category">UI Config</h2>
<ul> <ul>
<li><a href="./material-uiconfig/">Material UI </a></li> <li><a href="./material-uiconfig/">Material UI </a></li>

+ 37
- 0
examples/screen-pass-extension-plugin/index.html Parādīt failu

<!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>

+ 91
- 0
examples/screen-pass-extension-plugin/script.ts Parādīt failu

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)

+ 98
- 0
examples/screen-pass-extension/index.html Parādīt failu

<!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>

+ 90
- 0
examples/screen-shader-advanced/index.html Parādīt failu

<!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>

+ 105
- 0
examples/screen-shader-material/index.html Parādīt failu

<!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>

+ 64
- 0
examples/screen-shader/index.html Parādīt failu

<!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>

+ 37
- 0
examples/shadertoy-player/index.html Parādīt failu

<!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>

+ 478
- 0
examples/shadertoy-player/script.ts Parādīt failu

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);
}`)

+ 3
- 2
examples/viewer-render-size/script.ts Parādīt failu

import {_testFinish, _testStart, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe'
import {_testFinish, _testStart, CanvasSnapshotPlugin, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'


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: true, msaa: true,
plugins: [LoadingScreenPlugin],
plugins: [LoadingScreenPlugin, CanvasSnapshotPlugin],
}) })


const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))
stepSize: 0.01, stepSize: 0.01,
}], }],
}) })
ui.setupPluginUi(CanvasSnapshotPlugin, {expanded: true})


} }



+ 6
- 0
src/plugins/postprocessing/FilmicGrainPlugin.ts Parādīt failu



protected _shaderPatch = 'diffuseColor = FilmicGrain(diffuseColor);' protected _shaderPatch = 'diffuseColor = FilmicGrain(diffuseColor);'


/**
* @deprecated
*/
get grainIntensity() { get grainIntensity() {
console.warn('FilmicGrainPlugin.grainIntensity is deprecated, use FilmicGrainPlugin.intensity instead') console.warn('FilmicGrainPlugin.grainIntensity is deprecated, use FilmicGrainPlugin.intensity instead')
return this.intensity return this.intensity
} }
/**
* @deprecated
*/
set grainIntensity(v) { set grainIntensity(v) {
console.warn('FilmicGrainPlugin.grainIntensity is deprecated, use FilmicGrainPlugin.intensity instead') console.warn('FilmicGrainPlugin.grainIntensity is deprecated, use FilmicGrainPlugin.intensity instead')
this.intensity = v this.intensity = v

+ 22
- 4
src/postprocessing/ScreenPass.ts Parādīt failu

export type TViewerScreenShaderFrag = string | [string, string] | {pars?: string, main: string} export type TViewerScreenShaderFrag = string | [string, string] | {pars?: string, main: string}
export type TViewerScreenShader = TViewerScreenShaderFrag | ShaderMaterialParameters | ShaderMaterial2 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') @uiFolderContainer('Screen Pass')
export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'screen'> { export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'screen'> {
declare uiConfig: UiObjectConfig declare uiConfig: UiObjectConfig
} }


function makeScreenShader(shader: string | [string, string] | {pars?: string; main: string} | ShaderMaterialParameters | ShaderMaterial2) { 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 { return {
...CopyShader, ...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: { uniforms: {
tDiffuse: {value: null}, tDiffuse: {value: null},
tTransparent: {value: null}, tTransparent: {value: null},

+ 2
- 0
website/.vitepress/config.ts Parādīt failu

{text: 'UI Configuration', link: 'guide/ui-config'}, {text: 'UI Configuration', link: 'guide/ui-config'},
{text: 'Serialization', link: 'guide/serialization'}, {text: 'Serialization', link: 'guide/serialization'},
{text: 'Plugin System', link: 'guide/plugin-system'}, {text: 'Plugin System', link: 'guide/plugin-system'},
{text: 'Screen Pass Shaders', link: 'guide/screen-pass'},
] ]
}, },
{ {
{text: 'Mesh Lines (Spiral)', link: 'notes/fat-lines'}, {text: 'Mesh Lines (Spiral)', link: 'notes/fat-lines'},
{text: 'glTF Mesh Lines', link: 'notes/gltf-mesh-lines'}, {text: 'glTF Mesh Lines', link: 'notes/gltf-mesh-lines'},
{text: 'Setting Background', link: 'notes/scene-background'}, {text: 'Setting Background', link: 'notes/scene-background'},
{text: 'ShaderToy Shader Tutorial', link: 'notes/shadertoy-player'},
] ]
}, },
{ {

+ 6
- 6
website/guide/plugin-system.md Parādīt failu

text: 'Serialization' text: 'Serialization'
link: './serialization' link: './serialization'


#next:
# text: 'UI Configuration'
# link: './ui-config'
next:
text: 'Screen Pass'
link: './screen-pass'
--- ---


# Plugin System # Plugin System
* 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. * 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 * 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. * `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. Check various plugins in the source code for more examples.

+ 1
- 2
website/guide/render-pipeline.md Parādīt failu

## Passes ## Passes


By default, the render pipeline([`ViewerRenderManager`](https://threepipe.org/docs/classes/ViewerRenderManager.html) includes 2 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 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. using the [registerPass](https://threepipe.org/docs/classes/RenderManager.html#registerPass) and [unregisterPass](https://threepipe.org/docs/classes/RenderManager.html#unregisterPass) methods.

+ 343
- 0
website/guide/screen-pass.md Parādīt failu

---
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
- 1
website/notes/scene-background.md Parādīt failu

text: 'GLTF Fat/Mesh Lines' text: 'GLTF Fat/Mesh Lines'
link: './gltf-mesh-lines' link: './gltf-mesh-lines'


next: false
next:
text: 'ShaderToy Shaders in Three.js'
link: './shadertoy-player'
aside: false aside: false
--- ---



+ 451
- 0
website/notes/shadertoy-player.md Parādīt failu

---
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).


Notiek ielāde…
Atcelt
Saglabāt