OpenGL C++ - 太阳系

3
我使用OpenGL和C++构建了一个太阳系,其中之一的特点是每个星球上都有指向北极的相机位置,这些相机位置会随着星球的变换而移动。这些相机位置分别是:一个在顶部,一个在星球后面稍微远一点,最后一个离星球很远。除此之外还有其他一些特性,但我对它们没有任何问题。
所以,我遇到的问题是,一些星球似乎在围绕它们的中心旋转时会发抖。如果我增加旋转速度,星球就不会抖动,或者抖动变得不明显。整个太阳系完全基于真实的纹理和比例空间计算,并且有多个相机位置,如前面所述。
以下是一些代码,可能有助于理解我正在尝试实现的内容:
//Caculate the earth postion
GLfloat UranusPos[3] = {Uranus_distance*DistanceScaler * cos(-uranus * M_PI / 180), 0, Uranus_distance*DistanceScaler * sin(-uranus * M_PI / 180)};
//Caculate the Camera Position
GLfloat cameraPos[3] = {Uranus_distance*DistanceScaler * cos(-uranus * M_PI / 180), (5*SizeScaler), Uranus_distance*DistanceScaler * sin(-uranus * M_PI / 180)};
//Setup the camear on the top of the moon pointing 
gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2], UranusPos[0], UranusPos[1], UranusPos[2]-(6*SizeScaler), 0, 0, -1);

SetPointLight(GL_LIGHT1,0.0,0.0,0.0,1,1,.9);
//SetMaterial(1,1,1,.2);
//Saturn Object
// Uranus Planet
UranusObject(  UranusSize * SizeScaler,   Uranus_distance*DistanceScaler,   uranusumbrielmoonSize*SizeScaler,   uranusumbrielmoonDistance*DistanceScaler,   uranustitaniamoonSize*SizeScaler,   uranustitaniamoonDistance*DistanceScaler,   uranusoberonmoonSize*SizeScaler,   uranusoberonmoonDistance*DistanceScaler);

以下是我调用的行星函数,用于在显示函数中绘制对象:
void UranusObject(float UranusSize, float UranusLocation, float UmbrielSize, float UmbrielLocation, float TitaniaSize, float TitaniaLocation, float OberonSize, float OberonLocation)
{
    glEnable(GL_TEXTURE_2D);
    glPushMatrix();

    glBindTexture( GL_TEXTURE_2D, Uranus_Tex);
    glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
    glRotatef( uranus, 0.0, 1.0, 0.0 );
    glTranslatef( UranusLocation, 0.0, 0.0 );
    glDisable( GL_LIGHTING );
    glColor3f( 0.58, 0.29, 0.04 );
    DoRasterString( 0., 5., 0., "     Uranus" );
    glEnable( GL_LIGHTING );
    glPushMatrix();
    // Venus Spinning
    glRotatef( uranusSpin, 0., 1.0, 0.0 );
    MjbSphere(UranusSize,50,50);

    glPopMatrix();
    glDisable(GL_TEXTURE_2D);
    glEnable(GL_TEXTURE_2D);
    glPushMatrix();

    glBindTexture( GL_TEXTURE_2D, Umbriel_Tex);
    glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
    glDisable(GL_LIGHTING);
    if (LinesEnabled)
    {
        glPushMatrix();
        gluLookAt( 0.0000001, 0., 0.,     0., 0., 0.,     0., 0., .000000001 );
        DrawCircle(0.0, 0.0, UmbrielLocation, 1000);
        glPopMatrix();
    }
    glEnable( GL_LIGHTING );
    glColor3f(1.,1.,1.);
    glRotatef( uranusumbrielmoon, 0.0, 1.0, 0.0 );
    glTranslatef( UmbrielLocation, 0.0, 0.0 );
    MjbSphere(UmbrielSize,50,50);

    glPopMatrix();
    glDisable(GL_TEXTURE_2D);
    glEnable(GL_TEXTURE_2D);
    glPushMatrix();

    glBindTexture( GL_TEXTURE_2D, Titania_Tex);
    glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
    glDisable(GL_LIGHTING);
    if (LinesEnabled)
    {
        glPushMatrix();
        gluLookAt( 0.0000001, 0., 0.,     0., 0., 0.,     0., 0., .000000001 );
        DrawCircle(0.0, 0.0, TitaniaLocation, 1000);
        glPopMatrix();
    }
    glEnable( GL_LIGHTING );
    glColor3f(1.,1.,1.);
    glRotatef( uranustitaniamoon, 0.0, 1.0, 0.0 );
    glTranslatef( TitaniaLocation, 0.0, 0.0 );
    MjbSphere(TitaniaSize,50,50);

    glPopMatrix();
    glDisable(GL_TEXTURE_2D);
    glEnable(GL_TEXTURE_2D);
    glPushMatrix();

    glBindTexture( GL_TEXTURE_2D, Oberon_Tex);
    glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
    glDisable(GL_LIGHTING);
    if (LinesEnabled)
    {
        glPushMatrix();
        gluLookAt( 0.0000001, 0., 0.,     0., 0., 0.,     0., 0., .000000001 );
        DrawCircle(0.0, 0.0, OberonLocation, 1000);
        glPopMatrix();
    }
    glEnable( GL_LIGHTING );
    glColor3f(1.,1.,1.);
    glRotatef( uranusoberonmoon, 0.0, 1.0, 0.0 );
    glTranslatef( OberonLocation, 0.0, 0.0 );
    MjbSphere(OberonSize,50,50);

    glPopMatrix();
    glDisable(GL_TEXTURE_2D);
    glPopMatrix();
    glDisable(GL_TEXTURE_2D);
}

