Rotate webGL canvas to appear landscape-oriented on a portrait-oriented mobile phone - three.js

I’m using a-frame and trying to accomplish this task - force the canvas to be rendered as “landscape” when a mobile device is in portrait orientation (ie. device-width = 414px and device-height = 736px).
I have successfully accomplished this with the following steps
camera.aspect = 736 / 414;
camera.updateProjectionMatrix();
renderer.setSize(736, 414);
In css...
.a-canvas {
transform: rotate(90deg) translate(161px, 161px);
height: 414px !important;
width: 736px !important;
}
This all works great except for one major thing…I have 3D buttons in my scene and when I go to click them they don’t line up with the rotated canvas, instead their clickable position remains in the same place as before the canvas was rotated.
I’ve tried to set matrixWorldNeedsUpdate = true on the scene’s object3D along with updateWorldMatrix() with no luck. I tried calling refreshObjects on the raycaster with no luck. I tried rotating the scene and the camera with no luck.
I’m not sure what else to do. Any help would be greatly appreciated!
ANSWER:
Thanks to Marquizzo and gman for the help. Here's the updated a-frame source code (v1.0.4) to make the raycaster handle this forced landscape canvas properly
// line: 66884
onMouseMove: (function () {
var direction = new THREE.Vector3();
var mouse = new THREE.Vector2();
var origin = new THREE.Vector3();
var rayCasterConfig = {origin: origin, direction: direction};
return function (evt) {
var bounds = this.canvasBounds;
var camera = this.el.sceneEl.camera;
var left;
var point;
var top;
camera.parent.updateMatrixWorld();
// Calculate mouse position based on the canvas element
if (evt.type === 'touchmove' || evt.type === 'touchstart') {
// Track the first touch for simplicity.
point = evt.touches.item(0);
} else {
point = evt;
}
left = point.clientX - bounds.left;
top = point.clientY - bounds.top;
// mouse.x = (left / bounds.width) * 2 - 1;
// mouse.y = -(top / bounds.height) * 2 + 1;
// HAYDEN's CODE: flipping x and y coordinates to force landscape
// --------------------------------------------------------------
let clickX = (left / bounds.width) * 2 - 1;
let clickY = - (top / bounds.height) * 2 + 1;
mouse.x = -clickY;
mouse.y = clickX;
// --------------------------------------------------------------
origin.setFromMatrixPosition(camera.matrixWorld);
direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(origin).normalize();
this.el.setAttribute('raycaster', rayCasterConfig);
if (evt.type === 'touchmove') { evt.preventDefault(); }
};
})(),

A-Frame uses a Raycaster internally to determine if the spot you clicked has hit an object. You can see in the Three.js documentation the raycaster needs the mouse x&y coordinates to determine where you clicked. Here's a working demo of that concept. However with your setup, x, y turns into -y, x.
I think you'll have to write your own Raycaster function to trigger on the click event instead of relying on the built-in AFrame functionality, and then swap the x&y values:
function onClick() {
let clickX = ( event.clientX / window.innerWidth ) * 2 - 1;
let clickY = - ( event.clientY / window.innerHeight ) * 2 + 1;
mouse.x = -clickY;
mouse.y = clickX;
// Then continue with raycaster.setFromCamera(), etc...
}
window.addEventListener( 'click', onClick, false );

Related

Applying user selected texture to cube dynamically?

I’m creating a cube using BoxGeometry and trying to apply the texture that the user has selected.
I created a raycaster and use that raycaster inside mouse down event.
If the object is present on cursor position then a user-selected texture gets applied.
So far everything works fine. Then I added if-else condition if the user selected single image for texture then that single image gets applied whole body of cube means all side of cube gets that image as material.
But if the user selected more than one image suppose three images then only three sides of cube get material. This also works very well.
Now a real problem when user select only three images remaining side of cube remain black and that is okay only material applied side shows up.
Now if user again select a single image and click on cube to apply single image texture to whole body again ray caster gives problem that
“–can’t access property “side”, the material is undefined–”
so this callback function which i added to select image files
function onUserMultiImageSelect(event) {
materialArray.length = 0;
length = multiFilesId.files.length;
let multiImage = [];
let userMultiImageUrl = [];
for (let i = 0; i < length; i++) {
multiImage[i] = multiFilesId.files.item(i);
userMultiImageUrl[i] = URL.createObjectURL(multiImage[i]);
materialArray.push(new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load(userMultiImageUrl[i]),side:THREE.DoubleSide }));
}
isTextureSelect = true;
}
this is callback funtion which gets called when i click on object to apply texture
function onMouseButtonDown(event) {
var rect = event.target.getBoundingClientRect();
var mouseX = event.clientX - rect.left;
var mouseY = event.clientY - rect.top;
let raycaster = new THREE.Raycaster();
let pointer = new THREE.Vector2();
pointer.set((mouseX / window.innerWidth) * 2 - 1, -(mouseY / window.innerHeight) * 2 + 1);
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
var curentIntersectedObject = intersects[0];
if (isTextureSelect) {
if(length == 1){
var singleImage = multiFilesId.files.item(0);
var singleImageUrl = URL.createObjectURL(singleImage);
var texture = new THREE.TextureLoader().load(singleImageUrl);
curentIntersectedObject.object.material = new THREE.MeshBasicMaterial({ map: texture });
}else{
curentIntersectedObject.object.material = materialArray;
}
}
}
}

