Skip to main content

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
168        // lighting) the entity will not participate in any lighting
169        // calculations
170        MaterialDescriptor {
171            has_lighting: false,
172            ..Default::default()
173        }
174    } else {
175        let mut material = material_slab.read_unchecked(material_id);
176        material.has_lighting &= has_lighting;
177        material
178    }
179}
180
181pub fn texture_color<A: Sample2dArray<Sampler = S>, S: IsSampler>(
182    texture_id: Id<AtlasTextureDescriptor>,
183    uv: Vec2,
184    atlas: &A,
185    sampler: &S,
186    atlas_size: glam::UVec2,
187    material_slab: &[u32],
188) -> Vec4 {
189    let texture = material_slab.read(texture_id);
190    // uv is [0, 0] when texture_id is Id::NONE
191    let uv = texture.uv(uv, atlas_size);
192    crate::println!("uv: {uv}");
193    let mut color: Vec4 = atlas.sample_by_lod(*sampler, uv, 0.0);
194    if texture_id.is_none() {
195        color = Vec4::splat(1.0);
196    }
197    color
198}
199
200/// PBR fragment shader capable of being run on CPU or GPU.
201#[allow(clippy::too_many_arguments)]
202pub fn fragment_impl<A, T, DtA, C, S>(
203    atlas: &A,
204    atlas_sampler: &S,
205    irradiance: &C,
206    irradiance_sampler: &S,
207    prefiltered: &C,
208    prefiltered_sampler: &S,
209    brdf: &T,
210    brdf_sampler: &S,
211    shadow_map: &DtA,
212    shadow_map_sampler: &S,
213
214    geometry_slab: &[u32],
215    material_slab: &[u32],
216    lighting_slab: &[u32],
217
218    renderlet_id: Id<PrimitiveDescriptor>,
219
220    frag_coord: Vec4,
221    in_color: Vec4,
222    in_uv0: Vec2,
223    in_uv1: Vec2,
224    in_norm: Vec3,
225    in_tangent: Vec3,
226    in_bitangent: Vec3,
227    in_pos: Vec3,
228
229    output: &mut Vec4,
230) where
231    A: Sample2dArray<Sampler = S>,
232    T: Sample2d<Sampler = S>,
233    DtA: Sample2dArray<Sampler = S>,
234    C: SampleCube<Sampler = S>,
235    S: IsSampler,
236{
237    let renderlet = geometry_slab.read_unchecked(renderlet_id);
238    let geom_desc = geometry_slab.read_unchecked(renderlet.geometry_descriptor_id);
239    crate::println!("pbr_desc_id: {:?}", renderlet.geometry_descriptor_id);
240    crate::println!("pbr_desc: {geom_desc:#?}");
241    let GeometryDescriptor {
242        camera_id,
243        atlas_size,
244        resolution: _,
245        debug_channel,
246        has_lighting,
247        has_skinning: _,
248        perform_frustum_culling: _,
249        perform_occlusion_culling: _,
250    } = geom_desc;
251
252    let material = get_material(renderlet.material_id, has_lighting, material_slab);
253    crate::println!("material: {:#?}", material);
254
255    let albedo_tex_uv = if material.albedo_tex_coord == 0 {
256        in_uv0
257    } else {
258        in_uv1
259    };
260    let albedo_tex_color = texture_color(
261        material.albedo_texture_id,
262        albedo_tex_uv,
263        atlas,
264        atlas_sampler,
265        atlas_size,
266        material_slab,
267    );
268    my_println!("albedo_tex_color: {:?}", albedo_tex_color);
269
270    let metallic_roughness_uv = if material.metallic_roughness_tex_coord == 0 {
271        in_uv0
272    } else {
273        in_uv1
274    };
275    let metallic_roughness_tex_color = texture_color(
276        material.metallic_roughness_texture_id,
277        metallic_roughness_uv,
278        atlas,
279        atlas_sampler,
280        atlas_size,
281        material_slab,
282    );
283    my_println!(
284        "metallic_roughness_tex_color: {:?}",
285        metallic_roughness_tex_color
286    );
287
288    let normal_tex_uv = if material.normal_tex_coord == 0 {
289        in_uv0
290    } else {
291        in_uv1
292    };
293    let normal_tex_color = texture_color(
294        material.normal_texture_id,
295        normal_tex_uv,
296        atlas,
297        atlas_sampler,
298        atlas_size,
299        material_slab,
300    );
301    my_println!("normal_tex_color: {:?}", normal_tex_color);
302
303    let ao_tex_uv = if material.ao_tex_coord == 0 {
304        in_uv0
305    } else {
306        in_uv1
307    };
308    let ao_tex_color = texture_color(
309        material.ao_texture_id,
310        ao_tex_uv,
311        atlas,
312        atlas_sampler,
313        atlas_size,
314        material_slab,
315    );
316
317    let emissive_tex_uv = if material.emissive_tex_coord == 0 {
318        in_uv0
319    } else {
320        in_uv1
321    };
322    let emissive_tex_color = texture_color(
323        material.emissive_texture_id,
324        emissive_tex_uv,
325        atlas,
326        atlas_sampler,
327        atlas_size,
328        material_slab,
329    );
330
331    let (norm, uv_norm) = if material.normal_texture_id.is_none() {
332        // there is no normal map, use the normal normal ;)
333        (in_norm, Vec3::ZERO)
334    } else {
335        // convert the normal from color coords to tangent space -1,1
336        let sampled_norm = (normal_tex_color.xyz() * 2.0 - Vec3::splat(1.0)).alt_norm_or_zero();
337        let tbn = glam::mat3(
338            in_tangent.alt_norm_or_zero(),
339            in_bitangent.alt_norm_or_zero(),
340            in_norm.alt_norm_or_zero(),
341        );
342        // convert the normal from tangent space to world space
343        let norm = (tbn * sampled_norm).alt_norm_or_zero();
344        (norm, sampled_norm)
345    };
346
347    let n = norm;
348    let albedo = albedo_tex_color * material.albedo_factor * in_color;
349    let roughness = metallic_roughness_tex_color.y * material.roughness_factor;
350    let metallic = metallic_roughness_tex_color.z * material.metallic_factor;
351    let ao = 1.0 + material.ao_strength * (ao_tex_color.x - 1.0);
352    let emissive =
353        emissive_tex_color.xyz() * material.emissive_factor * material.emissive_strength_multiplier;
354    let irradiance = sample_irradiance(irradiance, irradiance_sampler, n);
355    let camera = geometry_slab.read(camera_id);
356    let specular = sample_specular_reflection(
357        prefiltered,
358        prefiltered_sampler,
359        camera.position(),
360        in_pos,
361        n,
362        roughness,
363    );
364    let brdf =
365        brdf::shader::sample_brdf(brdf, brdf_sampler, camera.position(), in_pos, n, roughness);
366
367    fn colorize(u: Vec3) -> Vec4 {
368        ((u.alt_norm_or_zero() + Vec3::splat(1.0)) / 2.0).extend(1.0)
369    }
370
371    crate::println!("debug_mode: {debug_channel:?}");
372    use debug::DebugChannel::*;
373    match debug_channel {
374        None => {}
375        UvCoords0 => {
376            *output = colorize(Vec3::new(in_uv0.x, in_uv0.y, 0.0));
377            return;
378        }
379        UvCoords1 => {
380            *output = colorize(Vec3::new(in_uv1.x, in_uv1.y, 0.0));
381            return;
382        }
383        Normals => {
384            *output = colorize(norm);
385            return;
386        }
387        VertexColor => {
388            *output = in_color;
389            return;
390        }
391        VertexNormals => {
392            *output = colorize(in_norm);
393            return;
394        }
395        UvNormals => {
396            *output = colorize(uv_norm);
397            return;
398        }
399        Tangents => {
400            *output = colorize(in_tangent);
401            return;
402        }
403        Bitangents => {
404            *output = colorize(in_bitangent);
405            return;
406        }
407        DiffuseIrradiance => {
408            *output = irradiance.extend(1.0);
409            return;
410        }
411        SpecularReflection => {
412            *output = specular.extend(1.0);
413            return;
414        }
415        Brdf => {
416            *output = brdf.extend(1.0).extend(1.0);
417            return;
418        }
419        Roughness => {
420            *output = Vec3::splat(roughness).extend(1.0);
421            return;
422        }
423        Metallic => {
424            *output = Vec3::splat(metallic).extend(1.0);
425            return;
426        }
427        Albedo => {
428            *output = albedo;
429            return;
430        }
431        Occlusion => {
432            *output = Vec3::splat(ao).extend(1.0);
433            return;
434        }
435        Emissive => {
436            *output = emissive.extend(1.0);
437            return;
438        }
439        UvEmissive => {
440            *output = emissive_tex_color.xyz().extend(1.0);
441            return;
442        }
443        EmissiveFactor => {
444            *output = material.emissive_factor.extend(1.0);
445            return;
446        }
447        EmissiveStrength => {
448            *output = Vec3::splat(material.emissive_strength_multiplier).extend(1.0);
449            return;
450        }
451    }
452
453    *output = if material.has_lighting {
454        shade_fragment(
455            shadow_map,
456            shadow_map_sampler,
457            camera.position(),
458            n,
459            in_pos,
460            albedo.xyz(),
461            metallic,
462            roughness,
463            ao,
464            emissive,
465            irradiance,
466            specular,
467            brdf,
468            lighting_slab,
469            frag_coord,
470        )
471    } else {
472        crate::println!("no shading!");
473        in_color * albedo_tex_color * material.albedo_factor
474    };
475}
476
477#[allow(clippy::too_many_arguments)]
478pub fn shade_fragment<S, T>(
479    shadow_map: &T,
480    shadow_map_sampler: &S,
481    // camera's position in world space
482    camera_pos: Vec3,
483    // normal of the fragment
484    in_norm: Vec3,
485    // position of the fragment in world space
486    in_pos: Vec3,
487    // base color of the fragment
488    albedo: Vec3,
489    metallic: f32,
490    roughness: f32,
491    ao: f32,
492    emissive: Vec3,
493    irradiance: Vec3,
494    prefiltered: Vec3,
495    brdf: Vec2,
496
497    light_slab: &[u32],
498    frag_coord: Vec4,
499) -> Vec4
500where
501    S: IsSampler,
502    T: Sample2dArray<Sampler = S>,
503{
504    let n = in_norm.alt_norm_or_zero();
505    let v = (camera_pos - in_pos).alt_norm_or_zero();
506    // There is always a `LightingDescriptor` stored at index `0` of the
507    // light slab.
508    let lighting_desc = light_slab.read_unchecked(Id::<LightingDescriptor>::new(0));
509    // If light tiling is enabled, use the pre-computed tile's light list
510    let analytical_lights_array = if lighting_desc.light_tiling_descriptor_id.is_none() {
511        lighting_desc.analytical_lights_array
512    } else {
513        let tiling_descriptor = light_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id);
514        let tile_index = tiling_descriptor.tile_index_for_fragment(frag_coord.xy());
515        let tile = light_slab.read_unchecked(tiling_descriptor.tiles_array.at(tile_index));
516        tile.lights_array
517    };
518    my_println!("lights: {analytical_lights_array:?}");
519    my_println!("surface normal: {n:?}");
520    my_println!("vector from surface to camera: {v:?}");
521
522    // accumulated outgoing radiance
523    let mut lo = Vec3::ZERO;
524    for light_id_id in analytical_lights_array.iter() {
525        // calculate per-light radiance
526        let light_id = light_slab.read(light_id_id);
527        if light_id.is_none() {
528            break;
529        }
530        let light = light_slab.read(light_id);
531        let transform = light_slab.read(light.transform_id);
532        crate::println!("transform: {transform:?}");
533        let transform = Mat4::from(transform);
534
535        // determine the light ray and the radiance
536        let (radiance, shadow) = match light.light_type {
537            LightStyle::Point => {
538                let PointLightDescriptor {
539                    position,
540                    color,
541                    intensity: Candela(intensity_candelas),
542                } = light_slab.read(light.into_point_id());
543                // Convert candelas into radiometric
544                // TODO: write true radiometric light conversions for Lux and
545                // Candela
546                let intensity = intensity_candelas / 683.0;
547                let position = transform.transform_point3(position);
548                // This definitely is the direction pointing from fragment to
549                // the light. It needs to stay this way.
550                // For more info, see
551                // <https://renderling.xyz/articles/live/light_tiling.html#point_and_spotlight_discrepancies__fri_11_june>
552                let frag_to_light = position - in_pos;
553                let distance = frag_to_light.length();
554                if distance == 0.0 {
555                    crate::println!("distance between point light and surface is zero");
556                    continue;
557                }
558                let l = frag_to_light.alt_norm_or_zero();
559                let attenuation = intensity / (distance * distance);
560                let radiance =
561                    outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness);
562                let shadow = if light.shadow_map_desc_id.is_some() {
563                    // Shadow is 1.0 when the fragment is in the shadow of this
564                    // light, and 0.0 in darkness
565                    ShadowCalculation::new(light_slab, light, in_pos, n, l).run_point(
566                        light_slab,
567                        shadow_map,
568                        shadow_map_sampler,
569                        position,
570                    )
571                } else {
572                    0.0
573                };
574                (radiance, shadow)
575            }
576
577            LightStyle::Spot => {
578                let spot_light_descriptor = light_slab.read(light.into_spot_id());
579                let calculation =
580                    SpotLightCalculation::new(spot_light_descriptor, transform, in_pos);
581                crate::println!("calculation: {calculation:#?}");
582                if calculation.frag_to_light_distance == 0.0 {
583                    continue;
584                }
585                // Convert from candelas to a radiometric unit.
586                //
587                // TODO: verify that spot light radiometric conversion is
588                // correct.
589                let intensity =
590                // TODO: write true radiometric light conversions for Lux and Candela
591                    spot_light_descriptor.intensity.0 / (683.0 * 4.0 * core::f32::consts::PI);
592                let attenuation: f32 = intensity * calculation.contribution;
593                let radiance = outgoing_radiance(
594                    spot_light_descriptor.color,
595                    albedo,
596                    attenuation,
597                    v,
598                    calculation.frag_to_light,
599                    n,
600                    metallic,
601                    roughness,
602                );
603                let shadow = if light.shadow_map_desc_id.is_some() {
604                    // Shadow is 1.0 when the fragment is in the shadow of this
605                    // light, and 0.0 in darkness
606                    ShadowCalculation::new(light_slab, light, in_pos, n, calculation.frag_to_light)
607                        .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler)
608                } else {
609                    0.0
610                };
611                (radiance, shadow)
612            }
613
614            LightStyle::Directional => {
615                let DirectionalLightDescriptor {
616                    direction,
617                    color,
618                    intensity: intensity_lux,
619                } = light_slab.read(light.into_directional_id());
620                let direction = transform.transform_vector3(direction);
621                let l = -direction.alt_norm_or_zero();
622                // TODO: write true radiometric light conversions for Lux and
623                // Candela
624                let attenuation = intensity_lux.0 / 683.0;
625                let radiance =
626                    outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness);
627                let shadow =
628                    if light.shadow_map_desc_id.is_some() {
629                        // Shadow is 1.0 when the fragment is in the shadow of
630                        // this light, and 0.0 in
631                        // darkness
632                        ShadowCalculation::new(light_slab, light, in_pos, n, l)
633                            .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler)
634                    } else {
635                        0.0
636                    };
637                (radiance, shadow)
638            }
639        };
640        crate::println!("radiance: {radiance}");
641        crate::println!("shadow: {shadow}");
642        lo += radiance * (1.0 - shadow);
643    }
644
645    my_println!("lo: {lo:?}");
646    // calculate reflectance at normal incidence; if dia-electric (like plastic)
647    // use F0 of 0.04 and if it's a metal, use the albedo color as F0
648    // (metallic workflow)
649    let f0: Vec3 = Vec3::splat(0.04).lerp(albedo, metallic);
650    let cos_theta = n.dot(v).max(0.0);
651    let fresnel = fresnel_schlick_roughness(cos_theta, f0, roughness);
652    let ks = fresnel;
653    let kd = (1.0 - ks) * (1.0 - metallic);
654    let diffuse = irradiance * albedo;
655    let specular = prefiltered * (fresnel * brdf.x + brdf.y);
656    // Global ambient light contribution, modulated by surface albedo and AO.
657    let ambient_light = lighting_desc.ambient_color.xyz() * lighting_desc.ambient_color.w;
658    let color = (kd * diffuse + specular) * ao + lo + emissive + ambient_light * albedo * ao;
659    color.extend(1.0)
660}