src
This commit is contained in:
217
src/Application.cpp
Normal file
217
src/Application.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#include <algorithm>
|
||||
#include "Application.h"
|
||||
#include "ModelLoader.h"
|
||||
#include "SceneBuilder.h"
|
||||
#include "OrbitManipulator.h"
|
||||
#include "MorphManager.h"
|
||||
#include "AppConfig.h"
|
||||
#include "ImGuiLayer.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
|
||||
#include <osg/Group>
|
||||
#include <osg/StateSet>
|
||||
#include <osg/CullFace>
|
||||
#include <osg/Program>
|
||||
#include <osg/NodeCallback>
|
||||
#include <osg/MatrixTransform>
|
||||
#include <osgViewer/ViewerEventHandlers>
|
||||
|
||||
// ── Update callback ───────────────────────────────────────────────────────────
|
||||
|
||||
class AppUpdateCallback : public osg::NodeCallback {
|
||||
public:
|
||||
explicit AppUpdateCallback(Application* app) : m_app(app) {}
|
||||
void operator()(osg::Node* node, osg::NodeVisitor* nv) override {
|
||||
m_app->applyPendingShader(); // shader switches
|
||||
m_app->applyMorphWeights(); // morph deformation — must be in update traversal
|
||||
traverse(node, nv);
|
||||
}
|
||||
private:
|
||||
Application* m_app;
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Application::Application()
|
||||
: m_shaderMgr(std::make_unique<ShaderManager>("assets/shaders"))
|
||||
, m_morphMgr (std::make_unique<MorphManager>())
|
||||
{
|
||||
m_config.load();
|
||||
m_imguiLayer = std::make_unique<ImGuiLayer>(m_morphMgr.get(), &m_config);
|
||||
}
|
||||
|
||||
Application::~Application() = default;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool Application::init(int width, int height, const std::string& title) {
|
||||
m_viewer = new osgViewer::Viewer;
|
||||
m_viewer->setUpViewInWindow(50, 50, width, height);
|
||||
|
||||
m_sceneRoot = new osg::Group;
|
||||
m_sceneRoot->setName("SceneRoot");
|
||||
m_sceneRoot->getOrCreateStateSet()
|
||||
->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
|
||||
|
||||
setupLighting();
|
||||
setupGrid();
|
||||
|
||||
m_shaderGroup = new osg::Group;
|
||||
m_shaderGroup->setName("ShaderGroup");
|
||||
m_shaderGroup->setUpdateCallback(new AppUpdateCallback(this));
|
||||
m_sceneRoot->addChild(m_shaderGroup);
|
||||
|
||||
auto* manip = new OrbitManipulator;
|
||||
manip->setDefaultHumanoidView();
|
||||
m_viewer->setCameraManipulator(manip);
|
||||
|
||||
m_viewer->addEventHandler(new osgViewer::StatsHandler);
|
||||
m_viewer->addEventHandler(new osgViewer::WindowSizeHandler);
|
||||
m_viewer->addEventHandler(this);
|
||||
|
||||
m_viewer->setSceneData(m_sceneRoot);
|
||||
m_viewer->realize();
|
||||
|
||||
// ImGui must init AFTER realize() (needs a live GL context)
|
||||
m_imguiLayer->init(m_viewer.get());
|
||||
|
||||
// Wire shader callbacks so ImGui can drive shader switching
|
||||
m_imguiLayer->onShaderChange = [this](const std::string& mode) {
|
||||
requestShader(mode);
|
||||
};
|
||||
m_imguiLayer->onScaleChange = [this](float s) {
|
||||
setModelScale(s);
|
||||
};
|
||||
m_imguiLayer->setInitialScale(m_config.getFloat("model.scale", 1.0f));
|
||||
m_imguiLayer->onShaderReload = [this]() {
|
||||
std::lock_guard<std::mutex> lock(m_shaderMutex);
|
||||
m_reloadShaders = true;
|
||||
m_shaderDirty = true;
|
||||
m_pendingShader = m_currentShader;
|
||||
};
|
||||
m_imguiLayer->setCurrentShader(m_currentShader);
|
||||
|
||||
{
|
||||
osgViewer::Viewer::Windows windows;
|
||||
m_viewer->getWindows(windows);
|
||||
for (auto* w : windows) w->setWindowName(title);
|
||||
}
|
||||
|
||||
std::cout << "[app] Window " << width << "x" << height
|
||||
<< " - \"" << title << "\" ready.\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool Application::loadModel(const std::string& filepath) {
|
||||
std::cout << "[app] Loading model: " << filepath << "\n";
|
||||
|
||||
ModelLoader loader;
|
||||
m_modelNode = loader.load(filepath, m_morphMgr.get());
|
||||
if (!m_modelNode) return false;
|
||||
|
||||
// Wrap model in a transform so scale/position can be adjusted at runtime
|
||||
m_modelXform = new osg::MatrixTransform;
|
||||
m_modelXform->setName("ModelTransform");
|
||||
|
||||
float initScale = m_config.getFloat("model.scale", 1.0f);
|
||||
m_modelXform->setMatrix(osg::Matrix::scale(initScale, initScale, initScale));
|
||||
m_modelXform->addChild(m_modelNode);
|
||||
m_shaderGroup->addChild(m_modelXform);
|
||||
requestShader(m_currentShader);
|
||||
m_viewer->home();
|
||||
|
||||
std::cout << "[app] Model loaded. Morphs: "
|
||||
<< m_morphMgr->morphNames().size() << "\n"
|
||||
<< " 1=flat 2=cel 3=toon 4=reload shaders\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
int Application::run() {
|
||||
// Hook the morph update into the viewer's update traversal.
|
||||
// We run applyWeights() every frame via a simple per-frame check.
|
||||
// The actual call is inside the viewer loop below.
|
||||
while (!m_viewer->done()) {
|
||||
m_viewer->frame(); // update traversal (callback above) handles morphs
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool Application::handle(const osgGA::GUIEventAdapter& ea,
|
||||
osgGA::GUIActionAdapter&) {
|
||||
// Forward to ImGui first — if it wants the event, don't pass to camera
|
||||
if (m_imguiLayer->handleEvent(ea)) return true;
|
||||
|
||||
if (ea.getEventType() != osgGA::GUIEventAdapter::KEYDOWN) return false;
|
||||
if (!m_modelNode) return false;
|
||||
|
||||
switch (ea.getKey()) {
|
||||
case '1': requestShader("flat"); return true;
|
||||
case '2': requestShader("cel"); return true;
|
||||
case '3': requestShader("toon"); return true;
|
||||
case '4': {
|
||||
std::lock_guard<std::mutex> lock(m_shaderMutex);
|
||||
m_reloadShaders = true;
|
||||
m_shaderDirty = true;
|
||||
m_pendingShader = m_currentShader;
|
||||
return true;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void Application::applyPendingShader() {
|
||||
std::string mode;
|
||||
bool dirty = false, reload = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_shaderMutex);
|
||||
if (!m_shaderDirty) return;
|
||||
mode = m_pendingShader;
|
||||
dirty = m_shaderDirty;
|
||||
reload = m_reloadShaders;
|
||||
m_shaderDirty = m_reloadShaders = false;
|
||||
}
|
||||
if (!dirty || !m_shaderGroup) return;
|
||||
if (reload) m_shaderMgr->reload();
|
||||
m_currentShader = mode;
|
||||
m_shaderMgr->applyTo(m_shaderGroup.get(), mode);
|
||||
m_imguiLayer->setCurrentShader(mode);
|
||||
}
|
||||
|
||||
void Application::applyMorphWeights() {
|
||||
if (m_morphMgr) m_morphMgr->applyWeights();
|
||||
}
|
||||
|
||||
void Application::setModelScale(float scale) {
|
||||
if (m_modelXform) {
|
||||
scale = std::max(0.001f, scale);
|
||||
m_modelXform->setMatrix(osg::Matrix::scale(scale, scale, scale));
|
||||
}
|
||||
}
|
||||
|
||||
void Application::requestShader(const std::string& mode) {
|
||||
std::lock_guard<std::mutex> lock(m_shaderMutex);
|
||||
m_pendingShader = mode;
|
||||
m_shaderDirty = true;
|
||||
}
|
||||
|
||||
void Application::setupLighting() {
|
||||
m_sceneRoot->addChild(SceneBuilder::createSunLight(0));
|
||||
m_sceneRoot->addChild(SceneBuilder::createAmbientLight(1));
|
||||
m_sceneRoot->getOrCreateStateSet()
|
||||
->setMode(GL_LIGHTING, osg::StateAttribute::ON);
|
||||
}
|
||||
|
||||
void Application::setupGrid() {
|
||||
m_sceneRoot->addChild(SceneBuilder::createGrid(10.f, 20));
|
||||
m_sceneRoot->addChild(SceneBuilder::createAxes(1.f));
|
||||
}
|
||||
485
src/ImGuiLayer.cpp
Normal file
485
src/ImGuiLayer.cpp
Normal file
@@ -0,0 +1,485 @@
|
||||
#include "ImGuiLayer.h"
|
||||
#include "MorphManager.h"
|
||||
#include "AppConfig.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
|
||||
#include <imgui/imgui.h>
|
||||
#include <imgui/imgui_impl_opengl3.h>
|
||||
|
||||
#include <osg/RenderInfo>
|
||||
#include <osg/GraphicsContext>
|
||||
#include <osg/Viewport>
|
||||
#include <osgViewer/Viewer>
|
||||
#include <osgGA/GUIEventAdapter>
|
||||
|
||||
// ── Draw callback ─────────────────────────────────────────────────────────────
|
||||
|
||||
struct ImGuiDrawCallback : public osg::Camera::DrawCallback {
|
||||
ImGuiLayer* layer;
|
||||
mutable bool glInitialized = false;
|
||||
|
||||
explicit ImGuiDrawCallback(ImGuiLayer* l) : layer(l) {}
|
||||
|
||||
void operator()(osg::RenderInfo& ri) const override {
|
||||
osg::GraphicsContext* gc = ri.getCurrentCamera()->getGraphicsContext();
|
||||
osg::Viewport* vp = ri.getCurrentCamera()->getViewport();
|
||||
|
||||
float w = vp ? static_cast<float>(vp->width())
|
||||
: (gc && gc->getTraits() ? static_cast<float>(gc->getTraits()->width) : 0.f);
|
||||
float h = vp ? static_cast<float>(vp->height())
|
||||
: (gc && gc->getTraits() ? static_cast<float>(gc->getTraits()->height) : 0.f);
|
||||
if (w < 1.f || h < 1.f) return;
|
||||
|
||||
if (!glInitialized) {
|
||||
bool ok = ImGui_ImplOpenGL3_Init(nullptr);
|
||||
if (!ok) ok = ImGui_ImplOpenGL3_Init("#version 130");
|
||||
if (!ok) { std::cerr << "[imgui] GL init failed\n"; return; }
|
||||
layer->markGLInitialized();
|
||||
glInitialized = true;
|
||||
std::cout << "[imgui] OpenGL backend initialized.\n";
|
||||
}
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.DisplaySize = ImVec2(w, h);
|
||||
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
layer->renderPanel();
|
||||
ImGui::Render();
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
ImGuiLayer::ImGuiLayer(MorphManager* morphMgr, const AppConfig* cfg)
|
||||
: m_morphMgr(morphMgr), m_cfg(cfg)
|
||||
{}
|
||||
|
||||
ImGuiLayer::~ImGuiLayer() {
|
||||
if (m_glInitialized) ImGui_ImplOpenGL3_Shutdown();
|
||||
if (m_contextCreated) ImGui::DestroyContext();
|
||||
}
|
||||
|
||||
void ImGuiLayer::markGLInitialized() { m_glInitialized = true; }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::init(osgViewer::Viewer* viewer) {
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
m_contextCreated = true;
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
|
||||
// ── Font ─────────────────────────────────────────────────────────────────
|
||||
static const ImWchar glyphRanges[] = {
|
||||
0x0020, 0x00FF,
|
||||
0x3000, 0x30FF, // Hiragana + Katakana
|
||||
0x4E00, 0x9FFF, // CJK common kanji
|
||||
0xFF00, 0xFFEF,
|
||||
0,
|
||||
};
|
||||
|
||||
std::string fontPath = m_cfg ? m_cfg->getString("ui.font_path") : std::string();
|
||||
float fontSize = m_cfg ? m_cfg->getFloat("ui.font_size", 14.f) : 14.f;
|
||||
m_panelWidth = m_cfg ? m_cfg->getFloat("ui.panel_width", 380.f) : 380.f;
|
||||
|
||||
bool fontLoaded = false;
|
||||
if (!fontPath.empty()) {
|
||||
ImFont* f = io.Fonts->AddFontFromFileTTF(
|
||||
fontPath.c_str(), fontSize, nullptr, glyphRanges);
|
||||
if (f) {
|
||||
std::cout << "[imgui] Font: " << fontPath << "\n";
|
||||
fontLoaded = true;
|
||||
} else {
|
||||
std::cerr << "[imgui] Failed to load font: " << fontPath
|
||||
<< "\n Check ui.font_path in assets/config.ini\n";
|
||||
}
|
||||
}
|
||||
if (!fontLoaded) {
|
||||
std::cout << "[imgui] Using built-in font (ASCII only).\n";
|
||||
io.Fonts->AddFontDefault();
|
||||
}
|
||||
|
||||
// ── Style ────────────────────────────────────────────────────────────────
|
||||
ImGui::StyleColorsDark();
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style.WindowRounding = 8.f;
|
||||
style.FrameRounding = 4.f;
|
||||
style.TabRounding = 4.f;
|
||||
style.ScrollbarRounding = 4.f;
|
||||
style.GrabRounding = 4.f;
|
||||
style.WindowPadding = ImVec2(10, 10);
|
||||
style.ItemSpacing = ImVec2(8, 5);
|
||||
style.WindowMinSize = ImVec2(220.f, 200.f);
|
||||
|
||||
auto& c = style.Colors;
|
||||
c[ImGuiCol_TitleBg] = ImVec4(0.18f, 0.12f, 0.28f, 1.f);
|
||||
c[ImGuiCol_TitleBgActive] = ImVec4(0.28f, 0.18f, 0.45f, 1.f);
|
||||
c[ImGuiCol_Tab] = ImVec4(0.18f, 0.12f, 0.28f, 1.f);
|
||||
c[ImGuiCol_TabHovered] = ImVec4(0.45f, 0.30f, 0.70f, 1.f);
|
||||
c[ImGuiCol_TabActive] = ImVec4(0.35f, 0.22f, 0.56f, 1.f);
|
||||
c[ImGuiCol_TabUnfocused] = ImVec4(0.14f, 0.09f, 0.22f, 1.f);
|
||||
c[ImGuiCol_TabUnfocusedActive]= ImVec4(0.28f, 0.18f, 0.45f, 1.f);
|
||||
c[ImGuiCol_ResizeGrip] = ImVec4(0.45f, 0.30f, 0.70f, 0.5f);
|
||||
c[ImGuiCol_ResizeGripHovered] = ImVec4(0.65f, 0.45f, 0.90f, 0.8f);
|
||||
c[ImGuiCol_ResizeGripActive] = ImVec4(0.80f, 0.60f, 1.00f, 1.0f);
|
||||
c[ImGuiCol_Header] = ImVec4(0.28f, 0.18f, 0.45f, 0.6f);
|
||||
c[ImGuiCol_HeaderHovered] = ImVec4(0.38f, 0.25f, 0.60f, 0.8f);
|
||||
c[ImGuiCol_SliderGrab] = ImVec4(0.65f, 0.45f, 0.90f, 1.f);
|
||||
c[ImGuiCol_SliderGrabActive] = ImVec4(0.80f, 0.60f, 1.00f, 1.f);
|
||||
c[ImGuiCol_FrameBg] = ImVec4(0.12f, 0.08f, 0.18f, 1.f);
|
||||
c[ImGuiCol_FrameBgHovered] = ImVec4(0.22f, 0.15f, 0.32f, 1.f);
|
||||
c[ImGuiCol_Button] = ImVec4(0.28f, 0.18f, 0.45f, 1.f);
|
||||
c[ImGuiCol_ButtonHovered] = ImVec4(0.40f, 0.27f, 0.62f, 1.f);
|
||||
c[ImGuiCol_CheckMark] = ImVec4(0.80f, 0.60f, 1.00f, 1.f);
|
||||
c[ImGuiCol_ScrollbarGrab] = ImVec4(0.45f, 0.30f, 0.70f, 1.f);
|
||||
|
||||
// ── Camera ───────────────────────────────────────────────────────────────
|
||||
osgViewer::Viewer::Windows windows;
|
||||
viewer->getWindows(windows);
|
||||
if (windows.empty()) { std::cerr << "[imgui] No windows found!\n"; return; }
|
||||
|
||||
osg::GraphicsContext* gc = windows[0];
|
||||
const auto* traits = gc ? gc->getTraits() : nullptr;
|
||||
int winW = traits ? traits->width : 1280;
|
||||
int winH = traits ? traits->height : 720;
|
||||
|
||||
m_camera = new osg::Camera;
|
||||
m_camera->setName("ImGuiCamera");
|
||||
m_camera->setRenderOrder(osg::Camera::POST_RENDER, 100);
|
||||
m_camera->setClearMask(0);
|
||||
m_camera->setAllowEventFocus(false);
|
||||
m_camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
|
||||
m_camera->setViewMatrix(osg::Matrix::identity());
|
||||
m_camera->setProjectionMatrix(osg::Matrix::ortho2D(0, winW, 0, winH));
|
||||
m_camera->setViewport(0, 0, winW, winH);
|
||||
m_camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
|
||||
m_camera->setGraphicsContext(gc);
|
||||
m_camera->setPostDrawCallback(new ImGuiDrawCallback(this));
|
||||
|
||||
viewer->addSlave(m_camera, false);
|
||||
m_viewer = viewer;
|
||||
std::cout << "[imgui] Overlay camera attached (" << winW << "x" << winH << ").\n";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool ImGuiLayer::handleEvent(const osgGA::GUIEventAdapter& ea) {
|
||||
if (!m_contextCreated) return false;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
switch (ea.getEventType()) {
|
||||
case osgGA::GUIEventAdapter::MOVE:
|
||||
case osgGA::GUIEventAdapter::DRAG:
|
||||
io.AddMousePosEvent(ea.getX(), io.DisplaySize.y - ea.getY());
|
||||
break;
|
||||
case osgGA::GUIEventAdapter::PUSH:
|
||||
case osgGA::GUIEventAdapter::RELEASE: {
|
||||
bool down = ea.getEventType() == osgGA::GUIEventAdapter::PUSH;
|
||||
if (ea.getButton() == osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON)
|
||||
io.AddMouseButtonEvent(0, down);
|
||||
else if (ea.getButton() == osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON)
|
||||
io.AddMouseButtonEvent(1, down);
|
||||
else if (ea.getButton() == osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON)
|
||||
io.AddMouseButtonEvent(2, down);
|
||||
break;
|
||||
}
|
||||
case osgGA::GUIEventAdapter::SCROLL:
|
||||
io.AddMouseWheelEvent(0.f,
|
||||
ea.getScrollingMotion() == osgGA::GUIEventAdapter::SCROLL_UP
|
||||
? 1.f : -1.f);
|
||||
break;
|
||||
case osgGA::GUIEventAdapter::KEYDOWN:
|
||||
case osgGA::GUIEventAdapter::KEYUP: {
|
||||
bool down = ea.getEventType() == osgGA::GUIEventAdapter::KEYDOWN;
|
||||
int key = ea.getKey();
|
||||
if (key >= 32 && key < 127 && down)
|
||||
io.AddInputCharacter(static_cast<unsigned int>(key));
|
||||
if (key == osgGA::GUIEventAdapter::KEY_BackSpace)
|
||||
io.AddKeyEvent(ImGuiKey_Backspace, down);
|
||||
if (key == osgGA::GUIEventAdapter::KEY_Delete)
|
||||
io.AddKeyEvent(ImGuiKey_Delete, down);
|
||||
if (key == osgGA::GUIEventAdapter::KEY_Return)
|
||||
io.AddKeyEvent(ImGuiKey_Enter, down);
|
||||
if (key == osgGA::GUIEventAdapter::KEY_Escape)
|
||||
io.AddKeyEvent(ImGuiKey_Escape, down);
|
||||
break;
|
||||
}
|
||||
case osgGA::GUIEventAdapter::RESIZE: {
|
||||
int w = ea.getWindowWidth(), h = ea.getWindowHeight();
|
||||
io.DisplaySize = ImVec2(static_cast<float>(w), static_cast<float>(h));
|
||||
if (m_camera) {
|
||||
m_camera->setViewport(0, 0, w, h);
|
||||
m_camera->setProjectionMatrix(osg::Matrix::ortho2D(0, w, 0, h));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
return io.WantCaptureMouse &&
|
||||
(ea.getEventType() == osgGA::GUIEventAdapter::PUSH ||
|
||||
ea.getEventType() == osgGA::GUIEventAdapter::RELEASE ||
|
||||
ea.getEventType() == osgGA::GUIEventAdapter::MOVE ||
|
||||
ea.getEventType() == osgGA::GUIEventAdapter::DRAG ||
|
||||
ea.getEventType() == osgGA::GUIEventAdapter::SCROLL);
|
||||
}
|
||||
|
||||
// ── Main panel with tabs ──────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::renderPanel() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x - m_panelWidth - 10.f, 10.f), ImGuiCond_Once);
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(m_panelWidth, io.DisplaySize.y - 20.f), ImGuiCond_Once);
|
||||
ImGui::SetNextWindowBgAlpha(0.88f);
|
||||
ImGui::SetNextWindowSizeConstraints(
|
||||
ImVec2(220.f, 200.f),
|
||||
ImVec2(io.DisplaySize.x * 0.9f, io.DisplaySize.y));
|
||||
|
||||
if (!ImGui::Begin("Model Controls", nullptr, ImGuiWindowFlags_NoCollapse)) {
|
||||
ImGui::End(); return;
|
||||
}
|
||||
|
||||
m_panelWidth = ImGui::GetWindowWidth();
|
||||
|
||||
if (ImGui::BeginTabBar("##tabs")) {
|
||||
if (ImGui::BeginTabItem("Morphs")) {
|
||||
renderMorphTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Shaders")) {
|
||||
renderShaderTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Transform")) {
|
||||
renderTransformTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ── Morphs tab ────────────────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::renderMorphTab() {
|
||||
if (!m_morphMgr) { ImGui::TextDisabled("No model loaded."); return; }
|
||||
const auto& names = m_morphMgr->morphNames();
|
||||
if (names.empty()) { ImGui::TextDisabled("No morphs found."); return; }
|
||||
|
||||
// Toolbar
|
||||
ImGui::SetNextItemWidth(-90.f);
|
||||
ImGui::InputText("Search##morph", m_searchBuf, sizeof(m_searchBuf));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset All")) {
|
||||
m_morphMgr->resetAll();
|
||||
m_searchBuf[0] = '\0';
|
||||
}
|
||||
ImGui::Checkbox("Active only", &m_showOnlyActive);
|
||||
ImGui::Separator();
|
||||
|
||||
std::string filter(m_searchBuf);
|
||||
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
||||
|
||||
// Two-column layout: slider | name
|
||||
// Slider column is fixed at 120px; name gets all the remaining space
|
||||
const float sliderW = 120.f;
|
||||
const float btnW = 22.f;
|
||||
const float nameW = ImGui::GetContentRegionAvail().x - sliderW - btnW
|
||||
- ImGui::GetStyle().ItemSpacing.x * 3.f;
|
||||
|
||||
int visible = 0;
|
||||
ImGui::BeginChild("##morphlist", ImVec2(0, 0), false);
|
||||
|
||||
for (const auto& name : names) {
|
||||
float w = m_morphMgr->getWeight(name);
|
||||
if (m_showOnlyActive && w < 1e-4f) continue;
|
||||
if (!filter.empty()) {
|
||||
std::string lname = name;
|
||||
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
||||
if (lname.find(filter) == std::string::npos) continue;
|
||||
}
|
||||
++visible;
|
||||
|
||||
bool isActive = (w > 1e-4f);
|
||||
|
||||
// Slider
|
||||
ImGui::SetNextItemWidth(sliderW);
|
||||
std::string sliderID = "##s" + name;
|
||||
if (isActive)
|
||||
ImGui::PushStyleColor(ImGuiCol_SliderGrab,
|
||||
ImVec4(1.0f, 0.75f, 0.3f, 1.f));
|
||||
if (ImGui::SliderFloat(sliderID.c_str(), &w, 0.f, 1.f))
|
||||
m_morphMgr->setWeight(name, w);
|
||||
if (isActive) ImGui::PopStyleColor();
|
||||
|
||||
// Reset button
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.1f, 0.1f, 1.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.f));
|
||||
std::string btnID = "x##" + name;
|
||||
if (ImGui::SmallButton(btnID.c_str()))
|
||||
m_morphMgr->setWeight(name, 0.f);
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
// Name — clipped to available width
|
||||
ImGui::SameLine();
|
||||
if (isActive)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.4f, 1.f));
|
||||
ImGui::SetNextItemWidth(nameW);
|
||||
// PushTextWrapPos clips long names cleanly
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + nameW);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
if (isActive) ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (visible == 0)
|
||||
ImGui::TextDisabled("No morphs match filter.");
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// ── Shaders tab ───────────────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::renderShaderTab() {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Select a shading mode:");
|
||||
ImGui::Spacing();
|
||||
|
||||
struct ShaderOption {
|
||||
const char* id;
|
||||
const char* label;
|
||||
const char* desc;
|
||||
};
|
||||
|
||||
static const ShaderOption options[] = {
|
||||
{ "flat", "Flat / Unlit",
|
||||
"Raw texture colours, no lighting.\nUseful for checking UV maps." },
|
||||
{ "cel", "Cel Shading",
|
||||
"Quantised diffuse bands.\nClean anime look without outlines." },
|
||||
{ "toon", "Toon Shading",
|
||||
"Cel bands + specular highlight\n+ rim light. Full anime style." },
|
||||
};
|
||||
|
||||
for (auto& opt : options) {
|
||||
bool selected = (m_currentShader == opt.id);
|
||||
|
||||
if (selected)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||||
ImVec4(0.45f, 0.28f, 0.72f, 1.f));
|
||||
|
||||
float bw = ImGui::GetContentRegionAvail().x;
|
||||
if (ImGui::Button(opt.label, ImVec2(bw, 36.f))) {
|
||||
m_currentShader = opt.id;
|
||||
if (onShaderChange) onShaderChange(opt.id);
|
||||
}
|
||||
|
||||
if (selected) ImGui::PopStyleColor();
|
||||
|
||||
// Description text, indented
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.7f, 1.f));
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.f);
|
||||
ImGui::TextUnformatted(opt.desc);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
float bw = ImGui::GetContentRegionAvail().x;
|
||||
if (ImGui::Button("Reload Shaders from Disk", ImVec2(bw, 30.f))) {
|
||||
if (onShaderReload) onShaderReload();
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.6f, 1.f));
|
||||
ImGui::TextWrapped("Reloads GLSL files without recompiling.\n"
|
||||
"Edit assets/shaders/*.vert / *.frag\nthen click.");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// ── Transform tab ─────────────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::renderTransformTab() {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Model scale:");
|
||||
ImGui::Spacing();
|
||||
|
||||
// Show current scale value
|
||||
ImGui::Text("Current: %.4f", m_scale);
|
||||
ImGui::Spacing();
|
||||
|
||||
// Text input for scale
|
||||
// Pre-fill the buffer with the current value if it's empty
|
||||
if (m_scaleBuf[0] == '\0')
|
||||
snprintf(m_scaleBuf, sizeof(m_scaleBuf), "%.4f", m_scale);
|
||||
|
||||
ImGui::SetNextItemWidth(-1.f);
|
||||
bool entered = ImGui::InputText("##scale", m_scaleBuf, sizeof(m_scaleBuf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue |
|
||||
ImGuiInputTextFlags_CharsDecimal);
|
||||
|
||||
ImGui::Spacing();
|
||||
float bw = ImGui::GetContentRegionAvail().x;
|
||||
bool clicked = ImGui::Button("Apply Scale", ImVec2(bw, 30.f));
|
||||
|
||||
if (entered || clicked) {
|
||||
try {
|
||||
float parsed = std::stof(std::string(m_scaleBuf));
|
||||
if (parsed > 0.f) {
|
||||
m_scale = parsed;
|
||||
if (onScaleChange) onScaleChange(m_scale);
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0.3f,0.3f,1));
|
||||
ImGui::TextUnformatted("Scale must be > 0");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
} catch (...) {
|
||||
// non-numeric input — just ignore
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Quick presets:");
|
||||
ImGui::Spacing();
|
||||
|
||||
// Common scale presets in a 3-column grid
|
||||
const std::pair<const char*, float> presets[] = {
|
||||
{"0.01x", 0.01f}, {"0.1x", 0.1f}, {"0.5x", 0.5f},
|
||||
{"1x", 1.0f}, {"2x", 2.0f}, {"10x", 10.0f},
|
||||
};
|
||||
|
||||
int col = 0;
|
||||
for (auto& [label, val] : presets) {
|
||||
if (col > 0) ImGui::SameLine();
|
||||
float colW = (ImGui::GetContentRegionAvail().x
|
||||
+ ImGui::GetStyle().ItemSpacing.x * (2 - col)) / (3 - col);
|
||||
|
||||
bool active = std::abs(m_scale - val) < 1e-4f;
|
||||
if (active)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.45f, 0.28f, 0.72f, 1.f));
|
||||
|
||||
if (ImGui::Button(label, ImVec2(colW, 28.f))) {
|
||||
m_scale = val;
|
||||
snprintf(m_scaleBuf, sizeof(m_scaleBuf), "%.4f", m_scale);
|
||||
if (onScaleChange) onScaleChange(m_scale);
|
||||
}
|
||||
|
||||
if (active) ImGui::PopStyleColor();
|
||||
col = (col + 1) % 3;
|
||||
}
|
||||
}
|
||||
301
src/ModelLoader.cpp
Normal file
301
src/ModelLoader.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#include "ModelLoader.h"
|
||||
#include "MorphManager.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/postprocess.h>
|
||||
|
||||
#include <osg/Geode>
|
||||
#include <osg/Geometry>
|
||||
#include <osg/Array>
|
||||
#include <osg/PrimitiveSet>
|
||||
#include <osg/Material>
|
||||
#include <osg/Texture2D>
|
||||
#include <osg/BlendFunc>
|
||||
#include <osg/StateSet>
|
||||
#include <osg/MatrixTransform>
|
||||
#include <osgDB/ReadFile>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
// Section header morphs like "------EYE------" are not real morphs
|
||||
bool isSectionHeader(const std::string& name) {
|
||||
return name.size() >= 4 &&
|
||||
name.front() == '-' && name.back() == '-';
|
||||
}
|
||||
|
||||
osg::ref_ptr<osg::Texture2D> loadTexture(const std::string& path) {
|
||||
osg::ref_ptr<osg::Image> img = osgDB::readImageFile(path);
|
||||
if (!img) {
|
||||
std::cerr << "[loader] texture not found: " << path << "\n";
|
||||
return {};
|
||||
}
|
||||
auto tex = new osg::Texture2D(img);
|
||||
tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
|
||||
tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
|
||||
tex->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR);
|
||||
tex->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
|
||||
return tex;
|
||||
}
|
||||
|
||||
osg::ref_ptr<osg::Geode> convertMesh(const aiMesh* mesh,
|
||||
const aiScene* scene,
|
||||
const std::string& baseDir,
|
||||
MorphManager* morphMgr) {
|
||||
auto geode = new osg::Geode;
|
||||
auto geom = new osg::Geometry;
|
||||
|
||||
// ── Base vertices ─────────────────────────────────────────────────────────
|
||||
auto baseVerts = new osg::Vec3Array;
|
||||
baseVerts->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
baseVerts->push_back({mesh->mVertices[i].x,
|
||||
mesh->mVertices[i].y,
|
||||
mesh->mVertices[i].z});
|
||||
|
||||
// We set a COPY as the live array; the base is kept separately for morphing
|
||||
auto liveVerts = new osg::Vec3Array(*baseVerts);
|
||||
geom->setVertexArray(liveVerts);
|
||||
|
||||
// ── Base normals ──────────────────────────────────────────────────────────
|
||||
osg::ref_ptr<osg::Vec3Array> baseNormals;
|
||||
if (mesh->HasNormals()) {
|
||||
baseNormals = new osg::Vec3Array;
|
||||
baseNormals->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
baseNormals->push_back({mesh->mNormals[i].x,
|
||||
mesh->mNormals[i].y,
|
||||
mesh->mNormals[i].z});
|
||||
auto liveNormals = new osg::Vec3Array(*baseNormals);
|
||||
geom->setNormalArray(liveNormals, osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
// ── UVs ───────────────────────────────────────────────────────────────────
|
||||
if (mesh->HasTextureCoords(0)) {
|
||||
auto uvs = new osg::Vec2Array;
|
||||
uvs->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
uvs->push_back({mesh->mTextureCoords[0][i].x,
|
||||
mesh->mTextureCoords[0][i].y});
|
||||
geom->setTexCoordArray(0, uvs, osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
// ── Vertex colours ────────────────────────────────────────────────────────
|
||||
if (mesh->HasVertexColors(0)) {
|
||||
auto cols = new osg::Vec4Array;
|
||||
cols->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
cols->push_back({mesh->mColors[0][i].r, mesh->mColors[0][i].g,
|
||||
mesh->mColors[0][i].b, mesh->mColors[0][i].a});
|
||||
geom->setColorArray(cols, osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
// ── Indices ───────────────────────────────────────────────────────────────
|
||||
auto indices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES);
|
||||
indices->reserve(mesh->mNumFaces * 3);
|
||||
for (unsigned f = 0; f < mesh->mNumFaces; ++f) {
|
||||
const aiFace& face = mesh->mFaces[f];
|
||||
if (face.mNumIndices != 3) continue;
|
||||
indices->push_back(face.mIndices[0]);
|
||||
indices->push_back(face.mIndices[1]);
|
||||
indices->push_back(face.mIndices[2]);
|
||||
}
|
||||
// Guard: skip meshes with no valid triangles (avoids front() crash on empty vector)
|
||||
if (indices->empty()) {
|
||||
std::cerr << "[loader] Skipping mesh with no valid triangles: "
|
||||
<< mesh->mName.C_Str() << "\n";
|
||||
return geode;
|
||||
}
|
||||
geom->addPrimitiveSet(indices);
|
||||
|
||||
// ── Material ──────────────────────────────────────────────────────────────
|
||||
osg::StateSet* ss = geom->getOrCreateStateSet();
|
||||
if (mesh->mMaterialIndex < scene->mNumMaterials) {
|
||||
const aiMaterial* mat = scene->mMaterials[mesh->mMaterialIndex];
|
||||
auto osgMat = new osg::Material;
|
||||
aiColor4D colour;
|
||||
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE, colour))
|
||||
osgMat->setDiffuse(osg::Material::FRONT_AND_BACK,
|
||||
{colour.r, colour.g, colour.b, colour.a});
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_AMBIENT, colour))
|
||||
osgMat->setAmbient(osg::Material::FRONT_AND_BACK,
|
||||
{colour.r, colour.g, colour.b, colour.a});
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR, colour))
|
||||
osgMat->setSpecular(osg::Material::FRONT_AND_BACK,
|
||||
{colour.r, colour.g, colour.b, colour.a});
|
||||
float shininess = 0.f;
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS, shininess))
|
||||
osgMat->setShininess(osg::Material::FRONT_AND_BACK,
|
||||
std::min(shininess, 128.f));
|
||||
ss->setAttribute(osgMat, osg::StateAttribute::ON);
|
||||
|
||||
if (mat->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
|
||||
aiString texPath;
|
||||
mat->GetTexture(aiTextureType_DIFFUSE, 0, &texPath);
|
||||
std::string fullPath = baseDir + "/" + texPath.C_Str();
|
||||
for (char& c : fullPath) if (c == '\\') c = '/';
|
||||
// Try the path as-is first, then common extension variants
|
||||
// (FBX often references .png when textures are actually .jpg or .tga)
|
||||
osg::ref_ptr<osg::Texture2D> tex = loadTexture(fullPath);
|
||||
if (!tex) {
|
||||
// Try swapping extension
|
||||
auto swapExt = [](const std::string& p, const std::string& newExt) {
|
||||
auto dot = p.rfind('.');
|
||||
return dot != std::string::npos ? p.substr(0, dot) + newExt : p;
|
||||
};
|
||||
for (auto& ext : {".jpg", ".jpeg", ".png", ".tga", ".bmp"}) {
|
||||
tex = loadTexture(swapExt(fullPath, ext));
|
||||
if (tex) break;
|
||||
}
|
||||
}
|
||||
if (tex) ss->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON);
|
||||
}
|
||||
|
||||
float opacity = 1.f;
|
||||
mat->Get(AI_MATKEY_OPACITY, opacity);
|
||||
if (opacity < 1.f) {
|
||||
ss->setMode(GL_BLEND, osg::StateAttribute::ON);
|
||||
ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
|
||||
ss->setAttribute(new osg::BlendFunc(
|
||||
osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE_MINUS_SRC_ALPHA));
|
||||
}
|
||||
}
|
||||
|
||||
geode->addDrawable(geom);
|
||||
|
||||
// ── Morph targets ─────────────────────────────────────────────────────────
|
||||
if (morphMgr && mesh->mNumAnimMeshes > 0) {
|
||||
// Mark geometry and arrays as DYNAMIC — tells OSG this data changes
|
||||
// every frame and must not be double-buffered or cached in display lists.
|
||||
geom->setDataVariance(osg::Object::DYNAMIC);
|
||||
geom->setUseDisplayList(false);
|
||||
geom->setUseVertexBufferObjects(true);
|
||||
|
||||
auto* vArr = dynamic_cast<osg::Vec3Array*>(geom->getVertexArray());
|
||||
auto* nArr = dynamic_cast<osg::Vec3Array*>(geom->getNormalArray());
|
||||
if (vArr) {
|
||||
vArr->setDataVariance(osg::Object::DYNAMIC);
|
||||
vArr->setBinding(osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
if (nArr) {
|
||||
nArr->setDataVariance(osg::Object::DYNAMIC);
|
||||
nArr->setBinding(osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
morphMgr->registerMesh(geom, baseVerts, baseNormals);
|
||||
|
||||
int registered = 0;
|
||||
for (unsigned a = 0; a < mesh->mNumAnimMeshes; ++a) {
|
||||
const aiAnimMesh* am = mesh->mAnimMeshes[a];
|
||||
std::string name = am->mName.C_Str();
|
||||
|
||||
if (name.empty() || isSectionHeader(name)) continue;
|
||||
if (am->mNumVertices != mesh->mNumVertices) continue;
|
||||
|
||||
// Compute vertex deltas (animMesh stores ABSOLUTE positions)
|
||||
auto deltaVerts = new osg::Vec3Array;
|
||||
deltaVerts->resize(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
|
||||
(*deltaVerts)[i] = osg::Vec3(
|
||||
am->mVertices[i].x - mesh->mVertices[i].x,
|
||||
am->mVertices[i].y - mesh->mVertices[i].y,
|
||||
am->mVertices[i].z - mesh->mVertices[i].z);
|
||||
}
|
||||
|
||||
// Normal deltas (optional)
|
||||
osg::ref_ptr<osg::Vec3Array> deltaNormals;
|
||||
if (am->mNormals && mesh->HasNormals()) {
|
||||
deltaNormals = new osg::Vec3Array;
|
||||
deltaNormals->resize(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
|
||||
(*deltaNormals)[i] = osg::Vec3(
|
||||
am->mNormals[i].x - mesh->mNormals[i].x,
|
||||
am->mNormals[i].y - mesh->mNormals[i].y,
|
||||
am->mNormals[i].z - mesh->mNormals[i].z);
|
||||
}
|
||||
}
|
||||
|
||||
morphMgr->addTarget(geom, name, deltaVerts, deltaNormals);
|
||||
++registered;
|
||||
}
|
||||
|
||||
if (registered > 0)
|
||||
std::cout << "[loader] Mesh \"" << mesh->mName.C_Str()
|
||||
<< "\": registered " << registered << " morph targets\n";
|
||||
}
|
||||
|
||||
return geode;
|
||||
}
|
||||
|
||||
osg::ref_ptr<osg::Group> convertNode(const aiNode* node,
|
||||
const aiScene* scene,
|
||||
const std::string& baseDir,
|
||||
MorphManager* morphMgr) {
|
||||
const aiMatrix4x4& m = node->mTransformation;
|
||||
osg::Matrixf mat(m.a1, m.b1, m.c1, m.d1,
|
||||
m.a2, m.b2, m.c2, m.d2,
|
||||
m.a3, m.b3, m.c3, m.d3,
|
||||
m.a4, m.b4, m.c4, m.d4);
|
||||
|
||||
auto xform = new osg::MatrixTransform(mat);
|
||||
xform->setName(node->mName.C_Str());
|
||||
|
||||
for (unsigned i = 0; i < node->mNumMeshes; ++i)
|
||||
xform->addChild(convertMesh(scene->mMeshes[node->mMeshes[i]],
|
||||
scene, baseDir, morphMgr));
|
||||
|
||||
for (unsigned i = 0; i < node->mNumChildren; ++i)
|
||||
xform->addChild(convertNode(node->mChildren[i], scene, baseDir, morphMgr));
|
||||
|
||||
return xform;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── ModelLoader ───────────────────────────────────────────────────────────────
|
||||
|
||||
osg::ref_ptr<osg::Node> ModelLoader::load(const std::string& filepath,
|
||||
MorphManager* morphMgr) {
|
||||
Assimp::Importer importer;
|
||||
|
||||
// NOTE: JoinIdenticalVertices is intentionally omitted —
|
||||
// it destroys the vertex correspondence that morph targets rely on.
|
||||
//
|
||||
// aiProcess_FlipUVs is omitted — FBX exports from Blender already have
|
||||
// UVs in the correct OpenGL orientation (Y=0 at bottom). Flipping them
|
||||
// was causing textures to appear mirrored/upside-down.
|
||||
// If loading a raw PMX via Assimp ever becomes needed, this flag would
|
||||
// need to be added back conditionally based on file extension.
|
||||
constexpr unsigned flags =
|
||||
aiProcess_Triangulate |
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_SortByPType |
|
||||
aiProcess_ImproveCacheLocality;
|
||||
|
||||
const aiScene* scene = importer.ReadFile(filepath, flags);
|
||||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
||||
std::cerr << "[loader] Assimp error: " << importer.GetErrorString() << "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::cout << "[loader] Meshes: " << scene->mNumMeshes
|
||||
<< " Materials: " << scene->mNumMaterials
|
||||
<< " Animations: " << scene->mNumAnimations << "\n";
|
||||
|
||||
const std::string baseDir = fs::path(filepath).parent_path().string();
|
||||
return buildOsgScene(scene, baseDir, morphMgr);
|
||||
}
|
||||
|
||||
osg::ref_ptr<osg::Node> ModelLoader::buildOsgScene(const aiScene* scene,
|
||||
const std::string& baseDir,
|
||||
MorphManager* morphMgr) {
|
||||
return convertNode(scene->mRootNode, scene, baseDir, morphMgr);
|
||||
}
|
||||
246
src/ShaderManager.cpp
Normal file
246
src/ShaderManager.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
#include "ShaderManager.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <osg/Shader>
|
||||
#include <osg/Uniform>
|
||||
#include <osg/Texture2D>
|
||||
#include <osg/CullFace>
|
||||
#include <osg/BlendFunc>
|
||||
#include <osg/NodeVisitor>
|
||||
#include <osg/Geode>
|
||||
#include <osg/Drawable>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ── File helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) {
|
||||
std::cerr << "[shader] Cannot open: " << path << "\n";
|
||||
return {};
|
||||
}
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Resolve shader directory relative to the executable, not the cwd.
|
||||
// Tries: <exeDir>/assets/shaders then <exeDir>/../assets/shaders
|
||||
static std::string resolveShaderDir(const std::string& hint) {
|
||||
// 1. Use hint if it already resolves
|
||||
if (fs::exists(hint)) return fs::canonical(hint).string();
|
||||
|
||||
// 2. Relative to exe
|
||||
std::string exePath;
|
||||
{
|
||||
char buf[4096] = {};
|
||||
ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
||||
if (n > 0) exePath = std::string(buf, n);
|
||||
}
|
||||
|
||||
if (!exePath.empty()) {
|
||||
fs::path exeDir = fs::path(exePath).parent_path();
|
||||
for (auto candidate : {
|
||||
exeDir / hint,
|
||||
exeDir / "assets/shaders",
|
||||
exeDir / "../assets/shaders"}) {
|
||||
if (fs::exists(candidate)) {
|
||||
std::string resolved = fs::canonical(candidate).string();
|
||||
std::cout << "[shader] Shader dir: " << resolved << "\n";
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cerr << "[shader] WARNING: could not resolve shader dir from hint \""
|
||||
<< hint << "\" — shaders will fail to load.\n";
|
||||
return hint;
|
||||
}
|
||||
|
||||
// ── Visitor: stamp u_hasTexture on every Geometry's StateSet ─────────────────
|
||||
// The shader is applied at the model root, but textures live on per-mesh
|
||||
// StateSets deeper in the tree. This visitor walks down and sets the uniform
|
||||
// on each Geode/Geometry so the fragment shader knows whether to sample.
|
||||
|
||||
// Walk every Geode and stamp u_hasTexture on each Drawable's StateSet.
|
||||
// Textures are stored on osg::Geometry (a Drawable), not on Node — so we
|
||||
// must reach into the Geode's drawable list directly.
|
||||
class HasTextureStamper : public osg::NodeVisitor {
|
||||
public:
|
||||
int texCount = 0, noTexCount = 0;
|
||||
|
||||
HasTextureStamper()
|
||||
: osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) {}
|
||||
|
||||
void apply(osg::Geode& geode) override {
|
||||
for (unsigned i = 0; i < geode.getNumDrawables(); ++i) {
|
||||
osg::Drawable* drawable = geode.getDrawable(i);
|
||||
if (!drawable) continue;
|
||||
|
||||
// Use getStateSet() — don't create one if absent, it means
|
||||
// this drawable truly has no material/texture.
|
||||
osg::StateSet* ss = drawable->getStateSet();
|
||||
if (!ss) {
|
||||
// No StateSet at all — create one and mark no texture
|
||||
ss = drawable->getOrCreateStateSet();
|
||||
ss->addUniform(new osg::Uniform("u_hasTexture", false),
|
||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
++noTexCount;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasTex = ss->getTextureAttribute(
|
||||
0, osg::StateAttribute::TEXTURE) != nullptr;
|
||||
ss->addUniform(new osg::Uniform("u_hasTexture", hasTex),
|
||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
if (hasTex) ++texCount; else ++noTexCount;
|
||||
}
|
||||
traverse(static_cast<osg::Node&>(geode));
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
ShaderManager::ShaderManager(const std::string& shaderDir)
|
||||
: m_shaderDir(resolveShaderDir(shaderDir))
|
||||
{}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
osg::ref_ptr<osg::Program> ShaderManager::buildProgram(const std::string& vertFile,
|
||||
const std::string& fragFile) {
|
||||
std::string vertPath = m_shaderDir + "/" + vertFile;
|
||||
std::string fragPath = m_shaderDir + "/" + fragFile;
|
||||
|
||||
std::string vertSrc = readFile(vertPath);
|
||||
std::string fragSrc = readFile(fragPath);
|
||||
|
||||
if (vertSrc.empty() || fragSrc.empty()) {
|
||||
std::cerr << "[shader] Failed to read shader sources for "
|
||||
<< vertFile << " / " << fragFile << "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto prog = new osg::Program;
|
||||
prog->setName(vertFile + "+" + fragFile);
|
||||
|
||||
auto vert = new osg::Shader(osg::Shader::VERTEX, vertSrc);
|
||||
auto frag = new osg::Shader(osg::Shader::FRAGMENT, fragSrc);
|
||||
vert->setFileName(vertPath);
|
||||
frag->setFileName(fragPath);
|
||||
|
||||
prog->addShader(vert);
|
||||
prog->addShader(frag);
|
||||
|
||||
std::cout << "[shader] Built program: " << prog->getName() << "\n";
|
||||
return prog;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void ShaderManager::reload() {
|
||||
m_programs.clear();
|
||||
std::cout << "[shader] Programs cleared; will rebuild on next applyTo().\n";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void ShaderManager::setCommonUniforms(osg::StateSet* ss) {
|
||||
ss->addUniform(new osg::Uniform("osg_Sampler0", 0));
|
||||
|
||||
osg::Vec3f lightDir(0.45f, 0.75f, 0.5f);
|
||||
lightDir.normalize();
|
||||
ss->addUniform(new osg::Uniform("u_lightDirVS", lightDir));
|
||||
ss->addUniform(new osg::Uniform("u_lightColor", osg::Vec3f(1.0f, 0.95f, 0.85f)));
|
||||
ss->addUniform(new osg::Uniform("u_ambientColor", osg::Vec3f(0.25f, 0.25f, 0.30f)));
|
||||
|
||||
// Default false; HasTextureStamper overrides per-mesh below
|
||||
ss->addUniform(new osg::Uniform("u_hasTexture", false));
|
||||
}
|
||||
|
||||
void ShaderManager::setCelUniforms(osg::StateSet* ss) {
|
||||
setCommonUniforms(ss);
|
||||
ss->addUniform(new osg::Uniform("u_bands", 4));
|
||||
ss->addUniform(new osg::Uniform("u_bandSharpness", 0.9f));
|
||||
}
|
||||
|
||||
void ShaderManager::setToonUniforms(osg::StateSet* ss) {
|
||||
setCommonUniforms(ss);
|
||||
ss->addUniform(new osg::Uniform("u_bands", 4));
|
||||
ss->addUniform(new osg::Uniform("u_specularThreshold", 0.92f));
|
||||
ss->addUniform(new osg::Uniform("u_specularIntensity", 0.6f));
|
||||
ss->addUniform(new osg::Uniform("u_rimThreshold", 0.65f));
|
||||
ss->addUniform(new osg::Uniform("u_rimIntensity", 0.4f));
|
||||
ss->addUniform(new osg::Uniform("u_rimColor", osg::Vec3f(0.7f, 0.85f, 1.0f)));
|
||||
ss->addUniform(new osg::Uniform("u_outlineColor", osg::Vec3f(0.05f, 0.02f, 0.08f)));
|
||||
ss->addUniform(new osg::Uniform("u_outlinePass", false));
|
||||
ss->addUniform(new osg::Uniform("u_outlineWidth", 0.025f));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void ShaderManager::applyTo(osg::Node* node, const std::string& mode) {
|
||||
if (!node) return;
|
||||
|
||||
osg::StateSet* ss = node->getOrCreateStateSet();
|
||||
|
||||
// ── Flat: bind an empty program to explicitly disable any active shader.
|
||||
// removeAttribute() leaves OSG in an undefined state; an empty osg::Program
|
||||
// forces the fixed-function path reliably.
|
||||
if (mode == "flat") {
|
||||
auto emptyProg = new osg::Program;
|
||||
ss->setAttribute(emptyProg,
|
||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
ss->setMode(GL_LIGHTING, osg::StateAttribute::ON);
|
||||
std::cout << "[shader] Mode: flat\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Build / fetch compiled program ────────────────────────────────────────
|
||||
if (m_programs.find(mode) == m_programs.end()) {
|
||||
osg::ref_ptr<osg::Program> prog;
|
||||
if (mode == "cel") prog = buildProgram("cel.vert", "cel.frag");
|
||||
else if (mode == "toon") prog = buildProgram("toon.vert", "toon.frag");
|
||||
else {
|
||||
std::cerr << "[shader] Unknown mode: " << mode << "\n";
|
||||
return;
|
||||
}
|
||||
if (!prog) {
|
||||
std::cerr << "[shader] Build failed for mode \"" << mode
|
||||
<< "\" — falling back to flat.\n";
|
||||
applyTo(node, "flat");
|
||||
return;
|
||||
}
|
||||
m_programs[mode] = prog;
|
||||
}
|
||||
|
||||
ss->setAttribute(m_programs[mode],
|
||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
|
||||
|
||||
// ── Uniforms at root ──────────────────────────────────────────────────────
|
||||
if (mode == "cel") setCelUniforms(ss);
|
||||
else if (mode == "toon") setToonUniforms(ss);
|
||||
|
||||
// ── Stamp u_hasTexture on every mesh's StateSet ───────────────────────────
|
||||
HasTextureStamper stamper;
|
||||
node->accept(stamper);
|
||||
|
||||
std::cout << "[shader] Mode: " << mode
|
||||
<< " (textures: " << stamper.texCount
|
||||
<< " no-tex: " << stamper.noTexCount << ")\n";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/*static*/
|
||||
void ShaderManager::setLightDir(osg::StateSet* ss, const osg::Vec3f& dirVS) {
|
||||
if (auto* u = ss->getUniform("u_lightDirVS"))
|
||||
u->set(dirVS);
|
||||
}
|
||||
Reference in New Issue
Block a user