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