最后,以下代码用于太阳动画中使用的转换计算:

uranus += 0.0119 * TimeControl;

if( uranus > 360.0 )
    uranus -= 360.0;

// Clockwise Rotation
uranusSpin -= 2.39 * TimeControl;
if( uranusSpin <= -360.0 )
    uranusSpin = 0.0;

注意:问题只发生在其中四个星球上。
非常感谢能够解决这个问题的任何想法。

为什么不像对天王星一样正确地将uranusSpin的包装设置为-360度?或者干脆不要设置,因为glRotatef()可以处理[-inf,inf]。 - patraulea
2
请注意,那个版本的OpenGL已经被弃用了十年。除非你因为某些非常独特的原因被迫使用它,否则请自己一个忙,学习现代OpenGL。 - GraphicsMuncher
2
如果您使用了正确的距离,问题可能是有限精度。 - geometrian
@GraphicsMuncher 也可以尝试一下Vulkan,会很有趣的! - Poriferous
我非常感激你的回答。在Vulkan中编码比OpenGL更难,它只是为了获得更好的性能和保护源代码。 - Hafed
显示剩余2条评论
2个回答

2
首先看一下:现在来看看你的问题。我懒得看你的代码,但是你很可能撞到了浮点精度限制,或者在旋转过程中出现了累积误差。我打赌这个误差在离太阳越远(外行星和更多的黄道面内)就越大。如果是这样的话,那么解决方法很明显。那么如何解决呢?
1. 浮点精度 在渲染时,您正在通过变换矩阵转换顶点。对于合理范围内的顶点来说,这是可以接受的,但如果您的顶点距离(0,0,0)非常远,则矩阵乘法就不是那么精确了。这意味着当您将其转换回相机空间时,顶点坐标会跳动。为了避免这种情况,在将其加载到OpenGL之前,您需要将顶点从相机位置减去(请注意,这是在将它们加载到OpenGL之前!),然后使用位置为(0,0,0)的相机进行渲染。这样,您就可以摆脱跳动甚至是错误的基元插值。详情请参见:如果您的对象仍然太大(这不适用于太阳系),那么您可以将更多的视锥体堆叠在一起,并使用一些步骤移动的单独相机渲染每个视锥体,以便您进入范围内。
在可行的情况下使用64位浮点数,但要注意GPU实现不支持64位插值器,因此片段着色器使用32位浮点数进行馈送。
2. 变换矩阵中的累积误差 如果您有一些“静态”矩阵并对其应用无数次操作,例如旋转、平移,那么它会在一段时间后失去精度。这可以通过改变比例(轴不再是单位大小)和添加偏斜(轴不再垂直于彼此)来识别,随着时间的推移,这种情况会越来越糟糕。
为了解决这个问题,您可以保持每个矩阵的操作计数器,如果达到某个阈值,则执行矩阵归一化。这很简单,只需提取所有轴向量,使它们再次成为垂直的,将它们设置回其原始大小,并将它们写回您的矩阵。使用基本的向量数学知识就可以做到这一点,只需利用叉积(它给出垂直的向量)。我使用Z轴作为视图方向,因此我保持Z轴方向不变,并更正X,Y轴方向。大小很容易,只需将每个向量除以其大小,您就会再次成为单位(或非常接近单位)。详情请参见:

