天空盒更新

This commit is contained in:
Rowland 2026-06-24 16:32:25 +08:00
parent 74416a6b60
commit 6c39655c8f
14 changed files with 972 additions and 108 deletions

View File

@ -6781,6 +6781,7 @@ public:
}
MetaCoreEditorStateSnapshot editorStateSnapshot;
editorStateSnapshot.SceneSnapshot.Environment = sceneDocument->Environment;
editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects;
editorStateSnapshot.SelectionSnapshot = sceneDocument->Selection;
editorContext.RestoreStateSnapshot(editorStateSnapshot);
@ -6853,7 +6854,9 @@ public:
MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_;
sceneDocument.GameObjects = editorContext.GetScene().CaptureSnapshot().GameObjects;
const MetaCoreSceneSnapshot sceneSnapshot = editorContext.GetScene().CaptureSnapshot();
sceneDocument.Environment = sceneSnapshot.Environment;
sceneDocument.GameObjects = sceneSnapshot.GameObjects;
sceneDocument.Selection.SelectedObjectIds = editorContext.GetSelectedObjectIds();
sceneDocument.Selection.ActiveObjectId = editorContext.GetActiveObjectId();
sceneDocument.Selection.SelectionAnchorId = editorContext.GetSelectionAnchorId();

View File

@ -2610,11 +2610,131 @@ private:
class MetaCoreScenePanelProvider final : public MetaCoreIEditorPanelProvider {
public:
std::string GetPanelId() const override { return "Scene"; }
std::string GetPanelId() const override { return "SceneSettings"; }
std::string GetPanelTitle() const override { return "场景"; }
bool IsOpenByDefault() const override { return true; }
void DrawPanel(MetaCoreEditorContext&) override {
void DrawPanel(MetaCoreEditorContext& editorContext) override {
MetaCoreScene& scene = editorContext.GetScene();
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
std::vector<std::filesystem::path> ktxPaths;
if (assetDatabaseService != nullptr && assetDatabaseService->HasProject()) {
for (const MetaCoreAssetRecord& asset : assetDatabaseService->GetAssetRegistry()) {
std::string extension = asset.RelativePath.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) {
return static_cast<char>(std::tolower(value));
});
const std::filesystem::path normalizedPath = asset.RelativePath.lexically_normal();
const auto firstPathComponent = normalizedPath.begin();
if (extension == ".ktx" &&
!normalizedPath.is_absolute() &&
firstPathComponent != normalizedPath.end() &&
*firstPathComponent == "Assets") {
ktxPaths.push_back(normalizedPath);
}
}
std::sort(ktxPaths.begin(), ktxPaths.end());
ktxPaths.erase(std::unique(ktxPaths.begin(), ktxPaths.end()), ktxPaths.end());
}
const auto applyDiscreteChange = [&](const char* label, const std::function<void(MetaCoreSceneEnvironmentSettings&)>& mutator) {
(void)editorContext.ExecuteSnapshotCommand(label, [&]() {
MetaCoreSceneEnvironmentSettings settings = scene.GetEnvironmentSettings();
mutator(settings);
return scene.SetEnvironmentSettings(settings);
});
};
ImGui::BeginChild("SceneEnvironmentPanel", ImVec2(0.0F, 0.0F), ImGuiChildFlags_Borders);
ImGui::TextUnformatted("环境 / 天空盒");
ImGui::Separator();
const MetaCoreSceneEnvironmentSettings& environment = scene.GetEnvironmentSettings();
int sourceIndex = environment.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair ? 1 : 0;
const char* sourceLabels[] = {"内置环境", "项目 KTX 对"};
if (ImGui::Combo("来源", &sourceIndex, sourceLabels, IM_ARRAYSIZE(sourceLabels))) {
applyDiscreteChange("修改场景环境来源", [sourceIndex](MetaCoreSceneEnvironmentSettings& settings) {
settings.Source = sourceIndex == 1
? MetaCoreSceneEnvironmentSource::ProjectKtxPair
: MetaCoreSceneEnvironmentSource::Builtin;
});
}
bool skyboxVisible = environment.SkyboxVisible;
if (ImGui::Checkbox("显示天空盒", &skyboxVisible)) {
applyDiscreteChange("切换天空盒显示", [skyboxVisible](MetaCoreSceneEnvironmentSettings& settings) {
settings.SkyboxVisible = skyboxVisible;
});
}
if (environment.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair) {
const auto drawKtxSelector = [&](const char* label, const std::filesystem::path& currentPath, bool ibl) {
const std::string preview = currentPath.empty() ? "未选择" : currentPath.generic_string();
if (!ImGui::BeginCombo(label, preview.c_str())) {
return;
}
if (ImGui::Selectable("清除", currentPath.empty())) {
applyDiscreteChange("修改天空盒资源", [ibl](MetaCoreSceneEnvironmentSettings& settings) {
if (ibl) settings.IblKtxPath.clear();
else settings.SkyboxKtxPath.clear();
});
}
ImGui::Separator();
for (const std::filesystem::path& path : ktxPaths) {
const std::string pathLabel = path.generic_string();
const bool selected = path == currentPath;
if (ImGui::Selectable(pathLabel.c_str(), selected)) {
applyDiscreteChange("修改天空盒资源", [ibl, path](MetaCoreSceneEnvironmentSettings& settings) {
if (ibl) settings.IblKtxPath = path;
else settings.SkyboxKtxPath = path;
});
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
};
drawKtxSelector("IBL KTX", environment.IblKtxPath, true);
drawKtxSelector("天空盒 KTX", environment.SkyboxKtxPath, false);
if (ktxPaths.empty()) {
ImGui::TextDisabled("Assets 下没有可用 KTX 资源。");
}
}
float rotationDegrees = environment.RotationDegrees;
if (ImGui::DragFloat("水平旋转", &rotationDegrees, 1.0F, 0.0F, 360.0F, "%.1f 度")) {
MetaCoreSceneEnvironmentSettings settings = environment;
settings.RotationDegrees = rotationDegrees;
(void)scene.SetEnvironmentSettings(settings);
}
MetaCoreTrackInspectorEdit(editorContext, "调整天空盒旋转", true);
float indirectIntensity = environment.IndirectLightIntensity;
if (ImGui::DragFloat("环境光强度", &indirectIntensity, 100.0F, 0.0F, 100000.0F, "%.0f")) {
MetaCoreSceneEnvironmentSettings settings = environment;
settings.IndirectLightIntensity = indirectIntensity;
(void)scene.SetEnvironmentSettings(settings);
}
MetaCoreTrackInspectorEdit(editorContext, "调整环境光强度", true);
float skyboxIntensity = environment.SkyboxIntensity;
if (ImGui::DragFloat("天空盒亮度", &skyboxIntensity, 100.0F, 0.0F, 100000.0F, "%.0f")) {
MetaCoreSceneEnvironmentSettings settings = environment;
settings.SkyboxIntensity = skyboxIntensity;
(void)scene.SetEnvironmentSettings(settings);
}
MetaCoreTrackInspectorEdit(editorContext, "调整天空盒亮度", true);
if (ImGui::Button("恢复内置环境")) {
applyDiscreteChange("恢复内置环境", [](MetaCoreSceneEnvironmentSettings& settings) {
settings = MetaCoreSceneEnvironmentSettings{};
});
}
ImGui::EndChild();
}
};
@ -6324,6 +6444,9 @@ public:
moduleRegistry.RegisterMenuProvider(std::make_unique<MetaCoreDefaultMenuProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreHierarchyPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreScenePanelProvider>());
// The central Scene / Game viewport is not a dockable provider, but it keeps
// its existing visibility state separate from the scene settings panel.
moduleRegistry.AccessPanelOpenState("Scene") = true;
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreInspectorPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreProjectPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreConsolePanelProvider>());

View File

