Home > Mobile >  Rendering with Three.js in vscode-extension
Rendering with Three.js in vscode-extension

Time:03-02

I am creating a vscode extension where I want to visualize 3D meshes. Below the function where I get the HTML content:

function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel) {
      const threejs =  vscode.Uri.file(context.extensionPath "/js/three.js");  
      const threejsUri = panel.webview.asWebviewUri(threejs);
      const geometryjs =  vscode.Uri.file(context.extensionPath "/js/geometry.js");  
      const geometryjsUri = panel.webview.asWebviewUri(geometryjs);
      return `
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title>My first three.js app</title>
          <style>
            body { margin: 0; }
          </style>
        </head>
        <body>
          <script src=${threejsUri}></script>
          <script src=${geometryjsUri}></script>
        </body>
      </html>
      `
}

Now if my geometry.js is

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

function animate() {
    requestAnimationFrame( animate );

    cube.rotation.x  = 0.01;
    cube.rotation.y  = 0.01;

    renderer.render( scene, camera );
};
animate();

it works as expected. But if I make a class out of it:

import { Scene, PerspectiveCamera, WebGLRenderer, BoxGeometry, MeshBasicMaterial, Mesh, requestAnimationFrame } from 'three';
class Viewer {
    constructor() {
        this.scene = new Scene();
        this.camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
    
        this.renderer = new WebGLRenderer();
        this.renderer.setSize( window.innerWidth, window.innerHeight );
        document.body.appendChild( renderer.domElement );
    
        this.geometry = new BoxGeometry();
        this.material = new MeshBasicMaterial( { color: 0x00ff00 } );
        this.cube = new Mesh( this.geometry, this.material );
        this.scene.add( cube );
    
        this.camera.position.z = 5;
    }
    
    animate() {
        requestAnimationFrame( this.animate );
    
        this.cube.rotation.x  = 0.01;
        this.cube.rotation.y  = 0.01;
    
        this.renderer.render( this.scene, this.camera );
    }
}

let viewer = new Viewer();
viewer.animate();

the HTML page is empty. What am I doing wrong here? I depend on an external package for rendering which uses classes, so I need to understand how to properly create and use these classes here.

Edit: I have changed geometry.js to typescript, but still no luck

import { Scene, PerspectiveCamera, WebGLRenderer, BoxGeometry, MeshBasicMaterial, Mesh } from 'three';

export class Viewer {
    private scene!: Scene;
    private camera: PerspectiveCamera;
    private renderer: WebGLRenderer;
    private geometry: BoxGeometry;
    private material: MeshBasicMaterial;
    private cube: Mesh;

    constructor(document: Document) {
        this.scene = new Scene();
        this.camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
    
        this.renderer = new WebGLRenderer();
        this.renderer.setSize( window.innerWidth, window.innerHeight );
        document.body.appendChild( this.renderer.domElement );

        this.geometry = new BoxGeometry();
        this.material = new MeshBasicMaterial( { color: 0x00ff00 } );
        this.cube = new Mesh( this.geometry, this.material );
        this.scene.add( this.cube );
    
        this.camera.position.z = 5;
        this.animate();
    }
    
    public animate() {
        requestAnimationFrame( this.animate );
    
        this.cube.rotation.x  = 0.01;
        this.cube.rotation.y  = 0.01;
    
        this.renderer.render( this.scene, this.camera );
    }
}

new Viewer(document);

Edit 2: Oddly, something like this works

function init() {
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );
    document.body.appendChild( renderer.domElement );

    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
    const cube = new THREE.Mesh( geometry, material );
    scene.add( cube );

    camera.position.z = 5;

    function animate() {
        requestAnimationFrame( animate );
    
        cube.rotation.x  = 0.01;
        cube.rotation.y  = 0.01;
    
        renderer.render( scene, camera );
    };

    animate();
}

window.addEventListener('message', (event) => {
    init();
});

I don't understand what's happening here. Why does the animate() function has to be inside init()? In a class, isn't this achieved by using this for the members?

Edit 3:

class Viewer {
    constructor() {
        this.init();
    }

    init() {
        this.scene = new THREE.Scene();
        this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
    
        this.renderer = new THREE.WebGLRenderer();
        this.renderer.setSize( window.innerWidth, window.innerHeight );
        document.body.appendChild( renderer.domElement );
    
        var geometry = new THREE.BoxGeometry();
        var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
        this.cube = new THREE.Mesh( geometry, material );
        this.scene.add( cube );
    
        this.camera.position.z = 5;
    
        this.boundAnimate = this.animate.bind(this);
        this.animate(); // this.boundAnimate();
    }
    
    animate() {
        requestAnimationFrame(this.boundAnimate);
    
        this.cube.rotation.x  = 0.01;
        this.cube.rotation.y  = 0.01;
    
        this.renderer.render( this.scene, this.camera );
    }
}

let viewer = new Viewer();

CodePudding user response:

requestAnimationFrame tries to call the provided function under the context of the window object (this === window).

When you run your code in a functional way (your original and edit 2 methods), some of the objects are scoped such that you still have access to them inside the animate function. But when you use the class, and requestAnimationFrame calls this.animate, it will work the first time, but the second time your this becomes window and your program has flown off the rails.

To correct this, I usually bind the function to a context to prevent an override. You can also call it directly and provide the context you want to use.

Using function binding

class MyClass{
  constructor(){
    this.boundAnimate = this.animate.bind(this);
  }

  animate(){
    requestAnimationFrame(this.boundAnimate);
  }
};

Using call

class MyClass{
  animate(){
    window.prototype.requestAnimationFrame.call(this, this.animate);
  }
};
  • Related