Render-on-Demand Three.js multiple canvases (with class implementation) - javascript

My case is like this: I have an asset inventory with multiple assets and I want whenever the user hovers on top of them to start the rendering with an OrbitController (I would prefer Trackball but I know that it is not possible due to a bug). The point is that the "this" variable of the class instance can not pass into the render method of the class :
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/build/three.module.js';
import {OrbitControls} from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/examples/jsm/controls/OrbitControls.js';
class Main {
constructor(canvasId)
{
this.canvas = document.querySelector(canvasId);
this.renderer = new THREE.WebGLRenderer({canvas: this.canvas});
this.fov = 75;
this.aspect = 2; // the canvas default
this.near = 0.1;
this.far = 5;
this.camera = new THREE.PerspectiveCamera(this.fov, this.aspect, this.near, this.far);
this.camera.position.z = 2;
this.controls = new OrbitControls(this.camera, this.canvas);
this.controls.target.set(0, 0, 0);
this.controls.update();
this.scene = new THREE.Scene();
this.color = 0xFFFFFF;
this.intensity = 1;
this.light = new THREE.DirectionalLight(this.color, this.intensity);
this.light.position.set(-1, 2, 4);
this.scene.add(this.light);
this.boxWidth = 1;
this.boxHeight = 1;
this.boxDepth = 1;
this.geometry = new THREE.BoxGeometry(this.boxWidth, this.boxHeight, this.boxDepth);
}
makeInstance(geometry, color, x){
let material = new THREE.MeshPhongMaterial(color);
let cube = new THREE.Mesh(geometry, material);
this.scene.add(cube);
cube.position.x = x;
return cube;
}
resizeRendererToDisplaySize(renderer) {
this.canvas = this.renderer.domElement;
let width = this.canvas.clientWidth;
let height = this.canvas.clientHeight;
let needResize = this.canvas.width !== width || this.canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
render() {
// this -> control object or window whereas it should be the class instance
if (this.resizeRendererToDisplaySize(this.renderer)) {
this.canvas = this.renderer.domElement;
this.camera.aspect = this.canvas.clientWidth / this.canvas.clientHeight;
this.camera.updateProjectionMatrix();
}
this.renderer.render(this.scene, this.camera);
}
starter() {
this.makeInstance(this.geometry, 0x44aa88, 0);
this.makeInstance(this.geometry, 0x8844aa, -2);
this.makeInstance(this.geometry, 0xaa8844, 2);
this.render();
this.controls.addEventListener('change', this.render);
window.addEventListener('resize', this.render);
// note: this is a workaround for an OrbitControls issue
// in an iframe. Will remove once the issue is fixed in
// three.js
window.addEventListener('mousedown', (e) => {
e.preventDefault();
window.focus();
});
window.addEventListener('keydown', (e) => {
e.preventDefault();
});
}
}
let main = new Main('#a');
main.starter();
The only thing that I could do to work (but not correctly) is to replace "this" inside the render method with the "main" instance
render() {
if (main.resizeRendererToDisplaySize(this.renderer)) {
main.canvas = main.renderer.domElement;
main.camera.aspect = main.canvas.clientWidth / main.canvas.clientHeight;
main.camera.updateProjectionMatrix();
}
main.renderer.render(main.scene, main.camera);
}
However in this manner I kill the class, and I am not able to generate other instances e.g. main2
I have generate a codepen: https://codepen.io/jimver04/pen/XWjLPLo

You are providing this.render to the event handlers. this.render is technically a pointer to the prototype function (Main.prototype.render).
When the handler is fired, the context will be dependent on how the origin executes the handler. In most cases, the context given to the handler will be that of the origin (or possibly even null). For example, window.addEventListener( 'resize', handler ); will fire resize events by calling the equivalent of handler.call( window, event );
There are a couple ways to get around this.
Arrow Functions implicit this
Arrow functions use the this of the context in which they are defined. So if your arrow function is defined in a member function of your class, and that member function is being executed within a context of an instance of that class, then this within the body of that arrow function will reference the instance.
class Test{
constructor(){
this.name = "Test"
// Arrow function defined within the context of an instance:
window.addEventListener( 'resize', () => {
console.log( this.name ) // prints: "Test"
} )
// Normal function will execute with "window" as its "this"
window.addEventListener( 'resize', function() {
console.log( this.name ) // results may vary based on your browser, but "this" is not an instance of Test
} )
}
}
Binding a function to a context
Using bind can attach a function to a specified context. The bind function returns a new function permanently bound to the provided context.
class Test{
constructor(){
this.name = "Test"
this.boundRender = this.render.bind( this ) // returns "render" bound permanently to "this"
window.addEventListener( 'resize', this.boundRender )
}
render(){
console.log( this.name )
}
}
The example above will still print "Test" to the console. Even though the resize event wants to execute the handler with the window context, the bind has overridden that and locked the handler to executing within the context of the instance to which it was bound.

Thanks TheJim01 ! Below is the final solution with bind methodology that works (the solution with arrows does not work). Also can be found analytically in https://codepen.io/jimver04/pen/XWjLPLo
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/build/three.module.js';
import {OrbitControls} from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/examples/jsm/controls/OrbitControls.js';
class Main {
constructor(canvasId)
{
this.canvas = document.querySelector(canvasId);
this.renderer = new THREE.WebGLRenderer({canvas: this.canvas});
this.fov = 75;
this.aspect = 2; // the canvas default
this.near = 0.1;
this.far = 5;
this.camera = new THREE.PerspectiveCamera(this.fov, this.aspect, this.near, this.far);
this.camera.position.z = 2;
this.controls = new OrbitControls(this.camera, this.canvas);
this.controls.target.set(0, 0, 0);
this.controls.update();
this.scene = new THREE.Scene();
this.color = 0xFFFFFF;
this.intensity = 1;
this.light = new THREE.DirectionalLight(this.color, this.intensity);
this.light.position.set(-1, 2, 4);
this.scene.add(this.light);
this.boxWidth = 1;
this.boxHeight = 1;
this.boxDepth = 1;
this.geometry = new THREE.BoxGeometry(this.boxWidth, this.boxHeight, this.boxDepth);
this.boundRender = this.render.bind( this );
}
makeInstance(geometry, color, x){
let material = new THREE.MeshPhongMaterial(color);
let cube = new THREE.Mesh(geometry, material);
this.scene.add(cube);
cube.position.x = x;
return cube;
}
resizeRendererToDisplaySize(renderer) {
this.canvas = this.renderer.domElement;
let width = this.canvas.clientWidth;
let height = this.canvas.clientHeight;
let needResize = this.canvas.width !== width || this.canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
render() {
if (this.resizeRendererToDisplaySize(this.renderer)) {
this.canvas = this.renderer.domElement;
this.camera.aspect = this.canvas.clientWidth / this.canvas.clientHeight;
this.camera.updateProjectionMatrix();
}
this.renderer.render(this.scene, this.camera);
}
starter() {
this.makeInstance(this.geometry, 0x44aa88, 0);
this.makeInstance(this.geometry, 0x8844aa, -2);
this.makeInstance(this.geometry, 0xaa8844, 2);
this.render();
this.controls.addEventListener('change', this.boundRender );
window.addEventListener('resize', this.boundRender);
// note: this is a workaround for an OrbitControls issue
// in an iframe. Will remove once the issue is fixed in
// three.js
window.addEventListener('mousedown', (e) => {
e.preventDefault();
window.focus();
});
window.addEventListener('keydown', (e) => {
e.preventDefault();
});
}
}
let main = new Main('#a');
main.starter();
let main2 = new Main('#b');
main2.starter();

Related

JS what is the difference of methods defined inside and outside of the constructor function

I have this class object that needs to have its draw function inside the constructor to update the score, if it is outside the score returns undefined.
export class Hud {
constructor(world) {
var self = this;
this.canvas = world.canvas;
this.ctx = world.ctx
this.score = 0;
this.draw = function() {
this.ctx.font = "16px Arial";
this.ctx.fillStyle = "#0095DD";
this.ctx.fillText("Score: " + self.score, 8, 20);
}
}
}
my other class objects have the draw function outside of the constructor like so works fine,
export class Ball {
constructor(world) {
var self = this;
this.canvas = world.canvas;
this.ctx = world.ctx;
self.x = canvas.width / 2;
self.y = canvas.height - 30;
this.ballRadius = 10
this.dx = 2;
this.dy = -2
}
draw() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.ballRadius, 0, Math.PI * 2);
this.ctx.fillStyle = "#0095DD";
this.ctx.fill();
this.ctx.closePath();
}
}
My question is what the difference between the two are. I thought if its defined in the constructor the variables are accessible throughout the class object. I guess I'm confused, should there be functions outside of constructor function? Having everything in the constructor seems to be hassle free.
Function defined in the class (not in the constructor) live on the class prototype which each instance is linked to. Functions defined in the constructor become own properties of each instance. If you defined the function in the constructor, each instance will get its own copy of the function. If you don't the instances will defer to the prototype chain and all the instances will point to the same function. For example:
class Hud {
constructor(world) {
this.name = world
this.draw = function() {
console.log("draw on instance from", this.name)
}
}
draw_outside(){
console.log("draw on class from", this.name )
}
}
let h1 = new Hud('h1')
let h2 = new Hud('h2')
console.log(h1.__proto__.draw) // draw not on prototype
console.log(h1.__proto__.draw_outside) // draw_outside is
console.log(h1.draw === h2.draw) // each object gets its own draw
console.log(h1.draw_outside === h2.draw_outside) // but both point to the same draw_outside
console.log(Object.getOwnPropertyNames(h1)) // only draw & name
// both access `this` the same way when called on an instance:
h1.draw()
h1.draw_outside()

