From 926a2f1e6ed8b4f9c1087d50f0b4f704be46815b Mon Sep 17 00:00:00 2001 From: Lemiort Date: Sun, 25 Oct 2026 10:48:50 +0100 Subject: [PATCH] Preserve deferred instance materials --- include/DeferredPipeline.hpp | 2 - include/Main.hpp | 2 +- include/Material.hpp | 1 + include/Mesh.hpp | 3 +- include/MeshInstanceRenderer.hpp | 6 +- src/DeferredPipeline.cpp | 3 +- src/Material.cpp | 23 +++++++ src/Mesh.cpp | 8 ++- src/MeshInstanceRenderer.cpp | 23 +++---- src/main.cpp | 6 -- tests/OpenGLRenderTests.cpp | 109 +++++++++++++++++++++++++++++++ 11 files changed, 156 insertions(+), 30 deletions(-) diff --git a/include/DeferredPipeline.hpp b/include/DeferredPipeline.hpp index 85dd166..07dc31d 100644 --- a/include/DeferredPipeline.hpp +++ b/include/DeferredPipeline.hpp @@ -22,7 +22,6 @@ struct DeferredPipelineResources { gsl_lite::not_null> pointLightShader; gsl_lite::not_null> spotLightShader; gsl_lite::not_null> directionalLightShader; - gsl_lite::not_null> geometryMaterial; gsl_lite::not_null> stencilMaterial; gsl_lite::not_null> pointLightMaterial; gsl_lite::not_null> spotLightMaterial; @@ -56,7 +55,6 @@ class DeferredPipeline { MeshShaderBindings stencilBindings_; MeshShaderBindings pointLightBindings_; MeshShaderBindings spotLightBindings_; - gsl_lite::not_null> geometryMaterial_; gsl_lite::not_null> stencilMaterial_; gsl_lite::not_null> pointLightMaterial_; gsl_lite::not_null> spotLightMaterial_; diff --git a/include/Main.hpp b/include/Main.hpp index f85f919..a9b636e 100644 --- a/include/Main.hpp +++ b/include/Main.hpp @@ -86,7 +86,7 @@ class Application { std::shared_ptr DSDirectionalLightShader, DSSpotLightShader, DSStencilPassShader; std::shared_ptr mainMaterial; - std::shared_ptr DSGeometryPassMaterial, DSPointLightMaterial; + std::shared_ptr DSPointLightMaterial; std::shared_ptr DSDirectionalLightMaterial, DSSpotLightMaterial, DSStencilPassMaterial; std::shared_ptr gBuffer1; diff --git a/include/Material.hpp b/include/Material.hpp index bc54c46..6d1fdcf 100644 --- a/include/Material.hpp +++ b/include/Material.hpp @@ -14,6 +14,7 @@ class Material { explicit Material(gsl_lite::not_null> shader); virtual ~Material(); void Use(float drawOpacity = 1.0F) const; + void UseWithShader(Shader& shader, float drawOpacity = 1.0F) const; void SetColorTexture(std::shared_ptr _colorMap); void SetNormalTexture(std::shared_ptr _normalMap); void SetSpecularTexture(std::shared_ptr _specularMap); diff --git a/include/Mesh.hpp b/include/Mesh.hpp index 9f9b72c..3c3be91 100644 --- a/include/Mesh.hpp +++ b/include/Mesh.hpp @@ -50,7 +50,8 @@ class Mesh : public MaterialObject, public Transformable { void Render(const Camera& cam) override; void Draw(const Transform& transform, const Material& material, const MeshShaderBindings& bindings, float opacity = 1.0F, - GLuint instanceBuffer = 0, GLsizei instanceCount = 1) const; + GLuint instanceBuffer = 0, GLsizei instanceCount = 1, + Shader* passShader = nullptr) const; private: int faceCount_{}; diff --git a/include/MeshInstanceRenderer.hpp b/include/MeshInstanceRenderer.hpp index 95445ac..cfc0125 100644 --- a/include/MeshInstanceRenderer.hpp +++ b/include/MeshInstanceRenderer.hpp @@ -16,11 +16,11 @@ class MeshInstanceRenderer { MeshInstanceRenderer(const MeshInstanceRenderer&) = delete; MeshInstanceRenderer& operator=(const MeshInstanceRenderer&) = delete; - // A pass material overrides all instance materials (e.g. the G-buffer - // pass). + // A pass shader overrides shader selection while each command retains its + // surface material and textures. void Render(std::span commands, const MeshShaderBindings& bindings, - const Material* passMaterial = nullptr); + Shader* passShader = nullptr); private: struct InstanceData { diff --git a/src/DeferredPipeline.cpp b/src/DeferredPipeline.cpp index 5465ffb..34695da 100644 --- a/src/DeferredPipeline.cpp +++ b/src/DeferredPipeline.cpp @@ -56,7 +56,6 @@ DeferredPipeline::DeferredPipeline(DeferredPipelineResources resources) stencilBindings_(MeshShaderBindings::FromShader(*stencilShader_)), pointLightBindings_(MeshShaderBindings::FromShader(*pointLightShader_)), spotLightBindings_(MeshShaderBindings::FromShader(*spotLightShader_)), - geometryMaterial_(std::move(resources.geometryMaterial)), stencilMaterial_(std::move(resources.stencilMaterial)), pointLightMaterial_(std::move(resources.pointLightMaterial)), spotLightMaterial_(std::move(resources.spotLightMaterial)), @@ -112,7 +111,7 @@ void DeferredPipeline::GeometryPass(const SceneView& scene) { glm::value_ptr(matrix)); instanceRenderer_.Render(scene.queue.GetOpaqueMeshes(), geometryBindings_, - geometryMaterial_.get()); + geometryShader_.get()); glDepthMask(GL_FALSE); glDisable(GL_DEPTH_TEST); } diff --git a/src/Material.cpp b/src/Material.cpp index cee8846..d8470e7 100644 --- a/src/Material.cpp +++ b/src/Material.cpp @@ -122,6 +122,29 @@ void Material::Use(float drawOpacity) const { } } +void Material::UseWithShader(Shader& shader, float drawOpacity) const { + shader.Use(); + colorMap->Bind(GL_TEXTURE0); + glUniform1i(shader.GetUniformLocation("colTexSampler"), 0); + normalMap->Bind(GL_TEXTURE1); + glUniform1i(shader.GetUniformLocation("normTexSampler"), 1); + specularMap->Bind(GL_TEXTURE2); + glUniform1i(shader.GetUniformLocation("specTexSampler"), 2); + shadowMap->Bind(GL_TEXTURE3); + glUniform1i(shader.GetUniformLocation("shadowTexSampler"), 3); + glUniform1f(shader.GetUniformLocation("opacity"), drawOpacity); + + for (GLint unit = 4; unit < max_texture_units; ++unit) { + const auto textureIndex = static_cast(unit); + if (texturesID.at(textureIndex) == 0) continue; + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(GL_TEXTURE_2D, texturesID.at(textureIndex)); + glUniform1i( + shader.GetUniformLocation("gSampler" + ConvertToString(unit)), + unit); + } +} + void Material::SetColorTexture(std::shared_ptr _colorMap) { colorMap = std::move(_colorMap); } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fab476e..ab83b10 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -173,11 +173,15 @@ void Mesh::Render(const Camera& /*cam*/) { void Mesh::Draw(const Transform& transform, const Material& material, const MeshShaderBindings& bindings, float opacity, - GLuint instanceBuffer, GLsizei instanceCount) const { + GLuint instanceBuffer, GLsizei instanceCount, + Shader* passShader) const { const glm::mat4 modelMatrix = transform.ModelMatrix(); const glm::mat4 normalMatrix = transform.NormalMatrix(); - material.Use(opacity); + if (passShader != nullptr) + material.UseWithShader(*passShader, opacity); + else + material.Use(opacity); glUniform1i(bindings.instanced, instanceBuffer != 0); glBindBuffer(GL_ARRAY_BUFFER, VBO); diff --git a/src/MeshInstanceRenderer.cpp b/src/MeshInstanceRenderer.cpp index 5435324..140f938 100644 --- a/src/MeshInstanceRenderer.cpp +++ b/src/MeshInstanceRenderer.cpp @@ -11,34 +11,31 @@ MeshInstanceRenderer::~MeshInstanceRenderer() { void MeshInstanceRenderer::Render(std::span commands, const MeshShaderBindings& bindings, - const Material* passMaterial) { + Shader* passShader) { static_assert(sizeof(InstanceData) == 2 * sizeof(glm::mat4)); sorted_.clear(); for (const auto& command : commands) { - if (passMaterial != nullptr || command.material != nullptr) - sorted_.push_back(&command); + if (command.material != nullptr) sorted_.push_back(&command); } - const auto materialFor = [passMaterial](const MeshRenderCommand* command) { - return passMaterial != nullptr ? passMaterial : command->material.get(); - }; std::ranges::stable_sort(sorted_, [&](const auto* a, const auto* b) { if (&a->mesh.get() != &b->mesh.get()) return std::less{}(&a->mesh.get(), &b->mesh.get()); - return std::less{}(materialFor(a), materialFor(b)); + return std::less{}(a->material.get(), + b->material.get()); }); for (std::size_t begin = 0; begin < sorted_.size();) { std::size_t end = begin + 1; while (end < sorted_.size() && &sorted_[end]->mesh.get() == &sorted_[begin]->mesh.get() && - materialFor(sorted_[end]) == materialFor(sorted_[begin])) + sorted_[end]->material == sorted_[begin]->material) ++end; const auto& first = *sorted_[begin]; - const Material& material = *materialFor(&first); + const Material& material = *first.material; if (end - begin == 1 || bindings.instanceWorld < 0 || bindings.instanceRotation < 0 || bindings.instanced < 0) { for (std::size_t index = begin; index < end; ++index) first.mesh.get().Draw(sorted_[index]->transform, material, - bindings); + bindings, 1.0F, 0, 1, passShader); } else { instances_.clear(); for (std::size_t index = begin; index < end; ++index) { @@ -65,9 +62,9 @@ void MeshInstanceRenderer::Render(std::span commands, static_cast(instances_.size() * sizeof(InstanceData)), instances_.data(), GL_STREAM_DRAW); - first.mesh.get().Draw(first.transform, material, bindings, 1.0F, - buffer_, - static_cast(instances_.size())); + first.mesh.get().Draw( + first.transform, material, bindings, 1.0F, buffer_, + static_cast(instances_.size()), passShader); } begin = end; } diff --git a/src/main.cpp b/src/main.cpp index 566a077..b51abe9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -447,11 +447,6 @@ int Application::InitScene(GLFWwindow& window) { {{"shaders/DSGeometryPass.vsh", ShaderType::Vertex}, {"shaders/DSGeometryPass.fsh", ShaderType::Fragment}}); } - // load material - { - DSGeometryPassMaterial = std::make_shared( - gsl_lite::not_null{DSGeometryPassShader}); - } } // light shaders @@ -577,7 +572,6 @@ int Application::InitScene(GLFWwindow& window) { .spotLightShader = gsl_lite::not_null{DSSpotLightShader}, .directionalLightShader = gsl_lite::not_null{DSDirectionalLightShader}, - .geometryMaterial = gsl_lite::not_null{DSGeometryPassMaterial}, .stencilMaterial = gsl_lite::not_null{DSStencilPassMaterial}, .pointLightMaterial = gsl_lite::not_null{DSPointLightMaterial}, .spotLightMaterial = gsl_lite::not_null{DSSpotLightMaterial}, diff --git a/tests/OpenGLRenderTests.cpp b/tests/OpenGLRenderTests.cpp index ee4d616..c13a7e8 100644 --- a/tests/OpenGLRenderTests.cpp +++ b/tests/OpenGLRenderTests.cpp @@ -65,6 +65,18 @@ std::shared_ptr LoadShader(const std::string& vertex, return shader; } +std::shared_ptr SolidTexture( + const std::array& color) { + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, + color.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + return std::make_shared(texture); +} + class OpenGLRender : public testing::Test { protected: void SetUp() override { @@ -439,6 +451,103 @@ TEST_F(OpenGLRender, InstancedAndIndividualTransformsProduceMatchingImages) { } } +TEST_F(OpenGLRender, PassShaderPreservesPerInstanceSurfaceMaterials) { + constexpr auto surfaceVertex = R"(#version 330 + in vec3 s_vPosition; + void main() { gl_Position = vec4(s_vPosition, 1.0); })"; + constexpr auto surfaceFragment = R"(#version 330 + out vec4 color; + void main() { color = vec4(1.0); })"; + auto surfaceShader = std::make_shared(); + surfaceShader->AddShader(surfaceVertex, ShaderType::Vertex); + surfaceShader->AddShader(surfaceFragment, ShaderType::Fragment); + surfaceShader->Link(); + + constexpr auto passFragment = R"(#version 330 + in vec3 fN; + in vec3 Tangent; + in vec2 UV; + uniform float attributeWeight; + uniform sampler2D colTexSampler; + uniform sampler2D normTexSampler; + uniform sampler2D specTexSampler; + out vec4 color; + void main() { + vec3 surface = vec3(texture(colTexSampler, UV).r, + texture(normTexSampler, UV).g, + texture(specTexSampler, UV).b); + color = vec4(surface + attributeWeight * (fN + Tangent), 1.0); + })"; + auto passShader = std::make_shared(); + passShader->AddShader(ReadFile(Asset("shaders/DSGeometryPass.vsh")), + ShaderType::Vertex); + passShader->AddShader(passFragment, ShaderType::Fragment); + passShader->Link(); + + auto leftMaterial = + std::make_shared(gsl_lite::not_null{surfaceShader}); + auto rightMaterial = + std::make_shared(gsl_lite::not_null{surfaceShader}); + const auto leftTexture = SolidTexture({51, 76, 102, 255}); + const auto rightTexture = SolidTexture({204, 178, 153, 255}); + for (const auto& [material, texture] : + {std::pair{leftMaterial, leftTexture}, + std::pair{rightMaterial, rightTexture}}) { + material->SetColorTexture(texture); + material->SetNormalTexture(texture); + material->SetSpecularTexture(texture); + } + + Mesh mesh(gsl_lite::not_null{leftMaterial}, + Asset("models/cube2x2x2.obj").string()); + RenderQueue queue; + for (const float y : {-0.5F, 0.5F}) { + queue.SubmitOpaqueMesh( + mesh, + Transform{.position = {-0.8F, y, -3.0F}, .scale = glm::vec3(0.35F)}, + leftMaterial); + queue.SubmitOpaqueMesh( + mesh, + Transform{.position = {0.8F, y, -3.0F}, .scale = glm::vec3(0.35F)}, + rightMaterial); + } + + RenderTarget target; + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), + GL_FRAMEBUFFER_COMPLETE); + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + glDisable(GL_CULL_FACE); + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + passShader->Use(); + const glm::mat4 projection = + glm::orthoRH_NO(-2.0F, 2.0F, -2.0F, 2.0F, 0.1F, 10.0F); + glUniformMatrix4fv(passShader->GetUniformLocation("gVC"), 1, GL_FALSE, + glm::value_ptr(projection)); + glUniform1f(passShader->GetUniformLocation("attributeWeight"), 0.0F); + MeshInstanceRenderer renderer; + renderer.Render(queue.GetOpaqueMeshes(), + MeshShaderBindings::FromShader(*passShader), + passShader.get()); + + const Image image = target.Read(); + const auto pixel = [&image](int x, int y, int channel) { + return image.at( + static_cast((y * kSize + x) * 4 + channel)); + }; + for (const int y : {48, 80}) { + EXPECT_NEAR(pixel(38, y, 0), 51, 1); + EXPECT_NEAR(pixel(38, y, 1), 76, 1); + EXPECT_NEAR(pixel(38, y, 2), 102, 1); + EXPECT_NEAR(pixel(90, y, 0), 204, 1); + EXPECT_NEAR(pixel(90, y, 1), 178, 1); + EXPECT_NEAR(pixel(90, y, 2), 153, 1); + } + EXPECT_EQ(glGetError(), GL_NO_ERROR); +} + TEST_F(OpenGLRender, GBufferCreationAndResizeAreComplete) { GBuffer buffer(kSize, kSize); EXPECT_EQ(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER),