From 69a4c52a815d1492f7a59d6d2b3064b48de11607 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:09:26 +0000 Subject: [PATCH 01/10] Initial plan From c926183f48c98c06fa3f164fe73bca19a0e95808 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:19:18 +0000 Subject: [PATCH 02/10] Add LOD (Level of Detail) system for mesh rendering New files: - render/lod/LodGroup.h: Core LOD data structures and screen-size calculation - render/lod/MeshLodGroup.h/.cpp: LOD group container for multiple mesh LOD levels - test/core/LodTest.cpp: Unit tests for LOD selection logic Modified files: - MeshRenderer.h/.cpp: Add LOD group support and UpdateLod method - MeshFeatureProcessor.h/.cpp: Add per-frame LOD evaluation from camera view - StaticMeshComponent.h/.cpp: Add LOD mesh configuration and bias settings Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- .../adaptor/components/StaticMeshComponent.h | 17 ++ .../src/components/StaticMeshComponent.cpp | 59 +++++- .../render/core/include/render/lod/LodGroup.h | 55 ++++++ .../core/include/render/lod/MeshLodGroup.h | 34 ++++ .../render/mesh/MeshFeatureProcessor.h | 6 + .../core/include/render/mesh/MeshRenderer.h | 8 + runtime/render/core/src/lod/MeshLodGroup.cpp | 26 +++ .../core/src/mesh/MeshFeatureProcessor.cpp | 24 +++ runtime/render/core/src/mesh/MeshRenderer.cpp | 31 ++++ test/core/LodTest.cpp | 172 ++++++++++++++++++ 10 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 runtime/render/core/include/render/lod/LodGroup.h create mode 100644 runtime/render/core/include/render/lod/MeshLodGroup.h create mode 100644 runtime/render/core/src/lod/MeshLodGroup.cpp create mode 100644 test/core/LodTest.cpp diff --git a/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h b/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h index 1b1583a0..3cc69572 100644 --- a/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h +++ b/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h @@ -9,9 +9,15 @@ #include #include #include +#include namespace sky { + struct LodMeshAssetData { + Uuid meshUuid; + float screenSize = 0.f; + }; + class StaticMeshComponent : public ComponentBase, public IAssetEvent { public: StaticMeshComponent() = default; @@ -40,11 +46,17 @@ namespace sky { void SetEnableMeshletConeDebug(bool enable); bool GetEnableMeshletConeDebug() const { return debugFlags.TestBit(MeshDebugFlagBit::MESHLET_CONE); } + void SetLodMeshes(const std::vector &lodMeshes); + const std::vector &GetLodMeshes() const { return lodMeshAssets; } + void SetLodBias(float bias); + float GetLodBias() const { return lodBias; } + void OnAttachToWorld() override; void OnDetachFromWorld() override; private: void ShutDown(); void BuildRenderer(); + void BuildLodGroup(); void OnAssetLoaded() override; @@ -59,6 +71,11 @@ namespace sky { RDMeshPtr meshInstance; MeshRenderer *renderer = nullptr; + std::vector lodMeshAssets; + std::vector lodMeshAssetPtrs; + RDMeshLodGroupPtr lodGroup; + float lodBias = 1.0f; + std::atomic_bool dirty = false; EventBinder binder; diff --git a/runtime/render/adaptor/src/components/StaticMeshComponent.cpp b/runtime/render/adaptor/src/components/StaticMeshComponent.cpp index 07b88fdb..8e431afa 100644 --- a/runtime/render/adaptor/src/components/StaticMeshComponent.cpp +++ b/runtime/render/adaptor/src/components/StaticMeshComponent.cpp @@ -39,6 +39,18 @@ namespace sky { ar.SaveValueObject(std::string("receiveShadow"), receiveShadow); ar.SaveValueObject(std::string("meshShading"), enableMeshShading); ar.SaveValueObject(std::string("mesh"), meshAsset ? meshAsset->GetUuid() : Uuid()); + ar.SaveValueObject(std::string("lodBias"), lodBias); + + ar.Key("lodMeshes"); + ar.StartArray(); + for (const auto &lod : lodMeshAssets) { + ar.StartObject(); + ar.SaveValueObject(std::string("mesh"), lod.meshUuid); + ar.SaveValueObject(std::string("screenSize"), lod.screenSize); + ar.EndObject(); + } + ar.EndArray(); + ar.EndObject(); } @@ -51,6 +63,7 @@ namespace sky { Uuid uuid; ar.LoadKeyValue("mesh", uuid); SetMeshUuid(uuid); + ar.LoadKeyValue("lodBias", lodBias); } void StaticMeshComponent::SetEnableMeshShading(bool enable) @@ -105,6 +118,43 @@ namespace sky { } } + void StaticMeshComponent::SetLodMeshes(const std::vector &lodMeshes) + { + lodMeshAssets = lodMeshes; + dirty.store(true); + } + + void StaticMeshComponent::SetLodBias(float bias) + { + lodBias = bias; + if (lodGroup) { + lodGroup->SetLodBias(lodBias); + } + } + + void StaticMeshComponent::BuildLodGroup() + { + if (lodMeshAssets.empty()) { + lodGroup = nullptr; + return; + } + + auto *am = AssetManager::Get(); + lodGroup = new MeshLodGroup(); + lodGroup->SetLodBias(lodBias); + lodMeshAssetPtrs.clear(); + + for (const auto &lodData : lodMeshAssets) { + auto asset = am->LoadAsset(lodData.meshUuid); + if (asset) { + asset->BlockUntilLoaded(); + auto meshPtr = CreateMeshFromAsset(asset); + lodGroup->AddLodMesh(meshPtr, lodData.screenSize); + lodMeshAssetPtrs.emplace_back(asset); + } + } + } + void StaticMeshComponent::BuildRenderer() { SKY_PROFILE_NAME("Build Static Render") @@ -121,7 +171,14 @@ namespace sky { } renderer = mf->CreateStaticMesh(); - renderer->SetMesh(meshInstance, enableMeshShading); + + BuildLodGroup(); + + if (lodGroup && lodGroup->GetLodCount() > 0) { + renderer->SetLodGroup(lodGroup); + } else { + renderer->SetMesh(meshInstance, enableMeshShading); + } } void StaticMeshComponent::ShutDown() diff --git a/runtime/render/core/include/render/lod/LodGroup.h b/runtime/render/core/include/render/lod/LodGroup.h new file mode 100644 index 00000000..a85d9582 --- /dev/null +++ b/runtime/render/core/include/render/lod/LodGroup.h @@ -0,0 +1,55 @@ +// +// Created by blues on 2025/2/16. +// + +#pragma once + +#include +#include +#include +#include +#include + +namespace sky { + + static constexpr uint32_t MAX_LOD_LEVEL = 8; + static constexpr uint32_t INVALID_LOD_LEVEL = ~(0U); + + struct LodLevel { + float screenSize = 0.f; + }; + + struct LodConfig { + std::vector lodLevels; + float lodBias = 1.0f; + }; + + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov, float screenHeight) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto extent = (worldBound.max - worldBound.min) * 0.5f; + float radius = extent.Length(); + + auto diff = center - viewPos; + float dist = diff.Length(); + + if (dist <= radius) { + return 1.0f; + } + + float screenRadius = radius / (dist * std::tan(fov * 0.5f)); + return screenRadius * 2.0f; + } + + inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) + { + float biasedSize = screenSize * config.lodBias; + for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { + if (biasedSize >= config.lodLevels[i].screenSize) { + return i; + } + } + return INVALID_LOD_LEVEL; + } + +} // namespace sky diff --git a/runtime/render/core/include/render/lod/MeshLodGroup.h b/runtime/render/core/include/render/lod/MeshLodGroup.h new file mode 100644 index 00000000..dee22e3c --- /dev/null +++ b/runtime/render/core/include/render/lod/MeshLodGroup.h @@ -0,0 +1,34 @@ +// +// Created by blues on 2025/2/16. +// + +#pragma once + +#include +#include + +namespace sky { + + class MeshLodGroup : public RenderResource { + public: + MeshLodGroup() = default; + ~MeshLodGroup() override = default; + + void AddLodMesh(const RDMeshPtr &mesh, float screenSize); + void SetLodBias(float bias); + + uint32_t GetLodCount() const { return static_cast(lodMeshes.size()); } + const RDMeshPtr &GetMesh(uint32_t lod) const { return lodMeshes[lod]; } + + uint32_t SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov, float screenHeight) const; + + const LodConfig &GetConfig() const { return config; } + + private: + LodConfig config; + std::vector lodMeshes; + }; + + using RDMeshLodGroupPtr = CounterPtr; + +} // namespace sky diff --git a/runtime/render/core/include/render/mesh/MeshFeatureProcessor.h b/runtime/render/core/include/render/mesh/MeshFeatureProcessor.h index 4d913ab7..867ea464 100644 --- a/runtime/render/core/include/render/mesh/MeshFeatureProcessor.h +++ b/runtime/render/core/include/render/mesh/MeshFeatureProcessor.h @@ -24,10 +24,16 @@ namespace sky { SkeletonMeshRenderer *CreateSkeletonMesh(); void RemoveSkeletonMesh(SkeletonMeshRenderer *mesh); + void SetMainViewName(const Name &name) { mainViewName = name; } + private: + void UpdateLod(); + std::list> staticMeshes; std::list> skeletonMeshes; + + Name mainViewName; }; } // namespace sky diff --git a/runtime/render/core/include/render/mesh/MeshRenderer.h b/runtime/render/core/include/render/mesh/MeshRenderer.h index 97eca5c9..d634ff36 100644 --- a/runtime/render/core/include/render/mesh/MeshRenderer.h +++ b/runtime/render/core/include/render/mesh/MeshRenderer.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -27,13 +28,17 @@ namespace sky { void Tick(); void AttachScene(RenderScene *scn); void SetMesh(const RDMeshPtr &mesh, bool meshShading = false); + void SetLodGroup(const RDMeshLodGroupPtr &lodGroup); void SetDebugFlags(const MeshDebugFlags& flag); void UpdateTransform(const Matrix4 &matrix); + void UpdateLod(const Vector3 &viewPos, float fov, float screenHeight); void BuildGeometry(); void SetMaterial(const RDMaterialInstancePtr &mat, uint32_t subMesh); + + uint32_t GetCurrentLod() const { return currentLod; } protected: virtual void PrepareUBO(); virtual RDResourceGroupPtr RequestResourceGroup(MeshFeature *feature); @@ -45,6 +50,9 @@ namespace sky { RenderScene *scene = nullptr; RDMeshPtr mesh; + RDMeshLodGroupPtr lodGroup; + uint32_t currentLod = 0; + std::vector> primitives; std::unique_ptr meshletDebug; std::vector meshletInfos; diff --git a/runtime/render/core/src/lod/MeshLodGroup.cpp b/runtime/render/core/src/lod/MeshLodGroup.cpp new file mode 100644 index 00000000..6e07a0c4 --- /dev/null +++ b/runtime/render/core/src/lod/MeshLodGroup.cpp @@ -0,0 +1,26 @@ +// +// Created by blues on 2025/2/16. +// + +#include + +namespace sky { + + void MeshLodGroup::AddLodMesh(const RDMeshPtr &mesh, float screenSize) + { + lodMeshes.emplace_back(mesh); + config.lodLevels.emplace_back(LodLevel{screenSize}); + } + + void MeshLodGroup::SetLodBias(float bias) + { + config.lodBias = bias; + } + + uint32_t MeshLodGroup::SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov, float screenHeight) const + { + float size = CalculateScreenSize(worldBound, viewPos, fov, screenHeight); + return SelectLodLevel(config, size); + } + +} // namespace sky diff --git a/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp b/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp index 851803a9..1b84f5ec 100644 --- a/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp +++ b/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp @@ -3,11 +3,16 @@ // #include +#include +#include +#include namespace sky { void MeshFeatureProcessor::Tick(float time) { + UpdateLod(); + for (auto &mesh : staticMeshes) { mesh->Tick(); } @@ -17,6 +22,25 @@ namespace sky { } } + void MeshFeatureProcessor::UpdateLod() + { + auto *view = scene->GetSceneView(mainViewName); + if (view == nullptr) { + return; + } + + const auto &worldMat = view->GetWorld(); + Vector3 viewPos(worldMat[3][0], worldMat[3][1], worldMat[3][2]); + + const auto &projMat = view->GetProject(); + float fov = 2.0f * std::atan(1.0f / projMat[1][1]); + float screenHeight = 1080.0f; + + for (auto &mesh : staticMeshes) { + mesh->UpdateLod(viewPos, fov, screenHeight); + } + } + void MeshFeatureProcessor::Render(rdg::RenderGraph &rdg) { } diff --git a/runtime/render/core/src/mesh/MeshRenderer.cpp b/runtime/render/core/src/mesh/MeshRenderer.cpp index 4cda00bb..c94bff00 100644 --- a/runtime/render/core/src/mesh/MeshRenderer.cpp +++ b/runtime/render/core/src/mesh/MeshRenderer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -146,6 +147,36 @@ namespace sky { } } + void MeshRenderer::SetLodGroup(const RDMeshLodGroupPtr &lodGroup_) + { + lodGroup = lodGroup_; + currentLod = 0; + if (lodGroup && lodGroup->GetLodCount() > 0) { + SetMesh(lodGroup->GetMesh(0), enableMeshShading); + } + } + + void MeshRenderer::UpdateLod(const Vector3 &viewPos, float fov, float screenHeight) + { + if (!lodGroup || lodGroup->GetLodCount() == 0 || primitives.empty()) { + return; + } + + const auto &worldBound = primitives[0]->worldBound; + uint32_t newLod = lodGroup->SelectLod(worldBound, viewPos, fov, screenHeight); + + if (newLod == INVALID_LOD_LEVEL) { + newLod = lodGroup->GetLodCount() - 1; + } + + if (newLod != currentLod && newLod < lodGroup->GetLodCount()) { + currentLod = newLod; + mesh = lodGroup->GetMesh(currentLod); + Reset(); + BuildGeometry(); + } + } + void MeshRenderer::SetMesh(const RDMeshPtr &mesh_, bool meshShading) { mesh = mesh_; diff --git a/test/core/LodTest.cpp b/test/core/LodTest.cpp new file mode 100644 index 00000000..5ac7c27c --- /dev/null +++ b/test/core/LodTest.cpp @@ -0,0 +1,172 @@ +// +// Created by blues on 2025/2/16. +// + +#include +#include +#include + +// Inline LOD functions for testing (duplicated from render/lod/LodGroup.h to avoid render dependency) +namespace sky { +namespace lod_test { + + static constexpr uint32_t INVALID_LOD_LEVEL = ~(0U); + + struct LodLevel { + float screenSize = 0.f; + }; + + struct LodConfig { + std::vector lodLevels; + float lodBias = 1.0f; + }; + + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov, float /*screenHeight*/) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto extent = (worldBound.max - worldBound.min) * 0.5f; + float radius = extent.Length(); + + auto diff = center - viewPos; + float dist = diff.Length(); + + if (dist <= radius) { + return 1.0f; + } + + float screenRadius = radius / (dist * std::tan(fov * 0.5f)); + return screenRadius * 2.0f; + } + + inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) + { + float biasedSize = screenSize * config.lodBias; + for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { + if (biasedSize >= config.lodLevels[i].screenSize) { + return i; + } + } + return INVALID_LOD_LEVEL; + } + +} // namespace lod_test +} // namespace sky + +using namespace sky; +using namespace sky::lod_test; + +TEST(LodTest, ScreenSizeCalculation) +{ + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + float fov = ToRadian(90.0f); + + // Camera at the object center → screen size should be 1.0 + { + Vector3 viewPos(0, 0, 0); + float size = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + ASSERT_FLOAT_EQ(size, 1.0f); + } + + // Camera far away → screen size should be small + { + Vector3 viewPos(0, 0, 100); + float size = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + ASSERT_GT(size, 0.0f); + ASSERT_LT(size, 0.1f); + } + + // Camera nearby → screen size should be larger + { + Vector3 viewPos(0, 0, 5); + float sizeNear = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + Vector3 viewPosFar(0, 0, 50); + float sizeFar = CalculateScreenSize(bound, viewPosFar, fov, 1080.0f); + ASSERT_GT(sizeNear, sizeFar); + } +} + +TEST(LodTest, SelectLodLevelBasic) +{ + LodConfig config; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.5f}, // LOD 0: screenSize >= 0.5 + LodLevel{0.25f}, // LOD 1: screenSize >= 0.25 + LodLevel{0.1f}, // LOD 2: screenSize >= 0.1 + LodLevel{0.0f}, // LOD 3: screenSize >= 0.0 (always matches as fallback) + }; + + // Large screen size → should select LOD 0 + ASSERT_EQ(SelectLodLevel(config, 0.8f), 0u); + + // Medium screen size → should select LOD 1 + ASSERT_EQ(SelectLodLevel(config, 0.3f), 1u); + + // Small screen size → should select LOD 2 + ASSERT_EQ(SelectLodLevel(config, 0.15f), 2u); + + // Very small screen size → should select LOD 3 + ASSERT_EQ(SelectLodLevel(config, 0.05f), 3u); + + // Exact boundary → should select that LOD + ASSERT_EQ(SelectLodLevel(config, 0.5f), 0u); + ASSERT_EQ(SelectLodLevel(config, 0.25f), 1u); + ASSERT_EQ(SelectLodLevel(config, 0.1f), 2u); +} + +TEST(LodTest, SelectLodLevelWithBias) +{ + LodConfig config; + config.lodLevels = { + LodLevel{0.5f}, + LodLevel{0.25f}, + LodLevel{0.1f}, + }; + + // Normal bias (1.0) + config.lodBias = 1.0f; + ASSERT_EQ(SelectLodLevel(config, 0.3f), 1u); + + // Higher bias (2.0) makes effective screen size larger, selecting higher quality LOD + config.lodBias = 2.0f; + ASSERT_EQ(SelectLodLevel(config, 0.3f), 0u); // 0.3 * 2.0 = 0.6 >= 0.5 + + // Lower bias (0.5) makes effective screen size smaller, selecting lower quality LOD + config.lodBias = 0.5f; + ASSERT_EQ(SelectLodLevel(config, 0.3f), 2u); // 0.3 * 0.5 = 0.15 >= 0.1 +} + +TEST(LodTest, SelectLodLevelEmpty) +{ + LodConfig config; + config.lodBias = 1.0f; + + // Empty config → should return INVALID + ASSERT_EQ(SelectLodLevel(config, 0.5f), INVALID_LOD_LEVEL); +} + +TEST(LodTest, SelectLodLevelSingleLevel) +{ + LodConfig config; + config.lodBias = 1.0f; + config.lodLevels = {LodLevel{0.0f}}; + + // Single level with 0 threshold → always matches + ASSERT_EQ(SelectLodLevel(config, 0.5f), 0u); + ASSERT_EQ(SelectLodLevel(config, 0.01f), 0u); + ASSERT_EQ(SelectLodLevel(config, 0.0f), 0u); +} + +TEST(LodTest, ScreenSizeDecreasesWithDistance) +{ + AABB bound(Vector3(-2, -2, -2), Vector3(2, 2, 2)); + float fov = ToRadian(60.0f); + + float prevSize = 2.0f; + for (float dist = 10.0f; dist <= 100.0f; dist += 10.0f) { + Vector3 viewPos(0, 0, dist); + float size = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + ASSERT_LT(size, prevSize); + prevSize = size; + } +} From eddb68559a6f112355e2eaf71f16c729ea53f339 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:21:15 +0000 Subject: [PATCH 03/10] Address code review: remove unused param, compute combined bounds for LOD - Remove unused screenHeight parameter from LOD calculation API - Compute combined AABB across all primitives for LOD selection - Remove hardcoded 1080.0f screen height from MeshFeatureProcessor Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- runtime/render/core/include/render/lod/LodGroup.h | 2 +- .../render/core/include/render/lod/MeshLodGroup.h | 2 +- .../render/core/include/render/mesh/MeshRenderer.h | 2 +- runtime/render/core/src/lod/MeshLodGroup.cpp | 4 ++-- .../render/core/src/mesh/MeshFeatureProcessor.cpp | 3 +-- runtime/render/core/src/mesh/MeshRenderer.cpp | 10 +++++++--- test/core/LodTest.cpp | 12 ++++++------ 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/runtime/render/core/include/render/lod/LodGroup.h b/runtime/render/core/include/render/lod/LodGroup.h index a85d9582..072e5342 100644 --- a/runtime/render/core/include/render/lod/LodGroup.h +++ b/runtime/render/core/include/render/lod/LodGroup.h @@ -24,7 +24,7 @@ namespace sky { float lodBias = 1.0f; }; - inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov, float screenHeight) + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) { auto center = (worldBound.min + worldBound.max) * 0.5f; auto extent = (worldBound.max - worldBound.min) * 0.5f; diff --git a/runtime/render/core/include/render/lod/MeshLodGroup.h b/runtime/render/core/include/render/lod/MeshLodGroup.h index dee22e3c..c7b57fde 100644 --- a/runtime/render/core/include/render/lod/MeshLodGroup.h +++ b/runtime/render/core/include/render/lod/MeshLodGroup.h @@ -20,7 +20,7 @@ namespace sky { uint32_t GetLodCount() const { return static_cast(lodMeshes.size()); } const RDMeshPtr &GetMesh(uint32_t lod) const { return lodMeshes[lod]; } - uint32_t SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov, float screenHeight) const; + uint32_t SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov) const; const LodConfig &GetConfig() const { return config; } diff --git a/runtime/render/core/include/render/mesh/MeshRenderer.h b/runtime/render/core/include/render/mesh/MeshRenderer.h index d634ff36..e117bf12 100644 --- a/runtime/render/core/include/render/mesh/MeshRenderer.h +++ b/runtime/render/core/include/render/mesh/MeshRenderer.h @@ -32,7 +32,7 @@ namespace sky { void SetDebugFlags(const MeshDebugFlags& flag); void UpdateTransform(const Matrix4 &matrix); - void UpdateLod(const Vector3 &viewPos, float fov, float screenHeight); + void UpdateLod(const Vector3 &viewPos, float fov); void BuildGeometry(); diff --git a/runtime/render/core/src/lod/MeshLodGroup.cpp b/runtime/render/core/src/lod/MeshLodGroup.cpp index 6e07a0c4..593b4fc2 100644 --- a/runtime/render/core/src/lod/MeshLodGroup.cpp +++ b/runtime/render/core/src/lod/MeshLodGroup.cpp @@ -17,9 +17,9 @@ namespace sky { config.lodBias = bias; } - uint32_t MeshLodGroup::SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov, float screenHeight) const + uint32_t MeshLodGroup::SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov) const { - float size = CalculateScreenSize(worldBound, viewPos, fov, screenHeight); + float size = CalculateScreenSize(worldBound, viewPos, fov); return SelectLodLevel(config, size); } diff --git a/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp b/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp index 1b84f5ec..e1da8565 100644 --- a/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp +++ b/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp @@ -34,10 +34,9 @@ namespace sky { const auto &projMat = view->GetProject(); float fov = 2.0f * std::atan(1.0f / projMat[1][1]); - float screenHeight = 1080.0f; for (auto &mesh : staticMeshes) { - mesh->UpdateLod(viewPos, fov, screenHeight); + mesh->UpdateLod(viewPos, fov); } } diff --git a/runtime/render/core/src/mesh/MeshRenderer.cpp b/runtime/render/core/src/mesh/MeshRenderer.cpp index c94bff00..0e098840 100644 --- a/runtime/render/core/src/mesh/MeshRenderer.cpp +++ b/runtime/render/core/src/mesh/MeshRenderer.cpp @@ -156,14 +156,18 @@ namespace sky { } } - void MeshRenderer::UpdateLod(const Vector3 &viewPos, float fov, float screenHeight) + void MeshRenderer::UpdateLod(const Vector3 &viewPos, float fov) { if (!lodGroup || lodGroup->GetLodCount() == 0 || primitives.empty()) { return; } - const auto &worldBound = primitives[0]->worldBound; - uint32_t newLod = lodGroup->SelectLod(worldBound, viewPos, fov, screenHeight); + AABB combinedBound = primitives[0]->worldBound; + for (uint32_t i = 1; i < static_cast(primitives.size()); ++i) { + Merge(combinedBound, primitives[i]->worldBound, combinedBound); + } + + uint32_t newLod = lodGroup->SelectLod(combinedBound, viewPos, fov); if (newLod == INVALID_LOD_LEVEL) { newLod = lodGroup->GetLodCount() - 1; diff --git a/test/core/LodTest.cpp b/test/core/LodTest.cpp index 5ac7c27c..64df8af0 100644 --- a/test/core/LodTest.cpp +++ b/test/core/LodTest.cpp @@ -21,7 +21,7 @@ namespace lod_test { float lodBias = 1.0f; }; - inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov, float /*screenHeight*/) + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) { auto center = (worldBound.min + worldBound.max) * 0.5f; auto extent = (worldBound.max - worldBound.min) * 0.5f; @@ -63,14 +63,14 @@ TEST(LodTest, ScreenSizeCalculation) // Camera at the object center → screen size should be 1.0 { Vector3 viewPos(0, 0, 0); - float size = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + float size = CalculateScreenSize(bound, viewPos, fov); ASSERT_FLOAT_EQ(size, 1.0f); } // Camera far away → screen size should be small { Vector3 viewPos(0, 0, 100); - float size = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + float size = CalculateScreenSize(bound, viewPos, fov); ASSERT_GT(size, 0.0f); ASSERT_LT(size, 0.1f); } @@ -78,9 +78,9 @@ TEST(LodTest, ScreenSizeCalculation) // Camera nearby → screen size should be larger { Vector3 viewPos(0, 0, 5); - float sizeNear = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + float sizeNear = CalculateScreenSize(bound, viewPos, fov); Vector3 viewPosFar(0, 0, 50); - float sizeFar = CalculateScreenSize(bound, viewPosFar, fov, 1080.0f); + float sizeFar = CalculateScreenSize(bound, viewPosFar, fov); ASSERT_GT(sizeNear, sizeFar); } } @@ -165,7 +165,7 @@ TEST(LodTest, ScreenSizeDecreasesWithDistance) float prevSize = 2.0f; for (float dist = 10.0f; dist <= 100.0f; dist += 10.0f) { Vector3 viewPos(0, 0, dist); - float size = CalculateScreenSize(bound, viewPos, fov, 1080.0f); + float size = CalculateScreenSize(bound, viewPos, fov); ASSERT_LT(size, prevSize); prevSize = size; } From 3714f8a3e4a362ea7ebebc58ba45cdf6143f13ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:21:43 +0000 Subject: [PATCH 04/10] Final LOD system implementation Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- _codeql_detected_source_root | 1 + 1 file changed, 1 insertion(+) create mode 120000 _codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 00000000..945c9b46 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file From 1f0021dc983497b1bd6a05aa398f371ccc4c7f7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:22:07 +0000 Subject: [PATCH 05/10] Remove codeql artifact and update .gitignore Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- .gitignore | 1 + _codeql_detected_source_root | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 120000 _codeql_detected_source_root diff --git a/.gitignore b/.gitignore index 9309668d..a38f0748 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /active_project /test/assets/assets.db /test/products/ +_codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root deleted file mode 120000 index 945c9b46..00000000 --- a/_codeql_detected_source_root +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file From bfded016e7635a94018aab680bcfba1757a05aba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:01:20 +0000 Subject: [PATCH 06/10] Add LOD policy support for distance-based and screen-size based selection - Add LodPolicy enum (SCREEN_SIZE, DISTANCE) to LodGroup.h - Add distance field to LodLevel struct - Add CalculateDistance() for distance-based LOD - Add SelectLodByDistance() and SelectLodByScreenSize() functions - Add SelectLodLevel() overload that dispatches by policy - Update MeshLodGroup with AddLodMesh(mesh, screenSize, distance) overload - Update MeshLodGroup with SetLodPolicy() method - Update StaticMeshComponent with LodPolicy support and serialization - Add comprehensive unit tests for distance-based LOD selection Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- .../adaptor/components/StaticMeshComponent.h | 4 + .../src/components/StaticMeshComponent.cpp | 16 +- .../render/core/include/render/lod/LodGroup.h | 42 ++++- .../core/include/render/lod/MeshLodGroup.h | 2 + runtime/render/core/src/lod/MeshLodGroup.cpp | 16 +- test/core/LodTest.cpp | 157 +++++++++++++++++- 6 files changed, 231 insertions(+), 6 deletions(-) diff --git a/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h b/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h index 3cc69572..fc008614 100644 --- a/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h +++ b/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h @@ -16,6 +16,7 @@ namespace sky { struct LodMeshAssetData { Uuid meshUuid; float screenSize = 0.f; + float distance = 0.f; }; class StaticMeshComponent : public ComponentBase, public IAssetEvent { @@ -50,6 +51,8 @@ namespace sky { const std::vector &GetLodMeshes() const { return lodMeshAssets; } void SetLodBias(float bias); float GetLodBias() const { return lodBias; } + void SetLodPolicy(LodPolicy policy); + LodPolicy GetLodPolicy() const { return lodPolicy; } void OnAttachToWorld() override; void OnDetachFromWorld() override; @@ -75,6 +78,7 @@ namespace sky { std::vector lodMeshAssetPtrs; RDMeshLodGroupPtr lodGroup; float lodBias = 1.0f; + LodPolicy lodPolicy = LodPolicy::SCREEN_SIZE; std::atomic_bool dirty = false; diff --git a/runtime/render/adaptor/src/components/StaticMeshComponent.cpp b/runtime/render/adaptor/src/components/StaticMeshComponent.cpp index 8e431afa..7edae460 100644 --- a/runtime/render/adaptor/src/components/StaticMeshComponent.cpp +++ b/runtime/render/adaptor/src/components/StaticMeshComponent.cpp @@ -40,6 +40,7 @@ namespace sky { ar.SaveValueObject(std::string("meshShading"), enableMeshShading); ar.SaveValueObject(std::string("mesh"), meshAsset ? meshAsset->GetUuid() : Uuid()); ar.SaveValueObject(std::string("lodBias"), lodBias); + ar.SaveValueObject(std::string("lodPolicy"), static_cast(lodPolicy)); ar.Key("lodMeshes"); ar.StartArray(); @@ -47,6 +48,7 @@ namespace sky { ar.StartObject(); ar.SaveValueObject(std::string("mesh"), lod.meshUuid); ar.SaveValueObject(std::string("screenSize"), lod.screenSize); + ar.SaveValueObject(std::string("distance"), lod.distance); ar.EndObject(); } ar.EndArray(); @@ -64,6 +66,9 @@ namespace sky { ar.LoadKeyValue("mesh", uuid); SetMeshUuid(uuid); ar.LoadKeyValue("lodBias", lodBias); + uint32_t policyVal = 0; + ar.LoadKeyValue("lodPolicy", policyVal); + lodPolicy = static_cast(policyVal); } void StaticMeshComponent::SetEnableMeshShading(bool enable) @@ -132,6 +137,14 @@ namespace sky { } } + void StaticMeshComponent::SetLodPolicy(LodPolicy policy) + { + lodPolicy = policy; + if (lodGroup) { + lodGroup->SetLodPolicy(lodPolicy); + } + } + void StaticMeshComponent::BuildLodGroup() { if (lodMeshAssets.empty()) { @@ -142,6 +155,7 @@ namespace sky { auto *am = AssetManager::Get(); lodGroup = new MeshLodGroup(); lodGroup->SetLodBias(lodBias); + lodGroup->SetLodPolicy(lodPolicy); lodMeshAssetPtrs.clear(); for (const auto &lodData : lodMeshAssets) { @@ -149,7 +163,7 @@ namespace sky { if (asset) { asset->BlockUntilLoaded(); auto meshPtr = CreateMeshFromAsset(asset); - lodGroup->AddLodMesh(meshPtr, lodData.screenSize); + lodGroup->AddLodMesh(meshPtr, lodData.screenSize, lodData.distance); lodMeshAssetPtrs.emplace_back(asset); } } diff --git a/runtime/render/core/include/render/lod/LodGroup.h b/runtime/render/core/include/render/lod/LodGroup.h index 072e5342..c59a906e 100644 --- a/runtime/render/core/include/render/lod/LodGroup.h +++ b/runtime/render/core/include/render/lod/LodGroup.h @@ -15,13 +15,20 @@ namespace sky { static constexpr uint32_t MAX_LOD_LEVEL = 8; static constexpr uint32_t INVALID_LOD_LEVEL = ~(0U); + enum class LodPolicy : uint32_t { + SCREEN_SIZE = 0, + DISTANCE + }; + struct LodLevel { float screenSize = 0.f; + float distance = 0.f; }; struct LodConfig { std::vector lodLevels; float lodBias = 1.0f; + LodPolicy policy = LodPolicy::SCREEN_SIZE; }; inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) @@ -41,7 +48,14 @@ namespace sky { return screenRadius * 2.0f; } - inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) + inline float CalculateDistance(const AABB &worldBound, const Vector3 &viewPos) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto diff = center - viewPos; + return diff.Length(); + } + + inline uint32_t SelectLodByScreenSize(const LodConfig &config, float screenSize) { float biasedSize = screenSize * config.lodBias; for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { @@ -52,4 +66,30 @@ namespace sky { return INVALID_LOD_LEVEL; } + inline uint32_t SelectLodByDistance(const LodConfig &config, float distance) + { + float biasedDist = distance / std::max(config.lodBias, 0.001f); + for (uint32_t i = static_cast(config.lodLevels.size()); i > 0; --i) { + if (biasedDist >= config.lodLevels[i - 1].distance) { + return i - 1; + } + } + return 0; + } + + inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) + { + return SelectLodByScreenSize(config, screenSize); + } + + inline uint32_t SelectLodLevel(const LodConfig &config, const AABB &worldBound, const Vector3 &viewPos, float fov) + { + if (config.policy == LodPolicy::DISTANCE) { + float dist = CalculateDistance(worldBound, viewPos); + return SelectLodByDistance(config, dist); + } + float size = CalculateScreenSize(worldBound, viewPos, fov); + return SelectLodByScreenSize(config, size); + } + } // namespace sky diff --git a/runtime/render/core/include/render/lod/MeshLodGroup.h b/runtime/render/core/include/render/lod/MeshLodGroup.h index c7b57fde..9fec07d4 100644 --- a/runtime/render/core/include/render/lod/MeshLodGroup.h +++ b/runtime/render/core/include/render/lod/MeshLodGroup.h @@ -15,7 +15,9 @@ namespace sky { ~MeshLodGroup() override = default; void AddLodMesh(const RDMeshPtr &mesh, float screenSize); + void AddLodMesh(const RDMeshPtr &mesh, float screenSize, float distance); void SetLodBias(float bias); + void SetLodPolicy(LodPolicy policy); uint32_t GetLodCount() const { return static_cast(lodMeshes.size()); } const RDMeshPtr &GetMesh(uint32_t lod) const { return lodMeshes[lod]; } diff --git a/runtime/render/core/src/lod/MeshLodGroup.cpp b/runtime/render/core/src/lod/MeshLodGroup.cpp index 593b4fc2..7bd856f9 100644 --- a/runtime/render/core/src/lod/MeshLodGroup.cpp +++ b/runtime/render/core/src/lod/MeshLodGroup.cpp @@ -9,7 +9,13 @@ namespace sky { void MeshLodGroup::AddLodMesh(const RDMeshPtr &mesh, float screenSize) { lodMeshes.emplace_back(mesh); - config.lodLevels.emplace_back(LodLevel{screenSize}); + config.lodLevels.emplace_back(LodLevel{screenSize, 0.f}); + } + + void MeshLodGroup::AddLodMesh(const RDMeshPtr &mesh, float screenSize, float distance) + { + lodMeshes.emplace_back(mesh); + config.lodLevels.emplace_back(LodLevel{screenSize, distance}); } void MeshLodGroup::SetLodBias(float bias) @@ -17,10 +23,14 @@ namespace sky { config.lodBias = bias; } + void MeshLodGroup::SetLodPolicy(LodPolicy policy) + { + config.policy = policy; + } + uint32_t MeshLodGroup::SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov) const { - float size = CalculateScreenSize(worldBound, viewPos, fov); - return SelectLodLevel(config, size); + return SelectLodLevel(config, worldBound, viewPos, fov); } } // namespace sky diff --git a/test/core/LodTest.cpp b/test/core/LodTest.cpp index 64df8af0..4e3b81f7 100644 --- a/test/core/LodTest.cpp +++ b/test/core/LodTest.cpp @@ -12,13 +12,20 @@ namespace lod_test { static constexpr uint32_t INVALID_LOD_LEVEL = ~(0U); + enum class LodPolicy : uint32_t { + SCREEN_SIZE = 0, + DISTANCE + }; + struct LodLevel { float screenSize = 0.f; + float distance = 0.f; }; struct LodConfig { std::vector lodLevels; float lodBias = 1.0f; + LodPolicy policy = LodPolicy::SCREEN_SIZE; }; inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) @@ -38,7 +45,14 @@ namespace lod_test { return screenRadius * 2.0f; } - inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) + inline float CalculateDistance(const AABB &worldBound, const Vector3 &viewPos) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto diff = center - viewPos; + return diff.Length(); + } + + inline uint32_t SelectLodByScreenSize(const LodConfig &config, float screenSize) { float biasedSize = screenSize * config.lodBias; for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { @@ -49,6 +63,32 @@ namespace lod_test { return INVALID_LOD_LEVEL; } + inline uint32_t SelectLodByDistance(const LodConfig &config, float distance) + { + float biasedDist = distance / std::max(config.lodBias, 0.001f); + for (uint32_t i = static_cast(config.lodLevels.size()); i > 0; --i) { + if (biasedDist >= config.lodLevels[i - 1].distance) { + return i - 1; + } + } + return 0; + } + + inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) + { + return SelectLodByScreenSize(config, screenSize); + } + + inline uint32_t SelectLodLevel(const LodConfig &config, const AABB &worldBound, const Vector3 &viewPos, float fov) + { + if (config.policy == LodPolicy::DISTANCE) { + float dist = CalculateDistance(worldBound, viewPos); + return SelectLodByDistance(config, dist); + } + float size = CalculateScreenSize(worldBound, viewPos, fov); + return SelectLodByScreenSize(config, size); + } + } // namespace lod_test } // namespace sky @@ -170,3 +210,118 @@ TEST(LodTest, ScreenSizeDecreasesWithDistance) prevSize = size; } } + +// ===== Distance-based LOD policy tests ===== + +TEST(LodTest, DistanceCalculation) +{ + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + + // Camera at the object center → distance should be 0 + { + Vector3 viewPos(0, 0, 0); + float dist = CalculateDistance(bound, viewPos); + ASSERT_FLOAT_EQ(dist, 0.0f); + } + + // Camera at known position + { + Vector3 viewPos(0, 0, 10); + float dist = CalculateDistance(bound, viewPos); + ASSERT_FLOAT_EQ(dist, 10.0f); + } + + // Distance increases monotonically + { + float prevDist = 0.0f; + for (float d = 10.0f; d <= 100.0f; d += 10.0f) { + Vector3 viewPos(0, 0, d); + float dist = CalculateDistance(bound, viewPos); + ASSERT_GT(dist, prevDist); + prevDist = dist; + } + } +} + +TEST(LodTest, SelectLodByDistanceBasic) +{ + // Distance thresholds: LOD 0 (close), LOD 1, LOD 2 (far) + // Higher LOD index = lower quality = used at greater distance + LodConfig config; + config.policy = LodPolicy::DISTANCE; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.0f, 0.f}, // LOD 0: distance >= 0 (closest) + LodLevel{0.0f, 50.f}, // LOD 1: distance >= 50 + LodLevel{0.0f, 100.f}, // LOD 2: distance >= 100 + }; + + // Close → LOD 0 + ASSERT_EQ(SelectLodByDistance(config, 10.f), 0u); + + // Medium distance → LOD 1 + ASSERT_EQ(SelectLodByDistance(config, 60.f), 1u); + + // Far → LOD 2 + ASSERT_EQ(SelectLodByDistance(config, 150.f), 2u); + + // Exact boundaries + ASSERT_EQ(SelectLodByDistance(config, 0.f), 0u); + ASSERT_EQ(SelectLodByDistance(config, 50.f), 1u); + ASSERT_EQ(SelectLodByDistance(config, 100.f), 2u); +} + +TEST(LodTest, SelectLodByDistanceWithBias) +{ + LodConfig config; + config.policy = LodPolicy::DISTANCE; + config.lodLevels = { + LodLevel{0.0f, 0.f}, + LodLevel{0.0f, 50.f}, + LodLevel{0.0f, 100.f}, + }; + + // Higher bias → distances are effectively shorter → keeps higher quality longer + config.lodBias = 2.0f; + ASSERT_EQ(SelectLodByDistance(config, 60.f), 0u); // 60 / 2.0 = 30 < 50 → LOD 0 + + // Lower bias → distances are effectively longer → switches to lower quality sooner + config.lodBias = 0.5f; + ASSERT_EQ(SelectLodByDistance(config, 60.f), 2u); // 60 / 0.5 = 120 >= 100 → LOD 2 +} + +TEST(LodTest, PolicyDispatch) +{ + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + Vector3 viewPos(0, 0, 60); + float fov = ToRadian(90.0f); + + // Screen-size policy + { + LodConfig config; + config.policy = LodPolicy::SCREEN_SIZE; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.5f, 0.f}, + LodLevel{0.0f, 0.f}, + }; + uint32_t lod = SelectLodLevel(config, bound, viewPos, fov); + // Screen size at dist 60 is small → should fall through to LOD 1 + ASSERT_EQ(lod, 1u); + } + + // Distance policy + { + LodConfig config; + config.policy = LodPolicy::DISTANCE; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.0f, 0.f}, + LodLevel{0.0f, 50.f}, + LodLevel{0.0f, 100.f}, + }; + uint32_t lod = SelectLodLevel(config, bound, viewPos, fov); + // Distance ~60 → LOD 1 + ASSERT_EQ(lod, 1u); + } +} From eb105f0fea6113440c6377339db64ea63bdd045f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:27:10 +0000 Subject: [PATCH 07/10] Pre-compute distance thresholds from screen-size for runtime LOD selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the dual LodPolicy (SCREEN_SIZE/DISTANCE) system with a unified approach: users author LOD thresholds as screen-size values, which are pre-computed into squared-distance thresholds. At runtime, LOD selection only needs a cheap distance² comparison instead of expensive screen-size calculation (which requires tan, division, sqrt). Key changes: - Add ScreenSizeToDistance() conversion and PreComputeDistances() - Add CalculateDistanceSq() for squared-distance (avoids sqrt) - SelectLodLevel(config, worldBound, viewPos) uses pre-computed distances - Remove LodPolicy enum, distance field from LodLevel - MeshLodGroup::PreComputeDistances(radius, fov) pre-computes cache - MeshRenderer::UpdateLod() no longer needs fov parameter - MeshFeatureProcessor::UpdateLod() no longer extracts FOV from projection - StaticMeshComponent simplified: removes policy/distance fields - Tests verify pre-computed distances match screen-size LOD selection Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- .../adaptor/components/StaticMeshComponent.h | 4 - .../src/components/StaticMeshComponent.cpp | 16 +- .../render/core/include/render/lod/LodGroup.h | 79 +++--- .../core/include/render/lod/MeshLodGroup.h | 5 +- .../core/include/render/mesh/MeshRenderer.h | 2 +- runtime/render/core/src/lod/MeshLodGroup.cpp | 16 +- .../core/src/mesh/MeshFeatureProcessor.cpp | 5 +- runtime/render/core/src/mesh/MeshRenderer.cpp | 4 +- test/core/LodTest.cpp | 248 +++++++++++------- 9 files changed, 206 insertions(+), 173 deletions(-) diff --git a/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h b/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h index fc008614..3cc69572 100644 --- a/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h +++ b/runtime/render/adaptor/include/render/adaptor/components/StaticMeshComponent.h @@ -16,7 +16,6 @@ namespace sky { struct LodMeshAssetData { Uuid meshUuid; float screenSize = 0.f; - float distance = 0.f; }; class StaticMeshComponent : public ComponentBase, public IAssetEvent { @@ -51,8 +50,6 @@ namespace sky { const std::vector &GetLodMeshes() const { return lodMeshAssets; } void SetLodBias(float bias); float GetLodBias() const { return lodBias; } - void SetLodPolicy(LodPolicy policy); - LodPolicy GetLodPolicy() const { return lodPolicy; } void OnAttachToWorld() override; void OnDetachFromWorld() override; @@ -78,7 +75,6 @@ namespace sky { std::vector lodMeshAssetPtrs; RDMeshLodGroupPtr lodGroup; float lodBias = 1.0f; - LodPolicy lodPolicy = LodPolicy::SCREEN_SIZE; std::atomic_bool dirty = false; diff --git a/runtime/render/adaptor/src/components/StaticMeshComponent.cpp b/runtime/render/adaptor/src/components/StaticMeshComponent.cpp index 7edae460..8e431afa 100644 --- a/runtime/render/adaptor/src/components/StaticMeshComponent.cpp +++ b/runtime/render/adaptor/src/components/StaticMeshComponent.cpp @@ -40,7 +40,6 @@ namespace sky { ar.SaveValueObject(std::string("meshShading"), enableMeshShading); ar.SaveValueObject(std::string("mesh"), meshAsset ? meshAsset->GetUuid() : Uuid()); ar.SaveValueObject(std::string("lodBias"), lodBias); - ar.SaveValueObject(std::string("lodPolicy"), static_cast(lodPolicy)); ar.Key("lodMeshes"); ar.StartArray(); @@ -48,7 +47,6 @@ namespace sky { ar.StartObject(); ar.SaveValueObject(std::string("mesh"), lod.meshUuid); ar.SaveValueObject(std::string("screenSize"), lod.screenSize); - ar.SaveValueObject(std::string("distance"), lod.distance); ar.EndObject(); } ar.EndArray(); @@ -66,9 +64,6 @@ namespace sky { ar.LoadKeyValue("mesh", uuid); SetMeshUuid(uuid); ar.LoadKeyValue("lodBias", lodBias); - uint32_t policyVal = 0; - ar.LoadKeyValue("lodPolicy", policyVal); - lodPolicy = static_cast(policyVal); } void StaticMeshComponent::SetEnableMeshShading(bool enable) @@ -137,14 +132,6 @@ namespace sky { } } - void StaticMeshComponent::SetLodPolicy(LodPolicy policy) - { - lodPolicy = policy; - if (lodGroup) { - lodGroup->SetLodPolicy(lodPolicy); - } - } - void StaticMeshComponent::BuildLodGroup() { if (lodMeshAssets.empty()) { @@ -155,7 +142,6 @@ namespace sky { auto *am = AssetManager::Get(); lodGroup = new MeshLodGroup(); lodGroup->SetLodBias(lodBias); - lodGroup->SetLodPolicy(lodPolicy); lodMeshAssetPtrs.clear(); for (const auto &lodData : lodMeshAssets) { @@ -163,7 +149,7 @@ namespace sky { if (asset) { asset->BlockUntilLoaded(); auto meshPtr = CreateMeshFromAsset(asset); - lodGroup->AddLodMesh(meshPtr, lodData.screenSize, lodData.distance); + lodGroup->AddLodMesh(meshPtr, lodData.screenSize); lodMeshAssetPtrs.emplace_back(asset); } } diff --git a/runtime/render/core/include/render/lod/LodGroup.h b/runtime/render/core/include/render/lod/LodGroup.h index c59a906e..97307d2d 100644 --- a/runtime/render/core/include/render/lod/LodGroup.h +++ b/runtime/render/core/include/render/lod/LodGroup.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -15,22 +16,48 @@ namespace sky { static constexpr uint32_t MAX_LOD_LEVEL = 8; static constexpr uint32_t INVALID_LOD_LEVEL = ~(0U); - enum class LodPolicy : uint32_t { - SCREEN_SIZE = 0, - DISTANCE - }; - struct LodLevel { float screenSize = 0.f; - float distance = 0.f; }; struct LodConfig { std::vector lodLevels; + std::vector distancesSq; float lodBias = 1.0f; - LodPolicy policy = LodPolicy::SCREEN_SIZE; }; + inline float ScreenSizeToDistance(float screenSize, float radius, float halfTanFov) + { + if (screenSize <= 0.f) { + return std::numeric_limits::max(); + } + return (2.0f * radius) / (screenSize * halfTanFov); + } + + inline void PreComputeDistances(LodConfig &config, float radius, float fov) + { + float halfTanFov = std::tan(fov * 0.5f); + config.distancesSq.resize(config.lodLevels.size()); + for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { + float d = ScreenSizeToDistance(config.lodLevels[i].screenSize, radius, halfTanFov); + config.distancesSq[i] = d * d; + } + } + + inline float CalculateDistance(const AABB &worldBound, const Vector3 &viewPos) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto diff = center - viewPos; + return diff.Length(); + } + + inline float CalculateDistanceSq(const AABB &worldBound, const Vector3 &viewPos) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto diff = center - viewPos; + return diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; + } + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) { auto center = (worldBound.min + worldBound.max) * 0.5f; @@ -48,13 +75,6 @@ namespace sky { return screenRadius * 2.0f; } - inline float CalculateDistance(const AABB &worldBound, const Vector3 &viewPos) - { - auto center = (worldBound.min + worldBound.max) * 0.5f; - auto diff = center - viewPos; - return diff.Length(); - } - inline uint32_t SelectLodByScreenSize(const LodConfig &config, float screenSize) { float biasedSize = screenSize * config.lodBias; @@ -66,30 +86,27 @@ namespace sky { return INVALID_LOD_LEVEL; } - inline uint32_t SelectLodByDistance(const LodConfig &config, float distance) - { - float biasedDist = distance / std::max(config.lodBias, 0.001f); - for (uint32_t i = static_cast(config.lodLevels.size()); i > 0; --i) { - if (biasedDist >= config.lodLevels[i - 1].distance) { - return i - 1; - } - } - return 0; - } - inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) { return SelectLodByScreenSize(config, screenSize); } - inline uint32_t SelectLodLevel(const LodConfig &config, const AABB &worldBound, const Vector3 &viewPos, float fov) + inline uint32_t SelectLodLevel(const LodConfig &config, const AABB &worldBound, const Vector3 &viewPos) { - if (config.policy == LodPolicy::DISTANCE) { - float dist = CalculateDistance(worldBound, viewPos); - return SelectLodByDistance(config, dist); + if (config.distancesSq.empty()) { + return INVALID_LOD_LEVEL; + } + + float distSq = CalculateDistanceSq(worldBound, viewPos); + float biasInv = 1.0f / std::max(config.lodBias, 0.001f); + float biasedDistSq = distSq * biasInv * biasInv; + + for (uint32_t i = 0; i < static_cast(config.distancesSq.size()); ++i) { + if (biasedDistSq < config.distancesSq[i]) { + return i; + } } - float size = CalculateScreenSize(worldBound, viewPos, fov); - return SelectLodByScreenSize(config, size); + return INVALID_LOD_LEVEL; } } // namespace sky diff --git a/runtime/render/core/include/render/lod/MeshLodGroup.h b/runtime/render/core/include/render/lod/MeshLodGroup.h index 9fec07d4..567b7938 100644 --- a/runtime/render/core/include/render/lod/MeshLodGroup.h +++ b/runtime/render/core/include/render/lod/MeshLodGroup.h @@ -15,14 +15,13 @@ namespace sky { ~MeshLodGroup() override = default; void AddLodMesh(const RDMeshPtr &mesh, float screenSize); - void AddLodMesh(const RDMeshPtr &mesh, float screenSize, float distance); void SetLodBias(float bias); - void SetLodPolicy(LodPolicy policy); + void PreComputeDistances(float radius, float fov); uint32_t GetLodCount() const { return static_cast(lodMeshes.size()); } const RDMeshPtr &GetMesh(uint32_t lod) const { return lodMeshes[lod]; } - uint32_t SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov) const; + uint32_t SelectLod(const AABB &worldBound, const Vector3 &viewPos) const; const LodConfig &GetConfig() const { return config; } diff --git a/runtime/render/core/include/render/mesh/MeshRenderer.h b/runtime/render/core/include/render/mesh/MeshRenderer.h index e117bf12..0f6d345b 100644 --- a/runtime/render/core/include/render/mesh/MeshRenderer.h +++ b/runtime/render/core/include/render/mesh/MeshRenderer.h @@ -32,7 +32,7 @@ namespace sky { void SetDebugFlags(const MeshDebugFlags& flag); void UpdateTransform(const Matrix4 &matrix); - void UpdateLod(const Vector3 &viewPos, float fov); + void UpdateLod(const Vector3 &viewPos); void BuildGeometry(); diff --git a/runtime/render/core/src/lod/MeshLodGroup.cpp b/runtime/render/core/src/lod/MeshLodGroup.cpp index 7bd856f9..8cf14897 100644 --- a/runtime/render/core/src/lod/MeshLodGroup.cpp +++ b/runtime/render/core/src/lod/MeshLodGroup.cpp @@ -9,13 +9,7 @@ namespace sky { void MeshLodGroup::AddLodMesh(const RDMeshPtr &mesh, float screenSize) { lodMeshes.emplace_back(mesh); - config.lodLevels.emplace_back(LodLevel{screenSize, 0.f}); - } - - void MeshLodGroup::AddLodMesh(const RDMeshPtr &mesh, float screenSize, float distance) - { - lodMeshes.emplace_back(mesh); - config.lodLevels.emplace_back(LodLevel{screenSize, distance}); + config.lodLevels.emplace_back(LodLevel{screenSize}); } void MeshLodGroup::SetLodBias(float bias) @@ -23,14 +17,14 @@ namespace sky { config.lodBias = bias; } - void MeshLodGroup::SetLodPolicy(LodPolicy policy) + void MeshLodGroup::PreComputeDistances(float radius, float fov) { - config.policy = policy; + sky::PreComputeDistances(config, radius, fov); } - uint32_t MeshLodGroup::SelectLod(const AABB &worldBound, const Vector3 &viewPos, float fov) const + uint32_t MeshLodGroup::SelectLod(const AABB &worldBound, const Vector3 &viewPos) const { - return SelectLodLevel(config, worldBound, viewPos, fov); + return SelectLodLevel(config, worldBound, viewPos); } } // namespace sky diff --git a/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp b/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp index e1da8565..617b709b 100644 --- a/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp +++ b/runtime/render/core/src/mesh/MeshFeatureProcessor.cpp @@ -32,11 +32,8 @@ namespace sky { const auto &worldMat = view->GetWorld(); Vector3 viewPos(worldMat[3][0], worldMat[3][1], worldMat[3][2]); - const auto &projMat = view->GetProject(); - float fov = 2.0f * std::atan(1.0f / projMat[1][1]); - for (auto &mesh : staticMeshes) { - mesh->UpdateLod(viewPos, fov); + mesh->UpdateLod(viewPos); } } diff --git a/runtime/render/core/src/mesh/MeshRenderer.cpp b/runtime/render/core/src/mesh/MeshRenderer.cpp index 0e098840..6599176c 100644 --- a/runtime/render/core/src/mesh/MeshRenderer.cpp +++ b/runtime/render/core/src/mesh/MeshRenderer.cpp @@ -156,7 +156,7 @@ namespace sky { } } - void MeshRenderer::UpdateLod(const Vector3 &viewPos, float fov) + void MeshRenderer::UpdateLod(const Vector3 &viewPos) { if (!lodGroup || lodGroup->GetLodCount() == 0 || primitives.empty()) { return; @@ -167,7 +167,7 @@ namespace sky { Merge(combinedBound, primitives[i]->worldBound, combinedBound); } - uint32_t newLod = lodGroup->SelectLod(combinedBound, viewPos, fov); + uint32_t newLod = lodGroup->SelectLod(combinedBound, viewPos); if (newLod == INVALID_LOD_LEVEL) { newLod = lodGroup->GetLodCount() - 1; diff --git a/test/core/LodTest.cpp b/test/core/LodTest.cpp index 4e3b81f7..aae14fb0 100644 --- a/test/core/LodTest.cpp +++ b/test/core/LodTest.cpp @@ -12,22 +12,41 @@ namespace lod_test { static constexpr uint32_t INVALID_LOD_LEVEL = ~(0U); - enum class LodPolicy : uint32_t { - SCREEN_SIZE = 0, - DISTANCE - }; - struct LodLevel { float screenSize = 0.f; - float distance = 0.f; }; struct LodConfig { std::vector lodLevels; + std::vector distancesSq; float lodBias = 1.0f; - LodPolicy policy = LodPolicy::SCREEN_SIZE; }; + inline float ScreenSizeToDistance(float screenSize, float radius, float halfTanFov) + { + if (screenSize <= 0.f) { + return std::numeric_limits::max(); + } + return (2.0f * radius) / (screenSize * halfTanFov); + } + + inline void PreComputeDistances(LodConfig &config, float radius, float fov) + { + float halfTanFov = std::tan(fov * 0.5f); + config.distancesSq.resize(config.lodLevels.size()); + for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { + float d = ScreenSizeToDistance(config.lodLevels[i].screenSize, radius, halfTanFov); + config.distancesSq[i] = d * d; + } + } + + inline float CalculateDistanceSq(const AABB &worldBound, const Vector3 &viewPos) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto diff = center - viewPos; + return diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; + } + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) { auto center = (worldBound.min + worldBound.max) * 0.5f; @@ -45,13 +64,6 @@ namespace lod_test { return screenRadius * 2.0f; } - inline float CalculateDistance(const AABB &worldBound, const Vector3 &viewPos) - { - auto center = (worldBound.min + worldBound.max) * 0.5f; - auto diff = center - viewPos; - return diff.Length(); - } - inline uint32_t SelectLodByScreenSize(const LodConfig &config, float screenSize) { float biasedSize = screenSize * config.lodBias; @@ -63,30 +75,27 @@ namespace lod_test { return INVALID_LOD_LEVEL; } - inline uint32_t SelectLodByDistance(const LodConfig &config, float distance) - { - float biasedDist = distance / std::max(config.lodBias, 0.001f); - for (uint32_t i = static_cast(config.lodLevels.size()); i > 0; --i) { - if (biasedDist >= config.lodLevels[i - 1].distance) { - return i - 1; - } - } - return 0; - } - inline uint32_t SelectLodLevel(const LodConfig &config, float screenSize) { return SelectLodByScreenSize(config, screenSize); } - inline uint32_t SelectLodLevel(const LodConfig &config, const AABB &worldBound, const Vector3 &viewPos, float fov) + inline uint32_t SelectLodLevel(const LodConfig &config, const AABB &worldBound, const Vector3 &viewPos) { - if (config.policy == LodPolicy::DISTANCE) { - float dist = CalculateDistance(worldBound, viewPos); - return SelectLodByDistance(config, dist); + if (config.distancesSq.empty()) { + return INVALID_LOD_LEVEL; } - float size = CalculateScreenSize(worldBound, viewPos, fov); - return SelectLodByScreenSize(config, size); + + float distSq = CalculateDistanceSq(worldBound, viewPos); + float biasInv = 1.0f / std::max(config.lodBias, 0.001f); + float biasedDistSq = distSq * biasInv * biasInv; + + for (uint32_t i = 0; i < static_cast(config.distancesSq.size()); ++i) { + if (biasedDistSq < config.distancesSq[i]) { + return i; + } + } + return INVALID_LOD_LEVEL; } } // namespace lod_test @@ -211,117 +220,152 @@ TEST(LodTest, ScreenSizeDecreasesWithDistance) } } -// ===== Distance-based LOD policy tests ===== +// ===== Pre-computed distance LOD tests ===== -TEST(LodTest, DistanceCalculation) +TEST(LodTest, ScreenSizeToDistanceConversion) { - AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + float radius = 1.7320508f; // sqrt(3) ~ extent of unit AABB + float fov = ToRadian(90.0f); + float halfTanFov = std::tan(fov * 0.5f); // tan(45°) = 1.0 - // Camera at the object center → distance should be 0 + // screenSize = 2*radius / (dist * halfTanFov) + // dist = 2*radius / (screenSize * halfTanFov) { - Vector3 viewPos(0, 0, 0); - float dist = CalculateDistance(bound, viewPos); - ASSERT_FLOAT_EQ(dist, 0.0f); + float d = ScreenSizeToDistance(0.5f, radius, halfTanFov); + float expected = (2.0f * radius) / (0.5f * halfTanFov); + ASSERT_FLOAT_EQ(d, expected); } - // Camera at known position + // screenSize 0 → infinite distance { - Vector3 viewPos(0, 0, 10); - float dist = CalculateDistance(bound, viewPos); - ASSERT_FLOAT_EQ(dist, 10.0f); + float d = ScreenSizeToDistance(0.0f, radius, halfTanFov); + ASSERT_EQ(d, std::numeric_limits::max()); } - // Distance increases monotonically + // Smaller screenSize → larger distance { - float prevDist = 0.0f; - for (float d = 10.0f; d <= 100.0f; d += 10.0f) { - Vector3 viewPos(0, 0, d); - float dist = CalculateDistance(bound, viewPos); - ASSERT_GT(dist, prevDist); - prevDist = dist; - } + float d1 = ScreenSizeToDistance(0.5f, radius, halfTanFov); + float d2 = ScreenSizeToDistance(0.25f, radius, halfTanFov); + ASSERT_GT(d2, d1); } } -TEST(LodTest, SelectLodByDistanceBasic) +TEST(LodTest, PreComputeDistancesMatchesScreenSize) { - // Distance thresholds: LOD 0 (close), LOD 1, LOD 2 (far) - // Higher LOD index = lower quality = used at greater distance + // Verify that pre-computed distance-based LOD selection matches + // screen-size-based LOD selection for various camera distances + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + auto extent = (bound.max - bound.min) * 0.5f; + float radius = extent.Length(); + float fov = ToRadian(90.0f); + LodConfig config; - config.policy = LodPolicy::DISTANCE; config.lodBias = 1.0f; config.lodLevels = { - LodLevel{0.0f, 0.f}, // LOD 0: distance >= 0 (closest) - LodLevel{0.0f, 50.f}, // LOD 1: distance >= 50 - LodLevel{0.0f, 100.f}, // LOD 2: distance >= 100 + LodLevel{0.5f}, + LodLevel{0.25f}, + LodLevel{0.1f}, + LodLevel{0.0f}, }; - // Close → LOD 0 - ASSERT_EQ(SelectLodByDistance(config, 10.f), 0u); + PreComputeDistances(config, radius, fov); - // Medium distance → LOD 1 - ASSERT_EQ(SelectLodByDistance(config, 60.f), 1u); + // Test at various distances: both methods should agree + for (float dist = 5.0f; dist <= 200.0f; dist += 5.0f) { + Vector3 viewPos(0, 0, dist); + float screenSize = CalculateScreenSize(bound, viewPos, fov); - // Far → LOD 2 - ASSERT_EQ(SelectLodByDistance(config, 150.f), 2u); + uint32_t lodByScreen = SelectLodByScreenSize(config, screenSize); + uint32_t lodByDist = SelectLodLevel(config, bound, viewPos); - // Exact boundaries - ASSERT_EQ(SelectLodByDistance(config, 0.f), 0u); - ASSERT_EQ(SelectLodByDistance(config, 50.f), 1u); - ASSERT_EQ(SelectLodByDistance(config, 100.f), 2u); + // For the fallback LOD (screenSize=0 → infinite distance), distance-based + // returns INVALID while screen-size returns 3. Both are valid. + if (lodByScreen == 3u && lodByDist == INVALID_LOD_LEVEL) { + continue; // screenSize=0 maps to infinite distance, this is expected + } + ASSERT_EQ(lodByScreen, lodByDist) + << "Mismatch at distance " << dist + << " (screenSize=" << screenSize << ")"; + } } -TEST(LodTest, SelectLodByDistanceWithBias) +TEST(LodTest, PreComputeDistancesWithBias) { + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + auto extent = (bound.max - bound.min) * 0.5f; + float radius = extent.Length(); + float fov = ToRadian(90.0f); + LodConfig config; - config.policy = LodPolicy::DISTANCE; config.lodLevels = { - LodLevel{0.0f, 0.f}, - LodLevel{0.0f, 50.f}, - LodLevel{0.0f, 100.f}, + LodLevel{0.5f}, + LodLevel{0.25f}, + LodLevel{0.1f}, }; + PreComputeDistances(config, radius, fov); + + Vector3 viewPos(0, 0, 10); - // Higher bias → distances are effectively shorter → keeps higher quality longer + // Normal bias + config.lodBias = 1.0f; + uint32_t lodNormal = SelectLodLevel(config, bound, viewPos); + + // Higher bias → should select higher quality (lower LOD index) config.lodBias = 2.0f; - ASSERT_EQ(SelectLodByDistance(config, 60.f), 0u); // 60 / 2.0 = 30 < 50 → LOD 0 + uint32_t lodHigh = SelectLodLevel(config, bound, viewPos); + ASSERT_LE(lodHigh, lodNormal); - // Lower bias → distances are effectively longer → switches to lower quality sooner + // Lower bias → should select lower quality (higher LOD index) config.lodBias = 0.5f; - ASSERT_EQ(SelectLodByDistance(config, 60.f), 2u); // 60 / 0.5 = 120 >= 100 → LOD 2 + uint32_t lodLow = SelectLodLevel(config, bound, viewPos); + ASSERT_GE(lodLow, lodNormal); } -TEST(LodTest, PolicyDispatch) +TEST(LodTest, PreComputeDistancesEmpty) { + LodConfig config; + config.lodBias = 1.0f; + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); - Vector3 viewPos(0, 0, 60); - float fov = ToRadian(90.0f); + Vector3 viewPos(0, 0, 10); + + // No pre-computed distances → should return INVALID + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), INVALID_LOD_LEVEL); +} + +TEST(LodTest, RuntimeSelectionOnlyUsesDistance) +{ + // Verify that runtime selection only needs distance (no fov/tan) + // by confirming the pre-computed approach works consistently + AABB bound(Vector3(-2, -2, -2), Vector3(2, 2, 2)); + auto extent = (bound.max - bound.min) * 0.5f; + float radius = extent.Length(); + float fov = ToRadian(60.0f); - // Screen-size policy + LodConfig config; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.6f}, + LodLevel{0.3f}, + LodLevel{0.1f}, + }; + PreComputeDistances(config, radius, fov); + + // Close camera → LOD 0 + { + Vector3 viewPos(0, 0, 5); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 0u); + } + + // Medium distance → LOD 1 { - LodConfig config; - config.policy = LodPolicy::SCREEN_SIZE; - config.lodBias = 1.0f; - config.lodLevels = { - LodLevel{0.5f, 0.f}, - LodLevel{0.0f, 0.f}, - }; - uint32_t lod = SelectLodLevel(config, bound, viewPos, fov); - // Screen size at dist 60 is small → should fall through to LOD 1 - ASSERT_EQ(lod, 1u); + Vector3 viewPos(0, 0, 20); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 1u); } - // Distance policy + // Far distance → LOD 2 { - LodConfig config; - config.policy = LodPolicy::DISTANCE; - config.lodBias = 1.0f; - config.lodLevels = { - LodLevel{0.0f, 0.f}, - LodLevel{0.0f, 50.f}, - LodLevel{0.0f, 100.f}, - }; - uint32_t lod = SelectLodLevel(config, bound, viewPos, fov); - // Distance ~60 → LOD 1 - ASSERT_EQ(lod, 1u); + Vector3 viewPos(0, 0, 80); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 2u); } } From 1b5e63aa2bdb72cb7f27ce13017a0dd6126042df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:28:04 +0000 Subject: [PATCH 08/10] Remove unused CalculateDistance() function (replaced by CalculateDistanceSq) Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- runtime/render/core/include/render/lod/LodGroup.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/runtime/render/core/include/render/lod/LodGroup.h b/runtime/render/core/include/render/lod/LodGroup.h index 97307d2d..94667093 100644 --- a/runtime/render/core/include/render/lod/LodGroup.h +++ b/runtime/render/core/include/render/lod/LodGroup.h @@ -44,13 +44,6 @@ namespace sky { } } - inline float CalculateDistance(const AABB &worldBound, const Vector3 &viewPos) - { - auto center = (worldBound.min + worldBound.max) * 0.5f; - auto diff = center - viewPos; - return diff.Length(); - } - inline float CalculateDistanceSq(const AABB &worldBound, const Vector3 &viewPos) { auto center = (worldBound.min + worldBound.max) * 0.5f; From 66fb1ac8632db5427c1cd88e67da64bc894f23d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:08:48 +0000 Subject: [PATCH 09/10] Adopt UE's ComputeBoundsScreenSize approach for LOD screen-size computation Replace FOV-based screen-size calculation with UE's projection-matrix-based approach using ComputeBoundsScreenSize: - GetScreenMultiple(projMatrix): extracts max(0.5*M[0][0], 0.5*M[1][1]) - ComputeBoundsScreenSize(radius, dist, screenMultiple): UE-style formula - CalculateScreenSize now takes Matrix4 projMatrix instead of float fov - ScreenSizeToDistance uses screenMultiple instead of halfTanFov - PreComputeDistances takes Matrix4 projMatrix instead of float fov - MeshLodGroup::PreComputeDistances updated to take Matrix4 projMatrix - Tests updated with MakePerspective and new aspect ratio test Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- .../render/core/include/render/lod/LodGroup.h | 59 ++++--- .../core/include/render/lod/MeshLodGroup.h | 2 +- runtime/render/core/src/lod/MeshLodGroup.cpp | 4 +- test/core/LodTest.cpp | 156 ++++++++++++------ 4 files changed, 141 insertions(+), 80 deletions(-) diff --git a/runtime/render/core/include/render/lod/LodGroup.h b/runtime/render/core/include/render/lod/LodGroup.h index 94667093..cfea9ab7 100644 --- a/runtime/render/core/include/render/lod/LodGroup.h +++ b/runtime/render/core/include/render/lod/LodGroup.h @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace sky { @@ -26,20 +27,51 @@ namespace sky { float lodBias = 1.0f; }; - inline float ScreenSizeToDistance(float screenSize, float radius, float halfTanFov) + // UE-style: extract screen-size multiplier from projection matrix + // ScreenMultiple = max(0.5 * ProjMatrix[0][0], 0.5 * ProjMatrix[1][1]) + inline float GetScreenMultiple(const Matrix4 &projMatrix) + { + return std::max(0.5f * projMatrix[0][0], 0.5f * projMatrix[1][1]); + } + + // UE-style: ComputeBoundsScreenSize + // ScreenSize = 2 * ScreenMultiple * SphereRadius / max(1, Distance) + inline float ComputeBoundsScreenSize(float sphereRadius, float dist, float screenMultiple) + { + float screenRadius = screenMultiple * sphereRadius / std::max(1.0f, dist); + return screenRadius * 2.0f; + } + + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, const Matrix4 &projMatrix) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto extent = (worldBound.max - worldBound.min) * 0.5f; + float radius = extent.Length(); + + auto diff = center - viewPos; + float dist = diff.Length(); + + float screenMultiple = GetScreenMultiple(projMatrix); + return ComputeBoundsScreenSize(radius, dist, screenMultiple); + } + + // Convert a screen-size threshold to a distance threshold + // From: ScreenSize = 2 * ScreenMultiple * SphereRadius / Distance + // Solving: Distance = 2 * ScreenMultiple * SphereRadius / ScreenSize + inline float ScreenSizeToDistance(float screenSize, float sphereRadius, float screenMultiple) { if (screenSize <= 0.f) { return std::numeric_limits::max(); } - return (2.0f * radius) / (screenSize * halfTanFov); + return (2.0f * screenMultiple * sphereRadius) / screenSize; } - inline void PreComputeDistances(LodConfig &config, float radius, float fov) + inline void PreComputeDistances(LodConfig &config, float sphereRadius, const Matrix4 &projMatrix) { - float halfTanFov = std::tan(fov * 0.5f); + float screenMultiple = GetScreenMultiple(projMatrix); config.distancesSq.resize(config.lodLevels.size()); for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { - float d = ScreenSizeToDistance(config.lodLevels[i].screenSize, radius, halfTanFov); + float d = ScreenSizeToDistance(config.lodLevels[i].screenSize, sphereRadius, screenMultiple); config.distancesSq[i] = d * d; } } @@ -51,23 +83,6 @@ namespace sky { return diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; } - inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) - { - auto center = (worldBound.min + worldBound.max) * 0.5f; - auto extent = (worldBound.max - worldBound.min) * 0.5f; - float radius = extent.Length(); - - auto diff = center - viewPos; - float dist = diff.Length(); - - if (dist <= radius) { - return 1.0f; - } - - float screenRadius = radius / (dist * std::tan(fov * 0.5f)); - return screenRadius * 2.0f; - } - inline uint32_t SelectLodByScreenSize(const LodConfig &config, float screenSize) { float biasedSize = screenSize * config.lodBias; diff --git a/runtime/render/core/include/render/lod/MeshLodGroup.h b/runtime/render/core/include/render/lod/MeshLodGroup.h index 567b7938..eb608e2f 100644 --- a/runtime/render/core/include/render/lod/MeshLodGroup.h +++ b/runtime/render/core/include/render/lod/MeshLodGroup.h @@ -16,7 +16,7 @@ namespace sky { void AddLodMesh(const RDMeshPtr &mesh, float screenSize); void SetLodBias(float bias); - void PreComputeDistances(float radius, float fov); + void PreComputeDistances(float sphereRadius, const Matrix4 &projMatrix); uint32_t GetLodCount() const { return static_cast(lodMeshes.size()); } const RDMeshPtr &GetMesh(uint32_t lod) const { return lodMeshes[lod]; } diff --git a/runtime/render/core/src/lod/MeshLodGroup.cpp b/runtime/render/core/src/lod/MeshLodGroup.cpp index 8cf14897..9f1da30c 100644 --- a/runtime/render/core/src/lod/MeshLodGroup.cpp +++ b/runtime/render/core/src/lod/MeshLodGroup.cpp @@ -17,9 +17,9 @@ namespace sky { config.lodBias = bias; } - void MeshLodGroup::PreComputeDistances(float radius, float fov) + void MeshLodGroup::PreComputeDistances(float sphereRadius, const Matrix4 &projMatrix) { - sky::PreComputeDistances(config, radius, fov); + sky::PreComputeDistances(config, sphereRadius, projMatrix); } uint32_t MeshLodGroup::SelectLod(const AABB &worldBound, const Vector3 &viewPos) const diff --git a/test/core/LodTest.cpp b/test/core/LodTest.cpp index aae14fb0..6840458b 100644 --- a/test/core/LodTest.cpp +++ b/test/core/LodTest.cpp @@ -22,20 +22,46 @@ namespace lod_test { float lodBias = 1.0f; }; - inline float ScreenSizeToDistance(float screenSize, float radius, float halfTanFov) + // UE-style: extract screen-size multiplier from projection matrix + inline float GetScreenMultiple(const Matrix4 &projMatrix) + { + return std::max(0.5f * projMatrix[0][0], 0.5f * projMatrix[1][1]); + } + + // UE-style: ComputeBoundsScreenSize + inline float ComputeBoundsScreenSize(float sphereRadius, float dist, float screenMultiple) + { + float screenRadius = screenMultiple * sphereRadius / std::max(1.0f, dist); + return screenRadius * 2.0f; + } + + inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, const Matrix4 &projMatrix) + { + auto center = (worldBound.min + worldBound.max) * 0.5f; + auto extent = (worldBound.max - worldBound.min) * 0.5f; + float radius = extent.Length(); + + auto diff = center - viewPos; + float dist = diff.Length(); + + float screenMultiple = GetScreenMultiple(projMatrix); + return ComputeBoundsScreenSize(radius, dist, screenMultiple); + } + + inline float ScreenSizeToDistance(float screenSize, float sphereRadius, float screenMultiple) { if (screenSize <= 0.f) { return std::numeric_limits::max(); } - return (2.0f * radius) / (screenSize * halfTanFov); + return (2.0f * screenMultiple * sphereRadius) / screenSize; } - inline void PreComputeDistances(LodConfig &config, float radius, float fov) + inline void PreComputeDistances(LodConfig &config, float sphereRadius, const Matrix4 &projMatrix) { - float halfTanFov = std::tan(fov * 0.5f); + float screenMultiple = GetScreenMultiple(projMatrix); config.distancesSq.resize(config.lodLevels.size()); for (uint32_t i = 0; i < static_cast(config.lodLevels.size()); ++i) { - float d = ScreenSizeToDistance(config.lodLevels[i].screenSize, radius, halfTanFov); + float d = ScreenSizeToDistance(config.lodLevels[i].screenSize, sphereRadius, screenMultiple); config.distancesSq[i] = d * d; } } @@ -47,23 +73,6 @@ namespace lod_test { return diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; } - inline float CalculateScreenSize(const AABB &worldBound, const Vector3 &viewPos, float fov) - { - auto center = (worldBound.min + worldBound.max) * 0.5f; - auto extent = (worldBound.max - worldBound.min) * 0.5f; - float radius = extent.Length(); - - auto diff = center - viewPos; - float dist = diff.Length(); - - if (dist <= radius) { - return 1.0f; - } - - float screenRadius = radius / (dist * std::tan(fov * 0.5f)); - return screenRadius * 2.0f; - } - inline uint32_t SelectLodByScreenSize(const LodConfig &config, float screenSize) { float biasedSize = screenSize * config.lodBias; @@ -107,29 +116,29 @@ using namespace sky::lod_test; TEST(LodTest, ScreenSizeCalculation) { AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); - float fov = ToRadian(90.0f); + auto projMatrix = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); - // Camera at the object center → screen size should be 1.0 + // Camera at the object center → screen size should be large (clamped by max(1,dist)) { Vector3 viewPos(0, 0, 0); - float size = CalculateScreenSize(bound, viewPos, fov); - ASSERT_FLOAT_EQ(size, 1.0f); + float size = CalculateScreenSize(bound, viewPos, projMatrix); + ASSERT_GT(size, 1.0f); } // Camera far away → screen size should be small { Vector3 viewPos(0, 0, 100); - float size = CalculateScreenSize(bound, viewPos, fov); + float size = CalculateScreenSize(bound, viewPos, projMatrix); ASSERT_GT(size, 0.0f); ASSERT_LT(size, 0.1f); } - // Camera nearby → screen size should be larger + // Camera nearby → screen size should be larger than far away { Vector3 viewPos(0, 0, 5); - float sizeNear = CalculateScreenSize(bound, viewPos, fov); + float sizeNear = CalculateScreenSize(bound, viewPos, projMatrix); Vector3 viewPosFar(0, 0, 50); - float sizeFar = CalculateScreenSize(bound, viewPosFar, fov); + float sizeFar = CalculateScreenSize(bound, viewPosFar, projMatrix); ASSERT_GT(sizeNear, sizeFar); } } @@ -209,55 +218,72 @@ TEST(LodTest, SelectLodLevelSingleLevel) TEST(LodTest, ScreenSizeDecreasesWithDistance) { AABB bound(Vector3(-2, -2, -2), Vector3(2, 2, 2)); - float fov = ToRadian(60.0f); + auto projMatrix = MakePerspective(ToRadian(60.0f), 1.0f, 0.1f, 1000.f); - float prevSize = 2.0f; + float prevSize = std::numeric_limits::max(); for (float dist = 10.0f; dist <= 100.0f; dist += 10.0f) { Vector3 viewPos(0, 0, dist); - float size = CalculateScreenSize(bound, viewPos, fov); + float size = CalculateScreenSize(bound, viewPos, projMatrix); ASSERT_LT(size, prevSize); prevSize = size; } } -// ===== Pre-computed distance LOD tests ===== +// ===== UE-style screen multiplier tests ===== + +TEST(LodTest, GetScreenMultipleFromProjMatrix) +{ + // 90° FOV, aspect 1.0: inverseHalfTan = 1/tan(45°) = 1.0 + auto proj90 = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); + float sm90 = GetScreenMultiple(proj90); + ASSERT_NEAR(sm90, 0.5f, 0.001f); // max(0.5*1.0, 0.5*1.0) = 0.5 + + // 60° FOV, aspect 1.0: inverseHalfTan = 1/tan(30°) ≈ 1.732 + auto proj60 = MakePerspective(ToRadian(60.0f), 1.0f, 0.1f, 1000.f); + float sm60 = GetScreenMultiple(proj60); + ASSERT_GT(sm60, sm90); // Narrower FOV → larger screen multiple + + // With aspect != 1.0 + auto projWide = MakePerspective(ToRadian(90.0f), 16.0f/9.0f, 0.1f, 1000.f); + float smWide = GetScreenMultiple(projWide); + // [0][0] = inverseHalfTan / aspect < inverseHalfTan = [1][1] + // max(0.5*[0][0], 0.5*[1][1]) = 0.5 * [1][1] = 0.5 + ASSERT_NEAR(smWide, 0.5f, 0.001f); +} TEST(LodTest, ScreenSizeToDistanceConversion) { + auto projMatrix = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); + float screenMultiple = GetScreenMultiple(projMatrix); float radius = 1.7320508f; // sqrt(3) ~ extent of unit AABB - float fov = ToRadian(90.0f); - float halfTanFov = std::tan(fov * 0.5f); // tan(45°) = 1.0 - // screenSize = 2*radius / (dist * halfTanFov) - // dist = 2*radius / (screenSize * halfTanFov) + // dist = 2 * screenMultiple * radius / screenSize { - float d = ScreenSizeToDistance(0.5f, radius, halfTanFov); - float expected = (2.0f * radius) / (0.5f * halfTanFov); + float d = ScreenSizeToDistance(0.5f, radius, screenMultiple); + float expected = (2.0f * screenMultiple * radius) / 0.5f; ASSERT_FLOAT_EQ(d, expected); } // screenSize 0 → infinite distance { - float d = ScreenSizeToDistance(0.0f, radius, halfTanFov); + float d = ScreenSizeToDistance(0.0f, radius, screenMultiple); ASSERT_EQ(d, std::numeric_limits::max()); } // Smaller screenSize → larger distance { - float d1 = ScreenSizeToDistance(0.5f, radius, halfTanFov); - float d2 = ScreenSizeToDistance(0.25f, radius, halfTanFov); + float d1 = ScreenSizeToDistance(0.5f, radius, screenMultiple); + float d2 = ScreenSizeToDistance(0.25f, radius, screenMultiple); ASSERT_GT(d2, d1); } } TEST(LodTest, PreComputeDistancesMatchesScreenSize) { - // Verify that pre-computed distance-based LOD selection matches - // screen-size-based LOD selection for various camera distances AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); auto extent = (bound.max - bound.min) * 0.5f; float radius = extent.Length(); - float fov = ToRadian(90.0f); + auto projMatrix = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); LodConfig config; config.lodBias = 1.0f; @@ -268,12 +294,12 @@ TEST(LodTest, PreComputeDistancesMatchesScreenSize) LodLevel{0.0f}, }; - PreComputeDistances(config, radius, fov); + PreComputeDistances(config, radius, projMatrix); // Test at various distances: both methods should agree for (float dist = 5.0f; dist <= 200.0f; dist += 5.0f) { Vector3 viewPos(0, 0, dist); - float screenSize = CalculateScreenSize(bound, viewPos, fov); + float screenSize = CalculateScreenSize(bound, viewPos, projMatrix); uint32_t lodByScreen = SelectLodByScreenSize(config, screenSize); uint32_t lodByDist = SelectLodLevel(config, bound, viewPos); @@ -281,7 +307,7 @@ TEST(LodTest, PreComputeDistancesMatchesScreenSize) // For the fallback LOD (screenSize=0 → infinite distance), distance-based // returns INVALID while screen-size returns 3. Both are valid. if (lodByScreen == 3u && lodByDist == INVALID_LOD_LEVEL) { - continue; // screenSize=0 maps to infinite distance, this is expected + continue; } ASSERT_EQ(lodByScreen, lodByDist) << "Mismatch at distance " << dist @@ -294,7 +320,7 @@ TEST(LodTest, PreComputeDistancesWithBias) AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); auto extent = (bound.max - bound.min) * 0.5f; float radius = extent.Length(); - float fov = ToRadian(90.0f); + auto projMatrix = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); LodConfig config; config.lodLevels = { @@ -302,7 +328,7 @@ TEST(LodTest, PreComputeDistancesWithBias) LodLevel{0.25f}, LodLevel{0.1f}, }; - PreComputeDistances(config, radius, fov); + PreComputeDistances(config, radius, projMatrix); Vector3 viewPos(0, 0, 10); @@ -335,12 +361,10 @@ TEST(LodTest, PreComputeDistancesEmpty) TEST(LodTest, RuntimeSelectionOnlyUsesDistance) { - // Verify that runtime selection only needs distance (no fov/tan) - // by confirming the pre-computed approach works consistently AABB bound(Vector3(-2, -2, -2), Vector3(2, 2, 2)); auto extent = (bound.max - bound.min) * 0.5f; float radius = extent.Length(); - float fov = ToRadian(60.0f); + auto projMatrix = MakePerspective(ToRadian(60.0f), 1.0f, 0.1f, 1000.f); LodConfig config; config.lodBias = 1.0f; @@ -349,7 +373,7 @@ TEST(LodTest, RuntimeSelectionOnlyUsesDistance) LodLevel{0.3f}, LodLevel{0.1f}, }; - PreComputeDistances(config, radius, fov); + PreComputeDistances(config, radius, projMatrix); // Close camera → LOD 0 { @@ -369,3 +393,25 @@ TEST(LodTest, RuntimeSelectionOnlyUsesDistance) ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 2u); } } + +TEST(LodTest, DifferentAspectRatios) +{ + // Verify that screen size handles different aspect ratios correctly + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + Vector3 viewPos(0, 0, 20); + + auto proj1to1 = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); + auto proj16to9 = MakePerspective(ToRadian(90.0f), 16.0f/9.0f, 0.1f, 1000.f); + + float size1to1 = CalculateScreenSize(bound, viewPos, proj1to1); + float size16to9 = CalculateScreenSize(bound, viewPos, proj16to9); + + // Both should be positive + ASSERT_GT(size1to1, 0.0f); + ASSERT_GT(size16to9, 0.0f); + + // For wider aspect, [1][1] is the same but [0][0] is smaller + // GetScreenMultiple uses max(), so same FOV → same screen multiple + // when [1][1] >= [0][0] + ASSERT_FLOAT_EQ(size1to1, size16to9); +} From 89e604878fb05e9ee87f618ed446df6f894b6ef5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 03:51:23 +0000 Subject: [PATCH 10/10] Add mesh streaming with LOD culling support When a mesh is beyond all LOD thresholds (INVALID_LOD_LEVEL), it is now culled by removing its primitives from the render scene. When the camera moves closer and the mesh comes back into LOD range, it is restored (streamed back in) by re-adding its primitives to the scene. Changes: - MeshRenderer: add isVisible/culledByLod tracking, ShowPrimitives/HidePrimitives - MeshRenderer::UpdateLod: cull on INVALID_LOD_LEVEL, restore on re-entry - MeshRenderer::Reset: reset visibility state - Tests: LodCullingWhenBeyondAllThresholds, LodCullingTransition, LodCullingWithScreenSizeFallback, LodCullingWithBias Co-authored-by: bluesky013 <35895395+bluesky013@users.noreply.github.com> --- .../core/include/render/mesh/MeshRenderer.h | 6 + runtime/render/core/src/mesh/MeshRenderer.cpp | 45 ++++++- test/core/LodTest.cpp | 119 ++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) diff --git a/runtime/render/core/include/render/mesh/MeshRenderer.h b/runtime/render/core/include/render/mesh/MeshRenderer.h index 0f6d345b..3fc11222 100644 --- a/runtime/render/core/include/render/mesh/MeshRenderer.h +++ b/runtime/render/core/include/render/mesh/MeshRenderer.h @@ -39,6 +39,8 @@ namespace sky { void SetMaterial(const RDMaterialInstancePtr &mat, uint32_t subMesh); uint32_t GetCurrentLod() const { return currentLod; } + bool IsVisible() const { return isVisible; } + bool IsCulledByLod() const { return culledByLod; } protected: virtual void PrepareUBO(); virtual RDResourceGroupPtr RequestResourceGroup(MeshFeature *feature); @@ -46,6 +48,8 @@ namespace sky { void SetupDebugMeshlet(); void Reset(); + void ShowPrimitives(); + void HidePrimitives(); RenderScene *scene = nullptr; @@ -59,6 +63,8 @@ namespace sky { RDDynamicUniformBufferPtr ubo; bool enableMeshShading = false; + bool isVisible = true; + bool culledByLod = false; MeshDebugFlags debugFlags; }; diff --git a/runtime/render/core/src/mesh/MeshRenderer.cpp b/runtime/render/core/src/mesh/MeshRenderer.cpp index 6599176c..92f9057c 100644 --- a/runtime/render/core/src/mesh/MeshRenderer.cpp +++ b/runtime/render/core/src/mesh/MeshRenderer.cpp @@ -145,6 +145,38 @@ namespace sky { scene->RemovePrimitive(meshletDebug->GetPrimitive()); meshletDebug = nullptr; } + isVisible = true; + culledByLod = false; + } + + void MeshRenderer::HidePrimitives() + { + if (!isVisible || scene == nullptr) { + return; + } + + for (auto &prim : primitives) { + scene->RemovePrimitive(prim.get()); + } + if (meshletDebug) { + scene->RemovePrimitive(meshletDebug->GetPrimitive()); + } + isVisible = false; + } + + void MeshRenderer::ShowPrimitives() + { + if (isVisible || scene == nullptr) { + return; + } + + for (auto &prim : primitives) { + scene->AddPrimitive(prim.get()); + } + if (meshletDebug && debugFlags.TestBit(MeshDebugFlagBit::MESHLET_CONE)) { + scene->AddPrimitive(meshletDebug->GetPrimitive()); + } + isVisible = true; } void MeshRenderer::SetLodGroup(const RDMeshLodGroupPtr &lodGroup_) @@ -170,7 +202,18 @@ namespace sky { uint32_t newLod = lodGroup->SelectLod(combinedBound, viewPos); if (newLod == INVALID_LOD_LEVEL) { - newLod = lodGroup->GetLodCount() - 1; + // Mesh is beyond all LOD thresholds — cull it + if (!culledByLod) { + culledByLod = true; + HidePrimitives(); + } + return; + } + + // Mesh is within LOD range — restore if previously culled + if (culledByLod) { + culledByLod = false; + ShowPrimitives(); } if (newLod != currentLod && newLod < lodGroup->GetLodCount()) { diff --git a/test/core/LodTest.cpp b/test/core/LodTest.cpp index 6840458b..d91cfb7d 100644 --- a/test/core/LodTest.cpp +++ b/test/core/LodTest.cpp @@ -415,3 +415,122 @@ TEST(LodTest, DifferentAspectRatios) // when [1][1] >= [0][0] ASSERT_FLOAT_EQ(size1to1, size16to9); } + +// ===== LOD culling tests ===== + +TEST(LodTest, LodCullingWhenBeyondAllThresholds) +{ + // When no LOD level has a screenSize=0 fallback, objects beyond all + // thresholds should return INVALID_LOD_LEVEL (mesh should be culled) + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + auto extent = (bound.max - bound.min) * 0.5f; + float radius = extent.Length(); + auto projMatrix = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); + + LodConfig config; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.5f}, // LOD 0: close + LodLevel{0.25f}, // LOD 1: medium + LodLevel{0.1f}, // LOD 2: far (but not infinite) + }; + PreComputeDistances(config, radius, projMatrix); + + // Close → valid LOD + { + Vector3 viewPos(0, 0, 5); + uint32_t lod = SelectLodLevel(config, bound, viewPos); + ASSERT_NE(lod, INVALID_LOD_LEVEL); + } + + // Very far → should return INVALID_LOD_LEVEL (culled) + { + Vector3 viewPos(0, 0, 10000); + uint32_t lod = SelectLodLevel(config, bound, viewPos); + ASSERT_EQ(lod, INVALID_LOD_LEVEL); + } +} + +TEST(LodTest, LodCullingTransition) +{ + // Test the transition from visible to culled and back + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + auto extent = (bound.max - bound.min) * 0.5f; + float radius = extent.Length(); + auto projMatrix = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); + + LodConfig config; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.5f}, + LodLevel{0.1f}, + }; + PreComputeDistances(config, radius, projMatrix); + + // Moving away: LOD 0 → LOD 1 → CULLED + { + Vector3 viewPos(0, 0, 2); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 0u); + } + { + Vector3 viewPos(0, 0, 10); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 1u); + } + { + Vector3 viewPos(0, 0, 10000); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), INVALID_LOD_LEVEL); + } + + // Moving back: CULLED → LOD 1 → LOD 0 (streaming back in) + { + Vector3 viewPos(0, 0, 10); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 1u); + } + { + Vector3 viewPos(0, 0, 2); + ASSERT_EQ(SelectLodLevel(config, bound, viewPos), 0u); + } +} + +TEST(LodTest, LodCullingWithScreenSizeFallback) +{ + // When the last LOD has screenSize=0 (fallback), it should always + // match — no culling occurs + LodConfig config; + config.lodBias = 1.0f; + config.lodLevels = { + LodLevel{0.5f}, + LodLevel{0.0f}, // fallback: always matches via screen-size + }; + + // Screen-size based: even tiny sizes match the fallback + ASSERT_EQ(SelectLodLevel(config, 0.001f), 1u); + ASSERT_EQ(SelectLodLevel(config, 0.0f), 1u); +} + +TEST(LodTest, LodCullingWithBias) +{ + // Bias should affect the culling threshold + AABB bound(Vector3(-1, -1, -1), Vector3(1, 1, 1)); + auto extent = (bound.max - bound.min) * 0.5f; + float radius = extent.Length(); + auto projMatrix = MakePerspective(ToRadian(90.0f), 1.0f, 0.1f, 1000.f); + + LodConfig config; + config.lodLevels = { + LodLevel{0.5f}, + LodLevel{0.1f}, + }; + PreComputeDistances(config, radius, projMatrix); + + // Find a distance where it's culled with bias=1.0 + Vector3 farViewPos(0, 0, 10000); + config.lodBias = 1.0f; + ASSERT_EQ(SelectLodLevel(config, bound, farViewPos), INVALID_LOD_LEVEL); + + // With higher bias, the same distance may no longer be culled + // (higher bias = keep higher quality longer = extend visible range) + config.lodBias = 100.0f; + uint32_t lodHighBias = SelectLodLevel(config, bound, farViewPos); + ASSERT_NE(lodHighBias, INVALID_LOD_LEVEL); +}