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
@@ -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();
}