|
|
@@ -0,0 +1,196 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+ <head>
|
|
|
+ <meta charset="utf-8" />
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
|
|
+ <title>three.js webgpu - HDR Draw</title>
|
|
|
+ <link type="text/css" rel="stylesheet" href="main.css" />
|
|
|
+ <style>
|
|
|
+ #no-hdr {
|
|
|
+ position: absolute;
|
|
|
+ font-family: monospace;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: normal;
|
|
|
+ text-align: center;
|
|
|
+ background: #000;
|
|
|
+ color: #fff;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ padding: 1.5em;
|
|
|
+ max-width: 600px;
|
|
|
+ margin: 5em auto 0;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+
|
|
|
+ <div id="info" style="color: #000">
|
|
|
+ <a href="https://threejs.org" target="_blank" rel="noopener">threejs</a> - HDR Draw
|
|
|
+ </div>
|
|
|
+ <div id="no-hdr" style="display: none">
|
|
|
+ <div>
|
|
|
+ The browser says your device or monitor doesn't support HDR.<br />
|
|
|
+ If you're on a laptop using an external monitor, try the built in
|
|
|
+ monitor<br />
|
|
|
+ or, try this site on your phone. Most phones support HDR.
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script type="importmap">
|
|
|
+ {
|
|
|
+ "imports": {
|
|
|
+ "three": "../build/three.webgpu.js",
|
|
|
+ "three/webgpu": "../build/three.webgpu.js",
|
|
|
+ "three/tsl": "../build/three.tsl.js",
|
|
|
+ "three/addons/": "./jsm/"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+
|
|
|
+ <script type="module">
|
|
|
+
|
|
|
+ import * as THREE from 'three/webgpu';
|
|
|
+ import { pass, uv, uniform } from 'three/tsl';
|
|
|
+ import WebGPU from 'three/addons/capabilities/WebGPU.js';
|
|
|
+ import { afterImage } from 'three/addons/tsl/display/AfterImageNode.js';
|
|
|
+ import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
|
|
+
|
|
|
+ import { ExtendedSRGBColorSpace, ExtendedSRGBColorSpaceImpl } from 'three/addons/math/ColorSpaces.js';
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ intensity: uniform( 4.0, 'float' ).setName( 'intensity' ),
|
|
|
+ hardness: uniform( 0.4, 'float' ).setName( 'hardness' ),
|
|
|
+ radius: uniform( 0.5, 'float' ).setName( 'radius' ),
|
|
|
+ afterImageDecay: uniform( 0.985, 'float' ).setName( 'afterImageDecay' ),
|
|
|
+ };
|
|
|
+
|
|
|
+ const hdrMediaQuery = window.matchMedia( '(dynamic-range: high)' );
|
|
|
+
|
|
|
+ function updateHDRWarning() {
|
|
|
+
|
|
|
+ const displayIsHDR = hdrMediaQuery.matches;
|
|
|
+ document.querySelector( '#no-hdr' ).style.display = displayIsHDR ? 'none' : '';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ hdrMediaQuery.addEventListener( 'change', updateHDRWarning );
|
|
|
+ updateHDRWarning();
|
|
|
+
|
|
|
+ if ( WebGPU.isAvailable() === false ) {
|
|
|
+
|
|
|
+ document.body.appendChild( WebGPU.getErrorMessage() );
|
|
|
+ throw new Error( 'No WebGPU support' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Enable Extended sRGB output color space for HDR presentation
|
|
|
+ THREE.ColorManagement.define( { [ ExtendedSRGBColorSpace ]: ExtendedSRGBColorSpaceImpl } );
|
|
|
+
|
|
|
+ // Renderer (HalfFloat output + Extended sRGB)
|
|
|
+ const renderer = new THREE.WebGPURenderer( {
|
|
|
+ antialias: true,
|
|
|
+ outputType: THREE.HalfFloatType,
|
|
|
+ } );
|
|
|
+
|
|
|
+ renderer.outputColorSpace = ExtendedSRGBColorSpace;
|
|
|
+ renderer.setPixelRatio( window.devicePixelRatio );
|
|
|
+ renderer.setSize( window.innerWidth, window.innerHeight );
|
|
|
+ document.body.appendChild( renderer.domElement );
|
|
|
+
|
|
|
+ const camera = new THREE.OrthographicCamera( 0, window.innerWidth, window.innerHeight, 0, 1, 2 );
|
|
|
+ camera.position.z = 1;
|
|
|
+
|
|
|
+ // Brush scene (rendered into drawTarget)
|
|
|
+ const brushScene = new THREE.Scene();
|
|
|
+
|
|
|
+ brushScene.background = new THREE.Color( 0xffffff );
|
|
|
+ const brushMat = new THREE.MeshBasicNodeMaterial();
|
|
|
+ brushMat.transparent = true;
|
|
|
+ brushMat.depthTest = false;
|
|
|
+ brushMat.depthWrite = false;
|
|
|
+ brushMat.blending = THREE.AdditiveBlending; // additive to build HDR energy
|
|
|
+
|
|
|
+ const postProcessing = new THREE.PostProcessing( renderer );
|
|
|
+ const brushPass = pass( brushScene, camera, { type: THREE.HalfFloatType } );
|
|
|
+ brushPass.renderTarget.texture.colorSpace = ExtendedSRGBColorSpace;
|
|
|
+
|
|
|
+ postProcessing.outputNode = afterImage( brushPass, params.afterImageDecay );
|
|
|
+
|
|
|
+ // HDR brush uniforms
|
|
|
+ const uColor = params.intensity;
|
|
|
+ const uHard = params.hardness;
|
|
|
+ const uRadius = params.radius;
|
|
|
+
|
|
|
+ // Radial falloff in TSL
|
|
|
+ const d = uv().sub( 0.5 ).length();
|
|
|
+ const t = d.div( uRadius );
|
|
|
+ const a = t.clamp().oneMinus().pow( uHard.mul( 8.0 ).add( 1.0 ) );
|
|
|
+
|
|
|
+ brushMat.colorNode = uColor.mul( a );
|
|
|
+ brushMat.opacityNode = a; // premultiplied style with additive blending
|
|
|
+
|
|
|
+ const brushMesh = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1 ), brushMat );
|
|
|
+ brushMesh.scale.set( 300, 300, 1 ); // ~300px default brush size
|
|
|
+ brushScene.add( brushMesh );
|
|
|
+
|
|
|
+ function onPointerMove( e ) {
|
|
|
+
|
|
|
+ const rect = renderer.domElement.getBoundingClientRect();
|
|
|
+ const x = e.clientX - rect.left;
|
|
|
+ const y = e.clientY - rect.top;
|
|
|
+
|
|
|
+ // camera has origin at bottom-left (0,0)
|
|
|
+ brushMesh.position.set( x, window.innerHeight - y, 0 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ window.addEventListener( 'pointermove', onPointerMove, { passive: false } );
|
|
|
+
|
|
|
+ // Prevent mobile scroll on touch
|
|
|
+ renderer.domElement.addEventListener( 'touchstart', ( e ) => e.preventDefault(), { passive: false } );
|
|
|
+ renderer.domElement.addEventListener( 'touchmove', ( e ) => e.preventDefault(), { passive: false } );
|
|
|
+ renderer.domElement.addEventListener( 'touchend', ( e ) => e.preventDefault(), { passive: false } );
|
|
|
+
|
|
|
+ // GUI setup
|
|
|
+ const gui = new GUI();
|
|
|
+
|
|
|
+ const colorFolder = gui.addFolder( 'HDR' );
|
|
|
+ colorFolder.add( params.intensity, 'value', 0, 10, 0.1 ).name( 'Intensity' );
|
|
|
+ colorFolder.open();
|
|
|
+
|
|
|
+ const brushFolder = gui.addFolder( 'Brush Settings' );
|
|
|
+ brushFolder.add( params.hardness, 'value', 0, 0.99, 0.01 ).name( 'Hardness' );
|
|
|
+ brushFolder.add( params.radius, 'value', 0.1, 2.0, 0.01 ).name( 'Radius' );
|
|
|
+ brushFolder.open();
|
|
|
+
|
|
|
+ const effectFolder = gui.addFolder( 'Effects' );
|
|
|
+ effectFolder
|
|
|
+ .add( params.afterImageDecay, 'value', 0.9, 0.999, 0.001 )
|
|
|
+ .name( 'After Image Decay' );
|
|
|
+ effectFolder.open();
|
|
|
+
|
|
|
+ gui.open();
|
|
|
+
|
|
|
+ // Resize handling
|
|
|
+ function onResize() {
|
|
|
+
|
|
|
+ renderer.setSize( window.innerWidth, window.innerHeight );
|
|
|
+ camera.right = window.innerWidth;
|
|
|
+ camera.top = window.innerHeight;
|
|
|
+ camera.updateProjectionMatrix();
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ window.addEventListener( 'resize', onResize );
|
|
|
+
|
|
|
+ // Main loop
|
|
|
+ renderer.setAnimationLoop( async () => {
|
|
|
+
|
|
|
+ postProcessing.render();
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ </script>
|
|
|
+ </body>
|
|
|
+</html>
|