2021SC@SDUSC
catalogue
In the previous article, we roughly combed some code related to graphics rendering. In this section, we will take a look at how the game engine presents such a window we see.
1.Driver
The Driver class creates an OpenGL environment for us to display windows.
class Driver { public: Driver(const Settings::DriverSettings& p_driverSettings); ~Driver() = default; bool IsActive() const; private: void InitGlew(); static void __stdcall GLDebugMessageCallback(uint32_t source, uint32_t type, uint32_t id, uint32_t severity, int32_t length, const char* message, const void* userParam); private: bool m_isActive; };
1.1 constructor
First, its constructor needs such a structure type:
struct DriverSettings { bool debugMode = false; };
This mainly implements the encapsulation of OpenGL built-in functions. First, use the InitGlew function to initialize a glew window and activate the state M_ Value isactive to true, and then judge P_ debugMode of driversettings.
If the current mode is debug, use the glGetIntegerv function to obtain the current environment tag flag and compare the flag with GL_CONTEXT_FLAG_DEBUG_BIT performs bitwise fetch and. If the result is consistent (i.e. the result is true), the next four functions will be called, and their specific functions will not be described in detail. In short, an environment suitable for debugging will be built.
OvRendering::Context::Driver::Driver(const Settings::DriverSettings& p_driverSettings) { InitGlew(); m_isActive = true; if (p_driverSettings.debugMode) { GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags); if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) { glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageCallback(GLDebugMessageCallback, nullptr); glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); } } glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glCullFace(GL_BACK); }
Finally, glBlendFunc sets the blending mode to alpha value blending, and glCullFace sets face culling to back culling, which will make the translucent objects blend according to the alpha value, and will no longer render the faces in the mesh opposite to the normal vector.
1.2InitGlew
The InitGlew function is used in the early constructor, which calls the OpenGL built-in glewInit function initialization window and makes error detection, if the value returned by glewInit is not equal to GLEW_. OK, the system will send an error message.
void OvRendering::Context::Driver::InitGlew() { const GLenum error = glewInit(); if (error != GLEW_OK) { std::string message = "Error Init GLEW: "; std::string glewError = reinterpret_cast<const char*>(glewGetErrorString(error)); OVLOG_INFO(message + glewError); } }
1.3GLDebugMessageCallback
The gldebugmessagecallback function also appears in the constructor. As the first parameter of the function gldebugmessagecallback, it is the implementation of the specific function of gldebugmessagecallback and will feed back the specific error information.
At the beginning, we will selectively ignore some warnings and errors:
void OvRendering::Context::Driver::GLDebugMessageCallback(uint32_t source, uint32_t type, uint32_t id, uint32_t severity, int32_t length, const char* message, const void* userParam) { // ignore non-significant error/warning codes if (id == 131169 || id == 131185 || id == 131218 || id == 131204) return;
After that, we need to set the format of the feedback information, use the string output to store the information, and indicate the ID (error ID), source (where the error occurred), type (error type), severity (error criticality) and other information.
std::string output; output += "OpenGL Debug Message:\n"; output += "Debug message (" + std::to_string(id) + "): " + message + "\n"; switch (source) { case GL_DEBUG_SOURCE_API: output += "Source: API"; break; case GL_DEBUG_SOURCE_WINDOW_SYSTEM: output += "Source: Window System"; break; case GL_DEBUG_SOURCE_SHADER_COMPILER: output += "Source: Shader Compiler"; break; case GL_DEBUG_SOURCE_THIRD_PARTY: output += "Source: Third Party"; break; case GL_DEBUG_SOURCE_APPLICATION: output += "Source: Application"; break; case GL_DEBUG_SOURCE_OTHER: output += "Source: Other"; break; } output += "\n"; switch (type) { case GL_DEBUG_TYPE_ERROR: output += "Type: Error"; break; case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: output += "Type: Deprecated Behaviour"; break; case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: output += "Type: Undefined Behaviour"; break; case GL_DEBUG_TYPE_PORTABILITY: output += "Type: Portability"; break; case GL_DEBUG_TYPE_PERFORMANCE: output += "Type: Performance"; break; case GL_DEBUG_TYPE_MARKER: output += "Type: Marker"; break; case GL_DEBUG_TYPE_PUSH_GROUP: output += "Type: Push Group"; break; case GL_DEBUG_TYPE_POP_GROUP: output += "Type: Pop Group"; break; case GL_DEBUG_TYPE_OTHER: output += "Type: Other"; break; } output += "\n"; switch (severity) { case GL_DEBUG_SEVERITY_HIGH: output += "Severity: High"; break; case GL_DEBUG_SEVERITY_MEDIUM: output += "Severity: Medium"; break; case GL_DEBUG_SEVERITY_LOW: output += "Severity: Low"; break; case GL_DEBUG_SEVERITY_NOTIFICATION: output += "Severity: Notification"; break; }
Finally, call the corresponding report macro according to different crisis levels (see OvDebug function library for details related to information feedback)
switch (severity) { case GL_DEBUG_SEVERITY_HIGH: OVLOG_ERROR(output); break; case GL_DEBUG_SEVERITY_MEDIUM: OVLOG_WARNING(output); break; case GL_DEBUG_SEVERITY_LOW: OVLOG_INFO(output); break; case GL_DEBUG_SEVERITY_NOTIFICATION: OVLOG_INFO(output); break; } }
2.Renderer
The Renderer class is a function class that collects a variety of tools. As can be seen from its header file, its content has a lot of repeated similar work, mostly for the encapsulation of various special functions in OpenGL, so we only pick up the important ones here.
#include <optional> #include "OvRendering/Context/Driver.h" #include "OvRendering/LowRenderer/Camera.h" #include "OvRendering/Resources/Shader.h" #include "OvRendering/Resources/Model.h" #include "OvRendering/Settings/ERenderingCapability.h" #include "OvRendering/Settings/EPrimitiveMode.h" #include "OvRendering/Settings/ERasterizationMode.h" #include "OvRendering/Settings/EComparaisonAlgorithm.h" #include "OvRendering/Settings/EOperation.h" #include "OvRendering/Settings/ECullFace.h" #include "OvRendering/Settings/ECullingOptions.h" #include "OvRendering/Settings/EPixelDataFormat.h" #include "OvRendering/Settings/EPixelDataType.h"
The Renderer contains several variables, m_driver, m_frameInfo, m_state.
struct FrameInfo { uint64_t batchCount = 0; uint64_t instanceCount = 0; uint64_t polyCount = 0; }; private: Context::Driver& m_driver; FrameInfo m_frameInfo; uint8_t m_state;
2.1Draw
The function of Draw is to Draw the mesh model. The parameters passed in are p_mesh (grid information), p_primitiveMode (mode of drawing grid), p_instances (number of drawing instances).
First, judge that the number of polygons is greater than 0. Each time this function is called, the batch count is increased by 1, and the total number of instances drawn is instanceCount plus p_instances, the total number of polygons drawn plus (number of vertex indexes / 3 * number of instances drawn this time).
Next, call p_mesh binding function, and judge whether to use VBO or EBO for this painting according to the number of vertex indexes. Call different functions according to different painting methods:
Call glDrawArrays when using VBO; glDrawElements is called when EBO is used.
VBO and EBO have been mentioned earlier. See [Overload game engine] source code analysis 4: OvRendering function library (2).
When the instance to be drawn is greater than 1, we can also use gldrawelementsinstanced / gldrawarrayinstanced for batch drawing. We only need to pass in multiple parameters p at the end_ Instances.
void OvRendering::Core::Renderer::Draw(Resources::IMesh& p_mesh, Settings::EPrimitiveMode p_primitiveMode, uint32_t p_instances) { if (p_instances > 0) { ++m_frameInfo.batchCount; m_frameInfo.instanceCount += p_instances; m_frameInfo.polyCount += (p_mesh.GetIndexCount() / 3) * p_instances; p_mesh.Bind(); if (p_mesh.GetIndexCount() > 0) { /* With EBO */ if (p_instances == 1) glDrawElements(static_cast<GLenum>(p_primitiveMode), p_mesh.GetIndexCount(), GL_UNSIGNED_INT, nullptr); else glDrawElementsInstanced(static_cast<GLenum>(p_primitiveMode), p_mesh.GetIndexCount(), GL_UNSIGNED_INT, nullptr, p_instances); } else { /* Without EBO */ if (p_instances == 1) glDrawArrays(static_cast<GLenum>(p_primitiveMode), 0, p_mesh.GetVertexCount()); else glDrawArraysInstanced(static_cast<GLenum>(p_primitiveMode), 0, p_mesh.GetVertexCount(), p_instances); } p_mesh.Unbind(); } }
After you finish drawing, you also call P_ Unbound function of mesh.
2.2FetchGLState
The FetchGLState function returns various state information of the current rendering mode, such as whether the RGBA buffer can be modified, whether the depth buffer can be modified, whether the mixing mode is enabled, whether the face culling is enabled, whether the depth test is started, etc.
The following code first defines an 8bit integer variable result to store the last state.
Then use the glgetboolean V function to_ COLOR_ Writemask writes the buffer status of the four color channels into cMask, judges the above modes at the same time, and performs bitwise OR operation between the result and the corresponding binary number.
Finally, if the face culling mode is started, judge the type of face culling and perform the same operation on result.
uint8_t OvRendering::Core::Renderer::FetchGLState() { using namespace OvRendering::Settings; uint8_t result = 0; GLboolean cMask[4]; glGetBooleanv(GL_COLOR_WRITEMASK, cMask); if (GetBool(GL_DEPTH_WRITEMASK)) result |= 0b0000'0001; if (cMask[0]) result |= 0b0000'0010; if (GetCapability(ERenderingCapability::BLEND)) result |= 0b0000'0100; if (GetCapability(ERenderingCapability::CULL_FACE)) result |= 0b0000'1000; if (GetCapability(ERenderingCapability::DEPTH_TEST)) result |= 0b0001'0000; switch (static_cast<ECullFace>(GetInt(GL_CULL_FACE))) { case OvRendering::Settings::ECullFace::BACK: result |= 0b0010'0000; break; case OvRendering::Settings::ECullFace::FRONT: result |= 0b0100'0000; break; case OvRendering::Settings::ECullFace::FRONT_AND_BACK: result |= 0b0110'0000; break; } return result; }
Through the coding methods of various modes, we can see that in order to store the state information of each mode, the function allocates a separate bit to store the information, so that 1 of any two modes is in different bit bits; Since there can only be one of the three modes of face removal, we give it two bit bits, and 1 is not required to be in different bit bits.
2.3ApplyStateMask
The ApplyStateMask function will take effect when the mode state changes.
When the past state is inconsistent with the current state, enter the if judgment of various modes, bitwise and operate the state variable with the decoding number of a mode, so that only the state information of a single mode can be left, and then compare the front and rear states. When the states are inconsistent, update the state information of the corresponding mode.
void OvRendering::Core::Renderer::ApplyStateMask(uint8_t p_mask) { if (p_mask != m_state) { using namespace OvRendering::Settings; if ((p_mask & 0x01) != (m_state & 0x01)) SetDepthWriting(p_mask & 0x01); if ((p_mask & 0x02) != (m_state & 0x02)) SetColorWriting(p_mask & 0x02); if ((p_mask & 0x04) != (m_state & 0x04)) SetCapability(ERenderingCapability::BLEND, p_mask & 0x04); if ((p_mask & 0x08) != (m_state & 0x08)) SetCapability(ERenderingCapability::CULL_FACE, p_mask & 0x8); if ((p_mask & 0x10) != (m_state & 0x10)) SetCapability(ERenderingCapability::DEPTH_TEST, p_mask & 0x10); if ((p_mask & 0x08) && ((p_mask & 0x20) != (m_state & 0x20) || (p_mask & 0x40) != (m_state & 0x40))) { int backBit = p_mask & 0x20; int frontBit = p_mask & 0x40; SetCullFace(backBit && frontBit ? ECullFace::FRONT_AND_BACK : (backBit ? ECullFace::BACK : ECullFace::FRONT)); } m_state = p_mask; } }
Similarly, when face culling is on, judge whether the three face culling modes have changed. Since the FRONT_AND_BACK is 1 on 0x20 and 0x40, it will be naturally classified into the judgment of FRONT and BACK.
In the determination of the opposite culling mode, two-layer ternary operators are used to return the result. When both backBit and frontBit are true, it means that the current mode is FRONT_AND_BACK, otherwise the backBit will be judged separately.
Finally, you need to update M_ The value of state.
That's all for this section.