Unravel Engine C++ Reference
Loading...
Searching...
No Matches
model.cpp
Go to the documentation of this file.
1#include "model.h"
2#include "gpu_program.h"
3#include "graphics/graphics.h"
6#include "material.h"
7#include "mesh.h"
8#include "camera.h"
9#include "batch_collector.h"
10
11#include <algorithm>
12#include <cmath>
13
14namespace unravel
15{
16
17namespace
18{
19
24auto get_cached_submesh_bounds(const model_submit_extras& extras,
25 uint32_t submesh_index,
26 size_t instance_index,
27 bool skinned) -> const math::bbox*
28{
29 if(extras.proxies == nullptr)
30 {
31 return nullptr;
32 }
33 return skinned ? extras.proxies->get_skinned_bounds(submesh_index)
34 : extras.proxies->get_instance_bounds(submesh_index, instance_index);
35}
36
43auto classify_submesh_cached(const math::frustum& frustum,
44 const model_submit_extras& extras,
45 uint32_t submesh_index,
46 size_t instance_index,
47 bool skinned) -> math::volume_query
48{
49 const auto* bounds = get_cached_submesh_bounds(extras, submesh_index, instance_index, skinned);
50 if(bounds != nullptr)
51 {
52 return frustum.classify_aabb(*bounds);
53 }
54 // No cached data - conservatively treat as visible.
56}
57
58auto is_submesh_visible_cached(const math::frustum& frustum,
59 const model_submit_extras& extras,
60 uint32_t submesh_index,
61 size_t instance_index,
62 bool skinned) -> bool
63{
64 return classify_submesh_cached(frustum, extras, submesh_index, instance_index, skinned) !=
66}
67
72auto resolve_submesh_material(const model_submit_extras& extras,
73 uint32_t submesh_index,
74 const material::sptr& group_material) -> const material::sptr&
75{
76 if(extras.material_overrides != nullptr && submesh_index < extras.material_overrides->size())
77 {
78 const auto& override_material = (*extras.material_overrides)[submesh_index];
79 if(override_material)
80 {
81 return override_material;
82 }
83 }
84 return group_material;
85}
86
87auto compute_bounds_screen_radius_squared(const math::vec3& origin,
88 float radius,
89 const math::vec3& view_origin,
90 const math::mat4& projection) -> float
91{
92 const float screen_multiple = 0.5f * std::max(std::abs(projection[0][0]), std::abs(projection[1][1]));
93 float projection_w_scale = std::abs(projection[2][3]);
94 if(projection_w_scale < 0.000001f)
95 {
96 projection_w_scale = 1.0f;
97 }
98 const float dist_sqr = glm::length2(origin - view_origin) * projection_w_scale;
99 // Guard against division by zero only. Clamping to 1.0 froze the projected size of anything closer than
100 // 1 world unit (= 1m here), making close-up geometry - especially small individual
101 // submeshes - report a tiny screen radius and drop to coarser LODs while filling
102 // the screen.
103 constexpr float min_dist_sqr = 0.0001f; // (1cm)^2
104 return math::square(screen_multiple * radius) / std::max(min_dist_sqr, dist_sqr);
105}
106
107auto compute_bounds_screen_radius_squared(const math::vec3& origin, float radius, const camera& view) -> float
108{
109 return compute_bounds_screen_radius_squared(origin, radius, view.get_position(), view.get_projection().get_matrix());
110}
111
112auto compute_submesh_world_bounds_sphere(const mesh::submesh& sm, const math::mat4& world_matrix) -> math::bsphere
113{
114 const math::transform world_transform(world_matrix);
115 const math::vec3 local_center = sm.bbox.get_center();
116 const math::vec3 local_extents = sm.bbox.get_extents();
117 const float local_radius = glm::length(local_extents);
118 const math::vec3 world_center = world_transform.transform_coord(local_center);
119 const auto scale = world_transform.get_scale();
120 const float max_scale = std::max({std::abs(scale.x), std::abs(scale.y), std::abs(scale.z)});
121 return math::bsphere{world_center, local_radius * max_scale};
122}
123
124auto compute_screen_rect_from_sphere(const camera& cam, const math::vec3& world_center, float screen_radius) -> irect32_t
125{
126 const auto& viewport_pos = cam.get_viewport_pos();
127 const auto& viewport_size = cam.get_viewport_size();
128 if(viewport_size.width == 0 || viewport_size.height == 0)
129 {
130 return {};
131 }
132 const auto view_proj = cam.get_view_projection();
133 math::vec4 clip = view_proj * math::vec4{world_center.x, world_center.y, world_center.z, 1.0f};
134 const float clip_w = clip.w;
135 if(std::abs(clip_w) < 0.000001f)
136 {
137 return {viewport_pos.x,
138 viewport_pos.y,
139 viewport_pos.x + static_cast<std::int32_t>(viewport_size.width),
140 viewport_pos.y + static_cast<std::int32_t>(viewport_size.height)};
141 }
142 const float recip_w = 1.0f / clip_w;
143 const float ndc_x = clip.x * recip_w;
144 const float ndc_y = clip.y * recip_w;
145 const float center_x = ((ndc_x * 0.5f) + 0.5f) * float(viewport_size.width) + float(viewport_pos.x);
146 const float center_y = ((ndc_y * -0.5f) + 0.5f) * float(viewport_size.height) + float(viewport_pos.y);
147 const float radius_px = screen_radius * float(viewport_size.height);
148 const float left_f = center_x - radius_px;
149 const float right_f = center_x + radius_px;
150 const float top_f = center_y - radius_px;
151 const float bottom_f = center_y + radius_px;
152 const std::int32_t min_x = viewport_pos.x;
153 const std::int32_t min_y = viewport_pos.y;
154 const std::int32_t max_x = viewport_pos.x + static_cast<std::int32_t>(viewport_size.width);
155 const std::int32_t max_y = viewport_pos.y + static_cast<std::int32_t>(viewport_size.height);
156 const std::int32_t left = math::clamp(static_cast<std::int32_t>(std::floor(left_f)), min_x, max_x);
157 const std::int32_t right = math::clamp(static_cast<std::int32_t>(std::ceil(right_f)), min_x, max_x);
158 const std::int32_t top = math::clamp(static_cast<std::int32_t>(std::floor(top_f)), min_y, max_y);
159 const std::int32_t bottom = math::clamp(static_cast<std::int32_t>(std::ceil(bottom_f)), min_y, max_y);
160 return {left, top, right, bottom};
161}
162}
163
165{
166 const auto& viewport_size = cam.get_viewport_size();
167 const auto& viewport_pos = cam.get_viewport_pos();
168
169 float screen_radius = percent * 0.005f;
170 rect = compute_screen_rect_from_sphere(cam, center, screen_radius);
171}
172
173
174auto model::is_valid() const -> bool
175{
176 return !mesh_lods_.empty();
177}
178
179auto model::get_lod(uint32_t lod) const -> asset_handle<mesh>
180{
181 if(mesh_lods_.empty())
182 {
183 return {};
184 }
185
186 lod = math::clamp<std::size_t>(lod, 0, mesh_lods_.size() - 1);
187
188 for(int i = int(lod); i >= 0; --i)
189 {
190 auto lod_mesh = mesh_lods_[i];
191 if(lod_mesh)
192 {
193 return lod_mesh;
194 }
195 }
196
197 return {};
198}
199
201{
202 bool recalculate_lod_limits = false;
203 if(lod >= mesh_lods_.size())
204 {
205 mesh_lods_.resize(lod + 1);
206
207 recalculate_lod_limits = true;
208
209 }
210 mesh_lods_[lod] = mesh;
211
212 if(recalculate_lod_limits)
213 {
215 }
216
217 resize_materials(mesh);
218}
219
221{
222 if(index >= materials_.size())
223 {
224 materials_.resize(index + 1);
225 }
226
227 materials_[index] = std::move(material);
228}
229
231{
232 if(index >= material_instances_.size())
233 {
234 material_instances_.resize(index + 1, nullptr);
235 }
236
237 material_instances_[index] = std::move(material);
238}
239
240auto model::get_lods() const -> const std::vector<asset_handle<mesh>>&
241{
242 return mesh_lods_;
243}
244
245auto model::get_lods_count() const -> uint32_t
246{
247 if(mesh_lods_.empty())
248 {
249 return 0;
250 }
251 // If there's only one mesh, it might have internal LODs (automatic generation)
252 if(mesh_lods_.size() == 1)
253 {
254 const auto& mesh_asset = mesh_lods_[0];
255 if(mesh_asset && mesh_asset.get())
256 {
257 return mesh_asset.get()->get_lod_count();
258 }
259 }
260 // Otherwise, return the number of separate mesh LODs (manual LODs)
261 return static_cast<uint32_t>(mesh_lods_.size());
262}
263
264void model::set_lods(const std::vector<asset_handle<mesh>>& lods)
265{
266 mesh_lods_ = lods;
267
269
270 if(!mesh_lods_.empty())
271 {
272 auto& mesh = mesh_lods_[0];
273 resize_materials(mesh);
274 }
275}
276
277auto model::get_materials() const -> const std::vector<asset_handle<material>>&
278{
279 return materials_;
280}
281
282auto model::get_material_instances() const -> const std::vector<material::sptr>&
283{
284 return material_instances_;
285}
286
287
288void model::set_materials(const std::vector<asset_handle<material>>& materials)
289{
290 materials_ = materials;
291}
292
293void model::set_material_instances(const std::vector<material::sptr>& materials)
294{
295 material_instances_ = materials;
296}
297
299{
300 if(materials_.size() <= index)
301 {
302 return {};
303 }
304
305 return materials_[index];
306}
307
309{
310 if(index < material_instances_.size())
311 {
312 auto instance = material_instances_[index];
313 if(instance)
314 {
315 return instance;
316 }
317 }
318
319 auto instance = get_material(index);
320 if(instance.is_valid())
321 {
322 return instance.get();
323 }
324
325 return nullptr;
326}
327
329{
330 if(index >= material_instances_.size())
331 {
332 auto asset_instance = get_material_instance(index);
333
334 material_instances_.resize(index + 1, nullptr);
335 material_instances_[index] = asset_instance->clone();
336 }
337
338 auto& instance = material_instances_[index];
339
340 if(!instance)
341 {
342 auto asset_instance = get_material_instance(index);
343
344 // if we already have an asset for that slot, promote it to instance
345 if(asset_instance)
346 {
347 instance = asset_instance->clone();
348 }
349 else
350 {
351 // create a new one
352 instance = std::make_shared<pbr_material>();
353 }
354 }
355
356 return instance;
357}
358
359
360auto model::calculate_lod_data(lod_data& data, const math::bbox& world_bounds, const camera& cam, float dt) const -> bool
361{
362 data.transition_time = get_lod_transition_time().count();
363 const auto lod_count = get_lods_count();
364 const auto base_mesh = get_lod(0);
365 if(!base_mesh)
366 {
367 return false;
368 }
369
370 // Unpopulated bounds mean the mesh has not been loaded/measured yet - nothing to size.
371 if(!world_bounds.is_populated())
372 {
373 return false;
374 }
375
376 // Enclosing sphere of the pose-aware world AABB. Same conservative construction the
377 // bind-pose path used, but built from the box that tracks the actual rendered geometry.
378 const math::bsphere bsphere{world_bounds.get_center(), glm::length(world_bounds.get_extents())};
379 const float screen_radius_squared = compute_bounds_screen_radius_squared(bsphere.position, bsphere.radius, cam);
380
381 const float screen_radius = std::sqrt(std::max(0.0f, screen_radius_squared));
382 data.percent = math::clamp(screen_radius * 200.0f, 0.0f, 100.0f);
383 data.center = bsphere.position;
384
385 const float lod_screen_size_min = 0.005f;
386 const float cull_threshold_squared = math::square(lod_screen_size_min * 0.5f);
387 const bool is_visible = cull_threshold_squared <= screen_radius_squared;
388 if(!is_visible)
389 {
390 return false;
391 }
392
393 std::size_t lod = 0;
394 if(lod_override_enabled_)
395 {
396 lod = math::clamp<std::size_t>(lod_override_level_, 0, lod_count - 1);
397 }
398 else if(lod_count > 1 && lod_screen_sizes_.size() >= lod_count)
399 {
400 // Use current LOD for hysteresis (what's being displayed, accounting for transitions)
401 const uint32_t prev_lod = data.current_lod_index;
402 const float hysteresis = lod_hysteresis_;
403
404 for(std::int32_t lod_index = static_cast<std::int32_t>(lod_count) - 1; lod_index >= 0; --lod_index)
405 {
406 const auto index = static_cast<size_t>(lod_index);
407 float screen_size = lod_screen_sizes_[index];
408 float screen_size_squared = math::square(screen_size * 0.5f);
409
410 // Apply hysteresis to create a "sticky" dead zone around the current LOD
411 // This prevents rapid switching at LOD boundaries
412 if(prev_lod == index)
413 {
414 // Currently at this LOD - INCREASE threshold to make it easier to stay
415 // (larger threshold = condition more likely to be true)
416 float adjusted_size = screen_size * (1.0f + hysteresis);
417 screen_size_squared = math::square(adjusted_size * 0.5f);
418 }
419 else
420 {
421 // Different from current LOD - DECREASE threshold to resist change
422 // (smaller threshold = condition less likely to be true)
423 float adjusted_size = screen_size * (1.0f - hysteresis);
424 screen_size_squared = math::square(adjusted_size * 0.5f);
425 }
426
427 if(screen_size_squared >= screen_radius_squared)
428 {
429 lod = static_cast<std::size_t>(lod_index);
430 break;
431 }
432 }
433 }
434
435 float biased_lod = static_cast<float>(lod) + lod_selection_bias_;
436 biased_lod = math::clamp(biased_lod, 0.0f, static_cast<float>(lod_count - 1));
437 lod = static_cast<std::size_t>(biased_lod);
438
439 // Hysteresis determined new LOD - now handle transition timing
440 // Only trigger a new transition if we're not currently transitioning
441 if(data.target_lod_index != lod && data.target_lod_index == data.current_lod_index)
442 {
443 data.target_lod_index = static_cast<std::uint32_t>(lod);
444 data.current_time = 0.0f;
445 }
446
447 // Update transition progress
448 if(data.current_lod_index != data.target_lod_index)
449 {
450 data.current_time += dt;
451 }
452
453 // Complete transition when time elapsed
454 if(data.current_time >= data.transition_time)
455 {
456 data.current_lod_index = data.target_lod_index;
457 data.current_time = 0.0f;
458 }
459
460 return true;
461}
462
463auto model::select_submesh_lod_for_sphere(const mesh& m,
464 uint32_t submesh_index,
465 uint32_t base_lod,
466 const math::bsphere& world_sphere,
467 const camera& cam) const -> uint32_t
468{
469 if(lod_override_enabled_)
470 {
471 return base_lod;
472 }
473
474 const auto lod_count = get_lods_count();
475 if(lod_count <= base_lod + 1)
476 {
477 return base_lod;
478 }
479
480 // Per-submesh LOD needs the underlying mesh asset to stay the same across LOD switches so
481 // submesh_index keeps its meaning. That is only true with a single mesh + internal LODs;
482 // multi-mesh manual LODs are a different asset per level with potentially unrelated
483 // submesh layouts.
484 if(mesh_lods_.size() != 1)
485 {
486 return base_lod;
487 }
488
489 if(lod_screen_sizes_.size() < lod_count)
490 {
491 return base_lod;
492 }
493
494 const float screen_radius_squared =
495 compute_bounds_screen_radius_squared(world_sphere.position, world_sphere.radius, cam);
496
497 // Walk from the lowest-quality LOD toward base_lod and pick the coarsest LOD whose
498 // screen-size threshold still fits the submesh's projected size. This mirrors the model-
499 // wide selection but without hysteresis / transitions (submesh-level ping-pong is bounded
500 // by the model LOD floor and is generally imperceptible for small distant submeshes).
501 for(std::int32_t lod_index = static_cast<std::int32_t>(lod_count) - 1;
502 lod_index >= static_cast<std::int32_t>(base_lod);
503 --lod_index)
504 {
505 const float screen_size = lod_screen_sizes_[static_cast<std::size_t>(lod_index)];
506 const float screen_size_squared = math::square(screen_size * 0.5f);
507 if(screen_size_squared < screen_radius_squared)
508 {
509 continue;
510 }
511 // If the LOD does not carry this submesh index at all (topology diverges), fall back.
512 if(submesh_index >= m.get_submeshes(static_cast<uint32_t>(lod_index)).size())
513 {
514 continue;
515 }
516
517 return static_cast<uint32_t>(lod_index);
518 }
519
520 return base_lod;
521}
522
524 uint32_t submesh_index,
525 uint32_t base_lod,
526 const math::mat4& world_matrix,
527 const camera& cam) const -> uint32_t
528{
529 // Cheap early-outs before paying for the world matrix decomposition.
530 if(lod_override_enabled_ || mesh_lods_.size() != 1 || get_lods_count() <= base_lod + 1)
531 {
532 return base_lod;
533 }
534
535 const auto& base_submeshes = m.get_submeshes(base_lod);
536 if(submesh_index >= base_submeshes.size())
537 {
538 return base_lod;
539 }
540 const auto* sm = base_submeshes[submesh_index];
541 if(sm == nullptr || !sm->bbox.is_populated())
542 {
543 // No per-submesh bbox means the submesh cannot be distinguished from the whole model,
544 // so per-submesh LOD would just mirror the model-wide selection.
545 return base_lod;
546 }
547
548 const auto sphere = compute_submesh_world_bounds_sphere(*sm, world_matrix);
549 return select_submesh_lod_for_sphere(m, submesh_index, base_lod, sphere, cam);
550}
551
553 uint32_t submesh_index,
554 uint32_t base_lod,
555 const math::bbox& world_bounds,
556 const camera& cam) const -> uint32_t
557{
558 if(!world_bounds.is_populated())
559 {
560 return base_lod;
561 }
562
563 const math::bsphere sphere{world_bounds.get_center(), glm::length(world_bounds.get_extents())};
564 return select_submesh_lod_for_sphere(m, submesh_index, base_lod, sphere, cam);
565}
566
567auto model::compute_lod_index(const math::bbox& world_bounds, const camera& cam, float extra_bias) const
568 -> uint32_t
569{
570 const auto lod_count = get_lods_count();
571 if(lod_count <= 1)
572 {
573 return 0;
574 }
575
576 if(lod_override_enabled_)
577 {
578 return math::clamp<uint32_t>(lod_override_level_, 0, lod_count - 1);
579 }
580
581 if(lod_screen_sizes_.size() < lod_count)
582 {
583 return 0;
584 }
585
586 if(!world_bounds.is_populated())
587 {
588 return 0;
589 }
590
591 // Enclosing sphere of the pose-aware world AABB (see calculate_lod_data).
592 const math::bsphere bsphere{world_bounds.get_center(), glm::length(world_bounds.get_extents())};
593 const float screen_radius_squared = compute_bounds_screen_radius_squared(bsphere.position, bsphere.radius, cam);
594
595 std::size_t lod = 0;
596 for(std::int32_t lod_index = static_cast<std::int32_t>(lod_count) - 1; lod_index >= 0; --lod_index)
597 {
598 const auto index = static_cast<size_t>(lod_index);
599 const float screen_size = lod_screen_sizes_[index];
600 const float screen_size_squared = math::square(screen_size * 0.5f);
601 if(screen_size_squared >= screen_radius_squared)
602 {
603 lod = index;
604 break;
605 }
606 }
607
608 float biased_lod = static_cast<float>(lod) + lod_selection_bias_ + extra_bias;
609 biased_lod = math::clamp(biased_lod, 0.0f, static_cast<float>(lod_count - 1));
610 return static_cast<uint32_t>(biased_lod);
611}
612
613
615{
616 lod_screen_sizes_.clear();
617 if(lod_count == 0)
618 {
619 return;
620 }
621 lod_screen_sizes_.resize(lod_count);
622 for(uint32_t i = 0; i < lod_count; ++i)
623 {
624 if(i == 0)
625 {
626 lod_screen_sizes_[i] = 1.0f;
627 }
628 else if(i == 1)
629 {
630 lod_screen_sizes_[i] = 0.3f;
631 }
632 else
633 {
634 lod_screen_sizes_[i] = lod_screen_sizes_[i - 1] * 0.5f;
635 }
636 }
637}
638
640{
641 return lod_override_enabled_;
642}
643
645{
646 lod_override_enabled_ = enabled;
647}
648
649auto model::get_lod_override_level() const -> uint32_t
650{
651 return lod_override_level_;
652}
653
655{
656 lod_override_level_ = level;
657}
658
659auto model::get_lod_selection_bias() const -> float
660{
661 return lod_selection_bias_;
662}
663
665{
666 lod_selection_bias_ = bias;
667}
668
669auto model::get_lod_hysteresis() const -> float
670{
671 return lod_hysteresis_;
672}
673
674void model::set_lod_hysteresis(float hysteresis)
675{
676 lod_hysteresis_ = hysteresis;
677}
678
680{
681 return lod_transition_time_;
682}
683
685{
686 lod_transition_time_ = time;
687}
688
690{
691 return 0.01f;
692}
693
695{
696 // Deprecated - hysteresis is now used instead
697}
698
700{
701 return 0.5f;
702}
703
705{
706 // Deprecated - fixed thresholds are now used
707}
708
709auto model::get_lod_screen_sizes() const -> const std::vector<float>&
710{
711 return lod_screen_sizes_;
712}
713
714void model::set_lod_screen_sizes(const std::vector<float>& sizes)
715{
716 lod_screen_sizes_ = sizes;
717}
718
719
720void model::submit(const math::mat4& world_transform,
721 const submesh_pose_mat4& submesh_transforms,
722 const pose_mat4& bone_transforms,
723 const std::vector<pose_mat4>& skinning_transforms,
724 unsigned int lod,
725 const submit_callbacks& callbacks,
726 const math::frustum* frustum,
727 const camera* view,
728 const model_submit_extras& extras) const
729{
730 const auto lod_mesh = get_lod(lod);
731 if(!lod_mesh)
732 {
733 return;
734 }
735
736 auto mesh = lod_mesh.get();
737
738 auto skinned_submeshes_count = mesh->get_skinned_submeshes_count(lod);
739 auto non_skinned_submeshes_count = mesh->get_non_skinned_submeshes_count(lod);
740 // Per-submesh culling applies to any multi-submesh mesh: cached world-space AABBs make
741 // the test cheap. Submeshes without cached bounds are conservatively drawn - local
742 // bind-pose bounds are never used for culling.
743 const bool cull_submeshes = frustum != nullptr && mesh->get_submeshes_count(lod) > 1;
744 const bool per_submesh_lod = cull_submeshes && view != nullptr;
745
747
748 // NON SKINNED
749 if(non_skinned_submeshes_count > 0)
750 {
751 params.skinned = false;
752
753 if(callbacks.setup_begin)
754 {
755 callbacks.setup_begin(params);
756 }
757
758 if(callbacks.setup_params_per_instance)
759 {
760 callbacks.setup_params_per_instance(params);
761 }
762
763 auto render_submesh = [this, frustum, cull_submeshes, per_submesh_lod, view, &extras]
764 (const std::shared_ptr<unravel::mesh>& mesh,
765 uint32_t lod,
766 uint32_t group_id,
767 const math::mat4& matrix,
768 const submesh_pose_mat4& pose,
770 const submit_callbacks& callbacks)
771 {
772 auto group_mat = get_material_instance(group_id);
773
774 const auto& submeshes = mesh->get_submeshes(lod);
775 const auto& indices = mesh->get_non_skinned_submeshes_indices(group_id, lod);
776
777 // Picks the LOD-adjusted submesh pointer/lod pair used for binding. Culling still
778 // uses the base-LOD bbox (higher LODs are simplified within the same envelope, so
779 // the base bbox is a valid upper bound and this keeps culling stable).
780 const auto resolve = [&](uint32_t submesh_index,
781 const math::mat4& world,
782 size_t instance) -> std::pair<const unravel::mesh::submesh*, uint32_t>
783 {
784 const auto* base_sm = submeshes[submesh_index];
785 if(!per_submesh_lod)
786 {
787 return {base_sm, lod};
788 }
789 // Prefer the cached world AABB (no matrix decomposition); fall back to the
790 // local bbox + world matrix path when no cached data exists.
791 const auto* cached_bounds = get_cached_submesh_bounds(extras, submesh_index, instance, false);
792 const uint32_t effective_lod =
793 cached_bounds != nullptr
794 ? calculate_submesh_lod_from_world_bounds(*mesh, submesh_index, lod, *cached_bounds, *view)
795 : calculate_submesh_lod(*mesh, submesh_index, lod, world, *view);
796 if(effective_lod == lod)
797 {
798 return {base_sm, lod};
799 }
800 const auto& lod_submeshes = mesh->get_submeshes(effective_lod);
801 if(submesh_index >= lod_submeshes.size())
802 {
803 return {base_sm, lod};
804 }
805 const auto* lod_sm = lod_submeshes[submesh_index];
806 return lod_sm != nullptr ? std::make_pair(lod_sm, effective_lod)
807 : std::make_pair(base_sm, lod);
808 };
809
810 for(const auto& index : indices)
811 {
812 const auto& mat = resolve_submesh_material(extras, static_cast<uint32_t>(index), group_mat);
813 if(!mat)
814 {
815 continue;
816 }
817
818 if(pose.has_transforms(index))
819 {
820 const size_t transform_count = pose.get_transform_count(index);
821
822 for(size_t i = 0; i < transform_count; ++i)
823 {
824 const auto* transform = pose.get_transform(index, i);
825 if(transform)
826 {
827 if(extras.shadow_pass && !pose.get_transform_casts_shadow(index, i))
828 {
829 continue;
830 }
831
832 if(cull_submeshes && !is_submesh_visible_cached(*frustum, extras, index, i, false))
833 {
834 continue;
835 }
836
837 const auto [sm, sm_lod] = resolve(static_cast<uint32_t>(index), *transform, i);
838 gfx::set_world_transform(*transform);
840 params.preserve_state = (&index != &indices.back());
841 callbacks.setup_params_per_submesh(params, *mat);
842 }
843 }
844 }
845 else
846 {
847 if(cull_submeshes && !is_submesh_visible_cached(*frustum, extras, index, 0, false))
848 {
849 continue;
850 }
851
852 const auto [sm, sm_lod] = resolve(static_cast<uint32_t>(index), matrix, 0);
855 params.preserve_state = &index != &indices.back();
856 callbacks.setup_params_per_submesh(params, *mat);
857 }
858 }
859 };
860
861 for(uint32_t i = 0; i < mesh->get_data_groups_count(); ++i)
862 {
863 render_submesh(mesh, lod, i, world_transform, submesh_transforms, params, callbacks);
864 }
865
866 if(callbacks.setup_end)
867 {
868 callbacks.setup_end(params);
869 }
870 }
871
872 // SKINNED
873 if(skinned_submeshes_count > 0 && !skinning_transforms.empty())
874 {
875 params.skinned = true;
876
877 if(callbacks.setup_begin)
878 {
879 callbacks.setup_begin(params);
880 }
881
882 if(callbacks.setup_params_per_instance)
883 {
884 callbacks.setup_params_per_instance(params);
885 }
886
887 auto render_submesh_skinned = [this, frustum, cull_submeshes, per_submesh_lod, view, &extras]
888 (const std::shared_ptr<unravel::mesh>& mesh,
889 uint32_t lod,
890 uint32_t group_id,
891 const submesh_pose_mat4& pose,
892 const std::vector<pose_mat4>& skinning_transforms,
894 const submit_callbacks& callbacks)
895 {
896 auto group_mat = get_material_instance(group_id);
897
898 const auto& submeshes = mesh->get_submeshes(lod);
899 const auto& indices = mesh->get_skinned_submeshes_indices(group_id, lod);
900
901 for(const auto& index : indices)
902 {
903 if(index >= skinning_transforms.size())
904 {
905 continue;
906 }
907
908 const auto& mat = resolve_submesh_material(extras, static_cast<uint32_t>(index), group_mat);
909 if(!mat)
910 {
911 continue;
912 }
913
914 // Per-submesh enable/shadow flags authored on the owning node entity apply to
915 // skinned submeshes too (the node transform itself is unused for skinning).
916 if(pose.has_transforms(index))
917 {
918 if(!pose.get_transform_active(index, 0))
919 {
920 continue;
921 }
922 if(extras.shadow_pass && !pose.get_transform_casts_shadow(index, 0))
923 {
924 continue;
925 }
926 }
927
928 // Skinned submeshes cull identically to static ones using the retained
929 // animated world bounds (union of bone-transformed bind-space bounds).
930 if(cull_submeshes && frustum != nullptr)
931 {
932 const auto* bounds = get_cached_submesh_bounds(extras, static_cast<uint32_t>(index), 0, true);
933 if(bounds != nullptr && frustum->classify_aabb(*bounds) == math::volume_query::outside)
934 {
935 continue;
936 }
937 }
938
939 const auto& submesh_skinning_transforms = skinning_transforms[index];
940
941 if(!submesh_skinning_transforms.transforms.empty())
942 {
943 // Per-submesh LOD for skinned submeshes uses the animated world bounds;
944 // submesh indices are stable across internal LODs so the same palette
945 // still applies to the simplified index range.
946 const auto* base_sm = submeshes[index];
947 const auto* sm = base_sm;
948 uint32_t sm_lod = lod;
949 if(per_submesh_lod)
950 {
951 const auto* bounds = get_cached_submesh_bounds(extras, static_cast<uint32_t>(index), 0, true);
952 if(bounds != nullptr)
953 {
954 const uint32_t effective_lod = calculate_submesh_lod_from_world_bounds(
955 *mesh, static_cast<uint32_t>(index), lod, *bounds, *view);
956 if(effective_lod != lod)
957 {
958 const auto& lod_submeshes = mesh->get_submeshes(effective_lod);
959 if(index < lod_submeshes.size() && lod_submeshes[index] != nullptr)
960 {
961 sm = lod_submeshes[index];
962 sm_lod = effective_lod;
963 }
964 }
965 }
966 }
967
968 gfx::set_world_transform(submesh_skinning_transforms.transforms);
969
971 params.preserve_state = &index != &indices.back();
972 callbacks.setup_params_per_submesh(params, *mat);
973 }
974
975 }
976 };
977
978 for(uint32_t i = 0; i < mesh->get_data_groups_count(); ++i)
979 {
980 render_submesh_skinned(mesh, lod, i, submesh_transforms, skinning_transforms, params, callbacks);
981 }
982
983 if(callbacks.setup_end)
984 {
985 callbacks.setup_end(params);
986 }
987 }
988}
989
990void model::submit_for_vertex_pulling(const math::mat4& world_transform,
991 const submesh_pose_mat4& submesh_transforms,
992 const std::vector<pose_mat4>& skinning_transforms,
993 unsigned int lod,
994 const submit_vertex_pulling_callbacks& callbacks,
995 const math::frustum* frustum,
996 const camera* view,
997 const model_submit_extras& extras) const
998{
999 const auto lod_mesh = get_lod(lod);
1000 if(!lod_mesh)
1001 {
1002 return;
1003 }
1004
1005 auto mesh = lod_mesh.get();
1006
1007 auto vb = mesh->get_hardware_vb();
1008 auto ib = mesh->get_hardware_ib(lod);
1009 if(!vb || !ib || !vb->is_valid() || !ib->is_valid())
1010 {
1011 return;
1012 }
1013
1014 // Vertex/index buffers are exposed to shaders as Buffer<float> / Buffer<uint>
1015 // so all byte offsets are converted to float-sized elements. Layouts used by
1016 // this engine always keep attributes float-aligned, so integer division is safe.
1017 constexpr uint32_t float_size = static_cast<uint32_t>(sizeof(float));
1018 const auto& vertex_format = mesh->get_vertex_format();
1019 const uint32_t stride_bytes = vertex_format.getStride();
1020 const uint32_t pos_offset_bytes = vertex_format.getOffset(gfx::attribute::Position);
1021
1023 params.vertex_stride_floats = stride_bytes / float_size;
1024 params.position_offset_floats = pos_offset_bytes / float_size;
1025
1026 const auto skinned_count = mesh->get_skinned_submeshes_count(lod);
1027 const auto non_skinned_count = mesh->get_non_skinned_submeshes_count(lod);
1028 const bool cull_submeshes = frustum != nullptr && mesh->get_submeshes_count(lod) > 1;
1029 const bool per_submesh_lod = cull_submeshes && view != nullptr;
1030 const auto& submeshes = mesh->get_submeshes(lod);
1031 const uint32_t group_count = static_cast<uint32_t>(mesh->get_data_groups_count());
1032
1033 // Binds the raw geometry buffers for @p effective_lod's index buffer and reads face
1034 // range from that LOD's submesh entry. When @p effective_lod matches the model-wide
1035 // @p lod, the pre-fetched @c ib is reused; otherwise the LOD-specific IB is looked up.
1036 // The callback is responsible for uniforms, state, vertex count and the actual submit
1037 // call - the model only guarantees that u_world and the raw buffers on stages 0/1
1038 // are set.
1039 auto bind_and_submit = [&](uint32_t submesh_index, uint32_t effective_lod) -> void
1040 {
1041 const auto& lod_submeshes = (effective_lod == lod) ? submeshes : mesh->get_submeshes(effective_lod);
1042 if(submesh_index >= lod_submeshes.size())
1043 {
1044 return;
1045 }
1046 const auto* sub = lod_submeshes[submesh_index];
1047 if(!sub || sub->face_count == 0)
1048 {
1049 return;
1050 }
1051
1052 const auto effective_ib = (effective_lod == lod) ? ib : mesh->get_hardware_ib(effective_lod);
1053 if(!effective_ib || !effective_ib->is_valid())
1054 {
1055 return;
1056 }
1057
1058 params.submesh_index = submesh_index;
1059 params.index_start = static_cast<uint32_t>(sub->face_start) * 3u;
1060 params.index_count = sub->face_count * 3u;
1061
1062 gfx::set_buffer(0, vb->native_handle(), gfx::access::Read);
1063 gfx::set_buffer(1, effective_ib->native_handle(), gfx::access::Read);
1064
1065 if(callbacks.setup_params_per_submesh)
1066 {
1067 callbacks.setup_params_per_submesh(params);
1068 }
1069 };
1070
1071 // ----------------- NON-SKINNED PASS -----------------
1072 if(non_skinned_count > 0)
1073 {
1074 params.skinned = false;
1075 params.weight_offset_floats = 0;
1076 params.indices_offset_floats = 0;
1077
1078 if(callbacks.setup_begin)
1079 {
1080 callbacks.setup_begin(params);
1081 }
1082 if(callbacks.setup_params_per_instance)
1083 {
1084 callbacks.setup_params_per_instance(params);
1085 }
1086
1087 for(uint32_t group_id = 0; group_id < group_count; ++group_id)
1088 {
1089 const auto& indices = mesh->get_non_skinned_submeshes_indices(group_id, lod);
1090 for(const auto& index : indices)
1091 {
1092 const auto* sub = submeshes[index];
1093 if(!sub || sub->face_count == 0)
1094 {
1095 continue;
1096 }
1097
1098 params.preserve_state = &index != &indices.back();
1099
1100 if(submesh_transforms.has_transforms(index))
1101 {
1102 const size_t transform_count = submesh_transforms.get_transform_count(index);
1103 for(size_t j = 0; j < transform_count; ++j)
1104 {
1105 const auto* transform = submesh_transforms.get_transform(index, j);
1106 if(!transform)
1107 {
1108 continue;
1109 }
1110 if(extras.shadow_pass && !submesh_transforms.get_transform_casts_shadow(index, j))
1111 {
1112 continue;
1113 }
1114 if(cull_submeshes && !is_submesh_visible_cached(*frustum, extras, index, j, false))
1115 {
1116 continue;
1117 }
1118 uint32_t effective_lod = lod;
1119 if(per_submesh_lod)
1120 {
1121 const auto* cached_bounds =
1122 get_cached_submesh_bounds(extras, static_cast<uint32_t>(index), j, false);
1123 effective_lod =
1124 cached_bounds != nullptr
1126 static_cast<uint32_t>(index),
1127 lod,
1128 *cached_bounds,
1129 *view)
1130 : calculate_submesh_lod(*mesh, static_cast<uint32_t>(index), lod, *transform, *view);
1131 }
1132 gfx::set_world_transform(*transform);
1133 bind_and_submit(static_cast<uint32_t>(index), effective_lod);
1134 }
1135 }
1136 else
1137 {
1138 if(cull_submeshes && !is_submesh_visible_cached(*frustum, extras, index, 0, false))
1139 {
1140 continue;
1141 }
1142 const uint32_t effective_lod = per_submesh_lod
1143 ? calculate_submesh_lod(*mesh, static_cast<uint32_t>(index), lod, world_transform, *view)
1144 : lod;
1145 gfx::set_world_transform(world_transform);
1146 bind_and_submit(static_cast<uint32_t>(index), effective_lod);
1147 }
1148 }
1149 }
1150
1151 if(callbacks.setup_end)
1152 {
1153 callbacks.setup_end(params);
1154 }
1155 }
1156
1157 // ------------------- SKINNED PASS -------------------
1158 // Skinned rendering additionally needs the bone weight/indices attribute
1159 // offsets so the shader can blend u_world[bone_i] per vertex.
1160 if(skinned_count > 0 && !skinning_transforms.empty()
1161 && vertex_format.has(gfx::attribute::Weight) && vertex_format.has(gfx::attribute::Indices))
1162 {
1163 params.skinned = true;
1164 params.weight_offset_floats = vertex_format.getOffset(gfx::attribute::Weight) / float_size;
1165 params.indices_offset_floats = vertex_format.getOffset(gfx::attribute::Indices) / float_size;
1166
1167 if(callbacks.setup_begin)
1168 {
1169 callbacks.setup_begin(params);
1170 }
1171 if(callbacks.setup_params_per_instance)
1172 {
1173 callbacks.setup_params_per_instance(params);
1174 }
1175
1176 for(uint32_t group_id = 0; group_id < group_count; ++group_id)
1177 {
1178 const auto& indices = mesh->get_skinned_submeshes_indices(group_id, lod);
1179 for(const auto& index : indices)
1180 {
1181 if(index >= skinning_transforms.size())
1182 {
1183 continue;
1184 }
1185 const auto& bones = skinning_transforms[index];
1186 if(bones.transforms.empty())
1187 {
1188 continue;
1189 }
1190 const auto* sub = submeshes[index];
1191 if(!sub || sub->face_count == 0)
1192 {
1193 continue;
1194 }
1195
1196 if(submesh_transforms.has_transforms(index))
1197 {
1198 if(!submesh_transforms.get_transform_active(index, 0))
1199 {
1200 continue;
1201 }
1202 if(extras.shadow_pass && !submesh_transforms.get_transform_casts_shadow(index, 0))
1203 {
1204 continue;
1205 }
1206 }
1207
1208 if(cull_submeshes && frustum != nullptr)
1209 {
1210 const auto* bounds = get_cached_submesh_bounds(extras, static_cast<uint32_t>(index), 0, true);
1211 if(bounds != nullptr && frustum->classify_aabb(*bounds) == math::volume_query::outside)
1212 {
1213 continue;
1214 }
1215 }
1216
1217 params.preserve_state = &index != &indices.back();
1218
1219 gfx::set_world_transform(bones.transforms);
1220 bind_and_submit(static_cast<uint32_t>(index), lod);
1221 }
1222 }
1223
1224 if(callbacks.setup_end)
1225 {
1226 callbacks.setup_end(params);
1227 }
1228 }
1229}
1230
1231
1232void model::resize_materials(const asset_handle<mesh>& mesh)
1233{
1234 const auto m = mesh.get();
1235 auto submeshes = m->get_data_groups_count();
1236 if(materials_.size() != submeshes)
1237 {
1238 materials_.resize(submeshes, default_material());
1239 }
1240}
1241
1243{
1244 static asset_handle<material> asset;
1245 return asset;
1246}
1247
1249{
1250 static asset_handle<material> asset;
1251 return asset;
1252}
1253
1255 const math::mat4& world_transform,
1256 const submesh_pose_mat4& submesh_transforms,
1257 uint32_t lod_index,
1258 float lod_param,
1259 const math::frustum* frustum,
1260 const camera* view,
1261 const model_submit_extras& extras) const
1262{
1263 auto mesh_asset = get_lod(lod_index);
1264 if(!mesh_asset)
1265 {
1266 return;
1267 }
1268
1269 auto mesh = mesh_asset.get();
1270 if(!mesh)
1271 {
1272 return;
1273 }
1274
1275 const bool cull_submeshes = frustum != nullptr && mesh->get_submeshes_count(lod_index) > 1;
1276 // The batch key already includes lod_index, so mixed per-submesh LODs land in distinct
1277 // batches automatically - the renderer already looks up (mesh, lod, submesh) per batch.
1278 const bool per_submesh_lod = cull_submeshes && view != nullptr;
1279
1280 // Iterate over data groups (material groups)
1281 const auto data_group_count = mesh->get_data_groups_count();
1282
1283 for (uint32_t data_group_id = 0; data_group_id < data_group_count; ++data_group_id)
1284 {
1285 // Get material for this data group
1286 auto group_material = get_material_instance(data_group_id);
1287
1288 // Get all non-skinned submeshes for this data group
1289 const auto& submesh_indices = mesh->get_non_skinned_submeshes_indices(data_group_id, lod_index);
1290
1291 // Collect each submesh in this data group as a separate batch entry
1292 for (size_t submesh_idx : submesh_indices)
1293 {
1294 uint32_t submesh_index = static_cast<uint32_t>(submesh_idx);
1295
1296 // Per-submesh material overrides participate in the batch key, so overridden
1297 // instances automatically batch separately from the model-material ones.
1298 const auto& material_ptr = resolve_submesh_material(extras, submesh_index, group_material);
1299 if(!material_ptr)
1300 {
1301 continue; // Skip submeshes without valid materials
1302 }
1303
1304 // Check if this submesh has specific transforms
1305 if (submesh_transforms.has_transforms(submesh_index))
1306 {
1307 // This submesh has one or more node transforms - create an instance for each
1308 const size_t transform_count = submesh_transforms.get_transform_count(submesh_index);
1309
1310 for (size_t instance_idx = 0; instance_idx < transform_count; ++instance_idx)
1311 {
1312 const math::mat4* transform_ptr = submesh_transforms.get_transform(submesh_index, instance_idx);
1313 if (!transform_ptr)
1314 {
1315 continue;
1316 }
1317
1318 if(extras.shadow_pass
1319 && !submesh_transforms.get_transform_casts_shadow(submesh_index, instance_idx))
1320 {
1321 continue;
1322 }
1323
1324 if(cull_submeshes && !is_submesh_visible_cached(*frustum, extras, submesh_index, instance_idx, false))
1325 {
1326 continue;
1327 }
1328
1329 uint32_t effective_lod = lod_index;
1330 if(per_submesh_lod)
1331 {
1332 const auto* cached_bounds =
1333 get_cached_submesh_bounds(extras, submesh_index, instance_idx, false);
1334 effective_lod =
1335 cached_bounds != nullptr
1337 submesh_index,
1338 lod_index,
1339 *cached_bounds,
1340 *view)
1341 : calculate_submesh_lod(*mesh, submesh_index, lod_index, *transform_ptr, *view);
1342 }
1343
1344 batch_key key(mesh, material_ptr, effective_lod, submesh_index);
1345 if (!key.is_valid())
1346 {
1347 continue;
1348 }
1349
1350 // Create batch instance with the specific transform
1351 batch_instance instance(transform_ptr);
1352 instance.lod_params.x = lod_param;
1353
1354 // Collect for batching
1355 collector.collect_renderable(key, instance);
1356 }
1357 }
1358 else
1359 {
1360 if(cull_submeshes && !is_submesh_visible_cached(*frustum, extras, submesh_index, 0, false))
1361 {
1362 continue;
1363 }
1364
1365 const uint32_t effective_lod = per_submesh_lod
1366 ? calculate_submesh_lod(*mesh, submesh_index, lod_index, world_transform, *view)
1367 : lod_index;
1368
1369 batch_key key(mesh, material_ptr, effective_lod, submesh_index);
1370 if (!key.is_valid())
1371 {
1372 continue;
1373 }
1374
1375 // Create batch instance with world transform
1376 batch_instance instance(&world_transform);
1377 instance.lod_params.x = lod_param;
1378
1379 // Collect for batching
1380 collector.collect_renderable(key, instance);
1381 }
1382 }
1383 }
1384}
1385
1386auto model::submit_for_shadow_batching_cascaded(std::vector<shadow_batch_collector>& collectors,
1387 uint8_t cascade_count,
1388 const math::mat4& world_transform,
1389 const submesh_pose_mat4& submesh_transforms,
1390 uint32_t lod_index,
1391 float lod_param,
1392 const math::frustum* frustums,
1393 bool nested_cascades,
1394 const model_submit_extras& extras) const -> bool
1395{
1396 auto mesh_asset = get_lod(lod_index);
1397 if(!mesh_asset)
1398 {
1399 return false;
1400 }
1401 auto mesh = mesh_asset.get();
1402 if(!mesh)
1403 {
1404 return false;
1405 }
1406
1407 bool collected_any = false;
1408 auto collect_into_cascades =
1409 [&](const shadow_batch_key& key, uint32_t submesh_index, const math::mat4& transform, size_t instance_idx) -> void
1410 {
1411 for(uint8_t ii = 0; ii < cascade_count; ++ii)
1412 {
1413 const auto query = classify_submesh_cached(frustums[ii], extras, submesh_index, instance_idx, false);
1414 if(query == math::volume_query::outside)
1415 {
1416 continue;
1417 }
1418
1419 batch_instance instance(&transform);
1420 instance.lod_params.x = lod_param;
1421 collectors[ii].collect_renderable(key, instance);
1422 collected_any = true;
1423
1424 if(nested_cascades && query == math::volume_query::inside)
1425 {
1426 break;
1427 }
1428 }
1429 };
1430
1431 const auto data_group_count = mesh->get_data_groups_count();
1432 for(uint32_t data_group_id = 0; data_group_id < data_group_count; ++data_group_id)
1433 {
1434 auto group_material = get_material_instance(data_group_id);
1435
1436 const auto& submesh_indices = mesh->get_non_skinned_submeshes_indices(data_group_id, lod_index);
1437 for(size_t submesh_idx : submesh_indices)
1438 {
1439 const uint32_t submesh_index = static_cast<uint32_t>(submesh_idx);
1440
1441 const auto& material_ptr = resolve_submesh_material(extras, submesh_index, group_material);
1442 if(!material_ptr)
1443 {
1444 continue;
1445 }
1446
1447 shadow_batch_key key = make_shadow_batch_key(mesh, lod_index, submesh_index, material_ptr);
1448 if(!key.is_valid())
1449 {
1450 continue;
1451 }
1452
1453 if(submesh_transforms.has_transforms(submesh_index))
1454 {
1455 const size_t transform_count = submesh_transforms.get_transform_count(submesh_index);
1456 for(size_t instance_idx = 0; instance_idx < transform_count; ++instance_idx)
1457 {
1458 const math::mat4* transform_ptr = submesh_transforms.get_transform(submesh_index, instance_idx);
1459 if(!transform_ptr)
1460 {
1461 continue;
1462 }
1463 if(!submesh_transforms.get_transform_casts_shadow(submesh_index, instance_idx))
1464 {
1465 continue;
1466 }
1467 collect_into_cascades(key, submesh_index, *transform_ptr, instance_idx);
1468 }
1469 }
1470 else
1471 {
1472 collect_into_cascades(key, submesh_index, world_transform, 0);
1473 }
1474 }
1475 }
1476
1477 return collected_any;
1478}
1479
1480} // namespace unravel
Provides storage for common representation of spherical bounding volume, and wraps up common function...
Definition bsphere.h:18
Storage for frustum planes / values and wraps up common functionality.
Definition frustum.h:18
auto classify_aabb(const bbox &bounds) const -> volume_query
Classifies an axis-aligned bounding box (AABB) with respect to the frustum.
Definition frustum.cpp:227
General purpose transformation class designed to maintain each component of the transformation separa...
Definition transform.hpp:27
void collect_renderable(const Key &key, const batch_instance &instance)
Class representing a camera. Contains functionality for manipulating and updating a camera....
Definition camera.h:62
auto get_viewport_pos() const -> const ipoint32_t &
Retrieves the position of the viewport.
Definition camera.cpp:42
auto get_viewport_size() const -> const usize32_t &
Retrieves the size of the viewport.
Definition camera.cpp:37
Base class for materials used in rendering.
Definition material.h:44
std::shared_ptr< material > sptr
Definition material.h:48
Main class representing a 3D mesh with support for different LODs, submeshes, and skinning.
Definition mesh.h:323
auto get_hardware_vb() const -> std::shared_ptr< gfx::vertex_buffer >
Retrieves the hardware vertex buffer for the mesh (shared across all LODs).
Definition mesh.cpp:1612
void bind_render_buffers_for_submesh(const submesh *submesh, uint32_t lod_index=0)
Binds the mesh data for rendering the selected batch of primitives.
Definition mesh.cpp:4110
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_vertex_format() const -> const gfx::vertex_layout &
Retrieves the format of the underlying mesh vertex data.
Definition mesh.cpp:1607
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_non_skinned_submeshes_count(uint32_t lod_index=0) const -> size_t
Gets the number of non-skinned submeshes for this mesh.
Definition mesh.cpp:2687
auto get_skinned_submeshes_indices(uint32_t data_group_id, uint32_t lod_index=0) const -> const submesh_array_indices_t &
Gets the indices of skinned submeshes for a specific data group.
Definition mesh.cpp:2658
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_hardware_ib(uint32_t lod_index=0) const -> std::shared_ptr< gfx::index_buffer >
Retrieves the hardware index buffer for a given LOD.
Definition mesh.cpp:1617
auto get_non_skinned_submeshes_indices(uint32_t data_group_id, uint32_t lod_index=0) const -> const submesh_array_indices_t &
Gets the indices of non-skinned submeshes for a specific data group.
Definition mesh.cpp:2701
auto get_data_groups_count() const -> size_t
Gets the number of data groups(materials) for this mesh.
Definition mesh.cpp:2740
void submit_for_vertex_pulling(const math::mat4 &world_transform, const submesh_pose_mat4 &submesh_transforms, const std::vector< pose_mat4 > &skinning_transforms, unsigned int lod, const submit_vertex_pulling_callbacks &callbacks, const math::frustum *frustum=nullptr, const camera *view=nullptr, const model_submit_extras &extras={}) const
Submits the model using vertex-pulling rendering.
Definition model.cpp:990
auto get_material(uint32_t index) const -> asset_handle< material >
Gets the material for the specified index.
Definition model.cpp:298
void set_lod_override_enabled(bool enabled)
Sets whether LOD override is enabled.
Definition model.cpp:644
auto calculate_submesh_lod(const mesh &m, uint32_t submesh_index, uint32_t base_lod, const math::mat4 &world_matrix, const camera &cam) const -> uint32_t
Selects a LOD for a specific submesh based on its own screen size.
Definition model.cpp:523
auto get_lod_screen_size_min() const -> float
Gets the minimum screen size used by the screen-radius-squared LOD and culling method.
Definition model.cpp:689
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_lod_transition_time(seconds_t time)
Sets the LOD transition time in seconds.
Definition model.cpp:684
auto calculate_submesh_lod_from_world_bounds(const mesh &m, uint32_t submesh_index, uint32_t base_lod, const math::bbox &world_bounds, const camera &cam) const -> uint32_t
Selects a LOD for a submesh from an already-known world-space AABB.
Definition model.cpp:552
static auto fallback_material() -> asset_handle< material > &
Gets the fallback material.
Definition model.cpp:1248
void set_material(asset_handle< material > material, uint32_t index)
Sets the material for the specified index.
Definition model.cpp:220
auto get_lod_override_level() const -> uint32_t
Gets the LOD override level.
Definition model.cpp:649
void set_lod_screen_sizes(const std::vector< float > &sizes)
Sets the per-LOD screen size table used by the screen-radius-squared method.
Definition model.cpp:714
auto is_valid() const -> bool
Checks if the model is valid.
Definition model.cpp:174
void set_materials(const std::vector< asset_handle< material > > &materials)
Sets the materials.
Definition model.cpp:288
void set_material_instances(const std::vector< material::sptr > &materials)
Definition model.cpp:293
static auto default_material() -> asset_handle< material > &
Gets the default material.
Definition model.cpp:1242
void submit(const math::mat4 &world_transform, const submesh_pose_mat4 &submesh_transforms, const pose_mat4 &bone_transforms, const std::vector< pose_mat4 > &skinning_transforms, unsigned int lod, const submit_callbacks &callbacks, const math::frustum *frustum=nullptr, const camera *view=nullptr, const model_submit_extras &extras={}) const
Submits the model for rendering.
Definition model.cpp:720
void submit_for_batching(batch_collector &collector, const math::mat4 &world_transform, const submesh_pose_mat4 &submesh_transforms, uint32_t lod_index, float lod_param=0.0f, const math::frustum *frustum=nullptr, const camera *view=nullptr, const model_submit_extras &extras={}) const
Collects this model into a batch collector for instanced rendering.
Definition model.cpp:1254
auto get_material_instance(uint32_t index) const -> material::sptr
Definition model.cpp:308
void set_lod_auto_screen_size_power_base(float value)
Sets the auto LOD screen size power base (used for generating a screen-size table).
Definition model.cpp:704
void set_lod_screen_size_min(float value)
Sets the minimum screen size used by the screen-radius-squared LOD and culling method.
Definition model.cpp:694
auto get_lod_selection_bias() const -> float
Gets the LOD selection bias.
Definition model.cpp:659
auto get_material_instances() const -> const std::vector< material::sptr > &
Definition model.cpp:282
void set_lod_hysteresis(float hysteresis)
Sets the LOD hysteresis factor.
Definition model.cpp:674
std::chrono::duration< float > seconds_t
Definition model.h:279
void set_lod_selection_bias(float bias)
Sets the LOD selection bias.
Definition model.cpp:664
auto get_lod_override_enabled() const -> bool
Gets whether LOD override is enabled.
Definition model.cpp:639
auto submit_for_shadow_batching_cascaded(std::vector< shadow_batch_collector > &collectors, uint8_t cascade_count, const math::mat4 &world_transform, const submesh_pose_mat4 &submesh_transforms, uint32_t lod_index, float lod_param, const math::frustum *frustums, bool nested_cascades, const model_submit_extras &extras={}) const -> bool
Collects shadow-map geometry into per-cascade shadow batch collectors. Batches by mesh/lod/submesh/cu...
Definition model.cpp:1386
void set_material_instance(material::sptr material, uint32_t index)
Definition model.cpp:230
void set_lods(const std::vector< asset_handle< mesh > > &lods)
Sets the LOD meshes.
Definition model.cpp:264
auto calculate_lod_data(lod_data &data, const math::bbox &world_bounds, const camera &cam, float dt) const -> bool
Calculates the LOD data for the model using distance-based hysteresis with time-based transitions....
Definition model.cpp:360
auto get_lods_count() const -> uint32_t
Gets the number of LOD levels available. If there is only one explicit mesh, returns the internal LOD...
Definition model.cpp:245
auto get_lod_screen_sizes() const -> const std::vector< float > &
Gets the per-LOD screen size table used by the screen-radius-squared method.
Definition model.cpp:709
void set_lod_override_level(uint32_t level)
Sets the LOD override level.
Definition model.cpp:654
auto get_or_emplace_material_instance(uint32_t index) -> material::sptr
Definition model.cpp:328
void recalulate_lod_screen_size_limits(uint32_t lod_count)
Recalculates the screen-size LOD thresholds for the provided LOD count. This is a separate mechanism ...
Definition model.cpp:614
void set_lod(asset_handle< mesh > mesh, uint32_t lod)
Sets the LOD (Level of Detail) mesh for the specified level.
Definition model.cpp:200
auto get_lod_auto_screen_size_power_base() const -> float
Gets the auto LOD screen size power base (used for generating a screen-size table).
Definition model.cpp:699
auto get_materials() const -> const std::vector< asset_handle< material > > &
Gets all the materials.
Definition model.cpp:277
auto get_lod_hysteresis() const -> float
Gets the LOD hysteresis factor used to prevent rapid LOD switching.
Definition model.cpp:669
auto get_lods() const -> const std::vector< asset_handle< mesh > > &
Gets all the LOD meshes.
Definition model.cpp:240
auto get_lod_transition_time() const -> seconds_t
Gets the LOD transition time in seconds.
Definition model.cpp:679
auto compute_lod_index(const math::bbox &world_bounds, const camera &cam, float extra_bias=0.0f) const -> uint32_t
Computes a LOD index for this model without hysteresis, transitions or visibility culling.
Definition model.cpp:567
uint16_t view
uint16_t index
transform_snapshot pose
void set_world_transform(const void *_mtx, uint16_t _num)
void set_buffer(uint8_t _stage, index_buffer_handle _handle, access _access)
T square(const T &t)
Definition math.h:154
volume_query
Definition math_types.h:13
Hash specialization for batch_key to enable use in std::unordered_map.
auto make_shadow_batch_key(const std::shared_ptr< mesh > &mesh_ptr, uint32_t lod_index, uint32_t submesh_index, const std::shared_ptr< material > &material_ptr) -> shadow_batch_key
Build a shadow batch key from mesh geometry and optional material cutout metadata.
@ sphere
Sphere type reflection probe.
std::vector< float > scale
std::vector< uint32_t > indices
Thread-safe handle to an asset.
Storage for box vector values and wraps up common functionality.
Definition bbox.h:21
Instance data for a single object in a batch.
math::vec3 lod_params
LOD blending parameters (x = transition factor: +[0,1] = fade out, -[0,1] = fade in; y,...
Batch key structure for grouping compatible draw calls.
Definition batch_key.h:28
Contains level of detail (LOD) data for an entity per view. Uses distance-based hysteresis for stable...
Definition model.h:34
void calculate_screen_rect(const camera &cam)
Definition model.cpp:164
float percent
Percentage of the model visible (0.0 to 100.0).
Definition model.h:39
math::vec3 center
Center of the model in world space.
Definition model.h:41
Parameters for the submit callbacks.
Definition model.h:551
bool skinned
Indicates if the model is skinned.
Definition model.h:553
Callbacks for submitting the model for rendering.
Definition model.h:545
std::function< void(const params &info, const material &)> setup_params_per_submesh
Callback for setting up per submesh.
Definition model.h:562
std::function< void(const params &info)> setup_begin
Callback for setup begin.
Definition model.h:558
std::function< void(const params &info)> setup_params_per_instance
Callback for setting up per instance.
Definition model.h:560
std::function< void(const params &info)> setup_end
Callback for setup end.
Definition model.h:564
Per-invocation information for a vertex-pulling submesh submit.
Definition model.h:619
uint32_t position_offset_floats
Byte offset of the position attribute converted to floats.
Definition model.h:626
uint32_t index_count
Number of indices making up the submesh.
Definition model.h:624
bool preserve_state
Hint: mirror submit_callbacks::params::preserve_state.
Definition model.h:621
uint32_t vertex_stride_floats
Vertex stride expressed in float-sized elements.
Definition model.h:625
uint32_t indices_offset_floats
Byte offset of the bone indices attribute converted to floats.
Definition model.h:628
uint32_t submesh_index
Submesh index within the LOD mesh.
Definition model.h:622
bool skinned
True during the skinned pass, false during non-skinned.
Definition model.h:620
uint32_t index_start
Starting index of the submesh in the index buffer (in indices).
Definition model.h:623
uint32_t weight_offset_floats
Byte offset of the bone weight attribute converted to floats.
Definition model.h:627
Callbacks for submitting the model using vertex-pulling rendering.
Definition model.h:608
std::function< void(const params &info)> setup_params_per_instance
Called once per pass after setup_begin. Typically used to set instance-level uniforms.
Definition model.h:634
std::function< void(const params &info)> setup_params_per_submesh
Called once per submesh instance after u_world and the raw VB/IB have been bound.
Definition model.h:636
std::function< void(const params &info)> setup_begin
Called once per pass (once for non-skinned, once for skinned). Typically used to bind the program.
Definition model.h:632
std::function< void(const params &info)> setup_end
Called once per pass at the end. Typically used to end the program.
Definition model.h:638
Optional retained render data consumed by the model submit paths.
Definition model.h:254
Batch key for shadow depth passes — geometry-first, optional cutout bucket.
Definition batch_key.h:119
auto has_transforms(uint32_t submesh_index) const -> bool
Checks if a submesh has any transforms.
Definition model.h:186
auto get_transform_casts_shadow(uint32_t submesh_index, size_t instance_index) const -> bool
Checks if an instance casts shadows.
Definition model.h:217
auto get_transform_active(uint32_t submesh_index, size_t instance_index) const -> bool
Checks if a transform is active.
Definition model.h:198
auto get_transform(uint32_t submesh_index, size_t instance_index) const -> const math::mat4 *
Gets a specific transform for a submesh by its instance index.
Definition model.h:164
auto get_transform_count(uint32_t submesh_index) const -> size_t
Gets the number of transform instances for a specific submesh.
Definition model.h:149
bool enabled