[编辑1] 发生了什么

先看一下没有渲染部分的单个星球代码:

// you are missing glMatrixMode(GL_????) here !!! what if has been changed?
glPushMatrix(); 
// rotate to match dayly rotation axis?
glRotatef( uranus, 0.0, 1.0, 0.0 );
// translate to Uranus avg year rotation radius
glTranslatef( UranusLocation, 0.0, 0.0 );
glPushMatrix();
// rotate Uranus to actual position (year rotation)
glRotatef( uranusSpin, 0., 1.0, 0.0 );
// render sphere
MjbSphere(UranusSize,50,50);
glPopMatrix();

// moons

glPopMatrix();

您正在做的事情是这样的。假设您正在使用ModelView矩阵,并指示OpenGL对其执行此操作:

ModelView = ModelView * glRotatef(uranus,0.0,1.0,0.0) * glTranslatef(UranusLocation,0.0,0.0) * glRotatef(uranusSpin,0.,1.0, 0.0);

那么这有什么问题吗?对于小场景来说没有问题,但是你正在使用比例大小,因此:

UranusLocation=2870480859811.71 [m]
UranusSize    =     25559000    [m]

那意味着glVertex的大小约为~25559000,并且应用变换后的大小为~2870480859811.71+25559000。现在这些值存在一些问题。
首先,任何glRotation调用都会对2870480859811.71应用sincos系数。假设我们的sin,cos误差约为0.000001,那么结果中的最终位置存在误差:
error=2870480859811.71*0.000001=2870480.85981171

OpenGLsin,cos实现可能具有更高的精度,但提高不多。无论如何,如果您将其与行星半径进行比较。

2870480.85981171/25559000=0.112308 -> 11%

您可以看到,跳跃误差大约为行星大小的11%。这是很大的。由此可以得出结论,跳跃越远离太阳,对于较小的行星更加明显(因为我们的感知通常是相对而不是绝对的)。
您可以尝试通过使用双精度(glRotated)来增强它,但这并不意味着它会解决问题(某些驱动程序没有sin,cos的双精度实现)。
如果您想摆脱这些问题,您需要遵循#1的要点或仅在至少双倍精度上进行旋转,并仅将最终矩阵提供给OpenGL。所以首先是#1方法。矩阵的平移只是+/-操作(也编码为乘法),但没有不准确的系数存在,因此您正在使用所使用变量的完全精度。无论如何,我会使用glTranslated来确保。因此,我们需要确保旋转不使用OpenGL中的大值。所以尝试这个:
// compute planet position
double x,y,z;
x=UranusLocation*cos(uranusSpin*M_PI/180.0);
y=0.0;
z=UranusLocation*sin(uranusSpin*M_PI/180.0);

// rotate to match dayly rotation axis?
glRotated( uranus, 0.0, 1.0, 0.0 );
// translate to Uranus to actual position (year rotation)
glTranslated(x,y,z);
// render sphere
MjbSphere(UranusSize,50,50);

这会影响日常旋转速度,因为日旋转角度和年旋转角度并不相加,但是您目前尚未实现日常旋转。如果这没有帮助,那么我们需要使用相机本地坐标来避免将大值发送到OpenGL:
// compute planet position
double x,y,z;
x=UranusLocation*cos(uranusSpin*M_PI/180.0);
y=0.0;
z=UranusLocation*sin(uranusSpin*M_PI/180.0);
// here change/compute camera position to (original_camera_position-(x,y,z))

