I need to run raycast off mouse coordinates and check for intersections on a group of Three CSS3DObject objects.
Here is the function:
RayCastCheck = function(event, objects){
var vector = new THREE.Vector3((event.clientX / window.innerWidth)*2 - 1, -(event.clientX / window.innerHeight )*2 + 1, 0.5);
new THREE.Projector().unprojectVector( vector, camera);
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects(objects);
console.log(intersects.length);
};
The objects argument is an array of css3dobjects. I am able to use similar function to target drops on the scene to the correct mouse location so I believe my calculation of the mouse point in world space is correct. This led to believe that the Raycaster is does not check intersections on css3dobjects.
My css3dobjects are typically constructed with a div as its element.
createObject = function(){
var element = document.createElement("div");
var obj = new THREE.CSS3DObject(element);
scene.add(obj);
}
My scene is created via this function
//global
var scene;
var camera;
var renderer;
createScene = function(){
camera = new THREE.PerspectiveCamera( 75, 400 / 600, 1, 1000 );
camera.position.z = 500;
scene = new THREE.Scene();
renderer = new THREE.CSS3DRenderer();
renderer.setSize(400, 600);
$(#body).appendChild(renderer.domElement);
}
Do I have all the required elements in the scene to enable raycasting?
Is it possible to perform raycasting on css3dobjects with the css3drenderer?
Thank you for your help
You can just use the usual events with the dom elements. You can even get the relative coordinates:
var x = e.offsetX==undefined?e.layerX:e.offsetX;
var y = e.offsetY==undefined?e.layerY:e.offsetY;
Using Raycaster on css3dobjects won't work. At least this is what I figured out.
Take a look at three.js r76 line 8933. There is the definition of the "raycast" function of the css3dobject.
It is empty so it isn't implemented and won't work because of this of course. probably on a further version. would need this function too
Still isn't implemented in r78.
Related
On the minimal example below (don't forget to adapt the URL of three.min.js) then open the html file in a window. You should see a (non-regular) tetrahedron. When moving the mouse over the canvas you should see the number of intersection of the ray from the camera to the mouse with all the objects of the scene object, tested with this line in the code:
raycaster.intersectObjects(scene.children,false);
Since apart from the ligths, there is only the tetrahedron, it says mostly 0 or 2 because it counts the number of faces that have been intersected by the infinite ray and because I have chosen a double sided material:
var material = new THREE.MeshLambertMaterial( { color: 0xd8f8b0, side: THREE.DoubleSide } );
Now click the checkbox. Another tetrahedron is created on the fly, its Geometry being a clone of the Geometry of the first Mesh.
geom2 = geom.clone();
I offset the new geom by adding 1 to all the coordinates of its vertices. However, the raycaster answers 0 for most rays intersecting the new object. Is there a bug or did I forget or misunderstand something?
If the geometry is not a clone (change clone=true; to clone=false; on the top of min.js) then it works.
Three.js version : r86
Minimal example
the html file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="three.min.js"></script>
</head>
<body>
<div style="text-align: center;">
<canvas id="ze-canvas" width="800" height="600"></canvas>
<p>
<input type="checkbox" id="filBox"> click me
<p>
<span id="info-text"></span>
<p>
<script src="min.js"></script>
<script>visualiseur("ze-canvas","info-text","filBox");</script>
</div>
</body>
</html>
the file min.js
var visualiseur = function(canvas_name,info_name,box_name) {
var clone = true;
var canvas = document.getElementById(canvas_name);
var cbox = document.getElementById(box_name);
var textInfo = document.getElementById(info_name);
cbox.checked = false;
var camera = new THREE.PerspectiveCamera( 33, canvas.width / canvas.height, 0.1, 1000 );
camera.position.set(-2,4,8);
camera.lookAt(new THREE.Vector3(0,0,0));
var scene = new THREE.Scene();
scene.add( new THREE.AmbientLight( 0xffffff, .3) );
var light1 = new THREE.PointLight( 0xCCffff, .7, 0, 2 );
var light2 = new THREE.PointLight( 0xffffCC, .7, 0, 2 );
light1.position.set( 50, -50, 20 );
light2.position.set( -50, 150, 60 );
scene.add( light1 );
scene.add( light2 );
var material = new THREE.MeshLambertMaterial( { color: 0xd8f8b0, side: THREE.DoubleSide } );
var makeGeom = function(geom) {
geom.vertices.push(new THREE.Vector3(0,0,0));
geom.vertices.push(new THREE.Vector3(0,0,1));
geom.vertices.push(new THREE.Vector3(0,1,0));
geom.vertices.push(new THREE.Vector3(1,0,0));
geom.faces.push(new THREE.Face3(0,1,2));
geom.faces.push(new THREE.Face3(1,3,2));
geom.faces.push(new THREE.Face3(0,2,3));
geom.faces.push(new THREE.Face3(0,3,1));
geom.computeFlatVertexNormals();
}
var geom = new THREE.Geometry();
makeGeom(geom);
var mesh = new THREE.Mesh(geom,material);
scene.add(mesh);
var renderer = new THREE.WebGLRenderer({ canvas : canvas, antialias: true});
var render = function() {
renderer.render( scene, camera );
}
function getMousePos(evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
var raycaster = new THREE.Raycaster();
canvas.onmousemove = function(e) {
render();
var p=getMousePos(e);
p.x = p.x/canvas.width*2 - 1;
p.y = -p.y/canvas.height*2 + 1;
raycaster.setFromCamera( new THREE.Vector2(p.x,p.y), camera);
var intersects = raycaster.intersectObjects(scene.children,false);
textInfo.innerHTML=intersects.length+" intersections";
}
var done=false;
cbox.onclick = function(e) {
if(done) return;
done = true;
var geom2;
if(clone) {
geom2 = geom.clone();
}
else {
geom2 = new THREE.Geometry();
makeGeom(geom2);
}
geom2.vertices.forEach(function(v) {
v.x += 1;
v.y += 1;
v.z += 1;
});
geom2.verticesNeedUpdate=true;
geom2.computeFlatVertexNormals();
scene.add(new THREE.Mesh(geom2,material));
render();
}
render();
}
This was a tricky one but I found the solution.
In short: add
geom2.computeBoundingSphere();
just after changing the vertices.
Long version: Here's how I found the solution.
I started looking at the source code of Geometry.js and looking at every member function or variable in Geometry that I might have overlooked.
I finally noticed this bounding box and bounding sphere things, which I had never heard of in THREE.js before. Looking back at the Geometry section of the documentation of THREE.js, they are mentioned but without any explanation on what they are used for.
One would naturally think then they are used to accelerate the rendering on the graphics card by first computing the intersection of a ray with the box/sphere (this is fast I suppose) and if there is none we can skip the whole object.
This turns out to be a false assumption: on my minimal example, the second tetrahedron does show up even though its bounding sphere is wrong.
Then it got even more strange. I had the script log the bounding boxes and spheres of the geometries when I click the box, once just before the cloning and once just after the next rendering pass.
They never get a bounding box.
The first geometry has bounding sphere before and after.
The second has a bounding sphere before rendering only if clone = true (so no bounding sphere when created).
After rendering, both objects have a bounding sphere.
Conclusion : the bounding sphere is used by the Raycaster but not the rendered. (this is a surprise to me)
By inspecting the bounding sphere centers, I realized that the bounding sphere of the second geometry was wrong when it is cloned and the vertices moved, and is not updated by render().
When an object is created and render() called, then its bounding sphere is created and is correct. However, if you change the vertices, and even if you set the verticesNeedUpdate flag to true, the bounding sphere does not get updated, you have to call manually computeBoundingSphere().
It is all the more puzzling that the bounding sphere is secretly created when you call render() but not the bounding box.
Let me sum up what I understood of this all:
the bounding sphere is used by the Raycaster but not the renderer
I ignore if the bounding box is used by either (I have not spent time testing that)
if the bounding sphere does not exist, it will get created when calling render(). If it exists it is not updated by calling render() even when the flag verticesNeedUpdate is set to true.
the bounding box does not get created by render()
Is this the designed behaviour or is this a bug?
I have some code at https://jsfiddle.net/72mnd2yt/1/ that doesn't display the sprite I'm trying to draw. I tried to follow the code over at https://stemkoski.github.io/Three.js/Sprite-Text-Labels.html and read it line by line, but I'm not sure where I went wrong. would someone mind taking a look at it?
Here is some relevant code:
// picture
var getPicture = function(message){
var canvas = document.createElement('canvas');
canvas.width = "100%";
canvas.height = "100%";
var context = canvas.getContext('2d');
context.font = "10px";
context.fillText(message, 0, 10);
var picture = canvas.toDataURL();
// return picture;
return canvas;
};
// let there be light and so forth...
var getScene = function(){
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
70,
$('body').width(),
$('body').height(),
1,
1000
);
var light = new THREE.PointLight(0xeeeeee);
scene.add(camera);
scene.add(light);
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xefefef);
renderer.setSize($('body').width(), $('body').height());
camera.position.z = -10;
camera.lookAt(new THREE.Vector3(0, 0, 0));
return [scene, renderer, camera];
};
// now for the meat
var getLabel = function(message){
var texture = new THREE.Texture(getPicture(message));
var spriteMaterial = new THREE.SpriteMaterial(
{map: texture }
);
var sprite = new THREE.Sprite(spriteMaterial);
//sprite.scale.set(100, 50, 1.0);
return sprite
};
var setup = function(){
var scene;
var renderer;
var camera;
[scene, renderer, camera] = getScene();
$('body').append(renderer.domElement);
var label = getLabel("Hello, World!");
scene.add(label);
var animate = function(){
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
};
setup();
A few points:
1) canvas.width = "100%" should be canvas.width = "100" (canvas sizes are assumed to be px). Same with canvas.height.
2) $('body').height() is 0, so the renderer canvas is not visible (you can check this out in dev tools, it's in the element tree but squashed to 0px high). I know nothing about jQuery, so not sure why this is, but I would recommend using window.innerHeight and window.innerWidth instead anyways. So renderer.setSize(window.innerWidth, window.innerHeight). You'll also want to make this change in the camera initialization.
3) Speaking of the camera initialization, you are passing in width and height as separate arguments, when there should only be an aspect ratio argument. See the docs. So
var camera = new THREE.PerspectiveCamera(70, window.innerWidth, window.innerHeight, 1, 1000)
becomes
var camera = new THREE.PerspectiveCamera(70, window.innerWidth/window.innerHeight, 1, 1000)
4) Because textures are assumed to be static, you need to add something to this part:
var texture = new THREE.Texture(getPicture(message))
texture.needsUpdate = true // this signals that the texture has changed
That's a one-time flag, so you need to set it every time the canvas changes if you want a dynamic texture. You don't have to make a new THREE.Texture each time, just add texture.needsUpdate in the render loop (or in an event that only fires when you want the texture to change, if you're going for efficiency). See the docs, under .needsUpdate.
At this point, it should work. Here are some further things to consider:
5) Instead of using Texture you could use CanvasTexture, which sets .needsUpdate for you. The fiddle you posted is using Three.js r71, which doesn't have it, but newer versions do. That would look like this:
var texture = new THREE.CanvasTexture(getPicture(message));
// no needsUpdate necessary
6) It looks like you were on this path already based on the commented out return picture, but you can use either canvas element, or a data url generated from the canvas for a texture. If getPicture now returns a data url, try this:
var texture = new THREE.TextureLoader().load(getPicture(message))
You can also indirectly use a data url with Texture:
var img = document.createElement('img')
var img = new Image()
img.src = getPicture(message)
var texture = new THREE.Texture(img);
texture.needsUpdate = true
If not, just stick with Texture or CanvasTexture, both will take a canvas element. Texture can indirectly take a url by passing in an image element whose src is set to the data url.
Fiddle with the outlined fixes:
https://jsfiddle.net/jbjw/x0uL1kbh/
I'm trying to use the three.js lookAt() method on a meshes (from CylinderBufferGeometry) so that it is oriented toward a point, but when I use the .lookAt() method, it causes the mesh to disappear from view.
The cylinder shows up fine if I comment out the .lookAt() method. I'm using a THREE.PerspectiveCamera and the THREE.WebGLRenderer incase that could have anything to do with the issue.
// Build cylinder
var cylinderRadius = 0.15
var cylinderHeight = 20
var geometry = new THREE.CylinderBufferGeometry(cylinderRadius, cylinderRadius, cylinderHeight);
var material = new THREE.MeshBasicMaterial({color: 0xffffff});
var cylinder = new THREE.Mesh(geometry, material);
// Point the cylinder up
cylinder.geometry.rotateX( Math.PI / 2);
cylinder.geometry.translate(0,0, cylinderHeight/2 );
// Move cylinder to position
cylinder.position.x = 10;
cylinder.position.y = 10;
// Look at point
cylinder.lookAt(0,0,15); // <-- ISSUE OCCURS HERE
scene.add(cylinder);
render();
Use cylinder.lookAt(new THREE.Vector3(0,0,15)); instead of cylinder.lookAt(0,0,15);
I set up my scene as follows:
document.addEventListener('mousedown', onDocumentMouseDown, false);
var container = document.getElementById("myCanvas");
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth*0.99, window.innerHeight*0.99 );
container.appendChild( renderer.domElement );
room_material = new THREE.MeshBasicMaterial({color: 0x00ff00});
room_material.side = THREE.DoubleSide;
objects = [];
camera.position.z = 19;
camera.position.x = 5;
camera.position.y = 30;
I have an array of objects that i'm trying to detect if a click intersects with them, defined as follows:
var thing0 = new THREE.Shape();
thing0.moveTo(-12.1321728566, 35.3935535858);
thing0.lineTo(7.10021556487,35.3935535858);
thing0.lineTo(7.10021556487,19.7039735578);
thing0.lineTo(5.12636517425,19.7166264449);
thing0.lineTo(5.12636517425,33.6221493891);
thing0.lineTo(-12.1377356984,33.6439534769);
var thing0Geom = new THREE.ShapeGeometry(thing0);
var thing0Mesh = new THREE.Mesh( thing0Geom, room_material );
thing0Mesh.name = "abcd";
scene.add(thing0Mesh);
objects.push(thing0Mesh);
I then render the scene with the following code:
renderer.render(scene, camera);
requestAnimationFrame(render);
And lastly I use the following code for the mouse click event:
function onDocumentMouseDown(event) {
var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
vector = vector.unproject(camera);
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects(objects, true);
alert("well, you clicked!");
if (intersects.length > 0) {
alert("wow, it worked");
}
}
However, no matter what I do the alert never gets called when it follows raycaster.intersectObjects(objects, true); However it does get called when it is placed anywhere before it. It seems that raycaster.intersectObjects(objects, true); is a bit of a black hole in this case?
I assume I simply have something wrong in my setup? Any help would be appreciated!
There are two things I tried that worked:
1) Make sure you're using Three.DoubleSide if your mesh face is not pointing towards origin of the ray. This is directly from the documentation:
"Note that for meshes, faces must be pointed towards the origin of the ray in order to be detected; intersections of the ray passing through the back of a face will not be detected. To raycast against both faces of an object, you'll want to set the material's side property to THREE.DoubleSide."
2) Use mesh.updateMatrixWorld() prior to raycasting. This comes from another stackoverflow post: threejs raycasting does not work
mesh.updateMatrixWorld(); // add this
raycaster.set(from, direction);
I think i have found the problem. I have to add the meshes to an extra array. An intersectObjects over the scene.children don't work, because there are other objects in there with the meshes.
So when i give the intersectObjects( mesh[] ) an mesh array than it works.
For more code detail see https://github.com/mrdoob/three.js/issues/8081
This is an old question, but I was running into a similar issue and thought what I learned might help someone else. My code setup is very similar to JohnnyDevNull's, so I won't repeat it.
The Problem
In my case, calling intersectObjects with scene.children doesn't work because it picks up other objects like ambientLighting, etc. I believe this is what the OP mentions in his own answer.
My Solution
The function below takes an empty array (intersects). It searches each child in scene for THREE Group objects and Mesh objects and adds Mesh objects to the array. For Groups, it sets recursive to true, which applies .intersectObject to each child. Once the array is built up, it calls your supplied callback function on the array.
function raycastMeshes(intersects, callback, theScene, theRaycaster) {
var scene = theScene || scene || new THREE.Scene();
var raycaster = theRaycaster || raycaster || new THREE.Raycaster();
for (var i in scene.children) {
if (scene.children[i] instanceof THREE.Group) {
intersects = raycaster.intersectObjects(scene.children[i].children, true);
} else if (scene.children[i] instanceof THREE.Mesh) {
intersects.push(raycaster.intersectObject(scene.children[i]));
}
}
if (intersects.length > 0) {
return callback(intersects);
} else {
return null;
}
}
Adapting to your Use Case
This is obviously a fairly naive and specific use case, but should serve as a launching board for you if you're having a similar problem.
I'm trying to have text sprites in the 3d scene with constant size (regardless of camera distance) using a PerspectiveCamera. In order to get non-sprites to have constant size, I make them children of a special "scaled" object which adjusts its scale as the camera distance to origin changes (see the code below). This works well to keep a general object roughly the same visual size, but when I add a sprite to the scaled object, the sprite seems to ignore its parent's scale (so it gets smaller and bigger as you zoom out and in).
Interestingly, when we switch to an orthographic camera (uncomment the appropriate line below), this special scaled object doesn't seem to affect children anymore (i.e., children don't stay a constant size). However, since we have an orthographic camera, sprites no longer scale as the camera distance changes (so they maintain a constant size), but this is independent of the scaled object.
I notice a few other similar questions and answers, including adjust the scale of the sprites themselves (it seems much easier to add all my sprites to a single scaling object), use an orthographic camera overlay to draw sprites (see also this) (but I want my sprites to be inside the 3d perspective scene).
So, my questions are: why do sprites not use scale according to their parent's scale when using a PerspectiveCamera? Also, why does my scaled object not work with the orthographic camera? Are these bugs or features of the cameras?
Thanks!
http://jsfiddle.net/LLbcs/8/
var camera, scene, renderer, geometry, material, mesh, text, controls;
init();
animate();
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000); var scenescale=1;
//camera = new THREE.OrthographicCamera( -7,7,7,-7, 1, 20 );
camera.position.z = 10;
scene.add(camera);
scaled=new THREE.Object3D();
scene.add(scaled);
var textmaterial = new THREE.SpriteMaterial( {color: 'red', useScreenCoordinates: true, map: texttexture("hi")});
text = new THREE.Sprite( textmaterial );
text.position.set( 1, 1, 0);
scaled.add(text);
var geometry = new THREE.BoxGeometry( 1, 1,1 );
var material = new THREE.MeshLambertMaterial( { color: 0xffffff } );
mesh = new THREE.Mesh( geometry, material );
mesh.position.set(0,3,0);
scaled.add(mesh);
var light = new THREE.PointLight('green');
light.position.set(10,15,10);
camera.add(light);
light = new THREE.PointLight(0x333333);
light.position.set(-10,-15,-8);
camera.add(light);
renderer = new THREE.WebGLRenderer();
controls = new THREE.OrbitControls( camera, renderer.domElement );
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function animate() {
requestAnimationFrame(animate);
var scale = camera.position.length()/10;
scaled.scale.set(scale,scale,scale);
render();
}
function render() {
renderer.render(scene, camera);
}
function texttexture(string) {
var fontFace = "Arial"
var size = "50";
var color = "white"
var squareTexture = true;
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.height = size;
var font = "Normal " + size + "px " + fontFace;
context.font = font;
var metrics = context.measureText(string);
var textWidth = metrics.width;
canvas.width = textWidth;
if (squareTexture) {
canvas.height = canvas.width;
}
var aspect = canvas.width / canvas.height;
context.textAlign = "center";
context.textBaseline = "middle";
context.fillStyle = color;
// Must set the font again for the fillText call
context.font = font;
context.fillText(string, canvas.width / 2, canvas.height / 2);
var t = new THREE.Texture(canvas);
t.needsUpdate = true;
return t;
}
If you want text to appear over a 3D scene and you don't care if it is static, why not try layering a div over the scene instead?
This will allow you to save graphics bandwidth and memory, improving performance of your scene and give you much better flexibility over what you display. It's also much easier to do and to maintain.