feat: add selection to meshrendercomponent
This commit is contained in:
@@ -40,6 +40,7 @@ set(SRC_FILES
|
||||
"src/Components/CameraComponent.cpp"
|
||||
"src/Components/LightComponent.cpp"
|
||||
"src/Components/MeshRendererComponent.cpp"
|
||||
"src/Components/MeshRendererInspector.cpp"
|
||||
"src/Components/Rotator.cpp"
|
||||
"src/Components/Spinner.cpp"
|
||||
"src/Components/OrbitAndSpin.cpp"
|
||||
@@ -128,6 +129,7 @@ set(SRC_FILES
|
||||
"src/Util/TransformGizmo.cpp"
|
||||
"src/Util/RectGizmo.cpp"
|
||||
"src/Util/SceneInspector.cpp"
|
||||
"src/Util/AssetBrowser.cpp"
|
||||
|
||||
"src/Physics/PhysicsWorld.cpp"
|
||||
"src/Physics/PhysicsSceneBridge.cpp"
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
#include <destrum/Graphics/Pipelines/ImguiPass.h>
|
||||
#include <destrum/Graphics/ResourcesInspector.h>
|
||||
#include <destrum/Util/AssetBrowser.h>
|
||||
|
||||
#include <destrum/Input/InputManager.h>
|
||||
|
||||
@@ -53,6 +54,7 @@ public:
|
||||
virtual void onWindowResize(int, int) {};
|
||||
virtual void onViewportResize(int, int) {}
|
||||
virtual void drawViewportOverlay() {}
|
||||
virtual void onViewportAssetDropped(std::string_view) {}
|
||||
|
||||
virtual void onPlayModeChanged(bool) {}
|
||||
|
||||
@@ -107,10 +109,12 @@ protected:
|
||||
bool physics{true};
|
||||
bool inspector{true};
|
||||
bool imageCache{false};
|
||||
bool assets{true};
|
||||
};
|
||||
|
||||
DebugWindows m_DebugWindows{};
|
||||
ResourcesInspector m_ResourcesInspector;
|
||||
AssetBrowser m_AssetBrowser;
|
||||
|
||||
void drawWindowsMenu();
|
||||
void drawPhysicsPanel(float dt, float fixedDt, float accumulator);
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
|
||||
#include <destrum/Graphics/ids.h>
|
||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
class GameRenderer;
|
||||
class RenderResources;
|
||||
|
||||
class MeshRendererComponent final: public Component {
|
||||
public:
|
||||
@@ -30,6 +37,18 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void DrawMeshField(RenderResources& resources);
|
||||
void DrawPrimitiveSettings(RenderResources& resources);
|
||||
void DrawMaterialField(RenderResources& resources, GameRenderer& renderer);
|
||||
void DrawMaterialEditor(RenderResources& resources, GameRenderer& renderer);
|
||||
void AssignPrimitive(RenderResources& resources, const PrimitiveMeshes::Desc& desc);
|
||||
void LoadDroppedModel(RenderResources& resources, const std::string& path);
|
||||
|
||||
std::string m_MeshSearch;
|
||||
std::string m_MaterialSearch;
|
||||
std::optional<PrimitiveMeshes::Desc> m_PrimitiveDraft;
|
||||
std::string m_PrimitiveDraftKey;
|
||||
|
||||
MeshID meshID{NULL_MESH_ID};
|
||||
MaterialID materialID{NULL_MATERIAL_ID};
|
||||
std::string meshKey;
|
||||
|
||||
@@ -34,6 +34,8 @@ public:
|
||||
|
||||
[[nodiscard]] std::optional<std::string> ToVirtualPath(const std::filesystem::path& fullPath) const;
|
||||
|
||||
[[nodiscard]] const std::vector<FSMount>& GetMounts() const { return mounts; }
|
||||
|
||||
private:
|
||||
static std::vector<uint8_t> ReadFile(const std::filesystem::path& fullPath);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
MaterialID addSimpleTextureMaterial(ImageID textureID);
|
||||
[[nodiscard]] const Material& getMaterial(MaterialID id) const;
|
||||
[[nodiscard]] const std::string& getMaterialKey(MaterialID id) const;
|
||||
[[nodiscard]] std::size_t getMaterialCount() const { return materials.size(); }
|
||||
[[nodiscard]] std::optional<MaterialID> findMaterialByKey(std::string_view key) const;
|
||||
[[nodiscard]] std::optional<MaterialID> findMaterialByName(
|
||||
std::string_view name,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef PRIMITIVEMESHES_H
|
||||
#define PRIMITIVEMESHES_H
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -10,9 +11,36 @@
|
||||
#include <destrum/Graphics/Resources/CPUMesh.h>
|
||||
|
||||
namespace PrimitiveMeshes {
|
||||
enum class Shape {
|
||||
Box,
|
||||
Plane,
|
||||
Sphere,
|
||||
Cylinder,
|
||||
Capsule,
|
||||
};
|
||||
|
||||
inline constexpr std::array<Shape, 5> AllShapes{
|
||||
Shape::Box, Shape::Plane, Shape::Sphere, Shape::Cylinder, Shape::Capsule,
|
||||
};
|
||||
|
||||
struct Desc {
|
||||
Shape shape{Shape::Box};
|
||||
glm::vec3 size{1.0f};
|
||||
float radius{0.5f};
|
||||
float height{2.0f};
|
||||
|
||||
bool operator==(const Desc&) const = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] const char* ShapeName(Shape shape);
|
||||
[[nodiscard]] Desc DefaultDesc(Shape shape);
|
||||
|
||||
[[nodiscard]] CPUMesh Make(const Desc& desc);
|
||||
[[nodiscard]] CPUMesh MakeBox(const glm::vec3& size);
|
||||
|
||||
[[nodiscard]] std::string MakeKey(const Desc& desc);
|
||||
[[nodiscard]] std::string BoxKey(const glm::vec3& size);
|
||||
[[nodiscard]] std::optional<Desc> ParseKey(std::string_view key);
|
||||
[[nodiscard]] bool IsPrimitiveKey(std::string_view key);
|
||||
[[nodiscard]] std::string_view StripDuplicateSuffix(std::string_view key);
|
||||
[[nodiscard]] std::optional<CPUMesh> MakeFromKey(std::string_view key);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef DESTRUM_ASSETBROWSER_H
|
||||
#define DESTRUM_ASSETBROWSER_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class AssetBrowser final {
|
||||
public:
|
||||
void Draw(bool* open = nullptr);
|
||||
|
||||
[[nodiscard]] static bool IsModel(std::string_view path);
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
std::string name;
|
||||
bool directory{false};
|
||||
};
|
||||
|
||||
void Refresh();
|
||||
void Navigate(const std::string& scheme, const std::filesystem::path& folder);
|
||||
bool DrawEntry(const Entry& entry);
|
||||
|
||||
std::string m_Scheme;
|
||||
std::filesystem::path m_Folder;
|
||||
std::vector<Entry> m_Entries;
|
||||
std::string m_Filter;
|
||||
bool m_Dirty{true};
|
||||
};
|
||||
|
||||
#endif // DESTRUM_ASSETBROWSER_H
|
||||
@@ -24,6 +24,7 @@ namespace ImGuiUtils {
|
||||
float speed = 0.1f);
|
||||
|
||||
inline constexpr const char* GameObjectPayload = "DESTRUM_GAMEOBJECT";
|
||||
inline constexpr const char* AssetPayload = "DESTRUM_ASSET";
|
||||
|
||||
bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags = 0, const char* hint = nullptr);
|
||||
bool InputStringMultiline(const char* label, std::string& value);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
#include <destrum/Util/DeltaTime.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
@@ -261,6 +262,11 @@ if (!isRunning) break;
|
||||
drawPhysicsPanel(dt, fixedDt, accumulator);
|
||||
}
|
||||
|
||||
if (m_DebugWindows.assets)
|
||||
{
|
||||
m_AssetBrowser.Draw(&m_DebugWindows.assets);
|
||||
}
|
||||
|
||||
if (m_DebugWindows.imageCache)
|
||||
{
|
||||
m_ResourcesInspector.drawImageCacheWindow(&m_DebugWindows.imageCache, resources, imguiPass);
|
||||
@@ -450,6 +456,7 @@ void App::drawWindowsMenu()
|
||||
ImGui::MenuItem("Physics", nullptr, &m_DebugWindows.physics);
|
||||
ImGui::MenuItem("Inspector", nullptr, &m_DebugWindows.inspector);
|
||||
ImGui::MenuItem("Image Cache", nullptr, &m_DebugWindows.imageCache);
|
||||
ImGui::MenuItem("Assets", nullptr, &m_DebugWindows.assets);
|
||||
|
||||
customWindowsMenu();
|
||||
|
||||
@@ -480,11 +487,13 @@ void App::drawDockSpace()
|
||||
ImGuiID center = dockspaceId;
|
||||
const ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.25f, nullptr, ¢er);
|
||||
const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.22f, nullptr, ¢er);
|
||||
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, ¢er);
|
||||
|
||||
ImGui::DockBuilderDockWindow("Viewport", center);
|
||||
ImGui::DockBuilderDockWindow("Inspector", right);
|
||||
ImGui::DockBuilderDockWindow("Debug", left);
|
||||
ImGui::DockBuilderDockWindow("Physics", left);
|
||||
ImGui::DockBuilderDockWindow("Assets", bottom);
|
||||
ImGui::DockBuilderFinish(dockspaceId);
|
||||
}
|
||||
|
||||
@@ -520,6 +529,14 @@ void App::drawViewportWindow()
|
||||
if (const ImTextureID texture = imguiPass.getFrameTexture(drawImage.imageView); texture != ImTextureID{})
|
||||
{
|
||||
ImGui::Image(texture, ImVec2{viewportRect.size.x, viewportRect.size.y});
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload))
|
||||
{
|
||||
onViewportAssetDropped(static_cast<const char*>(payload->Data));
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
viewportHovered = ImGui::IsWindowHovered();
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "destrum/ObjectModel/GameObject.h"
|
||||
#include "destrum/Util/GameState.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
@@ -179,30 +178,6 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::ImGuiInspector() {
|
||||
if (meshID == NULL_MESH_ID) {
|
||||
ImGui::TextDisabled("Mesh: <none>");
|
||||
} else {
|
||||
ImGui::Text("Mesh ID: %zu", meshID);
|
||||
ImGui::TextWrapped(
|
||||
"Mesh key: %s",
|
||||
meshKey.empty() ? "<not resolved>" : meshKey.c_str());
|
||||
}
|
||||
|
||||
if (materialID == NULL_MATERIAL_ID) {
|
||||
ImGui::TextDisabled("Material: <none>");
|
||||
} else {
|
||||
ImGui::Text("Material ID: %u", materialID);
|
||||
ImGui::TextWrapped(
|
||||
"Material key: %s",
|
||||
materialKey.empty() ? "<not resolved>" : materialKey.c_str());
|
||||
}
|
||||
|
||||
ImGui::Text(
|
||||
"Skinning: %s",
|
||||
m_skinnedMesh != nullptr ? "active" : "static");
|
||||
}
|
||||
|
||||
void MeshRendererComponent::SetMeshID(MeshID id) {
|
||||
meshID = id;
|
||||
meshKey.clear();
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/Components/Animator.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Util/AssetBrowser.h>
|
||||
#include <destrum/Util/GameState.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
namespace {
|
||||
struct TextureSlot {
|
||||
const char* label;
|
||||
ImageID Material::* member;
|
||||
TextureIntent intent;
|
||||
};
|
||||
|
||||
constexpr std::array<TextureSlot, 6> TextureSlots{{
|
||||
{"Diffuse", &Material::diffuseTexture, TextureIntent::ColorSrgb},
|
||||
{"Normal", &Material::normalTexture, TextureIntent::DataLinear},
|
||||
{"Metallic", &Material::metallicTexture, TextureIntent::DataLinear},
|
||||
{"Roughness", &Material::roughnessTexture, TextureIntent::DataLinear},
|
||||
{"Occlusion", &Material::occlusionTexture, TextureIntent::DataLinear},
|
||||
{"Emissive", &Material::emissiveTexture, TextureIntent::ColorSrgb},
|
||||
}};
|
||||
|
||||
[[nodiscard]] std::string Lowercase(std::string_view value)
|
||||
{
|
||||
std::string result{value};
|
||||
std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool Matches(std::string_view text, const std::string& filter)
|
||||
{
|
||||
return filter.empty() || Lowercase(text).find(Lowercase(filter)) != std::string::npos;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsImage(std::string_view path)
|
||||
{
|
||||
const std::string extension = Lowercase(std::filesystem::path{path}.extension().string());
|
||||
return extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" ||
|
||||
extension == ".bmp";
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<std::string> AcceptAssetDrop()
|
||||
{
|
||||
std::optional<std::string> dropped;
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload)) {
|
||||
dropped = static_cast<const char*>(payload->Data);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool PickerButton(const char* label, const std::string& value)
|
||||
{
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine(90.0f);
|
||||
const bool clicked = ImGui::Button((value + "##" + label).c_str(), ImVec2{-FLT_MIN, 0.0f});
|
||||
ImGui::SetItemTooltip("%s\nClick to pick, or drop a file from the Assets window", value.c_str());
|
||||
return clicked;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string TextureLabel(const RenderResources& resources, ImageID id)
|
||||
{
|
||||
if (id == NULL_IMAGE_ID) {
|
||||
return "<none>";
|
||||
}
|
||||
if (const ImageCache::LoadedImageInfo* info = resources.images().findLoadedImageInfo(id)) {
|
||||
const auto virtualPath = AssetFS::GetInstance().ToVirtualPath(info->path);
|
||||
return virtualPath ? *virtualPath : info->path.filename().string();
|
||||
}
|
||||
return "texture " + std::to_string(id);
|
||||
}
|
||||
|
||||
[[nodiscard]] int CountMaterialUsers(const GameObject* self, MaterialID id)
|
||||
{
|
||||
const Scene* scene = self != nullptr ? self->GetScene() : nullptr;
|
||||
if (scene == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
int users = 0;
|
||||
for (const auto& object : scene->GetObjects()) {
|
||||
for (const auto& component : object->GetComponents()) {
|
||||
const auto* meshRenderer = dynamic_cast<const MeshRendererComponent*>(component.get());
|
||||
if (meshRenderer != nullptr && meshRenderer->GetMaterialID() == id) {
|
||||
++users;
|
||||
}
|
||||
}
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void PickerList(const char* id, Fn&& drawItems)
|
||||
{
|
||||
const float height = std::min(ImGui::GetTextLineHeightWithSpacing() * 14.0f, 320.0f);
|
||||
if (ImGui::BeginChild(id, ImVec2{420.0f, height}, ImGuiChildFlags_None)) {
|
||||
drawItems();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::ImGuiInspector()
|
||||
{
|
||||
if (!GameState::GetInstance().HasRenderer() || !GameState::GetInstance().HasGfxDevice()) {
|
||||
ImGui::TextDisabled("Meshes and materials can only be picked with a renderer");
|
||||
return;
|
||||
}
|
||||
GameRenderer& renderer = GameState::GetInstance().Renderer();
|
||||
RenderResources* resources = renderer.getResources();
|
||||
if (resources == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
DrawMeshField(*resources);
|
||||
ImGui::Spacing();
|
||||
DrawMaterialField(*resources, renderer);
|
||||
}
|
||||
|
||||
void MeshRendererComponent::DrawMeshField(RenderResources& resources)
|
||||
{
|
||||
// The skinning buffer is sized for the mesh in Start, a different mesh would overrun it
|
||||
const bool skinned = GetGameObject()->GetComponent<Animator>() != nullptr;
|
||||
ImGui::BeginDisabled(skinned);
|
||||
if (PickerButton("Mesh", meshID == NULL_MESH_ID ? "<none>" : meshKey)) {
|
||||
m_MeshSearch.clear();
|
||||
ImGui::OpenPopup("MeshPicker");
|
||||
}
|
||||
if (const auto dropped = AcceptAssetDrop(); dropped && AssetBrowser::IsModel(*dropped)) {
|
||||
LoadDroppedModel(resources, *dropped);
|
||||
std::vector<MeshID> parts;
|
||||
for (MeshID id = 0; id < resources.meshes().getMeshes().size(); ++id) {
|
||||
if (resources.meshes().getMeshKey(id).starts_with(*dropped + "#") && !resources.meshes().getMesh(id).hasSkeleton) {
|
||||
parts.push_back(id);
|
||||
}
|
||||
}
|
||||
if (parts.size() == 1) {
|
||||
SetMeshID(parts.front());
|
||||
} else if (!parts.empty()) {
|
||||
m_MeshSearch = *dropped;
|
||||
ImGui::OpenPopup("MeshPicker");
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (skinned) {
|
||||
ImGui::TextDisabled("Animated meshes can't be swapped here");
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("MeshPicker")) {
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
ImGui::SetNextItemWidth(420.0f);
|
||||
ImGuiUtils::InputString("##Search", m_MeshSearch, 0, "Search");
|
||||
|
||||
ImGui::SeparatorText("Primitives");
|
||||
for (const PrimitiveMeshes::Shape shape : PrimitiveMeshes::AllShapes) {
|
||||
const char* name = PrimitiveMeshes::ShapeName(shape);
|
||||
if (Matches(name, m_MeshSearch) && ImGui::Selectable(name)) {
|
||||
AssignPrimitive(resources, PrimitiveMeshes::DefaultDesc(shape));
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Loaded");
|
||||
PickerList("LoadedMeshes", [&] {
|
||||
const MeshCache& meshes = resources.meshes();
|
||||
for (MeshID id = 0; id < meshes.getMeshes().size(); ++id) {
|
||||
const std::string& key = meshes.getMeshKey(id);
|
||||
if (meshes.getMesh(id).hasSkeleton || !Matches(key, m_MeshSearch)) {
|
||||
continue;
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(id));
|
||||
if (ImGui::Selectable(key.c_str(), id == meshID)) {
|
||||
SetMeshID(id);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
});
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
DrawPrimitiveSettings(resources);
|
||||
}
|
||||
|
||||
void MeshRendererComponent::DrawPrimitiveSettings(RenderResources& resources)
|
||||
{
|
||||
if (meshID == NULL_MESH_ID || !PrimitiveMeshes::IsPrimitiveKey(meshKey)) {
|
||||
m_PrimitiveDraft.reset();
|
||||
return;
|
||||
}
|
||||
// Edits go into a draft and only build a mesh once the drag ends, every step would add one to the cache
|
||||
if (!m_PrimitiveDraft || m_PrimitiveDraftKey != meshKey) {
|
||||
m_PrimitiveDraft = PrimitiveMeshes::ParseKey(meshKey);
|
||||
m_PrimitiveDraftKey = meshKey;
|
||||
}
|
||||
if (!m_PrimitiveDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
PrimitiveMeshes::Desc& draft = *m_PrimitiveDraft;
|
||||
bool apply = false;
|
||||
const auto edited = [&apply] { apply = apply || ImGui::IsItemDeactivatedAfterEdit(); };
|
||||
constexpr float Min = 0.01f;
|
||||
constexpr float Max = 1000.0f;
|
||||
|
||||
switch (draft.shape) {
|
||||
case PrimitiveMeshes::Shape::Box:
|
||||
ImGui::DragFloat3("Size", &draft.size.x, 0.05f, Min, Max, "%.2f m");
|
||||
edited();
|
||||
break;
|
||||
case PrimitiveMeshes::Shape::Plane: {
|
||||
float size[2]{draft.size.x, draft.size.z};
|
||||
if (ImGui::DragFloat2("Size", size, 0.05f, Min, Max, "%.2f m")) {
|
||||
draft.size.x = size[0];
|
||||
draft.size.z = size[1];
|
||||
}
|
||||
edited();
|
||||
break;
|
||||
}
|
||||
case PrimitiveMeshes::Shape::Sphere:
|
||||
ImGui::DragFloat("Radius", &draft.radius, 0.01f, Min, Max, "%.2f m");
|
||||
edited();
|
||||
break;
|
||||
case PrimitiveMeshes::Shape::Cylinder:
|
||||
case PrimitiveMeshes::Shape::Capsule:
|
||||
ImGui::DragFloat("Radius", &draft.radius, 0.01f, Min, Max, "%.2f m");
|
||||
edited();
|
||||
ImGui::DragFloat("Height", &draft.height, 0.05f, Min, Max, "%.2f m");
|
||||
edited();
|
||||
break;
|
||||
}
|
||||
|
||||
if (apply) {
|
||||
draft.size = glm::max(draft.size, glm::vec3{Min});
|
||||
draft.radius = std::max(draft.radius, Min);
|
||||
draft.height = std::max(draft.height, Min);
|
||||
AssignPrimitive(resources, draft);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::AssignPrimitive(RenderResources& resources, const PrimitiveMeshes::Desc& desc)
|
||||
{
|
||||
const std::string key = PrimitiveMeshes::MakeKey(desc);
|
||||
auto id = resources.meshes().findMeshByKey(key);
|
||||
if (!id) {
|
||||
id = resources.meshes().addMesh(GameState::GetInstance().Gfx(), PrimitiveMeshes::Make(desc));
|
||||
}
|
||||
SetMeshID(*id);
|
||||
}
|
||||
|
||||
void MeshRendererComponent::LoadDroppedModel(RenderResources& resources, const std::string& path)
|
||||
{
|
||||
try {
|
||||
resources.loadModelAsset(GameState::GetInstance().Gfx(), path);
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Could not load {}: {}", path, exception.what());
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::DrawMaterialField(RenderResources& resources, GameRenderer& renderer)
|
||||
{
|
||||
MaterialCache& materials = resources.materials();
|
||||
if (PickerButton("Material", materialID == NULL_MATERIAL_ID ? "<none>" : materialKey)) {
|
||||
m_MaterialSearch.clear();
|
||||
ImGui::OpenPopup("MaterialPicker");
|
||||
}
|
||||
if (const auto dropped = AcceptAssetDrop(); dropped && AssetBrowser::IsModel(*dropped)) {
|
||||
LoadDroppedModel(resources, *dropped);
|
||||
m_MaterialSearch = *dropped;
|
||||
ImGui::OpenPopup("MaterialPicker");
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("MaterialPicker")) {
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
ImGui::SetNextItemWidth(420.0f);
|
||||
ImGuiUtils::InputString("##Search", m_MaterialSearch, 0, "Search");
|
||||
|
||||
if (ImGui::Selectable("New Material")) {
|
||||
SetMaterialID(materials.addMaterial(Material::SimpleColor(glm::vec3{1.0f}, "Material")));
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Loaded");
|
||||
PickerList("LoadedMaterials", [&] {
|
||||
for (MaterialID id = 0; id < materials.getMaterialCount(); ++id) {
|
||||
const std::string& key = materials.getMaterialKey(id);
|
||||
if (id == materials.getPlaceholderMaterialId() || !Matches(key, m_MaterialSearch)) {
|
||||
continue;
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(id));
|
||||
if (ImGui::Selectable(key.c_str(), id == materialID)) {
|
||||
SetMaterialID(id);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
});
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (materialID != NULL_MATERIAL_ID) {
|
||||
DrawMaterialEditor(resources, renderer);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameRenderer& renderer)
|
||||
{
|
||||
if (!ImGui::TreeNodeEx("Material Settings", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MaterialCache& materials = resources.materials();
|
||||
const bool placeholder = materialID == materials.getPlaceholderMaterialId();
|
||||
const bool fileBacked = !materials.getMaterial(materialID).assetReference.empty();
|
||||
const int users = CountMaterialUsers(GetGameObject(), materialID);
|
||||
|
||||
// A copy without a source file is saved inside the scene, so it can be edited freely
|
||||
const auto copyForThisObject = [&] {
|
||||
Material copy = materials.getMaterial(materialID);
|
||||
copy.assetReference = {};
|
||||
if (copy.name.empty() || placeholder) {
|
||||
copy.name = "Material";
|
||||
}
|
||||
SetMaterialID(materials.addMaterial(std::move(copy)));
|
||||
};
|
||||
|
||||
bool copied = false;
|
||||
if (placeholder) {
|
||||
ImGui::TextDisabled("The default material, pick New Material to edit one");
|
||||
} else if (fileBacked) {
|
||||
ImGui::TextDisabled("From %s, edits would not be saved", materials.getMaterial(materialID).assetReference.path.c_str());
|
||||
if (ImGui::Button("Make Editable Copy")) {
|
||||
copyForThisObject();
|
||||
copied = true;
|
||||
}
|
||||
} else if (users > 1) {
|
||||
ImGui::Text("Shared by %d objects, edits change all of them", users);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Make Unique")) {
|
||||
copyForThisObject();
|
||||
copied = true;
|
||||
}
|
||||
}
|
||||
if (copied) {
|
||||
// addMaterial may have moved the material storage, draw the copy next frame
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(placeholder || fileBacked);
|
||||
Material& material = materials.getMaterialMutable(materialID);
|
||||
bool changed = false;
|
||||
changed |= ImGui::ColorEdit3("Base Color", &material.baseColor.x);
|
||||
changed |= ImGui::SliderFloat("Metallic", &material.metallicFactor, 0.0f, 1.0f);
|
||||
changed |= ImGui::SliderFloat("Roughness", &material.roughnessFactor, 0.0f, 1.0f);
|
||||
changed |= ImGui::ColorEdit3("Emissive", &material.emissiveColor.x, ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float);
|
||||
changed |= ImGui::DragFloat("Normal Scale", &material.normalScale, 0.01f, 0.0f, 4.0f);
|
||||
changed |= ImGui::SliderFloat("Occlusion", &material.occlusionStrength, 0.0f, 1.0f);
|
||||
|
||||
constexpr std::array<const char*, 3> filteringNames{"Nearest", "Linear", "Anisotropic"};
|
||||
int filtering = static_cast<int>(material.textureFilteringMode);
|
||||
if (ImGui::Combo("Filtering", &filtering, filteringNames.data(), static_cast<int>(filteringNames.size()))) {
|
||||
material.textureFilteringMode = static_cast<TextureFilteringMode>(filtering);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Textures");
|
||||
for (const TextureSlot& slot : TextureSlots) {
|
||||
ImGui::PushID(slot.label);
|
||||
ImageID& texture = material.*slot.member;
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(slot.label);
|
||||
ImGui::SameLine(90.0f);
|
||||
const float clearWidth = ImGui::GetFrameHeight();
|
||||
ImGui::Button(TextureLabel(resources, texture).c_str(),
|
||||
ImVec2{ImGui::GetContentRegionAvail().x - clearWidth - ImGui::GetStyle().ItemSpacing.x, 0.0f});
|
||||
ImGui::SetItemTooltip("Drop an image from the Assets window");
|
||||
if (const auto dropped = AcceptAssetDrop(); dropped && IsImage(*dropped)) {
|
||||
try {
|
||||
texture = resources.loadImageFromFile(GameState::GetInstance().Gfx(), AssetFS::GetInstance().GetFullPath(*dropped),
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT, true, slot.intent);
|
||||
changed = true;
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Could not load {}: {}", *dropped, exception.what());
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(texture == NULL_IMAGE_ID);
|
||||
if (ImGui::Button("x", ImVec2{clearWidth, 0.0f})) {
|
||||
texture = NULL_IMAGE_ID;
|
||||
changed = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (changed) {
|
||||
renderer.updateMaterialGPU(materialID);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
@@ -1,17 +1,28 @@
|
||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
||||
{
|
||||
struct Face {
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
namespace {
|
||||
constexpr std::string_view PrimitiveScheme = "prim://";
|
||||
constexpr std::uint32_t Segments = 32;
|
||||
constexpr std::uint32_t Rings = 16;
|
||||
|
||||
struct BoxFace {
|
||||
glm::vec3 normal;
|
||||
glm::vec3 tangent;
|
||||
};
|
||||
constexpr std::array<Face, 6> faces{{
|
||||
|
||||
constexpr std::array<BoxFace, 6> BoxFaces{{
|
||||
{{1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, -1.0f}},
|
||||
{{-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
|
||||
{{0.0f, 1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
|
||||
@@ -19,17 +30,13 @@ CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
||||
{{0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},
|
||||
{{0.0f, 0.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}},
|
||||
}};
|
||||
constexpr std::array<glm::vec2, 4> corners{{
|
||||
{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f},
|
||||
}};
|
||||
|
||||
CPUMesh mesh{};
|
||||
// The name becomes the mesh cache key, saved scenes rebuild the box from it
|
||||
mesh.name = BoxKey(size);
|
||||
mesh.minPos = -size * 0.5f;
|
||||
mesh.maxPos = size * 0.5f;
|
||||
void AddBoxFace(CPUMesh& mesh, const BoxFace& face, const glm::vec3& size)
|
||||
{
|
||||
constexpr std::array<glm::vec2, 4> corners{{
|
||||
{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f},
|
||||
}};
|
||||
|
||||
for (const Face& face : faces) {
|
||||
const glm::vec3 bitangent = glm::cross(face.normal, face.tangent);
|
||||
const float width = glm::dot(glm::abs(face.tangent), size);
|
||||
const float height = glm::dot(glm::abs(bitangent), size);
|
||||
@@ -51,17 +58,299 @@ CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
||||
}
|
||||
}
|
||||
|
||||
// Same convention as the box faces: the shader's cross(N, T) * w points along increasing v
|
||||
[[nodiscard]] float TangentSign(const glm::vec3& normal, const glm::vec3& tangent, const glm::vec3& alongV)
|
||||
{
|
||||
return glm::dot(glm::cross(normal, tangent), alongV) >= 0.0f ? 1.0f : -1.0f;
|
||||
}
|
||||
|
||||
// Front faces are counter clockwise seen from outside, flip the ones a generator got backwards
|
||||
void AddTriangle(CPUMesh& mesh, std::uint32_t a, std::uint32_t b, std::uint32_t c)
|
||||
{
|
||||
const auto& va = mesh.vertices[a];
|
||||
const auto& vb = mesh.vertices[b];
|
||||
const auto& vc = mesh.vertices[c];
|
||||
const glm::vec3 faceNormal = glm::cross(vb.position - va.position, vc.position - va.position);
|
||||
if (glm::dot(faceNormal, va.normal + vb.normal + vc.normal) < 0.0f) {
|
||||
std::swap(b, c);
|
||||
}
|
||||
mesh.indices.insert(mesh.indices.end(), {a, b, c});
|
||||
}
|
||||
|
||||
struct ProfilePoint {
|
||||
float radius;
|
||||
float y;
|
||||
glm::vec2 normal;
|
||||
};
|
||||
|
||||
// Spins a top to bottom outline around the y axis, used for spheres, capsules and cylinder sides
|
||||
void AddLathe(CPUMesh& mesh, const std::vector<ProfilePoint>& profile, float uScale)
|
||||
{
|
||||
std::vector<float> v(profile.size(), 0.0f);
|
||||
for (std::size_t k = 1; k < profile.size(); ++k) {
|
||||
const glm::vec2 step{profile[k].radius - profile[k - 1].radius, profile[k].y - profile[k - 1].y};
|
||||
v[k] = v[k - 1] + glm::length(step);
|
||||
}
|
||||
|
||||
const auto first = static_cast<std::uint32_t>(mesh.vertices.size());
|
||||
for (std::size_t k = 0; k < profile.size(); ++k) {
|
||||
const ProfilePoint& point = profile[k];
|
||||
const ProfilePoint& next = profile[k + 1 < profile.size() ? k + 1 : k];
|
||||
const ProfilePoint& previous = profile[k + 1 < profile.size() ? k : k - 1];
|
||||
for (std::uint32_t s = 0; s <= Segments; ++s) {
|
||||
const float angle = glm::two_pi<float>() * static_cast<float>(s) / static_cast<float>(Segments);
|
||||
const float c = std::cos(angle);
|
||||
const float sn = std::sin(angle);
|
||||
const glm::vec3 normal = glm::normalize(glm::vec3{point.normal.x * c, point.normal.y, point.normal.x * sn});
|
||||
const glm::vec3 tangent{-sn, 0.0f, c};
|
||||
const glm::vec3 alongV{(next.radius - previous.radius) * c, next.y - previous.y, (next.radius - previous.radius) * sn};
|
||||
mesh.vertices.push_back(CPUMesh::Vertex{
|
||||
.position = {point.radius * c, point.y, point.radius * sn},
|
||||
.uv_x = angle * uScale,
|
||||
.normal = normal,
|
||||
.uv_y = v[k],
|
||||
.tangent = glm::vec4{tangent, TangentSign(normal, tangent, alongV)},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const std::uint32_t stride = Segments + 1;
|
||||
for (std::uint32_t k = 0; k + 1 < profile.size(); ++k) {
|
||||
for (std::uint32_t s = 0; s < Segments; ++s) {
|
||||
const std::uint32_t a = first + k * stride + s;
|
||||
const std::uint32_t b = a + 1;
|
||||
const std::uint32_t c = a + stride;
|
||||
const std::uint32_t d = c + 1;
|
||||
AddTriangle(mesh, a, c, b);
|
||||
AddTriangle(mesh, b, c, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddDisc(CPUMesh& mesh, float y, float normalY, float radius)
|
||||
{
|
||||
const glm::vec3 normal{0.0f, normalY, 0.0f};
|
||||
const glm::vec3 tangent{1.0f, 0.0f, 0.0f};
|
||||
const glm::vec4 packedTangent{tangent, TangentSign(normal, tangent, {0.0f, 0.0f, 1.0f})};
|
||||
const auto center = static_cast<std::uint32_t>(mesh.vertices.size());
|
||||
mesh.vertices.push_back(CPUMesh::Vertex{
|
||||
.position = {0.0f, y, 0.0f}, .uv_x = 0.0f, .normal = normal, .uv_y = 0.0f, .tangent = packedTangent,
|
||||
});
|
||||
for (std::uint32_t s = 0; s <= Segments; ++s) {
|
||||
const float angle = glm::two_pi<float>() * static_cast<float>(s) / static_cast<float>(Segments);
|
||||
const glm::vec3 position{radius * std::cos(angle), y, radius * std::sin(angle)};
|
||||
mesh.vertices.push_back(CPUMesh::Vertex{
|
||||
.position = position, .uv_x = position.x, .normal = normal, .uv_y = position.z, .tangent = packedTangent,
|
||||
});
|
||||
}
|
||||
for (std::uint32_t s = 0; s < Segments; ++s) {
|
||||
AddTriangle(mesh, center, center + 1 + s, center + 2 + s);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<ProfilePoint> RoundProfile(float radius, float straightHalfHeight)
|
||||
{
|
||||
std::vector<ProfilePoint> profile;
|
||||
for (std::uint32_t k = 0; k <= Rings; ++k) {
|
||||
const float theta = glm::pi<float>() * static_cast<float>(k) / static_cast<float>(Rings);
|
||||
const glm::vec2 normal{std::sin(theta), std::cos(theta)};
|
||||
const float offset = k * 2 < Rings ? straightHalfHeight : -straightHalfHeight;
|
||||
if (k * 2 == Rings && straightHalfHeight > 0.0f) {
|
||||
// The equator twice, once per hemisphere, the band between them is the straight part
|
||||
profile.push_back({radius, straightHalfHeight, normal});
|
||||
profile.push_back({radius, -straightHalfHeight, normal});
|
||||
continue;
|
||||
}
|
||||
profile.push_back({radius * normal.x, radius * normal.y + offset, normal});
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
void SetBounds(CPUMesh& mesh)
|
||||
{
|
||||
mesh.minPos = glm::vec3{std::numeric_limits<float>::max()};
|
||||
mesh.maxPos = glm::vec3{std::numeric_limits<float>::lowest()};
|
||||
for (const auto& vertex : mesh.vertices) {
|
||||
mesh.minPos = glm::min(mesh.minPos, vertex.position);
|
||||
mesh.maxPos = glm::max(mesh.maxPos, vertex.position);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<std::vector<float>> ParseFloats(std::string_view text)
|
||||
{
|
||||
std::vector<float> values;
|
||||
while (true) {
|
||||
float value = 0.0f;
|
||||
const auto [next, error] = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (error != std::errc{} || !(value > 0.0f) || value == std::numeric_limits<float>::infinity()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
values.push_back(value);
|
||||
text.remove_prefix(static_cast<std::size_t>(next - text.data()));
|
||||
if (text.empty()) {
|
||||
return values;
|
||||
}
|
||||
if (!text.starts_with(',')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
text.remove_prefix(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* PrimitiveMeshes::ShapeName(Shape shape)
|
||||
{
|
||||
switch (shape) {
|
||||
case Shape::Box: return "box";
|
||||
case Shape::Plane: return "plane";
|
||||
case Shape::Sphere: return "sphere";
|
||||
case Shape::Cylinder: return "cylinder";
|
||||
case Shape::Capsule: return "capsule";
|
||||
}
|
||||
return "box";
|
||||
}
|
||||
|
||||
PrimitiveMeshes::Desc PrimitiveMeshes::DefaultDesc(Shape shape)
|
||||
{
|
||||
Desc desc{.shape = shape};
|
||||
if (shape == Shape::Plane) {
|
||||
desc.size = {10.0f, 1.0f, 10.0f};
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
CPUMesh PrimitiveMeshes::Make(const Desc& desc)
|
||||
{
|
||||
CPUMesh mesh{};
|
||||
// The name becomes the mesh cache key, saved scenes rebuild the mesh from it
|
||||
mesh.name = MakeKey(desc);
|
||||
|
||||
switch (desc.shape) {
|
||||
case Shape::Box:
|
||||
for (const BoxFace& face : BoxFaces) {
|
||||
AddBoxFace(mesh, face, desc.size);
|
||||
}
|
||||
break;
|
||||
case Shape::Plane:
|
||||
AddBoxFace(mesh, BoxFaces[2], {desc.size.x, 0.0f, desc.size.z});
|
||||
break;
|
||||
case Shape::Sphere:
|
||||
AddLathe(mesh, RoundProfile(desc.radius, 0.0f), desc.radius);
|
||||
break;
|
||||
case Shape::Cylinder: {
|
||||
const float half = desc.height * 0.5f;
|
||||
AddLathe(mesh, {{desc.radius, half, {1.0f, 0.0f}}, {desc.radius, -half, {1.0f, 0.0f}}}, desc.radius);
|
||||
AddDisc(mesh, half, 1.0f, desc.radius);
|
||||
AddDisc(mesh, -half, -1.0f, desc.radius);
|
||||
break;
|
||||
}
|
||||
case Shape::Capsule:
|
||||
// height is the full length tip to tip, like the CapsuleCollider
|
||||
AddLathe(mesh, RoundProfile(desc.radius, std::max(desc.height * 0.5f - desc.radius, 0.0f)), desc.radius);
|
||||
break;
|
||||
}
|
||||
|
||||
SetBounds(mesh);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::string_view PrimitiveScheme = "prim://";
|
||||
constexpr std::string_view BoxPrefix = "prim://box?size=";
|
||||
CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
||||
{
|
||||
return Make({.shape = Shape::Box, .size = size});
|
||||
}
|
||||
|
||||
std::string PrimitiveMeshes::MakeKey(const Desc& desc)
|
||||
{
|
||||
switch (desc.shape) {
|
||||
case Shape::Box:
|
||||
return std::format("{}box?size={},{},{}", PrimitiveScheme, desc.size.x, desc.size.y, desc.size.z);
|
||||
case Shape::Plane:
|
||||
return std::format("{}plane?size={},{}", PrimitiveScheme, desc.size.x, desc.size.z);
|
||||
case Shape::Sphere:
|
||||
return std::format("{}sphere?radius={}", PrimitiveScheme, desc.radius);
|
||||
case Shape::Cylinder:
|
||||
case Shape::Capsule:
|
||||
return std::format("{}{}?radius={}&height={}", PrimitiveScheme, ShapeName(desc.shape), desc.radius, desc.height);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string PrimitiveMeshes::BoxKey(const glm::vec3& size)
|
||||
{
|
||||
return std::format("{}{},{},{}", BoxPrefix, size.x, size.y, size.z);
|
||||
return MakeKey({.shape = Shape::Box, .size = size});
|
||||
}
|
||||
|
||||
std::optional<PrimitiveMeshes::Desc> PrimitiveMeshes::ParseKey(std::string_view key)
|
||||
{
|
||||
key = StripDuplicateSuffix(key);
|
||||
if (!key.starts_with(PrimitiveScheme)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
key.remove_prefix(PrimitiveScheme.size());
|
||||
|
||||
const auto query = key.find('?');
|
||||
if (query == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string_view shapeName = key.substr(0, query);
|
||||
std::optional<Shape> shape;
|
||||
for (const Shape candidate : AllShapes) {
|
||||
if (shapeName == ShapeName(candidate)) {
|
||||
shape = candidate;
|
||||
}
|
||||
}
|
||||
if (!shape) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::vector<float>> size;
|
||||
std::optional<std::vector<float>> radius;
|
||||
std::optional<std::vector<float>> height;
|
||||
std::string_view params = key.substr(query + 1);
|
||||
while (!params.empty()) {
|
||||
const auto end = params.find('&');
|
||||
const std::string_view param = params.substr(0, end);
|
||||
params = end == std::string_view::npos ? std::string_view{} : params.substr(end + 1);
|
||||
|
||||
const auto equals = param.find('=');
|
||||
if (equals == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string_view name = param.substr(0, equals);
|
||||
auto values = ParseFloats(param.substr(equals + 1));
|
||||
auto* target = name == "size" ? &size : name == "radius" ? &radius : name == "height" ? &height : nullptr;
|
||||
if (target == nullptr || target->has_value() || !values) {
|
||||
return std::nullopt;
|
||||
}
|
||||
*target = std::move(values);
|
||||
}
|
||||
|
||||
const auto single = [](const std::optional<std::vector<float>>& values) {
|
||||
return values.has_value() && values->size() == 1;
|
||||
};
|
||||
|
||||
Desc desc{.shape = *shape};
|
||||
switch (*shape) {
|
||||
case Shape::Box:
|
||||
if (!size || size->size() != 3 || radius || height) return std::nullopt;
|
||||
desc.size = {(*size)[0], (*size)[1], (*size)[2]};
|
||||
break;
|
||||
case Shape::Plane:
|
||||
if (!size || size->size() != 2 || radius || height) return std::nullopt;
|
||||
desc.size = {(*size)[0], 1.0f, (*size)[1]};
|
||||
break;
|
||||
case Shape::Sphere:
|
||||
if (!single(radius) || size || height) return std::nullopt;
|
||||
desc.radius = radius->front();
|
||||
break;
|
||||
case Shape::Cylinder:
|
||||
case Shape::Capsule:
|
||||
if (!single(radius) || !single(height) || size) return std::nullopt;
|
||||
desc.radius = radius->front();
|
||||
desc.height = height->front();
|
||||
break;
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
bool PrimitiveMeshes::IsPrimitiveKey(std::string_view key)
|
||||
@@ -86,32 +375,9 @@ std::string_view PrimitiveMeshes::StripDuplicateSuffix(std::string_view key)
|
||||
|
||||
std::optional<CPUMesh> PrimitiveMeshes::MakeFromKey(std::string_view key)
|
||||
{
|
||||
key = StripDuplicateSuffix(key);
|
||||
if (!key.starts_with(BoxPrefix)) {
|
||||
const auto desc = ParseKey(key);
|
||||
if (!desc) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string_view rest = key.substr(BoxPrefix.size());
|
||||
glm::vec3 size{0.0f};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const char* end = rest.data() + rest.size();
|
||||
float value = 0.0f;
|
||||
const auto [next, error] = std::from_chars(rest.data(), end, value);
|
||||
if (error != std::errc{} || !(value > 0.0f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
size[i] = value;
|
||||
rest.remove_prefix(static_cast<std::size_t>(next - rest.data()));
|
||||
if (i < 2) {
|
||||
if (!rest.starts_with(',')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
rest.remove_prefix(1);
|
||||
}
|
||||
}
|
||||
if (!rest.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return MakeBox(size);
|
||||
return Make(*desc);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#include <destrum/Util/AssetBrowser.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] std::string Lowercase(std::string_view value) {
|
||||
std::string result{value};
|
||||
std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool HasExtension(std::string_view path, std::initializer_list<std::string_view> extensions) {
|
||||
const std::string extension = Lowercase(std::filesystem::path{path}.extension().string());
|
||||
return std::ranges::find(extensions, extension) != extensions.end();
|
||||
}
|
||||
|
||||
// Cooker outputs and bookkeeping sit next to the sources in the runtime folders, nobody picks those
|
||||
[[nodiscard]] bool IsHidden(const std::filesystem::path& path) {
|
||||
return path.filename() == "manifest.json" || HasExtension(path.string(), {".dmodel", ".dtex", ".spv"});
|
||||
}
|
||||
|
||||
[[nodiscard]] const FSMount* FindMount(const std::string& scheme) {
|
||||
for (const FSMount& mount : AssetFS::GetInstance().GetMounts()) {
|
||||
if (mount.scheme == scheme) {
|
||||
return &mount;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetBrowser::IsModel(std::string_view path) {
|
||||
return HasExtension(path, {".fbx", ".glb", ".gltf", ".obj"});
|
||||
}
|
||||
|
||||
void AssetBrowser::Draw(bool* open) {
|
||||
if (!ImGui::Begin("Assets", open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& mounts = AssetFS::GetInstance().GetMounts();
|
||||
if (mounts.empty()) {
|
||||
ImGui::TextDisabled("No asset folders are mounted");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (FindMount(m_Scheme) == nullptr) {
|
||||
Navigate(mounts.front().scheme, {});
|
||||
}
|
||||
|
||||
for (const FSMount& mount : mounts) {
|
||||
if (ImGui::RadioButton(mount.scheme.c_str(), m_Scheme == mount.scheme)) {
|
||||
Navigate(mount.scheme, {});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (ImGui::Button("Refresh")) {
|
||||
m_Dirty = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
ImGuiUtils::InputString("##Filter", m_Filter, 0, "Search this folder");
|
||||
|
||||
ImGui::BeginDisabled(m_Folder.empty());
|
||||
if (ImGui::ArrowButton("##Up", ImGuiDir_Up)) {
|
||||
Navigate(m_Scheme, m_Folder.parent_path());
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted((m_Scheme + "://" + m_Folder.generic_string()).c_str());
|
||||
ImGui::Separator();
|
||||
|
||||
if (m_Dirty) {
|
||||
Refresh();
|
||||
}
|
||||
|
||||
if (ImGui::BeginChild("Entries")) {
|
||||
const std::string filter = Lowercase(m_Filter);
|
||||
for (const Entry& entry : m_Entries) {
|
||||
if ((filter.empty() || Lowercase(entry.name).find(filter) != std::string::npos) && DrawEntry(entry)) {
|
||||
// Entered a folder, the list is rebuilt next frame
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
bool AssetBrowser::DrawEntry(const Entry& entry) {
|
||||
bool navigated = false;
|
||||
const std::string virtualPath = m_Scheme + "://" + (m_Folder / entry.name).generic_string();
|
||||
const std::string label = (entry.directory ? "[dir] " : IsModel(entry.name) ? "[model] " : "") + entry.name;
|
||||
|
||||
ImGui::PushID(entry.name.c_str());
|
||||
if (ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick) &&
|
||||
entry.directory && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
Navigate(m_Scheme, m_Folder / entry.name);
|
||||
navigated = true;
|
||||
}
|
||||
|
||||
if (!entry.directory && ImGui::BeginDragDropSource()) {
|
||||
ImGui::SetDragDropPayload(ImGuiUtils::AssetPayload, virtualPath.c_str(), virtualPath.size() + 1);
|
||||
ImGui::TextUnformatted(virtualPath.c_str());
|
||||
if (IsModel(entry.name)) {
|
||||
ImGui::TextDisabled("Drop it in the viewport to place it");
|
||||
}
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupContextItem("EntryMenu")) {
|
||||
if (ImGui::MenuItem("Copy Path")) {
|
||||
ImGui::SetClipboardText(virtualPath.c_str());
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", virtualPath.c_str());
|
||||
ImGui::PopID();
|
||||
return navigated;
|
||||
}
|
||||
|
||||
void AssetBrowser::Navigate(const std::string& scheme, const std::filesystem::path& folder) {
|
||||
m_Scheme = scheme;
|
||||
m_Folder = folder;
|
||||
m_Filter.clear();
|
||||
m_Dirty = true;
|
||||
}
|
||||
|
||||
void AssetBrowser::Refresh() {
|
||||
m_Dirty = false;
|
||||
m_Entries.clear();
|
||||
|
||||
const FSMount* mount = FindMount(m_Scheme);
|
||||
if (mount == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code error;
|
||||
for (const auto& item : std::filesystem::directory_iterator(mount->root / m_Folder, error)) {
|
||||
if (IsHidden(item.path())) {
|
||||
continue;
|
||||
}
|
||||
m_Entries.push_back({
|
||||
.name = item.path().filename().string(),
|
||||
.directory = item.is_directory(error),
|
||||
});
|
||||
}
|
||||
|
||||
std::ranges::sort(m_Entries, [](const Entry& a, const Entry& b) {
|
||||
if (a.directory != b.directory) {
|
||||
return a.directory;
|
||||
}
|
||||
return Lowercase(a.name) < Lowercase(b.name);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user