Passing variable to function within class method

I have a class I am attempting to write that looks like the below.
When I run the class, I get an error:
Audio.js:53 Uncaught ReferenceError: bufferLength is not defined
I believe this is because bufferLength is not available in the drawCanvas function.
I have tried adding the this keyword, however this did not work.
How can I make these variables available to functions within a method of this class?
export const LINE_COLORS = ['rgba(255, 23, 204, 0.5)', 'rgba(130, 23, 255, 0.5)'];
// The dimensions of the current viewport
// - Used to set canvas width & height
export const PAGE_DIMENSIONS = {
width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
};
class AudioEngine {
constructor(params) {
this.params = params;
this.audio = new Audio();
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.ctx.createAnalyser();
this.source = this.ctx.createMediaElementSource(this.audio);
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
this.bufferLength = this.analyser.frequencyBinCount;
this.canvas = document.getElementById(params.waveform);
this.onInit();
}
onInit() {
this.ConfigAudio();
this.DrawAudioWave();
}
ConfigAudio = () => {
this.audio.src = this.params.stream;
this.audio.controls = false;
this.audio.autoplay = false;
this.audio.crossOrigin = 'anonymous';
this.analyser.smoothingTimeConstant = 0.6;
this.source.connect(this.ctx.destination);
this.source.connect(this.analyser);
this.analyser.fftSize = 2048;
this.analyser.getByteFrequencyData(this.dataArray);
document.body.appendChild(this.audio);
};
DrawAudioWave = () => {
// Bind to the context
const canvasCtx = this.canvas.getContext('2d');
function drawCanvas() {
// We always start the drawing function by clearing the canvas. Otherwise
// we will be drawing over the previous frames, which gets messy real quick
canvasCtx.clearRect(0, 0, PAGE_DIMENSIONS.width, PAGE_DIMENSIONS.height);
requestAnimationFrame(drawCanvas);
const sliceWidth = (PAGE_DIMENSIONS.width * 1.0) / bufferLength;
// Create a new bounding rectangle to act as our backdrop, and set the
// fill color to black.
canvasCtx.fillStyle = '#000';
canvasCtx.fillRect(0, 0, PAGE_DIMENSIONS.width, PAGE_DIMENSIONS.height);
// Loop over our line colors. This allows us to draw two lines with
// different colors.
LINE_COLORS.forEach((color, index) => {
let x = 0;
// Some basic line width/color config
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = color;
// Start drawing the path
canvasCtx.beginPath();
for (let i = 0; i < bufferLength; i++) {
// We offset using the loops index (stops both colored lines
// from overlapping one another)
const v = dataArray[i] / 120 + index / 20;
// Set the Y point to be half of the screen size (the middle)
const y = (v * PAGE_DIMENSIONS.height) / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height / 2);
canvasCtx.stroke();
});
}
drawCanvas();
};
audioToggle = () => (this.audio.paused ? this.audio.play() : this.audio.pause());
}
export default AudioEngine;
requestAnimationFrame will call drawCanvas with the global context bound, so this.bufferLength will not be defined. Easiest solution is to take advantage of lexical scoping, which arrow functions make easy.
If you look at how you're defining your class's methods, they're using arrow functions. This is precisely to avoid the issue with rebinding this that you're currently having. You should read up on this and scoping in JavaScript to better understand why your code isn't working as you'd expect.
Couple solutions, both require drawCanvas -> this.drawCanvas:
(A) Bind the context of drawCanvas before passing it to requestAnimationFrame:
requestAnimationFrame(drawCanvas.bind(this))
Or (B) take advantange of lexical scoping:
requestAnimationFrame(() => drawCanvas())
"Lexical" scope is scope derived from the "text", i.e. where your arrow function is defined relative to other scopes. Non-lexical scoping uses the caller function to determine bound context.
You can also change the drawCanvas function itself to be bound to the appropriate scope using .bind or changing it to an arrow function.
Further Reading:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
How does the "this" keyword work?
Pass correct "this" context to setTimeout callback? (replace setTimeout with requestAnimationFrame in their examples)

