Skip to main content

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