Skip to content

Commit 65024b0

Browse files
authored
Implement OpenPBR's sheen BRDF (#1819)
Add Zeltner-Burley sheen BSDF as a new mode of the MaterialX sheen closure. Simplify math for computing tangent frame aligned with a specified tangent vector. Signed-off-by: Chris Kulla <ckulla@gmail.com>
1 parent 02e2bc3 commit 65024b0

File tree

32 files changed

+165
-31
lines changed

32 files changed

+165
-31
lines changed

src/testrender/sampling.h

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,30 @@ OSL_NAMESPACE_ENTER
1414

1515
struct TangentFrame {
1616
// build frame from unit normal
17-
TangentFrame(const Vec3& n) : w(n)
17+
static TangentFrame from_normal(const Vec3& n)
1818
{
19-
u = (fabsf(w.x) > .01f ? Vec3(w.z, 0, -w.x) : Vec3(0, -w.z, w.y))
20-
.normalize();
21-
v = w.cross(u);
19+
// https://graphics.pixar.com/library/OrthonormalB/paper.pdf
20+
const float sign = copysignf(1.0f, n.z);
21+
const float a = -1 / (sign + n.z);
22+
const float b = n.x * n.y * a;
23+
const Vec3 u = Vec3(1 + sign * n.x * n.x * a, sign * b, -sign * n.x);
24+
const Vec3 v = Vec3(b, sign + n.y * n.y * a, -n.y);
25+
return { u, v, n };
2226
}
2327

2428
// build frame from unit normal and unit tangent
25-
TangentFrame(const Vec3& n, const Vec3& t) : w(n)
29+
// fallsback to an arbitrary basis if the tangent is 0 or colinear with n
30+
static TangentFrame from_normal_and_tangent(const Vec3& n, const Vec3& t)
2631
{
27-
v = w.cross(t);
28-
u = v.cross(w);
32+
Vec3 x = t - n * dot(n, t);
33+
float xlen2 = dot(x, x);
34+
if (xlen2 > 0) {
35+
x *= 1.0f / sqrtf(xlen2);
36+
return { x, n.cross(x), n };
37+
} else {
38+
// degenerate case, fallback to generic tangent frame
39+
return from_normal(n);
40+
}
2941
}
3042

3143
// transform vector
@@ -42,7 +54,6 @@ struct TangentFrame {
4254
}
4355
Vec3 toworld(const Vec3& a) const { return get(a.x, a.y, a.z); }
4456

45-
private:
4657
Vec3 u, v, w;
4758
};
4859

@@ -76,8 +87,7 @@ struct Sampling {
7687
{
7788
to_unit_disk(rndx, rndy);
7889
float cos_theta = sqrtf(std::max(1 - rndx * rndx - rndy * rndy, 0.0f));
79-
TangentFrame f(N);
80-
out = f.get(rndx, rndy, cos_theta);
90+
out = TangentFrame::from_normal(N).get(rndx, rndy, cos_theta);
8191
pdf = cos_theta * float(M_1_PI);
8292
}
8393

@@ -87,8 +97,9 @@ struct Sampling {
8797
float phi = float(2 * M_PI) * rndx;
8898
float cos_theta = rndy;
8999
float sin_theta = sqrtf(1 - cos_theta * cos_theta);
90-
TangentFrame f(N);
91-
out = f.get(sin_theta * cosf(phi), sin_theta * sinf(phi), cos_theta);
100+
out = TangentFrame::from_normal(N).get(sin_theta * cosf(phi),
101+
sin_theta * sinf(phi),
102+
cos_theta);
92103
pdf = float(0.5 * M_1_PI);
93104
}
94105
};

src/testrender/shading.cpp

Lines changed: 134 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ struct MxSheenParams {
207207
Color3 albedo;
208208
float roughness;
209209
// optional
210+
int mode;
210211
ustringhash label;
211212
};
212213

