Skip to content

Commit

Permalink
[Render/SobelFilterRenderProcess] Added a Sobel filter/operator proc.
Browse files Browse the repository at this point in the history
- This computes the gradient & gradient direction of an image's pixels

- Added a dedicated unit test & render result images
  • Loading branch information
Razakhel committed Aug 18, 2024
1 parent 31af1dc commit c79a9eb
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 17 deletions.
1 change: 1 addition & 0 deletions include/RaZ/RaZ.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
#include "Render/ScreenSpaceReflectionsRenderProcess.hpp"
#include "Render/Shader.hpp"
#include "Render/ShaderProgram.hpp"
#include "Render/SobelFilterRenderProcess.hpp"
#include "Render/SubmeshRenderer.hpp"
#include "Render/Texture.hpp"
#include "Render/UniformBuffer.hpp"
Expand Down
39 changes: 39 additions & 0 deletions include/RaZ/Render/SobelFilterRenderProcess.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#ifndef RAZ_SOBELFILTERRENDERPROCESS_HPP
#define RAZ_SOBELFILTERRENDERPROCESS_HPP

#include "RaZ/Render/MonoPassRenderProcess.hpp"

namespace Raz {

/// [Sobel filter](https://en.wikipedia.org/wiki/Sobel_operator) render process.
class SobelFilterRenderProcess final : public MonoPassRenderProcess {
public:
explicit SobelFilterRenderProcess(RenderGraph& renderGraph);

void resizeBuffers(unsigned int width, unsigned int height) override;
void setInputBuffer(Texture2DPtr colorBuffer);
/// Sets the output buffer which will contain the gradient's value.
/// \param gradientBuffer Gradient's values buffer.
void setOutputGradientBuffer(Texture2DPtr gradientBuffer);
/// Sets the output buffer which will contain the gradient's direction.
///
/// /--0.75--\
/// / \
/// / \
/// 0.5 0/1
/// \ /
/// \ /
/// \--0.25--/
///
/// \note The direction's values are just like those of [atan2](https://en.wikipedia.org/wiki/Atan2) (see image below), but remapped between [0; 1].
/// \imageSize{https://upload.cppreference.com/mwiki/images/9/91/math-atan2.png, height: 20%; width: 20%;}
/// \image html https://upload.cppreference.com/mwiki/images/9/91/math-atan2.png
/// \param gradDirBuffer Gradient's direction buffer.
void setOutputGradientDirectionBuffer(Texture2DPtr gradDirBuffer);
};

} // namespace Raz

#endif // RAZ_SOBELFILTERRENDERPROCESS_HPP
49 changes: 49 additions & 0 deletions shaders/sobel_filter.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#define PI 3.1415926535897932384626433832795

in vec2 fragTexcoords;

uniform sampler2D uniBuffer;
uniform vec2 uniInvBufferSize;

layout(location = 0) out vec4 fragGradient;
layout(location = 1) out vec4 fragGradDir;

const float[9] horizKernel = float[](
1.0, 0.0, -1.0,
2.0, 0.0, -2.0,
1.0, 0.0, -1.0
);

const float[9] vertKernel = float[](
1.0, 2.0, 1.0,
0.0, 0.0, 0.0,
-1.0, -2.0, -1.0
);

const vec2[9] offsets = vec2[](
vec2(-1.0, 1.0), vec2(0.0, 1.0), vec2(1.0, 1.0),
vec2(-1.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0),
vec2(-1.0, -1.0), vec2(0.0, -1.0), vec2(1.0, -1.0)
);

const float invTau = 1.0 / (2.0 * PI);

void main() {
vec3 horizVal = vec3(0.0);
vec3 vertVal = vec3(0.0);

for (int i = 0; i < 9; ++i) {
vec2 offset = offsets[i] * uniInvBufferSize;
vec3 pixel = texture(uniBuffer, fragTexcoords + offset).rgb;

horizVal += pixel * horizKernel[i];
vertVal += pixel * vertKernel[i];
}

vec3 gradient = sqrt(horizVal * horizVal + vertVal * vertVal);
vec3 gradDir = atan(vertVal, horizVal);
gradDir = (gradDir + PI) * invTau; // Remapping from [-Pi; Pi] to [0; 1]

fragGradient = vec4(gradient, 1.0);
fragGradDir = vec4(gradDir, 1.0);
}
38 changes: 38 additions & 0 deletions src/RaZ/Render/SobelFilterRenderProcess.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "RaZ/Render/SobelFilterRenderProcess.hpp"
#include "RaZ/Render/RenderPass.hpp"

#include <string_view>

