I am creating a flight animation, I have objects (airplanes) moving along a curve path. My problem is some of my objects are rotating while travelling along the curve path.
Screenshot
function createPlane(image) {
return new THREE.Mesh(
new THREE.PlaneGeometry(5, 5),
new THREE.MeshPhongMaterial({ map: new THREE.TextureLoader().load('textures/planes/' + image), side: THREE.DoubleSide, transparent: true })
);
}
// this is how we get the line.vertices
// var curve = new THREE.Curve();
// line.vertices = curve.getPoints(50);
function animatePlane(plane, line, key) {
if (key > 1) {
key = 1;
} else {
key = Math.round(key * 100) / 100;
}
var curve = new THREE.CatmullRomCurve3(line.vertices), up = new THREE.Vector3(0, 1, 0), axis = new THREE.Vector3();
var angle, duration = 300, position = curve.getPointAt(key), tangent = curve.getTangentAt(key).normalize();
// rotation
axis.crossVectors(up, tangent).normalize();
angle = Math.acos(up.dot(tangent));
plane.quaternion.setFromAxisAngle(axis, angle);
// position
new TWEEN.Tween(plane.position)
.to({ x: position.x, y: position.y, z: position.z }, duration)
.onUpdate(function() {
plane.position.set(this.x, this.y, this.z);
})
.onComplete(function() {
if (key < 1) {
key += 0.02;
} else {
key = 0;
}
animatePlane(plane, line, key);
})
.start();
}
// lines
var point = latLongToVector3(14.512274, 121.016508, radius, 0), // Philippines
line1 = createCurveLine(createSphereArc(point, latLongToVector3(2.745364, 101.707079, radius, 0), 0.3), green, 'MALAYSIA');
animatePlane(createPlane('airplane.png'), line1, 0);
The above code is how I create and what moves my objects along the curve path.
Note that curve paths were dynamically created, from the screenshot above it is from Philippines going to other countries but it could be from other countries.
Curve paths may lie in different part of the earth, for example, from Australia (which is at the bottom part of the earth) going to other countries.
Is there anything I can do to always keep my airplane upright as it is moving along the curve path?
I got it working now by using Frenet–Serret formulas (TNB Frame).
I used Tangent, Normal, and Binormal to create a matrix (Matrix4) then used that matrix to set the rotation of planes by using .setRotationFromMatrix().
This is now the code to animate the planes:
function animatePlane(plane, line, key) {
if (key > 1) {
key = 1;
} else {
key = Math.round(key * 100) / 100;
}
var curve = new THREE.CatmullRomCurve3(line.vertices), matrix = new THREE.Matrix4();
var duration = 300, position = curve.getPointAt(key);
if ((key > 0) && (key < 1)) {
var D = curve.getPointAt(1).normalize(), X = plane.position.clone().normalize();
var T = position.clone().normalize(), B = curve.getTangentAt(key), N = new THREE.Vector3();
N.crossVectors(D, T).normalize();
matrix.set(
N.x, B.x, T.x, X.x,
N.y, B.y, T.y, X.y,
N.z, B.z, T.z, X.z,
0, 0, 0, 1
);
plane.setRotationFromMatrix(matrix);
}
new TWEEN.Tween(plane.position)
.to({ x: position.x, y: position.y, z: position.z }, duration)
.onUpdate(function() {
plane.position.set(this.x, this.y, this.z);
})
.onComplete(function() {
if (key < 1) {
key += 0.02;
} else {
key = 0;
}
animatePlane(plane, line, key);
})
.start();
}
and added this to make the plane above the curve path:
var plane1 = createPlane('airplane.png');
plane1.renderOrder = 1;
plane1.onBeforeRender = function(renderer) {
renderer.clearDepth();
}
animatePlane(plane1, line1, 0);
Related
I'm trying to implement a simple turn-around-and-move feature with Three.js. On mouse click, the object is supposed to first turn around and then move to the clicked location.
Codepen
The rotation is achieved with raycasting and lookAt(). It works by itself and it always works on the first click. If you remove the translation, it works continuously. The issue occurs when rotation and translation are implemented together. If you click a second time, after the object has moved to the previous clicked location, it doesn't rotate as expected. Depending on the mouse location it can flip to the other side without rotating at all.
Clarification: When you click the first time, notice how the object slowly and steadily turns around to face that direction? But the second time, after the object has moved, the rotation is quicker and/or flimsier or it simply flips over and there is no rotation at all. It depends on where you click in relation to the object.
I believe the issue stems from trying to implement lookAt while being located at the current lookAt location? If I stop the translation half way, the next rotation will work better. But of course I need it to go all the way.
I'm somewhat lost on how to proceed with this issue. Any help would be appreciated.
/*** Setup scene ***/
let width = 800
let height = 600
let scene
let renderer
let worldAxis
let box
let angle
let boxAxes
scene = new THREE.Scene()
worldAxis = new THREE.AxesHelper(200);
scene.add(worldAxis);
// Setup renderer
renderer = new THREE.WebGLRenderer({alpha: true, antialias: true})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height)
document.body.appendChild(renderer.domElement)
// Setup camera
const camera = new THREE.OrthographicCamera(
width / - 2, // left
width / 2, // right
height / 2, // top
height / - 2, // bottom
0, // near
1000 ); // far
camera.position.set(0, 0, 500)
camera.updateProjectionMatrix()
// Setup box
let geometry = new THREE.BoxGeometry( 15, 15, 15 );
let material = new THREE.MeshBasicMaterial( { color: "grey" } );
box = new THREE.Mesh( geometry, material );
box.position.set(100, 150, 0)
box.lookAt(getPointOfIntersection(new THREE.Vector2(0, 0)))
addAngle()
boxAxes = new THREE.AxesHelper(50);
box.add(boxAxes)
scene.add(box)
renderer.render(scene, camera);
/*** Setup animation ***/
let animate = false
let currentlyObservedPoint = new THREE.Vector2();
let rotationIncrement = {}
let translationIncrement = {}
let frameCount = 0
document.addEventListener('click', (event) => {
let mousePosForRotate = getMousePos(event.clientX, event.clientY)
rotationIncrement.x = (mousePosForRotate.x - currentlyObservedPoint.x)/100
rotationIncrement.y = (mousePosForRotate.y - currentlyObservedPoint.y)/100
let mousePosForTranslate = getMousePosForTranslate(event)
translationIncrement.x = (mousePosForTranslate.x - box.position.x)/100
translationIncrement.y = (mousePosForTranslate.y - box.position.y)/100
animate = true
})
function animationLoop() {
if (animate === true) {
if (frameCount < 100) {
rotate()
} else if (frameCount < 200) {
translate()
} else {
animate = false
frameCount = 0
}
frameCount++
renderer.render(scene, camera)
}
requestAnimationFrame(animationLoop)
}
function rotate() {
currentlyObservedPoint.x += rotationIncrement.x
currentlyObservedPoint.y += rotationIncrement.y
let pointOfIntersection = getPointOfIntersection(currentlyObservedPoint)
box.lookAt(pointOfIntersection)
addAngle()
}
function translate() {
box.position.x += translationIncrement.x
box.position.y += translationIncrement.y
}
function getMousePos(x, y) {
let mousePos = new THREE.Vector3(
(x / width) * 2 - 1,
- (y / height) * 2 + 1,
0)
return mousePos
}
function getMousePosForTranslate(event) {
let rect = event.target.getBoundingClientRect();
let mousePos = { x: event.clientX - rect.top, y: event.clientY - rect.left }
let vec = getMousePos(mousePos.x, mousePos.y)
vec.unproject(camera);
vec.sub(camera.position).normalize();
let distance = - camera.position.z / vec.z;
let pos = new THREE.Vector3(0, 0, 0);
pos.copy(camera.position).add(vec.multiplyScalar(distance));
return pos
}
function getPointOfIntersection(mousePos) {
let plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
let pointOfIntersection = new THREE.Vector3()
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mousePos, camera)
raycaster.ray.intersectPlane(plane, pointOfIntersection)
return pointOfIntersection
}
function addAngle() {
let angle = box.rotation.x - 32
box.rotation.x = angle
}
animationLoop()
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js'></script>
how can I get line width from 2D sheet to create custom line with correct width or relative original line width.
VertexBufferReader returns is two point without line width.
Are you looking for this?
onLineSegment(x1, y1, x2, y2, vpId, lineWidth) has a lineWidth argument at the end of the function argument
onTexQuad(centerX, centerY, width, height, rotation, vpId) has a width argument in the function argument
Snapshots:
Demo code:
function GeometryCallback(viewer,copiedGeometryName,curveMaterial) {
this.viewer = viewer;
this.opiedGeometryName = copiedGeometryName;
// linewidth does not take effect in Chrome and Firefox
// It is a known issue with OpenGL core
// try with Safari
this.curveMaterial = curveMaterial;
this.is2PITimes = function(angle1,angle2){
return Math.abs(angle1-angle2)/Math.PI % 2 == 0
}
}
GeometryCallback.prototype.onLineSegment = function(x1, y1, x2, y2, vpId, lineWidth) {
var vpXform = this.viewer.model.getPageToModelTransform(vpId);
//if in CAD coordinate system, applyMatrix4 with vpXform
var pt1 = new THREE.Vector3().set(x1, y1, 0)//.applyMatrix4(vpXform);
var pt2 = new THREE.Vector3().set(x2, y2, 0)//.applyMatrix4(vpXform);
console.log('Line segment vertices coordinates: ', {
pointX1: pt1.x,
pointY1: pt1.y,
pointX2: pt2.x,
pointY2: pt2.y
},
'width:', lineWidth
);
//add overlay geometry
var geometry = new THREE.Geometry ()
geometry.vertices.push (new THREE.Vector3 ( pt1.x, pt1.y, 0))
geometry.vertices.push (new THREE.Vector3 ( pt2.x, pt2.y, 0))
var lines = new THREE.Line (geometry,
this.curveMaterial,
THREE.LinePieces)
this.viewer.impl.addOverlay (this.opiedGeometryName, lines)
this.viewer.impl.invalidate (false,false,true)
}
GeometryCallback.prototype.onCircularArc = function(cx, cy, start, end, radius, vpId) {
var vpXform = this.viewer.model.getPageToModelTransform(vpId);
//if in CAD coordinate system, applyMatrix4 with vpXform
var center = new THREE.Vector3().set(cx, cy, 0)//.applyMatrix4(vpXform);
console.log('CircleArc segment: ', {
centerX: center.x,
centerY: center.y,
radius: radius,
startAngle: start,
endAngle: end
});
//add overlay geometry
var curve = new THREE.EllipseCurve(
center.x, center.y,
radius, radius,
start, end,
false
);
var path = new THREE.Path(curve.getPoints(50));
var geometry = path.createPointsGeometry(50);
//remove last vertex if it an arc
if(!this.is2PITimes(start,end))
geometry.vertices.pop();
var circularArc = new THREE.Line(geometry, this.curveMaterial);
this.viewer.impl.addOverlay (this.opiedGeometryName, circularArc)
this.viewer.impl.invalidate (false,false,true)
};
GeometryCallback.prototype.onEllipticalArc = function(cx, cy, start, end, major, minor, tilt, vpId) {
var vpXform = this.viewer.model.getPageToModelTransform(vpId);
//if in CAD coordinate system, applyMatrix4 with vpXform
var center = new THREE.Vector3().set(cx, cy, 0)//.applyMatrix4(vpXform);
console.log('EllipticalArc segment: ', {
centerX: center.x,
centerY: center.y,
radius: radius,
startAngle: start,
endAngle: end
});
//add overlay geometry
var curve = new THREE.EllipseCurve(
center.x, center.y,
major, minor,
start, end,
false
);
var path = new THREE.Path(curve.getPoints(50));
var geometry = path.createPointsGeometry(50);
//remove last vertex if it an arc
if(!this.is2PITimes(start,end))
geometry.vertices.pop();
var ellipticalArc = new THREE.Line(geometry, this.curveMaterial);
this.viewer.impl.addOverlay (this.opiedGeometryName, ellipticalArc)
this.viewer.impl.invalidate (false,false,true)
};
GeometryCallback.prototype.onOneTriangle = function(x1, y1, x2, y2, x3, y3, vpId){
//Similar logic as above
};
GeometryCallback.prototype.onTexQuad = function(centerX, centerY, width, height, rotation, vpId){
//from VertexBufferReader.js:
//Currently this case does not actually come up
};
//extension of MyShow2dCurve
function MyShow2dCurve(viewer, options) {
Autodesk.Viewing.Extension.call(this, viewer, options)
var _viewer = this.viewer
var _copiedGeometryName = 'copiedGeometryName'
// linewidth does not take effect in Chrome and Firefox
// It is a known issue with OpenGL core
// try with Safari
var _curveMaterial = new THREE.LineBasicMaterial ({
color: new THREE.Color (0xFF0000),
transparent: true,
depthWrite: false,
depthTest: false,
linewidth: 5,
opacity: 1.0
})
//when extension is loaded
this.load = function() {
console.log('MyShow2dCurve is loaded!');
//bind keyup event
$(document).bind('keyup', onKeyUp);
_viewer.impl.invalidate(true);
return true;
};
//when extension is unloaded
this.unload = function() {
console.log('MyShow2dCurve is now unloaded!');
//unbind keyup event
$(document).unbind('keyup', this.onKeyUp);
return true;
};
//when key up
function onKeyUp(evt) {
console.log('onKeyUp:' + evt.keyCode);
//when key 'S' is pressed
if(evt.keyCode == 83){
//create overlay
_viewer.impl.createOverlayScene (_copiedGeometryName, _curveMaterial)
//start to monitor select event
_viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, onSelectObj)
}
//when key 'Q' is pressed
if(evt.keyCode == 81){
//undelegate selection event
_viewer.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, onSelectObj)
//remove overlay
_viewer.impl.removeOverlayScene(_copiedGeometryName);
_viewer.impl.invalidate(false, false, true);
}
return true;
}
//when selecting object
function onSelectObj(evt){
console.log(evt)
//selected object id
var dbId = evt.dbIdArray[0]
//get instance tree
var it = _viewer.model.getData().instanceTree;
//dump fragments of the object
it.enumNodeFragments( dbId, function( fragId ) {
//get each fragment
var m = _viewer.impl.getRenderProxy(_viewer.model, fragId);
//initialize VertexBufferReader
var vbr = new Autodesk.Viewing.Private.VertexBufferReader(m.geometry, _viewer.impl.use2dInstancing);
//dump geometry of this fragment
vbr.enumGeomsForObject(dbId, new GeometryCallback(_viewer,_copiedGeometryName, _curveMaterial));
});
}
}
MyShow2dCurve.prototype = Object.create(Autodesk.Viewing.Extension.prototype);
MyShow2dCurve.prototype.varructor = MyShow2dCurve;
Autodesk.Viewing.theExtensionManager.registerExtension('MyShow2dCurve', MyShow2dCurve);
Modified from: https://forge.autodesk.com/blog/dump-geometries-2d-curve
I have n+1 hexshapes in a honeycomb grid. The objects are stacked close together. With this code:
// Get intersected objects, a.k.a objects "hit" by mouse, a.k.a objects that are mouse-overed
const intersects = raycaster.intersectObjects(hexObjects);
// If there is one (or more) intersections
let scaleTween = null;
if (intersects.length > 0) {
// If mouse is not currently over an object
// Set cursor to pointer so that the user can see that the object is clickable
document.body.style.cursor = 'pointer';
// Get the last intersected object, it's most likely that object we are currently hovering
const is = intersects.length > 0 ? intersects.length - 1 : 0;
// Is the object hovered over for the first time?
if (INTERSECTED === null) {
// Save current hovered object
INTERSECTED = intersects[is].object;
// HIGHLIGHT
// Save current color
INTERSECTED.currentHex = INTERSECTED.material.color.getHex();
// Set highlight color
INTERSECTED.material.color.setHex(COLOR_HIGHLIGHT);
// SCALE UP
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1.5,
y: 1.5
},
100
);
scaleTween.start();
// SET Z-INDEX
INTERSECTED.position.z = 10;
} else {
// If the mouse is over an object
// Do we have a previous hovered item?
if (INTERSECTED !== null) {
// Revert color
INTERSECTED.material.color.setHex(INTERSECTED.currentHex);
// SCALE DOWN
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1,
y: 1
},
100
);
scaleTween.start();
// REVERT Z-INDEX
INTERSECTED.position.z = 1;
}
// Save current intersected object
INTERSECTED = intersects[is].object;
// HIGHLIGHT
// Save current color
INTERSECTED.currentHex = INTERSECTED.material.color.getHex();
// Set highlight color
INTERSECTED.material.color.setHex(COLOR_HIGHLIGHT);
// SCALE UP
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1.5,
y: 1.5
},
100
);
scaleTween.start();
// SET Z-INDEX
INTERSECTED.position.z = 10;
}
} else {
// If there are no intersections
// Reset cursor
document.body.style.cursor = 'default';
// Restore previous intersection object (if it exists) to its original color
if (INTERSECTED !== null) {
// REVERT COLOR
INTERSECTED.material.color.setHex(INTERSECTED.currentHex);
// SCALE DOWN
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1,
y: 1
},
100
);
scaleTween.start();
// REVERT "Z-INDEX"
INTERSECTED.position.z = 1;
}
// Remove previous intersection object reference by setting current intersection object to "nothing"
INTERSECTED = null;
}
I've managed to highlight the object and scale it up with a tween quite nicely, but when I move the mouse out of the object onto the next object (the scaled object is scaled over the next object a bit), the highlight is gone, but the scale persists. How do I manage to scale the object down? And preferably with a tween?
A pen for this code can be found here: https://codepen.io/phun-ky/pen/erBZZy, the relevant part is at about line 1284 or search for INTERSECTED.
I wrote my own one. It's hell imperfect, but, at least, it scales up and down the hexagons:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, 10);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x101010);
document.body.appendChild(renderer.domElement);
var hexes = [];
var colCount = 5;
var rowCount = 4;
var hexDiameter = 3;
var xStart = -(colCount) * hexDiameter * 0.5;
var rowSpace = Math.sqrt(3) * hexDiameter * 0.5;
var yStart = (rowCount - 1) * rowSpace * 0.5;
var hexGeom = new THREE.CylinderGeometry(hexDiameter * 0.5, hexDiameter * 0.5, 0.0625, 6, 1);
hexGeom.rotateX(Math.PI * 0.5);
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
color: Math.random() * 0x7e7e7e + 0x7e7e7e,
wireframe: false
}));
hex.position.set(xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0), yStart - j * rowSpace, 0);
hex.userData.scaleUp = function(h) {
if (h.userData.scaleDownTween) h.userData.scaleDownTween.stop();
let initScale = h.scale.clone();
let finalScale = new THREE.Vector3().setScalar(2);
h.userData.scaleUpTween = new TWEEN.Tween(initScale).to(finalScale, 500).onUpdate(function(obj) {
h.scale.copy(obj)
}).start();
}
hex.userData.scaleDown = function(h) {
if (h.userData.scaleUpTween) h.userData.scaleUpTween.stop();
let initScale = h.scale.clone();
let finalScale = new THREE.Vector3().setScalar(1);
h.userData.scaleUpTween = new TWEEN.Tween(initScale).to(finalScale, 500).onUpdate(function(obj) {
h.scale.copy(obj)
}).start();
}
scene.add(hex);
hexes.push(hex);
}
}
window.addEventListener("mousemove", onMouseMove, false);
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var intersects = [];
var intersected;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
intersects = raycaster.intersectObjects(hexes);
if (intersects.length > 0) {
if (intersected != intersects[0].object) {
if (intersected) intersected.userData.scaleDown(intersected);
intersected = intersects[0].object;
intersected.userData.scaleUp(intersected);
}
} else {
if (intersected) intersected.userData.scaleDown(intersected);
intersected = null;
}
}
render();
function render() {
requestAnimationFrame(render);
TWEEN.update();
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/17.2.0/Tween.min.js"></script>
Given two faces f and f' with a common edge e, i'm looking for a way to rotate f around e.
See: illustration of f/f' and e
My goal is to unfold f and f' so they can be mapped on the same plan. More specifically, I want the coordinate of the vertex r of f that is not part of e after such unfolding (r').
See: after unfolding with r/r'
Currently i've tried to apply the method described here: https://sites.google.com/site/glennmurray/Home/rotation-matrices-and-formulas/rotation-about-an-arbitrary-axis-in-3-dimensions
In the case from the screenshot, i've simplified it as the rotation axis is already on the Z-axis. So my code looks like this:
// Object contains only two faces
var geometry = object.children[0].geometry;
var f = geometry.faces[0];
var fprime = geometry.faces[1];
// Find two vertices in common
var edge = [f.a, f.b];
if (f.a != fprime.a && f.a != fprime.b && f.a != fprime.c) {
edge = [f.b, f.c];
} else if (f.b != fprime.a && f.b != fprime.b && f.b != fprime.c) {
edge = [f.a, f.c];
}
var v1 = geometry.vertices[edge[0]];
var v2 = geometry.vertices[edge[1]];
polyhedron.translateOnAxis(v1, -1);
polyhedron.rotateOnAxis(v2, THREE.Math.degToRad(90));
polyhedron.translateOnAxis(v1, 1);
But this only send my object into space:
Before
After
Without the rotation, the object does not move (as expected). Any hints on how to fix the rotation ?
If I got you correctly, here's a rough concept of you can rotate a vertex around an axis:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(1, 5, 10);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene.add(new THREE.GridHelper(10, 10));
var planeGeom = new THREE.PlaneGeometry(5, 5);
planeGeom.rotateZ(Math.PI * 0.25);
planeGeom.vertices[0].basePosition = new THREE.Vector3().copy(planeGeom.vertices[0]);
planeGeom.vertices[2].set(0, 0, 0); // let's make a triangle from the plane
var plane = new THREE.Mesh(planeGeom, new THREE.MeshBasicMaterial({
color: "aqua",
wireframe: true
}));
scene.add(plane);
var axis = new THREE.Vector3(0, 1, 0); // in three.js, up is positive Y
render();
function render() {
requestAnimationFrame(render);
planeGeom.vertices[0].copy(planeGeom.vertices[0].basePosition).applyAxisAngle(axis, (Math.sin(Date.now() * 0.001) * 0.5 + 0.5) * Math.PI * 0.5); // we'll use .applyAxisAngle() method
planeGeom.verticesNeedUpdate = true;
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/91/three.min.js"></script>
I was able to unfold a triangle to another triangle using the following snippet: Rotate object on specific axis anywhere in Three.js - including outside of mesh
// Object contains only two faces
var geometry = object.children[0].geometry;
geometry.computeFaceNormals();
geometry.computeVertexNormals();
var f = geometry.faces[0];
var fprime = geometry.faces[1];
// Find two vertices in common
var edge = [f.a, f.b];
if (f.a != fprime.a && f.a != fprime.b && f.a != fprime.c) {
edge = [f.b, f.c];
} else if (f.b != fprime.a && f.b != fprime.b && f.b != fprime.c) {
edge = [f.a, f.c];
}
var point = geometry.vertices[edge[0]].clone();
var axis = geometry.vertices[edge[1]].clone();
axis = axis.sub(point);
axis.normalize();
// Adds a triangle to show rotation
var newGeometry = new THREE.Geometry();
newGeometry.vertices.push(
geometry.vertices[f.a].clone(),
geometry.vertices[f.b].clone(),
geometry.vertices[f.c].clone()
);
newGeometry.faces.push(new THREE.Face3(0, 1, 2));
var material = new THREE.MeshBasicMaterial({color: 0xffff00, side: THREE.DoubleSide});
var mesh = new THREE.Mesh(newGeometry, material);
scene.add(mesh);
var dot_product = f.normal.dot(fprime.normal); // Give cosinus of the angle
var angle = Math.acos(dot_product);
mesh.rotateAroundWorldAxis(point, axis, angle);
THREE.Object3D.prototype.rotateAroundWorldAxis = function() {
// rotate object around axis in world space (the axis passes through point)
// axis is assumed to be normalized
// assumes object does not have a rotated parent
var q = new THREE.Quaternion();
return function rotateAroundWorldAxis(point, axis, angle) {
q.setFromAxisAngle(axis, angle);
this.applyQuaternion(q);
this.position.sub(point);
this.position.applyQuaternion(q);
this.position.add(point);
return this;
}
}();
Result
I'm using Three.js and I wonder how to get all objects in a given area?
For example, get all objects that found in the green-square:
Solution:
getEntitiesInSelection: function(x, z, width, height, inGroup) {
var self = this,
entitiesMap = [],
color = 0,
colors = [],
ids = [],
pickingGeometry = new THREE.Geometry(),
pickingMaterial = new THREE.MeshBasicMaterial( { vertexColors: THREE.VertexColors } ),
pickingScene = new THREE.Scene(),
pickingTexture = new THREE.WebGLRenderTarget( this._renderer.domElement.width, this._renderer.domElement.height),
cloneMesh,
entities = inGroup ?
engine.getObjectsByGroup(inGroup) : engine.getRegisteredEntities();
pickingTexture.generateMipmaps = false;
//Go over each entity, change its color into its ID
_.forEach(entities, function(entity) {
if(undefined == entity.threeRenderable) {
return ;
}
//Clone entity
cloneMesh = entity.threeRenderable.mesh().clone();
cloneMesh.material = entity.threeRenderable.mesh().material.clone();
cloneMesh.material.map = null;
cloneMesh.material.vertexColors = THREE.VertexColors;
cloneMesh.geometry = entity.threeRenderable.mesh().geometry.clone();
cloneMesh.position.copy( entity.threeRenderable.mesh().position );
cloneMesh.rotation.copy( entity.threeRenderable.mesh().rotation );
cloneMesh.scale.copy( entity.threeRenderable.mesh().scale );
//Cancel shadow
cloneMesh.castShadow = false;
cloneMesh.receiveShadow = false;
//Set color as entity ID
entitiesMap[color] = entity.id();
self._applyVertexColors(cloneMesh.geometry, new THREE.Color( color ) );
color++;
THREE.GeometryUtils.merge( pickingGeometry, cloneMesh);
});
pickingScene.add( new THREE.Mesh( pickingGeometry, pickingMaterial ) );
//render the picking scene off-screen
this._renderer.render(pickingScene, this._objs[this._mainCamera], pickingTexture );
var gl = this._renderer.getContext();
//read the pixel under the mouse from the texture
var pixelBuffer = new Uint8Array( 4 * width * height );
gl.readPixels( x, this._renderer.domElement.height - z, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer );
//Convert RGB in the selected area back to color
for(var i=0; i<pixelBuffer.length; i+=4) {
if( 0 == pixelBuffer[i] && 0 == pixelBuffer[i+1] && 0 == pixelBuffer[i+2] && 0 == pixelBuffer[i+3] ) {
continue;
}
color = ( pixelBuffer[i] << 16 ) | ( pixelBuffer[i+1] << 8 ) | ( pixelBuffer[i+2] );
colors.push(color);
}
colors = _.unique(colors);
//Convert colors to ids
_.forEach(colors, function(color) {
ids.push(entitiesMap[color]);
});
return ids;
}
The line engine.getObjectsByGroup(inGroup) : engine.getRegisteredEntities();
just return an array of entities, which in turn, I iterate over the entities:
_.forEach(entities, function(entity) { ...
Only entities that have the 'threeRenderable' property (object) are visible, therefore, I ignore those that doesn't have it:
if(undefined == entity.threeRenderable) {
return ;
}
then I merge the entity's cloned mesh with with the pickingGeometry:
THREE.GeometryUtils.merge( pickingGeometry, cloneMesh);
eventually, I add the pickingGeometry to the pickingScene:
pickingScene.add( new THREE.Mesh( pickingGeometry, pickingMaterial ) );
Then I read the colors of the selected area, and return an array of IDs.
You can checkout the Node.js game engine I wrote back then.
I've wanted to implement something like this and I choose a very different method - maybe much worse, I don't really know - but much easier to do IMO, so I put it here in case someone wants it.
Basically, I used only 2 raycasts to know the first and last points of the selection rectangle, projected on my ground plane, and iterate over my objects to know which ones are in.
Some very basic code:
function onDocumentMouseDown(event) {
// usual Raycaster stuff ...
// get the ground intersection
var intersects = raycaster.intersectObject(ground);
GlobalGroundSelection = {
screen: { x: event.clientX, y: event.clientY },
ground: intersects[0].point
};
}
function onDocumentMouseUp(event) {
// ends a ground selection
if (GlobalGroundSelection) {
// usual Raycaster stuff ...
// get the ground intersection
var intersects = raycaster.intersectObjects(ground);
var selection = {
begins: GlobalGroundSelection.ground,
ends: intersects[0].point
};
GlobalGroundSelection = null;
selectCharactersInZone(selection.begins, selection.ends);
}
}
function onDocumentMouseMove(event) {
if (GlobalGroundSelection) {
// in a selection, draw a rectangle
var p1 = GlobalGroundSelection.screen,
p2 = { x: event.clientX, y: event.clientY };
/* with these coordinates
left: p1.x > p2.x ? p2.x : p1.x,
top: p1.y > p2.y ? p2.y : p1.y,
width: Math.abs(p1.x - p2.x),
height: Math.abs(p1.y - p2.y)
*/
}
}
Here is my select function:
function selectCharactersInZone (start, end) {
var selected = _.filter( SELECTABLE_OBJECTS , function(object) {
// warning: this ignore the Y elevation value
var itsin = object.position.x > start.x
&& object.position.z > start.z
&& object.position.x < end.x
&& object.position.z < end.z;
return itsin;
});
return selected;
}
Some warnings: as far as I know, this technique is only usable when you don't care about Y positions AND your selection is a basic rectangle.
My 2c