// rotate to match dayly rotation axis?
glRotated( uranus, 0.0, 1.0, 0.0 );
// render sphere
MjbSphere(UranusSize,50,50);

希望我匹配了你的坐标系,如果没有,请交换轴或取反它们(x、y、z)。

最好拥有自己精确的矩阵数学工具,并在CPU端高精度计算glTranslate、glRotate,并仅使用结果矩阵在OpenGL中。请参见上面的了解4x4同构变换矩阵链接,了解如何操作。


感谢您的回复... 我在火星上也有同样的问题,那里非常靠近太阳。为了进一步澄清,这个问题发生在火星、天王星、海王星和冥王星上。其它的都运行正常,没有问题。此外,与其他行星相比,火星球体的大小非常小,这是否是导致火星出现这个错误的另一个原因? - Hafed
@Hafed,错误本身的大小不应该影响(只有在OpneGL渲染的任何阶段顶点值的大小)但是在像木星、土星这样的大行星上,您可能会忽略这一点,因为它们很大,所以您可以将相机放得更远,这在透视投影中降低了跳动。您可以尝试仅渲染十字架而不是行星,并非常接近它们(靠近行星中心),您应该能看到跳动。无论如何,如果您将顶点转换为相机本地位置,则应该没问题。 - Spektre
谢谢,我现在明白了。但是最后一句话对我来说不太清楚,因为我认为我是通过使用余弦和正弦函数根据行星当前位置计算行星位置并将其放入向量[3]中,然后再根据行星当前位置使用正弦和余弦函数计算相机位置并将其放入另一个向量[3]中。 - Hafed
@Hafed 我使用自己的矩阵类,从未真正使用过 gluLookAt,所以很难说。我通过设置摄像机的位置和视角直接定位它。但是这可能是你问题的一部分,因为我之前看到过 glu 矩阵精度错误。例如,gluPerspective 具有不精确的正切值。人们使用 glm 库来处理这些内容,通常使用欧拉角(我不喜欢/使用它们,因为它们存在问题并且对我的需求太限制)。无论如何,尝试按照我在 #1 中建议的方式首先将顶点和相机(在 CPU 方面)进行转换应该就足够了。 - Spektre
例如,您可以使用gluLookAt(cameraPos [0] - UranusPos [0],cameraPos [1] - UranusPos [1],cameraPos [2] - UranusPos [2],0.0,0.0,0.0-(6 * SizeScaler),0,0,-1); 并添加glTranslatef-UranusLocation - Spektre
显示剩余8条评论

1
我无法根据您提供的内容给出确切答案。我无法编译和运行您当前的代码,并使用数字和调试器进行测试。我能做的是给你一些建议,应该会帮助你。
从我所看到的代码中,您是通过函数调用创建Planet对象的。因此,我可以推断出您为每个星球都这样做了。如果是这种情况,那么如果您查看完整的代码库,除了它们的名称和使用的数字之外,这些函数中的每一个都基本上是它们自己的副本或重复代码。这就是您需要更具通用性的结构的地方。
以下是在您的结构中需要考虑的一些重要事项。
创建一个完整的3D运动摄像机类和一个单独的玩家类 - 摄像机类将允许您通过(受限角度)鼠标控制向上,向下,向左和向右查看 - 玩家类将在camera_eye_level处附加相机对象,并且lookAtDirection垂直于upVector。玩家类将能够通过键盘控件自由向前,向后,向上,向下转向左和转向右。这为您提供了灵活性。 - 注意:如果您按比例进行操作并且您的星球相距很远,请使您的玩家线性速率更高,以便您更快地向对象移动,覆盖更多距离。
创建一个基础几何类 - 派生几何类:Box,Flat Grid,Cylinder,Sphere,Pyramid,Cone - 这些类将包含有限的Vector3s(顶点)容器,Vector3(索引)容器,Vector2(纹理坐标)容器,Vector4(带Alpha的材料-颜色)容器和Vector3(法线)容器。 - 基类可能只持有无符号整数(ID值)和/或std :: string(名称标识符),以及正在创建的类型几何体的Enum值。 - 每个派生类将具有所需参数的构造函数,即三个维度的大小(不需要在此处担心纹理和颜色信息,稍后会出现)
创建一个材料类 创建一个基础光类 - 派生类型:定向,点和聚光灯
创建一个纹理类 创建一个纹理变换类-这将允许您直接在附加到几何体的纹理上应用变换,以使物体移动的效果,而仅有纹理变换。 - 例如:Geometry Type Box(2,1,4)Texture Applied“conveyor_belt1.png”,这样您就不必更改盒子的变换并在每个渲染通道上移动所有顶点,纹理坐标和法线,您只需简单地使纹理移动到原地。计算成本更低。
创建节点类-所有节点都属于场景图的基类 - 场景图的节点类型-ShapeNode(几何),LightNode,TransformNode(包含平移,旋转和缩放的矢量和矩阵信息) - 节点类的组合和场景图将创建像这样的树形结构:


