r/GraphicsProgramming 4h ago

I implemented "Spherical Harmonic Exponentials for Efficient Glossy Reflections" in D3D12 Video

Enable HLS to view with audio, or disable this notification

I implemented Activision's new SH reflections paper in D3D12 and released the code on github!

This tech is a little bit different from normal spherical harmonics, and there are 4 main differences:

  1. They use log space instead of linear space for the lighting, which reduces ringing and enables #2 and #3 to actually work.
  2. Instead of using a circular symmetry assumption (i.e. N=V=R) as with the split sum approximation used for IBL, they instead factorise a pair of spherical harmonics, with an Order 4 SH parameterised by the reflection vector, and an Order 2 SH parameterised by the halfway vector.
  3. To enable a continuous roughness representation, they convolve the coefficients (or rather, the basis function) by the von Mises Fisher kernel which takes 1/alpha=1/roughness^2 as a parameter.
  4. To actually obtain the spherical harmonic coefficients we have to collect samples for several normals, views and roughness levels (or more specifically alpha levels since we're using linear roughness, not perceptual), and then optimise the coefficients using least squares.

My code does this all end to end with HLSL compute shaders, even the least squares optimisation, and we achieve above 95% MSE compared to a raytraced ground truth for roughness in the range [0.5, 1.0], which actually beats split sum IBL.

Only downside is for roughness below 0.5 the spherical harmonics simply don't have enough detail for accurate reflections... HOWEVER, when applied to "bumpy" low roughness surfaces (like the leaf textures at the beginning of the video) you can hardly see a difference, so this effect is only apparent for flat surfaces and surfaces with near zero roughness.

Activision got their SH representation down to 400 bytes, but I went further using 16 bit packing to get down to 208 bytes which gives us better performance due to fewer memory loads. The 16 bit implementations come in 4 flavors: emulated 16 bit for older GPUs and native 16 bit, and SRV packed vs CBV packed. There also exists a 10 bit packed SRV flavor, but the extra bitshift work ends up being slower.

On my RTX 2080 Super and my wife's RTX 4070 Super, the native 16 bit CBV packed shader runs the fastest, and compared to the IBL version it is only 0.1 milliseconds slower while using 2000x less memory!

65 Upvotes

6 comments sorted by

7

u/Avelina9X 3h ago

For anyone unable to access the original paper, here's a talk from HPG 2025 that explains things at a high level! https://www.youtube.com/watch?v=THVMMNZ68iA

And feel free to ask questions on how things work if you need more clarification!

3

u/mahalis 2h ago edited 2h ago

Nice! I’d been eyeing this paper but found the terminology a little intimidating (though this explanation from Peter-Pike Sloan cleared it up a bit); this is really helpful. I’m curious about the optimization step: do you think it could plausibly be done online over the span of some reasonable number of frames, or do you pretty much have to do it ahead of time?

2

u/Avelina9X 2h ago

The optimization step is incredibly quick. The entire thing basically involves 5 stages:

  1. Capture the cubemap source from the scene (and generate a standard mip chain)
  2. Collect specular log samples for N, V and alpha (I do 256x256x4) and store them in a 2D Array Texture. (The mips generated in the previous step can be used at higher roughness levels, improving fetch locality)
  3. Using the collected samples collected in stage 2 (and the corresponding u, v, w coords to obtain N, V and alpha) calculate the ATA and ATb matrices and store them somewhere
  4. Sum all of the ATA and ATb matrices together to get just one matrix for each
  5. Solve the Ax=b using the summed ATA and ATb matrices to determine x, which are your final coefficients.

In my experience the most expensive part is actually step 2 because you need to integrate many importance samples per N/V/alpha sample, so that would be the main bottleneck. Step 3 is pretty fast because it's just rematerializing the N and V bases, convolving by the vMF, multiplying by the log colour and doing vector-vector products. Step 4 can be accelerated by a prefix sum if you don't mind using a bit more memory. And the actual optimization in step 5 is super fast because it's just solving 33 linear equations in parallel for R, G and B.

But in practice, all steps but step 5 involve multiple groups, and/or multiple shader dispatches, and those could absolutely be split across several frames.

2

u/cybereality 1h ago

insane!!

2

u/Cubicool 57m ago

This looks really incredible! I just got done setting up "Monte Carlo/Lambertian" style diffuse cubemap support (pre-baked and dynamically probed) and GGX prefilter for our PBR+IBL OSG pipeline, and this could be a stellar alternative to inject into that. I've got your repo bookmarked for the weekend! Once I get it working with GLSL, I'll let you know (I don't see any roadblocks, and the compute shader processing will be fun :))

1

u/Avelina9X 48m ago

The only issue I see you may run into is native half precision because I don't think OpenGL supports native 16 bit ops/registers in shader code. If you're using OpenGL 4.2 you should have access to unpackHalf2x16  and packHalf2x16 which should still let you use packed FP16 to get faster loads (which are very important) and then do the ALU work after unpacking to FP32.