PyQt5 OpenGL swapBuffers очень медленно
Я пытаюсь сделать небольшое приложение, используя PyQt5 и PyOpenGL. Все работает нормально, однако рендеринг занимает слишком много времени даже с одной сферой. Я пробовал разные маршруты, чтобы попытаться оптимизировать скорость приложения, и сейчас я использую простое QWindow с OpenGLSurface.
Мне удалось выяснить, что это вызов context.swapBuffers, который занимает много времени и колеблется между прибл. 0,01 с (что хорошо) и 0,05 с (что очень долго) при отображении 1 сферы с некоторым затенением и 240 вершинами.
Теперь мои вопросы следующие: это нормально? Если да, есть ли способ ускорить этот процесс или это связано с тем, как работает pyqt, поскольку это оболочка вокруг библиотеки? По сути: есть ли способ продолжить разработку этой программы без изучения C++. Это довольно простое приложение, которое просто должно визуализировать некоторую атомную структуру и уметь манипулировать ею.
Есть ли другой графический инструментарий, который я мог бы использовать, чтобы иметь меньше накладных расходов при работе с OpenGL из pyopengl?
Это определение, которое делает рендеринг:
def renderNow(self):
if not self.isExposed():
return
self.m_update_pending = False
needsInitialize = False
if self.m_context is None:
self.m_context = QOpenGLContext(self)
self.m_context.setFormat(self.requestedFormat())
self.m_context.create()
needsInitialize = True
self.m_context.makeCurrent(self)
if needsInitialize:
self.m_gl = self.m_context.versionFunctions()
self.m_gl.initializeOpenGLFunctions()
self.initialize()
self.render()
self.m_context.swapBuffers(self)
if self.m_animating:
self.renderLater()
Я использую OpenGl напрямую, без использования определений Qt opengl, формат для поверхности определяется следующим образом:
fmt = QSurfaceFormat()
fmt.setVersion(4, 2)
fmt.setProfile(QSurfaceFormat.CoreProfile)
fmt.setSamples(4)
fmt.setSwapInterval(1)
QSurfaceFormat.setDefaultFormat(fmt)
Edit1: еще несколько пояснений о том, как работает мой код:
def render(self):
t1 = time.time()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
wtvMatrix = self.camera.get_wtv_mat()
transformMatrix = matrices.get_projection_matrix(60, self.width() / self.height(), 0.1, 30, matrix=wtvMatrix)
transformMatrixLocation = glGetUniformLocation(self.shader,"transformMatrix")
glUniformMatrix4fv(transformMatrixLocation,1,GL_FALSE,transformMatrix)
eye_pos_loc = glGetUniformLocation(self.shader, "eye_world_pos0")
glUniform3f(eye_pos_loc, self.camera.position[0], self.camera.position[1], self.camera.position[2])
glDrawElementsInstanced(GL_TRIANGLES,self.num_vertices,GL_UNSIGNED_INT,None,self.num_objects)
print("drawing took:{}".format(time.time()-t1))
self.frame+=1
t1=time.time()
self.m_context.swapBuffers(self)
print('swapping buffers took:{}'.format(time.time()-t1))
Это единственный drawElementsInstanced, который я вызываю. Шейдеры настроены следующим образом (извините за беспорядок):
VERTEX_SHADER = compileShader("""#version 410
layout(location = 0) in vec3 vertex_position;
layout(location = 1) in vec3 vertex_colour;
layout(location = 2) in vec3 vertex_normal;
layout(location = 3) in mat4 model_mat;
layout(location = 7) in float mat_specular_intensity;
layout(location = 8) in float mat_specular_power;
uniform mat4 transformMatrix;
uniform vec3 eye_world_pos0;
out vec3 normal0;
out vec3 colour;
out vec3 world_pos;
out float specular_intensity;
out float specular_power;
out vec3 eye_world_pos;
void main () {
colour = vertex_colour;
normal0 = (model_mat*vec4(vertex_normal,0.0)).xyz;
world_pos = (model_mat*vec4(vertex_position,1.0)).xyz;
eye_world_pos = eye_world_pos0;
specular_intensity = mat_specular_intensity;
specular_power = mat_specular_power;
gl_Position = transformMatrix*model_mat*vec4(vertex_position,1.0);
}""", GL_VERTEX_SHADER)
FRAGMENT_SHADER = compileShader("""#version 410
in vec3 colour;
in vec3 normal0;
in vec3 world_pos;
in float specular_intensity;
in float specular_power;
in vec3 eye_world_pos;
out vec4 frag_colour;
struct directional_light {
vec3 colour;
float amb_intensity;
float diff_intensity;
vec3 direction;
};
uniform directional_light gdirectional_light;
void main () {
vec4 ambient_colour = vec4(gdirectional_light.colour * gdirectional_light.amb_intensity,1.0f);
vec3 light_direction = -gdirectional_light.direction;
vec3 normal = normalize(normal0);
float diffuse_factor = dot(normal,light_direction);
vec4 diffuse_colour = vec4(0,0,0,0);
vec4 specular_colour = vec4(0,0,0,0);
if (diffuse_factor>0){
diffuse_colour = vec4(gdirectional_light.colour,1.0f) * gdirectional_light.diff_intensity*diffuse_factor;
vec3 vertex_to_eye = normalize(eye_world_pos-world_pos);
vec3 light_reflect = normalize(reflect(gdirectional_light.direction,normal));
float specular_factor = dot(vertex_to_eye, light_reflect);
if(specular_factor>0) {
specular_factor = pow(specular_factor,specular_power);
specular_colour = vec4(gdirectional_light.colour*specular_intensity*specular_factor,1.0f);
}
}
frag_colour = vec4(colour,1.0)*(ambient_colour+diffuse_colour+specular_colour);
}""", GL_FRAGMENT_SHADER)
Теперь код, который я использую, когда хочу повернуть сцену, выглядит следующим образом (обновления камеры и т. Д. Выполняются, как обычно, на самом деле):
def mouseMoveEvent(self, event):
dx = event.x() - self.lastPos.x()
dy = event.y() - self.lastPos.y()
self.lastPos = event.pos()
if event.buttons() & QtCore.Qt.RightButton:
self.camera.mouse_update(dx,dy)
elif event.buttons()& QtCore.Qt.LeftButton:
pass
self.renderNow()
Немного заключительной информации: вся информация о вершинах, необходимая в шейдерах, передается через vao, который я инициализировал и связывал ранее в определении инициализации, не содержит слишком много объектов (я просто тестирую и использую икосаэдр с 2 подразделами для визуализации также я удалил дублирующиеся вершины, но это ничего не сделало, так как это не должно быть узким местом, я думаю).
Чтобы ответить на некоторые вопросы: я попробовал с varius разные версии opengl только для gigglez, без изменений, попробовал без vsync, ничего не изменилось, попробовал с разными размерами выборки, без изменений.
Edit2: может быть подсказка: swapBuffers большую часть времени занимает около 0,015 с, но когда я начинаю много двигаться, он заикается и прыгает до 0,05 с для некоторых рендеров. Почему это происходит? Из того, что я понимаю, каждый рендер должен обрабатывать все данные в любом случае?
1 ответ
Благодаря тому, что OpenGL работает, отправляемые вами команды рендеринга отправляются в графический процессор и выполняются асинхронно (честно говоря, даже процесс отправки их в графический процессор асинхронный). Когда вы запрашиваете отображение заднего буфера при вызове swapBuffers
драйвер дисплея должен ждать, пока содержимое заднего буфера завершит рендеринг (т. е. все ранее выполненные команды завершат выполнение), и только тогда он может поменять буферы.†
Если у вас низкая частота кадров, вы должны оптимизировать код рендеринга, то есть то, что вы отправляете в графический процессор. Переход на C++ здесь вам не поможет (хотя это было бы неплохо независимо).
РЕДАКТИРОВАТЬ: Вы говорите, что, когда вы ничего не делаете, то ваш swapBuffers
выполняется за 0,015 секунды, что подозрительно составляет ~1/60 секунды. Это означает, что ваш код рендеринга достаточно эффективен для рендеринга со скоростью 60 FPS, и у вас пока нет причин его оптимизировать. Вероятно, случается так, что ваш звонок renderNow()
от mouseMoveEvent
вызывает повторную визуализацию сцены более 60 раз в секунду, что является избыточным. Вместо этого вы должны позвонить renderLater()
в mouseMoveEvent
и соответственно реструктурируйте свой код.
ПРИМЕЧАНИЕ: вы звоните swapBuffers
дважды, один раз в render()
и однажды в renderNow()
незамедлительно после.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Я не знаком с PyOpenGL.
† swapBuffer
может также выполняться асинхронно, но даже в этом случае, если драйвер дисплея меняет местами буферы быстрее, чем вы можете сделать, вы в конечном итоге заблокируете swapBuffer
вызов.