Skip to content

Instantly share code, notes, and snippets.

@sketchpunk
Created November 14, 2024 23:08
Show Gist options
  • Save sketchpunk/a282edc9a11fd16233fabb3662e66dd1 to your computer and use it in GitHub Desktop.
Save sketchpunk/a282edc9a11fd16233fabb3662e66dd1 to your computer and use it in GitHub Desktop.
Basic Template for using ThreeJS's TSL Compute with WebGL / GSLS Backend
<!DOCTYPE html><html lang="en"><head><title></title></head>
<style>canvas{ display:block; } body, html { padding:0px; margin:0px; width:100%; height:100%; }</style>
<body><script type="module">
// #region IMPORTS
import useForcedWebGL, { THREE, useDarkScene } from '../lib/useForcedWebGL.js';
// #endregion
// #region MAIN
let App = useDarkScene( useForcedWebGL() );
let Debug = {};
window.addEventListener( 'load', async ()=>{
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
await App.renderer.init();
App.sphericalLook( 0, 20, 6 );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const data = new Float32Array( [ 1,2,3, 4,5,6 ] ); // Raw Data
const compCnt = 3; // Component Count: Vec3 has 3 Components
const elmCnt = data.length / compCnt; // How many elements in data, Twp Vec3s
// Create GPU buffer space to store data
const bufAttr = new THREE.StorageInstancedBufferAttribute( data, compCnt );
// Create a TSL node as an Accessor for the buffer
// NOTE: .label doesn't work to name variable in GLSL
const nBuf = THREE.storage( bufAttr, 'vec3', data.length );
// -----------------------------------------
// Create compute shader using TSL instead of GLSL
// Create a pile of GLSL code we want to include
const nCode = THREE.code( '#define XX 10.0');
// Create a single GLSL function
// NOTE: glslFn allows for importing, we use this function as a way to include a big pile of code
// into the compute shader. Its a lil hacky but its the only way I found to do it when using Fn as
// your main execution object
const nTestFunc = THREE.glslFn(`vec3 testFunc( vec3 a ){ return a + vec3( 1.0 ) * XX; }`, [ nCode ] );
// Create our main compute function
const fnCompute = THREE.Fn( ()=>{
// NOTE: Examples use element, but its not really needed.
// const attr = nBuf.element( THREE.instanceIndex ).label( 'attr' );
const attr = nBuf; // Dont use ".toVar", assign won't work correctly if so
const offset = THREE.vec3( 1,2,3 ).toVar( 'offset' ); // Create const vec3
const val = attr.add( offset ).toVar( 'val'); // Input + Offset
const val2 = nTestFunc( val ).toVar( 'val2' ); // Change val with custom GLSL function
// The attribute is used as both Input and Output, so save final value
// to the buffer/attribute/varying tsl node
attr.assign( val2 );
} );
// -----------------------------------------
// Create a Compute Node
const nCompute = fnCompute().compute( elmCnt );
// Execute Compute node
await App.renderer.computeAsync( nCompute );
// Transfer Byte Array from GPU into a Float Array on the CPU
const result = new Float32Array( await App.renderer.getArrayBufferAsync( bufAttr ) );
console.log( result );
// -----------------------------------------
// Hack into THREEJS's new WebGL Backend to extract the GLSL generated from Compute Node
console.log( App.renderer._pipelines.nodes.getForCompute( nCompute ).computeShader );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// App.createRenderLoop( onPreRender ).start();
App.renderLoop();
});
function onPreRender( dt, et ){}
// #endregion
</script></body></html>
// #region IMPORTS
// NOTE: "three" must map to three.webgpu.js or .min.js globally to prevent
// certain errors/warnings from showing up
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export { THREE };
// #endregion
// #region OPTIONS
export function useDarkScene( tjs, props={} ){
const pp = Object.assign( { ambient:0x404040, grid:true }, props );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Light
const light = new THREE.DirectionalLight( 0xffffff, 1.0 );
light.position.set( 4, 10, 1 );
tjs.scene.add( light );
tjs.scene.add( new THREE.AmbientLight( pp.ambient ) );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Floor : For grid to work correctly, need import from three needs to globally point to gpu version
// else there will be errors about grid material not supporting NodeMaterial
if( pp.grid ) tjs.scene.add( new THREE.GridHelper( 20, 20, 0x0c610c, 0x444444 ) );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Renderer
tjs.renderer.setClearColor( 0x3a3a3a, 1 );
return tjs;
};
// #endregion
export default function useForcedWebGL( props ){
props = Object.assign( {
// colorMode : false,
// shadows : false,
preserverBuffer : false,
power : '',
}, props );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RENDERER
const options = {
forceWebGL : true,
antialias : true,
preserveDrawingBuffer : props.preserverBuffer,
powerPreference : ( props.power === '') ? 'default' :
( props.power === 'high' ) ? 'high-performance' : 'low-power',
};
const canvas = document.createElement( 'canvas' );
options.canvas = canvas;
options.context = canvas.getContext( 'webgl2', { preserveDrawingBuffer: props.preserverBuffer } );
const renderer = new THREE.WebGPURenderer( options );
renderer.setPixelRatio( Math.max( 1, window.devicePixelRatio ) );
renderer.setClearColor( 0x3a3a3a, 1 );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.setAnimationLoop( animation );
document.body.appendChild( renderer.domElement );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CORE
const scene = new THREE.Scene();
const clock = new THREE.Clock();
clock.start();
const camera = new THREE.PerspectiveCamera( 45, 1.0, 0.01, 1000 );
camera.position.set( 0, 5, 20 );
const camCtrl = new OrbitControls( camera, renderer.domElement );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// METHODS
let self; // Need to declare before methods for it to be useable
const render = ( onPreRender=null, onPostRender=null ) =>{
const deltaTime = clock.getDelta();
const ellapseTime = clock.getElapsedTime();
if( onPreRender ) onPreRender( deltaTime, ellapseTime );
renderer.render( scene, camera );
if( onPostRender ) onPostRender( deltaTime, ellapseTime );
return self;
};
const renderLoop = ()=>{
window.requestAnimationFrame( renderLoop );
render();
return self;
};
const createRenderLoop = ( fnPreRender=null, fnPostRender=null )=>{
let reqId = 0;
const rLoop = {
stop : ()=>window.cancelAnimationFrame( reqId ),
start : ()=>rLoop.onRender(),
onRender : ()=>{
render( fnPreRender, fnPostRender );
reqId = window.requestAnimationFrame( rLoop.onRender );
},
};
return rLoop;
};
const sphericalLook = ( lon, lat, radius, target=null )=>{
const phi = ( 90 - lat ) * Math.PI / 180;
const theta = ( lon + 180 ) * Math.PI / 180;
camera.position.set(
-(radius * Math.sin( phi ) * Math.sin(theta)),
radius * Math.cos( phi ),
-(radius * Math.sin( phi ) * Math.cos(theta))
);
if( target ) camCtrl.target.fromArray( target );
camCtrl.update();
return self;
};
const resize = ( w=0, h=0 )=>{
const W = w || window.innerWidth;
const H = h || window.innerHeight;
renderer.setSize( W, H ); // Update Renderer
camera.aspect = W / H; // Update Camera
camera.updateProjectionMatrix();
return self;
};
const getBufferSize = ()=>{ return renderer.getDrawingBufferSize( new THREE.Vector2() ).toArray(); };
const debugMaterial = ( mesh )=>{ return renderer.debug.getShaderAsync( scene, camera, mesh ); }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
window.addEventListener( 'resize', ()=>resize() );
resize();
return self = {
renderer,
scene,
camera,
camCtrl,
render,
renderLoop,
createRenderLoop,
sphericalLook,
resize,
getBufferSize,
debugMaterial,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment