Apply Rotation to Cylinder based on Tube Ending Normal - javascript
I am attempting to make a curved 3D arrow in three.js. To accomplish this task, I have created a Tube that follows a curved path and a Cylinder shaped as a cone (by setting radiusTop to be tiny). They currently look like so:
I am attempting to position the Arrow Head (Cylinder shaped as a cone) at the end of the Tube like so: (Photoshopped)
I am not terribly strong in math and pretty new to three.js. Could someone help me understand how to connect the two?
Here is my current code:
import T from 'three';
var findY = function(r, x)
{
return Math.sqrt((r * r) - (x * x));
}
var radius = 25;
var x = 0;
var z = 0;
var numberOfPoints = 10;
var interval = (radius/numberOfPoints);
var points = [];
for (var i = numberOfPoints; i >= 0; i--)
{
var y = findY(radius, x);
points.push(new T.Vector3(x, y, z))
x = x + interval;
}
x = x - interval;
for (var i = numberOfPoints - 1 ; i >= 0; i--)
{
y = findY(radius, x) * -1;
points.push(new T.Vector3(x, y, z));
x = x - interval;
}
var path = new T.CatmullRomCurve3(points);
var tubeGeometry = new T.TubeGeometry(
path, //path
10, //segments
radius / 10, //radius
8, //radiusSegments
false //closed
);
var coneGeometry = new T.CylinderGeometry(
radiusTop = 0.1,
radiusBottom = radius/5,
height = 10,
radialSegments = 10,
heightSegments = 10,
openEnded = 1
);
var material = new T.MeshBasicMaterial( { color: 0x00ff00 } );
var tube = new T.Mesh( tubeGeometry, material );
var cone = new T.Mesh( coneGeometry, material );
// Translate and Rotate cone?
I would greatly appreciate if someone could attempt a simple explanation of what is necessary mathematically and programmatically accomplish
Finding the normal located at the end of the tube
Shifting the Cone to the correct location
Any help is appreciated!
Do not use rotation for this when you can create the arrowhead directly in place. Similarly the bended tube can be done this way too. Only thing you need for it is the last line segment defined by A,B endpoints.
Let A be the sharp point and B the disc base center. To create arrowhead you need 2 additional basis vectors let call them U,V and radius r of base disc. From them you can create disc points with simple circle formula like this:
obtain AB endpoints
compute U,V basis vectors
The U,V should lie in the disc base of arrowhead and should be perpendicular to each other. direction of the arrowhead (line |BA|) is the disc base normal so exploit cross product which returns perpendicular vector to the multiplied ones so:
W = B-A;
W /= |W|; // unit vector
T = (1,0,0); // temp any non zero vector not parallel to W
if ( |(W.T)|>0.75 ) T = (0,1,0); // if abs dot product of T and W is close to 1 it means they are close to parallel so chose different T
U = (T x W) // U is perpendicular to T,W
V = (U x W) // V is perpendicular to U,W
create/render arrowhead geometry
That is easy booth A,B are centers of triangle fan (need 2) and the disc base points are computed like this:
P(ang) = B + U.r.cos(ang) + V.r.sin(ang)
So just loop ang through the whole circle with some step so you got enough points (usually 36 is enough) and do both triangle fans from them. Do not forget the last disc point must be the same as the first one otherwise you will got ugly seems or hole on the ang = 0 or 360 deg.
If you still want to go for rotations instead then this is doable like this. compute U,V,W in the same way as above and construct transformation matrix from them. the origin O will be point B and axises X,Y,Z will be U,V,W the order depends on your arrowhead model. W should match the model axis. U,V can be in any order. So just copy all the vectors to their places and use this matrix for rendering. For more info see:
Understanding 4x4 homogenous transform matrices
[Notes]
If you do not know how to compute vector operations like cross/dot products or absolute value see:
// cross product: W = U x V
W.x=(U.y*V.z)-(U.z*V.y)
W.y=(U.z*V.x)-(U.x*V.z)
W.z=(U.x*V.y)-(U.y*V.x)
// dot product: a = (U.V)
a=U.x*V.x+U.y*V.y+U.z*V.z
// abs of vector a = |U|
a=sqrt((U.x*U.x)+(U.y*U.y)+(U.z*U.z))
[Edit1] simple GL implementation
I do not code in your environment but as downvote and comment suggest you guys are not able to put this together on your own which is odd considering you got this far so here simple C++/GL exmaple of how to do this (you can port this to your environment):
void glArrowRoundxy(GLfloat x0,GLfloat y0,GLfloat z0,GLfloat r,GLfloat r0,GLfloat r1,GLfloat a0,GLfloat a1,GLfloat a2)
{
const int _glCircleN=50; // points per circle
const int n=3*_glCircleN;
int i,j,ix,e;
float x,y,z,x1,y1,z1,a,b,da,db=pi2/(_glCircleN-1);
float ux,uy,uz,vx,vy,vz,u,v;
// buffers
GLfloat ptab[6*_glCircleN],*p0,*p1,*n0,*n1,*p;
p0=ptab+(0*_glCircleN); // previous tube segment circle points
p1=ptab+(3*_glCircleN); // actual tube segment circle points
da=+db; if (a0>a1) da=-db; // main angle step direction
ux=0.0; // U is normal to arrow plane
uy=0.0;
uz=1.0;
// arc interpolation a=<a0,a1>
for (e=1,j=0,a=a0;e;j++,a+=da)
{
// end conditions
if ((da>0.0)&&(a>=a1)) { a=a1; e=0; }
if ((da<0.0)&&(a<=a1)) { a=a1; e=0; }
// compute actual tube ceneter
x1=x0+(r*cos(a));
y1=y0+(r*sin(a));
z1=z0;
// V is direction from (x0,y0,z0) to (x1,y1,z1)
vx=x1-x0;
vy=y1-y0;
vz=z1-z0;
// and unit of coarse
b=sqrt((vx*vx)+(vy*vy)+(vz*vz));
if (b>1e-6) b=1.0/b; else b=0.0;
vx*=b;
vy*=b;
vz*=b;
// tube segment
for (ix=0,b=0.0,i=0;i<_glCircleN;i++,b+=db)
{
u=r0*cos(b);
v=r0*sin(b);
p1[ix]=x1+(ux*u)+(vx*v); ix++;
p1[ix]=y1+(uy*u)+(vy*v); ix++;
p1[ix]=z1+(uz*u)+(vz*v); ix++;
}
if (!j)
{
glBegin(GL_TRIANGLE_FAN);
glVertex3f(x1,y1,z1);
for (ix=0;ix<n;ix+=3) glVertex3fv(p1+ix);
glEnd();
}
else{
glBegin(GL_QUAD_STRIP);
for (ix=0;ix<n;ix+=3)
{
glVertex3fv(p0+ix);
glVertex3fv(p1+ix);
}
glEnd();
}
// swap buffers
p=p0; p0=p1; p1=p;
p=n0; n0=n1; n1=p;
}
// arrowhead a=<a1,a2>
for (ix=0,b=0.0,i=0;i<_glCircleN;i++,b+=db)
{
u=r1*cos(b);
v=r1*sin(b);
p1[ix]=x1+(ux*u)+(vx*v); ix++;
p1[ix]=y1+(uy*u)+(vy*v); ix++;
p1[ix]=z1+(uz*u)+(vz*v); ix++;
}
glBegin(GL_TRIANGLE_FAN);
glVertex3f(x1,y1,z1);
for (ix=0;ix<n;ix+=3) glVertex3fv(p1+ix);
glEnd();
x1=x0+(r*cos(a2));
y1=y0+(r*sin(a2));
z1=z0;
glBegin(GL_TRIANGLE_FAN);
glVertex3f(x1,y1,z1);
for (ix=n-3;ix>=0;ix-=3) glVertex3fv(p1+ix);
glEnd();
}
This renders bended arrow in XY plane with center x,y,z and big radius r. The r0 is tube radius and r1 is arrowhead base radius. As I do not have your curve definition I choose circle in XY plane. The a0,a1,a2 are angles where arrow starts (a0), arrowhead starts (a1) and ends (a2). The pi2 is just constant pi2=6.283185307179586476925286766559.
The idea is to remember actual and previous tube segment circle points so there for the ptab,p0,p1 otherwise you would need to compute everything twice.
As I chose XY plane directly then I know that one base vector is normal to it. and second is perpendicular to it and to arrow direction luckily circle properties provides this on its own therefore no need for cross products in this case.
Hope it is clear enough if not comment me.
[Edit2]
I needed to add this to my engine so here is the 3D version (not bound just to axis aligned arrows and the cone is bended too). It is the same except the basis vector computation and I also change the angles a bit in the header <a0,a1> is the whole interval and aa is the arrowhead size but latter in code it is converted to the original convention. I added also normals for lighting computations. I added also linear Arrow where the computation of basis vectors is not taking advantage of circle properties in case you got different curve. Here result:
//---------------------------------------------------------------------------
const int _glCircleN=50; // points per circle
//---------------------------------------------------------------------------
void glCircleArrowxy(GLfloat x0,GLfloat y0,GLfloat z0,GLfloat r,GLfloat r0,GLfloat r1,GLfloat a0,GLfloat a1,GLfloat aa)
{
double pos[3]={ x0, y0, z0};
double nor[3]={0.0,0.0,1.0};
double bin[3]={1.0,0.0,0.0};
glCircleArrow3D(pos,nor,bin,r,r0,r1,a0,a1,aa);
}
//---------------------------------------------------------------------------
void glCircleArrowyz(GLfloat x0,GLfloat y0,GLfloat z0,GLfloat r,GLfloat r0,GLfloat r1,GLfloat a0,GLfloat a1,GLfloat aa)
{
double pos[3]={ x0, y0, z0};
double nor[3]={1.0,0.0,0.0};
double bin[3]={0.0,1.0,0.0};
glCircleArrow3D(pos,nor,bin,r,r0,r1,a0,a1,aa);
}
//---------------------------------------------------------------------------
void glCircleArrowxz(GLfloat x0,GLfloat y0,GLfloat z0,GLfloat r,GLfloat r0,GLfloat r1,GLfloat a0,GLfloat a1,GLfloat aa)
{
double pos[3]={ x0, y0, z0};
double nor[3]={0.0,1.0,0.0};
double bin[3]={0.0,0.0,1.0};
glCircleArrow3D(pos,nor,bin,r,r0,r1,a0,a1,aa);
}
//---------------------------------------------------------------------------
void glCircleArrow3D(double *pos,double *nor,double *bin,double r,double r0,double r1,double a0,double a1,double aa)
{
// const int _glCircleN=20; // points per circle
int e,i,j,N=3*_glCircleN;
double U[3],V[3],u,v;
double a,b,da,db=pi2/double(_glCircleN-1),a2,rr;
double *ptab,*p0,*p1,*n0,*n1,*pp,p[3],q[3],c[3],n[3],tan[3];
// buffers
ptab=new double [12*_glCircleN]; if (ptab==NULL) return;
p0=ptab+(0*_glCircleN);
n0=ptab+(3*_glCircleN);
p1=ptab+(6*_glCircleN);
n1=ptab+(9*_glCircleN);
// prepare angles
a2=a1; da=db; aa=fabs(aa);
if (a0>a1) { da=-da; aa=-aa; }
a1-=aa;
// compute missing basis vectors
vector_copy(U,nor); // U is normal to arrow plane
vector_mul(tan,nor,bin); // tangent is perpendicular to normal and binormal
// arc interpolation a=<a0,a2>
for (e=0,j=0,a=a0;e<5;j++,a+=da)
{
// end conditions
if (e==0) // e=0
{
if ((da>0.0)&&(a>=a1)) { a=a1; e++; }
if ((da<0.0)&&(a<=a1)) { a=a1; e++; }
rr=r0;
}
else{ // e=1,2,3,4
if ((da>0.0)&&(a>=a2)) { a=a2; e++; }
if ((da<0.0)&&(a<=a2)) { a=a2; e++; }
rr=r1*fabs(divide(a-a2,a2-a1));
}
// compute actual tube segment center c[3]
u=r*cos(a);
v=r*sin(a);
vector_mul(p,bin,u);
vector_mul(q,tan,v);
vector_add(c,p, q);
vector_add(c,c,pos);
// V is unit direction from arrow center to tube segment center
vector_sub(V,c,pos);
vector_one(V,V);
// tube segment interpolation
for (b=0.0,i=0;i<N;i+=3,b+=db)
{
u=cos(b);
v=sin(b);
vector_mul(p,U,u); // normal
vector_mul(q,V,v);
vector_add(n1+i,p,q);
vector_mul(p,n1+i,rr); // vertex
vector_add(p1+i,p,c);
}
if (e>1) // recompute normals for cone
{
for (i=3;i<N;i+=3)
{
vector_sub(p,p0+i ,p1+i);
vector_sub(q,p1+i-3,p1+i);
vector_mul(p,p,q);
vector_one(n1+i,p);
}
vector_sub(p,p0 ,p1);
vector_sub(q,p1+N-3,p1);
vector_mul(p,q,p);
vector_one(n1,p);
if (da>0.0) for (i=0;i<N;i+=3) vector_neg(n1+i,n1+i);
if (e== 3) for (i=0;i<N;i+=3) vector_copy(n0+i,n1+i);
}
// render base disc
if (!j)
{
vector_mul(n,U,V);
glBegin(GL_TRIANGLE_FAN);
glNormal3dv(n);
glVertex3dv(c);
if (da<0.0) for (i=N-3;i>=0;i-=3) glVertex3dv(p1+i);
else for (i= 0;i< N;i+=3) glVertex3dv(p1+i);
glEnd();
}
// render tube
else{
glBegin(GL_QUAD_STRIP);
if (da<0.0) for (i=0;i<N;i+=3)
{
glNormal3dv(n1+i); glVertex3dv(p1+i);
glNormal3dv(n0+i); glVertex3dv(p0+i);
}
else for (i=0;i<N;i+=3)
{
glNormal3dv(n0+i); glVertex3dv(p0+i);
glNormal3dv(n1+i); glVertex3dv(p1+i);
}
glEnd();
}
// swap buffers
pp=p0; p0=p1; p1=pp;
pp=n0; n0=n1; n1=pp;
// handle r0 -> r1 edge
if (e==1) a-=da;
if ((e==1)||(e==2)||(e==3)) e++;
}
// release buffers
delete[] ptab;
}
//---------------------------------------------------------------------------
void glLinearArrow3D(double *pos,double *dir,double r0,double r1,double l,double al)
{
// const int _glCircleN=20; // points per circle
int e,i,N=3*_glCircleN;
double U[3],V[3],W[3],u,v;
double a,da=pi2/double(_glCircleN-1),r,t;
double *ptab,*p0,*p1,*n1,*pp,p[3],q[3],c[3],n[3];
// buffers
ptab=new double [9*_glCircleN]; if (ptab==NULL) return;
p0=ptab+(0*_glCircleN);
p1=ptab+(3*_glCircleN);
n1=ptab+(6*_glCircleN);
// compute basis vectors
vector_one(W,dir);
vector_ld(p,1.0,0.0,0.0);
vector_ld(q,0.0,1.0,0.0);
vector_ld(n,0.0,0.0,1.0);
a=fabs(vector_mul(W,p)); pp=p; t=a;
a=fabs(vector_mul(W,q)); if (t>a) { pp=q; t=a; }
a=fabs(vector_mul(W,n)); if (t>a) { pp=n; t=a; }
vector_mul(U,W,pp);
vector_mul(V,U,W);
vector_mul(U,V,W);
for (e=0;e<4;e++)
{
// segment center
if (e==0) { t=0.0; r= r0; }
if (e==1) { t=l-al; r= r0; }
if (e==2) { t=l-al; r= r1; }
if (e==3) { t=l; r=0.0; }
vector_mul(c,W,t);
vector_add(c,c,pos);
// tube segment interpolation
for (a=0.0,i=0;i<N;i+=3,a+=da)
{
u=cos(a);
v=sin(a);
vector_mul(p,U,u); // normal
vector_mul(q,V,v);
vector_add(n1+i,p,q);
vector_mul(p,n1+i,r); // vertex
vector_add(p1+i,p,c);
}
if (e>2) // recompute normals for cone
{
for (i=3;i<N;i+=3)
{
vector_sub(p,p0+i ,p1+i);
vector_sub(q,p1+i-3,p1+i);
vector_mul(p,p,q);
vector_one(n1+i,p);
}
vector_sub(p,p0 ,p1);
vector_sub(q,p1+N-3,p1);
vector_mul(p,q,p);
vector_one(n1,p);
}
// render base disc
if (!e)
{
vector_neg(n,W);
glBegin(GL_TRIANGLE_FAN);
glNormal3dv(n);
glVertex3dv(c);
for (i=0;i<N;i+=3) glVertex3dv(p1+i);
glEnd();
}
// render tube
else{
glBegin(GL_QUAD_STRIP);
for (i=0;i<N;i+=3)
{
glNormal3dv(n1+i);
glVertex3dv(p0+i);
glVertex3dv(p1+i);
}
glEnd();
}
// swap buffers
pp=p0; p0=p1; p1=pp;
}
// release buffers
delete[] ptab;
}
//---------------------------------------------------------------------------
usage:
glColor3f(0.5,0.5,0.5);
glCircleArrowyz(+3.5,0.0,0.0,0.5,0.1,0.2,0.0*deg,+270.0*deg,45.0*deg);
glCircleArrowyz(-3.5,0.0,0.0,0.5,0.1,0.2,0.0*deg,-270.0*deg,45.0*deg);
glCircleArrowxz(0.0,+3.5,0.0,0.5,0.1,0.2,0.0*deg,+270.0*deg,45.0*deg);
glCircleArrowxz(0.0,-3.5,0.0,0.5,0.1,0.2,0.0*deg,-270.0*deg,45.0*deg);
glCircleArrowxy(0.0,0.0,+3.5,0.5,0.1,0.2,0.0*deg,+270.0*deg,45.0*deg);
glCircleArrowxy(0.0,0.0,-3.5,0.5,0.1,0.2,0.0*deg,-270.0*deg,45.0*deg);
glColor3f(0.2,0.2,0.2);
glLinearArrow3D(vector_ld(+2.0,0.0,0.0),vector_ld(+1.0,0.0,0.0),0.1,0.2,2.0,0.5);
glLinearArrow3D(vector_ld(-2.0,0.0,0.0),vector_ld(-1.0,0.0,0.0),0.1,0.2,2.0,0.5);
glLinearArrow3D(vector_ld(0.0,+2.0,0.0),vector_ld(0.0,+1.0,0.0),0.1,0.2,2.0,0.5);
glLinearArrow3D(vector_ld(0.0,-2.0,0.0),vector_ld(0.0,-1.0,0.0),0.1,0.2,2.0,0.5);
glLinearArrow3D(vector_ld(0.0,0.0,+2.0),vector_ld(0.0,0.0,+1.0),0.1,0.2,2.0,0.5);
glLinearArrow3D(vector_ld(0.0,0.0,-2.0),vector_ld(0.0,0.0,-1.0),0.1,0.2,2.0,0.5);
and overview of the arows (on the right side of image):
I am using my vector lib so here are some explanations:
vector_mul(a[3],b[3],c[3]) is cross product a = b x c
vector_mul(a[3],b[3],c) is simple multiplication by scalar a = b.c
a = vector_mul(b[3],c[3]) is dot product a = (b.c)
vector_one(a[3],b[3]) is unit vector a = b/|b|
vector_copy(a[3],b[3]) is just copy a = b
vector_add(a[3],b[3],c[3]) is adding a = b + c
vector_sub(a[3],b[3],c[3]) is substracting a = b - c
vector_neg(a[3],b[3]) is negation a = -b
vector_ld(a[3],x,y,z) is just loading a = (x,y,z)
The pos is the center position of your circle arrow and nor is normal of the plane in which the arrow lies. bin is bi-normal and the angles are starting from this axis. should be perpendicular to nor. r,r0,r1 are the radiuses of the arrow (bend,tube,cone)
The linear arrow is similar the dir is direction of the arrow, l is arrow size and al is arrowhead size.
Related
How would I go about writing a program to rotate at point around a sphere based on an angle as if walking around it?
I am working on a project where I need to (as a 2d point) walk around a 3d sphere. I am having trouble figuring out how to achieve this without polar distortion. Basically I want to have Move Forward, Backward, Left, and Right, as well as Turn Left, and Turn Right. I've been trying to get this working with spherical coordinates, but my functions seem to be incorrect. What can I do to get this working? (I am making this in JavaScript, using the p5.js library) Currently, I'm trying to map an x, and y variable to spherical space's phi, and theta respectively. It doesn't seem to be working, though, and I'm not sure whether, correctly implemented, the point would move in Great Circles. I'm also using an angle variable, to move x, and y by (cos(A), sin(A)), but I'm not sure if this is working either. I think what I need to do is related to Great Circle Navigation, but I don't understand the mathematics behind it. Link to my current version: https://editor.p5js.org/hpestock/sketches/FXtn82-0k Current code looks something like var X,Y,Z; X=0; Y=0; Z=0; var A=0; var scaler = 100; var MOVE_FORWARD = true; var MOVE_BACKWARD= false; var MOVE_LEFT = false; var MOVE_RIGHT = false; var TURN_LEFT = false; var TURN_RIGHT = false; //var i=0; var x = 0; var y = 0; function setup() { createCanvas(400, 400, WEBGL); x= 0; y= 0; A= 0; background(220); } function keyPressed(){ if(key == "w"){ MOVE_FORWARD = true; }else if(key == "ArrowLeft"){ TURN_LEFT = true; }else if(key == "ArrowRight"){ TURN_RIGHT = true; } } function keyReleased(){ if(key == "w"){ MOVE_FORWARD = false; }else if(key == "ArrowLeft"){ TURN_LEFT = false; }else if(key == "ArrowRight"){ TURN_RIGHT = false; } } function draw() { if(MOVE_FORWARD){ x+=0.005*cos(A); y+=0.005*sin(A); } if(TURN_LEFT){ A+=PI/64; } if(TURN_RIGHT){ A-=PI/64; } var xyz = Sph(1,y,x); X=xyz[0]; Y=xyz[1]; Z=xyz[2]; background(220); sphere(scaler); push(); translate(X*scaler,Y*scaler,Z*scaler); sphere(5); pop(); /*i+=PI/32; if(i>2*PI){ i=0; }*/ } function Move(a,d){ // } function Sph(p,t,h){ //p = radius //t (theta) = 2d rotation //h (phi) = 3d roation return ([p*cos(h)*cos(t),p*cos(h)*sin(t),p*sin(h)]); //x=ρsinφcosθ,y=ρsinφsinθ, and z=ρcosφ }
I'm not sure about the math for doing this in polar coordinates but it can be done simply by tracking position and orientation as a pair of 3d cartesian vectors and using Rodrigues' rotation formula to move forward (by rotating the position) and turn (by rotating the axis). // position vector let pos; // axis of rotation let axis; let scaler = 150; function setup() { createCanvas(400, 400, WEBGL); background(220); pos = createVector(1, 0, 0); axis = createVector(0, 1, 0); fill('red'); noStroke(); } function draw() { if (keyIsDown(87) || keyIsDown(UP_ARROW)) { // move "forward" rotateVector(pos, axis, PI / 64); } if (keyIsDown(LEFT_ARROW)) { // turn left rotateVector(axis, pos, -PI / 64); } if (keyIsDown(RIGHT_ARROW)) { // turn right rotateVector(axis, pos, PI / 64); } ambientLight(100); directionalLight(200, 200, 200, 1, 1, -1); push(); translate(pos.x * scaler, pos.y * scaler, pos.z * scaler); sphere(5); pop(); } function rotateVector(v, axis, angle) { // v * cos(θ) + (axis ✕ v) * sin + axis * (axis · v) * (1 - cos) return v.mult(cos(angle)) .add(axis.copy().cross(v).mult(sin(angle))) .add(axis.copy().mult(axis.dot(v) * (1 - cos(angle)))); } <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
I do not know javascript, but you could implement the following functions (which I implemented in python and hopefully you can read off from them the mathematical/geometric logic behind them) that allow you to select a direction of motion on the sphere, to move along a great circle along a step of given angle ds, and to change directions of motion. I am assuming the motion is on the unit sphere (of radius 1). If not, you need to scale the code to the appropriate radius. import numpy as np import math def direct(r, a): ''' given position-vector r on the unit sphere and initial angle a, measured from the meridian, i.e. direction north being a = 0, the result is the unit vector t pointing in that direction ''' e_z = np.array([0,0,1]) u = np.cross(e_z, r) u = u / math.sqrt(u.dot(u)) v = np.cross(r, u) t = math.cos(a) * v + math.sin(a) * u return r, t def move(r, t, ds): ''' given unit position-vector r and unit direction vector t on the unit sphere, make a step of arclength ds radians from point r in the direction of t along the great circle that passes through r and tangent to t. The result is the new position r_ and the new direction vector t_ still tangent to the same great circle. ''' co = math.cos(ds) cs = math.sin(ds) r_ = co * r + si * t t_ = -si * r + cs * t return t_, t_ def redirect(r, t, da): ''' given unit position-vector r and unit direction vector t on the unit sphere, rotate the vector t at an angle da. The result is the new direction vector t_ when da > 0 redirect right when da < 0 redirect left ''' rot_axis = np.cross(r, t) t_ = math.cos(da) * t - math.sin(da) * rot_axis return r, t_
Procedural circle mesh with uniform faces
I'm trying to create a 2d circle procedurally with uniform faces like so. Normally, I would create it with a triangle fan structure, but I need faces to be roughly identical. I looked for examples, but I could only find "cube to sphere" examples. A compromise could be something similar to this : Could you help me finding a way to draw this structure? I'd like to do it in C# but js or even pseudo code would do! Thanks a lot
You got me interested with your question, and I think I've got the solution you were looking for. Here is how we can create a topology that you desired: 1) We start with a hexagon. Why hexagon and not other shape? Because hexagon is the only magic shape with its radius equal too the length of its side. We will call this radius R. We will now try to create a shape that resembles circle and is made of triangles with side length approximately R. 2) Now imagine some concentric circles, with radius R, 2R, 3R and so on - the more, the higher is the resolution. 3) Circle number 1 has radius R. We will now replace that circle with a hexagon with radius R. 4) We will now add more nodes on second circle to expand our hexagon. What is the circumference of circle number N? It is 2PiRN. Now we want to divide it into X edges of length approximately R. Hence X=2PiN, which is approximately 6N. So we will divide first circle into 6 edges (hexagon), second one into 12, then 18, 24 and so on. 5) Now we have lots of circles divided into edges. We now need to connect edges into triangles. How do we build triangles between circle N (outer) and N-1 (inner)? Outer circle has 6 more edges than the inner one. If they had identical number of vertices, we could connect them with quads. But they don't. So, we will still try to build quads, but for each N quads we build, we will need to add 1 triangle. Each quad uses 2 vertices from inner and 2 vertices from outer circle. Each triangle uses 2 vertices from the outer circle and only 1 from inner, thus compensating the excess of vertices. 6) And now at last, there is some tested sample code that does what you need. It will generate a circle with uniform topology, with center point at origin and radius of 1, divided into *resolution sub circles. It could use some minor performance optimization (that's out of scope for now), but all in all it should do the job. using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(MeshFilter))] public class UniformCirclePlane : MonoBehaviour { public int resolution = 4; // Use this for initialization void Start() { GetComponent<MeshFilter>().mesh = GenerateCircle(resolution); } // Update is called once per frame void Update() { } // Get the index of point number 'x' in circle number 'c' static int GetPointIndex(int c, int x) { if (c < 0) return 0; // In case of center point x = x % ((c + 1) * 6); // Make the point index circular // Explanation: index = number of points in previous circles + central point + x // hence: (0+1+2+...+c)*6+x+1 = ((c/2)*(c+1))*6+x+1 = 3*c*(c+1)+x+1 return (3 * c * (c + 1) + x + 1); } public static Mesh GenerateCircle(int res) { float d = 1f / res; var vtc = new List<Vector3>(); vtc.Add(Vector3.zero); // Start with only center point var tris = new List<int>(); // First pass => build vertices for (int circ = 0; circ < res; ++circ) { float angleStep = (Mathf.PI * 2f) / ((circ + 1) * 6); for (int point = 0; point < (circ + 1) * 6; ++point) { vtc.Add(new Vector2( Mathf.Cos(angleStep * point), Mathf.Sin(angleStep * point)) * d * (circ + 1)); } } // Second pass => connect vertices into triangles for (int circ = 0; circ < res; ++circ) { for (int point = 0, other = 0; point < (circ + 1) * 6; ++point) { if (point % (circ + 1) != 0) { // Create 2 triangles tris.Add(GetPointIndex(circ - 1, other + 1)); tris.Add(GetPointIndex(circ - 1, other)); tris.Add(GetPointIndex(circ, point)); tris.Add(GetPointIndex(circ, point)); tris.Add(GetPointIndex(circ, point + 1)); tris.Add(GetPointIndex(circ - 1, other + 1)); ++other; } else { // Create 1 inverse triange tris.Add(GetPointIndex(circ, point)); tris.Add(GetPointIndex(circ, point + 1)); tris.Add(GetPointIndex(circ - 1, other)); // Do not move to the next point in the smaller circle } } } // Create the mesh var m = new Mesh(); m.SetVertices(vtc); m.SetTriangles(tris, 0); m.RecalculateNormals(); m.UploadMeshData(true); return m; } } Final Result:
How to check if a mouse is in many (120) different regions in HTML5 canvas efficiently?
I have a polar graph (see image) with 120 different points. I want to make it so if the user clicks or hovers on one of the points, the coordinate of that point is displayed. I have an array called pointCoordinates that stores each canvas coordinate of each points like this: [[x1, y1], [x2, y2] ... [x120, y120]] This is how I am capturing mouse coordinates (which I might later change to click): document.onmousemove = function(e) { var x = e.clientX; var y = e.clientY; } I was originally planning to use a formula to check if the mouse is in a certain region (using the distance formula) or simplifying it all into a circle. Either way, this will require me to have 120 different if statements to check for this. I feel like this is inefficient and probably slow. Are there other methods for doing this? Edit: To provide more information, these points will NOT be draggable. I am planning to display something like a tooltip near the point that was clicked where the polar coordinates of the point will be shown. Edit 2: After using the code posted below and drawing a rectangle in the "clickable" spot on the map, I get this image. I do not want the click detection to be perfect, but this is pretty far off after pi/3. Any ideas how to fix this? I used this code to generate the black spots: for(var x = 0; x < WIDTH*2/3; x++){ for(var y = 0; y < HEIGHT; y++){ var mp = realToPolar(x, y);//converts canvas x and y into polar if(checkRadialDistance(mp[0], mp[1])){ //returns true if in bounds ctx.fillRect(x, y, 1, 1); } } } Playing around with the constants still generates the same pattern, just of different thicknesses. checkRadialDistance is just the renamed checkr function that inside calls checkrt. JSBIN Keep in mind, width of screen has to be greater than height for this to work properly. The image generated by mt-rt. I later made a minor edit, so that whole circle is covered when theta = 0.
EDIT: My (accepted) answer was bad. This corrects it: This assumes r to be 1 to 5. Convert mouse cartesian mx,my to polar mr,mt. First check if mr is close to 1 of the 5 radii. Function checkr does that. If it is close, then check if mt is close to 1 of the 24 thetas. Function checkt does that. A complication is that the atan2 function is not continuous at pi radians which is where points are at, so make the discontinuity at -pi/24 radians where there are no points. A "close" value is pi/24 since the arc distance between two adjacent points at r=1 will be pi/12. var del = 1*Math.PI/24*.7; // for example function xy2rt(xy) { // to polar cordinates var rt = []; rt.push(Math.sqrt(xy[0]*xy[0]+xy[1]*xy[1])); // r var zatan = Math.atan2(xy[1], xy[0]); // make the discontinuity at -pi/24 if (zatan < -Math.PI/24) zatan += 2*Math.PI; rt.push(zatan); // theta return rt; } function checkr() { // check radial distance for (var pr=1; pr<=5; pr+=1) { // 5 radii if (Math.abs(mr-pr) < del) { checkt(pr); break; } } } function checkt(pr) { // check theta var pt; for (var ipt=0; ipt<24; ipt+=1) { // 24 thetas pt = ipt / 24 * 2 * Math.PI; if (Math.abs(mt-pt) < del/pr) { // is close -- do whatever break; } } } My problem was when checking the arc distance, I was using mr and pr whereas only pr should be used. The OP found my error by processing every pixel on the canvas and found there was a problem. I also processed every pixel and this image shows the routines to be correct now. The black is where the routines determine that the pixel is close to one of the 120 points. EDIT: Faster processing There are a lot of Math.* functions being executed. Although I haven't timed anything, I think this has to be much faster. 1) The x,y coordintates of the 120 points are stored in arrays. 2) Instead of getting polar mr, mt, pr, and pt, use vector processing. Here is the derivation of arcd, the arc distance using vectors. sint = sin(theta) = (M cross P)/mr/pr (cross product Mouse X Point) cost = cos(theta) = (M dot P)/mr/pr (dot product Mouse . Point) sint will be used to get arc distance, but sint goes to zero at theta=+-pi as well as theta=0, so: mdotp will be used to determine if theta is near zero and not +-pi arcd = pr*theta arcd = pr*sin(theta) (good approximation for small theta) arcd = pr*abs(M cross P)/mr/mp (from above) if ardd < del, check if mdotp > 0. Here are the load-xy-arrays and the new checkr and checkt routines. apx=[], apy=[]; // the saved x,y of the 120 points function loadapxapy() { // load arrays of px, py var itheta, theta for (var pr=1; pr<=5; pr+=1) { // 2-dimension arrays apx[pr] = []; apy[pr] = []; // 5 arrays, 1 for each pr for (itheta=0; itheta<24; itheta+=1) { // 24 x's and y's theta = Math.PI*itheta/12; apx[pr][itheta] = pr*Math.cos(theta); apy[pr][itheta] = pr*Math.sin(theta); } } } function checkr() { // check radial distance var mr = Math.sqrt(mx*mx+my*my); // mouse r for (var pr=1; pr<=5; pr+=1) { // check 1 to 5 if (Math.abs(mr-pr) < del) { // mouser - pointr checkt(mr, pr); // if close, check thetas } } } function checkt(mr, pr) { // check thetas var px, py, sint, mdotp, arcd; for (var itheta=0; itheta<24; itheta+=1) { // check 24 px = apx[pr][itheta]; // get saved x py = apy[pr][itheta]; // and y // This arcd is derived from vector processing // At least this doesn't use the accursed "atan"! sint = Math.abs(mx*py-my*px)/mr/pr; // sine arcd = pr*sint; // arc distance if (arcd<del) { // arc distance check mdotp = (mx*px+my*py); // final check if (mdotp > 0) { // to see if theta is near zero and not +-pi setpixelxy([mx, my]); // or whatever.. } } } }
Javascript, Math: calculate the area of a flat, 2D surface that is situated in 3D
I want to be able to calculate the surface area of a 2D polygon of any shape, given a set of 3D vertices. For example, what is the surface area of this figure? var polygon = new Polygon([new Point(0,0,0), new Point(5,8,2), new Point(11,15,7)]) polygon.areaIfPolygonIs3D() --> some predictable result, no matter how many vertices the polygon has... Keep in mind that polygons only have one surface. They are flat but could be triangle shaped or trapezoid shaped or randomly shaped, and could be floating at a 3D angle... imagine them as pieces of paper turned any which way in 3D space. What I've tried to do so far is rotate the thing flat, and then use a basic formula for calculating the area of a 2D irregular polygon which is currently working in my code (formula: http://www.wikihow.com/Calculate-the-Area-of-a-Polygon). I had such a hard figuring out how to rotate all the vertices so the polygon lays flat (all "z" values are 0) that I abandoned that path, though I'm open to trying it if someone can get there. (Perhaps there is a bug in Point.rotateBy().) I can work with Points, and Edges (created with point.to(point)), and Edges have 'theta' (edge.theta()) and 'phi' (edge.phi()). In any case, if someone can fill in what goes here and help me after a full days effort of trying to relearn all the geometry I forgot from high school, that would be much appreciated! var locatorRho = function(x,y,z) { return Math.sqrt(x*x + y*y + z*z); } var locatorTheta = function(x,y) { return Math.atan2(y,x); }; var locatorPhi = function(x,y,z) { return z == 0 ? Math.PI_2 : Math.acos(z/locatorRho(x, y, z)); } // rotates a point according to another point ('locator'), and their 2D angle ('theta') and 3D angle ('phi') Point.prototype.rotateBy = function(locator, theta, phi) { phi = (phi == undefined ? 0 : phi); var relativeX = this.x() - locator.x(); var relativeY = this.y() - locator.y(); var relativeZ = this.z() - locator.z(); var distance = locatorRho(relativeX, relativeY, relativeZ); var newTheta = locatorTheta(relativeX, relativeY) + theta; var newPhi = locatorPhi(relativeX, relativeY, relativeZ) + phi; this._x = locatorX(distance, newTheta, newPhi) + locator.x(); this._y = locatorY(distance, newTheta, newPhi) + locator.y(); this._z = locatorZ(distance, newPhi) + locator.z(); } Polygon.prototype.signedArea = function() { var vertices = this.vertices(); var area = 0; for(var i=0, j=1, length=vertices.length; i<length; ++i, j=(i+1)%length) { area += vertices[i].x()*vertices[j].y() - vertices[j].x()*vertices[i].y(); } return 0.5*area } Polygon.prototype.areaIfPolygonIs2D = function() { return Math.abs(rotatedFlatCopy.signedArea()) } Polygon.prototype.areaIfPolygonIs3D = function() { ... help here I am so stuck ... } var vertices = [some number of Points, e.g., new Point(x,y,z)] var polygon = new Polygon(vertices) var polygon.areaIfPolygonIs3D() --> result
If your polygon plane is not parallel to Z axis, you can calculate area projection with known approach using X and Y coordinates only, then divide result by cosine of angle between Z axis and normal N to that plane Area = Sum[x1*y2-x2*y1 +...] ////shoelace formula True_Area = Area / Cos(Angle between N and Z axis)) = Area / DotProduct((N.x,N.y,N.z), (0,0,1)) = Area / N.z //// if N is normalized (unit)
Use the shoelace formula three times, on the 2D vertices (X, Y), (Y, Z) and (Z, X). The desired area is given by √Axy²+Ayz²+Azx² (provided the polygon is flat).
Error on drawing Fibonacci in web
Currently I have this fiddle from Blindman67 which draws Golden spiral figure 1(see image below). function renderSpiral(pointA, pointB, turns){ var dx, dy, rad, i, ang, cx, cy, dist, a, c, angleStep, numberTurns, nTFPB, scale, styles; // clear the canvas ctx.clearRect(0, 0, ctx.canvas.width,ctx.canvas.height) // spiral stuff a = 1; // the larger this number the larger the spiral c = 1.358456; // constant See https://en.wikipedia.org/wiki/Golden_spiral angleStep = Math.PI/20; // set the angular resultion for drawing numberTurns = 6; // total half turns drawn nTFPB = 2; // numberOfTurnsForPointB is the number of turns to point // B should be integer and describes the number off // turns made befor reaching point B // get the ang from pointA to B ang = Math.atan2(pointB.y-pointA.y,pointB.x-pointA.x); // get the distance from A to B dist = Math.sqrt(Math.pow(pointB.y-pointA.y,2)+Math.pow(pointB.x-pointA.x,2)); if(dist === 0){ return; // this makes no sense so exit as nothing to draw } // get the spiral radius at point B rad = Math.pow(c,ang + nTFPB * 2 * Math.PI); // spiral radius at point2 // now just need to get the correct scale so the spiral fist to the // constraints requiered. scale = dist / rad; // ajust the number of turns so that the spiral fills the canvas while(Math.pow(c,Math.PI*numberTurns)*scale < ctx.canvas.width){ numberTurns += 2; } // set the scale, and origin to centre ctx.setTransform(scale, 0, 0, scale, pointA.x, pointA.y) // make it look nice create some line styles // first just draw the line A-B ctx.strokeStyle = "black"; ctx.lineWidth = 2 * ( 1 / scale); // because it is scaled invert the scale // can calculate the width requiered // ready to draw ctx.beginPath(); ctx.moveTo(0, 0) // start at center ctx.lineTo((pointB.x-pointA.x)*(1/scale),(pointB.y-pointA.y)*(1/scale) ); // add line ctx.stroke(); // draw it all // Now draw the sporal. draw it for each style styles.forEach( function(style) { ctx.strokeStyle = style.colour; ctx.lineWidth = style.width * ( 1 / scale); // because it is scaled invert the scale // can calculate the width requiered // ready to draw ctx.beginPath(); for( i = 0; i <= Math.PI *numberTurns; i+= angleStep){ dx = Math.cos(i); // get the vector for angle i dy = Math.sin(i); var rad = Math.pow(c, i); // calculate the radius if(i === 0) { ctx.moveTo(0, 0) // start at center }else{ ctx.lineTo(dx * rad, dy * rad ); // add line } } ctx.stroke(); // draw it all }); ctx.setTransform(1,0,0,1,0,0); // reset tranfrom to default; } What I want to obtain is figure 2 (see image below). Q1. How can I change mine spiral so line AB will fit between first and second screw while A is the start of spiral? You can also refer to my earlier question for better understanding of my problem.
To achieve the properties you need you need to adjust your spiral like following: choose the right angular position of the line AB I choose 1.5*M_PI [rad] for A and 3.5*M_PI [rad] for B (on unrotated spiral) rotate your spiral by angle of your AB line that is easy just add the angle to the final polar -> cartesian coordinates conversion and that will rotate entire spiral so computed angular positions of A,B on spiral will match the real points AB direction rescale your spiral to match the AB size So compute the radiuses for angular points A,B positons on spiral and then compute the scale=|AB|-(r(b)-r(a)). Now just multiply this to compute radius of each rendered point ... I played a bit with the golden ratio and spiral a bit and here is the result Yellow spiral is approximation by quarter circle arcs Aqua is the Golden spiral As you can see they do not match so much (this is with ratio*0.75 to make them more similar but it should be just ratio) Either I have a bug somewhere, or the origin of spiral is shifted (but does not look like it) or I have wrong ratio constant ratio = 0.3063489 or the Golden rectangles are introducing higher floating round errors then I taught or I am missing something stupid. Here the C++ source code so you can extract what you need: //--------------------------------------------------------------------------- #include <Math.h> //--------------------------------------------------------------------------- bool _redraw=false; // just signal to repaint window after spiral change double Ax,Ay,Bx,By; // mouse eddited points double gr=0.75; // golden spiral ratio scale should be 1 !!! void GoldenSpiral_draw(TCanvas *can) // GDI draw { double a0,a,b,l,x,y,r=5,ratio; // draw AB line can->Pen->Color=clWhite; can->MoveTo(Ax,Ay); can->LineTo(Bx,By); // draw A,B points can->Pen->Color=clBlue; can->Brush->Color=clAqua; can->Ellipse(Ax-r,Ay-r,Ax+r,Ay+r); can->Ellipse(Bx-r,By-r,Bx+r,By+r); // draw golden ratio rectangles can->Pen->Color=clDkGray; can->Brush->Style=bsClear; ratio=1.6180339887; a=5.0; b=a/ratio; x=Ax; y=Ay; y-=0.5*b; x-=0.5*b; // bias to match real golden spiral can->Rectangle(x,y,x+a,y+b); y-=a; for (int i=0;i<5;i++) { can->Rectangle(x,y,x+a,y+a); b=a; a*=ratio; x-=a; can->Rectangle(x,y,x+a,y+a); y+=a; b=a; a*=ratio; can->Rectangle(x,y,x+a,y+a); x+=a; y-=b; b=a; a*=ratio; can->Rectangle(x,y,x+a,y+a); x-=b; b=a; a*=ratio; y-=a; } // draw circle arc approximation of golden spiral ratio=1.6180339887; a=5.0; b=a/ratio; x=Ax; y=Ay; r=10000; y-=a; y-=0.5*b; x-=0.5*b; // bias to match real golden spiral can->Pen->Color=clYellow; for (int i=0;i<5;i++) { can->Arc(x-a,y,x+a,y+a+a,+r, 0, 0,-r); b=a; a*=ratio; x-=a; can->Arc(x,y,x+a+a,y+a+a, 0,-r,-r, 0); y+=a; b=a; a*=ratio; can->Arc(x,y-a,x+a+a,y+a,-r, 0, 0,+r); x+=a; y-=b; b=a; a*=ratio; can->Arc(x-a,y-a,x+a,y+a, 0,+r,+r, 0); x-=b; b=a; a*=ratio; y-=a; } can->Brush->Style=bsSolid; // compute golden spiral parameters ratio=0.3063489*gr; x=Bx-Ax; y=By-Ay; l=sqrt(x*x+y*y); // l=|AB| if (l<1.0) return; // prevent domain errors a0=atan2(-y,x); // a=atan2(AB) a0+=0.5*M_PI; // offset so direction of AB matches the normal a=1.5*M_PI; r=a*exp(ratio*a); b=r; a+=2.0*M_PI; r=a*exp(ratio*a); b=r-b; b=l/r; // b=zoom of spiral to match AB screw distance // draw golden spiral can->Pen->Color=clAqua; can->MoveTo(Ax,Ay); for (a=0.0;a<100.0*M_PI;a+=0.001) { r=a*b*exp(ratio*a); if (r>512.0) break; x=Ax+r*cos(a0+a); y=Ay-r*sin(a0+a); can->LineTo(x,y); } } //--------------------------------------------------------------------------- You can ignore the golden ratio rectangles and circular arcs ... change the drawings based on can-> to your gfx API. It is just GDI Canvas Hard to say if your spiral is correct ... you can check with the golden ratio rectangles (as I did). If you got correct spiral then just apply the bullets #1,#2,#3 to it and you should be fine.
Here's a fiddle which I believe gives you the output your looking for. https://jsfiddle.net/8a7fdg3d/4/ The main problem was starting the spiral from 0 results in the initial straight line. Starting the spiral from 1 removes this part of the graph and then you just had to adjust the starting point of your black |AB| line. This was done by adjusting for( i = 0; i <= Math.PI *numberTurns; i+= angleStep) to for( i = 1; i <= Math.PI *numberTurns; i+= angleStep) to change the starting point of the spiral, then changing // ready to draw ctx.beginPath(); ctx.moveTo(0, 0) // start at center to // ready to draw ctx.beginPath(); dx = Math.cos(1); // get the vector for angle i dy = Math.sin(1); var rad = Math.pow(c, 1); // calculate the radius ctx.moveTo(dx * rad, dy * rad ) // start at center to make your |AB| line match up.