namespace Raz {

namespace {

constexpr std::string_view sobelSource = {
#include "sobel_filter.frag.embed"
};

} // namespace

SobelFilterRenderProcess::SobelFilterRenderProcess(RenderGraph& renderGraph)
: MonoPassRenderProcess(renderGraph, FragmentShader::loadFromSource(sobelSource), "Sobel filter") {}

void SobelFilterRenderProcess::resizeBuffers(unsigned int width, unsigned int height) {
const Vec2f invBufferSize(1.f / static_cast<float>(width), 1.f / static_cast<float>(height));
m_pass.getProgram().setAttribute(invBufferSize, "uniInvBufferSize");
m_pass.getProgram().sendAttributes();
}

void SobelFilterRenderProcess::setInputBuffer(Texture2DPtr colorBuffer) {
resizeBuffers(colorBuffer->getWidth(), colorBuffer->getHeight());
MonoPassRenderProcess::setInputBuffer(std::move(colorBuffer), "uniBuffer");
}

void SobelFilterRenderProcess::setOutputGradientBuffer(Texture2DPtr colorBuffer) {
MonoPassRenderProcess::setOutputBuffer(std::move(colorBuffer), 0);
}

void SobelFilterRenderProcess::setOutputGradientDirectionBuffer(Texture2DPtr colorBuffer) {
MonoPassRenderProcess::setOutputBuffer(std::move(colorBuffer), 1);
}

} // namespace Raz
11 changes: 11 additions & 0 deletions src/RaZ/Script/LuaRenderGraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "RaZ/Render/RenderGraph.hpp"
#include "RaZ/Render/RenderPass.hpp"
#include "RaZ/Render/ScreenSpaceReflectionsRenderProcess.hpp"
#include "RaZ/Render/SobelFilterRenderProcess.hpp"
#include "RaZ/Render/VignetteRenderProcess.hpp"
#include "RaZ/Script/LuaWrapper.hpp"
#include "RaZ/Utils/TypeUtils.hpp"
Expand Down Expand Up @@ -45,6 +46,7 @@ void LuaWrapper::registerRenderGraphTypes() {
renderGraph["addGaussianBlurRenderProcess"] = &RenderGraph::addRenderProcess<GaussianBlurRenderProcess>;
renderGraph["addPixelizationRenderProcess"] = &RenderGraph::addRenderProcess<PixelizationRenderProcess>;
renderGraph["addScreenSpaceReflectionsRenderProcess"] = &RenderGraph::addRenderProcess<ScreenSpaceReflectionsRenderProcess>;
renderGraph["addSobelFilterRenderProcess"] = &RenderGraph::addRenderProcess<SobelFilterRenderProcess>;
renderGraph["addVignetteRenderProcess"] = &RenderGraph::addRenderProcess<VignetteRenderProcess>;
renderGraph["resizeViewport"] = &RenderGraph::resizeViewport;
renderGraph["updateShaders"] = &RenderGraph::updateShaders;
Expand Down Expand Up @@ -189,6 +191,15 @@ void LuaWrapper::registerRenderGraphTypes() {
ssrRenderProcess["setOutputBuffer"] = &ScreenSpaceReflectionsRenderProcess::setOutputBuffer;
}

{
auto sobelRenderProcess = state.new_usertype<SobelFilterRenderProcess>("SobelFilterRenderProcess",
sol::constructors<SobelFilterRenderProcess(RenderGraph&)>(),
sol::base_classes, sol::bases<MonoPassRenderProcess, RenderProcess>());
sobelRenderProcess["setInputBuffer"] = &SobelFilterRenderProcess::setInputBuffer;
sobelRenderProcess["setOutputGradientBuffer"] = &SobelFilterRenderProcess::setOutputGradientBuffer;
sobelRenderProcess["setOutputGradientDirectionBuffer"] = &SobelFilterRenderProcess::setOutputGradientDirectionBuffer;
}

{
auto vignetteRenderProcess = state.new_usertype<VignetteRenderProcess>("VignetteRenderProcess",
sol::constructors<VignetteRenderProcess(RenderGraph&)>(),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 55 additions & 17 deletions tests/src/RaZ/Render/RenderProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "RaZ/Render/PixelizationRenderProcess.hpp"
#include "RaZ/Render/RenderProcess.hpp"
#include "RaZ/Render/RenderSystem.hpp"
#include "RaZ/Render/SobelFilterRenderProcess.hpp"
#include "RaZ/Render/VignetteRenderProcess.hpp"
#include "RaZ/Render/Window.hpp"

Expand All @@ -19,13 +20,8 @@

namespace {

Raz::Image renderFrame(Raz::World& world, const Raz::Texture2DPtr& output, const Raz::FilePath& renderedImgPath = {}) {
// Rendering a frame of the scene by updating the World's RenderSystem
// Running the window shouldn't be useful as we render to a texture, and more importantly can make this file's tests
// fail under Linux (as the second rendered frame of each test might be empty)
world.update({});

Raz::Image renderedImg = output->recoverImage();
Raz::Image recoverImage(const Raz::Texture2DPtr& outputTexture, const Raz::FilePath& renderedImgPath = {}) {
Raz::Image renderedImg = outputTexture->recoverImage();

if (!renderedImgPath.isEmpty())
Raz::ImageFormat::save(renderedImgPath, renderedImg, true);
Expand Down Expand Up @@ -57,7 +53,8 @@ TEST_CASE("ChromaticAberrationRenderProcess execution", "[render]") {
chromaticAberration.setInputBuffer(std::move(input));
chromaticAberration.setOutputBuffer(output);

CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(baseImg));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(baseImg));

// 1
// 0
Expand All @@ -74,7 +71,9 @@ TEST_CASE("ChromaticAberrationRenderProcess execution", "[render]") {
chromaticAberration.setStrength(5.f);
chromaticAberration.setDirection(Raz::Vec2f(1.f, 1.f));
chromaticAberration.setMaskTexture(std::move(texture));
CHECK_THAT(renderFrame(world, output),

world.update({});
CHECK_THAT(recoverImage(output),
IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_chromatic_aberration.png", true)));
}

Expand Down Expand Up @@ -103,12 +102,14 @@ TEST_CASE("ConvolutionRenderProcess execution", "[render]") {
convolution.setInputBuffer(std::move(input));
convolution.setOutputBuffer(output);

CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(baseImg));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(baseImg));

convolution.setKernel(Raz::Mat3f(-1.f, -1.f, -1.f,
-1.f, 8.f, -1.f,
-1.f, -1.f, -1.f));
CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_convolved.png", true)));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_convolved.png", true)));
}

TEST_CASE("FilmGrainRenderProcess execution", "[render][!mayfail]") { // May fail under Linux for yet unknown reasons (second frame is empty)
Expand All @@ -129,10 +130,12 @@ TEST_CASE("FilmGrainRenderProcess execution", "[render][!mayfail]") { // May fai
filmGrain.setInputBuffer(std::move(input));
filmGrain.setOutputBuffer(output);

CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/film_grain_weak.png", true)));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/film_grain_weak.png", true)));

filmGrain.setStrength(0.5f);
CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/film_grain_strong.png", true), 0.062f));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/film_grain_strong.png", true), 0.062f));
}

