three_d/
renderer.rs

1//!
2//! High-level features for easy rendering of different types of objects with different types of shading.
3//! Can be combined seamlessly with the mid-level features in the [core](crate::core) module as well as functionality in the [context](crate::context) module.
4//!
5//! This module contains five main traits
6//! - [Geometry] - a geometric representation in 3D space
7//! - [Material] - a material that can be applied to a geometry or the screen
8//! - [Effect] - an effect that can be applied to a geometry or the screen after the rest of the scene has been rendered
9//! - [Object] - an object in 3D space which has both geometry and material information (use the [Gm] struct to combine any [Material] and [Geometry] into an object)
10//! - [Light] - a light that shines onto objects in the scene (some materials are affected by lights, others are not)
11//!
12//! Common implementations of these traits are found in their respective modules but it is also possible to do a custom implementation by implementing one of the four traits.
13//!
14//! There are several ways to render something.
15//! Objects can be rendered directly using [Object::render] or used in a render call, for example [RenderTarget::render].
16//! Geometries can be rendered with a given material using [Geometry::render_with_material] or combined into an object using the [Gm] struct and again used in a render call.
17//!
18
19pub use crate::core::*;
20
21use thiserror::Error;
22///
23/// Error in the [renderer](crate::renderer) module.
24///
25#[derive(Error, Debug)]
26#[allow(missing_docs)]
27pub enum RendererError {
28    #[error("{0} buffer length must be {1}, actual length is {2}")]
29    InvalidBufferLength(String, usize, usize),
30    #[error("the material {0} is required by the geometry {1} but could not be found")]
31    MissingMaterial(String, String),
32    #[cfg(feature = "text")]
33    #[error("Failed to find font with index {0} in the given font collection")]
34    MissingFont(u32),
35}
36
37mod shader_ids;
38pub use shader_ids::*;
39
40mod viewer;
41pub use viewer::*;
42
43pub mod material;
44pub use material::*;
45
46pub mod effect;
47pub use effect::*;
48
49pub mod light;
50pub use light::*;
51
52pub mod geometry;
53pub use geometry::*;
54
55pub mod object;
56pub use object::*;
57
58pub mod control;
59pub use control::*;
60
61#[cfg(feature = "text")]
62mod text;
63#[cfg(feature = "text")]
64pub use text::*;
65
66macro_rules! impl_render_target_extensions_body {
67    () => {
68        ///
69        /// Render the objects using the given viewer and lights into this render target.
70        /// Use an empty array for the `lights` argument, if the objects does not require lights to be rendered.
71        /// Also, objects outside the viewer frustum are not rendered and the objects are rendered in the order given by [cmp_render_order].
72        ///
73        pub fn render(
74            &self,
75            viewer: impl Viewer,
76            objects: impl IntoIterator<Item = impl Object>,
77            lights: &[&dyn Light],
78        ) -> &Self {
79            self.render_partially(self.scissor_box(), viewer, objects, lights)
80        }
81
82        ///
83        /// Render the objects using the given viewer and lights into the part of this render target defined by the scissor box.
84        /// Use an empty array for the `lights` argument, if the objects does not require lights to be rendered.
85        /// Also, objects outside the viewer frustum are not rendered and the objects are rendered in the order given by [cmp_render_order].
86        ///
87        pub fn render_partially(
88            &self,
89            scissor_box: ScissorBox,
90            viewer: impl Viewer,
91            objects: impl IntoIterator<Item = impl Object>,
92            lights: &[&dyn Light],
93        ) -> &Self {
94            let frustum = Frustum::new(viewer.projection() * viewer.view());
95            let (mut deferred_objects, mut forward_objects): (Vec<_>, Vec<_>) = objects
96                .into_iter()
97                .filter(|o| frustum.contains(o.aabb()))
98                .partition(|o| o.material_type() == MaterialType::Deferred);
99
100            // Deferred
101            if deferred_objects.len() > 0 {
102                // Geometry pass
103                let geometry_pass_camera = GeometryPassCamera(&viewer);
104                let viewport = geometry_pass_camera.viewport();
105                deferred_objects.sort_by(|a, b| cmp_render_order(&geometry_pass_camera, a, b));
106                let mut geometry_pass_texture = Texture2DArray::new_empty::<[u8; 4]>(
107                    &self.context,
108                    viewport.width,
109                    viewport.height,
110                    3,
111                    Interpolation::Nearest,
112                    Interpolation::Nearest,
113                    None,
114                    Wrapping::ClampToEdge,
115                    Wrapping::ClampToEdge,
116                );
117                let mut geometry_pass_depth_texture = DepthTexture2D::new::<f32>(
118                    &self.context,
119                    viewport.width,
120                    viewport.height,
121                    Wrapping::ClampToEdge,
122                    Wrapping::ClampToEdge,
123                );
124                let gbuffer_layers = [0, 1, 2];
125                RenderTarget::new(
126                    geometry_pass_texture.as_color_target(&gbuffer_layers, None),
127                    geometry_pass_depth_texture.as_depth_target(),
128                )
129                .clear(ClearState::default())
130                .write::<RendererError>(|| {
131                    for object in deferred_objects {
132                        object.render(&geometry_pass_camera, lights);
133                    }
134                    Ok(())
135                })
136                .unwrap();
137
138                // Lighting pass
139                self.apply_screen_effect_partially(
140                    scissor_box,
141                    &lighting_pass::LightingPassEffect {},
142                    &viewer,
143                    lights,
144                    Some(ColorTexture::Array {
145                        texture: &geometry_pass_texture,
146                        layers: &gbuffer_layers,
147                    }),
148                    Some(DepthTexture::Single(&geometry_pass_depth_texture)),
149                );
150            }
151
152            // Forward
153            forward_objects.sort_by(|a, b| cmp_render_order(&viewer, a, b));
154            self.write_partially::<RendererError>(scissor_box, || {
155                for object in forward_objects {
156                    object.render(&viewer, lights);
157                }
158                Ok(())
159            })
160            .unwrap();
161            self
162        }
163
164        ///
165        /// Render the geometries with the given [Material] using the given viewer and lights into this render target.
166        /// Use an empty array for the `lights` argument, if the material does not require lights to be rendered.
167        ///
168        pub fn render_with_material(
169            &self,
170            material: &dyn Material,
171            viewer: impl Viewer,
172            geometries: impl IntoIterator<Item = impl Geometry>,
173            lights: &[&dyn Light],
174        ) -> &Self {
175            self.render_partially_with_material(
176                self.scissor_box(),
177                material,
178                viewer,
179                geometries,
180                lights,
181            )
182        }
183
184        ///
185        /// Render the geometries with the given [Material] using the given viewer and lights into the part of this render target defined by the scissor box.
186        /// Use an empty array for the `lights` argument, if the material does not require lights to be rendered.
187        ///
188        pub fn render_partially_with_material(
189            &self,
190            scissor_box: ScissorBox,
191            material: &dyn Material,
192            viewer: impl Viewer,
193            geometries: impl IntoIterator<Item = impl Geometry>,
194            lights: &[&dyn Light],
195        ) -> &Self {
196            let frustum = Frustum::new(viewer.projection() * viewer.view());
197            self.write_partially::<RendererError>(scissor_box, || {
198                for geometry in geometries
199                    .into_iter()
200                    .filter(|o| frustum.contains(o.aabb()))
201                {
202                    render_with_material(&self.context, &viewer, geometry, material, lights);
203                }
204                Ok(())
205            })
206            .unwrap();
207            self
208        }
209
210        ///
211        /// Render the geometries with the given [Effect] using the given viewer and lights into this render target.
212        /// Use an empty array for the `lights` argument, if the effect does not require lights to be rendered.
213        ///
214        pub fn render_with_effect(
215            &self,
216            effect: &dyn Effect,
217            viewer: impl Viewer,
218            geometries: impl IntoIterator<Item = impl Geometry>,
219            lights: &[&dyn Light],
220            color_texture: Option<ColorTexture>,
221            depth_texture: Option<DepthTexture>,
222        ) -> &Self {
223            self.render_partially_with_effect(
224                self.scissor_box(),
225                effect,
226                viewer,
227                geometries,
228                lights,
229                color_texture,
230                depth_texture,
231            )
232        }
233
234        ///
235        /// Render the geometries with the given [Effect] using the given viewer and lights into the part of this render target defined by the scissor box.
236        /// Use an empty array for the `lights` argument, if the effect does not require lights to be rendered.
237        ///
238        pub fn render_partially_with_effect(
239            &self,
240            scissor_box: ScissorBox,
241            effect: &dyn Effect,
242            viewer: impl Viewer,
243            geometries: impl IntoIterator<Item = impl Geometry>,
244            lights: &[&dyn Light],
245            color_texture: Option<ColorTexture>,
246            depth_texture: Option<DepthTexture>,
247        ) -> &Self {
248            let frustum = Frustum::new(viewer.projection() * viewer.view());
249            self.write_partially::<RendererError>(scissor_box, || {
250                for geometry in geometries
251                    .into_iter()
252                    .filter(|o| frustum.contains(o.aabb()))
253                {
254                    render_with_effect(
255                        &self.context,
256                        &viewer,
257                        geometry,
258                        effect,
259                        lights,
260                        color_texture,
261                        depth_texture,
262                    );
263                }
264                Ok(())
265            })
266            .unwrap();
267            self
268        }
269
270        ///
271        /// Apply the given [Material] to this render target.
272        /// Use an empty array for the `lights` argument, if the material does not require lights to be rendered.
273        ///
274        pub fn apply_screen_material(
275            &self,
276            material: &dyn Material,
277            viewer: impl Viewer,
278            lights: &[&dyn Light],
279        ) -> &Self {
280            self.apply_screen_material_partially(self.scissor_box(), material, viewer, lights)
281        }
282
283        ///
284        /// Apply the given [Material] to the part of this render target defined by the scissor box.
285        /// Use an empty array for the `lights` argument, if the material does not require lights to be rendered.
286        ///
287        pub fn apply_screen_material_partially(
288            &self,
289            scissor_box: ScissorBox,
290            material: &dyn Material,
291            viewer: impl Viewer,
292            lights: &[&dyn Light],
293        ) -> &Self {
294            self.write_partially::<RendererError>(scissor_box, || {
295                apply_screen_material(&self.context, material, viewer, lights);
296                Ok(())
297            })
298            .unwrap();
299            self
300        }
301
302        ///
303        /// Apply the given [Effect] to this render target.
304        /// Use an empty array for the `lights` argument, if the effect does not require lights to be rendered.
305        ///
306        pub fn apply_screen_effect(
307            &self,
308            effect: &dyn Effect,
309            viewer: impl Viewer,
310            lights: &[&dyn Light],
311            color_texture: Option<ColorTexture>,
312            depth_texture: Option<DepthTexture>,
313        ) -> &Self {
314            self.apply_screen_effect_partially(
315                self.scissor_box(),
316                effect,
317                viewer,
318                lights,
319                color_texture,
320                depth_texture,
321            )
322        }
323
324        ///
325        /// Apply the given [Effect] to the part of this render target defined by the scissor box.
326        /// Use an empty array for the `lights` argument, if the effect does not require lights to be rendered.
327        ///
328        pub fn apply_screen_effect_partially(
329            &self,
330            scissor_box: ScissorBox,
331            effect: &dyn Effect,
332            viewer: impl Viewer,
333            lights: &[&dyn Light],
334            color_texture: Option<ColorTexture>,
335            depth_texture: Option<DepthTexture>,
336        ) -> &Self {
337            self.write_partially::<RendererError>(scissor_box, || {
338                apply_screen_effect(
339                    &self.context,
340                    effect,
341                    viewer,
342                    lights,
343                    color_texture,
344                    depth_texture,
345                );
346                Ok(())
347            })
348            .unwrap();
349            self
350        }
351    };
352}
353
354macro_rules! impl_render_target_extensions {
355    // 2 generic arguments with bounds
356    ($name:ident < $a:ident : $ta:tt , $b:ident : $tb:tt >) => {
357        impl<$a: $ta, $b: $tb> $name<$a, $b> {
358            impl_render_target_extensions_body!();
359        }
360    };
361    // 1 generic argument with bound
362    ($name:ident < $a:ident : $ta:tt >) => {
363        impl<$a: $ta> $name<$a> {
364            impl_render_target_extensions_body!();
365        }
366    };
367    // 1 liftetime argument
368    ($name:ident < $lt:lifetime >) => {
369        impl<$lt> $name<$lt> {
370            impl_render_target_extensions_body!();
371        }
372    };
373    // without any arguments
374    ($name:ty) => {
375        impl $name {
376            impl_render_target_extensions_body!();
377        }
378    };
379}
380
381impl_render_target_extensions!(RenderTarget<'a>);
382impl_render_target_extensions!(ColorTarget<'a>);
383impl_render_target_extensions!(DepthTarget<'a>);
384impl_render_target_extensions!(
385    RenderTargetMultisample<C: TextureDataType, D: DepthTextureDataType>
386);
387impl_render_target_extensions!(ColorTargetMultisample<C: TextureDataType>);
388impl_render_target_extensions!(DepthTargetMultisample<D: DepthTextureDataType>);
389
390///
391/// Combines shader ID components together into a single ID vector, to be used as a key in shader caching.
392///
393fn combine_ids(
394    geometry: GeometryId,
395    effect_material: EffectMaterialId,
396    lights: impl Iterator<Item = LightId>,
397) -> Vec<u8> {
398    let mut id = geometry.0.to_le_bytes().to_vec();
399    id.extend(effect_material.0.to_le_bytes());
400    id.extend(lights.map(|l| l.0));
401    return id;
402}
403
404///
405/// Render the given [Geometry] with the given [Material].
406/// Must be called in the callback given as input to a [RenderTarget], [ColorTarget] or [DepthTarget] write method.
407/// Use an empty array for the `lights` argument, if the material does not require lights to be rendered.
408///
409pub fn render_with_material(
410    context: &Context,
411    viewer: impl Viewer,
412    geometry: impl Geometry,
413    material: impl Material,
414    lights: &[&dyn Light],
415) {
416    let id = combine_ids(geometry.id(), material.id(), lights.iter().map(|l| l.id()));
417
418    let mut programs = context.programs.write().unwrap();
419    let program = programs.entry(id).or_insert_with(|| {
420        match Program::from_source(
421            context,
422            &geometry.vertex_shader_source(),
423            &material.fragment_shader_source(lights),
424        ) {
425            Ok(program) => program,
426            Err(err) => panic!("{}", err.to_string()),
427        }
428    });
429    material.use_uniforms(program, &viewer, lights);
430    geometry.draw(&viewer, program, material.render_states());
431}
432
433///
434/// Render the given [Geometry] with the given [Effect].
435/// Must be called in the callback given as input to a [RenderTarget], [ColorTarget] or [DepthTarget] write method.
436/// Use an empty array for the `lights` argument, if the effect does not require lights to be rendered.
437///
438pub fn render_with_effect(
439    context: &Context,
440    viewer: impl Viewer,
441    geometry: impl Geometry,
442    effect: impl Effect,
443    lights: &[&dyn Light],
444    color_texture: Option<ColorTexture>,
445    depth_texture: Option<DepthTexture>,
446) {
447    let id = combine_ids(
448        geometry.id(),
449        effect.id(color_texture, depth_texture),
450        lights.iter().map(|l| l.id()),
451    );
452
453    let mut programs = context.programs.write().unwrap();
454    let program = programs.entry(id).or_insert_with(|| {
455        match Program::from_source(
456            context,
457            &geometry.vertex_shader_source(),
458            &effect.fragment_shader_source(lights, color_texture, depth_texture),
459        ) {
460            Ok(program) => program,
461            Err(err) => panic!("{}", err.to_string()),
462        }
463    });
464    effect.use_uniforms(program, &viewer, lights, color_texture, depth_texture);
465    geometry.draw(&viewer, program, effect.render_states());
466}
467
468///
469/// Apply the given [Material] to the entire sceen.
470/// Must be called in the callback given as input to a [RenderTarget], [ColorTarget] or [DepthTarget] write method.
471/// Use an empty array for the `lights` argument, if the material does not require lights to be rendered.
472///
473pub fn apply_screen_material(
474    context: &Context,
475    material: impl Material,
476    viewer: impl Viewer,
477    lights: &[&dyn Light],
478) {
479    let id = combine_ids(
480        GeometryId::Screen,
481        material.id(),
482        lights.iter().map(|l| l.id()),
483    );
484
485    let mut programs = context.programs.write().unwrap();
486    let program = programs.entry(id).or_insert_with(|| {
487        match Program::from_source(
488            context,
489            full_screen_vertex_shader_source(),
490            &material.fragment_shader_source(lights),
491        ) {
492            Ok(program) => program,
493            Err(err) => panic!("{}", err.to_string()),
494        }
495    });
496    material.use_uniforms(program, &viewer, lights);
497    full_screen_draw(
498        context,
499        program,
500        material.render_states(),
501        viewer.viewport(),
502    );
503}
504
505///
506/// Apply the given [Effect] to the entire sceen.
507/// Must be called in the callback given as input to a [RenderTarget], [ColorTarget] or [DepthTarget] write method.
508/// Use an empty array for the `lights` argument, if the effect does not require lights to be rendered.
509///
510pub fn apply_screen_effect(
511    context: &Context,
512    effect: impl Effect,
513    viewer: impl Viewer,
514    lights: &[&dyn Light],
515    color_texture: Option<ColorTexture>,
516    depth_texture: Option<DepthTexture>,
517) {
518    let id = combine_ids(
519        GeometryId::Screen,
520        effect.id(color_texture, depth_texture),
521        lights.iter().map(|l| l.id()),
522    );
523
524    let mut programs = context.programs.write().unwrap();
525    let program = programs.entry(id).or_insert_with(|| {
526        match Program::from_source(
527            context,
528            full_screen_vertex_shader_source(),
529            &effect.fragment_shader_source(lights, color_texture, depth_texture),
530        ) {
531            Ok(program) => program,
532            Err(err) => panic!("{}", err.to_string()),
533        }
534    });
535    effect.use_uniforms(program, &viewer, lights, color_texture, depth_texture);
536    full_screen_draw(context, program, effect.render_states(), viewer.viewport());
537}
538
539///
540/// Compare function for sorting objects based on distance from the viewer.
541/// The order is opaque objects from nearest to farthest away from the viewer,
542/// then transparent objects from farthest away to closest to the viewer.
543///
544pub fn cmp_render_order(
545    viewer: impl Viewer,
546    obj0: impl Object,
547    obj1: impl Object,
548) -> std::cmp::Ordering {
549    if obj0.material_type() == MaterialType::Transparent
550        && obj1.material_type() != MaterialType::Transparent
551    {
552        std::cmp::Ordering::Greater
553    } else if obj0.material_type() != MaterialType::Transparent
554        && obj1.material_type() == MaterialType::Transparent
555    {
556        std::cmp::Ordering::Less
557    } else {
558        let distance_a = viewer.position().distance2(obj0.aabb().center());
559        let distance_b = viewer.position().distance2(obj1.aabb().center());
560        if distance_a.is_nan() || distance_b.is_nan() {
561            distance_a.is_nan().cmp(&distance_b.is_nan()) // whatever - just save us from panicing on unwrap below
562        } else if obj0.material_type() == MaterialType::Transparent {
563            distance_b.partial_cmp(&distance_a).unwrap()
564        } else {
565            distance_a.partial_cmp(&distance_b).unwrap()
566        }
567    }
568}
569
570///
571/// Finds the closest intersection between a ray from the given camera in the given pixel coordinate and the given geometries.
572/// The pixel coordinate must be in physical pixels, where (viewport.x, viewport.y) indicate the bottom left corner of the viewport
573/// and (viewport.x + viewport.width, viewport.y + viewport.height) indicate the top right corner.
574/// Returns ```None``` if no geometry was hit between the near (`z_near`) and far (`z_far`) plane for this camera.
575///
576pub fn pick(
577    context: &Context,
578    camera: &Camera,
579    pixel: impl Into<PhysicalPoint> + Copy,
580    geometries: impl IntoIterator<Item = impl Geometry>,
581) -> Option<IntersectionResult> {
582    let pos = camera.position_at_pixel(pixel);
583    let dir = camera.view_direction_at_pixel(pixel);
584    ray_intersect(
585        context,
586        pos + dir * camera.z_near(),
587        dir,
588        camera.z_far() - camera.z_near(),
589        geometries,
590    )
591}
592
593/// Result from an intersection test
594#[derive(Debug, Clone, Copy)]
595pub struct IntersectionResult {
596    /// The position of the intersection.
597    pub position: Vec3,
598    /// The index of the intersected geometry in the list of geometries.
599    pub geometry_id: u32,
600    /// The index of the intersected instance in the list of instances, ie. [gl_InstanceID](https://registry.khronos.org/OpenGL-Refpages/gl4/html/gl_InstanceID.xhtml).
601    /// This is 0 if the intersection did not hit an instanced geometry.
602    pub instance_id: u32,
603}
604
605///
606/// Finds the closest intersection between a ray starting at the given position in the given direction and the given geometries.
607/// Returns ```None``` if no geometry was hit before the given maximum depth.
608///
609pub fn ray_intersect(
610    context: &Context,
611    position: Vec3,
612    direction: Vec3,
613    max_depth: f32,
614    geometries: impl IntoIterator<Item = impl Geometry>,
615) -> Option<IntersectionResult> {
616    use crate::core::*;
617    let viewport = Viewport::new_at_origo(1, 1);
618    let up = if direction.dot(vec3(1.0, 0.0, 0.0)).abs() > 0.99 {
619        direction.cross(vec3(0.0, 1.0, 0.0))
620    } else {
621        direction.cross(vec3(1.0, 0.0, 0.0))
622    };
623    let camera = Camera::new_orthographic(
624        viewport,
625        position,
626        position + direction,
627        up,
628        0.01,
629        0.0,
630        max_depth,
631    );
632    let mut texture = Texture2D::new_empty::<[f32; 4]>(
633        context,
634        viewport.width,
635        viewport.height,
636        Interpolation::Nearest,
637        Interpolation::Nearest,
638        None,
639        Wrapping::ClampToEdge,
640        Wrapping::ClampToEdge,
641    );
642    let mut depth_texture = DepthTexture2D::new::<f32>(
643        context,
644        viewport.width,
645        viewport.height,
646        Wrapping::ClampToEdge,
647        Wrapping::ClampToEdge,
648    );
649    let mut material = IntersectionMaterial {
650        ..Default::default()
651    };
652    let result = RenderTarget::new(
653        texture.as_color_target(None),
654        depth_texture.as_depth_target(),
655    )
656    .clear(ClearState::color_and_depth(1.0, 1.0, 1.0, 1.0, 1.0))
657    .write::<RendererError>(|| {
658        for (id, geometry) in geometries.into_iter().enumerate() {
659            material.geometry_id = id as u32;
660            render_with_material(context, &camera, &geometry, &material, &[]);
661        }
662        Ok(())
663    })
664    .unwrap()
665    .read_color::<[f32; 4]>()[0];
666    let depth = result[0];
667    if depth < 1.0 {
668        Some(IntersectionResult {
669            position: position + direction * depth * max_depth,
670            geometry_id: result[1].to_bits(),
671            instance_id: result[2].to_bits(),
672        })
673    } else {
674        None
675    }
676}
677
678struct GeometryPassCamera<T>(T);
679
680impl<T: Viewer> Viewer for GeometryPassCamera<T> {
681    fn position(&self) -> Vec3 {
682        self.0.position()
683    }
684
685    fn view(&self) -> Mat4 {
686        self.0.view()
687    }
688
689    fn projection(&self) -> Mat4 {
690        self.0.projection()
691    }
692
693    fn viewport(&self) -> Viewport {
694        Viewport::new_at_origo(self.0.viewport().width, self.0.viewport().height)
695    }
696
697    fn z_near(&self) -> f32 {
698        self.0.z_near()
699    }
700
701    fn z_far(&self) -> f32 {
702        self.0.z_far()
703    }
704
705    fn color_mapping(&self) -> ColorMapping {
706        self.0.color_mapping()
707    }
708
709    fn tone_mapping(&self) -> ToneMapping {
710        self.0.tone_mapping()
711    }
712}