WebGL: how to prevent camera bounce when changing center of rotation - javascript

I've hit a mental block of sorts, and was looking for some advice or suggestions. My problem is this:
I have a WebGL scene (I'm not using a 3rd party library, except gl-matrix), in which the user can rotate the camera up/down and left/right (rotate around X/Y axis). They can also rotate the model as well (yaw/pitch).
To see the problem, imagine the model has two blocks, A and B in the scene, with A at the center and B to the right (in the viewport), and the rotation center in the center of A. If the user rotates the model, it rotates about the center of block A. But if the user clicks on object B, I need to be able to change the center of rotation to B's center, but still maintain the current camera orientation. Currently, when the center of rotation switches to B, block B moves to the center of the screen, and block A moves to the left. Basically, the code always centers on the current center or rotation.
I use the following code for the modelview matrix update:
var mvMatrix = this.mvMatrix;
mat4.identity(mvMatrix);
mat4.translate(mvMatrix, mvMatrix, this.orbit);
mat4.rotateY(mvMatrix, mvMatrix, this.orbitYaw);
mat4.rotateX(mvMatrix, mvMatrix, this.orbitPitch);
mat4.translate(mvMatrix, mvMatrix, this.eye);
mat4.rotateY(mvMatrix, mvMatrix, this.eyeYaw);
mat4.rotateX(mvMatrix, mvMatrix, this.eyePitch);
I'm trying to figure out what the right yaw and pitch values for orbit and eye I should use in order to move back the current location and to achieve the present camera/eye orientation to avoid the "bounce" from one object to another as the rotation center moves.
I've searched a lot and can't seem to find how best to do this (my current attempt(s) have issues). Any sample code, or just good descriptions would be appreciated.
Edit
I followed gman's advice and tried the following code, but switching orbits just jumped around. My model is composed of multiple objects, and the orbit center can change, but after changing orbits, the orientation of the camera needs to remain steady, which is why I have to calculate the correction to the orbit yaw/pitch and eye yaw/pitch to put the eye back in the same spot and pointing in the same direction after changing orbits. BTW, I only have one orbit yaw and pitch, based on where the current orbit is, so that's a little different from gman's sample:
Camera.prototype.changeOrbit = function (newOrbit) {
var matA = mat4.create();
var matB = mat4.create();
mat4.translate(matA, matA, this.orbit);
mat4.rotateY(matA, matA, this.orbitYaw);
mat4.rotateX(matA, matA, this.orbitPitch);
mat4.translate(matB, matB, newOrbit);
mat4.rotateY(matB, matB, this.orbitYaw);
mat4.rotateX(matB, matB, this.orbitPitch);
var matInverseNewOrbit = mat4.create();
var matNewOrbitToCamera = mat4.create();
mat4.invert(matInverseNewOrbit, matB);
mat4.multiply(matNewOrbitToCamera, matInverseNewOrbit, matA);
var m = matNewOrbitToCamera;
this.eye[0] = m[12];
this.eye[1] = m[13];
this.eye[2] = m[14];
this.eyePitch = ExtractPitch(m);
this.eyeYaw = ExtractYaw(m);
this.update();
};
ExtractPitch and ExtractYaw work as gman had specified, but I do rotate around different axes since pitch is normally defined around the Y axis, and so on. Thanks for the suggestions, though.

I'm not sure I can explain this but basically:
When switching from A to B, at switch time,
Compute the matrix for the camera going around A (the code you have above). (camera)
Compute the matrix for B (matB)
Compute the inverse of the matrix for B. (inverseMatB)
Multiply camera by inverseMatB. (matBtoCamera)
You now have a matrix that goes from B to the camera.
Decompose this matrix (matBToCamera) back into translation and rotation.
Unfortunately I don't know of a good decompose matrix function to point you at. I haven't needed one in a long time. Translation is basically elements 12, 13, 14 of your matrix. (Assuming you are using 16 element matrices which I think is what glMatrix uses).
var translation = [m[12], m[13], m[14]];
For rotation the upper/left 3x3 part of the matrix represents rotation. As long as there is no scaling or skewing involved, according to this page (http://nghiaho.com/?page_id=846) it's
var rotXInRadians = Math.atan2(m[9], m[10]);
var rotYInRadians = Math.atan2(-m[8], Math.sqrt(m[9] * m[9] + m[10] * m[10]));
var rotZInRadians = Math.atan2(m[4], m[0]);
Here's an example
http://jsfiddle.net/greggman/q7Bsy/
I'll paste the code here specific to glMatrix
// first let's make 3 nodes, 'a', 'b', and 'camera
var degToRad = function(v) {
return v * Math.PI / 180;
}
var a = {
name: "a",
translation: [0, -50, -75],
pitch: 0,
yaw: degToRad(30),
};
var b = {
name: "b",
translation: [0, 100, 50],
pitch: 0,
yaw: degToRad(-75),
}
var camera = {
name: "cam",
translation: [0, 15, 10],
pitch: 0,
yaw: degToRad(16),
parent: a,
};
Here's the code that computes the matrix of each
var matA = mat4.create();
mat4.identity(matA);
mat4.translate(matA, matA, a.translation);
mat4.rotateY(matA, matA, a.pitch);
mat4.rotateX(matA, matA, a.yaw);
a.mat = matA;
var matB = mat4.create();
mat4.identity(matB);
mat4.translate(matB, matB, b.translation);
mat4.rotateY(matB, matB, b.pitch);
mat4.rotateX(matB, matB, b.yaw);
b.mat = matB;
var matCamera = mat4.create();
mat4.identity(matCamera);
var parent = camera.parent;
mat4.translate(matCamera, matCamera, parent.translation);
mat4.rotateY(matCamera, matCamera, parent.pitch);
mat4.rotateX(matCamera, matCamera, parent.yaw);
mat4.translate(matCamera, matCamera, camera.translation);
mat4.rotateY(matCamera, matCamera, camera.pitch);
mat4.rotateX(matCamera, matCamera, camera.yaw);
camera.mat = matCamera;
and here's the code that swaps cameras
// Note: Assumes matrices on objects are updated.
var reparentObject = function(obj, newParent) {
var matInverseNewParent = mat4.create();
var matNewParentToObject = mat4.create();
mat4.invert(matInverseNewParent, newParent.mat);
mat4.multiply(matNewParentToObject, matInverseNewParent, obj.mat);
var m = matNewParentToObject;
obj.translation[0] = m[12];
obj.translation[1] = m[13];
obj.translation[2] = m[14];
var rotXInRadians = Math.atan2(m[9], m[10]);
var rotYInRadians = Math.atan2(-m[8], Math.sqrt(m[9] * m[9] + m[10] * m[10]));
var rotZInRadians = Math.atan2(m[4], m[0]);
obj.pitch = rotYInRadians;
obj.yaw = rotXInRadians;
obj.parent = newParent;
};
var newParent = camera.parent == a ? b : a;
reparentObject(camera, newParent);

Related

How do I find the Y-intercept of a Functiongraph/Curve in jsxgraph?

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/

How do I calculate mxgraph pixel co-ordinates from lat/long when integrating with gmap3?

The mxgraph Google Maps example (and demo) shows a simple implementation that create a Google Map overlay.
However I cannot figure out the correlation between the vertex pixel co-ordinates and the equivalent lat/long co-ordinates.
Line 140 of the example shows some hard-coded pixel co-ordinates (23,23) for Seattle:
var n1 = graph.insertVertex(parent, null, {label:'Seattle'}, 23, 23, 10, 10);
I want to display a geographic data set (i.e. I have latitude/longitude) but I can not figure out the conversion to mxGraph vertex pixel co-ordinates.
The Google API's "fromLatLngToDivPixel" does not work (it returns the lat/long of the centre of the map as 0,0), neither does "fromLatLngToContainerPixel".
Even weirder, a vertex with co-ordinates of 0,0 does not appear in the top left corner of the map (it is a little inside the map).
So it's a little complicated.
I finally created two classes to help out:
// Represent a map pixel
class PixelPos {
constructor(x,y) {
this._x = x;
this._y = y;
}
get x(){
return this._x;
}
get y(){
return this._y;
}
}
// Handles converting between lat/log and pixels
// Based on the initial created map div container.
class CoordTranslator {
constructor(graph, projection, bounds) {
this._graph = graph;
this._projection = projection;
this._bounds = bounds;
//
var swb = this._bounds.getSouthWest();
var neb = this._bounds.getNorthEast();
this._neb_lat = neb.lat();
this._swb_lng = swb.lng();
this._map_width = neb.lng() - swb.lng();
this._map_height = neb.lat() - swb.lat();
// Get pixel values from the Geo boundary box
var sw = this._projection.fromLatLngToDivPixel(swb);
var ne = this._projection.fromLatLngToDivPixel(neb);
// Map pixel width and Height
this._pix_width = (ne.x - sw.x);
this._pix_height = (sw.y - ne.y);
this._scale = graph.view.getScale();
}
// Convert the provided lat/long into a PixelPos
convert(lat, lng){
var x = (lng-this._swb_lng)/this._map_width;
x = x*this._pix_width;
x = x/this._scale;
var y = (this._neb_lat - lat)/this._map_height;
y = y*this._pix_height;
y = y/this._scale;
// And, for some unknown reason, I have to magic a final offset!!
return new PixelPos(x-5,y-3);
}
}
I initialise the CoordTranslator class in the mxGraphOverlay.prototype.draw:
mxGraphOverlay.prototype.draw = function()
{
if (drawn = 0){
drawn = 1;
....
let ct = new CoordTranslator(this.graph_, overlayProjection, this.bounds_);
....
// 6193351 # 46.8127123,14.5866472
var pp = ct.convert(46.8127123, 14.58664720);
var n3 = graph.insertVertex(parent, null, {label:'6193351'}, pp.x, pp.y, 10, 10);
....
}
}
There are probably better ways to initialise and use the CoordTranslator, but the ideas are here.

Three JS Pivot point

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

Openlayers 3 drawn circle is smaller than the line

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.

Resize rectangle in Paper.js

I have a very ordinary rectangle created in Paper.js and I'd like to resize it, but I can't find any obvious ways to do it.
var rect = new Rectangle([0, 0],[width,height]);
rect.center = mousePoint;
var path = new Path.Rectangle(rect, 4);
path.fillColor = fillColor;
path.meta = fillColor;
There's a scale transformation method, but it's not really for mouse interaction and my goal is to create a handle that can resize a component.
Note that PaperJS has three different kinds of Rectangles:
Rectangle — This is the basic type (data structure) that defines a rectangle. Basically, top-left point, width, and height. (Nothing is displayed on the screen.) This kind of rectangle can be resized by setting its size property, for instance:
let rect;
const originalSize = [50, 50];
const newSize = [100, 100];
rect = new Rectangle([10, 50], originalSize);
rect.size = newSize;
Path.Rectangle — This is a method for generating a list of Segments that make up a rectangular-shaped Path. This does get displayed, but a Path lacks methods associated with a rectangle. For instance, a Path.Rectangle has no size property (so trying to modify it has no effect). To resize a Path you can use the scale() method as another answer proposes, or modify its Segments:
rect = new Path.Rectangle([210, 50], originalSize);
rect.strokeColor = "red";
rect.strokeWidth = 3;
rect.segments[0].point = rect.segments[0].point.add([-25, 25]); // lower left point
rect.segments[1].point = rect.segments[1].point.add([-25, -25]); // upper left point
rect.segments[2].point = rect.segments[2].point.add([25, -25]); // upper right point
rect.segments[3].point = rect.segments[3].point.add([25, 25]); // lower right point
Shape.Rectangle — This kind of rectangle gets displayed and exposes properties about its shape, such as size. To resize a Shape.Rectangle you can modify its size property directly:
rect = new Shape.Rectangle([410, 50], originalSize)
rect.strokeColor = "blue"
rect.strokeWidth = 3
rect.size = newSize
Most likely, if you want to draw a rectangle and modify its properties after the fact, the rectangle you are looking for is Shape.Rectangle.
Here is a Sketch that lets you play around with the different kinds of rectangles.
You can calculate the scaling by dividing the intended width/height of your rectangle with the current width/height of your rectangle.
Then you can use that scaling 'coefficient' to apply the scaling.
Based on your code above, you can get the current width/height of your rectangle by using: rect.bounds.width and rect.bounds.height
Here's a function you can use
var rectangle = new Shape.Rectangle({
from: [0, 0],
to: [100, 50],
fillColor: 'red'
});
function resizeDimensions(elem,width,height){
//calc scale coefficients and store current position
var scaleX = width/elem.bounds.width;
var scaleY = height/elem.bounds.height;
var prevPos = new Point(elem.bounds.x,elem.bounds.y);
//apply calc scaling
elem.scale(scaleX,scaleY);
//reposition the elem to previous pos(scaling moves the elem so we reset it's position);
var newPos = prevPos + new Point(elem.bounds.width/2,elem.bounds.height/2);
elem.position = newPos;
}
resizeDimensions(rectangle,300,200)
And here's the Sketch for it.
Be aware that the above function will also reposition the element at it's previous position but it will use top-left positioning. Paper.js uses the element's center to position them so I'm clarifying this so it doesn't cause confusion

Categories