feat: move editor to project / add post processing etc

This commit is contained in:
2026-10-23 20:54:12 +02:00
parent f7e7c56392
commit a3d3da4a67
104 changed files with 11750 additions and 2675 deletions
+275 -676
View File
File diff suppressed because it is too large Load Diff
+177
View File
@@ -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();
}
+47
View File
@@ -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) {
+27 -6
View 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
+61 -9
View File
@@ -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);
}
+60 -7
View File
@@ -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);
}
+140 -34
View File
@@ -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");
}
-439
View File
@@ -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();
}
+39
View File
@@ -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);
}
}
+9 -3
View File
@@ -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) {
-6
View File
@@ -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());
-164
View File
@@ -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);
});
}
-166
View File
@@ -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;
}
-748
View File
@@ -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();
}
-162
View File
@@ -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();
}