pub mod animation;
pub mod assets;
pub mod camera;
pub mod dynamic;
pub mod light;
pub mod material;
pub mod particles;
pub mod texture;
pub use assets::SceneAssets;
pub use particles::{drive_emitter, EmitterHandle};
use std::collections::HashMap;
use animation::AnimResolveMaps;
use anyhow::{anyhow, Result};
use awsm_renderer::animation::AnimationClipKey;
use awsm_renderer::cameras::CameraKey;
use awsm_renderer::decals::DecalKey;
use awsm_renderer::lights::LightKey;
use awsm_renderer::materials::unlit::UnlitMaterial;
use awsm_renderer::materials::{Material, MaterialAlphaMode, MaterialKey};
use awsm_renderer::meshes::MeshKey;
use awsm_renderer::pipeline_scheduler::CompileProgress;
use awsm_renderer::raw_mesh::RawMeshData;
use awsm_renderer::render_passes::lines::LineKey;
use awsm_renderer::transforms::{Transform, TransformKey};
use awsm_renderer::{AwsmRenderer, LoadPhase};
use awsm_renderer_core::texture::mipmap::MipmapTextureKind;
use awsm_renderer_gltf::loader::GltfLoader;
use awsm_renderer_gltf::{AwsmRendererGltfExt, GltfMaterialSource, PopulateGltfOpts};
use awsm_scene::{
mesh_glb_filename, AssetId, AssetSource, CameraConfig, CurveDef, DecalConfig, EditorNode,
InstancesAlongCurveDef, LightConfig, LineDef, MaterialInstance, MaterialShading, NodeId,
NodeKind, ParticleEmitterDef, RuntimeMesh, Scene, SpriteDef, Trs, ASSETS_DIR,
};
use glam::{Mat4, Quat, Vec3, Vec4};
#[derive(Clone, Debug, Default)]
pub struct NodeHandles {
pub transform: TransformKey,
pub meshes: Vec<MeshKey>,
pub light: Option<LightKey>,
pub camera: Option<CameraKey>,
pub camera_config: Option<CameraConfig>,
pub line: Option<LineKey>,
pub decal: Option<DecalKey>,
pub emitter: Option<EmitterHandle>,
}
#[derive(Default, Debug)]
pub struct LoadedScene {
pub nodes: HashMap<NodeId, NodeHandles>,
pub prefabs: HashMap<NodeId, PrefabTemplate>,
pub meshes: Vec<MeshKey>,
pub lights: Vec<LightKey>,
pub clips: Vec<AnimationClipKey>,
pub lines: Vec<LineKey>,
pub decals: Vec<DecalKey>,
pub transforms: Vec<TransformKey>,
}
#[derive(Debug)]
pub struct PrefabTemplate {
root: NodeId,
nodes: Vec<PrefabNode>,
}
#[derive(Debug)]
struct PrefabNode {
id: NodeId,
local: Trs,
parent: Option<NodeId>,
template_meshes: Vec<MeshKey>,
replay: PrefabReplay,
}
#[derive(Debug, Clone)]
enum PrefabReplay {
None,
ParticleEmitter(ParticleEmitterDef),
Light(LightConfig),
Camera(CameraConfig),
Line(LineDef),
Decal { texture_index: u32, alpha: f32 },
InstancesAlongCurve {
transforms: Vec<Transform>,
source_node: NodeId,
per_instance_colors: Vec<[f32; 4]>,
},
}
fn replay_prefab_node(
renderer: &mut AwsmRenderer,
replay: &PrefabReplay,
tk: TransformKey,
world: Mat4,
handles: &mut NodeHandles,
) {
match replay {
PrefabReplay::None => {}
PrefabReplay::InstancesAlongCurve { .. } => {}
PrefabReplay::ParticleEmitter(def) => {
match particles::build_emitter(renderer, def, tk, world) {
Ok(handle) => handles.emitter = Some(handle),
Err(err) => {
tracing::warn!("prefab replay: ParticleEmitter build failed: {err}")
}
}
}
PrefabReplay::Light(cfg) => {
let pos = world.w_axis.truncate();
let dir = world.transform_vector3(Vec3::NEG_Z).normalize_or_zero();
let lt = light::light_from_config(cfg, pos, dir);
let shadow = light::light_shadow_params_from_config(cfg.shadow());
if let Ok(k) = renderer.insert_light(lt, Some(shadow)) {
renderer.lights.bind_transform(k, tk);
handles.light = Some(k);
}
}
PrefabReplay::Camera(cfg) => {
let ck = renderer
.cameras
.insert(camera::camera_params_from_config(cfg));
handles.camera = Some(ck);
handles.camera_config = Some(cfg.clone());
}
PrefabReplay::Line(def) => {
if def.points.len() < 2 {
return;
}
let positions: Vec<Vec3> = def
.points
.iter()
.map(|p| world.transform_point3(Vec3::from_array(p.pos)))
.collect();
let colors: Vec<Vec4> = def
.points
.iter()
.map(|p| Vec4::from_array(p.color))
.collect();
match renderer.add_line_strip(&positions, &colors, def.width_px, def.depth_test_always)
{
Ok(Some(k)) => handles.line = Some(k),
Ok(None) => {}
Err(err) => tracing::warn!("prefab instantiate: add_line_strip failed: {err}"),
}
}
PrefabReplay::Decal {
texture_index,
alpha,
} => {
use awsm_renderer::decals::AwsmDecalError;
match renderer.insert_decal(world, *texture_index, *alpha) {
Ok(k) => handles.decal = Some(k),
Err(AwsmDecalError::FeatureNotEnabled) => {}
Err(err) => tracing::warn!("prefab instantiate: insert_decal failed: {err:?}"),
}
}
}
}
impl PrefabTemplate {
pub fn root_id(&self) -> NodeId {
self.root
}
pub fn node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
self.nodes.iter().map(|n| n.id)
}
pub fn instantiate(
&self,
renderer: &mut AwsmRenderer,
world_trs: Trs,
) -> Result<PrefabInstance> {
let mut tk_for: HashMap<NodeId, TransformKey> = HashMap::with_capacity(self.nodes.len());
let mut world_for: HashMap<NodeId, Mat4> = HashMap::with_capacity(self.nodes.len());
let mut nodes: HashMap<NodeId, NodeHandles> = HashMap::with_capacity(self.nodes.len());
let mut root_tk: Option<TransformKey> = None;
for pn in &self.nodes {
let (local, parent_tk) = match pn.parent {
None => (trs_to_transform(&world_trs), None),
Some(parent_id) => (trs_to_transform(&pn.local), tk_for.get(&parent_id).copied()),
};
let world = match pn.parent {
None => local.to_matrix(),
Some(parent_id) => {
world_for.get(&parent_id).copied().unwrap_or(Mat4::IDENTITY) * local.to_matrix()
}
};
let tk = renderer.transforms.insert(local, parent_tk);
tk_for.insert(pn.id, tk);
world_for.insert(pn.id, world);
if pn.parent.is_none() {
root_tk = Some(tk);
}
let mut mesh_keys = Vec::with_capacity(pn.template_meshes.len());
for &template_key in &pn.template_meshes {
let new_key = renderer.duplicate_mesh_with_transform(template_key, tk)?;
renderer.set_mesh_hidden(new_key, false)?;
mesh_keys.push(new_key);
}
let mut handles = NodeHandles {
transform: tk,
meshes: mesh_keys,
..Default::default()
};
replay_prefab_node(renderer, &pn.replay, tk, world, &mut handles);
nodes.insert(pn.id, handles);
}
for pn in &self.nodes {
let PrefabReplay::InstancesAlongCurve {
transforms,
source_node,
per_instance_colors,
} = &pn.replay
else {
continue;
};
if transforms.is_empty() {
continue;
}
let Some(&source_mesh) = nodes.get(source_node).and_then(|h| h.meshes.first()) else {
tracing::warn!(
"prefab: InstancesAlongCurve source {:?} has no duplicated mesh in this \
instance — instancing skipped",
source_node
);
continue;
};
let transform_key = match renderer.meshes.get(source_mesh) {
Ok(m) => m.transform_key,
Err(_) => continue,
};
if let Err(err) = renderer.enable_mesh_instancing_opaque(source_mesh, transforms) {
tracing::warn!("prefab: enable_mesh_instancing_opaque failed: {err}");
continue;
}
if !per_instance_colors.is_empty() {
let attrs: Vec<awsm_renderer::instances::InstanceAttr> =
expand_instance_colors(per_instance_colors, transforms.len())
.into_iter()
.map(|c| {
awsm_renderer::instances::InstanceAttr::from_rgba_alpha_size(
c, 1.0, 1.0,
)
})
.collect();
if let Err(err) = renderer.set_mesh_instance_attrs(transform_key, &attrs) {
tracing::warn!("prefab: curve per-instance colours failed: {err}");
}
}
}
let root = root_tk.ok_or_else(|| anyhow!("prefab template has no root node"))?;
Ok(PrefabInstance { root, nodes })
}
}
#[derive(Clone, Debug)]
pub struct PrefabInstance {
pub root: TransformKey,
pub nodes: HashMap<NodeId, NodeHandles>,
}
impl LoadedScene {
pub fn teardown(self, renderer: &mut AwsmRenderer) {
for mesh in self.meshes {
renderer.remove_mesh(mesh);
}
for light in self.lights {
renderer.remove_light(light);
}
for clip in self.clips {
renderer.animations.remove_clip(clip);
}
for line in self.lines {
renderer.remove_line(line);
}
for decal in self.decals {
renderer.remove_decal(decal);
}
for tk in self.transforms {
renderer.transforms.remove(tk);
}
}
}
impl PrefabInstance {
pub fn teardown(self, renderer: &mut AwsmRenderer) {
for handles in self.nodes.values() {
for &mesh in &handles.meshes {
renderer.remove_mesh(mesh);
}
if let Some(light) = handles.light {
renderer.remove_light(light);
}
if let Some(line) = handles.line {
renderer.remove_line(line);
}
if let Some(decal) = handles.decal {
renderer.remove_decal(decal);
}
if let Some(emitter) = &handles.emitter {
renderer.remove_mesh(emitter.mesh);
renderer.transforms.remove(emitter.instance_transform);
}
}
for handles in self.nodes.values() {
renderer.transforms.remove(handles.transform);
}
}
}
pub fn set_node_visible(renderer: &mut AwsmRenderer, handles: &NodeHandles, visible: bool) {
for &k in &handles.meshes {
let _ = renderer.set_mesh_hidden(k, !visible);
}
}
pub async fn populate_awsm_scene(
renderer: &mut AwsmRenderer,
scene: &Scene,
assets: &HashMap<String, Vec<u8>>,
on_phase: impl FnMut(LoadPhase),
) -> Result<LoadedScene> {
load_scene_for_player(renderer, scene, assets, on_phase).await
}
pub async fn load_scene_for_player(
renderer: &mut AwsmRenderer,
scene: &Scene,
assets: &impl SceneAssets,
mut on_phase: impl FnMut(LoadPhase),
) -> Result<LoadedScene> {
let custom = dynamic::register_custom_materials(renderer, scene, assets).await;
let placeholder = insert_placeholder_material(renderer);
let mut maps = AnimResolveMaps::default();
let renderables = collect_renderables(&scene.nodes);
let total = renderables.len();
for (i, (id, material)) in renderables.iter().enumerate() {
on_phase(LoadPhase::BuildingMaterials { done: i, total });
let key = resolve_material(renderer, material.as_ref(), placeholder, assets, &custom).await;
maps.node_materials.insert(*id, key);
if let Some(inst) = material.as_ref() {
if custom.contains_key(&inst.asset) {
maps.custom_materials.entry(inst.asset).or_insert(key);
}
}
}
on_phase(LoadPhase::BuildingMaterials { done: total, total });
maps.custom_shaders = custom;
let mut loaded = LoadedScene::default();
let mut uploaded = 0usize;
for node in &scene.nodes {
materialize(
renderer,
scene,
node,
None,
glam::Mat4::IDENTITY,
true,
assets,
&mut maps,
placeholder,
&mut on_phase,
&mut uploaded,
total,
&mut loaded,
)
.await?;
}
for (&node_id, &tk) in &maps.transforms {
loaded.transforms.push(tk);
loaded.nodes.insert(
node_id,
NodeHandles {
transform: tk,
meshes: maps.node_meshes.get(&node_id).cloned().unwrap_or_default(),
light: maps.lights.get(&node_id).copied(),
camera: maps.cameras.get(&node_id).copied(),
camera_config: maps.camera_configs.get(&node_id).cloned(),
line: maps.lines.get(&node_id).copied(),
decal: maps.decals.get(&node_id).copied(),
emitter: maps.emitters.get(&node_id).cloned(),
},
);
}
loaded.lines.extend(maps.lines.values().copied());
loaded.decals.extend(maps.decals.values().copied());
loaded.clips = animation::load_animations(renderer, scene, &maps);
renderer
.commit_load(|stats| {
use awsm_renderer::loading::LoadPhase as P;
match stats.phase {
P::UploadingGeometry => on_phase(LoadPhase::UploadingMeshes {
done: stats.geometry_uploaded,
total: stats.geometry_total,
}),
P::FinalizingTextures => on_phase(LoadPhase::UploadingTextures),
P::Compiling => on_phase(LoadPhase::CompilingPipelines(CompileProgress {
materials_pending: stats.pipelines_pending,
materials_ready: stats.pipelines_ready,
materials_failed: stats.pipelines_failed,
in_flight_subcompiles: stats.in_flight_subcompiles,
})),
P::Idle | P::Ready => {}
}
})
.await?;
Ok(loaded)
}
fn collect_renderables(nodes: &[EditorNode]) -> Vec<(NodeId, &Option<MaterialInstance>)> {
let mut out = Vec::new();
fn walk<'a>(nodes: &'a [EditorNode], out: &mut Vec<(NodeId, &'a Option<MaterialInstance>)>) {
for n in nodes {
match &n.kind {
NodeKind::Mesh { material, .. } | NodeKind::SkinnedMesh { material, .. } => {
out.push((n.id, material));
}
_ => {}
}
walk(&n.children, out);
}
}
walk(nodes, &mut out);
out
}
#[allow(clippy::too_many_arguments)]
async fn materialize(
renderer: &mut AwsmRenderer,
scene: &Scene,
node: &EditorNode,
parent: Option<TransformKey>,
parent_world: glam::Mat4,
parent_effective_visible: bool,
assets: &impl SceneAssets,
maps: &mut AnimResolveMaps,
placeholder: MaterialKey,
on_phase: &mut dyn FnMut(LoadPhase),
uploaded: &mut usize,
total: usize,
loaded: &mut LoadedScene,
) -> Result<()> {
let effective_visible = parent_effective_visible && node.visible;
if node.prefab {
let tmpl = capture_prefab(
renderer,
scene,
node,
None,
assets,
&maps.node_materials,
placeholder,
loaded,
)
.await?;
loaded.prefabs.insert(node.id, tmpl);
return Ok(());
}
let local = trs_to_transform(&node.transform);
let node_world = parent_world * local.to_matrix();
let tk = renderer.transforms.insert(local, parent);
maps.transforms.insert(node.id, tk);
let mat = maps
.node_materials
.get(&node.id)
.copied()
.unwrap_or(placeholder);
match &node.kind {
NodeKind::Mesh { mesh, .. } => {
if let Some(entry) = scene.assets.get(mesh.0) {
match &entry.source {
AssetSource::Mesh(RuntimeMesh::Primitive(shape)) => {
let md = awsm_meshgen::primitive_mesh(shape);
let key = renderer.add_raw_mesh(mesh_data_to_raw(md), tk, mat)?;
maps.meshes.entry(node.id).or_insert(key);
maps.node_meshes.entry(node.id).or_default().push(key);
loaded.meshes.push(key);
}
AssetSource::Mesh(RuntimeMesh::Glb) => {
let (keys, _) = load_glb_under(
renderer,
assets,
&mesh_glb_filename(mesh.0),
Some(tk),
mat,
)
.await?;
if let Some(&first) = keys.first() {
maps.meshes.entry(node.id).or_insert(first);
}
maps.node_meshes
.entry(node.id)
.or_default()
.extend(keys.iter().copied());
loaded.meshes.extend(keys);
}
_ => {}
}
}
*uploaded += 1;
on_phase(LoadPhase::UploadingMeshes {
done: *uploaded,
total,
});
}
NodeKind::SkinnedMesh { skin, .. } => {
let (keys, node_index_transforms) =
load_glb_under(renderer, assets, &mesh_glb_filename(skin.source), None, mat)
.await?;
if let Some(&first) = keys.first() {
maps.meshes.entry(node.id).or_insert(first);
}
maps.node_meshes
.entry(node.id)
.or_default()
.extend(keys.iter().copied());
for j in &skin.joints {
if let Some(&tk) = node_index_transforms.get(&(j.index as usize)) {
maps.skin_joints.insert(j.node, tk);
}
}
loaded.meshes.extend(keys);
*uploaded += 1;
on_phase(LoadPhase::UploadingMeshes {
done: *uploaded,
total,
});
}
NodeKind::Light(cfg) => {
if effective_visible {
let pos = Vec3::from_array(node.transform.translation);
let dir =
(Quat::from_array(node.transform.rotation) * Vec3::NEG_Z).normalize_or_zero();
let lt = light::light_from_config(cfg, pos, dir);
let shadow = light::light_shadow_params_from_config(cfg.shadow());
let casts = shadow.cast;
if let Ok(k) = renderer.insert_light(lt, Some(shadow)) {
renderer.lights.bind_transform(k, tk);
maps.lights.insert(node.id, k);
loaded.lights.push(k);
}
if casts {
renderer.ensure_shadow_pipelines_compiled().await?;
}
}
}
NodeKind::Camera(cfg) => {
let ck = renderer
.cameras
.insert(camera::camera_params_from_config(cfg));
maps.cameras.insert(node.id, ck);
maps.camera_configs.insert(node.id, cfg.clone());
}
NodeKind::Line(def) => {
if effective_visible {
materialize_line(renderer, def, node.id, node_world, maps).await?;
}
}
NodeKind::Sprite(def) => {
materialize_sprite(renderer, assets, def, node.id, tk, maps, loaded).await?;
}
NodeKind::Decal(cfg) => {
if effective_visible {
materialize_decal(renderer, assets, cfg, node.id, node_world, maps).await?;
}
}
NodeKind::InstancesAlongCurve(def) => {
materialize_instances_along_curve(renderer, scene, def, maps)?;
}
NodeKind::Curve(_) => {}
NodeKind::ParticleEmitter(def) => {
if effective_visible {
match particles::build_emitter(renderer, def, tk, node_world) {
Ok(handle) => {
loaded.meshes.push(handle.mesh);
loaded.transforms.push(handle.instance_transform);
maps.emitters.insert(node.id, handle);
}
Err(err) => {
tracing::warn!("scene-loader: ParticleEmitter build failed: {err}");
}
}
}
}
NodeKind::Group | NodeKind::Collider(_) => {}
}
if !effective_visible {
if let Some(keys) = maps.node_meshes.get(&node.id) {
for &k in keys {
let _ = renderer.set_mesh_hidden(k, true);
}
}
}
for child in &node.children {
Box::pin(materialize(
renderer,
scene,
child,
Some(tk),
node_world,
effective_visible,
assets,
maps,
placeholder,
on_phase,
uploaded,
total,
loaded,
))
.await?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn capture_prefab(
renderer: &mut AwsmRenderer,
scene: &Scene,
node: &EditorNode,
parent: Option<NodeId>,
assets: &impl SceneAssets,
node_materials: &HashMap<NodeId, MaterialKey>,
placeholder: MaterialKey,
loaded: &mut LoadedScene,
) -> Result<PrefabTemplate> {
debug_assert!(
parent.is_none(),
"prefab root capture starts at the subtree root"
);
let layout = prefab_subtree_layout(node);
let scratch = renderer.transforms.insert(Transform::default(), None);
let mut nodes = Vec::with_capacity(layout.len());
for step in &layout {
let n = step.node;
if step.nested_prefab {
let tmpl = Box::pin(capture_prefab(
renderer,
scene,
n,
None,
assets,
node_materials,
placeholder,
loaded,
))
.await?;
loaded.prefabs.insert(n.id, tmpl);
continue;
}
let mat = node_materials.get(&n.id).copied().unwrap_or(placeholder);
let template_meshes =
build_node_meshes(renderer, scene, n, scratch, mat, assets, true).await?;
let replay = match &n.kind {
NodeKind::Light(cfg) => PrefabReplay::Light(cfg.clone()),
NodeKind::Camera(cfg) => PrefabReplay::Camera(cfg.clone()),
NodeKind::Line(def) => PrefabReplay::Line(def.clone()),
NodeKind::Decal(cfg) => PrefabReplay::Decal {
texture_index: resolve_decal_texture_index(renderer, assets, cfg).await,
alpha: cfg.alpha,
},
NodeKind::ParticleEmitter(def) => PrefabReplay::ParticleEmitter(def.clone()),
NodeKind::InstancesAlongCurve(def) => match find_curve(&scene.nodes, def.curve_node) {
Some(curve) => PrefabReplay::InstancesAlongCurve {
transforms: curve_instance_transforms(curve, def),
source_node: def.source_node,
per_instance_colors: def.per_instance_colors.clone(),
},
None => PrefabReplay::None,
},
_ => PrefabReplay::None,
};
nodes.push(PrefabNode {
id: n.id,
local: n.transform,
parent: step.parent,
template_meshes,
replay,
});
}
for pn in &nodes {
loaded.meshes.extend(pn.template_meshes.iter().copied());
}
loaded.transforms.push(scratch);
Ok(PrefabTemplate {
root: node.id,
nodes,
})
}
struct PrefabLayoutStep<'a> {
node: &'a EditorNode,
parent: Option<NodeId>,
nested_prefab: bool,
}
fn prefab_subtree_layout(root: &EditorNode) -> Vec<PrefabLayoutStep<'_>> {
fn walk<'a>(
node: &'a EditorNode,
parent: Option<NodeId>,
is_root: bool,
out: &mut Vec<PrefabLayoutStep<'a>>,
) {
if !is_root && node.prefab {
out.push(PrefabLayoutStep {
node,
parent,
nested_prefab: true,
});
return;
}
out.push(PrefabLayoutStep {
node,
parent,
nested_prefab: false,
});
for child in &node.children {
walk(child, Some(node.id), false, out);
}
}
let mut out = Vec::new();
walk(root, None, true, &mut out);
out
}
async fn build_node_meshes(
renderer: &mut AwsmRenderer,
scene: &Scene,
node: &EditorNode,
tk: TransformKey,
mat: MaterialKey,
assets: &impl SceneAssets,
hidden: bool,
) -> Result<Vec<MeshKey>> {
let mut keys: Vec<MeshKey> = Vec::new();
match &node.kind {
NodeKind::Mesh { mesh, .. } => {
if let Some(entry) = scene.assets.get(mesh.0) {
match &entry.source {
AssetSource::Mesh(RuntimeMesh::Primitive(shape)) => {
let md = awsm_meshgen::primitive_mesh(shape);
let key = renderer.add_raw_mesh(mesh_data_to_raw(md), tk, mat)?;
keys.push(key);
}
AssetSource::Mesh(RuntimeMesh::Glb) => {
let (glb_keys, _) = load_glb_under(
renderer,
assets,
&mesh_glb_filename(mesh.0),
Some(tk),
mat,
)
.await?;
keys.extend(glb_keys);
}
_ => {}
}
}
}
NodeKind::SkinnedMesh { skin, .. } => {
let (glb_keys, _) =
load_glb_under(renderer, assets, &mesh_glb_filename(skin.source), None, mat)
.await?;
keys.extend(glb_keys);
}
NodeKind::Sprite(def) => {
let key = build_sprite_mesh(renderer, assets, def, tk).await?;
keys.push(key);
}
_ => {}
}
if hidden {
for &k in &keys {
renderer.set_mesh_hidden(k, true)?;
}
}
Ok(keys)
}
async fn materialize_line(
renderer: &mut AwsmRenderer,
def: &LineDef,
node_id: NodeId,
node_world: Mat4,
maps: &mut AnimResolveMaps,
) -> Result<()> {
if def.points.len() < 2 {
return Ok(());
}
let positions: Vec<Vec3> = def
.points
.iter()
.map(|p| node_world.transform_point3(Vec3::from_array(p.pos)))
.collect();
let colors: Vec<Vec4> = def
.points
.iter()
.map(|p| Vec4::from_array(p.color))
.collect();
if let Some(key) =
renderer.add_line_strip(&positions, &colors, def.width_px, def.depth_test_always)?
{
maps.lines.insert(node_id, key);
renderer.ensure_line_pipelines_compiled().await?;
}
Ok(())
}
async fn materialize_sprite(
renderer: &mut AwsmRenderer,
assets: &impl SceneAssets,
def: &SpriteDef,
node_id: NodeId,
tk: TransformKey,
maps: &mut AnimResolveMaps,
loaded: &mut LoadedScene,
) -> Result<()> {
let key = build_sprite_mesh(renderer, assets, def, tk).await?;
maps.meshes.entry(node_id).or_insert(key);
maps.node_meshes.entry(node_id).or_default().push(key);
loaded.meshes.push(key);
Ok(())
}
async fn build_sprite_mesh(
renderer: &mut AwsmRenderer,
assets: &impl SceneAssets,
def: &SpriteDef,
tk: TransformKey,
) -> Result<MeshKey> {
use awsm_renderer::materials::flipbook::{FlipBookMaterial, FlipBookMode};
use awsm_renderer::materials::unlit::UnlitMaterial;
use awsm_renderer::meshes::mesh::BillboardMode as RBillboard;
use awsm_renderer_core::texture::mipmap::MipmapTextureKind;
use awsm_scene::{BillboardMode, FlipBookModeDef, SpriteAlphaMode};
let alpha = match def.alpha_mode {
SpriteAlphaMode::Opaque => MaterialAlphaMode::Opaque,
SpriteAlphaMode::Mask { cutoff_x1000 } => MaterialAlphaMode::Mask {
cutoff: cutoff_x1000 as f32 / 1000.0,
},
SpriteAlphaMode::Blend => MaterialAlphaMode::Blend,
};
let tex = match &def.texture {
Some(t) => {
texture::load_texture(renderer, assets, t, true, MipmapTextureKind::Albedo).await
}
None => None,
};
let material = match &def.flipbook {
Some(fb) => {
let mut m = FlipBookMaterial::new(alpha, true);
m.tint = def.tint;
m.cols = fb.cols.max(1);
m.rows = fb.rows.max(1);
m.frame_count = fb.frame_count.max(1);
m.fps = fb.fps;
m.time_offset = fb.time_offset;
m.mode = match fb.mode {
FlipBookModeDef::Loop => FlipBookMode::Loop,
FlipBookModeDef::PingPong => FlipBookMode::PingPong,
FlipBookModeDef::Clamp => FlipBookMode::Clamp,
FlipBookModeDef::Once => FlipBookMode::Once,
};
m.flip_y = fb.flip_y;
m.atlas_tex = tex;
Material::FlipBook(Box::new(m))
}
None => {
let mut m = UnlitMaterial::new(alpha, true);
m.base_color_factor = def.tint;
m.base_color_tex = tex;
Material::Unlit(m)
}
};
let mat = renderer.materials.insert(
material,
&renderer.textures,
&renderer.dynamic_materials,
&renderer.extras_pool,
);
let md = awsm_meshgen::sprite_quad(def.size[0], def.size[1]);
let key = renderer.add_raw_mesh(mesh_data_to_raw(md), tk, mat)?;
let rbillboard = match def.billboard {
BillboardMode::None => RBillboard::None,
BillboardMode::YAxis => RBillboard::YAxis,
BillboardMode::Full => RBillboard::Full,
};
if !matches!(rbillboard, RBillboard::None) {
renderer.set_mesh_billboard_mode(key, rbillboard)?;
}
Ok(key)
}
async fn materialize_decal(
renderer: &mut AwsmRenderer,
assets: &impl SceneAssets,
cfg: &DecalConfig,
node_id: NodeId,
node_world: Mat4,
maps: &mut AnimResolveMaps,
) -> Result<()> {
use awsm_renderer::decals::AwsmDecalError;
let texture_index = resolve_decal_texture_index(renderer, assets, cfg).await;
match renderer.insert_decal(node_world, texture_index, cfg.alpha) {
Ok(key) => {
maps.decals.insert(node_id, key);
}
Err(AwsmDecalError::FeatureNotEnabled) => warn_decal_feature_off(),
Err(err) => tracing::warn!("scene-loader: insert_decal failed: {err:?}"),
}
Ok(())
}
async fn resolve_decal_texture_index(
renderer: &mut AwsmRenderer,
assets: &impl SceneAssets,
cfg: &DecalConfig,
) -> u32 {
let stride = awsm_renderer::decals::decal_texture_index_stride(&renderer.gpu);
match &cfg.texture {
Some(t) => {
match texture::load_texture(renderer, assets, t, true, MipmapTextureKind::Albedo).await
{
Some(mt) => renderer
.textures
.get_entry(mt.key)
.map(|e| (e.array_index as u32) * stride + e.layer_index as u32)
.unwrap_or(0),
None => 0,
}
}
None => 0,
}
}
fn materialize_instances_along_curve(
renderer: &mut AwsmRenderer,
scene: &Scene,
def: &InstancesAlongCurveDef,
maps: &mut AnimResolveMaps,
) -> Result<()> {
let Some(curve) = find_curve(&scene.nodes, def.curve_node) else {
tracing::warn!(
"scene-loader: InstancesAlongCurve references missing/non-curve node {:?}",
def.curve_node
);
return Ok(());
};
let Some(&source_mesh) = maps.meshes.get(&def.source_node) else {
tracing::warn!(
"scene-loader: InstancesAlongCurve source node {:?} has no materialized mesh \
(must precede the instances node) — skipped",
def.source_node
);
return Ok(());
};
let transforms = curve_instance_transforms(curve, def);
if transforms.is_empty() {
return Ok(());
}
let transform_key = renderer.meshes.get(source_mesh)?.transform_key;
if let Err(err) = renderer.enable_mesh_instancing_opaque(source_mesh, &transforms) {
tracing::warn!("scene-loader: enable_mesh_instancing_opaque failed: {err}");
return Ok(());
}
if !def.per_instance_colors.is_empty() {
let attrs: Vec<awsm_renderer::instances::InstanceAttr> =
expand_instance_colors(&def.per_instance_colors, transforms.len())
.into_iter()
.map(|c| awsm_renderer::instances::InstanceAttr::from_rgba_alpha_size(c, 1.0, 1.0))
.collect();
if let Err(err) = renderer.set_mesh_instance_attrs(transform_key, &attrs) {
tracing::warn!("scene-loader: curve per-instance colours failed: {err}");
}
}
Ok(())
}
fn expand_instance_colors(colors: &[[f32; 4]], count: usize) -> Vec<[f32; 4]> {
let last = *colors.last().expect("non-empty per_instance_colors");
(0..count)
.map(|i| colors.get(i).copied().unwrap_or(last))
.collect()
}
fn find_curve(nodes: &[EditorNode], id: NodeId) -> Option<&CurveDef> {
for n in nodes {
if n.id == id {
if let NodeKind::Curve(def) = &n.kind {
return Some(def);
}
}
if let Some(found) = find_curve(&n.children, id) {
return Some(found);
}
}
None
}
fn curve_instance_transforms(curve: &CurveDef, def: &InstancesAlongCurveDef) -> Vec<Transform> {
use awsm_curves::{Curve3, FrameSequence};
let points: Vec<Vec3> = curve
.control_points
.iter()
.map(|p| Vec3::from_array(*p))
.collect();
if points.len() < 2 {
return Vec::new();
}
let mut crom = awsm_curves::CatmullRomCurve::new(points, curve.closed);
crom.tension = curve.tension;
let sample_count = curve.sample_count.max(2) as usize;
let total_len = crom.total_length(sample_count);
let spacing = def.spacing.max(1.0e-3);
if total_len <= 0.0 {
return Vec::new();
}
let frames = FrameSequence::parallel_transport(&crom, sample_count, Vec3::Y);
let mut out = Vec::new();
let mut dist = 0.0_f32;
while dist <= total_len + 1.0e-4 {
let t = (dist / total_len).clamp(0.0, 1.0);
let frame_pos = (t * (sample_count - 1) as f32).round() as usize;
let frame = &frames.frames[frame_pos.min(frames.frames.len() - 1)];
let position = frame.position + frame.normal * def.side_offset;
let rotation = if def.orient_to_tangent {
frame.rotation()
} else {
Quat::IDENTITY
};
out.push(Transform {
translation: position,
rotation,
scale: Vec3::ONE,
});
dist += spacing;
}
out
}
fn warn_decal_feature_off() {
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if !WARNED.swap(true, Ordering::Relaxed) {
tracing::warn!(
"scene-loader: Decal node skipped — the renderer's `decals` feature is off, so the \
per-decal GPU pass doesn't exist (would render as 'decal missing')"
);
}
}
pub async fn load_glb_under(
renderer: &mut AwsmRenderer,
assets: &impl SceneAssets,
leaf: &str,
parent: Option<TransformKey>,
material: MaterialKey,
) -> Result<(Vec<MeshKey>, HashMap<usize, TransformKey>)> {
let key = format!("{ASSETS_DIR}/{leaf}");
let bytes = assets
.fetch(&key)
.await
.map_err(|_| anyhow!("bundle is missing mesh glb `{key}`"))?;
let transparent = renderer.materials.is_transparency_pass(material);
let data = GltfLoader::from_glb_bytes(&bytes).await?.into_data(None)?;
let ctx = renderer
.populate_gltf_with(
data,
PopulateGltfOpts {
scene: None,
parent_transform: parent,
material_source: GltfMaterialSource::Single(material),
},
)
.await?;
let (keys, node_index_transforms): (Vec<MeshKey>, HashMap<usize, TransformKey>) = {
let lookups = ctx.key_lookups.lock().unwrap();
let keys = lookups.all_mesh_keys.values().flatten().copied().collect();
(keys, lookups.node_index_to_transform.clone())
};
if transparent {
for &k in &keys {
let _ = renderer.set_mesh_shadow_flags(
k,
awsm_renderer::shadows::MeshShadowFlags {
cast: false,
receive: false,
},
);
}
}
Ok((keys, node_index_transforms))
}
pub async fn materialize_node_mesh(
renderer: &mut AwsmRenderer,
scene: &Scene,
node: &EditorNode,
assets: &impl SceneAssets,
material: MaterialKey,
) -> Result<Vec<MeshKey>> {
let tk = renderer
.transforms
.insert(trs_to_transform(&node.transform), None);
build_node_meshes(renderer, scene, node, tk, material, assets, false).await
}
fn trs_to_transform(trs: &Trs) -> Transform {
Transform {
translation: Vec3::from_array(trs.translation),
rotation: Quat::from_array(trs.rotation),
scale: Vec3::from_array(trs.scale),
}
}
pub fn mesh_data_to_raw(md: awsm_meshgen::MeshData) -> RawMeshData {
RawMeshData {
positions: md.positions,
normals: md.normals,
uv_sets: md.uvs,
colors: md.colors,
indices: md.indices,
..Default::default()
}
}
async fn resolve_material(
renderer: &mut AwsmRenderer,
instance: Option<&MaterialInstance>,
placeholder: MaterialKey,
assets: &impl SceneAssets,
custom: &HashMap<AssetId, awsm_materials::MaterialShaderId>,
) -> MaterialKey {
let Some(inst) = instance else {
return placeholder;
};
if let Some(&shader_id) = custom.get(&inst.asset) {
if let Some(mat) = dynamic::build_custom_material(renderer, shader_id, inst, assets).await {
renderer.upload_dynamic_material_buffers(&mat);
return renderer.materials.insert(
mat,
&renderer.textures,
&renderer.dynamic_materials,
&renderer.extras_pool,
);
}
return placeholder;
}
let def = &inst.inline;
let material = match def.shading {
MaterialShading::Pbr => {
let alpha = material::alpha_mode_of(def);
let mut pbr = material::material_to_pbr(def, alpha, None);
use MipmapTextureKind as K;
if let Some(t) = &def.base_color_texture {
pbr.base_color_tex =
texture::load_texture(renderer, assets, t, true, K::Albedo).await;
}
if let Some(t) = &def.metallic_roughness_texture {
pbr.metallic_roughness_tex =
texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
if let Some(t) = &def.normal_texture {
pbr.normal_tex = texture::load_texture(renderer, assets, t, false, K::Normal).await;
}
if let Some(t) = &def.occlusion_texture {
pbr.occlusion_tex =
texture::load_texture(renderer, assets, t, false, K::Occlusion).await;
}
if let Some(t) = &def.emissive_texture {
pbr.emissive_tex =
texture::load_texture(renderer, assets, t, true, K::Emissive).await;
}
bind_extension_textures(renderer, assets, def, &mut pbr).await;
Material::Pbr(Box::new(pbr))
}
_ => material::material_to_renderer(def),
};
renderer.materials.insert(
material,
&renderer.textures,
&renderer.dynamic_materials,
&renderer.extras_pool,
)
}
async fn bind_extension_textures(
renderer: &mut AwsmRenderer,
assets: &impl SceneAssets,
def: &awsm_scene::MaterialDef,
pbr: &mut awsm_renderer::materials::pbr::PbrMaterial,
) {
use MipmapTextureKind as K;
let ext = &def.extensions;
if let (Some(e), Some(p)) = (ext.specular.as_ref(), pbr.specular.as_mut()) {
if let Some(t) = &e.tex {
p.tex = texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
if let Some(t) = &e.color_tex {
p.color_tex = texture::load_texture(renderer, assets, t, true, K::Albedo).await;
}
}
if let (Some(e), Some(p)) = (ext.transmission.as_ref(), pbr.transmission.as_mut()) {
if let Some(t) = &e.tex {
p.tex = texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
}
if let (Some(e), Some(p)) = (
ext.diffuse_transmission.as_ref(),
pbr.diffuse_transmission.as_mut(),
) {
if let Some(t) = &e.tex {
p.tex = texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
if let Some(t) = &e.color_tex {
p.color_tex = texture::load_texture(renderer, assets, t, true, K::Albedo).await;
}
}
if let (Some(e), Some(p)) = (ext.volume.as_ref(), pbr.volume.as_mut()) {
if let Some(t) = &e.thickness_tex {
p.thickness_tex =
texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
}
if let (Some(e), Some(p)) = (ext.clearcoat.as_ref(), pbr.clearcoat.as_mut()) {
if let Some(t) = &e.tex {
p.tex = texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
if let Some(t) = &e.roughness_tex {
p.roughness_tex =
texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
if let Some(t) = &e.normal_tex {
p.normal_tex = texture::load_texture(renderer, assets, t, false, K::Normal).await;
}
}
if let (Some(e), Some(p)) = (ext.sheen.as_ref(), pbr.sheen.as_mut()) {
if let Some(t) = &e.color_tex {
p.color_tex = texture::load_texture(renderer, assets, t, true, K::Albedo).await;
}
if let Some(t) = &e.roughness_tex {
p.roughness_tex =
texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
}
if let (Some(e), Some(p)) = (ext.anisotropy.as_ref(), pbr.anisotropy.as_mut()) {
if let Some(t) = &e.tex {
p.tex = texture::load_texture(renderer, assets, t, false, K::Normal).await;
}
}
if let (Some(e), Some(p)) = (ext.iridescence.as_ref(), pbr.iridescence.as_mut()) {
if let Some(t) = &e.tex {
p.tex = texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
if let Some(t) = &e.thickness_tex {
p.thickness_tex =
texture::load_texture(renderer, assets, t, false, K::MetallicRoughness).await;
}
}
}
fn insert_placeholder_material(renderer: &mut AwsmRenderer) -> MaterialKey {
let mut m = UnlitMaterial::new(MaterialAlphaMode::Opaque, false);
m.base_color_factor = [1.0, 0.0, 1.0, 1.0];
renderer.materials.insert(
Material::Unlit(m),
&renderer.textures,
&renderer.dynamic_materials,
&renderer.extras_pool,
)
}
#[cfg(test)]
mod prefab_tests {
use super::{expand_instance_colors, prefab_subtree_layout};
use awsm_scene::{EditorNode, NodeId, NodeKind};
#[test]
fn instance_colors_repeat_last_when_short_and_truncate_when_long() {
let red = [1.0, 0.0, 0.0, 1.0];
let green = [0.0, 1.0, 0.0, 1.0];
let out = expand_instance_colors(&[red, green], 4);
assert_eq!(out, vec![red, green, green, green]);
let out = expand_instance_colors(&[red, green, red], 2);
assert_eq!(out, vec![red, green]);
let out = expand_instance_colors(&[red, green], 2);
assert_eq!(out, vec![red, green]);
}
fn node(id: NodeId, prefab: bool, children: Vec<EditorNode>) -> EditorNode {
EditorNode {
id,
name: String::new(),
transform: Default::default(),
kind: NodeKind::Group,
locked: false,
visible: true,
prefab,
children,
}
}
#[test]
fn layout_is_dfs_preorder_with_parent_wiring() {
let (root, child1, grandchild, child2) =
(NodeId::new(), NodeId::new(), NodeId::new(), NodeId::new());
let tree = node(
root,
true,
vec![
node(child1, false, vec![node(grandchild, false, vec![])]),
node(child2, false, vec![]),
],
);
let layout = prefab_subtree_layout(&tree);
let plan: Vec<_> = layout
.iter()
.map(|s| (s.node.id, s.parent, s.nested_prefab))
.collect();
assert_eq!(
plan,
vec![
(root, None, false),
(child1, Some(root), false),
(grandchild, Some(child1), false),
(child2, Some(root), false),
]
);
}
#[test]
fn nested_prefab_is_a_boundary_and_excludes_its_descendants() {
let (root, nested, deep) = (NodeId::new(), NodeId::new(), NodeId::new());
let tree = node(
root,
true,
vec![node(nested, true, vec![node(deep, false, vec![])])],
);
let layout = prefab_subtree_layout(&tree);
let ids: Vec<_> = layout.iter().map(|s| s.node.id).collect();
assert_eq!(ids, vec![root, nested]);
let nested_step = layout.iter().find(|s| s.node.id == nested).unwrap();
assert!(nested_step.nested_prefab);
assert_eq!(nested_step.parent, Some(root));
assert!(!layout[0].nested_prefab);
}
}