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
2 changes: 2 additions & 0 deletions assets/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,6 @@ shaders:
depthprepass.frag: shaders/slang/depthprepass.slang:fragMain
occlusionbox.vert: shaders/slang/occlusionbox.slang:vertMain
occlusionbox.frag: shaders/slang/occlusionbox.slang:fragMain
ssao.frag: shaders/slang/ssao.slang:fragMain
ssao_blur.frag: shaders/slang/ssaoblur.slang:fragMain
cluster_assign.comp: shaders/slang/cluster_assign.slang:csMain
3 changes: 2 additions & 1 deletion assets/scenes/maze.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ camera:
lighting:
ambient:
strength: 0.25
occlusion: 0.25
# 1 = no extra attenuation; SSAO now supplies real per-pixel occlusion.
occlusion: 1.0

directional:
enabled: true
Expand Down
35 changes: 30 additions & 5 deletions assets/shaders/slang/depthprepass.slang
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
// depthprepass.slang: camera-space depth-only prepass, plus the screen-space
// velocity buffer TAA resolves through.
// velocity buffer TAA resolves through and the view-space normal buffer SSAO
// reads.
uniform float4x4 mvp; // jittered; must match the scene pass
uniform float4x4 mvpNoJitter; // this frame, unjittered
uniform float4x4 prevMvpNoJitter; // last frame, unjittered
uniform float4x4 normalMatrix; // transpose(inverse(view * model))

