use std::collections::HashSet;
use crate::assets::{Assets, MaterialHandle, TextureDesc, TextureHandle};
use crate::diagnostics::{Diagnostic, DiagnosticCode};
use crate::material::{MaterialDesc, MaterialKind, TextureTransform};
use crate::scene::Scene;
#[cfg(test)]
#[derive(Debug, Clone)]
pub(in crate::render) struct PreparedBaseColorTexture {
pub(in crate::render) handle: TextureHandle,
pub(in crate::render) desc: TextureDesc,
pub(in crate::render) transform: Option<TextureTransform>,
}
#[derive(Debug, Clone)]
pub(in crate::render) struct PreparedMaterialTexture {
pub(in crate::render) handle: TextureHandle,
pub(in crate::render) desc: TextureDesc,
pub(in crate::render) transform: Option<TextureTransform>,
}
#[derive(Debug, Clone)]
pub(in crate::render) struct PreparedMaterialSlot {
pub(in crate::render) handle: MaterialHandle,
pub(in crate::render) material: MaterialDesc,
pub(in crate::render) base_color: Option<PreparedMaterialTexture>,
pub(in crate::render) normal: Option<PreparedMaterialTexture>,
pub(in crate::render) metallic_roughness: Option<PreparedMaterialTexture>,
pub(in crate::render) occlusion: Option<PreparedMaterialTexture>,
pub(in crate::render) emissive: Option<PreparedMaterialTexture>,
pub(in crate::render) clearcoat: Option<PreparedMaterialTexture>,
pub(in crate::render) clearcoat_roughness: Option<PreparedMaterialTexture>,
pub(in crate::render) clearcoat_normal: Option<PreparedMaterialTexture>,
pub(in crate::render) sheen_color: Option<PreparedMaterialTexture>,
pub(in crate::render) sheen_roughness: Option<PreparedMaterialTexture>,
pub(in crate::render) anisotropy: Option<PreparedMaterialTexture>,
pub(in crate::render) iridescence: Option<PreparedMaterialTexture>,
pub(in crate::render) iridescence_thickness: Option<PreparedMaterialTexture>,
pub(in crate::render) transmission: Option<PreparedMaterialTexture>,
pub(in crate::render) thickness: Option<PreparedMaterialTexture>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(in crate::render) struct PreparedLogicalResourceStats {
pub(in crate::render) materials: u64,
pub(in crate::render) textures: u64,
pub(in crate::render) material_bindings: u64,
pub(in crate::render) material_texture_bindings: u64,
pub(in crate::render) material_sampler_bindings: u64,
pub(in crate::render) material_textures_missing_decoded_pixels: u64,
pub(in crate::render) environments: u64,
pub(in crate::render) live_logical_handles: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct MaterialTextureStats {
bindings: usize,
missing_decoded_pixels: usize,
}
pub(in crate::render) fn collect_logical_resource_stats<F>(
scene: &Scene,
assets: Option<&Assets<F>>,
environment_count: u64,
) -> PreparedLogicalResourceStats {
let mut geometries = HashSet::new();
let mut materials = HashSet::new();
let mut textures = HashSet::new();
let mut material_texture_bindings = 0;
let mut material_textures_missing_decoded_pixels = 0;
for (_node, mesh, _transform) in scene.mesh_nodes() {
geometries.insert(mesh.geometry());
if materials.insert(mesh.material()) {
let counts = collect_material_textures(assets, mesh.material(), &mut textures);
material_texture_bindings += counts.bindings;
material_textures_missing_decoded_pixels += counts.missing_decoded_pixels;
}
}
for (_node, instance_set, _transform) in scene.instance_set_nodes() {
geometries.insert(instance_set.geometry());
if materials.insert(instance_set.material()) {
let counts = collect_material_textures(assets, instance_set.material(), &mut textures);
material_texture_bindings += counts.bindings;
material_textures_missing_decoded_pixels += counts.missing_decoded_pixels;
}
}
let materials = materials.len() as u64;
let textures = textures.len() as u64;
let material_texture_bindings = material_texture_bindings as u64;
let environments = environment_count;
let live_logical_handles = geometries.len() as u64 + materials + textures + environments;
PreparedLogicalResourceStats {
materials,
textures,
material_bindings: materials,
material_texture_bindings,
material_sampler_bindings: material_texture_bindings,
material_textures_missing_decoded_pixels: material_textures_missing_decoded_pixels as u64,
environments,
live_logical_handles,
}
}
pub(in crate::render) fn collect_material_texture_diagnostics<F>(
scene: &Scene,
assets: &Assets<F>,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let mut materials = HashSet::new();
for (_node, mesh, _transform) in scene.mesh_nodes() {
if materials.insert(mesh.material()) {
collect_material_texture_diagnostics_from_material(
assets,
mesh.material(),
&mut diagnostics,
);
}
}
for (_node, instance_set, _transform) in scene.instance_set_nodes() {
if materials.insert(instance_set.material()) {
collect_material_texture_diagnostics_from_material(
assets,
instance_set.material(),
&mut diagnostics,
);
}
}
diagnostics
}
#[cfg(test)]
fn collect_primary_base_color_texture<F>(
scene: &Scene,
assets: Option<&Assets<F>>,
) -> Option<PreparedBaseColorTexture> {
let assets = assets?;
let mut materials = HashSet::new();
let mut selected_texture: Option<TextureHandle> = None;
for (_node, mesh, _transform) in scene.mesh_nodes() {
if materials.insert(mesh.material()) {
selected_texture = collect_primary_base_color_texture_from_material(
assets,
mesh.material(),
selected_texture,
)
.ok()?;
}
}
for (_node, instance_set, _transform) in scene.instance_set_nodes() {
if materials.insert(instance_set.material()) {
selected_texture = collect_primary_base_color_texture_from_material(
assets,
instance_set.material(),
selected_texture,
)
.ok()?;
}
}
selected_texture.and_then(|handle| {
assets.texture(handle).map(|desc| PreparedBaseColorTexture {
handle,
desc,
transform: None,
})
})
}
#[cfg(test)]
pub(in crate::render) fn collect_backend_base_color_textures<F>(
scene: &Scene,
assets: Option<&Assets<F>>,
) -> Vec<PreparedBaseColorTexture> {
let Some(assets) = assets else {
return Vec::new();
};
let mut materials = HashSet::new();
let mut textures = HashSet::new();
let mut selected = Vec::new();
for (_node, mesh, _transform) in scene.mesh_nodes() {
if materials.insert(mesh.material()) {
collect_backend_base_color_texture_from_material(
assets,
mesh.material(),
&mut textures,
&mut selected,
);
}
}
for (_node, instance_set, _transform) in scene.instance_set_nodes() {
if materials.insert(instance_set.material()) {
collect_backend_base_color_texture_from_material(
assets,
instance_set.material(),
&mut textures,
&mut selected,
);
}
}
selected
}
pub(in crate::render) fn collect_backend_material_slots<F>(
scene: &Scene,
assets: Option<&Assets<F>>,
) -> Vec<PreparedMaterialSlot> {
let Some(assets) = assets else {
return Vec::new();
};
let mut materials = HashSet::new();
let mut selected = Vec::new();
for (_node, mesh, _transform) in scene.mesh_nodes() {
if materials.insert(mesh.material())
&& let Some(slot) = collect_backend_material_slot(assets, mesh.material())
{
selected.push(slot);
}
}
for (_node, instance_set, _transform) in scene.instance_set_nodes() {
if materials.insert(instance_set.material())
&& let Some(slot) = collect_backend_material_slot(assets, instance_set.material())
{
selected.push(slot);
}
}
selected
}
#[cfg(test)]
fn collect_primary_base_color_texture_from_material<F>(
assets: &Assets<F>,
material: crate::assets::MaterialHandle,
selected_texture: Option<TextureHandle>,
) -> Result<Option<TextureHandle>, ()> {
let material = assets.material(material).ok_or(())?;
let Some(texture) = material.base_color_texture() else {
return Ok(selected_texture);
};
if material.base_color_texture_transform().is_some() {
return Ok(selected_texture);
}
let texture_desc = assets.texture(texture).ok_or(())?;
if !texture_desc.has_decoded_pixels() {
return Ok(selected_texture);
}
match selected_texture {
Some(selected) if selected != texture => Err(()),
Some(selected) => Ok(Some(selected)),
None => Ok(Some(texture)),
}
}
#[cfg(test)]
fn collect_backend_base_color_texture_from_material<F>(
assets: &Assets<F>,
material: crate::assets::MaterialHandle,
textures: &mut HashSet<TextureHandle>,
selected: &mut Vec<PreparedBaseColorTexture>,
) {
let Some(material) = assets.material(material) else {
return;
};
let Some(texture) = material.base_color_texture() else {
return;
};
if !textures.insert(texture) {
return;
}
let Some(desc) = assets.texture(texture) else {
return;
};
if desc.has_decoded_pixels() {
selected.push(PreparedBaseColorTexture {
handle: texture,
desc,
transform: material.base_color_texture_transform(),
});
}
}
fn collect_backend_material_slot<F>(
assets: &Assets<F>,
handle: MaterialHandle,
) -> Option<PreparedMaterialSlot> {
let material = assets.material(handle)?;
let base_color = collect_backend_material_texture(
assets,
material.base_color_texture(),
material.base_color_texture_transform(),
);
let normal = collect_backend_material_texture(
assets,
material.normal_texture(),
material.normal_texture_transform(),
);
let metallic_roughness = collect_backend_material_texture(
assets,
material.metallic_roughness_texture(),
material.metallic_roughness_texture_transform(),
);
let occlusion = collect_backend_material_texture(
assets,
material.occlusion_texture(),
material.occlusion_texture_transform(),
);
let emissive = collect_backend_material_texture(
assets,
material.emissive_texture(),
material.emissive_texture_transform(),
);
let clearcoat = collect_backend_material_texture(
assets,
material.clearcoat_texture(),
material.clearcoat_texture_transform(),
);
let clearcoat_roughness = collect_backend_material_texture(
assets,
material.clearcoat_roughness_texture(),
material.clearcoat_roughness_texture_transform(),
);
let clearcoat_normal = collect_backend_material_texture(
assets,
material.clearcoat_normal_texture(),
material.clearcoat_normal_texture_transform(),
);
let sheen_color = collect_backend_material_texture(
assets,
material.sheen_color_texture(),
material.sheen_color_texture_transform(),
);
let sheen_roughness = collect_backend_material_texture(
assets,
material.sheen_roughness_texture(),
material.sheen_roughness_texture_transform(),
);
let anisotropy = collect_backend_material_texture(
assets,
material.anisotropy_texture(),
material.anisotropy_texture_transform(),
);
let iridescence = collect_backend_material_texture(
assets,
material.iridescence_texture(),
material.iridescence_texture_transform(),
);
let iridescence_thickness = collect_backend_material_texture(
assets,
material.iridescence_thickness_texture(),
material.iridescence_thickness_texture_transform(),
);
let transmission = collect_backend_material_texture(
assets,
material.transmission_texture(),
material.transmission_texture_transform(),
);
let thickness = collect_backend_material_texture(
assets,
material.thickness_texture(),
material.thickness_texture_transform(),
);
if matches!(material.kind(), MaterialKind::Unlit)
&& base_color.is_none()
&& normal.is_none()
&& metallic_roughness.is_none()
&& occlusion.is_none()
&& emissive.is_none()
&& clearcoat.is_none()
&& clearcoat_roughness.is_none()
&& clearcoat_normal.is_none()
&& sheen_color.is_none()
&& sheen_roughness.is_none()
&& anisotropy.is_none()
&& iridescence.is_none()
&& iridescence_thickness.is_none()
&& transmission.is_none()
&& thickness.is_none()
{
return None;
}
Some(PreparedMaterialSlot {
handle,
base_color,
normal,
metallic_roughness,
occlusion,
emissive,
clearcoat,
clearcoat_roughness,
clearcoat_normal,
sheen_color,
sheen_roughness,
anisotropy,
iridescence,
iridescence_thickness,
transmission,
thickness,
material,
})
}
fn collect_backend_material_texture<F>(
assets: &Assets<F>,
handle: Option<TextureHandle>,
transform: Option<TextureTransform>,
) -> Option<PreparedMaterialTexture> {
let handle = handle?;
let desc = assets.texture(handle)?;
desc.has_decoded_pixels()
.then_some(PreparedMaterialTexture {
handle,
desc,
transform,
})
}
fn collect_material_textures<F>(
assets: Option<&Assets<F>>,
material: crate::assets::MaterialHandle,
textures: &mut HashSet<crate::assets::TextureHandle>,
) -> MaterialTextureStats {
let Some(assets) = assets else {
return MaterialTextureStats::default();
};
let Some(material) = assets.material(material) else {
return MaterialTextureStats::default();
};
let mut stats = MaterialTextureStats::default();
for texture in [
material.base_color_texture(),
material.normal_texture(),
material.metallic_roughness_texture(),
material.occlusion_texture(),
material.emissive_texture(),
material.clearcoat_texture(),
material.clearcoat_roughness_texture(),
material.clearcoat_normal_texture(),
material.sheen_color_texture(),
material.sheen_roughness_texture(),
material.anisotropy_texture(),
material.iridescence_texture(),
material.iridescence_thickness_texture(),
material.transmission_texture(),
material.thickness_texture(),
]
.into_iter()
.flatten()
{
if let Some(desc) = assets.texture(texture) {
textures.insert(texture);
stats.bindings += 1;
if !desc.has_decoded_pixels() {
stats.missing_decoded_pixels += 1;
}
}
}
stats
}
fn collect_material_texture_diagnostics_from_material<F>(
assets: &Assets<F>,
handle: MaterialHandle,
diagnostics: &mut Vec<Diagnostic>,
) {
let Some(material) = assets.material(handle) else {
return;
};
for (slot, texture) in material_texture_slots(&material) {
let Some(texture) = texture else {
continue;
};
let Some(desc) = assets.texture(texture) else {
continue;
};
if desc.has_decoded_pixels() {
continue;
}
diagnostics.push(Diagnostic::warning(
DiagnosticCode::MaterialTextureMissingDecodedPixels,
format!(
"material texture slot {slot} references '{}' but has no decoded pixels",
desc.path().as_str()
),
"check external glTF image URI rewriting/CSP, inspect \
AssetLoadWarning::ExternalImageMissing from the asset load report, or use \
AssetLoadOptions::with_strict_textures(true)",
));
}
}
fn material_texture_slots(material: &MaterialDesc) -> [(&'static str, Option<TextureHandle>); 15] {
[
("base_color", material.base_color_texture()),
("normal", material.normal_texture()),
("metallic_roughness", material.metallic_roughness_texture()),
("occlusion", material.occlusion_texture()),
("emissive", material.emissive_texture()),
("clearcoat", material.clearcoat_texture()),
(
"clearcoat_roughness",
material.clearcoat_roughness_texture(),
),
("clearcoat_normal", material.clearcoat_normal_texture()),
("sheen_color", material.sheen_color_texture()),
("sheen_roughness", material.sheen_roughness_texture()),
("anisotropy", material.anisotropy_texture()),
("iridescence", material.iridescence_texture()),
(
"iridescence_thickness",
material.iridescence_thickness_texture(),
),
("transmission", material.transmission_texture()),
("thickness", material.thickness_texture()),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assets::{AssetPath, Assets, TextureSourceFormat};
use crate::geometry::GeometryDesc;
use crate::material::{Color, MaterialDesc, TextureColorSpace, TextureTransform};
use crate::scene::Scene;
#[test]
fn primary_base_color_texture_selection_defers_texture_transforms_to_cpu_bake() {
let assets = Assets::new();
let texture = decoded_test_texture(&assets);
let geometry = assets.create_geometry(GeometryDesc::box_xyz(0.25, 0.25, 0.25));
let material = assets.create_material(
MaterialDesc::unlit(Color::WHITE)
.with_base_color_texture(texture)
.with_base_color_texture_transform(TextureTransform::new(
[0.25, 0.0],
0.0,
[1.0, 1.0],
None,
)),
);
let mut scene = Scene::new();
scene.mesh(geometry, material).add().expect("mesh inserts");
assert!(
collect_primary_base_color_texture(&scene, Some(&assets)).is_none(),
"backend upload must not bypass CPU baking for transformed texture coordinates"
);
}
#[test]
fn primary_base_color_texture_selection_keeps_simple_decoded_texture() {
let assets = Assets::new();
let texture = decoded_test_texture(&assets);
let geometry = assets.create_geometry(GeometryDesc::box_xyz(0.25, 0.25, 0.25));
let material = assets
.create_material(MaterialDesc::unlit(Color::WHITE).with_base_color_texture(texture));
let mut scene = Scene::new();
scene.mesh(geometry, material).add().expect("mesh inserts");
let selected = collect_primary_base_color_texture(&scene, Some(&assets))
.expect("simple decoded texture is backend eligible");
assert_eq!(selected.handle, texture);
assert_eq!(selected.desc.decoded_dimensions(), Some((1, 1)));
}
#[test]
fn backend_base_color_texture_selection_keeps_multiple_decoded_textures() {
let assets = Assets::new();
let first_texture = decoded_test_texture(&assets);
let second_texture = decoded_test_texture(&assets);
let geometry = assets.create_geometry(GeometryDesc::box_xyz(0.25, 0.25, 0.25));
let first_material = assets.create_material(
MaterialDesc::unlit(Color::WHITE).with_base_color_texture(first_texture),
);
let second_material = assets.create_material(
MaterialDesc::unlit(Color::WHITE).with_base_color_texture(second_texture),
);
let mut scene = Scene::new();
scene
.mesh(geometry, first_material)
.add()
.expect("first mesh inserts");
scene
.mesh(geometry, second_material)
.add()
.expect("second mesh inserts");
let textures = collect_backend_base_color_textures(&scene, Some(&assets));
assert_eq!(
textures
.iter()
.map(|texture| texture.handle)
.collect::<Vec<_>>(),
vec![first_texture, second_texture],
"backend material slots must preserve all eligible decoded base-color textures in \
discovery order instead of silently falling back to CPU baking after the first one"
);
}
#[test]
fn backend_base_color_texture_selection_preserves_texture_transform_uniforms() {
let assets = Assets::new();
let texture = decoded_test_texture(&assets);
let geometry = assets.create_geometry(GeometryDesc::box_xyz(0.25, 0.25, 0.25));
let transform = TextureTransform::new([0.25, 0.5], 0.5, [0.75, 0.5], None);
let material = assets.create_material(
MaterialDesc::unlit(Color::WHITE)
.with_base_color_texture(texture)
.with_base_color_texture_transform(transform),
);
let mut scene = Scene::new();
scene.mesh(geometry, material).add().expect("mesh inserts");
let textures = collect_backend_base_color_textures(&scene, Some(&assets));
assert_eq!(textures.len(), 1);
assert_eq!(textures[0].handle, texture);
assert_eq!(
textures[0].transform,
Some(transform),
"backend material slots must preserve KHR_texture_transform metadata so GPU/WebGL2 \
sampling does not silently use untransformed UVs"
);
}
#[test]
fn backend_material_slots_skip_unlit_materials_without_decoded_texture_pixels() {
let assets = Assets::new();
let missing_texture = assets
.create_texture_for_test(
AssetPath::from("textures/missing-albedo.png"),
TextureColorSpace::Srgb,
TextureSourceFormat::Png,
None,
)
.expect("missing external texture descriptor inserts without decoded pixels");
let geometry = assets.create_geometry(GeometryDesc::box_xyz(0.25, 0.25, 0.25));
let material = assets.create_material(
MaterialDesc::unlit(Color::from_linear_rgba(0.9, 0.4, 0.1, 1.0))
.with_base_color_texture(missing_texture),
);
let mut scene = Scene::new();
scene.mesh(geometry, material).add().expect("mesh inserts");
let slots = collect_backend_material_slots(&scene, Some(&assets));
assert!(
slots.is_empty(),
"unlit materials that have no decoded texture pixels should stay on the CPU-baked \
path; sending them through a backend material slot adds browser WebGPU risk without \
adding any material sampling capability"
);
}
#[test]
fn backend_material_slots_preserve_all_texture_roles_and_material_only_slots() {
let assets = Assets::new();
let base_color = decoded_test_texture(&assets);
let normal = decoded_test_texture(&assets);
let metallic_roughness = decoded_test_texture(&assets);
let occlusion = decoded_test_texture(&assets);
let emissive = decoded_test_texture(&assets);
let clearcoat = decoded_test_texture(&assets);
let clearcoat_roughness = decoded_test_texture(&assets);
let clearcoat_normal = decoded_test_texture(&assets);
let sheen_color = decoded_test_texture(&assets);
let sheen_roughness = decoded_test_texture(&assets);
let anisotropy = decoded_test_texture(&assets);
let iridescence = decoded_test_texture(&assets);
let iridescence_thickness = decoded_test_texture(&assets);
let transmission = decoded_test_texture(&assets);
let thickness = decoded_test_texture(&assets);
let geometry = assets.create_geometry(GeometryDesc::box_xyz(0.25, 0.25, 0.25));
let material_with_textures = assets.create_material(
MaterialDesc::pbr_metallic_roughness(Color::WHITE, 0.25, 0.75)
.with_base_color_texture(base_color)
.with_normal_texture(normal)
.with_metallic_roughness_texture(metallic_roughness)
.with_occlusion_texture(occlusion)
.with_emissive_texture(emissive)
.with_clearcoat_factor(1.0)
.with_clearcoat_texture(clearcoat)
.with_clearcoat_roughness_factor(1.0)
.with_clearcoat_roughness_texture(clearcoat_roughness)
.with_clearcoat_normal_texture(clearcoat_normal)
.with_sheen_color_factor(Color::WHITE)
.with_sheen_color_texture(sheen_color)
.with_sheen_roughness_factor(1.0)
.with_sheen_roughness_texture(sheen_roughness)
.with_anisotropy_strength_factor(1.0)
.with_anisotropy_texture(anisotropy)
.with_iridescence_factor(1.0)
.with_iridescence_texture(iridescence)
.with_iridescence_thickness_texture(iridescence_thickness)
.with_transmission_factor(1.0)
.with_transmission_texture(transmission)
.with_thickness_factor(1.0)
.with_thickness_texture(thickness),
);
let material_without_textures =
assets.create_material(MaterialDesc::pbr_metallic_roughness(
Color::from_linear_rgba(0.2, 0.4, 0.6, 1.0),
0.5,
0.25,
));
let mut scene = Scene::new();
scene
.mesh(geometry, material_with_textures)
.add()
.expect("textured mesh inserts");
scene
.mesh(geometry, material_without_textures)
.add()
.expect("factor-only mesh inserts");
let slots = collect_backend_material_slots(&scene, Some(&assets));
assert_eq!(slots.len(), 2);
assert_eq!(slots[0].handle, material_with_textures);
assert_eq!(
slots[0].base_color.as_ref().map(|slot| slot.handle),
Some(base_color)
);
assert_eq!(
slots[0].normal.as_ref().map(|slot| slot.handle),
Some(normal)
);
assert_eq!(
slots[0].metallic_roughness.as_ref().map(|slot| slot.handle),
Some(metallic_roughness)
);
assert_eq!(
slots[0].occlusion.as_ref().map(|slot| slot.handle),
Some(occlusion)
);
assert_eq!(
slots[0].emissive.as_ref().map(|slot| slot.handle),
Some(emissive)
);
assert_eq!(
slots[0].clearcoat.as_ref().map(|slot| slot.handle),
Some(clearcoat)
);
assert_eq!(
slots[0]
.clearcoat_roughness
.as_ref()
.map(|slot| slot.handle),
Some(clearcoat_roughness)
);
assert_eq!(
slots[0].clearcoat_normal.as_ref().map(|slot| slot.handle),
Some(clearcoat_normal)
);
assert_eq!(
slots[0].sheen_color.as_ref().map(|slot| slot.handle),
Some(sheen_color)
);
assert_eq!(
slots[0].sheen_roughness.as_ref().map(|slot| slot.handle),
Some(sheen_roughness)
);
assert_eq!(
slots[0].anisotropy.as_ref().map(|slot| slot.handle),
Some(anisotropy)
);
assert_eq!(
slots[0].iridescence.as_ref().map(|slot| slot.handle),
Some(iridescence)
);
assert_eq!(
slots[0]
.iridescence_thickness
.as_ref()
.map(|slot| slot.handle),
Some(iridescence_thickness)
);
assert_eq!(
slots[0].transmission.as_ref().map(|slot| slot.handle),
Some(transmission)
);
assert_eq!(
slots[0].thickness.as_ref().map(|slot| slot.handle),
Some(thickness)
);
assert_eq!(
slots[1].handle, material_without_textures,
"backend material slots must include factor-only materials so per-draw uniforms do \
not collapse to the fallback material slot"
);
assert!(slots[1].base_color.is_none());
}
fn decoded_test_texture(assets: &Assets) -> crate::assets::TextureHandle {
assets
.create_texture_for_test(
AssetPath::from(
"data:image/png;base64,\
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==",
),
TextureColorSpace::Srgb,
TextureSourceFormat::Png,
None,
)
.expect("inline texture decodes")
}
}