目录
深入讲解 OpenGL 地形生成与物理模拟代码
程序概述
1. 物理参数与小球状态
物理参数
小球状态
2. 地形参数与生成
地形整体参数
宏观轮廓振幅
噪声与 fBM 参数
区段细节振幅
3. 地形生成算法
伪随机梯度哈希
平滑插值
1D Perlin 噪声
分形布朗运动 (fBM)
地形高度计算
4. 坡度与法线计算
5. 绘制地形
6. 物理更新
7. 投影与渲染
投影设置
渲染循环
8. 用户交互
9. 主函数
完整代码
总结
本文将详细讲解一个使用 OpenGL 编写的程序,该程序通过 Perlin 噪声和分形布朗运动(fBM)生成复杂的地形,并在该地形上模拟一个小球的物理滚动行为。这段代码结合了图形渲染、噪声生成和简单物理模拟,适合对计算机图形学和实时模拟感兴趣的读者。我们将逐步分解代码的每个部分,分析其功能、实现原理及背后的思想,最终帮助读者全面理解其工作机制。
这个程序的核心目标是创建一个动态的二维地形,并让一个小球在地形上滚动。地形的高度通过 Perlin 噪声和 fBM 生成,呈现出自然的山峰、平原和山谷特征。小球的运动则受到重力、碰撞反弹和摩擦的影响,用户可以通过键盘控制视图缩放、摄像机位置以及小球的跳跃。程序使用 OpenGL 进行渲染,GLUT 库处理窗口管理和用户输入。
以下是代码的主要组成部分:
接下来,我们将深入探讨每一部分。
物理模拟是程序的核心之一,涉及小球与地形的交互。以下是定义的相关参数:
这些参数为后续的物理更新奠定了基础。
地形是程序的视觉和交互核心,通过噪声算法生成自然的高度变化。
这些参数共同定义了地形的形状和细节。
地形高度通过以下步骤计算:
函数 gradHash(int i) 是一个伪随机数生成器,用于 Perlin 噪声。它通过位运算生成一个范围在 [-1, 1] 的随机梯度值。
函数 smoothstep(float t) 提供平滑的插值曲线,返回值从 0 到 1,用于平滑噪声值之间的过渡。
函数 perlin1D(float x) 生成一维 Perlin 噪声:
函数 fbm(float x) 通过多层 Perlin 噪声叠加生成 fBM:
函数 terrainHeight(float x) 是地形生成的核心:
这种方法生成了既有宏观起伏又有局部细节的自然地形。
为了物理模拟和渲染,需要计算地形的坡度和法线:
函数 drawTerrain() 使用 OpenGL 绘制地形:
函数 updatePhysics() 更新小球状态:
这种方法模拟了真实的物理交互。
函数 applyProjection() 配置正交投影,根据缩放和摄像机位置调整视图。
函数 keyboardFunc(unsigned char key, int x, int y) 处理键盘输入:
main() 初始化程序:
以下是完整的代码实现:
#define GLUT_DISABLE_ATEXIT_HACK
#include
#include
#include
#include
#include
#include
// —— 物理参数 ——
float g = 9.81f;
float restitution = 0.80f, mu = 0.01f;
const float jumpImpulse = 5.0f;
const float dt = 0.0046f;
// —— 小球状态 ——
float px = 0.0f, py = 5.0f;
float vx = 1.5f, vy = 0.0f;
const float radius = 0.5f;
// —— 地形整体参数 ——
const float TERRAIN_WIDTH = 20.0f;
const float TERRAIN_THICK = 0.5f;
const int SEGMENTS = 2000;
// —— 宏观轮廓振幅 ——
const float CTRL_AMP = 80.0f; // 加在这里
// —— 噪声与 fBM 参数 ——
const int OCTAVES = 6;
const float LACUNARITY = 2.0f;
const float GAIN = 0.5f;
const float CTRL_SCALE = 0.05f; // 宏观轮廓“波长”
const float DETAIL_SCALE = 1.0f; // 细节噪声缩放
const float EPSILON = 0.01f; // 坡度数值微分
// —— 区段细节振幅 ——
const float MOUNTAIN_THR = 0.5f;
const float PLAIN_THR = -0.2f;
const float AMP_MOUNTAIN = 1.0f;
const float AMP_PLAIN = 0.2f;
const float AMP_VALLEY = 0.8f;
// —— 视图控制 ——
int winW = 800, winH = 600;
float zoom = 1.0f, camX = 0.0f, camY = 0.0f;
bool followMode = false;
// —— 伪随机梯度哈希 ——
float gradHash(int i) {
unsigned int x = (unsigned int)i;
x = (x << 13) ^ x;
unsigned int h = (x * (x * x * 15731u + 789221u) + 1376312589u) & 0x7fffffff;
return 1.0f - (float)h / 1073741824.0f;
}
// —— 平滑插值函数 ——
float smoothstep(float t) {
return t * t * (3.0f - 2.0f * t);
}
// —— 1D Perlin 噪声 ——
float perlin1D(float x) {
int i = (int)floorf(x);
float t = x - i;
float g0 = gradHash(i), g1 = gradHash(i + 1);
float d0 = g0 * t;
float d1 = g1 * (t - 1.0f);
float u = smoothstep(t);
return d0 + (d1 - d0) * u;
}
// —— 分形布朗运动 fBM ——
float fbm(float x) {
float sum = 0.0f, amp = 1.0f, freq = 1.0f;
for (int o = 0; o < OCTAVES; ++o) {
sum += perlin1D(x * freq) * amp;
freq *= LACUNARITY;
amp *= GAIN;
}
return sum;
}
// —— 地形高度 ——
float terrainHeight(float x) {
// 1. 计算宏观控制噪声
float ctrl = perlin1D(x * CTRL_SCALE);
// 2. 非线性放大:异号立方,使正值更快爬升,负值更迅速下陷
float ctrlShape = (ctrl >= 0.0f)
? ctrl * ctrl * ctrl
: - (ctrl * ctrl * ctrl);
// 3. 整体放大系数,决定山峰海拔高度
const float CTRL_AMP = 5.0f;
float macro = ctrlShape * CTRL_AMP;
// 4. 区段细节振幅选择(平滑过渡)
float ampDetail;
if (ctrl > MOUNTAIN_THR) ampDetail = AMP_MOUNTAIN;
else if (ctrl < PLAIN_THR) ampDetail = AMP_VALLEY;
else {
// 中间过渡段
float t = (ctrl - PLAIN_THR) / (MOUNTAIN_THR - PLAIN_THR);
ampDetail = AMP_PLAIN + smoothstep(t) * (AMP_MOUNTAIN - AMP_PLAIN);
}
// 5. 局部 fBM 细节
float detail = fbm(x * DETAIL_SCALE) * ampDetail;
return macro + detail;
}
// —— 坡度与法线 ——
float terrainSlope(float x) {
return (terrainHeight(x + EPSILON) - terrainHeight(x - EPSILON)) / (2.0f * EPSILON);
}
void terrainNormal(float x, float& nx, float& ny) {
float m = terrainSlope(x);
nx = -m; ny = 1.0f;
float L = sqrtf(nx*nx + ny*ny);
nx /= L; ny /= L;
}
// —— 绘制地形 ——
void drawTerrain() {
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_COLOR_MATERIAL);
GLfloat lightPos[] = { 0.0f, 10.0f, 5.0f, 1.0f };
glLightfv(GL_LIGHT0, GL_POSITION, lightPos);
float cx = followMode ? px : camX;
float viewW = TERRAIN_WIDTH * zoom;
float left = cx - viewW * 0.5f;
float right = cx + viewW * 0.5f;
glBegin(GL_TRIANGLE_STRIP);
for (int i = 0; i <= SEGMENTS; ++i) {
float t = float(i) / SEGMENTS;
float x = left + t * (right - left);
float y = terrainHeight(x);
float yb = y - TERRAIN_THICK;
float nx, ny;
terrainNormal(x, nx, ny);
// 根据海拔映射颜色
float c = (y + CTRL_AMP) / (2.0f * CTRL_AMP);
glColor3f(0.2f*c, 0.6f*(1.0f-c)+0.2f, 0.2f);
glNormal3f(nx, ny, 0.0f);
glVertex3f(x, y, 0.0f);
glNormal3f(nx, ny, 0.0f);
glVertex3f(x, yb, 0.0f);
}
glEnd();
glDisable(GL_COLOR_MATERIAL);
glDisable(GL_LIGHT0);
glDisable(GL_LIGHTING);
}
// —— 物理更新 ——
void updatePhysics() {
vy -= g * dt;
px += vx * dt;
py += vy * dt;
float h = terrainHeight(px);
if (py - radius < h) {
py = h + radius;
float m = terrainSlope(px);
float tx = 1.0f, ty = m;
float len = sqrtf(tx*tx + ty*ty);
tx /= len; ty /= len;
float nx = -ty, ny = tx;
float vdotn = vx*nx + vy*ny;
float v_nx = vdotn * nx, v_ny = vdotn * ny;
float v_tx = vx - v_nx, v_ty = vy - v_ny;
v_nx = -restitution * v_nx;
v_ny = -restitution * v_ny;
v_tx *= (1.0f - mu);
v_ty *= (1.0f - mu);
vx = v_nx + v_tx;
vy = v_ny + v_ty;
}
}
// —— 投影与渲染 ——
void applyProjection() {
glViewport(0, 0, winW, winH);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
float cx = followMode ? px : camX;
float cy = followMode ? py : camY;
float viewW = TERRAIN_WIDTH * zoom;
float viewH = viewW * winH / winW;
glOrtho(cx - viewW*0.5f, cx + viewW*0.5f,
cy - viewH*0.5f, cy + viewH*0.5f,
-1.0f, 1.0f);
glMatrixMode(GL_MODELVIEW);
}
void display() {
applyProjection();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glLoadIdentity();
drawTerrain();
glPushMatrix();
glTranslatef(px, py, 0.0f);
glColor3f(0.8f, 0.1f, 0.1f);
glutSolidSphere(radius, 20, 20);
glPopMatrix();
glutSwapBuffers();
}
void idle() {
updatePhysics();
glutPostRedisplay();
}
void reshape(int w, int h) {
winW = w; winH = h;
}
void keyboardFunc(unsigned char key, int x, int y) {
bool moved = false;
float pan = TERRAIN_WIDTH * 0.05f * zoom;
switch (key) {
case '+': case '=': zoom *= 1.1f; moved = true; break;
case '-': case '_': zoom /= 1.1f; moved = true; break;
case 'w': case 'W': if (!followMode) { camY += pan; moved = true; } break;
case 's': case 'S': if (!followMode) { camY -= pan; moved = true; } break;
case 'a': case 'A': if (!followMode) { camX -= pan; moved = true; } break;
case 'd': case 'D': if (!followMode) { camX += pan; moved = true; } break;
case ' ': followMode = !followMode; moved = true; break;
case 'p': case 'P': vy = jumpImpulse; moved = true; break;
default: return;
}
if (moved) glutPostRedisplay();
}
int main(int argc, char** argv) {
srand(12345);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
int screenW = GetSystemMetrics(SM_CXSCREEN);
int screenH = GetSystemMetrics(SM_CYSCREEN);
glutInitWindowSize(screenW, screenH);
glutInitWindowPosition(0, 0);
glutCreateWindow("大幅度山岳地形示例");
glutFullScreen();
glClearColor(0.7f, 0.9f, 1.0f, 1.0f);
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutIdleFunc(idle);
glutKeyboardFunc(keyboardFunc);
glutMainLoop();
return 0;
}
这个程序通过 Perlin 噪声和 fBM 生成了一个复杂而自然的地形,并结合 OpenGL 渲染和简单物理模拟,实现了小球在该地形上的动态滚动。代码展示了图形学、噪声生成和物理模拟的巧妙结合,用户可以通过键盘交互控制视图和模拟行为。希望本文的讲解能帮助读者深入理解其实现原理,并在图形编程领域激发更多灵感。