Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions assets/shaders/slang/tonemap.slang
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,56 @@ uniform float bloomIntensity;
// runs after it and dithers on its own write, and dithering into an
// intermediate float target would just seed noise for TAA to accumulate.
uniform bool ditherOutput;
// Must match the order of ToneMapper in scenetarget.hpp.
uniform int toneMapper;

static const int toneReinhard = 0;
static const int toneFilmic = 1;
static const int toneAces = 2;
static const int toneAgx = 3;

// Uncharted 2 curve (Hable). Exposure bias 2 and white point 11.2 as in the
// original talk.
float3 hable(float3 x) {
const float a = 0.15, b = 0.50, c = 0.10, d = 0.20, e = 0.02, f = 0.30;
return ((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f;
}

float3 filmic(float3 color) {
return hable(color * 2.0) / hable(float3(11.2));
}

// Narkowicz fit of the ACES RRT and ODT.
float3 aces(float3 x) {
return saturate((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14));
}

// AgX with the polynomial contrast fit (minimal AgX, Wrensch). Returns linear
// display light, so the shared gamma encode below applies as for the others.
float3 agx(float3 color) {
const float3x3 inset = float3x3(
0.842479062253094, 0.0784335999999992, 0.0792237451477643,
0.0423282422610123, 0.878468636469772, 0.0791661274605434,
0.0423756549057051, 0.0784336, 0.879142973793104);
const float3x3 outset = float3x3(
1.19687900512017, -0.0980208811401368, -0.0990297440797205,
-0.0528968517574562, 1.15190312990417, -0.0989611768448433,
-0.0529716355144438, -0.0980434501171241, 1.15107367264116);
const float minEv = -12.47393;
const float maxEv = 4.026069;

float3 v = mul(inset, color);
v = clamp(log2(max(v, float3(1e-10))), minEv, maxEv);
v = (v - minEv) / (maxEv - minEv);

float3 v2 = v * v;
float3 v4 = v2 * v2;
v = 15.5 * v4 * v2 - 40.14 * v4 * v + 31.96 * v4 - 6.868 * v2 * v +
0.4298 * v2 + 0.1191 * v - 0.00232;

v = mul(outset, v);
return pow(max(v, float3(0.0)), float3(2.2));
}

// clang-format off
[shader("fragment")]
Expand All @@ -27,8 +77,15 @@ float4 fragMain(FSInput input, float4 fragCoord : SV_Position) : SV_Target {
color += bloomTex.Sample(input.texCoords).rgb * bloomIntensity;
}

// Reinhard
color = color / (color + float3(1.0));
if (toneMapper == toneFilmic) {
color = filmic(color);
} else if (toneMapper == toneAces) {
color = aces(color);
} else if (toneMapper == toneAgx) {
color = agx(color);
} else {
color = color / (color + float3(1.0));
}

color = pow(color, float3(1.0 / 2.2));

Expand Down
31 changes: 31 additions & 0 deletions game/src/layer/imgui/imguilayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ void ImGuiLayer::showLightsSection() {
showBloomControls();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Tone Map##Tab")) {
showToneMapControls();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("SSAO##Tab")) {
showSsaoControls();
ImGui::EndTabItem();
Expand Down Expand Up @@ -263,6 +267,33 @@ void ImGuiLayer::showBloomControls() {
}
}

void ImGuiLayer::showToneMapControls() {
// Order must match ToneMapper.
static constexpr std::array<const char*, 4> names = { "Reinhard", "Filmic",
"ACES", "AgX" };
if (ImGui::BeginTable("ToneMap##Table", 2, tableFlags)) {
const auto mazeLayer = Maze::get().getMazeLayer();

auto current = static_cast<int>(mazeLayer->getToneMapper());
showTableRow([&] {
ImGui::Text("Operator");
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (ImGui::Combo("##tonemapper", &current, names.data(),
static_cast<int>(names.size()))) {
mazeLayer->setToneMapper(
static_cast<sponge::platform::opengl::scene::ToneMapper>(
current));
sponge::core::Settings::set("video.toneMapper",
static_cast<uint32_t>(current));
sponge::core::Settings::save();
}
});

ImGui::EndTable();
}
}

void ImGuiLayer::showSsaoControls() {
if (ImGui::BeginTable("Ssao##Table", 2, tableFlags)) {
const auto mazeLayer = Maze::get().getMazeLayer();
Expand Down
1 change: 1 addition & 0 deletions game/src/layer/imgui/imguilayer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ImGuiLayer final : public sponge::layer::Layer {
static void showMenu(); // App info window sections
static void showLightsSection();
static void showBloomControls();
static void showToneMapControls();
static void showSsaoControls();
static void showSsrControls();
static void showDirectionalLightControls();
Expand Down
15 changes: 13 additions & 2 deletions game/src/layer/mazelayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ using sponge::platform::opengl::scene::ShadowMap;
using sponge::platform::opengl::scene::Ssao;
using sponge::platform::opengl::scene::Ssr;
using sponge::platform::opengl::scene::TAA;
using sponge::platform::opengl::scene::ToneMapper;
using thread::AntiAliasing;

MazeLayer::MazeLayer() :
Expand Down Expand Up @@ -557,6 +558,7 @@ void MazeLayer::captureRenderFrame(const uint32_t slotIndex) {
frame.bloomEnabled = bloomEnabled;
frame.bloomThreshold = bloomThreshold;
frame.bloomIntensity = bloomIntensity;
frame.toneMapper = toneMapper;

frame.ssaoEnabled = ssaoEnabled;
frame.ssaoRadius = ssaoRadius;
Expand Down Expand Up @@ -823,8 +825,8 @@ void MazeLayer::onRender() {
taa->begin();
}

sceneTarget->resolve(bloomTexId, bloomWeight,
!fxaaActive && !taaActive);
sceneTarget->resolve(bloomTexId, bloomWeight, !fxaaActive && !taaActive,
frame.toneMapper);
gpuEnd();
}

Expand Down Expand Up @@ -1597,6 +1599,15 @@ void MazeLayer::setBloomIntensity(const float val) {
bloomIntensity = val;
}

ToneMapper MazeLayer::getToneMapper() const {
return toneMapper;
}

void MazeLayer::setToneMapper(const ToneMapper val) {
std::scoped_lock lock(settingsMutex);
toneMapper = val;
}

bool MazeLayer::isSsaoEnabled() const {
return ssaoEnabled;
}
Expand Down
20 changes: 12 additions & 8 deletions game/src/layer/mazelayer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,14 @@ class MazeLayer final : public sponge::layer::Layer {

void setAntiAliasing(thread::AntiAliasing val);

bool isBloomEnabled() const;
void setBloomEnabled(bool val);
float getBloomThreshold() const;
void setBloomThreshold(float val);
float getBloomIntensity() const;
void setBloomIntensity(float val);
bool isBloomEnabled() const;
void setBloomEnabled(bool val);
float getBloomThreshold() const;
void setBloomThreshold(float val);
float getBloomIntensity() const;
void setBloomIntensity(float val);
sponge::platform::opengl::scene::ToneMapper getToneMapper() const;
void setToneMapper(sponge::platform::opengl::scene::ToneMapper val);

bool isSsaoEnabled() const;
void setSsaoEnabled(bool val);
Expand Down Expand Up @@ -342,8 +344,10 @@ class MazeLayer final : public sponge::layer::Layer {
float ao = .25F;
int32_t attenuationIndex = 4;
thread::AntiAliasing antiAliasing = thread::AntiAliasing::Taa;
bool bloomEnabled = true;
float bloomThreshold = 0.8F;
sponge::platform::opengl::scene::ToneMapper toneMapper =
sponge::platform::opengl::scene::ToneMapper::Reinhard;
bool bloomEnabled = true;
float bloomThreshold = 0.8F;
// Additive weight for the bloom texture, applied in LINEAR space before
// tone mapping. The bloom texture holds radiance (measured peak ~2.6 in
// this scene), not the [0,1] tone-mapped values it held when bloom
Expand Down
8 changes: 8 additions & 0 deletions game/src/maze.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ bool Maze::onUserCreate() {
setBloomEnabled(
sponge::core::Settings::getBool("video.bloomEnabled", true));

using sponge::platform::opengl::scene::ToneMapper;
const auto savedTone = sponge::core::Settings::getUInt32(
"video.toneMapper", static_cast<uint32_t>(ToneMapper::Reinhard));
mazeLayer->setToneMapper(savedTone <
static_cast<uint32_t>(ToneMapper::Count) ?
static_cast<ToneMapper>(savedTone) :
ToneMapper::Reinhard);

// After the settings above, so the maze is configured when it loads.
if (captureFrames > 0) {
loadingLayer->setActive(true);
Expand Down
5 changes: 5 additions & 0 deletions game/src/thread/mazeframe.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "platform/opengl/scene/clusteredlights.hpp"
#include "platform/opengl/scene/model.hpp"
#include "platform/opengl/scene/scenetarget.hpp"
#include "platform/opengl/scene/shadowmap.hpp"
#include "scene/refraction.hpp"

Expand Down Expand Up @@ -115,6 +116,10 @@ struct MazeRenderFrame {
float bloomThreshold{ 0.8F };
float bloomIntensity{ 0.08F };

sponge::platform::opengl::scene::ToneMapper toneMapper{
sponge::platform::opengl::scene::ToneMapper::Reinhard
};

bool ssaoEnabled{ true };
float ssaoRadius{ 0.5F };

Expand Down
4 changes: 4 additions & 0 deletions sponge/src/platform/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ than calling GLFW/OpenGL/OS APIs directly.
`Ssr` and the glass pass each sample `SceneTarget::copyColor()`, never the
color attachment they draw into. The prepass normal alpha is the SSR
strength mask.
* resolve() takes a `ToneMapper` (Reinhard, Filmic, ACES, AgX). Every
operator outputs linear display light and shares the one gamma encode, so
AgX includes its own inverse transfer step. Keep `ToneMapper` in
`scenetarget.hpp` in step with the `tone*` constants in `tonemap.slang`.
* Dither exactly once, at the final 8-bit write, and keep every intermediate
target float (`GL_RGB16F`). An 8-bit intermediate quantises undithered and
then quantises again at the real output — the banding `dither8` exists to
Expand Down
4 changes: 3 additions & 1 deletion sponge/src/platform/opengl/scene/scenetarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,16 @@ void SceneTarget::blitDepthTo(const uint32_t destFbo, const int w,
}

void SceneTarget::resolve(const uint32_t bloomTexId, const float bloomIntensity,
const bool ditherOutput) const {
const bool ditherOutput,
const ToneMapper toneMapper) const {
// Depth testing would discard the full-screen quad behind whatever the
// scene pass left in the depth buffer.
glDisable(GL_DEPTH_TEST);

shader->bind();
shader->setFloat("bloomIntensity", bloomIntensity);
shader->setBoolean("ditherOutput", ditherOutput);
shader->setInteger("toneMapper", static_cast<int>(toneMapper));

glBindTextureUnit(0, colorTexture);
glBindTextureUnit(1, bloomTexId);
Expand Down
13 changes: 11 additions & 2 deletions sponge/src/platform/opengl/scene/scenetarget.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@

namespace sponge::platform::opengl::scene {

// Order must match the `tone*` constants in tonemap.slang.
enum class ToneMapper : uint8_t {
Reinhard = 0,
Filmic,
Aces,
Agx,
Count,
};

// The linear HDR buffer the scene renders into, and the pass that turns it
// into a display image.
//
Expand Down Expand Up @@ -57,8 +66,8 @@ class SceneTarget {
// `bloomIntensity` is 0. Set `ditherOutput` only when this pass writes the
// final 8-bit target — anti-aliasing, when enabled, runs afterwards and
// dithers on its own write.
void resolve(uint32_t bloomTexId, float bloomIntensity,
bool ditherOutput) const;
void resolve(uint32_t bloomTexId, float bloomIntensity, bool ditherOutput,
ToneMapper toneMapper) const;

void resize(uint32_t newWidth, uint32_t newHeight);

Expand Down
Loading