So I'm coming from two years of teaching myself unity/c# and am starting to feel like I have the coding chops to move from an engine to a framework so I have more control over the environment. I've completed the first few steps of first person environment stuff and wanted to split up my code into seperate files. In c# this is very easy, you just include a:
class className = new className();
for whatever class is in the same folder, but it seems to be less easy in javascript to accomplish this. My attempts so far have led to me losing all three.js functionality.
//Edit://This is what I tried from my knowledge of c#:
initializeObjects(scene);// on the native html javascript function
function initializeObjects(scene){ code referring to 3js scene object}// on the satellite scripts
but it isn't reacting the way I'd imagine(as in no errors, but no functionality either). I need to figure out how to write to console on a website build. This is all new to me.
is the answer:
var currentMesh = mesh;//?
I don't have time to test it right now.
//end Edit//
Any tips? I will include all code below.
var mesh, floorMesh;
function initializeObjects(scene){
mesh = new THREE.Mesh(
new THREE.BoxGeometry(2,2,2, 4, 4, 4),
new THREE.MeshPhongMaterial({color:'green', wireframe:false})
);
mesh.position.y = 2;
mesh.receiveShadow = true;
mesh.castShadow = true;
scene.add(mesh);
floorMesh = new THREE.Mesh(
new THREE.PlaneGeometry(100, 100, 10, 10),
new THREE.MeshPhongMaterial({color:'grey', wireframe:false})
);
floorMesh.rotation.x -= Math.PI /2;
floorMesh.receiveShadow = true;
scene.add(floorMesh);
ambientLight = new THREE.AmbientLight('blue', .3);
scene.add(ambientLight);
light = new THREE.PointLight('white', 0.8, 18);
light.position.set(-3, 6, -3);
light.castShadow = true;
light.shadow.camera.near = 0.1;
light.shadow.camera.far = 25;
light.shadowMapHeight = 2048;
light.shadowMapWidth = 2048;
scene.add(ambientLight);
}
function objectUpdate(){
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
}
//this is the start of the first person script
var keyboard = {};
var player = { height:1.8, speed:1, turnSpeed:Math.PI * 0.02 };
var camera;
function initializeControls(scene){
camera = new THREE.PerspectiveCamera (90, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, player.height,-4);
camera.lookAt(new THREE.Vector3(0,player.height,0));
scene.add(camera);
}
function checkInput(){
if(keyboard[87]){
camera.position.x -= Math.sin(camera.rotation.y) * player.speed;
camera.position.z -= -Math.cos(camera.rotation.y) * player.speed;
}
if(keyboard[83]){
camera.position.x += Math.sin(camera.rotation.y) * player.speed;
camera.position.z += -Math.cos(camera.rotation.y) * player.speed;
}
if(keyboard[65]){
camera.position.x += Math.sin(camera.rotation.y + Math.PI/2) * player.speed;
camera.position.z += -Math.cos(camera.rotation.y + Math.PI/2) * player.speed;
}
if(keyboard[68]){
camera.position.x += Math.sin(camera.rotation.y - Math.PI/2) * player.speed;
camera.position.z += -Math.cos(camera.rotation.y - Math.PI/2) * player.speed;
}
if(keyboard[37]){
camera.rotation.y -= player.turnSpeed;
}
if(keyboard[39]){
camera.rotation.y += player.turnSpeed;
}
}
function keyDown(event){
keyboard[event.keyCode] = true;
}
function keyUp(event){
keyboard[event.keyCode] = false;
}
window.addEventListener('keydown', keyDown);
window.addEventListener('keyup', keyUp);
<script src="https://github.com/mrdoob/three.js/blob/dev/build/three.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<title>Demo 1</title>
<meta charset="UTF-8">
<style>
body {
background-color: #000000;
margin: 0px;
overflow: hidden;
}
a {
color:#0078ff;
}
</style>
<script type="text/javascript" src = "https://github.com/mrdoob/three.js/blob/dev/build/three.min.js"></script>
<script type="text/javascript" src = "objectManager.js"></script>
<script type="text/javascript" src = "firstPersonController.js"></script>
</head>
<body>
</body>
<script>
var scene, renderer;
function init(){
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
initializeControls(scene);
initializeObjects(scene);
renderer.shadowMap.enabled = true;
renderer.shadowMapSoft = true;
renderer.shadowMapType = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
updateRenderer();
}
function updateRenderer(){
requestAnimationFrame(updateRenderer);
checkInput();
objectUpdate();
}
window.onload = init();
</script>
</html>
JavaScript does not (yet) have an implemented native module system. Its been ratified, but not implemented. Leaving you a few different options:
Option 1: Global namespace variables, aka the Revealing Module Pattern:
This is a pretty common approach. Looks like this in code:
// top-level name declaration
window.myNamespace = (function() {
// internal implementation stuff goes here
return {
// public API goes here, for example
fooMethod: function (n) {
return n + 'foo';
}
};
})();
Then in another file you can just say
// remember myNamespace is a global variable.
myNamespace.fooMethod('bar'); // 'barfoo'
This has a couple of disadvantages though. First, if you're using a lot of third-party libraries there's a pretty good chance of a name collision. This is particularly true if the code modifies a global object like Array or Object. It also requires a fair amount of effort from the developer to maintain.
Option 2 Third party module system. See this for a more in-depth discussion.
In Js only functions create a new scope, but global stuff is available everywhere. Classes and inheritance is different to most other languages.
Tutorials: scoping,
Prototypal inheritance.
Google will give you more infos.
By the way, you defined var keyboard = {}; and later used keyboard as array.
So ultimately what I did (this is contingent on the website thing I think, I wouldn't know how javascript compiles besides a website at this point) is put all of the variables on the first loaded script. It's definitely confusing to think about as a c# coder, but that was the thing that made everything work, and then from there I just broke up the code exactly as it was written already and placed it on separate files. It's working like a charm and I'm ready to start working through more functionality. Wanted to mark one of the answer's right, but there was a few extra steps I needed to take with the information I was given. Thanks everyone for your help.
Related
I apologize in advance if I'm missing something obvious here.
I'm trying to use the three.js VOXLoader while using Flask.
I'm able to load three.js fine, but when I try and load the VOXLoader, it doesn't seem to work and breaks all three.js functionality on the page.
Here is the code from the page I'm trying to run.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ThreeJS Starter</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<script src="/static/three/build/three.js"></script>
<script type="module" src="/static/three/examples/jsm/controls/OrbitControls.js"></script>
<script type="module" src="/static/three/examples/jsm/loaders/VOXLoader.js"></script>
<script type="module">
// Canvas
const canvas = document.querySelector('canvas.webgl')
// Scene
const scene = new THREE.Scene()
// Problem Loading Here
const loader = new VOXLoader();
const geometry = new THREE.TorusGeometry( .7, .2, 16, 100 );
// Materials
const material = new THREE.MeshBasicMaterial()
material.color = new THREE.Color(0xff0000)
// Mesh
const sphere = new THREE.Mesh(geometry,material)
scene.add(sphere)
// Lights
const pointLight = new THREE.PointLight(0xffffff, 0.1)
pointLight.position.x = 2
pointLight.position.y = 3
pointLight.position.z = 4
scene.add(pointLight)
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
window.addEventListener('resize', () =>
{
// Update sizes
sizes.width = window.innerWidth
sizes.height = window.innerHeight
// Update camera
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
// Update renderer
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.x = 0
camera.position.y = 0
camera.position.z = 2
scene.add(camera)
// Controls
// const controls = new OrbitControls(camera, canvas)
// controls.enableDamping = true
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update objects
sphere.rotation.y = .5 * elapsedTime
// Update Orbital Controls
// controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()
</script>
TEST
<canvas class="webgl"></canvas>
Currently, when the page loads correctly it displays a red spinning Torus, but if it doesn't it show a white background with black text saying "TEST".
Ideally, I should be able to create a new instance of the VOXLoader without it breaking the page.
I've tried adding "import { VOXLoader } from '/static/three/examples/jsm/loaders/VOXLoader.js';" too, but the import function alone also seems to break the page.
I also checked the three.js documentation but didn't find anything helpful there yet.
It seems most people using three.js, aren't typically using it with flask, so I'm not sure if this is flask specific?
What's confusing to me is that the three.js file is imported fine and works with no issues, but for some reason, VOXLoader doesn't.
I double-checked the path and directory for VOXLoader and it's 100% correct.
This page is also currently only being used as a template for other pages to extend from, so it doesn't have any python code on it either.
I'm just trying to get the VOXLoader working and any suggestions or advice would be greatly appreciated.
Thank you in advance!
I'm trying to create a 3D editor, where the user can edit a 3D scene, and then hit a "play" button and see the result. To render the result, I'm using an iframe. Here is my HTML code:
<iframe id="testing_frame" class="ui"></iframe>
The class ui is just position: absolute; top: 0;. I don't want to have a URL or FILE as the src to this iframe, instead I want to write directly to it. Here is how I do it:
generatedCode += `
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"><\/script>
</head>
<body style="margin: 0;">
<script async>"use strict"
function init(){
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);
var mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({
color: 0xff0000
}));
scene.add(mesh);
camera.position.z = -5;
camera.lookAt(0, 0, 0);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
}
window.onload = init;
<\/script>
</body>
</html>`;
That code is stored in the variable generatedCode, which is what I will write to the iframe, here:
var iframe = document.getElementById("testing_frame");
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
iframeDocument.open();
iframeDocument.write(generatedCode);
iframeDocument.close();
This works fine.
My Problem: I have a start/stop button, which runs this code everytime it is clicked. Each time I hit stop, it says WARNING: Multiple instances of Three.js being imported., and if I start and stop the testing iframe around 10 times, it says THREE.WebGLRenderer: Context Lost.
Here I have a video demonstrating my problem. (Don't worry about the things in the console before I start doing anything)
Thanks!
Edit: Here is the start/stop code:
<button id='play_btn' onClick='test_iframe();';>Play</button>
This is the button, below is the test_iframe() function:
function test_iframe() {
let code = generateCodeFromProjectData();
var iframe = document.getElementById("testing_frame");
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
//Write to iframe
iframeDocument.open();
iframeDocument.write(code);
iframeDocument.close();
//If they press play, change it to stop and show the iframe.
if(document.getElementById("play_btn").innerHTML == "Play"){
document.getElementById("testing_frame").style.display = "block";
document.getElementById("play_btn").innerHTML = "Stop";
} else { //If they press stop, hide the iframe and change it to play.
document.getElementById("testing_frame").style.display = "none";
document.getElementById("play_btn").innerHTML = "Play";
}
}
Finally, here is the generateCodeFromProjectData() function, which receives the code:
function generateCodeFromProjectData(){
generatedCode = "";
//Opening
generatedCode += `
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"><\/script>
</head>
<body style="margin: 0;">
<script async>"use strict"
function init(){
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);
var mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({
color: 0xff0000
}));
scene.add(mesh);
camera.position.z = -5;
camera.lookAt(0, 0, 0);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
}
window.onload = init;
<\/script>
</body>
</html>`;
return generatedCode;
}
Edit 2: Here's my new code for the `iframe`
generatedCode += `
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"><\/script>
</head>
<body style="margin: 0;">
<script>
function init(){
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);
var mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({
color: 0xff0000
}));
scene.add(mesh);
camera.position.z = -5;
camera.lookAt(0, 0, 0);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
function destroy() {
scene = null;
camera = null;
mesh = null;
renderer.dispose()
}
return {destroy};
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
}
window.onload = () => {
var instance = init();
window.onbeforeunload = () => {
instance.destroy();
}
}
<\/script>
</body>
</html>`;
Now I get no error for Context Lost, but the red cube in the center does not show up.
It seems like a memory leak to me. Anything happened inside <iframe /> stays in the memory unless it is deallocated manually or the tab(window) is closed. I've experienced exact same problem when using storybook with different frontend libraries; storybook also uses <iframe /> as an isolated demonstration, but it always needed manual deallocation.
Manual memory deallocation, or dispose() function is necessary in Three.JS as well. Here's an excerpt from the official Three.js document:
Why can't three.js dispose objects automatically?
This question was asked many times by the community so it's important to clarify this matter. Fact is that three.js does not know the lifetime or scope of user-created entities like geometries or materials. This is the responsibility of the application. For example even if a material is currently not used for rendering, it might be necessary for the next frame. So if the application decides that a certain object can be deleted, it has to notify the engine via calling the respective dispose() method.
It says the dispose is necessary for Three.Object3D and says nothing about WebGLRender. Interestingly enough, the dispose() function is listed as WebGLRenderer's spec.
https://threejs.org/docs/?q=renderer#api/en/renderers/WebGLRenderer
to sync the dispose action to iframes, use window.onbeforeunload event handler or add dispose function to your page move action.
// add this code to your iframe code.
function init () {
var scene = // ...
var camera = // ...
var mesh = // ...
var renderer = // ...
/* ... */
function destroy() {
scene = null;
camera = null;
mesh = null;
renderer.dispose()
}
return {destroy};
}
window.onload = () => {
var instance = init();
window.onbeforeunload = () => {
instance.destroy();
}
}
edit: the answer only solves the context lost issue. It has no problem in terms of destroying the instance. Multiple instances being imported should be regarded as another problem. Anything that's added inside <iframe/> is considered in the same scope as its parent. adding <script src="..." /> inside iframe again and again causes multiple instances being imported. So to solve this issue, code must be provided from its parent.
generatedCode should not include the <script />
code should be provided in the same scope as the iframe controller code, so that it can sync the lifecycle of iframe and three.js renderer.
it's little bit more complex than disposing. here's the working code and working demo at codepen
<script src="your three.js url"></script>
<body>
<div id="root">
</div>
<button id='play_btn' onClick='test_iframe()'>Play</button>
</body>
// multiple instances being imported & context lost issue solved
function init(width, height) {
console.log("init", width, height);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(90, width / height, 0.1, 1000);
var mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({
color: 0xff0000
})
);
scene.add(mesh);
camera.position.z = 5;
camera.lookAt(0, 0, 0);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
function destroy() {
console.log("destroy");
scene = null;
camera = null;
mesh = null;
renderer.dispose();
}
function animate() {
if (!renderer) return;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
return { destroy, domElement: renderer.domElement };
}
let iframe;
const getIframe = () => {
const code = generateCodeFromProjectData();
const root = document.getElementById("root");
const iframe = document.createElement("iframe");
iframe.setAttribute("id", "testing_frame");
iframe.setAttribute("class", "ui");
iframe.style.width = "300px";
iframe.style.height = "300px";
root.appendChild(iframe);
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
iframeDoc.open();
iframeDoc.write(code);
iframeDoc.close();
const body = iframeDoc.querySelector("body");
const instance = init(body.clientWidth, body.clientHeight);
body.appendChild(instance.domElement);
function destroy() {
root.removeChild(iframe);
instance.destroy();
}
return { iframe, iframeDoc, destroy };
};
function test_iframe() {
if (!iframe) {
// init
console.log("init");
iframe = getIframe();
document.getElementById("play_btn").innerHTML = "Stop";
return;
}
console.log("dispose");
// disposing
document.getElementById("play_btn").innerHTML = "Play";
iframe.destroy();
iframe = null;
}
function generateCodeFromProjectData() {
generatedCode = "";
//Opening
generatedCode += `
<!DOCTYPE html>
<html>
<body style="margin: 0;width: 300px;height:300px;">
</body>
</html>`;
return generatedCode;
}
The page has a divs with classes "skin", "skin2", "skin3"...
And for each class you need to place your 3d model.
I'm trying to do this, but all the 3d models, are tied to the first model.
I want to do this using the example of this site minecraft-skins
Image code results
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(10, window.innerWidth / window.innerHeight, 0.1, 50);
camera.position.z = 20;
camera.position.y = 1.5;
camera.position.x = 0;
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setClearColor(0x000000, 0);
renderer.setSize(200, 300);
renderer.gammaOutput = true;
var light = new THREE.AmbientLight();
scene.add(light);
let loader = new THREE.GLTFLoader();
loader.load('/Стив.gltf', function (gltf) {
let obj = gltf;
document.getElementsByClassName('skin')[0].insertBefore(renderer.domElement, document.getElementsByClassName('skin')[0].firstChild);
scene.add(obj.scene);
obj.scene.scale.set(4.8, 1.5, 4.9);
obj.scene.rotation.y += 3.6;
obj.scene.rotation.x += 0.06;
});
loader.load('/Стив1.gltf', function (gltf2) {
let obj = gltf2;
document.getElementsByClassName('skin1')[0].insertBefore(renderer.domElement, document.getElementsByClassName('skin1')[0].firstChild);
scene.add(obj.scene);
obj.scene.scale.set(4.8, 1.5, 4.9);
obj.scene.rotation.y += 3.6;
obj.scene.rotation.x += 0.06;
});
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
<!DOCTYPE html>
<htm>
<head>
<title>Skins</title>
<script src="https://cdn.jsdelivr.net/npm/three#0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.128.0/examples/js/loaders/GLTFLoader.js"></script>
</head>
<body>
<div class="block">
<div class="skin">
</div>
</div>
<div class="block">
<div class="skin1">
</div>
</div>
<script src="./script_01.js"></script>
</body>
</html>
Your code creates one renderer. On the first call to .insertBefore(), that renderer puts its context in the skin div. On the second call to .insertBefore(), that context is moved out of the skin div and into the skin1 div; at that point, there's no rendering being done inside of skin.
To achieve what you want, it's best to create a renderer that covers the whole window. Then you can use the renderer's .setScissor() method to render inside the limits of each div. Three.js provides an example of this.
We have an application that also runs on an iPad. Using three.js r100.
It has a "main" and several "popups", each with its own canvas, scene and renderer. The "main" has a scene etc. too that is always shown.
To avoid memory issues, we create all the objects when the popup is opened, and clean up when the popup is closed.
But on the iPad, the webinfo still shows the canvasses of closed popups.
And after opening/closing several popups we get an error about too many contexts ("There are too many active WebGL contexts on this page, the oldest context will be lost.").
The first context that is lost is the "main" scene. After that, the system tries to loose a "popup" context. A second error is shown: "WebGL: INVALID_OPERATION: loseContext: context already lost". That seems logical because we did a forceContextLoss() when closing the popup.
At popup close we:
dispose everything (material etc.) in the scene
dispose the OrbitControl
dispose the renderer
forceContextLoss() the renderer
remove the canvas from the DOM
I suspect the canvas is keeping the contexts from being cleaned up, but maybe I miss something?
So, how can we fully remove the contexts of the popups?
Thanks, Willem
Not sure this is a direct answer but I think you will have better luck either
(a) using a single context and the scissor test to emulate multiple canvases (recommended)
See techniques like this
or
(b) using a virtual webgl context that simulates multiple contexts on top of a single context.
Where you really only have 1 context and others are virtual
AFAIK there is no way to force the browser to free a context. Even forcing a context lost is not guaranteed to get rid of the WebGLRenderingContext object, in fact it explicitly does not. When you get a context lost event you keep using the same context object even after restoring.
So, there's no guarantee the browser isn't just going to delete the oldest context as soon as the 9th context is created (or whatever the limit is). The only guarantee is generally when new contexts are created only old ones lose theirs.
Whether it's the context least recently used or the oldest context or the context will the least resources or the context with no more references is up to the browser. Really there is no easy way for the browser to know which contexts to free.
Here's a quick test of creating and deleting contexts. The oldest context gets lost as the 17th context is created on Chrome desktop
'use strict';
/* global THREE */
function makeScene(canvas, color = 0x44aa88, timeout = 0) {
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const scene = new THREE.Scene();
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const material = new THREE.MeshPhongMaterial({color});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
let requestId;
function render(time) {
time *= 0.001; // convert time to seconds
cube.rotation.x = time;
cube.rotation.y = time;
renderer.render(scene, camera);
requestId = requestAnimationFrame(render);
}
requestId = requestAnimationFrame(render);
if (timeout) {
setTimeout(() => {
cancelAnimationFrame(requestId);
canvas.parentElement.removeChild(canvas);
// manually free all three objects that hold GPU resoucres
geometry.dispose();
material.dispose();
renderer.dispose();
}, timeout);
}
}
makeScene(document.querySelector('#c'));
let count = 0;
setInterval(() => {
console.log(++count);
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
makeScene(canvas, Math.random() * 0xFFFFFF | 0, 500);
}, 1000);
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/three.min.js"></script>
Here's the same test with virtual-webgl
'use strict';
/* global THREE */
function makeScene(canvas, color = 0x44aa88, timeout = 0) {
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const scene = new THREE.Scene();
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const material = new THREE.MeshPhongMaterial({color});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
let requestId;
function render(time) {
time *= 0.001; // convert time to seconds
cube.rotation.x = time;
cube.rotation.y = time;
renderer.render(scene, camera);
requestId = requestAnimationFrame(render);
}
requestId = requestAnimationFrame(render);
if (timeout) {
setTimeout(() => {
cancelAnimationFrame(requestId);
// take the canvas out of the dom
canvas.parentElement.removeChild(canvas);
// manually free all three objects that hold GPU resoures
geometry.dispose();
material.dispose();
// hold on to the context incase the rendered forgets it
const gl = renderer.context;
// dispose the rendered in case it has any GPU resources
renderer.dispose();
// dispose the virutal context
gl.dispose(); // added by virtual-webgl
}, timeout);
}
}
makeScene(document.querySelector('#c'));
let count = 0;
setInterval(() => {
console.log(++count);
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
makeScene(canvas, Math.random() * 0xFFFFFF | 0, 500);
}, 1000);
<canvas id="c"></canvas>
<script src="https://greggman.github.io/virtual-webgl/src/virtual-webgl.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/three.min.js"></script>
Edit;
working codepen (need to provide video file to avoid cross-origin policy)
https://codepen.io/bw1984/pen/pezOXm
I am attempting to modify the excellent rutt etra example here https://airtightinteractive.com/demos/js/ruttetra/ to work for video (still using threejs) and am encountering strange issues with performance.
My code currently works as expected, and actually runs quite smoothly on chrome on my macbook pro, but seems to cause some sort of slow memory leak which i assume is to do with all the heavy lifting which is having to be done by getImageData. Strangely enough its only noticeable once i attempt to refresh the tab, so looks like it may be related to the garbage collection in chrome maybe? anyway to shunt the grunt work onto the GPU instead of killing the CPU?
I just wondered if i am missing anything obvious in terms of code optimisation or if the performance issues i am facing are to be expected given the nature of what i am trying to do.
I am only interested in WebGL / chrome functionality so dont really need to worry about browser compatibility of any kind.
<script>
var container, camera, scene, renderer, controls;
// PI
var PI = Math.PI;
var TWO_PI = PI*2;
// size
SCREEN_WIDTH = window.innerWidth;
SCREEN_HEIGHT = window.innerHeight;
SCREEN_PIXEL_RATIO = window.devicePixelRatio;
// camera
var VIEW_ANGLE = 45;
var ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT;
var NEAR = 0.1;
var FAR = 20000000;
// video raster
var video;
var videoImage;
var videoImageContext;
var _imageHeight;
var _imageWidth;
// lines
var _lineGroup;
// gui
var _guiOptions = {
stageSize: 1,
scale: 1.0,
scanStep: 5,
lineThickness: 10.0,
opacity: 1.0,
depth: 50,
autoRotate: false
};
// triggered from audio.php getMediaStream
function runme()
{
console.log('runme running');
init();
animate();
}
runme();
function init()
{
container = document.createElement('div');
document.body.appendChild(container);
//----------
// scene
//----------
scene = new THREE.Scene();
//----------
// camera
//----------
camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR);
//camera.position.set(0,0,450);
camera.position.set(0,150,300);
//----------
// objects
//----------
// create the video element
video = document.createElement('video');
// video.id = 'video';
// video.type = ' video/ogg; codecs="theora, vorbis" ';
video.src = 'data/sintel.ogv';
//video.src = 'data/az.mp4';
video.load(); // must call after setting/changing source
video.play();
videoImage = document.createElement('canvas');
//videoImage.width = 480;
//videoImage.height = 204;
videoImageContext = videoImage.getContext('2d');
_imageWidth = videoImage.width;
_imageHeight = videoImage.height;
//videoImageContext.fillStyle = '#ffffff';
//videoImageContext.fillRect(0, 0, videoImage.width, videoImage.height);
//----------
// controls
//----------
controls = new THREE.OrbitControls(camera);
//----------
// events
//----------
window.addEventListener('resize', onWindowResize, false);
//----------
// render
//----------
var args = {
//antialias: true // too slow
}
renderer = new THREE.WebGLRenderer(args);
renderer.setClearColor(0x000000, 1);
renderer.setPixelRatio(SCREEN_PIXEL_RATIO); //Set pixel aspect ratio
renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
// attach to dom
container.appendChild(renderer.domElement);
//render();
}
function render()
{
if(video.readyState === video.HAVE_ENOUGH_DATA && !video.paused && !video.ended) // and video.currentTime > 0
{
//_imageWidth = videoImage.width;
//_imageHeight = videoImage.height;
videoImageContext.drawImage(video,0,0,_imageWidth,_imageHeight);
// Grab the pixel data from the backing canvas
var _data = videoImageContext.getImageData(0,0,videoImage.width,videoImage.height).data;
//log(data);
//_pixels = data;
var x = 0, y = 0;
if(_lineGroup)
{
scene.remove(_lineGroup);
//_lineGroup = null;
}
_lineGroup = new THREE.Object3D();
var _material = new THREE.LineBasicMaterial({
color: 0xffffff,
linewidth: _guiOptions.lineThickness
});
// loop through the image pixels
for(y = 0; y < _imageHeight; y+= _guiOptions.scanStep)
{
var _geometry = new THREE.Geometry();
for(x=0; x<_imageWidth; x+=_guiOptions.scanStep)
{
var color = new THREE.Color(getColor(x, y, _data));
var brightness = getBrightness(color);
var posn = new THREE.Vector3(x -_imageWidth/2,y - _imageHeight/2, -brightness * _guiOptions.depth + _guiOptions.depth/2);
//_geometry.vertices.push(new THREE.Vertex(posn));
_geometry.vertices.push(posn);
_geometry.colors.push(color);
_color = null;
_brightness = null;
_posn = null;
}
// add a line
var _line = new THREE.Line(_geometry, _material);
//log(line);
_lineGroup.add(_line);
// gc
_geometry = null;
}
scene.add(_lineGroup);
_data = null;
_line = null;
}
renderer.render(scene,camera);
}
function animate(){
requestAnimationFrame(animate);
stats.update();
render();
}
function onWindowResize(){
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
render();
}
// Returns a hexadecimal color for a given pixel in the pixel array.
function getColor(x, y, _pixels)
{
var base = (Math.floor(y) * _imageWidth + Math.floor(x)) * 4;
var c = {
r: _pixels[base + 0],
g: _pixels[base + 1],
b: _pixels[base + 2],
a: _pixels[base + 3]
};
return (c.r << 16) + (c.g << 8) + c.b;
}
// return pixel brightness between 0 and 1 based on human perceptual bias
function getBrightness(c)
{
return ( 0.34 * c.r + 0.5 * c.g + 0.16 * c.b );
}
</script>
any help anyone could provide would be much appreciated, even if its just pointing me in the right direction as i am only just beginning to experiment with this stuff and have almost given myself an aneurysm trying to wrap my tiny mind around it.
The slow memory leak is most likely due to:
// add a line
var _line = new THREE.Line(_geometry, _material);
//log(line);
_lineGroup.add(_line);
THREE.Line is an object, containing other objects and lots of data. Every time you instantiate it, it creates .matrix, .matrixWorld, .modelViewMatrix, .normalMatrix which are all arrays with a bunch of numbers. .position,.quaternion, .scale, .rotation and probably .up are vectors,quats etc. and are slightly smaller but also arrays with special constructors.
Allocating all this every 16 miliseconds only to be released the next frame is probably the cause of your "leak".
You should create a pool of THREE.Line objects, and draw that every frame instead. The number of drawn objects you can control with .visible and mutate their transformation properties.
#pailhead I took your advice about pre-rendering the lines and lineGroup in advance and then updating the vertices on each animation frame instead and now its purring like a kitten. Also needed to insert the following line to make sure updated coords are picked up;
e.geometry.verticesNeedUpdate = true;
I cant figure out how to get a hosted video to work on codepen (cross-origin policy violation issues) but i have put a version up anyway to show the working code.
https://codepen.io/bw1984/pen/pezOXm
I will try to get a self-hosted (working) version up as soon as i can
I've been trying in vain to get colour working, but that will have to be an exercise for another day.