Why the scene don't update?

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?

Looking at you code, it seems the issue is that currentScene is always sceneA since you do not evaluate the isSceneAActive in Transition.render().

1 Like

Simple transition between scenes, from scratch (double click the screen):


Demo: https://codepen.io/prisoner849/full/YzbVrMj

6 Likes