1#![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#[macro_export]
248macro_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 pub trait BlockOnFuture {
345 type Output;
346
347 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 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 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 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 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 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 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 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 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 cube_two.set_visible(false);
643
644 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 cube_two.set_visible(true);
653
654 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 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 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 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 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 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 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 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 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 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 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 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 material.set_albedo_texture(&entries[1]);
792
793 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 }
808
809 #[test]
810 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 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 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 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 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 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 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 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}