cancelAnimationFrame inside of object method not working

cancelAnimationFrame() does not seem to work when called inside an object's method. I have tried binding the this value to the callback function (as demonstrated on MDN with setTimeout) but I received a TypeError when using cancelAnimationFrame(). I then tried setting the this value to a local variable called _this and called cancelAnimationFrame() again. That time, I did not receive an error but the animation itself is still playing. How do I cancel the animation?
I have recreated the issue I am having below. If you open a console window, you will see that the animation is still running.
function WhyWontItCancel() {
this.canvas = document.createElement("canvas");
this.canvas.width = 200;
this.canvas.height = 10;
document.body.appendChild(this.canvas);
this.draw = this.canvas.getContext("2d");
this.draw.fillStyle = "#f00";
this.position = 0;
};
WhyWontItCancel.prototype.play = function() {
if (this.position <= 190) {
this.draw.clearRect(0, 0, 400, 10);
this.draw.fillRect(this.position, 0, 10, 10);
this.position += 2;
} else {
//window.cancelAnimationFrame(this.animation.bind(this));
var _this = this;
window.cancelAnimationFrame(_this.animation);
console.log("still running");
}
this.animation = window.requestAnimationFrame(this.play.bind(this));
};
var animation = new WhyWontItCancel();
animation.play();
Seems that you miss two things here. First, this.animation = window.requestAnimationFrame(this.play.bind(this)); line is invoked always when play() is called. Contrary to what you might think, cancelAnimationFrame only removes the previously requested RAF call. Strictly speaking, it's not even necessary here. Second, you don't have to bind on each RAF call; you might do it just once:
function AnimatedCanvas() {
this.canvas = document.createElement("canvas");
this.canvas.width = 200;
this.canvas.height = 10;
document.body.appendChild(this.canvas);
this.draw = this.canvas.getContext("2d");
this.draw.fillStyle = "#f00";
this.position = 0;
this.play = this.play.bind(this); // takes `play` from prototype object
};
AnimatedCanvas.prototype.play = function() {
if (this.position <= 190) {
this.draw.clearRect(0, 0, 400, 10);
this.draw.fillRect(this.position, 0, 10, 10);
this.position += 2;
this.animationId = window.requestAnimationFrame(this.play);
}
};
You might want to add cancel into your prototype to be able to stop your animation, for example:
AnimatedCanvas.prototype.cancel = function() {
if (this.animationId) {
window.cancelAnimationFrame(this.animationId);
}
};
... but the point is, it's not useful in the use case described in the question.

