add simple node editor(use ImGui-Node-Editor), some tweaks
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -35,3 +35,6 @@
|
|||||||
[submodule "Prism/vendor/efsw"]
|
[submodule "Prism/vendor/efsw"]
|
||||||
path = Prism/vendor/efsw
|
path = Prism/vendor/efsw
|
||||||
url = https://github.com/SpartanJ/efsw
|
url = https://github.com/SpartanJ/efsw
|
||||||
|
[submodule "Prism/vendor/imgui-node-editor"]
|
||||||
|
path = Prism/vendor/imgui-node-editor
|
||||||
|
url = https://github.com/thedmd/imgui-node-editor.git
|
||||||
|
|||||||
@ -719,7 +719,7 @@ namespace Prism
|
|||||||
m_ViewportBounds[1] = { maxBound.x, maxBound.y };
|
m_ViewportBounds[1] = { maxBound.x, maxBound.y };
|
||||||
|
|
||||||
// ImGuizmo
|
// ImGuizmo
|
||||||
if (m_GizmoType != -1 && !m_SelectionContext.empty() && m_SceneState == SceneState::Edit)
|
if ((m_ViewportPanelFocused || m_ViewportPanelHovered) && m_GizmoType != -1 && !m_SelectionContext.empty() && m_SceneState == SceneState::Edit)
|
||||||
{
|
{
|
||||||
auto& selection = m_SelectionContext[0];
|
auto& selection = m_SelectionContext[0];
|
||||||
|
|
||||||
@ -921,15 +921,19 @@ namespace Prism
|
|||||||
|
|
||||||
if (ImViewGuizmo::Rotate(cameraPos, cameraRot, glm::vec3(0.0f), gizmoCenter))
|
if (ImViewGuizmo::Rotate(cameraPos, cameraRot, glm::vec3(0.0f), gizmoCenter))
|
||||||
{
|
{
|
||||||
|
if (m_ViewportPanelFocused || m_ViewportPanelHovered)
|
||||||
|
{
|
||||||
|
glm::vec3 forward = cameraRot * glm::vec3(0.0f, 0.0f, -1.0f);
|
||||||
|
float newPitch = glm::asin(glm::clamp(-forward.y, -1.0f, 1.0f));
|
||||||
|
float newYaw = -glm::atan(forward.x, -forward.z);
|
||||||
|
|
||||||
glm::vec3 forward = cameraRot * glm::vec3(0.0f, 0.0f, -1.0f);
|
float distance = glm::length(cameraPos - focusPosition);
|
||||||
float newPitch = glm::asin(glm::clamp(-forward.y, -1.0f, 1.0f));
|
m_EditorCamera.SetDistance(distance);
|
||||||
float newYaw = -glm::atan(forward.x, -forward.z);
|
m_EditorCamera.SetFocusPosition(focusPosition);
|
||||||
|
m_EditorCamera.SetYawPitch(newYaw, newPitch);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
float distance = glm::length(cameraPos - focusPosition);
|
|
||||||
m_EditorCamera.SetDistance(distance);
|
|
||||||
m_EditorCamera.SetFocusPosition(focusPosition);
|
|
||||||
m_EditorCamera.SetYawPitch(newYaw, newPitch);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -994,20 +998,20 @@ namespace Prism
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch (e.GetKeyCode())
|
switch (e.GetKeyCode())
|
||||||
{
|
|
||||||
case KeyCode::DELETE:
|
|
||||||
if (m_SelectionContext.size())
|
|
||||||
{
|
{
|
||||||
const Entity selectedEntity = m_SelectionContext[0].Entity;
|
case KeyCode::DELETE:
|
||||||
m_EditorScene->DestroyEntity(selectedEntity);
|
if (m_SelectionContext.size())
|
||||||
m_SelectionContext.clear();
|
{
|
||||||
m_EditorScene->SetSelectedEntity({});
|
const Entity selectedEntity = m_SelectionContext[0].Entity;
|
||||||
m_SceneHierarchyPanel->SetSelected({});
|
m_EditorScene->DestroyEntity(selectedEntity);
|
||||||
|
m_SelectionContext.clear();
|
||||||
|
m_EditorScene->SetSelectedEntity({});
|
||||||
|
m_SceneHierarchyPanel->SetSelected({});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input::IsKeyPressed(KeyCode::LEFT_CONTROL))
|
if (Input::IsKeyPressed(KeyCode::LEFT_CONTROL))
|
||||||
|
|||||||
@ -50,6 +50,19 @@ list(APPEND SRC_SOURCE ${IMGUIZMO_SOURCE})
|
|||||||
# ----------- ImViewGuizmo -------------
|
# ----------- ImViewGuizmo -------------
|
||||||
set(IMVIEWGUIZMO_DIR vendor/ImViewGuizmo)
|
set(IMVIEWGUIZMO_DIR vendor/ImViewGuizmo)
|
||||||
|
|
||||||
|
# ----------- ImGui-Node-Editor -------------
|
||||||
|
set(IMGUI_NODE_EDITOR_DIR vendor/imgui-node-editor)
|
||||||
|
set(IMGUI_NODE_EDITOR_SOURCES
|
||||||
|
${IMGUI_NODE_EDITOR_DIR}/imgui_node_editor.cpp
|
||||||
|
${IMGUI_NODE_EDITOR_DIR}/imgui_node_editor_api.cpp
|
||||||
|
${IMGUI_NODE_EDITOR_DIR}/imgui_canvas.cpp
|
||||||
|
${IMGUI_NODE_EDITOR_DIR}/crude_json.cpp
|
||||||
|
|
||||||
|
${IMGUI_NODE_EDITOR_DIR}/examples/blueprints-example/utilities/widgets.cpp
|
||||||
|
${IMGUI_NODE_EDITOR_DIR}/examples/blueprints-example/utilities/drawing.cpp
|
||||||
|
)
|
||||||
|
list(APPEND SRC_SOURCE ${IMGUI_NODE_EDITOR_SOURCES})
|
||||||
|
|
||||||
|
|
||||||
# ------------- NVIDIA PhysX -------------
|
# ------------- NVIDIA PhysX -------------
|
||||||
# PhysX/physx/buildtools/presets/*.xml
|
# PhysX/physx/buildtools/presets/*.xml
|
||||||
@ -125,6 +138,7 @@ set(TARGET_INCLUDE_DIR
|
|||||||
${IMGUI_DIR}
|
${IMGUI_DIR}
|
||||||
${IMGUIZMO_DIR}
|
${IMGUIZMO_DIR}
|
||||||
${IMVIEWGUIZMO_DIR}
|
${IMVIEWGUIZMO_DIR}
|
||||||
|
${IMGUI_NODE_EDITOR_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------- debug Defines -------------
|
# ------------- debug Defines -------------
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
#include "AssetEditorPanel.h"
|
#include "AssetEditorPanel.h"
|
||||||
|
|
||||||
#include "DefaultAssetEditors.h"
|
#include "DefaultAssetEditors/PBRMaterialAssetEditor.h"
|
||||||
|
#include "DefaultAssetEditors/PhysicsMaterialEditor.h"
|
||||||
|
#include "DefaultAssetEditors/TextureAssetEditor.h"
|
||||||
#include "Prism/Asset/AssetSerializer.h"
|
#include "Prism/Asset/AssetSerializer.h"
|
||||||
#include "Prism/Core/Log.h"
|
#include "Prism/Core/Log.h"
|
||||||
|
|
||||||
|
|||||||
@ -1,284 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Atdunbg on 2026/2/14.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "DefaultAssetEditors.h"
|
|
||||||
|
|
||||||
#include "Prism/Asset/AssetSerializer.h"
|
|
||||||
#include "Prism/Core/Log.h"
|
|
||||||
|
|
||||||
namespace Prism
|
|
||||||
{
|
|
||||||
|
|
||||||
// MaterialEditor
|
|
||||||
PhysicsMaterialEditor::PhysicsMaterialEditor()
|
|
||||||
: AssetEditor("Edit Physics Material") {}
|
|
||||||
|
|
||||||
void PhysicsMaterialEditor::SetAsset(const Ref<Asset>& asset)
|
|
||||||
{
|
|
||||||
if (m_Asset) AssetSerializer::SerializeAsset(m_Asset);
|
|
||||||
m_Asset = static_cast<Ref<PhysicsMaterialAsset>>(asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PhysicsMaterialEditor::Render()
|
|
||||||
{
|
|
||||||
if (!m_Asset)
|
|
||||||
SetOpen(false);
|
|
||||||
|
|
||||||
UI::BeginPropertyGrid();
|
|
||||||
UI::Property("Static Friction", m_Asset->StaticFriction);
|
|
||||||
UI::Property("Dynamic Friction", m_Asset->DynamicFriction);
|
|
||||||
UI::Property("Bounciness", m_Asset->Bounciness);
|
|
||||||
UI::EndPropertyGrid();
|
|
||||||
|
|
||||||
if (ImGui::Button("Save"))
|
|
||||||
AssetSerializer::SerializeAsset(m_Asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MaterialEditor
|
|
||||||
TextureViewer::TextureViewer()
|
|
||||||
: AssetEditor("Edit Texture")
|
|
||||||
{
|
|
||||||
SetMinSize(200, 600);
|
|
||||||
SetMaxSize(500, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureViewer::SetAsset(const Ref<Asset>& asset)
|
|
||||||
{
|
|
||||||
if (m_Asset) AssetSerializer::SerializeAsset(m_Asset);
|
|
||||||
m_Asset = static_cast<Ref<Texture>>(asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureViewer::Render()
|
|
||||||
{
|
|
||||||
if (!m_Asset)
|
|
||||||
SetOpen(false);
|
|
||||||
|
|
||||||
float textureWidth = (float)m_Asset->GetWidth();
|
|
||||||
float textureHeight = (float)m_Asset->GetHeight();
|
|
||||||
float bitsPerPixel = (float)Texture::GetBPP(m_Asset->GetFormat());
|
|
||||||
float imageSize = ImGui::GetWindowWidth() - 40;
|
|
||||||
imageSize = glm::min(imageSize, 500.0f);
|
|
||||||
|
|
||||||
ImGui::SetCursorPosX(20);
|
|
||||||
ImGui::Image((ImTextureID)m_Asset->GetRendererID(), { imageSize, imageSize });
|
|
||||||
|
|
||||||
UI::BeginPropertyGrid();
|
|
||||||
UI::Property("Width", textureWidth, 0.1f, 0.0f, 0.0f, true);
|
|
||||||
UI::Property("Height", textureHeight, 0.1f, 0.0f, 0.0f, true);
|
|
||||||
UI::Property("Bits", bitsPerPixel, 0.1f, 0.0f, 0.0f, true);
|
|
||||||
UI::EndPropertyGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
PBRMaterialEditor::PBRMaterialEditor() : AssetEditor("Material Editor")
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 辅助函数:绘制纹理拖拽槽
|
|
||||||
void DrawTextureSlot(Ref<Texture2D>& texture, const std::function<void(Ref<Texture2D>)>& onDrop)
|
|
||||||
{
|
|
||||||
static Ref<Texture2D> s_Checkerboard = nullptr;
|
|
||||||
if (!s_Checkerboard)
|
|
||||||
s_Checkerboard = AssetsManager::GetAsset<Texture2D>("assets/editor/Checkerboard.tga");
|
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 10));
|
|
||||||
ImGui::Image(texture ? (ImTextureID)(intptr_t)texture->GetRendererID() : (ImTextureID)(intptr_t)s_Checkerboard->GetRendererID(),
|
|
||||||
ImVec2(64, 64));
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
|
|
||||||
if (ImGui::BeginDragDropTarget())
|
|
||||||
{
|
|
||||||
if (const auto data = ImGui::AcceptDragDropPayload("asset_payload"))
|
|
||||||
{
|
|
||||||
AssetHandle assetHandle = *(AssetHandle*)data->Data;
|
|
||||||
if (AssetsManager::IsAssetType(assetHandle, AssetType::Texture))
|
|
||||||
{
|
|
||||||
Ref<Texture2D> newTex = AssetsManager::GetAsset<Texture2D>(assetHandle);
|
|
||||||
if (onDrop) onDrop(newTex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndDragDropTarget();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
{
|
|
||||||
if (texture)
|
|
||||||
{
|
|
||||||
ImGui::BeginTooltip();
|
|
||||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
|
|
||||||
ImGui::TextUnformatted(texture->GetPath().c_str());
|
|
||||||
ImGui::PopTextWrapPos();
|
|
||||||
ImGui::Image((ImTextureID)(intptr_t)texture->GetRendererID(), ImVec2(384, 384));
|
|
||||||
ImGui::EndTooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// TODO: how to solve this
|
|
||||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
|
||||||
{
|
|
||||||
std::string filename = FileSystem::OpenFileSelector("*.png;*.tga;*.jpg;*.jpeg");
|
|
||||||
if (!filename.empty())
|
|
||||||
{
|
|
||||||
Ref<Texture2D> newTex = Texture2D::Create(filename);
|
|
||||||
if (onDrop) onDrop(newTex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void PBRMaterialEditor::SetAsset(const Ref<Asset>& asset)
|
|
||||||
{
|
|
||||||
if (m_Asset)
|
|
||||||
{
|
|
||||||
if (m_Asset->Handle == asset->Handle) return;
|
|
||||||
if (AssetsManager::IsAssetHandleValid(m_Asset->Handle))
|
|
||||||
AssetSerializer::SerializeAsset(m_Asset);
|
|
||||||
}
|
|
||||||
m_Asset = static_cast<Ref<PBRMaterialAsset>>(asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PBRMaterialEditor::Render()
|
|
||||||
{
|
|
||||||
if (!m_Asset) return;
|
|
||||||
|
|
||||||
auto& material = m_Asset;
|
|
||||||
|
|
||||||
if (ImGui::BeginTabBar("MaterialProperties", ImGuiTabBarFlags_None))
|
|
||||||
{
|
|
||||||
// ==================== Parameters ====================
|
|
||||||
if (ImGui::BeginTabItem("Parameters"))
|
|
||||||
{
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
if (ImGui::CollapsingHeader("Albedo", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
ImGui::Indent();
|
|
||||||
|
|
||||||
if (ImGui::ColorEdit3("Color##Albedo", glm::value_ptr(material->AlbedoColor)))
|
|
||||||
{
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool useAlbedoMap = (material->AlbedoTexToggle > 0.5f);
|
|
||||||
if (ImGui::Checkbox("Use Texture##Albedo", &useAlbedoMap))
|
|
||||||
{
|
|
||||||
material->AlbedoTexToggle = useAlbedoMap ? 1.0f : 0.0f;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawTextureSlot(material->AlbedoTexture, [&](const Ref<Texture2D>& newTex) {
|
|
||||||
material->AlbedoTexture = newTex;
|
|
||||||
material->AlbedoTexToggle = true;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
});
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::Text("Albedo Texture");
|
|
||||||
|
|
||||||
ImGui::Unindent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal Map
|
|
||||||
if (ImGui::CollapsingHeader("Normal Map", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
ImGui::Indent();
|
|
||||||
|
|
||||||
bool useNormalMap = (material->NormalTexToggle > 0.5f);
|
|
||||||
if (ImGui::Checkbox("Use Texture##Normal", &useNormalMap))
|
|
||||||
{
|
|
||||||
material->NormalTexToggle = useNormalMap ? 1.0f : 0.0f;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawTextureSlot(material->NormalTexture, [&](const Ref<Texture2D>& newTex) {
|
|
||||||
material->NormalTexture = newTex;
|
|
||||||
material->NormalTexToggle = true;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
});
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::Text("Normal Texture");
|
|
||||||
|
|
||||||
ImGui::Unindent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metalness
|
|
||||||
if (ImGui::CollapsingHeader("Metalness", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
ImGui::Indent();
|
|
||||||
|
|
||||||
if (ImGui::SliderFloat("Value##Metalness", &material->Metalness, 0.0f, 1.0f))
|
|
||||||
{
|
|
||||||
// 自动保存
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool useMetalnessMap = (material->MetalnessTexToggle > 0.5f);
|
|
||||||
if (ImGui::Checkbox("Use Texture##Metalness", &useMetalnessMap))
|
|
||||||
{
|
|
||||||
material->MetalnessTexToggle = useMetalnessMap ? 1.0f : 0.0f;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawTextureSlot(material->MetalnessTexture, [&](const Ref<Texture2D>& newTex) {
|
|
||||||
material->MetalnessTexture = newTex;
|
|
||||||
material->MetalnessTexToggle = true;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
});
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::Text("Metalness Texture");
|
|
||||||
|
|
||||||
ImGui::Unindent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roughness
|
|
||||||
if (ImGui::CollapsingHeader("Roughness", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
ImGui::Indent();
|
|
||||||
|
|
||||||
if (ImGui::SliderFloat("Value##Roughness", &material->Roughness, 0.0f, 1.0f))
|
|
||||||
{
|
|
||||||
// 自动保存
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool useRoughnessMap = (material->RoughnessTexToggle > 0.5f);
|
|
||||||
if (ImGui::Checkbox("Use Texture##Roughness", &useRoughnessMap))
|
|
||||||
{
|
|
||||||
material->RoughnessTexToggle = useRoughnessMap ? 1.0f : 0.0f;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawTextureSlot(material->RoughnessTexture, [&](const Ref<Texture2D>& newTex) {
|
|
||||||
material->RoughnessTexture = newTex;
|
|
||||||
material->RoughnessTexToggle = true;
|
|
||||||
m_Asset->IsDirty = true;
|
|
||||||
});
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::Text("Roughness Texture");
|
|
||||||
|
|
||||||
ImGui::Unindent();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndTabItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 预览 ====================
|
|
||||||
if (ImGui::BeginTabItem("Preview"))
|
|
||||||
{
|
|
||||||
// 简单显示纹理预览
|
|
||||||
if (material->AlbedoTexture)
|
|
||||||
{
|
|
||||||
ImGui::Text("Albedo Preview");
|
|
||||||
ImGui::Image((ImTextureID)(intptr_t)material->AlbedoTexture->GetRendererID(), ImVec2(128, 128));
|
|
||||||
}
|
|
||||||
ImGui::EndTabItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndTabBar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Atdunbg on 2026/2/14.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef PRISM_DEFAULTASSETEDITORS_H
|
|
||||||
#define PRISM_DEFAULTASSETEDITORS_H
|
|
||||||
#include "AssetEditorPanel.h"
|
|
||||||
#include "Prism/Renderer/Texture.h"
|
|
||||||
|
|
||||||
|
|
||||||
namespace Prism
|
|
||||||
{
|
|
||||||
class RenderPass;
|
|
||||||
|
|
||||||
class PhysicsMaterialEditor : public AssetEditor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PhysicsMaterialEditor();
|
|
||||||
|
|
||||||
Ref<Asset> GetAsset() override { return m_Asset; }
|
|
||||||
void SetAsset(const Ref<Asset>& asset) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void Render() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Ref<PhysicsMaterialAsset> m_Asset;
|
|
||||||
};
|
|
||||||
|
|
||||||
class TextureViewer : public AssetEditor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TextureViewer();
|
|
||||||
|
|
||||||
Ref<Asset> GetAsset() override { return m_Asset; }
|
|
||||||
void SetAsset(const Ref<Asset>& asset) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void Render() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Ref<Texture> m_Asset;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PBRMaterialEditor : public AssetEditor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PBRMaterialEditor();
|
|
||||||
|
|
||||||
Ref<Asset> GetAsset() override { return m_Asset; }
|
|
||||||
void SetAsset(const Ref<Asset>& asset) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void Render() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Ref<PBRMaterialAsset> m_Asset;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif //PRISM_DEFAULTASSETEDITORS_H
|
|
||||||
@ -0,0 +1,718 @@
|
|||||||
|
//
|
||||||
|
// Created by Atdunbg on 2026/2/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "PBRMaterialAssetEditor.h"
|
||||||
|
|
||||||
|
#include "examples/blueprints-example/utilities/widgets.h"
|
||||||
|
#include "Prism/Asset/AssetSerializer.h"
|
||||||
|
#include "Prism/Core/Log.h"
|
||||||
|
|
||||||
|
namespace Prism
|
||||||
|
{
|
||||||
|
PBRMaterialEditor::PBRMaterialEditor() : AssetEditor("Material Editor")
|
||||||
|
{
|
||||||
|
ed::Config config;
|
||||||
|
config.SettingsFile = "";
|
||||||
|
m_EditorContext = ed::CreateEditor(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
PBRMaterialEditor::~PBRMaterialEditor()
|
||||||
|
{
|
||||||
|
if (m_EditorContext)
|
||||||
|
{
|
||||||
|
ed::DestroyEditor(m_EditorContext);
|
||||||
|
m_EditorContext = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:绘制纹理拖拽槽
|
||||||
|
void DrawTextureSlot(Ref<Texture2D>& texture, const std::function<void(Ref<Texture2D>)>& onDrop)
|
||||||
|
{
|
||||||
|
static Ref<Texture2D> s_Checkerboard = nullptr;
|
||||||
|
if (!s_Checkerboard)
|
||||||
|
s_Checkerboard = AssetsManager::GetAsset<Texture2D>("assets/editor/Checkerboard.tga");
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 10));
|
||||||
|
ImGui::Image(texture
|
||||||
|
? (ImTextureID)(intptr_t)texture->GetRendererID()
|
||||||
|
: (ImTextureID)(intptr_t)s_Checkerboard->GetRendererID(),
|
||||||
|
ImVec2(64, 64));
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
|
||||||
|
if (ImGui::BeginDragDropTarget())
|
||||||
|
{
|
||||||
|
if (const auto data = ImGui::AcceptDragDropPayload("asset_payload"))
|
||||||
|
{
|
||||||
|
AssetHandle assetHandle = *(AssetHandle*)data->Data;
|
||||||
|
if (AssetsManager::IsAssetType(assetHandle, AssetType::Texture))
|
||||||
|
{
|
||||||
|
Ref<Texture2D> newTex = AssetsManager::GetAsset<Texture2D>(assetHandle);
|
||||||
|
if (onDrop) onDrop(newTex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDragDropTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
if (texture)
|
||||||
|
{
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
|
||||||
|
ImGui::TextUnformatted(texture->GetPath().c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::Image((ImTextureID)(intptr_t)texture->GetRendererID(), ImVec2(384, 384));
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO: how to solve this
|
||||||
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||||
|
{
|
||||||
|
std::string filename = FileSystem::OpenFileSelector("*.png;*.tga;*.jpg;*.jpeg");
|
||||||
|
if (!filename.empty())
|
||||||
|
{
|
||||||
|
Ref<Texture2D> newTex = Texture2D::Create(filename);
|
||||||
|
if (onDrop) onDrop(newTex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PBRMaterialEditor::SetAsset(const Ref<Asset>& asset)
|
||||||
|
{
|
||||||
|
if (m_Asset)
|
||||||
|
{
|
||||||
|
if (m_Asset->Handle == asset->Handle) return;
|
||||||
|
if (AssetsManager::IsAssetHandleValid(m_Asset->Handle))
|
||||||
|
AssetSerializer::SerializeAsset(m_Asset);
|
||||||
|
}
|
||||||
|
m_Asset = static_cast<Ref<PBRMaterialAsset>>(asset);
|
||||||
|
LoadAssetToGraph(); // 加载资产数据到节点图
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::Render()
|
||||||
|
{
|
||||||
|
if (!m_Asset) return;
|
||||||
|
|
||||||
|
if (ImGui::BeginTabBar("MaterialProperties", ImGuiTabBarFlags_None))
|
||||||
|
{
|
||||||
|
if (ImGui::BeginTabItem("Basic Edit panel"))
|
||||||
|
{
|
||||||
|
RenderBasicEditorPanel();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem("Graph Node Edit"))
|
||||||
|
{
|
||||||
|
// TODO: using ImGui Node Editor impl
|
||||||
|
RenderNodeEditorPanel();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PBRMaterialEditor::RenderBasicEditorPanel()
|
||||||
|
{
|
||||||
|
auto& material = m_Asset;
|
||||||
|
// ==================== Parameters ====================
|
||||||
|
{
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("Albedo", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
|
{
|
||||||
|
ImGui::Indent();
|
||||||
|
|
||||||
|
if (ImGui::ColorEdit3("Color##Albedo", glm::value_ptr(material->AlbedoColor)))
|
||||||
|
{
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useAlbedoMap = (material->AlbedoTexToggle > 0.5f);
|
||||||
|
if (ImGui::Checkbox("Use Texture##Albedo", &useAlbedoMap))
|
||||||
|
{
|
||||||
|
material->AlbedoTexToggle = useAlbedoMap ? 1.0f : 0.0f;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawTextureSlot(material->AlbedoTexture, [&](const Ref<Texture2D>& newTex)
|
||||||
|
{
|
||||||
|
material->AlbedoTexture = newTex;
|
||||||
|
material->AlbedoTexToggle = true;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
});
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("Albedo Texture");
|
||||||
|
|
||||||
|
ImGui::Unindent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal Map
|
||||||
|
if (ImGui::CollapsingHeader("Normal Map", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
|
{
|
||||||
|
ImGui::Indent();
|
||||||
|
|
||||||
|
bool useNormalMap = (material->NormalTexToggle > 0.5f);
|
||||||
|
if (ImGui::Checkbox("Use Texture##Normal", &useNormalMap))
|
||||||
|
{
|
||||||
|
material->NormalTexToggle = useNormalMap ? 1.0f : 0.0f;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawTextureSlot(material->NormalTexture, [&](const Ref<Texture2D>& newTex)
|
||||||
|
{
|
||||||
|
material->NormalTexture = newTex;
|
||||||
|
material->NormalTexToggle = true;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
});
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("Normal Texture");
|
||||||
|
|
||||||
|
ImGui::Unindent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metalness
|
||||||
|
if (ImGui::CollapsingHeader("Metalness", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
|
{
|
||||||
|
ImGui::Indent();
|
||||||
|
|
||||||
|
if (ImGui::SliderFloat("Value##Metalness", &material->Metalness, 0.0f, 1.0f))
|
||||||
|
{
|
||||||
|
// 自动保存
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useMetalnessMap = (material->MetalnessTexToggle > 0.5f);
|
||||||
|
if (ImGui::Checkbox("Use Texture##Metalness", &useMetalnessMap))
|
||||||
|
{
|
||||||
|
material->MetalnessTexToggle = useMetalnessMap ? 1.0f : 0.0f;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawTextureSlot(material->MetalnessTexture, [&](const Ref<Texture2D>& newTex)
|
||||||
|
{
|
||||||
|
material->MetalnessTexture = newTex;
|
||||||
|
material->MetalnessTexToggle = true;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
});
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("Metalness Texture");
|
||||||
|
|
||||||
|
ImGui::Unindent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roughness
|
||||||
|
if (ImGui::CollapsingHeader("Roughness", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
|
{
|
||||||
|
ImGui::Indent();
|
||||||
|
|
||||||
|
if (ImGui::SliderFloat("Value##Roughness", &material->Roughness, 0.0f, 1.0f))
|
||||||
|
{
|
||||||
|
// 自动保存
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useRoughnessMap = (material->RoughnessTexToggle > 0.5f);
|
||||||
|
if (ImGui::Checkbox("Use Texture##Roughness", &useRoughnessMap))
|
||||||
|
{
|
||||||
|
material->RoughnessTexToggle = useRoughnessMap ? 1.0f : 0.0f;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawTextureSlot(material->RoughnessTexture, [&](const Ref<Texture2D>& newTex)
|
||||||
|
{
|
||||||
|
material->RoughnessTexture = newTex;
|
||||||
|
material->RoughnessTexToggle = true;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
});
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("Roughness Texture");
|
||||||
|
|
||||||
|
ImGui::Unindent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImColor PBRMaterialEditor::GetPinColor(int pinType)
|
||||||
|
{
|
||||||
|
switch (pinType)
|
||||||
|
{
|
||||||
|
case Albedo: return ImColor(220, 48, 48);
|
||||||
|
case Normal: return ImColor(68, 201, 156);
|
||||||
|
case Metallic: return ImColor(147, 226, 74);
|
||||||
|
case Roughness: return ImColor(124, 21, 153);
|
||||||
|
case AO: return ImColor(51, 150, 215);
|
||||||
|
default: return ImColor(255, 255, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::DrawPinIcon(bool connected, int alpha, int pinType)
|
||||||
|
{
|
||||||
|
ImColor color = GetPinColor(pinType);
|
||||||
|
color.Value.w = alpha / 255.0f;
|
||||||
|
ax::Widgets::Icon(ImVec2(24, 24), ax::Drawing::IconType::Circle, connected, color, ImColor(32, 32, 32, alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::DrawOutputPinIcon(bool connected, int alpha)
|
||||||
|
{
|
||||||
|
ax::Widgets::Icon(ImVec2(24, 24), ax::Drawing::IconType::Circle, connected, ImColor(255, 255, 255, alpha),
|
||||||
|
ImColor(32, 32, 32, alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
float PBRMaterialEditor::DrawNodeHeader(const char* title, float minWidth)
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted(title);
|
||||||
|
ImGui::Dummy(ImVec2(minWidth, 4.0f));
|
||||||
|
float headerBottomY = ImGui::GetCursorScreenPos().y;
|
||||||
|
ImGui::Dummy(ImVec2(0, 6.0f));
|
||||||
|
return headerBottomY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 节点编辑器面板
|
||||||
|
void PBRMaterialEditor::RenderNodeEditorPanel()
|
||||||
|
{
|
||||||
|
ed::SetCurrentEditor(m_EditorContext);
|
||||||
|
|
||||||
|
// 顶部工具栏
|
||||||
|
if (ImGui::Button("Add Texture Node"))
|
||||||
|
{
|
||||||
|
int newId = m_NextNodeId++;
|
||||||
|
float yOffset = m_TextureNodes.size() * 170.0f;
|
||||||
|
m_TextureNodes.push_back({newId, AssetHandle(0), 50.0f, yOffset});
|
||||||
|
m_FirstFrame = true;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Compile to Asset"))
|
||||||
|
{
|
||||||
|
CompileMaterialToAsset();
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ed::Begin("MaterialNodeEditor");
|
||||||
|
|
||||||
|
if (m_FirstFrame)
|
||||||
|
{
|
||||||
|
// 定位节点
|
||||||
|
ed::SetNodePosition(ed::NodeId(OUTPUT_NODE_ID), ImVec2(400, 100));
|
||||||
|
for (auto& node : m_TextureNodes)
|
||||||
|
ed::SetNodePosition(ed::NodeId(node.ID), ImVec2(node.PosX, node.PosY));
|
||||||
|
ed::NavigateToContent();
|
||||||
|
m_FirstFrame = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制输出节点
|
||||||
|
DrawOutputNode();
|
||||||
|
|
||||||
|
// 绘制所有纹理节点
|
||||||
|
for (auto& node : m_TextureNodes)
|
||||||
|
DrawTextureNode(node.ID, node.TextureHandle);
|
||||||
|
|
||||||
|
// 绘制连线
|
||||||
|
for (const auto& link : m_Links)
|
||||||
|
{
|
||||||
|
int endPinIndex = link.EndPinID % 100;
|
||||||
|
ImColor linkColor = GetPinColor(endPinIndex);
|
||||||
|
ed::Link(ed::LinkId(link.ID), ed::PinId(link.StartPinID), ed::PinId(link.EndPinID), linkColor, 2.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理交互
|
||||||
|
HandleNodeEditorInteractions();
|
||||||
|
|
||||||
|
|
||||||
|
ed::End();
|
||||||
|
ed::SetCurrentEditor(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::DrawOutputNode()
|
||||||
|
{
|
||||||
|
const float nodeWidth = 240.0f;
|
||||||
|
|
||||||
|
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(8, 4, 8, 8));
|
||||||
|
ed::BeginNode(ed::NodeId(OUTPUT_NODE_ID));
|
||||||
|
|
||||||
|
ImVec2 headerMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
float headerMaxY = DrawNodeHeader("PBR Material Output", nodeWidth);
|
||||||
|
|
||||||
|
// TODO: this will add preview texture
|
||||||
|
ImGui::Dummy(ImVec2(nodeWidth, 128));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// 输入引脚
|
||||||
|
auto drawInputPin = [&](PinType pinType, const char* label, auto valueWidget)
|
||||||
|
{
|
||||||
|
int pinId = InputPinID(pinType);
|
||||||
|
ed::BeginPin(ed::PinId(pinId), ed::PinKind::Input);
|
||||||
|
DrawPinIcon(IsPinLinked(pinId), 255, pinType);
|
||||||
|
ed::EndPin();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextUnformatted(label);
|
||||||
|
if (!IsPinLinked(pinId))
|
||||||
|
{
|
||||||
|
ImGui::SameLine();
|
||||||
|
valueWidget();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
drawInputPin(Albedo, "Albedo", [&]()
|
||||||
|
{
|
||||||
|
ImGui::SetNextItemWidth(80);
|
||||||
|
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||||
|
if (ImGui::ColorButton(
|
||||||
|
"##albedo", ImVec4(m_Asset->AlbedoColor.r, m_Asset->AlbedoColor.g, m_Asset->AlbedoColor.b, 1.0f),
|
||||||
|
ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoBorder, ImVec2(20, 20)))
|
||||||
|
{
|
||||||
|
m_OpenAlbedoPicker = true;
|
||||||
|
m_AlbedoPickerPos = btnPos;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
drawInputPin(Normal, "Normal", []() { ImGui::NewLine(); });
|
||||||
|
|
||||||
|
drawInputPin(Metallic, "Metallic", [&]()
|
||||||
|
{
|
||||||
|
ImGui::SetNextItemWidth(80);
|
||||||
|
if (ImGui::SliderFloat("##metallic", &m_Asset->Metalness, 0.0f, 1.0f))
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
drawInputPin(Roughness, "Roughness", [&]()
|
||||||
|
{
|
||||||
|
ImGui::SetNextItemWidth(80);
|
||||||
|
if (ImGui::SliderFloat("##roughness", &m_Asset->Roughness, 0.0f, 1.0f))
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
drawInputPin(AO, "AO", [&]()
|
||||||
|
{
|
||||||
|
ImGui::SetNextItemWidth(80);
|
||||||
|
float ao = 1.0f; // 如果资产没有 AO 字段可以忽略
|
||||||
|
if (ImGui::SliderFloat("##ao", &ao, 0.0f, 1.0f))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ImGui::EndGroup();
|
||||||
|
ImVec2 groupMin = ImGui::GetItemRectMin();
|
||||||
|
ImVec2 groupMax = ImGui::GetItemRectMax();
|
||||||
|
ed::EndNode();
|
||||||
|
|
||||||
|
// 绘制标题栏背景
|
||||||
|
float pad = 7.0f;
|
||||||
|
auto* drawList = ed::GetNodeBackgroundDrawList(ed::NodeId(OUTPUT_NODE_ID));
|
||||||
|
drawList->AddRectFilled(
|
||||||
|
ImVec2(groupMin.x - pad, headerMin.y - 3.0f),
|
||||||
|
ImVec2(groupMax.x + pad, headerMaxY),
|
||||||
|
IM_COL32(128, 16, 16, 230), 12.0f, ImDrawFlags_RoundCornersTop);
|
||||||
|
|
||||||
|
ed::PopStyleVar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::DrawTextureNode(int nodeId, AssetHandle& textureHandle)
|
||||||
|
{
|
||||||
|
const float nodeWidth = 120.0f;
|
||||||
|
|
||||||
|
ImGui::PushID(nodeId);
|
||||||
|
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(8, 4, 8, 8));
|
||||||
|
ed::BeginNode(ed::NodeId(nodeId));
|
||||||
|
|
||||||
|
ImVec2 headerMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
float headerMaxY = DrawNodeHeader("Texture2D", nodeWidth);
|
||||||
|
|
||||||
|
// 纹理预览
|
||||||
|
bool hasTexture = (textureHandle != 0);
|
||||||
|
float previewSize = 80.0f;
|
||||||
|
float offsetX = (nodeWidth - previewSize) * 0.5f;
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
|
||||||
|
|
||||||
|
if (hasTexture)
|
||||||
|
{
|
||||||
|
Ref<Texture2D> tex = AssetsManager::GetAsset<Texture2D>(textureHandle);
|
||||||
|
if (tex)
|
||||||
|
ImGui::Image((ImTextureID)(intptr_t)tex->GetRendererID(), ImVec2(previewSize, previewSize), ImVec2(0,1), ImVec2(1,0));
|
||||||
|
else
|
||||||
|
ImGui::Dummy(ImVec2(previewSize, previewSize));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
// 修正:显式构造 ImVec2,避免依赖运算符重载
|
||||||
|
ImVec2 pos_end = ImVec2(pos.x + previewSize, pos.y + previewSize);
|
||||||
|
dl->AddRectFilled(pos, pos_end, IM_COL32(50,50,50,255), 4.0f);
|
||||||
|
dl->AddRect(pos, pos_end, IM_COL32(90,90,90,255), 4.0f);
|
||||||
|
ImGui::Dummy(ImVec2(previewSize, previewSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽接收
|
||||||
|
if (ImGui::BeginDragDropTarget())
|
||||||
|
{
|
||||||
|
if (const auto* payload = ImGui::AcceptDragDropPayload("asset_payload"))
|
||||||
|
{
|
||||||
|
AssetHandle handle = *(AssetHandle*)payload->Data;
|
||||||
|
if (AssetsManager::IsAssetType(handle, AssetType::Texture))
|
||||||
|
{
|
||||||
|
textureHandle = handle;
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDragDropTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// 输出引脚(右对齐)
|
||||||
|
float pinOffset = nodeWidth - 50.0f;
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + pinOffset);
|
||||||
|
ImGui::TextUnformatted("Out");
|
||||||
|
ImGui::SameLine();
|
||||||
|
int outPinId = OutputPinID(nodeId);
|
||||||
|
ed::BeginPin(ed::PinId(outPinId), ed::PinKind::Output);
|
||||||
|
DrawOutputPinIcon(IsPinLinked(outPinId), 255);
|
||||||
|
ed::EndPin();
|
||||||
|
|
||||||
|
ImGui::EndGroup();
|
||||||
|
ImVec2 groupMin = ImGui::GetItemRectMin();
|
||||||
|
ImVec2 groupMax = ImGui::GetItemRectMax();
|
||||||
|
ed::EndNode();
|
||||||
|
|
||||||
|
float pad = 8.0f;
|
||||||
|
auto* bgDrawList = ed::GetNodeBackgroundDrawList(ed::NodeId(nodeId));
|
||||||
|
bgDrawList->AddRectFilled(
|
||||||
|
ImVec2(groupMin.x - pad, headerMin.y - 4.0f),
|
||||||
|
ImVec2(groupMax.x + pad, headerMaxY),
|
||||||
|
IM_COL32(0, 80, 120, 230), 12.0f, ImDrawFlags_RoundCornersTop);
|
||||||
|
|
||||||
|
ed::PopStyleVar();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::HandleNodeEditorInteractions()
|
||||||
|
{
|
||||||
|
// ========== 1. 创建连线 ==========
|
||||||
|
if (ed::BeginCreate())
|
||||||
|
{
|
||||||
|
ed::PinId startId, endId;
|
||||||
|
if (ed::QueryNewLink(&startId, &endId))
|
||||||
|
{
|
||||||
|
if (startId && endId)
|
||||||
|
{
|
||||||
|
int start = (int)startId.Get();
|
||||||
|
int end = (int)endId.Get();
|
||||||
|
|
||||||
|
bool startIsOutput = (start / 100 >= 2) && (start % 100 == 1);
|
||||||
|
bool endIsInput = (end / 100 == OUTPUT_NODE_ID) && (end % 100 >= 1 && end % 100 <= 5);
|
||||||
|
|
||||||
|
if (!startIsOutput || !endIsInput)
|
||||||
|
{
|
||||||
|
std::swap(start, end);
|
||||||
|
startIsOutput = (start / 100 >= 2) && (start % 100 == 1);
|
||||||
|
endIsInput = (end / 100 == OUTPUT_NODE_ID) && (end % 100 >= 1 && end % 100 <= 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIsOutput && endIsInput)
|
||||||
|
{
|
||||||
|
int pinType = end % 100;
|
||||||
|
if (!IsPinLinked(end))
|
||||||
|
{
|
||||||
|
if (ed::AcceptNewItem(GetPinColor(pinType), 2.0f))
|
||||||
|
{
|
||||||
|
m_Links.push_back({m_NextLinkId++, start, end});
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ed::RejectNewItem(ImVec4(1, 0, 0, 1), 2.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ed::RejectNewItem(ImVec4(1, 0, 0, 1), 2.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ed::EndCreate();
|
||||||
|
|
||||||
|
// ========== 2. 删除连线/节点 ==========
|
||||||
|
if (ed::BeginDelete())
|
||||||
|
{
|
||||||
|
ed::LinkId linkId;
|
||||||
|
while (ed::QueryDeletedLink(&linkId))
|
||||||
|
{
|
||||||
|
if (ed::AcceptDeletedItem())
|
||||||
|
{
|
||||||
|
int id = (int)linkId.Get();
|
||||||
|
m_Links.erase(std::remove_if(m_Links.begin(), m_Links.end(),
|
||||||
|
[id](const Link& l) { return l.ID == id; }), m_Links.end());
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ed::NodeId nodeId;
|
||||||
|
while (ed::QueryDeletedNode(&nodeId))
|
||||||
|
{
|
||||||
|
int id = (int)nodeId.Get();
|
||||||
|
if (id == OUTPUT_NODE_ID)
|
||||||
|
ed::RejectDeletedItem();
|
||||||
|
else if (ed::AcceptDeletedItem())
|
||||||
|
{
|
||||||
|
int outPin = OutputPinID(id);
|
||||||
|
m_Links.erase(std::remove_if(m_Links.begin(), m_Links.end(),
|
||||||
|
[outPin](const Link& l) { return l.StartPinID == outPin; }),
|
||||||
|
m_Links.end());
|
||||||
|
m_TextureNodes.erase(std::remove_if(m_TextureNodes.begin(), m_TextureNodes.end(),
|
||||||
|
[id](const TextureNode& n) { return n.ID == id; }),
|
||||||
|
m_TextureNodes.end());
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ed::EndDelete();
|
||||||
|
|
||||||
|
|
||||||
|
ed::Suspend();
|
||||||
|
|
||||||
|
if (ed::ShowBackgroundContextMenu())
|
||||||
|
ImGui::OpenPopup("NodeEditorContextMenu");
|
||||||
|
|
||||||
|
ed::NodeId contextNodeId;
|
||||||
|
if (ed::ShowNodeContextMenu(&contextNodeId))
|
||||||
|
{
|
||||||
|
m_ContextMenuNodeId = (int)contextNodeId.Get();
|
||||||
|
ImGui::OpenPopup("NodeContextMenu");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (ImGui::BeginPopup("NodeEditorContextMenu"))
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem("Add Texture2D Node"))
|
||||||
|
{
|
||||||
|
int newId = m_NextNodeId++;
|
||||||
|
float yOffset = m_TextureNodes.size() * 170.0f;
|
||||||
|
m_TextureNodes.push_back({newId, AssetHandle(0), 50.0f, yOffset});
|
||||||
|
m_FirstFrame = true;
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginPopup("NodeContextMenu"))
|
||||||
|
{
|
||||||
|
int id = m_ContextMenuNodeId;
|
||||||
|
if (id != OUTPUT_NODE_ID)
|
||||||
|
{
|
||||||
|
if (ImGui::MenuItem("Delete Node"))
|
||||||
|
{
|
||||||
|
int outPin = OutputPinID(id);
|
||||||
|
m_Links.erase(std::remove_if(m_Links.begin(), m_Links.end(),
|
||||||
|
[outPin](const Link& l) { return l.StartPinID == outPin; }),
|
||||||
|
m_Links.end());
|
||||||
|
m_TextureNodes.erase(std::remove_if(m_TextureNodes.begin(), m_TextureNodes.end(),
|
||||||
|
[id](const TextureNode& n) { return n.ID == id; }),
|
||||||
|
m_TextureNodes.end());
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ImGui::TextDisabled("Cannot delete output node");
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_OpenAlbedoPicker)
|
||||||
|
{
|
||||||
|
ImGui::OpenPopup("AlbedoColorPicker");
|
||||||
|
m_OpenAlbedoPicker = false;
|
||||||
|
}
|
||||||
|
if (ImGui::BeginPopup("AlbedoColorPicker"))
|
||||||
|
{
|
||||||
|
if (ImGui::ColorPicker3("##picker", glm::value_ptr(m_Asset->AlbedoColor)))
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ed::Resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PBRMaterialEditor::IsPinLinked(int pinId) const
|
||||||
|
{
|
||||||
|
for (const auto& link : m_Links)
|
||||||
|
if (link.StartPinID == pinId || link.EndPinID == pinId)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetHandle PBRMaterialEditor::GetLinkedTextureHandle(int inputPinId) const
|
||||||
|
{
|
||||||
|
for (const auto& link : m_Links)
|
||||||
|
{
|
||||||
|
if (link.EndPinID == inputPinId)
|
||||||
|
{
|
||||||
|
int nodeId = link.StartPinID / 100;
|
||||||
|
for (const auto& node : m_TextureNodes)
|
||||||
|
if (node.ID == nodeId)
|
||||||
|
return node.TextureHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AssetHandle(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::CompileMaterialToAsset()
|
||||||
|
{
|
||||||
|
if (!m_Asset) return;
|
||||||
|
|
||||||
|
auto getTexture = [](AssetHandle handle) -> Ref<Texture2D> {
|
||||||
|
if (handle == 0) return nullptr;
|
||||||
|
return AssetsManager::GetAsset<Texture2D>(handle);
|
||||||
|
};
|
||||||
|
|
||||||
|
m_Asset->AlbedoTexture = getTexture(GetLinkedTextureHandle(InputPinID(Albedo)));
|
||||||
|
m_Asset->AlbedoTexToggle = m_Asset->AlbedoTexture ? 1.0f : 0.0f;
|
||||||
|
|
||||||
|
m_Asset->NormalTexture = getTexture(GetLinkedTextureHandle(InputPinID(Normal)));
|
||||||
|
m_Asset->NormalTexToggle = m_Asset->NormalTexture ? 1.0f : 0.0f;
|
||||||
|
|
||||||
|
m_Asset->MetalnessTexture = getTexture(GetLinkedTextureHandle(InputPinID(Metallic)));
|
||||||
|
m_Asset->MetalnessTexToggle = m_Asset->MetalnessTexture ? 1.0f : 0.0f;
|
||||||
|
|
||||||
|
m_Asset->RoughnessTexture = getTexture(GetLinkedTextureHandle(InputPinID(Roughness)));
|
||||||
|
m_Asset->RoughnessTexToggle = m_Asset->RoughnessTexture ? 1.0f : 0.0f;
|
||||||
|
|
||||||
|
m_Asset->IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PBRMaterialEditor::LoadAssetToGraph()
|
||||||
|
{
|
||||||
|
if (!m_Asset) return;
|
||||||
|
|
||||||
|
m_TextureNodes.clear();
|
||||||
|
m_Links.clear();
|
||||||
|
m_NextNodeId = 2;
|
||||||
|
m_NextLinkId = 1;
|
||||||
|
|
||||||
|
// 辅助 lambda:从 Ref<Texture2D> 获取 AssetHandle
|
||||||
|
auto getHandle = [](const Ref<Texture2D>& tex) -> AssetHandle {
|
||||||
|
return tex ? tex->Handle : AssetHandle(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addTextureNode = [&](AssetHandle handle, PinType pinType, float yPos) {
|
||||||
|
if (handle != 0)
|
||||||
|
{
|
||||||
|
int id = m_NextNodeId++;
|
||||||
|
m_TextureNodes.push_back({id, handle, 50.0f, yPos});
|
||||||
|
m_Links.push_back({m_NextLinkId++, OutputPinID(id), InputPinID(pinType)});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addTextureNode(getHandle(m_Asset->AlbedoTexture), Albedo, 0.0f);
|
||||||
|
addTextureNode(getHandle(m_Asset->NormalTexture), Normal, 150.0f);
|
||||||
|
addTextureNode(getHandle(m_Asset->MetalnessTexture), Metallic, 300.0f);
|
||||||
|
addTextureNode(getHandle(m_Asset->RoughnessTexture), Roughness, 450.0f);
|
||||||
|
|
||||||
|
m_FirstFrame = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// Created by Atdunbg on 2026/2/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef PRISM_PBRMATERIALASSETEDITOR_H
|
||||||
|
#define PRISM_PBRMATERIALASSETEDITOR_H
|
||||||
|
|
||||||
|
#include "../AssetEditorPanel.h"
|
||||||
|
#include "Prism/Renderer/Texture.h"
|
||||||
|
#include "imgui_node_editor.h"
|
||||||
|
|
||||||
|
namespace ed = ax::NodeEditor;
|
||||||
|
|
||||||
|
namespace Prism
|
||||||
|
{
|
||||||
|
class PBRMaterialEditor : public AssetEditor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PBRMaterialEditor();
|
||||||
|
~PBRMaterialEditor();
|
||||||
|
|
||||||
|
Ref<Asset> GetAsset() override { return m_Asset; }
|
||||||
|
void SetAsset(const Ref<Asset>& asset) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 原有成员
|
||||||
|
Ref<PBRMaterialAsset> m_Asset;
|
||||||
|
ed::EditorContext* m_EditorContext = nullptr;
|
||||||
|
|
||||||
|
// 原有渲染函数
|
||||||
|
void Render() override;
|
||||||
|
void RenderBasicEditorPanel();
|
||||||
|
|
||||||
|
// ---------- 节点编辑器相关 ----------
|
||||||
|
void RenderNodeEditorPanel(); // 新的节点编辑器面板
|
||||||
|
void DrawOutputNode();
|
||||||
|
void DrawTextureNode(int nodeId, AssetHandle& textureHandle);
|
||||||
|
void HandleNodeEditorInteractions();
|
||||||
|
bool IsPinLinked(int pinId) const;
|
||||||
|
AssetHandle GetLinkedTextureHandle(int inputPinId) const;
|
||||||
|
void CompileMaterialToAsset(); // 将节点图数据写回 m_Asset
|
||||||
|
void LoadAssetToGraph(); // 从 m_Asset 加载数据到节点图
|
||||||
|
|
||||||
|
// 节点编辑器数据
|
||||||
|
static constexpr int OUTPUT_NODE_ID = 1;
|
||||||
|
int m_NextNodeId = 2;
|
||||||
|
int m_NextLinkId = 1;
|
||||||
|
|
||||||
|
struct TextureNode
|
||||||
|
{
|
||||||
|
int ID;
|
||||||
|
AssetHandle TextureHandle;
|
||||||
|
float PosX, PosY;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TextureNode> m_TextureNodes;
|
||||||
|
|
||||||
|
struct Link
|
||||||
|
{
|
||||||
|
int ID;
|
||||||
|
int StartPinID;
|
||||||
|
int EndPinID;
|
||||||
|
};
|
||||||
|
std::vector<Link> m_Links;
|
||||||
|
|
||||||
|
// 编辑器状态
|
||||||
|
bool m_FirstFrame = true;
|
||||||
|
bool m_OpenAlbedoPicker = false;
|
||||||
|
ImVec2 m_AlbedoPickerPos;
|
||||||
|
int m_ContextMenuNodeId = 0;
|
||||||
|
|
||||||
|
// 引脚ID辅助函数
|
||||||
|
static int InputPinID(int type) { return OUTPUT_NODE_ID * 100 + type; }
|
||||||
|
static int OutputPinID(int nodeId) { return nodeId * 100 + 1; }
|
||||||
|
|
||||||
|
// 引脚类型枚举(与PBR材质对应)
|
||||||
|
enum PinType
|
||||||
|
{
|
||||||
|
Albedo = 1,
|
||||||
|
Normal,
|
||||||
|
Metallic,
|
||||||
|
Roughness,
|
||||||
|
AO
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从 MaterialEditorPanel 借用的工具函数
|
||||||
|
static ImColor GetPinColor(int pinType);
|
||||||
|
static void DrawPinIcon(bool connected, int alpha, int pinType);
|
||||||
|
static void DrawOutputPinIcon(bool connected, int alpha);
|
||||||
|
float DrawNodeHeader(const char* title, float minWidth);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //PRISM_PBRMATERIALASSETEDITOR_H
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// Created by Atdunbg on 2026/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "PhysicsMaterialEditor.h"
|
||||||
|
|
||||||
|
#include "Prism/Asset/AssetSerializer.h"
|
||||||
|
|
||||||
|
namespace Prism
|
||||||
|
{
|
||||||
|
|
||||||
|
PhysicsMaterialEditor::PhysicsMaterialEditor()
|
||||||
|
: AssetEditor("Edit Physics Material") {}
|
||||||
|
|
||||||
|
void PhysicsMaterialEditor::SetAsset(const Ref<Asset>& asset)
|
||||||
|
{
|
||||||
|
if (m_Asset) AssetSerializer::SerializeAsset(m_Asset);
|
||||||
|
m_Asset = static_cast<Ref<PhysicsMaterialAsset>>(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsMaterialEditor::Render()
|
||||||
|
{
|
||||||
|
if (!m_Asset)
|
||||||
|
SetOpen(false);
|
||||||
|
|
||||||
|
UI::BeginPropertyGrid();
|
||||||
|
UI::Property("Static Friction", m_Asset->StaticFriction);
|
||||||
|
UI::Property("Dynamic Friction", m_Asset->DynamicFriction);
|
||||||
|
UI::Property("Bounciness", m_Asset->Bounciness);
|
||||||
|
UI::EndPropertyGrid();
|
||||||
|
|
||||||
|
if (ImGui::Button("Save"))
|
||||||
|
AssetSerializer::SerializeAsset(m_Asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// Created by Atdunbg on 2026/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef PRISM_PHYSICSMATERIALEDITOR_H
|
||||||
|
#define PRISM_PHYSICSMATERIALEDITOR_H
|
||||||
|
#include "Prism/Core/Ref.h"
|
||||||
|
#include "Prism/Editor/AssetEditorPanel.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace Prism
|
||||||
|
{
|
||||||
|
class PhysicsMaterialAsset;
|
||||||
|
|
||||||
|
class PhysicsMaterialEditor : public AssetEditor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PhysicsMaterialEditor();
|
||||||
|
|
||||||
|
Ref<Asset> GetAsset() override { return m_Asset; }
|
||||||
|
void SetAsset(const Ref<Asset>& asset) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Render() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ref<PhysicsMaterialAsset> m_Asset;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif //PRISM_PHYSICSMATERIALEDITOR_H
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// Created by Atdunbg on 2026/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "TextureAssetEditor.h"
|
||||||
|
|
||||||
|
#include "Prism/Renderer/Texture.h"
|
||||||
|
#include "Prism/Asset/AssetSerializer.h"
|
||||||
|
|
||||||
|
namespace Prism
|
||||||
|
{
|
||||||
|
|
||||||
|
TextureViewer::TextureViewer()
|
||||||
|
: AssetEditor("Edit Texture")
|
||||||
|
{
|
||||||
|
SetMinSize(200, 600);
|
||||||
|
SetMaxSize(500, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureViewer::SetAsset(const Ref<Asset>& asset)
|
||||||
|
{
|
||||||
|
if (m_Asset) AssetSerializer::SerializeAsset(m_Asset);
|
||||||
|
m_Asset = static_cast<Ref<Texture>>(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureViewer::Render()
|
||||||
|
{
|
||||||
|
if (!m_Asset)
|
||||||
|
SetOpen(false);
|
||||||
|
|
||||||
|
float textureWidth = (float)m_Asset->GetWidth();
|
||||||
|
float textureHeight = (float)m_Asset->GetHeight();
|
||||||
|
float bitsPerPixel = (float)Texture::GetBPP(m_Asset->GetFormat());
|
||||||
|
float imageSize = ImGui::GetWindowWidth() - 40;
|
||||||
|
imageSize = glm::min(imageSize, 500.0f);
|
||||||
|
|
||||||
|
ImGui::SetCursorPosX(20);
|
||||||
|
ImGui::Image((ImTextureID)m_Asset->GetRendererID(), {imageSize, imageSize});
|
||||||
|
|
||||||
|
UI::BeginPropertyGrid();
|
||||||
|
UI::Property("Width", textureWidth, 0.1f, 0.0f, 0.0f, true);
|
||||||
|
UI::Property("Height", textureHeight, 0.1f, 0.0f, 0.0f, true);
|
||||||
|
UI::Property("Bits", bitsPerPixel, 0.1f, 0.0f, 0.0f, true);
|
||||||
|
UI::EndPropertyGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// Created by Atdunbg on 2026/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef PRISM_TEXTUREASSETEDITOR_H
|
||||||
|
#define PRISM_TEXTUREASSETEDITOR_H
|
||||||
|
#include "Prism/Editor/AssetEditorPanel.h"
|
||||||
|
|
||||||
|
namespace Prism
|
||||||
|
{
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
class TextureViewer : public AssetEditor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TextureViewer();
|
||||||
|
|
||||||
|
Ref<Asset> GetAsset() override { return m_Asset; }
|
||||||
|
void SetAsset(const Ref<Asset>& asset) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Render() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ref<Texture> m_Asset;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif //PRISM_TEXTUREASSETEDITOR_H
|
||||||
1
Prism/vendor/imgui-node-editor
vendored
Submodule
1
Prism/vendor/imgui-node-editor
vendored
Submodule
Submodule Prism/vendor/imgui-node-editor added at 021aa0ea4d
Reference in New Issue
Block a user