Skip to main content

renderling/
lib.rs

1//! <div style="float: right; padding: 1em;">
2//!    <img
3//!       style="image-rendering: pixelated; image-rendering: -moz-crisp-edges;
4//! image-rendering: crisp-edges;"       alt="renderling mascot" width="180"
5//!       src="https://github.com/user-attachments/assets/83eafc47-287c-4b5b-8fd7-2063e56b2338"
6//!    />
7//! </div>
8//!
9//! `renderling` is a "GPU driven" renderer with a focus on simplicity and ease
10//! of use, targeting WebGPU.
11//!
12//! Shaders are written in Rust using [`rust-gpu`](https://rust-gpu.github.io/).
13//!
14//! ## Hello triangle
15//!
16//! Here we'll run through the classic "hello triangle", which will
17//! display a colored triangle.
18//!
19//! ### Context creation
20//!
21//! First you must create a [`Context`].
22//! The `Context` holds the render target - either a native window, an HTML
23//! canvas or a texture.
24//!
25//! ```
26//! use renderling::{context::Context, geometry::Vertex, stage::Stage};
27//!
28//! // create a headless context with dimensions 100, 100.
29//! let ctx = futures_lite::future::block_on(Context::headless(100, 100));
30//! ```
31//!
32//! [`Context::headless`] creates a `Context` that renders to a texture.
33//!
34//! [`Context::from_winit_window`] creates a `Context` that renders to a native
35//! window.
36//!
37//! [`Context::try_new_with_surface`] creates a `Context` that renders to any
38//! [`wgpu::SurfaceTarget`].
39//!
40//! See the [`renderling::context`](context) module documentation for
41//! more info.
42//!
43//! ### Staging resources
44//!
45//! We then create a "stage" to place the camera, geometry, materials and
46//! lights.
47//!
48//! ```
49//! # use renderling::{context::Context, stage::Stage};
50//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100));
51//! let stage: Stage = ctx
52//!     .new_stage()
53//!     .with_background_color([1.0, 1.0, 1.0, 1.0])
54//!     // For this demo we won't use lighting
55//!     .with_lighting(false);
56//! ```
57//!
58//! The [`Stage`] is neat in that it allows you to "stage" data
59//! directly onto the GPU. Those values can be modified on the CPU and
60//! synchronization will happen during [`Stage::render`].
61//!
62//! Use one of the many `Stage::new_*` functions to stage data on the GPU:
63//! * [`Stage::new_camera`]
64//! * [`Stage::new_vertices`]
65//! * [`Stage::new_indices`]
66//! * [`Stage::new_material`]
67//! * [`Stage::new_primitive`]
68//! * ...and more
69//!
70//! In order to render, we need to "stage" a
71//! [`Primitive`], which is a bundle of rendering
72//! resources, roughly representing a singular mesh.
73//!
74//! But first we'll need a list of [`Vertex`] organized
75//! as triangles with counter-clockwise winding. Here we'll use the builder
76//! pattern to create a staged [`Primitive`] using our vertices.
77//!
78//! We'll also create a [`Camera`] so we can see the stage.
79//!
80//! ```
81//! # use renderling::{context::Context, geometry::Vertex, stage::Stage};
82//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100));
83//! # let stage: Stage = ctx.new_stage();
84//! let vertices = stage.new_vertices([
85//!     Vertex::default()
86//!         .with_position([0.0, 0.0, 0.0])
87//!         .with_color([0.0, 1.0, 1.0, 1.0]),
88//!     Vertex::default()
89//!         .with_position([0.0, 100.0, 0.0])
90//!         .with_color([1.0, 1.0, 0.0, 1.0]),
91//!     Vertex::default()
92//!         .with_position([100.0, 0.0, 0.0])
93//!         .with_color([1.0, 0.0, 1.0, 1.0]),
94//! ]);
95//! let triangle_prim = stage.new_primitive().with_vertices(vertices);
96//!
97//! let camera = stage.new_camera().with_default_ortho2d(100.0, 100.0);
98//! ```
99//!
100//! ### Rendering
101//!
102//! Finally, we get the next frame from the context with
103//! [`Context::get_next_frame`]. Then we render to it using [`Stage::render`]
104//! and then present the frame with [`Frame::present`].
105//!
106//! ```
107//! # use renderling::{context::Context, geometry::Vertex, stage::Stage};
108//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100));
109//! # let stage = ctx.new_stage();
110//! # let camera = stage.new_camera().with_default_ortho2d(100.0, 100.0);
111//! # let vertices = stage.new_vertices([
112//! #     Vertex::default()
113//! #         .with_position([0.0, 0.0, 0.0])
114//! #         .with_color([0.0, 1.0, 1.0, 1.0]),
115//! #     Vertex::default()
116//! #         .with_position([0.0, 100.0, 0.0])
117//! #         .with_color([1.0, 1.0, 0.0, 1.0]),
118//! #     Vertex::default()
119//! #         .with_position([100.0, 0.0, 0.0])
120//! #         .with_color([1.0, 0.0, 1.0, 1.0]),
121//! #     ]);
122//! # let triangle_prim = stage
123//! #     .new_primitive()
124//! #     .with_vertices(vertices);
125//! let frame = ctx.get_next_frame().unwrap();
126//! stage.render(&frame.view());
127//! let img = futures_lite::future::block_on(frame.read_image()).unwrap();
128//! frame.present();
129//! ```
130//!
131//! Here for our purposes we also read the rendered frame as an image.
132//! Saving `img` should give us this:
133//!
134//! ![renderling hello triangle](https://github.com/schell/renderling/blob/main/test_img/cmy_triangle/hdr.png?raw=true)
135//!
136//! ### Modifying resources
137//!
138//! Later, if we want to modify any of the staged values, we can do so through
139//! each resource's struct, using `set_*`, `modify_*` and `with_*` functions.
140//!
141//! The changes made will be synchronized to the GPU at the beginning of the
142//! next [`Stage::render`] function.
143//!
144//! ### Removing and hiding primitives
145//!
146//! To remove primitives from the stage, use [`Stage::remove_primitive`].
147//! This will remove the primitive from rendering entirely, but the GPU
148//! resources will not be released until all clones have been dropped.
149//!
150//! If you just want to mark a [`Primitive`] invisible, use
151//! [`Primitive::set_visible`].
152//!
153//! ### Releasing resources
154//!
155//! GPU resources are automatically released when all clones are dropped.
156//! The data they occupy on the GPU is reclaimed during calls to
157//! [`Stage::render`].
158//! If you would like to manually reclaim the resources of fully dropped
159//! resources without rendering, you can do so with
160//! [`Stage::commit`].
161//!
162//! #### Ensuring resources are released
163//!
164//! Keep in mind that many resource functions (like [`Primitive::set_material`]
165//! for example) take another resource as a parameter. In these functions the
166//! parameter resource is cloned and held internally. This is done to keep
167//! resources that are in use from being released. Therefore if you want a
168//! resource to be released, you must ensure that all references to it are
169//! removed. You can use the `remove_*` functions on many resources for this
170//! purpose, like [`Primitive::remove_material`], for example, which would
171//! remove the material from the primitive. After that call, if no other
172//! primitives are using that material and the material is dropped from
173//! user code, the next call to [`Stage::render`] or [`Stage::commit`] will
174//! reclaim the GPU resources of the material to be re-used.
175//!
176//! Other resources like [`Vertices`], [`Indices`], [`Transform`],
177//! [`NestedTransform`] and others can simply be dropped.
178//!
179//! # Next steps
180//!
181//! For further introduction to what renderling can do, take a tour of the
182//! [`Stage`] type, or get started with
183//! [the manual](https://renderling.xyz/manual/index.html).
184//!
185//! # WARNING
186//!
187//! This is very much a work in progress.
188//!
189//! Your mileage may vary, but I hope you get good use out of this library.
190//!
191//! PRs, criticisms and ideas are all very much welcomed [at the
192//! repo](https://github.com/schell/renderling).
193//!
194//! 😀☕
195#![cfg_attr(gpu, no_std)]
196#![deny(clippy::disallowed_methods)]
197
198#[cfg(doc)]
199use crate::{camera::Camera, geometry::*, primitive::Primitive, stage::Stage, transform::*};
200
201pub mod atlas;
202#[cfg(cpu)]
203pub(crate) mod bindgroup;
204pub mod bloom;
205pub mod bvol;
206pub mod camera;
207pub mod color;
208pub mod compositor;
209#[cfg(cpu)]
210pub mod context;
211pub mod convolution;
212pub mod cubemap;
213pub mod cull;
214pub mod debug;
215pub mod draw;
216pub mod geometry;
217#[cfg(all(cpu, gltf))]
218pub mod gltf;
219#[cfg(cpu)]
220pub mod internal;
221pub mod light;
222#[cfg(cpu)]
223pub mod linkage;
224pub mod material;
225pub mod math;
226pub mod pbr;
227pub mod primitive;
228pub mod sdf;
229pub mod skybox;
230pub mod stage;
231pub mod sync;
232#[cfg(cpu)]
233pub mod texture;
234pub mod tonemapping;
235pub mod transform;
236pub mod tutorial;
237#[cfg(cpu)]
238pub mod types;
239pub mod ui_slab;
240
241pub extern crate glam;
242
243// TODO: document the crate's feature flags here.
244// Similar to [ndarray](https://docs.rs/ndarray/latest/ndarray/#crate-feature-flags).
245
246#[macro_export]
247/// A wrapper around `std::println` that is a noop on the GPU.
248macro_rules! println {
249    ($($arg:tt)*) => {
250        #[cfg(cpu)]
251        {
252            std::println!($($arg)*);
253        }
254    }
255}
256
257#[cfg(all(cpu, any(test, feature = "test-utils")))]
258#[allow(unused, reason = "Used in debugging on macos")]
259pub fn capture_gpu_frame<T>(
260    ctx: &crate::context::Context,
261    path: impl AsRef<std::path::Path>,
262    f: impl FnOnce() -> T,
263) -> T {
264    let path = path.as_ref();
265    let parent = path.parent().unwrap();
266    std::fs::create_dir_all(parent).unwrap();
267
268    #[cfg(target_os = "macos")]
269    {
270        if path.exists() {
271            log::info!(
272                "deleting {} before writing gpu frame capture",
273                path.display()
274            );
275            std::fs::remove_dir_all(path).unwrap();
276        }
277
278        if std::env::var("METAL_CAPTURE_ENABLED").is_err() {
279            log::error!("Env var METAL_CAPTURE_ENABLED must be set");
280            panic!("missing METAL_CAPTURE_ENABLED=1");
281        }
282
283        let m = metal::CaptureManager::shared();
284        let desc = metal::CaptureDescriptor::new();
285
286        desc.set_destination(metal::MTLCaptureDestination::GpuTraceDocument);
287        desc.set_output_url(path);
288        let maybe_metal_device = unsafe { ctx.get_device().as_hal::<wgpu_core::api::Metal>() };
289        if let Some(metal_device) = maybe_metal_device {
290            desc.set_capture_device(metal_device.raw_device().try_lock().unwrap().as_ref());
291        } else {
292            panic!("not a capturable device")
293        }
294        m.start_capture(&desc).unwrap();
295        let t = f();
296        m.stop_capture();
297        t
298    }
299    #[cfg(not(target_os = "macos"))]
300    {
301        log::warn!("capturing a GPU frame is only supported on macos");
302        f()
303    }
304}
305
306#[cfg(all(cpu, any(test, feature = "test-utils")))]
307#[allow(unused, reason = "Used in sync tests in userland")]
308/// Marker trait to block on futures in synchronous code.
309///
310/// This is a simple convenience.
311/// Many of the tests in this crate render something and then read a
312/// texture in order to perform a diff on the result using a known image.
313/// Since reading from the GPU is async, this trait helps cut down
314/// boilerplate.
315pub trait BlockOnFuture {
316    type Output;
317
318    /// Block on the future using [`futures_util::future::block_on`].
319    fn block(self) -> Self::Output;
320}
321
322#[cfg(all(cpu, any(test, feature = "test-utils")))]
323impl<T: std::future::Future> BlockOnFuture for T {
324    type Output = <Self as std::future::Future>::Output;
325
326    fn block(self) -> Self::Output {
327        futures_lite::future::block_on(self)
328    }
329}
330
331#[cfg(test)]
332mod test {
333    use super::*;
334    use crate::{atlas::AtlasImage, context::Context, geometry::Vertex, light::Lux};
335
336    use glam::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4};
337    use img_diff::DiffCfg;
338    use light::AnalyticalLight;
339    use pretty_assertions::assert_eq;
340    use stage::Stage;
341
342    #[allow(unused_imports)]
343    pub use renderling_build::{test_output_dir, workspace_dir};
344
345    pub use super::BlockOnFuture;
346
347    #[cfg_attr(not(target_arch = "wasm32"), ctor::ctor)]
348    fn init_logging() {
349        let _ = env_logger::builder().is_test(true).try_init();
350        log::info!("logging is on");
351    }
352
353    #[allow(unused, reason = "Used in debugging on macos")]
354    pub fn capture_gpu_frame<T>(
355        ctx: &Context,
356        path: impl AsRef<std::path::Path>,
357        f: impl FnOnce() -> T,
358    ) -> T {
359        let path = workspace_dir().join("test_output").join(path);
360        super::capture_gpu_frame(ctx, path, f)
361    }
362
363    pub fn make_two_directional_light_setup(stage: &Stage) -> (AnalyticalLight, AnalyticalLight) {
364        let sunlight_a = stage
365            .new_directional_light()
366            .with_direction(Vec3::new(-0.8, -1.0, 0.5).normalize())
367            .with_color(Vec4::ONE)
368            .with_intensity(Lux::OUTDOOR_DIRECT_SUNLIGHT_HIGH);
369        let sunlight_b = stage
370            .new_directional_light()
371            .with_direction(Vec3::new(1.0, 1.0, -0.1).normalize())
372            .with_color(Vec4::ONE)
373            .with_intensity(Lux::OUTDOOR_FOXS_WEDDING);
374        (sunlight_a.into_generic(), sunlight_b.into_generic())
375    }
376
377    #[test]
378    fn sanity_transmute() {
379        let zerof32 = 0f32;
380        let zerof32asu32: u32 = zerof32.to_bits();
381        assert_eq!(0, zerof32asu32);
382
383        let foure_45 = 4e-45f32;
384        let in_u32: u32 = foure_45.to_bits();
385        assert_eq!(3, in_u32);
386
387        let u32max = u32::MAX;
388        let f32nan: f32 = f32::from_bits(u32max);
389        assert!(f32nan.is_nan());
390
391        let u32max: u32 = f32nan.to_bits();
392        assert_eq!(u32::MAX, u32max);
393    }
394
395    pub fn right_tri_vertices() -> Vec<Vertex> {
396        vec![
397            Vertex::default()
398                .with_position([0.0, 0.0, 0.0])
399                .with_color([0.0, 1.0, 1.0, 1.0]),
400            Vertex::default()
401                .with_position([0.0, 100.0, 0.0])
402                .with_color([1.0, 1.0, 0.0, 1.0]),
403            Vertex::default()
404                .with_position([100.0, 0.0, 0.0])
405                .with_color([1.0, 0.0, 1.0, 1.0]),
406        ]
407    }
408
409    #[test]
410    // This tests our ability to draw a CMYK triangle in the top left corner.
411    fn cmy_triangle_sanity() {
412        let ctx = Context::headless(100, 100).block();
413        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));
414        let (p, v) = crate::camera::default_ortho2d(100.0, 100.0);
415        let _camera = stage.new_camera().with_projection_and_view(p, v);
416
417        let _prim = stage
418            .new_primitive()
419            .with_vertices(stage.new_vertices(right_tri_vertices()));
420
421        let frame = ctx.get_next_frame().unwrap();
422        stage.render(&frame.view());
423        frame.present();
424
425        let depth_texture = stage.get_depth_texture();
426        let depth_img = depth_texture.read_image().block().unwrap().unwrap();
427        img_diff::assert_img_eq("cmy_triangle/depth.png", depth_img);
428
429        let hdr_img = stage
430            .hdr_texture
431            .read()
432            .unwrap()
433            .read_hdr_image(&ctx)
434            .block()
435            .unwrap();
436        img_diff::assert_img_eq("cmy_triangle/hdr.png", hdr_img);
437
438        let bloom_mix = stage
439            .bloom
440            .get_mix_texture()
441            .read_hdr_image(&ctx)
442            .block()
443            .unwrap();
444        img_diff::assert_img_eq("cmy_triangle/bloom_mix.png", bloom_mix);
445    }
446
447    #[test]
448    // This tests our ability to draw a CMYK triangle in the top left corner,
449    // using CW geometry.
450    fn cmy_triangle_backface() {
451        use img_diff::DiffCfg;
452
453        let ctx = Context::headless(100, 100).block();
454        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));
455        let (p, v) = crate::camera::default_ortho2d(100.0, 100.0);
456        let _camera = stage.new_camera().with_projection_and_view(p, v);
457        let _rez = stage.new_primitive().with_vertices(stage.new_vertices({
458            let mut vs = right_tri_vertices();
459            vs.reverse();
460            vs
461        }));
462
463        let frame = ctx.get_next_frame().unwrap();
464        stage.render(&frame.view());
465        let img = frame.read_linear_image().block().unwrap();
466        img_diff::assert_img_eq_cfg(
467            "cmy_triangle/hdr.png",
468            img,
469            DiffCfg {
470                test_name: Some("cmy_triangle_backface.png"),
471                ..Default::default()
472            },
473        );
474    }
475
476    #[test]
477    // This tests our ability to update the transform of a `Renderlet` after it
478    // has already been sent to the GPU.
479    // We do this by writing over the previous transform in the stage.
480    fn cmy_triangle_update_transform() {
481        let ctx = Context::headless(100, 100).block();
482        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));
483        let (p, v) = crate::camera::default_ortho2d(100.0, 100.0);
484        let _camera = stage.new_camera().with_projection_and_view(p, v);
485        let transform = stage.new_transform();
486        let _renderlet = stage
487            .new_primitive()
488            .with_vertices(stage.new_vertices(right_tri_vertices()))
489            .with_transform(&transform);
490
491        let frame = ctx.get_next_frame().unwrap();
492        stage.render(&frame.view());
493
494        transform
495            .set_translation(Vec3::new(100.0, 0.0, 0.0))
496            .set_rotation(Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2))
497            .set_scale(Vec3::new(0.5, 0.5, 1.0));
498
499        stage.render(&frame.view());
500        let img = frame.read_linear_image().block().unwrap();
501        img_diff::assert_img_eq("cmy_triangle/update_transform.png", img);
502    }
503
504    /// Points around a pyramid height=1 with the base around the origin.
505    ///
506    ///    yb
507    ///    |               *top
508    ///    |___x       tl_____tr
509    ///   /    g        /    /
510    /// z/r          bl/____/br
511    fn pyramid_points() -> [Vec3; 5] {
512        let tl = Vec3::new(-0.5, -0.5, -0.5);
513        let tr = Vec3::new(0.5, -0.5, -0.5);
514        let br = Vec3::new(0.5, -0.5, 0.5);
515        let bl = Vec3::new(-0.5, -0.5, 0.5);
516        let top = Vec3::new(0.0, 0.5, 0.0);
517        [tl, tr, br, bl, top]
518    }
519
520    fn pyramid_indices() -> [u16; 18] {
521        let (tl, tr, br, bl, top) = (0, 1, 2, 3, 4);
522        [
523            tl, br, bl, tl, tr, br, br, top, bl, bl, top, tl, tl, top, tr, tr, top, br,
524        ]
525    }
526
527    fn cmy_gpu_vertex(p: Vec3) -> Vertex {
528        let r: f32 = p.z + 0.5;
529        let g: f32 = p.x + 0.5;
530        let b: f32 = p.y + 0.5;
531        Vertex::default()
532            .with_position([p.x.min(1.0), p.y.min(1.0), p.z.min(1.0)])
533            .with_color([r, g, b, 1.0])
534    }
535
536    pub fn gpu_cube_vertices() -> Vec<Vertex> {
537        math::UNIT_INDICES
538            .iter()
539            .map(|i| cmy_gpu_vertex(math::UNIT_POINTS[*i]))
540            .collect()
541    }
542
543    #[test]
544    // Tests our ability to draw a CMYK cube.
545    fn cmy_cube_sanity() {
546        let ctx = Context::headless(100, 100).block();
547        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));
548        let camera_position = Vec3::new(0.0, 12.0, 20.0);
549        let _camera = stage.new_camera().with_projection_and_view(
550            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),
551            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),
552        );
553        let _rez = stage
554            .new_primitive()
555            .with_vertices(stage.new_vertices(gpu_cube_vertices()))
556            .with_transform(
557                stage
558                    .new_transform()
559                    .with_scale(Vec3::new(6.0, 6.0, 6.0))
560                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),
561            );
562
563        let frame = ctx.get_next_frame().unwrap();
564        stage.render(&frame.view());
565        let img = frame.read_image().block().unwrap();
566        img_diff::assert_img_eq("cmy_cube/sanity.png", img);
567    }
568
569    #[test]
570    // Tests our ability to draw a CMYK cube using indexed geometry.
571    fn cmy_cube_indices() {
572        let ctx = Context::headless(100, 100).block();
573        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));
574        let camera_position = Vec3::new(0.0, 12.0, 20.0);
575        let _camera = stage.new_camera().with_projection_and_view(
576            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),
577            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),
578        );
579
580        let _rez = stage
581            .new_primitive()
582            .with_vertices(stage.new_vertices(math::UNIT_POINTS.map(cmy_gpu_vertex)))
583            .with_indices(stage.new_indices(math::UNIT_INDICES.map(|i| i as u32)))
584            .with_transform(
585                stage
586                    .new_transform()
587                    .with_scale(Vec3::new(6.0, 6.0, 6.0))
588                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),
589            );
590
591        let frame = ctx.get_next_frame().unwrap();
592        stage.render(&frame.view());
593        let img = frame.read_image().block().unwrap();
594        img_diff::assert_img_eq_cfg(
595            "cmy_cube/sanity.png",
596            img,
597            DiffCfg {
598                test_name: Some("cmy_cube/indices"),
599                ..Default::default()
600            },
601        );
602    }
603
604    #[test]
605    // Test our ability to create two cubes and toggle the visibility of one of
606    // them.
607    fn cmy_cube_visible() {
608        let ctx = Context::headless(100, 100).block();
609        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));
610        let (projection, view) = camera::default_perspective(100.0, 100.0);
611        let _camera = stage
612            .new_camera()
613            .with_projection_and_view(projection, view);
614        let geometry = stage.new_vertices(gpu_cube_vertices());
615        let _cube_one = stage
616            .new_primitive()
617            .with_vertices(&geometry)
618            .with_transform(
619                stage
620                    .new_transform()
621                    .with_translation(Vec3::new(-4.5, 0.0, 0.0))
622                    .with_scale(Vec3::new(6.0, 6.0, 6.0))
623                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),
624            );
625
626        let cube_two = stage
627            .new_primitive()
628            .with_vertices(&geometry)
629            .with_transform(
630                stage
631                    .new_transform()
632                    .with_translation(Vec3::new(4.5, 0.0, 0.0))
633                    .with_scale(Vec3::new(6.0, 6.0, 6.0))
634                    .with_rotation(Quat::from_axis_angle(Vec3::Y, std::f32::consts::FRAC_PI_4)),
635            );
636
637        // we should see two colored cubes
638        let frame = ctx.get_next_frame().unwrap();
639        stage.render(&frame.view());
640        let img = frame.read_image().block().unwrap();
641        img_diff::assert_img_eq("cmy_cube/visible_before.png", img.clone());
642        let img_before = img;
643        frame.present();
644
645        // update cube two making it invisible
646        cube_two.set_visible(false);
647
648        // we should see only one colored cube
649        let frame = ctx.get_next_frame().unwrap();
650        stage.render(&frame.view());
651        let img = frame.read_image().block().unwrap();
652        img_diff::assert_img_eq("cmy_cube/visible_after.png", img);
653        frame.present();
654
655        // update cube two making in visible again
656        cube_two.set_visible(true);
657
658        // we should see two colored cubes again
659        let frame = ctx.get_next_frame().unwrap();
660        stage.render(&frame.view());
661        let img = frame.read_image().block().unwrap();
662        img_diff::assert_eq("cmy_cube/visible_before_again.png", img_before, img);
663    }
664
665    #[test]
666    // Tests the ability to specify indexed vertices, as well as the ability to
667    // update a field within a struct stored on the slab by using a `Hybrid`.
668    fn cmy_cube_remesh() {
669        let ctx = Context::headless(100, 100).block();
670        let stage = ctx
671            .new_stage()
672            .with_lighting(false)
673            .with_background_color(Vec4::splat(1.0));
674        let (projection, view) = camera::default_perspective(100.0, 100.0);
675        let _camera = stage
676            .new_camera()
677            .with_projection_and_view(projection, view);
678        let cube = stage
679            .new_primitive()
680            .with_vertices(
681                stage
682                    .new_vertices(math::UNIT_INDICES.map(|i| cmy_gpu_vertex(math::UNIT_POINTS[i]))),
683            )
684            .with_transform(
685                stage
686                    .new_transform()
687                    .with_scale(Vec3::new(10.0, 10.0, 10.0)),
688            );
689
690        // we should see a cube (in sRGB color space)
691        let frame = ctx.get_next_frame().unwrap();
692        stage.render(&frame.view());
693        let img = frame.read_image().block().unwrap();
694        img_diff::assert_img_eq("cmy_cube/remesh_before.png", img);
695        frame.present();
696
697        // Update the cube mesh to a pyramid by overwriting the `.vertices`
698        // field of `Renderlet`
699        let pyramid_points = pyramid_points();
700        let pyramid_geometry = stage
701            .new_vertices(pyramid_indices().map(|i| cmy_gpu_vertex(pyramid_points[i as usize])));
702        cube.set_vertices(pyramid_geometry);
703
704        // we should see a pyramid (in sRGB color space)
705        let frame = ctx.get_next_frame().unwrap();
706        stage.render(&frame.view());
707        let img = frame.read_image().block().unwrap();
708        img_diff::assert_img_eq("cmy_cube/remesh_after.png", img);
709    }
710
711    fn gpu_uv_unit_cube() -> Vec<Vertex> {
712        let p: [Vec3; 8] = math::UNIT_POINTS;
713        let tl = Vec2::new(0.0, 0.0);
714        let tr = Vec2::new(1.0, 0.0);
715        let bl = Vec2::new(0.0, 1.0);
716        let br = Vec2::new(1.0, 1.0);
717
718        vec![
719            // top
720            Vertex::default().with_position(p[0]).with_uv0(bl),
721            Vertex::default().with_position(p[2]).with_uv0(tr),
722            Vertex::default().with_position(p[1]).with_uv0(tl),
723            Vertex::default().with_position(p[0]).with_uv0(bl),
724            Vertex::default().with_position(p[3]).with_uv0(br),
725            Vertex::default().with_position(p[2]).with_uv0(tr),
726            // bottom
727            Vertex::default().with_position(p[4]).with_uv0(bl),
728            Vertex::default().with_position(p[6]).with_uv0(tr),
729            Vertex::default().with_position(p[5]).with_uv0(tl),
730            Vertex::default().with_position(p[4]).with_uv0(bl),
731            Vertex::default().with_position(p[7]).with_uv0(br),
732            Vertex::default().with_position(p[6]).with_uv0(tr),
733            // left
734            Vertex::default().with_position(p[7]).with_uv0(bl),
735            Vertex::default().with_position(p[0]).with_uv0(tr),
736            Vertex::default().with_position(p[1]).with_uv0(tl),
737            Vertex::default().with_position(p[7]).with_uv0(bl),
738            Vertex::default().with_position(p[4]).with_uv0(br),
739            Vertex::default().with_position(p[0]).with_uv0(tr),
740            // right
741            Vertex::default().with_position(p[5]).with_uv0(bl),
742            Vertex::default().with_position(p[2]).with_uv0(tr),
743            Vertex::default().with_position(p[3]).with_uv0(tl),
744            Vertex::default().with_position(p[5]).with_uv0(bl),
745            Vertex::default().with_position(p[6]).with_uv0(br),
746            Vertex::default().with_position(p[2]).with_uv0(tr),
747            // front
748            Vertex::default().with_position(p[4]).with_uv0(bl),
749            Vertex::default().with_position(p[3]).with_uv0(tr),
750            Vertex::default().with_position(p[0]).with_uv0(tl),
751            Vertex::default().with_position(p[4]).with_uv0(bl),
752            Vertex::default().with_position(p[5]).with_uv0(br),
753            Vertex::default().with_position(p[3]).with_uv0(tr),
754        ]
755    }
756
757    #[test]
758    // Tests that updating the material actually updates the rendering of an
759    // unlit mesh
760    fn unlit_textured_cube_material() {
761        let ctx = Context::headless(100, 100).block();
762        let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0));
763        let (projection, view) = camera::default_perspective(100.0, 100.0);
764        let _camera = stage
765            .new_camera()
766            .with_projection_and_view(projection, view);
767
768        let sandstone = AtlasImage::from(image::open("../../img/sandstone.png").unwrap());
769        let dirt = AtlasImage::from(image::open("../../img/dirt.jpg").unwrap());
770        let entries = stage.set_images([sandstone, dirt]).unwrap();
771
772        let material = stage
773            .new_material()
774            .with_albedo_texture(&entries[0])
775            .with_has_lighting(false);
776        let cube = stage
777            .new_primitive()
778            .with_vertices(stage.new_vertices(gpu_uv_unit_cube()))
779            .with_transform(
780                stage
781                    .new_transform()
782                    .with_scale(Vec3::new(10.0, 10.0, 10.0)),
783            )
784            .with_material(&material);
785        println!("cube: {:?}", cube.descriptor());
786
787        // we should see a cube with a stoney texture
788        let frame = ctx.get_next_frame().unwrap();
789        stage.render(&frame.view());
790        let img = frame.read_image().block().unwrap();
791        img_diff::assert_img_eq("unlit_textured_cube_material_before.png", img);
792        frame.present();
793
794        // update the material's texture on the GPU
795        material.set_albedo_texture(&entries[1]);
796
797        // we should see a cube with a dirty texture
798        let frame = ctx.get_next_frame().unwrap();
799        stage.render(&frame.view());
800        let img = frame.read_image().block().unwrap();
801        img_diff::assert_img_eq("unlit_textured_cube_material_after.png", img);
802
803        // let size = stage.atlas.get_size();
804        // for i in 0..size.depth_or_array_layers {
805        //     let atlas_img = stage.atlas.atlas_img(&ctx, i);
806        //     img_diff::save(
807        //         &format!("unlit_texture_cube_atlas_layer_{i}.png"),
808        //         atlas_img,
809        //     );
810        // }
811    }
812
813    #[test]
814    // Ensures that we can render multiple nodes with mesh primitives
815    // that share the same geometry, but have different materials.
816    fn multi_node_scene() {
817        let ctx = Context::headless(100, 100).block();
818        let stage = ctx
819            .new_stage()
820            .with_background_color(Vec3::splat(0.0).extend(1.0));
821
822        let (projection, view) = camera::default_ortho2d(100.0, 100.0);
823        let _camera = stage
824            .new_camera()
825            .with_projection_and_view(projection, view);
826
827        // now test the textures functionality
828        let img = AtlasImage::from_path("../../img/cheetah.jpg").unwrap();
829        let entries = stage.set_images([img]).unwrap();
830
831        let geometry = stage.new_vertices([
832            Vertex {
833                position: Vec3::new(0.0, 0.0, 0.0),
834                color: Vec4::new(1.0, 1.0, 0.0, 1.0),
835                uv0: Vec2::new(0.0, 0.0),
836                uv1: Vec2::new(0.0, 0.0),
837                ..Default::default()
838            },
839            Vertex {
840                position: Vec3::new(100.0, 100.0, 0.0),
841                color: Vec4::new(0.0, 1.0, 1.0, 1.0),
842                uv0: Vec2::new(1.0, 1.0),
843                uv1: Vec2::new(1.0, 1.0),
844                ..Default::default()
845            },
846            Vertex {
847                position: Vec3::new(100.0, 0.0, 0.0),
848                color: Vec4::new(1.0, 0.0, 1.0, 1.0),
849                uv0: Vec2::new(1.0, 0.0),
850                uv1: Vec2::new(1.0, 0.0),
851                ..Default::default()
852            },
853        ]);
854        let _color_prim = stage.new_primitive().with_vertices(&geometry);
855
856        let material = stage
857            .new_material()
858            .with_albedo_texture(&entries[0])
859            .with_has_lighting(false);
860        let transform = stage
861            .new_transform()
862            .with_translation(Vec3::new(15.0, 35.0, 0.5))
863            .with_scale(Vec3::new(0.5, 0.5, 1.0));
864        let _rez = stage
865            .new_primitive()
866            .with_vertices(&geometry)
867            .with_material(material)
868            .with_transform(transform);
869
870        let frame = ctx.get_next_frame().unwrap();
871        stage.render(&frame.view());
872        let img = frame.read_image().block().unwrap();
873        img_diff::assert_img_eq("stage/shared_node_with_different_materials.png", img);
874    }
875
876    #[test]
877    /// Tests shading with directional light.
878    fn scene_cube_directional() {
879        let ctx = Context::headless(100, 100).block();
880        let stage = ctx
881            .new_stage()
882            .with_bloom(false)
883            .with_background_color(Vec3::splat(0.0).extend(1.0));
884
885        let (projection, _) = camera::default_perspective(100.0, 100.0);
886        let view = Mat4::look_at_rh(Vec3::new(1.8, 1.8, 1.8), Vec3::ZERO, Vec3::Y);
887        let _camera = stage
888            .new_camera()
889            .with_projection_and_view(projection, view);
890
891        let red = Vec3::X.extend(1.0);
892        let green = Vec3::Y.extend(1.0);
893        let blue = Vec3::Z.extend(1.0);
894        let _dir_red = stage
895            .new_directional_light()
896            .with_direction(Vec3::NEG_Y)
897            .with_color(red)
898            .with_intensity(Lux::OUTDOOR_FULL_DAYLIGHT_LOW);
899        let _dir_green = stage
900            .new_directional_light()
901            .with_direction(Vec3::NEG_X)
902            .with_color(green)
903            .with_intensity(Lux::OUTDOOR_FULL_DAYLIGHT_LOW);
904        let _dir_blue = stage
905            .new_directional_light()
906            .with_direction(Vec3::NEG_Z)
907            .with_color(blue)
908            .with_intensity(Lux::OUTDOOR_FULL_DAYLIGHT_LOW);
909
910        let _rez = stage
911            .new_primitive()
912            .with_material(stage.default_material());
913
914        let frame = ctx.get_next_frame().unwrap();
915        stage.render(&frame.view());
916        println!(
917            "lighting_descriptor: {:#?}",
918            stage.lighting.lighting_descriptor.get()
919        );
920        let img = frame.read_image().block().unwrap();
921        let depth_texture = stage.get_depth_texture();
922        let depth_img = depth_texture.read_image().block().unwrap().unwrap();
923        img_diff::assert_img_eq("stage/cube_directional_depth.png", depth_img);
924        img_diff::assert_img_eq("stage/cube_directional.png", img);
925    }
926
927    #[test]
928    // Test to make sure that we can reconstruct a normal matrix without using
929    // the inverse transpose of a model matrix, so long as we have the T R S
930    // transformation components (we really only need the scale).
931    //
932    // see Eric's comment here https://computergraphics.stackexchange.com/questions/1502/why-is-the-transposed-inverse-of-the-model-view-matrix-used-to-transform-the-nor?newreg=ffeabc7602da4fa2bc15fb9c84179dff
933    // see Eric's blog post here https://lxjk.github.io/2017/10/01/Stop-Using-Normal-Matrix.html
934    // squaring a vector https://math.stackexchange.com/questions/1419887/squaring-a-vector#1419889
935    // more convo wrt shaders https://github.com/mrdoob/three.js/issues/18497
936    fn square_scale_norm_check() {
937        let quat = Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_4);
938        let scale = Vec3::new(10.0, 20.0, 1.0);
939        let model_matrix = Mat4::from_translation(Vec3::new(10.0, 10.0, 20.0))
940            * Mat4::from_quat(quat)
941            * Mat4::from_scale(scale);
942        let normal_matrix = model_matrix.inverse().transpose();
943        let scale2 = scale * scale;
944
945        for i in 0..9 {
946            for j in 0..9 {
947                for k in 0..9 {
948                    if i == 0 && j == 0 && k == 0 {
949                        continue;
950                    }
951                    let norm = Vec3::new(i as f32, j as f32, k as f32).normalize();
952                    let model = Mat3::from_mat4(model_matrix);
953                    let norm_a = (Mat3::from_mat4(normal_matrix) * norm).normalize();
954                    let norm_b = (model * (norm / scale2)).normalize();
955                    assert!(
956                        norm_a.abs_diff_eq(norm_b, f32::EPSILON),
957                        "norm:{norm}, scale2:{scale2}"
958                    );
959                }
960            }
961        }
962    }
963
964    #[test]
965    // shows how to "nest" children to make them appear transformed by their
966    // parent's transform
967    fn scene_parent_sanity() {
968        let ctx = Context::headless(100, 100).block();
969        let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0));
970        let (projection, view) = camera::default_ortho2d(100.0, 100.0);
971        let _camera = stage
972            .new_camera()
973            .with_projection_and_view(projection, view);
974
975        let root_node = stage
976            .new_nested_transform()
977            .with_local_scale(Vec3::new(25.0, 25.0, 1.0));
978        println!("root_node: {:#?}", root_node.global_descriptor());
979
980        let offset = Vec3::new(1.0, 1.0, 0.0);
981
982        let cyan_node = stage.new_nested_transform().with_local_translation(offset);
983        println!("cyan_node: {:#?}", cyan_node.global_descriptor());
984
985        let yellow_node = stage.new_nested_transform().with_local_translation(offset);
986        println!("yellow_node: {:#?}", yellow_node.global_descriptor());
987
988        let red_node = stage.new_nested_transform().with_local_translation(offset);
989        println!("red_node: {:#?}", red_node.global_descriptor());
990
991        root_node.add_child(&cyan_node);
992        println!("cyan_node: {:#?}", cyan_node.global_descriptor());
993        cyan_node.add_child(&yellow_node);
994        println!("yellow_node: {:#?}", yellow_node.global_descriptor());
995        yellow_node.add_child(&red_node);
996        println!("red_node: {:#?}", red_node.global_descriptor());
997
998        let geometry = stage.new_vertices({
999            let size = 1.0;
1000            [
1001                Vertex::default().with_position([0.0, 0.0, 0.0]),
1002                Vertex::default().with_position([size, size, 0.0]),
1003                Vertex::default().with_position([size, 0.0, 0.0]),
1004            ]
1005        });
1006        let _cyan_primitive = stage
1007            .new_primitive()
1008            .with_vertices(&geometry)
1009            .with_material(
1010                stage
1011                    .new_material()
1012                    .with_albedo_factor(Vec4::new(0.0, 1.0, 1.0, 1.0))
1013                    .with_has_lighting(false),
1014            )
1015            .with_transform(&cyan_node);
1016        let _yellow_primitive = stage
1017            .new_primitive()
1018            .with_vertices(&geometry)
1019            .with_material(
1020                stage
1021                    .new_material()
1022                    .with_albedo_factor(Vec4::new(1.0, 1.0, 0.0, 1.0))
1023                    .with_has_lighting(false),
1024            )
1025            .with_transform(&yellow_node);
1026        let _red_primitive = stage
1027            .new_primitive()
1028            .with_vertices(&geometry)
1029            .with_material(
1030                stage
1031                    .new_material()
1032                    .with_albedo_factor(Vec4::new(1.0, 0.0, 0.0, 1.0))
1033                    .with_has_lighting(false),
1034            )
1035            .with_transform(&red_node);
1036
1037        let frame = ctx.get_next_frame().unwrap();
1038        stage.render(&frame.view());
1039        let img = frame.read_image().block().unwrap();
1040        img_diff::assert_img_eq("scene_parent_sanity.png", img);
1041    }
1042
1043    #[test]
1044    // sanity tests that we can extract the position of the camera using the
1045    // camera's view transform
1046    fn camera_position_from_view_matrix() {
1047        let position = Vec3::new(1.0, 2.0, 12.0);
1048        let view = Mat4::look_at_rh(position, Vec3::new(1.0, 2.0, 0.0), Vec3::Y);
1049        let extracted_position = view.inverse().transform_point3(Vec3::ZERO);
1050        assert_eq!(position, extracted_position);
1051    }
1052
1053    #[test]
1054    fn can_resize_context_and_stage() {
1055        let size = UVec2::new(100, 100);
1056        let mut ctx = Context::headless(size.x, size.y).block();
1057        let stage = ctx.new_stage();
1058
1059        // create the CMY cube
1060        let camera_position = Vec3::new(0.0, 12.0, 20.0);
1061        let _camera = stage.new_camera().with_projection_and_view(
1062            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),
1063            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),
1064        );
1065        let _rez = stage
1066            .new_primitive()
1067            .with_vertices(stage.new_vertices(gpu_cube_vertices()))
1068            .with_transform(
1069                stage
1070                    .new_transform()
1071                    .with_scale(Vec3::new(6.0, 6.0, 6.0))
1072                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),
1073            );
1074
1075        let frame = ctx.get_next_frame().unwrap();
1076        stage.render(&frame.view());
1077        let img = frame.read_image().block().unwrap();
1078        assert_eq!(size, UVec2::new(img.width(), img.height()));
1079        img_diff::assert_img_eq("stage/resize_100.png", img);
1080        frame.present();
1081
1082        let new_size = UVec2::new(200, 200);
1083        ctx.set_size(new_size);
1084        stage.set_size(new_size);
1085
1086        let frame = ctx.get_next_frame().unwrap();
1087        stage.render(&frame.view());
1088        let img = frame.read_image().block().unwrap();
1089        assert_eq!(new_size, UVec2::new(img.width(), img.height()));
1090        img_diff::assert_img_eq("stage/resize_200.png", img);
1091        frame.present();
1092    }
1093
1094    #[test]
1095    fn can_direct_draw_cube() {
1096        let size = UVec2::new(100, 100);
1097        let ctx = Context::headless(size.x, size.y)
1098            .block()
1099            .with_use_direct_draw(true);
1100        let stage = ctx.new_stage();
1101
1102        // create the CMY cube
1103        let camera_position = Vec3::new(0.0, 12.0, 20.0);
1104        let _camera = stage.new_camera().with_projection_and_view(
1105            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),
1106            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),
1107        );
1108        let _rez = stage
1109            .new_primitive()
1110            .with_vertices(stage.new_vertices(gpu_cube_vertices()))
1111            .with_transform(
1112                stage
1113                    .new_transform()
1114                    .with_scale(Vec3::new(6.0, 6.0, 6.0))
1115                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),
1116            );
1117
1118        let frame = ctx.get_next_frame().unwrap();
1119        stage.render(&frame.view());
1120        let img = frame.read_image().block().unwrap();
1121        assert_eq!(size, UVec2::new(img.width(), img.height()));
1122        img_diff::assert_img_eq("stage/resize_100.png", img);
1123        frame.present();
1124    }
1125}