469 lines
16 KiB
C++
469 lines
16 KiB
C++
#include <array>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
|
|
#include <callbackObject.h>
|
|
#include <displayRegion.h>
|
|
#include <graphicsWindow.h>
|
|
#include <imgui.h>
|
|
#include <imgui_impl_opengl3.h>
|
|
#include <imgui_impl_win32.h>
|
|
#include <lineSegs.h>
|
|
#include <load_prc_file.h>
|
|
#include <luse.h>
|
|
#include <nodePath.h>
|
|
#include <pandaFramework.h>
|
|
#include <windowProperties.h>
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
|
|
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
void log_step(const std::string& message) {
|
|
std::cout << "[PandaCubeEditorDemo] " << message << std::endl;
|
|
}
|
|
|
|
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
|
|
|
|
struct DemoState {
|
|
float camera_x = 0.0F;
|
|
float camera_y = -8.0F;
|
|
float camera_z = 2.0F;
|
|
float heading = 0.0F;
|
|
float pitch = -10.0F;
|
|
float rotation_speed = 20.0F;
|
|
bool auto_rotate = true;
|
|
bool show_demo_window = false;
|
|
bool overlay_enabled = true;
|
|
float model_position[3] = {0.0F, 0.0F, 0.0F};
|
|
float model_rotation[3] = {0.0F, 0.0F, 0.0F};
|
|
float model_scale[3] = {1.0F, 1.0F, 1.0F};
|
|
bool using_fallback_cube = false;
|
|
};
|
|
|
|
WNDPROC g_previous_wnd_proc = nullptr;
|
|
ImDrawData* g_pending_draw_data = nullptr;
|
|
HWND g_panda_hwnd = nullptr;
|
|
PT(DisplayRegion) g_overlay_region = nullptr;
|
|
|
|
class ImGuiOverlayDrawCallback final : public CallbackObject {
|
|
public:
|
|
void do_callback(CallbackData* cbdata) override {
|
|
(void)cbdata;
|
|
if (g_pending_draw_data != nullptr) {
|
|
ImGui_ImplOpenGL3_RenderDrawData(g_pending_draw_data);
|
|
}
|
|
}
|
|
};
|
|
|
|
LRESULT CALLBACK panda_imgui_wnd_proc(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param) {
|
|
if (ImGui_ImplWin32_WndProcHandler(hwnd, message, w_param, l_param)) {
|
|
return 1;
|
|
}
|
|
if (g_previous_wnd_proc != nullptr) {
|
|
return CallWindowProc(g_previous_wnd_proc, hwnd, message, w_param, l_param);
|
|
}
|
|
return DefWindowProc(hwnd, message, w_param, l_param);
|
|
}
|
|
|
|
void configure_runtime_environment() {
|
|
const auto panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin";
|
|
const auto panda_etc = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "etc";
|
|
if (std::filesystem::exists(panda_bin)) {
|
|
SetDllDirectoryW(panda_bin.wstring().c_str());
|
|
|
|
const DWORD required = GetEnvironmentVariableW(L"PATH", nullptr, 0);
|
|
std::wstring existing_path(required > 0 ? required - 1 : 0, L'\0');
|
|
if (required > 0) {
|
|
GetEnvironmentVariableW(L"PATH", existing_path.data(), required);
|
|
}
|
|
const std::wstring new_path = panda_bin.wstring() + L";" + existing_path;
|
|
SetEnvironmentVariableW(L"PATH", new_path.c_str());
|
|
}
|
|
if (std::filesystem::exists(panda_etc)) {
|
|
SetEnvironmentVariableW(L"PANDA_PRC_DIR", panda_etc.wstring().c_str());
|
|
}
|
|
|
|
wchar_t exe_path_buffer[MAX_PATH];
|
|
const DWORD path_length = GetModuleFileNameW(nullptr, exe_path_buffer, MAX_PATH);
|
|
if (path_length > 0 && path_length < MAX_PATH) {
|
|
std::filesystem::current_path(std::filesystem::path(exe_path_buffer).parent_path());
|
|
}
|
|
}
|
|
|
|
void configure_local_prc() {
|
|
wchar_t exe_path_buffer[MAX_PATH];
|
|
const DWORD path_length = GetModuleFileNameW(nullptr, exe_path_buffer, MAX_PATH);
|
|
if (path_length == 0 || path_length >= MAX_PATH) {
|
|
return;
|
|
}
|
|
|
|
const std::filesystem::path exe_dir = std::filesystem::path(exe_path_buffer).parent_path();
|
|
const std::filesystem::path prc_path = exe_dir / "PandaCubeEditorDemo.prc";
|
|
const std::filesystem::path panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin";
|
|
std::ofstream prc_file(prc_path, std::ios::trunc);
|
|
prc_file
|
|
<< "plugin-path " << panda_bin.generic_string() << "\n"
|
|
<< "load-display pandagl\n"
|
|
<< "aux-display p3tinydisplay\n"
|
|
<< "load-file-type p3assimp\n"
|
|
<< "load-file-type p3ptloader\n"
|
|
<< "window-type onscreen\n"
|
|
<< "win-size 1280 720\n"
|
|
<< "sync-video #f\n"
|
|
<< "audio-library-name null\n";
|
|
prc_file.close();
|
|
load_prc_file(Filename::from_os_specific(prc_path.string()));
|
|
}
|
|
|
|
NodePath create_wire_cube(const NodePath& parent) {
|
|
LineSegs segments("editor-demo-cube");
|
|
segments.set_thickness(2.5F);
|
|
segments.set_color(0.85F, 0.9F, 1.0F, 1.0F);
|
|
|
|
const LPoint3f corners[] = {
|
|
{-0.5F, -0.5F, -0.5F},
|
|
{0.5F, -0.5F, -0.5F},
|
|
{0.5F, 0.5F, -0.5F},
|
|
{-0.5F, 0.5F, -0.5F},
|
|
{-0.5F, -0.5F, 0.5F},
|
|
{0.5F, -0.5F, 0.5F},
|
|
{0.5F, 0.5F, 0.5F},
|
|
{-0.5F, 0.5F, 0.5F},
|
|
};
|
|
const int edges[][2] = {
|
|
{0, 1}, {1, 2}, {2, 3}, {3, 0},
|
|
{4, 5}, {5, 6}, {6, 7}, {7, 4},
|
|
{0, 4}, {1, 5}, {2, 6}, {3, 7},
|
|
};
|
|
|
|
for (const auto& edge : edges) {
|
|
segments.move_to(corners[edge[0]]);
|
|
segments.draw_to(corners[edge[1]]);
|
|
}
|
|
|
|
return parent.attach_new_node(segments.create());
|
|
}
|
|
|
|
NodePath create_world_axes(const NodePath& parent) {
|
|
LineSegs segments("world-axes");
|
|
segments.set_thickness(3.0F);
|
|
|
|
segments.set_color(0.95F, 0.2F, 0.2F, 1.0F);
|
|
segments.move_to(0.0F, 0.0F, 0.0F);
|
|
segments.draw_to(2.0F, 0.0F, 0.0F);
|
|
|
|
segments.set_color(0.2F, 0.95F, 0.35F, 1.0F);
|
|
segments.move_to(0.0F, 0.0F, 0.0F);
|
|
segments.draw_to(0.0F, 2.0F, 0.0F);
|
|
|
|
segments.set_color(0.25F, 0.55F, 1.0F, 1.0F);
|
|
segments.move_to(0.0F, 0.0F, 0.0F);
|
|
segments.draw_to(0.0F, 0.0F, 2.0F);
|
|
|
|
return parent.attach_new_node(segments.create());
|
|
}
|
|
|
|
NodePath create_ground_grid(const NodePath& parent) {
|
|
LineSegs segments("ground-grid");
|
|
segments.set_thickness(1.0F);
|
|
segments.set_color(0.35F, 0.38F, 0.42F, 0.85F);
|
|
|
|
constexpr int half_extent = 10;
|
|
for (int i = -half_extent; i <= half_extent; ++i) {
|
|
segments.move_to(static_cast<float>(i), -static_cast<float>(half_extent), 0.0F);
|
|
segments.draw_to(static_cast<float>(i), static_cast<float>(half_extent), 0.0F);
|
|
segments.move_to(-static_cast<float>(half_extent), static_cast<float>(i), 0.0F);
|
|
segments.draw_to(static_cast<float>(half_extent), static_cast<float>(i), 0.0F);
|
|
}
|
|
|
|
return parent.attach_new_node(segments.create());
|
|
}
|
|
|
|
NodePath load_demo_model(WindowFramework* window, DemoState& state) {
|
|
const Filename model_path = Filename::from_os_specific("D:/MetaCore/box1.glb");
|
|
NodePath model = window->load_model(window->get_render(), model_path);
|
|
if (!model.is_empty()) {
|
|
log_step("Loaded model: D:/MetaCore/box1.glb");
|
|
state.using_fallback_cube = false;
|
|
return model;
|
|
}
|
|
|
|
log_step("Failed to load box1.glb, falling back to wire cube");
|
|
state.using_fallback_cube = true;
|
|
NodePath fallback = create_wire_cube(window->get_render());
|
|
fallback.set_scale(1.5F);
|
|
return fallback;
|
|
}
|
|
|
|
void focus_camera_on_model(const NodePath& model, DemoState& state) {
|
|
LPoint3 min_point;
|
|
LPoint3 max_point;
|
|
if (!model.calc_tight_bounds(min_point, max_point)) {
|
|
state.camera_x = 0.0F;
|
|
state.camera_y = -8.0F;
|
|
state.camera_z = 2.0F;
|
|
state.model_position[0] = 0.0F;
|
|
state.model_position[1] = 0.0F;
|
|
state.model_position[2] = 0.0F;
|
|
return;
|
|
}
|
|
|
|
const LPoint3 center(
|
|
(min_point[0] + max_point[0]) * 0.5F,
|
|
(min_point[1] + max_point[1]) * 0.5F,
|
|
(min_point[2] + max_point[2]) * 0.5F
|
|
);
|
|
const LVecBase3 extent = max_point - min_point;
|
|
const float max_extent = (std::max)((std::max)(std::fabs(extent[0]), std::fabs(extent[1])), std::fabs(extent[2]));
|
|
const float distance = (std::max)(3.0F, max_extent * 3.0F);
|
|
|
|
state.model_position[0] = -center[0];
|
|
state.model_position[1] = -center[1];
|
|
state.model_position[2] = -center[2];
|
|
state.camera_x = 0.0F;
|
|
state.camera_y = -distance;
|
|
state.camera_z = (std::max)(1.5F, max_extent * 0.9F);
|
|
state.heading = 0.0F;
|
|
state.pitch = -10.0F;
|
|
|
|
log_step(
|
|
"Model bounds centered. Extent=" + std::to_string(max_extent) +
|
|
" camera_y=" + std::to_string(state.camera_y) +
|
|
" camera_z=" + std::to_string(state.camera_z)
|
|
);
|
|
}
|
|
|
|
std::uintptr_t native_window_handle(WindowFramework* window) {
|
|
if (window == nullptr || window->get_graphics_window() == nullptr) {
|
|
return 0;
|
|
}
|
|
if (WindowHandle* handle = window->get_graphics_window()->get_window_handle(); handle != nullptr) {
|
|
if (WindowHandle::OSHandle* os_handle = handle->get_os_handle(); os_handle != nullptr) {
|
|
return static_cast<std::uintptr_t>(os_handle->get_int_handle());
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void render_editor_ui(DemoState& state) {
|
|
if (!state.overlay_enabled) {
|
|
return;
|
|
}
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.78F);
|
|
ImGui::SetNextWindowPos(ImVec2(16.0F, 16.0F), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(360.0F, 0.0F), ImGuiCond_Always);
|
|
if (ImGui::Begin("MetaCore Editor Overlay", nullptr,
|
|
ImGuiWindowFlags_NoCollapse |
|
|
ImGuiWindowFlags_NoSavedSettings)) {
|
|
ImGui::TextUnformatted("Single-window Panda3D + ImGui test");
|
|
ImGui::Separator();
|
|
ImGui::Checkbox("Overlay Enabled", &state.overlay_enabled);
|
|
ImGui::Checkbox("Auto Rotate", &state.auto_rotate);
|
|
ImGui::Checkbox("Show ImGui Demo", &state.show_demo_window);
|
|
ImGui::Text("Model: %s", state.using_fallback_cube ? "Fallback Wire Cube" : "box1.glb");
|
|
ImGui::SliderFloat("Rotation Speed", &state.rotation_speed, 0.0F, 180.0F, "%.1f deg/s");
|
|
ImGui::DragFloat3("Model Position", state.model_position, 0.05F);
|
|
ImGui::DragFloat3("Model Rotation", state.model_rotation, 1.0F);
|
|
ImGui::DragFloat3("Model Scale", state.model_scale, 0.02F, 0.05F, 10.0F);
|
|
ImGui::DragFloat3("Camera Position", &state.camera_x, 0.05F);
|
|
ImGui::SliderFloat("Heading", &state.heading, -180.0F, 180.0F, "%.1f");
|
|
ImGui::SliderFloat("Pitch", &state.pitch, -89.0F, 89.0F, "%.1f");
|
|
ImGui::Separator();
|
|
ImGui::TextUnformatted("Controls");
|
|
ImGui::BulletText("W/A/S/D move");
|
|
ImGui::BulletText("R/F up and down");
|
|
ImGui::BulletText("Arrow keys look");
|
|
ImGui::BulletText("Esc closes");
|
|
ImGui::TextUnformatted("This ImGui panel is rendered over the Panda viewport.");
|
|
}
|
|
ImGui::End();
|
|
|
|
if (state.show_demo_window) {
|
|
ImGui::ShowDemoWindow(&state.show_demo_window);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
|
|
log_step("Starting PandaCubeEditorDemo");
|
|
configure_runtime_environment();
|
|
configure_local_prc();
|
|
|
|
PandaFramework framework;
|
|
framework.open_framework();
|
|
framework.set_window_title("MetaCore Single-Window Editor Demo");
|
|
|
|
WindowProperties props;
|
|
props.set_title("MetaCore Single-Window Editor Demo");
|
|
props.set_size(1280, 720);
|
|
|
|
WindowFramework* panda_window = framework.open_window(props, 0);
|
|
if (panda_window == nullptr) {
|
|
log_step("Panda window failed to open");
|
|
framework.close_framework();
|
|
return 2;
|
|
}
|
|
|
|
create_ground_grid(panda_window->get_render());
|
|
create_world_axes(panda_window->get_render());
|
|
|
|
DemoState state;
|
|
NodePath model = load_demo_model(panda_window, state);
|
|
focus_camera_on_model(model, state);
|
|
|
|
g_panda_hwnd = reinterpret_cast<HWND>(native_window_handle(panda_window));
|
|
if (g_panda_hwnd == nullptr) {
|
|
log_step("Failed to resolve Panda window handle");
|
|
framework.close_window(panda_window);
|
|
framework.close_framework();
|
|
return 3;
|
|
}
|
|
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
|
ImGui::StyleColorsDark();
|
|
|
|
ImGui_ImplWin32_Init(g_panda_hwnd);
|
|
ImGui_ImplOpenGL3_Init("#version 130");
|
|
g_previous_wnd_proc = reinterpret_cast<WNDPROC>(
|
|
SetWindowLongPtr(g_panda_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(panda_imgui_wnd_proc))
|
|
);
|
|
|
|
g_overlay_region = panda_window->get_graphics_output()->make_display_region();
|
|
g_overlay_region->set_sort(1000);
|
|
g_overlay_region->set_incomplete_render(true);
|
|
g_overlay_region->set_clear_color_active(false);
|
|
g_overlay_region->set_clear_depth_active(false);
|
|
PT(CallbackObject) overlay_callback = new ImGuiOverlayDrawCallback();
|
|
g_overlay_region->set_draw_callback(overlay_callback);
|
|
|
|
DWORD previous_tick = GetTickCount();
|
|
bool running = true;
|
|
while (running) {
|
|
if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) {
|
|
running = false;
|
|
}
|
|
if (panda_window->get_graphics_window() != nullptr && panda_window->get_graphics_window()->is_closed()) {
|
|
running = false;
|
|
}
|
|
|
|
const DWORD current_tick = GetTickCount();
|
|
const float delta_seconds = static_cast<float>(current_tick - previous_tick) / 1000.0F;
|
|
previous_tick = current_tick;
|
|
|
|
const float move_speed = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0 ? 5.0F : 2.5F) * delta_seconds;
|
|
const float turn_speed = 90.0F * delta_seconds;
|
|
|
|
const auto key_down = [](const int key) {
|
|
return (GetAsyncKeyState(key) & 0x8000) != 0;
|
|
};
|
|
if (key_down(VK_LEFT)) {
|
|
state.heading -= turn_speed;
|
|
}
|
|
if (key_down(VK_RIGHT)) {
|
|
state.heading += turn_speed;
|
|
}
|
|
if (key_down(VK_UP)) {
|
|
state.pitch += turn_speed;
|
|
}
|
|
if (key_down(VK_DOWN)) {
|
|
state.pitch -= turn_speed;
|
|
}
|
|
if (state.pitch > 89.0F) {
|
|
state.pitch = 89.0F;
|
|
}
|
|
if (state.pitch < -89.0F) {
|
|
state.pitch = -89.0F;
|
|
}
|
|
|
|
const float heading_radians = state.heading * 3.1415926535F / 180.0F;
|
|
const float forward_x = std::sin(heading_radians);
|
|
const float forward_y = std::cos(heading_radians);
|
|
const float right_x = std::cos(heading_radians);
|
|
const float right_y = -std::sin(heading_radians);
|
|
|
|
if (key_down('W')) {
|
|
state.camera_x += forward_x * move_speed;
|
|
state.camera_y += forward_y * move_speed;
|
|
}
|
|
if (key_down('S')) {
|
|
state.camera_x -= forward_x * move_speed;
|
|
state.camera_y -= forward_y * move_speed;
|
|
}
|
|
if (key_down('D')) {
|
|
state.camera_x += right_x * move_speed;
|
|
state.camera_y += right_y * move_speed;
|
|
}
|
|
if (key_down('A')) {
|
|
state.camera_x -= right_x * move_speed;
|
|
state.camera_y -= right_y * move_speed;
|
|
}
|
|
if (key_down('R')) {
|
|
state.camera_z += move_speed;
|
|
}
|
|
if (key_down('F')) {
|
|
state.camera_z -= move_speed;
|
|
}
|
|
|
|
panda_window->get_camera_group().set_pos(state.camera_x, state.camera_y, state.camera_z);
|
|
panda_window->get_camera_group().set_hpr(state.heading, state.pitch, 0.0F);
|
|
model.set_pos(state.model_position[0], state.model_position[1], state.model_position[2]);
|
|
model.set_hpr(state.model_rotation[0], state.model_rotation[1], state.model_rotation[2]);
|
|
model.set_scale(state.model_scale[0], state.model_scale[1], state.model_scale[2]);
|
|
if (state.auto_rotate) {
|
|
state.model_rotation[0] += state.rotation_speed * delta_seconds;
|
|
}
|
|
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
ImGui_ImplWin32_NewFrame();
|
|
ImGui::NewFrame();
|
|
render_editor_ui(state);
|
|
ImGui::Render();
|
|
g_pending_draw_data = ImGui::GetDrawData();
|
|
|
|
framework.do_frame(nullptr);
|
|
}
|
|
|
|
if (g_overlay_region != nullptr) {
|
|
g_overlay_region->clear_draw_callback();
|
|
if (panda_window->get_graphics_output() != nullptr) {
|
|
panda_window->get_graphics_output()->remove_display_region(g_overlay_region);
|
|
}
|
|
g_overlay_region = nullptr;
|
|
}
|
|
g_pending_draw_data = nullptr;
|
|
|
|
if (g_previous_wnd_proc != nullptr) {
|
|
SetWindowLongPtr(g_panda_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_previous_wnd_proc));
|
|
g_previous_wnd_proc = nullptr;
|
|
}
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
ImGui_ImplWin32_Shutdown();
|
|
ImGui::DestroyContext();
|
|
|
|
framework.close_window(panda_window);
|
|
framework.close_framework();
|
|
return 0;
|
|
#else
|
|
log_step("Panda3D or Dear ImGui support is not enabled in this build.");
|
|
return 1;
|
|
#endif
|
|
}
|