Three js keyboard rotation

can anyone help me in making a 3D object rotate on a y axis? with a press of keyboard button? (B)
I've been sitting on this issue for many days now, my code breaks every time i try to do something,
please please please
this is the 3D object that needs turning
https://www.dropbox.com/s/8yefhx3yc11zbik/b.obj?dl=0
and this is the code
var lesson6 = {
scene: null,
camera: null,
renderer: null,
container: null,
controls: null,
clock: null,
stats: null,
init: function() { // Initialization
this.scene = new THREE.Scene();
var SCREEN_WIDTH = window.innerWidth,
SCREEN_HEIGHT = window.innerHeight;
// prepare camera
var VIEW_ANGLE = 15, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 1, FAR = 0;
this.camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR);
this.scene.add(this.camera);
this.camera.position.set(10, 200, 0);
this.camera.lookAt(new THREE.Vector3(0,30,0));
// prepare renderer
this.renderer = new THREE.WebGLRenderer({ antialias:true });
this.renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
this.renderer.shadowMapEnabled = true;
this.renderer.shadowMapSoft = true;
// prepare container
this.container = document.createElement('div');
document.body.appendChild(this.container);
this.container.appendChild(this.renderer.domElement);
// events
THREEx.WindowResize(this.renderer, this.camera);
// prepare controls (OrbitControls)
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.target = new THREE.Vector3(0, 0, 0);
this.controls.maxDistance = 2000;
//keyboard control
// L I G H T S
//bottom
var light = new THREE.DirectionalLight(0xffffff, 1);
light.castShadow = true;
light.shadowCameraVisible = true;
light.shadowCameraNear = 100;
light.shadowCameraFar = 200;
light.shadowCameraLeft = -20; // CHANGED
light.shadowCameraRight = 20; // CHANGED
light.shadowCameraTop = 20; // CHANGED
light.shadowCameraBottom = -20; // CHANGED
light.position.set(180 ,20, 0); // CHANGED
this.scene.add(light);
this.scene.add( new THREE.DirectionalLightHelper(light, 0.2) );
//left
var dlight = new THREE.DirectionalLight(0xffffff, 1);
dlight.castShadow = true;
dlight.shadowCameraVisible = true;
dlight.shadowCameraNear = 100;
dlight.shadowCameraFar = 200;
dlight.shadowCameraLeft = -20; // CHANGED
dlight.shadowCameraRight = 20; // CHANGED
dlight.shadowCameraTop = 20; // CHANGED
dlight.shadowCameraBottom = -20; // CHANGED
dlight.position.set(0, 20, 100); // CHANGED
this.scene.add(dlight);
this.scene.add( new THREE.DirectionalLightHelper(dlight, 0.2) );
// add simple ground
// load a model
this.loadModel();
},
loadModel: function() {
// prepare loader and load the model
var oLoader = new THREE.OBJLoader();
oLoader.load('b.obj', function( geometry ) {
var material = new THREE.MeshLambertMaterial({ color: "red" });
var mesh = new THREE.Mesh (geometry, material);
geometry.traverse( function(child) {
if (child instanceof THREE.Mesh) {
// apply custom material
child.material = material;
child.castShadow = true;
child.receiveShadow = true;
}
});
geometry.rotation.y = -Math.PI / -1.7;
geometry.position.x = 0;
geometry.position.y = 0;
geometry.position.z = 0;
geometry.scale.set(1, 1, 1);
lesson6.scene.add(geometry);
});
}
};
// Animate the scene
function animate() {
requestAnimationFrame(animate);
render();
}
// Render the scene
function render() {
if (lesson6.renderer) {
lesson6.renderer.render(lesson6.scene, lesson6.camera);
}
}
// Initialize lesson on page load
function initializeLesson() {
lesson6.init();
animate();
}
if (window.addEventListener)
window.addEventListener('load', initializeLesson, false);
else if (window.attachEvent)
window.attachEvent('onload', initializeLesson);
else window.onload = initializeLesson;
When dealing with that sort of input, you want the button press to NOT modify any object. You want your keyup/keydown/other input events to do as little as possible: toggle a true/false variable or change a number.
In this example:
function handleKeyDown(event) {
if (event.keyCode === 66) { //66 is "b"
window.isBDown = true;
}
}
function handleKeyUp(event) {
if (event.keyCode === 66) {
window.isBDown = false;
}
}
window.addEventListener('keydown', handleKeyDown, false);
window.addEventListener('keyup', handleKeyUp, false);
From there, you can modify your animate() function to be:
function animate() {
requestAnimationFrame(animate);
if (window.isBDown) {
//Increment your object's Y rotation value a little bit.
//Ideally, you would listen to animate()'s first argument,
//which is a high resolution timestamp that says the current time
//in milliseconds since the tab was opened.
}
render();
}
Note how all the work happens in animate(). Choose the correct speed, and the object will slowly rotate until you release the key. (Once comfortable with this concept, you can also add acceleration and momentum toanimate().)
Gamepad joysticks work the same way, except that instead of true or false, you are given a decimal number. You can multiply this into whatever value you use for "speed". Usually 0 means the joystick is centered, 1 means the joystick is maxed out at that axis, and some number between means input is being delivered, but not all the way (or is being pulled diagonally).

