renderling/pbr/
shader.rs

1//! Physically based shader code.
2use crabslab::{Id, Slab};
3use glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles};
4
5#[allow(unused)]
6use spirv_std::num_traits::{Float, Zero};
7
8use crate::{
9    atlas::shader::AtlasTextureDescriptor,
10    geometry::shader::GeometryDescriptor,
11    light::shader::{
12        Candela, DirectionalLightDescriptor, LightStyle, LightingDescriptor, PointLightDescriptor,
13        ShadowCalculation, SpotLightCalculation,
14    },
15    material::shader::MaterialDescriptor,
16    math::{self, IsSampler, IsVector, Sample2d, Sample2dArray, SampleCube},
17    primitive::shader::PrimitiveDescriptor,
18    println as my_println,
19};
20
21use super::{brdf, debug};
22
23/// Trowbridge-Reitz GGX normal distribution function (NDF).
24///
25/// The normal distribution function D statistically approximates the relative
26/// surface area of microfacets exactly aligned to the (halfway) vector h.
27pub fn normal_distribution_ggx(n: Vec3, h: Vec3, roughness: f32) -> f32 {
28    let a = roughness * roughness;
29    let a2 = a * a;
30    let ndot_h = n.dot(h).max(0.0);
31    let ndot_h2 = ndot_h * ndot_h;
32
33    let num = a2;
34    let denom = (ndot_h2 * (a2 - 1.0) + 1.0).powf(2.0) * core::f32::consts::PI;
35
36    num / denom
37}
38
39fn geometry_schlick_ggx(ndot_v: f32, roughness: f32) -> f32 {
40    let r = roughness + 1.0;
41    let k = (r * r) / 8.0;
42    let num = ndot_v;
43    let denom = ndot_v * (1.0 - k) + k;
44
45    num / denom
46}
47
48/// The geometry function statistically approximates the relative surface area
49/// where its micro surface-details overshadow each other, causing light rays to
50/// be occluded.
51fn geometry_smith(n: Vec3, v: Vec3, l: Vec3, roughness: f32) -> f32 {
52    let ndot_v = n.dot(v).max(0.0);
53    let ndot_l = n.dot(l).max(0.0);
54    let ggx1 = geometry_schlick_ggx(ndot_v, roughness);
55    let ggx2 = geometry_schlick_ggx(ndot_l, roughness);
56
57    ggx1 * ggx2
58}
59
60/// Fresnel-Schlick approximation function.
61///
62/// The Fresnel equation describes the ratio of light that gets reflected over
63/// the light that gets refracted, which varies over the angle we're looking at
64/// a surface. The moment light hits a surface, based on the surface-to-view
65/// angle, the Fresnel equation tells us the percentage of light that gets
66/// reflected. From this ratio of reflection and the energy conservation
67/// principle we can directly obtain the refracted portion of light.
68fn fresnel_schlick(
69    // dot product result between the surface's normal n and the halfway h (or view v) direction.
70    cos_theta: f32,
71    // surface reflection at zero incidence (how much the surface reflects if looking directly at
72    // the surface)
73    f0: Vec3,
74) -> Vec3 {
75    f0 + (1.0 - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0)
76}
77
78fn fresnel_schlick_roughness(cos_theta: f32, f0: Vec3, roughness: f32) -> Vec3 {
79    f0 + (Vec3::splat(1.0 - roughness).max(f0) - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0)
80}
81
82#[allow(clippy::too_many_arguments)]
83pub fn outgoing_radiance(
84    light_color: Vec4,
85    albedo: Vec3,
86    attenuation: f32,
87    v: Vec3,
88    l: Vec3,
89    n: Vec3,
90    metalness: f32,
91    roughness: f32,
92) -> Vec3 {
93    my_println!("outgoing_radiance");
94    my_println!("    light_color: {light_color:?}");
95    my_println!("    albedo: {albedo:?}");
96    my_println!("    attenuation: {attenuation:?}");
97    my_println!("    v: {v:?}");
98    my_println!("    l: {l:?}");
99    my_println!("    n: {n:?}");
100    my_println!("    metalness: {metalness:?}");
101    my_println!("    roughness: {roughness:?}");
102
103    let f0 = Vec3::splat(0.4).lerp(albedo, metalness);
104    my_println!("    f0: {f0:?}");
105    let radiance = light_color.xyz() * attenuation;
106    my_println!("    radiance: {radiance:?}");
107    let h = (v + l).alt_norm_or_zero();
108    my_println!("    h: {h:?}");
109    // cook-torrance brdf
110    let ndf: f32 = normal_distribution_ggx(n, h, roughness);
111    my_println!("    ndf: {ndf:?}");
112    let g: f32 = geometry_smith(n, v, l, roughness);
113    my_println!("    g: {g:?}");
114    let f: Vec3 = fresnel_schlick(h.dot(v).max(0.0), f0);
115    my_println!("    f: {f:?}");
116
117    let k_s = f;
118    let k_d = (Vec3::splat(1.0) - k_s) * (1.0 - metalness);
119    my_println!("    k_s: {k_s:?}");
120
121    let numerator: Vec3 = ndf * g * f;
122    my_println!("    numerator: {numerator:?}");
123    let n_dot_l = n.dot(l).max(0.0);
124    my_println!("    n_dot_l: {n_dot_l:?}");
125    let denominator: f32 = 4.0 * n.dot(v).max(0.0) * n_dot_l + 0.0001;
126    my_println!("    denominator: {denominator:?}");
127    let specular: Vec3 = numerator / denominator;
128    my_println!("    specular: {specular:?}");
129
130    (k_d * albedo / core::f32::consts::PI + specular) * radiance * n_dot_l
131}
132
133pub fn sample_irradiance<T: SampleCube<Sampler = S>, S: IsSampler>(
134    irradiance: &T,
135    irradiance_sampler: &S,
136    // Normal vector
137    n: Vec3,
138) -> Vec3 {
139    irradiance.sample_by_lod(*irradiance_sampler, n, 0.0).xyz()
140}
141
142pub fn sample_specular_reflection<T: SampleCube<Sampler = S>, S: IsSampler>(
143    prefiltered: &T,
144    prefiltered_sampler: &S,
145    // camera position in world space
146    camera_pos: Vec3,
147    // fragment position in world space
148    in_pos: Vec3,
149    // normal vector
150    n: Vec3,
151    roughness: f32,
152) -> Vec3 {
153    let v = (camera_pos - in_pos).alt_norm_or_zero();
154    let reflect_dir = math::reflect(-v, n);
155    prefiltered
156        .sample_by_lod(*prefiltered_sampler, reflect_dir, roughness * 4.0)
157        .xyz()
158}
159
160/// Returns the `Material` from the stage's slab.
161pub fn get_material(
162    material_id: Id<MaterialDescriptor>,
163    has_lighting: bool,
164    material_slab: &[u32],
165) -> MaterialDescriptor {
166    if material_id.is_none() {
167        // without an explicit material (or if the entire render has no lighting)
168        // the entity will not participate in any lighting calculations
169        MaterialDescriptor {
170            has_lighting: false,
171            ..Default::default()
172        }
173    } else {
174        let mut material = material_slab.read_unchecked(material_id);
175        material.has_lighting &= has_lighting;
176        material
177    }
178}
179
180pub fn texture_color<A: Sample2dArray<Sampler = S>, S: IsSampler>(
181    texture_id: Id<AtlasTextureDescriptor>,
182    uv: Vec2,
183    atlas: &A,
184    sampler: &S,
185    atlas_size: glam::UVec2,
186    material_slab: &[u32],
187) -> Vec4 {
188    let texture = material_slab.read(texture_id);
189    // uv is [0, 0] when texture_id is Id::NONE
190    let uv = texture.uv(uv, atlas_size);
191    crate::println!("uv: {uv}");
192    let mut color: Vec4 = atlas.sample_by_lod(*sampler, uv, 0.0);
193    if texture_id.is_none() {
194        color = Vec4::splat(1.0);
195    }
196    color
197}
198
199/// PBR fragment shader capable of being run on CPU or GPU.
200#[allow(clippy::too_many_arguments)]
201pub fn fragment_impl<A, T, DtA, C, S>(
202    atlas: &A,
203    atlas_sampler: &S,
204    irradiance: &C,
205    irradiance_sampler: &S,
206    prefiltered: &C,
207    prefiltered_sampler: &S,
208    brdf: &T,
209    brdf_sampler: &S,
210    shadow_map: &DtA,
211    shadow_map_sampler: &S,
212
213    geometry_slab: &[u32],
214    material_slab: &[u32],
215    lighting_slab: &[u32],
216
217    renderlet_id: Id<PrimitiveDescriptor>,
218
219    frag_coord: Vec4,
220    in_color: Vec4,
221    in_uv0: Vec2,
222    in_uv1: Vec2,
223    in_norm: Vec3,
224    in_tangent: Vec3,
225    in_bitangent: Vec3,
226    in_pos: Vec3,
227
228    output: &mut Vec4,
229) where
230    A: Sample2dArray<Sampler = S>,
231    T: Sample2d<Sampler = S>,
232    DtA: Sample2dArray<Sampler = S>,
233    C: SampleCube<Sampler = S>,
234    S: IsSampler,
235{
236    let renderlet = geometry_slab.read_unchecked(renderlet_id);
237    let geom_desc = geometry_slab.read_unchecked(renderlet.geometry_descriptor_id);
238    crate::println!("pbr_desc_id: {:?}", renderlet.geometry_descriptor_id);
239    crate::println!("pbr_desc: {geom_desc:#?}");
240    let GeometryDescriptor {
241        camera_id,
242        atlas_size,
243        resolution: _,
244        debug_channel,
245        has_lighting,
246        has_skinning: _,
247        perform_frustum_culling: _,
248        perform_occlusion_culling: _,
249    } = geom_desc;
250
251    let material = get_material(renderlet.material_id, has_lighting, material_slab);
252    crate::println!("material: {:#?}", material);
253
254    let albedo_tex_uv = if material.albedo_tex_coord == 0 {
255        in_uv0
256    } else {
257        in_uv1
258    };
259    let albedo_tex_color = texture_color(
260        material.albedo_texture_id,
261        albedo_tex_uv,
262        atlas,
263        atlas_sampler,
264        atlas_size,
265        material_slab,
266    );
267    my_println!("albedo_tex_color: {:?}", albedo_tex_color);
268
269    let metallic_roughness_uv = if material.metallic_roughness_tex_coord == 0 {
270        in_uv0
271    } else {
272        in_uv1
273    };
274    let metallic_roughness_tex_color = texture_color(
275        material.metallic_roughness_texture_id,
276        metallic_roughness_uv,
277        atlas,
278        atlas_sampler,
279        atlas_size,
280        material_slab,
281    );
282    my_println!(
283        "metallic_roughness_tex_color: {:?}",
284        metallic_roughness_tex_color
285    );
286
287    let normal_tex_uv = if material.normal_tex_coord == 0 {
288        in_uv0
289    } else {
290        in_uv1
291    };
292    let normal_tex_color = texture_color(
293        material.normal_texture_id,
294        normal_tex_uv,
295        atlas,
296        atlas_sampler,
297        atlas_size,
298        material_slab,
299    );
300    my_println!("normal_tex_color: {:?}", normal_tex_color);
301
302    let ao_tex_uv = if material.ao_tex_coord == 0 {
303        in_uv0
304    } else {
305        in_uv1
306    };
307    let ao_tex_color = texture_color(
308        material.ao_texture_id,
309        ao_tex_uv,
310        atlas,
311        atlas_sampler,
312        atlas_size,
313        material_slab,
314    );
315
316    let emissive_tex_uv = if material.emissive_tex_coord == 0 {
317        in_uv0
318    } else {
319        in_uv1
320    };
321    let emissive_tex_color = texture_color(
322        material.emissive_texture_id,
323        emissive_tex_uv,
324        atlas,
325        atlas_sampler,
326        atlas_size,
327        material_slab,
328    );
329
330    let (norm, uv_norm) = if material.normal_texture_id.is_none() {
331        // there is no normal map, use the normal normal ;)
332        (in_norm, Vec3::ZERO)
333    } else {
334        // convert the normal from color coords to tangent space -1,1
335        let sampled_norm = (normal_tex_color.xyz() * 2.0 - Vec3::splat(1.0)).alt_norm_or_zero();
336        let tbn = glam::mat3(
337            in_tangent.alt_norm_or_zero(),
338            in_bitangent.alt_norm_or_zero(),
339            in_norm.alt_norm_or_zero(),
340        );
341        // convert the normal from tangent space to world space
342        let norm = (tbn * sampled_norm).alt_norm_or_zero();
343        (norm, sampled_norm)
344    };
345
346    let n = norm;
347    let albedo = albedo_tex_color * material.albedo_factor * in_color;
348    let roughness = metallic_roughness_tex_color.y * material.roughness_factor;
349    let metallic = metallic_roughness_tex_color.z * material.metallic_factor;
350    let ao = 1.0 + material.ao_strength * (ao_tex_color.x - 1.0);
351    let emissive =
352        emissive_tex_color.xyz() * material.emissive_factor * material.emissive_strength_multiplier;
353    let irradiance = sample_irradiance(irradiance, irradiance_sampler, n);
354    let camera = geometry_slab.read(camera_id);
355    let specular = sample_specular_reflection(
356        prefiltered,
357        prefiltered_sampler,
358        camera.position(),
359        in_pos,
360        n,
361        roughness,
362    );
363    let brdf =
364        brdf::shader::sample_brdf(brdf, brdf_sampler, camera.position(), in_pos, n, roughness);
365
366    fn colorize(u: Vec3) -> Vec4 {
367        ((u.alt_norm_or_zero() + Vec3::splat(1.0)) / 2.0).extend(1.0)
368    }
369
370    crate::println!("debug_mode: {debug_channel:?}");
371    use debug::DebugChannel::*;
372    match debug_channel {
373        None => {}
374        UvCoords0 => {
375            *output = colorize(Vec3::new(in_uv0.x, in_uv0.y, 0.0));
376            return;
377        }
378        UvCoords1 => {
379            *output = colorize(Vec3::new(in_uv1.x, in_uv1.y, 0.0));
380            return;
381        }
382        Normals => {
383            *output = colorize(norm);
384            return;
385        }
386        VertexColor => {
387            *output = in_color;
388            return;
389        }
390        VertexNormals => {
391            *output = colorize(in_norm);
392            return;
393        }
394        UvNormals => {
395            *output = colorize(uv_norm);
396            return;
397        }
398        Tangents => {
399            *output = colorize(in_tangent);
400            return;
401        }
402        Bitangents => {
403            *output = colorize(in_bitangent);
404            return;
405        }
406        DiffuseIrradiance => {
407            *output = irradiance.extend(1.0);
408            return;
409        }
410        SpecularReflection => {
411            *output = specular.extend(1.0);
412            return;
413        }
414        Brdf => {
415            *output = brdf.extend(1.0).extend(1.0);
416            return;
417        }
418        Roughness => {
419            *output = Vec3::splat(roughness).extend(1.0);
420            return;
421        }
422        Metallic => {
423            *output = Vec3::splat(metallic).extend(1.0);
424            return;
425        }
426        Albedo => {
427            *output = albedo;
428            return;
429        }
430        Occlusion => {
431            *output = Vec3::splat(ao).extend(1.0);
432            return;
433        }
434        Emissive => {
435            *output = emissive.extend(1.0);
436            return;
437        }
438        UvEmissive => {
439            *output = emissive_tex_color.xyz().extend(1.0);
440            return;
441        }
442        EmissiveFactor => {
443            *output = material.emissive_factor.extend(1.0);
444            return;
445        }
446        EmissiveStrength => {
447            *output = Vec3::splat(material.emissive_strength_multiplier).extend(1.0);
448            return;
449        }
450    }
451
452    *output = if material.has_lighting {
453        shade_fragment(
454            shadow_map,
455            shadow_map_sampler,
456            camera.position(),
457            n,
458            in_pos,
459            albedo.xyz(),
460            metallic,
461            roughness,
462            ao,
463            emissive,
464            irradiance,
465            specular,
466            brdf,
467            lighting_slab,
468            frag_coord,
469        )
470    } else {
471        crate::println!("no shading!");
472        in_color * albedo_tex_color * material.albedo_factor
473    };
474}
475
476#[allow(clippy::too_many_arguments)]
477pub fn shade_fragment<S, T>(
478    shadow_map: &T,
479    shadow_map_sampler: &S,
480    // camera's position in world space
481    camera_pos: Vec3,
482    // normal of the fragment
483    in_norm: Vec3,
484    // position of the fragment in world space
485    in_pos: Vec3,
486    // base color of the fragment
487    albedo: Vec3,
488    metallic: f32,
489    roughness: f32,
490    ao: f32,
491    emissive: Vec3,
492    irradiance: Vec3,
493    prefiltered: Vec3,
494    brdf: Vec2,
495
496    light_slab: &[u32],
497    frag_coord: Vec4,
498) -> Vec4
499where
500    S: IsSampler,
501    T: Sample2dArray<Sampler = S>,
502{
503    let n = in_norm.alt_norm_or_zero();
504    let v = (camera_pos - in_pos).alt_norm_or_zero();
505    // There is always a `LightingDescriptor` stored at index `0` of the
506    // light slab.
507    let lighting_desc = light_slab.read_unchecked(Id::<LightingDescriptor>::new(0));
508    // If light tiling is enabled, use the pre-computed tile's light list
509    let analytical_lights_array = if lighting_desc.light_tiling_descriptor_id.is_none() {
510        lighting_desc.analytical_lights_array
511    } else {
512        let tiling_descriptor = light_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id);
513        let tile_index = tiling_descriptor.tile_index_for_fragment(frag_coord.xy());
514        let tile = light_slab.read_unchecked(tiling_descriptor.tiles_array.at(tile_index));
515        tile.lights_array
516    };
517    my_println!("lights: {analytical_lights_array:?}");
518    my_println!("surface normal: {n:?}");
519    my_println!("vector from surface to camera: {v:?}");
520
521    // accumulated outgoing radiance
522    let mut lo = Vec3::ZERO;
523    for light_id_id in analytical_lights_array.iter() {
524        // calculate per-light radiance
525        let light_id = light_slab.read(light_id_id);
526        if light_id.is_none() {
527            break;
528        }
529        let light = light_slab.read(light_id);
530        let transform = light_slab.read(light.transform_id);
531        crate::println!("transform: {transform:?}");
532        let transform = Mat4::from(transform);
533
534        // determine the light ray and the radiance
535        let (radiance, shadow) = match light.light_type {
536            LightStyle::Point => {
537                let PointLightDescriptor {
538                    position,
539                    color,
540                    intensity: Candela(intensity_candelas),
541                } = light_slab.read(light.into_point_id());
542                // Convert candelas into radiometric
543                // TODO: write true radiometric light conversions for Lux and Candela
544                let intensity = intensity_candelas / 683.0;
545                let position = transform.transform_point3(position);
546                // This definitely is the direction pointing from fragment to the light.
547                // It needs to stay this way.
548                // For more info, see
549                // <https://renderling.xyz/articles/live/light_tiling.html#point_and_spotlight_discrepancies__fri_11_june>
550                let frag_to_light = position - in_pos;
551                let distance = frag_to_light.length();
552                if distance == 0.0 {
553                    crate::println!("distance between point light and surface is zero");
554                    continue;
555                }
556                let l = frag_to_light.alt_norm_or_zero();
557                let attenuation = intensity / (distance * distance);
558                let radiance =
559                    outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness);
560                let shadow = if light.shadow_map_desc_id.is_some() {
561                    // Shadow is 1.0 when the fragment is in the shadow of this light,
562                    // and 0.0 in darkness
563                    ShadowCalculation::new(light_slab, light, in_pos, n, l).run_point(
564                        light_slab,
565                        shadow_map,
566                        shadow_map_sampler,
567                        position,
568                    )
569                } else {
570                    0.0
571                };
572                (radiance, shadow)
573            }
574
575            LightStyle::Spot => {
576                let spot_light_descriptor = light_slab.read(light.into_spot_id());
577                let calculation =
578                    SpotLightCalculation::new(spot_light_descriptor, transform, in_pos);
579                crate::println!("calculation: {calculation:#?}");
580                if calculation.frag_to_light_distance == 0.0 {
581                    continue;
582                }
583                // Convert from candelas to a radiometric unit.
584                //
585                // TODO: verify that spot light radiometric conversion is correct.
586                let intensity =
587                // TODO: write true radiometric light conversions for Lux and Candela
588                    spot_light_descriptor.intensity.0 / (683.0 * 4.0 * core::f32::consts::PI);
589                let attenuation: f32 = intensity * calculation.contribution;
590                let radiance = outgoing_radiance(
591                    spot_light_descriptor.color,
592                    albedo,
593                    attenuation,
594                    v,
595                    calculation.frag_to_light,
596                    n,
597                    metallic,
598                    roughness,
599                );
600                let shadow = if light.shadow_map_desc_id.is_some() {
601                    // Shadow is 1.0 when the fragment is in the shadow of this light,
602                    // and 0.0 in darkness
603                    ShadowCalculation::new(light_slab, light, in_pos, n, calculation.frag_to_light)
604                        .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler)
605                } else {
606                    0.0
607                };
608                (radiance, shadow)
609            }
610
611            LightStyle::Directional => {
612                let DirectionalLightDescriptor {
613                    direction,
614                    color,
615                    intensity: intensity_lux,
616                } = light_slab.read(light.into_directional_id());
617                let direction = transform.transform_vector3(direction);
618                let l = -direction.alt_norm_or_zero();
619                // TODO: write true radiometric light conversions for Lux and Candela
620                let attenuation = intensity_lux.0 / 683.0;
621                let radiance =
622                    outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness);
623                let shadow =
624                    if light.shadow_map_desc_id.is_some() {
625                        // Shadow is 1.0 when the fragment is in the shadow of this light,
626                        // and 0.0 in darkness
627                        ShadowCalculation::new(light_slab, light, in_pos, n, l)
628                            .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler)
629                    } else {
630                        0.0
631                    };
632                (radiance, shadow)
633            }
634        };
635        crate::println!("radiance: {radiance}");
636        crate::println!("shadow: {shadow}");
637        lo += radiance * (1.0 - shadow);
638    }
639
640    my_println!("lo: {lo:?}");
641    // calculate reflectance at normal incidence; if dia-electric (like plastic) use
642    // F0 of 0.04 and if it's a metal, use the albedo color as F0 (metallic
643    // workflow)
644    let f0: Vec3 = Vec3::splat(0.04).lerp(albedo, metallic);
645    let cos_theta = n.dot(v).max(0.0);
646    let fresnel = fresnel_schlick_roughness(cos_theta, f0, roughness);
647    let ks = fresnel;
648    let kd = (1.0 - ks) * (1.0 - metallic);
649    let diffuse = irradiance * albedo;
650    let specular = prefiltered * (fresnel * brdf.x + brdf.y);
651    let color = (kd * diffuse + specular) * ao + lo + emissive;
652    color.extend(1.0)
653}