// Same VSInput layout as pbr.slang (position/texCoord/normal/tangent), so
// slangc assigns the same attribute locations the mesh VAO was built
// against. texCoord/tangent go unused here. Light cubes draw through this
// shader too but their VAO only has position bound (see cube.cpp), so their
// normal reads the GL default (0,0,0) — filtered out downstream by the
// zero-length check in ssao.slang.
struct VSInput {
float3 position : POSITION;
float2 texCoord : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct VSOutput {
float4 pos : SV_Position;
// Unjittered clip positions: motion is measured between pixel centres, so
// the sub-pixel jitter must not enter the difference.
float4 currClip : TEXCOORD0;
float4 prevClip : TEXCOORD1;
float3 viewNormal : TEXCOORD2;
};
struct FSOutput {
float2 velocity : SV_Target0;
float4 normal : SV_Target1;
};

// clang-format off
Expand All @@ -24,14 +40,23 @@ VSOutput vertMain(VSInput input) {
o.pos = mul(mvp, position);
o.currClip = mul(mvpNoJitter, position);
o.prevClip = mul(prevMvpNoJitter, position);
o.viewNormal = mul((float3x3)normalMatrix, input.normal);
return o;
};

// clang-format off
[shader("fragment")]
float2 fragMain(VSOutput input) : SV_Target {
FSOutput fragMain(VSOutput input) {
// clang-format on
float2 currUV = input.currClip.xy / input.currClip.w * 0.5 + 0.5;
float2 prevUV = input.prevClip.xy / input.prevClip.w * 0.5 + 0.5;
return currUV - prevUV;
FSOutput o;
float2 currUV = input.currClip.xy / input.currClip.w * 0.5 + 0.5;
float2 prevUV = input.prevClip.xy / input.prevClip.w * 0.5 + 0.5;
o.velocity = currUV - prevUV;

// Guard against normalizing a true-zero normal (light cubes / geometry
// with no NORMAL attribute bound): 0/0 is NaN, and a NaN sentinel breaks
// the "no surface here" length check ssao.slang relies on.
float len = length(input.viewNormal);
o.normal = float4(len > 1e-4 ? input.viewNormal / len : float3(0.0), 0.0);
return o;
};
1 change: 1 addition & 0 deletions assets/shaders/slang/include/uniforms.slang
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct PbrParams {
float metallicFactor;
float roughnessFactor;
float ao;
bool ssaoEnabled;
float ambientStrength;
float evsmBleedThreshold;
bool hasNoTexture;
Expand Down
4 changes: 4 additions & 0 deletions assets/shaders/slang/pbr.slang
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ VSOutput vertMain(VSInput input) {
[[vk::binding(7)]] Sampler2D<float4> occlusionMap;
[[vk::binding(8)]] Sampler2D<float4> emissiveMap;
[[vk::binding(9)]] Sampler2D<float4> metallicRoughnessMap;
[[vk::binding(10)]] Sampler2D<float> ssaoTexture;

// KHR_texture_transform: t.xy = offset, t.zw = scale (no rotation support)
float2 applyUVTransform(float2 uv, float4 t) {
Expand Down Expand Up @@ -85,6 +86,9 @@ float4 fragMain(VSOutput input, float4 fragCoord : SV_Position) : SV_Target {
float2 occlusionUV = applyUVTransform(input.texCoords, params.occlusionUVTransform);
ao *= occlusionMap.Sample(occlusionUV).r;
}
if (params.ssaoEnabled) {
ao *= ssaoTexture.Sample(fragCoord.xy / params.screenSize).r;
}

float3 emissiveTexColor = float3(0.0);
if (params.hasEmissiveMap) {
Expand Down
87 changes: 87 additions & 0 deletions assets/shaders/slang/ssao.slang
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// ssao.slang: screen-space ambient occlusion, hemisphere-kernel sampling
// against the depth prepass's depth + view-space normal targets. Technique
// follows the classic Crytek/Rendu approach (github.com/kosua20/Rendu,
// resources/common/shaders/screens/ssao.frag): reconstruct view-space
// position from depth, orient a hemisphere kernel with a random per-pixel
// rotation, and count how many kernel samples land behind real geometry.
struct FSInput {
float2 texCoords : TEXCOORD0;
};

[[vk::binding(0)]] Sampler2D<float> depthTex;
[[vk::binding(1)]] Sampler2D<float4> normalTex; // view-space normal; (0,0,0) = no surface (background/light cubes)
[[vk::binding(2)]] Sampler2D<float4> noiseTex; // 4x4 tiling rotation vectors

uniform float4x4 projection;
uniform float4x4 invProjection;
uniform float2 noiseScale; // screenSize / 4, tiles noiseTex across the screen
uniform float radius;

// Without this, a flat unoccluded surface still darkens by roughly half:
// depth-buffer/normal quantization noise makes about half the kernel samples
// read back as "behind" the very surface they were cast from. The bias
// demands the occluder sit meaningfully closer than the sample point before
// it counts.
static const float bias = 0.025;

// 16 hemisphere sample offsets, cosine-weighted and biased toward the
// origin (more samples close to the surface, where occlusion detail is),
// precomputed offline with a fixed seed. Scaled by `radius` below.
static const float3 kernel[16] = {
float3(0.031343, 0.008854, 0.048690),
float3(-0.054242, 0.052629, 0.031128),
float3(0.014520, -0.006623, 0.017081),
float3(0.037989, 0.100225, 0.013283),
float3(-0.009715, -0.041701, 0.033541),
float3(-0.023567, 0.073727, 0.097785),
float3(0.173635, 0.049325, 0.120350),
float3(0.149972, -0.094466, 0.175101),
float3(-0.003614, -0.019836, 0.008477),
float3(0.001372, 0.006399, 0.015564),
float3(-0.078911, 0.130415, 0.264265),
float3(0.088872, -0.199457, 0.093786),
float3(0.374838, 0.268386, 0.141412),
float3(0.101602, -0.062204, 0.362429),
float3(0.431340, 0.086597, 0.504608),
float3(-0.219564, 0.363803, 0.202840),
};

float3 viewPositionFromDepth(float2 uv, float depth) {
float4 clip = float4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
float4 view = mul(invProjection, clip);
return view.xyz / view.w;
}

// clang-format off
[shader("fragment")]
float fragMain(FSInput input) : SV_Target {
// clang-format on
float3 n = normalTex.Sample(input.texCoords).xyz;
if (length(n) < 0.1) {
return 1.0;
}

float3 randomVec = normalize(float3(noiseTex.Sample(input.texCoords * noiseScale).xy, 0.0));
float3 t = normalize(randomVec - n * dot(randomVec, n));
float3 b = cross(n, t);

float depth = depthTex.Sample(input.texCoords).r;
float3 position = viewPositionFromDepth(input.texCoords, depth);

float occlusion = 0.0;
for (int i = 0; i < 16; ++i) {
float3 offset = t * kernel[i].x + b * kernel[i].y + n * kernel[i].z;
float3 samplePos = position + radius * offset;

float4 sampleClip = mul(projection, float4(samplePos, 1.0));
float2 sampleUV = (sampleClip.xy / sampleClip.w) * 0.5 + 0.5;

float sampleDepth = depthTex.Sample(sampleUV).r;
float3 sampledPos = viewPositionFromDepth(sampleUV, sampleDepth);

float isValid = abs(position.z - sampledPos.z) < radius ? 1.0 : 0.0;
occlusion += (sampledPos.z >= samplePos.z + bias) ? isValid : 0.0;
}

return 1.0 - occlusion / 16.0;
};
36 changes: 36 additions & 0 deletions assets/shaders/slang/ssaoblur.slang
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// ssaoblur.slang: depth-aware box blur over the raw SSAO pass. Plain box
// blur would smear occlusion across depth discontinuities (an object's edge
// bleeding onto the background behind it); weighting each tap by how close
// its depth is to the center pixel's keeps edges sharp.
struct FSInput {
float2 texCoords : TEXCOORD0;
};

[[vk::binding(0)]] Sampler2D<float> aoTex;
[[vk::binding(1)]] Sampler2D<float> depthTex;

uniform float2 texelSize; // 1 / ssao target resolution

// clang-format off
[shader("fragment")]
float fragMain(FSInput input) : SV_Target {
// clang-format on
float centerDepth = depthTex.Sample(input.texCoords).r;

float total = 0.0;
float weight = 0.0;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
float2 uv = input.texCoords + float2(x, y) * texelSize;
float depth = depthTex.Sample(uv).r;
// Depth is nonlinear (raw buffer values), but the tap footprint
// is only 3x3 texels, so the resulting depth-difference bias
// stays negligible without linearizing.
float w = 1.0 / (1e-5 + abs(depth - centerDepth));
total += aoTex.Sample(uv).r * w;
weight += w;
}
}

return total / weight;
};
42 changes: 33 additions & 9 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("SSAO##Tab")) {
showSsaoControls();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
Expand Down Expand Up @@ -255,6 +259,34 @@ void ImGuiLayer::showBloomControls() {
}
}

void ImGuiLayer::showSsaoControls() {
if (ImGui::BeginTable("Ssao##Table", 2, tableFlags)) {
const auto mazeLayer = Maze::get().getMazeLayer();

auto ssaoEnabled = mazeLayer->isSsaoEnabled();
showTableRow([&] {
ImGui::Text("Enabled");
ImGui::TableNextColumn();
if (ImGui::Checkbox("##ssaoenabled", &ssaoEnabled)) {
mazeLayer->setSsaoEnabled(ssaoEnabled);
}
});

auto radius = mazeLayer->getSsaoRadius();
showTableRow([&] {
ImGui::Text("Radius");
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (ImGui::SliderFloat("##ssaoradius", &radius, 0.05F, 2.F, "%.2f",
ImGuiSliderFlags_AlwaysClamp)) {
mazeLayer->setSsaoRadius(radius);
}
});

ImGui::EndTable();
}
}

void ImGuiLayer::showDirectionalLightControls() {
if (ImGui::BeginTable("DirectionalLights##Table", 2, tableFlags)) {
const auto mazeLayer = Maze::get().getMazeLayer();
Expand Down Expand Up @@ -364,8 +396,7 @@ void ImGuiLayer::showPointLightControls() {
if (ImGui::BeginTable("PointLights##Table", 1,
ImGuiTableFlags_NoPadInnerX |
ImGuiTableFlags_NoPadOuterX)) {
auto ambientStrength = mazeLayer->getAmbientStrength();
auto ambientOcclusion = mazeLayer->getAmbientOcclusion();
auto ambientStrength = mazeLayer->getAmbientStrength();

showTableRow([&] {
if (ImGui::SliderFloat("Ambient Strength", &ambientStrength, 0.F,
Expand All @@ -374,13 +405,6 @@ void ImGuiLayer::showPointLightControls() {
}
});

showTableRow([&] {
if (ImGui::SliderFloat("Ambient Occlusion", &ambientOcclusion, 0.F,
1.F, "%.3f", ImGuiSliderFlags_AlwaysClamp)) {
mazeLayer->setAmbientOcclusion(ambientOcclusion);
}
});

ImGui::EndTable();
}
}
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 showSsaoControls();
static void showDirectionalLightControls();
static void showPointLightControls();
static void showAttenuationSlider(int32_t& attenuationIndex);
Expand Down
Loading
Loading