@@ -398,6 +399,7 @@ register_closures(OSL::ShadingSystem* shadingsys)
398399
CLOSURE_COLOR_PARAM(MxSheenParams, albedo),
399400
CLOSURE_FLOAT_PARAM(MxSheenParams, roughness),
400401
CLOSURE_STRING_KEYPARAM(MxSheenParams, label, "label"),
402+
CLOSURE_INT_KEYPARAM(MxSheenParams, mode, "mode"),
401403
CLOSURE_FINISH_PARAM(MxSheenParams) } },
402404
{ "uniform_edf",
403405
MX_UNIFORM_EDF_ID,
@@ -589,15 +591,15 @@ struct Phong final : public BSDF, PhongParams {
589591
float cosNO = N.dot(wo);
590592
if (cosNO > 0) {
591593
// reflect the view vector
592-
Vec3 R = (2 * cosNO) * N - wo;
593-
TangentFrame tf(R);
594+
Vec3 R = (2 * cosNO) * N - wo;
594595
float phi = 2 * float(M_PI) * rx;
595596
float sp, cp;
596597
OIIO::fast_sincos(phi, &sp, &cp);
597598
float cosTheta = OIIO::fast_safe_pow(ry, 1 / (exponent + 1));
598599
float sinTheta2 = 1 - cosTheta * cosTheta;
599600
float sinTheta = sinTheta2 > 0 ? sqrtf(sinTheta2) : 0;
600-
Vec3 wi = tf.get(cp * sinTheta, sp * sinTheta, cosTheta);
601+
Vec3 wi = TangentFrame::from_normal(R).get(cp * sinTheta,
602+
sp * sinTheta, cosTheta);
601603
return eval(wo, wi);
602604
}
603605
return {};
@@ -614,7 +616,7 @@ struct Ward final : public BSDF, WardParams {
614616
// get half vector and get x,y basis on the surface for anisotropy
615617
Vec3 H = wi + wo;
616618
H.normalize(); // normalize needed for pdf
617-
TangentFrame tf(N, T);
619+
TangentFrame tf = TangentFrame::from_normal_and_tangent(N, T);
618620
// eq. 4
619621
float dotx = tf.getx(H) / ax;
620622
float doty = tf.gety(H) / ay;
@@ -636,7 +638,7 @@ struct Ward final : public BSDF, WardParams {
636638
float cosNO = N.dot(wo);
637639
if (cosNO > 0) {
638640
// get x,y basis on the surface for anisotropy
639-
TangentFrame tf(N, T);
641+
TangentFrame tf = TangentFrame::from_normal_and_tangent(N, T);
640642
// generate random angles for the half vector
641643
float phi = 2 * float(M_PI) * rx;
642644
float sp, cp;
@@ -809,8 +811,7 @@ struct Microfacet final : public BSDF, MicrofacetParams {
809811
Microfacet(const MicrofacetParams& params)
810812
: BSDF()
811813
, MicrofacetParams(params)
812-
, tf(U == Vec3(0) || xalpha == yalpha ? TangentFrame(N)
813-
: TangentFrame(N, U))
814+
, tf(TangentFrame::from_normal_and_tangent(N, U))
814815
{
815816
}
816817
Color3 get_albedo(const Vec3& wo) const override
@@ -1034,11 +1035,8 @@ struct MxMicrofacet final : public BSDF, MxMicrofacetParams {
10341035
MxMicrofacet(const MxMicrofacetParams& params, float refraction_ior)
10351036
: BSDF()
10361037
, MxMicrofacetParams(params)
1037-
, tf(MxMicrofacetParams::U == Vec3(0)
1038-
|| MxMicrofacetParams::roughness_x
1039-
== MxMicrofacetParams::roughness_y
1040-
? TangentFrame(MxMicrofacetParams::N)
1041-
: TangentFrame(MxMicrofacetParams::N, MxMicrofacetParams::U))
1038+
, tf(TangentFrame::from_normal_and_tangent(MxMicrofacetParams::N,
1039+
MxMicrofacetParams::U))
10421040
, refraction_ior(refraction_ior)
10431041
{
10441042
}
@@ -1398,8 +1396,12 @@ struct MxBurleyDiffuse final : public BSDF, MxBurleyDiffuseParams {
13981396
}
13991397
};
14001398

1401-
struct MxSheen final : public BSDF, MxSheenParams {
1402-
MxSheen(const MxSheenParams& params) : BSDF(), MxSheenParams(params) {}
1399+
// Implementation of the "Charlie Sheen" model [Conty & Kulla, 2017]
1400+
// https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_sheen.pdf
1401+
// To simplify the implementation, the simpler shadowing/masking visibility term below is used:
1402+
// https://dassaultsystemes-technology.github.io/EnterprisePBRShadingModel/spec-2022x.md.html#components/sheen
1403+
struct CharlieSheen final : public BSDF, MxSheenParams {
1404+
CharlieSheen(const MxSheenParams& params) : BSDF(), MxSheenParams(params) {}
14031405

14041406
Color3 get_albedo(const Vec3& wo) const override
14051407
{
@@ -1444,6 +1446,114 @@ struct MxSheen final : public BSDF, MxSheenParams {
14441446
}
14451447
};
14461448

1449+
// Implement the sheen model proposed in:
1450+
// "Practical Multiple-Scattering Sheen Using Linearly Transformed Cosines"
1451+
// Tizian Zeltner, Brent Burley, Matt Jen-Yuan Chiang - Siggraph 2022
1452+
// https://tizianzeltner.com/projects/Zeltner2022Practical/
1453+
struct ZeltnerBurleySheen final : public BSDF, MxSheenParams {
1454+
ZeltnerBurleySheen(const MxSheenParams& params)
1455+
: BSDF(), MxSheenParams(params)
1456+
{
1457+
}
1458+
1459+
#define USE_LTC_SAMPLING 1
1460+
1461+
Color3 get_albedo(const Vec3& wo) const override
1462+
{
1463+
const float NdotV = clamp(N.dot(wo), 1e-5f, 1.0f);
1464+
return Color3(fetch_ltc(NdotV).z);
1465+
}
1466+
1467+
Sample eval(const Vec3& wo, const Vec3& wi) const override
1468+
{
1469+
const Vec3 L = wi, V = wo;
1470+
const float NdotV = clamp(N.dot(V), 0.0f, 1.0f);
1471+
const Vec3 ltc = fetch_ltc(NdotV);
1472+
1473+
const Vec3 localL = TangentFrame::from_normal_and_tangent(N, V).tolocal(
1474+
L);
1475+
1476+
const float aInv = ltc.x, bInv = ltc.y, R = ltc.z;
1477+
Vec3 wiOriginal(aInv * localL.x + bInv * localL.z, aInv * localL.y,
1478+
localL.z);
1479+
const float len2 = dot(wiOriginal, wiOriginal);
1480+
1481+
float det = aInv * aInv;
1482+
float jacobian = det / (len2 * len2);
1483+
1484+
#if USE_LTC_SAMPLING == 1
1485+
float pdf = jacobian * std::max(wiOriginal.z, 0.0f) * float(M_1_PI);
1486+
return { wi, Color3(R), pdf, 1.0f };
1487+
#else
1488+
float pdf = float(0.5 * M_1_PI);
1489+
// NOTE: sheen closure has no fresnel/masking
1490+
return { wi, Color3(2 * R * jacobian * std::max(wiOriginal.z, 0.0f)),
1491+
pdf, 1.0f };
1492+
#endif
1493+
}
1494+
1495+
Sample sample(const Vec3& wo, float rx, float ry, float rz) const override
1496+
{
1497+
#if USE_LTC_SAMPLING == 1
1498+
const Vec3 V = wo;
1499+
const float NdotV = clamp(N.dot(V), 0.0f, 1.0f);
1500+
const Vec3 ltc = fetch_ltc(NdotV);
1501+
const float aInv = ltc.x, bInv = ltc.y, R = ltc.z;
1502+
Vec3 wi;
1503+
float pdf;
1504+
Sampling::sample_cosine_hemisphere(Vec3(0, 0, 1), rx, ry, wi, pdf);
1505+
1506+
const Vec3 w = Vec3(wi.x - wi.z * bInv, wi.y, wi.z * aInv);
1507+
const float len2 = dot(w, w);
1508+
const float jacobian = len2 * len2 / (aInv * aInv);
1509+
const Vec3 wn = w / sqrtf(len2);
1510+
1511+
const Vec3 L = TangentFrame::from_normal_and_tangent(N, V).toworld(wn);
1512+
1513+
pdf = jacobian * std::max(wn.z, 0.0f) * float(M_1_PI);
1514+
1515+
return { L, Color3(R), pdf, 1.0f };
1516+
#else
1517+
// plain uniform-sampling for validation
1518+
Vec3 out_dir;
1519+
float pdf;
1520+
Sampling::sample_uniform_hemisphere(N, rx, ry, out_dir, pdf);
1521+
return eval(wo, out_dir);
1522+
#endif
1523+
}
1524+
1525+
private:
1526+
Vec3 fetch_ltc(float NdotV) const
1527+
{
1528+
// To avoid look-up tables, we use a fit of the LTC coefficients derived by Stephen Hill
1529+
// for the implementation in MaterialX:
1530+
// https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/pbrlib/genglsl/lib/mx_microfacet_sheen.glsl
1531+
const float x = NdotV;
1532+
const float y = std::max(roughness, 1e-3f);
1533+
const float A = ((2.58126f * x + 0.813703f * y) * y)
1534+
/ (1.0f + 0.310327f * x * x + 2.60994f * x * y);
1535+
const float B = sqrtf(1.0f - x) * (y - 1.0f) * y * y * y
1536+
/ (0.0000254053f + 1.71228f * x - 1.71506f * x * y
1537+
+ 1.34174f * y * y);
1538+
const float invs = (0.0379424f + y * (1.32227f + y))
1539+
/ (y * (0.0206607f + 1.58491f * y));
1540+
const float m = y
1541+
* (-0.193854f
1542+
+ y * (-1.14885 + y * (1.7932f - 0.95943f * y * y)))
1543+
/ (0.046391f + y);
1544+
const float o = y * (0.000654023f + (-0.0207818f + 0.119681f * y) * y)
1545+
/ (1.26264f + y * (-1.92021f + y));
1546+
float q = (x - m) * invs;
1547+
const float inv_sqrt2pi = 0.39894228040143f;
1548+
float R = expf(-0.5f * q * q) * invs * inv_sqrt2pi + o;
1549+
assert(isfinite(A));
1550+
assert(isfinite(B));
1551+
assert(isfinite(R));
1552+
return Vec3(A, B, R);
1553+
}
1554+
};
1555+
1556+
14471557
Color3
14481558
evaluate_layer_opacity(const OSL::ShaderGlobals& sg,
14491559
const ClosureColor* closure)
@@ -1493,8 +1603,11 @@ evaluate_layer_opacity(const OSL::ShaderGlobals& sg,
14931603
return w * mf.get_albedo(-sg.I);
14941604
}
14951605
case MX_SHEEN_ID: {
1496-
MxSheen bsdf(*comp->as<MxSheenParams>());
1497-
return w * bsdf.get_albedo(-sg.I);
1606+
const MxSheenParams& params = *comp->as<MxSheenParams>();
1607+
if (params.mode == 1)
1608+
return w * ZeltnerBurleySheen(params).get_albedo(-sg.I);
1609+
// otherwise, default to old sheen model
1610+
return w * CharlieSheen(params).get_albedo(-sg.I);
14981611
}
14991612
default: // Assume unhandled BSDFs are opaque
15001613
return Color3(1);
@@ -1765,7 +1878,11 @@ process_bsdf_closure(const OSL::ShaderGlobals& sg, ShadingResult& result,
17651878
}
17661879
case MX_SHEEN_ID: {
17671880
const MxSheenParams& params = *comp->as<MxSheenParams>();
1768-
ok = result.bsdf.add_bsdf<MxSheen>(cw, params);
1881+
if (params.mode == 1)
1882+
ok = result.bsdf.add_bsdf<ZeltnerBurleySheen>(cw, params);
1883+
else
1884+
ok = result.bsdf.add_bsdf<CharlieSheen>(
1885+
cw, params); // default to legacy closure
17691886
break;
17701887
}
17711888
case MX_LAYER_ID: {
-10.9 KB
Binary file not shown.
-12.3 KB
Binary file not shown.
-325 KB
Binary file not shown.
325 KB
Binary file not shown.
-325 KB
Binary file not shown.
-316 KB
Binary file not shown.
316 KB
Binary file not shown.
-8.88 KB
Binary file not shown.

0 commit comments

Comments
 (0)