From d2701a3d9bf26354948a1cdb3f5e1d728e84bef8 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Thu, 8 Oct 2026 22:28:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=A8=8B=E5=BA=8F=E5=8C=96?= =?UTF-8?q?=E7=AB=8B=E6=96=B9=E4=BD=93=E4=B8=8E=E5=8F=AF=E7=A2=8E=E5=A2=99?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=EF=BC=9AModel::CreateCube=20=E4=BA=94?= =?UTF-8?q?=E5=B1=82=E9=93=BE=E8=B7=AF=20+=20WallFracture=20=E7=A2=8E?= =?UTF-8?q?=E7=89=87=E5=88=9A=E4=BD=93=E7=BE=A4=EF=BC=88=E5=91=BD=E4=B8=AD?= =?UTF-8?q?=E7=A2=8E=E8=A3=82/=E5=BE=84=E5=90=91=E9=A3=9E=E6=95=A3/?= =?UTF-8?q?=E5=AF=BF=E5=91=BD=E6=B8=85=E7=90=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/scripts/WallFracture.cpp | 276 ++++++++++++++++++ Engine/include/Engine/Assets/AssetManager.h | 3 + Engine/include/Engine/Assets/IAssetUploader.h | 3 + Engine/include/Engine/Renderer/Model.h | 16 + .../Engine/Renderer/VulkanAssetUploader.h | 1 + Engine/src/Assets/AssetManager.cpp | 10 + Engine/src/Renderer/Model.cpp | 83 ++++++ Engine/src/Renderer/VulkanAssetUploader.cpp | 6 + Engine/src/Serialization/SceneSerializer.cpp | 17 ++ 9 files changed, 415 insertions(+) create mode 100644 App/scripts/WallFracture.cpp diff --git a/App/scripts/WallFracture.cpp b/App/scripts/WallFracture.cpp new file mode 100644 index 0000000..8b41387 --- /dev/null +++ b/App/scripts/WallFracture.cpp @@ -0,0 +1,276 @@ +// +// WallFracture.cpp:侧线A 验收脚本——可碎墙 + 碎片刚体群 + 寿命清理。 +// +// WallFracture(挂墙实体:Static Box 刚体 + MeshRenderer 立方体): +// OnCollisionEnter(Dynamic 体以足够速度命中)→ 触发一次: +// 藏墙(MeshRenderer/RigidBody.active = false,PhysicsWorld 对账 +// 自然消化:不渲染 + body 销毁)→ 按网格把墙体积切成 N 个碎片 +// 实体(共享一份程序化立方体模型 + Dynamic Box 刚体),初速记入 +// 待办表 +// OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet +// OnCreate 施速的时序契约)即统一施加径向初速(命中点→碎片方向 + +// 向上偏置);寿命到期统一 Destroy;总量上限防泄漏 +// +// 碎片不挂脚本:单一 WallFracture 集中管理全部碎片的施速/寿命/ +// 清理(一帧 spawn 几十个实体,每颗一个脚本实例纯属浪费—— +// Unity 也用同一个 Explosion 脚本管一片 Fragments)。 +// +// 覆盖的引擎设施: +// - Spawn()/Destroy():帧内即时创建 / 帧尾延迟销毁 +// - 组件运行时装配:MeshRenderer(程序化 CreateCube 资产)+ +// RigidBody(下一帧对账建 body;active=false 对账销毁) +// - OnCollisionEnter:命中点/法线 + 对方实线速度查询 +// - SetLinearVelocity:body 建好后的初速注入 +// +// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube + +// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里 +// 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击: +// 墙消失,碎片群从命中点径向飞散、翻滚、落地堆积,若干秒后清空。 +// + +#include +#include +#include +#include // CreateCube(门面只前向声明) +#include + +#include +#include + +#include + +using namespace Engine; // u32/f32/Entity 等基础类型 + 门面(同 TestShoot) + +namespace { + + // bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验) + ECS::Entity EntityFromBits(u32 bits) { + return static_cast(bits); + } + + // 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记 + // 需要任意实体的同款换算) + u32 BitsOf(const ECS::EntityHandle& e) { + return static_cast( + entt::to_integral(static_cast(e))); + } + + // ---- 可碎墙(挂墙实体;触发后自管一整批碎片)---- + class WallFracture final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "WallFracture"; } + + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + // 碎片网格:每轴切几刀(总碎片 = x*y*z;占位档位 = 每碎片 + // 一实体,网格 6*4*1=24 起步,中等规模才考虑 instanced draw) + pb.Field("GridX", &m_GridX, 6); + pb.Field("GridY", &m_GridY, 4); + pb.Field("GridZ", &m_GridZ, 1); + pb.Field("BurstSpeed", &m_BurstSpeed, 4.5f); // 径向初速 m/s + pb.Field("UpBias", &m_UpBias, 2.0f); // 向上偏置 m/s + pb.Field("Lifetime", &m_Lifetime, 6.0f); // 碎片寿命(秒) + pb.Field("MaxFragments", &m_MaxFragments, 96); // 活碎片上限 + pb.Field("MinImpactSpeed", &m_MinImpactSpeed, 2.0f); // 触发阈值 + } + + void OnCollisionEnter(u32 other, const glm::vec3& point, + const glm::vec3& normal) override { + (void)normal; // 法线留给将来定向喷射 + + if (m_Fired) return; + + // 触发条件:对方是活的 Dynamic 刚体且速度够快(过滤 + // 静止贴靠/角色轻碰——子弹/抛体必中) + auto& scene = App().GetScene(); + const ECS::Entity otherE = EntityFromBits(other); + if (!scene.GetRegistry().valid(otherE)) return; + const auto* otherRb = + scene.GetRegistry().try_get( + otherE); + if (!otherRb || + otherRb->type != + Engine::ECS::RigidBodyComponent::BodyType::Dynamic) { + return; + } + const f32 speed = glm::length( + App().GetPhysics().GetLinearVelocity(other)); + if (speed < m_MinImpactSpeed) return; + + Fire(point); + } + + void OnUpdate(f32 dt) override { + ApplyPendingVelocities(); + AgeAndCleanup(dt); + } + + private: + // ---- 触发碎裂:藏墙 + 切网格 + spawn 碎片群 ---- + void Fire(const glm::vec3& hitPoint) { + m_Fired = true; + + // 藏墙:渲染/拾取跳过 + body 对账销毁(组件级 active, + // 与 Unity Renderer.enabled + Rigidbody 摘除同语义) + if (auto* mr = TryGetComponent< + Engine::ECS::MeshRendererComponent>()) { + mr->active = false; + } + if (auto* rb = TryGetComponent< + Engine::ECS::RigidBodyComponent>()) { + rb->active = false; + } + + // 墙几何:立方体模型半边长 0.5(单位体),墙尺寸 = + // Transform 世界缩放;碎片尺寸 = 墙尺寸 / 网格 + const auto& wall = Transform(); + const glm::vec3 wallPos = wall.GetWorldPosition(); + const glm::quat wallRot = wall.GetWorldRotationQuat(); + const glm::vec3 wallSize = wall.GetWorldScale(); + const glm::ivec3 grid{m_GridX, m_GridY, m_GridZ}; + const glm::vec3 fragSize = wallSize / + glm::vec3{std::max(1, m_GridX), std::max(1, m_GridY), + std::max(1, m_GridZ)}; + + // 共享一份程序化立方体(缓存去重:同参数全场景一把 mesh) + const Engine::ModelRef brick = + App().GetAssets().CreateCube(0.5f, 140, 92, 72); + + for (int i = 0; i < grid.x; ++i) { + for (int j = 0; j < grid.y; ++j) { + for (int k = 0; k < grid.z; ++k) { + // 单元中心(墙局部)→ 世界(墙旋转/平移搬运; + // 加单元内抖动避免完美网格的假感) + const glm::vec3 cell{ + (i + 0.5f) * fragSize.x - wallSize.x * 0.5f, + (j + 0.5f) * fragSize.y - wallSize.y * 0.5f, + (k + 0.5f) * fragSize.z - wallSize.z * 0.5f}; + const glm::vec3 jitter{ + Rand(-0.2f, 0.2f) * fragSize.x, + Rand(-0.2f, 0.2f) * fragSize.y, + Rand(-0.2f, 0.2f) * fragSize.z}; + const glm::vec3 pos = + wallPos + wallRot * (cell + jitter); + + auto frag = Spawn("fragment"); + auto& tr = frag.GetComponent< + Engine::ECS::TransformComponent>(); + tr.SetPosition(pos); + // 随机小倾角:落地堆积不齐整,视觉更像碎块 + tr.SetRotationQuat(glm::quat(glm::vec3{ + glm::radians(Rand(-30.0f, 30.0f)), + glm::radians(Rand(-180.0f, 180.0f)), + glm::radians(Rand(-30.0f, 30.0f))})); + tr.SetScale(fragSize); + + auto& fmr = + frag.AddComponent< + Engine::ECS::MeshRendererComponent>(); + fmr.model = brick; + + // Dynamic Box:boxHalfExtents 默认 0.5(单位体) + // × Transform 缩放 = 恰好碎片尺寸,无需改字段 + auto& frb = + frag.AddComponent< + Engine::ECS::RigidBodyComponent>(); + frb.type = + Engine::ECS::RigidBodyComponent::BodyType::Dynamic; + frb.collider = + Engine::ECS::RigidBodyComponent::ColliderType::Box; + frb.mass = 0.4f; + frb.friction = 0.7f; + frb.restitution = 0.05f; + + // 初速待办:body 下一帧建好,OnUpdate 统一施加 + //(同 Bullet OnCreate 施速的时序契约) + glm::vec3 dir = pos - hitPoint; + const f32 len = glm::length(dir); + if (len < 1e-3f) { + dir = glm::vec3{Rand(-1.0f, 1.0f), 0.6f, + Rand(-1.0f, 1.0f)}; + } else { + dir /= len; + } + const glm::vec3 vel = + dir * m_BurstSpeed * Rand(0.6f, 1.3f) + + glm::vec3{0.0f, m_UpBias * Rand(0.5f, 1.0f), + 0.0f}; + + // 总量上限:超限立即回收最老碎片(防泄漏的 + // 兜底闸;寿命清理才是常态回收) + const u32 bits = BitsOf(frag); + if (m_Fragments.size() >= + static_cast(m_MaxFragments)) { + Destroy(EntityFromBits(m_Fragments.front().bits)); + m_Fragments.erase(m_Fragments.begin()); + } + m_Fragments.push_back({bits, vel, 0.0f}); + } + } + } + } + + // ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ---- + void ApplyPendingVelocities() { + auto& registry = App().GetScene().GetRegistry(); + for (auto it = m_Fragments.begin(); + it != m_Fragments.end();) { + if (it->applied) { ++it; continue; } + const ECS::Entity e = EntityFromBits(it->bits); + if (!registry.valid(e)) { // 实体没了(外部销毁) + it = m_Fragments.erase(it); + continue; + } + const auto* rb = registry.try_get< + Engine::ECS::RigidBodyComponent>(e); + if (rb && rb->bodyValid) { + App().GetPhysics().SetLinearVelocity(it->bits, + it->velocity); + it->applied = true; + } + ++it; // body 未建:留待下一帧(对账晚一帧必到) + } + } + + // ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)---- + void AgeAndCleanup(f32 dt) { + auto& registry = App().GetScene().GetRegistry(); + for (auto it = m_Fragments.begin(); + it != m_Fragments.end();) { + it->age += dt; + if (it->age < m_Lifetime) { ++it; continue; } + const ECS::Entity e = EntityFromBits(it->bits); + if (registry.valid(e)) { + Destroy(e); + } + it = m_Fragments.erase(it); + } + } + + static f32 Rand(f32 lo, f32 hi) { + thread_local std::mt19937 rng{std::random_device{}()}; + return std::uniform_real_distribution(lo, hi)(rng); + } + + struct Fragment { + u32 bits = 0; // PhysicsWorld 键(实体位模式) + glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入) + f32 age = 0.0f; // 存活秒数(寿命清理用) + bool applied = false; // 初速已注入 + }; + + int m_GridX = 6; + int m_GridY = 4; + int m_GridZ = 1; + f32 m_BurstSpeed = 4.5f; + f32 m_UpBias = 2.0f; + f32 m_Lifetime = 6.0f; + int m_MaxFragments = 96; + f32 m_MinImpactSpeed = 2.0f; + + bool m_Fired = false; // 一次性触发保护 + std::vector m_Fragments; // 活碎片簿记 + }; + +} // namespace + +ENGINE_REGISTER_SCRIPT(WallFracture) diff --git a/Engine/include/Engine/Assets/AssetManager.h b/Engine/include/Engine/Assets/AssetManager.h index 61d5685..7a90d27 100644 --- a/Engine/include/Engine/Assets/AssetManager.h +++ b/Engine/include/Engine/Assets/AssetManager.h @@ -213,6 +213,9 @@ namespace Engine // 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重 ModelRef CreateTransparentPlane(f32 halfSize, u8 r, u8 g, u8 b, u8 a); + // 程序化立方体(halfSize 为半边长):同参数缓存去重。 + // 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存 + ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b); // 环境资产(HDR → IBL 预积分四件套)。路径缓存去重, // 重复 Load 同一路径零成本(动态切换场景直接反复调用) diff --git a/Engine/include/Engine/Assets/IAssetUploader.h b/Engine/include/Engine/Assets/IAssetUploader.h index 4150337..f26b898 100644 --- a/Engine/include/Engine/Assets/IAssetUploader.h +++ b/Engine/include/Engine/Assets/IAssetUploader.h @@ -42,6 +42,9 @@ namespace Engine::Assets { virtual Scope CreateTransparentPlane(f32 halfSize, u8 r, u8 g, u8 b, u8 a) = 0; + // 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟) + virtual Scope CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0; + // 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr) virtual Scope LoadEnvironment(const std::string& path) = 0; diff --git a/Engine/include/Engine/Renderer/Model.h b/Engine/include/Engine/Renderer/Model.h index 4e8a383..696e100 100644 --- a/Engine/include/Engine/Renderer/Model.h +++ b/Engine/include/Engine/Renderer/Model.h @@ -158,6 +158,15 @@ namespace Engine vk::DescriptorSetLayout textureLayout, f32 halfSize, u8 r, u8 g, u8 b, u8 a); + // 程序化立方体(6 面 24 顶点纯色贴图,halfSize = 半边长): + // 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform + // 缩放——同 halfSize 同色共享一份缓存模型 + struct CubeTag {}; + static Scope CreateCube(Device& device, MemoryAllocator& allocator, + DescriptorManager& descriptorManager, + vk::DescriptorSetLayout textureLayout, + f32 halfSize, u8 r, u8 g, u8 b); + ~Model(); Model(const Model&) = delete; @@ -209,6 +218,13 @@ namespace Engine ProceduralTag, f32 halfSize, const std::array& rgba, bool transparent); + // 程序化立方体骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 由 + // 立方体路径填(6 面,顶点法线朝外) + Model(Device& device, MemoryAllocator& allocator, + DescriptorManager& descriptorManager, + vk::DescriptorSetLayout textureLayout, + CubeTag, f32 halfSize, const std::array& rgba); + // 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填 Model(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, diff --git a/Engine/include/Engine/Renderer/VulkanAssetUploader.h b/Engine/include/Engine/Renderer/VulkanAssetUploader.h index 3172b3b..34ac4af 100644 --- a/Engine/include/Engine/Renderer/VulkanAssetUploader.h +++ b/Engine/include/Engine/Renderer/VulkanAssetUploader.h @@ -37,6 +37,7 @@ namespace Engine { Scope CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override; Scope CreateTransparentPlane(f32 halfSize, u8 r, u8 g, u8 b, u8 a) override; + Scope CreateCube(f32 halfSize, u8 r, u8 g, u8 b) override; Scope LoadEnvironment(const std::string& path) override; Scope LoadEnvironment(const std::string& path, DecodedEquirect&& decoded) override; diff --git a/Engine/src/Assets/AssetManager.cpp b/Engine/src/Assets/AssetManager.cpp index fe37371..cd8c75c 100644 --- a/Engine/src/Assets/AssetManager.cpp +++ b/Engine/src/Assets/AssetManager.cpp @@ -202,6 +202,16 @@ namespace Engine::Assets { }); } + ModelRef AssetManager::CreateCube(f32 halfSize, u8 r, u8 g, u8 b) + { + // 程序化资产复用同一缓存:键 = 伪路径 + 参数(与 plane 同约定) + std::string key = "procedural://cube/" + std::to_string(halfSize) + + "/" + std::to_string(r) + "," + std::to_string(g) + + "," + std::to_string(b); + return m_Models.Load(key, + [&](const std::string&) { return m_Uploader.CreateCube(halfSize, r, g, b); }); + } + EnvRef AssetManager::LoadEnvironment(const std::string& path) { // GPU 生成(compute 预积分)经接口发生在渲染层实现里 diff --git a/Engine/src/Renderer/Model.cpp b/Engine/src/Renderer/Model.cpp index 03c0de4..f77ec9d 100644 --- a/Engine/src/Renderer/Model.cpp +++ b/Engine/src/Renderer/Model.cpp @@ -161,6 +161,89 @@ Scope Model::CreateTransparentPlane( true)); } +Scope Model::CreateCube(Device& device, MemoryAllocator& allocator, + DescriptorManager& descriptorManager, + vk::DescriptorSetLayout textureLayout, + f32 halfSize, u8 r, u8 g, u8 b) +{ + return Scope(new Model(device, allocator, descriptorManager, + textureLayout, CubeTag{}, + halfSize, std::array{r, g, b, 255})); +} + +// 程序化立方体:6 面 24 顶点(面法线朝外,CCW 从外看),36 索引。 +// 结构镜像平面 ctor:默认纹理 → 纯色 albedo 覆盖 → 单 mesh +Model::Model(Device& device, MemoryAllocator& allocator, + DescriptorManager& descriptorManager, + vk::DescriptorSetLayout textureLayout, + CubeTag, f32 halfSize, const std::array& rgba) + : m_Device(device) + , m_Allocator(allocator) + , m_DescriptorManager(descriptorManager) + , m_TextureLayout(textureLayout) +{ + // 纯色贴图直供 albedo 槽;生命周期挂进纹理缓存(键含 rgba 值, + // 同色共享、异色互不冲突)——与平面 ctor 同一套 + InitDefaultTextures(); + Scope tex = Texture::CreateSolidColor(device, allocator, + rgba[0], rgba[1], rgba[2]); + Texture* albedoOverride = tex.get(); + const std::string texKey = "proc-cube:" + std::to_string(rgba[0]) + ',' + + std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' + + std::to_string(rgba[3]); + m_TextureCache.emplace(texKey, std::move(tex)); + + const f32 h = halfSize; + // 逐面 4 顶点(底左/底右/顶右/顶左,从面外看)+ 单位 UV + const glm::vec3 P[24] = { + // +Z(前) + {-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h}, + // -Z(后) + { h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h}, + // +Y(上) + {-h, h, h}, { h, h, h}, { h, h,-h}, {-h, h,-h}, + // -Y(下) + {-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h}, + // +X(右) + { h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h}, + // -X(左) + {-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h}, + }; + const glm::vec3 N[6] = { + {0, 0, 1}, {0, 0, -1}, {0, 1, 0}, {0, -1, 0}, {1, 0, 0}, {-1, 0, 0}, + }; + const glm::vec2 UV[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; + + std::vector vertices; + vertices.reserve(24); + for (u32 f = 0; f < 6; ++f) { + for (u32 i = 0; i < 4; ++i) { + vertices.push_back({P[f * 4 + i], N[f], UV[i]}); + } + } + std::vector indices; + indices.reserve(36); + for (u32 f = 0; f < 6; ++f) { + const u32 b = f * 4; + const std::vector quad = {b, b + 1, b + 2, b, b + 2, b + 3}; + indices.insert(indices.end(), quad.begin(), quad.end()); + } + const std::vector> lodIndices = {indices}; + + std::string paths[Material::kSlotCount] = {"", "", "", ""}; + std::array overrides{}; + overrides[Material::Albedo] = albedoOverride; + Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, false, + overrides); + + m_Meshes.push_back(CreateScope( + m_Device, m_Allocator, vertices, lodIndices, mat)); + // 内建材质快照(与 m_Meshes 下标对齐,编辑器材质槽 UI 不越界) + m_MeshMaterialInfos.push_back( + {"Cube", {}, MaterialParams{}, false}); + RebuildUnionBounds(); +} + ParsedModel Model::ParseFromDisk(const std::string& path) { ParsedModel out; diff --git a/Engine/src/Renderer/VulkanAssetUploader.cpp b/Engine/src/Renderer/VulkanAssetUploader.cpp index 7986b4e..f9bc707 100644 --- a/Engine/src/Renderer/VulkanAssetUploader.cpp +++ b/Engine/src/Renderer/VulkanAssetUploader.cpp @@ -95,6 +95,12 @@ namespace Engine { halfSize, r, g, b, a); } + Scope VulkanAssetUploader::CreateCube(f32 halfSize, u8 r, u8 g, u8 b) + { + return Model::CreateCube(m_Device, m_Allocator, m_Descriptors, + m_TextureLayout, halfSize, r, g, b); + } + Scope VulkanAssetUploader::LoadEnvironment(const std::string& path) { // compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index 9aa45a9..d8c050c 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -185,6 +185,23 @@ namespace Engine::Serialization { constexpr std::string_view kPlane = "procedural://plane/"; constexpr std::string_view kTPlane = "procedural://transparent-plane/"; + constexpr std::string_view kCube = "procedural://cube/"; + + if (path.rfind(kCube, 0) == 0) { + // "{halfSize}/{r},{g},{b}" + const std::string rest = path.substr(kCube.size()); + const auto slash = rest.find('/'); + if (slash == std::string::npos) return {}; + int c[3]; + if (!ParseCsvInts(rest.substr(slash + 1), c)) return {}; + try { + return assets.CreateCube( + std::stof(rest.substr(0, slash)), + static_cast(c[0]), static_cast(c[1]), + static_cast(c[2])); + } + catch (const std::exception&) { return {}; } + } if (path.rfind(kTPlane, 0) == 0) { // "{halfSize}/{r},{g},{b},{a}"