In three.js how can I know how much the camera has panned?
I need to keep an SVG layer on top of the three.js canvas aligned with the 3D objects as I pan the camera with orbitControls.
I use svg-pan-zoom for the SVG layer, and it works great for zooming.
For panning, I currently use the 'change' event and follow the camera accordingly like this (SVG and three.js setup omitted):
var unproject = function(vector3d, transform) {
var app = this.app;
var v = vector3d.clone();
v.project( app.three.camera );
v.x = Math.round( ( v.x + 1 ) * three_element.offsetWidth / 2 );
v.y = Math.round( ( - v.y + 1 ) * three_element.offsetHeight / 2 );
v.z = 0;
if (transform) {
var matrix = svg.node.getCTM().inverse()
var p = svg.node.createSVGPoint()
p.x = v.x; p.y = v.y
p = p.matrixTransform(matrix)
v.x = p.x
v.y = p.y
}
return v;
}
var panBy = function(d){ panZoom.panBy(d) };
var controls = new THREE.OrbitControls( camera, document.getElementById('canvas') );
var ref = unproject(new THREE.Vector3(0, 0, 0));
controls.addEventListener( 'change', function(e) {
var curr = unproject(new THREE.Vector3(0, 0, 0))
var dist = curr.clone().sub(ref)
panBy(dist)
ref = curr;
} );
But obviously the SVG panning lags a bit behind the 3D window panning. Is there a better way?
Related
I have been trying to make an app that takes models from google poly and puts them onto the scene in an iframe.
The initial problem was that models were too large or small so an optimum way was suggested to me by the aframe community which worked fine for a while but gave errors when scaling and rotation was being changed.
Here is the component I am using to make sure that models are properly scaled.
AFRAME.registerComponent('autoscale', {schema: {type: 'number', default: 1},
init: function () {
this.scale();this.el.addEventListener('object3dset', () => this.scale());},scale: function () {
const el = this.el;
const span = this.data;
const mesh = el.getObject3D('mesh');
if (!mesh) return;
const bbox = new THREE.Box3().setFromObject(mesh);
const scale = span / bbox.getSize().length();
var sx = this.el.getAttribute('scale').x;
var sy = this.el.getAttribute('scale').y;
var sz = this.el.getAttribute('scale').z;
var rx = this.el.getAttribute('rotation').x * (Math.PI / 180);
var ry = this.el.getAttribute('rotation').y * (Math.PI / 180);
var rz = this.el.getAttribute('rotation').z * (Math.PI / 180);
mesh.rotation.set(rx,ry,rz);
mesh.scale.set(scale*sx, scale*sy, scale*sz);
var a = new THREE.Box3().setFromObject(this.el.object3D);
var cx = (a.min.x + a.max.x)/2;
var cy = (a.min.y + a.max.y)/2;
var cz = (a.min.z + a.max.z)/2;
var posx = this.el.object3D.position.x;
var posy = this.el.object3D.position.y;
var posz = this.el.object3D.position.z;
console.log("boundingBox xyz: x: "+cx+", y: "+cy+" z: "+cz);
console.log("box position xyz: x: "+posx+", y: "+posy+" z: "+posz);
var translateX = posx - cx;
var translateY = posy - cy;
var translateZ = posz - cz;
this.el.object3DMap.mesh.translateX(translateX/sx);
this.el.object3DMap.mesh.translateY(translateY/sy);
this.el.object3DMap.mesh.translateZ(translateZ/sz);
}
});
There are 2 issues with the above approach:
When I scale the model from their attribute value like this: scale="2 10 2" with one being too large the center that is shown in the frame inspector messes up.
When I rotate model using attribute values the pivot goes off. I tried setting the rotation but no luck.
Any help will be appreciated on the code above.
I think maybe you want to build a transformation matrix with the transformation order you want (maybe scale then rotate then position or something?). THREE.js: Can you force a different order of operations for three.js?
There's been a-lot of questions around this but none of those have fixed my problem. Any image that I upload onto the object becomes pixelated regardless of the minFilter or magFilter that I use - and I've used all of them:
THREE.NearestFilter
THREE.NearestMipMapNearestFilter
THREE.NearestMipMapLinearFilter
THREE.LinearFilter
THREE.LinearMipMapNearestFilter
THREE.LinearMipMapLinearFilter
Here's the object with a pixelated image:
And here's a snapshot of how I'm loading the image on:
// Build a canvas object and add the image to it
var imageCanvas = this.getCanvas(imageLayer.guid, 'image');
var imageLoader = new THREE.ImageLoader();
imageLoader.load(imageUrl, img => {
// this.drawImage(img, gr, imageCanvas.canvas, imageCanvas.ctx);
var canvas = imageCanvas.canvas;
var ctx = imageCanvas.ctx;
canvas.width = 1024;
canvas.height = 1024;
var imgAspectRatioAdjustedWidth, imgAspectRatioAdjustedHeight;
var pushDownValueOnDy = 0;
var grWidth = canvas.width / 1.618;
if(img.width > img.height) {
grWidth = canvas.width - grWidth;
}
var subtractFromDx = (canvas.width - grWidth) / 2;
var grHeight = canvas.height / 1.618;
if(img.height > img.height) {
grHeight = canvas.height - grHeight;
}
var subtractFromDy = (canvas.height - grHeight) / 2;
var dx = (canvas.width / 2);
dx -= subtractFromDx;
var dy = (canvas.height / 2);
dy -= (subtractFromDy + pushDownValueOnDy);
imgAspectRatioAdjustedWidth = (canvas.width - grWidth) + 50;
imgAspectRatioAdjustedHeight = (canvas.height - grHeight) + 50;
ctx.globalAlpha = 0.5;
ctx.fillStyle = 'blue;'
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1.0;
ctx.drawImage(img, dx, dy, imgAspectRatioAdjustedWidth, imgAspectRatioAdjustedHeight);
});
After this the canvas data is added to an array to be painted onto the object - it is at this point that the CanvasTexture gets the mapped canvas:
var canvasTexture = new THREE.CanvasTexture(mainCanvas.canvas);
canvasTexture.magFilter = THREE.LinearFilter;
canvasTexture.minFilter = THREE.LinearMipMapLinearFilter;
// Flip the canvas
if(this.currentSide === 'front' || this.currentSide === 'back'){
canvasTexture.wrapS = THREE.RepeatWrapping;
canvasTexture.repeat.x = -1;
}
canvasTexture.needsUpdate = true;
// { ...overdraw: true... } seems to allow the other sides to be transparent so we can see inside
var material = new THREE.MeshBasicMaterial({map: canvasTexture, side: THREE.FrontSide, transparent: false});
for(var i = 0; i < this.layers[this.currentSide].length; i++) {
mainCanvas.ctx.drawImage( this.layers[this.currentSide][i].canvas, 0, 0, this.canvasWidth, this.canvasHeight);
}
Thanks to #2pha for the help as his suggestions lead me to the correct answer and, it turns out, that the pixelated effect was caused by different dimensions of the canvases.
For example the main canvas itself was 1024x1024 whereas the text & image canvases were only 512x512 pixels meaning that it would have to be stretched to cover the size of the main canvas.
I've trying to draw the square wall by getting mouse clicks coordinates and extrude it.
I've picking up the mouse coordinates by clicking at the scene.
var onDocumentMouseDown = function ( event )
{
//update the mouse variable
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = -( event.clientY / window.innerHeight ) * 2 + 1;
var vector = new THREE.Vector3(mouse.x, mouse.y, 0.5);
vector.unproject( camera );
var dir = vector.sub( camera.position ).normalize();
var distance = - camera.position.z / dir.z;
var pos = camera.position.clone().add( dir.multiplyScalar( distance));
console.log('mouse_x ' + pos.x + ' mouse_y ' + pos.y);
if (clickCount <= 3){
coord[clickCount] = {'x' : pos.x, 'y' : pos.y};
clickCount ++;
} else {
//make new wall and stop function
newshape = new THREE.Shape();
shape.moveTo(coord['0'].x ,coord['0'].y);
shape.lineTo(coord['0'].x, coord['1'].y);
shape.lineTo(coord['2'].x, +coord['2'].y);
shape.lineTo(coord['3'].x, coord['3'].y);
shape.lineTo(coord['0'].x, coord['0'].y);
var newextrudeSettings = {
//*******/
};
}
And when I've recived four coordinates, three.js throw the error:
Uncaught TypeError: Cannot read property 'length' of null
at Object.triangulateShape (three.js:26140)
at ExtrudeGeometry.addShape (three.js:26330)
at ExtrudeGeometry.addShapeList (three.js:26235)
at new ExtrudeGeometry (three.js:26211)
at HTMLDocument.onDocumentMouseDown (script.js:116)
To find points of intersection I prefer to use THREE.Raycaster() (though I've never used THREE.Projector() for this purpose).
This is the result of my code:
I hope I got your conception. Thus, all the stuff you need is here:
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var intersects;
var controlPoints = [];
var clickCount = 0;
function onMouseDown(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
intersects = raycaster.intersectObjects(objects); // objects is an array which contains just the mesh of the plane
if (intersects.length > 0) {
if (clickCount <= 3) { // I took your idea of 4 clicks
controlPoints[clickCount] = intersects[0].point.clone(); // add a control point to the array
// visualization of a control point
var cp = new THREE.Mesh(new THREE.SphereGeometry(0.125, 16, 12), new THREE.MeshBasicMaterial({color: "red"}));
cp.position.copy(intersects[0].point);
scene.add(cp);
clickCount++;
} else { // on the fifth click we'll create our wall
shape = new THREE.Shape();
shape.moveTo(controlPoints[0].x, -controlPoints[0].z);
shape.lineTo(controlPoints[1].x, -controlPoints[1].z);
shape.lineTo(controlPoints[2].x, -controlPoints[2].z);
shape.lineTo(controlPoints[3].x, -controlPoints[3].z);
shape.lineTo(controlPoints[0].x, -controlPoints[0].z);
var extrudeSettings = {
steps: 1,
amount: 2,
bevelEnabled: false
};
var extrudeGeom = new THREE.ExtrudeGeometry(shape, extrudeSettings);
extrudeGeom.rotateX(-Math.PI / 2);
var wall = new THREE.Mesh(extrudeGeom, new THREE.MeshStandardMaterial({
color: "gray"
}));
scene.add(wall);
controlPoints = []; // we clear the array of control points
clickCount = 0; // and reset the counter of clicks
};
};
};
jsfiddle example. 4 clicks for setting control points, the fifth click creates a wall, and so on.
The following jsFiddle http://jsfiddle.net/QsMVn/6/ has animated circles and the percentage showing how much of the circle has been filled. My aim is to have the percentages animated as well so that they move along with the line right next to the end of it. I can't figure out how to do that.
Code of jsFiddle:
// requestAnimationFrame Shim
(function() {
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
var endPercent = 85;
var curPerc = 0;
var counterClockwise = false;
var circ = Math.PI * 2;
var quart = Math.PI / 2;
context.lineWidth = 10;
context.strokeStyle = '#ad2323';
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.shadowBlur = 10;
context.shadowColor = '#656565';
function animate(current) {
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x, y, radius, -(quart), ((circ) * current) - quart, false);
context.stroke();
curPerc++;
if (curPerc < endPercent) {
requestAnimationFrame(function () {
animate(curPerc / 100);
});
}
}
animate();
You just use the angle you have (in radians) and calculate a distance based on that.
Prerequisites: Change a couple of lines above so you can reuse the radians:
var radians = (degrees - 90) * Math.PI / 180; // subtract 90 here
...
ctx.arc(W / 2, H / 2, W / 3, 0 - 90 * Math.PI / 180, radians, false);
Then use textAlign and textBaseline to center the text:
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
Calculate a position, demo shows text on the inside - for outside (or in the middle of arc) just adjust the dist value:
var dist = W / 3 - 40;
var tx = W * 0.5 + dist * Math.cos(radians);
var ty = H * 0.5 + dist * Math.sin(radians);
ctx.fillText(text, tx, ty);
Modified fiddle here
Hope this helps!
I was following mrdoobs examples of how to use raycasting to handle clickable objects. I have also looked at all of the many many similar questions and have tried countless things.
The raycasting works... If I am at a distance of less than 1.
The Raycaster is set to near 0 and far infinity though (defaults).
I haven't seen any code examples where distance was set.
I am hoping for another pair of eyes.
// snippet
glow.clickables = [];
var cubeGeo = new THREE.CubeGeometry(2, 2, 2);
cubeGeo.computeFaceNormals();
var cube = new THREE.Mesh(cubeGeo, redmat);
cube.position.y = 10;
cube.position.x = 0;
cube.position.z = -12;
cube.overdraw = true;
glow.Vis.scene.add(cube);
glow.clickables.push(cube);
onclick_();
var onclick_ = function() {
$('#world').on('mousedown', function(e){
var mouseX = (event.offsetX / $('#world').width()) * 2 - 1;
var mouseY = - ( event.offsetY / $('#world').height()) * 2 + 1;
var vector = new THREE.Vector3(mouseX, mouseY, 0.1); //what should z be set to?
//console.info(vector); // A vector between -1,1 for both x and y. Z is whatever is set on the line above
projector.unprojectVector(vector, glow.Vis.camera);
var conts = glow.Vis.controls.getObject().position; // control 3dObject which the camera is added to.
var clickRay = new THREE.Raycaster(conts, vector.sub(conts).normalize());
var intersects = clickRay.intersectObjects(glow.clickables);
console.info(intersects.length);
if(intersects.length > 0) {
alert("Click detected!");
}
});
}
Setting the mouse position this way is more accurate.
var rect = renderer.domElement.getBoundingClientRect();
mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
For mouse detecting (far or near! no matter!), do this:
put this in your global:
var pointerDetectRay, projector, mouse2D;
put this in your init() function:
pointerDetectRay = new THREE.Raycaster();
pointerDetectRay.ray.direction.set(0, -1, 0);
projector = new THREE.Projector();
mouse2D = new THREE.Vector3(0, 0, 0);
put this in your render() loop function:
pointerDetectRay = projector.pickingRay(mouse2D.clone(), camera);
and it's your mouse event:
function onDocumentMouseMove(event) {
event.preventDefault();
mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
Now, every where that you want to detect the objects under your mouse pointer, use this:
var intersects = pointerDetectRay.intersectObjects(scene.children);
if (intersects.length > 0) {
var intersect = intersects[0];
// intersect is the object under your mouse!
// do what ever you want!
}
I use the following which, for me, works for larger distances:
var projector = new THREE.Projector();
function onDocumentMouseDown( event ) {
event.preventDefault();
var vector = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1, 0.5 );
projector.unprojectVector(vector,camera);
var raycaster = new THREE.Raycaster(camera.position,vector.sub(camera.position).normalize() );
var intersects = raycaster.intersectObjects( [sphere,cylinder,cube] );
if ( intersects.length > 0 ) {
intersects[ 0 ].object.material.transparent=true;
intersects[ 0 ].object.material.opacity=0.1;
console.log(intersects[0]);
}
}
This just sets the first selected object to semitransparent. Complete example at: https://github.com/josdirksen/learning-threejs/blob/master/chapter-09/02-selecting-objects.html