diff --git a/examples/src/scene_viewer/bistro.png b/examples/src/scene_viewer/bistro.png index b8d952fe..2ef50b6b 100644 Binary files a/examples/src/scene_viewer/bistro.png and b/examples/src/scene_viewer/bistro.png differ diff --git a/rend3-routine/src/forward.rs b/rend3-routine/src/forward.rs index 3b5f4d6d..966aca88 100644 --- a/rend3-routine/src/forward.rs +++ b/rend3-routine/src/forward.rs @@ -2,13 +2,15 @@ //! //! Will default to the PBR shader code if custom code is not specified. -use std::{marker::PhantomData, sync::Arc}; +use std::{cmp::Ordering, marker::PhantomData, sync::Arc}; use arrayvec::ArrayVec; use encase::{ShaderSize, StorageBuffer}; +use ordered_float::OrderedFloat; use rend3::{ graph::{DataHandle, NodeResourceUsage, RenderGraph, RenderPassTargets}, - types::{Material, SampleCount}, + managers::{CameraState, InternalObject, MaterialArchetypeView, TextureBindGroupIndex}, + types::{Material, RawObjectHandle, SampleCount, SortingOrder, SortingReason}, util::bind_merge::BindGroupBuilder, ProfileData, Renderer, RendererDataCore, RendererProfile, ShaderPreProcessor, }; @@ -165,6 +167,8 @@ impl ForwardRoutine { CameraSpecifier::Shadow(idx) => &ctx.eval_output.shadows[idx as usize].camera, }; + let objects = sort(objects, archetype_view, self.material_key, camera); + let per_camera_uniform_values = PerCameraUniform { view: camera.view(), view_proj: camera.view_proj(), @@ -230,6 +234,104 @@ impl ForwardRoutine { } } +fn sort<'a, M, I>( + objects: I, + material_archetype: MaterialArchetypeView<'_, M>, + requested_material_key: u64, + camera: &CameraState, +) -> Vec<(RawObjectHandle, &'a InternalObject)> +where + M: Material, + I: IntoIterator)>, + I::IntoIter: ExactSizeIterator, +{ + let objects = objects.into_iter(); + + let mut sorted_objects = Vec::with_capacity(objects.len()); + { + profiling::scope!("Sort Key Creation"); + for (raw_handle, object) in objects { + let material = material_archetype.material(*object.material_handle); + let object_material_key = material.inner.key(); + let sorting = material.inner.sorting(); + + if object_material_key != requested_material_key { + continue; + } + + // Frustum culling + if !camera.world_frustum().contains_sphere(object.inner.bounding_sphere) { + continue; + } + + let bind_group_index = material.bind_group_index.map_gpu(|_| TextureBindGroupIndex::DUMMY).into_common(); + + let mut distance_sq = camera.location().distance_squared(object.location.into()); + + if sorting.order == SortingOrder::BackToFront { + distance_sq = -distance_sq; + } + sorted_objects.push(( + ObjectSortingKey { + bind_group_index, + distance: OrderedFloat(distance_sq), + sorting_reason: sorting.reason, + }, + (raw_handle, object), + )) + } + } + + { + profiling::scope!("Sorting"); + sorted_objects.sort_unstable_by_key(|(k, _)| *k); + } + + sorted_objects.into_iter().map(|(_, o)| o).collect() +} + +#[derive(Debug, Clone, Copy, Eq)] +pub(super) struct ObjectSortingKey { + pub bind_group_index: TextureBindGroupIndex, + pub distance: OrderedFloat, + pub sorting_reason: SortingReason, +} + +impl PartialEq for ObjectSortingKey { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl PartialOrd for ObjectSortingKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ObjectSortingKey { + fn cmp(&self, other: &Self) -> Ordering { + match self.sorting_reason.cmp(&other.sorting_reason) { + Ordering::Equal => {} + ord => return ord, + } + // The above comparison means that both sides are equal + if self.sorting_reason == SortingReason::Requirement { + match self.distance.cmp(&other.distance) { + Ordering::Equal => {} + ord => return ord, + } + self.bind_group_index.cmp(&other.bind_group_index) + } else { + match self.bind_group_index.cmp(&other.bind_group_index) { + Ordering::Equal => {} + ord => return ord, + } + self.distance.cmp(&other.distance) + } + } +} + fn build_forward_pipeline_inner( pll: &wgpu::PipelineLayout, args: &ForwardRoutineCreateArgs<'_, M>, diff --git a/rend3-test/src/helpers.rs b/rend3-test/src/helpers.rs index e7a9dbe7..3b1c5109 100644 --- a/rend3-test/src/helpers.rs +++ b/rend3-test/src/helpers.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use glam::{Mat4, Vec3, Vec4}; use rend3::types::{DirectionalLightHandle, MaterialHandle, MeshBuilder, ObjectHandle}; -use rend3_routine::pbr::PbrMaterial; +use rend3_routine::pbr::{PbrMaterial, Transparency}; use wgpu::Device; use crate::TestRunner; @@ -43,6 +43,15 @@ impl TestRunner { }) } + pub fn add_transparent_material(&self, color: Vec4) -> MaterialHandle { + self.renderer.add_material(PbrMaterial { + albedo: rend3_routine::pbr::AlbedoComponent::Value(color), + unlit: true, + transparency: Transparency::Blend, + ..Default::default() + }) + } + pub fn add_lit_material(&self, color: Vec4) -> MaterialHandle { self.renderer.add_material(PbrMaterial { albedo: rend3_routine::pbr::AlbedoComponent::Value(color), diff --git a/rend3-test/tests/results/transparency/blending.png b/rend3-test/tests/results/transparency/blending.png new file mode 100644 index 00000000..da60c9ce Binary files /dev/null and b/rend3-test/tests/results/transparency/blending.png differ diff --git a/rend3-test/tests/root.rs b/rend3-test/tests/root.rs index ab1b6f0f..580bfd72 100644 --- a/rend3-test/tests/root.rs +++ b/rend3-test/tests/root.rs @@ -2,3 +2,4 @@ mod msaa; mod object; mod shadow; mod simple; +mod transparency; diff --git a/rend3-test/tests/transparency.rs b/rend3-test/tests/transparency.rs new file mode 100644 index 00000000..3fd0fafc --- /dev/null +++ b/rend3-test/tests/transparency.rs @@ -0,0 +1,50 @@ +use anyhow::Context; +use glam::{Mat4, Quat, Vec3, Vec4}; +use rend3::types::{Camera, Handedness}; +use rend3_test::{no_gpu_return, test_attr, FrameRenderSettings, TestRunner, Threshold}; + +/// Ensure that transparency is ordered correctly +// +// Todo: This test never fails, even if I remove the sort. +#[test_attr] +pub async fn transparency() -> anyhow::Result<()> { + let iad = no_gpu_return!(rend3::create_iad(None, None, None, None).await) + .context("InstanceAdapterDevice creation failed")?; + + let Ok(runner) = TestRunner::builder().iad(iad.clone()).handedness(Handedness::Left).build().await else { + return Ok(()); + }; + + runner.set_camera_data(Camera { + projection: rend3::types::CameraProjection::Raw(Mat4::IDENTITY), + view: Mat4::IDENTITY, + }); + + let material1 = runner.add_transparent_material(Vec4::new(1.0, 0.0, 0.0, 0.5)); + let material2 = runner.add_transparent_material(Vec4::new(0.0, 1.0, 0.0, 0.5)); + let _object_left_1 = runner.plane( + material1.clone(), + Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(-0.5, 0.0, -0.5)), + ); + + let _object_left_2 = runner.plane( + material2.clone(), + Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(-0.5, 0.0, 0.5)), + ); + + let _object_right_2 = runner.plane( + material2, + Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(0.5, 0.0, 0.5)), + ); + + let _object_right_1 = runner.plane( + material1, + Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(0.5, 0.0, -0.5)), + ); + + runner + .render_and_compare(FrameRenderSettings::new(), "tests/results/transparency/blending.png", Threshold::Mean(0.0)) + .await?; + + Ok(()) +} diff --git a/rend3/src/managers/material.rs b/rend3/src/managers/material.rs index a5260c81..343f6044 100644 --- a/rend3/src/managers/material.rs +++ b/rend3/src/managers/material.rs @@ -65,6 +65,14 @@ pub struct MaterialArchetypeView<'a, M: Material> { data_vec: &'a [Option>], } +impl<'a, M: Material> Copy for MaterialArchetypeView<'a, M> {} + +impl<'a, M: Material> Clone for MaterialArchetypeView<'a, M> { + fn clone(&self) -> Self { + *self + } +} + impl<'a, M: Material> MaterialArchetypeView<'a, M> { pub fn buffer(&self) -> &'a Buffer { self.buffer