使用OpenGL创建科学实验用闪烁刺激视频的正确方法
问题描述
我们为开展科学实验,需要开发一款显示闪烁刺激(如屏幕闪烁方块)的程序,采用C++与OpenGL实现。方案是在display函数中跟踪帧数,偶数帧绘制方块,奇数帧不绘制,同时实现FPS统计功能。
测试环境为Windows 10系统 + NVIDIA Geforce RTX 3080 Ti GPU,在两款显示器上的测试结果:
- 144Hz显示器未限FPS时,方块有时闪烁、多数时间固定,FPS显示144;窗口状态变化时闪烁模式改变,但FPS仍为144
- 60Hz显示器方块持续闪烁,FPS为60
- 限FPS为120时闪烁更稳定,但仍有时方块固定,FPS显示120
我们推测问题出在帧绘制/擦除方式及GPU、OS未考虑的配置上,恳请提供解决思路。
代码片段
#include <iostream> #include <Windows.h> #include <gl/GL.h> #include <gl/GLU.h> #include <gl/glut.h> #include <time.h> # define FPS 120 using namespace std; bool limitFPS = false; int initial_time = time(NULL), final_time, frame_count = 0; int frameNum = 0; void timer(int) { glutPostRedisplay(); glutTimerFunc(1000/FPS, timer, 0); } void idle() { glutPostRedisplay(); } void display() { // display callback function glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); // resets coordinate system // draw something here if (frameNum % 2 == 0) { glBegin(GL_POLYGON); glColor3f(1.0, 1.0, 1.0); glVertex3f(-5.0, 5.0, 0.0); glVertex3f(-5.0, -5.0, 0.0); glVertex3f(5.0, -5.0, 0.0); glVertex3f(5.0, 5.0, 0.0); glEnd(); } glutSwapBuffers(); // presents buffer in the screen frame_count++; frameNum++; final_time = time(NULL); if (final_time - initial_time > 0) { cout << "FPS: " << frame_count / (final_time - initial_time) << endl; frame_count = 0; initial_time = final_time; } } void init() { // initializing window params glClearColor(0.0, 0.0, 0.0, 1.0); } void reshape(int w, int h) { // set viewport glViewport(0, 0, w, h); // set projection glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(-10, 10, -10, 10); glMatrixMode(GL_MODELVIEW); } int main(int argc, char** argv){ cout << "Starting..." << endl; glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowPosition(200, 100); glutInitWindowSize(500, 500); glutCreateWindow("Test Window"); // first create the window, then register the callback glutDisplayFunc(display); glutReshapeFunc(reshape); // how window reshape is going to be handled if (limitFPS) glutTimerFunc(1000/FPS, timer, 0); // function to perform animation, timer func will be called after X ms else glutIdleFunc(idle); init(); glutMainLoop(); // requires a display callback: a function when the window needs to be drawn cout << "Finishing..." << endl; return 0; }
解决思路
1. 同步绘制逻辑与显示器垂直刷新
当前代码的帧计数和显示器垂直同步(VSync)完全脱节,导致绘制的帧可能被GPU/显示器丢弃或重复显示,出现闪烁异常。需要强制开启垂直同步,并基于显示器刷新周期控制闪烁,而非程序自身帧计数:
- 在OpenGL上下文创建后调用
wglSwapIntervalEXT(1)开启垂直同步,让glutSwapBuffers()严格等待显示器垂直刷新后再交换缓冲区。 - 改用基于时间的闪烁判断:用高精度计时器(如
QueryPerformanceCounter())记录每次缓冲区交换的时间,计算与显示器刷新周期的倍数关系,决定是否绘制方块。比如144Hz显示器刷新周期约为6.94ms,每经过一个周期切换一次绘制状态。
2. 修正FPS统计与帧控制逻辑
- 当前用
time(NULL)统计FPS精度仅为秒级,改用QueryPerformanceCounter()计算准确的帧间隔和FPS,避免统计误差。 - 手动限FPS的
glutTimerFunc方式精度不足(Windows定时器本身存在误差),开启垂直同步后,无需手动限FPS,让VSync自动匹配显示器刷新率,再在此基础上控制闪烁频率。
3. 避免GPU帧合并优化
部分GPU会对连续空帧(奇数帧只清屏不绘制)进行合并优化,导致实际显示的帧并非严格交替状态。可以修改奇数帧逻辑:
- 奇数帧不只是清屏,而是绘制一个全黑的全屏多边形,确保每帧都有明确的绘制操作,避免GPU跳过帧。
- 或直接切换
glClearColor的颜色值,奇数帧清屏为白色,偶数帧清屏为黑色,简化逻辑同时避免空帧。
4. 禁用窗口 compositor 优化
Windows 10的桌面 compositor(如DWM)可能对窗口内容进行额外渲染处理,干扰帧显示时序。可以尝试:
- 启用窗口独占全屏模式,绕过DWM的 compositor 处理。
- 调用
DwmSetWindowAttribute禁用窗口的 compositor 优化(需Windows API支持)。
内容的提问来源于stack exchange,提问作者hernandezurbina
相关产品推荐
相关产品推荐

