use super::{load::BodySpec, FigureModelEntry}; use crate::{ mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_terrain}, render::{BoneMeshes, ColLightInfo, FigureModel, Mesh, Renderer, TerrainVertex}, scene::camera::CameraMode, }; use anim::Skeleton; use common::{ assets::ReloadWatcher, comp::{ inventory::{ slot::{ArmorSlot, EquipSlot}, Inventory, }, item::{item_key::ItemKey, modular, Item, ItemDefinitionId}, CharacterState, }, figure::Segment, slowjob::SlowJobPool, vol::BaseVol, }; use core::{hash::Hash, ops::Range}; use crossbeam_utils::atomic; use hashbrown::{hash_map::Entry, HashMap}; use serde::Deserialize; use std::sync::Arc; use vek::*; /// A type produced by mesh worker threads corresponding to the information /// needed to mesh figures. struct MeshWorkerResponse { col_light: ColLightInfo, opaque: Mesh, bounds: anim::vek::Aabb, vertex_range: [Range; N], } /// NOTE: To test this cell for validity, we currently first use /// Arc::get_mut(), and then only if that succeeds do we call AtomicCell::take. /// This way, we avoid all atomic updates for the fast path read in the "not yet /// updated" case (though it would be faster without weak pointers); since once /// it's updated, we switch from `Pending` to `Done`, this is only suboptimal /// for one frame. type MeshWorkerCell = atomic::AtomicCell>>; /// A future FigureModelEntryLod. enum FigureModelEntryFuture { /// We can poll the future to see whether the figure model is ready. // TODO: See if we can find away to either get rid of this Arc, or reuse Arcs across different // figures. Updates to uvth for thread pool shared storage might obviate this requirement. Pending(Arc>), /// Stores the already-meshed model. Done(FigureModelEntry), } const LOD_COUNT: usize = 3; type FigureModelEntryLod<'b> = Option<&'b FigureModelEntry>; #[derive(Clone, Eq, Hash, PartialEq)] /// TODO: merge item_key and extra field into an enum pub struct FigureKey { /// Body pointed to by this key. pub(super) body: Body, /// Only used by Body::ItemDrop pub item_key: Option>, /// Extra state. pub(super) extra: Option>, } #[derive(Deserialize, Eq, Hash, PartialEq, Debug)] pub enum ToolKey { Tool(String), Modular(modular::ModularWeaponKey), } /// Character data that should be visible when tools are visible (i.e. in third /// person or when the character is in a tool-using state). #[derive(Eq, Hash, PartialEq)] pub(super) struct CharacterToolKey { pub active: Option, pub second: Option, } /// Character data that exists in third person only. #[derive(Eq, Hash, PartialEq)] pub(super) struct CharacterThirdPersonKey { pub head: Option, pub shoulder: Option, pub chest: Option, pub belt: Option, pub back: Option, pub pants: Option, } #[derive(Eq, Hash, PartialEq)] /// NOTE: To avoid spamming the character cache with player models, we try to /// store only the minimum information required to correctly update the model. /// /// TODO: Memoize, etc. pub(super) struct CharacterCacheKey { /// Character state that is only visible in third person. pub third_person: Option, /// Tool state should be present when a character is either in third person, /// or is in first person and the character state is tool-using. /// /// NOTE: This representation could be tightened in various ways to /// eliminate incorrect states, e.g. setting active_tool to None when no /// tools are equipped, but currently we are more focused on the big /// performance impact of recreating the whole model whenever the character /// state changes, so for now we don't bother with this. pub tool: Option, pub lantern: Option, pub glider: Option, pub hand: Option, pub foot: Option, pub head: Option, } impl CharacterCacheKey { fn from(cs: Option<&CharacterState>, camera_mode: CameraMode, inventory: &Inventory) -> Self { let is_first_person = match camera_mode { CameraMode::FirstPerson => true, CameraMode::ThirdPerson | CameraMode::Freefly => false, }; let key_from_slot = |slot| { inventory .equipped(slot) .map(|i| i.item_definition_id()) .map(|id| match id { // TODO: Properly handle items with components here. Probably wait until modular // armor? ItemDefinitionId::Simple(id) => id, ItemDefinitionId::Compound { simple_base, .. } => simple_base, ItemDefinitionId::Modular { pseudo_base, .. } => pseudo_base, }) .map(String::from) }; // Third person tools are only modeled when the camera is either not first // person, or the camera is first person and we are in a tool-using // state. let are_tools_visible = !is_first_person || cs .map(|cs| cs.is_attack() || cs.is_block() || cs.is_wield()) // If there's no provided character state but we're still somehow in first person, // We currently assume there's no need to visually model tools. // // TODO: Figure out what to do here, and/or refactor how this works. .unwrap_or(false); Self { // Third person armor is only modeled when the camera mode is not first person. third_person: if is_first_person { None } else { Some(CharacterThirdPersonKey { head: key_from_slot(EquipSlot::Armor(ArmorSlot::Head)), shoulder: key_from_slot(EquipSlot::Armor(ArmorSlot::Shoulders)), chest: key_from_slot(EquipSlot::Armor(ArmorSlot::Chest)), belt: key_from_slot(EquipSlot::Armor(ArmorSlot::Belt)), back: key_from_slot(EquipSlot::Armor(ArmorSlot::Back)), pants: key_from_slot(EquipSlot::Armor(ArmorSlot::Legs)), }) }, tool: if are_tools_visible { let tool_key_from_item = |item: &Item| match item.item_definition_id() { ItemDefinitionId::Simple(id) => ToolKey::Tool(String::from(id)), ItemDefinitionId::Modular { .. } => { ToolKey::Modular(modular::weapon_to_key(item)) }, ItemDefinitionId::Compound { simple_base, .. } => { ToolKey::Tool(String::from(simple_base)) }, }; Some(CharacterToolKey { active: inventory .equipped(EquipSlot::ActiveMainhand) .map(tool_key_from_item), second: inventory .equipped(EquipSlot::ActiveOffhand) .map(tool_key_from_item), }) } else { None }, lantern: key_from_slot(EquipSlot::Lantern), glider: key_from_slot(EquipSlot::Glider), hand: key_from_slot(EquipSlot::Armor(ArmorSlot::Hands)), foot: key_from_slot(EquipSlot::Armor(ArmorSlot::Feet)), head: key_from_slot(EquipSlot::Armor(ArmorSlot::Head)), } } } pub struct FigureModelCache where Skel: Skeleton, Skel::Body: BodySpec, { models: HashMap, ((FigureModelEntryFuture, Skel::Attr), u64)>, manifests: ::Manifests, watcher: ReloadWatcher, } impl FigureModelCache where Skel::Body: BodySpec + Eq + Hash, { #[allow(clippy::new_without_default)] pub fn new() -> Self { // NOTE: It might be better to bubble this error up rather than panicking. let manifests = ::load_spec().unwrap(); let watcher = ::reload_watcher(&manifests); Self { models: HashMap::new(), manifests, watcher, } } pub fn watcher_reloaded(&mut self) -> bool { self.watcher.reloaded() } /// NOTE: Intended for render time (useful with systems like wgpu that /// expect data used by the rendering pipelines to be stable throughout /// the render pass). /// /// NOTE: Since this is intended to be called primarily in order to render /// the model, we don't return skeleton data. pub fn get_model<'b>( &'b self, // TODO: If we ever convert to using an atlas here, use this. _col_lights: &super::FigureColLights, body: Skel::Body, inventory: Option<&Inventory>, // TODO: Consider updating the tick by putting it in a Cell. _tick: u64, camera_mode: CameraMode, character_state: Option<&CharacterState>, item_key: Option, ) -> FigureModelEntryLod<'b> { // TODO: Use raw entries to avoid lots of allocation (among other things). let key = FigureKey { body, item_key: item_key.map(Arc::new), extra: inventory.map(|inventory| { Arc::new(CharacterCacheKey::from( character_state, camera_mode, inventory, )) }), }; if let Some(((FigureModelEntryFuture::Done(model), _), _)) = self.models.get(&key) { Some(model) } else { None } } #[allow(clippy::too_many_arguments)] pub fn get_or_create_model<'c>( &'c mut self, renderer: &mut Renderer, col_lights: &mut super::FigureColLights, body: Skel::Body, inventory: Option<&Inventory>, extra: ::Extra, tick: u64, camera_mode: CameraMode, character_state: Option<&CharacterState>, slow_jobs: &SlowJobPool, item_key: Option, ) -> (FigureModelEntryLod<'c>, &'c Skel::Attr) where for<'a> &'a Skel::Body: Into, Skel::Body: Clone + Send + Sync + 'static, ::Spec: Send + Sync + 'static, { let skeleton_attr = (&body).into(); let key = FigureKey { body, item_key: item_key.map(Arc::new), extra: inventory.map(|inventory| { Arc::new(CharacterCacheKey::from( character_state, camera_mode, inventory, )) }), }; // TODO: Use raw entries to avoid significant performance overhead. match self.models.entry(key) { Entry::Occupied(o) => { let ((model, skel), last_used) = o.into_mut(); *last_used = tick; ( match model { FigureModelEntryFuture::Pending(recv) => { if let Some(MeshWorkerResponse { col_light, opaque, bounds, vertex_range, }) = Arc::get_mut(recv).take().and_then(|cell| cell.take()) { let model_entry = col_lights.create_figure( renderer, col_light, (opaque, bounds), vertex_range, ); *model = FigureModelEntryFuture::Done(model_entry); // NOTE: Borrow checker isn't smart enough to figure this out. if let FigureModelEntryFuture::Done(model) = model { Some(model) } else { unreachable!(); } } else { None } }, FigureModelEntryFuture::Done(model) => Some(model), }, skel, ) }, Entry::Vacant(v) => { let key = v.key().clone(); let slot = Arc::new(atomic::AtomicCell::new(None)); let manifests = self.manifests.clone(); let slot_ = Arc::clone(&slot); slow_jobs.spawn("FIGURE_MESHING", move || { // First, load all the base vertex data. let meshes = ::bone_meshes(&key, &manifests, extra); // Then, set up meshing context. let mut greedy = FigureModel::make_greedy(); let mut opaque = Mesh::::new(); // Choose the most conservative bounds for any LOD model. let mut figure_bounds = anim::vek::Aabb { min: anim::vek::Vec3::zero(), max: anim::vek::Vec3::zero(), }; // Meshes all bone models for this figure using the given mesh generation // function, attaching it to the current greedy mesher and opaque vertex // list. Returns the vertex bounds of the meshed model within the opaque // mesh. let mut make_model = |generate_mesh: for<'a, 'b> fn( &mut GreedyMesh<'a>, &'b mut _, &'a _, _, _, ) -> _| { let vertex_start = opaque.vertices().len(); meshes .iter() .enumerate() // NOTE: Cast to u8 is safe because i < 16. .filter_map(|(i, bm)| bm.as_ref().map(|bm| (i as u8, bm))) .for_each(|(i, (segment, offset))| { // Generate this mesh. let (_opaque_mesh, bounds) = generate_mesh(&mut greedy, &mut opaque, segment, *offset, i); // Update the figure bounds to the largest granularity seen so far // (NOTE: this is more than a little imperfect). // // FIXME: Maybe use the default bone position in the idle animation // to figure this out instead? figure_bounds.expand_to_contain(bounds); }); // NOTE: vertex_start and vertex_end *should* fit in a u32, by the // following logic: // // Our new figure maximum is constrained to at most 2^8 × 2^8 × 2^8. // This uses at most 24 bits to store every vertex exactly once. // Greedy meshing can store each vertex in up to 3 quads, we have 3 // greedy models, and we store 1.5x the vertex count, so the maximum // total space a model can take up is 3 * 3 * 1.5 * 2^24; rounding // up to 4 * 4 * 2^24 gets us to 2^28, which clearly still fits in a // u32. // // (We could also, though we prefer not to, reason backwards from the // maximum figure texture size of 2^15 × 2^15, also fits in a u32; we // can also see that, since we can have at most one texture entry per // vertex, any texture atlas of size 2^14 × 2^14 or higher should be // able to store data for any figure. So the only reason we would fail // here would be if the user's computer could not store a texture large // enough to fit all the LOD models for the figure, not for fundamental // reasons related to fitting in a u32). // // Therefore, these casts are safe. vertex_start as u32..opaque.vertices().len() as u32 }; fn generate_mesh<'a>( greedy: &mut GreedyMesh<'a>, opaque_mesh: &mut Mesh, segment: &'a Segment, offset: Vec3, bone_idx: u8, ) -> BoneMeshes { let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain( segment, (greedy, opaque_mesh, offset, Vec3::one(), bone_idx), ); (opaque, bounds) } fn generate_mesh_lod_mid<'a>( greedy: &mut GreedyMesh<'a>, opaque_mesh: &mut Mesh, segment: &'a Segment, offset: Vec3, bone_idx: u8, ) -> BoneMeshes { let lod_scale = 0.6; let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain( segment.scaled_by(Vec3::broadcast(lod_scale)), ( greedy, opaque_mesh, offset * lod_scale, Vec3::one() / lod_scale, bone_idx, ), ); (opaque, bounds) } fn generate_mesh_lod_low<'a>( greedy: &mut GreedyMesh<'a>, opaque_mesh: &mut Mesh, segment: &'a Segment, offset: Vec3, bone_idx: u8, ) -> BoneMeshes { let lod_scale = 0.3; let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain( segment.scaled_by(Vec3::broadcast(lod_scale)), ( greedy, opaque_mesh, offset * lod_scale, Vec3::one() / lod_scale, bone_idx, ), ); (opaque, bounds) } let models = [ make_model(generate_mesh), make_model(generate_mesh_lod_mid), make_model(generate_mesh_lod_low), ]; slot_.store(Some(MeshWorkerResponse { col_light: greedy.finalize(), opaque, bounds: figure_bounds, vertex_range: models, })); }); let skel = &(v .insert(((FigureModelEntryFuture::Pending(slot), skeleton_attr), tick)) .0) .1; (None, skel) }, } } pub fn clear_models(&mut self) { self.models.clear(); } pub fn clean(&mut self, col_lights: &mut super::FigureColLights, tick: u64) where ::Spec: Clone, { // TODO: Don't hard-code this. if tick % 60 == 0 { self.models.retain(|_, ((model_entry, _), last_used)| { // Wait about a minute at 60 fps before invalidating old models. let delta = 60 * 60; let alive = *last_used + delta > tick; if !alive { if let FigureModelEntryFuture::Done(model_entry) = model_entry { col_lights.atlas.deallocate(model_entry.allocation.id); } } alive }); } } }