/* Copyright (C) 2017 Eric Wasylishen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA See file, 'COPYING', for details. */ #include "glview.h" // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include GLView::GLView(QWidget *parent) : QOpenGLWidget(parent), m_keysPressed(0), m_keymoveUpdateTimer(0), m_lastMouseDownPos(0, 0), m_moveSpeed(1000), m_displayAspect(1), m_cameraOrigin(0, 0, 0), m_cameraFwd(0, 1, 0), m_vao(), m_indexBuffer(QOpenGLBuffer::IndexBuffer) { setFocusPolicy(Qt::StrongFocus); // allow keyboard focus } GLView::~GLView() { makeCurrent(); delete m_program; delete m_program_wireframe; m_vbo.destroy(); m_indexBuffer.destroy(); m_vao.destroy(); lightmap_texture.reset(); m_drawcalls.clear(); doneCurrent(); } static const char *s_fragShader_Wireframe = R"( #version 330 core out vec4 color; void main() { color = vec4(1.0); } )"; static const char *s_vertShader_Wireframe = R"( #version 330 core layout (location = 0) in vec3 position; uniform mat4 MVP; void main() { gl_Position = MVP * vec4(position, 1.0); } )"; static const char *s_fragShader = R"( #version 330 core in vec2 uv; in vec2 lightmap_uv; in vec3 normal; flat in vec3 flat_color; out vec4 color; uniform sampler2D texture_sampler; uniform sampler2D lightmap_sampler; uniform float opacity; uniform bool lightmap_only; uniform bool fullbright; uniform bool drawnormals; uniform bool showtris; uniform bool drawflat; void main() { if (drawnormals) { // remap -1..+1 to 0..1 color = vec4((normal + vec3(1.0)) / vec3(2.0), opacity); } else if (drawflat) { color = vec4(flat_color, opacity); } else { vec3 texcolor = lightmap_only ? vec3(0.5) : texture(texture_sampler, uv).rgb; vec3 lmcolor = fullbright ? vec3(0.5) : texture(lightmap_sampler, lightmap_uv).rgb; // 2.0 for overbright color = vec4(texcolor * lmcolor * 2.0, opacity); } } )"; static const char *s_vertShader = R"( #version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec2 vertex_uv; layout (location = 2) in vec2 vertex_lightmap_uv; layout (location = 3) in vec3 vertex_normal; layout (location = 4) in vec3 vertex_flat_color; out vec2 uv; out vec2 lightmap_uv; out vec3 normal; flat out vec3 flat_color; uniform mat4 MVP; void main() { gl_Position = MVP * vec4(position.x, position.y, position.z, 1.0); uv = vertex_uv; lightmap_uv = vertex_lightmap_uv; normal = vertex_normal; flat_color = vertex_flat_color; } )"; void GLView::initializeGL() { initializeOpenGLFunctions(); // set up shader m_program = new QOpenGLShaderProgram(); m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, s_vertShader); m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, s_fragShader); assert(m_program->link()); m_program_wireframe = new QOpenGLShaderProgram(); m_program_wireframe->addShaderFromSourceCode(QOpenGLShader::Vertex, s_vertShader_Wireframe); m_program_wireframe->addShaderFromSourceCode(QOpenGLShader::Fragment, s_fragShader_Wireframe); assert(m_program_wireframe->link()); m_program->bind(); m_program_mvp_location = m_program->uniformLocation("MVP"); m_program_texture_sampler_location = m_program->uniformLocation("texture_sampler"); m_program_lightmap_sampler_location = m_program->uniformLocation("lightmap_sampler"); m_program_opacity_location = m_program->uniformLocation("opacity"); m_program_lightmap_only_location = m_program->uniformLocation("lightmap_only"); m_program_fullbright_location = m_program->uniformLocation("fullbright"); m_program_drawnormals_location = m_program->uniformLocation("drawnormals"); m_program_showtris_location = m_program->uniformLocation("showtris"); m_program_drawflat_location = m_program->uniformLocation("drawflat"); m_program->release(); m_program_wireframe->bind(); m_program_wireframe_mvp_location = m_program_wireframe->uniformLocation("MVP"); m_program_wireframe->release(); m_vao.create(); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glFrontFace(GL_CW); } void GLView::paintGL() { // draw glClearColor(0.1, 0.1, 0.1, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); QMatrix4x4 modelMatrix; QMatrix4x4 viewMatrix; QMatrix4x4 projectionMatrix; projectionMatrix.perspective(90, m_displayAspect, 1.0f, 1'000'000.0f); viewMatrix.lookAt(m_cameraOrigin, m_cameraOrigin + m_cameraFwd, QVector3D(0, 0, 1)); QMatrix4x4 MVP = projectionMatrix * viewMatrix * modelMatrix; // wireframe if (m_showTris) { m_program_wireframe->bind(); m_program_wireframe->setUniformValue(m_program_wireframe_mvp_location, MVP); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); for (auto &draw : m_drawcalls) { glDrawElements(GL_TRIANGLES, draw.index_count, GL_UNSIGNED_INT, reinterpret_cast(draw.first_index * sizeof(uint32_t))); } glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); m_program_wireframe->release(); } m_program->bind(); m_program->setUniformValue(m_program_mvp_location, MVP); m_program->setUniformValue(m_program_texture_sampler_location, 0 /* texture unit */); m_program->setUniformValue(m_program_lightmap_sampler_location, 1 /* texture unit */); m_program->setUniformValue(m_program_opacity_location, 1.0f); m_program->setUniformValue(m_program_lightmap_only_location, m_lighmapOnly); m_program->setUniformValue(m_program_fullbright_location, m_fullbright); m_program->setUniformValue(m_program_drawnormals_location, m_drawNormals); m_program->setUniformValue(m_program_showtris_location, m_showTris); m_program->setUniformValue(m_program_drawflat_location, m_drawFlat); // opaque draws for (auto &draw : m_drawcalls) { if (draw.opacity != 1.0f) continue; draw.texture->bind(0 /* texture unit */); lightmap_texture->bind(1 /* texture unit */); QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); glDrawElements(GL_TRIANGLES, draw.index_count, GL_UNSIGNED_INT, reinterpret_cast(draw.first_index * sizeof(uint32_t))); } // translucent draws { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); for (auto &draw : m_drawcalls) { if (draw.opacity == 1.0f) continue; draw.texture->bind(0 /* texture unit */); lightmap_texture->bind(1 /* texture unit */); m_program->setUniformValue(m_program_opacity_location, draw.opacity); QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); glDrawElements(GL_TRIANGLES, draw.index_count, GL_UNSIGNED_INT, reinterpret_cast(draw.first_index * sizeof(uint32_t))); } glDisable(GL_BLEND); } m_program->release(); } void GLView::setCamera(const qvec3d &origin, const qvec3d &fwd) { m_cameraOrigin = {(float)origin[0], (float)origin[1], (float)origin[2]}; m_cameraFwd = {(float)fwd[0], (float)fwd[1], (float)fwd[2]}; } void GLView::setLighmapOnly(bool lighmapOnly) { m_lighmapOnly = lighmapOnly; update(); } void GLView::setFullbright(bool fullbright) { m_fullbright = fullbright; update(); } void GLView::setDrawNormals(bool drawnormals) { m_drawNormals = drawnormals; update(); } void GLView::setShowTris(bool showtris) { m_showTris = showtris; update(); } void GLView::setDrawFlat(bool drawflat) { m_drawFlat = drawflat; update(); } void GLView::setKeepOrigin(bool keeporigin) { m_keepOrigin = keeporigin; } void GLView::takeScreenshot(QString destPath, int w, int h) { // update aspect ratio float backupDisplayAspect = m_displayAspect; m_displayAspect = static_cast(w) / static_cast(h); makeCurrent(); { QOpenGLFramebufferObjectFormat format; format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); format.setSamples(4); QOpenGLFramebufferObject fbo(w, h, format); assert(fbo.bind()); glViewport(0, 0, w, h); paintGL(); QImage image = fbo.toImage(); image.save(destPath); assert(fbo.release()); } doneCurrent(); // restore aspect ratio m_displayAspect = backupDisplayAspect; update(); } void GLView::renderBSP(const QString &file, const mbsp_t &bsp, const bspxentries_t &bspx, const std::vector &entities, const settings::common_settings &settings) { img::load_textures(&bsp, settings); // build lightmap atlas auto atlas = build_lightmap_atlas(bsp, bspx, false, true); auto facenormals = BSPX_FaceNormals(bsp, bspx); // NOTE: according to https://doc.qt.io/qt-6/qopenglwidget.html#resource-initialization-and-cleanup // we can only do this after `initializeGL()` has run once. makeCurrent(); // clear old data lightmap_texture.reset(); m_drawcalls.clear(); m_vbo.allocate(0); m_indexBuffer.allocate(0); // upload lightmap atlas { const auto &lm_tex = atlas.style_to_lightmap_atlas.at(0); lightmap_texture = std::make_unique(QImage(reinterpret_cast(lm_tex.pixels.data()), lm_tex.width, lm_tex.height, QImage::Format_RGBA8888)); lightmap_texture->setAutoMipMapGenerationEnabled(false); lightmap_texture->setMagnificationFilter(QOpenGLTexture::Linear); lightmap_texture->setMinificationFilter(QOpenGLTexture::Linear); } // this determines what can be batched together in a draw call struct material_key { std::string texname; float opacity; auto as_tuple() const { return std::make_tuple(texname, opacity); } bool operator<(const material_key &other) const { return as_tuple() < other.as_tuple(); } }; struct face_payload { const mface_t *face; qvec3d model_offset; }; // collect faces grouped by material_key std::map> faces_by_material_key; // collect entity bmodels for (int mi = 0; mi < bsp.dmodels.size(); mi++) { qvec3d origin{}; if (mi != 0) { // find matching entity std::string modelStr = fmt::format("*{}", mi); bool found = false; for (auto &ent : entities) { if (ent.get("model") == modelStr) { found = true; ent.get_vector("origin", origin); break; } } if (!found) continue; } auto &m = bsp.dmodels[mi]; for (int i = m.firstface; i < m.firstface + m.numfaces; ++i) { auto &f = bsp.dfaces[i]; std::string t = Face_TextureName(&bsp, &f); // FIXME: keep empty texture names? if (t.empty()) continue; if (f.numedges < 3) continue; const mtexinfo_t *texinfo = Face_Texinfo(&bsp, &f); if (!texinfo) continue; // FIXME: render as checkerboard? // determine opacity float opacity = 1.0f; if (bsp.loadversion->game->id == GAME_QUAKE_II) { if (texinfo->flags.native & (Q2_SURF_NODRAW | Q2_SURF_SKY)) { continue; } if (texinfo->flags.native & Q2_SURF_TRANS33) { opacity = 0.33f; } if (texinfo->flags.native & Q2_SURF_TRANS66) { opacity = 0.66f; } } material_key k = {.texname = t, .opacity = opacity}; faces_by_material_key[k].push_back({.face = &f, .model_offset = origin}); } } // populate the vertex/index buffers struct vertex_t { qvec3f pos; qvec2f uv; qvec2f lightmap_uv; qvec3f normal; qvec3f flat_color; }; std::vector verts; std::vector indexBuffer; for (const auto &[k, faces] : faces_by_material_key) { // upload texture // FIXME: we should have a separate lightpreview_options auto *texture = img::find(k.texname); if (!texture) { logging::print("warning, couldn't locate {}", k.texname); continue; } std::unique_ptr qtexture = std::make_unique(QImage(reinterpret_cast(texture->pixels.data()), texture->width, texture->height, QImage::Format_RGBA8888)); qtexture->setMaximumAnisotropy(16); qtexture->setAutoMipMapGenerationEnabled(true); const size_t dc_first_index = indexBuffer.size(); for (const auto &[f, model_offset] : faces) { const int fnum = Face_GetNum(&bsp, f); const auto plane_normal = Face_Normal(&bsp, f); const qvec3f flat_color = qvec3f{Random(), Random(), Random()}; const size_t first_vertex_of_face = verts.size(); const auto lm_uvs = atlas.facenum_to_lightmap_uvs.at(fnum); // output a vertex for each vertex of the face for (int j = 0; j < f->numedges; ++j) { qvec3f pos = Face_PointAtIndex(&bsp, f, j); qvec2f uv = Face_WorldToTexCoord(&bsp, f, pos); uv[0] *= (1.0 / texture->width); uv[1] *= (1.0 / texture->height); qvec2f lightmap_uv = lm_uvs.at(j); qvec3f vertex_normal; if (facenormals) { auto normal_index = facenormals->per_face[fnum].per_vert[j].normal; vertex_normal = facenormals->normals[normal_index]; } else { vertex_normal = plane_normal; } verts.push_back({.pos = pos + model_offset, .uv = uv, .lightmap_uv = lightmap_uv, .normal = vertex_normal, .flat_color = flat_color}); } // output the vertex indices for this face for (int j = 2; j < f->numedges; ++j) { indexBuffer.push_back(first_vertex_of_face); indexBuffer.push_back(first_vertex_of_face + j - 1); indexBuffer.push_back(first_vertex_of_face + j); } } const size_t dc_index_count = indexBuffer.size() - dc_first_index; drawcall_t dc = {.opacity = k.opacity, .texture = std::move(qtexture), .first_index = dc_first_index, .index_count = dc_index_count}; m_drawcalls.push_back(std::move(dc)); } QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); // upload index buffer m_indexBuffer.create(); m_indexBuffer.bind(); m_indexBuffer.allocate(indexBuffer.data(), indexBuffer.size() * sizeof(indexBuffer[0])); // upload vertex buffer m_vbo.create(); m_vbo.bind(); m_vbo.allocate(verts.data(), verts.size() * sizeof(verts[0])); // positions glEnableVertexAttribArray(0 /* attrib */); glVertexAttribPointer(0 /* attrib */, 3, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, pos)); // texture uvs glEnableVertexAttribArray(1 /* attrib */); glVertexAttribPointer(1 /* attrib */, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, uv)); // lightmap uvs glEnableVertexAttribArray(2 /* attrib */); glVertexAttribPointer( 2 /* attrib */, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, lightmap_uv)); // normals glEnableVertexAttribArray(3 /* attrib */); glVertexAttribPointer(3 /* attrib */, 3, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, normal)); // flat shading color glEnableVertexAttribArray(4 /* attrib */); glVertexAttribPointer( 4 /* attrib */, 3, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, flat_color)); doneCurrent(); // schedule repaint update(); } void GLView::resizeGL(int width, int height) { m_displayAspect = static_cast(width) / static_cast(height); } void GLView::mousePressEvent(QMouseEvent *event) { m_lastMouseDownPos = event->screenPos(); } void GLView::mouseMoveEvent(QMouseEvent *event) { if (!(event->buttons() & Qt::RightButton)) return; QPointF delta = event->screenPos() - m_lastMouseDownPos; m_lastMouseDownPos = event->screenPos(); // handle mouse movement float pitchDegrees = delta.y() * -0.2; float yawDegrees = delta.x() * -0.2; QMatrix4x4 mouseRotation; mouseRotation.rotate(pitchDegrees, cameraRight()); mouseRotation.rotate(yawDegrees, QVector3D(0, 0, 1)); // now rotate m_cameraFwd and m_cameraUp by mouseRotation m_cameraFwd = mouseRotation * m_cameraFwd; update(); } static keys_t Qt_Key_To_keys_t(int key) { switch (key) { case Qt::Key_W: return keys_t::up; case Qt::Key_A: return keys_t::left; case Qt::Key_S: return keys_t::down; case Qt::Key_D: return keys_t::right; case Qt::Key_Q: return keys_t::fly_down; case Qt::Key_E: return keys_t::fly_up; } return keys_t::none; } void GLView::startMovementTimer() { if (m_keymoveUpdateTimer) return; m_lastKeymoveFrame = I_FloatTime(); m_keymoveUpdateTimer = startTimer(1, Qt::PreciseTimer); // repaint timer, calls timerEvent() } void GLView::stopMovementTimer() { if (m_keymoveUpdateTimer != 0) { killTimer(m_keymoveUpdateTimer); m_keymoveUpdateTimer = 0; } } void GLView::keyPressEvent(QKeyEvent *event) { keys_t key = Qt_Key_To_keys_t(event->key()); m_keysPressed |= static_cast(key); startMovementTimer(); } void GLView::keyReleaseEvent(QKeyEvent *event) { keys_t key = Qt_Key_To_keys_t(event->key()); m_keysPressed &= ~static_cast(key); if (!m_keysPressed) stopMovementTimer(); } void GLView::wheelEvent(QWheelEvent *event) { if (!(event->buttons() & Qt::RightButton)) return; double delta = event->angleDelta().y(); m_moveSpeed += delta; m_moveSpeed = clamp(m_moveSpeed, 10.0f, 5000.0f); } void GLView::timerEvent(QTimerEvent *event) { // update frame time auto current_time = I_FloatTime(); auto duration = current_time - m_lastKeymoveFrame; m_lastKeymoveFrame = current_time; // qDebug() << "timer event: duration: " << duration.count(); const float distance = m_moveSpeed * duration.count(); if (m_keysPressed & static_cast(keys_t::up)) m_cameraOrigin += m_cameraFwd * distance; if (m_keysPressed & static_cast(keys_t::down)) m_cameraOrigin -= m_cameraFwd * distance; if (m_keysPressed & static_cast(keys_t::left)) m_cameraOrigin -= cameraRight() * distance; if (m_keysPressed & static_cast(keys_t::right)) m_cameraOrigin += cameraRight() * distance; if (m_keysPressed & static_cast(keys_t::fly_down)) m_cameraOrigin -= QVector3D(0, 0, 1) * distance; if (m_keysPressed & static_cast(keys_t::fly_up)) m_cameraOrigin += QVector3D(0, 0, 1) * distance; update(); // schedule a repaint }