2

I am trying to write an object viewer using threejs and typescript. It is embedded in a react service.

When calling initializing the viewer object and the scene everything works fine. However, after rendering the scene once (one execution of the animate function), the viewer object gets destroyed and I get an error that this is undefined in requestAnimationFrame(this.animate) (cannot read property animate of undefined). Here is my code:

import * as THREE from 'three'

export default class ObjectViewer{
    scene : THREE.Scene;
    camera : THREE.PerspectiveCamera;
    renderer : THREE.WebGLRenderer;

    mesh : THREE.Mesh;

    public constructor(public node : any,
        public width : number = 1100,
        public height: number = 600) {

            this.init();
        }

    protected init() {
        //.. initializing renderer, scene and camera here and adding camera to scene

        this.node.appendChild(this.renderer.domElement);       

        this.camera.position.z = 5;

        this.initTestScene();
    }

    private initTestScene() {
        var geometry = new THREE.BoxGeometry( 1, 1, 1 );
        var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
        this.mesh = new THREE.Mesh( geometry, material );
        this.scene.add( this.mesh );
    }

    private animate() {
        requestAnimationFrame(this.animate);
        this.mesh.rotation.x += 0.1;
        this.renderer.render( this.scene, this.camera );
    }

    public render() {
        this.animate();
    }
}

I initialized the green rotating cube from the live example of threejs without lighting. The result if I add an if(this) around the animate block is the green cube rotated once: rendered cube

Code in animate:

if(this){
    requestAnimationFrame(this.animate);
    this.mesh.rotation.x += 0.1;
    this.renderer.render( this.scene, this.camera );
}

Am I doing something wrong in the renderer or does it seem to be a problem at a higher level (e.g. the object gets destroyed by the react code wrapping it)?

To give some more context: I have a wrapper managing the actual viewer and making it available to the surrounding react environment:

type Props = {
    width?: number,
    height?: number,
};

export default class ObjectViewerWrapper extends React.Component<Props, {}> {
    node : HTMLDivElement | null;
    viewer : ObjectViewer;

    constructor(props : Props) {
        super(props);

        this.node = null;
    }

    componentDidMount() {
        this.viewer = new ObjectViewer(this.node, this.props.width, this.props.height);
        this.forceUpdate();
    }

    componentDidUpdate(){
        if(this.viewer) {
            this.viewer.render();
        }
    }

    render() {
        return(
            <div style={{"height": "100%", "width": "100%", "position": "relative"}} ref={ inst => { this.node = inst } }/>
        );
    }
}

2 Answers 2

1

I get an error that this is undefined in requestAnimationFrame(this.animate)

You have to bind function this.animate = this.animate.bind(this);

Working example

const { useState, useEffect, Component, createRef } = React

class ObjectViewer {
    
  constructor(node, width = 1100, height = 600) {
    this.node = node;
    this.width = width;
    this.height = height;
    this.requestAnimationFrameHandle = null;
    this.animate = this.animate.bind(this);
    this.init();
  }

  init() {
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera( 45, this.width / this.height, 1, 1000 )
    this.renderer = new THREE.WebGLRenderer();
    const element = this.node.current;
    element.appendChild(this.renderer.domElement);
    this.renderer.setSize(this.width, this.height);
    this.camera.position.z = 5;
    this.initTestScene();
    this.render();
  }
  
  destroy() {
    if(this.requestAnimationFrameHandle) {
      cancelAnimationFrame(this.requestAnimationFrameHandle)
    }
    
    this.node = null;
    this.camera = null;
    this.scene = null;
    this.mesh = null;
  }

  initTestScene() {
    var geometry = new THREE.BoxGeometry( 1, 1, 1 );
    var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
    this.mesh = new THREE.Mesh( geometry, material );
    this.scene.add( this.mesh );
  }

  animate() {
    this.requestAnimationFrameHandle = requestAnimationFrame(this.animate);
    this.mesh.rotation.x += 0.1;
    this.renderer.render( this.scene, this.camera );
  }

  render() {
    this.animate();
  }
}

class ObjectViewerWrapper extends Component {
   
  constructor(props) {
    super(props);

    this.node = createRef();
  }

  componentDidMount() {
      const { width, height } = this.props;
      
      this.viewer = new ObjectViewer(this.node, width, height);
      this.forceUpdate();
  }
  
  componentWillUnmount() {
    this.viewer.destroy();
  }

  componentDidUpdate() {
      if(this.viewer) {
          this.viewer.render();
      }
  }

  render() {
    const style = {
      "height": "100%",
      "width": "100%",
      "position": "relative"
    }
  
    return(<div style={style} ref={this.node}/>);
  }
}

const App = () => {
  const [dimension, setDimension] = useState(null);

  useEffect(() => {
    setDimension({
      width: window.innerWidth,
      height: window.innerHeight
    })
  }, [])
  
  if(!dimension) {
    return <span>Loading...</span>
  }
  
  const { width, height } = dimension;

  return <ObjectViewerWrapper width={width} height={height}/>
}

ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
html, body {
  margin: 0;
  height: 100%;
}

#root {
  height: 100%;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r99/three.js"></script>
<div id="root"></div>

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you, works perfectly. Why does my answer I posted below work?
Its because you saved this context to viewer. when you use function(){} this changes and you have to either use bind or save context to variable
0

Found the problem and a fix, even though I don't know why it did not work earlier. I had to define the animate function inside the render function:

public render() {
    let viewer = this;
    function animate() {
        requestAnimationFrame(animate);
        // animation stuff
        viewer.renderer.render( viewer.scene, viewer.camera );
    }
    animate();
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.