So I want a simple solution. A toggle button which refreshes a Three.js scene depending on the user, if he wants an anti-aliased version of the scene or not. My refresh_scene function is as follows:
function refresh_scene()
{
scene.remove( floor );
floor.geometry.dispose();
floor.material.dispose();
floor = null;
scene.dispose();
scene = null;
controls.dispose();
controls = null;
camera = null;
renderer.dispose();
renderer = null;
canvas = null;
cancelAnimationFrame(reqAnimFrame);
visualizer();
}
This function is called every time the button is pressed. The visualizer() then just displays all objects again. This also includes getting the canvas, making a new renderer, new scene, etc. For some reason, the new renderer does not display an anti-aliased version of the scene, but the same scene over and over again, despite the fact that the toggle button toggles an antialiasing_ value which is then used in the visualizer() function to reinitialize the renderer according to the new value:
renderer = new THREE.WebGLRenderer({canvas, antialias: antialiasing_});
where antialiasing_ is a bool which toggles true or false. Am I doing something wrong? Any help would be appreciated. Can upload other parts of code if necessary.
Related
I am making a 3D map in Three.js.
I am constructing the geometry and texture from fetched 256x256 pixel images, construct a THREE.Mesh and add it to the scene when it's done.
However, adding tiles to the map causes MapControls to lag noticeably, if the user is panning or zooming around quickly. This can be alleviated by using smaller tiles (such as 128*128), but I want to find a better way, as I've seen examples of very smooth maps using Three.js. The controls work smoothly after all the tiles in view have loaded.
I have 2 event listeners. One triggers when controls change:
this.controls.addEventListener('change', this.update);
And renders the map:
update = () => {
this.renderer.render(this.scene, this.camera);
};
The other listens for the move to end, and then fetches the tiles in view:
this.controls.addEventListener('end', this.fetchTilesIfNecessary);
fetchTilesIfNecessary then creates Promises, which start fetching the tiles. When the tiles are fetched, they are added to the map and this.update is called:
addTile(tile) {
this.scene.add(tile.mesh);
this.update();
}
Perhaps I should mention that I have a callback when the tile mesh gets rendered:
this.mesh.onAfterRender = this.disposeAttributes;
Which clears the attributes which are causing a lot of memory usage
disposeAttributes(renderer, scene, camera, geometry, material, group) {
geometry.getAttribute('position').array = [];
geometry.getAttribute('normal').array = [];
geometry.getAttribute('uv').array = [];
}
Is there a better way? How can I add meshes to the scene dynamically and keep the controls running smoothly?
One way to mitigate this issue is to pre-compile all shaders via WebGLRenderer.compile(). Meaning when your app starts, you add all tiles to your scene and then call renderer.compile() once with the scene's camera.
this.mesh.onAfterRender = this.disposeAttributes;
This is not recommended since it will be executed on each render call. If you want to free the geometry data one the JS side, use the onUpdate() callback of BufferAttribute().
geometry.getAttribute( 'position' ).onUpload( disposeArray );
function disposeArray() {
this.array = null;
}
I'm not sure if the issue has something to do with how I've set things up in my application, some configuration issue with Chrome, the DOM, OrbitControls or another feature of Three.js.
When I load my application inside a tab in Chrome, everything works perfectly. When I change to another tab in Chrome, or minimize Chrome then return to it (I'm on a Windows machine), OrbitControls behavior changes. Notably, clicking on the left mouse button and moving the mouse around no longer orbits as before, but instead zooms out, up the Y-axis. It also appears like the camera-angle is suddenly a lot wider, and camera rotation center-point changes.
The only apparent fix is to reload the application in the tab, which works, but as I'm getting near alpha release on the product I'm starting to think about how users are not going to tolerate the issue as I have...
Here's what I believe to be my relevant code.
document.addEventListener( "DOMContentLoaded", init );
function init() {
cameras();
initRenderer();
initEventListeners();
entities();
// Create the Stereoscopic viewing object (Not applied yet)
var effect = new THREE.StereoEffect( renderer );
render();
}
function render() {
renderer.render(scene, entities.cameras.perspCamera );
requestAnimationFrame( render );
}
// if the device we're using has 'alpha' attribute, then it's a mixedReality-compatible mobile browser...
function setOrientationControls(e) {
if (e.alpha) {
initVRControls ();
}
else {
initbrowserControls ();
var camera = entities.cameras.perspCamera;
entities.cameras.init( camera );
}
}
function initbrowserControls() {
// Create the Mouse-Based Controls - Hold down left mouse button and move around the window...
var camera = entities.cameras.perspCamera;
entities.browserControls = new THREE.OrbitControls ( camera , container );
entities.browserControls.target.set(
camera.position.x + 0.15,
camera.position.y,
camera.position.z
);
entities.browserControls.noPan = true;
entities.browserControls.noZoom = true;
}
function initEventListeners() {
// Listen for Device Orientation events.
window.addEventListener('deviceorientation', setOrientationControls, true);
}
function initRenderer() {
renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setClearColor( 0xffffff, 1 );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.domElement.setAttribute( 'id' , 'renderSpace' );
renderer.domElement.setAttribute( 'class' , 'threeWebGLRenderer' );
container.appendChild( renderer.domElement );
};
if the code provided isn't sufficient to answer the question, let me know. I can provide what's needed. Thanks for the help!
UPDATE:
I've edited my code as suggested by #PerrinPrograms in the comments to try to reset the controls when document.visibilityState is reset to "visible".
This seems a workaround... I'd rather get to the source of the problem, but thought following the tracks of it might provide more illumination.
The result is that the behavior is exactly the same, with the added bonus, as noted in the comments, that when I return to the application tab and have a console-break point at setOrientationControls(e) inside of my new if statement, I cannot restart the console action and have to reload the tab.
Here's what I did to my code:
function initEventListeners() {
// Listen for Device Orientation events.
window.addEventListener('deviceorientation', setOrientationControls, true);
document.addEventListener('visibilitychange', onDocumentVisible, true); // ADDED LINE
}
// ADDED FUNCTION
function onDocumentVisible(e){
if ( document.visibilityState === "visible" ){ // CONSOLE LOCKS UP IF I PLACE BREAK HERE
setOrientationControls(e); // HERE TOO
}
}
The plot thickens... Thanks all for looking into this with me.
PREMISE:
I'm trying to create a static scene that has a fixed image background and a geometry in front of it. Since the scene is static, I don't need and envMap.
I created two scenes and cameras (one for the background and one for the geometries) as suggested in this SO question, and this demo, and updated the procedure to consider that THREE.ImageUtils.loadTexture() is deprecated. Here is the working code:
// load the background texture
var loader = new THREE.TextureLoader();
texture = loader.load('path_to_image.jpg');
var backgroundMesh = new THREE.Mesh(
new THREE.PlaneGeometry(2, 2, 0),
new THREE.MeshBasicMaterial({
map: texture
})
);
backgroundMesh.material.depthTest = false;
backgroundMesh.material.depthWrite = false;
// create your background scene
backgroundScene = new THREE.Scene();
backgroundCamera = new THREE.Camera();
backgroundScene.add( backgroundCamera );
backgroundScene.add( backgroundMesh );
The variables backgroundScene and backgroundCamera are global and the following procedure is called inside the init() function. All scenes and cameras are later rendered using:
renderer.autoClear = false;
renderer.clear();
renderer.render( backgroundScene , backgroundCamera);
renderer.render(scene, camera);
PROBLEM:
I implemented an event listener that is supposed to change the background image and geometry when a button is pressed, however this is not working.
I thought that loading the new texture and changing the material property of the backgroundScene variable, clearing the renderer and rendering the scene again would do the job. Here is the code:
var loader = new THREE.TextureLoader();
var texture = loader.load('path_to_new_image.jpg');
console.debug(texture);
console.debug(backgroundScene.children[1].material.map);
backgroundScene.children[1].material.map = texture;
console.debug(backgroundScene.children[1].material.map);
renderer.clear();
renderer.render( backgroundScene , backgroundCamera );
renderer.render(scene, camera);
The console.debug() show me that the new texture is actually loaded and the backgroundScene material is changed accordingly.
However, while the geometries are rendered fine I am left with a blank background and get the following error: [.Offscreen-For-WebGL-0x364ad7e56700]RENDER WARNING: there is no texture bound to the unit 0.
Any ideas of what is going on? Thanks for your help!
you will need to call object.material.needsUpdate = true; for the change to take effect (see here). When the map-property of the material is changed, three.js needs to re-setup the texture-binding, which is skipped unless the needsUpdate-flag is set.
Alternatively, if you just change the material.map.image-property it should work without that extra-step.
I have a world map layer as a plane geometry, now i want to handle click events on different parts (For example continents or country). How to handle popup events on particular part like (popup of information or video playing or image data with links etc.)
Here's a fiddle that already has the scene setup.
var camera, scene, renderer;
var geometry, material, mesh;
window.addEventListener("click", onClick, false);
function onClick() {
alert("Replace me with code to add an object!");
}
var init = function () {
renderer = new THREE.CanvasRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 500;
scene = new THREE.Scene();
geometry = new THREE.CubeGeometry(200, 200, 200);
material = new THREE.MeshBasicMaterial({
color: 0x000000,
wireframe: true,
wireframeLinewidth: 2
});
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
var animate = function () {
requestAnimationFrame(animate);
mesh.rotation.x = Date.now() * 0.0005;
mesh.rotation.y = Date.now() * 0.001;
renderer.render(scene, camera);
}
init();
animate();
the click event is applied to whole scene, i have created multiple cubes in that example and when clicked on that it is showing the same click event for all parts.
What you ask is too complex, you need to break it down. First problem you want to solve is to find out which object is being clicked.
You can do raycasting or gpu picking. since you sound like a beginner I would suggest you to start with raycasting.
There are some things you need to do in order to figure out where on the map the click occured (and this is just one way to do it, another one would be to use GPU-picking, which could be even easier):
use the raycaster that three.js provides to get information about the object that is under the cursor when the click happened. (docs/example)
in the result from the raycaster you will get the uv-coordinates for the point of intersection (so basically, where in your map-texture the click occurred).
now, you can either store your geographic features as values relative to these UV-coordinates or you need to convert the UV-values into a geographical format to work with (WGS84, or latitude/longitude). For that you need to know the type of projection that was used to create the map (probably a mercator-projection). Note that there are lots of libraries available for these kinds of conversions.
Now that you know where in your world-map the click occurred, you can test it against geographic features. Let's say continents. For each continent you will need to define a polygon in the same coordinate-system you are using for the coordinates of the clicks. Then you can do a 'point in polygon'-test (here's an implementation) to check if the clicked location is inside the polygon.
I am working with ThreeJS on a basic 3d scene that has OrbitControls. Everything works great, except it causes my entire site to lag, as it is looping itself even when the user is not looking at it. I want a function that I can call to start and stop the rendering when certain conditions are met (in this case, the user isn't viewing the canvas). I have a start function that works just fine, but the stop function does not seem to be working, as my site goes unbearably slow after ThreeJS has initialized.
I have looked and looked for a solution to this problem, and have found a couple 'solutions', but for whatever reason they do not work with my application. My assumption is that these solutions are from old versions of ThreeJS.
Here is my code in my main.js file:
var scene,
camera,
controls,
render,
requestId = undefined;
function init() {
scene = new THREE.Scene();
var threeJSCanvas = document.getElementById("threeJS");
camera = new THREE.PerspectiveCamera( 75, threeJSCanvas.width / threeJSCanvas.height, 0.1, 1000 );
controls = new THREE.OrbitControls( camera );
// Controls and Camera settings
// Create Geometry.
}
function render() {
requestId = requestAnimationFrame(render);
renderer.render(scene, camera);
}
function start() {
render();
}
function stop() {
window.cancelAnimationFrame(requestId);
requestId = undefined;
}
In my other javascript file, there is a conditional inside of my pageChange function (this is a multipage app), that looks like the following:
if (page == 5) { // The page with the canvas on it
if (!locationsRendered) {
init();
locationsRendered = true;
}
} else { // If the page is not the page with the canvas on it
if (locationsRendered) {
stop();
}
}
locationsRendered is initialized earlier in this second javascript file in the local scope.
Any help would be much appreciated, as I can not let this simple 3D scene lag my entire app after it has been loaded. It's just not realistic.
If your scene is static, there is no reason for an animation loop. You only need to re-render when the camera moves due to a mouse or touch event.
Just use this pattern:
controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.addEventListener( 'change', render );
function render() {
renderer.render( scene, camera );
}
three.js r.67
I was using trackball controls in my scene and therefore couldn't use the solution above (as the trackball controls continue updating after mouse events finish triggering).
To solve this problem, I used:
function animate() {
renderer.render(scene, camera);
controls.update();
}
renderer.setAnimationLoop(animate);
That runs the animation loop indefinitely. To pause the animation, one can then specify null as the animation loop:
renderer.setAnimationLoop(null); // pause the animation
And to resume the animation, just pass the animation loop again:
renderer.setAnimationLoop(animate); // resume the animation
An alternative solution to completely stopping the render loop is to reduce the frames per second rate and thereby reducing resource consumption.
This approach is particularly useful if you need responsive update on your scene while not necessarily animating, but also need to snap back normal speeds when you need to.
a simple setTimout() achieves this nicely.
var fps 10;
function render() {
//reduce framerate
setTimeout(()=>{
requestAnimationFrame(render);
//must be called to enable rotating
controls.update();
renderer.render(scene, camera);
}, 1000/fps)
};