Getting double render by error

The glb model is getting render twice and i have no clue why, has anyone been here?

This is model

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

export function loadGLTFModel(
  scene,
  glbPath,
  options = { receiveShadow: true, castShadow: true }
) {
  const { receiveShadow, castShadow } = options;
  return new Promise((resolve, reject) => {
    const loader = new GLTFLoader();

    loader.load(
      glbPath,
      (gltf) => {
        const obj = gltf.scene;
        obj.name = "dog";
        obj.position.y = 0;
        obj.position.x = 0;
        obj.receiveShadow = receiveShadow;
        obj.castShadow = castShadow;
        scene.add(obj);

        obj.traverse(function (child) {
          if (child.isMesh) {
            child.castShadow = castShadow;
            child.receiveShadow = receiveShadow;
          }
        });
        resolve(obj);
      },
      undefined,
      function (error) {
        reject(error);
      }
    );
  });
}

This is the component

import { useState, useEffect, useRef, useCallback } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { loadGLTFModel } from "./model";


function easeOutCirc(x) {
  return Math.sqrt(1 - Math.pow(x - 1, 4));
}

const VoxelDog = () => {
  const refContainer = useRef();
  const [loading, setLoading] = useState(true);
  const [renderer, setRenderer] = useState();
  const [_camera, setCamera] = useState();
  const [target] = useState(new THREE.Vector3(-0.5, 1.2, 0));
  const [initialCameraPosition] = useState(
    new THREE.Vector3(
      20 * Math.sin(0.2 * Math.PI),
      10,
      20 * Math.cos(0.2 * Math.PI)
    )
  );
  const [scene] = useState(new THREE.Scene());
  const [_controls, setControls] = useState();

  const handleWindowResize = useCallback(() => {
    const { current: container } = refContainer;
    if (container && renderer) {
      const scW = container.clientWidth;
      const scH = container.clientHeight;

      renderer.setSize(scW, scH);
    }
  }, [renderer]);

  /* eslint-disable react-hooks/exhaustive-deps */
  useEffect(() => {
    const { current: container } = refContainer;
    if (container && !renderer) {
      const scW = container.clientWidth;
      const scH = container.clientHeight;

      const renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(scW, scH);
      renderer.outputEncoding = THREE.sRGBEncoding;
      container.appendChild(renderer.domElement);
      setRenderer(renderer);

      // 640 -> 240
      // 8   -> 6
      const scale = scH * 0.001 + 4.8;
      const camera = new THREE.OrthographicCamera(
        -scale,
        scale,
        scale,
        -scale,
        0.01,
        50000
      );
      camera.position.copy(initialCameraPosition);
      camera.lookAt(target);
      setCamera(camera);

      const ambientLight = new THREE.AmbientLight(0xcccccc, 1);
      scene.add(ambientLight);

      const controls = new OrbitControls(camera, renderer.domElement);
      controls.autoRotate = true;
      controls.target = target;
      setControls(controls);

      loadGLTFModel(scene, "/dog.glb", {
        receiveShadow: false,
        castShadow: false,
      }).then(() => {
        animate();
        setLoading(false);
      });

      let req = null;
      let frame = 0;
      const animate = () => {
        req = requestAnimationFrame(animate);

        frame = frame <= 100 ? frame + 1 : frame;

        if (frame <= 100) {
          const p = initialCameraPosition;
          const rotSpeed = -easeOutCirc(frame / 120) * Math.PI * 20;

          camera.position.y = 20;
          camera.position.x =
            p.x * Math.cos(rotSpeed) + p.z * Math.sin(rotSpeed);
          camera.position.z =
            p.z * Math.cos(rotSpeed) - p.x * Math.sin(rotSpeed);
          camera.lookAt(target);
        } else {
          controls.update();
        }

        renderer.render(scene, camera);
      };

      return () => {
        console.log("unmount");
        cancelAnimationFrame(req);
        renderer.dispose();
      };
    }
  }, []);

  useEffect(() => {
    window.addEventListener("resize", handleWindowResize, false);
    return () => {
      window.removeEventListener("resize", handleWindowResize, false);
    };
  }, [renderer, handleWindowResize]);

  return (
    <div
      style={{ height: "400px", width: "540px", position: "" }}
      ref={refContainer}
    >
      {loading && (
        <span style={{ position: "absolute", left: "50%", top: "50%" }}>
          Loading...
        </span>
      )}
    </div>
  );
};