// This would be an example of text file that you would read in to parse the data 
// and it will construct the scene graph for you, as well as rending it.
// Amb = Ambient, Dif = Diffuse - The Materials Work With Lighting & Shading and will blend with which ever texture is applied      

// Items needed to construct
// Objects
Grid geoid_1 Name_plane Wid_5 Dep_5 divx_10 divz_10
Sphere geoid_2 name_uranus radius_20 
Sphere geoid_3 name_earth radius_1
Sphere geoid_4 name_earth_moon radius_0.3

// Materials
Material matID_1 Amb_1,1,1,1 Dif_1,1,1,1 // (white fully opaque)

// Textures   
Texture texID_1 fileFromTexture_"assets\textures\Uranus.png"
Texture texID_2 fileFromTexture_"assets\textures\Earth.png"
Texture texID_3 fileFromTexture "assets\textures\EarthMoon.png"

// Lights (Directional, Point, & Spot Types)

Transform Trans_0,0,0 // This is the center of the World Space and has to be a 
+Shape geoID_1 matID_1 // Applies flat grid to root node to create your horizontal plane
+Transform Trans_10,2,10000 // Nest a transform node that is relative to the root; this will allow you to rotate, translate and scale every object that is attached and nested below this node. Any Nodes Higher in the branch will not be affected
++Shape geoID_2 matID_1 texID_1
+Transform Trans_10,1.5,200 // This node has 1 '+' so it is nested under the root and not Uranus
++Shape geoID_3 matID_1 tex1ID_2
+++Transform Trans_10,1.5,201 // This node is nested under the Earth's Transform Node and will belong to the Earth's Moon.
+++Shape geoID_4 matID_1 textID_3

END // End Of File To Parse

使用这种结构,您可以独立地对对象进行平移、旋转和缩放,或者通过层次结构来进行操作。例如:对于天王星的卫星,您可以应用与地球及其卫星相同的技术。每个卫星都会有自己的变换,但这些变换将嵌套在行星的变换下面,而行星的变换将嵌套在根节点甚至是太阳的变换下面(太阳=光源),它将有几个光节点附加在上面(我已经在太阳-地球&月亮上做过这个了,并让物体相应地旋转)。
您有一辆吉普车的模型,但需要渲染4个不同的模型才能在游戏中完整呈现。1车身、2前轮、3后轮和4方向盘。使用这种结构,图表可能如下所示。
Model modID_1 modelFromFile_"Assets/Models/jeep_base.mod"
Model modID_2 modelFromFile_"Assets/Models/jeep_front_wheel.mod"
Model modID_3 modelFromFile_"Assets/Models/jeep_rear_wheel.mod"
Model modID_4 modelFromFile_"Assets/Models/jeep_steering.mod"

Material matID_1 Amb_1_1_1_1 Diff_1_1_1_1
Texture texID_1 textureFromFile_"Assets/Textures/jeep1_.png"    

TextureTransform texTransID_1 name_front
TextureTransform texTransID_2 name_back
TextureTransform texTransID_3 name_steering

