Distance Field Ambient Occlusion (Movable Skylight shadowing)

This doesn’t seem to work. If I enable it and close the editor, it will be set to true in DefaultEngine.ini, and if I then start the editor again and open the project settings, it will be disabled.

That’s the bug I fixed just now in cl 2125165, can you grab latest again? I’m not sure how often p4 changes are propagated to latest on Github, might be some latency.

I downloaded latest 4.3 from github, but it seems like the issue is still here.

And that checkbox in project settings keep unchecking, even though there is proper setting in ini file.

Could this be related:


Nah different issue. We’ll just have to wait for the fix to get propagated to github. You’ll know when it has because the project setting checkbox will stick.

Don’t wnt to make you sad but:

I guess you talk about this change. I downloaded code from github when this commit was already in, and that issue was still present ;(.

Sorry for being obscure with my reply, but is there a way to make it not update every frame? That’s all I was looking for, would be suitable for dynamic scenes as you could force it to update only when required, saving a bit of performance when nothing is changing.

Could you give an overview of how it works? I’m very curious about the implementation. You said the static meshes are represented into signed distance fields. I can only imagine those being volume textures, where each texel stores the signed distance to the closest surface in the static mesh, is that correct?

What even is a signed distance field by the way? :eek:

That does make me sad. I’m looking into it

Only the pixels on your screen are considered for lighting, so if you moved the camera around you would quickly see areas that did not have lighting computed. It doesn’t simulate the entire level. The algorithm already reuses shading work on surfaces that do not move between frames.

Sure thing. High level view is

Generate signed distance field for each UStaticMesh, store in a volume texture tightly bounding the mesh. The distance field stores the distance to the nearest surface, with sign indicating inside or outside.

AO sample points are placed in world space using a Surface Cache, which places more samples in corners where AO is changing quickly. This is basically a GPU implementation of the standard Irradiance Caching algorithm used in offline raytracing. Irradiance caching isn’t inherently parallelizable because each sample placement relies on the other samples placed before it. This is solved by doing multiple passes, with a sample grid aligned to the screen. The first pass only checks every 500 pixels of the screen for whether shading is needed, next pass every 250 pixels, etc. Shading is needed if no existing Surface Cache samples cover that position with a positive weight. Surface Cache samples from last frame are fed in, with some percent trimmed out to support dynamic scene changes (AO will converge over multiple frames).

The actual AO computation that happens at these sparse points from the Surface Cache is done by cone-stepping through the per-object distance fields. We trace 9 cones covering the hemisphere, accumulate the min visibility across all objects affecting the sample point. This is a super slow operation, which relies on the sparse sampling from the Surface Cache to make it realtime. Distance fields are nice to cone trace (compared to most other data structures) because you just do a series of sphere-occlusion tests along the cone axis. At each sample point on the cone axis you find the distance to the nearest surface from that object’s distance field, and compute overall sphere occlusion using a heuristic. The cone visibility is the min of the sphere visibilities. We track the min distance to occluding surface for the shading point which is needed for the Surface Cache algorithm, it indicates how much area that sample is representative over (and therefore no other shading is necessary).

Then we interpolate all the generated AO Surface Cache samples onto the pixels of the screen by splatting the samples and normalizing with the final weight. This happens at half res. The tolerances of the Surface Cache interpolation are increased which effectively smooths the lighting in world space, in a way that preserves lighting details in corners.

The resulting AO is really unstable at this point, so we apply a temporal filter to stabilize it, a gap-filling pass for any pixels that still weren’t covered with a valid AO sample, and finally a bilateral upsample. Then it is applied to the diffuse lighting of the Movable Skylight.

All of this is like 15 unique shaders, about half of which are compute. All of the lighting data structures exist only on the GPU, the CPU doesn’t even know how much work there is to do, so Draw/DispatchIndirect is used a lot. There’s still a fair amount of optimization potential, as the inner loop cone-stepping is pretty brute force.

The key benefits of this implementation are:

  • Computes lighting on surfaces, not volumes. This allows much higher quality than something like LPV used for sky occlusion where you are limited by the volume texture resolution that lighting is computed in, and you don’t know the surface normal anymore.
  • Thin surfaces are represented well. The per-object distance field allows enough resolution to handle thin surfaces like walls which would disappear in a voxel based approach.
  • Less work is done where less work is needed - static and flat surfaces cost less, while still supporting dynamic scene changes. Even in games that need fully dynamic lighting, not everything is moving all the time so you don’t want to pay for that.

The weaknesses:

  • Limited transfer distance, it doesn’t scale up to global illumination
  • Requires rigid objects for the distance field representation, only uniform scaling of mesh instances
  • Cost can be much higher depending on scene content, a field of grass would be pretty much worst case (no interpolation).

Thanks, that was a nice read. But boy, that’s quite more involved than I was expecting! No wonder it takes so much frame time.

There’s one part I didn’t get: since there is a field per mesh, how are they iterated? Are all fields loaded at once and the GPU picks the ones that intersect with the current sample point or is it the other way around, iterating over each field and finding out which sample points fall inside them?

I also don’t quite understand why they need to be signed. Wouldn’t just the distance to the closest surface be enough? Why is it needed to know whether a point is inside the object or not? Wouldn’t unsigned fields eliminate the need for closed meshes?

I ended up using the rasterizer to scatter each object into tile lists by rendering a bounding sphere. It turned out to be 10x faster than a tiled gather implementation. There are a lot of small samples, which tiled culling doesn’t do well with.

The sign is required for interpolation to work correctly. The magic of using distance fields to represent surfaces is that you can store the distance field at a pretty low resolution, and reconstruct the original surface (distance = 0) after interpolation really accurately (except on corners, they get rounded out). If you store unsigned distance, the distance field will never be 0 after interpolation.

Oh, I didn’t realise it was being done in the traditional deferred way, as usually this sort of thing is done globally from what I’ve seen. Looking forward to messing around with it, and thanks again for the reply. Glad to hear about the optimisation also, because now you’ve explained it there’s no real need for a static version anymore in my head.

I downloaded latest build from GitHub and now it works, but I discovered another bug.

When you enable Distance Fields in rendering settings and place any full meshes (that is fully closed) this happens in viewport:

The black walls are BPS and the black mesh in middle in static mesh without one face.

Disabling this option put everything to normal (or just not using closed meshes).

Will landscape support be added in the future?

Love how it looks btw. Really great work.

I think it can work with landscape, the main challenge is getting the distance field volumes to tightly bound each section. Large objects tend to cause artifacts as they don’t get high enough resolution, because volume textures don’t scale up very well (memory gets out of hand quickly).

Doc is now up!

And a video of it in action (Fortnite)

Yes, but Distance Field AO doesn’t simulate color transmission and color bleeding (GI), so it’s just a realtime AO solution really :slight_smile: It’s useful for dynamic scenes and looks great, but it doesn’t give us multiple lighting paths. I just wonder why did Epics chose to keep developing SVOGI even when it was obvious that it’s too heavy for current gen? Why not integrate LPVs instead so we would have a real working alternative for static lighting?

I’m sorry for such a rant and please don’t take it personal. I realy love working with UE4 and we are starting a project with this engine, but almost all other AAA engines do have a working realtime GI solution and we still don’t and it looks like LPVs in UE4 are not getting proper care any time soon.

Awesome video!

I particularly liked watching what happened when you started deleting stuff from the scene! Amazing!

Thanks for sharing Daniel!

[FONT=Comic Sans MS]:heart: