feat: move editor to project / add post processing etc
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user