TEST_CASE("PixelizationRenderProcess execution", "[render][!mayfail]") { // May fail under Linux for yet unknown reasons (second frame is empty)
Expand All @@ -157,10 +160,43 @@ TEST_CASE("PixelizationRenderProcess execution", "[render][!mayfail]") { // May
pixelization.setInputBuffer(std::move(input));
pixelization.setOutputBuffer(output);

CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(baseImg));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(baseImg));

pixelization.setStrength(0.75f);
CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_pixelated.png", true)));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_pixelated.png", true)));
}

TEST_CASE("SobelFilterRenderProcess execution", "[render]") {
Raz::World world;

const Raz::Window& window = TestUtils::getWindow();

auto& render = world.addSystem<Raz::RenderSystem>(window.getWidth(), window.getHeight());

// RenderSystem::update() needs a Camera with a Transform component
world.addEntityWithComponents<Raz::Camera, Raz::Transform>();

const Raz::Image baseImg = Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_base.png", true);
REQUIRE(baseImg.getWidth() == window.getWidth());
REQUIRE(baseImg.getHeight() == window.getHeight());

Raz::Texture2DPtr input = Raz::Texture2D::create(baseImg);
const Raz::Texture2DPtr outputGrad = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::RGB, Raz::TextureDataType::BYTE);
const Raz::Texture2DPtr outputGradDir = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::RGB, Raz::TextureDataType::BYTE);

auto& sobel = render.getRenderGraph().addRenderProcess<Raz::SobelFilterRenderProcess>();
sobel.addParent(render.getGeometryPass());
sobel.setInputBuffer(std::move(input));
sobel.setOutputGradientBuffer(outputGrad);
sobel.setOutputGradientDirectionBuffer(outputGradDir);

world.update({});
CHECK_THAT(recoverImage(outputGrad),
IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_sobel_grad.png", true)));
CHECK_THAT(recoverImage(outputGradDir),
IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_sobel_grad_dir.png", true), 0.06f));
}

TEST_CASE("VignetteRenderProcess execution", "[render]") {
Expand All @@ -181,10 +217,12 @@ TEST_CASE("VignetteRenderProcess execution", "[render]") {
vignette.setInputBuffer(std::move(input));
vignette.setOutputBuffer(output);

CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/vignette_weak_black.png", true)));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/vignette_weak_black.png", true)));

vignette.setStrength(1.f);
vignette.setOpacity(0.5f);
vignette.setColor(Raz::ColorPreset::Red);
CHECK_THAT(renderFrame(world, output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/vignette_strong_red.png", true)));
world.update({});
CHECK_THAT(recoverImage(output), IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/vignette_strong_red.png", true)));
}
1 change: 1 addition & 0 deletions tests/src/RaZ/Script/LuaRender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ TEST_CASE("LuaRender RenderGraph", "[script][lua][render]") {
assert(renderGraph:addGaussianBlurRenderProcess() ~= nil)
assert(renderGraph:addPixelizationRenderProcess() ~= nil)
assert(renderGraph:addScreenSpaceReflectionsRenderProcess() ~= nil)
assert(renderGraph:addSobelFilterRenderProcess() ~= nil)
assert(renderGraph:addVignetteRenderProcess() ~= nil)
renderGraph:resizeViewport(1, 1)
renderGraph:updateShaders()
Expand Down

0 comments on commit c79a9eb

Please sign in to comment.