-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Gui Stencil Mask Optimization #1740
base: master
Are you sure you want to change the base?
Conversation
Hey thanks for contributing again :) Unfortunately, I think the solution you made clashes a bit with our design of the renderer. In general, the renderer code is separated into two levels (see our renderer docs for details). The (low-level) level 1 interface handles the bare-metal stuff, e.g. interfacing with OpenGL to talk to the GPU, and should generally be inaccessible to other parts of the engine. Instead, the rest of the engine mainly interacts with the level 2 interface of the renderer which abstracts the low-level stuff away and implements systems that can be used for gameplay, e.g. playing sprite animations. The reason we have this level structure is mostly thread-safety, but also easier maintainance, since we can change or replace the level 1 implementaton without changing the entire engine. The latter is important for offering multiple renderer backends simultaneously to the user, e.g. OpenGL and Vulkan. In your solution, there would be "bare-metal" OpenGL calls in the presenter, even though the presenter is technically part of the level 2 interface. If we do it like this, then the presenter does not work with a level 1 Vulkan renderer backend anymore, for example. Ideally, the presenter does not have to care what backend we initialize the renderer with, whether it's OpenGL or Vulkan. |
For solving this task properly, we should try to add stencil tests as a more general feature to the level 1 renderer first. If we want to activate it for the GUI, the stencil test write code should be moved into the GUI subsystem. To use the stencil tests in the other render passes, we could add settings to the So in summary my thoughts would be:
|
Apologies btw for askng for so many redesigns 😄 For issues that are not labelled "good first issue", the issue description can sometimes be a bit vague because we are ourselves not sure of what we want as a solution yet. You can always ask me for where to look in the docs before you do another issue like this one, maybe that helps :) |
Thanks a lot for the detailed explanation! It all makes sense now. I’ll ensure to separate level 1 and level 2 as suggested and adjust my solution to follow this structure. I really appreciate your guidance! |
d63438c
to
18622d4
Compare
Here are updates:
|
eb58af1
to
7eca3b5
Compare
Finally had time to look at this 😄 I think we are getting there, but after looking at your code, I'm pretty sure that your stencil tests currently won't work. It's hard to test since we currently have deactivated the GUI, but here are my reasons:
I would recommend that you don't implement this functionality in the presenter yet. A renderer demo is much better suited and allows you to test the level 1 implementation without having to worry about level 2 abstractions logic. |
// set passes indices | ||
this->index_gui_stencil_pass = this->render_passes.size() - 3; | ||
this->index_gui_render_pass = this->render_passes.size() - 2; | ||
this->index_final_render_pass = this->render_passes.size() - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding these indices makes the code much harder to maintain because we can't add new render passes between GUI and final pass. Ideally, the solution should work without any index assignment.
auto stencil_pass = this->gui->get_stencil_pass(); | ||
this->render_passes.push_back(stencil_pass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why we need an additional render pass just for stencil testing. If you want to write stencil values, you can use the normal GUI render pass and activate stencil writing there. But as I pointed out in my main comment, you can't even read from the stencil values unless you are using the GUI framebuffer.
If you want the world stage render pass to use stencil testing, you would first have to write to its own framebuffer's stencil buffer and then afterwards do stencil testing. As I said before, you have to transfer the stencil buffer state from one framebuffer to the other. This can probably be done by rebinding the stencil attachement from the GUI framebuffer to the other framebuffers (via GL_STENCIL_ATTACHEMENT
).
this->gui->render(); | ||
this->renderer->render(this->render_passes[this->index_gui_stencil_pass]); | ||
|
||
for (auto &pass : this->render_passes) { | ||
this->renderer->render(pass); | ||
for (size_t i = 0; i < this->index_gui_stencil_pass; ++i) { | ||
this->renderer->render(this->render_passes[i]); | ||
} | ||
|
||
this->gui->render(); | ||
this->renderer->render(this->render_passes[this->index_gui_render_pass]); | ||
|
||
this->renderer->render(this->render_passes[this->index_final_render_pass]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GUI is now rendered twice, which is not what we want if the goal is performance ;)
Also, code like this needs comment because otherwise it becomes very confusing to understand why the order of render passes is structured this way.
@@ -98,6 +102,10 @@ void GUI::initialize_render_pass(size_t width, | |||
// TODO: Rendering into the FBO is a bit redundant right now because we | |||
// just copy the GUI texture into the output texture | |||
this->render_pass = renderer->add_render_pass({display_obj}, fbo); | |||
|
|||
auto stencil_pass = renderer->add_render_pass({display_obj}, fbo); | |||
stencil_pass->set_stencil_state(StencilState::WRITE_STENCIL_MASK); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if you just set the stencil state for this->render_pass
, you would effectively have the same result without the need for an additional render pass.
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); | ||
glDepthMask(GL_FALSE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary? Running these two statement would mean that the only buffer you write to is the stencil buffer. However, I don't see a reason why we shouldn't be able to draw into the color and depth buffer simultaneously.
// ensure that an (empty) VAO is bound before rendering geometry | ||
// a bound VAO is required to render bufferless quad geometries by OpenGL | ||
// see https://www.khronos.org/opengl/wiki/Vertex_Rendering#Causes_of_rendering_failure | ||
shared_quad_vao->bind(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This duplicates a statement + comment from further above. Please remove it.
switch (layer.stencil_state) { | ||
case StencilState::WRITE_STENCIL_MASK: | ||
setupStencilWrite(); | ||
break; | ||
|
||
case StencilState::USE_STENCIL_TEST: | ||
setupStencilTest(); | ||
break; | ||
|
||
case StencilState::DISABLE_STENCIL: | ||
disableStencilTest(); | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing stencil testing per layer is a bit overkill for now IMO, but it's a good idea :)
We have to make sure that the draw order is correct and should make performance tests for this.
void setupStencilWrite(); | ||
|
||
void setupStencilTest(); | ||
|
||
void disableStencilTest(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These functions should use snake case naming, e.g. setup_stencil_write
, to adhere to our code style conventions.
/** | ||
* Stencil states for the render pass. | ||
*/ | ||
enum class StencilState { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here, enums should use snake case naming
/// State for writing GUI elements to stencil buffer. | ||
WRITE_STENCIL_MASK, | ||
/// State for using the mask when rendering scene. | ||
USE_STENCIL_TEST, | ||
/// State for normal rendering (GUI rendering). | ||
DISABLE_STENCIL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These docstrings refer to the GUI, but stencil testing should not just be used for this purpose. So it's better to make the docstrings more agnostic.
Oh.. I was too focused on implementing the stencil test functions and completely missed the fundamentals of how framebuffers work in OpenGL. I'll start fresh with a renderer demo this week to re-write and test the core functionality properly. Thanks! |
Resolve #1538
Changes:
Presenter::render()
to use three-pass rendering: