From 9fbddb6ffed2d778999747ef31b77a512d0ce216 Mon Sep 17 00:00:00 2001 From: stujones11 Date: Thu, 24 May 2018 22:28:00 +0100 Subject: [PATCH] Initial commit --- .gitignore | 6 + CMakeLists.txt | 42 ++ LICENSE | 21 + LICENSE.txt | 57 +++ README.md | 78 ++++ assets/browse.png | Bin 0 -> 584 bytes assets/fontlucida.png | Bin 0 -> 19727 bytes assets/pause.png | Bin 0 -> 285 bytes assets/play_fwd.png | Bin 0 -> 410 bytes assets/play_rev.png | Bin 0 -> 395 bytes assets/sam_icon.svg | 414 +++++++++++++++++++ assets/sam_icon_128.png | Bin 0 -> 8500 bytes assets/sam_icon_16.png | Bin 0 -> 409 bytes assets/skip_fwd.png | Bin 0 -> 433 bytes assets/skip_rev.png | Bin 0 -> 422 bytes assets/title.png | Bin 0 -> 2744 bytes media/blank.png | Bin 0 -> 95 bytes media/character.b3d | Bin 0 -> 73433 bytes media/character.png | Bin 0 -> 2754 bytes media/pickaxe.obj | 881 ++++++++++++++++++++++++++++++++++++++++ media/pickaxe.png | Bin 0 -> 219 bytes screenshot.png | Bin 0 -> 45753 bytes src/cmake_config.h.in | 5 + src/config.cpp | 82 ++++ src/config.h | 37 ++ src/dialog.cpp | 492 ++++++++++++++++++++++ src/dialog.h | 109 +++++ src/gui.cpp | 573 ++++++++++++++++++++++++++ src/gui.h | 133 ++++++ src/main.cpp | 74 ++++ src/scene.cpp | 320 +++++++++++++++ src/scene.h | 64 +++ src/trackball.cpp | 84 ++++ src/trackball.h | 33 ++ src/viewer.cpp | 640 +++++++++++++++++++++++++++++ src/viewer.h | 78 ++++ 36 files changed, 4223 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 assets/browse.png create mode 100644 assets/fontlucida.png create mode 100644 assets/pause.png create mode 100644 assets/play_fwd.png create mode 100644 assets/play_rev.png create mode 100644 assets/sam_icon.svg create mode 100644 assets/sam_icon_128.png create mode 100644 assets/sam_icon_16.png create mode 100644 assets/skip_fwd.png create mode 100644 assets/skip_rev.png create mode 100644 assets/title.png create mode 100644 media/blank.png create mode 100644 media/character.b3d create mode 100644 media/character.png create mode 100644 media/pickaxe.obj create mode 100644 media/pickaxe.png create mode 100644 screenshot.png create mode 100644 src/cmake_config.h.in create mode 100644 src/config.cpp create mode 100644 src/config.h create mode 100644 src/dialog.cpp create mode 100644 src/dialog.h create mode 100644 src/gui.cpp create mode 100644 src/gui.h create mode 100644 src/main.cpp create mode 100644 src/scene.cpp create mode 100644 src/scene.h create mode 100644 src/trackball.cpp create mode 100644 src/trackball.h create mode 100644 src/viewer.cpp create mode 100644 src/viewer.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c5eec1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +## Generic ignorable patterns and files +*~ +.*.swp +*bak* +tags +*.vim diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0447e25 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 2.6) +project(samviewer) + +set(CMAKE_CXX_STANDARD 11) + +set(PROJECT_NAME_CAPITALIZED "SAM-Viewer") +set(PROJECT_LINK_URL "https://github.com/stujones11/SAM-Viewer") +set(PROJECT_LINK_TEXT "github.com/stujones11/SAM-Viewer") + +set(VERSION_MAJOR 0) +set(VERSION_MINOR 1) +set(VERSION_PATCH 0) +set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") + +add_definitions(-DUSE_CMAKE_CONFIG_H) + +configure_file ( + "${PROJECT_SOURCE_DIR}/src/cmake_config.h.in" + "${PROJECT_SOURCE_DIR}/src/cmake_config.h" +) + +if(NOT IRRLICHT_INCLUDE_DIR) + set(IRRLICHT_INCLUDE_DIR "/usr/include/irrlicht") +endif() + +if(NOT IRRLICHT_LIBRARY) + set(IRRLICHT_LIBRARY "/usr/local/lib/libIrrlicht.so") +endif() + +include_directories( + ${IRRLICHT_INCLUDE_DIR} + ${PROJECT_SOURCE_DIR}/src +) +file(GLOB SRCS src/*.cpp) +file(MAKE_DIRECTORY "bin") + +set(EXECUTABLE_OUTPUT_PATH "${CMAKE_SOURCE_DIR}/bin") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") + +add_executable(${PROJECT_NAME} ${SRCS}) +target_link_libraries(${PROJECT_NAME} ${IRRLICHT_LIBRARY}) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab60297 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1c31c9d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,57 @@ +SAM-Viewer +========== + +License of SAM-Viewer source code +--------------------------------- + +Copyright (C) 2018, Stuart Jones - (MIT License) + +License of SAM-Viewer media +--------------------------- + +Includes all textures and models contained in this distribution. + +Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) + +Minetest media files: + + character.b3d + character.png + pickaxe.png + blank.png + +Copyright (C) 2010-2018 celeron55, Perttu Ahola + +Irrlicht media files: + + fontlucida.png + +All other media files: + +Copyright (C) 2018, Stuart Jones - (CC BY-SA 3.0) + +Irrlicht +-------- + +This program uses the Irrlicht Engine. http://irrlicht.sourceforge.net/ + +The Irrlicht Engine License: + + Copyright (C) 2002-2015 Nikolaus Gebhardt + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be clearly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..003727b --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +SAM-Viewer +========== + +**Skin & Model Viewer - Version 0.1.0** + +A simple 3d mesh viewer built with Irrlicht rendering engine. + +Features +-------- + +* Wielded item or 'attachment' model support. +* Multiple textures, up to 6 material layers. +* Mesh debug view. (wire-frame, skeleton and normals) +* Animation playback amd frame controls. + +Supported Formats +----------------- + +Compatible with all mesh formats supported by Irrlicht engine. + +* B3D files (.b3d) +* DirectX (.x) +* Alias Wavefront Maya (.obj) +* 3D Studio meshes (.3ds) +* Lightwave Objects (.lwo) +* COLLADA 1.4 (.xml, .dae) + +Installation +------------ + +For now this assumes you are using some sane linux distro and have +a c++11 compliant compiler, although I am pretty sure it could be +made to work on any platform or device that meets the requirements. + +**Requirements:** cmake, opengl, Irrlicht +``` +cmake . +make -j2 +``` + +**CMake options:** (defaults) +``` +IRRLICHT_INCLUDE_DIR=/usr/include/irrlicht +IRRLICHT_LIBRARY="/usr/local/lib/libIrrlicht.so" +``` + +**Example:** +``` +cmake . -DIRRLICHT_LIBRARY="/usr/lib/libIrrlicht.so"` +``` + +Controls +-------- + +| Control | Action | +|-------------------------------|----------------------------------------------------------------| +| Left mouse button + move | Trackball style rotation | +| Mouse wheel | Zoom | +| + | Zoom in | +| - | Zoom out | +| Arrow keys | Rotate around X and Y axes in 15 degree steps | +| Z, X | Rotate around Z axis in 15 degree steps | +| Home | Reset zoom and rotation | +| F5 | Reload textures | +| Space | Jump (experimental) | + +To Do +----- + +* Improve file-browser. +* Basic lighting. +* Image capture tools. + +Screenshot +---------- + +![Imgur](https://i.imgur.com/xIS7pRj.png) + diff --git a/assets/browse.png b/assets/browse.png new file mode 100644 index 0000000000000000000000000000000000000000..1286a5e47690ed9d508acf8c69767f1f4731e818 GIT binary patch literal 584 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 Vj@RZU_=gF|P|sAiRNL{WatTi@9HmZEmqZ)% zZZIDBIeEqEU22Pz8J^53?T$@ernAWPi~OR6uO`lPPm^OjA{H&5d%r^Y_pjbRTYhcd a!|*TF(lqwPk&{4IGI+ZBxvX;vm5Gpk9pg#>`Jr(bn3o0xa>4iun)D32oiF>B6Z);vBOTmY7xSYf|D^ zq!c0ZgC+c#Rp*4BUe1Sml&3%Wmo+nTpX9cP1kZ(^4xD+-kk|Rb_VlLD7Jcfu3Zb1Y zzZuG0p4=#$Gx6H51;QaBt*frGyO^Xrvrd!S()8?!z=4jL{Q=c&dIAh<7(03l`}e)i z|I*07ptNcF^PlX-@m2Glm$d!c#P#4%>D}`ASYHCJWxf1$Ru= zHcbg?^)M~aQBn^SxofU(!`gX*xncUc`zJ1a;$F2dHg?L=V>f`|$l&Sf=d#Wzp$Pyd CNueSD literal 0 HcmV?d00001 diff --git a/assets/play_rev.png b/assets/play_rev.png new file mode 100644 index 0000000000000000000000000000000000000000..4d75bac5213f7c61d78344e7cbd8f52ae2dcefa9 GIT binary patch literal 395 zcmV;60d)R}P)I@hI_k*da>Oxgd&1?!Rmc^q{+Y5hL pmJdzSj71~|Ud-%qd6%ntY(FE)Uz3N}8KnRK002ovPDHLkV1n`-pt}G7 literal 0 HcmV?d00001 diff --git a/assets/sam_icon.svg b/assets/sam_icon.svg new file mode 100644 index 0000000..1642241 --- /dev/null +++ b/assets/sam_icon.svg @@ -0,0 +1,414 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/sam_icon_128.png b/assets/sam_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..9578987bebaaaacbeaeb0ff8dd76c5a4de3674fe GIT binary patch literal 8500 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 b^1t#87KC0-4Zj#r00000NkvXXu0mjfOz5`k literal 0 HcmV?d00001 diff --git a/assets/skip_rev.png b/assets/skip_rev.png new file mode 100644 index 0000000000000000000000000000000000000000..68bb1d18e97c7df2a2be53954c33e0b4438a5119 GIT binary patch literal 422 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 literal 0 HcmV?d00001 diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..78c75bef9b21d9ec65f135adac7f61b27a4b8b55 GIT binary patch literal 45753 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 literal 0 HcmV?d00001 diff --git a/src/cmake_config.h.in b/src/cmake_config.h.in new file mode 100644 index 0000000..c5452a3 --- /dev/null +++ b/src/cmake_config.h.in @@ -0,0 +1,5 @@ +// Template for cmake_config.h + +#define D_ABOUT_LINK_URL "@PROJECT_LINK_URL@" +#define D_ABOUT_LINK_TEXT "@PROJECT_LINK_TEXT@" +#define D_VERSION "@VERSION_STRING@" diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..9b28cfb --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,82 @@ +#include +#include + +#include "config.h" + +Vector::Vector(const std::string &str) +{ + sscanf(str.c_str(), "%f,%f,%f", &x, &y, &z); +} + +bool Config::load() +{ + std::ifstream file(filename.c_str()); + if (!file) + return false; + + std::string line; + while (std::getline(file, line)) + { + auto index = line.find("="); + std::string key = line.substr(0, index); + std::string value = line.substr(index + 1); + config[key] = value; + } + file.close(); + return true; +} + +bool Config::save() +{ + std::ofstream file(filename.c_str()); + if (!file) + return false; + + for(std::map::iterator it = config.begin(); + it != config.end(); it++) + { + file << it->first << "=" << it->second << "\n"; + } + file.close(); + return true; +} + +bool Config::hasKey(const std::string &key) const +{ + return config.find(key) != config.end(); +} + +void Config::set(const std::string &key, const std::string &value) +{ + config[key] = value; +} + +const std::string &Config::get(const std::string &key) const +{ + return config.find(key)->second; +} + +const char *Config::getCStr(const std::string &key) const +{ + return get(key).c_str(); +} + +int Config::getInt(const std::string &key) const +{ + return atoi(getCStr(key)); +} + +int Config::getHex(const std::string &key) const +{ + return std::stoul(get(key), nullptr, 16); +} + +bool Config::getBool(const std::string &key) const +{ + return get(key) == "true"; +} + +Vector Config::getVector(const std::string &key) const +{ + return Vector(get(key)); +} \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..49bc918 --- /dev/null +++ b/src/config.h @@ -0,0 +1,37 @@ +#ifndef D_CONFIG_H +#define D_CONFIG_H + +#include + +class Vector +{ +public: + Vector() : x(0), y(0), z(0) {} + Vector(float x, float y, float z) : x(x), y(y), z(z) {} + Vector(const std::string &str); + float x; + float y; + float z; +}; + +class Config +{ +public: + Config(const std::string &filename) : filename(filename) {} + bool load(); + bool save(); + bool hasKey(const std::string &key) const; + void set(const std::string &key, const std::string &value); + const std::string &get(const std::string &key) const; + const char *getCStr(const std::string &key) const; + int getInt(const std::string &key) const; + int getHex(const std::string &key) const; + bool getBool(const std::string &key) const; + Vector getVector(const std::string &key) const; + +private: + std::map config; + std::string filename; +}; + +#endif // D_CONFIG_H diff --git a/src/dialog.cpp b/src/dialog.cpp new file mode 100644 index 0000000..6a383c9 --- /dev/null +++ b/src/dialog.cpp @@ -0,0 +1,492 @@ +#include +#include +#include + +#ifdef USE_CMAKE_CONFIG_H +#include "cmake_config.h" +#else +#define D_ABOUT_LINK_URL "https://github.com/stujones11/SAM-Viewer" +#define D_ABOUT_LINK_TEXT "github.com/stujones11/SAM-Viewer" +#define D_VERSION "0.0.0" +#endif + +#include "config.h" +#include "scene.h" +#include "dialog.h" + +static inline void open_url(std::string url) +{ + system((std::string("xdg-open \"") + url + std::string("\"")).c_str()); +} + +HyperlinkCtrl::HyperlinkCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, const wchar_t *title, std::string url) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle), + url(url), + is_active(false) +{ + IGUIStaticText *text = env->addStaticText(title, + rect(0,0,rectangle.getWidth(),20), false, false, this); + text->setOverrideColor(SColor(255,0,0,255)); + text->setTextAlignment(EGUIA_CENTER, EGUIA_CENTER); +} + +void HyperlinkCtrl::draw() +{ + if (is_active) + { + IVideoDriver *driver = Environment->getVideoDriver(); + rect pos = getAbsolutePosition(); + vector2di end = pos.LowerRightCorner; + vector2di start = end - vector2di(pos.getWidth(), 0); + driver->draw2DLine(start, end, SColor(255,0,0,255)); + } + IGUIElement::draw(); +} + +bool HyperlinkCtrl::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT) + { + if (event.GUIEvent.EventType == EGET_ELEMENT_HOVERED) + is_active = true; + else if (event.GUIEvent.EventType == EGET_ELEMENT_LEFT) + is_active = false; + } + else if (is_active && event.EventType == EET_MOUSE_INPUT_EVENT && + event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) + { + open_url(url); + } + return IGUIElement::OnEvent(event); +} + +static inline bool isValidHexString(std::string hex) +{ + return (hex.length() == 6 && + hex.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos); +} + +ColorCtrl::ColorCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, const wchar_t *label) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle) +{ + IVideoDriver *driver = env->getVideoDriver(); + IGUIStaticText *text = env->addStaticText(label, rect(0,0,160,20), + false, false, this); + IGUIEditBox *edit = env->addEditBox(L"", rect(180,0,250,20), true, + this, E_DIALOG_ID_COLOR_EDIT); + edit->setMax(6); + edit->setToolTipText(L"Hex color string RRGGBB"); + + ITexture *texture = driver->findTexture("color_preview"); + if (!texture) + { + IImage *image = driver->createImage(ECF_A8R8G8B8, dimension2du(30,20)); + image->fill(SColor(255,255,255,255)); + texture = driver->addTexture("color_preview", image); + image->drop(); + } + IGUIImage *preview = env->addImage(rect(270,0,300,20), this, + E_DIALOG_ID_COLOR_PREVIEW); + preview->setImage(texture); +} + +void ColorCtrl::setColor(const std::string &hex) +{ + if (!isValidHexString(hex)) + return; + + stringw text = hex.c_str(); + IGUIEditBox *edit = (IGUIEditBox*) + getElementFromId(E_DIALOG_ID_COLOR_EDIT); + if (edit) + edit->setText(text.c_str()); + IGUIImage *preview = (IGUIImage*) + getElementFromId(E_DIALOG_ID_COLOR_PREVIEW); + if (preview) + { + SColor color; + color.color = std::stoul(hex, nullptr, 16); + color.setAlpha(255); + preview->setColor(color); + } +} + +std::string ColorCtrl::getColor() const +{ + std::string hex = ""; + IGUIEditBox *edit = (IGUIEditBox*) + getElementFromId(E_DIALOG_ID_COLOR_EDIT); + if (edit) + hex = stringc(edit->getText()).c_str(); + return hex; +} + +bool ColorCtrl::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT && + event.GUIEvent.EventType == EGET_EDITBOX_CHANGED && + event.GUIEvent.Caller->getID() == E_DIALOG_ID_COLOR_EDIT) + { + IGUIEditBox *edit = (IGUIEditBox*)event.GUIEvent.Caller; + std::string hex = stringc(edit->getText()).c_str(); + setColor(hex); + } + return IGUIElement::OnEvent(event); +} + +AboutDialog::AboutDialog(IGUIEnvironment *env, IGUIElement *parent, + s32 id, const rect &rectangle) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle) +{ + IVideoDriver *driver = env->getVideoDriver(); + ITexture *icon = driver->findTexture("sam_icon_128.png"); + if (!icon) + icon = driver->getTexture("sam_icon_128.png"); + if (icon) + { + IGUIImage *image = env->addImage(rect(86,10,214,138), this); + image->setImage(icon); + + } + ITexture *title = driver->findTexture("title.png"); + if (!title) + title = driver->getTexture("title.png"); + if (title) + { + IGUIImage *image = env->addImage(rect(50,140,250,170), this); + image->setImage(title); + } + stringw desc = stringw("Skin & Model Viewer - Version ") + D_VERSION; + IGUIStaticText *text; + text = env->addStaticText(desc.c_str(), rect(20,175,280,195), + false, false, this); + text->setTextAlignment(EGUIA_CENTER, EGUIA_CENTER); + + HyperlinkCtrl *link = new HyperlinkCtrl(env, this, + E_DIALOG_ID_ABOUT_LINK, rect(32,200,268,216), + stringw(D_ABOUT_LINK_TEXT).c_str(), D_ABOUT_LINK_URL); + link->drop(); + + IGUIButton *button = env->addButton(rect(110,235,190,265), this, + E_DIALOG_ID_ABOUT_OK, L"OK"); +} + + +SettingsDialog::SettingsDialog(IGUIEnvironment *env, IGUIElement *parent, + s32 id, const rect &rectangle, Config *conf) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle), + conf(conf) +{ + IGUITabControl *tabs = env->addTabControl(rect(2,2,398,250), + this, true, true); + + IGUITab *tab_general = tabs->addTab(L"General"); + IGUITab *tab_debug = tabs->addTab(L"Debug"); + IGUIStaticText *text; + IGUIEditBox *edit; + IGUISpinBox *spin; + IGUICheckBox *check; + IGUIButton *button; + ColorCtrl *color; + + color = new ColorCtrl(env, tab_general, E_DIALOG_ID_BG_COLOR, + rect(20,20,320,40), L"Background Color:"); + color->setColor(conf->get("bg_color")); + color->drop(); + + color = new ColorCtrl(env, tab_general, E_DIALOG_ID_GRID_COLOR, + rect(20,50,320,70), L"Grid Color:"); + color->setColor(conf->get("grid_color")); + color->drop(); + + text = env->addStaticText(L"Wield Attachment Bone:", rect(20,80,180,100), + false, false, tab_general, -1); + stringw bone_name = conf->getCStr("wield_bone"); + edit = env->addEditBox(bone_name.c_str(), rect(200,80,320,100), + true, tab_general, E_DIALOG_ID_WIELD_BONE); + + text = env->addStaticText(L"Default Screen Width:", + rect(20,110,180,130), false, false, tab_general, -1); + spin = env->addSpinBox(L"", rect(200,110,270,130), + true, tab_general, E_DIALOG_ID_SCREEN_WIDTH); + spin->setValue(conf->getInt("screen_width")); + spin->setDecimalPlaces(0); + + text = env->addStaticText(L"Default Screen Height:", + rect(20,140,180,160), false, false, tab_general, -1); + spin = env->addSpinBox(L"", rect(200,140,270,160), + true, tab_general, E_DIALOG_ID_SCREEN_HEIGHT); + spin->setValue(conf->getInt("screen_height")); + spin->setDecimalPlaces(0); + + check = env->addCheckBox(false, rect(20,20,380,40), tab_debug, + E_DIALOG_ID_DEBUG_BBOX, L"Show bounding boxes"); + check->setChecked(conf->getInt("debug_flags") & EDS_BBOX); + check = env->addCheckBox(false, rect(20,50,380,70), tab_debug, + E_DIALOG_ID_DEBUG_NORMALS, L"Show vertex normals"); + check->setChecked(conf->getInt("debug_flags") & EDS_NORMALS); + check = env->addCheckBox(false, rect(20,80,380,100), tab_debug, + E_DIALOG_ID_DEBUG_SKELETON, L"Show skeleton"); + check->setChecked(conf->getInt("debug_flags") & EDS_SKELETON); + check = env->addCheckBox(false, rect(20,110,380,130), tab_debug, + E_DIALOG_ID_DEBUG_WIREFRANE, L"Wireframe overaly"); + check->setChecked(conf->getInt("debug_flags") & EDS_MESH_WIRE_OVERLAY); + check = env->addCheckBox(false, rect(20,140,380,160), tab_debug, + E_DIALOG_ID_DEBUG_ALPHA, L"Use transparent material"); + check->setChecked(conf->getInt("debug_flags") & EDS_HALF_TRANSPARENCY); + check = env->addCheckBox(false, rect(20,170,380,190), tab_debug, + E_DIALOG_ID_DEBUG_BUFFERS, L"Show all mesh buffers"); + check->setChecked(conf->getInt("debug_flags") & EDS_BBOX_BUFFERS); + + button = env->addButton(rect(315,255,395,285), this, + E_DIALOG_ID_SETTINGS_OK, L"OK"); + button = env->addButton(rect(230,255,310,285), this, + E_DIALOG_ID_SETTINGS_CANCEL, L"Cancel"); +} + +bool SettingsDialog::isBoxChecked(s32 id) const +{ + IGUICheckBox *check = (IGUICheckBox*)getElementFromId(id, true); + if (check) + return check->isChecked(); + + return false; +} + +bool SettingsDialog::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT && + event.GUIEvent.EventType == EGET_BUTTON_CLICKED && + event.GUIEvent.Caller->getID() == E_DIALOG_ID_SETTINGS_OK) + { + IGUIEditBox *edit; + IGUISpinBox *spin; + ColorCtrl *color; + + color = (ColorCtrl*)getElementFromId(E_DIALOG_ID_BG_COLOR, true); + if (color) + { + const std::string hex = color->getColor(); + if (isValidHexString(hex)) + conf->set("bg_color", hex); + } + color = (ColorCtrl*)getElementFromId(E_DIALOG_ID_GRID_COLOR, true); + if (color) + { + const std::string hex = color->getColor(); + if (isValidHexString(hex)) + conf->set("grid_color", hex); + } + edit = (IGUIEditBox*) + getElementFromId(E_DIALOG_ID_WIELD_BONE, true); + std::string bone = stringc(edit->getText()).c_str(); + conf->set("wield_bone", bone); + + spin = (IGUISpinBox*) + getElementFromId(E_DIALOG_ID_SCREEN_WIDTH, true); + u32 width = spin->getValue(); + conf->set("screen_width", std::to_string(width)); + spin = (IGUISpinBox*) + getElementFromId(E_DIALOG_ID_SCREEN_HEIGHT, true); + u32 height = spin->getValue(); + conf->set("screen_height", std::to_string(height)); + + u32 flags = 0; + if (isBoxChecked(E_DIALOG_ID_DEBUG_BBOX)) + flags |= EDS_BBOX; + if (isBoxChecked(E_DIALOG_ID_DEBUG_NORMALS)) + flags |= EDS_NORMALS; + if (isBoxChecked(E_DIALOG_ID_DEBUG_SKELETON)) + flags |= EDS_SKELETON; + if (isBoxChecked(E_DIALOG_ID_DEBUG_WIREFRANE)) + flags |= EDS_MESH_WIRE_OVERLAY; + if (isBoxChecked(E_DIALOG_ID_DEBUG_ALPHA)) + flags |= EDS_HALF_TRANSPARENCY; + if (isBoxChecked(E_DIALOG_ID_DEBUG_BUFFERS)) + flags |= EDS_BBOX_BUFFERS; + conf->set("debug_flags", std::to_string(flags)); + } + return IGUIElement::OnEvent(event); +} + +TexturesDialog::TexturesDialog(IGUIEnvironment *env, IGUIElement *parent, + s32 id, const rect &rectangle, Config *conf, ISceneManager *smgr) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle), + conf(conf), + smgr(smgr) +{ + IGUITabControl *tabs = env->addTabControl(rect(2,2,398,250), this, + true, true); + + IGUITab *tab_model = tabs->addTab(L"Model"); + IGUITab *tab_wield = tabs->addTab(L"Wield"); + IGUIStaticText *text; + IGUIEditBox *edit; + IGUIButton *button; + stringw fn; + std::string key; + + ITexture *image = getTexture("browse.png"); + ISceneNode *model = smgr->getSceneNodeFromId(E_SCENE_ID_MODEL); + ISceneNode *wield = smgr->getSceneNodeFromId(E_SCENE_ID_WIELD); + u32 mc_model = (model) ? model->getMaterialCount() : 0; + u32 mc_wield = (wield) ? wield->getMaterialCount() : 0; + + for (s32 i = 0; i < 6; ++i) + { + s32 top = i * 30 + 20; + stringw num = stringw(i + 1); + + key = "model_texture_" + std::to_string(i + 1); + fn = conf->getCStr(key); + text = env->addStaticText(num.c_str(), rect(15,top,25,top+20), + false, false, tab_model, -1); + edit = env->addEditBox(fn.c_str(), rect(35,top,350,top+20), + true, tab_model, E_TEXTURE_ID_MODEL + i); + edit->setEnabled(i < mc_model); + edit->setOverrideColor(SColor(255,255,0,0)); + edit->enableOverrideColor(false); + button = env->addButton(rect(360,top,380,top+20), tab_model, + E_BUTTON_ID_MODEL + i); + button->setToolTipText(L"Browse"); + if (image) + { + button->setImage(image); + button->setUseAlphaChannel(true); + button->setDrawBorder(false); + } + button->setEnabled(i < mc_model); + + key = "wield_texture_" + std::to_string(i + 1); + fn = conf->getCStr(key); + text = env->addStaticText(num.c_str(), rect(15,top,25,top+20), + false, false, tab_wield, -1); + edit = env->addEditBox(fn.c_str(), rect(35,top,350,top+20), + true, tab_wield, E_TEXTURE_ID_WIELD + i); + edit->setEnabled(i < mc_wield); + edit->setOverrideColor(SColor(255,255,0,0)); + edit->enableOverrideColor(false); + button = env->addButton(rect(360,top,380,top+20), tab_wield, + E_BUTTON_ID_WIELD + i); + if (image) + { + button->setImage(image); + button->setUseAlphaChannel(true); + button->setDrawBorder(false); + } + button->setEnabled(i < mc_wield); + } + button = env->addButton(rect(315,255,395,285), this, + E_DIALOG_ID_TEXTURES_OK, L"OK"); + button = env->addButton(rect(230,255,310,285), this, + E_DIALOG_ID_TEXTURES_CANCEL, L"Cancel"); +} + +ITexture *TexturesDialog::getTexture(const io::path &filename) +{ + IVideoDriver *driver = Environment->getVideoDriver(); + ITexture *texture = driver->findTexture(filename); + if (!texture) + texture = driver->getTexture(filename); + return texture; +} + +bool TexturesDialog::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT) + { + s32 id = event.GUIEvent.Caller->getID(); + if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) + { + if (event.GUIEvent.Caller->getType() == EGUIET_EDIT_BOX) + { + IGUIEditBox *edit = (IGUIEditBox*)event.GUIEvent.Caller; + if (edit) + { + stringc fn = stringc(edit->getText()).c_str(); + edit->enableOverrideColor(!(getTexture(fn))); + } + } + } + else if (event.GUIEvent.EventType == EGET_BUTTON_CLICKED) + { + if (id == E_DIALOG_ID_TEXTURES_OK) + { + ISceneNode *model = smgr->getSceneNodeFromId(E_SCENE_ID_MODEL); + ISceneNode *wield = smgr->getSceneNodeFromId(E_SCENE_ID_WIELD); + IGUIEditBox *edit; + + for (s32 i = 0; i < 6; ++i) + { + std::string idx = std::to_string(i + 1); + edit = (IGUIEditBox*) + getElementFromId(E_TEXTURE_ID_MODEL + i, true); + if (edit && model && i < model->getMaterialCount()) + { + stringc fn = stringc(edit->getText()).c_str(); + ITexture *texture = getTexture(fn); + if (texture) + { + std::string key = "model_texture_" + idx; + conf->set(key, fn.c_str()); + SMaterial &material = model->getMaterial(i); + material.TextureLayer[0].Texture = texture; + } + } + edit = (IGUIEditBox*) + getElementFromId(E_TEXTURE_ID_WIELD + i, true); + if (edit && wield && i < wield->getMaterialCount()) + { + stringc fn = stringc(edit->getText()).c_str(); + ITexture *texture = getTexture(fn); + if (texture) + { + std::string key = "wield_texture_" + idx; + conf->set(key, fn.c_str()); + SMaterial &material = wield->getMaterial(i); + material.TextureLayer[0].Texture = texture; + } + } + } + } + else + { + for (s32 i = 0; i < 6; ++i) + { + if (id == E_BUTTON_ID_MODEL + i) + { + Environment->addFileOpenDialog(L"Open Image File", + true, this, E_TEXTURE_ID_MODEL + i); + break; + } + else if (id == E_BUTTON_ID_WIELD + i) + { + Environment->addFileOpenDialog(L"Open Image File", + true, this, E_TEXTURE_ID_WIELD + i); + break; + } + } + } + } + else if (event.GUIEvent.EventType == EGET_FILE_SELECTED) + { + IGUIFileOpenDialog *dialog = + (IGUIFileOpenDialog*)event.GUIEvent.Caller; + + stringw fn = stringw(dialog->getFileName()); + if (!fn.empty()) + { + s32 id = dialog->getID(); + IGUIEditBox *edit = (IGUIEditBox*)getElementFromId(id, true); + if (edit) + { + edit->setText(fn.c_str()); + edit->enableOverrideColor(!(getTexture(fn))); + } + } + } + } + return IGUIElement::OnEvent(event); +} diff --git a/src/dialog.h b/src/dialog.h new file mode 100644 index 0000000..5eeba62 --- /dev/null +++ b/src/dialog.h @@ -0,0 +1,109 @@ +#ifndef D_DIALOG_H +#define D_DIALOG_H + +using namespace irr; +using namespace core; +using namespace scene; +using namespace gui; +using namespace video; + +enum +{ + E_DIALOG_ID_ABOUT = 0x1000, + E_DIALOG_ID_SETTINGS, + E_DIALOG_ID_BG_COLOR, + E_DIALOG_ID_GRID_COLOR, + E_DIALOG_ID_COLOR_EDIT, + E_DIALOG_ID_COLOR_PREVIEW, + E_DIALOG_ID_WIELD_BONE, + E_DIALOG_ID_SCREEN_WIDTH, + E_DIALOG_ID_SCREEN_HEIGHT, + E_DIALOG_ID_DEBUG_BBOX, + E_DIALOG_ID_DEBUG_NORMALS, + E_DIALOG_ID_DEBUG_SKELETON, + E_DIALOG_ID_DEBUG_WIREFRANE, + E_DIALOG_ID_DEBUG_ALPHA, + E_DIALOG_ID_DEBUG_BUFFERS, + E_DIALOG_ID_ABOUT_OK, + E_DIALOG_ID_ABOUT_LINK, + E_DIALOG_ID_SETTINGS_OK, + E_DIALOG_ID_SETTINGS_CANCEL, + E_DIALOG_ID_TEXTURES, + E_DIALOG_ID_TEXTURES_OK, + E_DIALOG_ID_TEXTURES_CANCEL +}; + +enum +{ + E_TEXTURE_ID_MODEL = 0x2000, + E_TEXTURE_ID_WIELD = 0x2010, + E_BUTTON_ID_MODEL = 0x2020, + E_BUTTON_ID_WIELD = 0x2030 +}; + +class Config; + +class HyperlinkCtrl : public IGUIElement +{ +public: + HyperlinkCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, const wchar_t *title, std::string url); + virtual ~HyperlinkCtrl() {} + virtual void draw(); + virtual bool OnEvent(const SEvent &event); + +private: + std::string url; + bool is_active; +}; + +class ColorCtrl : public IGUIElement +{ +public: + ColorCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, const wchar_t *label); + virtual ~ColorCtrl() {} + virtual bool OnEvent(const SEvent &event); + void setColor(const std::string &hex); + std::string getColor() const; +}; + +class AboutDialog : public IGUIElement +{ +public: + AboutDialog(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle); + virtual ~AboutDialog() {} +}; + +class SettingsDialog : public IGUIElement +{ +public: + SettingsDialog(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, Config *conf); + virtual ~SettingsDialog() {} + virtual bool OnEvent(const SEvent &event); + +private: + bool isBoxChecked(s32 id) const; + void colorFromHexStr(const std::string &hex); + + Config *conf; +}; + +class TexturesDialog : public IGUIElement +{ +public: + TexturesDialog(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, Config *conf, ISceneManager *smgr); + virtual ~TexturesDialog() {} + virtual bool OnEvent(const SEvent &event); + +private: + ITexture *getTexture(const io::path &filename); + + Config *conf; + ISceneManager *smgr; +}; + +#endif // D_DIALOG_H \ No newline at end of file diff --git a/src/gui.cpp b/src/gui.cpp new file mode 100644 index 0000000..a909bd2 --- /dev/null +++ b/src/gui.cpp @@ -0,0 +1,573 @@ +#include +#include +#include + +#include "config.h" +#include "scene.h" +#include "dialog.h" +#include "gui.h" + +VertexCtrl::VertexCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, f32 step, const wchar_t *label) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle), + vertex(0) +{ + IGUIStaticText *text = env->addStaticText(label, + rect(0,0,20,20), false, false, this); + + IGUISpinBox *spin = env->addSpinBox(L"", rect(20,0,120,20), + true, this, E_GUI_ID_VERTEX); + spin->setDecimalPlaces(2); + spin->setValue(0); + spin->setStepSize(step); + spin->setRange(-1000, 1000); +} + +void VertexCtrl::setValue(const f32 &value) +{ + IGUISpinBox *spin = (IGUISpinBox*)getElementFromId(E_GUI_ID_VERTEX); + if (spin) + { + spin->setValue(value); + vertex = value; + } +} + +bool VertexCtrl::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT) + { + if (event.GUIEvent.EventType == EGET_SPINBOX_CHANGED) + { + IGUISpinBox *spin = (IGUISpinBox*)event.GUIEvent.Caller; + if (spin) + vertex = spin->getValue(); + } + } + else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) + { + setValue(vertex); + } + return IGUIElement::OnEvent(event); +} + +VectorCtrl::VectorCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, f32 step, const wchar_t *label) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle), + vector(vector3df(0,0,0)) +{ + IGUIStaticText *text = env->addStaticText(label, + rect(10,0,150,20), false, false, this); + + VertexCtrl *x = new VertexCtrl(env, this, E_GUI_ID_VECTOR_X, + rect(10,30,150,50), step, L"X"); + x->drop(); + VertexCtrl *y = new VertexCtrl(env, this, E_GUI_ID_VECTOR_Y, + rect(10,60,150,80), step, L"Y"); + y->drop(); + VertexCtrl *z = new VertexCtrl(env, this, E_GUI_ID_VECTOR_Z, + rect(10,90,150,110), step, L"Z"); + z->drop(); +} + +void VectorCtrl::setVector(const vector3df &vec) +{ + vector = vec; + VertexCtrl *vertex; + vertex = (VertexCtrl*)getElementFromId(E_GUI_ID_VECTOR_X); + if (vertex) + vertex->setValue(vector.X); + vertex = (VertexCtrl*)getElementFromId(E_GUI_ID_VECTOR_Y); + if (vertex) + vertex->setValue(vector.Y); + vertex = (VertexCtrl*)getElementFromId(E_GUI_ID_VECTOR_Z); + if (vertex) + vertex->setValue(vector.Z); +} + +bool VectorCtrl::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT && + event.GUIEvent.EventType == EGET_SPINBOX_CHANGED) + { + VertexCtrl *vertex = (VertexCtrl*)event.GUIEvent.Caller->getParent(); + if (vertex) + { + switch (vertex->getID()) + { + case E_GUI_ID_VECTOR_X: + vector.X = vertex->getValue(); + break; + case E_GUI_ID_VECTOR_Y: + vector.Y = vertex->getValue(); + break; + case E_GUI_ID_VECTOR_Z: + vector.Z = vertex->getValue(); + break; + default: + break; + } + SEvent new_event = event; + new_event.GUIEvent.Caller = this; + return IGUIElement::OnEvent(new_event); + } + } + return IGUIElement::OnEvent(event); +} + +AnimCtrl::AnimCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle), + button_id(E_GUI_ID_PAUSE) +{ + IVideoDriver *driver = env->getVideoDriver(); + ITexture *image; + IGUIButton *button; + + image = driver->getTexture("skip_rev.png"); + button = env->addButton(rect(0,4,23,27), this, + E_GUI_ID_SKIP_REV); + button->setImage(image); + button->setUseAlphaChannel(true); + + image = driver->getTexture("play_rev.png"); + button = env->addButton(rect(24,4,47,27), this, + E_GUI_ID_PLAY_REV); + button->setImage(image); + button->setUseAlphaChannel(true); + + image = driver->getTexture("pause.png"); + button = env->addButton(rect(48,4,71,27), this, + E_GUI_ID_PAUSE); + button->setImage(image); + button->setUseAlphaChannel(true); + + image = driver->getTexture("play_fwd.png"); + button = env->addButton(rect(72,4,95,27), this, + E_GUI_ID_PLAY_FWD); + button->setImage(image); + button->setUseAlphaChannel(true); + + image = driver->getTexture("skip_fwd.png"); + button = env->addButton(rect(96,4,119,27), this, + E_GUI_ID_SKIP_FWD); + button->setImage(image); + button->setUseAlphaChannel(true); +} + +bool AnimCtrl::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT) + { + if (event.GUIEvent.EventType == EGET_BUTTON_CLICKED) + { + reset(true); + IGUIButton *button = (IGUIButton*)event.GUIEvent.Caller; + button->setEnabled(false); + button->setPressed(true); + button_id = button->getID(); + } + else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) + { + if (button_id == E_GUI_ID_SKIP_FWD || + button_id == E_GUI_ID_SKIP_REV) + { + reset(true); + IGUIButton *button = (IGUIButton*) + getElementFromId(E_GUI_ID_PAUSE); + if (button) + button->setPressed(true); + button_id = E_GUI_ID_PAUSE; + } + } + } + return IGUIElement::OnEvent(event); +} + +void AnimCtrl::reset(bool enabled) +{ + const list &children = getChildren(); + list::ConstIterator iter = children.begin(); + while (iter != children.end()) + { + if ((*iter)->getType() == EGUIET_BUTTON) + { + IGUIButton *button = (IGUIButton*)(*iter); + button->setEnabled(enabled); + button->setPressed(false); + } + ++iter; + } +} + +ToolBox::ToolBox(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, ISceneNode *node) : + IGUIElement(EGUIET_ELEMENT, env, parent, id, rectangle) +{ + smgr = node->getSceneManager(); + node_id = node->getID(); + + IGUIWindow *dialog = (IGUIWindow*)parent; + dialog->setDraggable(false); + VectorCtrl *position = new VectorCtrl(env, this, E_GUI_ID_POSITION, + rect(10,0,150,120), 1.0, L"Position:"); + + VectorCtrl *rotation = new VectorCtrl(env, this, E_GUI_ID_ROTATION, + rect(10,130,150,250), 15.0, L"Rotation:"); + + IGUIStaticText *text; + text = env->addStaticText(L"Scale:", + rect(20,260,150,280), false, false, this); + + text = env->addStaticText(L"%", + rect(20,290,40,310), false, false, this); + + IGUISpinBox *scale = env->addSpinBox(L"", rect(40,290,140,310), + true, this, E_GUI_ID_SCALE); + + scale->setDecimalPlaces(0); + scale->setStepSize(10.0); + scale->setRange(0, 1000); + + position->setVector(node->getPosition()); + position->drop(); + rotation->setVector(node->getRotation()); + rotation->drop(); + scale->setValue(node->getScale().Y * 100); + + text = env->addStaticText(L"Transparency:", + rect(20,330,150,350), false, false, this); + + IGUIComboBox *combo = env->addComboBox(rect(20,360,140,380), + this, E_GUI_ID_MATERIAL); + combo->addItem(L"Opaque"); + combo->addItem(L"Alpha Channel"); + combo->addItem(L"Alpha Test"); + + switch (node->getMaterial(0).MaterialType) + { + case EMT_TRANSPARENT_ALPHA_CHANNEL: + combo->setSelected(1); + break; + case EMT_TRANSPARENT_ALPHA_CHANNEL_REF: + combo->setSelected(2); + break; + default: + break; + } +} + +bool ToolBox::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT) + { + s32 id = event.GUIEvent.Caller->getID(); + if (event.GUIEvent.EventType == EGET_SPINBOX_CHANGED) + { + ISceneNode *node = smgr->getSceneNodeFromId(node_id); + if (node) + { + IGUIElement *elem = (IGUIElement*)event.GUIEvent.Caller; + switch(id) + { + case E_GUI_ID_POSITION: + { + VectorCtrl *position = (VectorCtrl*)elem; + node->setPosition(position->getVector()); + break; + } + case E_GUI_ID_ROTATION: + { + VectorCtrl *rotation = (VectorCtrl*)elem; + node->setRotation(rotation->getVector()); + break; + } + case E_GUI_ID_SCALE: + { + IGUISpinBox *spin = (IGUISpinBox*)elem; + f32 s = spin->getValue() / 100; + node->setScale(vector3df(s,s,s)); + break; + } + default: + break; + } + } + } + else if(event.GUIEvent.EventType == EGET_COMBO_BOX_CHANGED) + { + ISceneNode *node = smgr->getSceneNodeFromId(node_id); + if (node) + { + IGUIComboBox *combo = (IGUIComboBox*)event.GUIEvent.Caller; + switch (combo->getSelected()) + { + case 0: + node->setMaterialType(EMT_SOLID); + break; + case 1: + node->setMaterialType(EMT_TRANSPARENT_ALPHA_CHANNEL); + break; + case 2: + node->setMaterialType(EMT_TRANSPARENT_ALPHA_CHANNEL_REF); + break; + } + } + } + } + return IGUIElement::OnEvent(event); +} + +GUI::GUI(IrrlichtDevice *device, Config *config) : + device(device), + conf(config), + has_focus(false) +{ + IGUIEnvironment *env = device->getGUIEnvironment(); + IGUISkin *skin = env->getSkin(); + IGUIFont *font = env->getFont("fontlucida.png"); + if (font) + skin->setFont(font); + + skin->setColor(EGDC_3D_FACE, SColor(255,232,232,232)); + skin->setColor(EGDC_3D_DARK_SHADOW, SColor(255,160,160,160)); + skin->setColor(EGDC_3D_HIGH_LIGHT, SColor(255,248,248,248)); + skin->setColor(EGDC_3D_LIGHT, SColor(255,255,255,255)); + skin->setColor(EGDC_3D_SHADOW, SColor(255,196,196,196)); + skin->setColor(EGDC_ACTIVE_BORDER, SColor(255,232,232,232)); + skin->setColor(EGDC_INACTIVE_BORDER, SColor(255,232,232,232)); + skin->setColor(EGDC_GRAY_EDITABLE, SColor(255,172,172,172)); + skin->setColor(EGDC_GRAY_TEXT, SColor(255,96,96,96)); + skin->setColor(EGDC_ACTIVE_CAPTION, SColor(255,16,16,16)); + skin->setColor(EGDC_INACTIVE_CAPTION, SColor(255,64,64,64)); + + for (s32 i=0; i < EGDC_COUNT; ++i) + { + SColor col = skin->getColor((EGUI_DEFAULT_COLOR)i); + col.setAlpha(255); + skin->setColor((EGUI_DEFAULT_COLOR)i, col); + } +} + +void GUI::initMenu() +{ + IGUIEnvironment *env = device->getGUIEnvironment(); + IGUIContextMenu *menu = env->addMenu(0, E_GUI_ID_MENU); + menu->addItem(L"File", -1, true, true); + menu->addItem(L"Edit", -1, true, true); + menu->addItem(L"View", -1, true, true); + menu->addItem(L"Help", -1, true, true); + + IGUIContextMenu *submenu; + submenu = menu->getSubMenu(0); + submenu->addItem(L"Load Model Mesh", E_GUI_ID_LOAD_MODEL_MESH); + submenu->addItem(L"Load Wield Mesh", E_GUI_ID_LOAD_WIELD_MESH); + submenu->addSeparator(); + submenu->addItem(L"Save Configuration", E_GUI_ID_SAVE_CONFIG); + submenu->addSeparator(); + submenu->addItem(L"Quit", E_GUI_ID_QUIT); + + submenu = menu->getSubMenu(1); + submenu->addItem(L"Textures", E_GUI_ID_TEXTURES_DIALOG); + submenu->addSeparator(); + submenu->addItem(L"Preferences", E_GUI_ID_SETTINGS_DIALOG); + + submenu = menu->getSubMenu(2); + submenu->addItem(L"Model Toolbox", E_GUI_ID_TOOLBOX_MODEL, true, false, + false, true); + submenu->addItem(L"Wield Toolbox", E_GUI_ID_TOOLBOX_WIELD, true, false, + false, true); + submenu->addSeparator(); + submenu->addItem(L"Show Grid", E_GUI_ID_SHOW_GRID, true, false, + true, true); + submenu->addItem(L"Show Axes", E_GUI_ID_SHOW_AXES, true, false, + true, true); + submenu->addSeparator(); + submenu->addItem(L"Projection", -1, true, true); + submenu->addItem(L"Filters", -1, true, true); + submenu->addSeparator(); + submenu->addItem(L"Show Wield Item", E_GUI_ID_ENABLE_WIELD, true, false, + conf->getBool("wield_show"), true); + submenu->addItem(L"Backface Culling", E_GUI_ID_BACK_FACE_CULL, true, false, + conf->getBool("backface_cull"), true); + submenu->addSeparator(); + submenu->addItem(L"Model Debug Info", E_GUI_ID_DEBUG_INFO, true, false, + conf->getBool("debug_info"), true); + + submenu = menu->getSubMenu(2)->getSubMenu(6); + submenu->addItem(L"Perspective", E_GUI_ID_PERSPECTIVE, true, false, + !conf->getBool("ortho"), true); + submenu->addItem(L"Orthogonal", E_GUI_ID_ORTHOGONAL, true, false, + conf->getBool("ortho"), true); + + submenu = menu->getSubMenu(2)->getSubMenu(7); + submenu->addItem(L"Bilinear", E_GUI_ID_BILINEAR, true, false, + conf->getBool("bilinear"), true); + submenu->addItem(L"Trilinear", E_GUI_ID_TRILINEAR, true, false, + conf->getBool("trilinear"), true); + submenu->addItem(L"Anisotropic", E_GUI_ID_ANISOTROPIC, true, false, + conf->getBool("anisotropic"), true); + + submenu = menu->getSubMenu(3); + submenu->addItem(L"About", E_DIALOG_ID_ABOUT); +} + +void GUI::initToolBar() +{ + IVideoDriver *driver = device->getVideoDriver(); + IGUIEnvironment *env = device->getGUIEnvironment(); + IGUIStaticText *text; + IGUISpinBox *spin; + + IGUIToolBar *toolbar = env->addToolBar(0, E_GUI_ID_TOOLBAR); + text = env->addStaticText(L"Animation:", + rect(20,5,120,25), false, false, toolbar); + + text = env->addStaticText(L"Start", + rect(130,5,160,25), false, false, toolbar); + spin = env->addSpinBox(L"", rect(170,5,230,25), + true, toolbar, E_GUI_ID_ANIM_START); + spin->setDecimalPlaces(0); + spin->setRange(0, 10000); + + text = env->addStaticText(L"End", + rect(255,5,280,25), false, false, toolbar); + spin = env->addSpinBox(L"", rect(290,5,350,25), + true, toolbar, E_GUI_ID_ANIM_END); + spin->setDecimalPlaces(0); + spin->setRange(0, 10000); + + text = env->addStaticText(L"Speed", + rect(370,5,410,25), false, false, toolbar); + spin = env->addSpinBox(L"", rect(420,5,480,25), + true, toolbar, E_GUI_ID_ANIM_SPEED); + spin->setDecimalPlaces(0); + spin->setRange(0, 10000); + + text = env->addStaticText(L"Frame", + rect(495,5,535,25), false, false, toolbar); + spin = env->addSpinBox(L"", rect(550,5,610,25), + true, toolbar, E_GUI_ID_ANIM_FRAME); + spin->setDecimalPlaces(0); + spin->setRange(0, 10000); + + s32 w = driver->getScreenSize().Width; + AnimCtrl *anim = new AnimCtrl(env, toolbar, E_GUI_ID_ANIM_CTRL, + rect(w-120,0,w,30)); + anim->drop(); +} + +void GUI::showToolBox(s32 id) +{ + ISceneManager *smgr = device->getSceneManager(); + IGUIEnvironment *env = device->getGUIEnvironment(); + IGUIElement *root = env->getRootGUIElement(); + if (root->getElementFromId(id, true)) + return; + + switch (id) + { + case E_GUI_ID_TOOLBOX_MODEL: + { + IVideoDriver *driver = device->getVideoDriver(); + s32 w = driver->getScreenSize().Width; + ISceneNode *node = smgr->getSceneNodeFromId(E_SCENE_ID_MODEL); + IGUIWindow *window = env->addWindow(rect(w-160,54,w,490), + false, L"Model Properties", root, id); + ToolBox *toolbox = new ToolBox(env, window, id, + rect(0,30,160,800), node); + toolbox->drop(); + env->setFocus(window); + break; + } + case E_GUI_ID_TOOLBOX_WIELD: + { + ISceneNode *node = smgr->getSceneNodeFromId(E_SCENE_ID_WIELD); + IGUIWindow *window = env->addWindow(rect(0,54,160,490), + false, L"Wield Properties", root, id); + ToolBox *toolbox = new ToolBox(env, window, id, + rect(0,30,160,800), node); + toolbox->drop(); + env->setFocus(window); + break; + } + default: + break; + } +} + +void GUI::closeToolBox(s32 id) +{ + IGUIElement *elem = getElement(id); + if (elem) + elem->remove(); +} + +void GUI::reloadToolBox(s32 id) +{ + IGUIElement *elem = getElement(id); + if (elem) + { + elem->remove(); + showToolBox(id); + } +} + +void GUI::moveElement(s32 id, const vector2di &move) +{ + IGUIElement *elem = getElement(id); + if (elem) + { + vector2di pos = elem->getRelativePosition().UpperLeftCorner + move; + elem->setRelativePosition(position2di(pos.X, pos.Y)); + } +} + +void GUI::showTexturesDialog() +{ + IGUIEnvironment *env = device->getGUIEnvironment(); + ISceneManager *smgr = device->getSceneManager(); + IGUIWindow *window = env->addWindow(getWindowRect(400, 310), + true, L"Textures"); + + TexturesDialog *dialog = new TexturesDialog(env, window, + E_DIALOG_ID_TEXTURES, rect(0,20,400,310), conf, smgr); + dialog->drop(); +} + +void GUI::showSettingsDialog() +{ + IGUIEnvironment *env = device->getGUIEnvironment(); + IGUIWindow *window = env->addWindow(getWindowRect(400, 310), + true, L"Settings"); + + SettingsDialog *dialog = new SettingsDialog(env, window, + E_DIALOG_ID_SETTINGS, rect(0,20,400,310), conf); + dialog->drop(); +} + +void GUI::showAboutDialog() +{ + IGUIEnvironment *env = device->getGUIEnvironment(); + IGUIWindow *window = env->addWindow(getWindowRect(300, 300), + true, L"About"); + + AboutDialog *dialog = new AboutDialog(env, window, + E_DIALOG_ID_ABOUT, rect(0,20,300,300)); + dialog->drop(); +} + +IGUIElement *GUI::getElement(s32 id) +{ + IGUIEnvironment *env = device->getGUIEnvironment(); + IGUIElement *root = env->getRootGUIElement(); + return root->getElementFromId(id, true); +} + +const rect GUI::getWindowRect(const u32 &width, const u32 &height) const +{ + IVideoDriver *driver = device->getVideoDriver(); + dimension2du s = driver->getScreenSize(); + vector2di pos = vector2di((s.Width - width) / 2, (s.Height - height) / 2); + return rect(pos, pos + vector2di(width, height)); +} \ No newline at end of file diff --git a/src/gui.h b/src/gui.h new file mode 100644 index 0000000..89104ea --- /dev/null +++ b/src/gui.h @@ -0,0 +1,133 @@ +#ifndef D_GUI_H +#define D_GUI_H + +using namespace irr; +using namespace core; +using namespace scene; +using namespace gui; +using namespace video; + +enum +{ + E_GUI_ID_MENU, + E_GUI_ID_TOOLBAR, + E_GUI_ID_LOAD_MODEL_MESH, + E_GUI_ID_LOAD_WIELD_MESH, + E_GUI_ID_SAVE_CONFIG, + E_GUI_ID_QUIT, + E_GUI_ID_TEXTURES_DIALOG, + E_GUI_ID_SETTINGS_DIALOG, + E_GUI_ID_TOOLBOX_MODEL, + E_GUI_ID_TOOLBOX_WIELD, + E_GUI_ID_SHOW_GRID, + E_GUI_ID_SHOW_AXES, + E_GUI_ID_ENABLE_WIELD, + E_GUI_ID_BACK_FACE_CULL, + E_GUI_ID_ORTHOGONAL, + E_GUI_ID_PERSPECTIVE, + E_GUI_ID_BILINEAR, + E_GUI_ID_TRILINEAR, + E_GUI_ID_ANISOTROPIC, + E_GUI_ID_DEBUG_INFO, + E_GUI_ID_VERTEX, + E_GUI_ID_VECTOR_X, + E_GUI_ID_VECTOR_Y, + E_GUI_ID_VECTOR_Z, + E_GUI_ID_POSITION, + E_GUI_ID_ROTATION, + E_GUI_ID_SCALE, + E_GUI_ID_MATERIAL, + E_GUI_ID_ANIM_CTRL, + E_GUI_ID_ANIM_START, + E_GUI_ID_ANIM_END, + E_GUI_ID_ANIM_FRAME, + E_GUI_ID_ANIM_SPEED, + E_GUI_ID_SKIP_REV, + E_GUI_ID_PLAY_REV, + E_GUI_ID_PAUSE, + E_GUI_ID_PLAY_FWD, + E_GUI_ID_SKIP_FWD +}; + +class Config; + +class VertexCtrl : public IGUIElement +{ +public: + VertexCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, f32 step, const wchar_t *label); + virtual ~VertexCtrl() {} + virtual bool OnEvent(const SEvent &event); + f32 getValue() const { return vertex; } + void setValue(const f32 &value); + +private: + f32 vertex; +}; + +class VectorCtrl : public IGUIElement +{ +public: + VectorCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, f32 step, const wchar_t *label); + virtual ~VectorCtrl() {} + virtual bool OnEvent(const SEvent &event); + vector3df getVector() const { return vector; } + void setVector(const vector3df &vec); + +private: + vector3df vector; +}; + +class AnimCtrl : public IGUIElement +{ +public: + AnimCtrl(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle); + virtual ~AnimCtrl() {} + virtual bool OnEvent(const SEvent &event); + void reset(bool enabled); + +private: + s32 button_id; +}; + +class ToolBox : public IGUIElement +{ +public: + ToolBox(IGUIEnvironment *env, IGUIElement *parent, s32 id, + const rect &rectangle, ISceneNode *node); + virtual ~ToolBox() {} + virtual bool OnEvent(const SEvent &event); + +private: + s32 node_id; + ISceneManager *smgr; +}; + +class GUI +{ +public: + GUI(IrrlichtDevice *device, Config *config); + void initMenu(); + void initToolBar(); + void showToolBox(s32 id); + void closeToolBox(s32 id); + void reloadToolBox(s32 id); + IGUIElement *getElement(s32 id); + bool getFocused() const { return has_focus; } + void setFocused(const bool &focus) { has_focus = focus; } + void moveElement(s32 id, const vector2di &move); + void showTexturesDialog(); + void showSettingsDialog(); + void showAboutDialog(); + +private: + const rect getWindowRect(const u32 &width, const u32 &height) const; + + IrrlichtDevice *device; + Config *conf; + bool has_focus; +}; + +#endif // D_GUI_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..871c663 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "config.h" +#include "viewer.h" + +int main() +{ + Config *conf = new Config("../bin/config.ini"); + std::map defaults = { + {"model_mesh", "character.b3d"}, + {"model_position", "0,-10,0"}, + {"model_rotation", "0,0,0"}, + {"model_scale", "100"}, + {"model_material", "14"}, + {"model_texture_1", "character.png"}, + {"model_texture_2", "blank.png"}, + {"model_texture_3", "blank.png"}, + {"model_texture_4", "blank.png"}, + {"model_texture_5", "blank.png"}, + {"model_texture_6", "blank.png"}, + {"wield_mesh", "pickaxe.obj"}, + {"wield_position", "0,5,0"}, + {"wield_rotation", "0,0,0"}, + {"wield_scale", "400"}, + {"wield_material", "14"}, + {"wield_show", "true"}, + {"wield_bone", "Arm_Right"}, + {"wield_texture_1", "pickaxe.png"}, + {"wield_texture_2", "blank.png"}, + {"wield_texture_3", "blank.png"}, + {"wield_texture_4", "blank.png"}, + {"wield_texture_5", "blank.png"}, + {"wield_texture_6", "blank.png"}, + {"anim_start", "168"}, + {"anim_end", "187"}, + {"anim_speed", "15"}, + {"ortho", "false"}, + {"bilinear", "false"}, + {"trilinear", "false"}, + {"anisotropic", "false"}, + {"backface_cull", "true"}, + {"bg_color", "808080"}, + {"grid_color", "404040"}, + {"screen_width", "800"}, + {"screen_height","600"}, + {"debug_info", "false"}, + {"debug_flags", "1"}, + }; + conf->load(); + for (std::map::iterator it = defaults.begin(); + it != defaults.end(); it++) + { + if (!conf->hasKey(it->first)) + conf->set(it->first, it->second); + } + conf->save(); + + u32 width = conf->getInt("screen_width"); + u32 height = conf->getInt("screen_height"); + IrrlichtDevice *device = createDevice(EDT_OPENGL, + dimension2d(width, height), 16, false, false, false); + + if (device && conf) + { + Viewer *viewer = new Viewer(conf); + viewer->run(device); + device->drop(); + delete viewer; + delete conf; + } + return 1; +} diff --git a/src/scene.cpp b/src/scene.cpp new file mode 100644 index 0000000..3ff2303 --- /dev/null +++ b/src/scene.cpp @@ -0,0 +1,320 @@ +#include +#include +#include + +#include "config.h" +#include "scene.h" + +Scene::Scene(ISceneNode *parent, ISceneManager *smgr, s32 id) : + ISceneNode(parent, smgr, id), + conf(0), + show_grid(true), + show_axes(true) +{ + material.Lighting = false; + material.MaterialType = EMT_TRANSPARENT_ALPHA_CHANNEL; + material.BackfaceCulling = false; + grid_color = SColor(64,128,128,128); +} + +bool Scene::load(Config *config) +{ + conf = config; + if (!loadModelMesh(conf->getCStr("model_mesh"))) + return false; + + setAttachment(); + loadWieldMesh(conf->getCStr("wield_mesh")); + setBackFaceCulling(conf->getBool("backface_cull")); + setGridColor(conf->getHex("grid_color")); + return true; +} + +bool Scene::loadModelMesh(const io::path &filename) +{ + if (!conf) + return false; + + IAnimatedMesh *mesh = SceneManager->getMesh(filename); + if (!mesh) + return false; + + ISceneNode *wield = getNode(E_SCENE_ID_WIELD); + if (wield) + wield->setParent(this); + + ISceneNode *model = getNode(E_SCENE_ID_MODEL); + if (model) + { + model->remove(); + model = 0; + } + model = SceneManager->addAnimatedMeshSceneNode(mesh, this, + E_SCENE_ID_MODEL); + if (!model) + return false; + + Vector pos = conf->getVector("model_position"); + Vector rot = conf->getVector("model_rotation"); + f32 s = (f32)conf->getInt("model_scale") / 100; + u32 mat = conf->getInt("model_material"); + + model->setMaterialFlag(EMF_LIGHTING, false); + model->setPosition(vector3df(pos.x, pos.y, pos.z)); + model->setRotation(vector3df(rot.x, rot.y, rot.z)); + model->setScale(vector3df(s,s,s)); + model->setMaterialType((E_MATERIAL_TYPE)mat); + model->setMaterialFlag(EMF_BILINEAR_FILTER, + conf->getBool("bilinear")); + model->setMaterialFlag(EMF_TRILINEAR_FILTER, + conf->getBool("trilinear")); + model->setMaterialFlag(EMF_ANISOTROPIC_FILTER, + conf->getBool("anisotropic")); + + setDebugInfo(conf->getBool("debug_info")); + if (wield) + setAttachment(); + + loadTextures(model, "model"); + return true; +} + +bool Scene::loadWieldMesh(const io::path &filename) +{ + if (!conf) + return false; + + IMesh *mesh = SceneManager->getMesh(filename); + if (!mesh) + return false; + + ISceneNode *wield = getNode(E_SCENE_ID_WIELD); + if (wield) + { + wield->remove(); + wield = 0; + } + wield = SceneManager->addMeshSceneNode(mesh, this, + E_SCENE_ID_WIELD); + + if (!wield) + return false; + + Vector pos = conf->getVector("wield_position"); + Vector rot = conf->getVector("wield_rotation"); + f32 s = (f32)conf->getInt("wield_scale") / 100; + u32 mat = conf->getInt("wield_material"); + + wield->setMaterialFlag(EMF_LIGHTING, false); + wield->setPosition(vector3df(pos.x, pos.y, pos.z)); + wield->setRotation(vector3df(rot.x, rot.y, rot.z)); + wield->setScale(vector3df(s,s,s)); + wield->setVisible(conf->getBool("wield_show")); + wield->setMaterialType((E_MATERIAL_TYPE)mat); + wield->setMaterialFlag(EMF_BILINEAR_FILTER, + conf->getBool("bilinear")); + wield->setMaterialFlag(EMF_TRILINEAR_FILTER, + conf->getBool("trilinear")); + wield->setMaterialFlag(EMF_ANISOTROPIC_FILTER, + conf->getBool("anisotropic")); + wield->setVisible(conf->getBool("wield_show")); + + setAttachment(); + loadTextures(wield, "wield"); + return true; +} + +void Scene::loadTextures(ISceneNode *node, const std::string &prefix) +{ + IVideoDriver *driver = SceneManager->getVideoDriver(); + for (u32 i = 0; i < node->getMaterialCount(); ++i) + { + std::string key = prefix + "_texture_" + std::to_string(i + 1); + io::path fn = conf->getCStr(key); + ITexture *texture = driver->getTexture(fn); + if (texture) + driver->removeTexture(texture); + texture = driver->getTexture(fn); + if (texture) + { + SMaterial &material = node->getMaterial(i); + material.TextureLayer[0].Texture = texture; + } + } +} + +ISceneNode *Scene::getNode(s32 id) +{ + return SceneManager->getSceneNodeFromId(id); +} + +void Scene::setAttachment() +{ + if (!conf) + return; + + IAnimatedMeshSceneNode *model = + (IAnimatedMeshSceneNode*)getNode(E_SCENE_ID_MODEL); + ISceneNode *wield = getNode(E_SCENE_ID_WIELD); + if (!model || !wield) + return; + + IBoneSceneNode *bone = 0; + if (model->getJointCount() > 0) + { + stringc wield_bone = conf->getCStr("wield_bone"); + bone = model->getJointNode(wield_bone.c_str()); + } + if (bone) + wield->setParent(bone); + else + wield->setParent(model); +} + +void Scene::setAnimation(const u32 &start, const u32 &end, const s32 &speed) +{ + if (!conf) + return; + + IAnimatedMeshSceneNode *model = + (IAnimatedMeshSceneNode*)getNode(E_SCENE_ID_MODEL); + + if (model) + { + model->setFrameLoop(start, end); + model->setAnimationSpeed(speed); + } +} + +void Scene::setFilter(E_MATERIAL_FLAG flag, const bool &is_enabled) +{ + ISceneNode *model = getNode(E_SCENE_ID_MODEL); + if (model) + model->setMaterialFlag(flag, is_enabled); + + ISceneNode *wield = getNode(E_SCENE_ID_WIELD); + if (wield) + wield->setMaterialFlag(flag, is_enabled); +} + +void Scene::setBackFaceCulling(const bool &is_enabled) +{ + ISceneNode *model = getNode(E_SCENE_ID_MODEL); + if (model) + model->setMaterialFlag(EMF_BACK_FACE_CULLING, is_enabled); + + ISceneNode *wield = getNode(E_SCENE_ID_WIELD); + if (wield) + wield->setMaterialFlag(EMF_BACK_FACE_CULLING, is_enabled); +} + +void Scene::setDebugInfo(const bool &is_visible) +{ + ISceneNode *model = getNode(E_SCENE_ID_MODEL); + if (model) + { + u32 state = (is_visible) ? conf->getInt("debug_flags") : EDS_OFF; + model->setDebugDataVisible(state); + } +} + +void Scene::rotate(s32 axis, const s32 &step) +{ + vector3df rot = getRotation(); + switch (axis) + { + case E_SCENE_AXIS_X: + rot.X = int(rot.X / step) * step + step; + break; + case E_SCENE_AXIS_Y: + rot.Y = int(rot.Y / step) * step + step; + break; + case E_SCENE_AXIS_Z: + rot.Z = int(rot.Z / step) * step + step; + break; + } + setRotation(rot); +} + +void Scene::refresh() +{ + ISceneNode *model = getNode(E_SCENE_ID_MODEL); + if (model) + loadTextures(model, "model"); + ISceneNode *wield = getNode(E_SCENE_ID_WIELD); + if (wield) + loadTextures(wield, "wield"); +} + +void Scene::jump() +{ + // quick and dirty jump animation to test attachment inertia + // this should eventually be improved or removed + ISceneNode *model = getNode(E_SCENE_ID_MODEL); + if (!model) + return; + + Vector p = conf->getVector("model_position"); + vector3df start = vector3df(p.x,p.y,p.z); + vector3df end = start + vector3df(0,10,0); + ISceneNodeAnimator *anim = + SceneManager->createFlyStraightAnimator(start, end, 380, false, true); + model->removeAnimators(); + model->addAnimator(anim); + anim->drop(); +} + +void Scene::OnRegisterSceneNode() +{ + if (IsVisible) + SceneManager->registerNodeForRendering(this); + + ISceneNode::OnRegisterSceneNode(); +} + +void Scene::render() +{ + if (!show_grid) + return; + + IVideoDriver *driver = SceneManager->getVideoDriver(); + driver->setMaterial(material); + driver->setTransform(ETS_WORLD, AbsoluteTransformation); + + SColor grid = grid_color; + grid.setAlpha(64); + for (f32 n = -10; n <= 10; n += 2) + { + driver->draw3DLine(vector3df(n,0,-10), vector3df(n,-0,10), grid); + driver->draw3DLine(vector3df(-10,0,n), vector3df(10,-0,n), grid); + } + if (!show_axes) + return; + + SColor red(128,255,0,0); + SColor green(128,0,255,0); + SColor blue(128,0,0,255); + + driver->draw3DLine(vector3df(-10,0,-10), vector3df(-8,0,-10), red); + driver->draw3DLine(vector3df(-8,0,-10), vector3df(-8.5,0,-9.8), red); + driver->draw3DLine(vector3df(-8,0,-10), vector3df(-8.5,0,-10.2), red); + + driver->draw3DLine(vector3df(-10,0,-10), vector3df(-10,2,-10), green); + driver->draw3DLine(vector3df(-10,2,-10), vector3df(-9.8,1.5,-10), green); + driver->draw3DLine(vector3df(-10,2,-10), vector3df(-10.2,1.5,-10), green); + + driver->draw3DLine(vector3df(-10,0,-10), vector3df(-10,0,-8), blue); + driver->draw3DLine(vector3df(-10,0,-8), vector3df(-9.8,0,-8.5), blue); + driver->draw3DLine(vector3df(-10,0,-8), vector3df(-10.2,0,-8.5), blue); + + IGUIEnvironment *env = SceneManager->getGUIEnvironment(); + IGUIFont *font = env->getFont("fontlucida.png"); + if (font) + { + s32 btm = driver->getScreenSize().Height; + s32 top = btm - 20; + font->draw(L"X", rect(5,top,15,btm), red); + font->draw(L"Y", rect(15,top,25,btm), green); + font->draw(L"Z", rect(25,top,35,btm), blue); + } +} diff --git a/src/scene.h b/src/scene.h new file mode 100644 index 0000000..d7bc4d0 --- /dev/null +++ b/src/scene.h @@ -0,0 +1,64 @@ +#ifndef D_SCENE_H +#define D_SCENE_H + +enum +{ + E_SCENE_ID, + E_SCENE_ID_MODEL, + E_SCENE_ID_WIELD +}; + +enum +{ + E_SCENE_AXIS_X, + E_SCENE_AXIS_Y, + E_SCENE_AXIS_Z +}; + +using namespace irr; +using namespace core; +using namespace scene; +using namespace gui; +using namespace video; + +class Config; + +class Scene : public ISceneNode +{ +public: + Scene(ISceneNode *parent, ISceneManager *mgr, s32 id); + ~Scene() {} + bool load(Config *config); + bool loadModelMesh(const io::path &filename); + bool loadWieldMesh(const io::path &filename); + ISceneNode *getNode(s32 id); + void setAttachment(); + void setAnimation(const u32 &start, const u32 &end, const s32 &speed); + void setFilter(E_MATERIAL_FLAG flag, const bool &is_enabled); + void setBackFaceCulling(const bool &is_enabled); + void setGridColor(SColor color) { grid_color = color; } + void setGridVisible(const bool &is_visible) { show_grid = is_visible; } + void setAxesVisible(const bool &is_visible) { show_axes = is_visible; } + void setDebugInfo(const bool &is_visible); + void rotate(s32 axis, const s32 &step); + void refresh(); + void jump(); + + virtual void OnRegisterSceneNode(); + virtual const aabbox3d &getBoundingBox() const { return box; } + virtual SMaterial &getMaterial(u32 i) { return material; } + virtual u32 getMaterialCount() const { return 1; } + virtual void render(); + +private: + void loadTextures(ISceneNode *node, const std::string &prefix); + + Config *conf; + bool show_grid; + bool show_axes; + SColor grid_color; + aabbox3d box; + SMaterial material; +}; + +#endif // D_SCENE_H diff --git a/src/trackball.cpp b/src/trackball.cpp new file mode 100644 index 0000000..4c51926 --- /dev/null +++ b/src/trackball.cpp @@ -0,0 +1,84 @@ +#include +#include + +#include "trackball.h" + +Trackball::Trackball(const u32 &width, const u32 &height) : + is_clicked(false), + is_moving(false) +{ + transform.makeIdentity(); + setBounds(width, height); +} + +vector3df Trackball::getVector(const position2df &pos) const +{ + vector3df vect; + vect.X = 1.0 * pos.X / screen.Width * 2 - 1.0; + vect.Y = 1.0 * pos.Y / screen.Height * 2 - 1.0; + vect.Z = 0; + + f32 len = vect.X * vect.X + vect.Y * vect.Y; + if (len > 1.0f) + vect.normalize(); + else + vect.Z = -sqrt(1.0f - len); + + return vect; +} + +void Trackball::setBounds(const u32 &width, const u32 &height) +{ + screen = dimension2di(width, height); +} + +void Trackball::setDragPos(const s32 &x, const s32 &y) +{ + drag_pos = position2df(x, y); +} + +void Trackball::animateNode(ISceneNode *node, u32 timeMs) +{ + if (!node) + return; + + if (!is_moving) + { + if (is_clicked) + { + is_moving = true; + transform = node->getAbsoluteTransformation(); + drag_start = drag_pos; + } + } + else + { + if (is_clicked) + { + matrix4 rotation; + quaternion quat; + vector3df start = getVector(drag_start); + vector3df end = getVector(drag_pos); + vector3df perp = start.crossProduct(end); + if (iszero(perp.getLength())) + { + quat.makeIdentity(); + } + else + { + quat.X = perp.X; + quat.Y = perp.Y; + quat.Z = perp.Z; + quat.W = start.dotProduct(end); + } + quat.getMatrix(rotation); + rotation *= transform; + node->setRotation(rotation.getRotationDegrees()); + node->updateAbsolutePosition(); + } + else + { + is_moving = false; + } + } +} diff --git a/src/trackball.h b/src/trackball.h new file mode 100644 index 0000000..3a42fbb --- /dev/null +++ b/src/trackball.h @@ -0,0 +1,33 @@ +#ifndef D_TRACKBALL_H +#define D_TRACKBALL_H + +using namespace irr; +using namespace core; +using namespace scene; + +class Trackball : public ISceneNodeAnimator +{ +public: + Trackball(const u32 &width, const u32 &height); + ~Trackball(void) {} + void setBounds(const u32 &width, const u32 &height); + void setDragPos(const s32 &x, const s32 &y); + void click() { is_clicked = true; } + void release() { is_clicked = false; } + bool isClicked() const { return is_clicked; } + virtual void animateNode(ISceneNode *node, u32 timeMs); + virtual ISceneNodeAnimator *createClone(ISceneNode *node, + ISceneManager *newManager = 0) { return 0; } + +private: + vector3df getVector(const position2df &pos) const; + + bool is_clicked; + bool is_moving; + position2df drag_start; + position2df drag_pos; + dimension2di screen; + matrix4 transform; +}; + +#endif // D_TRACKBALL_H diff --git a/src/viewer.cpp b/src/viewer.cpp new file mode 100644 index 0000000..e179ae4 --- /dev/null +++ b/src/viewer.cpp @@ -0,0 +1,640 @@ +#include +#include +#include +#include + +#include "config.h" +#include "scene.h" +#include "trackball.h" +#include "gui.h" +#include "dialog.h" +#include "viewer.h" + +#define M_ZOOM_IN(fov) std::max(fov - DEGTORAD * 2, PI * 0.0125f) +#define M_ZOOM_OUT(fov) std::min(fov + DEGTORAD * 2, PI * 0.5f) + +Viewer::Viewer(Config *conf) : + conf(conf), + device(0), + camera(0), + scene(0), + trackball(0), + gui(0), + animation(0) +{} + +Viewer::~Viewer() +{ + if (scene) + scene->drop(); + if (trackball) + delete trackball; + if (gui) + delete gui; + if (animation) + delete animation; +} + +bool Viewer::run(IrrlichtDevice *irr_device) +{ + device = irr_device; + device->getFileSystem()->addFileArchive("../assets/"); + device->getFileSystem()->addFileArchive("../media/"); + device->getFileSystem()->changeWorkingDirectoryTo("../media/"); + device->setEventReceiver(this); + + IVideoDriver *driver = device->getVideoDriver(); + ISceneManager *smgr = device->getSceneManager(); + IGUIEnvironment *env = device->getGUIEnvironment(); + + screen = driver->getScreenSize(); + trackball = new Trackball(screen.Width, screen.Height); + scene = new Scene(smgr->getRootSceneNode(), smgr, E_SCENE_ID); + scene->addAnimator(trackball); + + gui = new GUI(device, conf); + gui->initMenu(); + gui->initToolBar(); + + if (!scene->load(conf)) + return false; + + animation = new AnimState(env); + animation->load(scene->getNode(E_SCENE_ID_MODEL)); + animation->setField(E_GUI_ID_ANIM_START, conf->getInt("anim_start")); + animation->setField(E_GUI_ID_ANIM_END, conf->getInt("anim_end")); + animation->setField(E_GUI_ID_ANIM_SPEED, conf->getInt("anim_speed")); + animation->setField(E_GUI_ID_ANIM_FRAME, conf->getInt("anim_start")); + scene->setAnimation(conf->getInt("anim_start"), + conf->getInt("anim_start"), conf->getInt("anim_speed")); + + camera = smgr->addCameraSceneNode(0, vector3df(0,0,30), vector3df(0,0,0)); + fov = camera->getFOV(); + fov_home = fov; + jump_time = 0; + + setCaptionFileName(conf->getCStr("model_mesh")); + setBackgroundColor(conf->getHex("bg_color")); + setProjection(); + + while (device->run()) + { + resize(); + driver->beginScene(true, true, bg_color); + smgr->drawAll(); + env->drawAll(); + driver->endScene(); + animation->update(scene->getNode(E_SCENE_ID_MODEL)); + } + return true; +} + +void Viewer::resize() +{ + IVideoDriver *driver = device->getVideoDriver(); + dimension2du dim = driver->getScreenSize(); + if (screen == dim) + return; + + const vector2di move = vector2di(dim.Width - screen.Width, 0); + gui->moveElement(E_GUI_ID_TOOLBOX_MODEL, move); + gui->moveElement(E_GUI_ID_ANIM_CTRL, move); + + screen = dim; + trackball->setBounds(screen.Width, screen.Height); + camera->setAspectRatio((f32)screen.Width / (f32)screen.Height); + setProjection(); +} + +void Viewer::setProjection() +{ + f32 width = (f32)screen.Width * fov / 20.0f; + f32 height = (f32)screen.Height * fov / 20.0f; + ortho.buildProjectionMatrixOrthoLH(width, height, 1.0f, 1000.f); + + if (conf->getBool("ortho")) + camera->setProjectionMatrix(ortho, true); + else + camera->setFOV(fov); +} + +void Viewer::setBackgroundColor(const u32 &color) +{ + bg_color.color = color; + bg_color.setAlpha(255); +} + +void Viewer::setCaptionFileName(const io::path &filename) +{ + io::IFileSystem *fs = device->getFileSystem(); + stringw caption = fs->getFileBasename(filename) + L" - SAM-Viewer"; + device->setWindowCaption(caption.c_str()); +} + +static inline std::string boolToString(bool b) +{ + return (b) ? "true" : "false"; +} + +static inline std::string vectorToString(vector3df v) +{ + std::ostringstream ss; + ss << v.X << "," << v.Y << "," << v.Z; + std::string str(ss.str()); + return str; +} + +bool Viewer::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_GUI_EVENT) + { + if (event.GUIEvent.EventType == EGET_MENU_ITEM_SELECTED) + { + IGUIContextMenu *menu = (IGUIContextMenu*)event.GUIEvent.Caller; + s32 item = menu->getSelectedItem(); + s32 id = menu->getItemCommandId(item); + IGUIEnvironment *env = device->getGUIEnvironment(); + switch (id) + { + case E_GUI_ID_LOAD_MODEL_MESH: + env->addFileOpenDialog(L"Open main model file", true, 0, + E_GUI_ID_LOAD_MODEL_MESH); + break; + case E_GUI_ID_LOAD_WIELD_MESH: + env->addFileOpenDialog(L"Open wield model file", true, 0, + E_GUI_ID_LOAD_WIELD_MESH); + break; + case E_GUI_ID_ENABLE_WIELD: + { + ISceneNode *wield = scene->getNode(E_SCENE_ID_WIELD); + if (wield) + { + wield->setVisible(menu->isItemChecked(item)); + conf->set("wield_show", + boolToString(menu->isItemChecked(item))); + } + break; + } + case E_GUI_ID_QUIT: + device->closeDevice(); + break; + case E_GUI_ID_TEXTURES_DIALOG: + gui->showTexturesDialog(); + break; + case E_GUI_ID_SETTINGS_DIALOG: + gui->showSettingsDialog(); + break; + case E_GUI_ID_TOOLBOX_MODEL: + { + if (menu->isItemChecked(item)) + gui->showToolBox(E_GUI_ID_TOOLBOX_MODEL); + else + gui->closeToolBox(E_GUI_ID_TOOLBOX_MODEL); + break; + } + case E_GUI_ID_TOOLBOX_WIELD: + { + if (menu->isItemChecked(item)) + gui->showToolBox(E_GUI_ID_TOOLBOX_WIELD); + else + gui->closeToolBox(E_GUI_ID_TOOLBOX_WIELD); + break; + } + case E_GUI_ID_SHOW_GRID: + scene->setGridVisible(menu->isItemChecked(item)); + menu->setItemEnabled(item + 1, menu->isItemChecked(item)); + break; + case E_GUI_ID_SHOW_AXES: + scene->setAxesVisible(menu->isItemChecked(item)); + break; + case E_GUI_ID_BILINEAR: + scene->setFilter(EMF_BILINEAR_FILTER, + menu->isItemChecked(item)); + conf->set("bilinear", + boolToString(menu->isItemChecked(item))); + break; + case E_GUI_ID_TRILINEAR: + scene->setFilter(EMF_TRILINEAR_FILTER, + menu->isItemChecked(item)); + conf->set("trilinear", + boolToString(menu->isItemChecked(item))); + break; + case E_GUI_ID_ANISOTROPIC: + scene->setFilter(EMF_ANISOTROPIC_FILTER, + menu->isItemChecked(item)); + conf->set("anisotropic", + boolToString(menu->isItemChecked(item))); + break; + case E_GUI_ID_PERSPECTIVE: + menu->setItemChecked(item + 1, !menu->isItemChecked(item)); + conf->set("ortho", boolToString(!menu->isItemChecked(item))); + setProjection(); + break; + case E_GUI_ID_ORTHOGONAL: + menu->setItemChecked(item - 1, !menu->isItemChecked(item)); + conf->set("ortho", boolToString(menu->isItemChecked(item))); + setProjection(); + break; + case E_GUI_ID_BACK_FACE_CULL: + scene->setBackFaceCulling(menu->isItemChecked(item)); + conf->set("backface_cull", + boolToString(menu->isItemChecked(item))); + break; + case E_GUI_ID_DEBUG_INFO: + scene->setDebugInfo(menu->isItemChecked(item)); + conf->set("debug_info", + boolToString(menu->isItemChecked(item))); + break; + case E_DIALOG_ID_ABOUT: + gui->showAboutDialog(); + break; + case E_GUI_ID_SAVE_CONFIG: + { + ISceneNode *model = scene->getNode(E_SCENE_ID_MODEL); + if (model) + { + conf->set("model_position", + vectorToString(model->getPosition())); + conf->set("model_rotation", + vectorToString(model->getRotation())); + conf->set("model_scale", + std::to_string(model->getScale().Y * 100)); + conf->set("model_material", + std::to_string(model->getMaterial(0).MaterialType)); + } + ISceneNode *wield = scene->getNode(E_SCENE_ID_WIELD); + if (wield) + { + conf->set("wield_position", + vectorToString(wield->getPosition())); + conf->set("wield_rotation", + vectorToString(wield->getRotation())); + conf->set("wield_scale", + std::to_string(wield->getScale().Y * 100)); + conf->set("wield_material", + std::to_string(wield->getMaterial(0).MaterialType)); + } + conf->set("anim_start", + std::to_string(animation->getField(E_GUI_ID_ANIM_START))); + conf->set("anim_end", + std::to_string(animation->getField(E_GUI_ID_ANIM_END))); + conf->set("anim_speed", + std::to_string(animation->getField(E_GUI_ID_ANIM_SPEED))); + conf->save(); + break; + } + default: + break; + } + } + else if (event.GUIEvent.EventType == EGET_FILE_SELECTED) + { + IGUIFileOpenDialog *dialog = + (IGUIFileOpenDialog*)event.GUIEvent.Caller; + + stringc fn = stringc(dialog->getFileName()).c_str(); + if (!fn.empty()) + { + s32 id = dialog->getID(); + switch (id) + { + case E_GUI_ID_LOAD_MODEL_MESH: + { + if (scene->loadModelMesh(fn)) + { + ISceneNode *model = scene->getNode(E_SCENE_ID_MODEL); + if (model) + { + animation->load(model); + setCaptionFileName(fn); + gui->reloadToolBox(E_GUI_ID_TOOLBOX_MODEL); + conf->set("model_mesh", fn.c_str()); + } + } + break; + } + case E_GUI_ID_LOAD_WIELD_MESH: + { + if (scene->loadWieldMesh(fn)) + { + gui->reloadToolBox(E_GUI_ID_TOOLBOX_WIELD); + conf->set("wield_mesh", fn.c_str()); + } + break; + } + default: + break; + } + } + gui->setFocused(false); + } + else if (event.GUIEvent.EventType == EGET_FILE_CHOOSE_DIALOG_CANCELLED) + { + gui->setFocused(false); + } + else if (event.GUIEvent.EventType == EGET_ELEMENT_CLOSED) + { + IGUIContextMenu *menu = + (IGUIContextMenu*)gui->getElement(E_GUI_ID_MENU); + if (menu) + { + s32 id = event.GUIEvent.Caller->getID(); + if (id == E_GUI_ID_TOOLBOX_MODEL) + menu->getSubMenu(2)->setItemChecked(0, false); + else if (id == E_GUI_ID_TOOLBOX_WIELD) + menu->getSubMenu(2)->setItemChecked(1, false); + } + gui->setFocused(false); + } + else if (event.GUIEvent.EventType == EGET_BUTTON_CLICKED) + { + s32 id = event.GUIEvent.Caller->getID(); + switch (id) + { + case E_GUI_ID_SKIP_REV: + scene->setAnimation( + animation->getField(E_GUI_ID_ANIM_START), + animation->getField(E_GUI_ID_ANIM_START), + animation->getField(E_GUI_ID_ANIM_SPEED)); + animation->setState(E_ANIM_STATE_PAUSED); + gui->setFocused(false); + break; + case E_GUI_ID_PLAY_REV: + scene->setAnimation( + animation->getField(E_GUI_ID_ANIM_START), + animation->getField(E_GUI_ID_ANIM_END), + -animation->getField(E_GUI_ID_ANIM_SPEED)); + animation->setState(E_ANIM_STATE_PLAY_REV); + gui->setFocused(false); + break; + case E_GUI_ID_PAUSE: + scene->setAnimation( + animation->getFrame(), + animation->getFrame(), + animation->getField(E_GUI_ID_ANIM_SPEED)); + animation->setState(E_ANIM_STATE_PAUSED); + gui->setFocused(false); + break; + case E_GUI_ID_PLAY_FWD: + scene->setAnimation( + animation->getField(E_GUI_ID_ANIM_START), + animation->getField(E_GUI_ID_ANIM_END), + animation->getField(E_GUI_ID_ANIM_SPEED)); + animation->setState(E_ANIM_STATE_PLAY_FWD); + gui->setFocused(false); + break; + case E_GUI_ID_SKIP_FWD: + scene->setAnimation( + animation->getField(E_GUI_ID_ANIM_END), + animation->getField(E_GUI_ID_ANIM_END), + animation->getField(E_GUI_ID_ANIM_SPEED)); + animation->setState(E_ANIM_STATE_PAUSED); + gui->setFocused(false); + break; + case E_DIALOG_ID_SETTINGS_OK: + event.GUIEvent.Caller->getParent()->getParent()->remove(); + gui->setFocused(false); + setBackgroundColor(conf->getHex("bg_color")); + scene->setGridColor(conf->getHex("grid_color")); + scene->setAttachment(); + scene->setDebugInfo(conf->getBool("debug_info")); + break; + case E_DIALOG_ID_SETTINGS_CANCEL: + event.GUIEvent.Caller->getParent()->getParent()->remove(); + gui->setFocused(false); + break; + case E_DIALOG_ID_ABOUT_OK: + case E_DIALOG_ID_TEXTURES_OK: + case E_DIALOG_ID_TEXTURES_CANCEL: + event.GUIEvent.Caller->getParent()->getParent()->remove(); + gui->setFocused(false); + break; + default: + break; + } + } + else if (event.GUIEvent.EventType == EGET_SPINBOX_CHANGED) + { + s32 id = event.GUIEvent.Caller->getID(); + switch (id) + { + case E_GUI_ID_ANIM_START: + case E_GUI_ID_ANIM_END: + { + if (animation->getState() != E_ANIM_STATE_PAUSED) + { + IAnimatedMeshSceneNode *model = (IAnimatedMeshSceneNode*) + scene->getNode(E_SCENE_ID_MODEL); + scene->setAnimation( + animation->getField(E_GUI_ID_ANIM_START), + animation->getField(E_GUI_ID_ANIM_END), + model->getAnimationSpeed()); + } + break; + } + case E_GUI_ID_ANIM_SPEED: + { + IAnimatedMeshSceneNode *model = (IAnimatedMeshSceneNode*) + scene->getNode(E_SCENE_ID_MODEL); + s32 speed = animation->getField(E_GUI_ID_ANIM_SPEED); + if (animation->getState() == E_ANIM_STATE_PLAY_REV) + speed = -speed; + + model->setAnimationSpeed(speed); + break; + } + case E_GUI_ID_ANIM_FRAME: + { + if (animation->getState() == E_ANIM_STATE_PAUSED) + { + u32 frame = animation->getField(E_GUI_ID_ANIM_FRAME); + scene->setAnimation(frame, frame, + animation->getField(E_GUI_ID_ANIM_SPEED)); + } + break; + } + default: + break; + } + } + else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) + { + gui->setFocused(false); + } + else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUSED) + { + gui->setFocused(true); + trackball->release(); + } + } + else if (event.EventType == EET_KEY_INPUT_EVENT && + event.KeyInput.PressedDown) + { + switch (event.KeyInput.Key) + { + case KEY_HOME: + { + scene->setRotation(vector3df(0,0,0)); + fov = fov_home; + setProjection(); + break; + } + case KEY_SPACE: + { + u32 now_time = device->getTimer()->getTime() + 1000; + if (jump_time + 800 < now_time) + { + jump_time = now_time; + scene->jump(); + } + break; + } + case KEY_LEFT: + scene->rotate(E_SCENE_AXIS_Y, 15); + break; + case KEY_RIGHT: + scene->rotate(E_SCENE_AXIS_Y, -15); + break; + case KEY_UP: + scene->rotate(E_SCENE_AXIS_X, -15); + break; + case KEY_DOWN: + scene->rotate(E_SCENE_AXIS_X, 15); + break; + case KEY_KEY_Z: + scene->rotate(E_SCENE_AXIS_Z, -15); + break; + case KEY_KEY_X: + scene->rotate(E_SCENE_AXIS_Z, 15); + break; + case KEY_PLUS: + fov = M_ZOOM_IN(fov); + setProjection(); + break; + case KEY_MINUS: + fov = M_ZOOM_OUT(fov); + setProjection(); + break; + case KEY_F5: + scene->refresh(); + break; + default: + break; + } + } + else if (event.EventType == EET_MOUSE_INPUT_EVENT && !gui->getFocused()) + { + switch (event.MouseInput.Event) + { + case EMIE_LMOUSE_LEFT_UP: + trackball->release(); + break; + case EMIE_MOUSE_MOVED: + { + if (event.MouseInput.isLeftPressed() && !trackball->isClicked()) + trackball->click(); + + trackball->setDragPos(event.MouseInput.X, event.MouseInput.Y); + break; + + } + case EMIE_MOUSE_WHEEL: + { + if (event.MouseInput.Wheel < 0) + fov = M_ZOOM_OUT(fov); + else + fov = M_ZOOM_IN(fov); + setProjection(); + break; + } + default: + break; + } + } + return false; +} + +AnimState::AnimState(IGUIEnvironment *env) : + env(env), + frame(0), + max(0), + state(E_ANIM_STATE_PAUSED) +{} + +void AnimState::load(ISceneNode *node) +{ + IAnimatedMeshSceneNode *model = (IAnimatedMeshSceneNode*)node; + if (!model) + return; + + IGUIToolBar *toolbar = getToolBar(); + if (!toolbar) + return; + + max = model->getEndFrame(); + model->setFrameLoop(0, 0); + state = E_ANIM_STATE_PAUSED; + + setField(E_GUI_ID_ANIM_START, 0); + setField(E_GUI_ID_ANIM_END, max); + setField(E_GUI_ID_ANIM_FRAME, 0); + + bool enabled = (max > 0); + const list &children = toolbar->getChildren(); + list::ConstIterator iter = children.begin(); + while (iter != children.end()) + { + if ((*iter)->getID() == E_GUI_ID_ANIM_CTRL) + { + AnimCtrl *anim = (AnimCtrl*)(*iter); + anim->reset(enabled); + if (enabled) + { + IGUIButton *button = + (IGUIButton*)anim->getElementFromId(E_GUI_ID_PAUSE, true); + button->setPressed(true); + } + } + else if ((*iter)->getType() != EGUIET_STATIC_TEXT) + { + (*iter)->setEnabled(enabled); + } + ++iter; + } +} + +void AnimState::update(ISceneNode *node) +{ + IAnimatedMeshSceneNode *model = (IAnimatedMeshSceneNode*)node; + frame = model->getFrameNr(); + setField(E_GUI_ID_ANIM_FRAME, frame); +} + +IGUIToolBar *AnimState::getToolBar() +{ + IGUIElement *root = env->getRootGUIElement(); + return (IGUIToolBar*)root->getElementFromId(E_GUI_ID_TOOLBAR, true); +} + +u32 AnimState::getField(s32 id) +{ + IGUIToolBar *toolbar = getToolBar(); + if (!toolbar) + return 0; + + IGUISpinBox *spin; + spin = (IGUISpinBox*)toolbar->getElementFromId(id, true); + return spin->getValue(); +} + +void AnimState::setField(s32 id, const u32 &value) +{ + IGUIToolBar *toolbar = getToolBar(); + if (!toolbar) + return; + + IGUISpinBox *spin; + spin = (IGUISpinBox*)toolbar->getElementFromId(id, true); + spin->setRange(0, max); + spin->setValue(std::min(value, max)); +} diff --git a/src/viewer.h b/src/viewer.h new file mode 100644 index 0000000..b41da74 --- /dev/null +++ b/src/viewer.h @@ -0,0 +1,78 @@ +#ifndef D_VIEWER_H +#define D_VIEWER_H + +using namespace irr; +using namespace core; +using namespace scene; +using namespace gui; +using namespace video; + +class Config; +class Scene; +class Trackball; +class GUI; + +enum +{ + E_ANIM_STATE_PLAY_FWD, + E_ANIM_STATE_PLAY_REV, + E_ANIM_STATE_PAUSED +}; + +enum +{ + E_PROJECTION_PERSPECTIVE, + E_PROJECTION_ORTHOGRAPHIC +}; + +class AnimState +{ +public: + AnimState(IGUIEnvironment *env); + void load(ISceneNode *node); + void update(ISceneNode *node); + void setField(s32 id, const u32 &value); + void setState(s32 id) { state = id; } + u32 getField(s32 id); + u32 getFrame() { return frame; } + s32 getState() { return state; } + +private: + IGUIToolBar *getToolBar(); + + IGUIEnvironment *env; + u32 frame; + u32 max; + s32 state; +}; + +class Viewer : public IEventReceiver +{ +public: + Viewer(Config *conf); + ~Viewer(); + bool run(IrrlichtDevice *irr_device); + virtual bool OnEvent(const SEvent &event); + +private: + void resize(); + void setProjection(); + void setBackgroundColor(const u32 &color); + void setCaptionFileName(const io::path &filename); + + Config *conf; + IrrlichtDevice *device; + ICameraSceneNode *camera; + Scene *scene; + Trackball *trackball; + GUI *gui; + AnimState *animation; + matrix4 ortho; + f32 fov; + f32 fov_home; + dimension2du screen; + SColor bg_color; + u32 jump_time; +}; + +#endif // D_VIEWER_H