├── .gitignore ├── FXScene.js ├── LICENSE ├── README.md ├── Transition.js ├── img ├── transition0.png ├── transition1.png └── transition2.png ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /FXScene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | const objCount = 5000; 4 | function getMeshProps() { 5 | const arr = []; 6 | for (let i = 0; i < objCount; i += 1) { 7 | arr.push( 8 | { 9 | position: { 10 | x: Math.random() * 10000 - 5000, 11 | y: Math.random() * 6000 - 3000, 12 | z: Math.random() * 8000 - 4000 13 | }, 14 | rotation: { 15 | x: Math.random() * 2 * Math.PI, 16 | y: Math.random() * 2 * Math.PI, 17 | z: Math.random() * 2 * Math.PI, 18 | }, 19 | scale: Math.random() * 200 + 100 20 | } 21 | ) 22 | } 23 | return arr; 24 | } 25 | 26 | const dummyProps = getMeshProps(); 27 | function getMesh(material, needsAnimatedColor = false) { 28 | const size = 0.25; 29 | const geometry = new THREE.IcosahedronGeometry(size, 1); 30 | const mesh = new THREE.InstancedMesh(geometry, material, objCount); 31 | 32 | const dummy = new THREE.Object3D(); 33 | const color = new THREE.Color(); 34 | let props; 35 | for (let i = 0; i < objCount; i++) { 36 | props = dummyProps[i]; 37 | dummy.position.x = props.position.x; 38 | dummy.position.y = props.position.y; 39 | dummy.position.z = props.position.z; 40 | 41 | dummy.rotation.x = props.rotation.x; 42 | dummy.rotation.y = props.rotation.y; 43 | dummy.rotation.z = props.rotation.z; 44 | 45 | dummy.scale.set(props.scale, props.scale, props.scale); 46 | 47 | dummy.updateMatrix(); 48 | 49 | mesh.setMatrixAt(i, dummy.matrix); 50 | if (needsAnimatedColor) { mesh.setColorAt(i, color.setScalar(0.1 + 0.9 * Math.random())); } 51 | } 52 | return mesh; 53 | } 54 | 55 | export function getFXScene({ renderer, material, clearColor, needsAnimatedColor = false }) { 56 | 57 | const w = window.innerWidth; 58 | const h = window.innerHeight; 59 | const camera = new THREE.PerspectiveCamera( 50, w / h, 1, 10000); 60 | camera.position.z = 2000; 61 | 62 | // Setup scene 63 | const scene = new THREE.Scene(); 64 | scene.fog = new THREE.FogExp2(clearColor, 0.0002); 65 | 66 | scene.add(new THREE.HemisphereLight(0xffffff, 0x555555, 1.0)); 67 | const mesh = getMesh(material, needsAnimatedColor); 68 | scene.add(mesh); 69 | 70 | const fbo = new THREE.WebGLRenderTarget(w, h); 71 | 72 | const rotationSpeed = new THREE.Vector3(0.1, -0.2, 0.15); 73 | const update = (delta) => { 74 | mesh.rotation.x += delta * rotationSpeed.x; 75 | mesh.rotation.y += delta * rotationSpeed.y; 76 | mesh.rotation.z += delta * rotationSpeed.z; 77 | if (needsAnimatedColor) { 78 | material.color.setHSL(0.1 + 0.5 * Math.sin(0.0002 * Date.now()), 1, 0.5); 79 | } 80 | } 81 | 82 | const render = (delta, rtt) => { 83 | update(delta); 84 | 85 | renderer.setClearColor(clearColor); 86 | 87 | if (rtt) { 88 | renderer.setRenderTarget(fbo); 89 | renderer.clear(); 90 | renderer.render(scene, camera); 91 | } else { 92 | renderer.setRenderTarget(null); 93 | renderer.render(scene, camera); 94 | } 95 | }; 96 | 97 | return { fbo, render, update }; 98 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Bobby Roe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transition Effect in THREE.js 2 | 3 | Watch the tutorial on [YouTube](https://youtu.be/gsz3dgT-g3Q) 4 | 5 | Based on [Fernando Serrano's example](https://threejs.org/examples/?q=cros#webgl_postprocessing_crossfade) -------------------------------------------------------------------------------- /Transition.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { TWEEN } from "https://cdn.jsdelivr.net/npm/three@0.131/examples/jsm/libs/tween.module.min.js"; 3 | const transitionParams = { 4 | // useTexture: true, 5 | transition: 0, 6 | texture: 5, 7 | cycle: true, 8 | animate: true, 9 | // threshold: 0.3, 10 | }; 11 | 12 | export function getTransition({ renderer, sceneA, sceneB }) { 13 | 14 | const scene = new THREE.Scene(); 15 | const w = window.innerWidth; 16 | const h = window.innerHeight; 17 | const camera = new THREE.OrthographicCamera(w / -2, w / 2, h / 2, h / -2, -10, 10); 18 | 19 | const textures = []; 20 | const loader = new THREE.TextureLoader(); 21 | 22 | for (let i = 0; i < 3; i++) { 23 | textures[i] = loader.load(`./img/transition${i}.png`); 24 | } 25 | 26 | const material = new THREE.ShaderMaterial({ 27 | uniforms: { 28 | tDiffuse1: { 29 | value: null, 30 | }, 31 | tDiffuse2: { 32 | value: null, 33 | }, 34 | mixRatio: { 35 | value: 0.0, 36 | }, 37 | threshold: { 38 | value: 0.1, 39 | }, 40 | useTexture: { 41 | value: 1, 42 | }, 43 | tMixTexture: { 44 | value: textures[0], 45 | }, 46 | }, 47 | vertexShader: `varying vec2 vUv; 48 | void main() { 49 | vUv = vec2( uv.x, uv.y ); 50 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 51 | 52 | }`, 53 | fragmentShader: ` 54 | uniform float mixRatio; 55 | uniform sampler2D tDiffuse1; 56 | uniform sampler2D tDiffuse2; 57 | uniform sampler2D tMixTexture; 58 | uniform int useTexture; 59 | uniform float threshold; 60 | varying vec2 vUv; 61 | 62 | void main() { 63 | vec4 texel1 = texture2D( tDiffuse1, vUv ); 64 | vec4 texel2 = texture2D( tDiffuse2, vUv ); 65 | 66 | if (useTexture == 1) { 67 | vec4 transitionTexel = texture2D( tMixTexture, vUv ); 68 | float r = mixRatio * (1.0 + threshold * 2.0) - threshold; 69 | float mixf=clamp((transitionTexel.r - r)*(1.0/threshold), 0.0, 1.0); 70 | 71 | gl_FragColor = mix( texel1, texel2, mixf ); 72 | } else { 73 | gl_FragColor = mix( texel2, texel1, mixRatio ); 74 | } 75 | }`, 76 | }); 77 | 78 | const geometry = new THREE.PlaneGeometry(w, h); 79 | const mesh = new THREE.Mesh(geometry, material); 80 | scene.add(mesh); 81 | 82 | material.uniforms.tDiffuse1.value = sceneA.fbo.texture; 83 | material.uniforms.tDiffuse2.value = sceneB.fbo.texture; 84 | 85 | new TWEEN.Tween(transitionParams) 86 | .to({ transition: 1 }, 4500) 87 | .repeat(Infinity) 88 | .delay(2000) 89 | .yoyo(true) 90 | .start(); 91 | let needsTextureChange = false; 92 | 93 | const render = (delta) => { 94 | // Transition animation 95 | if (transitionParams.animate) { 96 | TWEEN.update(); 97 | 98 | // Change the current alpha texture after each transition 99 | if (transitionParams.cycle) { 100 | if ( 101 | transitionParams.transition == 0 || 102 | transitionParams.transition == 1 103 | ) { 104 | if (needsTextureChange) { 105 | transitionParams.texture = 106 | (transitionParams.texture + 1) % textures.length; 107 | material.uniforms.tMixTexture.value = 108 | textures[transitionParams.texture]; 109 | needsTextureChange = false; 110 | } 111 | } else { 112 | needsTextureChange = true; 113 | } 114 | } else { 115 | needsTextureChange = true; 116 | } 117 | } 118 | 119 | material.uniforms.mixRatio.value = transitionParams.transition; 120 | 121 | // Prevent render both scenes when it's not necessary 122 | if (transitionParams.transition === 0) { 123 | sceneA.update(delta); 124 | sceneB.render(delta, false); 125 | } else if (transitionParams.transition === 1) { 126 | sceneA.render(delta, false); 127 | sceneB.update(delta); 128 | } else { 129 | // When 0 2 | 3 | 4 | three.js webgl - scenes transition 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 |
17 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { getFXScene } from "./FXScene.js"; 3 | import { getTransition } from "./Transition.js"; 4 | 5 | const clock = new THREE.Clock(); 6 | let transition; 7 | init(); 8 | animate(); 9 | 10 | function init() { 11 | const container = document.getElementById("container"); 12 | 13 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 14 | renderer.setPixelRatio(window.devicePixelRatio); 15 | renderer.setSize(window.innerWidth, window.innerHeight); 16 | container.appendChild(renderer.domElement); 17 | 18 | const materialA = new THREE.MeshBasicMaterial({ 19 | color: 0x00FF00, 20 | wireframe: true 21 | }); 22 | const materialB = new THREE.MeshStandardMaterial({ 23 | color: 0xFF9900, 24 | flatShading: true, 25 | }); 26 | const sceneA = getFXScene({ 27 | renderer, 28 | material: materialA, 29 | clearColor: 0x000000 30 | }); 31 | const sceneB = getFXScene({ 32 | renderer, 33 | material: materialB, 34 | clearColor: 0x000000, 35 | needsAnimatedColor: true, 36 | }); 37 | 38 | transition = getTransition({ renderer, sceneA, sceneB }); 39 | } 40 | 41 | function animate() { 42 | requestAnimationFrame(animate); 43 | transition.render(clock.getDelta()); 44 | } 45 | --------------------------------------------------------------------------------