+Transform_0,0,0 name_root
++Transform_0,0.1,1 name_jeep
+++Shape modID_1 matID_1 texID_1  texCoord_0,0, size_75,40
+++Transform_0.1,0.101,1 name_jeepFrontWheel
+++Shape modID_2 matID_1 texID_1  texCoord_80,45 size_8,8
+++Transform_-0.1,-0.101,1 name_jeepBackWheel
+++Shape modID_3 matID_1 texID_2  texCoord_80,45 size_8,8
+++Transform_0.07,0.05,-0.02 name_jeepSteering
+++Shape modID_4 matID_1 texID_2  texCoord_80,55 size_10,10

END

在你的代码中,你可以获取属于吉普车的变换节点,当你将其沿着屏幕平移时,所有吉普车部件一起移动作为一个对象,但是所有轮胎都可以使用纹理变换独立地前后旋转,前轮可以通过受限度数的自身节点变换左右转向,同时你的转向也可以使用纹理变换向与轮子相同的方向转动,但可能会旋转更少或更多,这取决于此特定车辆的特性以及它在场景或游戏中的处理方式。

有了这种类型的系统,构建过程有点困难,但一旦它工作起来,就可以简单地自动生成场景图。这种方法的缺点是文本文件容易在人类水平上阅读,但解析文本文件比解析二进制文件要困难得多。主要原因是您的Scene类所属的函数需要解析并创建所有这些不同的节点(所有几何形状、所有灯光、材质纹理和变换节点)。

拥有几何体和灯光的基类的原因在于,当您创建一个附加或嵌套到TransformNode的ShapeNode或LightNode时,它可以接受基类的任何派生类型。这样,您就不必编写场景图构造和解析器来接受每种类型的几何体和灯光,而是可以接受任何几何体和灯光,但要告诉它它是什么类型。
现在,您可以通过创建一个可读取二进制文件的解析器使此过程变得更加容易。好处是只要您知道文件的结构,要读取的数据量以及当前读取位置的预期数据类型,就可以更轻松地编写解析器。缺点是您不能像我上面演示的使用人类可读的文本文件手动读取或设置此类信息。然而,采用这种方法需要您拥有一个良好的十六进制编辑器程序,该程序允许文件模板类型,例如 010 Editor,这样当您使用应用了模板的二进制文件进行读取时,可以查看值是否正确,并且字段具有适当的数据类型和值。
实际上,我能给你的最好建议是:以上结构仍可遵循,但可能不是最佳选择,它是一个很好的起点。但你现在正在学习的OpenGL似乎是过时和废弃的版本OpenGL v1.0。你应该放弃从这个API遗留下来的所有知识,并开始学习任何版本高于3.3的现代OpenGL。从那里,你就可以学习如何在GLSL中构建和编写着色器,一旦你的框架正常工作,这将简化很多事情。然后,一旦你把它们放在合适的位置,而不是从CPU渲染到屏幕,而是从GPU渲染到屏幕效率会更高。一旦你做到了这一点,就考虑研究批量渲染的概念。批量渲染将允许你控制有多少个桶以及每个桶包含多少个顶点。这个批处理过程可以防止CPU通过总线向GPU传输I/O速度的瓶颈,因为GPU计算比CPU快得多。然后,在你的场景图中,你将不必担心创建灯光、创建材质并将它们应用于所有内容,因为这些都将在你的片段着色器(GLSL)或所谓的像素着色器(DirectX)中完成。所有你的顶点和索引信息都将传递到顶点着色器中。然后只需将你的着色器链接到一个OpenGL程序即可。
如果您想查看并学习我上面所描述的一切,请访问我自2007-08年以来一直是成员的社区www.MarekKnows.com并加入我们的社区。他有几个视频教程系列可供学习。

当我有时间时,我可以看一下它。但是我强烈建议从OpenGL 1.0过渡到现代OpenGL和GLSL。 - Francis Cugler
@Hafed 407MB 的压缩包太大了(我甚至不敢尝试下载),你应该将其解压后压缩到10MB以内。我的星历数据解压后大约是25/110/310 MB,取决于使用的DTM模型,但你似乎只使用纹理和恒星目录,没有使用DTM。你在使用未压缩的纹理吗?这样加载应用程序肯定需要很长时间... - Spektre
谢谢您的评论。是的,我正在尽量使用真实纹理使其尽可能真实,并且它并不像您想象中的那么糟糕。加载所有内容最多需要5-8秒钟。 - Hafed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接