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