将平面网格变形为球体

Deforming plane mesh to sphere

美好的一天,

目前我正在尝试将平面弯曲成球体。 我已经准备好尝试 Mercator projection together with lla to ecef。所以结果延迟但它不像球体(半球体)。 最成功的变体看起来像这样(更像一个帐篷,而不是半个球体):

Code for this tent (pastebin)。我正在使用 three.js 进行渲染。

所以我想寻求一些建议。我做错了什么?

使用球坐标系统。角度 long,lat 是平面中的 2D 线性 u,v 坐标,输出是 3D x,y,z

  1. 将平面网格的顶点(点)转换为球面

    我怀疑你得到了 (x,y,z) 形式的分数,所以你需要先计算 u,v。设 U,V 是位于平面上的垂直单位基向量。它们可以通过在平面网格上减去 2 个点、归一化尺寸并利用叉积来确保垂直性来获得。所以:

    u = `dot_product((x,y,z),U)` = x*U.x + y*U.y + z*U.z
    v = `dot_product((x,y,z),V)` = x*V.x + y*V.y + z*V.z
    

    现在转换为球面角度:

    long=6.2831853*u/u_size_of_mesh
    lat =3.1415926*v/v_size_of_mesh
    

    最后在球面上计算新的(x,y,z)

    x = R*cos(lat)*cos(long)
    y = R*cos(lat)*sin(long)
    z = R*sin(lat)
    
  2. 网格

    平面网格必须具有足够密集的点结构(足够 triangles/faces),否则球体看起来不会像它应该的那样。另一个问题是平面网格确实有边而球面没有。 Ti s 可能会在平面边缘连接的球体表面上创建 seems/gaps。如果你想避免这种情况,你可以在平面网格相对两侧的边之间添加面来填充间隙,或者完全丢弃你的网格并使用均匀的点网格重新采样平面。

    如果你想完全重新采样你的网格,那么你能做的最好的事情就是首先创建规则的球体网格,例如:

    Sphere triangulation by mesh subdivision

    然后通过逆过程计算平面上的对应点到#1所以你可以插值点的其他参数(如颜色,纹理坐标等)

[备注]

如果你想要动画,那么只需在原始平面点 P0(x,y,z) 和相应的球面点 P1(x,y,z) 之间使用线性插值,动画参数 t=<0.0,1.0> 如下所示:

P = P0 + (P1-P0)*t

if t=0 则输出为平面网格 else if t=1 则输出为球体。介于两者之间的任何地方都是包装过程,因此以足够小的步长(如 0.01)将 t 增加到 1 并在某个计时器中渲染 ...

[Edit1] U,V 基向量

