feat: move editor to project / add post processing etc
This commit is contained in:
+275
-676
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
#include <destrum/Assets/AssetDatabase.h>
|
||||
|
||||
#include <charconv>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/Assets/AssetTypes.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] bool IsVirtualPath(std::string_view path)
|
||||
{
|
||||
return path.find("://") != std::string_view::npos;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<AssetGuid> ReadMetaFile(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream file(path);
|
||||
if (!file) {
|
||||
return std::nullopt;
|
||||
}
|
||||
try {
|
||||
const auto meta = nlohmann::json::parse(file);
|
||||
return AssetGuid::Parse(meta.at("guid").get<std::string>());
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Ignoring broken asset meta file {}: {}", path.string(), exception.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssetGuid AssetGuid::Generate()
|
||||
{
|
||||
static std::mt19937_64 random{std::random_device{}() ^ (static_cast<std::uint64_t>(std::random_device{}()) << 32)};
|
||||
AssetGuid guid;
|
||||
while (!guid.IsValid()) {
|
||||
guid = {random(), random()};
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
|
||||
std::optional<AssetGuid> AssetGuid::Parse(std::string_view text)
|
||||
{
|
||||
if (text.size() != 32) {
|
||||
return std::nullopt;
|
||||
}
|
||||
AssetGuid guid;
|
||||
const auto parseHalf = [](std::string_view half, std::uint64_t& out) {
|
||||
const auto [end, error] = std::from_chars(half.data(), half.data() + half.size(), out, 16);
|
||||
return error == std::errc{} && end == half.data() + half.size();
|
||||
};
|
||||
if (!parseHalf(text.substr(0, 16), guid.high) || !parseHalf(text.substr(16), guid.low) || !guid.IsValid()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
|
||||
std::string AssetGuid::ToString() const
|
||||
{
|
||||
return std::format("{:016x}{:016x}", high, low);
|
||||
}
|
||||
|
||||
AssetDatabase::MetaFileScope::MetaFileScope()
|
||||
{
|
||||
++AssetDatabase::GetInstance().m_MetaFileScopes;
|
||||
}
|
||||
|
||||
AssetDatabase::MetaFileScope::~MetaFileScope()
|
||||
{
|
||||
--AssetDatabase::GetInstance().m_MetaFileScopes;
|
||||
}
|
||||
|
||||
void AssetDatabase::Clear()
|
||||
{
|
||||
m_Guids.clear();
|
||||
m_Paths.clear();
|
||||
}
|
||||
|
||||
void AssetDatabase::Refresh()
|
||||
{
|
||||
Clear();
|
||||
for (const FSMount& mount : AssetFS::GetInstance().GetMounts()) {
|
||||
std::error_code error;
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(mount.root, error)) {
|
||||
if (!entry.is_regular_file(error) || !AssetTypes::IsMeta(entry.path().string())) {
|
||||
continue;
|
||||
}
|
||||
if (const auto guid = ReadMetaFile(entry.path())) {
|
||||
std::filesystem::path asset = entry.path();
|
||||
asset.replace_extension();
|
||||
Register(mount.scheme + "://" + asset.lexically_relative(mount.root).generic_string(), *guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssetDatabase::Register(std::string_view path, const AssetGuid& guid)
|
||||
{
|
||||
if (const auto existing = m_Paths.find(guid); existing != m_Paths.end() && existing->second != path) {
|
||||
spdlog::warn("Assets {} and {} share the id {}, copy assets without their .meta file",
|
||||
existing->second, path, guid.ToString());
|
||||
return;
|
||||
}
|
||||
m_Guids.insert_or_assign(std::string{path}, guid);
|
||||
m_Paths.insert_or_assign(guid, std::string{path});
|
||||
}
|
||||
|
||||
std::optional<AssetGuid> AssetDatabase::FindGuid(std::string_view path) const
|
||||
{
|
||||
const auto it = m_Guids.find(std::string{path});
|
||||
return it != m_Guids.end() ? std::optional{it->second} : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> AssetDatabase::FindPath(const AssetGuid& guid) const
|
||||
{
|
||||
const auto it = m_Paths.find(guid);
|
||||
return it != m_Paths.end() ? std::optional{it->second} : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<AssetGuid> AssetDatabase::GuidForSave(std::string_view path)
|
||||
{
|
||||
if (auto guid = FindGuid(path)) {
|
||||
return guid;
|
||||
}
|
||||
if (m_MetaFileScopes == 0 || !IsVirtualPath(path)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto& assetFs = AssetFS::GetInstance();
|
||||
std::error_code error;
|
||||
if (!std::filesystem::is_regular_file(assetFs.GetFullPath(path), error)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const AssetGuid guid = AssetGuid::Generate();
|
||||
const nlohmann::json meta{{"guid", guid.ToString()}};
|
||||
if (!assetFs.WriteFile(std::string{path} + std::string{AssetTypes::MetaExtension}, meta.dump(4))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Register(path, guid);
|
||||
return guid;
|
||||
}
|
||||
|
||||
nlohmann::json AssetDatabase::WriteReference(std::string_view path, std::string_view subresource)
|
||||
{
|
||||
nlohmann::json reference{{"path", path}};
|
||||
if (!subresource.empty()) {
|
||||
reference["sub"] = subresource;
|
||||
}
|
||||
if (const auto guid = GuidForSave(path)) {
|
||||
reference["guid"] = guid->ToString();
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
std::optional<AssetReference> AssetDatabase::ReadReference(const nlohmann::json& reference) const
|
||||
{
|
||||
if (!reference.is_object() || !reference.contains("path") || !reference.at("path").is_string()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
AssetReference result{
|
||||
.path = reference.at("path").get<std::string>(),
|
||||
.subresource = reference.value("sub", std::string{}),
|
||||
};
|
||||
if (const auto guid = AssetGuid::Parse(reference.value("guid", std::string{}))) {
|
||||
if (auto current = FindPath(*guid); current && *current != result.path) {
|
||||
spdlog::info("{} moved to {}, following its id", result.path, *current);
|
||||
result.path = std::move(*current);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
#include <destrum/Assets/AssetDatabase.h>
|
||||
#include <destrum/Assets/AssetReference.h>
|
||||
#include <destrum/Serialization/MaterialSerialization.h>
|
||||
#include <destrum/Graphics/GfxDevice.h>
|
||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
@@ -15,6 +17,58 @@
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] bool IsAssetPath(std::string_view path)
|
||||
{
|
||||
return path.find("://") != std::string_view::npos;
|
||||
}
|
||||
|
||||
[[nodiscard]] nlohmann::json MeshReference(const std::string& key)
|
||||
{
|
||||
if (key.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (PrimitiveMeshes::IsPrimitiveKey(key)) {
|
||||
return std::string{PrimitiveMeshes::StripDuplicateSuffix(key)};
|
||||
}
|
||||
if (const auto asset = AssetReference::fromCacheKey(key); asset && IsAssetPath(asset->path)) {
|
||||
return AssetDatabase::GetInstance().WriteReference(asset->path, asset->subresource);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
[[nodiscard]] nlohmann::json MaterialReference(const std::string& key)
|
||||
{
|
||||
if (key.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (MaterialSerialization::IsFileKey(key)) {
|
||||
return AssetDatabase::GetInstance().WriteReference(key);
|
||||
}
|
||||
if (const auto asset = AssetReference::fromCacheKey(key); asset && IsAssetPath(asset->path)) {
|
||||
return AssetDatabase::GetInstance().WriteReference(asset->path, asset->subresource);
|
||||
}
|
||||
return {{"runtimeMaterial", key}};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string ReadKey(const nlohmann::json& reference)
|
||||
{
|
||||
if (reference.is_string()) {
|
||||
return reference.get<std::string>();
|
||||
}
|
||||
if (!reference.is_object()) {
|
||||
return {};
|
||||
}
|
||||
if (const auto runtime = reference.find("runtimeMaterial"); runtime != reference.end() && runtime->is_string()) {
|
||||
return runtime->get<std::string>();
|
||||
}
|
||||
if (const auto asset = AssetDatabase::GetInstance().ReadReference(reference)) {
|
||||
return asset->subresource.empty() ? asset->path : asset->cacheKey();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
||||
|
||||
}
|
||||
@@ -76,15 +130,23 @@ nlohmann::json MeshRendererComponent::Serialize() const {
|
||||
throw std::runtime_error("MeshRendererComponent has a material without a key, it cannot be saved");
|
||||
}
|
||||
|
||||
if (GameState::GetInstance().HasRenderer()) {
|
||||
if (const auto* resources = GameState::GetInstance().Renderer().getResources();
|
||||
resources != nullptr && materialID == resources->materials().getPlaceholderMaterialId() &&
|
||||
savedMaterialKey == resources->materials().getMaterialKey(materialID)) {
|
||||
savedMaterialKey.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
{"meshKey", savedMeshKey},
|
||||
{"materialKey", savedMaterialKey}
|
||||
{"mesh", MeshReference(savedMeshKey)},
|
||||
{"material", MaterialReference(savedMaterialKey)},
|
||||
};
|
||||
}
|
||||
|
||||
void MeshRendererComponent::Deserialize(const nlohmann::json& data) {
|
||||
meshKey = data.value("meshKey", std::string{});
|
||||
materialKey = data.value("materialKey", std::string{});
|
||||
meshKey = data.contains("mesh") ? ReadKey(data.at("mesh")) : data.value("meshKey", std::string{});
|
||||
materialKey = data.contains("material") ? ReadKey(data.at("material")) : data.value("materialKey", std::string{});
|
||||
|
||||
// Older scenes also stored raw ids, only trust them when there is no key
|
||||
meshID = meshKey.empty() ? data.value("meshId", NULL_MESH_ID) : NULL_MESH_ID;
|
||||
@@ -104,8 +166,7 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
||||
const auto loadAssetForKey = [&](const std::string& key) {
|
||||
const auto asset = AssetReference::fromCacheKey(key);
|
||||
// Runtime-only keys such as "BulbMaterial#2" also contain a '#', they are not files
|
||||
if (!asset || (asset->path.find("://") == std::string::npos &&
|
||||
!std::filesystem::exists(asset->path))) {
|
||||
if (!asset || (!IsAssetPath(asset->path) && !std::filesystem::exists(asset->path))) {
|
||||
return;
|
||||
}
|
||||
if (!GameState::GetInstance().HasGfxDevice()) {
|
||||
@@ -143,6 +204,13 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
||||
}
|
||||
if (!materialKey.empty()) {
|
||||
auto resolved = resources->materials().findMaterialByKey(materialKey);
|
||||
if (!resolved && MaterialSerialization::IsFileKey(materialKey) && GameState::GetInstance().HasGfxDevice()) {
|
||||
try {
|
||||
resolved = MaterialSerialization::LoadFile(*resources, GameState::GetInstance().Gfx(), materialKey);
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Material file {} could not be loaded: {}", materialKey, exception.what());
|
||||
}
|
||||
}
|
||||
if (!resolved) {
|
||||
loadAssetForKey(materialKey);
|
||||
resolved = resources->materials().findMaterialByKey(materialKey);
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
|
||||
#include <destrum/Components/Animator.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Serialization/MaterialSerialization.h>
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Util/AssetBrowser.h>
|
||||
#include <destrum/Assets/AssetTypes.h>
|
||||
#include <destrum/Util/GameState.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
@@ -47,13 +48,6 @@ namespace {
|
||||
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;
|
||||
@@ -143,11 +137,12 @@ void MeshRendererComponent::DrawMeshField(RenderResources& resources)
|
||||
m_MeshSearch.clear();
|
||||
ImGui::OpenPopup("MeshPicker");
|
||||
}
|
||||
if (const auto dropped = AcceptAssetDrop(); dropped && AssetBrowser::IsModel(*dropped)) {
|
||||
if (const auto dropped = AcceptAssetDrop(); dropped && AssetTypes::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) {
|
||||
if (resources.meshes().isValid(id) && resources.meshes().getMeshKey(id).starts_with(*dropped + "#") &&
|
||||
!resources.meshes().getMesh(id).hasSkeleton) {
|
||||
parts.push_back(id);
|
||||
}
|
||||
}
|
||||
@@ -184,7 +179,7 @@ void MeshRendererComponent::DrawMeshField(RenderResources& resources)
|
||||
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)) {
|
||||
if (!meshes.isValid(id) || meshes.getMesh(id).hasSkeleton || !Matches(key, m_MeshSearch)) {
|
||||
continue;
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(id));
|
||||
@@ -283,10 +278,18 @@ void MeshRendererComponent::DrawMaterialField(RenderResources& resources, GameRe
|
||||
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 (const auto dropped = AcceptAssetDrop()) {
|
||||
if (AssetTypes::IsMaterial(*dropped)) {
|
||||
try {
|
||||
SetMaterialID(MaterialSerialization::LoadFile(resources, GameState::GetInstance().Gfx(), *dropped));
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Could not load {}: {}", *dropped, exception.what());
|
||||
}
|
||||
} else if (AssetTypes::IsModel(*dropped)) {
|
||||
LoadDroppedModel(resources, *dropped);
|
||||
m_MaterialSearch = *dropped;
|
||||
ImGui::OpenPopup("MaterialPicker");
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("MaterialPicker")) {
|
||||
@@ -305,7 +308,7 @@ void MeshRendererComponent::DrawMaterialField(RenderResources& resources, GameRe
|
||||
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)) {
|
||||
if (!materials.isValid(id) || id == materials.getPlaceholderMaterialId() || !Matches(key, m_MaterialSearch)) {
|
||||
continue;
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(id));
|
||||
@@ -331,8 +334,10 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
|
||||
}
|
||||
|
||||
MaterialCache& materials = resources.materials();
|
||||
const std::string sourcePath = materials.getMaterial(materialID).assetReference.path;
|
||||
const bool placeholder = materialID == materials.getPlaceholderMaterialId();
|
||||
const bool fileBacked = !materials.getMaterial(materialID).assetReference.empty();
|
||||
const bool materialFile = MaterialSerialization::IsFileKey(sourcePath);
|
||||
const bool fromModel = !sourcePath.empty() && !materialFile;
|
||||
const int users = CountMaterialUsers(GetGameObject(), materialID);
|
||||
|
||||
// A copy without a source file is saved inside the scene, so it can be edited freely
|
||||
@@ -345,44 +350,68 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
|
||||
SetMaterialID(materials.addMaterial(std::move(copy)));
|
||||
};
|
||||
|
||||
bool copied = false;
|
||||
bool replaced = 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")) {
|
||||
} else if (fromModel) {
|
||||
ImGui::TextDisabled("From %s, edits would not be saved", sourcePath.c_str());
|
||||
replaced = ImGui::Button("Make Editable Copy");
|
||||
if (replaced) {
|
||||
copyForThisObject();
|
||||
copied = true;
|
||||
}
|
||||
} else if (users > 1) {
|
||||
ImGui::Text("Shared by %d objects, edits change all of them", users);
|
||||
ImGui::SameLine();
|
||||
} else if (materialFile) {
|
||||
ImGui::TextDisabled("%s, used by %d objects here", sourcePath.c_str(), users);
|
||||
if (ImGui::SmallButton("Make Unique")) {
|
||||
copyForThisObject();
|
||||
copied = true;
|
||||
replaced = 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();
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
if (!replaced && ImGui::SmallButton("Save as Material File")) {
|
||||
const std::string name = materials.getMaterial(materialID).name.empty() ? "material" : materials.getMaterial(materialID).name;
|
||||
std::string path = "game://materials/" + name + std::string{AssetTypes::MaterialExtension};
|
||||
for (int suffix = 1; std::filesystem::exists(AssetFS::GetInstance().GetFullPath(path)); ++suffix) {
|
||||
path = "game://materials/" + name + "_" + std::to_string(suffix) + std::string{AssetTypes::MaterialExtension};
|
||||
}
|
||||
if (MaterialSerialization::SaveFile(resources, materialID, path)) {
|
||||
SetMaterialID(MaterialSerialization::LoadFile(resources, GameState::GetInstance().Gfx(), path));
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (copied) {
|
||||
// addMaterial may have moved the material storage, draw the copy next frame
|
||||
if (replaced) {
|
||||
// addMaterial may have moved the material storage, draw the new one next frame
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(placeholder || fileBacked);
|
||||
ImGui::BeginDisabled(placeholder || fromModel);
|
||||
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);
|
||||
bool finished = false;
|
||||
const auto track = [&](bool valueChanged) {
|
||||
changed |= valueChanged;
|
||||
finished |= ImGui::IsItemDeactivatedAfterEdit();
|
||||
};
|
||||
track(ImGui::ColorEdit3("Base Color", &material.baseColor.x));
|
||||
track(ImGui::SliderFloat("Metallic", &material.metallicFactor, 0.0f, 1.0f));
|
||||
track(ImGui::SliderFloat("Roughness", &material.roughnessFactor, 0.0f, 1.0f));
|
||||
track(ImGui::ColorEdit3("Emissive", &material.emissiveColor.x, ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float));
|
||||
track(ImGui::DragFloat("Normal Scale", &material.normalScale, 0.01f, 0.0f, 4.0f));
|
||||
track(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;
|
||||
changed = finished = true;
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Textures");
|
||||
@@ -396,11 +425,10 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
|
||||
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)) {
|
||||
if (const auto dropped = AcceptAssetDrop(); dropped && AssetTypes::IsImage(*dropped)) {
|
||||
try {
|
||||
texture = resources.loadImageFromFile(GameState::GetInstance().Gfx(), AssetFS::GetInstance().GetFullPath(*dropped),
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT, true, slot.intent);
|
||||
changed = true;
|
||||
texture = resources.loadMaterialTexture(GameState::GetInstance().Gfx(), AssetFS::GetInstance().GetFullPath(*dropped), slot.intent);
|
||||
changed = finished = true;
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Could not load {}: {}", *dropped, exception.what());
|
||||
}
|
||||
@@ -409,7 +437,7 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
|
||||
ImGui::BeginDisabled(texture == NULL_IMAGE_ID);
|
||||
if (ImGui::Button("x", ImVec2{clearWidth, 0.0f})) {
|
||||
texture = NULL_IMAGE_ID;
|
||||
changed = true;
|
||||
changed = finished = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopID();
|
||||
@@ -419,5 +447,8 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
|
||||
if (changed) {
|
||||
renderer.updateMaterialGPU(materialID);
|
||||
}
|
||||
if (finished && materialFile) {
|
||||
MaterialSerialization::SaveFile(resources, materialID, sourcePath);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
@@ -214,6 +214,53 @@ std::optional<std::string> AssetFS::ToVirtualPath(const std::filesystem::path& f
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void AssetFS::SetSourceRoot(std::string_view scheme, std::filesystem::path sourceRoot) {
|
||||
for (FSMount& mount : mounts) {
|
||||
if (mount.scheme == scheme) {
|
||||
mount.sourceRoot = std::filesystem::absolute(std::move(sourceRoot)).lexically_normal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("asset mount not found: " + std::string{scheme});
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> AssetFS::GetSourcePath(std::string_view vpath) const {
|
||||
const ParsedVirtualPath parsed = ParseVirtualPath(vpath);
|
||||
const FSMount* mount = FindMount(mounts, parsed.scheme);
|
||||
if (mount == nullptr || !mount->sourceRoot) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return (*mount->sourceRoot / parsed.relativePath).lexically_normal();
|
||||
}
|
||||
|
||||
bool AssetFS::WriteFile(std::string_view vpath, std::string_view contents) {
|
||||
std::vector<std::filesystem::path> targets{GetFullPath(vpath)};
|
||||
if (auto source = GetSourcePath(vpath)) {
|
||||
targets.push_back(std::move(*source));
|
||||
}
|
||||
|
||||
for (const auto& target : targets) {
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(target.parent_path(), error);
|
||||
std::filesystem::path temporary = target;
|
||||
temporary += ".tmp";
|
||||
{
|
||||
std::ofstream file(temporary, std::ios::binary | std::ios::trunc);
|
||||
if (!file.write(contents.data(), static_cast<std::streamsize>(contents.size()))) {
|
||||
spdlog::error("Failed to write {}", temporary.string());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::filesystem::rename(temporary, target, error);
|
||||
if (error) {
|
||||
spdlog::error("Failed to replace {}: {}", target.string(), error.message());
|
||||
std::filesystem::remove(temporary, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AssetFS::ReadFile(const std::filesystem::path& fullPath) {
|
||||
std::ifstream file(fullPath, std::ios::binary);
|
||||
if (!file) {
|
||||
|
||||
@@ -46,6 +46,7 @@ void MaterialCache::cleanup(GfxDevice& gfxDevice)
|
||||
materialDataBuffer = {};
|
||||
materials.clear();
|
||||
materialKeys.clear();
|
||||
freeIds = std::make_shared<std::vector<MaterialID>>();
|
||||
placeholderMaterialId = NULL_MATERIAL_ID;
|
||||
device = nullptr;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ MaterialID MaterialCache::addMaterial(Material material, std::string key)
|
||||
{
|
||||
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
|
||||
|
||||
const auto id = getFreeMaterialId();
|
||||
const MaterialID id = getFreeMaterialId();
|
||||
if (id >= MAX_MATERIALS) {
|
||||
throw std::runtime_error("MaterialCache capacity exhausted");
|
||||
}
|
||||
@@ -70,7 +71,7 @@ MaterialID MaterialCache::addMaterial(Material material, std::string key)
|
||||
static_cast<VkDeviceSize>(id) * sizeof(MaterialData),
|
||||
sizeof(MaterialData));
|
||||
|
||||
const auto materialId = static_cast<MaterialID>(materials.size());
|
||||
const MaterialID materialId = id;
|
||||
if (key.empty() && !material.assetReference.empty()) {
|
||||
key = material.assetReference.cacheKey();
|
||||
} else if (key.empty()) {
|
||||
@@ -84,12 +85,29 @@ MaterialID MaterialCache::addMaterial(Material material, std::string key)
|
||||
key = baseKey + "#" + std::to_string(suffix++);
|
||||
}
|
||||
|
||||
materials.push_back(std::move(material));
|
||||
materialKeys.push_back(std::move(key));
|
||||
if (materialId == materials.size()) {
|
||||
materials.push_back(std::move(material));
|
||||
materialKeys.push_back(std::move(key));
|
||||
} else {
|
||||
std::erase(*freeIds, materialId);
|
||||
materials[materialId] = std::move(material);
|
||||
materialKeys[materialId] = std::move(key);
|
||||
}
|
||||
|
||||
return materialId;
|
||||
}
|
||||
|
||||
void MaterialCache::removeMaterial(MaterialID id)
|
||||
{
|
||||
if (!isValid(id) || id == placeholderMaterialId) {
|
||||
return;
|
||||
}
|
||||
|
||||
materials[id] = {};
|
||||
materialKeys[id].clear();
|
||||
device->deferDestroy([ids = freeIds, id] { ids->push_back(id); });
|
||||
}
|
||||
|
||||
MaterialID MaterialCache::addSimpleTextureMaterial(ImageID textureID)
|
||||
{
|
||||
Material material = Material::SimpleColor(
|
||||
@@ -117,6 +135,9 @@ const std::string& MaterialCache::getMaterialKey(MaterialID id) const
|
||||
|
||||
std::optional<MaterialID> MaterialCache::findMaterialByKey(std::string_view key) const
|
||||
{
|
||||
if (key.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto it = std::find(materialKeys.begin(), materialKeys.end(), key);
|
||||
if (it == materialKeys.end()) {
|
||||
return std::nullopt;
|
||||
@@ -131,7 +152,7 @@ std::optional<MaterialID> MaterialCache::findMaterialByName(
|
||||
std::optional<MaterialID> fallback;
|
||||
for (MaterialID id = 0; id < materials.size(); ++id) {
|
||||
const auto& material = materials[id];
|
||||
if (material.name != name) {
|
||||
if (!isValid(id) || material.name != name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -186,7 +207,7 @@ MaterialData MaterialCache::toMaterialData(const Material& material) const
|
||||
|
||||
MaterialID MaterialCache::getFreeMaterialId() const
|
||||
{
|
||||
return static_cast<MaterialID>(materials.size());
|
||||
return freeIds->empty() ? static_cast<MaterialID>(materials.size()) : freeIds->back();
|
||||
}
|
||||
|
||||
MaterialID MaterialCache::getPlaceholderMaterialId() const
|
||||
|
||||
@@ -29,9 +29,17 @@ MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
|
||||
|
||||
uploadMesh(gfxDevice, cpuMesh, gpuMesh);
|
||||
|
||||
const auto id = meshes.size();
|
||||
meshes.push_back(std::move(gpuMesh));
|
||||
cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh
|
||||
MeshID id = meshes.size();
|
||||
if (!recycleBin->ids.empty()) {
|
||||
id = recycleBin->ids.back();
|
||||
recycleBin->ids.pop_back();
|
||||
meshes[id] = std::move(gpuMesh);
|
||||
cpuMeshes[id] = cpuMesh;
|
||||
} else {
|
||||
meshes.push_back(std::move(gpuMesh));
|
||||
cpuMeshes.push_back(cpuMesh);
|
||||
meshKeys.emplace_back();
|
||||
}
|
||||
std::string key;
|
||||
if (!cpuMesh.assetReference.empty()) {
|
||||
key = cpuMesh.assetReference.cacheKey();
|
||||
@@ -45,10 +53,52 @@ MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
|
||||
while (std::find(meshKeys.begin(), meshKeys.end(), key) != meshKeys.end()) {
|
||||
key = baseKey + "#" + std::to_string(suffix++);
|
||||
}
|
||||
meshKeys.push_back(std::move(key));
|
||||
meshKeys[id] = std::move(key);
|
||||
return id;
|
||||
}
|
||||
|
||||
void MeshCache::removeMesh(GfxDevice& gfxDevice, MeshID id)
|
||||
{
|
||||
if (!isValid(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
GPUMesh& mesh = meshes[id];
|
||||
gfxDevice.destroyBuffer(mesh.vertexBuffer);
|
||||
gfxDevice.destroyBuffer(mesh.skinningDataBuffer);
|
||||
const IndexRange range{mesh.firstIndex, mesh.numIndices};
|
||||
mesh = {};
|
||||
cpuMeshes[id] = {};
|
||||
meshKeys[id].clear();
|
||||
|
||||
gfxDevice.deferDestroy([bin = recycleBin, id, range] {
|
||||
bin->ids.push_back(id);
|
||||
if (range.count > 0) {
|
||||
bin->indexRanges.push_back(range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::uint32_t MeshCache::allocateIndices(GfxDevice& gfxDevice, std::uint32_t count)
|
||||
{
|
||||
auto& ranges = recycleBin->indexRanges;
|
||||
const auto fit = std::ranges::find_if(ranges, [count](const IndexRange& range) { return range.count >= count; });
|
||||
if (fit != ranges.end()) {
|
||||
const std::uint32_t first = fit->first;
|
||||
fit->first += count;
|
||||
fit->count -= count;
|
||||
if (fit->count == 0) {
|
||||
ranges.erase(fit);
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
ensureIndexCapacity(gfxDevice, indexCount + count);
|
||||
const std::uint32_t first = indexCount;
|
||||
indexCount += count;
|
||||
return first;
|
||||
}
|
||||
|
||||
void MeshCache::ensureIndexCapacity(GfxDevice& gfxDevice, std::uint32_t requiredIndices)
|
||||
{
|
||||
if (requiredIndices <= indexCapacity) {
|
||||
@@ -111,8 +161,7 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
|
||||
{
|
||||
try {
|
||||
const auto numIndices = static_cast<std::uint32_t>(cpuMesh.indices.size());
|
||||
ensureIndexCapacity(gfxDevice, indexCount + numIndices);
|
||||
gpuMesh.firstIndex = indexCount;
|
||||
gpuMesh.firstIndex = allocateIndices(gfxDevice, numIndices);
|
||||
|
||||
const auto indexBufferSize = cpuMesh.indices.size() * sizeof(std::uint32_t);
|
||||
const auto indexBufferOffset = static_cast<VkDeviceSize>(gpuMesh.firstIndex) * sizeof(std::uint32_t);
|
||||
@@ -139,8 +188,8 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
|
||||
memcpy(static_cast<char*>(vertexIndexData) + vertexBufferSize, cpuMesh.indices.data(), indexBufferSize);
|
||||
gfxDevice.getMemoryManager().flushAllocation(vertexIndexStaging);
|
||||
|
||||
// Writing into the shared index buffer is safe while frames are in flight:
|
||||
// this range is new, they only read the ranges of meshes added before it.
|
||||
// Writing into the shared index buffer is safe while frames are in flight: the range is either
|
||||
// new or was recycled only after every frame that used it finished.
|
||||
gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) {
|
||||
vkutil::bufferHostWriteToTransferReadBarrier(
|
||||
cmd,
|
||||
@@ -201,7 +250,6 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
|
||||
}
|
||||
|
||||
gfxDevice.destroyBufferNow(vertexIndexStaging);
|
||||
indexCount += numIndices;
|
||||
|
||||
const auto vtxBufferName = cpuMesh.name + " (vtx)";
|
||||
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
|
||||
@@ -295,6 +343,9 @@ const std::string& MeshCache::getMeshKey(MeshID id) const
|
||||
|
||||
std::optional<MeshID> MeshCache::findMeshByKey(std::string_view key) const
|
||||
{
|
||||
if (key.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto it = std::find(meshKeys.begin(), meshKeys.end(), key);
|
||||
if (it == meshKeys.end()) {
|
||||
return std::nullopt;
|
||||
@@ -315,4 +366,5 @@ void MeshCache::cleanup(GfxDevice& gfxDevice)
|
||||
meshes.clear();
|
||||
cpuMeshes.clear();
|
||||
meshKeys.clear();
|
||||
recycleBin = std::make_shared<RecycleBin>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#include <destrum/Graphics/Pipelines/PostProcessPipeline.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <volk.h>
|
||||
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/GfxDevice.h>
|
||||
#include <destrum/Graphics/RenderResources.h>
|
||||
|
||||
namespace {
|
||||
constexpr std::uint32_t NoTexture = 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
void PostProcessPipeline::init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat outputFormat)
|
||||
{
|
||||
try {
|
||||
const auto vertexShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/fullscreen_triangle.vert");
|
||||
const auto fragmentShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/post.frag");
|
||||
|
||||
const VkPushConstantRange pushConstants{
|
||||
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
.offset = 0,
|
||||
.size = sizeof(PushConstants),
|
||||
};
|
||||
const VkDescriptorSetLayout bindlessLayout = resources.getBindlessDescSetLayout();
|
||||
const VkPipelineLayoutCreateInfo layoutInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
|
||||
.setLayoutCount = 1,
|
||||
.pSetLayouts = &bindlessLayout,
|
||||
.pushConstantRangeCount = 1,
|
||||
.pPushConstantRanges = &pushConstants,
|
||||
};
|
||||
if (vkCreatePipelineLayout(gfxDevice.getDevice(), &layoutInfo, nullptr, &m_PipelineLayout) != VK_SUCCESS) {
|
||||
throw std::runtime_error("Could not create the post process pipeline layout");
|
||||
}
|
||||
|
||||
PipelineConfigInfo config{};
|
||||
Pipeline::DefaultPipelineConfigInfo(config);
|
||||
config.name = "Post Process Pipeline";
|
||||
config.pipelineLayout = m_PipelineLayout;
|
||||
config.vertexAttributeDescriptions = {};
|
||||
config.vertexBindingDescriptions = {};
|
||||
config.colorAttachments = {outputFormat};
|
||||
config.depthAttachment = VK_FORMAT_UNDEFINED;
|
||||
config.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
|
||||
config.depthStencilInfo.depthTestEnable = VK_FALSE;
|
||||
config.depthStencilInfo.depthWriteEnable = VK_FALSE;
|
||||
|
||||
m_Pipeline = std::make_unique<Pipeline>(gfxDevice, vertexShader.string(), fragmentShader.string(), config);
|
||||
} catch (...) {
|
||||
cleanup(gfxDevice.getDevice());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessPipeline::cleanup(VkDevice device)
|
||||
{
|
||||
m_Pipeline.reset();
|
||||
if (m_PipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
|
||||
vkDestroyPipelineLayout(device, m_PipelineLayout, nullptr);
|
||||
}
|
||||
m_PipelineLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void PostProcessPipeline::draw(VkCommandBuffer cmd, const RenderResources& resources, VkExtent2D extent, const Settings& settings) const
|
||||
{
|
||||
m_Pipeline->bind(cmd);
|
||||
resources.bindBindlessDescSet(cmd, m_PipelineLayout);
|
||||
|
||||
const VkViewport viewport{
|
||||
.width = static_cast<float>(extent.width),
|
||||
.height = static_cast<float>(extent.height),
|
||||
.maxDepth = 1.0f,
|
||||
};
|
||||
const VkRect2D scissor{.extent = extent};
|
||||
vkCmdSetViewport(cmd, 0, 1, &viewport);
|
||||
vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||
|
||||
const PushConstants constants{
|
||||
.sceneColorTexture = settings.sceneColor,
|
||||
.selectionTexture = settings.selection ? *settings.selection : NoTexture,
|
||||
.tonemapper = static_cast<std::uint32_t>(settings.tonemapper),
|
||||
.exposure = std::exp2(settings.exposure),
|
||||
.hoverPixel = settings.hoverPixel.value_or(glm::ivec2{-1}),
|
||||
};
|
||||
vkCmdPushConstants(cmd, m_PipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstants), &constants);
|
||||
vkCmdDraw(cmd, 3, 1, 0, 0);
|
||||
}
|
||||
@@ -193,7 +193,63 @@ ImageID RenderResources::loadImageFromFile(
|
||||
TextureIntent intent)
|
||||
{
|
||||
(void)gfxDevice;
|
||||
return imageCache->loadImageFromFile(path, usage, mipMap, intent);
|
||||
const ImageID id = imageCache->loadImageFromFile(path, usage, mipMap, intent);
|
||||
pinnedImages.insert(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
ImageID RenderResources::loadMaterialTexture(
|
||||
GfxDevice& gfxDevice,
|
||||
const std::filesystem::path& path,
|
||||
TextureIntent intent)
|
||||
{
|
||||
(void)gfxDevice;
|
||||
const ImageID id = imageCache->loadImageFromFile(path, VK_IMAGE_USAGE_SAMPLED_BIT, true, intent);
|
||||
if (id != errorImageId) {
|
||||
materialTextures.insert(id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
RenderResources::ReleaseStats RenderResources::releaseUnused(
|
||||
GfxDevice& gfxDevice,
|
||||
const std::unordered_set<MeshID>& usedMeshes,
|
||||
const std::unordered_set<MaterialID>& usedMaterials)
|
||||
{
|
||||
ReleaseStats stats;
|
||||
for (MeshID id = 0; id < meshCache->getSlotCount(); ++id) {
|
||||
if (meshCache->isValid(id) && !usedMeshes.contains(id)) {
|
||||
meshCache->removeMesh(gfxDevice, id);
|
||||
++stats.meshes;
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<ImageID> usedImages;
|
||||
for (MaterialID id = 0; id < materialCache->getMaterialCount(); ++id) {
|
||||
if (!materialCache->isValid(id)) {
|
||||
continue;
|
||||
}
|
||||
if (id != materialCache->getPlaceholderMaterialId() && !usedMaterials.contains(id)) {
|
||||
materialCache->removeMaterial(id);
|
||||
++stats.materials;
|
||||
continue;
|
||||
}
|
||||
const Material& material = materialCache->getMaterial(id);
|
||||
for (const ImageID texture : {material.diffuseTexture, material.normalTexture, material.metallicTexture,
|
||||
material.roughnessTexture, material.occlusionTexture, material.emissiveTexture}) {
|
||||
usedImages.insert(texture);
|
||||
}
|
||||
}
|
||||
|
||||
std::erase_if(materialTextures, [&](ImageID id) {
|
||||
if (usedImages.contains(id) || pinnedImages.contains(id)) {
|
||||
return false;
|
||||
}
|
||||
imageCache->removeImage(id);
|
||||
++stats.images;
|
||||
return true;
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
|
||||
const GPUImage& RenderResources::getImage(ImageID id) const {
|
||||
@@ -253,12 +309,7 @@ MaterialID RenderResources::addModelMaterial(
|
||||
if (texture == nullptr) {
|
||||
return NULL_IMAGE_ID;
|
||||
}
|
||||
return loadImageFromFile(
|
||||
gfxDevice,
|
||||
std::filesystem::path{texture->path},
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||
true,
|
||||
intent);
|
||||
return loadMaterialTexture(gfxDevice, std::filesystem::path{texture->path}, intent);
|
||||
};
|
||||
|
||||
Material material = Material::SimpleColor(
|
||||
@@ -326,6 +377,8 @@ void RenderResources::cleanup(GfxDevice& gfxDevice)
|
||||
return;
|
||||
}
|
||||
|
||||
materialTextures.clear();
|
||||
pinnedImages.clear();
|
||||
if (materialCache) {
|
||||
materialCache->cleanup(gfxDevice);
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
|
||||
sizeof(GPUSceneData),
|
||||
"scene data");
|
||||
|
||||
createDrawImage(gfxDevice, drawImageSize, true);
|
||||
createDrawImage(gfxDevice, drawImageSize);
|
||||
|
||||
meshPipeline = std::make_unique<MeshPipeline>();
|
||||
meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat, selectionImageFormat);
|
||||
meshPipeline->init(gfxDevice, _resources, sceneColorFormat, depthImageFormat, selectionImageFormat);
|
||||
|
||||
for (GPUBuffer& pickBuffer : pickBuffers)
|
||||
{
|
||||
@@ -44,7 +44,7 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
|
||||
}
|
||||
|
||||
skyboxPipeline = std::make_unique<SkyboxPipeline>();
|
||||
skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat);
|
||||
skyboxPipeline->init(gfxDevice, _resources, sceneColorFormat, depthImageFormat);
|
||||
|
||||
skinningPipeline = std::make_unique<SkinningPipeline>();
|
||||
skinningPipeline->init(gfxDevice);
|
||||
@@ -61,6 +61,9 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
|
||||
renderer2DPass = std::make_unique<Renderer2DPass>();
|
||||
renderer2DPass->init(gfxDevice, _resources, drawImageFormat);
|
||||
|
||||
postProcessPipeline = std::make_unique<PostProcessPipeline>();
|
||||
postProcessPipeline->init(gfxDevice, _resources, drawImageFormat);
|
||||
|
||||
GameState::GetInstance().SetRenderer(this);
|
||||
initialized = true;
|
||||
}
|
||||
@@ -79,7 +82,7 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice)
|
||||
{
|
||||
pickPending[frameIndex] = false;
|
||||
gfxDevice.getMemoryManager().invalidateAllocation(pickBuffers[frameIndex]);
|
||||
pickResult = *static_cast<const std::uint32_t*>(pickBuffers[frameIndex].info.pMappedData);
|
||||
pickResult = *static_cast<const std::uint32_t*>(pickBuffers[frameIndex].info.pMappedData) & ~HighlightedObjectFlag;
|
||||
}
|
||||
|
||||
meshDrawCommands.clear();
|
||||
@@ -250,18 +253,19 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
|
||||
);
|
||||
}
|
||||
|
||||
const auto& sceneColor = resources->getImage(sceneColorImageId);
|
||||
const auto& drawImage = resources->getImage(drawImageId);
|
||||
const auto& depthImage = resources->getImage(depthImageId);
|
||||
|
||||
{
|
||||
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Transition Draw Image");
|
||||
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Transition Scene Color");
|
||||
|
||||
vkutil::transitionImage(
|
||||
cmd,
|
||||
drawImage.image,
|
||||
drawImage.layout,
|
||||
sceneColor.image,
|
||||
sceneColor.layout,
|
||||
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
drawImage.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
sceneColor.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -376,8 +380,8 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
|
||||
}
|
||||
|
||||
auto renderInfo = vkutil::createRenderingInfo({
|
||||
.renderExtent = drawImage.getExtent2D(),
|
||||
.colorImageView = drawImage.imageView,
|
||||
.renderExtent = sceneColor.getExtent2D(),
|
||||
.colorImageView = sceneColor.imageView,
|
||||
.colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f},
|
||||
.depthImageView = depthImage.imageView,
|
||||
});
|
||||
@@ -420,6 +424,38 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
|
||||
|
||||
vkutil::cmdEndLabel(cmd);
|
||||
|
||||
{
|
||||
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Post Process");
|
||||
vkutil::ScopedCmdLabel label(cmd, "Post Process", vkutil::labelColors::opaque);
|
||||
|
||||
vkutil::transitionImage(cmd, sceneColor.image, sceneColor.layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
sceneColor.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
if (selectionPassEnabled)
|
||||
{
|
||||
const auto& selectionImage = resources->getImage(selectionImageId);
|
||||
vkutil::transitionImage(cmd, selectionImage.image, selectionImage.layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
selectionImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
}
|
||||
vkutil::transitionImage(cmd, drawImage.image, drawImage.layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
drawImage.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
|
||||
auto postInfo = vkutil::createRenderingInfo({
|
||||
.renderExtent = drawImage.getExtent2D(),
|
||||
.colorImageView = drawImage.imageView,
|
||||
});
|
||||
postInfo.renderingInfo.pColorAttachments = &postInfo.colorAttachment;
|
||||
|
||||
vkCmdBeginRendering(cmd, &postInfo.renderingInfo);
|
||||
postProcessPipeline->draw(cmd, *resources, drawImage.getExtent2D(), {
|
||||
.sceneColor = sceneColorImageId,
|
||||
.selection = selectionPassEnabled ? std::optional{selectionImageId} : std::nullopt,
|
||||
.hoverPixel = hoverPixel,
|
||||
.tonemapper = renderSelectionView ? Tonemapper::None : sceneData.tonemapper,
|
||||
.exposure = renderSelectionView ? 0.0f : sceneData.exposure,
|
||||
});
|
||||
vkCmdEndRendering(cmd);
|
||||
}
|
||||
|
||||
{
|
||||
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "LineRenderingPass::draw");
|
||||
vkutil::ScopedCmdLabel label(cmd, "LineRenderingPass::draw", vkutil::labelColors::debug);
|
||||
@@ -524,6 +560,9 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
|
||||
if (meshPipeline)
|
||||
meshPipeline->cleanup(device);
|
||||
|
||||
if (postProcessPipeline)
|
||||
postProcessPipeline->cleanup(device);
|
||||
|
||||
sceneDataBuffer.cleanup(gfxDevice);
|
||||
drawList.cleanup(gfxDevice);
|
||||
for (GPUBuffer& pickBuffer : pickBuffers)
|
||||
@@ -540,9 +579,13 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
|
||||
// if (depthImageId != NULL_IMAGE_ID)
|
||||
// gfxDevice.destroyImage(depthImageId);
|
||||
|
||||
sceneColorImageId = NULL_IMAGE_ID;
|
||||
drawImageId = NULL_IMAGE_ID;
|
||||
depthImageId = NULL_IMAGE_ID;
|
||||
selectionImageId = NULL_IMAGE_ID;
|
||||
postProcessPipeline.reset();
|
||||
highlightedObjects.clear();
|
||||
hoverPixel.reset();
|
||||
resources = nullptr;
|
||||
meshPipeline.reset();
|
||||
skyboxPipeline.reset();
|
||||
@@ -572,7 +615,7 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma
|
||||
.transformMatrix = transform,
|
||||
.worldBoundingSphere = worldBoundingSphere,
|
||||
.materialId = materialId,
|
||||
.objectId = objectId,
|
||||
.objectId = highlightedObjects.contains(objectId) ? objectId | HighlightedObjectFlag : objectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -595,7 +638,7 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
|
||||
.materialId = materialId,
|
||||
.skinnedMesh = skinnedMesh,
|
||||
.jointMatricesStartIndex = static_cast<std::uint32_t>(jointMatricesStartIndex),
|
||||
.objectId = objectId,
|
||||
.objectId = highlightedObjects.contains(objectId) ? objectId | HighlightedObjectFlag : objectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -661,10 +704,73 @@ void GameRenderer::updateMaterialGPU(MaterialID id)
|
||||
pendingMaterialUploads.push_back(id);
|
||||
}
|
||||
|
||||
void GameRenderer::setSkyboxTexture(ImageID skyboxImageId)
|
||||
void GameRenderer::setSkyboxTexture(ImageID imageId)
|
||||
{
|
||||
spdlog::debug("Set skybox texture to image id {}", skyboxImageId);
|
||||
skyboxPipeline->setSkyboxImage(skyboxImageId);
|
||||
spdlog::debug("Set skybox texture to image id {}", imageId);
|
||||
skyboxImageId = imageId;
|
||||
skyboxPipeline->setSkyboxImage(imageId);
|
||||
}
|
||||
|
||||
bool GameRenderer::reloadShaders(GfxDevice& gfxDevice)
|
||||
{
|
||||
const VkDevice device = gfxDevice.getDevice();
|
||||
const VkExtent3D extent = getDrawImage().extent;
|
||||
|
||||
auto mesh = std::make_unique<MeshPipeline>();
|
||||
auto skybox = std::make_unique<SkyboxPipeline>();
|
||||
auto skinning = std::make_unique<SkinningPipeline>();
|
||||
auto lightCulling = std::make_unique<LightCullingPipeline>();
|
||||
auto shadows = std::make_unique<ShadowMapPipeline>();
|
||||
auto lines = std::make_unique<LineRenderingPass>();
|
||||
auto ui = std::make_unique<Renderer2DPass>();
|
||||
auto post = std::make_unique<PostProcessPipeline>();
|
||||
|
||||
try
|
||||
{
|
||||
mesh->init(gfxDevice, *resources, sceneColorFormat, depthImageFormat, selectionImageFormat);
|
||||
skybox->init(gfxDevice, *resources, sceneColorFormat, depthImageFormat);
|
||||
skinning->init(gfxDevice);
|
||||
lightCulling->init(gfxDevice, *resources, glm::ivec2{extent.width, extent.height});
|
||||
shadows->init(gfxDevice, *resources);
|
||||
lines->init(gfxDevice, drawImageFormat);
|
||||
ui->init(gfxDevice, *resources, drawImageFormat);
|
||||
post->init(gfxDevice, *resources, drawImageFormat);
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
spdlog::error("Shader reload failed, keeping the current pipelines: {}", exception.what());
|
||||
mesh->cleanup(device);
|
||||
skybox->cleanup(device);
|
||||
skinning->cleanup(gfxDevice);
|
||||
lightCulling->cleanup(gfxDevice);
|
||||
shadows->cleanup(device);
|
||||
lines->cleanup(gfxDevice);
|
||||
ui->cleanup(gfxDevice);
|
||||
post->cleanup(device);
|
||||
return false;
|
||||
}
|
||||
|
||||
mesh->setRenderWireframe(meshPipeline->getRenderWireframe());
|
||||
skybox->setSkyboxImage(skyboxImageId);
|
||||
shadows->setShadowDistance(shadowMapPipeline->getShadowDistance());
|
||||
|
||||
// Frames in flight still use the old pipelines, the deletion queue destroys them once those are done
|
||||
const auto retire = [&gfxDevice](auto& current, auto& replacement, auto destroy) {
|
||||
std::shared_ptr old{std::move(current)};
|
||||
gfxDevice.deferDestroy([old, destroy]() mutable { destroy(*old); });
|
||||
current = std::move(replacement);
|
||||
};
|
||||
retire(meshPipeline, mesh, [device](MeshPipeline& pipeline) { pipeline.cleanup(device); });
|
||||
retire(skyboxPipeline, skybox, [device](SkyboxPipeline& pipeline) { pipeline.cleanup(device); });
|
||||
retire(skinningPipeline, skinning, [&gfxDevice](SkinningPipeline& pipeline) { pipeline.cleanup(gfxDevice); });
|
||||
retire(lightCullingPipeline, lightCulling, [&gfxDevice](LightCullingPipeline& pipeline) { pipeline.cleanup(gfxDevice); });
|
||||
retire(shadowMapPipeline, shadows, [device](ShadowMapPipeline& pipeline) { pipeline.cleanup(device); });
|
||||
retire(lineRenderingPass, lines, [&gfxDevice](LineRenderingPass& pass) { pass.cleanup(gfxDevice); });
|
||||
retire(renderer2DPass, ui, [&gfxDevice](Renderer2DPass& pass) { pass.cleanup(gfxDevice); });
|
||||
retire(postProcessPipeline, post, [device](PostProcessPipeline& pipeline) { pipeline.cleanup(device); });
|
||||
|
||||
spdlog::info("Reloaded shaders");
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameRenderer::recordMaterialUpdates(VkCommandBuffer cmd)
|
||||
@@ -696,8 +802,7 @@ void GameRenderer::recordMaterialUpdates(VkCommandBuffer cmd)
|
||||
}
|
||||
|
||||
void GameRenderer::createDrawImage(GfxDevice& gfxDevice,
|
||||
const glm::ivec2& drawImageSize,
|
||||
bool firstCreate)
|
||||
const glm::ivec2& drawImageSize)
|
||||
{
|
||||
const VkExtent3D drawImageExtent{
|
||||
.width = (std::uint32_t)drawImageSize.x,
|
||||
@@ -708,29 +813,30 @@ void GameRenderer::createDrawImage(GfxDevice& gfxDevice,
|
||||
constexpr VkSampleCountFlagBits noMsaa = VK_SAMPLE_COUNT_1_BIT;
|
||||
|
||||
{
|
||||
// setup draw image (single-sampled)
|
||||
VkImageUsageFlags usages{};
|
||||
usages |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
usages |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
usages |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
usages |= VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
|
||||
auto createImageInfo = vkutil::CreateImageInfo{
|
||||
.format = drawImageFormat,
|
||||
.usage = usages,
|
||||
// HDR target of the forward pass, the post pass tonemaps it into the draw image
|
||||
const auto createInfo = vkutil::CreateImageInfo{
|
||||
.format = sceneColorFormat,
|
||||
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||
.extent = drawImageExtent,
|
||||
.samples = noMsaa,
|
||||
};
|
||||
|
||||
// a fresh bindless slot, the old one may still be read by frames in flight
|
||||
resources->destroyImage(drawImageId);
|
||||
drawImageId = resources->createImage(gfxDevice, createImageInfo, "draw image");
|
||||
resources->destroyImage(sceneColorImageId);
|
||||
sceneColorImageId = resources->createImage(gfxDevice, createInfo, "scene color");
|
||||
}
|
||||
|
||||
if (firstCreate)
|
||||
{
|
||||
// Optional: a separate post-fx target (ping-pong)
|
||||
// postFXDrawImageId = gfxDevice.createImage(createImageInfo, "post FX draw image");
|
||||
}
|
||||
{
|
||||
const auto createInfo = vkutil::CreateImageInfo{
|
||||
.format = drawImageFormat,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||
.extent = drawImageExtent,
|
||||
.samples = noMsaa,
|
||||
};
|
||||
|
||||
resources->destroyImage(drawImageId);
|
||||
drawImageId = resources->createImage(gfxDevice, createInfo, "draw image");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
#include <destrum/Graphics/ResourcesInspector.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <system_error>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <vulkan/vk_enum_string_helper.h>
|
||||
|
||||
#include <destrum/Graphics/Caches/ImageCache.h>
|
||||
#include <destrum/Graphics/Caches/MaterialCache.h>
|
||||
#include <destrum/Graphics/Pipelines/ImguiPass.h>
|
||||
#include <destrum/Graphics/RenderResources.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
[[nodiscard]] const char* StripPrefix(const char* name, const char* prefix)
|
||||
{
|
||||
const std::size_t length = std::strlen(prefix);
|
||||
return std::strncmp(name, prefix, length) == 0 ? name + length : name;
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* FormatName(VkFormat format)
|
||||
{
|
||||
return StripPrefix(string_VkFormat(format), "VK_FORMAT_");
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* LayoutName(VkImageLayout layout)
|
||||
{
|
||||
return StripPrefix(string_VkImageLayout(layout), "VK_IMAGE_LAYOUT_");
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string UsageNames(VkImageUsageFlags usage)
|
||||
{
|
||||
struct UsageName
|
||||
{
|
||||
VkImageUsageFlagBits bit;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr UsageName usageNames[] = {
|
||||
{VK_IMAGE_USAGE_SAMPLED_BIT, "sampled"},
|
||||
{VK_IMAGE_USAGE_STORAGE_BIT, "storage"},
|
||||
{VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, "color attachment"},
|
||||
{VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, "depth attachment"},
|
||||
{VK_IMAGE_USAGE_TRANSFER_SRC_BIT, "transfer src"},
|
||||
{VK_IMAGE_USAGE_TRANSFER_DST_BIT, "transfer dst"},
|
||||
};
|
||||
|
||||
std::string result;
|
||||
for (const UsageName& usageName : usageNames)
|
||||
{
|
||||
if ((usage & usageName.bit) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!result.empty())
|
||||
{
|
||||
result += ", ";
|
||||
}
|
||||
result += usageName.name;
|
||||
}
|
||||
return result.empty() ? "none" : result;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsDepthFormat(VkFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case VK_FORMAT_D16_UNORM:
|
||||
case VK_FORMAT_X8_D24_UNORM_PACK32:
|
||||
case VK_FORMAT_D32_SFLOAT:
|
||||
case VK_FORMAT_D16_UNORM_S8_UINT:
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||
case VK_FORMAT_D32_SFLOAT_S8_UINT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ContainsCaseInsensitive(std::string_view haystack, std::string_view needle)
|
||||
{
|
||||
const auto it = std::search(
|
||||
haystack.begin(), haystack.end(),
|
||||
needle.begin(), needle.end(),
|
||||
[](char a, char b)
|
||||
{
|
||||
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
return it != haystack.end();
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* TextureIntentName(TextureIntent intent)
|
||||
{
|
||||
switch (intent)
|
||||
{
|
||||
case TextureIntent::ColorSrgb: return "color (sRGB)";
|
||||
case TextureIntent::DataLinear: return "data (linear)";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
ResourcesInspector::PreviewInfo ResourcesInspector::getPreviewInfo(const GPUImage& image)
|
||||
{
|
||||
if (image.image == VK_NULL_HANDLE || image.imageView == VK_NULL_HANDLE)
|
||||
{
|
||||
return {false, "no image view"};
|
||||
}
|
||||
if ((image.usage & VK_IMAGE_USAGE_SAMPLED_BIT) == 0)
|
||||
{
|
||||
return {false, "not sampleable"};
|
||||
}
|
||||
if (image.isCubemap)
|
||||
{
|
||||
return {false, "cubemap"};
|
||||
}
|
||||
if (image.numLayers > 1)
|
||||
{
|
||||
return {false, "image array"};
|
||||
}
|
||||
|
||||
if (image.getLayout() == VK_IMAGE_LAYOUT_UNDEFINED)
|
||||
{
|
||||
return {false, "never written"};
|
||||
}
|
||||
|
||||
// ImGui reads the image when the UI gets rendered at the very end of the frame. Render targets
|
||||
// like the draw / depth image are in an attachment or transfer layout by then.
|
||||
if (image.getLayout() != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
||||
{
|
||||
return {false, "render target"};
|
||||
}
|
||||
|
||||
return {true, ""};
|
||||
}
|
||||
|
||||
void ResourcesInspector::drawImagePreview(const GPUImage& image, float maxSize, ImguiPass& imguiPass) const
|
||||
{
|
||||
const float width = static_cast<float>(image.extent.width);
|
||||
const float height = static_cast<float>(image.extent.height);
|
||||
const float scale = maxSize / std::max({width, height, 1.0f});
|
||||
const ImVec2 size{std::max(width * scale, 1.0f), std::max(height * scale, 1.0f)};
|
||||
|
||||
const PreviewInfo preview = getPreviewInfo(image);
|
||||
if (preview.previewable)
|
||||
{
|
||||
const ImTextureID textureId = imguiPass.getFrameTexture(image.imageView);
|
||||
if (textureId != ImTextureID{})
|
||||
{
|
||||
ImGui::ImageWithBg(textureId, size, ImVec2{0.0f, 0.0f}, ImVec2{1.0f, 1.0f}, ImVec4{0.1f, 0.1f, 0.1f, 1.0f});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// placeholder box with the reason it can't be shown
|
||||
const ImVec2 boxSize{maxSize, std::min(maxSize, std::max(size.y, ImGui::GetTextLineHeight() * 2.0f))};
|
||||
const ImVec2 min = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 max{min.x + boxSize.x, min.y + boxSize.y};
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
drawList->AddRectFilled(min, max, ImGui::GetColorU32(ImGuiCol_FrameBg));
|
||||
drawList->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border));
|
||||
|
||||
// small thumbnails just get a dash, the reason is in the tooltip
|
||||
const char* text = ImGui::CalcTextSize(preview.reason).x <= boxSize.x - 8.0f ? preview.reason : "-";
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(text);
|
||||
drawList->AddText(
|
||||
ImVec2{min.x + (boxSize.x - textSize.x) * 0.5f, min.y + (boxSize.y - textSize.y) * 0.5f},
|
||||
ImGui::GetColorU32(ImGuiCol_TextDisabled),
|
||||
text);
|
||||
|
||||
ImGui::Dummy(boxSize);
|
||||
}
|
||||
|
||||
void ResourcesInspector::drawImageDetails(ImageID id, const RenderResources& resources) const
|
||||
{
|
||||
const ImageCache& cache = resources.images();
|
||||
const GPUImage& image = cache.images[id];
|
||||
|
||||
ImGui::Text("Image ID: %u", static_cast<unsigned>(id));
|
||||
ImGui::TextWrapped("Name: %s", image.debugName.empty() ? "<unnamed>" : image.debugName.c_str());
|
||||
|
||||
if (id == resources.getWhiteTextureID())
|
||||
{
|
||||
ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default white (also metallic/roughness + emissive default)");
|
||||
}
|
||||
if (id == resources.getErrorTextureID())
|
||||
{
|
||||
ImGui::TextColored(ImVec4{1.0f, 0.4f, 0.8f, 1.0f}, "Error texture (used when a load fails)");
|
||||
}
|
||||
if (id == resources.getDefaultNormalTextureID())
|
||||
{
|
||||
ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default normal map");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Size: %u x %u", image.extent.width, image.extent.height);
|
||||
ImGui::Text("Format: %s%s", FormatName(image.format), IsDepthFormat(image.format) ? " (shown as red)" : "");
|
||||
ImGui::Text("Mips: %u Layers: %u%s", image.mipLevels, image.numLayers, image.isCubemap ? " cubemap" : "");
|
||||
ImGui::Text("Layout: %s", LayoutName(image.getLayout()));
|
||||
if (const PreviewInfo preview = getPreviewInfo(image); !preview.previewable)
|
||||
{
|
||||
ImGui::TextDisabled("No preview: %s", preview.reason);
|
||||
}
|
||||
ImGui::TextWrapped("Usage: %s", UsageNames(image.usage).c_str());
|
||||
|
||||
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("File: %s", it->second.path.generic_string().c_str());
|
||||
ImGui::Text("Intent: %s%s", TextureIntentName(it->second.intent), it->second.mipMap ? ", mipmapped" : "");
|
||||
}
|
||||
|
||||
const MaterialCache& materials = resources.materials();
|
||||
std::vector<std::size_t> users;
|
||||
for (std::size_t materialId = 0; materialId < materials.materials.size(); ++materialId)
|
||||
{
|
||||
if (materials.materials[materialId].UsesTexture(id))
|
||||
{
|
||||
users.push_back(materialId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!users.empty())
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Used by %zu material(s):", users.size());
|
||||
|
||||
constexpr std::size_t maxListed = 8;
|
||||
for (std::size_t i = 0; i < std::min(users.size(), maxListed); ++i)
|
||||
{
|
||||
const Material& material = materials.materials[users[i]];
|
||||
ImGui::BulletText(
|
||||
"#%zu %s",
|
||||
users[i],
|
||||
material.name.empty() ? "<unnamed>" : material.name.c_str());
|
||||
}
|
||||
if (users.size() > maxListed)
|
||||
{
|
||||
ImGui::TextDisabled("... and %zu more", users.size() - maxListed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourcesInspector::matchesFilter(ImageID id, const RenderResources& resources) const
|
||||
{
|
||||
const std::string_view filterText{filter};
|
||||
if (filterText.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// a plain number matches the image id exactly
|
||||
unsigned filterId = 0;
|
||||
const auto [end, error] = std::from_chars(filterText.data(), filterText.data() + filterText.size(), filterId);
|
||||
if (error == std::errc{} && end == filterText.data() + filterText.size())
|
||||
{
|
||||
return filterId == id;
|
||||
}
|
||||
|
||||
const ImageCache& cache = resources.images();
|
||||
if (ContainsCaseInsensitive(cache.images[id].debugName, filterText))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
|
||||
{
|
||||
return ContainsCaseInsensitive(it->second.path.generic_string(), filterText);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ResourcesInspector::drawImageCacheWindow(bool* open, const RenderResources& resources, ImguiPass& imguiPass)
|
||||
{
|
||||
if (!ImGui::Begin("Image Cache", open))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resources.isInitialized())
|
||||
{
|
||||
ImGui::TextDisabled("Render resources aren't initialized yet");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const ImageCache& cache = resources.images();
|
||||
|
||||
std::vector<ImageID> visibleIds;
|
||||
std::size_t imageCount = 0;
|
||||
for (std::size_t i = 0; i < cache.images.size(); ++i)
|
||||
{
|
||||
const auto id = static_cast<ImageID>(i);
|
||||
const GPUImage& image = cache.images[i];
|
||||
if (image.image == VK_NULL_HANDLE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
++imageCount;
|
||||
if (!matchesFilter(id, resources))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (onlyPreviewable && !getPreviewInfo(image).previewable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
visibleIds.push_back(id);
|
||||
}
|
||||
|
||||
ImGui::Text("%zu images (bindless capacity %u)", imageCount, cache.getMaxImageCount());
|
||||
|
||||
ImGui::SetNextItemWidth(220.0f);
|
||||
ImGui::InputTextWithHint("##filter", "Filter by name, path or ID", filter, sizeof(filter));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
ImGui::SliderFloat("Thumbnail", &thumbnailSize, 24.0f, 256.0f, "%.0f px");
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Previewable only", &onlyPreviewable);
|
||||
|
||||
if (selectedImageId != NULL_IMAGE_ID &&
|
||||
(selectedImageId >= cache.images.size() || cache.images[selectedImageId].image == VK_NULL_HANDLE))
|
||||
{
|
||||
selectedImageId = NULL_IMAGE_ID;
|
||||
}
|
||||
|
||||
if (selectedImageId != NULL_IMAGE_ID)
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Selected Image", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
drawImagePreview(cache.images[selectedImageId], 256.0f, imguiPass);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
drawImageDetails(selectedImageId, resources);
|
||||
if (ImGui::SmallButton("Clear selection"))
|
||||
{
|
||||
selectedImageId = NULL_IMAGE_ID;
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
}
|
||||
|
||||
constexpr ImGuiTableFlags tableFlags =
|
||||
ImGuiTableFlags_Borders |
|
||||
ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_Resizable |
|
||||
ImGuiTableFlags_SizingStretchProp;
|
||||
|
||||
if (ImGui::BeginTable("##images", 6, tableFlags))
|
||||
{
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
||||
ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthFixed, thumbnailSize + 4.0f);
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 3.0f);
|
||||
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
ImGui::TableSetupColumn("Format", ImGuiTableColumnFlags_WidthStretch, 1.5f);
|
||||
ImGui::TableSetupColumn("Layout", ImGuiTableColumnFlags_WidthStretch, 1.5f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// only the visible rows get a texture descriptor
|
||||
const float rowHeight = thumbnailSize + ImGui::GetStyle().CellPadding.y * 2.0f;
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(static_cast<int>(visibleIds.size()), rowHeight);
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
||||
{
|
||||
const ImageID id = visibleIds[static_cast<std::size_t>(row)];
|
||||
const GPUImage& image = cache.images[id];
|
||||
|
||||
ImGui::PushID(static_cast<int>(id));
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_None, thumbnailSize);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
const std::string idLabel = std::to_string(id);
|
||||
if (ImGui::Selectable(
|
||||
idLabel.c_str(),
|
||||
selectedImageId == id,
|
||||
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap,
|
||||
ImVec2{0.0f, thumbnailSize}))
|
||||
{
|
||||
selectedImageId = selectedImageId == id ? NULL_IMAGE_ID : id;
|
||||
}
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
drawImagePreview(image, 256.0f, imguiPass);
|
||||
drawImageDetails(id, resources);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
drawImagePreview(image, thumbnailSize, imguiPass);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
|
||||
{
|
||||
// loaded images are named after their full path, the file name is enough here
|
||||
ImGui::TextWrapped("%s", it->second.path.filename().generic_string().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextWrapped("%s", image.debugName.empty() ? "<unnamed>" : image.debugName.c_str());
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%u x %u", image.extent.width, image.extent.height);
|
||||
if (image.mipLevels > 1)
|
||||
{
|
||||
ImGui::TextDisabled("%u mips", image.mipLevels);
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(FormatName(image.format));
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(LayoutName(image.getLayout()));
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <destrum/Scene/ResourceCollector.h>
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
#include <destrum/Graphics/RenderResources.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
|
||||
void CollectUnusedResources(SceneManager& sceneManager, RenderResources& resources, GfxDevice& gfxDevice)
|
||||
{
|
||||
std::unordered_set<MeshID> usedMeshes;
|
||||
std::unordered_set<MaterialID> usedMaterials;
|
||||
|
||||
for (const auto& scene : sceneManager.GetScenes()) {
|
||||
for (const auto* objects : {&scene->GetObjects(), &scene->GetPendingAdditions()}) {
|
||||
for (const auto& object : *objects) {
|
||||
if (object == nullptr || object->IsBeingDestroyed()) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& component : object->GetComponents()) {
|
||||
if (const auto* meshRenderer = dynamic_cast<const MeshRendererComponent*>(component.get())) {
|
||||
usedMeshes.insert(meshRenderer->GetMeshID());
|
||||
usedMaterials.insert(meshRenderer->GetMaterialID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto stats = resources.releaseUnused(gfxDevice, usedMeshes, usedMaterials);
|
||||
if (stats.meshes + stats.materials + stats.images > 0) {
|
||||
spdlog::info("Released {} meshes, {} materials and {} textures nothing uses any more",
|
||||
stats.meshes, stats.materials, stats.images);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
@@ -150,6 +151,14 @@ void Scene::DrawSettingsImGui() {
|
||||
ImGui::ColorEdit3("Fog color", &environment.fogColor.x, ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float);
|
||||
ImGui::DragFloat("Fog density", &environment.fogDensity, 0.001f, 0.0f, 1.0f, "%.4f");
|
||||
|
||||
ImGui::SeparatorText("Camera");
|
||||
ImGui::DragFloat("Exposure (EV)", &environment.exposure, 0.05f, -10.0f, 10.0f, "%.2f");
|
||||
constexpr std::array<const char*, 3> tonemappers{"None", "ACES", "Khronos PBR Neutral"};
|
||||
int tonemapper = static_cast<int>(environment.tonemapper);
|
||||
if (ImGui::Combo("Tonemapper", &tonemapper, tonemappers.data(), static_cast<int>(tonemappers.size()))) {
|
||||
environment.tonemapper = static_cast<Tonemapper>(tonemapper);
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Skybox");
|
||||
float speedDegrees = glm::degrees(environment.skyboxRotationSpeed);
|
||||
if (ImGui::DragFloat("Spin (deg/s)", &speedDegrees, 0.05f)) {
|
||||
@@ -236,9 +245,6 @@ void Scene::SetCurrentCamera(const CameraComponent* camera) {
|
||||
m_CurrentCameraId = camera != nullptr ? camera->GetGameObject()->GetId() : InvalidObjectId;
|
||||
}
|
||||
|
||||
void Scene::RenderImgui(bool* open) {
|
||||
m_Inspector.Draw(*this, m_SelectedObjectId, [this] { DrawSettingsImGui(); }, open);
|
||||
}
|
||||
|
||||
bool Scene::MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore) {
|
||||
if (IsIterating() || object.IsBeingDestroyed() || object.GetScene() != this || parent == &object) {
|
||||
|
||||
@@ -48,12 +48,6 @@ void SceneManager::Render(const RenderContext& ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
void SceneManager::RenderImgui(bool* open) {
|
||||
for (const auto& scene : m_activeScenes) {
|
||||
scene->RenderImgui(open);
|
||||
}
|
||||
}
|
||||
|
||||
void SceneManager::HandleGameObjectDestroy() {
|
||||
for (const auto& scene : m_scenes) {
|
||||
scene->CleanupDestroyedGameObjects();
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
#include <destrum/Serialization/MaterialSerialization.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/Assets/AssetDatabase.h>
|
||||
#include <destrum/Assets/AssetTypes.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/GfxDevice.h>
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
@@ -26,31 +31,76 @@ namespace {
|
||||
{"emissive", &Material::emissiveTexture},
|
||||
}};
|
||||
|
||||
[[nodiscard]] const char* FilteringName(TextureFilteringMode mode) {
|
||||
switch (mode) {
|
||||
case TextureFilteringMode::Nearest: return "nearest";
|
||||
case TextureFilteringMode::Anisotropic: return "anisotropic";
|
||||
case TextureFilteringMode::Linear: break;
|
||||
}
|
||||
return "linear";
|
||||
}
|
||||
constexpr std::array<std::string_view, 3> FilteringNames{"nearest", "linear", "anisotropic"};
|
||||
|
||||
[[nodiscard]] TextureFilteringMode ParseFiltering(const std::string& name) {
|
||||
if (name == "nearest") return TextureFilteringMode::Nearest;
|
||||
if (name == "anisotropic") return TextureFilteringMode::Anisotropic;
|
||||
[[nodiscard]] TextureFilteringMode ParseFiltering(std::string_view name)
|
||||
{
|
||||
for (std::size_t i = 0; i < FilteringNames.size(); ++i) {
|
||||
if (FilteringNames[i] == name) {
|
||||
return static_cast<TextureFilteringMode>(i);
|
||||
}
|
||||
}
|
||||
return TextureFilteringMode::Linear;
|
||||
}
|
||||
|
||||
[[nodiscard]] RenderResources* GetResources() {
|
||||
if (!GameState::GetInstance().HasRenderer() || !GameState::GetInstance().HasGfxDevice()) {
|
||||
[[nodiscard]] RenderResources* GetResources()
|
||||
{
|
||||
auto& state = GameState::GetInstance();
|
||||
return state.HasRenderer() && state.HasGfxDevice() ? state.Renderer().getResources() : nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] json WriteTexture(const RenderResources& resources, ImageID id)
|
||||
{
|
||||
const ImageCache::LoadedImageInfo* info = resources.images().findLoadedImageInfo(id);
|
||||
if (info == nullptr) {
|
||||
spdlog::warn("Texture {} was not loaded from a file, it is not saved", id);
|
||||
return nullptr;
|
||||
}
|
||||
return GameState::GetInstance().Renderer().getResources();
|
||||
const auto virtualPath = AssetFS::GetInstance().ToVirtualPath(info->path);
|
||||
json texture = virtualPath ? AssetDatabase::GetInstance().WriteReference(*virtualPath)
|
||||
: json{{"path", info->path.generic_string()}};
|
||||
texture["srgb"] = info->intent == TextureIntent::ColorSrgb;
|
||||
return texture;
|
||||
}
|
||||
|
||||
[[nodiscard]] ImageID ReadTexture(RenderResources& resources, GfxDevice& gfxDevice, const json& texture)
|
||||
{
|
||||
const auto reference = AssetDatabase::GetInstance().ReadReference(texture);
|
||||
if (!reference) {
|
||||
return NULL_IMAGE_ID;
|
||||
}
|
||||
const TextureIntent intent = texture.value("srgb", true) ? TextureIntent::ColorSrgb : TextureIntent::DataLinear;
|
||||
try {
|
||||
const std::filesystem::path fullPath = reference->path.find("://") != std::string::npos
|
||||
? AssetFS::GetInstance().GetFullPath(reference->path)
|
||||
: std::filesystem::path{reference->path};
|
||||
return resources.loadMaterialTexture(gfxDevice, fullPath, intent);
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Material texture \"{}\" could not be loaded: {}", reference->path, exception.what());
|
||||
return resources.getErrorTextureID();
|
||||
}
|
||||
}
|
||||
|
||||
void VisitObjects(json& node, const std::function<void(json&)>& visit)
|
||||
{
|
||||
if (node.is_object()) {
|
||||
visit(node);
|
||||
for (auto& [key, value] : node.items()) {
|
||||
VisitObjects(value, visit);
|
||||
}
|
||||
} else if (node.is_array()) {
|
||||
for (auto& element : node) {
|
||||
VisitObjects(element, visit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::atomic<std::uint32_t> s_LoadCounter{0};
|
||||
}
|
||||
|
||||
namespace MaterialSerialization {
|
||||
json ToJson(const Material& material, const TextureWriter& writeTexture) {
|
||||
json ToJson(const Material& material, const TextureWriter& writeTexture)
|
||||
{
|
||||
json data = {
|
||||
{"name", material.name},
|
||||
{"baseColor", material.baseColor},
|
||||
@@ -59,7 +109,7 @@ namespace MaterialSerialization {
|
||||
{"emissiveColor", material.emissiveColor},
|
||||
{"normalScale", material.normalScale},
|
||||
{"occlusionStrength", material.occlusionStrength},
|
||||
{"filtering", FilteringName(material.textureFilteringMode)},
|
||||
{"filtering", FilteringNames.at(static_cast<std::size_t>(material.textureFilteringMode))},
|
||||
};
|
||||
|
||||
json textures = json::object();
|
||||
@@ -67,8 +117,7 @@ namespace MaterialSerialization {
|
||||
if (material.*member == NULL_IMAGE_ID) {
|
||||
continue;
|
||||
}
|
||||
json texture = writeTexture(material.*member);
|
||||
if (!texture.is_null()) {
|
||||
if (json texture = writeTexture(material.*member); !texture.is_null()) {
|
||||
textures[slot] = std::move(texture);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +127,8 @@ namespace MaterialSerialization {
|
||||
return data;
|
||||
}
|
||||
|
||||
Material FromJson(const json& data, const TextureReader& readTexture) {
|
||||
Material FromJson(const json& data, const TextureReader& readTexture)
|
||||
{
|
||||
Material material{};
|
||||
material.name = data.value("name", std::string{});
|
||||
material.baseColor = data.value("baseColor", material.baseColor);
|
||||
@@ -100,7 +150,8 @@ namespace MaterialSerialization {
|
||||
return material;
|
||||
}
|
||||
|
||||
std::optional<std::string> Validate(const json& materials) {
|
||||
std::optional<std::string> Validate(const json& materials)
|
||||
{
|
||||
if (!materials.is_object()) {
|
||||
return "\"materials\" must be an object";
|
||||
}
|
||||
@@ -126,7 +177,48 @@ namespace MaterialSerialization {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
json CollectEmbedded(const json& objects) {
|
||||
bool IsFileKey(std::string_view key)
|
||||
{
|
||||
return AssetTypes::IsMaterial(key);
|
||||
}
|
||||
|
||||
json ToFile(const Material& material, const RenderResources* resources)
|
||||
{
|
||||
json data = ToJson(material, [resources](ImageID id) -> json {
|
||||
return resources != nullptr ? WriteTexture(*resources, id) : json{};
|
||||
});
|
||||
data["version"] = CurrentFileVersion;
|
||||
return data;
|
||||
}
|
||||
|
||||
MaterialID LoadFile(RenderResources& resources, GfxDevice& gfxDevice, std::string_view path)
|
||||
{
|
||||
if (const auto cached = resources.materials().findMaterialByKey(path)) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
const auto bytes = AssetFS::GetInstance().ReadBytes(path);
|
||||
const json data = json::parse(bytes.begin(), bytes.end());
|
||||
if (const int version = data.value("version", 0); version < 1 || version > CurrentFileVersion) {
|
||||
throw std::runtime_error("unsupported material file version " + std::to_string(version));
|
||||
}
|
||||
|
||||
Material material = FromJson(data, [&](const json& texture) { return ReadTexture(resources, gfxDevice, texture); });
|
||||
material.assetReference = AssetReference{.path = std::string{path}};
|
||||
if (material.name.empty()) {
|
||||
material.name = std::filesystem::path{path}.stem().string();
|
||||
}
|
||||
return resources.materials().addMaterial(std::move(material), std::string{path});
|
||||
}
|
||||
|
||||
bool SaveFile(const RenderResources& resources, MaterialID id, std::string_view path)
|
||||
{
|
||||
const AssetDatabase::MetaFileScope createMetaFiles;
|
||||
return AssetFS::GetInstance().WriteFile(path, ToFile(resources.materials().getMaterial(id), &resources).dump(4));
|
||||
}
|
||||
|
||||
json EmbedLocalMaterials(json& objects)
|
||||
{
|
||||
json materials = json::object();
|
||||
const RenderResources* resources = GetResources();
|
||||
if (resources == nullptr) {
|
||||
@@ -134,77 +226,59 @@ namespace MaterialSerialization {
|
||||
}
|
||||
const MaterialCache& cache = resources->materials();
|
||||
|
||||
const auto writeTexture = [&](ImageID id) -> json {
|
||||
const ImageCache::LoadedImageInfo* info = resources->images().findLoadedImageInfo(id);
|
||||
if (info == nullptr) {
|
||||
spdlog::warn("Texture {} was not loaded from a file, it is not saved", id);
|
||||
return nullptr;
|
||||
std::unordered_map<std::string, std::string> localIds;
|
||||
VisitObjects(objects, [&](json& node) {
|
||||
const auto it = node.find("runtimeMaterial");
|
||||
if (it == node.end() || !it->is_string()) {
|
||||
return;
|
||||
}
|
||||
const std::string key = it->get<std::string>();
|
||||
const auto id = cache.findMaterialByKey(key);
|
||||
if (!id) {
|
||||
spdlog::warn("Material \"{}\" no longer exists, the object loses it", key);
|
||||
node = nullptr;
|
||||
return;
|
||||
}
|
||||
const auto virtualPath = AssetFS::GetInstance().ToVirtualPath(info->path);
|
||||
return {
|
||||
{"path", virtualPath ? *virtualPath : info->path.generic_string()},
|
||||
{"srgb", info->intent == TextureIntent::ColorSrgb},
|
||||
{"mipMap", info->mipMap},
|
||||
};
|
||||
};
|
||||
|
||||
for (const json& objectJson : objects) {
|
||||
if (!objectJson.contains("components")) {
|
||||
continue;
|
||||
auto [local, inserted] = localIds.try_emplace(key, std::to_string(localIds.size() + 1));
|
||||
if (inserted) {
|
||||
materials[local->second] = ToJson(cache.getMaterial(*id), [resources](ImageID texture) {
|
||||
return WriteTexture(*resources, texture);
|
||||
});
|
||||
}
|
||||
for (const json& componentJson : objectJson.at("components")) {
|
||||
if (!componentJson.contains("data") || !componentJson.at("data").contains("materialKey")) {
|
||||
continue;
|
||||
}
|
||||
const json& keyJson = componentJson.at("data").at("materialKey");
|
||||
if (!keyJson.is_string() || materials.contains(keyJson.get<std::string>())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string key = keyJson.get<std::string>();
|
||||
const auto id = cache.findMaterialByKey(key);
|
||||
if (!id || *id == cache.getPlaceholderMaterialId() ||
|
||||
!cache.getMaterial(*id).assetReference.empty()) {
|
||||
continue;
|
||||
}
|
||||
materials[key] = ToJson(cache.getMaterial(*id), writeTexture);
|
||||
}
|
||||
}
|
||||
node = json{{"localMaterial", local->second}};
|
||||
});
|
||||
return materials;
|
||||
}
|
||||
|
||||
void RegisterEmbedded(const json& materials, bool overwriteExisting) {
|
||||
void LoadLocalMaterials(const json& materials, json& objects)
|
||||
{
|
||||
RenderResources* resources = GetResources();
|
||||
if (resources == nullptr || !materials.is_object()) {
|
||||
return;
|
||||
}
|
||||
GfxDevice& gfxDevice = GameState::GetInstance().Gfx();
|
||||
|
||||
const auto readTexture = [&](const json& texture) -> ImageID {
|
||||
const std::string path = texture.at("path").get<std::string>();
|
||||
const TextureIntent intent = texture.value("srgb", true)
|
||||
? TextureIntent::ColorSrgb
|
||||
: TextureIntent::DataLinear;
|
||||
try {
|
||||
const std::filesystem::path fullPath = path.find("://") != std::string::npos
|
||||
? AssetFS::GetInstance().GetFullPath(path)
|
||||
: std::filesystem::path{path};
|
||||
return resources->loadImageFromFile(
|
||||
gfxDevice, fullPath, VK_IMAGE_USAGE_SAMPLED_BIT, texture.value("mipMap", true), intent);
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Material texture \"{}\" could not be loaded: {}", path, exception.what());
|
||||
return resources->getErrorTextureID();
|
||||
}
|
||||
};
|
||||
|
||||
for (auto it = materials.begin(); it != materials.end(); ++it) {
|
||||
const auto existing = resources->materials().findMaterialByKey(it.key());
|
||||
if (!existing) {
|
||||
resources->materials().addMaterial(FromJson(it.value(), readTexture), it.key());
|
||||
} else if (overwriteExisting) {
|
||||
resources->materials().getMaterialMutable(*existing) = FromJson(it.value(), readTexture);
|
||||
GameState::GetInstance().Renderer().updateMaterialGPU(*existing);
|
||||
}
|
||||
// A fresh key per load keeps two scenes, or two loads of one scene, from sharing a local material
|
||||
const std::string prefix = "local:" + std::to_string(++s_LoadCounter) + ":";
|
||||
std::unordered_map<std::string, std::string> keys;
|
||||
for (const auto& [local, data] : materials.items()) {
|
||||
const std::string key = prefix + local;
|
||||
Material material = FromJson(data, [&](const json& texture) { return ReadTexture(*resources, gfxDevice, texture); });
|
||||
resources->materials().addMaterial(std::move(material), key);
|
||||
keys.emplace(local, key);
|
||||
}
|
||||
|
||||
VisitObjects(objects, [&](json& node) {
|
||||
if (const auto it = node.find("localMaterial"); it != node.end() && it->is_string()) {
|
||||
const auto key = keys.find(it->get<std::string>());
|
||||
node = key != keys.end() ? json{{"runtimeMaterial", key->second}} : json{};
|
||||
} else if (const auto legacy = node.find("materialKey"); legacy != node.end() && legacy->is_string()) {
|
||||
// Scenes saved before local ids keyed their embedded materials by the material's cache key
|
||||
if (const auto key = keys.find(legacy->get<std::string>()); key != keys.end()) {
|
||||
*legacy = key->second;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/Assets/AssetDatabase.h>
|
||||
#include <destrum/Assets/AssetReference.h>
|
||||
#include <destrum/Components/Physics/Collider.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
@@ -309,10 +310,9 @@ namespace ObjectSerialization {
|
||||
// Generic: any nested object with a "path" field is an AssetReference.
|
||||
std::function<void(const json&)> walk = [&](const json& node) {
|
||||
if (node.is_object()) {
|
||||
if (node.contains("path") && node.at("path").is_string()) {
|
||||
const std::string path = node.at("path").get<std::string>();
|
||||
if (!path.empty() && !AssetFileExists(path)) {
|
||||
report(objectName, path);
|
||||
if (const auto reference = AssetDatabase::GetInstance().ReadReference(node)) {
|
||||
if (!reference->path.empty() && !AssetFileExists(reference->path)) {
|
||||
report(objectName, reference->path);
|
||||
}
|
||||
}
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
|
||||
@@ -34,10 +34,11 @@ namespace {
|
||||
}
|
||||
|
||||
try {
|
||||
json objects = prefab.at("objects");
|
||||
if (prefab.contains("materials")) {
|
||||
MaterialSerialization::RegisterEmbedded(prefab.at("materials"));
|
||||
MaterialSerialization::LoadLocalMaterials(prefab.at("materials"), objects);
|
||||
}
|
||||
auto built = ObjectSerialization::BuildObjects(scene, prefab.at("objects"), externalObjects);
|
||||
auto built = ObjectSerialization::BuildObjects(scene, objects, externalObjects);
|
||||
GameObject* root = built.objects.front();
|
||||
if (parent != nullptr) {
|
||||
root->GetTransform().SetParent(parent, false);
|
||||
@@ -62,7 +63,7 @@ std::optional<json> PrefabSerializer::Capture(const GameObject& root) {
|
||||
prefab["name"] = root.GetName();
|
||||
try {
|
||||
prefab["objects"] = ObjectSerialization::SerializeObjects(ObjectSerialization::CollectHierarchy(root));
|
||||
if (json materials = MaterialSerialization::CollectEmbedded(prefab["objects"]); !materials.empty()) {
|
||||
if (json materials = MaterialSerialization::EmbedLocalMaterials(prefab["objects"]); !materials.empty()) {
|
||||
prefab["materials"] = std::move(materials);
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
|
||||
@@ -93,6 +93,8 @@ std::optional<json> SceneSerializer::Capture(Scene& scene) {
|
||||
{"fogDensity", environment.fogDensity},
|
||||
{"skyboxRotationSpeed", environment.skyboxRotationSpeed},
|
||||
{"skyboxRotation", environment.skyboxRotation},
|
||||
{"exposure", environment.exposure},
|
||||
{"tonemapper", ToString(environment.tonemapper)},
|
||||
};
|
||||
|
||||
std::vector<const GameObject*> objects;
|
||||
@@ -105,7 +107,7 @@ std::optional<json> SceneSerializer::Capture(Scene& scene) {
|
||||
|
||||
try {
|
||||
root["objects"] = ObjectSerialization::SerializeObjects(objects);
|
||||
if (json materials = MaterialSerialization::CollectEmbedded(root["objects"]); !materials.empty()) {
|
||||
if (json materials = MaterialSerialization::EmbedLocalMaterials(root["objects"]); !materials.empty()) {
|
||||
root["materials"] = std::move(materials);
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
@@ -205,12 +207,13 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
std::string context = "scene";
|
||||
|
||||
try {
|
||||
json objects = root.at("objects");
|
||||
if (root.contains("materials")) {
|
||||
context = "materials";
|
||||
MaterialSerialization::RegisterEmbedded(root.at("materials"), true);
|
||||
MaterialSerialization::LoadLocalMaterials(root.at("materials"), objects);
|
||||
context = "scene";
|
||||
}
|
||||
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
|
||||
idMap = ObjectSerialization::BuildObjects(stagingScene, objects).idMap;
|
||||
|
||||
// Move objects from pending to objects list without physics registration
|
||||
// (staging scene has no physics world).
|
||||
@@ -284,6 +287,8 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
environment.fogDensity = data.value("fogDensity", environment.fogDensity);
|
||||
environment.skyboxRotationSpeed = data.value("skyboxRotationSpeed", environment.skyboxRotationSpeed);
|
||||
environment.skyboxRotation = data.value("skyboxRotation", environment.skyboxRotation);
|
||||
environment.exposure = data.value("exposure", environment.exposure);
|
||||
environment.tonemapper = ParseTonemapper(data.value("tonemapper", std::string{})).value_or(environment.tonemapper);
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Scene \"{}\" has invalid environment settings, using defaults: {}",
|
||||
scene.m_name, exception.what());
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
#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);
|
||||
});
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
#include <destrum/Util/RectGizmo.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#include <destrum/Components/UI/Canvas.h>
|
||||
#include <destrum/Components/UI/RectTransform.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace {
|
||||
constexpr int MoveHandle = 0;
|
||||
constexpr float HandleRadius = 5.0f;
|
||||
constexpr std::array<glm::vec2, 8> HandleDirections{{
|
||||
{-1.0f, -1.0f}, {0.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 0.0f},
|
||||
{1.0f, 1.0f}, {0.0f, 1.0f}, {-1.0f, 1.0f}, {-1.0f, 0.0f},
|
||||
}};
|
||||
|
||||
constexpr ImU32 OutlineColor = IM_COL32(60, 200, 255, 255);
|
||||
constexpr ImU32 ActiveColor = IM_COL32(255, 200, 60, 255);
|
||||
constexpr ImU32 HiddenColor = IM_COL32(150, 150, 150, 200);
|
||||
constexpr ImU32 AnchorColor = IM_COL32(255, 255, 255, 200);
|
||||
|
||||
[[nodiscard]] ImVec2 ToImVec(const glm::vec2& value) { return ImVec2{value.x, value.y}; }
|
||||
[[nodiscard]] glm::vec2 ToVec(const ImVec2& value) { return glm::vec2{value.x, value.y}; }
|
||||
|
||||
[[nodiscard]] ImGuiMouseCursor CursorFor(int handle)
|
||||
{
|
||||
if (handle == MoveHandle) {
|
||||
return ImGuiMouseCursor_ResizeAll;
|
||||
}
|
||||
const glm::vec2 direction = HandleDirections[static_cast<std::size_t>(handle - 1)];
|
||||
if (direction.x == 0.0f) {
|
||||
return ImGuiMouseCursor_ResizeNS;
|
||||
}
|
||||
if (direction.y == 0.0f) {
|
||||
return ImGuiMouseCursor_ResizeEW;
|
||||
}
|
||||
return direction.x == direction.y ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_ResizeNESW;
|
||||
}
|
||||
|
||||
void DrawAnchor(ImDrawList& drawList, const glm::vec2& point, const glm::vec2& outwards)
|
||||
{
|
||||
constexpr float size = 8.0f;
|
||||
const glm::vec2 tip = point;
|
||||
const glm::vec2 a = point + glm::vec2{outwards.x * size, outwards.y * size * 0.4f};
|
||||
const glm::vec2 b = point + glm::vec2{outwards.x * size * 0.4f, outwards.y * size};
|
||||
drawList.AddTriangleFilled(ToImVec(tip), ToImVec(a), ToImVec(b), AnchorColor);
|
||||
}
|
||||
}
|
||||
|
||||
bool RectGizmo::CanManipulate(GameObject& object)
|
||||
{
|
||||
return object.GetComponent<RectTransform>() != nullptr && object.GetComponent<Canvas>() == nullptr &&
|
||||
Canvas::FindFor(object) != nullptr;
|
||||
}
|
||||
|
||||
bool RectGizmo::Manipulate(GameObject& object, const glm::vec2& drawImageSize, const glm::vec2& viewMin, const glm::vec2& viewSize)
|
||||
{
|
||||
m_Hovered = false;
|
||||
auto* rectTransform = object.GetComponent<RectTransform>();
|
||||
const Canvas* canvas = Canvas::FindFor(object);
|
||||
if (!CanManipulate(object) || drawImageSize.x <= 0.0f || drawImageSize.y <= 0.0f) {
|
||||
m_ActiveHandle = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
const glm::vec2 viewScale = viewSize / drawImageSize;
|
||||
const auto toView = [&](const glm::vec2& point) { return viewMin + point * viewScale; };
|
||||
|
||||
const UIRect& rect = rectTransform->GetScreenRect();
|
||||
const glm::vec2 rectMin = toView(rect.min);
|
||||
const glm::vec2 rectMax = toView(rect.Max());
|
||||
const glm::vec2 viewCenter = (rectMin + rectMax) * 0.5f;
|
||||
const glm::vec2 viewHalfSize = (rectMax - rectMin) * 0.5f;
|
||||
|
||||
const glm::vec2 mouse = ToVec(ImGui::GetIO().MousePos);
|
||||
int hovered = -1;
|
||||
// Called inside the window that shows the scene, anything on top of it blocks the handles
|
||||
const bool overImGui = !ImGui::IsWindowHovered() || ImGui::IsAnyItemActive();
|
||||
if (m_ActiveHandle < 0 && !overImGui) {
|
||||
for (std::size_t i = 0; i < HandleDirections.size(); ++i) {
|
||||
const glm::vec2 handle = viewCenter + HandleDirections[i] * viewHalfSize;
|
||||
if (glm::length(mouse - handle) <= HandleRadius + 2.0f) {
|
||||
hovered = static_cast<int>(i) + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hovered < 0 && mouse.x >= rectMin.x && mouse.y >= rectMin.y && mouse.x <= rectMax.x && mouse.y <= rectMax.y) {
|
||||
hovered = MoveHandle;
|
||||
}
|
||||
if (hovered >= 0 && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
m_ActiveHandle = hovered;
|
||||
m_DragStartMouse = mouse;
|
||||
m_DragStartPosition = rectTransform->GetPosition();
|
||||
m_DragStartSize = rectTransform->GetSize();
|
||||
}
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
if (m_ActiveHandle >= 0) {
|
||||
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
m_ActiveHandle = -1;
|
||||
} else {
|
||||
const float canvasScale = canvas->GetScale(drawImageSize);
|
||||
glm::vec2 delta = (mouse - m_DragStartMouse) / (viewScale * canvasScale);
|
||||
if (snap && snapStep > 0.0f) {
|
||||
delta = glm::round(delta / snapStep) * snapStep;
|
||||
}
|
||||
|
||||
glm::vec2 position = m_DragStartPosition;
|
||||
glm::vec2 size = m_DragStartSize;
|
||||
if (m_ActiveHandle == MoveHandle) {
|
||||
position += delta;
|
||||
} else {
|
||||
// The edge opposite the handle stays put, so the pivot moves by the pivot's share of the resize
|
||||
const glm::vec2 direction = HandleDirections[static_cast<std::size_t>(m_ActiveHandle - 1)];
|
||||
const glm::vec2& pivot = rectTransform->GetPivot();
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
if (direction[axis] == 0.0f) {
|
||||
continue;
|
||||
}
|
||||
size[axis] += direction[axis] * delta[axis];
|
||||
position[axis] += delta[axis] * (direction[axis] > 0.0f ? pivot[axis] : 1.0f - pivot[axis]);
|
||||
}
|
||||
}
|
||||
|
||||
if (position != rectTransform->GetPosition() || size != rectTransform->GetSize()) {
|
||||
rectTransform->SetPosition(position);
|
||||
rectTransform->SetSize(size);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int highlighted = m_ActiveHandle >= 0 ? m_ActiveHandle : hovered;
|
||||
m_Hovered = highlighted >= 0;
|
||||
if (highlighted >= 0) {
|
||||
ImGui::SetMouseCursor(CursorFor(highlighted));
|
||||
}
|
||||
|
||||
ImDrawList& drawList = *ImGui::GetWindowDrawList();
|
||||
const ImU32 outlineColor = object.IsActiveInHierarchy() ? OutlineColor : HiddenColor;
|
||||
|
||||
const UIRect& parentRect = rectTransform->GetParentRect();
|
||||
const glm::vec2 anchorMin = toView(parentRect.min + parentRect.size * rectTransform->GetAnchorMin());
|
||||
const glm::vec2 anchorMax = toView(parentRect.min + parentRect.size * rectTransform->GetAnchorMax());
|
||||
DrawAnchor(drawList, {anchorMin.x, anchorMin.y}, {-1.0f, -1.0f});
|
||||
DrawAnchor(drawList, {anchorMax.x, anchorMin.y}, {1.0f, -1.0f});
|
||||
DrawAnchor(drawList, {anchorMax.x, anchorMax.y}, {1.0f, 1.0f});
|
||||
DrawAnchor(drawList, {anchorMin.x, anchorMax.y}, {-1.0f, 1.0f});
|
||||
|
||||
drawList.AddRect(ToImVec(rectMin), ToImVec(rectMax), highlighted == MoveHandle ? ActiveColor : outlineColor, 0.0f, 0, 1.5f);
|
||||
|
||||
const glm::vec2 pivot = rectMin + (rectMax - rectMin) * rectTransform->GetPivot();
|
||||
drawList.AddCircle(ToImVec(pivot), 4.0f, outlineColor, 12, 1.5f);
|
||||
|
||||
for (std::size_t i = 0; i < HandleDirections.size(); ++i) {
|
||||
const glm::vec2 handle = viewCenter + HandleDirections[i] * viewHalfSize;
|
||||
const ImU32 color = highlighted == static_cast<int>(i) + 1 ? ActiveColor : outlineColor;
|
||||
drawList.AddRectFilled(ToImVec(handle - glm::vec2{HandleRadius}), ToImVec(handle + glm::vec2{HandleRadius}), color);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
@@ -1,748 +0,0 @@
|
||||
#include <destrum/Util/SceneInspector.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/Components/UI/Canvas.h>
|
||||
#include <destrum/Components/UI/RectTransform.h>
|
||||
#include <destrum/Components/UI/UIImage.h>
|
||||
#include <destrum/Components/UI/UIText.h>
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/ObjectModel/Transform.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Serialization/ComponentFactory.h>
|
||||
#include <destrum/Serialization/ComponentRegistry.h>
|
||||
#include <destrum/Serialization/PrefabSerializer.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] bool IsInspectable(const GameObject* object)
|
||||
{
|
||||
return object != nullptr && !object->IsBeingDestroyed();
|
||||
}
|
||||
|
||||
[[nodiscard]] GameObject* ParentOf(const GameObject& object)
|
||||
{
|
||||
const Transform* parent = object.GetTransform().GetParent();
|
||||
return parent != nullptr ? parent->GetOwner() : nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool HasInspectableChildren(const GameObject& object)
|
||||
{
|
||||
return std::ranges::any_of(object.GetTransform().GetChildren(), [](const Transform* child) {
|
||||
return child != nullptr && IsInspectable(child->GetOwner());
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<GameObject*> CollectObjects(const Scene& scene)
|
||||
{
|
||||
std::vector<GameObject*> objects;
|
||||
objects.reserve(scene.GetObjects().size() + scene.GetPendingAdditions().size());
|
||||
for (const auto* source : {&scene.GetObjects(), &scene.GetPendingAdditions()}) {
|
||||
for (const auto& object : *source) {
|
||||
if (IsInspectable(object.get())) {
|
||||
objects.push_back(object.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsRoot(const GameObject& object)
|
||||
{
|
||||
return !IsInspectable(ParentOf(object));
|
||||
}
|
||||
|
||||
[[nodiscard]] GameObject* NextSibling(const Scene& scene, const GameObject& object)
|
||||
{
|
||||
if (const Transform* parent = object.GetTransform().GetParent()) {
|
||||
const auto& siblings = parent->GetChildren();
|
||||
const auto it = std::ranges::find(siblings, &object.GetTransform());
|
||||
for (auto next = it == siblings.end() ? it : it + 1; next != siblings.end(); ++next) {
|
||||
if (IsInspectable((*next)->GetOwner())) {
|
||||
return (*next)->GetOwner();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto objects = CollectObjects(scene);
|
||||
const auto it = std::ranges::find(objects, &object);
|
||||
for (auto next = it == objects.end() ? it : it + 1; next != objects.end(); ++next) {
|
||||
if (IsRoot(**next)) {
|
||||
return *next;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ContainsIgnoreCase(std::string_view text, std::string_view query)
|
||||
{
|
||||
const auto it = std::search(text.begin(), text.end(), query.begin(), query.end(), [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
return it != text.end();
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* DisplayName(const GameObject& object)
|
||||
{
|
||||
return object.GetName().empty() ? "<unnamed>" : object.GetName().c_str();
|
||||
}
|
||||
|
||||
GameObject* CreateChild(Scene& scene, GameObject* parent, const std::string& name)
|
||||
{
|
||||
GameObject* object = scene.CreateGameObject(name);
|
||||
if (parent != nullptr) {
|
||||
object->GetTransform().SetParent(&parent->GetTransform(), false);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
GameObject* CreateUIChild(Scene& scene, GameObject* parent, const std::string& name)
|
||||
{
|
||||
if (parent == nullptr || Canvas::FindFor(*parent) == nullptr) {
|
||||
parent = CreateChild(scene, parent, "Canvas");
|
||||
parent->AddComponent<Canvas>();
|
||||
}
|
||||
GameObject* object = CreateChild(scene, parent, name);
|
||||
object->AddComponent<RectTransform>();
|
||||
return object;
|
||||
}
|
||||
|
||||
// Returns the created object so the caller can select and rename it
|
||||
GameObject* DrawCreateMenu(Scene& scene, GameObject* parent)
|
||||
{
|
||||
GameObject* created = nullptr;
|
||||
if (ImGui::MenuItem("Empty")) {
|
||||
created = CreateChild(scene, parent, "GameObject");
|
||||
}
|
||||
if (ImGui::BeginMenu("UI")) {
|
||||
if (ImGui::MenuItem("Canvas")) {
|
||||
created = CreateChild(scene, parent, "Canvas");
|
||||
created->AddComponent<Canvas>();
|
||||
}
|
||||
if (ImGui::MenuItem("Rect")) {
|
||||
created = CreateUIChild(scene, parent, "Rect");
|
||||
}
|
||||
if (ImGui::MenuItem("Image")) {
|
||||
created = CreateUIChild(scene, parent, "Image");
|
||||
created->AddComponent<UIImage>();
|
||||
}
|
||||
if (ImGui::MenuItem("Text")) {
|
||||
created = CreateUIChild(scene, parent, "Text");
|
||||
created->GetComponent<RectTransform>()->SetSize({300.0f, 60.0f});
|
||||
created->AddComponent<UIText>("Text", 32.0f);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
// Right aligned controls drawn over a collapsing header, x is from the start of the line
|
||||
[[nodiscard]] float RightAlignedX(float lineStartX, float lineWidth, float itemsWidth)
|
||||
{
|
||||
return lineStartX + lineWidth - itemsWidth;
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::Draw(Scene& scene, ObjectId& selectedObjectId, const std::function<void()>& drawSceneSettings, bool* open)
|
||||
{
|
||||
RegisterEngineComponents();
|
||||
|
||||
if (scene.FindGameObject(selectedObjectId) == nullptr) {
|
||||
selectedObjectId = InvalidObjectId;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(720.0f, 560.0f), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Inspector", open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
HandleShortcuts(scene, selectedObjectId);
|
||||
|
||||
if (ImGui::BeginChild("Hierarchy", ImVec2(std::max(220.0f, ImGui::GetContentRegionAvail().x * 0.4f), 0.0f),
|
||||
ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX)) {
|
||||
DrawHierarchy(scene, selectedObjectId);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::BeginChild("Properties", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) {
|
||||
if (GameObject* selected = scene.FindGameObject(selectedObjectId)) {
|
||||
DrawObject(*selected);
|
||||
} else if (drawSceneSettings) {
|
||||
drawSceneSettings();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
|
||||
ApplyPendingActions(scene, selectedObjectId);
|
||||
}
|
||||
|
||||
void SceneInspector::HandleShortcuts(Scene& scene, ObjectId& selectedObjectId)
|
||||
{
|
||||
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) || ImGui::GetIO().WantTextInput ||
|
||||
selectedObjectId == InvalidObjectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
|
||||
m_PendingDelete = selectedObjectId;
|
||||
}
|
||||
if (ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_D)) {
|
||||
m_PendingDuplicate = selectedObjectId;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) {
|
||||
if (const GameObject* selected = scene.FindGameObject(selectedObjectId)) {
|
||||
BeginRename(*selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::DrawHierarchy(Scene& scene, ObjectId& selectedObjectId)
|
||||
{
|
||||
if (ImGui::Button("+")) {
|
||||
ImGui::OpenPopup("CreateObject");
|
||||
}
|
||||
if (ImGui::BeginPopup("CreateObject")) {
|
||||
if (GameObject* created = DrawCreateMenu(scene, nullptr)) {
|
||||
selectedObjectId = created->GetId();
|
||||
BeginRename(*created);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
ImGuiUtils::InputString("##Search", m_Filter, 0, "Search");
|
||||
|
||||
if (!ImGui::BeginChild("Tree")) {
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<GameObject*> objects = CollectObjects(scene);
|
||||
std::unordered_set<const GameObject*> visited;
|
||||
visited.reserve(objects.size());
|
||||
|
||||
if (m_Filter.empty()) {
|
||||
for (GameObject* object : objects) {
|
||||
if (IsRoot(*object)) {
|
||||
DrawNode(scene, *object, selectedObjectId, visited);
|
||||
}
|
||||
}
|
||||
// Anything not reachable from a root is stuck in a parent cycle, still show it
|
||||
std::unordered_set<const GameObject*> reachable;
|
||||
std::vector<const GameObject*> pending;
|
||||
for (const GameObject* object : objects) {
|
||||
if (IsRoot(*object)) {
|
||||
pending.push_back(object);
|
||||
}
|
||||
}
|
||||
while (!pending.empty()) {
|
||||
const GameObject* object = pending.back();
|
||||
pending.pop_back();
|
||||
if (!reachable.insert(object).second) {
|
||||
continue;
|
||||
}
|
||||
for (const Transform* child : object->GetTransform().GetChildren()) {
|
||||
if (child != nullptr && IsInspectable(child->GetOwner())) {
|
||||
pending.push_back(child->GetOwner());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (GameObject* object : objects) {
|
||||
if (!reachable.contains(object)) {
|
||||
DrawNode(scene, *object, selectedObjectId, visited);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (GameObject* object : objects) {
|
||||
if (ContainsIgnoreCase(object->GetName(), m_Filter)) {
|
||||
DrawNode(scene, *object, selectedObjectId, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The empty space below the tree: click to deselect, drop to move to the root, right click to create
|
||||
const ImVec2 remaining = ImGui::GetContentRegionAvail();
|
||||
ImGui::InvisibleButton("##Background", ImVec2(std::max(remaining.x, 1.0f), std::max(remaining.y, ImGui::GetFrameHeight())));
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||
selectedObjectId = InvalidObjectId;
|
||||
}
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::GameObjectPayload)) {
|
||||
m_PendingMove = {.object = *static_cast<const ObjectId*>(payload->Data)};
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
if (ImGui::BeginPopupContextItem("BackgroundMenu")) {
|
||||
DrawObjectMenu(scene, nullptr, selectedObjectId);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void SceneInspector::DrawNode(Scene& scene, GameObject& object, ObjectId& selectedObjectId,
|
||||
std::unordered_set<const GameObject*>& visited)
|
||||
{
|
||||
if (!IsInspectable(&object) || !visited.insert(&object).second) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool flat = !m_Filter.empty();
|
||||
const bool hasChildren = !flat && HasInspectableChildren(object);
|
||||
const bool renaming = m_RenamingId == object.GetId();
|
||||
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth |
|
||||
ImGuiTreeNodeFlags_DrawLinesToNodes;
|
||||
if (object.GetId() == selectedObjectId) {
|
||||
flags |= ImGuiTreeNodeFlags_Selected;
|
||||
}
|
||||
if (!hasChildren) {
|
||||
flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
}
|
||||
if (m_OpenNextFrame == object.GetId()) {
|
||||
ImGui::SetNextItemOpen(true);
|
||||
m_OpenNextFrame = InvalidObjectId;
|
||||
}
|
||||
|
||||
const bool inactive = !object.IsActiveInHierarchy();
|
||||
if (inactive) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
||||
}
|
||||
const bool open = ImGui::TreeNodeEx(reinterpret_cast<void*>(static_cast<std::uintptr_t>(object.GetId())), flags, "%s",
|
||||
renaming ? "" : DisplayName(object));
|
||||
if (inactive) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if ((ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right)) &&
|
||||
!ImGui::IsItemToggledOpen()) {
|
||||
selectedObjectId = object.GetId();
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && !ImGui::IsItemToggledOpen()) {
|
||||
BeginRename(object);
|
||||
}
|
||||
|
||||
DrawNodeDragDrop(object);
|
||||
|
||||
// Leaf nodes push no ID, without this every sibling shares one popup and draws it again
|
||||
ImGui::PushID(reinterpret_cast<void*>(static_cast<std::uintptr_t>(object.GetId())));
|
||||
if (ImGui::BeginPopupContextItem("ObjectMenu")) {
|
||||
DrawObjectMenu(scene, &object, selectedObjectId);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
if (renaming) {
|
||||
ImGui::SameLine();
|
||||
DrawRenameField(object);
|
||||
}
|
||||
|
||||
if (open && hasChildren) {
|
||||
for (Transform* child : object.GetTransform().GetChildren()) {
|
||||
if (child != nullptr && IsInspectable(child->GetOwner())) {
|
||||
DrawNode(scene, *child->GetOwner(), selectedObjectId, visited);
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::DrawNodeDragDrop(GameObject& object)
|
||||
{
|
||||
const ObjectId id = object.GetId();
|
||||
if (ImGui::BeginDragDropSource()) {
|
||||
ImGui::SetDragDropPayload(ImGuiUtils::GameObjectPayload, &id, sizeof(id));
|
||||
ImGui::TextUnformatted(DisplayName(object));
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
if (!ImGui::BeginDragDropTarget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(
|
||||
ImGuiUtils::GameObjectPayload,
|
||||
ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect);
|
||||
if (payload != nullptr) {
|
||||
const ObjectId dragged = *static_cast<const ObjectId*>(payload->Data);
|
||||
const ImVec2 min = ImGui::GetItemRectMin();
|
||||
const ImVec2 max = ImGui::GetItemRectMax();
|
||||
const float zone = (max.y - min.y) * 0.25f;
|
||||
const float mouseY = ImGui::GetMousePos().y;
|
||||
const bool before = mouseY < min.y + zone;
|
||||
const bool after = mouseY > max.y - zone;
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_DragDropTarget);
|
||||
if (before) {
|
||||
drawList->AddLine(min, ImVec2(max.x, min.y), color, 2.0f);
|
||||
} else if (after) {
|
||||
drawList->AddLine(ImVec2(min.x, max.y), max, color, 2.0f);
|
||||
} else {
|
||||
drawList->AddRect(min, max, color, 0.0f, 0, 2.0f);
|
||||
}
|
||||
|
||||
if (payload->IsDelivery() && dragged != id) {
|
||||
const GameObject* parent = ParentOf(object);
|
||||
const ObjectId parentId = IsInspectable(parent) ? parent->GetId() : InvalidObjectId;
|
||||
if (before || after) {
|
||||
m_PendingMove = {.object = dragged, .parent = parentId, .anchor = id, .after = after};
|
||||
} else {
|
||||
m_PendingMove = {.object = dragged, .parent = id};
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
void SceneInspector::DrawRenameField(GameObject& object)
|
||||
{
|
||||
if (m_FocusRename) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
m_FocusRename = false;
|
||||
}
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
const bool submitted = ImGuiUtils::InputString("##Rename", m_RenameBuffer,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
m_RenamingId = InvalidObjectId;
|
||||
return;
|
||||
}
|
||||
if (submitted || ImGui::IsItemDeactivated()) {
|
||||
if (!m_RenameBuffer.empty()) {
|
||||
object.SetName(m_RenameBuffer);
|
||||
}
|
||||
m_RenamingId = InvalidObjectId;
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::BeginRename(const GameObject& object)
|
||||
{
|
||||
m_RenamingId = object.GetId();
|
||||
m_RenameBuffer = object.GetName();
|
||||
m_FocusRename = true;
|
||||
}
|
||||
|
||||
void SceneInspector::DrawObjectMenu(Scene& scene, GameObject* object, ObjectId& selectedObjectId)
|
||||
{
|
||||
if (ImGui::BeginMenu(object != nullptr ? "Create Child" : "Create")) {
|
||||
if (GameObject* created = DrawCreateMenu(scene, object)) {
|
||||
selectedObjectId = created->GetId();
|
||||
if (object != nullptr) {
|
||||
m_OpenNextFrame = object->GetId();
|
||||
}
|
||||
BeginRename(*created);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (object == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Rename", "F2")) {
|
||||
BeginRename(*object);
|
||||
}
|
||||
if (ImGui::MenuItem("Duplicate", "Ctrl+D")) {
|
||||
m_PendingDuplicate = object->GetId();
|
||||
}
|
||||
if (ImGui::MenuItem("Unparent", nullptr, false, !IsRoot(*object))) {
|
||||
m_PendingMove = {.object = object->GetId()};
|
||||
}
|
||||
bool active = object->IsActive();
|
||||
if (ImGui::MenuItem("Active", nullptr, &active)) {
|
||||
object->SetActive(active);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Delete", "Del")) {
|
||||
m_PendingDelete = object->GetId();
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::ApplyPendingActions(Scene& scene, ObjectId& selectedObjectId)
|
||||
{
|
||||
if (m_PendingMove.object != InvalidObjectId) {
|
||||
GameObject* object = scene.FindGameObject(m_PendingMove.object);
|
||||
GameObject* parent = scene.FindGameObject(m_PendingMove.parent);
|
||||
GameObject* insertBefore = scene.FindGameObject(m_PendingMove.anchor);
|
||||
if (insertBefore != nullptr && m_PendingMove.after) {
|
||||
insertBefore = NextSibling(scene, *insertBefore);
|
||||
}
|
||||
if (insertBefore == object && object != nullptr) {
|
||||
insertBefore = NextSibling(scene, *object);
|
||||
}
|
||||
if (object != nullptr && scene.MoveInHierarchy(*object, parent, insertBefore) && parent != nullptr) {
|
||||
m_OpenNextFrame = parent->GetId();
|
||||
}
|
||||
m_PendingMove = {};
|
||||
}
|
||||
|
||||
if (m_PendingDuplicate != InvalidObjectId) {
|
||||
if (GameObject* object = scene.FindGameObject(m_PendingDuplicate)) {
|
||||
if (const GameObject* duplicate = PrefabSerializer::Duplicate(*object)) {
|
||||
selectedObjectId = duplicate->GetId();
|
||||
}
|
||||
}
|
||||
m_PendingDuplicate = InvalidObjectId;
|
||||
}
|
||||
|
||||
if (m_PendingDelete != InvalidObjectId) {
|
||||
if (GameObject* object = scene.FindGameObject(m_PendingDelete)) {
|
||||
object->Destroy();
|
||||
}
|
||||
if (selectedObjectId == m_PendingDelete) {
|
||||
selectedObjectId = InvalidObjectId;
|
||||
}
|
||||
m_PendingDelete = InvalidObjectId;
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::DrawObject(GameObject& object)
|
||||
{
|
||||
bool active = object.IsActive();
|
||||
if (ImGui::Checkbox("##Active", &active)) {
|
||||
object.SetActive(active);
|
||||
}
|
||||
ImGui::SetItemTooltip("Active");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
std::string name = object.GetName();
|
||||
if (ImGuiUtils::InputString("##Name", name)) {
|
||||
object.SetName(name);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
DrawTransform(object);
|
||||
DrawComponents(object);
|
||||
DrawAddComponent(object);
|
||||
}
|
||||
|
||||
void SceneInspector::DrawTransform(GameObject& object)
|
||||
{
|
||||
auto* rectTransform = object.GetComponent<RectTransform>();
|
||||
// Everything under a canvas has a RectTransform, a stretched one keeps the old layout
|
||||
if (rectTransform == nullptr && object.GetComponent<Canvas>() == nullptr && Canvas::FindFor(object) != nullptr) {
|
||||
rectTransform = object.AddComponent<RectTransform>();
|
||||
rectTransform->Stretch();
|
||||
}
|
||||
|
||||
if (rectTransform != nullptr) {
|
||||
const bool open = ImGui::CollapsingHeader("Rect Transform", ImGuiTreeNodeFlags_DefaultOpen);
|
||||
if (ImGui::BeginPopupContextItem("RectTransformMenu")) {
|
||||
if (ImGui::MenuItem("Stretch")) {
|
||||
rectTransform->Stretch();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
if (open) {
|
||||
ImGui::PushID(rectTransform);
|
||||
rectTransform->ImGuiInspector();
|
||||
ImGui::PopID();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Transform& transform = object.GetTransform();
|
||||
const bool open = ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen);
|
||||
bool changed = false;
|
||||
if (ImGui::BeginPopupContextItem("TransformMenu")) {
|
||||
if (ImGui::MenuItem("Reset Position")) {
|
||||
transform.SetLocalPosition(glm::vec3{0.0f});
|
||||
changed = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Reset Rotation")) {
|
||||
transform.SetLocalRotation(glm::quat{1.0f, 0.0f, 0.0f, 0.0f});
|
||||
changed = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Reset Scale")) {
|
||||
transform.SetLocalScale(glm::vec3{1.0f});
|
||||
changed = true;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (open) {
|
||||
glm::vec3 position = transform.GetLocalPosition();
|
||||
if (ImGuiUtils::DrawVec3Control("Position", position, 0.05f)) {
|
||||
transform.SetLocalPosition(position);
|
||||
changed = true;
|
||||
}
|
||||
glm::vec3 rotation = glm::degrees(glm::eulerAngles(transform.GetLocalRotation()));
|
||||
if (ImGuiUtils::DrawVec3Control("Rotation", rotation, 0.5f)) {
|
||||
transform.SetLocalRotation(rotation);
|
||||
changed = true;
|
||||
}
|
||||
glm::vec3 scale = transform.GetLocalScale();
|
||||
if (ImGuiUtils::DrawVec3Control("Scale", scale, 0.05f)) {
|
||||
transform.SetLocalScale(scale);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
object.RefreshPhysicsInHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::DrawComponents(GameObject& object)
|
||||
{
|
||||
auto& components = object.GetComponents();
|
||||
const auto* rectTransform = object.GetComponent<RectTransform>();
|
||||
const bool underCanvas = Canvas::FindFor(object) != nullptr;
|
||||
|
||||
for (std::size_t i = 0; i < components.size(); ++i) {
|
||||
Component* component = components[i].get();
|
||||
if (component == nullptr || component->IsBeingDestroyed() || component == rectTransform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::PushID(component);
|
||||
|
||||
const float lineStart = ImGui::GetCursorPosX();
|
||||
const float lineWidth = ImGui::GetContentRegionAvail().x;
|
||||
const bool enabled = component->isEnabled();
|
||||
if (!enabled) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
||||
}
|
||||
const std::string typeName = component->GetTypeName();
|
||||
const bool open = ImGui::CollapsingHeader(typeName.c_str(),
|
||||
ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_AllowOverlap);
|
||||
if (!enabled) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
const bool removable = !(underCanvas && dynamic_cast<RectTransform*>(component) != nullptr);
|
||||
if (ImGui::BeginPopupContextItem("ComponentMenu")) {
|
||||
DrawComponentMenu(object, *component, i, removable);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float buttonWidth = ImGui::CalcTextSize("...").x + style.FramePadding.x * 2.0f;
|
||||
const float controlsWidth = ImGui::GetFrameHeight() + style.ItemSpacing.x + buttonWidth + style.FramePadding.x;
|
||||
ImGui::SameLine(RightAlignedX(lineStart, lineWidth, controlsWidth));
|
||||
bool enabledToggle = enabled;
|
||||
if (ImGui::Checkbox("##Enabled", &enabledToggle)) {
|
||||
component->SetEnabled(enabledToggle);
|
||||
}
|
||||
ImGui::SetItemTooltip("Enabled");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("...")) {
|
||||
ImGui::OpenPopup("ComponentMenu");
|
||||
}
|
||||
|
||||
if (open) {
|
||||
component->ImGuiInspector();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
|
||||
// A move or remove from the menu changed the list, the rest is drawn next frame
|
||||
if (components.size() <= i || components[i].get() != component) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::DrawComponentMenu(GameObject& object, Component& component, std::size_t index, bool removable)
|
||||
{
|
||||
auto& components = object.GetComponents();
|
||||
if (ImGui::MenuItem("Move Up", nullptr, false, index > 0)) {
|
||||
std::swap(components[index], components[index - 1]);
|
||||
}
|
||||
if (ImGui::MenuItem("Move Down", nullptr, false, index + 1 < components.size())) {
|
||||
std::swap(components[index], components[index + 1]);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
const std::string typeName = component.GetTypeName();
|
||||
if (ImGui::MenuItem("Copy Values")) {
|
||||
m_ClipboardType = typeName;
|
||||
m_Clipboard = component.Serialize();
|
||||
}
|
||||
if (ImGui::MenuItem("Paste Values", nullptr, false, m_ClipboardType == typeName)) {
|
||||
try {
|
||||
component.Deserialize(m_Clipboard);
|
||||
if (component.AffectsPhysics()) {
|
||||
object.RefreshPhysics();
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Pasting {} values failed: {}", typeName, exception.what());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Remove", nullptr, false, removable)) {
|
||||
const bool affectsPhysics = component.AffectsPhysics();
|
||||
component.Destroy();
|
||||
if (affectsPhysics) {
|
||||
object.RefreshPhysics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneInspector::DrawAddComponent(GameObject& object)
|
||||
{
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Add Component", ImVec2(-FLT_MIN, 0.0f))) {
|
||||
m_ComponentFilter.clear();
|
||||
ImGui::OpenPopup("AddComponent");
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(ImGui::GetItemRectSize().x, 0.0f), ImVec2(FLT_MAX, 320.0f));
|
||||
if (!ImGui::BeginPopup("AddComponent")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
const bool submitted = ImGuiUtils::InputString("##ComponentSearch", m_ComponentFilter,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue, "Search");
|
||||
|
||||
std::vector<std::string> types = ComponentFactory::GetRegisteredTypeNames();
|
||||
std::ranges::sort(types);
|
||||
std::erase_if(types, [&](const std::string& type) {
|
||||
const bool added = std::ranges::any_of(object.GetComponents(), [&](const auto& component) {
|
||||
return component != nullptr && !component->IsBeingDestroyed() && component->GetTypeName() == type;
|
||||
});
|
||||
return added || !ContainsIgnoreCase(type, m_ComponentFilter);
|
||||
});
|
||||
|
||||
std::string chosen = submitted && !types.empty() ? types.front() : std::string{};
|
||||
for (const std::string& type : types) {
|
||||
if (ImGui::Selectable(type.c_str())) {
|
||||
chosen = type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!chosen.empty()) {
|
||||
try {
|
||||
if (ComponentFactory::Create(chosen, object) == nullptr) {
|
||||
spdlog::warn("Component type is not registered: {}", chosen);
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Adding {} failed: {}", chosen, exception.what());
|
||||
}
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
#include <destrum/Util/TransformGizmo.h>
|
||||
|
||||
#include <destrum/Graphics/Camera.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtx/matrix_decompose.hpp>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <ImGuizmo.h>
|
||||
|
||||
namespace {
|
||||
ImGuizmo::OPERATION toImGuizmo(GizmoOperation operation)
|
||||
{
|
||||
switch (operation) {
|
||||
case GizmoOperation::Rotate:
|
||||
return ImGuizmo::ROTATE;
|
||||
case GizmoOperation::Scale:
|
||||
return ImGuizmo::SCALE;
|
||||
case GizmoOperation::Translate:
|
||||
break;
|
||||
}
|
||||
return ImGuizmo::TRANSLATE;
|
||||
}
|
||||
}
|
||||
|
||||
void TransformGizmo::HandleShortcuts()
|
||||
{
|
||||
if (ImGui::GetIO().WantTextInput || ImGuizmo::IsUsing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_T, false)) {
|
||||
operation = GizmoOperation::Translate;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_R, false)) {
|
||||
operation = GizmoOperation::Rotate;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Y, false)) {
|
||||
operation = GizmoOperation::Scale;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_L, false)) {
|
||||
space = space == GizmoSpace::World ? GizmoSpace::Local : GizmoSpace::World;
|
||||
}
|
||||
}
|
||||
|
||||
void TransformGizmo::DrawToolbar()
|
||||
{
|
||||
if (!ImGui::Begin("Gizmo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto operationButton = [this](const char* label, GizmoOperation buttonOperation) {
|
||||
if (ImGui::RadioButton(label, operation == buttonOperation)) {
|
||||
operation = buttonOperation;
|
||||
}
|
||||
};
|
||||
operationButton("Move (T)", GizmoOperation::Translate);
|
||||
ImGui::SameLine();
|
||||
operationButton("Rotate (R)", GizmoOperation::Rotate);
|
||||
ImGui::SameLine();
|
||||
operationButton("Scale (Y)", GizmoOperation::Scale);
|
||||
|
||||
if (operation != GizmoOperation::Scale) {
|
||||
bool local = space == GizmoSpace::Local;
|
||||
if (ImGui::Checkbox("Local (L)", &local)) {
|
||||
space = local ? GizmoSpace::Local : GizmoSpace::World;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
ImGui::Checkbox("Snap", &snap);
|
||||
if (snap) {
|
||||
ImGui::SetNextItemWidth(80.0f);
|
||||
switch (operation) {
|
||||
case GizmoOperation::Translate:
|
||||
ImGui::DragFloat("Step", &translationSnap, 0.05f, 0.01f, 10.0f, "%.2f m");
|
||||
break;
|
||||
case GizmoOperation::Rotate:
|
||||
ImGui::DragFloat("Step", &rotationSnapDegrees, 1.0f, 1.0f, 90.0f, "%.0f deg");
|
||||
break;
|
||||
case GizmoOperation::Scale:
|
||||
ImGui::DragFloat("Step", &scaleSnap, 0.01f, 0.01f, 1.0f, "%.2f");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
bool TransformGizmo::Manipulate(GameObject& object, const Camera& camera, const glm::vec2& viewMin, const glm::vec2& viewSize)
|
||||
{
|
||||
ImGuizmo::BeginFrame();
|
||||
|
||||
// Drawn into the window that shows the scene, so it stays on top of it and hovers with it
|
||||
ImGuizmo::SetDrawlist();
|
||||
ImGuizmo::SetOrthographic(false);
|
||||
ImGuizmo::SetRect(viewMin.x, viewMin.y, viewSize.x, viewSize.y);
|
||||
|
||||
const glm::mat4 view = camera.GetViewMatrix();
|
||||
glm::mat4 projection = camera.GetProjectionMatrix();
|
||||
// ImGuizmo expects OpenGL's y up clip space, the engine flips y for Vulkan
|
||||
projection[1][1] *= -1.0f;
|
||||
|
||||
Transform& transform = object.GetTransform();
|
||||
glm::mat4 worldMatrix = transform.GetWorldMatrix();
|
||||
|
||||
const float snapStep = operation == GizmoOperation::Translate ? translationSnap
|
||||
: operation == GizmoOperation::Rotate ? rotationSnapDegrees
|
||||
: scaleSnap;
|
||||
const float snapValues[3] = {snapStep, snapStep, snapStep};
|
||||
const ImGuizmo::MODE mode =
|
||||
operation == GizmoOperation::Scale || space == GizmoSpace::Local ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
|
||||
|
||||
const bool changed = ImGuizmo::Manipulate(
|
||||
glm::value_ptr(view),
|
||||
glm::value_ptr(projection),
|
||||
toImGuizmo(operation),
|
||||
mode,
|
||||
glm::value_ptr(worldMatrix),
|
||||
nullptr,
|
||||
snap ? snapValues : nullptr);
|
||||
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
glm::vec3 position;
|
||||
glm::quat rotation;
|
||||
glm::vec3 scale;
|
||||
glm::vec3 skew;
|
||||
glm::vec4 perspective;
|
||||
if (!glm::decompose(worldMatrix, scale, rotation, position, skew, perspective)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case GizmoOperation::Translate:
|
||||
transform.SetWorldPosition(position);
|
||||
break;
|
||||
case GizmoOperation::Rotate:
|
||||
transform.SetWorldRotation(rotation);
|
||||
break;
|
||||
case GizmoOperation::Scale:
|
||||
transform.SetWorldScale(scale);
|
||||
break;
|
||||
}
|
||||
|
||||
object.RefreshPhysicsInHierarchy();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TransformGizmo::IsUsing() const
|
||||
{
|
||||
return ImGuizmo::IsUsing();
|
||||
}
|
||||
|
||||
bool TransformGizmo::IsOver() const
|
||||
{
|
||||
return ImGuizmo::IsOver();
|
||||
}
|
||||
Reference in New Issue
Block a user