#include "MetaCoreRender/MetaCorePandaSceneBridge.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreRender/MetaCoreRenderTypes.h" #include "MetaCoreScene/MetaCoreComponents.h" #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreFoundation/MetaCoreAssetTypes.h" #include "MetaCoreFoundation/MetaCorePackage.h" #include "ambientLight.h" #include "camera.h" #include "directionalLight.h" #include "geom.h" #include "geomNode.h" #include "geomLines.h" #include "geomTriangles.h" #include "geomVertexData.h" #include "geomVertexFormat.h" #include "geomVertexWriter.h" #include "internalName.h" #include "lineSegs.h" #include "loader.h" #include "nodePath.h" #include "perspectiveLens.h" #include "pandaNode.h" #include "shader.h" #include "texture.h" #include "texturePool.h" #include "transparencyAttrib.h" #include "windowFramework.h" #include "pointerTo.h" #include "dcast.h" #include #include #include #include #include #include #include #include #include #include #include #include #define GLM_ENABLE_EXPERIMENTAL #include #include #include #include #include #include #include #include #include #include "MetaCoreFoundation/MetaCoreAssetTypes.h" #include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreFoundation/MetaCoreArchive.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" namespace MetaCore { namespace { void MetaCoreTrace(const char* message); [[nodiscard]] NodePath MetaCoreBuildMeshFromBinary( const std::string& name, std::span payload, std::span subMeshes ); [[nodiscard]] PT(Texture) MetaCoreBuildTextureFromBinary(const std::string& name, const MetaCoreTextureAssetDocument& doc, std::span payload); [[nodiscard]] PT(Texture) MetaCoreResolveTextureWithFallback(const std::filesystem::path& projectRoot, const std::string& relativeTexturePath, const PT(Texture)& fallbackTexture); [[nodiscard]] PT(Texture) MetaCoreGetDefaultBaseColorTexture(); [[nodiscard]] PT(Texture) MetaCoreGetDefaultMetalRoughnessTexture(); [[nodiscard]] PT(Texture) MetaCoreGetDefaultNormalTexture(); [[nodiscard]] PT(Texture) MetaCoreGetDefaultEmissionTexture(); [[nodiscard]] PT(Texture) MetaCoreGetDefaultAoTexture(); [[nodiscard]] PT(Shader) MetaCoreLoadSimplePbrShader(); void MetaCoreApplySimplePbrViewInputs(NodePath& meshNode, const NodePath& sceneRootNode, const NodePath& cameraNode); void MetaCoreTrace(const char* message) { if (message == nullptr) { return; } const auto now = std::chrono::system_clock::now(); const auto time = std::chrono::system_clock::to_time_t(now); struct tm timeInfo {}; localtime_s(&timeInfo, &time); char timeBuffer[32]{}; std::strftime(timeBuffer, sizeof(timeBuffer), "[%H:%M:%S] ", &timeInfo); FILE* file = nullptr; if (fopen_s(&file, "editor.crash.log", "a") == 0 && file != nullptr) { std::fputs(timeBuffer, file); std::fputs(message, file); std::fputs("\n", file); std::fflush(file); std::fclose(file); } std::printf("%s%s\n", timeBuffer, message); } glm::mat4 MetaCoreBuildBasisSwapMatrix() { glm::mat4 basis(1.0F); basis[0] = glm::vec4(1.0F, 0.0F, 0.0F, 0.0F); basis[1] = glm::vec4(0.0F, 0.0F, 1.0F, 0.0F); basis[2] = glm::vec4(0.0F, 1.0F, 0.0F, 0.0F); basis[3] = glm::vec4(0.0F, 0.0F, 0.0F, 1.0F); return basis; } LMatrix4f MetaCoreConvertTransformToPanda(const MetaCoreTransformComponent& transform) { const glm::mat4 translation = glm::translate(glm::mat4(1.0F), transform.Position); const glm::mat4 rotation = glm::yawPitchRoll( glm::radians(transform.RotationEulerDegrees.y), glm::radians(transform.RotationEulerDegrees.x), glm::radians(transform.RotationEulerDegrees.z) ); const glm::mat4 scale = glm::scale(glm::mat4(1.0F), transform.Scale); const glm::mat4 metaCoreMatrix = translation * rotation * scale; const glm::mat4 basis = MetaCoreBuildBasisSwapMatrix(); const glm::mat4 pandaMatrix = basis * metaCoreMatrix * basis; LMatrix4f result; for (int row = 0; row < 4; ++row) { for (int column = 0; column < 4; ++column) { result.set_cell(row, column, pandaMatrix[column][row]); } } return result; } LPoint3f MetaCoreToPandaPoint(const glm::vec3& value) { return LPoint3f(value.x, -value.z, value.y); } LVector3f MetaCoreToPandaVector(const glm::vec3& value) { return LVector3f(value.x, -value.z, value.y); } LVecBase3f MetaCoreToPandaScale(const glm::vec3& value) { return LVecBase3f(value.x, value.z, value.y); } glm::mat4 MetaCoreBuildLocalTransformMatrix(const MetaCoreTransformComponent& transform) { const glm::mat4 translation = glm::translate(glm::mat4(1.0F), transform.Position); const glm::mat4 rotation = glm::yawPitchRoll( glm::radians(transform.RotationEulerDegrees.y), glm::radians(transform.RotationEulerDegrees.x), glm::radians(transform.RotationEulerDegrees.z) ); const glm::mat4 scale = glm::scale(glm::mat4(1.0F), transform.Scale); return translation * rotation * scale; } MetaCoreTransformComponent MetaCoreBuildWorldTransformComponent(const MetaCoreScene& scene, const MetaCoreGameObject& gameObject) { glm::mat4 worldMatrix = MetaCoreBuildLocalTransformMatrix(gameObject.GetComponent()); MetaCoreGameObject current = gameObject; std::size_t guard = 0; while (current && current.GetParentId() != 0 && guard++ < scene.GetGameObjects().size()) { current = scene.FindGameObject(current.GetParentId()); if (current) { worldMatrix = MetaCoreBuildLocalTransformMatrix(current.GetComponent()) * worldMatrix; } } MetaCoreTransformComponent worldTransform{}; glm::vec3 skew{}; glm::vec4 perspective{}; glm::quat rotation{}; glm::decompose(worldMatrix, worldTransform.Scale, rotation, worldTransform.Position, skew, perspective); worldTransform.RotationEulerDegrees = glm::degrees(glm::eulerAngles(rotation)); return worldTransform; } void MetaCoreApplyTransformToPandaNode(const MetaCoreTransformComponent& transform, NodePath& nodePath) { nodePath.set_pos(MetaCoreToPandaPoint(transform.Position)); nodePath.set_hpr( transform.RotationEulerDegrees.y, transform.RotationEulerDegrees.x, transform.RotationEulerDegrees.z ); nodePath.set_scale(MetaCoreToPandaScale(transform.Scale)); } NodePath MetaCoreCreateGridNode(const NodePath& parentNode) { (void)parentNode; MetaCoreTrace("metacore.render: debug scene grid paused due to backend runtime instability"); return NodePath(); } [[nodiscard]] std::filesystem::path MetaCoreEnsureBuiltinCubeEggPath() { const std::filesystem::path cubePath = std::filesystem::current_path() / "metacore_builtin_cube.egg"; if (std::filesystem::exists(cubePath)) { return cubePath; } std::ofstream cubeFile(cubePath, std::ios::trunc); if (!cubeFile.is_open()) { return {}; } cubeFile << " { Y-Up }\n" << " MetaCoreUnitCube {\n" << " cube.verts {\n" << " 1 { -0.5 -0.5 -0.5 { 0 0 -1 } { 0 0 } }\n" << " 2 { 0.5 -0.5 -0.5 { 0 0 -1 } { 1 0 } }\n" << " 3 { 0.5 0.5 -0.5 { 0 0 -1 } { 1 1 } }\n" << " 4 { -0.5 0.5 -0.5 { 0 0 -1 } { 0 1 } }\n" << " 5 { -0.5 -0.5 0.5 { 0 0 1 } { 0 0 } }\n" << " 6 { 0.5 -0.5 0.5 { 0 0 1 } { 1 0 } }\n" << " 7 { 0.5 0.5 0.5 { 0 0 1 } { 1 1 } }\n" << " 8 { -0.5 0.5 0.5 { 0 0 1 } { 0 1 } }\n" << " }\n" << " { { 0.75 0.78 0.84 1 } { 1 2 3 4 { cube.verts } } }\n" << " { { 0.75 0.78 0.84 1 } { 5 8 7 6 { cube.verts } } }\n" << " { { 0.75 0.78 0.84 1 } { 1 5 6 2 { cube.verts } } }\n" << " { { 0.75 0.78 0.84 1 } { 2 6 7 3 { cube.verts } } }\n" << " { { 0.75 0.78 0.84 1 } { 3 7 8 4 { cube.verts } } }\n" << " { { 0.75 0.78 0.84 1 } { 4 8 5 1 { cube.verts } } }\n" << "}\n"; return cubePath; } [[nodiscard]] std::filesystem::path MetaCoreEnsureBuiltinCubeModelPath() { const std::filesystem::path bamPath = std::filesystem::current_path() / "metacore_builtin_cube.bam"; if (std::filesystem::exists(bamPath)) { return bamPath; } const std::filesystem::path eggPath = MetaCoreEnsureBuiltinCubeEggPath(); if (eggPath.empty()) { return {}; } const std::filesystem::path converterPath = std::filesystem::current_path() / ".." / ".." / ".." / ".metacore" / "deps" / "panda3d" / "1.10.16-x64" / "bin" / "egg2bam.exe"; if (std::filesystem::exists(converterPath)) { const std::string command = "\"" + converterPath.lexically_normal().string() + "\" \"" + eggPath.string() + "\" \"" + bamPath.string() + "\""; const int result = std::system(command.c_str()); if (result == 0 && std::filesystem::exists(bamPath)) { return bamPath; } } return eggPath; } NodePath MetaCoreCreateUnitCubeNode(WindowFramework* windowFrameworkHandle, const NodePath& parentNode) { const std::filesystem::path cubePath = MetaCoreEnsureBuiltinCubeModelPath(); if (windowFrameworkHandle != nullptr && !cubePath.empty()) { NodePath loadedNode = windowFrameworkHandle->load_model(parentNode, Filename::from_os_specific(cubePath.string())); if (!loadedNode.is_empty()) { return loadedNode; } MetaCoreTrace("metacore.render.sync: builtin cube asset load failed"); } #if defined(_DEBUG) MetaCoreTrace("metacore.render.sync: debug fallback skipped for manual GeomVertexData cube"); return NodePath(); #else MetaCoreTrace("metacore.render.sync: create cube vertex data begin"); PT(GeomVertexData) vertexData = new GeomVertexData( "MetaCoreUnitCube", GeomVertexFormat::get_v3n3(), Geom::UH_static ); MetaCoreTrace("metacore.render.sync: create cube vertex writers begin"); GeomVertexWriter vertexWriter(vertexData, "vertex"); GeomVertexWriter normalWriter(vertexData, "normal"); const auto addFace = [&](const glm::vec3& normal, const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, const glm::vec3& d) { const glm::vec3 corners[4] = {a, b, c, d}; for (const glm::vec3& corner : corners) { const LPoint3f pandaPoint = MetaCoreToPandaPoint(corner); const LVector3f pandaNormal = MetaCoreToPandaVector(normal); vertexWriter.add_data3(pandaPoint); normalWriter.add_data3(pandaNormal); } }; MetaCoreTrace("metacore.render.sync: write cube vertices begin"); addFace(glm::vec3(0.0F, 0.0F, 1.0F), glm::vec3(-0.5F, -0.5F, 0.5F), glm::vec3(0.5F, -0.5F, 0.5F), glm::vec3(0.5F, 0.5F, 0.5F), glm::vec3(-0.5F, 0.5F, 0.5F)); addFace(glm::vec3(0.0F, 0.0F, -1.0F), glm::vec3(0.5F, -0.5F, -0.5F), glm::vec3(-0.5F, -0.5F, -0.5F), glm::vec3(-0.5F, 0.5F, -0.5F), glm::vec3(0.5F, 0.5F, -0.5F)); addFace(glm::vec3(-1.0F, 0.0F, 0.0F), glm::vec3(-0.5F, -0.5F, -0.5F), glm::vec3(-0.5F, -0.5F, 0.5F), glm::vec3(-0.5F, 0.5F, 0.5F), glm::vec3(-0.5F, 0.5F, -0.5F)); addFace(glm::vec3(1.0F, 0.0F, 0.0F), glm::vec3(0.5F, -0.5F, 0.5F), glm::vec3(0.5F, -0.5F, -0.5F), glm::vec3(0.5F, 0.5F, -0.5F), glm::vec3(0.5F, 0.5F, 0.5F)); addFace(glm::vec3(0.0F, 1.0F, 0.0F), glm::vec3(-0.5F, 0.5F, 0.5F), glm::vec3(0.5F, 0.5F, 0.5F), glm::vec3(0.5F, 0.5F, -0.5F), glm::vec3(-0.5F, 0.5F, -0.5F)); addFace(glm::vec3(0.0F, -1.0F, 0.0F), glm::vec3(-0.5F, -0.5F, -0.5F), glm::vec3(0.5F, -0.5F, -0.5F), glm::vec3(0.5F, -0.5F, 0.5F), glm::vec3(-0.5F, -0.5F, 0.5F)); MetaCoreTrace("metacore.render.sync: create cube triangles begin"); PT(GeomTriangles) triangles = new GeomTriangles(Geom::UH_static); for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { const int baseVertex = faceIndex * 4; triangles->add_vertices(baseVertex + 0, baseVertex + 1, baseVertex + 2); triangles->add_vertices(baseVertex + 0, baseVertex + 2, baseVertex + 3); } MetaCoreTrace("metacore.render.sync: create cube geom begin"); PT(Geom) geom = new Geom(vertexData); geom->add_primitive(triangles); MetaCoreTrace("metacore.render.sync: create cube geom node begin"); PT(GeomNode) geomNode = new GeomNode("MetaCoreUnitCubeNode"); geomNode->add_geom(geom); MetaCoreTrace("metacore.render.sync: attach cube geom node begin"); return parentNode.attach_new_node(geomNode); #endif } [[nodiscard]] std::filesystem::path MetaCoreResolveProjectRootFromEnvironment() { char* rawProjectPath = nullptr; std::size_t valueLength = 0; if (_dupenv_s(&rawProjectPath, &valueLength, "METACORE_PROJECT_PATH") != 0 || rawProjectPath == nullptr || valueLength == 0) { if (rawProjectPath != nullptr) { std::free(rawProjectPath); } return {}; } const std::filesystem::path projectRoot(rawProjectPath); std::free(rawProjectPath); return projectRoot; } [[nodiscard]] std::string MetaCoreBuildModelCacheKey( const MetaCoreAssetGuid& sourceModelAssetGuid, const std::string& relativeSourcePath ) { if (sourceModelAssetGuid.IsValid()) { return "model:" + sourceModelAssetGuid.ToString(); } return "path:" + relativeSourcePath; } [[nodiscard]] bool MetaCoreShouldCompareSourceModelPath( const MetaCoreAssetGuid& previousSourceModelGuid, const MetaCoreAssetGuid& nextSourceModelGuid ) { return !previousSourceModelGuid.IsValid() && !nextSourceModelGuid.IsValid(); } struct MetaCoreRuntimeModelLoadRequest { MetaCoreAssetGuid SourceModelAssetGuid{}; std::filesystem::path ProjectRoot{}; std::string RelativeSourcePath{}; bool PreferSourceLoader = false; }; [[nodiscard]] std::filesystem::path MetaCoreResolveModelRuntimePackagePath( const MetaCoreRuntimeModelLoadRequest& request, const std::filesystem::path& absoluteSourcePath ) { if (request.SourceModelAssetGuid.IsValid() && !request.ProjectRoot.empty()) { const std::filesystem::path libraryPackagePath = request.ProjectRoot / "Library" / "RuntimeAssets" / "Models" / (request.SourceModelAssetGuid.ToString() + ".mcasset"); if (std::filesystem::exists(libraryPackagePath)) { return libraryPackagePath; } } if (absoluteSourcePath.empty()) { return {}; } const std::filesystem::path sourceAdjacentPackagePath = std::filesystem::path(absoluteSourcePath.string() + ".mcasset"); if (std::filesystem::exists(sourceAdjacentPackagePath)) { return sourceAdjacentPackagePath; } return sourceAdjacentPackagePath; } [[nodiscard]] std::filesystem::path MetaCoreResolveAbsoluteModelSourcePath( const std::filesystem::path& projectRoot, const std::string& relativeSourcePath ) { return relativeSourcePath.empty() ? std::filesystem::path{} : (projectRoot / std::filesystem::path(relativeSourcePath)).lexically_normal(); } [[nodiscard]] std::optional MetaCoreReadRuntimeModelDocument( const MetaCoreRuntimeModelLoadRequest& request ) { const std::filesystem::path absoluteSourcePath = MetaCoreResolveAbsoluteModelSourcePath(request.ProjectRoot, request.RelativeSourcePath); const std::filesystem::path binaryPackagePath = MetaCoreResolveModelRuntimePackagePath(request, absoluteSourcePath); if (binaryPackagePath.empty() || !std::filesystem::exists(binaryPackagePath)) { return std::nullopt; } MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); const auto package = MetaCoreReadPackageFile(binaryPackagePath, registry); if (!package.has_value() || package->PayloadSections.empty()) { return std::nullopt; } MetaCoreModelAssetDocument modelDoc; if (!MetaCoreDeserializeFromBytes(package->PayloadSections[0], modelDoc, registry)) { return std::nullopt; } return modelDoc; } [[nodiscard]] NodePath MetaCoreTryLoadBinaryModelPackage(const std::filesystem::path& binaryPackagePath) { if (binaryPackagePath.empty() || !std::filesystem::exists(binaryPackagePath)) { return NodePath(); } MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); const auto package = MetaCoreReadPackageFile(binaryPackagePath, registry); if (!package.has_value() || package->PayloadSections.empty()) { MetaCoreTrace(("metacore.render: model binary package missing payload: " + binaryPackagePath.string()).c_str()); return NodePath(); } MetaCoreModelAssetDocument modelDoc; if (!MetaCoreDeserializeFromBytes(package->PayloadSections[0], modelDoc, registry)) { MetaCoreTrace(("metacore.render: model binary metadata failed: " + binaryPackagePath.string()).c_str()); return NodePath(); } NodePath rootNode("ModelRoot"); std::vector pandaNodes; pandaNodes.resize(modelDoc.Nodes.size()); for (std::size_t i = 0; i < modelDoc.Nodes.size(); ++i) { const auto& nodeDoc = modelDoc.Nodes[i]; pandaNodes[i] = NodePath(new PandaNode(nodeDoc.Name)); pandaNodes[i].set_tag("MetaCoreNodeIndex", std::to_string(i)); const std::size_t meshPayloadIndex = nodeDoc.MeshIndex >= 0 ? static_cast(nodeDoc.MeshIndex) + 1U : 0U; if (nodeDoc.MeshIndex >= 0 && meshPayloadIndex < package->PayloadSections.size()) { std::span subMeshes; if (static_cast(nodeDoc.MeshIndex) < modelDoc.GeneratedMeshAssets.size()) { subMeshes = modelDoc.GeneratedMeshAssets[static_cast(nodeDoc.MeshIndex)].SubMeshes; } NodePath meshGeom = MetaCoreBuildMeshFromBinary( nodeDoc.Name + "_mesh", package->PayloadSections[meshPayloadIndex], subMeshes ); if (!meshGeom.is_empty()) { meshGeom.reparent_to(pandaNodes[i]); } } } for (std::size_t i = 0; i < modelDoc.Nodes.size(); ++i) { const auto& nodeDoc = modelDoc.Nodes[i]; if (nodeDoc.ParentIndex >= 0 && static_cast(nodeDoc.ParentIndex) < pandaNodes.size()) { pandaNodes[i].reparent_to(pandaNodes[static_cast(nodeDoc.ParentIndex)]); } else { pandaNodes[i].reparent_to(rootNode); } } MetaCoreTrace(("metacore.render: model binary load success: " + binaryPackagePath.string()).c_str()); return rootNode; } [[nodiscard]] NodePath MetaCoreTryLoadSourceModelNode(const std::filesystem::path& absoluteSourcePath) { if (!std::filesystem::exists(absoluteSourcePath)) { MetaCoreTrace(("metacore.render: model source file missing: " + absoluteSourcePath.string()).c_str()); return NodePath(); } const Filename pandaPath = Filename::from_os_specific(absoluteSourcePath.string()); // Fallback to naive Panda3D loader for raw assets. PointerTo loadedNode = Loader::get_global_ptr()->load_sync(pandaPath); if (loadedNode == nullptr) { MetaCoreTrace(("metacore.render: backend model load failed: " + absoluteSourcePath.string()).c_str()); return NodePath(); } MetaCoreTrace(("metacore.render: model source load success: " + absoluteSourcePath.string()).c_str()); return NodePath(loadedNode); } // Must be called on the render/main thread. This function creates Panda3D nodes. [[nodiscard]] NodePath MetaCoreTryLoadModelRuntimeAsset(const MetaCoreRuntimeModelLoadRequest& request) { if (request.ProjectRoot.empty()) { MetaCoreTrace("metacore.render: model project root is empty"); return NodePath(); } if (!request.SourceModelAssetGuid.IsValid() && request.RelativeSourcePath.empty()) { MetaCoreTrace("metacore.render: model has neither asset guid nor source path"); return NodePath(); } const std::filesystem::path absoluteSourcePath = MetaCoreResolveAbsoluteModelSourcePath(request.ProjectRoot, request.RelativeSourcePath); const std::filesystem::path binaryPackagePath = MetaCoreResolveModelRuntimePackagePath(request, absoluteSourcePath); if (!request.PreferSourceLoader) { NodePath binaryModel = MetaCoreTryLoadBinaryModelPackage(binaryPackagePath); if (!binaryModel.is_empty()) { return binaryModel; } if (!absoluteSourcePath.empty() && std::filesystem::exists(binaryPackagePath)) { MetaCoreTrace(("metacore.render: model binary fallback to source: " + absoluteSourcePath.string()).c_str()); } } if (absoluteSourcePath.empty()) { MetaCoreTrace(("metacore.render: model runtime package missing for guid: " + request.SourceModelAssetGuid.ToString()).c_str()); return NodePath(); } return MetaCoreTryLoadSourceModelNode(absoluteSourcePath); } // Build a Panda3D Mesh from MetaCore's binary mesh payload. // format: [u32 vcount, u32 icount, Vertex[vcount], u32[icount]] [[nodiscard]] NodePath MetaCoreBuildMeshFromBinary( const std::string& name, std::span payload, std::span subMeshes ) { if (payload.size() < sizeof(std::uint32_t) * 2) { MetaCoreTrace("metacore.render: mesh payload too small for header"); return NodePath(); } std::uint32_t vCount = 0; std::uint32_t iCount = 0; std::memcpy(&vCount, payload.data(), sizeof(std::uint32_t)); std::memcpy(&iCount, payload.data() + sizeof(std::uint32_t), sizeof(std::uint32_t)); const std::size_t expectedSize = sizeof(std::uint32_t) * 2 + vCount * sizeof(MetaCoreMeshVertex) + iCount * sizeof(std::uint32_t); if (payload.size() < expectedSize) { MetaCoreTrace(("metacore.render: mesh payload size mismatch. expected=" + std::to_string(expectedSize) + " actual=" + std::to_string(payload.size())).c_str()); return NodePath(); } if (iCount % 3 != 0) { MetaCoreTrace(("metacore.render: mesh payload index count is not triangular: " + std::to_string(iCount)).c_str()); return NodePath(); } const MetaCoreMeshVertex* vertices = reinterpret_cast(payload.data() + sizeof(std::uint32_t) * 2); const std::uint32_t* indices = reinterpret_cast(payload.data() + sizeof(std::uint32_t) * 2 + vCount * sizeof(MetaCoreMeshVertex)); PT(GeomVertexData) vdata = new GeomVertexData(name, GeomVertexFormat::get_v3n3t2(), Geom::UH_static); vdata->unclean_set_num_rows(vCount); GeomVertexWriter vertexWriter(vdata, "vertex"); GeomVertexWriter normalWriter(vdata, "normal"); GeomVertexWriter texcoordWriter(vdata, "texcoord"); for (std::uint32_t i = 0; i < vCount; ++i) { const LPoint3f pandaPoint = MetaCoreToPandaPoint(glm::vec3(vertices[i].px, vertices[i].py, vertices[i].pz)); const LVector3f pandaNormal = MetaCoreToPandaVector(glm::vec3(vertices[i].nx, vertices[i].ny, vertices[i].nz)); vertexWriter.add_data3(pandaPoint); normalWriter.add_data3(pandaNormal); texcoordWriter.add_data2(vertices[i].u, vertices[i].v); } NodePath meshRoot(new PandaNode(name)); auto appendSubMesh = [&]( const std::string& subMeshName, std::int32_t materialSlotIndex, std::uint32_t firstIndex, std::uint32_t subMeshIndexCount ) { if (firstIndex >= iCount || subMeshIndexCount == 0) { return; } const std::uint32_t clampedEnd = std::min(iCount, firstIndex + subMeshIndexCount); const std::uint32_t triangleEnd = firstIndex + ((clampedEnd - firstIndex) / 3U) * 3U; if (triangleEnd <= firstIndex) { return; } PT(GeomTriangles) triangles = new GeomTriangles(Geom::UH_static); for (std::uint32_t i = firstIndex; i + 2 < triangleEnd; i += 3) { triangles->add_vertices(indices[i], indices[i + 1], indices[i + 2]); } PT(Geom) geom = new Geom(vdata); geom->add_primitive(triangles); PT(GeomNode) node = new GeomNode(subMeshName); node->add_geom(geom); NodePath subMeshNode = meshRoot.attach_new_node(node); subMeshNode.set_tag("MetaCoreMaterialSlotIndex", std::to_string(materialSlotIndex)); }; bool appendedAnySubMesh = false; for (const MetaCoreMeshSubMeshDocument& subMesh : subMeshes) { const std::uint32_t firstIndex = subMesh.FirstIndex; const std::uint32_t subMeshIndexCount = subMesh.IndexCount == 0 ? iCount : subMesh.IndexCount; const int childCountBefore = meshRoot.get_num_children(); appendSubMesh( subMesh.Name.empty() ? (name + "_submesh") : subMesh.Name, subMesh.MaterialSlotIndex, firstIndex, subMeshIndexCount ); appendedAnySubMesh = appendedAnySubMesh || meshRoot.get_num_children() > childCountBefore; } if (!appendedAnySubMesh) { appendSubMesh(name + "_submesh_0", 0, 0, iCount); } return meshRoot; } // Build a Panda3D Texture from MetaCore's binary texture payload. [[nodiscard]] PT(Texture) MetaCoreBuildTextureFromBinary( const std::string& name, const MetaCoreTextureAssetDocument& doc, std::span payload ) { if (doc.Width <= 0 || doc.Height <= 0 || doc.Channels <= 0) { MetaCoreTrace("metacore.render: texture payload has invalid dimensions"); return nullptr; } PT(Texture) texture = new Texture(name); // Choose the right format based on channel count Texture::Format format = Texture::F_rgba8; if (doc.Channels == 3) format = Texture::F_rgb8; else if (doc.Channels == 1) format = Texture::F_luminance; texture->setup_2d_texture(doc.Width, doc.Height, Texture::T_unsigned_byte, format); // Copy payload data directly to Panda3D's RAM image buffer PTA_uchar ramImage = texture->modify_ram_image(); if (ramImage.size() == payload.size()) { std::memcpy(&ramImage[0], reinterpret_cast(payload.data()), payload.size()); } else { MetaCoreTrace(("metacore.render: texture payload size mismatch. expected=" + std::to_string(ramImage.size()) + " actual=" + std::to_string(payload.size())).c_str()); return nullptr; } // Enable linear filtering by default for smoother look texture->set_minfilter(SamplerState::FT_linear_mipmap_linear); texture->set_magfilter(SamplerState::FT_linear); return texture; } [[nodiscard]] PT(Shader) MetaCoreLoadSimplePbrShader() { static PT(Shader) cachedShader; static bool attemptedLoad = false; if (attemptedLoad) { return cachedShader; } attemptedLoad = true; const std::filesystem::path shaderRoot = std::filesystem::current_path() / "simplepbr" / "shaders"; const std::filesystem::path vertexPath = shaderRoot / "simplepbr.vert"; const std::filesystem::path fragmentPath = shaderRoot / "simplepbr.frag"; if (!std::filesystem::exists(vertexPath) || !std::filesystem::exists(fragmentPath)) { MetaCoreTrace("simplepbr: shader files missing in runtime directory"); return nullptr; } cachedShader = Shader::load( Shader::SL_GLSL, Filename::from_os_specific(vertexPath.string()), Filename::from_os_specific(fragmentPath.string()) ); if (cachedShader == nullptr) { MetaCoreTrace("simplepbr: failed to load GLSL shader"); } else { MetaCoreTrace("simplepbr: shader loaded"); } return cachedShader; } [[nodiscard]] PT(Texture) MetaCoreCreateSolidTexture( const std::string& textureName, unsigned char r, unsigned char g, unsigned char b, unsigned char a ) { PT(Texture) texture = new Texture(textureName); texture->setup_2d_texture(1, 1, Texture::T_unsigned_byte, Texture::F_rgba8); PTA_uchar image = texture->modify_ram_image(); if (image.size() >= 4) { image[0] = r; image[1] = g; image[2] = b; image[3] = a; } texture->set_minfilter(SamplerState::FT_nearest); texture->set_magfilter(SamplerState::FT_nearest); texture->set_wrap_u(SamplerState::WM_repeat); texture->set_wrap_v(SamplerState::WM_repeat); return texture; } [[nodiscard]] PT(Texture) MetaCoreCreateSolidCubeMap( const std::string& textureName, unsigned char r, unsigned char g, unsigned char b, unsigned char a ) { PT(Texture) texture = new Texture(textureName); texture->setup_cube_map(1, Texture::T_unsigned_byte, Texture::F_rgba8); PTA_uchar image = texture->modify_ram_image(); for (size_t index = 0; index + 3 < image.size(); index += 4) { image[index + 0] = r; image[index + 1] = g; image[index + 2] = b; image[index + 3] = a; } texture->set_minfilter(SamplerState::FT_nearest); texture->set_magfilter(SamplerState::FT_nearest); texture->set_wrap_u(SamplerState::WM_clamp); texture->set_wrap_v(SamplerState::WM_clamp); texture->set_wrap_w(SamplerState::WM_clamp); return texture; } [[nodiscard]] PT(Texture) MetaCoreGetDefaultBaseColorTexture() { static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultBaseColor", 255, 255, 255, 255); return texture; } [[nodiscard]] PT(Texture) MetaCoreGetDefaultMetalRoughnessTexture() { static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultMetalRoughness", 255, 255, 255, 255); return texture; } [[nodiscard]] PT(Texture) MetaCoreGetDefaultEmissionTexture() { static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultEmission", 0, 0, 0, 255); return texture; } [[nodiscard]] PT(Texture) MetaCoreGetDefaultAoTexture() { static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultAo", 255, 255, 255, 255); return texture; } [[nodiscard]] PT(Texture) MetaCoreGetDefaultNormalTexture() { static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultNormal", 128, 128, 255, 255); return texture; } [[nodiscard]] PT(Texture) MetaCoreLoadBrdfLutTexture() { static PT(Texture) cachedTexture; static bool attemptedLoad = false; if (attemptedLoad) { return cachedTexture; } attemptedLoad = true; const std::filesystem::path texturePath = std::filesystem::current_path() / "simplepbr" / "textures" / "brdf_lut.txo"; if (!std::filesystem::exists(texturePath)) { MetaCoreTrace("simplepbr: brdf_lut.txo missing in runtime directory"); cachedTexture = MetaCoreCreateSolidTexture("MetaCoreFallbackBrdfLut", 255, 255, 255, 255); return cachedTexture; } cachedTexture = TexturePool::load_texture(Filename::from_os_specific(texturePath.string())); if (cachedTexture == nullptr) { MetaCoreTrace("simplepbr: failed to load brdf_lut.txo"); cachedTexture = MetaCoreCreateSolidTexture("MetaCoreFallbackBrdfLut", 255, 255, 255, 255); } return cachedTexture; } [[nodiscard]] PT(Texture) MetaCoreGetDefaultFilteredEnvMap() { static PT(Texture) texture = MetaCoreCreateSolidCubeMap("MetaCoreDefaultFilteredEnvMap", 0, 0, 0, 255); return texture; } [[nodiscard]] PT(Texture) MetaCoreResolveTextureWithFallback( const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& textureGuid, const std::string& relativeTexturePath, const PT(Texture)& fallbackTexture ) { if (projectRoot.empty() || (!textureGuid.IsValid() && relativeTexturePath.empty())) { return fallbackTexture; } const std::filesystem::path absoluteTexturePath = relativeTexturePath.empty() ? std::filesystem::path{} : projectRoot / std::filesystem::path(relativeTexturePath); std::filesystem::path binaryPackagePath; if (textureGuid.IsValid()) { binaryPackagePath = projectRoot / "Library" / "RuntimeAssets" / "Textures" / (textureGuid.ToString() + ".mcasset"); } if ((binaryPackagePath.empty() || !std::filesystem::exists(binaryPackagePath)) && !absoluteTexturePath.empty()) { binaryPackagePath = std::filesystem::path(absoluteTexturePath.string() + ".mcasset"); } if (std::filesystem::exists(binaryPackagePath)) { MetaCoreTypeRegistry registry; // We only need basic types for the asset document MetaCoreRegisterFoundationGeneratedTypes(registry); const auto package = MetaCoreReadPackageFile(binaryPackagePath, registry); if (package.has_value() && package->PayloadSections.size() > 1) { // First payload is the metadata (MetaCoreTextureAssetDocument) MetaCoreTextureAssetDocument texDoc; if (MetaCoreDeserializeFromBytes(package->PayloadSections[0], texDoc, registry)) { PT(Texture) binaryTex = MetaCoreBuildTextureFromBinary( relativeTexturePath, texDoc, package->PayloadSections[1] ); if (binaryTex != nullptr) { MetaCoreTrace(("metacore.render: texture binary load success: " + binaryPackagePath.string()).c_str()); return binaryTex; } } } } if (absoluteTexturePath.empty()) { return fallbackTexture; } if (!std::filesystem::exists(absoluteTexturePath)) { return fallbackTexture; } PT(Texture) texture = TexturePool::load_texture(Filename::from_os_specific(absoluteTexturePath.string())); if (texture == nullptr) { MetaCoreTrace(("metacore.render: backend texture load failed: " + absoluteTexturePath.string()).c_str()); return fallbackTexture; } return texture; } [[nodiscard]] PT(Texture) MetaCoreResolveBaseColorTexture( const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& textureGuid, const std::string& relativeTexturePath ) { return MetaCoreResolveTextureWithFallback(projectRoot, textureGuid, relativeTexturePath, MetaCoreGetDefaultBaseColorTexture()); } void MetaCoreApplyBaseColorTexture( NodePath& meshNode, const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& textureGuid, const std::string& relativeTexturePath ) { if (meshNode.is_empty()) { return; } PT(Texture) texture = MetaCoreResolveBaseColorTexture(projectRoot, textureGuid, relativeTexturePath); if (texture == nullptr) { meshNode.set_texture_off(1); return; } meshNode.set_texture(texture, 1); meshNode.set_shader_input(InternalName::make("p3d_TextureBaseColor"), texture); } void MetaCoreApplyMetallicRoughnessTexture( NodePath& meshNode, const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& textureGuid, const std::string& relativeTexturePath ) { if (meshNode.is_empty()) { return; } PT(Texture) texture = MetaCoreResolveTextureWithFallback( projectRoot, textureGuid, relativeTexturePath, MetaCoreGetDefaultMetalRoughnessTexture() ); if (texture == nullptr) { return; } meshNode.set_shader_input(InternalName::make("p3d_TextureMetalRoughness"), texture); } void MetaCoreApplyNormalTexture( NodePath& meshNode, const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& textureGuid, const std::string& relativeTexturePath ) { if (meshNode.is_empty()) { return; } PT(Texture) texture = MetaCoreResolveTextureWithFallback( projectRoot, textureGuid, relativeTexturePath, MetaCoreGetDefaultNormalTexture() ); if (texture == nullptr) { return; } meshNode.set_shader_input(InternalName::make("p3d_TextureNormal"), texture); } void MetaCoreApplyEmissionTexture( NodePath& meshNode, const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& textureGuid, const std::string& relativeTexturePath ) { if (meshNode.is_empty()) { return; } PT(Texture) texture = MetaCoreResolveTextureWithFallback( projectRoot, textureGuid, relativeTexturePath, MetaCoreGetDefaultEmissionTexture() ); if (texture == nullptr) { return; } meshNode.set_shader_input(InternalName::make("p3d_TextureEmission"), texture); } void MetaCoreApplyAoTexture( NodePath& meshNode, const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& textureGuid, const std::string& relativeTexturePath ) { if (meshNode.is_empty()) { return; } PT(Texture) texture = MetaCoreResolveTextureWithFallback( projectRoot, textureGuid, relativeTexturePath, MetaCoreGetDefaultAoTexture() ); if (texture == nullptr) { return; } meshNode.set_shader_input(InternalName::make("p3d_TextureOcclusion"), texture); } void MetaCoreApplySimplePbrPreview(NodePath& meshNode) { if (meshNode.is_empty()) { return; } PT(Shader) simplePbrShader = MetaCoreLoadSimplePbrShader(); if (simplePbrShader == nullptr) { meshNode.clear_shader(); return; } meshNode.set_shader(simplePbrShader, 1); } void MetaCoreApplySimplePbrMaterialInputs( NodePath& meshNode, const MetaCoreMeshRendererComponent& meshRenderer ) { if (meshNode.is_empty()) { return; } meshNode.set_shader_input( InternalName::make("p3d_Material.baseColor"), LVecBase4( meshRenderer.BaseColor.r, meshRenderer.BaseColor.g, meshRenderer.BaseColor.b, 1.0F ) ); meshNode.set_shader_input( InternalName::make("p3d_Material.roughness"), LVecBase4(meshRenderer.Roughness, 0.0F, 0.0F, 0.0F) ); meshNode.set_shader_input( InternalName::make("p3d_Material.metallic"), LVecBase4(meshRenderer.Metallic, 0.0F, 0.0F, 0.0F) ); meshNode.set_shader_input( InternalName::make("p3d_Material.emission"), LVecBase4( meshRenderer.EmissiveColor.r, meshRenderer.EmissiveColor.g, meshRenderer.EmissiveColor.b, 1.0F ) ); meshNode.set_shader_input( InternalName::make("p3d_TextureMetalRoughness"), MetaCoreGetDefaultMetalRoughnessTexture() ); meshNode.set_shader_input( InternalName::make("p3d_TextureNormal"), MetaCoreGetDefaultNormalTexture() ); meshNode.set_shader_input( InternalName::make("p3d_TextureEmission"), MetaCoreGetDefaultEmissionTexture() ); meshNode.set_shader_input( InternalName::make("p3d_TextureOcclusion"), MetaCoreGetDefaultAoTexture() ); meshNode.set_shader_input( InternalName::make("brdf_lut"), MetaCoreLoadBrdfLutTexture() ); meshNode.set_shader_input( InternalName::make("filtered_env_map"), MetaCoreGetDefaultFilteredEnvMap() ); meshNode.set_shader_input( InternalName::make("max_reflection_lod"), LVecBase4(0.0F, 0.0F, 0.0F, 0.0F) ); meshNode.set_shader_input( InternalName::make("metacore_AlphaCutoff"), LVecBase4(meshRenderer.AlphaCutoff, 0.0F, 0.0F, 0.0F) ); meshNode.set_shader_input( InternalName::make("metacore_AlphaMode"), LVecBase4(static_cast(meshRenderer.AlphaMode), 0.0F, 0.0F, 0.0F) ); } [[nodiscard]] std::string MetaCoreFindTextureSourcePath( const MetaCoreModelAssetDocument& modelDoc, const MetaCoreAssetGuid& textureGuid ) { if (!textureGuid.IsValid()) { return {}; } const auto textureIterator = std::find_if( modelDoc.GeneratedTextureAssets.begin(), modelDoc.GeneratedTextureAssets.end(), [&](const MetaCoreTextureAssetDocument& textureAsset) { return textureAsset.AssetGuid == textureGuid; } ); return textureIterator != modelDoc.GeneratedTextureAssets.end() ? textureIterator->SourcePath.generic_string() : std::string{}; } [[nodiscard]] MetaCoreMeshRendererComponent MetaCoreBuildMaterialPreviewComponent( const MetaCoreModelAssetDocument& modelDoc, const MetaCoreMaterialAssetDocument& materialAsset ) { MetaCoreMeshRendererComponent preview; preview.BaseColor = materialAsset.BaseColor; preview.DoubleSided = materialAsset.DoubleSided; preview.Metallic = materialAsset.Metallic; preview.Roughness = materialAsset.Roughness; preview.AlphaCutoff = materialAsset.AlphaCutoff; preview.EmissiveColor = materialAsset.EmissiveColor; preview.AlphaMode = materialAsset.AlphaMode == MetaCoreMaterialAlphaMode::Mask ? MetaCoreMeshAlphaMode::Mask : (materialAsset.AlphaMode == MetaCoreMaterialAlphaMode::Blend ? MetaCoreMeshAlphaMode::Blend : MetaCoreMeshAlphaMode::Opaque); preview.BaseColorTextureGuid = materialAsset.BaseColorTexture; preview.MetallicRoughnessTextureGuid = materialAsset.MetallicRoughnessTexture; preview.NormalTextureGuid = materialAsset.NormalTexture; preview.EmissiveTextureGuid = materialAsset.EmissiveTexture; preview.AoTextureGuid = materialAsset.AoTexture; preview.BaseColorTexturePath = MetaCoreFindTextureSourcePath(modelDoc, materialAsset.BaseColorTexture); preview.MetallicRoughnessTexturePath = MetaCoreFindTextureSourcePath(modelDoc, materialAsset.MetallicRoughnessTexture); preview.NormalTexturePath = MetaCoreFindTextureSourcePath(modelDoc, materialAsset.NormalTexture); preview.EmissiveTexturePath = MetaCoreFindTextureSourcePath(modelDoc, materialAsset.EmissiveTexture); preview.AoTexturePath = MetaCoreFindTextureSourcePath(modelDoc, materialAsset.AoTexture); return preview; } void MetaCoreApplyMaterialPreviewToNode( NodePath& meshNode, const std::filesystem::path& projectRoot, const MetaCoreMeshRendererComponent& materialPreview, const NodePath& sceneRootNode, const NodePath* cameraNode ) { if (meshNode.is_empty()) { return; } meshNode.set_material_off(1); meshNode.set_light_off(1); meshNode.set_two_sided(materialPreview.DoubleSided); meshNode.set_color( materialPreview.BaseColor.r, materialPreview.BaseColor.g, materialPreview.BaseColor.b, 1.0F ); MetaCoreApplyBaseColorTexture( meshNode, projectRoot, materialPreview.BaseColorTextureGuid, materialPreview.BaseColorTexturePath ); MetaCoreApplyMetallicRoughnessTexture( meshNode, projectRoot, materialPreview.MetallicRoughnessTextureGuid, materialPreview.MetallicRoughnessTexturePath ); MetaCoreApplyNormalTexture( meshNode, projectRoot, materialPreview.NormalTextureGuid, materialPreview.NormalTexturePath ); MetaCoreApplyEmissionTexture( meshNode, projectRoot, materialPreview.EmissiveTextureGuid, materialPreview.EmissiveTexturePath ); MetaCoreApplyAoTexture( meshNode, projectRoot, materialPreview.AoTextureGuid, materialPreview.AoTexturePath ); MetaCoreApplySimplePbrPreview(meshNode); MetaCoreApplySimplePbrMaterialInputs(meshNode, materialPreview); if (cameraNode != nullptr) { MetaCoreApplySimplePbrViewInputs(meshNode, sceneRootNode, *cameraNode); } if (materialPreview.AlphaMode == MetaCoreMeshAlphaMode::Blend) { meshNode.set_transparency(TransparencyAttrib::M_alpha); } else if (materialPreview.AlphaMode == MetaCoreMeshAlphaMode::Mask) { meshNode.set_transparency(TransparencyAttrib::M_binary); } else { meshNode.clear_transparency(); } } void MetaCoreApplyMaterialSlotsToSubMeshes( NodePath& meshNode, const std::filesystem::path& projectRoot, const MetaCoreMeshRendererComponent& meshRenderer, const MetaCoreModelAssetDocument& modelDoc, const NodePath& sceneRootNode, const NodePath* cameraNode ) { if (meshNode.is_empty()) { return; } NodePathCollection subMeshNodes = meshNode.find_all_matches("**/=MetaCoreMaterialSlotIndex"); for (int nodeIndex = 0; nodeIndex < subMeshNodes.get_num_paths(); ++nodeIndex) { NodePath subMeshNode = subMeshNodes.get_path(nodeIndex); const std::string slotText = subMeshNode.get_tag("MetaCoreMaterialSlotIndex"); char* parseEnd = nullptr; const long slotIndex = std::strtol(slotText.c_str(), &parseEnd, 10); if (parseEnd == slotText.c_str() || slotIndex < 0 || static_cast(slotIndex) >= meshRenderer.MaterialAssetGuids.size()) { MetaCoreApplyMaterialPreviewToNode(subMeshNode, projectRoot, meshRenderer, sceneRootNode, cameraNode); continue; } const MetaCoreAssetGuid materialGuid = meshRenderer.MaterialAssetGuids[static_cast(slotIndex)]; if (!materialGuid.IsValid()) { MetaCoreApplyMaterialPreviewToNode(subMeshNode, projectRoot, meshRenderer, sceneRootNode, cameraNode); continue; } const auto materialIterator = std::find_if( modelDoc.GeneratedMaterialAssets.begin(), modelDoc.GeneratedMaterialAssets.end(), [&](const MetaCoreMaterialAssetDocument& materialAsset) { return materialAsset.AssetGuid == materialGuid; } ); if (materialIterator == modelDoc.GeneratedMaterialAssets.end()) { MetaCoreApplyMaterialPreviewToNode(subMeshNode, projectRoot, meshRenderer, sceneRootNode, cameraNode); continue; } MetaCoreMeshRendererComponent materialPreview = MetaCoreBuildMaterialPreviewComponent(modelDoc, *materialIterator); MetaCoreApplyMaterialPreviewToNode( subMeshNode, projectRoot, materialPreview, sceneRootNode, cameraNode ); } } void MetaCoreApplySimplePbrViewInputs( NodePath& meshNode, const NodePath& sceneRootNode, const NodePath& cameraNode ) { if (meshNode.is_empty()) { return; } LPoint3f cameraWorldPosition(0.0F, 0.0F, 0.0F); if (!cameraNode.is_empty()) { cameraWorldPosition = cameraNode.get_pos(sceneRootNode); } meshNode.set_shader_input( InternalName::make("camera_world_position"), LVecBase3f(cameraWorldPosition.get_x(), cameraWorldPosition.get_y(), cameraWorldPosition.get_z()) ); } glm::mat4 MetaCoreConvertPandaMatrixToGlm(const LMatrix4f& matrix) { glm::mat4 result(1.0F); for (int col = 0; col < 4; ++col) { for (int row = 0; row < 4; ++row) { result[col][row] = matrix.get_cell(col, row); } } return result; } } // namespace class MetaCorePandaSceneBridge::MetaCorePandaSceneBridgeImpl { public: struct MetaCorePandaObjectState { NodePath RootNode{}; NodePath MeshNode{}; NodePath LightNode{}; MetaCoreMeshSourceKind MeshSource = MetaCoreMeshSourceKind::Builtin; MetaCoreBuiltinMeshType BuiltinMesh = MetaCoreBuiltinMeshType::Cube; MetaCoreAssetGuid MeshAssetGuid{}; MetaCoreAssetGuid SourceModelAssetGuid{}; std::string SourceModelPath{}; std::int32_t ModelNodeIndex = -1; std::vector MaterialAssetGuids{}; bool IsUsingPlaceholder = false; }; MetaCoreRenderDevice* RenderDevice = nullptr; NodePath GridNode{}; NodePath AmbientLightNode{}; std::filesystem::path ProjectRootPath{}; MetaCoreId SelectedObjectId = 0; std::unordered_map ObjectStates{}; // Asset Management std::mutex CacheMutex{}; std::unordered_map ModelCache{}; bool RuntimeSyncFailure = false; std::string LastRuntimeSyncFailure{}; }; MetaCorePandaSceneBridge::MetaCorePandaSceneBridge() : Impl_(std::make_unique()) { } MetaCorePandaSceneBridge::~MetaCorePandaSceneBridge() { Shutdown(); } bool MetaCorePandaSceneBridge::Initialize(MetaCoreRenderDevice& renderDevice) { Shutdown(); auto* windowFrameworkHandle = static_cast(renderDevice.GetNativeWindowFrameworkHandle()); auto* sceneRootHandle = static_cast(renderDevice.GetNativeSceneRootHandle()); if (windowFrameworkHandle == nullptr || sceneRootHandle == nullptr || sceneRootHandle->is_empty()) { return false; } Impl_->RenderDevice = &renderDevice; Impl_->ProjectRootPath = MetaCoreResolveProjectRootFromEnvironment(); Impl_->GridNode = MetaCoreCreateGridNode(*sceneRootHandle); if (!Impl_->GridNode.is_empty()) { Impl_->GridNode.set_light_off(1); } PT(AmbientLight) ambientLight = new AmbientLight("MetaCoreAmbientLight"); ambientLight->set_color(LColor(0.85F, 0.85F, 0.85F, 1.0F)); Impl_->AmbientLightNode = sceneRootHandle->attach_new_node(ambientLight); sceneRootHandle->set_light(Impl_->AmbientLightNode); return true; } void MetaCorePandaSceneBridge::Shutdown() { if (Impl_ == nullptr) { return; } for (auto& [objectId, objectState] : Impl_->ObjectStates) { (void)objectId; if (!objectState.MeshNode.is_empty()) { objectState.MeshNode.remove_node(); objectState.MeshNode = NodePath(); } if (!objectState.LightNode.is_empty()) { objectState.LightNode.remove_node(); objectState.LightNode = NodePath(); } if (!objectState.RootNode.is_empty()) { objectState.RootNode.remove_node(); objectState.RootNode = NodePath(); } } Impl_->ObjectStates.clear(); // Clear asset cache { std::lock_guard lock(Impl_->CacheMutex); for (auto& [path, node] : Impl_->ModelCache) { node.remove_node(); } Impl_->ModelCache.clear(); } if (!Impl_->AmbientLightNode.is_empty()) { Impl_->AmbientLightNode.remove_node(); Impl_->AmbientLightNode = NodePath(); } if (!Impl_->GridNode.is_empty()) { Impl_->GridNode.remove_node(); Impl_->GridNode = NodePath(); } Impl_->RenderDevice = nullptr; Impl_->ProjectRootPath.clear(); Impl_->SelectedObjectId = 0; Impl_->RuntimeSyncFailure = false; Impl_->LastRuntimeSyncFailure.clear(); } void MetaCorePandaSceneBridge::SetProjectRootPath(const std::filesystem::path& projectRootPath) { if (Impl_ == nullptr) { return; } if (Impl_->ProjectRootPath != projectRootPath) { std::lock_guard lock(Impl_->CacheMutex); for (auto& [cacheKey, node] : Impl_->ModelCache) { (void)cacheKey; node.remove_node(); } Impl_->ModelCache.clear(); } Impl_->ProjectRootPath = projectRootPath; MetaCoreTrace(("metacore.render: project root path set to " + projectRootPath.string()).c_str()); } void MetaCorePandaSceneBridge::SyncScene(const MetaCoreScene& scene, bool compatibilityMeshOnly) { if (Impl_->RuntimeSyncFailure) { return; } if (Impl_->RenderDevice == nullptr) { return; } auto* windowFrameworkHandle = static_cast(Impl_->RenderDevice->GetNativeWindowFrameworkHandle()); auto* sceneRootHandle = static_cast(Impl_->RenderDevice->GetNativeSceneRootHandle()); if (windowFrameworkHandle == nullptr || sceneRootHandle == nullptr || sceneRootHandle->is_empty()) { return; } std::unordered_set aliveObjectIds; for (const MetaCoreGameObject& gameObject : scene.GetGameObjects()) { if (compatibilityMeshOnly && !gameObject.HasComponent()) { continue; } std::string syncStage = "begin"; try { aliveObjectIds.insert(gameObject.GetId()); syncStage = "object_state_lookup"; auto [iterator, inserted] = Impl_->ObjectStates.try_emplace(gameObject.GetId()); MetaCorePandaSceneBridgeImpl::MetaCorePandaObjectState& objectState = iterator->second; if (!compatibilityMeshOnly) { if (inserted || objectState.RootNode.is_empty()) { syncStage = "create_root_node"; PT(PandaNode) rootNode = new PandaNode("MetaCoreObject"); syncStage = "attach_root_node"; objectState.RootNode = sceneRootHandle->attach_new_node(rootNode); } syncStage = "apply_transform"; MetaCoreApplyTransformToPandaNode(gameObject.GetComponent(), objectState.RootNode); if (gameObject.GetParentId() != 0) { syncStage = "reparent_parent"; const auto parentIterator = Impl_->ObjectStates.find(gameObject.GetParentId()); if (parentIterator != Impl_->ObjectStates.end() && !parentIterator->second.RootNode.is_empty()) { objectState.RootNode.reparent_to(parentIterator->second.RootNode); } else { syncStage = "reparent_scene_root_from_parent"; objectState.RootNode.reparent_to(*sceneRootHandle); } } else { syncStage = "reparent_scene_root"; objectState.RootNode.reparent_to(*sceneRootHandle); } } else if (!objectState.RootNode.is_empty()) { objectState.RootNode.remove_node(); objectState.RootNode = NodePath(); } if (gameObject.HasComponent()) { syncStage = "mesh_renderer"; const bool materialSlotsChanged = objectState.MaterialAssetGuids != gameObject.GetComponent().MaterialAssetGuids; const bool requiresMeshRebuild = objectState.MeshNode.is_empty() || objectState.MeshSource != gameObject.GetComponent().MeshSource || (gameObject.GetComponent().MeshSource == MetaCoreMeshSourceKind::Builtin && objectState.BuiltinMesh != gameObject.GetComponent().BuiltinMesh) || (gameObject.GetComponent().MeshSource == MetaCoreMeshSourceKind::Asset && (objectState.MeshAssetGuid != gameObject.GetComponent().MeshAssetGuid || objectState.SourceModelAssetGuid != gameObject.GetComponent().SourceModelAssetGuid || (MetaCoreShouldCompareSourceModelPath( objectState.SourceModelAssetGuid, gameObject.GetComponent().SourceModelAssetGuid ) && objectState.SourceModelPath != gameObject.GetComponent().SourceModelPath) || objectState.ModelNodeIndex != gameObject.GetComponent().ModelNodeIndex)); if (requiresMeshRebuild && !objectState.MeshNode.is_empty()) { objectState.MeshNode.remove_node(); objectState.MeshNode = NodePath(); } if (requiresMeshRebuild) { NodePath meshParent = compatibilityMeshOnly ? *sceneRootHandle : objectState.RootNode; if (gameObject.GetComponent().MeshSource == MetaCoreMeshSourceKind::Builtin) { objectState.MeshNode = MetaCoreCreateUnitCubeNode(windowFrameworkHandle, meshParent); objectState.IsUsingPlaceholder = false; } else { const std::string& path = gameObject.GetComponent().SourceModelPath; const std::string modelCacheKey = MetaCoreBuildModelCacheKey( gameObject.GetComponent().SourceModelAssetGuid, path ); // 1. Check Model Cache bool foundInCache = false; { std::lock_guard lock(Impl_->CacheMutex); auto it = Impl_->ModelCache.find(modelCacheKey); if (it != Impl_->ModelCache.end()) { NodePath masterNode = it->second; if (gameObject.GetComponent().ModelNodeIndex < 0) { objectState.MeshNode = masterNode.copy_to(meshParent); } else { NodePath subNode = masterNode.find("**/=MetaCoreNodeIndex=" + std::to_string(gameObject.GetComponent().ModelNodeIndex)); if (!subNode.is_empty()) { objectState.MeshNode = subNode.copy_to(meshParent); objectState.MeshNode.set_mat(LMatrix4::ident_mat()); // Only remove children that are tagged as MetaCore model nodes, // as these represent separate game objects in our hierarchy. // This preserves internal geometry nodes (GeomNodes) of the targeted node. NodePathCollection children = objectState.MeshNode.get_children(); for (int i = 0; i < children.get_num_paths(); ++i) { NodePath child = children.get_path(i); if (child.has_tag("MetaCoreNodeIndex")) { child.remove_node(); } } } else { objectState.MeshNode = masterNode.copy_to(meshParent); } } objectState.IsUsingPlaceholder = false; foundInCache = true; } } if (!foundInCache) { const std::filesystem::path& projectRoot = !Impl_->ProjectRootPath.empty() ? Impl_->ProjectRootPath : (Impl_->ProjectRootPath = MetaCoreResolveProjectRootFromEnvironment()); NodePath masterNode = MetaCoreTryLoadModelRuntimeAsset(MetaCoreRuntimeModelLoadRequest{ gameObject.GetComponent().SourceModelAssetGuid, projectRoot, path, false }); if (!masterNode.is_empty()) { { std::lock_guard lock(Impl_->CacheMutex); Impl_->ModelCache[modelCacheKey] = masterNode; } if (gameObject.GetComponent().ModelNodeIndex < 0) { objectState.MeshNode = masterNode.copy_to(meshParent); } else { NodePath subNode = masterNode.find("**/=MetaCoreNodeIndex=" + std::to_string(gameObject.GetComponent().ModelNodeIndex)); if (!subNode.is_empty()) { objectState.MeshNode = subNode.copy_to(meshParent); objectState.MeshNode.set_mat(LMatrix4::ident_mat()); NodePathCollection children = objectState.MeshNode.get_children(); for (int i = 0; i < children.get_num_paths(); ++i) { NodePath child = children.get_path(i); if (child.has_tag("MetaCoreNodeIndex")) { child.remove_node(); } } } else { objectState.MeshNode = masterNode.copy_to(meshParent); } } objectState.IsUsingPlaceholder = false; } else { objectState.MeshNode = MetaCoreCreateUnitCubeNode(windowFrameworkHandle, meshParent); objectState.MeshNode.set_scale(0.2F); objectState.IsUsingPlaceholder = true; } } } objectState.MeshSource = gameObject.GetComponent().MeshSource; objectState.BuiltinMesh = gameObject.GetComponent().BuiltinMesh; objectState.MeshAssetGuid = gameObject.GetComponent().MeshAssetGuid; objectState.SourceModelAssetGuid = gameObject.GetComponent().SourceModelAssetGuid; objectState.SourceModelPath = gameObject.GetComponent().SourceModelPath; objectState.ModelNodeIndex = gameObject.GetComponent().ModelNodeIndex; } if (!objectState.MeshNode.is_empty()) { if (compatibilityMeshOnly) { syncStage = "apply_mesh_transform"; const MetaCoreTransformComponent worldTransform = MetaCoreBuildWorldTransformComponent(scene, gameObject); MetaCoreApplyTransformToPandaNode(worldTransform, objectState.MeshNode); } if (materialSlotsChanged) { syncStage = "refresh_material_slots"; } const bool useAssetPreviewPath = gameObject.GetComponent().MeshSource == MetaCoreMeshSourceKind::Asset && !objectState.IsUsingPlaceholder; objectState.MeshNode.set_material_off(1); objectState.MeshNode.set_light_off(1); objectState.MeshNode.set_two_sided(gameObject.GetComponent().DoubleSided); objectState.MeshNode.set_color( gameObject.GetComponent().BaseColor.r, gameObject.GetComponent().BaseColor.g, gameObject.GetComponent().BaseColor.b, 1.0F ); if (useAssetPreviewPath) { MetaCoreApplyBaseColorTexture( objectState.MeshNode, Impl_->ProjectRootPath, gameObject.GetComponent().BaseColorTextureGuid, gameObject.GetComponent().BaseColorTexturePath ); MetaCoreApplyMetallicRoughnessTexture( objectState.MeshNode, Impl_->ProjectRootPath, gameObject.GetComponent().MetallicRoughnessTextureGuid, gameObject.GetComponent().MetallicRoughnessTexturePath ); MetaCoreApplyNormalTexture( objectState.MeshNode, Impl_->ProjectRootPath, gameObject.GetComponent().NormalTextureGuid, gameObject.GetComponent().NormalTexturePath ); MetaCoreApplyEmissionTexture( objectState.MeshNode, Impl_->ProjectRootPath, gameObject.GetComponent().EmissiveTextureGuid, gameObject.GetComponent().EmissiveTexturePath ); MetaCoreApplyAoTexture( objectState.MeshNode, Impl_->ProjectRootPath, gameObject.GetComponent().AoTextureGuid, gameObject.GetComponent().AoTexturePath ); MetaCoreApplySimplePbrPreview(objectState.MeshNode); MetaCoreApplySimplePbrMaterialInputs(objectState.MeshNode, gameObject.GetComponent()); auto* editorCameraHandle = static_cast(Impl_->RenderDevice->GetNativeEditorCameraHandle()); if (editorCameraHandle != nullptr) { MetaCoreApplySimplePbrViewInputs(objectState.MeshNode, *sceneRootHandle, *editorCameraHandle); } const std::filesystem::path& projectRoot = !Impl_->ProjectRootPath.empty() ? Impl_->ProjectRootPath : (Impl_->ProjectRootPath = MetaCoreResolveProjectRootFromEnvironment()); const auto modelDoc = MetaCoreReadRuntimeModelDocument(MetaCoreRuntimeModelLoadRequest{ gameObject.GetComponent().SourceModelAssetGuid, projectRoot, gameObject.GetComponent().SourceModelPath, false }); if (modelDoc.has_value()) { MetaCoreApplyMaterialSlotsToSubMeshes( objectState.MeshNode, projectRoot, gameObject.GetComponent(), *modelDoc, *sceneRootHandle, editorCameraHandle ); } objectState.MaterialAssetGuids = gameObject.GetComponent().MaterialAssetGuids; } else { objectState.MeshNode.set_texture_off(1); objectState.MeshNode.set_shader_off(1); objectState.MaterialAssetGuids.clear(); } if (gameObject.GetComponent().AlphaMode == MetaCoreMeshAlphaMode::Blend) { objectState.MeshNode.set_transparency(TransparencyAttrib::M_alpha); } else if (gameObject.GetComponent().AlphaMode == MetaCoreMeshAlphaMode::Mask) { objectState.MeshNode.set_transparency(TransparencyAttrib::M_binary); } else { objectState.MeshNode.clear_transparency(); } if (gameObject.GetComponent().Visible) { objectState.MeshNode.show(); } else { objectState.MeshNode.hide(); } if (gameObject.GetId() == Impl_->SelectedObjectId) { objectState.MeshNode.set_color_scale(1.18F, 1.02F, 0.72F, 1.0F); } else { objectState.MeshNode.clear_color_scale(); } } } else if (!objectState.MeshNode.is_empty()) { objectState.MeshNode.remove_node(); objectState.MeshNode = NodePath(); objectState.MaterialAssetGuids.clear(); } if (gameObject.HasComponent()) { syncStage = "light_component"; if (objectState.LightNode.is_empty()) { PT(DirectionalLight) newLight = new DirectionalLight("MetaCoreDirectionalLight"); objectState.LightNode = objectState.RootNode.attach_new_node(newLight); sceneRootHandle->set_light(objectState.LightNode); } auto* directionalLight = DCAST(DirectionalLight, objectState.LightNode.node()); if (directionalLight != nullptr) { directionalLight->set_color(LColor( gameObject.GetComponent().Color.x * gameObject.GetComponent().Intensity, gameObject.GetComponent().Color.y * gameObject.GetComponent().Intensity, gameObject.GetComponent().Color.z * gameObject.GetComponent().Intensity, 1.0F )); } } else if (!objectState.LightNode.is_empty()) { sceneRootHandle->clear_light(objectState.LightNode); objectState.LightNode.remove_node(); objectState.LightNode = NodePath(); } } catch (const std::exception& exceptionObject) { Impl_->RuntimeSyncFailure = true; Impl_->LastRuntimeSyncFailure = "metacore.render object " + std::to_string(gameObject.GetId()) + " (" + gameObject.GetName() + ") stage=" + syncStage + ": " + exceptionObject.what(); MetaCoreTrace(Impl_->LastRuntimeSyncFailure.c_str()); return; } catch (...) { Impl_->RuntimeSyncFailure = true; Impl_->LastRuntimeSyncFailure = "metacore.render object " + std::to_string(gameObject.GetId()) + " (" + gameObject.GetName() + ") stage=" + syncStage + ": non-std exception"; MetaCoreTrace(Impl_->LastRuntimeSyncFailure.c_str()); return; } } for (auto iterator = Impl_->ObjectStates.begin(); iterator != Impl_->ObjectStates.end();) { if (!aliveObjectIds.contains(iterator->first)) { if (!iterator->second.MeshNode.is_empty()) { iterator->second.MeshNode.remove_node(); } if (!iterator->second.LightNode.is_empty()) { iterator->second.LightNode.remove_node(); } if (!iterator->second.RootNode.is_empty()) { iterator->second.RootNode.remove_node(); } iterator = Impl_->ObjectStates.erase(iterator); } else { ++iterator; } } } void MetaCorePandaSceneBridge::ApplySceneView(const MetaCoreSceneView& sceneView) { if (Impl_->RenderDevice == nullptr) { return; } auto* editorCameraHandle = static_cast(Impl_->RenderDevice->GetNativeEditorCameraHandle()); if (editorCameraHandle == nullptr || editorCameraHandle->is_empty()) { return; } Impl_->SelectedObjectId = sceneView.SelectedObjectId; editorCameraHandle->set_pos(MetaCoreToPandaPoint(sceneView.CameraPosition)); editorCameraHandle->look_at(MetaCoreToPandaPoint(sceneView.CameraTarget), MetaCoreToPandaVector(sceneView.CameraUp)); auto* cameraNode = DCAST(Camera, editorCameraHandle->node()); if (cameraNode != nullptr) { auto* perspectiveLens = DCAST(PerspectiveLens, cameraNode->get_lens()); if (perspectiveLens != nullptr) { const float aspect = std::max(0.001F, perspectiveLens->get_aspect_ratio()); const float vFovRad = glm::radians(sceneView.VerticalFieldOfViewDegrees); const float hFovDeg = glm::degrees(2.0F * std::atan(aspect * std::tan(vFovRad * 0.5F))); perspectiveLens->set_fov(LVecBase2f(std::max(0.001F, hFovDeg), std::max(0.001F, sceneView.VerticalFieldOfViewDegrees))); } } } bool MetaCorePandaSceneBridge::TryGetObjectWorldMatrix(MetaCoreId objectId, glm::mat4& worldMatrix) const { if (Impl_ == nullptr || Impl_->RenderDevice == nullptr) { return false; } auto* sceneRootHandle = static_cast(Impl_->RenderDevice->GetNativeSceneRootHandle()); if (sceneRootHandle == nullptr || sceneRootHandle->is_empty()) { return false; } const auto objectIterator = Impl_->ObjectStates.find(objectId); if (objectIterator == Impl_->ObjectStates.end()) { return false; } const NodePath& targetNode = !objectIterator->second.RootNode.is_empty() ? objectIterator->second.RootNode : objectIterator->second.MeshNode; if (targetNode.is_empty()) { return false; } worldMatrix = MetaCoreConvertPandaMatrixToGlm(targetNode.get_mat(*sceneRootHandle)); return true; } bool MetaCorePandaSceneBridge::HasRuntimeSyncFailure() const { return Impl_ != nullptr && Impl_->RuntimeSyncFailure; } const std::string& MetaCorePandaSceneBridge::GetLastRuntimeSyncFailure() const { static const std::string empty; if (Impl_ == nullptr) { return empty; } return Impl_->LastRuntimeSyncFailure; } } // namespace MetaCore