catch the click event on a specific mesh in the renderer - javascript

I set a canvas renderer which contain two meshs (cubes). What i need to do is to catch the click event on each cube to call the convenient method for it.
So far, i could catch the click event on all the renderer, means when i click on cube1 and cube2, the click belong the same 'cause it's bound to the renderer :)
My question is, how to bind the click event on each cube?
My relevant code is the following:
//dom
var containerPopUp=document.getElementById('popup');
//renderer
var rendererPopUp = new THREE.CanvasRenderer();
rendererPopUp.setSize(420,200);
containerPopUp.appendChild(rendererPopUp.domElement);
//Scene
var scenePopUp = new THREE.Scene();
//Camera
var cameraPopUp = new THREE.PerspectiveCamera(50,60/60,1,1000);
cameraPopUp.position.z = 220;
cameraPopUp.position.y = 20;
//
scenePopUp.add(cameraPopUp);
//Add texture for the cube
//Use image as texture
var img2D = new THREE.MeshBasicMaterial({ //CHANGED to MeshBasicMaterial
map:THREE.ImageUtils.loadTexture('img/2d.png')
});
img2D.map.needsUpdate = true; //ADDED
//Add Cube
var cubeFor2D = new THREE.Mesh(new THREE.CubeGeometry(40,80,40),img2D);
cubeFor2D.position.x =- 60;
cubeFor2D.position.y = 20;
scenePopUp.add(cubeFor2D);
//
var img3D = new THREE.MeshBasicMaterial({ //CHANGED to MeshBasicMaterial
map:THREE.ImageUtils.loadTexture('img/3d.png')
});
img3D.map.needsUpdate = true;
var cubeFor3D = new THREE.Mesh(new THREE.CubeGeometry(40,80,40),img3D);
cubeFor3D.position.x = 60;
cubeFor3D.position.y=20;
scenePopUp.add(cubeFor3D);
//
rendererPopUp.render(scenePopUp,cameraPopUp);
//
animate();
rendererPopUp.domElement.addEventListener('click',testCall,false);//Here the click event is bound on the whole renderer, means what ever object in the renderer is clicked, the testCall method is called.
As you can see, cubeFor2D and cubeFor3D are contained in the renderer. I need to bind the click event on each mesh. I tried this with the threex.domevent.js:
var meshes = {};
meshes['mesh1'] = cubeFor2D;
meshes['mesh1'].on('mouseover', function(event){
//response to click...
console.log('you have clicked on cube 2D');
});
But it doesn't work, in the console, i got this error:
TypeError: meshes.mesh1.on is not a function
Of course, i included the API source code file:
<script src="threex.domevent.js"></script>

You can generate a callback like this. First define your callback function for each object:
mesh.callback = function() { console.log( this.name ); }
Then follow the standard picking pattern:
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onDocumentMouseDown( event ) {
event.preventDefault();
mouse.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
mouse.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );
var intersects = raycaster.intersectObjects( objects );
if ( intersects.length > 0 ) {
intersects[0].object.callback();
}
}
EDIT: updated to three.js r.70

Create a click handler
window.addEventListener('click', onDocumentMouseDown, false);
Define the function onDocumentMouseDown, note that raycaster the difference in above answer is the index position of the object clicked!
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onDocumentMouseDown( event ) {
event.preventDefault();
mouse.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
mouse.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );
console.log(scene.children);
var intersects = raycaster.intersectObjects( scene.children );
console.log(intersects[1]);
if ( intersects.length > 0 ) {
intersects[1].object.callback();
}}
Define the Mesh object
var mesh_menu_title = new THREE.Mesh(geometry_menu, materials_menu);
mesh_menu_title.name = 'select_lang';
mesh_menu_title.callback = function() { select_language();}
scene.add(mesh_menu_title);
define the callback function
function select_language(){
var selectedObject = scene.getObjectByName("select_lang");
scene.remove( selectedObject );
var selectedObject = scene.getObjectByName("start");
scene.remove( selectedObject );
var selectedObject = scene.getObjectByName("menu");
scene.remove( selectedObject );
}
So this code above will handle specific object clicked inside my canvas, then callback a function, the "mesh.callback" and it will remove some scene childs from the canvas.
It doesn't work if you use intersects[0].object.callback(); because at the index 0 the stored object are the vertices.

