在 OpenGL 中生成 Superellipsoid 的表面
Generating the surface of Superellipsoid in OpenGL
我正在尝试在 OpenGL 中生成 Superellipsoid 的表面。
由于该表面可以使用参数方程表示:
我在这个表面上生成一定数量的点,改变参数 u v 并使用看起来像这样的函数将它们放入数组中:
float du = 1.0/(float)(u_max-1);
float dv = 1.0/(float)(v_max-1);
int u, v;
vertices.reserve(u_max*v_max);
normals.reserve(u_max*v_max);
texcoords.reserve(u_max*v_max);
for(u = 0; u < u_max; u++)
for(v = 0; v < v_max; v++)
{
texcoords.emplace(texcoords.end(),vec2(u*du,v*dv));
float cos_v_dv=cos(2*M_PI*v*dv);
float sin_v_dv=sin(2*M_PI*v*dv);
float sin_u_du=sin(M_PI*u*du);
float cos_u_du=cos(M_PI*u*du);
// Parametric equation of the surface
float x=A*sgn(cos_v_dv)*sgn(sin_u_du)*pow(std::abs(cos_v_dv),n)*pow(std::abs(sin_u_du),m);
float y=B*sgn(sin_v_dv)*sgn(sin_u_du)*pow(std::abs(sin_v_dv),n)*pow(std::abs(sin_u_du),m);
float z=C*sgn(cos_u_du)*pow(std::abs(cos_u_du),m);
vertices.emplace(vertices.end(),x,y,z);
// Derivative with respect to u
float dx_du=A*sgn(cos_v_dv)*cos_u_du*pow(std::abs(cos_v_dv),n)*pow(std::abs(sin_u_du),m-1);
float dy_du=B*sgn(sin_v_dv)*cos_u_du*pow(std::abs(sin_v_dv),n)*pow(std::abs(sin_u_du),m-1);
float dz_du=-C*sin_u_du*pow(std::abs(cos_u_du),m-1);
// Derivative with respect to v
float dx_dv=-A*sgn(sin_u_du)*pow(std::abs(sin_u_du),m)*sin_v_dv*pow(std::abs(cos_v_dv),n-1);
float dy_dv=B*sgn(sin_u_du)*pow(std::abs(sin_u_du),m)*cos_v_dv*pow(std::abs(sin_v_dv),n-1);
// derivative of z with respect to v is 0
//Crossing the tangent vectors to get the normal
vec3 normal(-dz_du*dy_dv,dx_dv*dz_du,dx_du*dy_dv-dx_dv*dy_du);
normal.normalize();
normals.push_back(normal);
}
然后我为连接相邻点的四边形生成索引,如:
indices.reserve(u_max * v_max * 4);
for(u = 0; u < u_max-1; u++)
for(v = 0; v < v_max-1; v++)
{
indicies.push_back(u* v_max + v);
indicies.push_back(u* v_max + (v+1));
indicies.push_back((u+1)* v_max + (v+1));
indicies.push_back((u+1)* v_max + v);
}
但是结果很糟糕......:
大多数时候,某些地方的镶嵌效果很差,而其他地方则太多了。
还有一些看起来很奇怪的黑点可能是由于 normals.At 不好引起的首先我认为我生成法线的方法是 flawed.I 用于通过交叉我得到的两个切线向量来计算法线通过区分顶点,如上面的代码所示,例如:
然后我决定通过相交切线来计算法线,切线是通过减去两个相邻顶点的坐标得到的。
结果完全一样,黑点仍然存在,从某种意义上说,闪电看起来不太好。
显然有一个很好的方法可以做到这一点,因为 wiki 文章显示了这个表面的非常好的图像。我可能正在做一些非常愚蠢的事情。
所以我想我的问题是:有没有好的方法来生成这个对象的表面?我怎样才能避免我现在遇到的问题?
我认为您的问题很可能是由于您的采样和参数化曲面本身的奇异性造成的。
您在参数-space(您的 u 和 v')中定期对表面进行采样,在几何 space(您显示的图像)中给出非均匀采样。
两极的奇点可能会产生您提到的阴影伪影。在参数 space 和几何 space 中,您对法线的计算变得病态,导致法线出现 "extreme" 错误。
维基百科上的图像可能是使用光线追踪和表面的完全隐式形式创建的。您可以尝试做同样的事情以获得更好的表面法线:不是在参数值 (u,v) 处微分显式参数方程,而是在位置 (x, y, z) 处微分隐式方程。
另一方面,您也可以简单地尝试明确给定的法线
http://www.gamedev.net/page/resources/_/technical/opengl/superquadric-ellipsoids-and-toroids-opengl-lig-r1172
你在画球体时会遇到类似的问题,球体的任何参数化都会至少有一个地方是奇异的,通过纬度和经度参数化在北极分解。
解决这个问题的一种方法是将曲面分成两个或多个沿曲线相交的小块。有几种方法可以做到这一点,你可以有两个半球,沿着赤道相遇。一个很好的方法是取一个单位立方体并将其沿半径向量投影到单位球体。一旦我们有了球体的一个点,我们就可以获得极坐标并使用它们来找到超椭球体上的点。
如果u,v是曲面的两个参数,0<=u<=1,0<=v<=1 立方体的一个面将由x=2u-1,y=2v给出-1,z=1。我们可以通过除以长度l=sqrt(x^2+y^2+z^2), x1=x/l, y1=y/l, z1=[=来找到单位球面上的点29=]。现在找到极坐标 th=atan2(y1, x1), phi=asin(z1) 并使用它们找到超椭圆上的坐标。
我在 JavaScript 中将其实现为 fiddle。 http://jsfiddle.net/SalixAlba/n1hjm35n/ 使用 OpenGL 实现应该相当简单。
// Auxiliary cos function
function auxC(w,m) {
var c = Math.cos(w);
return sign(c) * Math.pow(Math.abs(c),m);
}
// Auxiliary sin function
function auxS(w,m) {
var s = Math.sin(w);
return sign(s) * Math.pow(Math.abs(s),m);
}
// Given a point on a sphere find the corresponding point on the super-ellipsoid
function SEvec(x,y,z) {
var th = Math.atan2(y,x);
var phi = Math.asin(z);
var xx = A * auxC(phi, 2/ t) * auxC(th, 2/r);
var yy = B * auxC(phi, 2/ t) * auxS(th, 2/r);
var zz = C * auxS(phi, 2/ t);
console.log(x,y,z,xx,yy,zz);
return new THREE.Vector3(xx*100,yy*100,zz*100);
}
// Generate points for the first face
function sf1(u,v) {
var x = 2*u-1;
var y= 2*v-1;
var z= 1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
// second face
function sf2(u,v) {
var x = 2*u-1;
var y= 2*v-1;
var z= -1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf3(u,v) {
var x = 2*u-1;
var z= 2*v-1;
var y= 1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf4(u,v) {
var x = 2*u-1;
var z= 2*v-1;
var y= -1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf5(u,v) {
var z = 2*u-1;
var y= 2*v-1;
var x= 1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf6(u,v) {
var z = 2*u-1;
var y= 2*v-1;
var x= -1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
我没有计算法线,但您的代码应该可以工作。唯一的问题发生在您位于北极或南极时。在这里,您可以只使用向量 (0,0,1)、(0,0, -1),或者您可以为步数选择奇数值,这样极点就永远不会包含在网格中。
使用立方体作为基础应该可以很容易地沿着边缘匹配相应的补丁。
我正在尝试在 OpenGL 中生成 Superellipsoid 的表面。 由于该表面可以使用参数方程表示:
我在这个表面上生成一定数量的点,改变参数 u v 并使用看起来像这样的函数将它们放入数组中:
float du = 1.0/(float)(u_max-1);
float dv = 1.0/(float)(v_max-1);
int u, v;
vertices.reserve(u_max*v_max);
normals.reserve(u_max*v_max);
texcoords.reserve(u_max*v_max);
for(u = 0; u < u_max; u++)
for(v = 0; v < v_max; v++)
{
texcoords.emplace(texcoords.end(),vec2(u*du,v*dv));
float cos_v_dv=cos(2*M_PI*v*dv);
float sin_v_dv=sin(2*M_PI*v*dv);
float sin_u_du=sin(M_PI*u*du);
float cos_u_du=cos(M_PI*u*du);
// Parametric equation of the surface
float x=A*sgn(cos_v_dv)*sgn(sin_u_du)*pow(std::abs(cos_v_dv),n)*pow(std::abs(sin_u_du),m);
float y=B*sgn(sin_v_dv)*sgn(sin_u_du)*pow(std::abs(sin_v_dv),n)*pow(std::abs(sin_u_du),m);
float z=C*sgn(cos_u_du)*pow(std::abs(cos_u_du),m);
vertices.emplace(vertices.end(),x,y,z);
// Derivative with respect to u
float dx_du=A*sgn(cos_v_dv)*cos_u_du*pow(std::abs(cos_v_dv),n)*pow(std::abs(sin_u_du),m-1);
float dy_du=B*sgn(sin_v_dv)*cos_u_du*pow(std::abs(sin_v_dv),n)*pow(std::abs(sin_u_du),m-1);
float dz_du=-C*sin_u_du*pow(std::abs(cos_u_du),m-1);
// Derivative with respect to v
float dx_dv=-A*sgn(sin_u_du)*pow(std::abs(sin_u_du),m)*sin_v_dv*pow(std::abs(cos_v_dv),n-1);
float dy_dv=B*sgn(sin_u_du)*pow(std::abs(sin_u_du),m)*cos_v_dv*pow(std::abs(sin_v_dv),n-1);
// derivative of z with respect to v is 0
//Crossing the tangent vectors to get the normal
vec3 normal(-dz_du*dy_dv,dx_dv*dz_du,dx_du*dy_dv-dx_dv*dy_du);
normal.normalize();
normals.push_back(normal);
}
然后我为连接相邻点的四边形生成索引,如:
indices.reserve(u_max * v_max * 4);
for(u = 0; u < u_max-1; u++)
for(v = 0; v < v_max-1; v++)
{
indicies.push_back(u* v_max + v);
indicies.push_back(u* v_max + (v+1));
indicies.push_back((u+1)* v_max + (v+1));
indicies.push_back((u+1)* v_max + v);
}
但是结果很糟糕......:
大多数时候,某些地方的镶嵌效果很差,而其他地方则太多了。 还有一些看起来很奇怪的黑点可能是由于 normals.At 不好引起的首先我认为我生成法线的方法是 flawed.I 用于通过交叉我得到的两个切线向量来计算法线通过区分顶点,如上面的代码所示,例如:
然后我决定通过相交切线来计算法线,切线是通过减去两个相邻顶点的坐标得到的。 结果完全一样,黑点仍然存在,从某种意义上说,闪电看起来不太好。
显然有一个很好的方法可以做到这一点,因为 wiki 文章显示了这个表面的非常好的图像。我可能正在做一些非常愚蠢的事情。
所以我想我的问题是:有没有好的方法来生成这个对象的表面?我怎样才能避免我现在遇到的问题?
我认为您的问题很可能是由于您的采样和参数化曲面本身的奇异性造成的。
您在参数-space(您的 u 和 v')中定期对表面进行采样,在几何 space(您显示的图像)中给出非均匀采样。
两极的奇点可能会产生您提到的阴影伪影。在参数 space 和几何 space 中,您对法线的计算变得病态,导致法线出现 "extreme" 错误。
维基百科上的图像可能是使用光线追踪和表面的完全隐式形式创建的。您可以尝试做同样的事情以获得更好的表面法线:不是在参数值 (u,v) 处微分显式参数方程,而是在位置 (x, y, z) 处微分隐式方程。
另一方面,您也可以简单地尝试明确给定的法线 http://www.gamedev.net/page/resources/_/technical/opengl/superquadric-ellipsoids-and-toroids-opengl-lig-r1172
你在画球体时会遇到类似的问题,球体的任何参数化都会至少有一个地方是奇异的,通过纬度和经度参数化在北极分解。
解决这个问题的一种方法是将曲面分成两个或多个沿曲线相交的小块。有几种方法可以做到这一点,你可以有两个半球,沿着赤道相遇。一个很好的方法是取一个单位立方体并将其沿半径向量投影到单位球体。一旦我们有了球体的一个点,我们就可以获得极坐标并使用它们来找到超椭球体上的点。
如果u,v是曲面的两个参数,0<=u<=1,0<=v<=1 立方体的一个面将由x=2u-1,y=2v给出-1,z=1。我们可以通过除以长度l=sqrt(x^2+y^2+z^2), x1=x/l, y1=y/l, z1=[=来找到单位球面上的点29=]。现在找到极坐标 th=atan2(y1, x1), phi=asin(z1) 并使用它们找到超椭圆上的坐标。
我在 JavaScript 中将其实现为 fiddle。 http://jsfiddle.net/SalixAlba/n1hjm35n/ 使用 OpenGL 实现应该相当简单。
// Auxiliary cos function
function auxC(w,m) {
var c = Math.cos(w);
return sign(c) * Math.pow(Math.abs(c),m);
}
// Auxiliary sin function
function auxS(w,m) {
var s = Math.sin(w);
return sign(s) * Math.pow(Math.abs(s),m);
}
// Given a point on a sphere find the corresponding point on the super-ellipsoid
function SEvec(x,y,z) {
var th = Math.atan2(y,x);
var phi = Math.asin(z);
var xx = A * auxC(phi, 2/ t) * auxC(th, 2/r);
var yy = B * auxC(phi, 2/ t) * auxS(th, 2/r);
var zz = C * auxS(phi, 2/ t);
console.log(x,y,z,xx,yy,zz);
return new THREE.Vector3(xx*100,yy*100,zz*100);
}
// Generate points for the first face
function sf1(u,v) {
var x = 2*u-1;
var y= 2*v-1;
var z= 1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
// second face
function sf2(u,v) {
var x = 2*u-1;
var y= 2*v-1;
var z= -1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf3(u,v) {
var x = 2*u-1;
var z= 2*v-1;
var y= 1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf4(u,v) {
var x = 2*u-1;
var z= 2*v-1;
var y= -1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf5(u,v) {
var z = 2*u-1;
var y= 2*v-1;
var x= 1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
function sf6(u,v) {
var z = 2*u-1;
var y= 2*v-1;
var x= -1;
var l = Math.sqrt(x*x+y*y+z*z);
return SEvec( x/l,y/l,z/l);
}
我没有计算法线,但您的代码应该可以工作。唯一的问题发生在您位于北极或南极时。在这里,您可以只使用向量 (0,0,1)、(0,0, -1),或者您可以为步数选择奇数值,这样极点就永远不会包含在网格中。
使用立方体作为基础应该可以很容易地沿着边缘匹配相应的补丁。