想法很简单,获得 2 个不平行的向量并改变其中一个,使其垂直于第一个但仍在同一平面上。

  1. 取任意网格面

    例如三角形ABC

  2. 计算平面上的2个非零非平行向量

    这很简单,只需减去任意 2 对顶点,例如:

    U.x=B.x-A.x
    U.y=B.y-A.y
    V.x=C.x-A.x
    V.y=C.y-A.y
    

    并使它们成为大小单位,然后除以它们的大小

    ul=sqrt((U.x*U.x)+(U.y*U.y))
    vl=sqrt((V.x*V.x)+(V.y*V.y))
    U.x/=ul
    U.y/=ul
    V.x/=vl
    V.y/=vl
    
  3. 使它们垂直

    所以保留一个向量(例如 U)并计算另一个向量,使其垂直。为此,您可以使用叉积。两个单位向量的叉积是垂直于两者的新单位向量。两种可能性中的哪一种仅取决于操作数的顺序 ((U x V) = - (V x U)) 例如:

    // W is perpendicular to U,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)
    // V is perpendicular to U,W
    V.x=(U.y*W.z)-(U.z*W.y)
    V.y=(U.z*W.x)-(U.x*W.z)
    V.z=(U.x*W.y)-(U.y*W.x)
    

    W 只是一个临时矢量(在图像中称为 V')顺便说一句,它是表面的法线矢量。

  4. 大小和对齐方式

    现在我没有关于你的网格的更多信息,我不知道它的形状、大小等...理想情况是网格是矩形并且 U,V 矢量与其边缘对齐.在这种情况下,您只需按每个方向的矩形大小对坐标进行归一化(如左上图所示)。

    如果您的网格不是这样的,并且您正在通过这种方法从面计算 U,V,那么结果可能根本不会与边缘对齐(它们可以旋转任何角度)...

    如果这种情况无法避免(通过选择角面),那么你的形状的角点将沿着每条边有不同的坐标限制你需要在某种意义上将它们插值或映射到正确的球面间隔方式(不能更具体,因为我不知道你到底在做什么)。

    对于几乎是矩形的形状,有时可以使用 U,V 的边,即使它们不是完全垂直的。

[Edit2] C++ 示例

好吧,如果你得到完美对齐的方形网格,在 Z 轴上有一些噪声(比如高度图),那么我将如何进行网格转换:

//---------------------------------------------------------------------------
struct _pnt // points
    {
    double xyz[3];
    _pnt(){}; _pnt(_pnt& a){ *this=a; }; ~_pnt(){}; _pnt* operator = (const _pnt *a) { *this=*a; return this; }; /*_pnt* operator = (const _pnt &a) { ...copy... return this; };*/
    };
struct _fac // faces (triangles)
    {
    int i0,i1,i2;
    double nor[3];
    _fac(){}; _fac(_fac& a){ *this=a; }; ~_fac(){}; _fac* operator = (const _fac *a) { *this=*a; return this; }; /*_fac* operator = (const _fac &a) { ...copy... return this; };*/
    };
// dynamic mesh
List<_pnt> pnt;
List<_fac> fac;
//---------------------------------------------------------------------------
void mesh_normals() // compute normals
    {
    int i;
    _fac *f;
    double a[3],b[3];
    for (f=&fac[0],i=0;i<fac.num;i++,f++)
        {
        vector_sub(a,pnt[f->i1].xyz,pnt[f->i0].xyz);    // a = pnt1 - pnt0
        vector_sub(b,pnt[f->i2].xyz,pnt[f->i0].xyz);    // b = pnt2 - pnt0
        vector_mul(a,a,b);                              // a = a x b
        vector_one(f->nor,a);                           // nor = a / |a|
        }
    }
//---------------------------------------------------------------------------
void mesh_init()    // generate plane mesh (your square with some z noise)
    {
    int u,v,n=40;   // 40x40 points
    double d=2.0/double(n-1);
    _pnt p;
    _fac f;
    Randomize();
    RandSeed=13;
    // create point list
    pnt.allocate(n*n); pnt.num=0;               // preallocate list size to avoid realocation
    for (p.xyz[0]=-1.0,u=0;u<n;u++,p.xyz[0]+=d) // x=<-1.0,+1.0>
     for (p.xyz[1]=-1.0,v=0;v<n;v++,p.xyz[1]+=d)// y=<-1.0,+1.0>
        {
        p.xyz[2]=0.0+(0.05*Random());           // z = <0.0,0.05> noise
        pnt.add(p);
        }
    // create face list
    vector_ld(f.nor,0.0,0.0,1.0);
    for (u=1;u<n;u++)
     for (v=1;v<n;v++)
        {
        f.i0=(v-1)+((u-1)*n);
        f.i1=(v-1)+((u  )*n);
        f.i2=(v  )+((u-1)*n);
        fac.add(f);
        f.i0=(v  )+((u-1)*n);
        f.i1=(v-1)+((u  )*n);
        f.i2=(v  )+((u  )*n);
        fac.add(f);
        }
    mesh_normals();
    }
//---------------------------------------------------------------------------
void mesh_sphere()  // convert to sphere
    {
    int i;
    _pnt *p;
    double u,v,lon,lat,r,R=1.0;
    // I know my generated mesh is aligned so:
    double U[3]={ 1.0,0.0,0.0 };
    double V[3]={ 0.0,1.0,0.0 };
    for (p=&pnt[0],i=0;i<pnt.num;i++,p++)   // process all points
        {
        // get the u,v coordinates
        u=vector_mul(p->xyz,U);
        v=vector_mul(p->xyz,V);
        // I also know the limits are <-1,+1> so conversion to spherical angles:
        lon=M_PI*(u+1.0);               // <-1.0,+1.0> -> <0.0,6.28>
        lat=M_PI*v*0.5;                 // <-1.0,+1.0> -> <-1.57,+1.57>
        // compute spherical position (superponate z to r preserve noise)
        r=R+p->xyz[2];
        p->xyz[0]=r*cos(lat)*cos(lon);
        p->xyz[1]=r*cos(lat)*sin(lon);
        p->xyz[2]=r*sin(lat);
        }
    mesh_normals();
    }
//---------------------------------------------------------------------------
void mesh_draw()    // render
    {
    int i;
    _fac *f;
    glColor3f(0.2,0.2,0.2);
    glBegin(GL_TRIANGLES);
    for (f=&fac[0],i=0;i<fac.num;i++,f++)
        {
        glNormal3dv(f->nor);
        glVertex3dv(pnt[f->i0].xyz);
        glVertex3dv(pnt[f->i1].xyz);
        glVertex3dv(pnt[f->i2].xyz);
        }
    glEnd();
    }
//---------------------------------------------------------------------------

我使用了我的动态列表模板:

  • List<double> xxx; 等同于 double xxx[];
  • xxx.add(5);5 添加到列表末尾
  • xxx[7] 访问数组元素(安全)
  • xxx.dat[7]访问数组元素(不安全但直接访问速度快)
  • xxx.num是数组实际使用的大小
  • xxx.reset()清空数组并设置xxx.num=0
  • xxx.allocate(100)100 项预分配 space

这个的用法是这样的:

mesh_init();
mesh_sphere();

这里是结果:

左边是生成的带有噪声的平面网格,右边是转换后的结果。

代码反映了上面的所有内容 + 添加 Z - 噪声到球体半径以保留特征。法线以标准方式从几何体重新计算。对于整个 TBN 矩阵,您需要拓扑中的连接信息并从中重新计算(或利用球体几何形状并从中使用 TBN。

顺便说一句,如果你想映射到球体而不是网格转换,你应该看看相关的 QAs:

  • Normal mapping gone horribly wrong
  • Applying map of the earth texture a Sphere
  • make seamless height-map texture for sphere (planet)
  • Convert satellite photos of Earth into texture maps on a sphere (OpenGL ES)