Related

How to bind onClick or onMouseEnter event to three.js?

Like their`s official example, we can use Raycaster to get current matched objects https://threejs.org/docs/?q=Raycaster#api/en/core/Raycaster.
The official example is:
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
function onPointerMove( event ) {
// calculate pointer position in normalized device coordinates
// (-1 to +1) for both components
pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
function render() {
// update the picking ray with the camera and pointer position
raycaster.setFromCamera( pointer, camera );
// calculate objects intersecting the picking ray
const intersects = raycaster.intersectObjects( scene.children );
for ( let i = 0; i < intersects.length; i ++ ) {
intersects[ i ].object.material.color.set( 0xff0000 );
}
renderer.render( scene, camera );
}
window.addEventListener( 'pointermove', onPointerMove );
window.requestAnimationFrame(render);
Accrodding to this example, in my understanding, I can just get intersects[i].object, which is a threejs Mesh class`s instance.
I want to bind onClick function to object3d like this way:
function createObject(id, position) {
// ...
const mesh = new Mesh()
// ...
mesh.onClick = () => handleClickFn(id, position);
scene.add(mesh);
}
Then I can call intersects[i].object.onClick(); to trigger it.
Every examples I found seems like they do some operation to intersects[i].object directly. Just like intersects[ i ].object.material.color.set( 0xff0000 );.
So, is there any way I can bind functions to each intersects[i].object like this?

Select, Update and Manipulate an Obj file using threejs

