Skip to content

Instantly share code, notes, and snippets.

@jdupuy
Last active December 30, 2024 13:11
Show Gist options
  • Select an option

  • Save jdupuy/4c6e782b62c92b9cb3d13fbb0a5bd7a0 to your computer and use it in GitHub Desktop.

Select an option

Save jdupuy/4c6e782b62c92b9cb3d13fbb0a5bd7a0 to your computer and use it in GitHub Desktop.
Sampling Visible GGX Normals with Spherical Caps
// Helper function: sample the visible hemisphere from a spherical cap
vec3 SampleVndf_Hemisphere(vec2 u, vec3 wi)
{
// sample a spherical cap in (-wi.z, 1]
float phi = 2.0f * M_PI * u.x;
float z = fma((1.0f - u.y), (1.0f + wi.z), -wi.z);
float sinTheta = sqrt(clamp(1.0f - z * z, 0.0f, 1.0f));
float x = sinTheta * cos(phi);
float y = sinTheta * sin(phi);
vec3 c = vec3(x, y, z);
// compute halfway direction;
vec3 h = c + wi;
// return without normalization as this is done later (see line 25)
return h;
}
// Sample the GGX VNDF
vec3 SampleVndf_GGX(vec2 u, vec3 wi, vec2 alpha)
{
// warp to the hemisphere configuration
vec3 wiStd = normalize(vec3(wi.xy * alpha, wi.z));
// sample the hemisphere
vec3 wmStd = SampleVndf_Hemisphere(u, wiStd);
// warp back to the ellipsoid configuration
vec3 wm = normalize(vec3(wmStd.xy * alpha, wmStd.z));
// return final normal
return wm;
}
#if 0 // alternative version from the two functions above: single function version
vec3 SampleVndf_GGX(vec2 u, vec3 wi, vec2 alpha)
{
// warp to the hemisphere configuration
vec3 wiStd = normalize(vec3(wi.xy * alpha, wi.z));
// sample a spherical cap in (-wi.z, 1]
float phi = (2.0f * u.x - 1.0f) * M_PI;
float z = fma((1.0f - u.y), (1.0f + wiStd.z), -wiStd.z);
float sinTheta = sqrt(clamp(1.0f - z * z, 0.0f, 1.0f));
float x = sinTheta * cos(phi);
float y = sinTheta * sin(phi);
vec3 c = vec3(x, y, z);
// compute halfway direction as standard normal
vec3 wmStd = c + wiStd;
// warp back to the ellipsoid configuration
vec3 wm = normalize(vec3(wmStd.xy * alpha, wmStd.z));
// return final normal
return wm;
}
#endif
#if 0 // world-space isotropic-only version
vec3 SampleVndf_GGX(vec2 u, vec3 wi, float alpha, vec3 n)
{
// decompose the vector in parallel and perpendicular components
vec3 wi_z = n * dot(wi, n);
vec3 wi_xy = wi - wi_z;
// warp to the hemisphere configuration
vec3 wiStd = normalize(wi_z - alpha * wi_xy);
// sample a spherical cap in (-wiStd.z, 1]
float wiStd_z = dot(wiStd, n);
float phi = (2.0f * u.x - 1.0f) * M_PI;
float z = (1.0f - u.y) * (1.0f + wiStd_z) - wiStd_z;
float sinTheta = sqrt(clamp(1.0f - z * z, 0.0f, 1.0f));
float x = sinTheta * cos(phi);
float y = sinTheta * sin(phi);
vec3 cStd = vec3(x, y, z);
// reflect sample to align with normal
vec3 up = vec3(0, 0, 1);
vec3 wr = n + up;
vec3 c = dot(wr, cStd) * wr / wr.z - cStd;
// compute halfway direction as standard normal
vec3 wmStd = c + wiStd;
vec3 wmStd_z = n * dot(n, wmStd);
vec3 wmStd_xy = wmStd_z - wmStd;
// warp back to the ellipsoid configuration
vec3 wm = normalize(wmStd_z + alpha * wmStd_xy);
// return final normal
return wm;
}
// benefits:
// - no need for moving to tangent space
// - it avoids the need for an orthonormal basis
// - it's (slightly) faster than the general version
#endif
@guoxx
Copy link
Copy Markdown

guoxx commented Jul 11, 2023

Hello Jonathan,

Great job on the code! I've been looking at it and appreciate you sharing this.
I'm curious about line 6 in your code. You used fma to calculate z. Is it intended to prevent the errors caused by float point precision?
If I break down that part, it seems to be:

float z = 1 - u.y - u.y * wi.z;

Does this seem right? Looking forward to your input. Thanks again!

@jdupuy
Copy link
Copy Markdown
Author

jdupuy commented Jul 17, 2023

Hi,

float z = 1 - u.y - u.y * wi.z;

Yes this is right.

@WeakKnight
Copy link
Copy Markdown

Hi,

float z = 1 - u.y - u.y * wi.z;

Yes this is right.

Can we use this expanded term instead of fma without introducing precision problem?

@jdupuy
Copy link
Copy Markdown
Author

jdupuy commented Jul 17, 2023

I would certainly think so because fma implementations are generally less precise than the expanded form.
But don't necessarily take my word for it, feel free to experiment on your side :)

@dahmadjid
Copy link
Copy Markdown

what would the pdf code looks like for this ?

@julcst
Copy link
Copy Markdown

julcst commented Sep 24, 2024

The world-space isotropic-only version sometimes generated NaNs for me, to prevent this I added two checks which work for me:

vec3 SampleVndf_GGX(vec2 u, vec3 wi, float alpha, vec3 n)
{
+   // Dirac function for alpha = 0
+   if (alpha == 0.0f) return n;
    // decompose the vector in parallel and perpendicular components
    vec3 wi_z = n * dot(wi, n);
    vec3 wi_xy = wi - wi_z;
    // warp to the hemisphere configuration
    vec3 wiStd = normalize(wi_z - alpha * wi_xy);
    // sample a spherical cap in (-wiStd.z, 1]
    float wiStd_z = dot(wiStd, n);
    float phi = (2.0f * u.x - 1.0f) * M_PI;
    float z = (1.0f - u.y) * (1.0f + wiStd_z) - wiStd_z;
    float sinTheta = sqrt(clamp(1.0f - z * z, 0.0f, 1.0f));
    float x = sinTheta * cos(phi);
    float y = sinTheta * sin(phi);
    vec3 cStd = vec3(x, y, z);
    // reflect sample to align with normal
    vec3 up = vec3(0, 0, 1);
    vec3 wr = n + up;
-   vec3 c = dot(wr, cStd) * wr / wr.z - cStd;
+   // prevent division by zero
+   float wrz_safe = max(wr.z, 1e-6);
+   vec3 c = dot(wr, cStd) * wr / wrz_safe - cStd;
    // compute halfway direction as standard normal
    vec3 wmStd = c + wiStd;
    vec3 wmStd_z = n * dot(n, wmStd);
    vec3 wmStd_xy = wmStd_z - wmStd;
    // warp back to the ellipsoid configuration
    vec3 wm = normalize(wmStd_z + alpha * wmStd_xy);
    // return final normal
    return wm;
}

Are these correct?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment