diff --git a/doc/lua_api.txt b/doc/lua_api.txt index d2ab9de29..26d8a3d91 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -5074,9 +5074,10 @@ This is basically a reference to a C++ `ServerActiveObject` * `acc` is a vector * `get_acceleration()`: returns the acceleration, a vector * `set_rotation(rot)` - * `rot` is a vector (radians) -* `get_rotation()` : returns the rotation, a vector (radians) -* `set_yaw(radians)` + * `rot` is a vector (radians). X is pitch (elevation), Y is yaw (heading) + and Z is roll (bank). +* `get_rotation()`: returns the rotation, a vector (radians) +* `set_yaw(radians)`: sets the yaw (heading). * `get_yaw()`: returns number in radians * `set_texture_mod(mod)` * `get_texture_mod()` returns current texture modifier diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 116a2e53b..6112edaff 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include "content_cao.h" -#include "util/numeric.h" // For IntervalLimiter +#include "util/numeric.h" // For IntervalLimiter & setPitchYawRoll #include "util/serialize.h" #include "util/basic_macros.h" #include "client/sound.h" @@ -365,7 +365,7 @@ void GenericCAO::processInitData(const std::string &data) return; } - // PROTOCOL_VERSION >= 37 + // PROTOCOL_VERSION >= 37 m_name = deSerializeString(is); m_is_player = readU8(is); m_id = readU16(is); @@ -402,10 +402,9 @@ bool GenericCAO::getSelectionBox(aabb3f *toset) const v3f GenericCAO::getPosition() { - if (getParent() != NULL) { - scene::ISceneNode *node = getSceneNode(); - if (node) - return node->getAbsolutePosition(); + if (getParent() != nullptr) { + if (m_matrixnode) + return m_matrixnode->getAbsolutePosition(); return m_position; } @@ -486,7 +485,7 @@ void GenericCAO::removeFromScene(bool permanent) LocalPlayer* player = m_env->getLocalPlayer(); if (this == player->parent) { - player->parent = NULL; + player->parent = nullptr; player->isAttached = false; } } @@ -494,24 +493,30 @@ void GenericCAO::removeFromScene(bool permanent) if (m_meshnode) { m_meshnode->remove(); m_meshnode->drop(); - m_meshnode = NULL; + m_meshnode = nullptr; } else if (m_animated_meshnode) { m_animated_meshnode->remove(); m_animated_meshnode->drop(); - m_animated_meshnode = NULL; + m_animated_meshnode = nullptr; } else if (m_wield_meshnode) { m_wield_meshnode->remove(); m_wield_meshnode->drop(); - m_wield_meshnode = NULL; + m_wield_meshnode = nullptr; } else if (m_spritenode) { m_spritenode->remove(); m_spritenode->drop(); - m_spritenode = NULL; + m_spritenode = nullptr; + } + + if (m_matrixnode) { + m_matrixnode->remove(); + m_matrixnode->drop(); + m_matrixnode = nullptr; } if (m_nametag) { m_client->getCamera()->removeNametag(m_nametag); - m_nametag = NULL; + m_nametag = nullptr; } } @@ -534,8 +539,11 @@ void GenericCAO::addToScene(ITextureSource *tsrc) if (m_prop.visual == "sprite") { infostream<<"GenericCAO::addToScene(): single_sprite"< + addDummyTransformationSceneNode(); + m_matrixnode->grab(); m_spritenode = RenderingEngine::get_scene_manager()->addBillboardSceneNode( - NULL, v2f(1, 1), v3f(0,0,0), -1); + m_matrixnode, v2f(1, 1), v3f(0,0,0), -1); m_spritenode->grab(); m_spritenode->setMaterialTexture(0, tsrc->getTextureForMesh("unknown_node.png")); @@ -608,17 +616,24 @@ void GenericCAO::addToScene(ITextureSource *tsrc) mesh->addMeshBuffer(buf); buf->drop(); } - m_meshnode = RenderingEngine::get_scene_manager()->addMeshSceneNode(mesh, NULL); + m_matrixnode = RenderingEngine::get_scene_manager()-> + addDummyTransformationSceneNode(); + m_matrixnode->grab(); + m_meshnode = RenderingEngine::get_scene_manager()-> + addMeshSceneNode(mesh, m_matrixnode); m_meshnode->grab(); mesh->drop(); // Set it to use the materials of the meshbuffers directly. // This is needed for changing the texture in the future m_meshnode->setReadOnlyMaterials(true); - } - else if(m_prop.visual == "cube") { + } else if (m_prop.visual == "cube") { infostream<<"GenericCAO::addToScene(): cube"<addMeshSceneNode(mesh, NULL); + m_matrixnode = RenderingEngine::get_scene_manager()-> + addDummyTransformationSceneNode(nullptr); + m_matrixnode->grab(); + m_meshnode = RenderingEngine::get_scene_manager()-> + addMeshSceneNode(mesh, m_matrixnode); m_meshnode->grab(); mesh->drop(); @@ -630,14 +645,15 @@ void GenericCAO::addToScene(ITextureSource *tsrc) m_meshnode->setMaterialFlag(video::EMF_BILINEAR_FILTER, false); m_meshnode->setMaterialType(material_type); m_meshnode->setMaterialFlag(video::EMF_FOG_ENABLE, true); - } - else if(m_prop.visual == "mesh") { + } else if (m_prop.visual == "mesh") { infostream<<"GenericCAO::addToScene(): mesh"<getMesh(m_prop.mesh, true); - if(mesh) - { + if (mesh) { + m_matrixnode = RenderingEngine::get_scene_manager()-> + addDummyTransformationSceneNode(nullptr); + m_matrixnode->grab(); m_animated_meshnode = RenderingEngine::get_scene_manager()-> - addAnimatedMeshSceneNode(mesh, NULL); + addAnimatedMeshSceneNode(mesh, m_matrixnode); m_animated_meshnode->grab(); mesh->drop(); // The scene node took hold of it m_animated_meshnode->animateJoints(); // Needed for some animations @@ -655,8 +671,7 @@ void GenericCAO::addToScene(ITextureSource *tsrc) m_animated_meshnode->setMaterialFlag(video::EMF_FOG_ENABLE, true); m_animated_meshnode->setMaterialFlag(video::EMF_BACK_FACE_CULLING, m_prop.backface_culling); - } - else + } else errorstream<<"GenericCAO::addToScene(): Could not load mesh "<idef()); } + m_matrixnode = RenderingEngine::get_scene_manager()-> + addDummyTransformationSceneNode(nullptr); + m_matrixnode->grab(); m_wield_meshnode = new WieldMeshSceneNode( RenderingEngine::get_scene_manager(), -1); + m_wield_meshnode->setParent(m_matrixnode); m_wield_meshnode->setItem(item, m_client, (m_prop.visual == "wielditem")); @@ -763,10 +782,12 @@ void GenericCAO::updateNodePos() if (node) { v3s16 camera_offset = m_env->getCameraOffset(); - node->setPosition(pos_translator.val_current - intToFloat(camera_offset, BS)); + v3f pos = pos_translator.val_current - + intToFloat(camera_offset, BS); + getPosRotMatrix().setTranslation(pos); if (node != m_spritenode) { // rotate if not a sprite v3f rot = m_is_local_player ? -m_rotation : -rot_translator.val_current; - node->setRotation(rot); + setPitchYawRoll(getPosRotMatrix(), rot); } } } @@ -858,8 +879,10 @@ void GenericCAO::step(float dtime, ClientEnvironment *env) ClientActiveObject *obj = m_env->getActiveObject(*ci); if (obj) { scene::ISceneNode *child_node = obj->getSceneNode(); + // The node's parent is always an IDummyTraformationSceneNode, + // so we need to reparent that one instead. if (child_node) - child_node->setParent(m_smgr->getRootSceneNode()); + child_node->getParent()->setParent(m_smgr->getRootSceneNode()); } ++ci; } @@ -1266,16 +1289,13 @@ void GenericCAO::updateBonePosition() void GenericCAO::updateAttachments() { - - if (!getParent()) { // Detach or don't attach - scene::ISceneNode *node = getSceneNode(); - if (node) { - v3f old_position = node->getAbsolutePosition(); - v3f old_rotation = node->getRotation(); - node->setParent(m_smgr->getRootSceneNode()); - node->setPosition(old_position); - node->setRotation(old_rotation); - node->updateAbsolutePosition(); + ClientActiveObject *parent = getParent(); + if (!parent) { // Detach or don't attach + if (m_matrixnode) { + v3f old_pos = m_matrixnode->getAbsolutePosition(); + m_matrixnode->setParent(m_smgr->getRootSceneNode()); + getPosRotMatrix().setTranslation(old_pos); + m_matrixnode->updateAbsolutePosition(); } if (m_is_local_player) { LocalPlayer *player = m_env->getLocalPlayer(); @@ -1284,20 +1304,20 @@ void GenericCAO::updateAttachments() } else // Attach { - scene::ISceneNode *my_node = getSceneNode(); - - scene::ISceneNode *parent_node = getParent()->getSceneNode(); + scene::ISceneNode *parent_node = parent->getSceneNode(); scene::IAnimatedMeshSceneNode *parent_animated_mesh_node = - getParent()->getAnimatedMeshSceneNode(); + parent->getAnimatedMeshSceneNode(); if (parent_animated_mesh_node && !m_attachment_bone.empty()) { parent_node = parent_animated_mesh_node->getJointNode(m_attachment_bone.c_str()); } - if (my_node && parent_node) { - my_node->setParent(parent_node); - my_node->setPosition(m_attachment_position); - my_node->setRotation(m_attachment_rotation); - my_node->updateAbsolutePosition(); + if (m_matrixnode && parent_node) { + m_matrixnode->setParent(parent_node); + getPosRotMatrix().setTranslation(m_attachment_position); + //setPitchYawRoll(getPosRotMatrix(), m_attachment_rotation); + // use Irrlicht eulers instead + getPosRotMatrix().setRotationDegrees(m_attachment_rotation); + m_matrixnode->updateAbsolutePosition(); } if (m_is_local_player) { LocalPlayer *player = m_env->getLocalPlayer(); diff --git a/src/client/content_cao.h b/src/client/content_cao.h index 98932137e..4627800ee 100644 --- a/src/client/content_cao.h +++ b/src/client/content_cao.h @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "object_properties.h" #include "itemgroup.h" #include "constants.h" +#include class Camera; class Client; @@ -81,6 +82,7 @@ private: scene::IAnimatedMeshSceneNode *m_animated_meshnode = nullptr; WieldMeshSceneNode *m_wield_meshnode = nullptr; scene::IBillboardSceneNode *m_spritenode = nullptr; + scene::IDummyTransformationSceneNode *m_matrixnode = nullptr; Nametag *m_nametag = nullptr; v3f m_position = v3f(0.0f, 10.0f * BS, 0); v3f m_velocity; @@ -163,6 +165,19 @@ public: scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode(); + // m_matrixnode controls the position and rotation of the child node + // for all scene nodes, as a workaround for an Irrlicht problem with + // rotations. The child node's position can't be used because it's + // rotated, and must remain as 0. + // Note that m_matrixnode.setPosition() shouldn't be called. Use + // m_matrixnode->getRelativeTransformationMatrix().setTranslation() + // instead (aka getPosRotMatrix().setTranslation()). + inline core::matrix4 &getPosRotMatrix() + { + assert(m_matrixnode); + return m_matrixnode->getRelativeTransformationMatrix(); + } + inline f32 getStepHeight() const { return m_prop.stepheight; diff --git a/src/irrlichttypes_extrabloated.h b/src/irrlichttypes_extrabloated.h index 83c4ca01b..b03ba7955 100644 --- a/src/irrlichttypes_extrabloated.h +++ b/src/irrlichttypes_extrabloated.h @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include #include #include diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index 703e96056..85436068f 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -193,6 +193,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Add TOCLIENT_NODEMETA_CHANGED New network float format ContentFeatures version 13 + Add full Euler rotations instead of just yaw */ #define LATEST_PROTOCOL_VERSION 37 diff --git a/src/unittest/test_utilities.cpp b/src/unittest/test_utilities.cpp index 234f622d5..8e8958d18 100644 --- a/src/unittest/test_utilities.cpp +++ b/src/unittest/test_utilities.cpp @@ -53,6 +53,7 @@ public: void testIsPowerOfTwo(); void testMyround(); void testStringJoin(); + void testEulerConversion(); }; static TestUtilities g_test_instance; @@ -82,6 +83,7 @@ void TestUtilities::runTests(IGameDef *gamedef) TEST(testIsPowerOfTwo); TEST(testMyround); TEST(testStringJoin); + TEST(testEulerConversion); } //////////////////////////////////////////////////////////////////////////////// @@ -394,3 +396,115 @@ void TestUtilities::testStringJoin() UASSERT(str_join(input, " and ") == "one and two and three"); } + +static bool within(const f32 value1, const f32 value2, const f32 precision) +{ + return std::fabs(value1 - value2) <= precision; +} + +static bool within(const v3f &v1, const v3f &v2, const f32 precision) +{ + return within(v1.X, v2.X, precision) && within(v1.Y, v2.Y, precision) + && within(v1.Z, v2.Z, precision); +} + +static bool within(const core::matrix4 &m1, const core::matrix4 &m2, + const f32 precision) +{ + const f32 *M1 = m1.pointer(); + const f32 *M2 = m2.pointer(); + for (int i = 0; i < 16; i++) + if (! within(M1[i], M2[i], precision)) + return false; + return true; +} + +static bool roundTripsDeg(const v3f &v, const f32 precision) +{ + core::matrix4 m; + setPitchYawRoll(m, v); + return within(v, getPitchYawRoll(m), precision); +} + +void TestUtilities::testEulerConversion() +{ + // This test may fail on non-IEEE systems. + // Low tolerance is 4 ulp(1.0) for binary floats with 24 bit mantissa. + // (ulp = unit in the last place; ulp(1.0) = 2^-23). + const f32 tolL = 4.76837158203125e-7f; + // High tolerance is 2 ulp(180.0), needed for numbers in degrees. + // ulp(180.0) = 2^-16 + const f32 tolH = 3.0517578125e-5f; + v3f v1, v2; + core::matrix4 m1, m2; + const f32 *M1 = m1.pointer(); + const f32 *M2 = m2.pointer(); + + // Check that the radians version and the degrees version + // produce the same results. Check also that the conversion + // works both ways for these values. + v1 = v3f(M_PI/3.0, M_PI/5.0, M_PI/4.0); + v2 = v3f(60.0f, 36.0f, 45.0f); + setPitchYawRollRad(m1, v1); + setPitchYawRoll(m2, v2); + UASSERT(within(m1, m2, tolL)); + UASSERT(within(getPitchYawRollRad(m1), v1, tolL)); + UASSERT(within(getPitchYawRoll(m2), v2, tolH)); + + // Check the rotation matrix produced. + UASSERT(within(M1[0], 0.932004869f, tolL)); + UASSERT(within(M1[1], 0.353553385f, tolL)); + UASSERT(within(M1[2], 0.0797927827f, tolL)); + UASSERT(within(M1[4], -0.21211791f, tolL)); + UASSERT(within(M1[5], 0.353553355f, tolL)); + UASSERT(within(M1[6], 0.911046684f, tolL)); + UASSERT(within(M1[8], 0.293892622f, tolL)); + UASSERT(within(M1[9], -0.866025448f, tolL)); + UASSERT(within(M1[10], 0.404508471f, tolL)); + + // Check that the matrix is still homogeneous with no translation + UASSERT(M1[3] == 0.0f); + UASSERT(M1[7] == 0.0f); + UASSERT(M1[11] == 0.0f); + UASSERT(M1[12] == 0.0f); + UASSERT(M1[13] == 0.0f); + UASSERT(M1[14] == 0.0f); + UASSERT(M1[15] == 1.0f); + UASSERT(M2[3] == 0.0f); + UASSERT(M2[7] == 0.0f); + UASSERT(M2[11] == 0.0f); + UASSERT(M2[12] == 0.0f); + UASSERT(M2[13] == 0.0f); + UASSERT(M2[14] == 0.0f); + UASSERT(M2[15] == 1.0f); + + // Compare to Irrlicht's results. To be comparable, the + // angles must come in a different order and the matrix + // elements to compare are different too. + m2.setRotationRadians(v3f(v1.Z, v1.X, v1.Y)); + UASSERT(within(M1[0], M2[5], tolL)); + UASSERT(within(M1[1], M2[6], tolL)); + UASSERT(within(M1[2], M2[4], tolL)); + + UASSERT(within(M1[4], M2[9], tolL)); + UASSERT(within(M1[5], M2[10], tolL)); + UASSERT(within(M1[6], M2[8], tolL)); + + UASSERT(within(M1[8], M2[1], tolL)); + UASSERT(within(M1[9], M2[2], tolL)); + UASSERT(within(M1[10], M2[0], tolL)); + + // Check that Eulers that produce near gimbal-lock still round-trip + UASSERT(roundTripsDeg(v3f(89.9999f, 17.f, 0.f), tolH)); + UASSERT(roundTripsDeg(v3f(89.9999f, 0.f, 19.f), tolH)); + UASSERT(roundTripsDeg(v3f(89.9999f, 17.f, 19.f), tolH)); + + // Check that Eulers at an angle > 90 degrees may not round-trip... + v1 = v3f(90.00001f, 1.f, 1.f); + setPitchYawRoll(m1, v1); + v2 = getPitchYawRoll(m1); + //UASSERT(within(v1, v2, tolL)); // this is typically false + // ... however the rotation matrix is the same for both + setPitchYawRoll(m2, v2); + UASSERT(within(m1, m2, tolL)); +} diff --git a/src/util/numeric.cpp b/src/util/numeric.cpp index a120e3207..bd298e94e 100644 --- a/src/util/numeric.cpp +++ b/src/util/numeric.cpp @@ -174,3 +174,38 @@ s16 adjustDist(s16 dist, float zoom_fov) return std::round(dist * std::cbrt((1.0f - std::cos(threshold_fov)) / (1.0f - std::cos(zoom_fov / 2.0f)))); } + +void setPitchYawRollRad(core::matrix4 &m, const v3f &rot) +{ + f64 a1 = rot.Z, a2 = rot.X, a3 = rot.Y; + f64 c1 = cos(a1), s1 = sin(a1); + f64 c2 = cos(a2), s2 = sin(a2); + f64 c3 = cos(a3), s3 = sin(a3); + f32 *M = m.pointer(); + + M[0] = s1 * s2 * s3 + c1 * c3; + M[1] = s1 * c2; + M[2] = s1 * s2 * c3 - c1 * s3; + + M[4] = c1 * s2 * s3 - s1 * c3; + M[5] = c1 * c2; + M[6] = c1 * s2 * c3 + s1 * s3; + + M[8] = c2 * s3; + M[9] = -s2; + M[10] = c2 * c3; +} + +v3f getPitchYawRollRad(const core::matrix4 &m) +{ + const f32 *M = m.pointer(); + + f64 a1 = atan2(M[1], M[5]); + f64 c2 = sqrt(M[10]*M[10] + M[8]*M[8]); + f32 a2 = atan2f(-M[9], c2); + f64 c1 = cos(a1); + f64 s1 = sin(a1); + f32 a3 = atan2f(s1*M[6] - c1*M[2], c1*M[0] - s1*M[4]); + + return v3f(a2, a3, a1); +} diff --git a/src/util/numeric.h b/src/util/numeric.h index bfdff84a0..6f82a18c1 100644 --- a/src/util/numeric.h +++ b/src/util/numeric.h @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irr_v2d.h" #include "irr_v3d.h" #include "irr_aabb3d.h" +#include #define rangelim(d, min, max) ((d) < (min) ? (min) : ((d) > (max) ? (max) : (d))) #define myfloor(x) ((x) < 0.0 ? (int)(x) - 1 : (int)(x)) @@ -417,3 +418,17 @@ inline void wrappedApproachShortest(T ¤t, const T target, const T stepsize current = target; } } + +void setPitchYawRollRad(core::matrix4 &m, const v3f &rot); + +inline void setPitchYawRoll(core::matrix4 &m, const v3f &rot) +{ + setPitchYawRollRad(m, rot * core::DEGTORAD64); +} + +v3f getPitchYawRollRad(const core::matrix4 &m); + +inline v3f getPitchYawRoll(const core::matrix4 &m) +{ + return getPitchYawRollRad(m) * core::RADTODEG64; +}