Apply Rotation to Cylinder based on Tube Ending No

2020-02-07 03:14发布

问题:

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!

回答1:

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:

  1. obtain AB endpoints

  2. 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
    
  3. 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.