renderling/
lib.rs

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