I have an axis, as defined by 2 vectors, for example one that points upwards at x = 10:
const axisStart = new Vector3(10, 0, 0)
const axisEnd = new Vector3(10, 0, 1)
I'm getting the normalized axis direction like so:
const axisDirection = new Vector3().subVectors(axisEnd, axisStart).normalize()
How can I rotate a vector (e.g. Vector3(50, 0, 0)) around my original axis?
I've tried using Vector3.applyAxisAngle(axisDirection , radians), but because the axis has been normalized, the rotation happens around the world center (0, 0) and not around the axis' original position.
I've solved this by finding the exact point on the axis around which the point rotates, using this answer and translating the pseudocode from it into typescript:
getPivotPoint(pointToRotate: Vector3, axisStart: Vector3, axisEnd: Vector3) {
const d = new Vector3().subVectors(axisEnd, axisStart).normalize()
const v = new Vector3().subVectors(pointToRotate, axisStart)
const t = v.dot(d)
const pivotPoint = axisStart.add(d.multiplyScalar(t))
return pivotPoint
}
Then, as #Ouroborus pointed out, I can then translate the point, apply the rotation, and translate it back:
rotatePointAroundAxis(pointToRotate: Vector3, axisStart: Vector3, axisEnd, radians: number) {
const axisDirection = new Vector3().subVectors(axisEnd, axisStart).normalize()
const pivotPoint = getPivotPoint(pointToRotate, axisStart, axisEnd)
const translationToWorldCenter = new Vector3().subVectors(pointToRotate, pivotPoint)
const translatedRotated = translationToWorldCenter.clone().applyAxisAngle(axisDirection, radians)
const destination = pointToRotate.clone().add(translatedRotated).sub(translationToWorldCenter)
return destination
}
The above code is working nicely, leaving it here for my future self and for other who might find this useful.
Related
I'm working recently to draw some shapes (svg images) along a curved linestring, i managed to draw the shapes using geometry by adding the styles for each shape. And now, i need to modify this linestring feature it means i want that the shapes move also when modifying the line and later the shapes will be changed with other images (circles..).
i have created my own function modify that recreate the feature and called drawShape function but it's heavy and not working correctly.
so is there any other solution to facilitate the draw of the shapes to facilate modify function also the other tasks (edit segment: change shapes), add segment...
Thanks in advance.
Some parts of code are passed here
// that function map the poignees passes the id( number of segment ) to drawShapes function
const drawShapesAll = function(feature){
poingees.forEach((elm, id) => {
if(id === poingees.length -1) return;
drawShapes(feature, id+1, elm.src); // id+1 : represent the number of segment
});
}
//drawShapes funtion: draw shapes for one segment
const drawShapes = function(feature, coordsSeg, src){
//src : svg image that i want to add it (triangle)
const styles = feature.getStyle();
const index0 = getClosestPointToCoords(coords, poignees[numSeg-1]);
const index1 = getClosestPointToCoords(coords, poignees[numSeg]);
const coordsSeg = []; //coordsSeg: represent coordinates (geometry) for segment i (the curved line from poigne i to poigne i+1)
for(var j=index0; j<= index1; j++) {
coordsSeg.push(coords[j]);
}
const coordsTrans = coordsSeg.map((coord) => (transform(coord, "EPSG:3857", "EPSG:4326"))); // get coordinates tranformed for each segment
const num = turf.distance(coordsTrans[0], coordsTrans[coordsTrans.length -1]) / 200; // calculate the distance for segment i then devide it by 200 to get the number of shapes for that segment (200 represent the distance between each shape)
let line = {
"type": "Feature",
"properties": {
},
"geometry": {
"type": "LineString",
"coordinates": coordsTrans
}
};
while(i < number ){ // a loop function that draw shapes with number equal = num
let {point,rotation:rotation2} = getPointInCurve(line, coordsSeg, 200 * i); //
let pointGeo = new Point(point);
styles.push(new Style({
geometry: pointGeo,
image: new Icon (({
rotation: - rotation2,
offset: [1, 1],
anchor : [0.9, 0.9],
src : src,
scale: 0.3
}))
}));
feature.setStyle(styles);
i++;
}
}
// getPointInCurve : get point geometry(coordinates) and rotation along the segment
function getPointInCurve(line, coords, distance){
//that function call bezier function , get the curved coordinates ,
// i used along to get the position of each shape which has distance = 200*i
const curved = turf.bezier(line);
const along = turf.along(curved, distance);
const alongCoordTrans = convertCoordinates(along.geometry.coordinates[0], along.geometry.coordinates[1]);
const prev = coords[closestPoint(coords, alongCoordTrans).index];
const dx = alongCoordTrans[0] - coords[coords.indexOf(prev) - 5][0] ; // calculate dx and dy
const dy = alongCoordTrans[1] - coords[coords.indexOf(prev) - 5][1];
const rotation2 = Math.atan2(dy, dx); // calculate rotation of that shape
const min = getClosestPointToCoords(coords, alongCoordTrans);
const closetPoint = coords[min];
return {point:closetPoint, rotation:rotation2}; // return the point coordinates and rotation of shape i,
}
I have a Functiongraph line defined like this:
const f1 = function(x) {
const slope = me.options.gLine1Slope;
return (x - 2.5) * slope + 2.5;
};
this.l1 = this.board.create('functiongraph', [f1, -30, 30], {
recursionDepthLow: 8,
recursionDepthHigh: 15
});
this.l1.setPosition(window.JXG.COORDS_BY_USER, [
forceFloat(this.options.gLine1OffsetX),
forceFloat(this.options.gLine1OffsetY)
]);
I'm aware that Functiongraph is just a wrapper for Curve, so I've been reading through both API docs.
I'm manually positioning this line based on these offset values because it can be dragged around the plane by the user.
I can get a value close to the Y-intercept, like this:
f1(0) + (this.options.gLine1OffsetY - this.options.gLine1OffsetX)
But it's not quite right, after this line is dragged around a bit. Can anyone give some guidance on how to get the Y-intercept for this curve? I suppose I can just iterate through the data array of points along this curve, and pick the one where Y is closest to 0. I was just hoping there is a more straightforward way as well, though.
You are right. Getting the y-intercept of the function graph after dragging it around freely is not easy. The reason is that dragging objects is mostly implemented using projective transformations. This makes it complicated to get the y-intercept of the curve which is visible at the moment. The easiest approach I can think of for the moment is to intersect the curve with the vertical axis and get the position of that point. Here is a slighlty modified version of your example:
const board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [-5, 5, 5, -5], axis:true
});
var me = {
gLine1Slope:2,
gLine1OffsetX: 1,
gLine1OffsetY: -1
};
const f1 = function(x) {
const slope = me.gLine1Slope;
return x * slope;
};
var l1 = board.create('functiongraph', [f1, -30, 30], {fixed: false});
var p = board.create('intersection', [l1, board.defaultAxes.y], {
withLabel: false, visible: false});
// Now, we can move the curve and get the y-intercept
// in p.Y()
l1.setPosition(window.JXG.COORDS_BY_USER, [
me.gLine1OffsetX,
me.gLine1OffsetY
]);
board.update();
board.on('up', function(evt) {
document.getElementById('jxg_debug').value = p.Y();
});
document.getElementById('jxg_debug').value = p.Y();
See it live at https://jsfiddle.net/3s90qx57/2/
What I'm trying to achieve is a rotation of the geometry around pivot point and make that the new definition of the geometry. I do not want te keep editing the rotationZ but I want to have the current rotationZ to be the new rotationZ 0.
This way when I create a new rotation task, it will start from the new given pivot point and the newly given rad.
What I've tried, but then the rotation point moves:
// Add cube to do calculations
var box = new THREE.Box3().setFromObject( o );
var size = box.getSize();
var offsetZ = size.z / 2;
o.geometry.translate(0, -offsetZ, 0)
// Do ratation
o.rotateZ(CalcUtils.degreeToRad(degree));
o.geometry.translate(0, offsetZ, 0)
I also tried to add a Group and rotate that group and then remove the group. But I need to keep the rotation without all the extra objects. The code I created
var box = new THREE.Box3().setFromObject( o );
var size = box.size();
var geometry = new THREE.BoxGeometry( 20, 20, 20 );
var material = new THREE.MeshBasicMaterial( { color: 0xcc0000 } );
var cube = new THREE.Mesh( geometry, material );
cube.position.x = o.position.x;
cube.position.y = 0; // Height / 2
cube.position.z = -size.z / 2;
o.position.x = 0;
o.position.y = 0;
o.position.z = size.z / 2;
cube.add(o);
scene.add(cube);
// Do ratation
cube.rotateY(CalcUtils.degreeToRad(degree));
// Remove cube, and go back to single object
var position = o.getWorldPosition();
scene.add(o)
scene.remove(cube);
console.log(o);
o.position.x = position.x;
o.position.y = position.y;
o.position.z = position.z;
So my question, how do I save the current rotation as the new 0 rotation point. Make the rotation final
EDIT
I added an image of what I want to do. The object is green. I have a 0 point of the world (black). I have a 0 point of the object (red). And I have rotation point (blue).
How can I rotate the object around the blue point?
I wouldn't recommend updating the vertices, because you'll run into trouble with the normals (unless you keep them up-to-date, too). Basically, it's a lot of hassle to perform an action for which the transformation matrices were intended.
You came pretty close by translating, rotating, and un-translating, so you were on the right track. There are some built-in methods which can help make this super easy.
// obj - your object (THREE.Object3D or derived)
// point - the point of rotation (THREE.Vector3)
// axis - the axis of rotation (normalized THREE.Vector3)
// theta - radian value of rotation
// pointIsWorld - boolean indicating the point is in world coordinates (default = false)
function rotateAboutPoint(obj, point, axis, theta, pointIsWorld){
pointIsWorld = (pointIsWorld === undefined)? false : pointIsWorld;
if(pointIsWorld){
obj.parent.localToWorld(obj.position); // compensate for world coordinate
}
obj.position.sub(point); // remove the offset
obj.position.applyAxisAngle(axis, theta); // rotate the POSITION
obj.position.add(point); // re-add the offset
if(pointIsWorld){
obj.parent.worldToLocal(obj.position); // undo world coordinates compensation
}
obj.rotateOnAxis(axis, theta); // rotate the OBJECT
}
After this method completes, the rotation/position IS persisted. The next time you call the method, it will transform the object from its current state to wherever your inputs define next.
Also note the compensation for using world coordinates. This allows you to use a point in either world coordinates or local space by converting the object's position vector into the correct coordinate system. It's probably best to use it this way any time your point and object are in different coordinate systems, though your observations may differ.
As a simple solution for anyone trying to quickly change the pivot point of an object, I would recommend creating a group and adding the mesh to the group, and rotating around that.
Full example
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube)
Right now, this will just rotate around its center
cube.rotation.z = Math.PI / 4
Create a new group and add the cube
const group = new THREE.Group();
group.add(cube)
scene.add(group)
At this point we are back where we started. Now move the mesh:
cube.position.set(0.5,0.5,0)
Then move the group
group.position.set(-0.5, -0.5, 0)
Now use your group to rotate the object:
group.rotation.z = Math.PI / 4
I have built this snake game where the snakes are animated around using a bunch of cube meshes and THREE.BoxGeometry:
I would prefer each snake to consist of just one mesh and one geometry so that I can easily add textures, rounded edges etc.
I'm trying to take a set of 3d points and convert them into a single geometry that resembles the box-like snakes in the demo (like several THREE.BoxGeometry attached together).
I've tried to achieve that using THREE.CurvePath and THREE.ExtrudeGeometry:
_makeSnakeGeometry(positions) {
const vectors = positions.map(p => new THREE.Vector3(...p));
const curvePath = new THREE.CurvePath();
const first = vectors[1];
const last = vectors[vectors.length - 1];
let curveStart = vectors[0];
let previous = curveStart;
let previousDirection;
// Walk through the positions. If there is a change in direction, add
// a new curve to the curve path.
for (let vector of vectors.slice(1)) {
let direction = previous.clone().sub(vector);
if (vector.equals(first)) {
curveStart.sub(direction.clone().divideScalar(2));
}
if (vector.equals(last)) {
previous.sub(previousDirection.clone().divideScalar(2));
}
if ((previousDirection && !previousDirection.equals(direction)) || vector.equals(last)) {
curvePath.add(new THREE.LineCurve3(curveStart, previous));
curveStart = previous;
}
previous = vector;
previousDirection = direction;
}
const p = Const.TILE_SIZE / 2;
const points = [[-p, -p], [-p, p], [p, p], [p, -p]];
const shape = new THREE.Shape(points.map(p => new THREE.Vector2(...p)));
const geometry = new THREE.ExtrudeGeometry(shape, {
steps: 20,
extrudePath: curvePath,
amount: 20
});
return geometry;
}
Unfortunately, it looks quite ugly and we end up with rounded corners where the path direction changes:
If I increase the steps option, the resulting mesh looks less ugly but the geometry has a large amount of vertices due to the smooth curved edges. I'm concerned about a large vertex count because I may have to recreate this geometry on every animation frame.
To animate the geometry, I've tried to use geometry.morphTargets with little success. The morph happens but it, too, looks quite ugly. Perhaps I need to manually animate the vertices of the geometry?
To summarize, my questions are:
How can I create a geometry with minimal vertices that resembles several box geometries pieced together?
What is the correct way to animate a geometry when the underlying vertex count can change?
To build the snake geometry, I ended up merging geometries:
_makeSnakeGeometry(positions) {
positions = positions.map(p => new THREE.Vector3(...p));
const geometry = new THREE.Geometry();
for (let i = 0; i < positions.length; i += 1) {
const position = positions[i];
const mesh = makeVoxelMesh(Const.TILE_SIZE, { position });
mesh.updateMatrix();
geometry.merge(mesh.geometry, mesh.matrix);
}
return geometry;
}
makeVoxelMesh returns a single cube mesh for one position of the snake. I join them together using geometry.merge and repeat the process on every animation frame. It's not perfect because the geometry ends up having extra vertices and faces that it doesn't need to have for such a simple shape. Maybe there is a way to reduce them after the fact?
To animate the snake, I find the four vertices that comprise the head face. I do it by looking at the vertex positions and face normals:
_findHeadFaceVertices() {
const direction = this._direction.clone().multiplyScalar(1 + (Const.TILE_SIZE / 10));
const headFacePosition = new THREE.Vector3(...this.head).add(direction);
const { vertices, faces } = this.mesh.geometry;
// Sort vertices by distance to a point near the snake head and only keep
// the first few that are equidistant.
const closest = vertices
.map(v => [v.distanceTo(headFacePosition), v])
.sort((a, b) => a[0] - b[0])
.filter((pair, i, sorted) => pair[0] === sorted[0][0])
.map(pair => pair[1]);
const result = [];
const seen = {};
// Filter the remaining vertices by keeping only ones which belong to faces
// that point in the same direction as the snake.
for (let face of faces) {
for (let vertex of [vertices[face.a], vertices[face.b], vertices[face.c]]) {
const key = vertex.toArray().toString();
if (!seen[key] && closest.includes(vertex) && face.normal.equals(this._direction)) {
seen[key] = true;
result.push(vertex);
}
}
}
return result;
}
For animation to work, I had to set this.mesh.geometry.verticesNeedUpdate = true; after each animation frame.
I am drawing a circle and inside it a line that shows its radius , i use same line coordinates , but as result i get a smaller circle , any help ???
function DrawLine() {
var lineCoordinates = [[3210202.3139208322, 5944966.311907868], [3075978.8922520624, 6055647.128864803]];
var line = new ol.geom.LineString(lineCoordinates);
var feature = new ol.Feature(line);
var id = guid();
feature.featureID = id;
feature.setProperties({
'id': id,
'name': typeSelect.value,
'description': 'Some values'
})
source.addFeature(feature);
};
function DrawCircle() {
var sourceProj = map.getView().getProjection();
var wgs84Sphere = new ol.Sphere(6378137);
var c1 = ol.proj.transform([3210202.3139208322, 5944966.311907868], sourceProj, 'EPSG:4326');
var c2 = ol.proj.transform([3075978.8922520624, 6055647.128864803], sourceProj, 'EPSG:4326');
var distance = wgs84Sphere.haversineDistance(c1, c2);
var point = new ol.geom.Circle([3210202.3139208322, 5944966.311907868],distance,'XY');
var feature = new ol.Feature(point);
console.log(distance);
var id = guid();
feature.featureID = id;
feature.setProperties({
'id': id,
'name': typeSelect.value,
'description': 'Some values'
})
source.addFeature(feature);
};
Your code looks pretty intense. If the radius is just for looks, why not just go with something simple along the lines of this:
function drawRadius(circle_, direction_){
context.moveTo(circle_.center_x, circle_.center_y);
context.lineTo(circle_.center_x + Math.cos(direction_) * circle_.radius, circle_.center_y + Math.sin(direction_) * circle_.radius);
context.stroke();
}
Where direction is maybe the rotation of the circle in radians: 0 to 2*PI,
and the context is a canvasRenderingContext2D.
Your circle generator could look like this:
function getCircleFromPoints(point_a_, point_b_){
var distance_x=point_b_.x-point_a_.x;
var distance_y=point_b_.y-point_a_.y;
var circle={
center_x:point_a_.x;
center_y:point_a_.y;
radius:Math.sqrt(distance_x*distance_x+distance_y*distance_y);
};
return circle;
}
This will put your circle's center at point_a_ and its edge at point_b_. It's radius will be equal to the distance between the two points.
I realize that this is all plain JavaScript, but the concept remains the same. Use the distance formula to get the radius of the circle equal to the distance between the two points and set the circle's center to one of the points.
You set the radius of the circle to the real distance between the two points, not the projected distance. Since ol3 works on the projected plane, those might be different for many projections.
I wrote more in depth about the difference between projected and real radii in ol3 in this answer.