Rotate an object like OrbitControls but only the object itself

We love three.js! And here is a page we built using it a few years ago.
https://www.jgprolock.com
We are in the process of revising the animations on this site.
Once the page loads, the user has the ability to drag and rotate the object. But it is really a trick. We are using orbit controls to rotate the camera around our scene, and thus our main object which is centered in the scene (positions x,y,z all equal to 0). If we did not place the object in the center, it starts to look uneven in its rotation as the camera now is rotating around a center that the object doesn't have.
In order to make it look like the object is on the left side, we ended up moving the canvas to the left and then we bring it back to the right or left as the animation continues after scrolling.
So, my question is .. does anyone have an example how to achieve this functionality just by rotating the actual object itself, instead of rotating the camera around the entire scene using the orbit controls plugin?
Or is there away to modify the orbit controls to rotate around an object and not the entire scene?
I've been searching for this for a while but right after asking this question I came across this link, which actually has an example of what we are trying to do.
https://jsfiddle.net/n6u6asza/1205/
The key to making this work as copied from the link: (although I am not 100% sure what this all means)
/* */
var isDragging = false;
var previousMousePosition = {
x: 0,
y: 0
};
$(renderer.domElement).on('mousedown', function(e) {
isDragging = true;
})
.on('mousemove', function(e) {
//console.log(e);
var deltaMove = {
x: e.offsetX-previousMousePosition.x,
y: e.offsetY-previousMousePosition.y
};
if(isDragging) {
var deltaRotationQuaternion = new three.Quaternion()
.setFromEuler(new three.Euler(
toRadians(deltaMove.y * 1),
toRadians(deltaMove.x * 1),
0,
'XYZ'
));
cube.quaternion.multiplyQuaternions(deltaRotationQuaternion, cube.quaternion);
}
previousMousePosition = {
x: e.offsetX,
y: e.offsetY
};
});
/* */
If you want an article on how to achieve this without the use of unnecessary jquery dependencies you can have a look here
This uses the eventListener to find a mousemove event whilst a mousedown event is occurring, and then passes the coordinates to a custom function.
var mouseDown = false,
mouseX = 0,
mouseY = 0;
var canvas = renderer.domElement
canvas.addEventListener('mousemove', function (evt) {
if (!mouseDown) {return}
//console.log('drag')
evt.preventDefault();
var deltaX = evt.clientX - mouseX,
deltaY = evt.clientY - mouseY;
mouseX = evt.clientX;
mouseY = evt.clientY;
// DO SOMETHING HERE WITH X and Y
object.rotation.x += deltaX
}, false);
canvas.addEventListener('mousedown', function (evt) {
evt.preventDefault();
mouseDown = true;
mouseX = evt.clientX;
mouseY = evt.clientY;
}, false);
canvas.addEventListener('mouseup', function (evt) {
evt.preventDefault();
mouseDown = false;
}, false);
}
But not that this will not work if you have OrbitControls or DragControls imported!

Clickable points on Three.js globe

