feat: add screen-space ambient occlusion (SSAO)#993
Conversation
|
@hubbardp this is very exciting! Looks great from a quick look so far |
| // World→UV scale: wClip is -P.z under perspective and 1 under ortho. | ||
| float wClip = uProjection[2][3] * P.z + uProjection[3][3]; | ||
| float screenRadius = uRadius * uProjection[1][1] / (2.0 * wClip); | ||
| screenRadius = min(screenRadius, MAX_KERNEL_FRACTION); |
There was a problem hiding this comment.
In the link I posted, I can't see any visible difference when changing the radius value, I'm wondering if the value is always larger than MAX_KERNEL_FRACTION
There was a problem hiding this comment.
Thanks for the observation. I checked in what I think is an improvement; see more with your later comment, below.
|
For those who want to try the new functionality without building the code, it's running here: |
|
@hubbardp neuroglancer github actions are set up to create a deployment for every PR, available if you click "view details", it's definitely not obvious: |
|
What is the reason for disabling the effect on highlighted segments? It makes the highlighting rather jarring, though I haven't tested with the alternative behavior. |
I tried not disabling the SSAO darkening on highlighted segments, and they were too dark to see when highlighted. Personally, I like the brighter highlighting. I found that before SSAO, sometimes the randomly-chosen segment color was bright enough that the highlighting seemed barely distinguishable. |
Thanks for the test case. I pushed a fix for the anisotropy. I think it looks better from all view angles.
I have not yet updated http://neuroglancer-ssao.janelia.org/ so use the version from the GitHub action. |
|
awesome thank you agreed looks better on my localhost version! |
seankmartin
left a comment
There was a problem hiding this comment.
Thanks for the changes! My comments are more related to the integration in neuroglancer, as I'll admit that I haven't dug into the SSAO implementation compared to the paper. Are there any known simplifications or deviations taken here over the paper description?
|
|
||
| SSAO is limited to mesh surfaces because only `MeshLayer` and | ||
| `MultiscaleMeshLayer` supply a view-space normal via the three-argument | ||
| `emit(color, pickId, viewNormal)` form. All other opaque geometry (skeletons, |
There was a problem hiding this comment.
I wouldn't see it as blocking, but in particular for the single meshes, I think it could be useful to expand here why they are not supported. Since they have normaIs, I imagine it's because the overhead in making the shader in the single mesh layer shader emitter aware and some duplication of the normal handling basically mimicking the changes made in mesh/frontend.ts
There was a problem hiding this comment.
Thanks for the suggestion. I checked in a revised README that expands on the reasons for omitting annotations, skeletons and single-mesh layers. For the latter, I could add support with some refactoring, so my preference would be to leave it for future work. Let me know what you think.
There was a problem hiding this comment.
This is perfect, thank you for updating the docs, looks great. No need to add right now in my opinion, just wanted to document why it wasn't added.
| // Referenced by glsl_perspectivePanelEmitWithNormals; declared here so any | ||
| // shader using this emitter (mesh, annotation, etc.) gets it without having | ||
| // to know the emit body's internal dependencies. | ||
| builder.addUniform("highp float", "uHighlighted"); |
There was a problem hiding this comment.
I think the use of a uniform for highlighted to disable AO in the shader will up being a bit limited in scope. This assumes that in a single draw everything is either highlighted or unhighlighted, which is a bit limiting.
Perhaps this could move to the responsibility of the mesh layer if this functionality is needed. So meshes zero-out their view normal to the no AO sentinel version. And then the perspective shaders don't have to worry about this anymore.
If other layers want to disable AO dynamically, they can use the same pattern
There was a problem hiding this comment.
Thanks for the suggestion. I checked in code that implements what I think is your idea.
There was a problem hiding this comment.
Yes exactly what I had in mind, thank you very much for the change
| export const glsl_perspectivePanelEmitWithNormals = ` | ||
| void emit(vec4 color, highp uint pickId, vec3 viewNormal) { | ||
| out_color = color; | ||
| float zValue = 1.0 - gl_FragCoord.z; | ||
| out_z = vec4(zValue, zValue, zValue, 1.0); | ||
| float pickIdFloat = float(pickId); | ||
| out_pickId = vec4(pickIdFloat, pickIdFloat, pickIdFloat, 1.0); | ||
| // Highlighted objects collapse to the zero sentinel so SSAO leaves them alone. | ||
| vec3 packedNormal = (1.0 - uHighlighted) * (normalize(viewNormal) * 0.5 + 0.5); | ||
| out_normal = vec4(packedNormal, 1.0); | ||
| } | ||
| void emit(vec4 color, highp uint pickId) { | ||
| out_color = color; | ||
| float zValue = 1.0 - gl_FragCoord.z; | ||
| out_z = vec4(zValue, zValue, zValue, 1.0); | ||
| float pickIdFloat = float(pickId); | ||
| out_pickId = vec4(pickIdFloat, pickIdFloat, pickIdFloat, 1.0); | ||
| // Zero-RGB sentinel; alpha=1 so the source overwrites dst under | ||
| // blend(SRC_ALPHA, ONE_MINUS_SRC_ALPHA). | ||
| out_normal = vec4(0.0, 0.0, 0.0, 1.0); |
There was a problem hiding this comment.
I think it would be nice if we could reduce the duplication in the bodies here so changes propagate if ever needed and to clarify what's changing between emitters. Maybe something like
const glslEmitBase = `
out_color = color;
float zValue = 1.0 - gl_FragCoord.z;
out_z = vec4(zValue, zValue, zValue, 1.0);
float pickIdFloat = float(pickId);
out_pickId = vec4(pickIdFloat, pickIdFloat, pickIdFloat, 1.0);`;
and then it could be used in both the 2-arg and 3-arg emits, and also be replaced in glsl_perspectivePanelEmit. Don't feel strongly on the name of the var glslEmitBase
There was a problem hiding this comment.
Thanks, I checked in this idea for factoring out shared code.
| // Mixed mesh + volume scenes are not yet supported; the opaque pass and | ||
| // NORMAL writes already happened from the with-normals emitter (minor | ||
| // waste, not a correctness issue). Notify once per panel lifetime via | ||
| // both the console and a dismissable status banner. |
There was a problem hiding this comment.
I'm unsure why volume rendering disables SSAO. SSAO happens in opaque rendering only, so I don't see how the later transparent rendering is interfered with. Could be missing something of course
There was a problem hiding this comment.
I did another test of the code modified to allow SSAO with volume rendering, and the result is not right. The problem is that SSAO involves a final compositing step that multiplies the existing color buffer by the AO darkening. By the time that step runs, the volume has been blended into the color buffer, so AO darkens the volume contribution, too, and not just the mesh underneath. The part of the volume in front of the mesh should hide those AO details, but instead it gets darkened along with them. It should be possible to restructure the pipeline so the volume rendering is added after the SSAO compositng, but that would add to the scale of the code changes. Let me know if you think it is worth doing.
There was a problem hiding this comment.
Thanks @hubbardp, makes sense! In my opinion it's fine for now to leave it, I was just conceptually missing why they weren't compatible. We can make a note/issue about it and come back to restructuring the pipeline
| ); | ||
| const radius = ssaoRadius * this.navigationState.zoomFactor.value; | ||
|
|
||
| this.ssaoManager.render( |
There was a problem hiding this comment.
Just wanted to note that as the complexity of the panel.ts file has increased, moving part of the rendering responsibility out of this file, as done here, seems reasonable and could be a good idea for a later refactoring of the volume rendering path
| // NORMAL writes already happened from the with-normals emitter (minor | ||
| // waste, not a correctness issue). Notify once per panel lifetime via | ||
| // both the console and a dismissable status banner. | ||
| if (ssaoRequested && hasVolumeRendering && !this.ssaoVolumeWarned) { |
There was a problem hiding this comment.
Related to https://github.com/google/neuroglancer/pull/993/changes#r3267587804, if we could leave AO on with volume rendering - maybe we could instead enable this warning message if you have an AO supported mesh layer but with transparent rendering for that layer?
There was a problem hiding this comment.
Thanks for the suggestion. A common use of transparency is silhouette rendering for ROIs, and in my tests, it looks fine with SSAO. So I feel that a UI warning would not be helpful. If you disagree, then it might be worth scheduling the work of restructuring the pipeline so transparency is added after the SSAO compositing. Other than some additional code, the main cost of that approach is one more RGB FBO.
There was a problem hiding this comment.
Sounds good, thanks. On rereading what I wrote, honestly I wasn't very clear, but really I just meant that if we're enabling volume rendering and SSAO - do we then benefit from replacing the warning here with something else. For example, if SSAO is on but the only mesh layer in the scene is transparent or there are no mesh layers in the scene, then that could also be a warning. But with volume rendering and SSAO not permitted to be on together, then I think we just keep the current warning as it is the most important one.
| @@ -0,0 +1,292 @@ | |||
| /** | |||
There was a problem hiding this comment.
As a general comment here, I could be missing it but I don't see a test for a case where ao is between 0 and 1, or in other words a non 1x1 size texture example to check sampling from nearby neighbours and having ao < 1.0 if the centre pixel is being occluded by neighbour pixels. Would that be possible to add?
There was a problem hiding this comment.
Thanks for checking. I committed two new tests for these situations.
There was a problem hiding this comment.
Those look great, thank you very much for that, will be really useful for catching any clear regressions.
| SSAO_RADIUS_RANGE.max, | ||
| Math.max(SSAO_RADIUS_RANGE.min, this.viewer.ssaoRadius.value), | ||
| ); | ||
| const radius = ssaoRadius * this.navigationState.zoomFactor.value; |
There was a problem hiding this comment.
removing this operation causes the radius slider to have an effect on my device. It does mean it now becomes softer as you zoom out.
There was a problem hiding this comment.
The old code scaled the radius incorrectly, so the clamping at MAX_KERNEL_FRACTION was happening too often. I checked in new code that clarifies how the slider value affects the sampling kernel and the falloff factor. I think it looks reasonable when zooming out, and lets the slider add some variation. The new default of 2 for the slider works well in every case I tried. I would not mind removing the slider altogether if it is confusing, but it also seems okay to leave it in for completeness.
|
One other thing that might be nice is if we add some kind of While the regular result with color contribution is: Could help for any later fine tuning, seems like there might be a bit of noise showing through in certain cases for e.g. |
|
|
||
| import type { ShaderBuilder } from "#src/webgl/shader.js"; | ||
|
|
||
| const glsl_gtao = ` |
There was a problem hiding this comment.
if it can be done, I would try defining gtao as a function which should allow not having to split up the shader code into fragment code and fragment main
and call gtao inside the fragment main
There was a problem hiding this comment.
Good idea. I checked in code that I think uses this approach.
| } | ||
|
|
||
| const glsl_blur = ` | ||
| // Bilateral falloff sharpness; tuned for normalized [0,1] depth so that |
There was a problem hiding this comment.
I would put this and glsl_ssaoComposite inside their respective functions
There was a problem hiding this comment.
The new code is using this idea, too.
|
I don't know much about the implementation here, so sorry if the following is just noise. But I was wondering... Could this PR be modified to allow SSAO to be enabled (and perhaps configured) on a per-layer basis? Admittedly, from a conceptual point of view, it should probably apply to the entire scene, not just particular layers. However, that goes a little against the grain of the current neuroglancer UI, which offers per-layer controls for mesh opacity and mesh silhouette settings. The fact that the user can't combine SSAO with opacity/silhouette effects is further motivation for moving its settings into the layer controls. That way, if the user enables SSAO for a layer, then the opacity and silhouette controls would just disappear and the SSAO controls would appear. (That's similar to how the volume rendering controls appear/disappear in If this is implementable, it has the advantage that users could specify different SSAO settings for each layer. I'm sure that violates the basic principles behind SSAO in some sense, but there could be practical reasons that a user might do this, for example, to highlight a particular population of cells without disabling SSAO on them entirely. (A disadvantage is that if users want all of their layers to use consistent SSAO settings anyway, they'd have to separately specify those settings in each layer.) Again, I know little of the implementation, so maybe this isn't feasible. But the fact that the current implementation is able to exclude specific objects from SSAO (e.g. the highlighted cell) made me wonder if SSAO really needs to be a global setting. |
Thanks for the suggestion. The goal of having per-layer control for SSAO sounds appealing, but the details get tricky. SSAO involves darkening a point on one mesh based on how much of the hemisphere above it is blocked by other meshes. What makes it fast is that the other meshes are found from the current state of the depth buffer. SSAO also involves a compositing step to do the actual darkening, once the depth buffer has been used to determine how much darkening is appropriate. Say there are three layers---A, B, C---and A and C opt into SSAO. What does it mean that B has opted out?
If the answer to question 2 is "yes" then the current sentinel mechanism could be reused; layer B would be treated like an annotation or skeleton layer. If the answer is "no" then the solution would require more complicated depth-buffer management that would not be trivial. I think it is important to focus on the most beneficial use cases for SSAO: using it on all the meshes to provide perceptual cues that help a user see and understand the data. Refinements to handle other cases make more sense as optional follow-on work. |
The debugging mode is a good idea, and I added a The noise you notice is real, and is part of the GTAO algorithm. A hash function based on the fragment coordinates jitters the angle and step for the horizon-angle sampling, and it works well in general. But the rejection of blurring across depth boundaries can reject enough samples that the jitter becomes visible sometimes. Some sort of temporal adjustment would help but would require additional machinery and an FBO to maintain state. In my experience, the noise is noticeable mainly on broad, smooth surfaces, and since such surfaces are uncommon in neuroscience data sets I think the current noise is acceptable. |
|
I think I have now addressed all the comments from reviewers. |
Thanks for all the changes! Sorry for the delay, I'll try to get around to taking a last look soon. Ideally I'd also like to see @jbms has any further thoughts on this one since it is a change that affects the neuroglancer state |
|
A few comments:
|








Summary
Screen-space ambient occlusion (SSAO) simulates shadows on 3D mesh surfaces by darkening crevices and concavities where ambient light would be occluded. It adds depth cues that help us perceive shapes, and it makes the display more appealing. SSAO is an efficient post-processing effect applied to the perspective view after opaque geometry is drawn. See
src/ssao/README.mdfor more details and example images.Screenshots
Usage
qto toggle SSAO on and off.Known limitations
Performance
Algorithm
GTAO (Ground Truth Ambient Occlusion): Jimenez et al., "Realtime Strategies for Accurate Indirect Occlusion", SIGGRAPH 2016
Testing
src/ssao/shaders.browser_test.tscover composite math, GTAO and composite sentinel paths, blurring pass.