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;
},
Related
I am creating my own game engine in Java Script. I have many things working great but one problem I am haveing is converting game space coordinates into screen space coordinates.
In my camera class I calulate the screen position of any given entity like so
draw(Engine, Entity, FollowEntity){
var x = Entity.render_position.x - (FollowEntity.render_position.x - Engine.rendersize.x / 2) - (Entity._get_render_size().x / 2);
var y = Entity.render_position.y - (FollowEntity.render_position.y - Engine.rendersize.y / 2) - (Entity._get_render_size().y / 2);
var _x = Entity.render_position.x - (FollowEntity.render_position.x - Engine.rendersize.x / 2);
var _y = Entity.render_position.y - (FollowEntity.render_position.y - Engine.rendersize.y / 2);
Engine.ctx.save();
Engine.ctx.translate(_x, _y);
Engine.ctx.rotate(Entity.rotation * Math.PI / 180);
Engine.ctx.translate(-_x, -_y);
Engine.ctx.drawImage(Engine.rasterizer.get_cached_img(Entity), x, y); /**/
Engine.ctx.restore();
}
I have tried extensively to aid myself in solving this with ChatGPT to no avail.
here is my current attempt
// Calculate the position of the entity in the game world
var gameX = this.ContextEntity.position.x;
var gameY = this.ContextEntity.position.y;
// Calculate the position of the camera in the game world
var cameraX = this.Engine.follow_entity.position.x;
var cameraY = this.Engine.follow_entity.position.y;
// Calculate the position of the entity on the screen, taking into account the difference in pixel size between the game entities and the GUI elements
var screenX = (gameX - cameraX) * this.GUI.rasterizer.pixel_size / this.Engine.rasterizer.pixel_size;
var screenY = (gameY - cameraY) * this.GUI.rasterizer.pixel_size / this.Engine.rasterizer.pixel_size;
// Adjust the position of the entity on the screen to take into account the size of the entity
screenX -= this.ContextEntity._get_render_size().x / 2;
screenY -= this.ContextEntity._get_render_size().y / 2;
gameToScreen(game) {
var game1 = (typeof game.x !== 'undefined') ? game.x : game[0];
var game2 = (typeof game.y !== 'undefined') ? game.y : game[1];
var x = game1 - (this.control_entity._get_render_midpoint().x - (this.rendersize.x / 2)) / this.rasterizer.pixel_size;
var y = game2 - (this.control_entity._get_render_midpoint().y - (this.rendersize.y / 2)) / this.rasterizer.pixel_size;
return new Vec2(Math.floor(x), Math.floor(y));
}
and here is another previous attempt which only seems to work when my pixel multipliers for the Engine entities are 0
the engine is running at https://soft-crab-57.telebit.io
Any help would be greatly appriciated.
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
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!
I need to draw a line in the following manner:
For now, it will be only drawn in code, no user input.
My question is, how to draw perpendiculars to a line, if I draw it point by point? (Obviously, this will be the case, because drawing with bezier curves will not give me the possibility to somehow impact the drawing).
The closest answer I found was possibly this one, but I can't reverse the equations to derive C. Also there is no length of the decoration mentioned, so I think this will not work as I'd like it to.
Find the segment perpendicular to another one is quite easy.
Say we have points A, B.
Compute vector AB.
Normalize it to compute NAB (== the 'same' vector, but having a length of 1).
Then if a vector has (x,y) as coordinates, its normal vector has (-y,x) as coordinates, so
you can have PNAB easily (PNAB = perpendicular normal vector to AB).
// vector AB
var ABx = B.x - A.x ;
var ABy = B.y - A.y ;
var ABLength = Math.sqrt( ABx*ABx + ABy*ABy );
// normalized vector AB
var NABx = ABx / ABLength;
var NABy = ABy / ABLength;
// Perpendicular + normalized vector.
var PNABx = -NABy ;
var PNABy = NABx ;
last step is to compute D, the point that is at a distance l of A : just add l * PNAB to A :
// compute D = A + l * PNAB
var Dx = A.x + l* PNAB.x;
var Dy = A.y + l *PNAB.y;
Updated JSBIN :
http://jsbin.com/bojozibuvu/1/edit?js,output
Edit :
A second step is to draw the decorations at regular distance, since it's Christmas time, here's how i would do it :
http://jsbin.com/gavebucadu/1/edit?js,console,output
function drawDecoratedSegment(A, B, l, runningLength) {
// vector AB
var ABx = B.x - A.x;
var ABy = B.y - A.y;
var ABLength = Math.sqrt(ABx * ABx + ABy * ABy);
// normalized vector AB
var NABx = ABx / ABLength;
var NABy = ABy / ABLength;
// Perpendicular + normalized vector.
var PNAB = { x: -NABy, y: NABx };
//
var C = { x: 0, y: 0 };
var D = { x: 0, y: 0 };
//
drawSegment(A, B);
// end length of drawn segment
var endLength = runningLength + ABLength;
// while we can draw a decoration on this line
while (lastDecorationPos + decorationSpacing < endLength) {
// compute relative position of decoration.
var decRelPos = (lastDecorationPos + decorationSpacing) - runningLength;
// compute C, the start point of decoration
C.x = A.x + decRelPos * NABx;
C.y = A.y + decRelPos * NABy;
// compute D, the end point of decoration
D.x = C.x + l * PNAB.x;
D.y = C.y + l * PNAB.y;
// draw
drawSegment(C, D);
// iterate
lastDecorationPos += decorationSpacing;
}
return ABLength;
}
All you need is direction of curve (or polyline segment) in every point, where you want to draw perpendicular.
If direction vector in point P0 is (dx, dy), then perpendicular (left one) will have direction vector (-dy, dx). To draw perpendicular with length Len, use this pseudocode:
Norm = Sqrt(dx*dx + dy*dy) //use Math.Hypot if available
P1.X = P0.X - Len * dy / Norm
P1.Y = P0.Y + Len * dx / Norm
P.S. If you know direction angle A, then direction vector
(dx, dy) = (Cos(A), Sin(A))
and you don't need to calculate Norm, it is equal to 1.0
I'm developing a user-interface for positioning an image on a google map.
I started from : http://overlay-tiler.googlecode.com/svn/trunk/upload.html which is pretty close to what I want.
But instead of 3 contact points I want a rotate tool, a scale tool and a translate tool (the later exists).
I tried to add a rotate tool but it doesn't work as I expected :
I put a dot on the left bottom corner that control the rotation (around the center of the image). The mouse drag the control dot and I calculate the 3 others points.
My code is based on the mover object but I changed the onMouseMove function :
overlaytiler.Rotater.prototype.rotateDot_ = function(dot, theta, origin) {
dot.x = ((dot.x - origin.x) * Math.cos(theta) - (dot.y - origin.y) * Math.sin(theta)) + origin.x;
dot.y = ((dot.x - origin.x) * Math.sin(theta) + (dot.y - origin.y) * Math.cos(theta)) + origin.y;
dot.render();
};
overlaytiler.Rotater.prototype.onMouseMove_ = function(e) {
var dots = this.controlDots_;
var center = overlaytiler.Rotater.prototype.getCenter_(dots);
// Diagonal length
var r = Math.sqrt(Math.pow(this.x - center.x, 2) + Math.pow(this.y - center.y, 2));
var old = {
x: this.x,
y: this.y
};
// Real position
var newPos = {
x: this.x + e.clientX - this.cx,
y: this.y + e.clientY - this.cy
}
var newR = Math.sqrt(Math.pow(newPos.x - center.x, 2) + Math.pow(newPos.y - center.y, 2));
var theta = - Math.acos((2 * r * r - (Math.pow(newPos.x - old.x, 2) + Math.pow(newPos.y - old.y, 2))) / (2 * r * r));
// Fixed distance position
this.x = (newPos.x - center.x) * (r / newR) + center.x;
this.y = (newPos.y - center.y) * (r / newR) + center.y;
dots[1].x = center.x + (center.x - this.x);
dots[1].y = center.y + (center.y - this.y);
dots[1].render();
overlaytiler.Rotater.prototype.rotateDot_(dots[2], theta, center);
overlaytiler.Rotater.prototype.rotateDot_(dots[0], theta, center);
// Render
this.render();
this.cx = e.clientX;
this.cy = e.clientY;
};
Unfortunately there is a problem with precision and angle sign.
http://jsbin.com/iQEbIzo/4/
After a few rotations the image is highly distorted and rotation is supported only in one direction.
I wonder how I can achieve a great precision and without any distortion.
Maybe my approach is useless here (try to move the corners at the right coordinates), I tried to rotate the image with the canvas but my attempts were unsuccessful.
Edit : Full working version : http://jsbin.com/iQEbIzo/7/
Here is my version of it. #efux and #Ben answers are far more complete and well designed however the maps don't scale in/out when you zoom in/out. Overlays very likely need to do this since they are used to put a "second map" or photograph over the existing map.
Here is the JSFiddle: http://jsfiddle.net/adelriosantiago/3tzzwmsx/4/
The code that does the drawing is the following:
DebugOverlay.prototype.draw = function() {
var overlayProjection = this.getProjection();
var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());
var div = this.div_;
div.style.left = sw.x + 'px';
div.style.top = ne.y + 'px';
div.style.width = (ne.x - sw.x) + 'px';
div.style.height = (sw.y - ne.y) + 'px';
div.style.transform = 'rotate(' + rot + 'deg)';
};
For sure this code could be implemented on efux and Ben code if needed but I haven't tried yet.
Note that the box marker does not updates its position when the rotation marker moves...
rotation is supported only in one direction
This is due to how you calculate the angle between two vectors.
It always gives you the same vector no matter if the mouse is right of the dot or not. I've found a solution in a german math board (unfortunately I cant access the site without using the cache of Google : cached version).
Note that in this example the angle α is on both sides the same and not as you would expect -α in the second one. To find out if the vector a is always on "the same side" of vector b you can use this formula.
ax*by - ay*bx
This is either positive or negative. You you simply can change the sign of the angle to α * -1.
I modified some parts of your code.
overlaytiler.Rotater.prototype.rotateDot_ = function(dot, theta, origin) {
// translate to origin
dot.x -= origin.x ;
dot.y -= origin.y ;
// perform rotation
newPos = {
x: dot.x*Math.cos(theta) - dot.y*Math.sin(theta),
y: dot.x*Math.sin(theta) + dot.y*Math.cos(theta)
} ;
dot.x = newPos.x ;
dot.y = newPos.y ;
// translate back to center
dot.x += origin.x ;
dot.y += origin.y ;
dot.render();
};
If you want to know, how I rotate the points please reference to this site and this one.
overlaytiler.Rotater.prototype.onMouseMove_ = function(e) {
var dots = this.controlDots_;
var center = overlaytiler.Rotater.prototype.getCenter_(dots);
// get the location of the canvas relative to the screen
var rect = new Array() ;
rect[0] = dots[0].canvas_.getBoundingClientRect() ;
rect[1] = dots[1].canvas_.getBoundingClientRect() ;
rect[2] = dots[2].canvas_.getBoundingClientRect() ;
// calculate the relative center of the image
var relCenter = {
x: (rect[0].left + rect[2].left) / 2,
y: (rect[0].top + rect[2].top) / 2
} ;
// calculate a vector from the center to the bottom left of the image
dotCorner = {
x: rect[1].left - (rect[1].left - relCenter.x) * 2 - relCenter.x,
y: rect[1].top - (rect[1].top - relCenter.y) * 2 - relCenter.y
} ;
// calculate a vector from the center to the mouse position
mousePos = {
x: e.clientX - relCenter.x,
y: e.clientY - relCenter.y
} ;
// calculate the angle between the two vector
theta = calculateAngle(dotCorner, mousePos) ;
// is the mouse-vector left of the dot-vector -> refer to the german math board
if(dotCorner.y*mousePos.x - dotCorner.x*mousePos.y > 0) {
theta *= -1 ;
}
// calculate new position of the dots and render them
overlaytiler.Rotater.prototype.rotateDot_(dots[2], theta, center);
overlaytiler.Rotater.prototype.rotateDot_(dots[1], theta, center);
overlaytiler.Rotater.prototype.rotateDot_(dots[0], theta, center);
// Render
this.render();
this.cx = e.clientX;
this.cy = e.clientY;
};
You can see that I wrote some function for vector calculations (just to make the code more readable):
function calculateScalarProduct(v1,v2)
{
return (v1.x * v2.x + v1.y * v2.y) ;
}
function calculateLength(v1)
{
return (Math.sqrt(v1.x*v1.x + v1.y*v1.y)) ;
}
function calculateAngle(v1, v2)
{
return (Math.acos(calculateScalarProduct(v1,v2) / (calculateLength(v1)*calculateLength(v2)))) ;
}
This is my working solution. Comment if you don't understand something, so I can make my answer more comprehensive.
Working example: JSBin
Wow, this was a tough one.