Here's a stumper...
Porting some old code, I have this 2D hex grid being rendered in 2.5D:
The y-scale & position of the tiles is calculated for perspective, but I'd like to scale & position them for perspective horizontally as well (the toons at the top of the board look squished). Here's the current code:
const SCALE_X = PixiStages.game._width * 0.0012;
const SCALE_Y = PixiStages.game._height * 0.0018;
this.scale.x = SCALE_X;
this.scale.y = SCALE_Y * ( 0.5 + 0.5 * gamePiece.y / Game.TILE_ROWS );
const getStageXFromBoardX = ( board_x ) => {
const tileWidth = SCALE_X * 38;
return board_x*tileWidth;
}
const getStageYFromBoardY = ( board_y ) => {
const tileHeight = SCALE_Y * 44;
return board_y*tileHeight/4 + board_y*board_y*tileHeight / (8*Game.TILE_ROWS);
}
Simply changing the x-scale to this.scale.x = SCALE_X * ( 0.5 + 0.5 * gamePiece.y / Game.TILE_ROWS ); looks like this:
... so I guess I just need an equation to set their x-position correctly.
Any ideas or links? Thanks!
Note that X-coordinate after perspective transformation depends both on X and on Y source coordinates. General expression
XPersp = (A * X + B * Y + C) / (G * X + H * Y + 1)
For your case (perspective sight along central axis) transformation of rectangle with corners (XCenter-W,0)-(XCenter +W, H) to trapezoid centered vertically at XCenter, shifted up by YShift, is:
XPersp = XCenter + (X - XCenter) / (H * Y + 1)
YPersp = (YShift + E * Y) / (H * Y + 1)
where H, E are some coefficients, adapted for good look.
Vary E (defines trapezoid height) about 0.5-2.0, H (defines trapezoid tilt) about 0.005
Related
I'm building a simple app that places a marker on your screen where at the top of certain landmarks in the real world, going to overlay the markers over the camera's view.
I have the latitude/longitude/altitude for both the viewing device and the world landmarks, and I convert those to ECEF coordinates. But I am having trouble with the 3D projection math. The point always seems to get placed in the middle of the screen... maybe my scaling is wrong somewhere so it looks like it's hardly moving from the center?
Viewing device GPS coordinates:
GPS:
lat: 45.492132
lon: -122.721062
alt: 124 (meters)
ECEF:
x: -2421034.078421273
y: -3768100.560012433
z: 4525944.676268726
Landmark GPS coordinates:
GPS:
lat: 45.499278
lon: -122.708417
alt: 479 (meters)
ECEF:
x: -2420030.781624382
y: -3768367.5284123267
z: 4526754.604333807
I tried following the math from here to build a function to get me screen coordinates from 3D point coordinates.
When I put those ECEF points into my projection function, with a viewport of 1440x335 I get: x: 721, y: 167
Here is my function:
function projectionCoordinates(origin, destination) {
const relativeX = destination.x - origin.x;
const relativeY = destination.y - origin.y;
const relativeZ = destination.z - origin.z;
const xPerspective = relativeX / relativeZ;
const yPerspective = relativeY / relativeZ;
const xNormalized = (xPerspective + viewPort.width / 2) / viewPort.width;
const yNormalized = (yPerspective + viewPort.height / 2) / viewPort.height;
const xRaster = Math.floor(xNormalized * viewPort.width);
const yRaster = Math.floor((1 - yNormalized) * viewPort.height);
return { x: xRaster, y: yRaster };
}
I believe the point should be placed much higher on the screen. That article I linked mentions 3x4 matrices which I couldn't follow along with (not sure how to build the 3x4 matrices from the 3D points). Maybe those are important, especially since I will eventually have to take the device's tilt into consideration (looking up or down with phone).
If it's needed, here is my function to convert latitude/longitude/altitude coordinates to ECEF (copy/pasted from another SO answer):
function llaToCartesion({ lat, lon, alt }) {
const cosLat = Math.cos((lat * Math.PI) / 180.0);
const sinLat = Math.sin((lat * Math.PI) / 180.0);
const cosLon = Math.cos((lon * Math.PI) / 180.0);
const sinLon = Math.sin((lon * Math.PI) / 180.0);
const rad = 6378137.0;
const f = 1.0 / 298.257224;
const C =
1.0 / Math.sqrt(cosLat * cosLat + (1 - f) * (1 - f) * sinLat * sinLat);
const S = (1.0 - f) * (1.0 - f) * C;
const h = alt;
const x = (rad * C + h) * cosLat * cosLon;
const y = (rad * C + h) * cosLat * sinLon;
const z = (rad * S + h) * sinLat;
return { x, y, z };
}
Your normalise and raster steps are cancelling out the view port scaling you need. Multiplying out this:
const xNormalized = (xPerspective + viewPort.width / 2) / viewPort.width;
gives you:
const xNormalized = xPerspective / viewPort.width + 0.5;
And applying this line:
const xRaster = Math.floor(xNormalized * viewPort.width);
gives you:
const xRaster = Math.floor(xPerspective + viewPort.width * 0.5);
Your calculation of xPerspective is correct (but see comment below) - however the value is going to be around 1 looking at your numbers. Which is why the point is near the centre of the screen.
The correct way to do this is:
const xRaster = Math.floor(xPerspective * viewPort.width /2 + viewPort.width /2);
You can simplify that. The idea is that xPerspective is the tan of the angle that xRelative subtends at the eye. Multiplying the tan by half the width of the screen gives you the x distance from the centre of the screen. You then add the x position of the centre of the screen to get the screen coordinate.
Your maths uses an implicit camera view which is aligned with the x, y, z axes. To move the view around you need to calculate xRelative etc relative to the camera before doing the perspective divide step (division by zRelative). An easy way to do this is to represent your camera as 3 vectors which are the X,Y,Z of the camera view. You then calculate the projection of the your 3D point on your camera by taking the dot product of the vector [xRelative, yRelative, zRelative] with each of X,Y and Z. This gives you a new [xCamera, yCamera, zCamera] which will change as you move your camera. You can also do this with matrices.
In the Social Isometric Games book, page 53, there is a description of the method, that defines a title using mouse cursor coordinates. It works well, but it is not clear what principles are used to make it work. Can someone explain this algorithm in details? Maybe you have some links to the formula. Or maybe you can advise me on which field of science i should look at.
Here is an example of a code that I am interested in:
var col = (e.clientY - gridOffsetY) * 2; //???
col = ((gridOffsetX + col) - e.clientX) / 2; //???
var row = ((e.clientX + col) - tile.height) - gridOffsetX; //???
As I can see, height of the rhombus cell is 2*p, width is 4*p, where p is characteristic size value.
Let's coordinates of base point - top vertice of top cell is (0, 0).
So top-right edge vector is tr=(2p, p), top-left vector is tl=(-2p, p).
Let's mouse point is (mx, my) relative to base point. To find what cell it belongs to, one could decompose vector (mx, my) by basis vectors tl, tr
mx = R * tl.x + C * tr.x
my = R * tl.y + C * tr.y
or
mx = R * 2 * p + C * p
my = R * (- 2 * p) + C * p
to find C , we can add both equations
mx + my = 2 * C * p
C = (mx + my) / (2 * p)
column = Floor(C) //integer part of C
to find R, subtract equations
R = (mx - my) / (4 * p)
row = Floor(R)
other calculations take into account shift of the grid on the screen
I've been trying to implement clicking in my webgl app for the last 6 hours and I can't find anything clear enough about this subject.
What I have came up with so far is, in pseudo code:
screenSpace = mousePosition;
normalizedScreenSpace = (screenSpace.x/screen.width, screenSpace.y/screen.height);
camSpace = invertProjectionMatrix * normalizedScreenSpace;
worldSpace = invertViewMatrix * camSpace;
Printing out the worldSpace coordinates, and it doesn't corresponds to other objects in the scene. What am I doing wrong?
The viewProjection matrix brings a vec3 from world space to clip space and so its inverse does the reverse, clip space to world space. Whats missing is the perspective divide that gpu handles for you behind the hood so you have to account for that as well. Add in the screen width and height and you have your screen to world:
screenToWorld: function(invViewProjection, screenWidth, screenHeight){
// expects this[2] (z value) to be -1 if want position at zNear and +1 at zFar
var x = 2*this[0]/screenWidth - 1.0;
var y = 1.0 - (2*this[1]/screenHeight); // note: Y axis oriented top -> down in screen space
var z = this[2];
this.setXYZ(x,y,z);
this.applyMat4(invViewProjection);
var m = invViewProjection;
var w = m[3] * x + m[7] * y + m[11] * z + m[15]; // required for perspective divide
if (w !== 0){
var invW = 1.0/w;
this[0] *= invW;
this[1] *= invW;
this[2] *= invW;
}
return this;
},
And the reverse calculation:
worldToScreen: function(viewProjectionMatrix, screenWidth, screenHeight){
var m = viewProjectionMatrix;
var w = m[3] * this[0] + m[7] * this[1] + m[11] * this[2] + m[15]; // required for perspective divide
this.applyMat4(viewProjectionMatrix);
if (w!==0){ // do perspective divide and NDC -> screen conversion
var invW = 1.0/w;
this[0] = (this[0]*invW + 1) / 2 * screenWidth;
this[1] = (1-this[1]*invW) / 2 * screenHeight; // screen space Y goes from top to bottom
this[2] *= invW;
}
return this;
},
I have a function which gets the mouse position in world space, then checks to see if the mouse is over or near to the circle's line.
The added complication how ever is the circle is transformed at an angle so it's more of an ellipse. I can't see to get the code to detect that the mouse is near the border of circle and am unsure where I am going wrong.
This is my code:
function check(evt){
var x = (evt.offsetX - element.width/2) + camera.x; // world space
var y = (evt.offsetY - element.height/2) + camera.y; // world space
var threshold = 20/scale; //margin to edge of circle
for(var i = 0; i < obj.length;i++){
// var mainAngle is related to the transform
var x1 = Math.pow((x - obj[i].originX), 2) / Math.pow((obj[i].radius + threshold) * 1,2);
var y1 = Math.pow((y - obj[i].originY),2) / Math.pow((obj[i].radius + threshold) * mainAngle,2);
var x0 = Math.pow((x - obj[i].originX),2) / Math.pow((obj[i].radius - threshold) * 1, 2);
var y0 = Math.pow((y - obj[i].originY),2) / Math.pow((obj[i].radius - threshold) * mainAngle, 2);
if(x1 + y1 <= 1 && x0 + y0 >= 1){
output.innerHTML += '<br/>Over';
return false;
}
}
output.innerHTML += '<br/>out';
}
To understand it better, I have a fiddle here: http://jsfiddle.net/nczbmbxm/ you can move the mouse over the circle, it should say "Over" when you are within the threshold of being near the circle's perimeter. Currently it does not seem to work. And I can't work out what the maths needs to be check for this.
There is a typo on line 34 with orignX
var x1 = Math.pow((x - obj[i].orignX), 2) / Math.pow((obj[i].radius + threshold) * 1,2);
should be
var x1 = Math.pow((x - obj[i].originX), 2) / Math.pow((obj[i].radius + threshold) * 1,2);
now you're good to go!
EDIT: In regards to the scaling of the image and further rotation of the circle, I would set up variables for rotation about the x-axis and y-axis, such as
var xAngle;
var yAngle;
then as an ellipse can be written in the form
x^2 / a^2 + y^2 / b^2 = 1
such as in Euclidean Geometry,
then the semi-major and semi-minor axes would be determined by the rotation angles. If radius is the circles actual radius. then
var semiMajor = radius * cos( xAngle );
var semiMinor = radius;
or
var semiMajor = radius;
var semiMinor = radius * cos( yAngle );
you would still need to do some more transformations if you wanted an x and y angle.
so if (xMouseC, yMouseC) are the mouse coordinates relative to the circles centre, all you must do is check if that point satisfies the equation of the ellipse to within a certain tolerance, i.e. plug in
a = semiMajor;
b = semiMinor;
x = xMouseC;
y = yMouseC;
and see if it is sufficiently close to 1.
Hope that helps!
This is all about mathematics. It's a shame that I'v forgotten those I learned in scool.
OK, I'm trying to get the image dimension after rotation (using canvas) with a certain angle in Javascript.
Since I don't have any tools other than MSPaint here, I'll re-use your image:
Say your original rectangle's size is R(ectangle)W(idth) * RH(eight),
in this case RW=200, RH=80;
After rotating a certain angle A, counterclockwise,
where 0deg <= A <= 90deg in degrees (or 0 <= A <= Math.PI/2 in radians),
in this case A=30deg or A=Math.PI/6,
In the new "outer" rectangle, each side is divided by two parts (for the convenience of describing; corresponding to the image).
On the left side, let's say the upper (purple) part is called N(ew)H(eight)U(p), and the lower (red) part is called NHL(ow);
Same rule on the bottom side, we have NW(idth)L(eft) (blue) and NWR(ight) (orange).
So the size (area) of new rectangle would be (NHU + NHL) * (NWL + NWR)
According to the definition of sin and cos:
NWL = RW * Math.cos(A); //where A is in radians
NHL = RW * Math.sin(A);
NHU = RH * Math.cos(A);
NWR = RH * Math.sin(A);
(if you're using A in degrees, replace A to Math.PI*A/180).
So the new "outer" width would be NWL + NWR, and new "outer" height would be NHU + NHL, and now you can calculate everything.
Here's a drop-in function that implements #Passerby's solution + a couple other safeguards:
function imageSizeAfterRotation(size, degrees) {
degrees = degrees % 180;
if (degrees < 0) {
degrees = 180 + degrees;
}
if (degrees >= 90) {
size = [ size[1], size[0] ];
degrees = degrees - 90;
}
if (degrees === 0) {
return size;
}
const radians = degrees * Math.PI / 180;
const width = (size[0] * Math.cos(radians)) + (size[1] * Math.sin(radians));
const height = (size[0] * Math.sin(radians)) + (size[1] * Math.cos(radians));
return [ width, height ];
}
// USAGE:
imageSizeAfterRotation([ 200, 80 ], 30) // [ 213.20508075688775, 169.28203230275508 ]