I’m learning Three.js and I want to know how to create scene transition using click events as a trigger. Like in this project: https://ohzi.io/
I used this project made by Mr. Robot as a base to create something close to what I want: GitHub - bobbyroe/transition-effect
I can achieve the transition using a click event, but I’m having a bug where I see the new scene with the new material and it quickly renders back the previous material. In this case, materialB always appears.
Setup:
HTML + Style
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - scenes transition</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
margin: 0;
}
#container { width: 100%; height: 100%; }
button { position: absolute; z-index: 100; }
#buttonA { top: 10px; left: 10px; }
#buttonB { top: 10px; left: 100px; }
</style>
</head>
<body>
<div id="container"></div>
<button id="buttonA">Scene A</button>
<button id="buttonB">Scene B</button>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.131/build/three.module.js"
}
}
</script>
<script type="module" src="./index.js"></script>
</body>
</html>
index.js:
import * as THREE from "three";
import { getFXScene } from "./FXScene.js";
import { getTransition } from "./Transition.js";
const clock = new THREE.Clock();
let transition;
let isSceneAActive = true; // Boolean to track if scene A is active
init();
animate();
function init() {
const container = document.getElementById("container");
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
const materialA = new THREE.MeshBasicMaterial({
color: 0x00FF00,
wireframe: true,
});
const materialB = new THREE.MeshStandardMaterial({
color: 0xFF9900,
flatShading: true,
});
const sceneA = getFXScene({
renderer,
material: materialA,
clearColor: 0x000000,
});
const sceneB = getFXScene({
renderer,
material: materialB,
clearColor: 0x000000,
needsAnimatedColor: true,
});
transition = getTransition({ renderer, sceneA, sceneB });
// Configure the buttons
document.getElementById('buttonA').addEventListener('click', () => {
startTransition(sceneA);
isSceneAActive = true;
});
document.getElementById('buttonB').addEventListener('click', () => {
startTransition(sceneB);
isSceneAActive = false;
});
}
function startTransition(targetScene) {
transition.startTransition(targetScene);
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
transition.render(delta, isSceneAActive);
}
FXScene.js:
import * as THREE from "three";
const objCount = 5000;
function getMeshProps() {
const arr = [];
for (let i = 0; i < objCount; i += 1) {
arr.push(
{
position: {
x: Math.random() * 10000 - 5000,
y: Math.random() * 6000 - 3000,
z: Math.random() * 8000 - 4000
},
rotation: {
x: Math.random() * 2 * Math.PI,
y: Math.random() * 2 * Math.PI,
z: Math.random() * 2 * Math.PI,
},
scale: Math.random() * 200 + 100
}
)
}
return arr;
}
const dummyProps = getMeshProps();
function getMesh(material, needsAnimatedColor = false) {
const size = 0.25;
const geometry = new THREE.IcosahedronGeometry(size, 1);
const mesh = new THREE.InstancedMesh(geometry, material, objCount);
const dummy = new THREE.Object3D();
const color = new THREE.Color();
let props;
for (let i = 0; i < objCount; i++) {
props = dummyProps[i];
dummy.position.x = props.position.x;
dummy.position.y = props.position.y;
dummy.position.z = props.position.z;
dummy.rotation.x = props.rotation.x;
dummy.rotation.y = props.rotation.y;
dummy.rotation.z = props.rotation.z;
dummy.scale.set(props.scale, props.scale, props.scale);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
if (needsAnimatedColor) { mesh.setColorAt(i, color.setScalar(0.1 + 0.9 * Math.random())); }
}
return mesh;
}
export function getFXScene({ renderer, material, clearColor, needsAnimatedColor = false }) {
const w = window.innerWidth;
const h = window.innerHeight;
const camera = new THREE.PerspectiveCamera( 50, w / h, 1, 10000);
camera.position.z = 2000;
// Setup scene
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(clearColor, 0.0002);
scene.add(new THREE.HemisphereLight(0xffffff, 0x555555, 1.0));
const mesh = getMesh(material, needsAnimatedColor);
scene.add(mesh);
const fbo = new THREE.WebGLRenderTarget(w, h);
const rotationSpeed = new THREE.Vector3(0.1, -0.2, 0.15);
const update = (delta) => {
mesh.rotation.x += delta * rotationSpeed.x;
mesh.rotation.y += delta * rotationSpeed.y;
mesh.rotation.z += delta * rotationSpeed.z;
if (needsAnimatedColor) {
material.color.setHSL(0.1 + 0.5 * Math.sin(0.0002 * Date.now()), 1, 0.5);
}
}
const render = (delta, rtt) => {
update(delta);
renderer.setClearColor(clearColor);
if (rtt) {
renderer.setRenderTarget(fbo);
renderer.clear();
renderer.render(scene, camera);
} else {
renderer.setRenderTarget(null);
renderer.render(scene, camera);
}
};
return { fbo, render, update };
};
Transition.js:
import * as THREE from "three";
import { TWEEN } from "https://cdn.jsdelivr.net/npm/three@0.131/examples/jsm/libs/tween.module.min.js";
const transitionParams = {
transition: 0,
texture: 5,
cycle: true,
animate: true,
};
export function getTransition({ renderer, sceneA, sceneB }) {
const scene = new THREE.Scene();
const w = window.innerWidth;
const h = window.innerHeight;
const camera = new THREE.OrthographicCamera(w / -2, w / 2, h / 2, h / -2, -10, 10);
const textures = [];
const loader = new THREE.TextureLoader();
for (let i = 0; i < 3; i++) {
textures[i] = loader.load(`./img/transition${i}.png`);
}
const material = new THREE.ShaderMaterial({
uniforms: {
tDiffuse1: { value: null },
tDiffuse2: { value: null },
mixRatio: { value: 0.0 },
threshold: { value: 0.1 },
useTexture: { value: 1 },
tMixTexture: { value: textures[0] },
},
vertexShader: `varying vec2 vUv;
void main() {
vUv = vec2( uv.x, uv.y );
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: `
uniform float mixRatio;
uniform sampler2D tDiffuse1;
uniform sampler2D tDiffuse2;
uniform sampler2D tMixTexture;
uniform int useTexture;
uniform float threshold;
varying vec2 vUv;
void main() {
vec4 texel1 = texture2D( tDiffuse1, vUv );
vec4 texel2 = texture2D( tDiffuse2, vUv );
if (useTexture == 1) {
vec4 transitionTexel = texture2D( tMixTexture, vUv );
float r = mixRatio * (1.0 + threshold * 2.0) - threshold;
float mixf = clamp((transitionTexel.r - r) * (1.0 / threshold), 0.0, 1.0);
gl_FragColor = mix(texel1, texel2, mixf);
} else {
gl_FragColor = mix(texel2, texel1, mixRatio);
}
}`,
});
const geometry = new THREE.PlaneGeometry(w, h);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
let currentScene = sceneA;
function startTransition(targetScene) {
if (targetScene === sceneA) {
material.uniforms.tDiffuse1.value = sceneB.fbo.texture;
material.uniforms.tDiffuse2.value = sceneA.fbo.texture;
} else {
material.uniforms.tDiffuse1.value = sceneA.fbo.texture;
material.uniforms.tDiffuse2.value = sceneB.fbo.texture;
}
new TWEEN.Tween(transitionParams)
.to({ transition: 1 }, 1000) // duração da transição
.onComplete(() => {
transitionParams.transition = 0;
})
.start();
}
const render = (delta) => {
TWEEN.update();
material.uniforms.mixRatio.value = transitionParams.transition;
if (transitionParams.transition === 0) {
currentScene.update(delta);
currentScene.render(delta, false);
} else {
sceneA.render(delta, true);
sceneB.render(delta, true);
renderer.setRenderTarget(null);
renderer.render(scene, camera);
}
};
return { render, startTransition };
}
I made a repository on git if you want to clone the project:
Can anyone help me please?