use serde::Deserialize;
use std::cell::RefCell;
use std::path::Path;
use std::sync::Once;
pub(crate) use crate::authoring::registry::RegisteredType;
pub use crate::authoring::resource_type::ResourceKind;
pub(crate) use crate::authoring::resource_type::{asset_resource_kind, is_mesh_source};
pub(crate) use concinnity_core::resource::ResourceHandles;
pub(crate) trait ResourceAssetCompile {
fn compile_payload(
self,
args: &serde_json::Value,
assets_dir: Option<&Path>,
) -> std::io::Result<Vec<u8>>;
fn source_files(self, args: &serde_json::Value, assets_dir: Option<&Path>) -> Vec<String>;
fn compile_data(self, name: &str, args: &serde_json::Value)
-> std::io::Result<Option<Vec<u8>>>;
}
impl ResourceAssetCompile for RegisteredType {
fn compile_payload(
self,
args: &serde_json::Value,
assets_dir: Option<&Path>,
) -> std::io::Result<Vec<u8>> {
match self {
Self::AudioClip => crate::compile::audio_clip::compile_audio_clip_payload(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::Texture => crate::compile::texture::compile_texture_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::CubemapTexture => crate::compile::cubemap::compile_cubemap_payload(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::EnvironmentMap => {
crate::compile::environment_map::compile_environment_map_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
Self::ColorLut => {
crate::compile::color_lut::compile_color_lut_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
Self::Font => crate::compile::font::compile_font_payload(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::Material => compile_material_data(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::Mesh => crate::compile::mesh_compile::compile_mesh_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::SkinnedMesh => compile_skinned_mesh_payload(args),
other => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{} is not a resource asset", other.as_str()),
)),
}
}
fn source_files(self, args: &serde_json::Value, assets_dir: Option<&Path>) -> Vec<String> {
match self {
Self::AudioClip
| Self::Texture
| Self::CubemapTexture
| Self::EnvironmentMap
| Self::ColorLut
| Self::Mesh
| Self::SkinnedMesh => {
let mut files: Vec<String> = args
.get("source")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
.into_iter()
.collect();
if let Some(src) = files.first().cloned()
&& src.to_lowercase().ends_with(".gltf")
{
files.extend(crate::import::gltf_source::referenced_files(
&src, assets_dir,
));
}
if let Some(arg) = args.get("character_model").and_then(|v| {
serde_json::from_value::<crate::compile::character::import::CharacterModelArg>(
v.clone(),
)
.ok()
}) {
files.extend(arg.source_files());
}
files
}
Self::Font => args
.get("path")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
.into_iter()
.collect(),
_ => Vec::new(),
}
}
fn compile_data(
self,
name: &str,
args: &serde_json::Value,
) -> std::io::Result<Option<Vec<u8>>> {
match self {
Self::SkinnedMesh => compile_skinned_mesh_data(name, args)
.map(Some)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
_ => Ok(None),
}
}
}
fn compile_material_data(args: &serde_json::Value) -> Result<Vec<u8>, String> {
let mat: crate::components::Material =
Deserialize::deserialize(args).map_err(|e| format!("Material args: {e}"))?;
let mat = crate::authoring::validate::material(mat);
postcard::to_allocvec(&mat).map_err(|e| format!("Material serialize: {e}"))
}
fn compile_skinned_mesh_payload(args: &serde_json::Value) -> std::io::Result<Vec<u8>> {
let mesh: crate::components::SkinnedMesh = Deserialize::deserialize(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let skeleton: Vec<crate::components::SkeletonJoint> = match args.get("skeleton") {
Some(v) => Deserialize::deserialize(v).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("SkinnedMesh: invalid skeleton args: {e}"),
)
})?,
None => Vec::new(),
};
crate::compile::geometry::compile_skinned_mesh_payload_with_lods(
&mesh.vertices,
&mesh.indices,
&skeleton,
&mesh.morph_target_names,
&mesh.morph_deltas,
&crate::compile::geometry::SkinnedLods {
levels: mesh.lod_levels,
distances: &mesh.lod_distances,
},
)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
fn compile_skinned_mesh_data(name: &str, args: &serde_json::Value) -> Result<Vec<u8>, String> {
let mut sm: crate::components::SkinnedMesh =
Deserialize::deserialize(args).map_err(|e| format!("SkinnedMesh args: {e}"))?;
if sm.scale == [0.0, 0.0, 0.0] {
sm.scale = [1.0, 1.0, 1.0];
}
if sm.lod_levels == 0 {
sm.lod_levels = 1;
}
sm.lod_levels = sm.lod_levels.min(8);
sm.max_instances = sm.max_instances.min(4096);
sm.vertices = Vec::new();
sm.indices = Vec::new();
sm.morph_target_names = Vec::new();
sm.morph_deltas = Vec::new();
let name_id = crate::ecs::asset_id::intern(name);
postcard::to_allocvec(&(name_id.0, sm)).map_err(|e| format!("SkinnedMesh serialize: {e}"))
}
pub(crate) fn assign_shader_handles(
handles: &mut ResourceHandles,
assets: &[crate::authoring::world::WorldJsonlAsset],
) {
for asset in assets
.iter()
.filter(|a| a.asset_type.to_lowercase().replace('_', "") == "shader")
{
handles.assign_shader(crate::ecs::asset_id::intern(&asset.name));
}
}
pub(crate) fn assign_mesh_source_handles(
handles: &mut ResourceHandles,
assets: &[crate::authoring::world::WorldJsonlAsset],
) {
handles.assign_mesh_sources(assets.iter().filter_map(|a| {
crate::authoring::resource_type::mesh_source_block(&a.asset_type, &a.args)
.map(|block| (crate::ecs::asset_id::intern(&a.name), block))
}));
}
thread_local! {
static RESOURCE_HANDLES: RefCell<ResourceHandles> = RefCell::new(ResourceHandles::default());
}
pub(crate) fn ensure_resource_handle_resolvers() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
crate::ecs::set_texture_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Texture, id))
});
crate::ecs::set_audio_clip_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::AudioClip, id))
});
crate::ecs::set_font_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Font, id))
});
crate::ecs::set_mesh_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Mesh, id))
});
crate::ecs::set_material_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Material, id))
});
crate::ecs::set_skinned_mesh_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::SkinnedMesh, id))
});
crate::ecs::set_shader_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().shader(id))
});
});
}
pub(crate) fn install_resource_handles(handles: ResourceHandles) {
ensure_resource_handle_resolvers();
RESOURCE_HANDLES.with(|h| *h.borrow_mut() = handles);
}
pub(crate) fn reset_resource_handles() {
ensure_resource_handle_resolvers();
RESOURCE_HANDLES.with(|h| *h.borrow_mut() = ResourceHandles::default());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::RegisteredType;
#[test]
fn mesh_source_handles_are_block_ordered_across_kinds() {
use crate::authoring::world::WorldJsonlAsset;
use crate::ecs::asset_id;
let a = |name: &str, ty: &str, args: serde_json::Value| WorldJsonlAsset {
name: name.to_string(),
asset_type: ty.to_string(),
args,
};
let assets = vec![
a("m0", "Mesh", serde_json::json!({})),
a(
"p0",
"ProceduralMesh",
serde_json::json!({"generator": "box"}),
),
a("f_tex", "File", serde_json::json!({"kind": "png"})),
a("v0", "VoxelChunk", serde_json::json!({})),
a("m1", "Mesh", serde_json::json!({})),
a("f_mesh", "File", serde_json::json!({"kind": "obj"})),
a("light", "PointLight", serde_json::json!({})),
a(
"p1",
"ProceduralMesh",
serde_json::json!({"generator": "sphere"}),
),
];
asset_id::reset_interner();
let names: Vec<&str> = assets.iter().map(|x| x.name.as_str()).collect();
asset_id::intern_all(&names);
let mut handles = ResourceHandles::default();
assign_mesh_source_handles(&mut handles, &assets);
let h = |name: &str| handles.get(ResourceKind::Mesh, asset_id::intern(name));
assert_eq!(h("m0"), Some(0));
assert_eq!(h("m1"), Some(1));
assert_eq!(h("p0"), Some(2));
assert_eq!(h("p1"), Some(3));
assert_eq!(h("v0"), Some(4));
assert_eq!(h("f_mesh"), Some(5));
assert_eq!(h("f_tex"), None);
assert_eq!(h("light"), None);
assert_eq!(handles.count(ResourceKind::Mesh), 6);
}
#[test]
fn a_material_reads_no_source_file_and_resolves_by_declaration_order() {
use crate::ecs::asset_id;
assert!(
RegisteredType::Material
.source_files(
&serde_json::json!({"albedo": "wall_tex", "source": "m.json"}),
None
)
.is_empty()
);
asset_id::reset_interner();
let stone = asset_id::intern("stone");
let wood = asset_id::intern("wood");
reset_resource_handles();
install_resource_handles(ResourceHandles::from_assets([
(stone, ResourceKind::Material),
(wood, ResourceKind::Material),
]));
let bytes = RegisteredType::Prop
.reserialize_args(
&serde_json::json!({"material": "wood"}),
concinnity_core::platform::Platform::Metal,
)
.unwrap();
let prop: crate::components::Prop = postcard::from_bytes(&bytes).unwrap();
assert_eq!(prop.material, Some(crate::ecs::MaterialHandle(1)));
let bytes = RegisteredType::Prop
.reserialize_args(
&serde_json::json!({"material": "granite"}),
concinnity_core::platform::Platform::Metal,
)
.unwrap();
let prop: crate::components::Prop = postcard::from_bytes(&bytes).unwrap();
assert_eq!(
prop.material,
Some(crate::ecs::MaterialHandle(
asset_id::intern("granite").0 as usize as u32
))
);
}
#[test]
fn a_shader_reference_name_bakes_to_its_declaration_order_handle() {
use crate::authoring::world::WorldJsonlAsset;
use crate::ecs::ShaderHandle;
use crate::ecs::asset_id;
asset_id::reset_interner();
let world_asset = |name: &str, ty: &str| WorldJsonlAsset {
name: name.to_string(),
asset_type: ty.to_string(),
args: serde_json::json!({}),
};
let assets = vec![
world_asset("mat", "Material"),
world_asset("shader_a", "Shader"),
world_asset("tex", "Texture"),
world_asset("shader_b", "Shader"),
];
reset_resource_handles();
let mut handles = ResourceHandles::default();
assign_shader_handles(&mut handles, &assets);
assert_eq!(handles.shader_count(), 2);
assert_eq!(handles.shader(asset_id::intern("shader_a")), Some(0));
assert_eq!(handles.shader(asset_id::intern("shader_b")), Some(1));
install_resource_handles(handles);
let bytes = RegisteredType::Material
.compile_payload(&serde_json::json!({"shader": "shader_b"}), None)
.unwrap();
let mat: crate::components::Material = postcard::from_bytes(&bytes).unwrap();
assert_eq!(mat.shader, Some(ShaderHandle(1)));
let bytes = RegisteredType::Material
.compile_payload(&serde_json::json!({}), None)
.unwrap();
let mat: crate::components::Material = postcard::from_bytes(&bytes).unwrap();
assert_eq!(mat.shader, None);
}
#[test]
fn a_texture_reference_name_reserializes_to_its_declaration_order_handle() {
use crate::ecs::asset_id;
asset_id::reset_interner();
let a = asset_id::intern("tex_a");
let b = asset_id::intern("tex_b");
reset_resource_handles();
install_resource_handles(ResourceHandles::from_assets([
(a, ResourceKind::Texture),
(b, ResourceKind::Texture),
]));
let bake = |field_json: serde_json::Value| -> crate::components::Material {
let bytes = RegisteredType::Material
.compile_payload(&field_json, None)
.unwrap();
postcard::from_bytes(&bytes).unwrap()
};
use crate::ecs::TextureHandle;
assert_eq!(
bake(serde_json::json!({"albedo": "tex_b"})).albedo,
Some(TextureHandle(1))
);
assert_eq!(
bake(serde_json::json!({"albedo": "tex_a"})).albedo,
Some(TextureHandle(0))
);
let decal_bytes = RegisteredType::Decal
.reserialize_args(
&serde_json::json!({"texture": "tex_b"}),
concinnity_core::platform::Platform::Metal,
)
.unwrap();
let decal: crate::components::Decal = postcard::from_bytes(&decal_bytes).unwrap();
assert_eq!(decal.texture, Some(TextureHandle(1)));
let prop_bytes = RegisteredType::Prop
.reserialize_args(
&serde_json::json!({"texture": "tex_b"}),
concinnity_core::platform::Platform::Metal,
)
.unwrap();
let prop: crate::components::Prop = postcard::from_bytes(&prop_bytes).unwrap();
assert_eq!(prop.texture, Some(TextureHandle(1)));
let inst_bytes = RegisteredType::InstancedProp
.reserialize_args(
&serde_json::json!({"texture": "tex_a"}),
concinnity_core::platform::Platform::Metal,
)
.unwrap();
let inst: crate::components::InstancedProp = postcard::from_bytes(&inst_bytes).unwrap();
assert_eq!(inst.texture, Some(TextureHandle(0)));
}
#[test]
fn an_audio_clip_reference_name_reserializes_to_its_declaration_order_handle() {
use crate::ecs::asset_id;
asset_id::reset_interner();
let clip_a = asset_id::intern("clip_a");
let tex = asset_id::intern("tex");
let clip_b = asset_id::intern("clip_b");
reset_resource_handles();
install_resource_handles(ResourceHandles::from_assets([
(clip_a, ResourceKind::AudioClip),
(tex, ResourceKind::Texture),
(clip_b, ResourceKind::AudioClip),
]));
use crate::ecs::AudioClipHandle;
let clip_field = |ct: RegisteredType, name: &str| -> Option<AudioClipHandle> {
let bytes = ct
.reserialize_args(
&serde_json::json!({ "clip": name }),
concinnity_core::platform::Platform::Metal,
)
.unwrap();
match ct {
RegisteredType::AudioEmitter => {
postcard::from_bytes::<crate::components::AudioEmitter>(&bytes)
.unwrap()
.clip
}
RegisteredType::AudioCue => {
postcard::from_bytes::<crate::components::AudioCue>(&bytes)
.unwrap()
.clip
}
other => panic!("unexpected type {other:?}"),
}
};
assert_eq!(
clip_field(RegisteredType::AudioEmitter, "clip_a"),
Some(AudioClipHandle(0))
);
assert_eq!(
clip_field(RegisteredType::AudioEmitter, "clip_b"),
Some(AudioClipHandle(1))
);
assert_eq!(
clip_field(RegisteredType::AudioCue, "clip_b"),
Some(AudioClipHandle(1))
);
}
#[test]
fn skinned_mesh_data_is_the_same_before_and_after_the_geometry_is_inlined() {
crate::ecs::asset_id::reset_interner();
let authored = serde_json::json!({
"source": "hero.glb",
"position": [1.0, 2.0, 3.0],
"scale": [1.0, 1.0, 1.0],
"lod_levels": 2,
"capsule": {"half_height": 0.9, "radius": 0.35},
});
let mut desugared = authored.clone();
let obj = desugared.as_object_mut().unwrap();
obj.insert(
"vertices".into(),
serde_json::json!([{"pos": [0.0, 0.0, 0.0]}, {"pos": [1.0, 0.0, 0.0]}]),
);
obj.insert("indices".into(), serde_json::json!([0, 1, 0]));
obj.insert("skeleton".into(), serde_json::json!([{"name": "root"}]));
obj.insert(
"morph_target_names".into(),
serde_json::json!(["weight", "jaw+"]),
);
obj.insert(
"morph_deltas".into(),
serde_json::json!([
{"position": [0.1, 0.0, 0.0]}, {"position": [0.2, 0.0, 0.0]},
{"position": [0.0, 0.3, 0.0]}, {"position": [0.0, 0.4, 0.0]}
]),
);
assert_eq!(
compile_skinned_mesh_data("body", &desugared).unwrap(),
compile_skinned_mesh_data("body", &authored).unwrap(),
);
}
}