Recursive Call in Javascript Class (requestanimationframe)

I have a simple class that I am using to manage scenes in three.js. I am having issues with the requestAnimationFrame loop finding the function reference. I know I am missing something fundamental here, trapped in some this nightmare. Do I need to use bind or call to pass the this reference to requestAnimationFrame?
var THREE = THREE || {};
var SceneBuddy = SceneBuddy || {};
SceneBuddy = function(scene, camera) {
this.scene = scene;
this.camera = camera;
this.sceneClock = new THREE.Clock();
this.renderer = {};
this.resolution = {};
this.controls = {};
};
//Start Animate
SceneBuddy.prototype.startAnimate = function() {
requestAnimationFrame(this.startAnimate); //this does not work, type error
this.render.call(this);
};
//Render Function
SceneBuddy.prototype.render = function() {
var delta = this.sceneClock.getDelta();
this.controls.update(delta);
this.renderer.render(this.scene,this.camera);
};
//Setup Renderer
SceneBuddy.prototype.initRenderer = function(resolution) {
if (!Detector.webgl) {
Detector.addGetWebGLMessage();
return;
}
else {
var renderer = new THREE.WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true
});
renderer.setSize(resolution.x, resolution.y);
renderer.shadowMapEnabled = true;
this.resolution = resolution;
this.renderer = renderer;
}
};
I am using currently using SceneBuddy like this :
var camera = new THREE.PerspectiveCamera(75, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 100000);
var scene = new THREE.Scene();
var sceneBuddy = new SceneBuddy(scene, camera);
sceneBuddy.initRenderer({x: 940, y: 400});
sceneBuddy.attachRenderer(container); //attaches renderer to dom element
sceneBuddy.initControls();
sceneBuddy.startAnimate(); // broken.
Use bind when passing a function to specify what this will be when the function is called:
SceneBuddy.prototype.startAnimate = function() {
requestAnimationFrame(this.startAnimate.bind(this));
this.render();
};

Categories

Resources