Skip to content

Commit b560c52

Browse files
authored
Fix SSAO visual artifacts when MSAA is enabled. Increase performance of SSAO, allow passing effect kernel size (#105)
* shaders/pbr : output the NDC depth to intermediate (color) resolved texture and use it * Optimize SSAO (reduce kernel size, precompute inverse projection matrix) * posteffects : Allow setting SSAO kernel size * multibody/RobotScene : pass ssao kernel size option * multibody/Visualizer : pass ssao kernel size option * Update CHANGELOG
1 parent 35884b2 commit b560c52

File tree

18 files changed

+126
-92
lines changed

18 files changed

+126
-92
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Expose `Visualizer.toggleGui(value=None)` (https://github.com/Simple-Robotics/candlewick/pull/102)
13+
- Add option to set SSAO effect kernel size in Visualizer and RobotScene (C++/Python) (https://github.com/Simple-Robotics/candlewick/pull/105)
1314

1415
### Changed
1516

1617
- Move cpp examples to their own directory (https://github.com/Simple-Robotics/candlewick/pull/103)
18+
- Improved performance of SSAO effect (https://github.com/Simple-Robotics/candlewick/pull/105)
19+
20+
### Fixed
21+
22+
- Fixed visual effects with SSAO + MSAA. Have SSAO sample from resolved multisampled depth texture (https://github.com/Simple-Robotics/candlewick/pull/105)
1723

1824
## [0.10.1] - 2026-01-19
1925

bindings/python/src/expose-visualizer.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ void exposeVisualizer() {
3636
bp::class_<Visualizer::Config>("VisualizerConfig", bp::no_init)
3737
.def_readwrite("width", &Visualizer::Config::width)
3838
.def_readwrite("height", &Visualizer::Config::height)
39+
.def_readwrite("sampleCount", &Visualizer::Config::sampleCount,
40+
"MSAA sample count.")
41+
.def_readwrite("ssaoKernelSize", &Visualizer::Config::ssaoKernelSize,
42+
"Kernel size for the SSAO effect.")
3943
.def(bp::init<>("self"_a))
4044
.def(bp::init<Uint32, Uint32>(("self"_a, "width", "height")));
4145

examples/cpp/Ur5WithSystems.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,16 @@ static void initialize() {
231231

232232
int main(int argc, char **argv) {
233233
CLI::App app{"Ur5 example"};
234-
bool performRecording{false};
234+
bool performRecording = false;
235235
RobotScene::Config robot_scene_config;
236236
robot_scene_config.triangle_has_prepass = true;
237237

238238
argv = app.ensure_utf8(argv);
239239
app.add_flag("-r,--record", performRecording, "Record output");
240240
app.add_option("--dims", wDims, "Window dimensions.")->capture_default_str();
241+
app.add_option("--ssao-kernel-size", robot_scene_config.ssao_kernel_size,
242+
"Number of SSAO kernel samples (max 64).")
243+
->capture_default_str();
241244
CLI11_PARSE(app, argc, argv);
242245

243246
if (!SDL_Init(SDL_INIT_VIDEO))

examples/cpp/Visualizer.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ int main(int argc, char *argv[]) {
5454
{"2", SDL_GPU_SAMPLECOUNT_2},
5555
{"4", SDL_GPU_SAMPLECOUNT_4},
5656
{"8", SDL_GPU_SAMPLECOUNT_8}};
57+
Uint32 ssaoKernelSize = 16u;
5758
const auto transform_validator =
5859
CLI::IsMember(sample_count_map) & CLI::Transformer(sample_count_map);
5960

@@ -67,6 +68,8 @@ int main(int argc, char *argv[]) {
6768
})
6869
->transform(transform_validator)
6970
->capture_default_str();
71+
app.add_option("--ssao-kernel-size", ssaoKernelSize,
72+
"Size of the SSAO effect kernel.");
7073

7174
CLI11_PARSE(app, argc, argv);
7275

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "samplers": 2, "storage_textures": 0, "storage_buffers": 0, "uniform_buffers": 4 }
1+
{ "samplers": 2, "storage_textures": 0, "storage_buffers": 0, "uniform_buffers": 4, "inputs": [{ "name": "fragLightPos", "type": "float3", "location": 2 }, { "name": "fragViewNormal", "type": "float3", "location": 1 }, { "name": "fragViewPos", "type": "float3", "location": 0 }], "outputs": [{ "name": "fragColor", "type": "float4", "location": 0 }, { "name": "outNormal", "type": "float2", "location": 1 }, { "name": "outDepth", "type": "float", "location": 2 }] }

shaders/compiled/PbrBasic.frag.msl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ struct main0_out
8787
{
8888
float4 fragColor [[color(0)]];
8989
float2 outNormal [[color(1)]];
90+
float outDepth [[color(2)]];
9091
};
9192

9293
struct main0_in
@@ -330,5 +331,6 @@ fragment main0_out main0(main0_in in [[stage_in]], constant Material& _572 [[buf
330331
color = powr(color, float3(0.4545454680919647216796875));
331332
out.fragColor = float4(color, _572.material.baseColor.w);
332333
out.outNormal = in.fragViewNormal.xy;
334+
out.outDepth = gl_FragCoord.z;
333335
return out;
334336
}

shaders/compiled/PbrBasic.frag.spv

136 Bytes
Binary file not shown.

shaders/compiled/SSAO.frag.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "samplers": 3, "storage_textures": 0, "storage_buffers": 0, "uniform_buffers": 2 }
1+
{ "samplers": 3, "storage_textures": 0, "storage_buffers": 0, "uniform_buffers": 2, "inputs": [{ "name": "inUV", "type": "float2", "location": 0 }], "outputs": [{ "name": "aoValue", "type": "float", "location": 0 }] }

shaders/compiled/SSAO.frag.msl

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,11 @@
55

66
using namespace metal;
77

8-
// Returns the determinant of a 2x2 matrix.
9-
static inline __attribute__((always_inline))
10-
float spvDet2x2(float a1, float a2, float b1, float b2)
11-
{
12-
return a1 * b2 - b1 * a2;
13-
}
14-
15-
// Returns the determinant of a 3x3 matrix.
16-
static inline __attribute__((always_inline))
17-
float spvDet3x3(float a1, float a2, float a3, float b1, float b2, float b3, float c1, float c2, float c3)
18-
{
19-
return a1 * spvDet2x2(b2, b3, c2, c3) - b1 * spvDet2x2(a2, a3, c2, c3) + c1 * spvDet2x2(a2, a3, b2, b3);
20-
}
21-
22-
// Returns the inverse of a matrix, by using the algorithm of calculating the classical
23-
// adjoint and dividing by the determinant. The contents of the matrix are changed.
24-
static inline __attribute__((always_inline))
25-
float4x4 spvInverse4x4(float4x4 m)
26-
{
27-
float4x4 adj; // The adjoint matrix (inverse after dividing by determinant)
28-
29-
// Create the transpose of the cofactors, as the classical adjoint of the matrix.
30-
adj[0][0] = spvDet3x3(m[1][1], m[1][2], m[1][3], m[2][1], m[2][2], m[2][3], m[3][1], m[3][2], m[3][3]);
31-
adj[0][1] = -spvDet3x3(m[0][1], m[0][2], m[0][3], m[2][1], m[2][2], m[2][3], m[3][1], m[3][2], m[3][3]);
32-
adj[0][2] = spvDet3x3(m[0][1], m[0][2], m[0][3], m[1][1], m[1][2], m[1][3], m[3][1], m[3][2], m[3][3]);
33-
adj[0][3] = -spvDet3x3(m[0][1], m[0][2], m[0][3], m[1][1], m[1][2], m[1][3], m[2][1], m[2][2], m[2][3]);
34-
35-
adj[1][0] = -spvDet3x3(m[1][0], m[1][2], m[1][3], m[2][0], m[2][2], m[2][3], m[3][0], m[3][2], m[3][3]);
36-
adj[1][1] = spvDet3x3(m[0][0], m[0][2], m[0][3], m[2][0], m[2][2], m[2][3], m[3][0], m[3][2], m[3][3]);
37-
adj[1][2] = -spvDet3x3(m[0][0], m[0][2], m[0][3], m[1][0], m[1][2], m[1][3], m[3][0], m[3][2], m[3][3]);
38-
adj[1][3] = spvDet3x3(m[0][0], m[0][2], m[0][3], m[1][0], m[1][2], m[1][3], m[2][0], m[2][2], m[2][3]);
39-
40-
adj[2][0] = spvDet3x3(m[1][0], m[1][1], m[1][3], m[2][0], m[2][1], m[2][3], m[3][0], m[3][1], m[3][3]);
41-
adj[2][1] = -spvDet3x3(m[0][0], m[0][1], m[0][3], m[2][0], m[2][1], m[2][3], m[3][0], m[3][1], m[3][3]);
42-
adj[2][2] = spvDet3x3(m[0][0], m[0][1], m[0][3], m[1][0], m[1][1], m[1][3], m[3][0], m[3][1], m[3][3]);
43-
adj[2][3] = -spvDet3x3(m[0][0], m[0][1], m[0][3], m[1][0], m[1][1], m[1][3], m[2][0], m[2][1], m[2][3]);
44-
45-
adj[3][0] = -spvDet3x3(m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2], m[3][0], m[3][1], m[3][2]);
46-
adj[3][1] = spvDet3x3(m[0][0], m[0][1], m[0][2], m[2][0], m[2][1], m[2][2], m[3][0], m[3][1], m[3][2]);
47-
adj[3][2] = -spvDet3x3(m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2], m[3][0], m[3][1], m[3][2]);
48-
adj[3][3] = spvDet3x3(m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2]);
49-
50-
// Calculate the determinant as a combination of the cofactors of the first row.
51-
float det = (adj[0][0] * m[0][0]) + (adj[0][1] * m[1][0]) + (adj[0][2] * m[2][0]) + (adj[0][3] * m[3][0]);
52-
53-
// Divide the classical adjoint matrix by the determinant.
54-
// If determinant is zero, matrix is not invertable, so leave it unchanged.
55-
return (det != 0.0f) ? (adj * (1.0f / det)) : m;
56-
}
57-
588
struct Camera
599
{
6010
float4x4 projection;
11+
float4x4 projectionInverse;
12+
uint kernelSize;
6113
};
6214

6315
struct SSAOParams
@@ -79,7 +31,7 @@ static inline __attribute__((always_inline))
7931
float3 getViewPos(thread const float& depth, thread const float2& uv, constant Camera& camera)
8032
{
8133
float4 clipPos = float4((uv * 2.0) - float2(1.0), depth, 1.0);
82-
float4 viewPos = spvInverse4x4(camera.projection) * clipPos;
34+
float4 viewPos = camera.projectionInverse * clipPos;
8335
return viewPos.xyz / float3(viewPos.w);
8436
}
8537

@@ -111,28 +63,28 @@ float calculatePixelAO(thread const float2& uv, constant Camera& camera, texture
11163
float3 bitangent = cross(tangent, viewNormal);
11264
float3x3 TBN = float3x3(float3(tangent), float3(bitangent), float3(viewNormal));
11365
float occlusion = 0.0;
114-
for (int i = 0; i < 64; i++)
66+
for (int i = 0; i < int(camera.kernelSize); i++)
11567
{
11668
float3 samplePos = TBN * kernel0.samples[i].xyz;
11769
samplePos = viewPos + (samplePos * 1.0);
11870
float4 offset = camera.projection * float4(samplePos, 1.0);
119-
float _197 = offset.w;
120-
float4 _198 = offset;
121-
float2 _201 = _198.xy / float2(_197);
122-
offset.x = _201.x;
123-
offset.y = _201.y;
124-
float4 _206 = offset;
125-
float2 _211 = (_206.xy * 0.5) + float2(0.5);
126-
offset.x = _211.x;
127-
offset.y = _211.y;
71+
float _201 = offset.w;
72+
float4 _202 = offset;
73+
float2 _205 = _202.xy / float2(_201);
74+
offset.x = _205.x;
75+
offset.y = _205.y;
76+
float4 _210 = offset;
77+
float2 _215 = (_210.xy * 0.5) + float2(0.5);
78+
offset.x = _215.x;
79+
offset.y = _215.y;
12880
float sampleDepth = depthTex.sample(depthTexSmplr, offset.xy).x;
12981
float param_3 = sampleDepth;
13082
float2 param_4 = offset.xy;
13183
float3 sampleViewPos = getViewPos(param_3, param_4, camera);
13284
float rangeCheck = smoothstep(0.0, 1.0, 1.0 / abs((viewPos.z - sampleViewPos.z) - 0.00999999977648258209228515625));
13385
occlusion += (float(sampleViewPos.z >= (samplePos.z + 0.00999999977648258209228515625)) * rangeCheck);
13486
}
135-
occlusion = 1.0 - ((occlusion / 64.0) * 1.5);
87+
occlusion = 1.0 - ((occlusion / float(camera.kernelSize)) * 1.5);
13688
return occlusion;
13789
}
13890

shaders/compiled/SSAO.frag.spv

236 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)