├── .gitignore
├── 01_TSL_Basics
├── textures
│ └── crate.gif
└── script_01.js
├── 00_Author_Sketchbook
├── textures
│ └── crate.gif
└── script_test.js
├── .editorconfig
├── vite.config.js
├── index.html
├── 0x_TSL_Showcase
└── Backdrop Water
│ ├── index.html
│ └── backdropWater.js
├── package.json
├── README.md
├── .eslintrc.json
├── main.css
└── 02_Compute_Shaders
└── script_02.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *node_modules
2 | *dist
3 | .env
--------------------------------------------------------------------------------
/01_TSL_Basics/textures/crate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmhhelgeson/Threejs_TSL_Tutorials/HEAD/01_TSL_Basics/textures/crate.gif
--------------------------------------------------------------------------------
/00_Author_Sketchbook/textures/crate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmhhelgeson/Threejs_TSL_Tutorials/HEAD/00_Author_Sketchbook/textures/crate.gif
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 |
9 | [*.{js,ts,html}]
10 | charset = utf-8
11 | indent_style = tab
12 |
13 | [*.{js,ts}]
14 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import topLevelAwait from "vite-plugin-top-level-await";
3 |
4 | export default defineConfig ({
5 | resolve: {
6 | alias: {
7 | 'three/addons': 'three/examples/jsm',
8 | 'three/tsl': 'three/webgpu',
9 | 'three': 'three/webgpu'
10 | }
11 | },
12 | plugins:[
13 | topLevelAwait({
14 | promiseExportName: "__tla",
15 | promiseImportName: i => `__tla_${i}`
16 | })
17 | ],
18 | server: {
19 | port: 5173,
20 | }
21 | });
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js TSL Tutorial Part 1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/0x_TSL_Showcase/Backdrop Water/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js TSL Tutorial Part 1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsl_basics",
3 | "type": "module",
4 | "main": "index.js",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build"
8 | },
9 | "devDependencies": {
10 | "@types/three": "^0.169.0",
11 | "eslint": "^9.13.0",
12 | "eslint-config-mdcs": "^5.0.0",
13 | "eslint-plugin-compat": "^6.0.1",
14 | "eslint-plugin-html": "^8.1.2",
15 | "eslint-plugin-import": "^2.31.0",
16 | "typescript": "^5.6.3",
17 | "vite-plugin-top-level-await": "^1.4.4"
18 | },
19 | "dependencies": {
20 | "three": "^0.169.0",
21 | "vite": "^5.4.10"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Three.js WebGPURenderer and TSL Tutorials
2 | Welcome to the Three.js TSL Tutorials! This series is designed to help you explore and master the Three.js Shading Language (TSL) and its integration with the WebGPURenderer. Whether you're just starting out or looking to deepen your understanding, these tutorials will guide you through creating shaders and utilizing the powerful new features offered by Three.js.
3 |
4 | ## WebGPU API Tutorials
5 |
6 | A step by step guide through the new WebGPURenderer API.
7 |
8 | ### Part 1: Fragment/Vertex Shaders
9 | In this tutorial, dive into the basics of applying vertex and fragment shaders to node materials using TSL.
10 |
11 | [Tutorial Link](https://medium.com/@christianhelgeson/three-js-webgpurenderer-part-1-fragment-vertex-shaders-1070063447f0)
12 |
13 | 
14 |
15 | ### Part 2: Compute Shaders
16 | *Coming soon!*
17 |
18 | ### Part 0: Author Sketchbook
19 | The sketchbook and test code for future tutorials. Not intended to be used for educational purposes.
20 |
21 | ## Three.js Examples Explainer
22 |
23 | Shorter explorations of the official Three.js WebGPU samples.
24 |
25 | *Coming soon!*
26 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "es2018": true
7 | },
8 | "parserOptions": {
9 | "ecmaVersion": 2018,
10 | "sourceType": "module"
11 | },
12 | "extends": [
13 | "mdcs",
14 | "plugin:compat/recommended"
15 | ],
16 | "plugins": [
17 | "html",
18 | "import"
19 | ],
20 | "settings": {
21 | "polyfills": [
22 | "WebGL2RenderingContext"
23 | ]
24 | },
25 | "globals": {
26 | "__THREE_DEVTOOLS__": "readonly",
27 | "potpack": "readonly",
28 | "fflate": "readonly",
29 | "Stats": "readonly",
30 | "XRWebGLBinding": "readonly",
31 | "XRWebGLLayer": "readonly",
32 | "GPUShaderStage": "readonly",
33 | "GPUBufferUsage": "readonly",
34 | "GPUTextureUsage": "readonly",
35 | "GPUTexture": "readonly",
36 | "GPUMapMode": "readonly",
37 | "QUnit": "readonly",
38 | "Ammo": "readonly",
39 | "XRRigidTransform": "readonly",
40 | "XRMediaBinding": "readonly",
41 | "CodeMirror": "readonly",
42 | "esprima": "readonly",
43 | "jsonlint": "readonly",
44 | "VideoFrame": "readonly"
45 | },
46 | "rules": {
47 | "no-throw-literal": [
48 | "error"
49 | ],
50 | "quotes": [
51 | "error",
52 | "single"
53 | ],
54 | "prefer-const": [
55 | "error",
56 | {
57 | "destructuring": "any",
58 | "ignoreReadBeforeAssign": false
59 | }
60 | ],
61 | "no-irregular-whitespace": [
62 | "error"
63 | ]
64 | }
65 | }
--------------------------------------------------------------------------------
/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | background-color: #000;
4 | color: #fff;
5 | font-family: Monospace;
6 | font-size: 13px;
7 | line-height: 24px;
8 | overscroll-behavior: none;
9 | }
10 |
11 | a {
12 | color: #ff0;
13 | text-decoration: none;
14 | }
15 |
16 | a:hover {
17 | text-decoration: underline;
18 | }
19 |
20 | button {
21 | cursor: pointer;
22 | text-transform: uppercase;
23 | }
24 |
25 | #info {
26 | position: absolute;
27 | top: 0px;
28 | width: 100%;
29 | padding: 10px;
30 | box-sizing: border-box;
31 | text-align: center;
32 | -moz-user-select: none;
33 | -webkit-user-select: none;
34 | -ms-user-select: none;
35 | user-select: none;
36 | pointer-events: none;
37 | z-index: 1; /* TODO Solve this in HTML */
38 | }
39 |
40 | a, button, input, select {
41 | pointer-events: auto;
42 | }
43 |
44 | .lil-gui {
45 | z-index: 2 !important; /* TODO Solve this in HTML */
46 | }
47 |
48 | @media all and ( max-width: 640px ) {
49 | .lil-gui.root {
50 | right: auto;
51 | top: auto;
52 | max-height: 50%;
53 | max-width: 80%;
54 | bottom: 0;
55 | left: 0;
56 | }
57 | }
58 |
59 | #overlay {
60 | position: absolute;
61 | font-size: 16px;
62 | z-index: 2;
63 | top: 0;
64 | left: 0;
65 | width: 100%;
66 | height: 100%;
67 | display: flex;
68 | align-items: center;
69 | justify-content: center;
70 | flex-direction: column;
71 | background: rgba(0,0,0,0.7);
72 | }
73 |
74 | #overlay button {
75 | background: transparent;
76 | border: 0;
77 | border: 1px solid rgb(255, 255, 255);
78 | border-radius: 4px;
79 | color: #ffffff;
80 | padding: 12px 18px;
81 | text-transform: uppercase;
82 | cursor: pointer;
83 | }
84 |
85 | #notSupported {
86 | width: 50%;
87 | margin: auto;
88 | background-color: #f00;
89 | margin-top: 20px;
90 | padding: 10px;
91 | }
--------------------------------------------------------------------------------
/00_Author_Sketchbook/script_test.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { uniform, temp, storage, If, float, Fn, vec3, instanceIndex, positionLocal, negate, abs, attribute } from 'three/tsl';
3 | import { LineSegments2 } from 'three/addons/lines/webgpu/LineSegments2.js';
4 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
5 |
6 | import GUI from 'three/addons/libs/lil-gui.module.min.js';
7 | import Stats from 'three/addons/libs/stats.module.js';
8 |
9 | let camera, scene, renderer;
10 |
11 | // CPU Compute
12 | let computeParticleCPU;
13 | // GPU Compute
14 | let computeParticleGPU;
15 |
16 | // Particles Mesh
17 | let particlesMesh;
18 |
19 | const numParticles = 1000;
20 |
21 | const params = {
22 | compute: 'GPU'
23 | }
24 | let stats;
25 |
26 |
27 | function init() {
28 |
29 | // Since we need the WebGPURenderer to perform some calculations, we go against convention by initializing the renderer first.
30 | renderer = new THREE.WebGPURenderer({ antialias: false })
31 | renderer.setPixelRatio( window.devicePixelRatio );
32 | renderer.setSize( window.innerWidth, window.innerHeight );
33 | renderer.setAnimationLoop( animate );
34 | document.body.appendChild( renderer.domElement );
35 |
36 | scene = new THREE.Scene();
37 |
38 | camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
39 | camera.position.z = 15;
40 |
41 | // Add bounds helper
42 | const boxGeometry = new THREE.BoxGeometry(20, 20, 20);
43 | const wireframe = new THREE.WireframeGeometry( boxGeometry );
44 | const line = new LineSegments2( wireframe );
45 | line.material.depthTest = false;
46 | line.material.opacity = 0.25;
47 | line.material.transparent = true;
48 | const boundsHelper = new THREE.BoxHelper( line, 0xffffff );
49 | scene.add( boundsHelper );
50 |
51 | const geometry = new THREE.SphereGeometry(0.1, 10, 10);
52 | const instancePositionBaseAttribute = geometry.attributes.instancePosition;
53 | const instancePositionStorageAttribute = new THREE.StorageInstancedBufferAttribute(instancePositionBaseAttribute.count, 3);
54 | const instanceVelocityStorageAttribute = new THREE.StorageInstancedBufferAttribute(instancePositionBaseAttribute.count, 3)
55 | const material = new THREE.MeshStandardNodeMaterial( { color: "red" });
56 |
57 | particlesMesh = new THREE.InstancedMesh(geometry, material, numParticles);
58 |
59 | scene.add(particlesMesh);
60 |
61 | const velocities = new Float32Array(numParticles * 3);
62 |
63 | for (let i = 0; i < numParticles; i ++) {
64 | // Assign random velocities between ( -3 to 3 ) to each particle
65 | velocities[i * 3] = (Math.random() * 2 - 1) * 0.1;
66 | velocities[i * 3 + 1] = (Math.random() * 2 - 1) * 0.1;
67 | velocities[i * 3 + 2] = (Math.random() * 2 - 1) * 0.1;
68 | }
69 |
70 | particlesMesh.geometry.setAttribute('instanceVelocity', new THREE.InstancedBufferAttribute(velocities, 3))
71 |
72 | computeParticleCPU = () => {
73 |
74 | const dummy = new THREE.Object3D();
75 | const position = new THREE.Vector3();
76 | const velocity = new THREE.Vector3();
77 | const matrix = new THREE.Matrix4();
78 |
79 | for ( let i = 0; i < numParticles; i++ ) {
80 |
81 | // Get the current transformation matrix
82 | // of instance i of the instanced mesh
83 | particlesMesh.getMatrixAt(i, matrix);
84 |
85 | // Extract the instance's position from the matrix
86 | position.setFromMatrixPosition(matrix);
87 |
88 | velocity.set(
89 | velocities[i * 3],
90 | velocities[i * 3 + 1],
91 | velocities[i * 3 + 2]
92 | );
93 | // Apply velocity to position
94 | position.add(velocity);
95 |
96 | // Second part of tutorial
97 |
98 | if (position.x < -10) {
99 | position.x = -10;
100 | velocities[ i * 3 ] = -velocities[i * 3]
101 | } else if (position.x > 10) {
102 | position.x = 10;
103 | velocities[ i * 3 ] = -velocities[i * 3]
104 | }
105 |
106 |
107 | if (position.y < -10) {
108 | position.y = -10;
109 | velocities[ i * 3 + 1 ] = -velocities[i * 3 + 1]
110 | } else if (position.y > 10) {
111 | position.y = 10;
112 | velocities[ i * 3 + 1] = -velocities[i * 3 + 1]
113 | }
114 |
115 |
116 | if (position.z < -10) {
117 | position.z = -10;
118 | velocities[ i * 3 + 2] = -velocities[i * 3 + 2]
119 | } else if (position.z > 10) {
120 | position.z = 10;
121 | velocities[ i * 3 + 2 ] = -velocities[i * 3 + 2]
122 | }
123 |
124 |
125 | dummy.position.set(position.x, position.y, position.z);
126 | dummy.updateMatrix();
127 |
128 | particlesMesh.setMatrixAt(i, dummy.matrix);
129 |
130 | }
131 |
132 | particlesMesh.instanceMatrix.needsUpdate = true;
133 |
134 | }
135 |
136 | material.positionNode = positionLocal.add(attribute('instanceVelocity').mul(100));
137 |
138 |
139 | const directionalLight = new THREE.AmbientLight(0xffffff, 20);
140 | const directionalLight2 = new THREE.DirectionalLight(0xffffff, 20);
141 | directionalLight.position.set(5, 3, 7);
142 | directionalLight2.position.set(5, 3, -7);
143 | scene.add(directionalLight);
144 | scene.add(directionalLight2);
145 | //
146 |
147 | const controls = new OrbitControls( camera, renderer.domElement );
148 | controls.minDistance = 1;
149 | controls.maxDistance = 40;
150 |
151 | const gui = new GUI();
152 | gui.add(params, 'compute', ['CPU', 'GPU']);
153 |
154 | stats = new Stats();
155 | document.body.appendChild(stats.dom)
156 |
157 |
158 | window.addEventListener( 'resize', onWindowResize );
159 |
160 | }
161 |
162 | function onWindowResize() {
163 |
164 | camera.aspect = window.innerWidth / window.innerHeight;
165 | camera.updateProjectionMatrix();
166 |
167 | renderer.setSize( window.innerWidth, window.innerHeight );
168 |
169 | }
170 |
171 | function animate() {
172 |
173 | if (params.compute === 'CPU') {
174 |
175 | computeParticleCPU();
176 |
177 | } else {
178 |
179 | //renderer.compute( computeParticleGPU );
180 |
181 | }
182 |
183 | renderer.render( scene, camera );
184 | stats.update();
185 |
186 | }
187 |
188 | init();
--------------------------------------------------------------------------------
/01_TSL_Basics/script_01.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { positionGeometry, cameraProjectionMatrix, modelViewProjection, modelScale, positionView, modelViewMatrix, storage, attribute, float, timerLocal, uniform, tslFn, vec3, vec4, rotate, PI2, sin, cos, instanceIndex, negate, texture, uv, vec2, positionLocal, int } from 'three/tsl';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 |
5 | import GUI from 'three/addons/libs/lil-gui.module.min.js';
6 |
7 | let camera, scene, renderer;
8 | let mesh;
9 |
10 | init();
11 |
12 | function init() {
13 |
14 | camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
15 | camera.position.z = 15;
16 |
17 | scene = new THREE.Scene();
18 |
19 | const instanceCount = 80;
20 | const numCircles = 4;
21 | const meshesPerCircle = instanceCount / numCircles;
22 |
23 | const geometry = new THREE.BoxGeometry( 0.1, 0.1, 0.1 );
24 |
25 | const texture = new THREE.TextureLoader().load( 'textures/crate.gif' );
26 | texture.colorSpace = THREE.SRGBColorSpace;
27 |
28 | // 1. Standard Three.js Material: const material = new THREE.MeshBasicMaterial( { map: texture } );
29 | // 2: Basic Three.js Node Material: const material = new MeshBasicNodeMaterial( { map: texture } );
30 | // 3. Three.js Node Material which accounts for lighting new MeshStandardNodeMaterial( { map: texture } );
31 | const material = new THREE.MeshBasicNodeMaterial( { map: texture } );
32 |
33 | const effectController = {
34 | uCircleRadius: uniform( 1.0 ),
35 | uCircleSpeed: uniform( 0.5 ),
36 | uSeparationStart: uniform( 1.0 ),
37 | uSeparationEnd: uniform( 2.0 ),
38 | uCircleBounce: uniform( 0.02 ),
39 | };
40 |
41 | const positionTSL = tslFn( () => {
42 |
43 | // Destructure uniforms
44 | const { uCircleRadius, uCircleSpeed, uSeparationStart, uSeparationEnd, uCircleBounce } = effectController;
45 |
46 | // Access the time elapsed since shader creation.
47 | const time = timerLocal();
48 | const circleSpeed = time.mul( uCircleSpeed );
49 |
50 | // Index of a cube within its respective circle.
51 | const instanceWithinCircle = instanceIndex.remainder( meshesPerCircle );
52 |
53 | // Index of the circle that the cube mesh belongs to.
54 | const circleIndex = instanceIndex.div( meshesPerCircle ).add( 1 );
55 |
56 | // Circle Index Even = 1, Circle Index Odd = -1.
57 | const evenOdd = circleIndex.remainder( 2 ).mul( 2 ).oneMinus();
58 |
59 | // Increase radius when we enter the next circle.
60 | const circleRadius = uCircleRadius.mul( circleIndex );
61 |
62 | // Normalize instanceWithinCircle to range [0, 2*PI].
63 | const angle = float( instanceWithinCircle ).div( meshesPerCircle ).mul( PI2 ).add( circleSpeed );
64 |
65 | // Rotate even and odd circles in opposite directions.
66 | const circleX = sin( angle ).mul( circleRadius ).mul( evenOdd );
67 | const circleY = cos( angle ).mul( circleRadius );
68 |
69 | // Scale cubes in later concentric circles to be larger.
70 | const scalePosition = positionLocal.mul( circleIndex );
71 |
72 | // Rotate the individual cubes that form the concentric circles.
73 | const rotatePosition = rotate( scalePosition, vec3( time, time, time ) );
74 |
75 | // Control how much the circles bounce vertically.
76 | const bounceOffset = cos( time.mul( 10 ) ).mul( uCircleBounce );
77 |
78 | // Bounce odd and even circles in opposite directions.
79 | const bounce = circleIndex.remainder( 2 ).equal( 0 ).cond( bounceOffset, negate( bounceOffset ) );
80 |
81 | // Distance between minimumn and maximumn z-distance between circles.
82 | const separationDistance = uSeparationEnd.sub( uSeparationStart );
83 |
84 | // Move sin into range of 0 to 1.
85 | const sinRange = ( sin( time ).add( 1 ) ).mul( 0.5 );
86 |
87 | // Make circle separation oscillate in a range of separationStart to separationEnd
88 | const separation = uSeparationStart.add( sinRange.mul( separationDistance ) );
89 |
90 | // Y pos offset by bounce. Z-distance from the origin increases with each circle.
91 | const newPosition = rotatePosition.add( vec3( circleX, circleY.add( bounce ), float( circleIndex ).mul( separation ) ) );
92 | return newPosition;
93 |
94 | } );
95 |
96 | material.positionNode = positionTSL();
97 | //material.colorNode = texture( crateTexture, uv().add( vec2( timerLocal(), negate( timerLocal()) ) ));
98 | const r = sin( timerLocal().add( instanceIndex ) );
99 | const g = cos( timerLocal().add( instanceIndex ) );
100 | const b = sin( timerLocal() );
101 | material.fragmentNode = vec4( r, g, b, 1.0 );
102 |
103 |
104 | mesh = new THREE.InstancedMesh( geometry, material, instanceCount );
105 | scene.add( mesh );
106 |
107 | const directionalLight = new THREE.DirectionalLight( 0xffffff, 1 );
108 | directionalLight.position.set( 5, 3, - 7.5 );
109 | scene.add( directionalLight );
110 | renderer = new THREE.WebGPURenderer( { antialias: false } );
111 | renderer.setPixelRatio( window.devicePixelRatio );
112 | renderer.setSize( window.innerWidth, window.innerHeight );
113 | renderer.setAnimationLoop( animate );
114 | document.body.appendChild( renderer.domElement );
115 |
116 | const controls = new OrbitControls( camera, renderer.domElement );
117 | controls.minDistance = 1;
118 | controls.maxDistance = 30;
119 |
120 | const gui = new GUI();
121 | gui.add( effectController.uCircleRadius, 'value', 0.1, 3.0, 0.1 ).name( 'Circle Radius' );
122 | gui.add( effectController.uCircleSpeed, 'value', 0.1, 3.0, 0.1 ).name( 'Circle Speed' );
123 | gui.add( effectController.uSeparationStart, 'value', 0.5, 4, 0.1 ).name( 'Separation Start' );
124 | gui.add( effectController.uSeparationEnd, 'value', 1.0, 5.0, 0.1 ).name( 'Separation End' );
125 | gui.add( effectController.uCircleBounce, 'value', 0.01, 0.2, 0.001 ).name( 'Circle Bounce' );
126 |
127 | window.addEventListener( 'resize', onWindowResize );
128 |
129 | }
130 |
131 | function onWindowResize() {
132 |
133 | camera.aspect = window.innerWidth / window.innerHeight;
134 | camera.updateProjectionMatrix();
135 |
136 | renderer.setSize( window.innerWidth, window.innerHeight );
137 |
138 | }
139 |
140 | function animate() {
141 |
142 | renderer.render( scene, camera );
143 |
144 | }
145 |
--------------------------------------------------------------------------------
/02_Compute_Shaders/script_02.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { uniform, temp, storage, If, float, Fn, vec3, instanceIndex, positionLocal, negate, abs } from 'three/tsl';
3 | import { LineSegments2 } from 'three/addons/lines/webgpu/LineSegments2.js';
4 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
5 |
6 | import GUI from 'three/addons/libs/lil-gui.module.min.js';
7 |
8 | let camera, scene, renderer;
9 | let computeParticle;
10 |
11 | function init() {
12 |
13 | // Since we need the WebGPURenderer to perform some calculations, we go against convention by initializing the renderer first.
14 | renderer = new THREE.WebGPURenderer({ antialias: false })
15 | renderer.setPixelRatio( window.devicePixelRatio );
16 | renderer.setSize( window.innerWidth, window.innerHeight );
17 | renderer.setAnimationLoop( animate );
18 | document.body.appendChild( renderer.domElement );
19 |
20 | scene = new THREE.Scene();
21 |
22 | camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
23 | camera.position.z = 15;
24 |
25 | const geometry = new THREE.SphereGeometry(0.1, 10, 10);
26 | const material = new THREE.MeshStandardNodeMaterial( { color: "red" });
27 |
28 | // Define necessary values for compute calculation
29 | const numParticles = 10000;
30 | // Bounds are from [ -10, 10 ] in all directions
31 | const maxBoundSize = 10;
32 | const uBoundsX = uniform( maxBoundSize );
33 | const uBoundsY = uniform( maxBoundSize );
34 | const uBoundsZ = uniform( maxBoundSize );
35 | const VEC3_SIZE = 3;
36 |
37 | const positionArray = new Float32Array( numParticles * 3 );
38 | const velocityArray = new Float32Array( numParticles * 3);
39 |
40 |
41 | // Two ways to initialize the values of a storage buffer
42 | // 1. Directly access the array ( good for populating buffers with randomized data, as seen below )
43 | for ( let i = 0; i < numParticles * 3; i += 3 ) {
44 |
45 | // Assign each component of velocity within range of [ -3, 3 ]
46 | const x = Math.random() * 2 - 1;
47 | const y = Math.random() * 2 - 1;
48 | const z = Math.random() * 2 - 1;
49 |
50 | positionArray[ i ] = 0;
51 | positionArray[ i + 1] = 0;
52 | positionArray[ i + 2] = 0;
53 |
54 | velocityArray[ i ] = x;
55 | velocityArray[ i + 1 ] = y;
56 | velocityArray[ i + 2 ] = z;
57 |
58 | }
59 |
60 | // Position of each particle
61 | const positionBufferAttribute = new THREE.StorageInstancedBufferAttribute( positionArray, 3 );
62 | // Velocity of each particle
63 | const velocityBufferAttribute = new THREE.StorageInstancedBufferAttribute( velocityArray, 3 );
64 |
65 |
66 | // Pass buffer attributes as arguments to a new StorageBufferNode.
67 | // Storage buffer nodes allow us to access buffer attribute data within our compute shader.
68 | const positionStorage = storage( positionBufferAttribute, 'vec3', positionBufferAttribute.count );
69 | const velocityStorage = storage( velocityBufferAttribute , 'vec3', velocityBufferAttribute.count );
70 |
71 | const positionInitFn = Fn(() => {
72 |
73 | // Initialize all particles at position ( 0, 0, 0 )
74 | positionStorage.element( instanceIndex ).assign( vec3( 0, 0, 0 ) );
75 |
76 | })
77 |
78 | const positionInit = positionInitFn().compute( numParticles );
79 | renderer.compute( positionInit );
80 |
81 | // 2. Run an initial compute pass over each value of the array
82 |
83 | const getSign = ( valueNode ) => {
84 |
85 | return valueNode.div( abs( valueNode ) );
86 |
87 | }
88 |
89 | const computeParticleFn = Fn(() => {
90 | const vel = velocityStorage.element( instanceIndex );
91 | const pos = positionStorage.element( instanceIndex );
92 | const newPosX = temp( pos.x.add( vel.x ), 'newPosX' );
93 | const newPosY = temp( pos.y.add( vel.y ), 'newPosY' );
94 | const newPosZ = temp( pos.z.add( vel.z ), 'newPosZ' );
95 |
96 | If( abs( newPosX ).greaterThan( uBoundsX ), () => {
97 |
98 | const reverseVel = negate( vel.x );
99 | const rescuePos = uBoundsX.mul( getSign( newPosX ) );
100 | newPosX.assign( rescuePos.add( reverseVel ) ) ;
101 | vel.x.assign( negate(vel.x) );
102 |
103 | })
104 |
105 | If( abs( newPosY ).greaterThan( uBoundsY ), () => {
106 |
107 | const reverseVel = negate( vel.y );
108 | const rescuePos = uBoundsY.mul( getSign( newPosY ) );
109 | newPosY.assign( rescuePos.add( reverseVel ) ) ;
110 | vel.y.assign( reverseVel );
111 |
112 | });
113 |
114 | If( abs( newPosZ ).greaterThan( uBoundsZ ), () => {
115 |
116 | const reverseVel = negate( vel.z );
117 | const rescuePos = uBoundsZ.mul( getSign( newPosZ ) );
118 | newPosZ.assign( rescuePos.add( reverseVel ) ) ;
119 | vel.z.assign( reverseVel );
120 |
121 | });
122 |
123 | pos.assign( vec3( newPosX, newPosY, newPosZ ) );
124 |
125 | });
126 |
127 | computeParticle = computeParticleFn().compute( numParticles );
128 |
129 | material.positionNode = positionLocal.add(positionStorage.toAttribute());
130 | material.colorNode = Fn(() => {
131 |
132 | const velocity = velocityStorage.element(instanceIndex)
133 |
134 | return vec3(velocity.x, velocity.y, 0.0);
135 |
136 | })();
137 |
138 | const instancedSphere = new THREE.InstancedMesh( geometry, material, 100 );
139 | console.log(instancedSphere)
140 | scene.add( instancedSphere );
141 |
142 | // Add bounds helper
143 | const boxGeometry = new THREE.BoxGeometry(20, 20, 20);
144 | const wireframe = new THREE.WireframeGeometry( boxGeometry );
145 | const wireframeMaterial = new THREE.MeshBasicNodeMaterial({color: 0xffffff});
146 | const line = new LineSegments2( wireframe );
147 | line.material.depthTest = false;
148 | line.material.opacity = 0.25;
149 | line.material.transparent = true;
150 | const boundsHelper = new THREE.BoxHelper( line, 0xffffff );
151 | scene.add( boundsHelper );
152 | console.log( boundsHelper );
153 |
154 | // Existing implementation of bounds helper doesn't use new features like StorageBufferAttributes
155 | // Needs to be modified
156 | const positionBaseAttribute = boundsHelper.geometry.attributes.position;
157 |
158 | const positionStorageBufferAttribute = new THREE.StorageBufferAttribute(
159 | positionBaseAttribute.array,
160 | positionBaseAttribute.itemSize
161 | );
162 |
163 | console.log(positionStorageBufferAttribute)
164 |
165 | boundsHelper.geometry.setAttribute( 'position', positionStorageBufferAttribute );
166 |
167 | boundsHelper.material = new THREE.LineBasicNodeMaterial({
168 | positionNode: Fn(() => {
169 | const size = float(maxBoundSize);
170 | const ratio = vec3( size.div(uBoundsX), size.div(uBoundsY), size.div(uBoundsZ));
171 | return positionLocal.div(ratio);
172 | })()
173 | });
174 |
175 | const directionalLight = new THREE.DirectionalLight(0xffffff, 20);
176 | const directionalLight2 = new THREE.DirectionalLight(0xffffff, 20);
177 | directionalLight.position.set(5, 3, 7);
178 | directionalLight2.position.set(5, 3, -7);
179 | scene.add(directionalLight);
180 | scene.add(directionalLight2);
181 | //
182 |
183 | const controls = new OrbitControls( camera, renderer.domElement );
184 | controls.minDistance = 1;
185 | controls.maxDistance = 40;
186 |
187 | const updateBoxHelper = () => {
188 |
189 | boundsHelper.scale.set(5, uBoundsY.value, uBoundsZ.value);
190 |
191 | }
192 |
193 | const gui = new GUI();
194 | gui.add( uBoundsX, 'value', 5, 10, 0.1 ).name( 'Bounds X' ).onChange( updateBoxHelper );
195 | gui.add( uBoundsY, 'value', 5, 10, 0.1 ).name( 'Bounds Y' );
196 | gui.add( uBoundsZ, 'value', 5, 10, 0.1 ).name( 'Bounds Z' );
197 |
198 | window.addEventListener( 'resize', onWindowResize );
199 |
200 | }
201 |
202 | function onWindowResize() {
203 |
204 | camera.aspect = window.innerWidth / window.innerHeight;
205 | camera.updateProjectionMatrix();
206 |
207 | renderer.setSize( window.innerWidth, window.innerHeight );
208 |
209 | }
210 |
211 | function animate() {
212 |
213 | renderer.compute( computeParticle );
214 |
215 | renderer.render( scene, camera );
216 |
217 | }
218 |
219 | init();
--------------------------------------------------------------------------------
/0x_TSL_Showcase/Backdrop Water/backdropWater.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import * as THREE from 'three';
4 | import { color, uniform, vec2, pass, linearDepth, normalWorld, triplanarTexture, texture, objectPosition, screenUV, viewportLinearDepth, viewportDepthTexture, viewportSharedTexture, blur, mx_worley_noise_float, positionWorld, timerLocal } from 'three/tsl';
5 | import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
6 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
7 | import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
8 | import Stats from 'three/addons/libs/stats.module.js';
9 |
10 | let camera, waterScene, backdropAlphaScene, renderer;
11 | let mixer, objects, clock;
12 | let model, floor, floorPosition;
13 | let postProcessing;
14 | let controls;
15 | let stats;
16 |
17 | const sunIntensity = 5;
18 | const skyAmbientIntensity = 1;
19 | const waterAmbientIntensity = 5;
20 |
21 | function init() {
22 |
23 | camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.25, 30 );
24 | camera.position.set( 3, 2, 4 );
25 |
26 | waterScene = new THREE.Scene();
27 | //waterScene.fog = new THREE.Fog( 0x0487e2, 7, 25 );
28 | waterScene.backgroundNode = normalWorld.y.mix( color( 0x0487e2 ), color( 0x0066ff ) );
29 | camera.lookAt( 0, 1, 0 );
30 |
31 | const sunLight = new THREE.DirectionalLight( 0xFFE499, sunIntensity );
32 | sunLight.castShadow = true;
33 | sunLight.shadow.camera.near = .1;
34 | sunLight.shadow.camera.far = 5;
35 | sunLight.shadow.camera.right = 2;
36 | sunLight.shadow.camera.left = - 2;
37 | sunLight.shadow.camera.top = 1;
38 | sunLight.shadow.camera.bottom = - 2;
39 | sunLight.shadow.mapSize.width = 2048;
40 | sunLight.shadow.mapSize.height = 2048;
41 | sunLight.shadow.bias = - 0.001;
42 | sunLight.position.set( .5, 3, .5 );
43 |
44 | const waterAmbientLight = new THREE.HemisphereLight( 0x333366, 0x74ccf4, waterAmbientIntensity );
45 | const skyAmbientLight = new THREE.HemisphereLight( 0x74ccf4, 0, skyAmbientIntensity );
46 |
47 | waterScene.add( sunLight );
48 | waterScene.add( skyAmbientLight );
49 | waterScene.add( waterAmbientLight );
50 |
51 | clock = new THREE.Clock();
52 |
53 | // animated model
54 |
55 | const loader = new GLTFLoader();
56 | /*loader.load( 'models/gltf/Michelle.glb', function ( gltf ) {
57 |
58 | model = gltf.scene;
59 | model.children[ 0 ].children[ 0 ].castShadow = true;
60 |
61 | mixer = new THREE.AnimationMixer( model );
62 |
63 | const action = mixer.clipAction( gltf.animations[ 0 ] );
64 | action.play();
65 |
66 | waterScene.add( model );
67 |
68 | } ); */
69 |
70 | // objects
71 |
72 | const textureLoader = new THREE.TextureLoader();
73 | const iceDiffuse = textureLoader.load( './textures/water.jpg' );
74 | iceDiffuse.wrapS = THREE.RepeatWrapping;
75 | iceDiffuse.wrapT = THREE.RepeatWrapping;
76 | iceDiffuse.colorSpace = THREE.NoColorSpace;
77 |
78 | const iceColorNode = triplanarTexture( texture( iceDiffuse ) ).add( color( 0x0066ff ) ).mul( .8 );
79 |
80 | const geometry = new THREE.IcosahedronGeometry( 1, 3 );
81 | const material = new THREE.MeshStandardNodeMaterial( { colorNode: iceColorNode } );
82 |
83 | const count = 100;
84 | const scale = 3.5;
85 | const column = 10;
86 |
87 | objects = new THREE.Group();
88 |
89 | for ( let i = 0; i < count; i ++ ) {
90 |
91 | const x = i % column;
92 | const y = i / column;
93 |
94 | const mesh = new THREE.Mesh( geometry, material );
95 | mesh.position.set( x * scale, 0, y * scale );
96 | mesh.rotation.set( Math.random(), Math.random(), Math.random() );
97 | objects.add( mesh );
98 |
99 | }
100 |
101 | objects.position.set(
102 | ( ( column - 1 ) * scale ) * - .5,
103 | - 1,
104 | ( ( count / column ) * scale ) * - .5
105 | );
106 |
107 | waterScene.add( objects );
108 |
109 | // water
110 |
111 | const timer = timerLocal( .8 );
112 | const floorUV = positionWorld.xzy;
113 |
114 | const waterLayer0Size = uniform( 4 );
115 | const waterLayer1Size = uniform( 2 );
116 |
117 | const waterLayer0 = mx_worley_noise_float( floorUV.mul( waterLayer0Size ).add( timer ) );
118 | const waterLayer1 = mx_worley_noise_float( floorUV.mul( waterLayer1Size ).add( timer ) );
119 |
120 | const waterIntensity = waterLayer0.mul( waterLayer1 );
121 | const waterContrast = uniform( 1.4 );
122 | const liquidColor = uniform( color( 0x0487e2 ) );
123 | const rippleColor = uniform( color( 0x74ccf4 ) );
124 | const waterColor = waterIntensity.mul( waterContrast ).mix( liquidColor, rippleColor );
125 |
126 | // linearDepth() returns the linear depth of the mesh
127 | const depth = linearDepth();
128 | const depthWater = viewportLinearDepth.sub( depth );
129 | const depthEffect = depthWater.remapClamp( - .002, .04 );
130 |
131 | const refractionUV = screenUV.add( vec2( 0, waterIntensity.mul( .1 ) ) );
132 |
133 | // linearDepth( viewportDepthTexture( uv ) ) return the linear depth of the scene
134 | const depthTestForRefraction = linearDepth( viewportDepthTexture( refractionUV ) ).sub( depth );
135 |
136 | const depthRefraction = depthTestForRefraction.remapClamp( 0, .1 );
137 | const finalUV = depthTestForRefraction.lessThan( 0 ).select( screenUV, refractionUV );
138 | const viewportTexture = viewportSharedTexture( finalUV );
139 |
140 | const waterMaterial = new THREE.MeshBasicNodeMaterial();
141 | waterMaterial.colorNode = waterColor;
142 | waterMaterial.backdropNode = depthEffect.mix( viewportSharedTexture(), viewportTexture.mul( depthRefraction.mix( 1, waterColor ) ) );
143 | waterMaterial.backdropAlphaNode = depthRefraction.oneMinus();
144 | waterMaterial.transparent = true;
145 |
146 | const water = new THREE.Mesh( new THREE.BoxGeometry( 50, .001, 50 ), waterMaterial );
147 | water.position.set( 0, 0, 0 );
148 | waterScene.add( water );
149 |
150 | // floor
151 |
152 | floor = new THREE.Mesh( new THREE.CylinderGeometry( 1.1, 1.1, 10 ), new THREE.MeshStandardNodeMaterial( { colorNode: iceColorNode } ) );
153 | floor.position.set( 0, - 5, 0 );
154 | waterScene.add( floor );
155 |
156 | // caustics
157 |
158 | const waterPosY = positionWorld.y.sub( water.position.y );
159 |
160 | let transition = waterPosY.add( .1 ).saturate().oneMinus();
161 | transition = waterPosY.lessThan( 0 ).select( transition, normalWorld.y.mix( transition, 0 ) ).toVar();
162 |
163 | const colorNode = transition.mix( material.colorNode, material.colorNode.add( waterLayer0 ) );
164 |
165 | //material.colorNode = colorNode;
166 | floor.material.colorNode = colorNode;
167 |
168 | // renderer
169 |
170 | renderer = new THREE.WebGPURenderer( { forceWebGL: false } );
171 | renderer.setPixelRatio( window.devicePixelRatio );
172 | renderer.setSize( window.innerWidth, window.innerHeight );
173 | renderer.setAnimationLoop( animate );
174 | document.body.appendChild( renderer.domElement );
175 |
176 | stats = new Stats();
177 | document.body.appendChild( stats.dom );
178 |
179 | controls = new OrbitControls( camera, renderer.domElement );
180 | controls.minDistance = 1;
181 | controls.maxDistance = 10;
182 | controls.maxPolarAngle = Math.PI * 0.9;
183 | controls.autoRotate = true;
184 | controls.autoRotateSpeed = 1;
185 | controls.target.set( 0, .2, 0 );
186 | controls.update();
187 |
188 | // gui
189 |
190 | const gui = new GUI();
191 | floorPosition = new THREE.Vector3( 0, .2, 0 );
192 |
193 | // post processing
194 |
195 | const scenePass = pass( waterScene, camera );
196 | const scenePassColor = scenePass.getTextureNode();
197 | const scenePassDepth = scenePass.getLinearDepthNode().remapClamp( .3, .5 );
198 |
199 | const waterMask = objectPosition( camera ).y.greaterThan( screenUV.y.sub( .5 ).mul( camera.near ) );
200 |
201 | const scenePassColorBlurred = blur( scenePassColor );
202 | scenePassColorBlurred.directionNode = waterMask.select( scenePassDepth, scenePass.getLinearDepthNode().mul( 5 ) );
203 |
204 | const vignet = screenUV.distance( .5 ).mul( 1.35 ).clamp().oneMinus();
205 |
206 | postProcessing = new THREE.PostProcessing( renderer );
207 | postProcessing.outputNode = waterMask.select( scenePassColorBlurred, scenePassColorBlurred.mul( color( 0x74ccf4 ) ).mul( vignet ) );
208 |
209 | const effectController = {
210 |
211 | waterAmbient: true,
212 | skyAmbient: true,
213 | sun: true,
214 | fog: true,
215 |
216 | toggleSun: function () {
217 |
218 | const intensity = effectController.sun === false ? 0 : sunIntensity;
219 | sunLight.intensity = intensity;
220 |
221 | },
222 |
223 | toggleSkyAmbient: function () {
224 |
225 | const intensity = effectController.skyAmbient === false ? 0 : skyAmbientIntensity;
226 | skyAmbientLight.intensity = intensity;
227 |
228 | },
229 |
230 | toggleWaterAmbient: function () {
231 |
232 | const intensity = effectController.waterAmbient === false ? 0 : waterAmbientIntensity;
233 | waterAmbientLight.intensity = intensity;
234 |
235 | },
236 |
237 | sceneBackground: 'standard',
238 | waterColorMode: 'standard',
239 | rippleContrast: 1.4,
240 | rippleLayer0Size: 4,
241 | rippleLayer1Size: 2,
242 | waterTransparency: true,
243 | backdropMode: 'on',
244 | alphaMode: 'on',
245 |
246 | };
247 |
248 | gui.add( floorPosition, 'y', - 1, 1, .001 ).name( 'position' );
249 | const sceneFolder = gui.addFolder( 'Scene' );
250 | sceneFolder.add( effectController, 'fog' ).onChange( () => {
251 |
252 | waterScene.fog = effectController.fog === true ? new THREE.Fog( 0x0487e2, 7, 25 ) : null;
253 |
254 | } );
255 | const lightsFolder = gui.addFolder( 'Lights' );
256 | lightsFolder.add( effectController, 'sun' ).onChange( effectController.toggleSun );
257 | lightsFolder.add( effectController, 'skyAmbient' ).onChange( effectController.toggleSkyAmbient );
258 | lightsFolder.add( effectController, 'waterAmbient' ).onChange( effectController.toggleWaterAmbient );
259 | lightsFolder.add( effectController, 'sceneBackground', [ 'standard', 'dark', 'none' ] ).onChange( () =>{
260 |
261 | switch ( effectController.sceneBackground ) {
262 |
263 | case 'standard': {
264 |
265 | waterScene.backgroundNode = normalWorld.y.mix( color( 0x0487e2 ), color( 0x0066ff ) );
266 | break;
267 |
268 | }
269 |
270 | case 'dark': {
271 |
272 | waterScene.backgroundNode = normalWorld.y.mix( color( 0x100f30 ), color( 0x0066ff ) );
273 | break;
274 |
275 | }
276 |
277 | case 'none': {
278 |
279 | waterScene.backgroundNode = color( 0 );
280 | break;
281 |
282 | }
283 |
284 | }
285 |
286 | waterScene.backgroundNode.needsUpdate = true;
287 |
288 | } );
289 |
290 | const waterSurfaceFolder = gui.addFolder( 'Water Surface Folder' );
291 | waterSurfaceFolder.add( effectController, 'waterColorMode', [
292 | 'standard',
293 | '1. Water Layer 0',
294 | '2. Water Layer 1',
295 | '3. Combined Layers',
296 | ] ).onChange( () => {
297 |
298 | switch ( effectController.waterColorMode ) {
299 |
300 | case 'standard': {
301 |
302 | waterMaterial.colorNode = waterColor;
303 | waterMaterial.needsUpdate = true;
304 |
305 | break;
306 |
307 | }
308 |
309 | case '1. Water Layer 0': {
310 |
311 | waterMaterial.colorNode = waterLayer0;
312 | waterMaterial.needsUpdate = true;
313 |
314 | break;
315 |
316 | }
317 |
318 | case '2. Water Layer 1': {
319 |
320 | waterMaterial.colorNode = waterLayer1;
321 | waterMaterial.needsUpdate = true;
322 |
323 | break;
324 |
325 | }
326 |
327 | case '3. Combined Layers': {
328 |
329 | waterMaterial.colorNode = waterIntensity;
330 | waterMaterial.needsUpdate = true;
331 |
332 | break;
333 |
334 | }
335 |
336 | }
337 |
338 | } );
339 | waterSurfaceFolder.add( effectController, 'rippleContrast', 0, 10.0, 0.1 ).onChange( () => {
340 |
341 | waterContrast.value = effectController.rippleContrast;
342 |
343 | } );
344 | waterSurfaceFolder.add( effectController, 'rippleLayer0Size', 0, 50, 1 ).onChange( () => {
345 |
346 | waterLayer0Size.value = effectController.rippleLayer0Size;
347 |
348 | } );
349 | waterSurfaceFolder.add( effectController, 'rippleLayer1Size', 0, 50, 1 ).onChange( () => {
350 |
351 | waterLayer1Size.value = effectController.rippleLayer1Size;
352 |
353 | } );
354 | const waterBackdropFolder = gui.addFolder( 'Water Backdrop' );
355 | const waterAlphaFolder = gui.addFolder( 'Water Alpha' );
356 | waterBackdropFolder.add( effectController, 'waterTransparency' ).onChange( () => {
357 |
358 | waterMaterial.transparent = effectController.waterTransparency;
359 |
360 | } );
361 | waterBackdropFolder.add( effectController, 'backdropMode', [ 'off', 'linearDepth', 'on' ] ).onChange( () => {
362 |
363 | switch ( effectController.backdropMode ) {
364 |
365 | case 'off': {
366 |
367 | waterMaterial.backdropNode = null;
368 | waterMaterial.needsUpdate = true;
369 |
370 | break;
371 |
372 | }
373 |
374 | case 'linearDepth': {
375 |
376 | waterMaterial.backdropNode = linearDepth();
377 | waterMaterial.needsUpdate = true;
378 |
379 | break;
380 |
381 | }
382 |
383 | case 'on': {
384 |
385 | waterMaterial.backdropNode = depthEffect.mix( viewportSharedTexture(), viewportTexture.mul( depthRefraction.mix( 1, waterColor ) ) );
386 | waterMaterial.needsUpdate = true;
387 |
388 | break;
389 |
390 |
391 | }
392 |
393 |
394 | }
395 |
396 |
397 | } );
398 |
399 | //
400 |
401 | window.addEventListener( 'resize', onWindowResize );
402 |
403 | }
404 |
405 | function onWindowResize() {
406 |
407 | camera.aspect = window.innerWidth / window.innerHeight;
408 | camera.updateProjectionMatrix();
409 | renderer.setSize( window.innerWidth, window.innerHeight );
410 |
411 | }
412 |
413 | function animate() {
414 |
415 | stats.update();
416 | controls.update();
417 | const delta = clock.getDelta();
418 | floor.position.y = floorPosition.y - 5;
419 | if ( model ) {
420 |
421 | mixer.update( delta );
422 | model.position.y = floorPosition.y;
423 |
424 | }
425 |
426 | for ( const object of objects.children ) {
427 |
428 | object.position.y = Math.sin( clock.elapsedTime + object.id ) * .3;
429 | object.rotation.y += delta * .3;
430 |
431 | }
432 |
433 | postProcessing.render();
434 |
435 | }
436 |
437 | init();
438 |
--------------------------------------------------------------------------------