Unravel Engine C++ Reference
Loading...
Searching...
No Matches
model_component.cpp
Go to the documentation of this file.
1#include "model_component.h"
6
7#include <algorithm>
8
9
10namespace unravel
11{
12namespace
13{
14
15auto get_bone_entity(const std::string& bone_id, const std::vector<entt::handle>& entities) -> entt::handle
16{
17 for(const auto& e : entities)
18 {
19 if(e)
20 {
21 const auto& tag = e.get<tag_component>();
22 if(tag.name == bone_id)
23 {
24 return e;
25 }
26 }
27 }
28
29 return {};
30}
31
37struct submesh_accum
38{
39 entt::handle entity;
40 std::vector<uint32_t> indices;
41};
42
43using submesh_accum_list = std::vector<submesh_accum>;
44
55void rebuild_submesh_entries(submesh_component& comp,
56 const std::vector<uint32_t>& node_submeshes,
57 const mesh& render_mesh)
58{
59 const std::vector<submesh_entry> previous_entries = std::move(comp.entries);
60
61 const auto& submeshes = render_mesh.get_submeshes(0);
62
63 comp.entries.clear();
64 comp.entries.reserve(node_submeshes.size());
65 for(uint32_t index : node_submeshes)
66 {
67 submesh_entry entry;
68 entry.submesh_index = index;
69 entry.stable_id = index < submeshes.size() && submeshes[index] != nullptr ? submeshes[index]->stable_id : 0;
70
71 // Preserve authored settings: stable id match wins (survives reimports that
72 // reorder submeshes), then fall back to an index match for legacy data.
73 const submesh_entry* match = nullptr;
74 if(entry.stable_id != 0)
75 {
76 for(const auto& previous : previous_entries)
77 {
78 if(previous.stable_id == entry.stable_id)
79 {
80 match = &previous;
81 break;
82 }
83 }
84 }
85 if(match == nullptr)
86 {
87 for(const auto& previous : previous_entries)
88 {
89 if(previous.stable_id == 0 && previous.submesh_index == index)
90 {
91 match = &previous;
92 break;
93 }
94 }
95 }
96 if(match != nullptr)
97 {
98 entry.material_override = match->material_override;
99 entry.casts_shadow = match->casts_shadow;
100 entry.enabled = match->enabled;
101 }
102
103 comp.entries.emplace_back(std::move(entry));
104 }
105}
106
107auto process_node_impl(const std::unique_ptr<mesh::armature_node>& node,
108 const mesh& render_mesh,
109 entt::handle& parent,
110 std::vector<entt::handle>& nodes,
111 animation_pose& ref_pose,
112 submesh_accum_list& submesh_accums) -> entt::handle
113{
114 const auto& bind_data = render_mesh.get_skin_bind_data();
115 auto entity_node = parent;
116
117 if(entity_node == parent)
118 {
119 auto& parent_trans_comp = parent.get<transform_component>();
120 const auto& children = parent_trans_comp.get_children();
121 auto found_node = get_bone_entity(node->name, children);
122 if(found_node)
123 {
124 entity_node = found_node;
125 }
126 else
127 {
128 auto& reg = *entity_node.registry();
129 entity_node = scene::create_entity(reg, node->name, parent);
130 }
131 auto& trans_comp = entity_node.get<transform_component>();
132 trans_comp.set_transform_local(node->local_transform);
133
134 nodes.emplace_back(entity_node);
135
136 if(!node->submeshes.empty())
137 {
138 // Don't rebuild entries here: several armature nodes can resolve to this same
139 // entity, each contributing its own submesh batch. Accumulate across the whole
140 // walk and rebuild once per entity afterwards (see process_armature), otherwise
141 // the last batch would clobber the earlier ones.
142 auto it = std::find_if(submesh_accums.begin(),
143 submesh_accums.end(),
144 [&](const submesh_accum& accum) -> bool
145 {
146 return accum.entity == entity_node;
147 });
148 if(it == submesh_accums.end())
149 {
150 submesh_accums.push_back({entity_node, node->submeshes});
151 }
152 else
153 {
154 it->indices.insert(it->indices.end(), node->submeshes.begin(), node->submeshes.end());
155 }
156 }
157
158 auto query = bind_data.find_bone_by_id(node->name);
159 if(query.bone && query.index >= 0)
160 {
161 auto& comp = entity_node.get_or_emplace<bone_component>();
162 comp.bone_index = query.index;
163 }
164
165 // Instead of storing anything in a bone_component,
166 // immediately add this node to the reference pose.
167 animation_pose::node ref_node;
168 ref_node.desc.index = node->index; // Use the node's index
169 ref_node.transform = node->local_transform;
170 ref_pose.nodes.push_back(ref_node);
171 }
172
173 return entity_node;
174}
175
176void process_node(const std::unique_ptr<mesh::armature_node>& node,
177 const mesh& render_mesh,
178 entt::handle parent,
179 std::vector<entt::handle>& nodes,
180 animation_pose& ref_pose,
181 submesh_accum_list& submesh_accums)
182{
183 if(!parent)
184 {
185 return;
186 }
187
188 auto entity_node = process_node_impl(node, render_mesh, parent, nodes, ref_pose, submesh_accums);
189 for(auto& child : node->children)
190 {
191 process_node(child, render_mesh, entity_node, nodes, ref_pose, submesh_accums);
192 }
193}
194
195auto process_armature(const mesh& render_mesh,
196 entt::handle parent,
197 std::vector<entt::handle>& nodes,
198 animation_pose& ref_pose) -> bool
199{
200 const auto& root = render_mesh.get_armature();
201 if(!root)
202 {
203 return false;
204 }
205
206 submesh_accum_list submesh_accums;
207 process_node(root, render_mesh, parent, nodes, ref_pose, submesh_accums);
208
209 // Apply the accumulated per-entity submesh sets in one pass so entries reflect the
210 // union of every armature node that maps to the entity (authored settings are
211 // preserved inside rebuild_submesh_entries via stable-id/index matching).
212 for(auto& accum : submesh_accums)
213 {
214 auto& comp = accum.entity.get_or_emplace<submesh_component>();
215 rebuild_submesh_entries(comp, accum.indices, render_mesh);
216 }
217
218 return true;
219}
220
223constexpr uint8_t pose_dirty_id = transform_component::dirty_ids::model_pose;
224
236auto get_transforms_for_entities(const std::vector<entt::handle>& entities,
237 const mesh& render_mesh,
238 submesh_pose_mat4& submesh_pose,
239 pose_mat4& bone_pose,
240 submesh_render_proxies& proxies,
241 std::vector<material::sptr>& material_overrides,
242 bool force) -> bool
243{
244 // Reused per pool-thread; update_armature runs one task per model with no interleaving.
245 thread_local std::vector<transform_component*> transform_scratch;
246 transform_scratch.clear();
247 transform_scratch.reserve(entities.size());
248
249 bool any_dirty = force;
250 for(const auto& e : entities)
251 {
252 auto* transform_comp = e.try_get<transform_component>();
253 transform_scratch.push_back(transform_comp);
254 any_dirty |= transform_comp != nullptr && transform_comp->is_dirty(pose_dirty_id);
255 }
256
257 if(!any_dirty)
258 {
259 return false;
260 }
261
262 const size_t submesh_count = render_mesh.get_submeshes_count(0);
263 const size_t bone_count = render_mesh.get_skin_bind_data().get_bones().size();
264 const auto& submeshes = render_mesh.get_submeshes(0);
265
266 submesh_pose.clear();
267 submesh_pose.reserve(submesh_count);
268 bone_pose.transforms.resize(bone_count);
269 proxies.begin_refresh(submesh_count);
270 material_overrides.assign(submesh_count, nullptr);
271
272 for(size_t i = 0; i < entities.size(); ++i)
273 {
274 auto* transform_comp = transform_scratch[i];
275 if(transform_comp == nullptr)
276 {
277 continue;
278 }
279
280 const auto e = entities[i];
281 auto&& [submesh_comp, bone_comp, active_comp] =
282 e.try_get<submesh_component, bone_component, active_component>();
283
284 const auto& transform_global = transform_comp->get_transform_global();
285 const auto& transform_matrix = transform_global.get_matrix();
286
287 // The pose has consumed this node's transform; clear our dirty slot so
288 // the next update_armature can skip when nothing changes again.
289 transform_comp->set_dirty(pose_dirty_id, false);
290
291 if(submesh_comp && !submesh_comp->entries.empty())
292 {
293 const bool node_active = active_comp != nullptr;
294
295 // Add the transform once; each submesh entry maps to it with its
296 // own per-instance flags.
297 const uint32_t trans_index = submesh_pose.add_transform(transform_matrix);
298
299 for(const auto& entry : submesh_comp->entries)
300 {
301 const uint32_t submesh_index = entry.submesh_index;
302 submesh_pose.map_submesh(submesh_index,
303 trans_index,
304 node_active && entry.enabled,
305 entry.casts_shadow);
306
307 // Cache the world-space bounds for this instance so culling
308 // and per-submesh LOD become cheap AABB tests. Alignment with
309 // the pose instance list is maintained by always pushing a
310 // bounds record (possibly unpopulated) per mapped instance.
311 //
312 // Skinned submeshes are excluded: their geometry lives in mesh
313 // bind space and is driven by bone palettes, so the owning
314 // node's transform is meaningless for bounds. Their world
315 // bounds come from skinned_bounds in update_armature instead;
316 // an unpopulated record keeps the instance list aligned.
317 math::bbox world_bounds{};
318 const auto* sm = submesh_index < submeshes.size() ? submeshes[submesh_index] : nullptr;
319 if(sm != nullptr && !sm->skinned && sm->bbox.is_populated())
320 {
321 world_bounds = math::bbox::mul(sm->bbox, transform_global);
322 }
323 proxies.add_instance_bounds(submesh_index, world_bounds);
324
325 if(submesh_index < material_overrides.size() && entry.material_override.is_valid())
326 {
327 material_overrides[submesh_index] = entry.material_override.get();
328 }
329 }
330 }
331
332 if(bone_comp)
333 {
334 auto bone_index = bone_comp->bone_index;
335 if(bone_index < bone_pose.transforms.size())
336 {
337 bone_pose.transforms[bone_index] = transform_matrix;
338 }
339 }
340 }
341
342 return true;
343}
344
345} // namespace
346
347auto model_component::create_armature(bool force) -> bool
348{
349 bool has_processed_armature = !get_armature_entities().empty();
350
351 if(force || !has_processed_armature)
352 {
353 auto lod = model_.get_lod(0);
354 if(!lod)
355 {
356 return false;
357 }
358 auto mesh = lod.get();
359
360 auto owner = get_owner();
361
362 std::vector<entt::handle> armature_entities;
363 if(process_armature(*mesh, owner, armature_entities, bind_pose_))
364 {
365 set_armature_entities(armature_entities);
366
367 const auto& skin_data = mesh->get_skin_bind_data();
368 // Has skinning data?
369 if(skin_data.has_bones())
370 {
371 set_static(false);
372 }
373
374 return true;
375 }
376 }
377
378 return false;
379}
380
382{
383 // APPLOG_TRACE_PERF_NAMED(std::chrono::microseconds, "Model/Update Armature");
384
385 // Visibility gate FIRST - before even touching the mesh asset handle. When no view
386 // (camera or shadow pass) consumed this model recently and conservative culling bounds
387 // exist, skip everything: no asset access, no dirty scan, no refresh. Correctness does
388 // NOT depend on this gate being right: update_world_bounds then uses the grow-only
389 // culling bounds anchored to the root bone, which track bone-driven root motion and
390 // never depend on fresh proxies. If those bounds enter a frustum the model is drawn
391 // (conservatively, last cached pose) and marked used, un-gating the refresh next
392 // frame. Explicit forces (pose_dirty_: set_model, armature rebuilds, inspector edits)
393 // bypass the gate since they can change what the bounds should be.
394 if(!pose_dirty_ && !was_used_last_frame() && culling_bounds_local_.is_populated())
395 {
396 // Cached per-submesh proxies may no longer match the real pose; submit paths must
397 // fall back to conservative behavior (draw) until the next full refresh.
398 render_proxies_stale_ = true;
399 return false;
400 }
401
402 auto lod = model_.get_lod(0);
403 if(!lod)
404 {
405 return false;
406 }
407
408 auto mesh = lod.get();
409
410 const auto& armature_entities = get_armature_entities();
411 const auto& skin_data = mesh->get_skin_bind_data();
412
413 // Change-driven refresh in ONE walk: the dirty check and the rebuild share the same
414 // pass over the armature entities (the transform lookup is done once and reused).
415 // When nothing changed and nothing forced a refresh, outputs are left untouched and
416 // we early-out - visible idle models cost a single pointer+bit scan.
417 const bool refreshed = get_transforms_for_entities(armature_entities,
418 *mesh,
419 submesh_pose_,
420 bone_pose_,
421 render_proxies_,
422 submesh_material_overrides_,
423 pose_dirty_);
424 if(!refreshed)
425 {
426 // Nothing changed since the last full refresh, so proxies flagged stale while
427 // off-screen turned out to be valid after all.
428 render_proxies_stale_ = false;
429 return false;
430 }
431 pose_dirty_ = false;
432 render_proxies_stale_ = false;
433
434 // Has skinning data?
435 if(skin_data.has_bones())
436 {
437 const auto& palettes = mesh->get_bone_palettes();
438 const size_t palette_count = palettes.size();
439
440 // Early exit if no palettes
441 if(palette_count == 0)
442 {
443 return true;
444 }
445
446 skinning_pose_.resize(palette_count);
447
448 // Cache bone transforms reference to avoid repeated lookups
449 const auto& bone_transforms = bone_pose_.transforms;
450 const auto& bones = skin_data.get_bones();
451
452 // Animated world-space bounds per skinned submesh: union of each palette bone's
453 // bind-space bounds transformed by its current world transform. Any vertex skinned
454 // by the palette is a convex combination of per-bone transformed points, so the
455 // enclosing AABB of all bone boxes is a valid conservative bound.
456 render_proxies_.skinned_bounds.assign(mesh->get_submeshes_count(0), math::bbox{});
457 const auto& submeshes = mesh->get_submeshes(0);
458
459 for(size_t i = 0; i < palette_count; ++i)
460 {
461 const auto& palette = palettes[i];
462 // Apply the bone palette.
463 skinning_pose_[i].transforms = palette.get_skinning_matrices(bone_transforms, skin_data);
464
465 // Palettes map 1:1 to submeshes (bind_skin creates one per submesh, in order);
466 // only skinned submeshes carry meaningful palette bones/bounds.
467 if(i < submeshes.size() && submeshes[i] != nullptr && !submeshes[i]->skinned)
468 {
469 continue;
470 }
471
472 math::bbox submesh_bounds{};
473 for(uint32_t bone_index : palette.get_bones())
474 {
475 // Unpopulated bounds mean the bone has no weighted vertex influences (e.g.
476 // assimp's zero-weight root joint entry) - it deforms nothing, so skipping
477 // it is exact, not merely conservative. On legacy assets without per-bone
478 // bounds every bone is skipped and the bounds simply stay unpopulated,
479 // which consumers already treat as "no cached data" (draw conservatively).
480 if(bone_index >= bones.size() || bone_index >= bone_transforms.size() ||
481 !bones[bone_index].bounds.is_populated())
482 {
483 continue;
484 }
485
486 const auto bone_world_bounds =
487 math::bbox::mul(bones[bone_index].bounds, math::transform(bone_transforms[bone_index]));
488 submesh_bounds.add_point(bone_world_bounds.min);
489 submesh_bounds.add_point(bone_world_bounds.max);
490 }
491
492 if(submesh_bounds.is_populated() && i < render_proxies_.skinned_bounds.size())
493 {
494 render_proxies_.skinned_bounds[i] = submesh_bounds;
495 render_proxies_.animated_bounds.add_point(submesh_bounds.min);
496 render_proxies_.animated_bounds.add_point(submesh_bounds.max);
497 }
498 }
499 }
500
501 return true;
502}
503
505{
506 auto lod = model_.get_lod(0);
507 if(!lod)
508 {
509 return false;
510 }
511
512 auto mesh = lod.get();
513 const auto& skin_data = mesh->get_skin_bind_data();
514 const auto& armature = mesh->get_armature();
515
516 bool recreate_armature = force;
517 recreate_armature |= armature && submesh_pose_.submesh_to_transform_indices.empty();
518 recreate_armature |= skin_data.has_bones() && skinning_pose_.empty();
519
520 if(recreate_armature)
521 {
522 if(create_armature(force))
523 {
524 return update_armature();
525 }
526 }
527
528 return false;
529}
530
532{
533 auto lod = model_.get_lod(0);
534 if(!lod)
535 {
536 return;
537 }
538
539 auto mesh = lod.get();
540 if(!mesh)
541 {
542 return;
543 }
544
545 world_bounds_transform_ = world_transform;
546
547 // Anchor transform for the conservative culling bounds. Root motion baked into bone
548 // animation moves the topmost bone, not the owner, so anchoring to it keeps the
549 // conservative box tracking the character even while the pose refresh is skipped
550 // (Unity's rootBone). Boneless armatures fall back to the owner transform.
551 math::transform anchor_transform = world_transform;
552 if(bounds_anchor_)
553 {
554 if(const auto* anchor_transform_comp = bounds_anchor_.try_get<transform_component>())
555 {
556 anchor_transform = anchor_transform_comp->get_transform_global();
557 }
558 }
559
560 const auto is_invertible = [](const math::transform& t) -> bool
561 {
562 constexpr float min_scale = 0.000001f;
563 const auto scale = t.get_scale();
564 return std::abs(scale.x) > min_scale && std::abs(scale.y) > min_scale && std::abs(scale.z) > min_scale;
565 };
566
567 // Fresh proxies this frame: rebuild the pose bounds union in world space.
568 // Replace the bind-pose bounds with the cached pose bounds so whole-model culling
569 // tracks the actual pose instead of the import-time rest pose (compiled mesh bounds
570 // are bind-pose only; animation-driven expansion happens exclusively here at runtime):
571 // - per-instance bounds cover node-attached rigid submeshes driven by node animation,
572 // - animated bounds cover skinned geometry (bone-transformed bind-space boxes).
573 // A model can have both kinds at once, so the result is the UNION of the two;
574 // dropping either would cull geometry that is actually on screen.
575 if(render_proxies_.version != captured_proxies_version_)
576 {
577 const bool has_instance = render_proxies_.has_instance_bounds();
578 const bool has_animated = render_proxies_.has_animated_bounds();
579 if(has_instance || has_animated)
580 {
581 // If the mesh has skinned geometry but no animated bounds could be derived
582 // (legacy asset without per-bone bounds), the pose bounds don't cover the
583 // skinned parts - keep the bind-pose box unioned in as a conservative
584 // fallback instead of culling them away.
585 const bool skinned_uncovered = !has_animated && mesh->get_skinned_submeshes_count(0) > 0;
586 world_bounds_ = skinned_uncovered ? math::bbox::mul(mesh->get_bounds(), world_transform) : math::bbox{};
587 if(has_instance)
588 {
589 world_bounds_.add_point(render_proxies_.instance_bounds_union.min);
590 world_bounds_.add_point(render_proxies_.instance_bounds_union.max);
591 }
592 if(has_animated)
593 {
594 world_bounds_.add_point(render_proxies_.animated_bounds.min);
595 world_bounds_.add_point(render_proxies_.animated_bounds.max);
596 }
597
598 // Capture two boxes from the tight world bounds (degenerate scales cannot be
599 // inverted - keep the previous captures in that case):
600 // - pose_local_bounds_ (owner space): tight, re-anchored on visible idle frames.
601 // - culling_bounds_local_ (anchor space): grow-only union of every observed
602 // pose, used whenever the refresh is skipped. Culling therefore never
603 // depends on fresh proxies and cannot deadlock a model into invisibility.
604 if(is_invertible(world_transform) && is_invertible(anchor_transform))
605 {
606 pose_local_bounds_ = math::bbox::mul(world_bounds_, math::inverse(world_transform));
607
608 const auto anchor_local = math::bbox::mul(world_bounds_, math::inverse(anchor_transform));
609 culling_bounds_local_.add_point(anchor_local.min);
610 culling_bounds_local_.add_point(anchor_local.max);
611
612 captured_proxies_version_ = render_proxies_.version;
613 }
614 }
615 else
616 {
617 world_bounds_ = math::bbox::mul(mesh->get_bounds(), world_transform);
618 }
619 return;
620 }
621
622 // Refresh skipped by the visibility gate: the cached pose (and thus the tight snapshot)
623 // may be stale. Use the conservative grow-only bounds re-anchored to the root bone's
624 // CURRENT transform - one transform read + one bbox transform - so a model whose
625 // animation carries it toward the frustum is re-discovered and re-enters rendering.
626 if(render_proxies_stale_ && culling_bounds_local_.is_populated())
627 {
628 world_bounds_ = math::bbox::mul(culling_bounds_local_, anchor_transform);
629 return;
630 }
631
632 // Proxies valid, simply nothing moved (visible idle model): re-anchor the tight
633 // pose-local bounds to the current owner transform so culling stays tight while
634 // game logic moves the whole character.
635 if(pose_local_bounds_.is_populated())
636 {
637 world_bounds_ = math::bbox::mul(pose_local_bounds_, world_transform);
638 return;
639 }
640
641 // No pose bounds at all (plain mesh without armature-driven placements): bind-pose box.
642 world_bounds_ = math::bbox::mul(mesh->get_bounds(), world_transform);
643}
644
645auto model_component::get_world_bounds() const -> const math::bbox&
646{
647 return world_bounds_;
648}
649
650auto model_component::get_world_bounds_transform() const -> const math::transform&
651{
652 return world_bounds_transform_;
653}
654
655auto model_component::get_local_bounds(uint32_t lod_index) const -> const math::bbox&
656{
657 auto lod = model_.get_lod(lod_index);
658 if(!lod)
659 {
660 return math::bbox::empty;
661 }
662
663 auto mesh = lod.get();
664 if(mesh)
665 {
666 return mesh->get_bounds();
667 }
668
669 return math::bbox::empty;
670}
671
673{
674 last_render_frame_ = frame;
675}
676
677auto model_component::get_last_render_frame() const noexcept -> uint64_t
678{
679 return last_render_frame_;
680}
681
682auto model_component::is_newly_created() const noexcept -> bool
683{
684 return last_render_frame_ == 0;
685}
686
687auto model_component::was_used_last_frame() const noexcept -> bool
688{
689 auto current_frame = gfx::get_render_frame();
690 bool is_new = is_newly_created();
691 bool was_used_recently = current_frame - last_render_frame_ <= 1;
692 return is_new || was_used_recently;
693}
694
695auto model_component::is_skinned() const -> bool
696{
697 auto lod = model_.get_lod(0);
698 if(!lod)
699 {
700 return false;
701 }
702
703 auto mesh = lod.get();
704 if(mesh)
705 {
706 return mesh->get_skinned_submeshes_count() > 0;
707 }
708
709 return false;
710}
711
713{
714 return bind_pose_;
715}
716
718{
719 auto lod = model_.get_lod(0);
720 if(!lod)
721 {
722 return {};
723 }
724
725 const auto mesh = lod.get();
726 const auto& armature = mesh->get_armature();
727 if(!armature)
728 {
729 return {};
730 }
731
732 for(const auto& entity : armature_entities_)
733 {
734 if(entity && entity.get<tag_component>().name == armature->name)
735 {
736 return entity;
737 }
738 }
739
740 if(!armature_entities_.empty())
741 {
742 return armature_entities_.front();
743 }
744
745 return {};
746}
747
749{
750 if(auto root = get_armature_root_entity())
751 {
752 return root.get<transform_component>().get_rotation_local();
753 }
754
755 if(!bind_pose_.nodes.empty())
756 {
757 return bind_pose_.nodes.front().transform.get_rotation();
758 }
759
760 return math::identity<math::quat>();
761}
762
763void model_component::on_create_component(entt::registry& r, entt::entity e)
764{
765 entt::handle entity(r, e);
766
767 auto& component = entity.get<model_component>();
768 component.set_owner(entity);
769
770 component.set_armature_entities({});
771}
772
773void model_component::on_destroy_component(entt::registry& r, entt::entity e)
774{
775}
776
778{
779 if(enabled_ == enabled)
780 {
781 return;
782 }
783
784 touch();
785
786 enabled_ = enabled;
787}
788
790{
791 if(casts_shadow_ == cast_shadow)
792 {
793 return;
794 }
795
796 touch();
797
798 casts_shadow_ = cast_shadow;
799}
800
801void model_component::set_static(bool is_static)
802{
803 if(static_ == is_static)
804 {
805 return;
806 }
807
808 touch();
809
810 static_ = is_static;
811}
812
813auto model_component::is_enabled() const -> bool
814{
815 return enabled_;
816}
817
819{
820 return casts_shadow_;
821}
822
823auto model_component::is_static() const -> bool
824{
825 return static_;
826}
827
828auto model_component::get_model() const -> const model&
829{
830 return model_;
831}
832
834{
835 model_ = model;
836
837 // Different mesh asset - poses/proxies and captured bounds derived from the old one
838 // are invalid.
839 pose_local_bounds_ = {};
840 culling_bounds_local_ = {};
841 captured_proxies_version_ = ~0ULL;
843
844 touch();
845}
846
848{
849 return bone_pose_;
850}
851
853{
854 return skinning_pose_;
855}
856
858{
859 return submesh_pose_;
860}
861
863{
864 return render_proxies_;
865}
866
868{
869 return submesh_material_overrides_;
870}
871
873{
874 model_submit_extras extras;
875 // Proxies flagged stale (pose refresh skipped while off-screen and transforms may have
876 // changed) are withheld: submit paths treat missing cached bounds as "draw
877 // conservatively", which only costs extra draws on the re-entry frame. The next
878 // update_armature runs a full refresh and clears the flag.
879 extras.proxies = render_proxies_stale_ ? nullptr : &render_proxies_;
880 extras.material_overrides = &submesh_material_overrides_;
881 extras.shadow_pass = shadow_pass;
882 return extras;
883}
884
885void model_component::set_armature_entities(const std::vector<entt::handle>& entities)
886{
887 armature_entities_ = entities;
888 rebuild_armature_cache();
889
890 // Culling-bounds anchor: the topmost bone (entities are in hierarchy order, parents
891 // first), so root motion baked into bone animation moves the conservative culling box
892 // with the character. Null (-> owner transform) when the armature has no bones.
893 bounds_anchor_ = {};
894 for(const auto& e : armature_entities_)
895 {
896 if(e && e.any_of<bone_component>())
897 {
898 bounds_anchor_ = e;
899 break;
900 }
901 }
902
903 // The entity set changed - cached poses/proxies and captured bounds no longer match it.
904 pose_local_bounds_ = {};
905 culling_bounds_local_ = {};
906 captured_proxies_version_ = ~0ULL;
908
909 touch();
910}
911
913{
914 pose_dirty_ = true;
915}
916
917void model_component::rebuild_armature_cache()
918{
919 armature_name_to_index_.clear();
920 armature_name_to_index_.reserve(armature_entities_.size());
921
922 for(size_t i = 0; i < armature_entities_.size(); ++i)
923 {
924 const auto& e = armature_entities_[i];
925 if(e)
926 {
927 const auto& tag_comp = e.get<tag_component>();
928 armature_name_to_index_[tag_comp.name] = i;
929 }
930 }
931}
932
933auto model_component::get_armature_index_by_name_cached(const std::string& node_name) const -> int
934{
935 auto it = armature_name_to_index_.find(node_name);
936 if(it != armature_name_to_index_.end())
937 {
938 return static_cast<int>(it->second);
939 }
940 return -1;
941}
942
944{
945 return armature_entities_;
946}
947
948auto model_component::get_armature_by_index(size_t index) const -> entt::handle
949{
950 if(index >= armature_entities_.size())
951 {
952 return {};
953 }
954
955 return armature_entities_[index];
956}
957
958auto model_component::get_lod_data_for_camera(const camera* cam, uint64_t current_frame) -> lod_data&
959{
960 if(!cam)
961 {
962 static thread_local lod_data empty_lod_data;
963 return empty_lod_data;
964 }
965 const auto unique_id = reinterpret_cast<uintptr_t>(cam);
966 // Get or create entry for this view
967 auto& camera_state = per_camera_lod_data_[unique_id];
968
969 // Update last access frame
970 camera_state.last_access_frame = current_frame;
971
972 return camera_state.data;
973}
974
975void model_component::cleanup_stale_lod_data(uint64_t current_frame, uint64_t max_frames_inactive)
976{
977 // Remove entries that haven't been accessed recently
978 for(auto it = per_camera_lod_data_.begin(); it != per_camera_lod_data_.end();)
979 {
980 const auto frames_since_access = current_frame - it->second.last_access_frame;
981 if(frames_since_access > max_frames_inactive)
982 {
983 it = per_camera_lod_data_.erase(it);
984 }
985 else
986 {
987 ++it;
988 }
989 }
990}
991
992} // namespace unravel
General purpose transformation class designed to maintain each component of the transformation separa...
Definition transform.hpp:27
Class representing a camera. Contains functionality for manipulating and updating a camera....
Definition camera.h:62
Base class for materials used in rendering.
Definition material.h:44
Main class representing a 3D mesh with support for different LODs, submeshes, and skinning.
Definition mesh.h:323
auto get_bone_palettes() const -> const bone_palette_array_t &
Retrieves the compiled bone combination palette data if this mesh has been bound as a skin.
Definition mesh.cpp:1637
auto get_bounds() const -> const math::bbox &
Gets the local bounding box for this mesh.
Definition mesh.cpp:2730
auto get_submeshes_count(uint32_t lod_index=0) const -> size_t
Gets the number of submeshes for this mesh.
Definition mesh.cpp:1880
auto get_skinned_submeshes_count(uint32_t lod_index=0) const -> size_t
Gets the number of skinned submeshes for this mesh.
Definition mesh.cpp:2644
auto get_armature() const -> const std::unique_ptr< armature_node > &
Retrieves the armature tree of the mesh.
Definition mesh.cpp:1642
auto get_submeshes(uint32_t lod_index=0) const -> const submesh_array_t &
Retrieves information about the submesh of the mesh associated with the specified data group identifi...
Definition mesh.cpp:1864
auto get_skin_bind_data() const -> const skin_bind_data &
Retrieves the skin bind data if this mesh has been bound as a skin.
Definition mesh.cpp:1632
Class that contains core data for meshes.
static void on_create_component(entt::registry &r, entt::entity e)
Called when the component is created.
auto get_facing_adjustment_rotation() const -> math::quat
Local rotation of the armature root (used for root motion / IK remapping).
auto was_used_last_frame() const noexcept -> bool
auto get_submesh_material_overrides() const -> const std::vector< material::sptr > &
Per-submesh material overrides (indexed by submesh index; null = model material), resolved from the s...
auto get_armature_by_index(size_t index) const -> entt::handle
auto get_last_render_frame() const noexcept -> uint64_t
auto get_submesh_transforms() const -> const submesh_pose_mat4 &
Gets the submesh transforms.
auto get_bone_transforms() const -> const pose_mat4 &
Gets the bone transforms.
void set_enabled(bool enabled)
Sets whether the model is enabled.
auto get_submit_extras(bool shadow_pass=false) const -> model_submit_extras
Convenience: builds the submit extras referencing the retained proxy data.
auto is_static() const -> bool
Checks if the model is static.
auto get_lod_data_for_camera(const camera *cam, uint64_t current_frame) -> lod_data &
Gets the per-view LOD data for a specific camera/view. Creates a new entry if this is the first acces...
auto is_newly_created() const noexcept -> bool
auto get_bind_pose() const -> const animation_pose &
auto casts_shadow() const -> bool
Checks if the model casts shadows.
auto init_armature(bool force) -> bool
Updates the armature of the model.
static void on_destroy_component(entt::registry &r, entt::entity e)
Called when the component is destroyed.
auto get_model() const -> const model &
Gets the model.
void cleanup_stale_lod_data(uint64_t current_frame, uint64_t max_frames_inactive=120)
Cleans up stale per-view LOD data entries that haven't been accessed recently. Call this periodically...
auto get_armature_index_by_name_cached(const std::string &node_name) const -> int
Gets armature index by name using cached lookup (O(1)).
auto is_enabled() const -> bool
Checks if the model is enabled.
void mark_pose_dirty() noexcept
Forces the next update_armature call to run a full refresh even when no armature transform is dirty (...
auto get_render_proxies() const -> const submesh_render_proxies &
Retained render proxies: cached world-space per-submesh bounds (including animated bounds for skinned...
auto is_skinned() const -> bool
auto get_skinning_transforms() const -> const std::vector< pose_mat4 > &
void set_model(const model &model)
Sets the model.
auto get_world_bounds_transform() const -> const math::transform &
auto get_local_bounds(uint32_t lod_index) const -> const math::bbox &
Gets the bind-pose local bounding box of the mesh asset for a given LOD.
auto get_armature_root_entity() const -> entt::handle
Armature root entity (first node of the imported skeleton hierarchy).
auto get_armature_entities() const -> const std::vector< entt::handle > &
Gets the armature entities.
void set_static(bool is_static)
Sets whether the model is static.
auto get_world_bounds() const -> const math::bbox &
Gets the pose-aware world-space bounding box for this model.
void update_world_bounds(const math::transform &world_transform)
void set_armature_entities(const std::vector< entt::handle > &submesh_entities)
Sets the armature entities.
auto update_armature() -> bool
Refreshes pose-derived render data: submesh/bone poses, cached world-space render-proxy bounds,...
void set_last_render_frame(uint64_t frame)
void set_casts_shadow(bool cast_shadow)
Sets whether the model casts shadows.
Structure describing a LOD group (set of meshes), LOD transitions, and their materials.
Definition model.h:275
auto get_lod(uint32_t lod) const -> asset_handle< mesh >
Gets the LOD (Level of Detail) mesh for the specified level.
Definition model.cpp:179
void set_owner(entt::handle owner)
Sets the owner of the component.
Component that handles transformations (position, rotation, scale, etc.) in the ACE framework.
uint16_t index
std::vector< render_pass_node > children
uint32_t frame
Definition graphics.cpp:23
std::string tag
Definition hub.cpp:32
uint32_t get_render_frame()
Definition bbox.cpp:5
auto inverse(transform_t< T, Q > const &t) noexcept -> transform_t< T, Q >
Hash specialization for batch_key to enable use in std::unordered_map.
std::vector< float > scale
entt::handle entity
std::vector< uint32_t > indices
Storage for box vector values and wraps up common functionality.
Definition bbox.h:21
bbox & add_point(const vec3 &point)
Grows the bounding box based on the point passed.
Definition bbox.cpp:924
vec3 max
The maximum vector value of the bounding box.
Definition bbox.h:311
bbox & mul(const transform &t)
Transforms an axis aligned bounding box by the specified matrix.
Definition bbox.cpp:876
vec3 min
The minimum vector value of the bounding box.
Definition bbox.h:306
static bbox empty
An empty bounding box.
Definition bbox.h:316
bool is_populated() const
Checks if the bounding box is populated.
Definition bbox.cpp:36
std::vector< node > nodes
void touch()
Marks the component as 'touched'.
Contains level of detail (LOD) data for an entity per view. Uses distance-based hysteresis for stable...
Definition model.h:34
Optional retained render data consumed by the model submit paths.
Definition model.h:254
const submesh_render_proxies * proxies
Definition model.h:260
const std::vector< material::sptr > * material_overrides
Definition model.h:264
auto create_entity(const std::string &tag={}, entt::handle parent={}) -> entt::handle
Creates an entity in the scene with an optional tag and parent.
Definition scene.cpp:360
Retained per-model render proxy data used by the culling/LOD code.
auto has_instance_bounds() const -> bool
auto has_animated_bounds() const -> bool
uint64_t version
Increments on every refresh so consumers can detect staleness.
Component that provides a tag (name or label) for an entity.
std::string name
The name of the entity.
gfx::uniform_handle handle
Definition uniform.cpp:9
std::string owner
bool enabled