A renderer's roughness from a surface's GGX alpha¶
The perceptual roughness a renderer takes (lusine.render.pbr's `roughness`) from a surface's GGX alpha, the width of its facet normals' distribution as the library states it (lusine.materials.microfacets' microfacet_alpha): roughness = sqrt(alpha), the inverse of pbr's alpha = roughness^2.
A physical alpha is the library's; the perceptual parameter is a renderer's convention, derived here. Source: Burley, Physically-Based Shading at Disney (SIGGRAPH 2012 course notes), section 5.4 (alpha = roughness^2); Karis, Real Shading in Unreal Engine 4 (SIGGRAPH 2013); Walter et al., Microfacet Models for Refraction (EGSR 2007). Checked against a reference in numpy, which has its own tests.
- Source
- Burley, Physically-Based Shading at Disney (SIGGRAPH 2012), 5.4; Karis, Real Shading in Unreal Engine 4 (SIGGRAPH 2013).
- Proof
- reference, invariant.
perceptual_roughness.trait
namespace lusine.render;
float perceptual_roughness(float alpha)
{
return sqrt(alpha);
}
The perceptual roughness lusine.render.pbr takes, √α, from a surface's GGX α, the width of its facet normals' distribution as the library states it (lusine.materials.microfacets' microfacet_alpha). pbr squares it back to α. The library stores the physical α, never the perceptual parameter: this kernel derives it.
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import trait_run
from lusine.library_sources import parse_rows
EPS = float(np.finfo(np.float32).eps) # Trait's float is binary32
The kernel¶
The law as the library states it.
print(trait_run.source('render/perceptual_roughness.trait'))
// lusine.render.perceptual_roughness: the perceptual roughness a renderer takes (lusine.render.pbr's `roughness`) from
// a surface's GGX alpha, the width of its facet normals' distribution as the library states it
// (lusine.materials.microfacets' microfacet_alpha): roughness = sqrt(alpha), the inverse of pbr's alpha = roughness^2.
// A physical alpha is the library's; the perceptual parameter is a renderer's convention, derived here.
// Source: Burley, Physically-Based Shading at Disney (SIGGRAPH 2012 course notes), section 5.4 (alpha = roughness^2);
// Karis, Real Shading in Unreal Engine 4 (SIGGRAPH 2013); Walter et al., Microfacet Models for Refraction (EGSR 2007).
// Checked against a reference in numpy, which has its own tests.
namespace lusine.render;
float perceptual_roughness(float alpha)
{
return sqrt(alpha);
}
The program¶
Each section runs the kernel over a lattice of items, one a case.
print(trait_run.law_program('render/perceptual_roughness.trait', 'perceptual_roughness'))
namespace probe;
lusine.version version = { { number = 1 } };
struct item {
host float alpha;
float result_0;
};
lattice<item, 1> items(spacing = 1 m, boundary = wall);
void main() { items.result_0 = lusine.render.perceptual_roughness(items.alpha); }
Reference: every surface's α in the library¶
Each row of microfacets, as committed, is held to √α, to binary32's rounding: a lawn's and a packed sand bed's α = 1 is a roughness of exactly 1.
rows = parse_rows(trait_run.source('materials/generated/look.trait'), 'microfacets')
alpha = np.array([r['microfacet_alpha'] for r in rows])
r = trait_run.law('render/perceptual_roughness.trait', 'perceptual_roughness', alpha=alpha)
want = np.sqrt(np.float32(alpha).astype(float))
assert np.all(np.abs(r - want) <= EPS * want), np.abs(r / want - 1).max()
for row, v in zip(rows, r):
print(f"{row['material']:15s} {row['state']:7s} alpha {row['microfacet_alpha']:.3g} roughness {v:.4f}")
grass solid alpha 1 roughness 1.0000 silicon_dioxide packed alpha 1 roughness 1.0000
Invariant: pbr's square is α again¶
Over 64 α from 0 to 1, the roughness squared, as pbr squares it, is α to a binary32 rounding or two; α = 0, 0.25 and 1 are exact.
a = np.float32(np.random.default_rng(361).uniform(0.0, 1.0, 64)).astype(float)
r = trait_run.law('render/perceptual_roughness.trait', 'perceptual_roughness', alpha=a)
assert np.all(np.abs(r * r - a) <= 2 * EPS * a)
e = trait_run.law('render/perceptual_roughness.trait', 'perceptual_roughness', alpha=[0.0, 0.25, 1.0])
assert list(e) == [0.0, 0.5, 1.0], e
fig, ax = plt.subplots(figsize=(6, 3))
order = np.argsort(a)
ax.plot(a[order], r[order])
ax.set_xlabel('GGX alpha')
ax.set_ylabel('perceptual roughness')
plt.show()