@ -1432,10 +1432,6 @@ void MetaCoreEditorApp::DrawEditorFrame() {
MetaCoreDrawInfernuxStatusBar(*EditorContext_, statusPosition, statusSize);
for (const auto& panelProvider : ModuleRegistry_.GetPanelProviders()) {
if (panelProvider->GetPanelId() == "Scene") {
continue;
}
bool& panelOpen = ModuleRegistry_.AccessPanelOpenState(panelProvider->GetPanelId());
if (!panelOpen) {
continue;
@ -1754,6 +1750,7 @@ void MetaCoreEditorApp::EnsureDefaultDockLayout(unsigned int dockSpaceId, const
ImGui::DockBuilderDockWindow("层级", leftNodeId);
ImGui::DockBuilderDockWindow("检查器", rightNodeId);
ImGui::DockBuilderDockWindow("场景", rightNodeId);
ImGui::DockBuilderDockWindow("项目", bottomNodeId);
ImGui::DockBuilderDockWindow("控制台", bottomNodeId);
ImGui::DockBuilderFinish(dockSpaceId);

View File

@ -175,6 +175,18 @@ private:
}
[[nodiscard]] bool MetaCoreStateSnapshotEqual(const MetaCoreEditorStateSnapshot& lhs, const MetaCoreEditorStateSnapshot& rhs) {
const MetaCoreSceneEnvironmentSettings& lhsEnvironment = lhs.SceneSnapshot.Environment;
const MetaCoreSceneEnvironmentSettings& rhsEnvironment = rhs.SceneSnapshot.Environment;
if (lhsEnvironment.Source != rhsEnvironment.Source ||
lhsEnvironment.IblKtxPath != rhsEnvironment.IblKtxPath ||
lhsEnvironment.SkyboxKtxPath != rhsEnvironment.SkyboxKtxPath ||
lhsEnvironment.SkyboxVisible != rhsEnvironment.SkyboxVisible ||
!MetaCoreNearlyEqual(lhsEnvironment.RotationDegrees, rhsEnvironment.RotationDegrees) ||
!MetaCoreNearlyEqual(lhsEnvironment.IndirectLightIntensity, rhsEnvironment.IndirectLightIntensity) ||
!MetaCoreNearlyEqual(lhsEnvironment.SkyboxIntensity, rhsEnvironment.SkyboxIntensity)) {
return false;
}
if (lhs.SceneSnapshot.GameObjects.size() != rhs.SceneSnapshot.GameObjects.size()) {
return false;
}

View File

@ -6,6 +6,7 @@ material {
depthWrite : false,
depthCulling : true,
doubleSided : true,
requires : [ color ],
parameters : [
{ type : float4, name : baseColorFactor }
],
@ -14,7 +15,7 @@ material {
fragment {
void material(inout MaterialInputs material) {
prepareMaterial(material);
material.baseColor = materialParams.baseColorFactor;
material.baseColor = materialParams.baseColorFactor * getColor();
material.baseColor.rgb *= material.baseColor.a;
}
}

View File

@ -55,6 +55,7 @@
#include <fstream>
#include <algorithm>
#include <array>
#include <cctype>
#include <cstddef>
#include <cstdlib>
#include <cstdint>
@ -88,6 +89,10 @@ namespace MetaCore {
namespace {
constexpr std::uint8_t kSceneContentLayerMask = 0x01U;
constexpr std::uint8_t kEnvironmentSkyboxLayerMask = 0x02U;
constexpr std::uint8_t kEditorGridLayerMask = 0x04U;
void* MetaCoreGetFilamentNativeWindowHandle(MetaCoreWindow& window) {
#if defined(_WIN32)
return window.GetNativeWindowHandle();
@ -292,6 +297,7 @@ public:
// 创建场景
Scene_ = Engine_->createScene();
EnvironmentScene_ = Engine_->createScene();
LoadDefaultEnvironmentLighting();
// 创建默认灯光
@ -315,6 +321,16 @@ public:
View_->setCamera(Camera_);
ConfigureSceneViewRenderState();
EnvironmentView_ = Engine_->createView();
EnvironmentView_->setScene(EnvironmentScene_);
EnvironmentView_->setCamera(Camera_);
ConfigureEnvironmentViewRenderState();
GridView_ = Engine_->createView();
GridView_->setScene(Scene_);
GridView_->setCamera(Camera_);
ConfigureGridViewRenderState();
if (offscreen) {
// 创建 UI 视图
UIView_ = Engine_->createView();
@ -352,15 +368,27 @@ public:
// 4. 设置到 View
View_->setRenderTarget(RenderTarget_);
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
EnvironmentView_->setRenderTarget(RenderTarget_);
EnvironmentView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
GridView_->setRenderTarget(RenderTarget_);
GridView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
ConfigureSceneViewRenderState();
ConfigureEnvironmentViewRenderState();
ConfigureGridViewRenderState();
} else {
auto [width, height] = window.GetFramebufferSize();
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
EnvironmentView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
GridView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
ConfigureSceneViewRenderState();
ConfigureEnvironmentViewRenderState();
ConfigureGridViewRenderState();
}
// 确保开启后处理为了HDR
View_->setPostProcessingEnabled(true);
EnvironmentView_->setPostProcessingEnabled(false);
GridView_->setPostProcessingEnabled(false);
// 初始化 gltfio
MaterialProvider_ = new MetaCoreGltfMaterialProvider(
@ -385,7 +413,6 @@ public:
}
void SetEditorGridVisible(bool visible) {
EditorGridVisibleRequested_ = visible;
if (Engine_ == nullptr || Scene_ == nullptr) {
return;
}
@ -399,6 +426,11 @@ public:
EntitiesInScene_.insert(EditorGridEntity_);
EditorGridEntityInScene_ = true;
}
if (!EditorGridAxisEntityInScene_) {
Scene_->addEntity(EditorGridAxisEntity_);
EntitiesInScene_.insert(EditorGridAxisEntity_);
EditorGridAxisEntityInScene_ = true;
}
return;
}
@ -407,10 +439,33 @@ public:
EntitiesInScene_.erase(EditorGridEntity_);
EditorGridEntityInScene_ = false;
}
if (EditorGridAxisEntityInScene_) {
Scene_->remove(EditorGridAxisEntity_);
EntitiesInScene_.erase(EditorGridAxisEntity_);
EditorGridAxisEntityInScene_ = false;
}
}
[[nodiscard]] bool VerifyEditorGridVisibleForTesting() const {
return EditorGridEntityInScene_;
return EditorGridEntityInScene_ && EditorGridAxisEntityInScene_;
}
[[nodiscard]] bool VerifyEnvironmentLoadedForTesting() const {
return EnvironmentLoaded_ &&
EnvironmentIblTexture_ != nullptr &&
EnvironmentSkyboxTexture_ != nullptr &&
EnvironmentIndirectLight_ != nullptr &&
EnvironmentSkybox_ != nullptr;
}
[[nodiscard]] bool VerifyEnvironmentBackgroundVisibleForTesting() const {
return EnvironmentBackgroundVisible_ &&
EnvironmentScene_ != nullptr &&
EnvironmentScene_->getSkybox() == EnvironmentSkybox_;
}
[[nodiscard]] float GetEnvironmentIndirectLightIntensityForTesting() const {
return EnvironmentIndirectLight_ != nullptr ? EnvironmentIndirectLight_->getIntensity() : 0.0F;
}
void Shutdown() {
@ -548,6 +603,14 @@ public:
}
// 销毁 View
Engine_->destroy(View_);
if (EnvironmentView_) {
Engine_->destroy(EnvironmentView_);
EnvironmentView_ = nullptr;
}
if (GridView_) {
Engine_->destroy(GridView_);
GridView_ = nullptr;
}
if (UIView_) {
Engine_->destroy(UIView_);
UIView_ = nullptr;
@ -565,6 +628,10 @@ public:
}
// 销毁其他资源
if (EnvironmentScene_) {
Engine_->destroy(EnvironmentScene_);
EnvironmentScene_ = nullptr;
}
Engine_->destroy(Scene_);
Engine_->destroy(Renderer_);
Engine_->destroy(SwapChain_);
@ -581,9 +648,10 @@ public:
void SetProjectRootPath(const std::filesystem::path& projectRootPath) {
if (ProjectRootPath_ != projectRootPath) {
DestroyMaterialTextureCache();
DestroyEnvironmentLighting();
}
ProjectRootPath_ = projectRootPath;
LoadDefaultEnvironmentLighting();
SynchronizeEnvironment(ActiveEnvironmentSettings_);
}
void DestroyMaterialTextureCache() {
@ -625,18 +693,27 @@ public:
std::filesystem::path("build_filament") / "libs" / "ktxreader" / filename
};
const std::filesystem::path cwd = std::filesystem::current_path();
for (const std::filesystem::path& relativePath : candidates) {
const std::filesystem::path absolutePath = (cwd / relativePath).lexically_normal();
if (std::filesystem::exists(absolutePath)) {
return absolutePath;
std::vector<std::filesystem::path> searchRoots;
std::filesystem::path current = std::filesystem::current_path();
for (int depth = 0; depth < 5 && !current.empty(); ++depth) {
searchRoots.push_back(current);
const std::filesystem::path parent = current.parent_path();
if (parent == current) {
break;
}
current = parent;
}
if (!ProjectRootPath_.empty()) {
searchRoots.push_back(ProjectRootPath_);
searchRoots.push_back(ProjectRootPath_.parent_path());
}
const std::filesystem::path fromProjectRoot = (ProjectRootPath_.empty()
? std::filesystem::path{}
: ProjectRootPath_ / ".." / relativePath).lexically_normal();
if (!fromProjectRoot.empty() && std::filesystem::exists(fromProjectRoot)) {
return fromProjectRoot;
for (const std::filesystem::path& root : searchRoots) {
for (const std::filesystem::path& relativePath : candidates) {
const std::filesystem::path absolutePath = (root / relativePath).lexically_normal();
if (std::filesystem::exists(absolutePath)) {
return absolutePath;
}
}
}
return {};
@ -669,50 +746,242 @@ public:
return ktxreader::Ktx1Reader::createTexture(Engine_, bundle.release(), srgb);
}
void LoadDefaultEnvironmentLighting() {
if (Engine_ == nullptr || Scene_ == nullptr || EnvironmentLoaded_) {
return;
[[nodiscard]] static bool IsSafeProjectRelativeKtxPath(const std::filesystem::path& path) {
if (path.empty() || path.is_absolute() || path.has_root_name() || path.has_root_directory()) {
return false;
}
for (const std::filesystem::path& component : path) {
if (component == "..") {
return false;
}
}
const std::filesystem::path iblPath = ResolveDefaultEnvironmentPath("lightroom_14b_ibl.ktx");
const std::filesystem::path skyboxPath = ResolveDefaultEnvironmentPath("lightroom_14b_skybox.ktx");
if (iblPath.empty() || skyboxPath.empty()) {
MetaCoreFilamentDebugLog("Default IBL assets not found; falling back to solid clear color");
return;
std::string extension = path.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) {
return static_cast<char>(std::tolower(value));
});
return extension == ".ktx";
}
[[nodiscard]] bool ResolveEnvironmentPaths(
const MetaCoreSceneEnvironmentSettings& settings,
std::filesystem::path& outIblPath,
std::filesystem::path& outSkyboxPath
) const {
outIblPath.clear();
outSkyboxPath.clear();
if (settings.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair) {
if (ProjectRootPath_.empty() ||
!IsSafeProjectRelativeKtxPath(settings.IblKtxPath) ||
!IsSafeProjectRelativeKtxPath(settings.SkyboxKtxPath)) {
return false;
}
outIblPath = (ProjectRootPath_ / settings.IblKtxPath).lexically_normal();
outSkyboxPath = (ProjectRootPath_ / settings.SkyboxKtxPath).lexically_normal();
std::error_code errorCode;
return std::filesystem::is_regular_file(outIblPath, errorCode) &&
std::filesystem::is_regular_file(outSkyboxPath, errorCode);
}
outIblPath = ResolveDefaultEnvironmentPath("lightroom_14b_ibl.ktx");
outSkyboxPath = ResolveDefaultEnvironmentPath("lightroom_14b_skybox.ktx");
return !outIblPath.empty() && !outSkyboxPath.empty();
}
[[nodiscard]] static bool EnvironmentSettingsEqual(
const MetaCoreSceneEnvironmentSettings& lhs,
const MetaCoreSceneEnvironmentSettings& rhs
) {
return lhs.Source == rhs.Source &&
lhs.IblKtxPath == rhs.IblKtxPath &&
lhs.SkyboxKtxPath == rhs.SkyboxKtxPath &&
lhs.SkyboxVisible == rhs.SkyboxVisible &&
std::abs(lhs.RotationDegrees - rhs.RotationDegrees) <= 0.0001F &&
std::abs(lhs.IndirectLightIntensity - rhs.IndirectLightIntensity) <= 0.0001F &&
std::abs(lhs.SkyboxIntensity - rhs.SkyboxIntensity) <= 0.0001F;
}
[[nodiscard]] static float NormalizeEnvironmentRotation(float degrees) {
if (!std::isfinite(degrees)) {
return 0.0F;
}
float normalized = std::fmod(degrees, 360.0F);
if (normalized < 0.0F) {
normalized += 360.0F;
}
return normalized;
}
void UpdateEnvironmentBackgroundVisibility() {
const bool shouldShow =
EnvironmentSkybox_ != nullptr &&
EnvironmentScene_ != nullptr &&
ActiveEnvironmentSettings_.SkyboxVisible &&
CurrentSceneView_.ClearFlags == MetaCoreCameraClearFlags::Skybox;
if (EnvironmentBackgroundVisible_ != shouldShow) {
MetaCoreFilamentDebugLog(
std::string("Scene environment background ") + (shouldShow ? "enabled" : "disabled") +
" clearFlags=" + std::to_string(static_cast<int>(CurrentSceneView_.ClearFlags)) +
" skyboxVisible=" + (ActiveEnvironmentSettings_.SkyboxVisible ? "true" : "false")
);
}
if (EnvironmentScene_ != nullptr) {
if (shouldShow && EnvironmentScene_->getSkybox() != EnvironmentSkybox_) {
EnvironmentScene_->setSkybox(EnvironmentSkybox_);
} else if (!shouldShow && EnvironmentScene_->getSkybox() == EnvironmentSkybox_) {
EnvironmentScene_->setSkybox(nullptr);
}
}
EnvironmentBackgroundVisible_ = shouldShow;
}
bool RebuildEnvironmentSkybox() {
if (Engine_ == nullptr || EnvironmentSkyboxTexture_ == nullptr) {
return false;
}
if (EnvironmentScene_ != nullptr && EnvironmentScene_->getSkybox() == EnvironmentSkybox_) {
EnvironmentScene_->setSkybox(nullptr);
}
if (EnvironmentSkybox_ != nullptr) {
Engine_->destroy(EnvironmentSkybox_);
EnvironmentSkybox_ = nullptr;
}
EnvironmentSkybox_ = filament::Skybox::Builder()
.environment(EnvironmentSkyboxTexture_)
.intensity(std::max(0.0F, ActiveEnvironmentSettings_.SkyboxIntensity))
.build(*Engine_);
if (EnvironmentSkybox_ == nullptr) {
return false;
}
EnvironmentSkybox_->setLayerMask(0xFFU, kEnvironmentSkyboxLayerMask);
UpdateEnvironmentBackgroundVisibility();
return true;
}
bool LoadEnvironmentResources(
const std::filesystem::path& iblPath,
const std::filesystem::path& skyboxPath
) {
filament::math::float3 sphericalHarmonics[9]{};
EnvironmentIblTexture_ = LoadKtx1Texture(iblPath, false, sphericalHarmonics);
EnvironmentSkyboxTexture_ = LoadKtx1Texture(skyboxPath, false);
if (EnvironmentIblTexture_ == nullptr || EnvironmentSkyboxTexture_ == nullptr) {
MetaCoreFilamentDebugLog("Default IBL KTX load failed");
DestroyEnvironmentLighting();
return;
return false;
}
EnvironmentIndirectLight_ = filament::IndirectLight::Builder()
.reflections(EnvironmentIblTexture_)
.irradiance(3, sphericalHarmonics)
.intensity(EnvironmentIntensity_)
.intensity(ActiveEnvironmentSettings_.IndirectLightIntensity)
.build(*Engine_);
EnvironmentSkybox_ = filament::Skybox::Builder()
.environment(EnvironmentSkyboxTexture_)
.intensity(EnvironmentIntensity_)
.build(*Engine_);
if (EnvironmentIndirectLight_ == nullptr || EnvironmentSkybox_ == nullptr) {
MetaCoreFilamentDebugLog("Default IBL object build failed");
DestroyEnvironmentLighting();
return;
if (EnvironmentIndirectLight_ == nullptr) {
return false;
}
Scene_->setIndirectLight(EnvironmentIndirectLight_);
Scene_->setSkybox(EnvironmentSkybox_);
EnvironmentLoaded_ = true;
MetaCoreFilamentDebugLog(
"Default IBL loaded ibl=\"" + iblPath.generic_string() +
"\" skybox=\"" + skyboxPath.generic_string() + "\""
);
return RebuildEnvironmentSkybox();
}
void SynchronizeEnvironment(const MetaCoreSceneEnvironmentSettings& requestedSettings) {
if (Engine_ == nullptr || Scene_ == nullptr) {
return;
}
std::filesystem::path iblPath;
std::filesystem::path skyboxPath;
const bool requestedPathsValid = ResolveEnvironmentPaths(requestedSettings, iblPath, skyboxPath);
if (!requestedPathsValid && requestedSettings.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair) {
const std::string failureSignature =
requestedSettings.IblKtxPath.generic_string() + "|" +
requestedSettings.SkyboxKtxPath.generic_string();
if (LastEnvironmentValidationFailure_ != failureSignature) {
MetaCoreFilamentDebugLog(
"Scene environment KTX pair is unavailable or unsafe; using the built-in environment. ibl=\"" +
requestedSettings.IblKtxPath.generic_string() + "\" skybox=\"" +
requestedSettings.SkyboxKtxPath.generic_string() + "\""
);
LastEnvironmentValidationFailure_ = failureSignature;
}
MetaCoreSceneEnvironmentSettings fallbackSettings;
if (!ResolveEnvironmentPaths(fallbackSettings, iblPath, skyboxPath)) {
DestroyEnvironmentLighting();
ActiveEnvironmentSettings_ = requestedSettings;
return;
}
} else if (!requestedPathsValid) {
DestroyEnvironmentLighting();
ActiveEnvironmentSettings_ = requestedSettings;
return;
} else {
LastEnvironmentValidationFailure_.clear();
}
const bool resourcesChanged =
!EnvironmentLoaded_ ||
ActiveEnvironmentSettings_.Source != requestedSettings.Source ||
ActiveEnvironmentSettings_.IblKtxPath != requestedSettings.IblKtxPath ||
ActiveEnvironmentSettings_.SkyboxKtxPath != requestedSettings.SkyboxKtxPath;
const bool skyboxIntensityChanged =
!EnvironmentSettingsEqual(ActiveEnvironmentSettings_, requestedSettings) &&
std::abs(ActiveEnvironmentSettings_.SkyboxIntensity - requestedSettings.SkyboxIntensity) > 0.0001F;
ActiveEnvironmentSettings_ = requestedSettings;
ActiveEnvironmentSettings_.RotationDegrees = NormalizeEnvironmentRotation(ActiveEnvironmentSettings_.RotationDegrees);
ActiveEnvironmentSettings_.IndirectLightIntensity = std::max(0.0F, ActiveEnvironmentSettings_.IndirectLightIntensity);
ActiveEnvironmentSettings_.SkyboxIntensity = std::max(0.0F, ActiveEnvironmentSettings_.SkyboxIntensity);
if (resourcesChanged) {
DestroyEnvironmentLighting();
if (!LoadEnvironmentResources(iblPath, skyboxPath)) {
DestroyEnvironmentLighting();
if (requestedSettings.Source != MetaCoreSceneEnvironmentSource::ProjectKtxPair) {
MetaCoreFilamentDebugLog("Built-in scene environment KTX load failed; falling back to solid clear color");
return;
}
MetaCoreFilamentDebugLog("Scene environment KTX load failed; falling back to the built-in environment");
MetaCoreSceneEnvironmentSettings fallbackSettings;
if (!ResolveEnvironmentPaths(fallbackSettings, iblPath, skyboxPath) ||
!LoadEnvironmentResources(iblPath, skyboxPath)) {
MetaCoreFilamentDebugLog("Built-in scene environment fallback load failed; falling back to solid clear color");
DestroyEnvironmentLighting();
return;
}
}
LoadedEnvironmentIblPath_ = iblPath;
LoadedEnvironmentSkyboxPath_ = skyboxPath;
MetaCoreFilamentDebugLog(
"Scene environment loaded ibl=\"" + iblPath.generic_string() +
"\" skybox=\"" + skyboxPath.generic_string() + "\""
);
} else {
if (EnvironmentIndirectLight_ != nullptr) {
EnvironmentIndirectLight_->setIntensity(ActiveEnvironmentSettings_.IndirectLightIntensity);
}
if (skyboxIntensityChanged && !RebuildEnvironmentSkybox()) {
MetaCoreFilamentDebugLog("Scene environment skybox intensity update failed");
}
}
if (EnvironmentIndirectLight_ != nullptr) {
const glm::mat3 rotation = glm::mat3(glm::rotate(
glm::mat4(1.0F),
glm::radians(ActiveEnvironmentSettings_.RotationDegrees),
glm::vec3(0.0F, 0.0F, 1.0F)
));
EnvironmentIndirectLight_->setRotation(
*reinterpret_cast<const filament::math::mat3f*>(glm::value_ptr(rotation))
);
}
UpdateEnvironmentBackgroundVisibility();
}
void LoadDefaultEnvironmentLighting() {
SynchronizeEnvironment(MetaCoreSceneEnvironmentSettings{});
}
void DestroyEnvironmentLighting() {
@ -720,9 +989,9 @@ public:
if (Scene_->getIndirectLight() == EnvironmentIndirectLight_) {
Scene_->setIndirectLight(nullptr);
}
if (Scene_->getSkybox() == EnvironmentSkybox_) {
Scene_->setSkybox(nullptr);
}
}
if (EnvironmentScene_ != nullptr && EnvironmentScene_->getSkybox() == EnvironmentSkybox_) {
EnvironmentScene_->setSkybox(nullptr);
}
if (Engine_ != nullptr) {
@ -745,6 +1014,9 @@ public:
}
}
EnvironmentLoaded_ = false;
EnvironmentBackgroundVisible_ = false;
LoadedEnvironmentIblPath_.clear();
LoadedEnvironmentSkyboxPath_.clear();
}
[[nodiscard]] filament::Skybox* GetOrCreateSolidColorSkybox(const MetaCoreSceneView& sceneView) {
@ -984,6 +1256,7 @@ public:
(void)compatibilityMeshOnly;
LastSyncSnapshot_ = snapshot;
SynchronizeEnvironment(LastSyncSnapshot_.Environment);
ObjectWorldMatrices_ = LastSyncSnapshot_.WorldMatrices;
const bool materialDebugLogging = MetaCoreMaterialDebugLogEnabled();
@ -1853,20 +2126,16 @@ public:
void ApplySceneView(const MetaCoreSceneView& sceneView) {
if (!Camera_ || !View_) return;
CurrentSceneView_ = sceneView;
UpdateEditorGridTransform(sceneView);
if (Scene_ != nullptr) {
filament::Skybox* skybox = nullptr;
if (sceneView.ClearFlags == MetaCoreCameraClearFlags::Skybox) {
skybox = EnvironmentSkybox_ != nullptr ? EnvironmentSkybox_ : GetOrCreateSolidColorSkybox(sceneView);
} else if (sceneView.ClearFlags == MetaCoreCameraClearFlags::SolidColor) {
skybox = GetOrCreateSolidColorSkybox(sceneView);
}
Scene_->setSkybox(skybox);
Scene_->setSkybox(nullptr);
}
UpdateEnvironmentBackgroundVisibility();
// 1. 视图矩阵同步
const glm::mat4 viewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp);
const glm::mat4 cameraModelMatrix = glm::inverse(viewMatrix);
Camera_->setModelMatrix(*reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(cameraModelMatrix)));
CameraModelMatrix_ = glm::inverse(viewMatrix);
Camera_->setModelMatrix(*reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(CameraModelMatrix_)));
// 2. 投影矩阵:回归 Filament 原生设置
const auto& viewport = View_->getViewport();
@ -1932,15 +2201,40 @@ public:
}
if (Renderer_->beginFrame(SwapChain_)) {
if (RenderTarget_) {
// 1. 渲染 3D 离屏视口 (输出到 RenderTarget)
const bool renderEnvironment = EnvironmentBackgroundVisible_ && EnvironmentView_ != nullptr;
const bool renderEditorGrid = GridView_ != nullptr &&
(EditorGridEntityInScene_ || EditorGridAxisEntityInScene_);
const auto renderScenePass = [&](bool outputToRenderTarget) {
filament::Renderer::ClearOptions options = BuildSceneClearOptions();
Renderer_->setClearOptions(options);
Renderer_->render(View_);
if (renderEnvironment) {
options.clear = false;
Renderer_->setClearOptions(options);
const glm::mat4 environmentCameraMatrix = glm::rotate(
glm::mat4(1.0F),
glm::radians(-ActiveEnvironmentSettings_.RotationDegrees),
glm::vec3(0.0F, 0.0F, 1.0F)
) * CameraModelMatrix_;
Camera_->setModelMatrix(*reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(environmentCameraMatrix)));
Renderer_->render(EnvironmentView_);
Camera_->setModelMatrix(*reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(CameraModelMatrix_)));
}
if (renderEditorGrid) {
options.clear = false;
Renderer_->setClearOptions(options);
Renderer_->render(GridView_);
}
(void)outputToRenderTarget;
};
if (RenderTarget_) {
// 1. 渲染 3D 离屏视口 (输出到 RenderTarget)
renderScenePass(true);
if (hasUiPass) {
// 2. 准备并渲染 UI 视口 (输出到 SwapChain)
filament::Renderer::ClearOptions options;
options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色
options.clear = true;
Renderer_->setClearOptions(options);
@ -1948,10 +2242,7 @@ public:
}
} else {
// 3. 直接上屏渲染
filament::Renderer::ClearOptions options = BuildSceneClearOptions();
Renderer_->setClearOptions(options);
Renderer_->render(View_);
renderScenePass(false);
}
Renderer_->endFrame();
@ -1975,6 +2266,12 @@ public:
" height=" + std::to_string(height)
);
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
if (EnvironmentView_ != nullptr) {
EnvironmentView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
}
if (GridView_ != nullptr) {
GridView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
}
return;
}
@ -2018,7 +2315,17 @@ public:
View_->setRenderTarget(RenderTarget_);
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
if (EnvironmentView_ != nullptr) {
EnvironmentView_->setRenderTarget(RenderTarget_);
EnvironmentView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
}
if (GridView_ != nullptr) {
GridView_->setRenderTarget(RenderTarget_);
GridView_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
}
ConfigureSceneViewRenderState();
ConfigureEnvironmentViewRenderState();
ConfigureGridViewRenderState();
DebugRenderTargetState("Resize after recreate resources");
}
@ -2160,6 +2467,7 @@ public:
private:
struct MetaCoreEditorGridVertex {
filament::math::float3 Position{};
std::array<std::uint8_t, 4> Color{255, 255, 255, 255};
};
struct MetaCoreEditorGridPrimitiveRange {
@ -2167,6 +2475,58 @@ private:
std::uint32_t Count = 0;
};
static constexpr int EditorGridExtent_ = 500;
static constexpr int EditorGridMajorStep_ = 10;
static constexpr int EditorGridAxisExtent_ = 100000;
[[nodiscard]] float ResolveEditorGridSpacing(const MetaCoreSceneView& sceneView) const {
float viewScale = glm::distance(sceneView.CameraPosition, sceneView.CameraTarget);
if (sceneView.ProjectionMode == MetaCoreCameraProjectionMode::Orthographic) {
viewScale = std::max(viewScale, sceneView.OrthographicSize * 2.0F);
}
const float desiredSpacing = std::max(1.0F, viewScale / 12.0F);
const float magnitude = std::pow(10.0F, std::floor(std::log10(desiredSpacing)));
const float normalizedSpacing = desiredSpacing / magnitude;
if (normalizedSpacing <= 1.0F) {
return magnitude;
}
if (normalizedSpacing <= 2.0F) {
return 2.0F * magnitude;
}
if (normalizedSpacing <= 5.0F) {
return 5.0F * magnitude;
}
return 10.0F * magnitude;
}
void UpdateEditorGridTransform(const MetaCoreSceneView& sceneView) {
if (!EditorGridBuilt_ || Engine_ == nullptr || !EditorGridEntity_) {
return;
}
const float spacing = ResolveEditorGridSpacing(sceneView);
const float majorSpacing = spacing * static_cast<float>(EditorGridMajorStep_);
const glm::vec3 snappedCenter{
std::round(sceneView.CameraTarget.x / majorSpacing) * majorSpacing,
std::round(sceneView.CameraTarget.y / majorSpacing) * majorSpacing,
0.0F
};
const glm::mat4 gridTransform = glm::scale(
glm::translate(glm::mat4(1.0F), snappedCenter),
glm::vec3(spacing, spacing, 1.0F)
);
auto& transformManager = Engine_->getTransformManager();
const auto transformInstance = transformManager.getInstance(EditorGridEntity_);
if (transformInstance) {
transformManager.setTransform(
transformInstance,
*reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(gridTransform))
);
}
}
[[nodiscard]] filament::Material* LoadEditorGridMaterial() {
if (Engine_ == nullptr) {
return nullptr;
@ -2255,8 +2615,6 @@ private:
return false;
}
constexpr int gridExtent = 100;
constexpr int majorStep = 10;
enum class GridLineKind : std::size_t {
Minor = 0,
Major = 1,
@ -2273,40 +2631,72 @@ private:
float x1,
float y1
) {
const bool shouldFade = kind == GridLineKind::Minor || kind == GridLineKind::Major;
const auto vertexAlpha = [&](float x, float y) {
if (!shouldFade) {
return std::uint8_t{255};
}
const float normalizedDistance = std::max(std::abs(x), std::abs(y)) /
static_cast<float>(EditorGridExtent_);
const float fadeProgress = std::clamp((normalizedDistance - 0.58F) / 0.42F, 0.0F, 1.0F);
const float smoothFade = fadeProgress * fadeProgress * (3.0F - 2.0F * fadeProgress);
return static_cast<std::uint8_t>(std::round((1.0F - smoothFade) * 255.0F));
};
auto& vertices = groupedVertices[static_cast<std::size_t>(kind)];
vertices.push_back(MetaCoreEditorGridVertex{filament::math::float3{x0, y0, 0.0F}});
vertices.push_back(MetaCoreEditorGridVertex{filament::math::float3{x1, y1, 0.0F}});
vertices.push_back(MetaCoreEditorGridVertex{
filament::math::float3{x0, y0, 0.0F},
{255, 255, 255, vertexAlpha(x0, y0)}
});
vertices.push_back(MetaCoreEditorGridVertex{
filament::math::float3{x1, y1, 0.0F},
{255, 255, 255, vertexAlpha(x1, y1)}
});
};
for (int coordinate = -gridExtent; coordinate <= gridExtent; ++coordinate) {
const bool isMajor = coordinate != 0 && (coordinate % majorStep) == 0;
const GridLineKind verticalKind = coordinate == 0
? GridLineKind::AxisY
: (isMajor ? GridLineKind::Major : GridLineKind::Minor);
appendLine(
verticalKind,
static_cast<float>(coordinate),
static_cast<float>(-gridExtent),
static_cast<float>(coordinate),
static_cast<float>(gridExtent)
);
for (int coordinate = -EditorGridExtent_; coordinate <= EditorGridExtent_; ++coordinate) {
if (coordinate == 0) {
continue;
}
const GridLineKind horizontalKind = coordinate == 0
? GridLineKind::AxisX
: (isMajor ? GridLineKind::Major : GridLineKind::Minor);
const GridLineKind lineKind = (coordinate % EditorGridMajorStep_) == 0
? GridLineKind::Major
: GridLineKind::Minor;
appendLine(
horizontalKind,
static_cast<float>(-gridExtent),
lineKind,
static_cast<float>(coordinate),
static_cast<float>(gridExtent),
static_cast<float>(-EditorGridExtent_),
static_cast<float>(coordinate),
static_cast<float>(EditorGridExtent_)
);
appendLine(
lineKind,
static_cast<float>(-EditorGridExtent_),
static_cast<float>(coordinate),
static_cast<float>(EditorGridExtent_),
static_cast<float>(coordinate)
);
}
appendLine(
GridLineKind::AxisX,
static_cast<float>(-EditorGridAxisExtent_),
0.0F,
static_cast<float>(EditorGridAxisExtent_),
0.0F
);
appendLine(
GridLineKind::AxisY,
0.0F,
static_cast<float>(-EditorGridAxisExtent_),
0.0F,
static_cast<float>(EditorGridAxisExtent_)
);
std::vector<MetaCoreEditorGridVertex> vertices;
std::vector<std::uint16_t> indices;
vertices.reserve((gridExtent * 2 + 1) * 4);
indices.reserve((gridExtent * 2 + 1) * 4);
vertices.reserve((EditorGridExtent_ * 2 + 1) * 4 + 4);
indices.reserve((EditorGridExtent_ * 2 + 1) * 4 + 4);
std::array<MetaCoreEditorGridPrimitiveRange, static_cast<std::size_t>(GridLineKind::Count)> ranges{};
for (std::size_t kindIndex = 0; kindIndex < groupedVertices.size(); ++kindIndex) {
@ -2335,6 +2725,14 @@ private:
offsetof(MetaCoreEditorGridVertex, Position),
sizeof(MetaCoreEditorGridVertex)
)
.attribute(
filament::VertexAttribute::COLOR,
0,
filament::VertexBuffer::AttributeType::UBYTE4,
offsetof(MetaCoreEditorGridVertex, Color),
sizeof(MetaCoreEditorGridVertex)
)
.normalized(filament::VertexAttribute::COLOR)
.build(*Engine_);
EditorGridIndexBuffer_ = filament::IndexBuffer::Builder()
.indexCount(indices.size())
@ -2400,15 +2798,16 @@ private:
}
EditorGridEntity_ = utils::EntityManager::get().create();
auto builder = filament::RenderableManager::Builder(static_cast<std::size_t>(GridLineKind::Count));
builder
.boundingBox({{0.0F, 0.0F, 0.0F}, {static_cast<float>(gridExtent), static_cast<float>(gridExtent), 0.05F}})
auto& transformManager = Engine_->getTransformManager();
transformManager.create(EditorGridEntity_);
auto gridBuilder = filament::RenderableManager::Builder(2);
gridBuilder
.boundingBox({{0.0F, 0.0F, 0.0F}, {static_cast<float>(EditorGridExtent_), static_cast<float>(EditorGridExtent_), 0.05F}})
.culling(false)
.castShadows(false)
.receiveShadows(false);
for (std::size_t kindIndex = 0; kindIndex < ranges.size(); ++kindIndex) {
builder
for (std::size_t kindIndex = 0; kindIndex < 2; ++kindIndex) {
gridBuilder
.geometry(
kindIndex,
filament::RenderableManager::PrimitiveType::LINES,
@ -2421,10 +2820,48 @@ private:
.blendOrder(kindIndex, static_cast<uint16_t>(kindIndex));
}
if (static_cast<int>(builder.build(*Engine_, EditorGridEntity_)) != 0) {
if (static_cast<int>(gridBuilder.build(*Engine_, EditorGridEntity_)) != 0) {
DestroyEditorGridRenderable();
return false;
}
auto& renderableManager = Engine_->getRenderableManager();
renderableManager.setLayerMask(
renderableManager.getInstance(EditorGridEntity_),
0xFFU,
kEditorGridLayerMask
);
EditorGridAxisEntity_ = utils::EntityManager::get().create();
transformManager.create(EditorGridAxisEntity_);
auto axisBuilder = filament::RenderableManager::Builder(2);
axisBuilder
.boundingBox({{0.0F, 0.0F, 0.0F}, {static_cast<float>(EditorGridAxisExtent_), static_cast<float>(EditorGridAxisExtent_), 0.05F}})
.culling(false)
.castShadows(false)
.receiveShadows(false);
for (std::size_t kindIndex = 0; kindIndex < 2; ++kindIndex) {
const std::size_t sourceIndex = kindIndex + 2;
axisBuilder
.geometry(
kindIndex,
filament::RenderableManager::PrimitiveType::LINES,
EditorGridVertexBuffer_,
EditorGridIndexBuffer_,
ranges[sourceIndex].Offset,
ranges[sourceIndex].Count
)
.material(kindIndex, EditorGridMaterialInstances_[sourceIndex])
.blendOrder(kindIndex, static_cast<uint16_t>(sourceIndex));
}
if (static_cast<int>(axisBuilder.build(*Engine_, EditorGridAxisEntity_)) != 0) {
DestroyEditorGridRenderable();
return false;
}
renderableManager.setLayerMask(
renderableManager.getInstance(EditorGridAxisEntity_),
0xFFU,
kEditorGridLayerMask
);
EditorGridBuilt_ = true;
return true;
@ -2440,17 +2877,27 @@ private:
EntitiesInScene_.erase(EditorGridEntity_);
EditorGridEntityInScene_ = false;
}
if (EditorGridEntity_) {
auto& renderableManager = Engine_->getRenderableManager();
if (renderableManager.getInstance(EditorGridEntity_)) {
renderableManager.destroy(EditorGridEntity_);
}
Engine_->destroy(EditorGridEntity_);
utils::EntityManager::get().destroy(EditorGridEntity_);
EditorGridEntity_ = {};
if (EditorGridAxisEntityInScene_ && Scene_ != nullptr) {
Scene_->remove(EditorGridAxisEntity_);
EntitiesInScene_.erase(EditorGridAxisEntity_);
EditorGridAxisEntityInScene_ = false;
}
const auto destroyGridEntity = [&](utils::Entity& entity) {
if (!entity) {
return;
}
auto& renderableManager = Engine_->getRenderableManager();
if (renderableManager.getInstance(entity)) {
renderableManager.destroy(entity);
}
Engine_->destroy(entity);
utils::EntityManager::get().destroy(entity);
entity = {};
};
destroyGridEntity(EditorGridEntity_);
destroyGridEntity(EditorGridAxisEntity_);
for (filament::MaterialInstance*& materialInstance : EditorGridMaterialInstances_) {
if (materialInstance != nullptr) {
Engine_->destroy(materialInstance);
@ -2814,11 +3261,38 @@ private:
}
View_->setBlendMode(filament::View::BlendMode::OPAQUE);
View_->setVisibleLayers(0xFFU, kSceneContentLayerMask);
for (uint8_t channel = 0; channel < 8; ++channel) {
View_->setChannelDepthClearEnabled(channel, true);
}
}
void ConfigureEnvironmentViewRenderState() {
if (EnvironmentView_ == nullptr) {
return;
}
// This view is rendered after the main scene, so its skybox composites
// only into pixels not covered by scene geometry.
EnvironmentView_->setBlendMode(filament::View::BlendMode::TRANSLUCENT);
EnvironmentView_->setVisibleLayers(0xFFU, kEnvironmentSkyboxLayerMask);
for (uint8_t channel = 0; channel < 8; ++channel) {
EnvironmentView_->setChannelDepthClearEnabled(channel, false);
}
}
void ConfigureGridViewRenderState() {
if (GridView_ == nullptr) {
return;
}
GridView_->setBlendMode(filament::View::BlendMode::TRANSLUCENT);
GridView_->setVisibleLayers(0xFFU, kEditorGridLayerMask);
for (uint8_t channel = 0; channel < 8; ++channel) {
GridView_->setChannelDepthClearEnabled(channel, false);
}
}
void DebugRenderTargetState(const char* event) const {
std::ostringstream stream;
stream
@ -2838,16 +3312,24 @@ private:
filament::SwapChain* SwapChain_ = nullptr;
filament::Renderer* Renderer_ = nullptr;
filament::Scene* Scene_ = nullptr;
filament::Scene* EnvironmentScene_ = nullptr;
filament::Camera* Camera_ = nullptr;
filament::View* View_ = nullptr;
filament::View* EnvironmentView_ = nullptr;
filament::View* GridView_ = nullptr;
filament::Texture* EnvironmentIblTexture_ = nullptr;
filament::Texture* EnvironmentSkyboxTexture_ = nullptr;
filament::IndirectLight* EnvironmentIndirectLight_ = nullptr;
filament::Skybox* EnvironmentSkybox_ = nullptr;
filament::Skybox* SolidColorSkybox_ = nullptr;
filament::math::float4 SolidColorSkyboxColor_{-1.0f, -1.0f, -1.0f, -1.0f};
float EnvironmentIntensity_ = 30000.0f;
bool EnvironmentLoaded_ = false;
bool EnvironmentBackgroundVisible_ = false;
MetaCoreSceneEnvironmentSettings ActiveEnvironmentSettings_{};
std::filesystem::path LoadedEnvironmentIblPath_{};
std::filesystem::path LoadedEnvironmentSkyboxPath_{};
std::string LastEnvironmentValidationFailure_{};
glm::mat4 CameraModelMatrix_{1.0F};
filament::gltfio::AssetLoader* AssetLoader_ = nullptr;
filament::gltfio::MaterialProvider* MaterialProvider_ = nullptr;
@ -2871,10 +3353,11 @@ private:
std::unordered_map<MetaCoreId, utils::Entity> SceneLightEntities_;
std::unordered_set<utils::Entity, utils::Entity::Hasher> EntitiesInScene_;
bool EditorGridVisibleRequested_ = false;
bool EditorGridBuilt_ = false;
bool EditorGridEntityInScene_ = false;
bool EditorGridAxisEntityInScene_ = false;
utils::Entity EditorGridEntity_{};
utils::Entity EditorGridAxisEntity_{};
filament::VertexBuffer* EditorGridVertexBuffer_ = nullptr;
filament::IndexBuffer* EditorGridIndexBuffer_ = nullptr;
filament::Material* EditorGridMaterial_ = nullptr;
@ -2985,4 +3468,16 @@ bool MetaCoreFilamentSceneBridge::VerifyEditorGridVisibleForTesting() const {
return Impl_->VerifyEditorGridVisibleForTesting();
}
bool MetaCoreFilamentSceneBridge::VerifyEnvironmentLoadedForTesting() const {
return Impl_->VerifyEnvironmentLoadedForTesting();
}
bool MetaCoreFilamentSceneBridge::VerifyEnvironmentBackgroundVisibleForTesting() const {
return Impl_->VerifyEnvironmentBackgroundVisibleForTesting();
}
float MetaCoreFilamentSceneBridge::GetEnvironmentIndirectLightIntensityForTesting() const {
return Impl_->GetEnvironmentIndirectLightIntensityForTesting();
}
} // namespace MetaCore

View File

@ -77,6 +77,7 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met
MetaCoreSceneRenderSyncSnapshot snapshot;
snapshot.FrameId = ++FrameId_;
snapshot.SceneRevision = scene.GetRevision();
snapshot.Environment = scene.GetEnvironmentSettings();
// 预处理:构建子树模型网格节点计数表,支撑在没有 Tag 时的 Fallback 回溯
std::unordered_map<MetaCoreId, std::unordered_map<std::string, int>> subtreeModelCounts;

View File

@ -45,6 +45,9 @@ public:
[[nodiscard]] float GetDefaultLightIntensityForTesting() const;
[[nodiscard]] bool VerifyEntityInSceneForTesting(MetaCoreId objectId) const;
[[nodiscard]] bool VerifyEditorGridVisibleForTesting() const;
[[nodiscard]] bool VerifyEnvironmentLoadedForTesting() const;
[[nodiscard]] bool VerifyEnvironmentBackgroundVisibleForTesting() const;
[[nodiscard]] float GetEnvironmentIndirectLightIntensityForTesting() const;
private:
class MetaCoreFilamentSceneBridgeImpl;

View File

@ -4,6 +4,7 @@
#include "MetaCoreFoundation/MetaCoreId.h"
#include "MetaCoreRender/MetaCoreRenderTypes.h"
#include "MetaCoreScene/MetaCoreComponents.h"
#include "MetaCoreScene/MetaCoreSceneDocument.h"
#include <glm/mat4x4.hpp>
#include <glm/vec2.hpp>
@ -76,6 +77,7 @@ struct MetaCoreRenderSyncLight {
};
struct MetaCoreSceneRenderSyncSnapshot {
MetaCoreSceneEnvironmentSettings Environment{};
std::unordered_map<MetaCoreId, glm::mat4> WorldMatrices{};
std::unordered_map<MetaCoreId, glm::mat4> LocalMatrices{};
std::vector<MetaCoreRenderSyncRenderable> Renderables{};

View File

@ -23,6 +23,19 @@ std::string MetaCoreBuildCopyName(const std::string& name) {
return name + " Copy";
}
[[nodiscard]] bool MetaCoreEnvironmentSettingsEqual(
const MetaCoreSceneEnvironmentSettings& lhs,
const MetaCoreSceneEnvironmentSettings& rhs
) {
return lhs.Source == rhs.Source &&
lhs.IblKtxPath == rhs.IblKtxPath &&
lhs.SkyboxKtxPath == rhs.SkyboxKtxPath &&
lhs.SkyboxVisible == rhs.SkyboxVisible &&
lhs.RotationDegrees == rhs.RotationDegrees &&
lhs.IndirectLightIntensity == rhs.IndirectLightIntensity &&
lhs.SkyboxIntensity == rhs.SkyboxIntensity;
}
} // namespace
MetaCoreGameObject MetaCoreScene::CreateGameObject(const std::string& name, MetaCoreId parentId) {
@ -405,8 +418,18 @@ bool MetaCoreScene::ReparentGameObjects(const std::vector<MetaCoreId>& objectIds
return true;
}
bool MetaCoreScene::SetEnvironmentSettings(const MetaCoreSceneEnvironmentSettings& settings) {
if (MetaCoreEnvironmentSettingsEqual(EnvironmentSettings_, settings)) {
return false;
}
EnvironmentSettings_ = settings;
IncrementRevision();
return true;
}
MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
MetaCoreSceneSnapshot snapshot;
snapshot.Environment = EnvironmentSettings_;
auto objects = GetGameObjects();
snapshot.GameObjects.reserve(objects.size());
for (auto obj : objects) {
@ -439,6 +462,7 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) {
Registry_.clear();
IdToEntity_.clear();
EnvironmentSettings_ = snapshot.Environment;
MetaCoreId maxId = 0;
for (const MetaCoreGameObjectData& data : snapshot.GameObjects) {

View File

@ -32,6 +32,33 @@ static MetaCoreAssetGuid StringToGuid(const std::string& str) {
return str.empty() ? MetaCoreAssetGuid{} : MetaCoreAssetGuid::Parse(str).value_or(MetaCoreAssetGuid{});
}
static json SceneEnvironmentToJson(const MetaCoreSceneEnvironmentSettings& environment) {
json environmentJson = json::object();
environmentJson["Source"] = static_cast<int>(environment.Source);
environmentJson["IblKtxPath"] = environment.IblKtxPath.generic_string();
environmentJson["SkyboxKtxPath"] = environment.SkyboxKtxPath.generic_string();
environmentJson["SkyboxVisible"] = environment.SkyboxVisible;
environmentJson["RotationDegrees"] = environment.RotationDegrees;
environmentJson["IndirectLightIntensity"] = environment.IndirectLightIntensity;
environmentJson["SkyboxIntensity"] = environment.SkyboxIntensity;
return environmentJson;
}
static MetaCoreSceneEnvironmentSettings JsonToSceneEnvironment(const json& environmentJson) {
MetaCoreSceneEnvironmentSettings environment;
if (!environmentJson.is_object()) {
return environment;
}
if (environmentJson.contains("Source")) environment.Source = static_cast<MetaCoreSceneEnvironmentSource>(environmentJson["Source"].get<int>());
if (environmentJson.contains("IblKtxPath")) environment.IblKtxPath = environmentJson["IblKtxPath"].get<std::string>();
if (environmentJson.contains("SkyboxKtxPath")) environment.SkyboxKtxPath = environmentJson["SkyboxKtxPath"].get<std::string>();
if (environmentJson.contains("SkyboxVisible")) environment.SkyboxVisible = environmentJson["SkyboxVisible"].get<bool>();
if (environmentJson.contains("RotationDegrees")) environment.RotationDegrees = environmentJson["RotationDegrees"].get<float>();
if (environmentJson.contains("IndirectLightIntensity")) environment.IndirectLightIntensity = environmentJson["IndirectLightIntensity"].get<float>();
if (environmentJson.contains("SkyboxIntensity")) environment.SkyboxIntensity = environmentJson["SkyboxIntensity"].get<float>();
return environment;
}
// 辅助方法:将单个 MetaCoreGameObjectData 转换为 json 格式
static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCoreTypeRegistry& registry) {
json objJson = json::object();
@ -340,6 +367,8 @@ bool MetaCoreSceneSerializer::SaveSceneToJson(
for (const auto& field : sceneDesc->Fields) {
if (field.Name == "Name") {
sceneJson["SceneName"] = sceneDocument.Name;
} else if (field.Name == "Environment") {
sceneJson["Environment"] = SceneEnvironmentToJson(sceneDocument.Environment);
} else if (field.Name == "Selection") {
const MetaCoreStructDescriptor* selDesc = registry.FindStruct<MetaCoreEditorSelectionSnapshot>();
if (selDesc) {
@ -400,6 +429,8 @@ std::optional<MetaCoreSceneDocument> MetaCoreSceneSerializer::LoadSceneFromJson(
for (const auto& field : sceneDesc->Fields) {
if (field.Name == "Name" && sceneJson.contains("SceneName")) {
sceneDocument.Name = sceneJson["SceneName"].get<std::string>();
} else if (field.Name == "Environment" && sceneJson.contains("Environment")) {
sceneDocument.Environment = JsonToSceneEnvironment(sceneJson["Environment"]);
} else if (field.Name == "Selection" && sceneJson.contains("Selection")) {
const auto& selectionJson = sceneJson["Selection"];
const MetaCoreStructDescriptor* selDesc = registry.FindStruct<MetaCoreEditorSelectionSnapshot>();

View File

@ -14,6 +14,9 @@ MC_STRUCT()
struct MetaCoreSceneSnapshot {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreSceneEnvironmentSettings Environment{};
MC_PROPERTY()
std::vector<MetaCoreGameObjectData> GameObjects;
};
@ -40,6 +43,9 @@ public:
[[nodiscard]] std::vector<MetaCoreId> DuplicateGameObjects(const std::vector<MetaCoreId>& objectIds);
bool ReparentGameObjects(const std::vector<MetaCoreId>& objectIds, MetaCoreId newParentId, bool keepWorldTransform);
[[nodiscard]] const MetaCoreSceneEnvironmentSettings& GetEnvironmentSettings() const { return EnvironmentSettings_; }
[[nodiscard]] bool SetEnvironmentSettings(const MetaCoreSceneEnvironmentSettings& settings);
[[nodiscard]] MetaCoreSceneSnapshot CaptureSnapshot() const;
void RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot);
@ -54,6 +60,7 @@ private:
entt::registry Registry_;
std::unordered_map<MetaCoreId, entt::entity> IdToEntity_;
MetaCoreSceneEnvironmentSettings EnvironmentSettings_{};
std::uint64_t Revision_ = 0;
};

View File

@ -7,6 +7,7 @@
#include "MetaCoreScene/MetaCoreGameObject.h"
#include <glm/vec3.hpp>
#include <filesystem>
#include <string>
#include <vector>
@ -26,6 +27,38 @@ struct MetaCoreEditorSelectionSnapshot {
MetaCoreId SelectionAnchorId = 0;
};
MC_ENUM()
enum class MetaCoreSceneEnvironmentSource {
Builtin = 0,
ProjectKtxPair
};
MC_STRUCT()
struct MetaCoreSceneEnvironmentSettings {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreSceneEnvironmentSource Source = MetaCoreSceneEnvironmentSource::Builtin;
MC_PROPERTY()
std::filesystem::path IblKtxPath{};
MC_PROPERTY()
std::filesystem::path SkyboxKtxPath{};
MC_PROPERTY()
bool SkyboxVisible = true;
MC_PROPERTY()
float RotationDegrees = 0.0F;
MC_PROPERTY()
float IndirectLightIntensity = 30000.0F;
MC_PROPERTY()
float SkyboxIntensity = 30000.0F;
};
MC_STRUCT()
struct MetaCoreSceneDocument {
MC_GENERATED_BODY()
@ -33,6 +66,9 @@ struct MetaCoreSceneDocument {
MC_PROPERTY()
std::string Name{};
MC_PROPERTY()
MetaCoreSceneEnvironmentSettings Environment{};
MC_PROPERTY()
std::vector<MetaCoreGameObjectData> GameObjects{};

View File

@ -4537,6 +4537,133 @@ void MetaCoreTestHotReloadQueuesExternalMoveUntilTick() {
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestSceneEnvironmentSettings() {
const std::filesystem::path tempRoot = std::filesystem::temp_directory_path() / "MetaCoreSceneEnvironmentSmoke";
std::filesystem::remove_all(tempRoot);
std::filesystem::create_directories(tempRoot / "Assets" / "Environments");
const std::filesystem::path sourceEnvironmentDirectory =
MetaCoreTestRepositoryRoot() / "third_party" / "filament_installed" / "bin" / "assets" / "ibl" / "lightroom_14b";
const std::filesystem::path sourceIbl = sourceEnvironmentDirectory / "lightroom_14b_ibl.ktx";
const std::filesystem::path sourceSkybox = sourceEnvironmentDirectory / "lightroom_14b_skybox.ktx";
MetaCoreExpect(std::filesystem::exists(sourceIbl) && std::filesystem::exists(sourceSkybox), "应提供默认 KTX 环境资源");
const std::filesystem::path projectIbl = tempRoot / "Assets" / "Environments" / "studio_ibl.ktx";
const std::filesystem::path projectSkybox = tempRoot / "Assets" / "Environments" / "studio_skybox.ktx";
std::filesystem::copy_file(sourceIbl, projectIbl, std::filesystem::copy_options::overwrite_existing);
std::filesystem::copy_file(sourceSkybox, projectSkybox, std::filesystem::copy_options::overwrite_existing);
MetaCore::MetaCoreSceneEnvironmentSettings environment;
environment.Source = MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair;
environment.IblKtxPath = std::filesystem::path("Assets") / "Environments" / "studio_ibl.ktx";
environment.SkyboxKtxPath = std::filesystem::path("Assets") / "Environments" / "studio_skybox.ktx";
environment.SkyboxVisible = true;
environment.RotationDegrees = 137.5F;
environment.IndirectLightIntensity = 25000.0F;
environment.SkyboxIntensity = 7500.0F;
MetaCore::MetaCoreScene scene;
MetaCoreExpect(scene.SetEnvironmentSettings(environment), "应能设置场景环境");
const MetaCore::MetaCoreSceneSnapshot environmentSnapshot = scene.CaptureSnapshot();
(void)scene.SetEnvironmentSettings(MetaCore::MetaCoreSceneEnvironmentSettings{});
scene.RestoreSnapshot(environmentSnapshot);
MetaCoreExpect(scene.GetEnvironmentSettings().Source == MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair, "场景快照应恢复环境来源");
MetaCoreExpect(scene.GetEnvironmentSettings().IblKtxPath == environment.IblKtxPath, "场景快照应恢复 IBL 路径");
MetaCoreExpect(std::abs(scene.GetEnvironmentSettings().SkyboxIntensity - environment.SkyboxIntensity) <= 0.0001F, "场景快照应恢复天空盒亮度");
MetaCore::MetaCoreWindow commandWindow;
MetaCore::MetaCoreRenderDevice commandRenderDevice;
MetaCore::MetaCoreEditorViewportRenderer commandViewportRenderer;
MetaCore::MetaCoreLogService commandLogService;
MetaCore::MetaCoreEditorModuleRegistry commandModuleRegistry;
MetaCore::MetaCoreEditorContext commandContext(
commandWindow,
commandRenderDevice,
commandViewportRenderer,
scene,
commandLogService,
commandModuleRegistry
);
MetaCoreExpect(commandContext.ExecuteSnapshotCommand("修改天空盒显示", [&]() {
MetaCore::MetaCoreSceneEnvironmentSettings changed = scene.GetEnvironmentSettings();
changed.SkyboxVisible = false;
return scene.SetEnvironmentSettings(changed);
}), "环境设置修改应进入撤销栈");
MetaCoreExpect(!scene.GetEnvironmentSettings().SkyboxVisible, "执行环境设置命令后应更新天空盒显示");
MetaCoreExpect(commandContext.UndoCommand(), "环境设置应支持撤销");
MetaCoreExpect(scene.GetEnvironmentSettings().SkyboxVisible, "撤销后应恢复天空盒显示");
MetaCoreExpect(commandContext.RedoCommand(), "环境设置应支持重做");
MetaCoreExpect(!scene.GetEnvironmentSettings().SkyboxVisible, "重做后应再次关闭天空盒显示");
(void)scene.SetEnvironmentSettings(environment);
MetaCore::MetaCoreSceneRenderSync renderSync;
const MetaCore::MetaCoreSceneRenderSyncSnapshot renderSnapshot = renderSync.BuildSnapshot(scene);
MetaCoreExpect(renderSnapshot.Environment.IblKtxPath == environment.IblKtxPath, "渲染快照应包含环境路径");
MetaCoreExpect(renderSnapshot.Renderables.empty(), "环境设置不应生成可渲染场景对象");
MetaCore::MetaCoreTypeRegistry registry;
MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry);
MetaCore::MetaCoreSceneDocument document;
document.Name = "EnvironmentScene";
document.Environment = environment;
const std::filesystem::path jsonPath = tempRoot / "Environment.mcscene.json";
MetaCoreExpect(MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(jsonPath, document, registry), "环境场景 JSON 应可保存");
const auto loadedDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(jsonPath, registry);
MetaCoreExpect(loadedDocument.has_value(), "环境场景 JSON 应可读取");
MetaCoreExpect(loadedDocument->Environment.SkyboxKtxPath == environment.SkyboxKtxPath, "环境场景 JSON 应保留天空盒路径");
MetaCoreExpect(std::abs(loadedDocument->Environment.RotationDegrees - environment.RotationDegrees) <= 0.0001F, "环境场景 JSON 应保留旋转");
const std::filesystem::path packagePath = tempRoot / "Environment.mcscene";
MetaCoreExpect(MetaCore::MetaCoreWriteScenePackage(packagePath, document), "环境场景包应可保存");
const auto loadedPackage = MetaCore::MetaCoreReadScenePackage(packagePath);
MetaCoreExpect(loadedPackage.has_value(), "环境场景包应可读取");
MetaCoreExpect(loadedPackage->Environment.Source == MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair, "环境场景包应保留来源");
const std::filesystem::path legacyJsonPath = tempRoot / "Legacy.mcscene.json";
{
std::ofstream legacyOutput(legacyJsonPath, std::ios::trunc);
legacyOutput << "{\"SceneName\":\"Legacy\",\"GameObjects\":[]}";
}
const auto legacyDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(legacyJsonPath, registry);
MetaCoreExpect(legacyDocument.has_value(), "旧场景缺少环境字段时仍应可读取");
MetaCoreExpect(legacyDocument->Environment.Source == MetaCore::MetaCoreSceneEnvironmentSource::Builtin, "旧场景应使用内置环境默认值");
MetaCore::MetaCoreWindow window;
if (!window.Initialize(640, 480, "SceneEnvironmentSmoke")) {
std::cout << "[SKIP] Scene environment Filament smoke requires an available display." << std::endl;
std::filesystem::remove_all(tempRoot);
return;
}
MetaCore::MetaCoreFilamentSceneBridge bridge;
bridge.SetProjectRootPath(tempRoot);
MetaCoreExpect(bridge.Initialize(window), "环境测试桥接器应初始化成功");
bridge.SyncScene(renderSnapshot, nullptr, false, false);
bridge.ApplySceneView(MetaCore::MetaCoreSceneView{});
MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "自定义 KTX 对应加载为环境资源");
MetaCoreExpect(bridge.VerifyEnvironmentBackgroundVisibleForTesting(), "天空盒应在 Skybox 清屏模式下显示");
MetaCoreExpect(
std::abs(bridge.GetEnvironmentIndirectLightIntensityForTesting() - environment.IndirectLightIntensity) <= 0.0001F,
"环境光强度应写入 Filament"
);
MetaCore::MetaCoreSceneEnvironmentSettings hiddenEnvironment = environment;
hiddenEnvironment.SkyboxVisible = false;
(void)scene.SetEnvironmentSettings(hiddenEnvironment);
bridge.SyncScene(renderSync.BuildSnapshot(scene), nullptr, false, false);
MetaCoreExpect(!bridge.VerifyEnvironmentBackgroundVisibleForTesting(), "关闭天空盒后背景不应显示");
MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "关闭天空盒不应销毁环境光");
MetaCore::MetaCoreSceneEnvironmentSettings invalidEnvironment = environment;
invalidEnvironment.IblKtxPath = std::filesystem::path("..") / "unsafe_ibl.ktx";
(void)scene.SetEnvironmentSettings(invalidEnvironment);
bridge.SyncScene(renderSync.BuildSnapshot(scene), nullptr, false, false);
MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "无效自定义路径应回退到内置环境");
bridge.Shutdown();
window.Shutdown();
std::filesystem::remove_all(tempRoot);
}
} // namespace
int main() {
@ -4618,6 +4745,8 @@ int main() {
MetaCoreTestNativeScriptBehavioursRestorePrePlaySnapshotOnStop();
std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl;
MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot();
std::cout << "[RUN] MetaCoreTestSceneEnvironmentSettings..." << std::endl;
MetaCoreTestSceneEnvironmentSettings();
std::cout << "[RUN] MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults..." << std::endl;
MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults();
std::cout << "[RUN] MetaCoreTestCameraSelectionAndProjectionUtilities..." << std::endl;