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().

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


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