I'm working with JavaScript(React) on a geometry program that creates the axonometry(better parallel projections) of the specified object defined by vertices and faces(a face can have different numbers of vertices).
It works perfectly when you do not need faces to be opaque otherwise there are faces above other that should be below.
So I want to order my list of faces from the further to the nearest:
[
[[100, 0, 100], [50, 50, 50], [120, 170, 120], [10, 200, 150]],
[[10, 20, 30], [10, 200, 250], [50, 50, 50], [100, 30, 30]],...
]
I will use faces.sort(sortingFunction).
I don't care about intersecting faces
(it will take the faces of all objects together)
How should sortingFunction be?
You have to consider how is the axonometry defined. It is defined by the X-, Y-axis rotation(Xrotation can be both greater and smaller than Yrotation), Z rotation is π / 4 (90°).
Here is an old version of the application that makes you understand what I mean: http://dev_proiezioni.surge.sh/
Sorry for my terrible English.
Thanks
What you are trying to do is called "back-face culling. One common technique is to determine if the list of the points in the representation of a polygon are in clockwise or counterclockwise order from the point of view of the camera. This requires that you are very careful about how you create the list of vertices. For more details, check out the wikipedia article: https://en.wikipedia.org/wiki/Back-face_culling. The Implementation section describes the mathematics involved which you will have to translate into JavaScript. Note that this technique is faster than sorting the list of faces because it requires checking each face only once rather than comparing each face against other faces.
I don't care about intersecting faces
That means that we can reduce the plains to points, by taking the point in the middle:
const vecOp = op => (a, b) => a.map((c, i) => op(c, b[i] || b));
const add = vecOp((a, b) => a + b);
const sub = vecOp((a, b) => a - b);
const mul = vecOp((a, b) => a * b);
const div = vecOp((a, b) => a / b);
const sum = v => v.reduce((a, b) => a + b, 0);
const middle = (a, b) => div(add(a, b), 2);
const planeToPoint = ([a, b, c, d]) => middle(
middle(a, b),
middle(c, d)
);
Now to sort by "closer to camera", one could draw a line between the centers of two planes, which will result in a direction:
const aToB = (a, b) =>
sub(
planeToPoint(b),
planeToPoint(a)
);
Now we could turn the camera rotation into a camera lookAt vector :
const rotToVec = (yaw, pitch) => ([
Math.cos(yaw) * Math.cos(pitch),
Math.sin(yaw) * Math.cos(pitch),
Math.sin(pitch)
]);
and that direction can be compared to the cameras direction resulting in an angle between them:
const angle = (a, b) => sum(mul(a, b)) / sum(a) * sum(b)
Now lets turn that alltogether:
const camVec = rotToVec(cam.yaw, cam.pitch);
planes.sort((a, b) =>
Math.abs(angle(aToB(a, b), camVec)) < Math.PI / 4 /*90°*/ ? 1 : -1
);
Disclaimer: I neither tried the code above, nor did I work with parallel projections, nor am I good at math, so take my words with caution, I have no idea what I'm talking about
For an approximate solution, use a 3D to 3D transform, and consider the Z coordinate. For every face, keep the nearest Z and sort the faces on this Z.
For a more exact solution, consider https://en.wikipedia.org/wiki/Newell%27s_algorithm.
Sort by the distance from where you camera is.
function distanceBetweenTwoPoints (p1, p2) {
return Math.hypot(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z)
}
function sortFunction (p1, p2) {
return distanceBetweenTwoPoints(camera, p1) > distanceBetweenTwoPoints(camera,p2) ? -1 : 1
}
Tweak the sign > depending on which order you'd like.
Related
I need to find a point in 3d space with a function that returns the distance between that point and any other point of my choosing. all the coordinates can vary from 0 to 100 and only can be integers. Of course, I could use brute force, but the complexity will be n^3, and that’s way too far from minimum steps
I was trying to reach the solution using binary search, because we can imagine, that I have an array from 0 to 100, where each point represents some axis, x-axis for example. And by moving either to the left or to the right I will be moving closer to the point, but my solution does not work correctly
This is the code I could come up with. At some points it works, but not all
const findClosestX = (low, high, destinationPoint) => {
const middle = Math.floor((low + high) / 2);
const starting_distance = new Point(middle, 0, 0).getDistance(destinationPoint);
// Point class is simple object with x,y,z coordinates and function
// for getting distance between points
const distance_1 = new Point(low, 0, 0).getDistance(destinationPoint);
const distance_2 = new Point(high, 0, 0).getDistance(destinationPoint);
const minPoint = Math.min(starting_distance, distance_1, distance_2);
if (minPoint === starting_distance) {
return middle;
} else if (minPoint === distance_1) return findClosestX(low, middle - 1, destinationPoint);
else if (minPoint === distance_2) return findClosestX(middle + 1, high, destinationPoint);
};
For the one-dimensional case there's the bisection method which is the continuous version of the well known binary search algorithm.
It is used to find the solution of an equation,
that is the zero of a function f(x) = 0, in an interval [a, b],
where the function is monotonous (increasing or decreasing),
and f(a) > 0 and f(b) < 0 (or f(a) < 0, f(b) > 0),
which guarantees that there is exactly one solution
in the interval [a, b].
It consists in a loop that maintains three points: left, right and middle.
Initially, they are the a, b and (a+b)/2.
At each step it updates the three points by either:
choosing the middle point to be the next left point (chooses the right half,
the new interval is [middle, right])
choosing the middle point to be the right point
(chooses the left half, the new interval is [left, middle]).
The next middle will be computed as the middle of the new interval.
the choice between the variants above is made in such a way
that the sign of the f(left) is the same as the sign of f(a) and
the sign of the f(right) is the same as f(b). This guarantees that
the solution is always inside the interval [left, right] and
since the interval is always shrinking (it's half the length
of the interval in the previous step) it's guaranteed to
get ever close to the true solution.
the iteration is stopped when the current middle point
is close enough to the solution |f(middle)| < eps
The first step in adjusting this algorithm to the problem
here is the most sensitive: making the algorithm work
for an optimization problem. I'll restrict the analysis
to a minimization problem, as it is the case here.
The change in the algorithm is trivial: from the points
{left, middle, right}, we choose the two with the minimal value
of f. That is if f(left) < f(right), we chose [left, middle]
and if f(left) > f(right), we choose [middle, right].
However, care should be taken that this algorithm is
convergent for a minimization problem only in very special cases.
A sufficient condition for the algorithm to converge (that is
fortunately satisfied by the Euclidean distance) is that
the function f is symmetrical with respect to
its minimum in the interval we work with:
f(xmin - d) = f(xmin + d), when
xmin - d and xmin + d are inside the
interval [a, b].
The second step now is to extend the above algorithm for
a 3-dimensional minimization problem. Instead of the
interval [a, b] we start with the initial cube (eight 3d points)
and its center.
In the next step we choose the
point among the eight corners that has the minimum f(x, y, z) - from that point and the center as diagonally opposite we form
a new cube - that is we chose one of the eight "half-cubes"
of the previous cube determined by the center.
The symmetry condition in this case can be written as f(xmin±dx, ymin±dy, zmin±dz) = f(xmin∓dx, ymin∓dy, zmin∓dz)
Here's the code for the continuous case (i.e., the coordinates are floating point numbers):
const x0 = Math.random()*100, y0 = Math.random()*100, z0 = Math.random()*100;
console.log('Original point', [x0, y0, z0]);
function dist(x, y, z){
return Math.sqrt((x-x0)*(x-x0)+(y-y0)*(y-y0)+(z-z0)*(z-z0));
}
//utility function
function makeCube([xA, yA, zA], [xB, yB, zB]){
return {
cube: [[xA, yA, zA],[xB, yA, zA],[xA, yB, zA],[xA, yA, zB],
[xA, yB, zB], [xB, yB, zA], [xB, yA, zB], [xB, yB, zB]],
center: [(xA+xB)/2, (yA+yB)/2, (zA+zB)/2]
}
}
//utility function
function minIndex(a){
return a.reduce(([minValue, minIndex], xi, i) => xi < minValue ? [xi, i] : [minValue, minIndex], [1/0, -1]);
}
function findPoint(eps = 1e-5, NMAX = 10000){
let A = [0, 0, 0], B = [100, 100, 100];
for(let i = 0; i < NMAX; i++){
let {cube, center} = makeCube(A, B);
const d = cube.map(p=>dist(...p));
const [dMin, iMin] = minIndex(d);
if(dMin < eps){
return center;
}
A = cube[iMin];
B = center;
}
return null; // failed after NMAX iterations
}
console.log('Found point', findPoint())
and its adaptation for the case the coordinates are integers
const x0 = Math.floor(Math.random()*101), y0 = Math.floor(Math.random()*101), z0 = Math.floor(Math.random()*101);
console.log('Original point', [x0, y0, z0]);
function dist(x, y, z){
return Math.sqrt((x-x0)*(x-x0)+(y-y0)*(y-y0)+(z-z0)*(z-z0));
}
//utility function
function makeCube([xA, yA, zA], [xB, yB, zB]){
return {
cube: [[xA, yA, zA],[xB, yA, zA],[xA, yB, zA],[xA, yA, zB],
[xA, yB, zB], [xB, yB, zA], [xB, yA, zB], [xB, yB, zB]],
center: [Math.round((xA+xB)/2), Math.round((yA+yB)/2), Math.round((zA+zB)/2)]
}
}
//utility function
function minIndex(a){
return a.reduce(([minValue, minIndex], xi, i) => xi < minValue ? [xi, i] : [minValue, minIndex], [1/0, -1]);
}
function findPoint(NMAX = 10000){
let A = [0, 0, 0], B = [100, 100, 100];
for(let i = 0; i < NMAX; i++){
let {cube, center} = makeCube(A, B);
const d = cube.map(p=>dist(...p));
const [dMin, iMin] = minIndex(d);
if(dMin === 0){
console.log(i+' steps');
return cube[iMin];
}
A = cube[iMin];
B = center;
}
return null; // failed after NMAX iterations
}
console.log('Found point', findPoint())
Note that in both cases an optimization can be made by reusing the previously computed distance for the point in the new cube that was inherited from the previous cube, but I feel that it would make the code less readable.
I am new to Ramda.js. I've been reading/learning a lot about the library lately, and am starting to apply my knowledge to "real life" code. One thing I am struggling w/ is refactoring functions with multiple parameters. I have something that works, but I'm not certain that this is the "best" way to do it. Any direction would be greatly appreciated.
So here are my input arguments:
const normalizedData = [
[-500, -500, -500, 0],
[-400, -500, -400, 0],
[-300, -500, -300, 0],
[-200, -500, -200, 0],
// ...
];
const is2D = {
x : false,
y : false,
z : true
};
const minHeight = 4;
The original function takes a 2D array of data and applies some transformation logic while flattening, but it requires 2 other parameters besides the data (minHeight, is2D). The output is fed to some WebGL code to render some 3D stuff.
This is my original function:
function computeTranslations_OLD(minHeight, is2D, normalizedData){
return normalizedData.flatMap(el => {
const [x, y1, y2, z] = el,
yMin = Math.min(y1, y2),
height = Math.max(Math.abs(y1 - y2), minHeight),
yOrigin = (height / 2) + yMin;
return [
is2D.x ? x + 2 : x, // x origin
yOrigin, // y origin
is2D.z ? z + 2 : z, // z origin
height
]
});
}
And these are my refactored functions, basically split into a function that operates on each iteration, and a HOF that 'glues' it all together:
function computeTranslation(minHeight, is2D, normalizedData){
const [x, y1, y2, z] = normalizedData,
yMin = Math.min(y1, y2),
height = Math.max(Math.abs(y1 - y2), minHeight),
yOrigin = (height / 2) + yMin;
return [
is2D.x ? x + 2 : x,
yOrigin,
is2D.z ? z + 2 : z,
height
]
}
// HOF
function computeTranslations(minHeight, is2D, normalizedData){
return R.chain(
R.partial(
computeTranslation,
[minHeight, is2D]
),
normalizedData
);
}
Is this an acceptable approach for refactoring something like this? Or is there a better way? As is, it works, but being new to Ramda, and still wrapping my head around FP techniques... it would be nice to have some outside input.
Thanks in advance!!!
I think you're right to question this. It feels awkward.
A first question, though, is why you want to use a Ramda implementation. The code seems clean and understandable as is. I'm a founder of Ramda and a big fan, but I treat it as a tool to use when it offers benefits, and to ignore otherwise. It this is a learning exercise, that's fine. But if not, they first question should probably not be how do I do this in Ramda? but would Ramda tools improve this?
Assuming you still want to use Ramda, then we can definitely improve the outer function. And the first step would be to curry the inner one:
const computeTranslation = R.curry(function(minHeight, is2D, normalizedData) {
const [x, y1, y2, z] = normalizedData,
yMin = Math.min(y1, y2),
height = Math.max(Math.abs(y1 - y2), minHeight),
yOrigin = (height / 2) + yMin;
return [
is2D.x ? x + 2 : x,
yOrigin,
is2D.z ? z + 2 : z,
height
]
})
Now all these are equivalent:
/* 1 */ computeTranslation (minHeight, is2D, el)
/* 2 */ computeTranslation (minHeight, is2D) (el)
/* 3 */ computeTranslation (minHeight) (is2D, el)
/* 4 */ computeTranslation (minHeight) (is2D) (el)
And we can use that to write a cleaner version of computeTranslations (and here I switch to arrow functions as they are almost always neater.)
const computeTranslations = (minHeight, is2D, normalizedData) =>
R.chain (computeTranslation (minHeight, is2D)) (normalizedData)
computeTranslations (minHeight, is2D, normalizedData)
If you don't mind switching calling conventions a bit, then this can become slightly nicer:
const computeTranslations = (minHeight, is2D) =>
R.chain (computeTranslation (minHeight, is2D))
computeTranslations (minHeight, is2D) (normalizedData)
But we can go one step further and turn that into
const computeTranslations = R.compose (R.chain, computeTranslation)
computeTranslations (minHeight, is2D) (normalizedData)
Currying is a very important feature of Ramda, and if it's not clear to you, I would suggest you investigate it further. It is in good part what makes the typical Ramda style possible. An old article of mine gives a pretty good introduction, and links to other useful sources.
Essentially I'm trying to create the game reversi.
To cut it short if you don't know what it is, I have a 8x8 board of squares. There are 2 coordinates and I need to determine all the squares that are between the two coordinates and fill them in. The 2 coordinates are either on the same y, same x or diagonal to each other.
Can someone explain the logic behind how I would go about doing something like this? How can I determine the coordinates of all the elements between the 2 coordinates.
You need a simple for loop, starting at one of the coordinates and moving towards the other.
let connect = (c1, c2) => {
// Determine the distance between c1 & c2
let delta = c1.map((v, i) => c2[i] - v);
let distance = Math.max(...delta.map(v => Math.abs(v)));
// Determine the unit vector (e.g. [1, -1]) to move each iteration
let direction = delta.map(v => v / distance);
// Starting at `c1`, iterate for `distance` iterations, moving in `direction` each iteration.
return [...Array(distance + 1)].map((_, i) => c1.map((v, j) => v + direction[j] * i));
// Same as above, but exclude `c1` and `c2` from the return array.
// return [...Array(distance - 1)].map((_, i) => c1.map((v, j) => v + direction[j] * (i + 1)));
};
let c1 = [3, 6];
let c2 = [8, 1];
console.log(connect(c1, c2));
For a camera movement in three.js I need to calculate point C so to move the camera from point A to a certain distance dist to point B.
three.js has methods to do that very easily.
Assuming a, b, and c are instances of THREE.Vector3(),
a.set( 2, 1, 4 );
b.set( 9, 4, 2 );
c.subVectors( a, b ).setLength( dist ).add( b );
three.js r.91
So you need to calculate the coordinates of point C, given that it lies on the line between B and A at the given distance from B? This is pretty straightforward using the following steps:
Calculate the vector from B to A (this will just be A - B).
Normalize that vector (make it's length 1).
Multiply that by the distance you want.
Add that vector to the point B.
So a short javascript example:
const A = [2, 1, 4];
const B = [9, 4, 2];
const dist = 3;
function addVectors(v1, v2) {
return v1.map((x, i) => x + v2[i]);
}
function scalarMultiply(v, c) {
return v.map(x => x*c);
}
function getLength(v) {
return Math.hypot(...v);
}
const vecB2A = addVectors(A, scalarMultiply(B, -1)); // Step 1
const normB2A = scalarMultiply(vecB2A, 1/getLength(vecB2A)); // Step 2
const distB2A = scalarMultiply(normB2A, dist); // Step 3
const C = addVectors(B, distB2A); // Final step
console.log(C);
Point C is equal to point B minus 'dist' times a unit vector whose direction is AB. So it is quite easy:
vector v from A to B is equal (xB-xA, yB-yA, zB-zA) / distance(AB)
Then C = B - d*v where d is the distance from B you want C to be.
I am trying to rotate a vector [x,y] around the origin such that when the rotation is completed it lies on the X axis. In order to do this, I'm first computing the angle between [x,y] and [1,0], then applying a simple 2D rotation matrix to it. I'm using numericjs to work with the vectors.
math.angleBetween = function(A, B) {
var x = numeric.dot(A, B) / (numeric.norm2(A) * numeric.norm2(B));
if(Math.abs(x) <= 1) {
return Math.acos(x);
} else {
throw "Bad input to angleBetween";
}
};
math.alignToX = function(V) {
var theta = -math.angleBetween([1,0], V);
var R = [[Math.cos(theta), -Math.sin(theta)],
[Math.sin(theta), Math.cos(theta)]];
return numeric.dot(R, V);
};
(Note: math is a namespace object within my project. Math is ye olde math object.)
This code works sometimes, however there are occasions where no matter how many times I run math.alignToX the vector never even gets close to aligning with the X axis. I'm testing this by checking if the y coordinate is less than 1e-10.
I've also tried using Math.atan2 with an implicit z coordinate of 0, but the results have been the same. Errors are not being thrown. Some example results:
math.alignToX([152.44444444444434, -55.1111111111111])
// result: [124.62691466033475, -103.65652585400568]
// expected: [?, 0]
math.alignToX([372, 40])
// result: [374.14435716712336, -2.0605739337042905e-13]
// expected: [?, 0]
// this value has abs(y coordinate) < 1e-10, so its considered aligned
What am I doing wrong?
If you're rotating something other than your vector, then you'll need to use your R matrix. But if you just need to rotate your vector, the result will be [Math.sqrt(x*x+y*y),0].
Actually, the task of building a rotation matrix that aligns a known 2d vector with [1, 0] doesn't require any trigonometric functions at all.
In fact, if [x y] is your vector and s is its length (s = Sqrt(x*x + y*y)), then the transformation that maps [x y] to align with [1 0] (pure rotation, no scaling) is just:
[ x y]
T = 1/s^2 [-y x]
For example, suppose your vector is [Sqrt(3)/2, 1/2]. This is a unit vector as you can easily check so s = 1.
[Sqrt(3)/2 1/2 ]
T = [ -1/2 Sqrt(3)/2]
Multiplying T by our vector we get:
[Sqrt(3)/2 1/2 ][Sqrt(3)/2] [1]
T = [ -1/2 Sqrt(3)/2][ 1/2 ] = [0]
So in finding the rotation angle (which in this case is Pi/6) and then creating the rotation matrix, you've just come full circle back to what you started with. The rotation angle for [Sqrt(3)/2, 1/2] is Pi/2, and cos(Pi/2) is Sqrt(3)/2 = x, sin(pi/2) is 1/2 = y.
Put another way, if you know the vector, you ALREADY know the sine and cosine of it's angle with the x axis from the definition of sine and cosine:
cos a = x/s
sin a = y/s where s = || [x, y] ||, is the length of the vector.
My problem is so mind-bendingly obvious that I cannot believe I didn't see it. While I'm checking the domain of Math.acos, I'm not checking the range at all! The problem occurs when the vector lies outside of the range (which is [0,PI]). Here is what I did to fix it:
math.alignToX = function(V) {
var theta = -math.angleBetween([1,0], V);
var R = [[Math.cos(theta), -Math.sin(theta)],
[Math.sin(theta), Math.cos(theta)]];
var result = numeric.dot(R, V);
if(Math.abs(result[1]) < ZERO_THRESHOLD) {
return result;
} else {
V = numeric.dot([[-1, 0], [0, -1]], V); // rotate by PI
theta = -math.angleBetween([1,0], V);
R = [[Math.cos(theta), -Math.sin(theta)],
[Math.sin(theta), Math.cos(theta)]];
result = numeric.dot(R, V);
if(Math.abs(result[1]) < ZERO_THRESHOLD) {
return result;
} else {
throw "Unable to align " + V; // still don't trust it 100%
}
}
};
For the broken example I gave above, this produces:
[162.10041088743887, 2.842170943040401e-14]
The Y coordinate on this result is significantly less than my ZERO_THRESHOLD (1e-10). I almost feel bad that I solved it myself, but I don't think I would have done so nearly as quickly had I not posted here. I saw the problem when I was checking over my post for typos.