From 3356da01513860d899cde503408436f7e1918f63 Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Wed, 4 Nov 2020 21:46:18 +0100 Subject: [PATCH] Add model[] formspec element (#10320) Formspec element to display models, written by @kilbith, rebased and tweaked. Co-authored-by: Jean-Patrick Guerrero Co-authored-by: sfan5 --- doc/lua_api.txt | 16 ++ games/devtest/mods/testformspec/LICENSE.txt | 14 + games/devtest/mods/testformspec/formspec.lua | 4 + .../models/testformspec_character.b3d | Bin 0 -> 73433 bytes .../models/testformspec_chest.obj | 79 ++++++ .../textures/default_chest_front.png | Bin 0 -> 423 bytes .../textures/default_chest_inside.png | Bin 0 -> 102 bytes .../textures/default_chest_side.png | Bin 0 -> 375 bytes .../textures/default_chest_top.png | Bin 0 -> 423 bytes .../textures/testformspec_character.png | Bin 0 -> 2754 bytes src/gui/CMakeLists.txt | 1 + src/gui/guiFormSpecMenu.cpp | 86 ++++++ src/gui/guiFormSpecMenu.h | 2 +- src/gui/guiScene.cpp | 257 ++++++++++++++++++ src/gui/guiScene.h | 85 ++++++ util/ci/clang-format-whitelist.txt | 2 + 16 files changed, 545 insertions(+), 1 deletion(-) create mode 100644 games/devtest/mods/testformspec/LICENSE.txt create mode 100644 games/devtest/mods/testformspec/models/testformspec_character.b3d create mode 100644 games/devtest/mods/testformspec/models/testformspec_chest.obj create mode 100644 games/devtest/mods/testformspec/textures/default_chest_front.png create mode 100644 games/devtest/mods/testformspec/textures/default_chest_inside.png create mode 100644 games/devtest/mods/testformspec/textures/default_chest_side.png create mode 100644 games/devtest/mods/testformspec/textures/default_chest_top.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_character.png create mode 100644 src/gui/guiScene.cpp create mode 100644 src/gui/guiScene.h diff --git a/doc/lua_api.txt b/doc/lua_api.txt index daf0da3d2..38fc3066a 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -2272,6 +2272,18 @@ Elements * `frame duration`: Milliseconds between each frame. `0` means the frames don't advance. * `frame start` (Optional): The index of the frame to start on. Default `1`. +### `model[,;,;;;;;;]` + +* Show a mesh model. +* `name`: Element name that can be used for styling +* `mesh`: The mesh model to use. +* `textures`: The mesh textures to use according to the mesh materials. + Texture names must be separated by commas. +* `rotation {X,Y}` (Optional): Initial rotation of the camera. + The axes are euler angles in degrees. +* `continuous` (Optional): Whether the rotation is continuous. Default `false`. +* `mouse control` (Optional): Whether the model can be controlled with the mouse. Default `true`. + ### `item_image[,;,;]` * Show an inventory image of registered item/node @@ -2842,6 +2854,10 @@ Some types may inherit styles from parent types. * font_size - Sets font size. See button `font_size` property for more information. * noclip - boolean, set to true to allow the element to exceed formspec bounds. * textcolor - color. Default white. +* model + * bgcolor - color, sets background color. + * noclip - boolean, set to true to allow the element to exceed formspec bounds. + * Default to false in formspec_version version 3 or higher * image * noclip - boolean, set to true to allow the element to exceed formspec bounds. * Default to false in formspec_version version 3 or higher diff --git a/games/devtest/mods/testformspec/LICENSE.txt b/games/devtest/mods/testformspec/LICENSE.txt new file mode 100644 index 000000000..07696cc30 --- /dev/null +++ b/games/devtest/mods/testformspec/LICENSE.txt @@ -0,0 +1,14 @@ +License of media files +---------------------- +Content imported from minetest_game. + + +BlockMen (CC BY-SA 3.0) + default_chest_front.png + default_chest_lock.png + default_chest_side.png + default_chest_top.png + +stujones11 (CC BY-SA 3.0) +An0n3m0us (CC BY-SA 3.0) + testformspec_character.b3d diff --git a/games/devtest/mods/testformspec/formspec.lua b/games/devtest/mods/testformspec/formspec.lua index 87a05fc96..5495896ce 100644 --- a/games/devtest/mods/testformspec/formspec.lua +++ b/games/devtest/mods/testformspec/formspec.lua @@ -327,6 +327,10 @@ Number] animated_image[3,4.25;1,1;;testformspec_animation.png;4;0;3] animated_image[5.5,0.5;5,2;;testformspec_animation.png;4;100] animated_image[5.5,2.75;5,2;;testformspec_animation.jpg;4;100] + + style[m1;bgcolor=black] + model[0.5,6;4,4;m1;testformspec_character.b3d;testformspec_character.png] + model[5,6;4,4;m2;testformspec_chest.obj;default_chest_top.png,default_chest_top.png,default_chest_side.png,default_chest_side.png,default_chest_front.png,default_chest_inside.png;30,1;true;true] ]], -- Scroll containers diff --git a/games/devtest/mods/testformspec/models/testformspec_character.b3d b/games/devtest/mods/testformspec/models/testformspec_character.b3d new file mode 100644 index 0000000000000000000000000000000000000000..8edbaf6377891568c248cd73d2d691f97c775311 GIT binary patch literal 73433 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%0D>Q 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 ZXpY6Y*b3|77Oab}F$*L6x^P}^{{uEY)gk}@ literal 0 HcmV?d00001 diff --git a/games/devtest/mods/testformspec/models/testformspec_chest.obj b/games/devtest/mods/testformspec/models/testformspec_chest.obj new file mode 100644 index 000000000..72ba175a0 --- /dev/null +++ b/games/devtest/mods/testformspec/models/testformspec_chest.obj @@ -0,0 +1,79 @@ +# Blender v2.78 (sub 0) OBJ File: 'chest-open.blend' +# www.blender.org +o Top_Cube.002_None_Top_Cube.002_None_bottom +v -0.500000 0.408471 0.720970 +v -0.500000 1.115578 0.013863 +v -0.500000 0.894607 -0.207108 +v -0.500000 0.187501 0.499999 +v 0.500000 1.115578 0.013863 +v 0.500000 0.408471 0.720970 +v 0.500000 0.187501 0.499999 +v 0.500000 0.894607 -0.207108 +v -0.500000 0.187500 -0.500000 +v -0.500000 -0.500000 -0.500000 +v -0.500000 -0.500000 0.500000 +v 0.500000 0.187500 -0.500000 +v 0.500000 -0.500000 0.500000 +v 0.500000 -0.500000 -0.500000 +vt 0.0000 1.0000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vt 0.0000 0.0000 +vt 0.0000 1.0000 +vt 1.0000 1.0000 +vt 1.0000 0.6875 +vt 0.0000 0.6875 +vt 1.0000 1.0000 +vt 0.0000 0.6875 +vt 1.0000 0.6875 +vt 1.0000 0.6875 +vt 1.0000 0.0000 +vt 0.0000 0.0000 +vt 1.0000 0.6875 +vt 1.0000 0.0000 +vt 1.0000 1.0000 +vt 1.0000 0.6875 +vt 1.0000 0.0000 +vt 0.0000 1.0000 +vt 0.0000 0.6875 +vt 0.0000 0.6875 +vt 0.0000 0.0000 +vt 1.0000 0.5000 +vt 1.0000 1.0000 +vt 0.0000 1.0000 +vt 0.0000 0.5000 +vt 0.0000 0.0000 +vt 1.0000 0.0000 +vn 0.0000 0.7071 0.7071 +vn -0.0000 -1.0000 -0.0000 +vn -1.0000 0.0000 0.0000 +vn 1.0000 0.0000 -0.0000 +vn 0.0000 -0.7071 0.7071 +vn 0.0000 0.0000 1.0000 +vn -0.0000 0.7071 -0.7071 +vn -0.0000 0.0000 -1.0000 +vn -0.0000 -0.7071 -0.7071 +vn -0.0000 1.0000 -0.0000 +g Top_Cube.002_None_Top_Cube.002_None_bottom_Top_Cube.002_None_Top_Cube.002_None_bottom_Top +s off +f 6/1/1 5/2/1 2/3/1 1/4/1 +g Top_Cube.002_None_Top_Cube.002_None_bottom_Top_Cube.002_None_Top_Cube.002_None_bottom_Bottom +f 11/5/2 10/6/2 14/7/2 13/8/2 +g Top_Cube.002_None_Top_Cube.002_None_bottom_Top_Cube.002_None_Top_Cube.002_None_bottom_Right-Left +f 1/9/3 2/10/3 3/11/3 4/12/3 +f 5/13/4 6/1/4 7/14/4 8/15/4 +f 4/12/3 9/16/3 10/17/3 11/18/3 +f 12/19/4 7/14/4 13/8/4 14/20/4 +g Top_Cube.002_None_Top_Cube.002_None_bottom_Top_Cube.002_None_Top_Cube.002_None_bottom_Back +f 6/21/5 1/9/5 4/12/5 7/22/5 +f 7/22/6 4/12/6 11/18/6 13/23/6 +g Top_Cube.002_None_Top_Cube.002_None_bottom_Top_Cube.002_None_Top_Cube.002_None_bottom_Front +f 2/10/7 5/24/7 8/25/7 3/11/7 +f 9/16/8 12/26/8 14/27/8 10/17/8 +g Top_Cube.002_None_Top_Cube.002_None_bottom_Top_Cube.002_None_Top_Cube.002_None_bottom_Inside +f 4/28/9 3/29/9 8/30/9 7/31/9 +f 7/31/10 12/32/10 9/33/10 4/28/10 diff --git a/games/devtest/mods/testformspec/textures/default_chest_front.png b/games/devtest/mods/testformspec/textures/default_chest_front.png new file mode 100644 index 0000000000000000000000000000000000000000..85227d8fd636ace9592070a2f7e86df531dc03a8 GIT binary patch literal 423 zcmV;Y0a*TtP)i&K4LJ(h4fd|)}Lf=h&H zLXT)Oby_pmu656)Y`~XZu7*#Kb4!G4MT>1goOVBpWiV}2FKSRK-QC@|u%qC!dc~Y& zzLs61d_{+CM1p2Nz__cUqocE{p6j=Mc6N54fmoDxPm^{|mw8KwZcBJ!K5tn$lWaGU zYBh#mEPz=jWJ@J{RU=tkP^|y}0JKR&K~xyiWzI#i!!Qs<(JxtGvCM3T^TSO0-&i_h z-{GmQuJfE_l-{oE`T}5=5$C4V3&q&H0!WvEqLgLXY__8{K&+!MG8kioLN@@6 zN>kDk@*aBGOD_RVl&R|knQ6>5Cjg{HdNti|CKb)I1AvErQM3I&FVvh>0Bdbfh?>Um zAmA$iTLe&1C`R-A2rxJ}fDLU}me>LiC%WV=gmA9K@ef4uc|E)2k#quJ)ECCe4HTG& R7=Qo(002ovPDHLkV1m!~wG03N literal 0 HcmV?d00001 diff --git a/games/devtest/mods/testformspec/textures/default_chest_inside.png b/games/devtest/mods/testformspec/textures/default_chest_inside.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7b6b13270890618c6a6332eee1c6c721b91f1b GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!2~3yyw0Buq;x%9978NlCvUkPo}j=vVadU` z_{Ppg7lT!aA;@D3{h6xK;WWK5B?*(dP@O1TaS?83{1OQEB B9=-qo literal 0 HcmV?d00001 diff --git a/games/devtest/mods/testformspec/textures/default_chest_side.png b/games/devtest/mods/testformspec/textures/default_chest_side.png new file mode 100644 index 0000000000000000000000000000000000000000..44a65a43d3c3f2ba44bceb519a0696e1ba441800 GIT binary patch literal 375 zcmV--0f_#IP)n`5knO{RcJoOVB!a5-&MFKSRK;In$zuXoa@aHD)h zf@VK%ST=rFCF{3-pn+J3ZcC(oM}}W4WJ@JsV)uan0065=L_t&-(`C+A5`!=dMbXH@ z-MB%3P}BSV*B6mr?&lfJ= zdsUum4#6X1C?EZJP^f8bSOa-|wF`=iC+qn|{rj V3gJ4$MZo|7002ovPDHLkV1iD9pBn%G literal 0 HcmV?d00001 diff --git a/games/devtest/mods/testformspec/textures/default_chest_top.png b/games/devtest/mods/testformspec/textures/default_chest_top.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a92ee07edb9c3b6b8e33320082ca222656c252 GIT binary patch literal 423 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 parts = split(element, ';'); + + if (parts.size() < 5 || (parts.size() > 8 && + m_formspec_version <= FORMSPEC_API_VERSION)) { + errorstream << "Invalid model element (" << parts.size() << "): '" << element + << "'" << std::endl; + return; + } + + // Avoid length checks by resizing + if (parts.size() < 8) + parts.resize(8); + + std::vector v_pos = split(parts[0], ','); + std::vector v_geom = split(parts[1], ','); + std::string name = unescape_string(parts[2]); + std::string meshstr = unescape_string(parts[3]); + std::vector textures = split(parts[4], ','); + std::vector vec_rot = split(parts[5], ','); + bool inf_rotation = is_yes(parts[6]); + bool mousectrl = is_yes(parts[7]) || parts[7].empty(); // default true + + MY_CHECKPOS("model", 0); + MY_CHECKGEOM("model", 1); + + v2s32 pos; + v2s32 geom; + + if (data->real_coordinates) { + pos = getRealCoordinateBasePos(v_pos); + geom = getRealCoordinateGeometry(v_geom); + } else { + pos = getElementBasePos(&v_pos); + geom.X = stof(v_geom[0]) * (float)imgsize.X; + geom.Y = stof(v_geom[1]) * (float)imgsize.Y; + } + + if (!data->explicit_size) + warningstream << "invalid use of model without a size[] element" << std::endl; + + scene::IAnimatedMesh *mesh = m_client->getMesh(meshstr); + + if (!mesh) { + errorstream << "Invalid model element: Unable to load mesh:" + << std::endl << "\t" << meshstr << std::endl; + return; + } + + FieldSpec spec( + name, + L"", + L"", + 258 + m_fields.size() + ); + + core::rect rect(pos, pos + geom); + + GUIScene *e = new GUIScene(Environment, RenderingEngine::get_scene_manager(), + data->current_parent, rect, spec.fid); + + auto meshnode = e->setMesh(mesh); + + for (u32 i = 0; i < textures.size() && i < meshnode->getMaterialCount(); ++i) + e->setTexture(i, m_tsrc->getTexture(textures[i])); + + if (vec_rot.size() >= 2) + e->setRotation(v2f(stof(vec_rot[0]), stof(vec_rot[1]))); + + e->enableContinuousRotation(inf_rotation); + e->enableMouseControl(mousectrl); + + auto style = getStyleForElement("model", spec.fname); + e->setStyles(style); + e->drop(); + + m_fields.push_back(spec); +} + void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element) { //some prechecks @@ -2891,6 +2972,11 @@ void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element) return; } + if (type == "model") { + parseModel(data, description); + return; + } + // Ignore others infostream << "Unknown DrawSpec: type=" << type << ", data=\"" << description << "\"" << std::endl; diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index 613acaa04..c5d662a69 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -38,7 +38,6 @@ with this program; if not, write to the Free Software Foundation, Inc., class InventoryManager; class ISimpleTextureSource; class Client; -class TexturePool; class GUIScrollContainer; typedef enum { @@ -444,6 +443,7 @@ private: void parseAnchor(parserData *data, const std::string &element); bool parseStyle(parserData *data, const std::string &element, bool style_type); void parseSetFocus(const std::string &element); + void parseModel(parserData *data, const std::string &element); void tryClose(); diff --git a/src/gui/guiScene.cpp b/src/gui/guiScene.cpp new file mode 100644 index 000000000..08f119e07 --- /dev/null +++ b/src/gui/guiScene.cpp @@ -0,0 +1,257 @@ +/* +Minetest +Copyright (C) 2020 Jean-Patrick Guerrero + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "guiScene.h" + +#include +#include +#include +#include "porting.h" + +GUIScene::GUIScene(gui::IGUIEnvironment *env, scene::ISceneManager *smgr, + gui::IGUIElement *parent, core::recti rect, s32 id) + : IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rect) +{ + m_driver = env->getVideoDriver(); + m_smgr = smgr->createNewSceneManager(false); + + m_cam = m_smgr->addCameraSceneNode(0, v3f(0.f, 0.f, -100.f), v3f(0.f)); + m_cam->setFOV(30.f * core::DEGTORAD); + + scene::ILightSceneNode *light = m_smgr->addLightSceneNode(m_cam); + light->setRadius(1000.f); + + m_smgr->getParameters()->setAttribute(scene::ALLOW_ZWRITE_ON_TRANSPARENT, true); +} + +GUIScene::~GUIScene() +{ + setMesh(nullptr); + + m_smgr->drop(); +} + +scene::IAnimatedMeshSceneNode *GUIScene::setMesh(scene::IAnimatedMesh *mesh) +{ + if (m_mesh) { + m_mesh->remove(); + m_mesh = nullptr; + } + + if (!mesh) + return nullptr; + + m_mesh = m_smgr->addAnimatedMeshSceneNode(mesh); + m_mesh->setPosition(-m_mesh->getBoundingBox().getCenter()); + m_mesh->animateJoints(); + return m_mesh; +} + +void GUIScene::setTexture(u32 idx, video::ITexture *texture) +{ + video::SMaterial &material = m_mesh->getMaterial(idx); + material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; + material.MaterialTypeParam = 0.5f; + material.TextureLayer[0].Texture = texture; + material.setFlag(video::EMF_LIGHTING, false); + material.setFlag(video::EMF_FOG_ENABLE, true); + material.setFlag(video::EMF_BILINEAR_FILTER, false); + material.setFlag(video::EMF_BACK_FACE_CULLING, false); +} + +void GUIScene::draw() +{ + // Control rotation speed based on time + u64 new_time = porting::getTimeMs(); + u64 dtime_ms = 0; + if (m_last_time != 0) + dtime_ms = porting::getDeltaMs(m_last_time, new_time); + m_last_time = new_time; + + core::rect oldViewPort = m_driver->getViewPort(); + m_driver->setViewPort(getAbsoluteClippingRect()); + core::recti borderRect = Environment->getRootGUIElement()->getAbsoluteClippingRect(); + + if (m_bgcolor != 0) { + Environment->getSkin()->draw3DSunkenPane( + this, m_bgcolor, false, true, borderRect, 0); + } + + core::dimension2d size = getAbsoluteClippingRect().getSize(); + m_smgr->getActiveCamera()->setAspectRatio((f32)size.Width / (f32)size.Height); + + if (!m_target) { + updateCamera(m_smgr->addEmptySceneNode()); + rotateCamera(v3f(0.f)); + m_cam->bindTargetAndRotation(true); + } + + cameraLoop(); + + // Continuous rotation + if (m_inf_rot) + rotateCamera(v3f(0.f, -0.03f * (float)dtime_ms, 0.f)); + + m_smgr->drawAll(); + + if (m_initial_rotation && m_mesh) { + rotateCamera(v3f(m_custom_rot.X, m_custom_rot.Y, 0.f)); + calcOptimalDistance(); + + m_initial_rotation = false; + } + + m_driver->setViewPort(oldViewPort); +} + +bool GUIScene::OnEvent(const SEvent &event) +{ + if (m_mouse_ctrl && event.EventType == EET_MOUSE_INPUT_EVENT) { + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + m_last_pos = v2f((f32)event.MouseInput.X, (f32)event.MouseInput.Y); + return true; + } else if (event.MouseInput.Event == EMIE_MOUSE_MOVED) { + if (event.MouseInput.isLeftPressed()) { + m_curr_pos = v2f((f32)event.MouseInput.X, (f32)event.MouseInput.Y); + + rotateCamera(v3f( + m_last_pos.Y - m_curr_pos.Y, + m_curr_pos.X - m_last_pos.X, 0.f)); + + m_last_pos = m_curr_pos; + return true; + } + } + } + + return gui::IGUIElement::OnEvent(event); +} + +void GUIScene::setStyles(const std::array &styles) +{ + StyleSpec::State state = StyleSpec::STATE_DEFAULT; + StyleSpec style = StyleSpec::getStyleFromStatePropagation(styles, state); + + setNotClipped(style.getBool(StyleSpec::NOCLIP, false)); + setBackgroundColor(style.getColor(StyleSpec::BGCOLOR, m_bgcolor)); +} + +/* Camera control functions */ + +inline void GUIScene::calcOptimalDistance() +{ + core::aabbox3df box = m_mesh->getBoundingBox(); + f32 width = box.MaxEdge.X - box.MinEdge.X; + f32 height = box.MaxEdge.Y - box.MinEdge.Y; + f32 depth = box.MaxEdge.Z - box.MinEdge.Z; + f32 max_width = width > depth ? width : depth; + + const scene::SViewFrustum *f = m_cam->getViewFrustum(); + f32 cam_far = m_cam->getFarValue(); + f32 far_width = core::line3df(f->getFarLeftUp(), f->getFarRightUp()).getLength(); + f32 far_height = core::line3df(f->getFarLeftUp(), f->getFarLeftDown()).getLength(); + + core::recti rect = getAbsolutePosition(); + f32 zoomX = rect.getWidth() / max_width; + f32 zoomY = rect.getHeight() / height; + f32 dist; + + if (zoomX < zoomY) + dist = (max_width / (far_width / cam_far)) + (0.5f * max_width); + else + dist = (height / (far_height / cam_far)) + (0.5f * max_width); + + m_cam_distance = dist; + m_update_cam = true; +} + +void GUIScene::updateCamera(scene::ISceneNode *target) +{ + m_target = target; + updateTargetPos(); + + m_last_target_pos = m_target_pos; + updateCameraPos(); + + m_update_cam = true; +} + +void GUIScene::updateTargetPos() +{ + m_last_target_pos = m_target_pos; + m_target->updateAbsolutePosition(); + m_target_pos = m_target->getAbsolutePosition(); +} + +void GUIScene::setCameraRotation(v3f rot) +{ + correctBounds(rot); + + core::matrix4 mat; + mat.setRotationDegrees(rot); + + m_cam_pos = v3f(0.f, 0.f, m_cam_distance); + mat.rotateVect(m_cam_pos); + + m_cam_pos += m_target_pos; + m_cam->setPosition(m_cam_pos); + m_update_cam = false; +} + +bool GUIScene::correctBounds(v3f &rot) +{ + const float ROTATION_MAX_1 = 60.0f; + const float ROTATION_MAX_2 = 300.0f; + + // Limit and correct the rotation when needed + if (rot.X < 90.f) { + if (rot.X > ROTATION_MAX_1) { + rot.X = ROTATION_MAX_1; + return true; + } + } else if (rot.X < ROTATION_MAX_2) { + rot.X = ROTATION_MAX_2; + return true; + } + + // Not modified + return false; +} + +void GUIScene::cameraLoop() +{ + updateCameraPos(); + updateTargetPos(); + + if (m_target_pos != m_last_target_pos) + m_update_cam = true; + + if (m_update_cam) { + m_cam_pos = m_target_pos + (m_cam_pos - m_target_pos).normalize() * m_cam_distance; + + v3f rot = getCameraRotation(); + if (correctBounds(rot)) + setCameraRotation(rot); + + m_cam->setPosition(m_cam_pos); + m_cam->setTarget(m_target_pos); + + m_update_cam = false; + } +} diff --git a/src/gui/guiScene.h b/src/gui/guiScene.h new file mode 100644 index 000000000..707e6f66a --- /dev/null +++ b/src/gui/guiScene.h @@ -0,0 +1,85 @@ +/* +Minetest +Copyright (C) 2020 Jean-Patrick Guerrero + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "ICameraSceneNode.h" +#include "StyleSpec.h" + +using namespace irr; + +class GUIScene : public gui::IGUIElement +{ +public: + GUIScene(gui::IGUIEnvironment *env, scene::ISceneManager *smgr, + gui::IGUIElement *parent, core::recti rect, s32 id = -1); + + ~GUIScene(); + + scene::IAnimatedMeshSceneNode *setMesh(scene::IAnimatedMesh *mesh = nullptr); + void setTexture(u32 idx, video::ITexture *texture); + void setBackgroundColor(const video::SColor &color) noexcept { m_bgcolor = color; }; + void enableMouseControl(bool enable) noexcept { m_mouse_ctrl = enable; }; + void setRotation(v2f rot) noexcept { m_custom_rot = rot; }; + void enableContinuousRotation(bool enable) noexcept { m_inf_rot = enable; }; + void setStyles(const std::array &styles); + + virtual void draw(); + virtual bool OnEvent(const SEvent &event); + +private: + void calcOptimalDistance(); + void updateTargetPos(); + void updateCamera(scene::ISceneNode *target); + void setCameraRotation(v3f rot); + /// @return true indicates that the rotation was corrected + bool correctBounds(v3f &rot); + void cameraLoop(); + + void updateCameraPos() { m_cam_pos = m_cam->getPosition(); }; + v3f getCameraRotation() const { return (m_cam_pos - m_target_pos).getHorizontalAngle(); }; + void rotateCamera(const v3f &delta) { setCameraRotation(getCameraRotation() + delta); }; + + scene::ISceneManager *m_smgr; + video::IVideoDriver *m_driver; + scene::ICameraSceneNode *m_cam; + scene::ISceneNode *m_target = nullptr; + scene::IAnimatedMeshSceneNode *m_mesh = nullptr; + + f32 m_cam_distance = 50.f; + + u64 m_last_time = 0; + + v3f m_cam_pos; + v3f m_target_pos; + v3f m_last_target_pos; + // Cursor positions + v2f m_curr_pos; + v2f m_last_pos; + // Initial rotation + v2f m_custom_rot; + + bool m_mouse_ctrl = true; + bool m_update_cam = false; + bool m_inf_rot = false; + bool m_initial_rotation = true; + + video::SColor m_bgcolor = 0; +}; diff --git a/util/ci/clang-format-whitelist.txt b/util/ci/clang-format-whitelist.txt index 3334257ae..75d99f4cd 100644 --- a/util/ci/clang-format-whitelist.txt +++ b/util/ci/clang-format-whitelist.txt @@ -183,6 +183,8 @@ src/gui/guiMainMenu.h src/gui/guiPasswordChange.cpp src/gui/guiPathSelectMenu.cpp src/gui/guiPathSelectMenu.h +src/gui/guiScene.cpp +src/gui/guiScene.h src/gui/guiScrollBar.cpp src/gui/guiSkin.cpp src/gui/guiSkin.h