I have looked into using three.js but could not find any examples specific to what I need.
I am trying to create a 3D Map (on a globe) that has 3 different pin points around the world. I'd like to be able to detect which pinpoint was clicked on, in order to display information elsewhere on the page.
Is anyone aware of a way to do this? Any demos or tutorials would be very helpful.
Thank you.
Use an event listener to fire off a raycaster on mouse click. I quickly cut and pasted some code from one of my projects so that it does a raycast from the mouse in screenspace to an object in 3D space and updates the color of the material to red.
This should at least get you started in the right direction:
var mouse = new THREE.Vector2(0,0);
var raygun = new THREE.Raycaster();
var useRaycast = true;
// Raycast when we click the mouse
function onClick() {
// Get mouse position in screen space
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
// Only raycast if not panning (optimization)
var hits;
if (useRaycast) {
raygun.setFromCamera(mouse, camera);
// Raycast to single object
hits = raygun.intersectObject(myTargetObect, false);
// Raycast to multiple objects
// hits = raygun.intersectObjects([myTargetObect, myTargetObect2]);
}
// Run if we have intersections
if (hits.length > 0) {
for (var i = 0; i < hits.length; i++ ) {
// Change material color of item we clicked on
hits[i].object.material.color.set(0xff0000);
}
renderer.render(scene, camera);
}
}
window.addEventListener('click', onClick, false);
MrDoob also has an example on github with a web demo.

three js drag and drop Uncaught TypeError

I have been trying to implement the drag and drop functionality found here...
http://www.smartjava.org/tjscb/07-animations-physics/07.08-drag-n-drop-object-around-scene.html
Whenever I customise it slightly and use it in my project I get the following..
"Uncaught TypeError: Cannot read property 'point' of undefined"
whenever I try to drag a cube. The rotation isn't occurring so it must be recognising that I'm trying to drag an object and it relates to this line of code..
"selectedObject.position.copy(intersects[0].point.sub(offset))"
I assumed since I am new to all of this that I had messed up, so I copied all of the code from the link above into a new page (so should be identical) and ran it and I get the same thing (everything else works good)
Im probably missing something really stupid, I have searched for this and looked at other examples on how to achieve this, but since I was working my way through a book which explained everything I thought I would stick with this, and also it would be a good learning experience to figure out why its not working. If anyone could point me in the right direction I would appreciate it
<!DOCTYPE html>
<html>
<head>
<title>07.08 - Drag and drop object around scene</title>
<script type="text/javascript" src="js/threejs/three.min.js"></script>
<script type="text/javascript" src ="js/threejs/OrbitControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script>
// global variables
var renderer;
var scene;
var camera;
var cube;
var control;
var orbit;
// used for drag and drop
var plane;
var selectedObject;
var offset = new THREE.Vector3();
var objects = [];
// based on http://mrdoob.github.io/three.js/examples/webgl_interactive_draggablecubes.html
function init() {
// create a scene, that will hold all our elements such as objects, cameras and lights.
scene = new THREE.Scene();
// create a camera, which defines where we're looking at.
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
// create a render, sets the background color and the size
renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xffffff, 1.0);
renderer.setSize(window.innerWidth, window.innerHeight);
plane = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000, 18, 18), new THREE.MeshBasicMaterial({
color: 0x00ff00,
opacity: 0.25,
transparent: true
}));
plane.visible = false;
scene.add(plane);
var dirLight = new THREE.DirectionalLight();
dirLight.position.set(25, 23, 15);
scene.add(dirLight);
var dirLight2 = new THREE.DirectionalLight();
dirLight2.position.set(-25, 23, 15);
scene.add(dirLight2);
for (var i = 0; i < 200; i++) {
// create a cube and add to scene
var cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
var cubeMaterial = new THREE.MeshLambertMaterial({color: Math.random() * 0xffffff});
cubeMaterial.transparent = true;
cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
objects.push(cube);
cube.scale.x = Math.random() + 0.5 * 2;
cube.scale.y = Math.random() + 0.5 * 2;
cube.scale.z = Math.random() + 0.5 * 2;
cube.position.x = Math.random() * 50 - 25;
cube.position.y = Math.random() * 50 - 25;
cube.position.z = Math.random() * 50 - 25;
cube.rotation.x = Math.random() * Math.PI * 2;
cube.rotation.y = Math.random() * Math.PI * 2;
cube.rotation.z = Math.random() * Math.PI * 2;
scene.add(cube);
}
// position and point the camera to the center of the scene
camera.position.x = 35;
camera.position.y = 35;
camera.position.z = 53;
camera.lookAt(scene.position);
// add some controls so we can rotate
orbit = new THREE.OrbitControls(camera);
// add the output of the renderer to the html element
document.body.appendChild(renderer.domElement);
// call the render function
render();
}
function render() {
renderer.render(scene, camera);
orbit.update();
requestAnimationFrame(render);
}
document.onmousemove = function (event) {
// make sure we don't access anything else
event.preventDefault();
// get the mouse positions
var mouse_x = ( event.clientX / window.innerWidth ) * 2 - 1;
var mouse_y = -( event.clientY / window.innerHeight ) * 2 + 1;
// get the 3D position and create a raycaster
var vector = new THREE.Vector3(mouse_x, mouse_y, 0.5);
vector.unproject(camera);
var raycaster = new THREE.Raycaster(camera.position,
vector.sub(camera.position).normalize());
// first check if we've already selected an object by clicking
if (selectedObject) {
// check the position where the plane is intersected
var intersects = raycaster.intersectObject(plane);
// reposition the selectedobject based on the intersection with the plane
selectedObject.position.copy(intersects[0].point.sub(offset));
} else {
// if we haven't selected an object, we check if we might need
// to reposition our plane. We need to do this here, since
// we need to have this position before the onmousedown
// to calculate the offset.
var intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
// now reposition the plane to the selected objects position
plane.position.copy(intersects[0].object.position);
// and align with the camera.
plane.lookAt(camera.position);
}
}
};
document.onmousedown = function (event) {
// get the mouse positions
var mouse_x = ( event.clientX / window.innerWidth ) * 2 - 1;
var mouse_y = -( event.clientY / window.innerHeight ) * 2 + 1;
// use the projector to check for intersections. First thing to do is unproject
// the vector.
var vector = new THREE.Vector3(mouse_x, mouse_y, 0.5);
// we do this by using the unproject function which converts the 2D mouse
// position to a 3D vector.
vector.unproject(camera);
// now we cast a ray using this vector and see what is hit.
var raycaster = new THREE.Raycaster(camera.position,
vector.sub(camera.position).normalize());
// intersects contains an array of objects that might have been hit
var intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
orbit.enabled = false;
// the first one is the object we'll be moving around
selectedObject = intersects[0].object;
// and calculate the offset
var intersects = raycaster.intersectObject(plane);
offset.copy(intersects[0].point).sub(plane.position);
}
};
document.onmouseup = function (event) {
orbit.enabled = true;
selectedObject = null;
}
// calls the init function when the window is done loading.
window.onload = init;
</script>
</head>
<body>
</body>
</html>
"Uncaught TypeError: Cannot read property 'point' of undefined"
"selectedObject.position.copy(intersects[0].point.sub(offset))"
This means, intersects[0] is undefined which means the array intersects has no element (length = 0). You are using raycasting and it isn't working properly.
You should share your modified code so that we can check what is going wrong in your raycasting.
Update: I think your three.js version is greater than 71 while three.js version of this website is 71 or less. In the 72th version, there is an update in the raycaster -
Ignore invisible objects. (#mrdoob, #tschw)
So, the problem is here -
var intersects = raycaster.intersectObject(plane);
Since the plane is invisible, the intersectObject is returning empty array.
Workaround: I found a workaround. You can remove the following line -
plane.visible = false;
You can hide the material of the plane instead in the following way -
plane = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000, 18, 18), new THREE.MeshBasicMaterial({
color: 0xffff00,
opacity: 0.50,
transparent: true,
visible: false
}));
In this way, the raycaster will work properly and the plane will be invisible as well.