export default VoxelDog;

and here i declare it just once!

<Box
        as="main"
        style={{}}
        pb={8}
        className="d-flex justify-content-center"
      >
        <Container style={{}} maxW="container.md" pt={14}>
          <VoxelDog />
        </Container>
      </Box>

Any idea? thanks so much!

For the loading twice, I expect your useEffect is running multiple times (I think in dev mode for react, useEffects can run twice).

Have you looked into using React three fiber?

Instead of wrapping loading in a promise you can now use GLTFLoader.loadAsync. You can see it in action here

https://threejs.org/examples/?q=tonem#webgl_tonemapping

this seems like a lot of trouble, even the loading, all of this is part of react (via suspense) but it’s circumventing it by creating a black box that runs aside it.

other than that there are subtle mistakes, useState(new THREE.Scene()) will re-create a scene every render, doesn’t hurt but just wastes memory. should be useState(() => new …) container.appendChild isn’t mirrored with a removeChild in the cleanup phase so this will create a leak. loadGLTFModel > then > animate seems like a race condition to me, what if the component unmounts while it’s loading, will most likely crash.

1 Like

Hi there! I am also facing the same problem :((( I can’t capture the entire length of screen, but basically its repeating the whole thing (the cube and the background) at the bottom although I only called it once… Would really appreciate some help resolving this issue!

function init() {
			const aspect = window.innerWidth / window.innerHeight;
			camera = new THREE.OrthographicCamera( frustumSize * aspect / - 2, frustumSize * aspect / 2, frustumSize / 2, frustumSize / - 2, 1, 1000 );
			camera.position.set( - 200, 200, 200 );
			scene = new THREE.Scene();
			scene.background = null;
			scene2 = new THREE.Scene();
            

			// left
			createPlane(
				100, 100,
			"transparent",
				new THREE.Vector3( - 50, 0, 0 ),
				new THREE.Euler( 0, - 90 * THREE.MathUtils.DEG2RAD, 0 ),
                   require("../assets/mapped_images/cplain.png"),
                 require("../assets/mapped_images/white.png"),
                "MSCP L8<br>Parklane Shopping Mall",
                 "center",);

			// right
			createPlane(
				100, 100,
	            "transparent",
				new THREE.Vector3( 0, 0, 50 ),
				new THREE.Euler( 0, 0, 0 ),
                 require("../assets/mapped_images/bplain.png"),
                 require("../assets/mapped_images/white.png"),
                "Countdown",
                 "center", );

			// top
			createPlane(
				100, 100,
			"white",
				new THREE.Vector3( 0, 50, 0 ),
				new THREE.Euler( - 90 * THREE.MathUtils.DEG2RAD, 0, 0 ),
                   require("../assets/mapped_images/a.png"),
                require("../assets/mapped_images/a_hover.png"),
                " ", //title alr on image
                 "right",);

			// bottom
			createPlane(
				100, 100,
			"transparent",
				new THREE.Vector3( 0, - 50, 0 ),
				new THREE.Euler( - 90 * THREE.MathUtils.DEG2RAD, 0, 0 ),
                  require("../assets/mapped_images/f.png"),
                require("../assets/mapped_images/f.png"),//bottom so no text needed
                " ",
                 "center",);

			// front
			createPlane(
				100, 100,
			"transparent",
				new THREE.Vector3( 50, 0, 0 ),
				new THREE.Euler(  0,  90 * THREE.MathUtils.DEG2RAD, 0 ),
                  require("../assets/mapped_images/eplain.png"),
                 require("../assets/mapped_images/white.png"),
                "Projects",
                "center",);
            
            // back
			createPlane(
				100, 100,
				"transparent",
				new THREE.Vector3( 0, 0, -50 ),
				new THREE.Euler( 0, -180 * THREE.MathUtils.DEG2RAD, 0 ),
                require("../assets/mapped_images/dplain.png"), 
                require("../assets/mapped_images/white.png"),
                "People",
                "center",);

			renderer.setPixelRatio( window.devicePixelRatio );
			renderer.setSize( window.innerWidth, window.innerHeight );
            renderer.setClearColor( 0x000000, 0 ); 
			document.body.appendChild( renderer.domElement );
			renderer2 = new CSS3DRenderer();
			renderer2.setSize( window.innerWidth, window.innerHeight );
			renderer2.domElement.style.position = 'absolute';
			document.body.appendChild( renderer2.domElement );

			const controls = new OrbitControls( camera, renderer2.domElement );
labelRenderer = new CSS2DRenderer();
            labelRenderer.setSize( window.innerWidth, window.innerHeight );
            labelRenderer.domElement.style.position = 'absolute';
              document.body.appendChild( labelRenderer.domElement );
			function createPlane( width, height, cssColor, pos, rot, backgroundImageUrl, newImageUrl, text, alignment) {

				const element = document.createElement( 'div' );
				element.style.width = width + 'px';
				element.style.height = height + 'px';
				element.style.opacity = 1;
				element.style.background = cssColor;
                element.style.backgroundImage = `url(${backgroundImageUrl})`;
                 element.style.backgroundSize = "cover";
                element.innerHTML = text;
                element.classList.add("plane-text-class");
                element.style.textAlign = alignment;
                element.addEventListener("mouseover", function() {
                    element.style.backgroundImage = `url(${newImageUrl})`;
                    });
                element.addEventListener("mouseout", function() {
                    element.style.backgroundImage = `url(${backgroundImageUrl})`;
                    });
if (text === "Countdown") {
const countdownElement = document.createElement('div');
setInterval(function() {
countdownElement.innerHTML = document.getElementById("timer").innerHTML;
element.innerHTML = countdownElement.innerHTML;
}, 1000);

} else {
element.innerHTML = text;
}
                               
function countdown(){
var countDownDate = new Date("6 Mar 2023 00:00:00").getTime();
var x = setInterval(function() {
  var now = new Date().getTime();
  var distance = countDownDate - now;
  var days = Math.floor(distance / (1000 * 60 * 60 * 24));
  var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
  var seconds = Math.floor((distance % (1000 * 60)) / 1000);
  document.getElementById("timer").innerHTML = days + "days " + hours + "hours "
  + minutes + "mins " + seconds + "secs ";
  if (distance < 0) {
    clearInterval(x);
    document.getElementById("timer").innerHTML = "6-31 March 2023";
  }
}, 1000);} countdown();
                
            

  const object = new CSS3DObject( element );
  object.position.copy( pos );
  object.rotation.copy( rot );
  scene2.add( object );

            
  if (element.style.textAlign != "center") { 
    const objDiv = document.createElement( 'div' );
    objDiv.className = 'label';
    objDiv.textContent = '💡 Drag to rotate me! ';
    const objectLabel = new CSS2DObject( objDiv );
    objectLabel.position.set( 50, 55, 5);
    object.add( objectLabel );
      
      function disappear(){
    object.remove(objectLabel);}
  setInterval(() => {
  disappear();
}, 10000);
  }
       
                }window.addEventListener( 'resize', onWindowResize );
			}

			function onWindowResize() {
				const aspect = window.innerWidth / window.innerHeight;
				camera.left = - frustumSize * aspect / 2;
				camera.right = frustumSize * aspect / 2;
				camera.top = frustumSize / 2;
				camera.bottom = - frustumSize / 2;
				camera.updateProjectionMatrix();
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer2.setSize( window.innerWidth, window.innerHeight );
                   labelRenderer.setSize( window.innerWidth, window.innerHeight );
			}

      
			function animate() {
				requestAnimationFrame( animate ); 
                camera.rotation.z +=0.005;
                renderer.render( scene, camera );
				renderer2.render( scene2, camera );
                labelRenderer.render( scene2, camera );
                
			}

I solved removing the strict mode in the main.jsx, i know it’s late but maybe still useful for some (: