webgpu-pt

monte carlo path tracer
Contents

brdf.wgsl

6.2 kB
  1// computes the geometry (shadowing/masking) term using smith's method.
  2fn smith_geometry(normal: vec3<f32>, view_dir: vec3<f32>, light_dir: vec3<f32>, roughness: f32) -> f32 {
  3    let alpha = roughness * roughness;
  4    let n_dot_v = max(dot(normal, view_dir), 0.0);
  5    let n_dot_l = max(dot(normal, light_dir), 0.0);
  6    let k = (alpha + 1.0) * (alpha + 1.0) / 8.0;
  7    let geom_v = n_dot_v / (n_dot_v * (1.0 - k) + k);
  8    let geom_l = n_dot_l / (n_dot_l * (1.0 - k) + k);
  9    return geom_v * geom_l;
 10}
 11
 12// computes the ggx normal distribution function (ndf) d.
 13fn ggx_distribution(normal: vec3<f32>, half_vec: vec3<f32>, roughness: f32) -> f32 {
 14    let rgh = max(0.03, roughness);
 15    let alpha = rgh * rgh;
 16    let alpha2 = alpha * alpha;
 17    let n_dot_h = max(dot(normal, half_vec), 0.001);
 18    let denom = (n_dot_h * n_dot_h) * (alpha2 - 1.0) + 1.0;
 19    return alpha2 / (PI * denom * denom);
 20}
 21
 22// samples a half–vector (h) from the ggx distribution in tangent space.
 23fn ggx_sample_vndf(view_dir: vec3<f32>, normal: vec3<f32>, roughness: f32, noise: vec2<f32>) -> vec3<f32> {
 24    // build local frame (t, b, n)
 25    let up: vec3<f32> = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.z) < 0.999);
 26    let tangent = normalize(cross(up, normal));
 27    let bitangent = cross(normal, tangent);
 28
 29    // transform view direction to local space
 30    let v = normalize(vec3<f32>(dot(view_dir, tangent), dot(view_dir, bitangent), dot(view_dir, normal)));
 31
 32    // stretch view vector
 33    let a = roughness;
 34    let vh = normalize(vec3<f32>(a * v.x, a * v.y, v.z));
 35
 36    // orthonormal basis
 37    let lensq = vh.x * vh.x + vh.y * vh.y;
 38    let t1 = select(vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(- vh.y, vh.x, 0.0) / sqrt(lensq), lensq > 1e-6);
 39    let t2 = cross(vh, t1);
 40
 41    // sample point with polar coordinates
 42    let r = sqrt(noise.x);
 43    let phi = 2.0 * PI * noise.y;
 44    let r1 = r * cos(phi);
 45    let r2 = r * sin(phi);
 46    let s = 0.5 * (1.0 + vh.z);
 47    let t2_ = mix(sqrt(1.0 - r1 * r1), r2, s);
 48
 49    // sampled halfway vector in local space
 50    let nh = r1 * t1 + t2_ * t2 + sqrt(max(0.0, 1.0 - r1 * r1 - t2_ * t2_)) * vh;
 51
 52    // unstretch
 53    let h = normalize(vec3<f32>(a * nh.x, a * nh.y, max(0.0, nh.z)));
 54
 55    // transform back to world space
 56    return normalize(h.x * tangent + h.y * bitangent + h.z * normal);
 57}
 58
 59fn ggx_specular_sample(view_dir: vec3<f32>, normal: vec3<f32>, seed: vec2<f32>, roughness: f32) -> vec3<f32> {
 60    let h = ggx_sample_vndf(view_dir, normal, roughness, seed);
 61    let r = reflect(- view_dir, h);
 62    return select(normalize(r), vec3<f32>(0.0), dot(r, normal) <= EPSILON);
 63}
 64
 65// cosine-weighted hemisphere sample in local space, then converted to world space.
 66fn cosine_hemisphere_sample(normal: vec3<f32>, noise: vec2<f32>) -> vec3<f32> {
 67    // var current_seed = seed;
 68    let r1 = noise.x;
 69    let r2 = noise.y;
 70
 71    // let r1 = uniform_float(state);
 72    // let r2 = uniform_float(state);
 73
 74    let phi = 2.0 * PI * r2;
 75    let cos_theta = sqrt(1.0 - r1);
 76    let sin_theta = sqrt(r1);
 77
 78    // sample in local coordinates (with z as the normal)
 79    let local_sample = vec3<f32>(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta);
 80
 81    // build tangent space and transform sample to world space.
 82    // let up = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.z) > 0.999);
 83
 84    let tangent = normalize(cross(select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) > 0.99), normal));
 85    let bitangent = cross(normal, tangent);
 86
 87    // let tangent = normalize(cross(up, normal));
 88    // let bitangent = cross(normal, tangent);
 89    let world_dir = normalize(local_sample.x * tangent + local_sample.y * bitangent + local_sample.z * normal);
 90    return world_dir;
 91}
 92
 93fn eval_f0(metallic: f32, albedo: vec3<f32>) -> vec3<f32> {
 94    let dielectric_f0 = vec3<f32>(0.04);
 95    return mix(dielectric_f0, albedo, metallic);
 96}
 97
 98fn fresnel_schlick_roughness(cos_theta: f32, f0: vec3<f32>, roughness: f32) -> vec3<f32> {
 99    let one_minus_cos = 1.0 - cos_theta;
100    let factor = pow(one_minus_cos, 5.0);
101    let fresnel = f0 + (max(vec3f(1.0 - roughness), f0) - f0) * factor;
102    return fresnel;
103}
104
105fn disney_diffuse(albedo: vec3<f32>, roughness: f32, n_dot_l: f32, n_dot_v: f32, l_dot_h: f32) -> vec3<f32> {
106    let fd90 = 0.5 + 2.0 * l_dot_h * l_dot_h * roughness;
107    let light_scatter = 1.0 + (fd90 - 1.0) * pow(1.0 - n_dot_l, 5.0);
108    let view_scatter = 1.0 + (fd90 - 1.0) * pow(1.0 - n_dot_v, 5.0);
109    return albedo * light_scatter * view_scatter * (1.0 / PI);
110}
111
112fn eval_brdf(normal: vec3<f32>, view_dir: vec3<f32>, light_dir: vec3<f32>, material: Material) -> vec3<f32> {
113    let n = normal;
114    let v = view_dir;
115    let l = light_dir;
116    let h = normalize(v + l);
117
118    let ndot_l = max(dot(n, l), EPSILON);
119    let ndot_v = max(dot(n, v), EPSILON);
120    let ndot_h = max(dot(n, h), EPSILON);
121    let vdot_h = max(dot(v, h), EPSILON);
122    let ldot_h = max(dot(l, h), EPSILON);
123
124    let f0 = mix(vec3f(0.04), material.albedo.rgb, material.metallic);
125    let f = fresnel_schlick_roughness(vdot_h, f0, material.roughness);
126
127    // frostbite specular
128    let d = ggx_distribution(n, h, material.roughness);
129    let g = smith_geometry(n, v, l, material.roughness);
130    let spec = (d * g * f) / (4.0 * ndot_v * ndot_l + 0.001);
131
132    let diffuse = (1.0 - material.metallic) * disney_diffuse(material.albedo.rgb, material.roughness, ndot_l, ndot_v, ldot_h);
133    return diffuse + spec;
134}
135
136fn ggx_pdf(view_dir: vec3<f32>, normal: vec3<f32>, h: vec3<f32>, roughness: f32) -> f32 {
137    let d = ggx_distribution(normal, h, roughness);
138    let n_dot_h = max(dot(normal, h), EPSILON);
139    let v_dot_h = max(dot(view_dir, h), EPSILON);
140    // change–of–variables: pdf(r) = d(h) * n_dot_h / (4 * v_dot_h)
141    return (d * n_dot_h) / (4.0 * v_dot_h);
142}
143
144fn cosine_pdf(normal: vec3<f32>, dir: vec3<f32>) -> f32 {
145    // cosine weighted density: d = cos(theta) / PI.
146    let cos_theta = max(dot(normal, dir), EPSILON);
147    if cos_theta <= 0.0 {
148        return 0.0;
149        // no contribution when direction is opposite or perpendicular to the normal.
150    }
151    return cos_theta / PI;
152}
153
154