zooming based on the position of cursor in mousewheel

I am currently working on a THREE.js project.While using trackball control zooming it is zooming relative to the center of the model.But i want to zoom according to the position of cursor with mouse wheel.Any one who is familiar please help me over this.
Thank you in advance.
In mousewheel add this code which helps you in zooming based on the cursor position and not relative to the model.
If you are using trackball controls,you could use that for panning and rotating.
So set trackball controls enabled to false in mousewheel.
renderer.domElement.addEventListener('mousewheel',
function (e) {
mousewheel(e);
},
false);
function mousewheel(event) {
trackBallControls.noZoom = true;
event.preventDefault();
var factor = 50;
var mX = (event.clientX / jQuery(container).width()) * 2 - 1;
var mY = -(event.clientY / jQuery(container).height()) * 2 + 1;
var vector = new THREE.Vector3(mX, mY, 0.5);
//console.log(vector);
vector.unproject(camera);
vector.sub(camera.position);
if (event.deltaY < 0) {
camera.position.addVectors(camera.position,
vector.setLength(factor));
trackBallControls.target.addVectors(trackBallControls.target,
vector.setLength(factor));
camera.updateProjectionMatrix();
} else {
camera.position.subVectors(camera.position,
vector.setLength(factor));
trackBallControls.target.subVectors(trackBallControls.target,
vector.setLength(factor));
camera.updateProjectionMatrix();
}
}

Resources