I am building a 3D Visualization and Interactive application using threejs.Following are the key functionalities I want to provide in this application:
In this User should be able to:
Rotate and Scale the Obj. -- done
Manipulate some certain parts of the Obj like, changing its color, replace that part with another. -- pending
I am following the vast threejs
documentation
and its list of examples, which
really helped me a lot and I am able to achieve a little.
Also I have come across an useful threejs inspector Chrome
Ext.
This threejs inspector Chrome Ext all in all does everything what I want to achieve, but unfortunately I am not able to understand that how does it work and how does it able to select and manipulate the parts of an Obj file.
I am using the following piece of code using threejs for now to just display, rotate and scale the Obj file.
Updated Code:
if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
var container, camera, controls, scene, renderer, mesh;
var mtlObject = {};
var strDownloadMime = "image/octet-stream";
init();
animate();
function init() {
var saveLink = document.createElement('div');
saveLink.style.position = 'absolute';
saveLink.style.top = '10px';
saveLink.style.width = '100%';
saveLink.style.color = 'white !important';
saveLink.style.textAlign = 'center';
saveLink.innerHTML ='Save Frame';
document.body.appendChild(saveLink);
document.getElementById("saveLink").addEventListener('click', saveAsImage);
renderer = new THREE.WebGLRenderer({
preserveDrawingBuffer: true
});
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.z = 500;
controls = new THREE.TrackballControls( camera );
controls.rotateSpeed = 2.0;
controls.zoomSpeed = 2.0;
controls.panSpeed = 2.0;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [ 65, 83, 68 ];
controls.addEventListener( 'change', render );
// world
scene = new THREE.Scene();
var ambient = new THREE.AmbientLight( 0x444444 );
scene.add( ambient );
var directionalLight = new THREE.DirectionalLight( 0xffeedd );
directionalLight.position.set( 0, 0, 1 ).normalize();
scene.add( directionalLight );
// model
var onProgress = function ( xhr ) {
if ( xhr.lengthComputable ) {
var percentComplete = xhr.loaded / xhr.total * 100;
console.log( Math.round(percentComplete, 2) + '% downloaded' );
}
};
var onError = function ( xhr ) { };
//mtl loader
THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );
var mtlLoader = new THREE.MTLLoader();
mtlLoader.setPath( 'obj/' );
mtlLoader.load( 'arm.mtl', function( materials ) {
materials.preload();
var objLoader = new THREE.OBJLoader();
objLoader.setMaterials( materials );
objLoader.setPath( 'obj/' );
objLoader.load( 'arm.obj', function ( object ) {
object.name = "object_name";
object.position.y = - 95;
scene.add( object );
//As 'TheJim01' suggested
//I have used an object variable.
//then traverse through the scene nodes
//and target some particular parts of the obj as:
var nameToObject = {};
scene.traverse( function( node ) {
nameToObject[node.name] = node;
if (node.name == ("Pad01")) {
node.visible = false;
}
if (node.name == ("Arm_01")) {
node.visible = false;
}
if (node.name == ("Pad02")) {
node.visible = false;
}
if (node.name == ("Arm_02")) {
node.visible = false;
}
});
}, onProgress, onError );
});
// lights
var light = new THREE.DirectionalLight( 0xffffff );
light.position.set( 1, 1, 1 );
scene.add( light );
var light = new THREE.DirectionalLight( 0x002288 );
light.position.set( -1, -1, -1 );
scene.add( light );
var light = new THREE.AmbientLight( 0x222222 );
scene.add( light );
// renderer
renderer = new THREE.WebGLRenderer( { antialias: false } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
container = document.getElementById( 'container' );
container.appendChild( renderer.domElement );
//
window.addEventListener( 'resize', onWindowResize, false );
//
render();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
controls.handleResize();
render();
}
function animate() {
requestAnimationFrame( animate );
controls.update();
}
function render() {
renderer.render( scene, camera );
}
//my next challenge is to save the canvas as image
//after making all the changes to the obj file
function saveAsImage() {
var imgData, imgNode;
try {
var strMime = "image/jpeg";
imgData = renderer.domElement.toDataURL(strMime);
saveFile(imgData.replace(strMime, strDownloadMime), "test.jpg");
} catch (e) {
console.log(e);
return;
}
}
var saveFile = function (strData, filename) {
var link = document.createElement('a');
if (typeof link.download === 'string') {
document.body.appendChild(link); //Firefox requires the link to be in the body
link.download = filename;
link.href = strData;
link.click();
document.body.removeChild(link); //remove the link when done
} else {
location.replace(uri);
}
}
$(document).ready(function() {
//Set Color of the Obj parts accordingly
$('#armblock').on('click', function(){
$(this).children('ul').slideToggle(400);
$(this).children('ul').children('li').on('click', function(){
$color = new THREE.Color($(this).css('backgroundColor'));
var selectedColor = '#' + $color.getHexString();
//As 'TheJim01' suggested
//I have used and object variable.
//then traverse through the scene nodes
//and target some perticular parts of the obj as:
var nameToObject = {};
scene.traverse( function( node ) {
nameToObject[node.name] = node;
if (node.name == ("Arm_01")) {
node.visible = true;
nameToObject["Arm_01"].material.color.set(selectedColor);
}
if (node.name == ("Arm_02")) {
node.visible = true;
nameToObject["Arm_02"].material.color.set(selectedColor);
}
});
});
});
$('#padblock').on('click', function(){
$(this).children('ul').slideToggle(400);
$(this).children('ul').children('li').on('click', function(){
$color = new THREE.Color($(this).css('backgroundColor'));
var selectedColor = '#' + $color.getHexString();
//As 'TheJim01' suggested
//I have used an object variable.
//then traverse through the scene nodes
//and target some particular parts of the obj as:
var nameToObject = {};
scene.traverse( function( node ) {
nameToObject[node.name] = node;
if (node.name == ("Pad01")) {
node.visible = true;
nameToObject["Pad01"].material.color.set(selectedColor);
}
if (node.name == ("Pad02")) {
node.visible = true;
nameToObject["Pad02"].material.color.set(selectedColor);
}
});
});
});
});
Please if anyone can help me out in this.
Thanks in advance and please comment if I am missing anything.
Update
Next Challenges
I am able to change the color of a particular node(part of the obj) but its not spontaneous as I have to click on the canvas/obj again to see the changes.
I am able to hide/show a particular node(part of the obj) but I want to replace that particular node(part of the obj) with another one.
I want to save the final obj file after making all the changes as an Image, but in future as an gif or video so that user can visualize 360deg view of the final product.
PS
TheJim01 helped me a lot into understanding the basic but very important concept of traversing the obj file and its parts.
That leads me to build at least closer to something what I want.
All three.js inspector is doing is parsing the scene, and displaying the various properties of the objects in an interactive UI.
Let's say you have an OBJ file arranged like this:
bike
frame
seat
drive
pedals
frontSprocket
chain
rearSprocket
rearWheel
steering
handlebars
fork
frontWheel
OBJLoader would create a scene hierarchy like this:
bike // THREE.Group
frame // THREE.Mesh
seat // THREE.Mesh
drive // THREE.Group
pedals // THREE.Mesh
frontSprocket // THREE.Mesh
chain // THREE.Mesh
rearSprocket // THREE.Mesh
rearWheel // THREE.Mesh
steering // THREE.Group
handlebars // THREE.Mesh
fork // THREE.Mesh
frontWheel // THREE.Mesh
three.js inspector displays this same hierarchy, using the names of the objects. When you click on an object in its tree, it references the object in the scene, and grabs/displays its properties, such as its position, rotation, visible state, etc. When you make a change in the three.js inspector UI, it sets the value on the object in the scene, resulting in the changes you see.
You can do all of this yourself, and you don't even need to be so general about it. Say you want to create a map of object name to the scene object for easier reference (searching the scene is fast enough, but it's recursive). So you could do this:
var nameToObject = {};
scene.traverse(function(node){
// watch out for duplicate empty names!
nameToObject[node.name] = node;
});
(That doesn't give you the hierarchy, but this is just an example.)
Now you can get and update any object by name:
// enlarge the rear tire
nameToObject["rearTire"].scale.addScalar(0.1);
You can read and set all properties of the object. For example, if MTLLoader created a basic material for the frame, you could do something like this:
// make the frame red
nameToObject["frame"].material.color.setRGB(1.0, 0.0, 0.0);
Or you could outright replace the entire material.
For your example of replacing an object, let's say you already loaded a new Mesh called newRearTire...
// replace the rear tire
var drive = nameToObject["drive"]; // the parent of rearTire
drive.remove(nameToObject["rearTire"]);
drive.add(newRearTire);
(Of course you would need to re-build your name map at this point.)
These are just very general examples, but should get you started. If you encounter any problems accessing your data, leave a comment and I'll try to clarify.

Can I add an invisible bounding box to a three.js scene?

I am trying to detect a click on a bounding box for an object (rather than just on the object itself - more clickable area). When I load the object like this:
var loader2 = new THREE.ObjectLoader();
loader2.load( "models/Platform/Platform.json", function(object, materials){
object.rotation.x = - (Math.PI / 2);
object.rotation.y = Math.PI;
object.scale.set(.025, .025, .025);
object.position.set(0, 1, .4);
var bbox = new THREE.BoundingBoxHelper(object, 0xffffff);
bbox.update();
scene.add(object);
scene.add(bbox);
objects.push(bbox);
});
And detect the click like this:
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
document.addEventListener( 'mousedown', onDocumentMouseDown, false );
document.addEventListener( 'touchstart', onDocumentTouchStart, false );
window.addEventListener( 'resize', onWindowResize, false );
function onDocumentTouchStart( event ) {
event.preventDefault();
event.clientX = event.touches[0].clientX;
event.clientY = event.touches[0].clientY;
onDocumentMouseDown( event );
}
function onDocumentMouseDown( event ) {
console.log("here");
event.preventDefault();
mouse.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
mouse.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );
console.log(mouse.x);
console.log(mouse.y);
var intersects = raycaster.intersectObjects( objects, true );
if ( intersects.length > 0 ) {
console.log("click");
}
The bounding box shows up correctly, and I can click on it!!!!! However, the bounding box is visible on the screen:
I want the bounding box to be transparent/invisible/hidden. Is there any way I can have a bounding box attached to the object which is clickable but not visible?
I read that to make the bounding box invisible I should remove the scene.add(bbox); (not add it to the scene), but if I do that, then it is not in the scene for the ray to intersect, and thus the click is not registered.
Solutions?
Thanks so much!!!
You can try to set the material to invisible:
bbox.material.visible = false;
So, there seem to be (at least) two solutions.
As suggested by #prisoner849:
bbox.material.opacity = 0;
bbox.material.transparent = true;
As suggested by #tomacco and refined by #WestLangley:
bbox.material.visible = false;
Both of these solutions worked for me!

Three.js invisible plane not working with raycaster.intersectObject

I am trying to make draggable objects, as seen in this example: https://www.script-tutorials.com/demos/467/index.html
The objects which should be draggable are in the array objectMoverLines.
I have added a plane to my scene with the following code:
plane = new THREE.Mesh(new THREE.PlaneBufferGeometry(500, 500, 8, 8), new THREE.MeshBasicMaterial({color: 0x248f24, alphaTest: 0}));
plane.visible = false;
scene.add(plane);
The problem occurs under the onDocumentMouseDown function. For some reason, if the planes visibility is set to false (plane.visible = false), then at a certain point, intersectsobjmovers will not be populated. If the plane's visibility is set to true, however, it will work fine (but obviously, that causes a huge plane to be in the way of everything):
function onDocumentMouseDown(event) {
// Object position movers
var vector = new THREE.Vector3(mouse.x, mouse.y, 1);
vector.unproject(camera);
raycaster.set( camera.position, vector.sub( camera.position ).normalize() );
var intersectsobjmovers = raycaster.intersectObjects(objectMoverLines);
if (intersectsobjmovers.length > 0) {
console.log('clicking an object mover');
// Disable the controls
controls.enabled = false;
// Set the selection - first intersected object
objmoverselection = intersectsobjmovers[0].object;
// Calculate the offset
var intersectsobjmovers = raycaster.intersectObject(plane);
// At this point, intersectsobjmovers does not include any items, even though
// it should (but it does work when plane.visible is set to true...)
offset.copy(intersectsobjmovers[0].point).sub(plane.position);
} else {
controls.enabled = true;
}
}
Also, this is what I currently have under the onDocumentMouseMove function:
function onDocumentMouseMove(event) {
event.preventDefault();
mouse.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
mouse.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1;
// Get 3D vector from 3D mouse position using 'unproject' function
var vector = new THREE.Vector3(mouse.x, mouse.y, 1);
vector.unproject(camera);
// Set the raycaster position
raycaster.set( camera.position, vector.sub( camera.position ).normalize() );
if (objmoverselection) {
// Check the position where the plane is intersected
var intersectsobjmovers = raycaster.intersectObject(plane);
// Reposition the object based on the intersection point with the plane
objmoverselection.position.copy(intersectsobjmovers[0].point.sub(offset));
} else {
// Update position of the plane if need
var intersectsobjmovers = raycaster.intersectObjects(objectMoverLines);
if (intersectsobjmovers.length > 0) {
// var lookAtVector = new THREE.Vector3(0,0, -1);
// lookAtVector.applyQuaternion(camera.quaternion);
plane.position.copy(intersectsobjmovers[0].object.position);
plane.lookAt(camera.position);
}
}
requestAnimationFrame( render );
}
try this:
plane = new THREE.Mesh(new THREE.PlaneBufferGeometry(500, 500, 8, 8),
new THREE.MeshBasicMaterial( {
color: 0x248f24, alphaTest: 0, visible: false
}));
scene.add(plane);

Want to have click event on the .obj file shown on the webpage

I have a .obj file on the web page using Three js.
My aim is, when I drag the mouse left/right, the OBJ model should rotate which I am able to do so USING THREE.TrackballControls().
Next thing is, I want to touch on the specific points on that OBJ model and if the mouse is down on those points something would happen(like a counter increase which will be shown on the web page).
I have seen DOMevents for three js but it looks like it allows us to click on the whole object not on specific points on the objects.
How can I achieve that?
You have to create a raycaster. (r69)
mouse_vector = new THREE.Vector3(),
mouse = { x: 0, y: 0, z: 1 };
var vector = new THREE.Vector3();
var raycaster = new THREE.Raycaster();
var dir = new THREE.Vector3();
function onMouseDown( event_info )
{
event_info.preventDefault();
mouse.x = ( event_info.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event_info.clientY / window.innerHeight ) * 2 + 1;
mouse_vector.set( mouse.x, mouse.y, mouse.z );
mouse_vector.unproject(camera);
var direction = mouse_vector.sub( camera.position ).normalize();
ray = new THREE.Raycaster( camera.position, direction );
ray.set( camera.position, direction );
intersects = ray.intersectObjects(scene.children, true);
if( intersects.length )
{
intersects.forEach(function(clicked)
{
// Your stuff like
if (clicked.object.typ == 'yourObject')
{
//Event on click..
}
});
}
}

Categories