diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index a25f1db124..29b335b441 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -111,7 +111,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 39; + api["API_REVISION"] = 40; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); MWBase::Environment::get().getStateManager()->requestQuit(); diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 4aea21f948..a19db0af66 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" @@ -141,6 +144,28 @@ namespace MWLua listT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); } + osg::Vec3f toEulerRotation(const sol::object& transform, bool isActor) + { + if (transform.is()) + { + const osg::Quat& q = transform.as().mQ; + return isActor ? Misc::toEulerAnglesXZ(q) : Misc::toEulerAnglesZYX(q); + } + else + { + const osg::Matrixf& m = LuaUtil::cast(transform).mM; + return isActor ? Misc::toEulerAnglesXZ(m) : Misc::toEulerAnglesZYX(m); + } + } + + osg::Quat toQuat(const ESM::Position& pos, bool isActor) + { + if (isActor) + return osg::Quat(pos.rot[0], osg::Vec3(-1, 0, 0)) * osg::Quat(pos.rot[2], osg::Vec3(0, 0, -1)); + else + return Misc::Convert::makeOsgQuat(pos.rot); + } + template void addBasicBindings(sol::usertype& objectT, const Context& context) { @@ -166,12 +191,14 @@ namespace MWLua [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getRefData().getPosition().asVec3(); }); objectT["scale"] = sol::readonly_property([](const ObjectT& o) -> float { return o.ptr().getCellRef().getScale(); }); - objectT["rotation"] = sol::readonly_property( - [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getRefData().getPosition().asRotationVec3(); }); + objectT["rotation"] = sol::readonly_property([](const ObjectT& o) -> LuaUtil::TransformQ { + return { toQuat(o.ptr().getRefData().getPosition(), o.ptr().getClass().isActor()) }; + }); objectT["startingPosition"] = sol::readonly_property( [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getCellRef().getPosition().asVec3(); }); - objectT["startingRotation"] = sol::readonly_property( - [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getCellRef().getPosition().asRotationVec3(); }); + objectT["startingRotation"] = sol::readonly_property([](const ObjectT& o) -> LuaUtil::TransformQ { + return { toQuat(o.ptr().getCellRef().getPosition(), o.ptr().getClass().isActor()) }; + }); objectT["getBoundingBox"] = [](const ObjectT& o) { MWRender::RenderingManager* renderingManager = MWBase::Environment::get().getWorld()->getRenderingManager(); @@ -401,12 +428,14 @@ namespace MWLua throw std::runtime_error("Object is either removed or already in the process of teleporting"); osg::Vec3f rot = ptr.getRefData().getPosition().asRotationVec3(); bool placeOnGround = false; - if (options.is()) - rot = options.as(); + if (LuaUtil::isTransform(options)) + rot = toEulerRotation(options, ptr.getClass().isActor()); else if (options != sol::nil) { sol::table t = LuaUtil::cast(options); - rot = LuaUtil::getValueOrDefault(t["rotation"], rot); + sol::object rotationArg = t["rotation"]; + if (rotationArg != sol::nil) + rot = toEulerRotation(rotationArg, ptr.getClass().isActor()); placeOnGround = LuaUtil::getValueOrDefault(t["onGround"], placeOnGround); } if (ptr.getContainerStore()) diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp index f13952a6b5..217a6fa958 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -41,8 +43,9 @@ namespace MWLua door["isTeleport"] = [](const Object& o) { return doorPtr(o).getCellRef().getTeleport(); }; door["destPosition"] = [](const Object& o) -> osg::Vec3f { return doorPtr(o).getCellRef().getDoorDest().asVec3(); }; - door["destRotation"] - = [](const Object& o) -> osg::Vec3f { return doorPtr(o).getCellRef().getDoorDest().asRotationVec3(); }; + door["destRotation"] = [](const Object& o) -> LuaUtil::TransformQ { + return { Misc::Convert::makeOsgQuat(doorPtr(o).getCellRef().getDoorDest().rot) }; + }; door["destCell"] = [](sol::this_state lua, const Object& o) -> sol::object { const MWWorld::CellRef& cellRef = doorPtr(o).getCellRef(); if (!cellRef.getTeleport()) @@ -80,8 +83,9 @@ namespace MWLua door["isTeleport"] = [](const Object& o) { return door4Ptr(o).getCellRef().getTeleport(); }; door["destPosition"] = [](const Object& o) -> osg::Vec3f { return door4Ptr(o).getCellRef().getDoorDest().asVec3(); }; - door["destRotation"] - = [](const Object& o) -> osg::Vec3f { return door4Ptr(o).getCellRef().getDoorDest().asRotationVec3(); }; + door["destRotation"] = [](const Object& o) -> LuaUtil::TransformQ { + return { Misc::Convert::makeOsgQuat(door4Ptr(o).getCellRef().getDoorDest().rot) }; + }; door["destCell"] = [](sol::this_state lua, const Object& o) -> sol::object { const MWWorld::CellRef& cellRef = door4Ptr(o).getCellRef(); if (!cellRef.getTeleport()) diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/openmw_test_suite/lua/test_utilpackage.cpp index 0631fbed19..26bdf3408b 100644 --- a/apps/openmw_test_suite/lua/test_utilpackage.cpp +++ b/apps/openmw_test_suite/lua/test_utilpackage.cpp @@ -157,7 +157,7 @@ namespace EXPECT_EQ(getAsString(lua, "moveAndScale:apply(v(300, 200, 100))"), "(156, 222, 68)"); EXPECT_THAT(getAsString(lua, "moveAndScale"), AllOf(StartsWith("TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) "), EndsWith(" }"))); - EXPECT_EQ(getAsString(lua, "T.identity"), "TransformM{ }"); + EXPECT_EQ(getAsString(lua, "T.identity"), "TransformQ{ rotation(angle=0, axis=(0, 0, 1)) }"); lua.safe_script("rx = T.rotateX(-math.pi / 2)"); lua.safe_script("ry = T.rotateY(-math.pi / 2)"); lua.safe_script("rz = T.rotateZ(-math.pi / 2)"); diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp index 5fbd1fe8b0..8e9f658ddc 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -175,7 +175,7 @@ namespace LuaUtil sol::table transforms(lua, sol::create); util["transform"] = LuaUtil::makeReadOnly(transforms); - transforms["identity"] = sol::make_object(lua, TransformM{ osg::Matrixf::identity() }); + transforms["identity"] = sol::make_object(lua, TransformQ{ osg::Quat() }); transforms["move"] = sol::overload([](const Vec3& v) { return TransformM{ osg::Matrixf::translate(v) }; }, [](float x, float y, float z) { return TransformM{ osg::Matrixf::translate(x, y, z) }; }); transforms["scale"] = sol::overload([](const Vec3& v) { return TransformM{ osg::Matrixf::scale(v) }; }, @@ -223,6 +223,22 @@ namespace LuaUtil throw std::runtime_error("This Transform is not invertible"); return res; }; + transMType["getYaw"] = [](const TransformM& m) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(m.mM); + return angles.z(); + }; + transMType["getPitch"] = [](const TransformM& m) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(m.mM); + return angles.x(); + }; + transMType["getAnglesXZ"] = [](const TransformM& m) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(m.mM); + return std::make_tuple(angles.x(), angles.z()); + }; + transMType["getAnglesZYX"] = [](const TransformM& m) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(m.mM); + return std::make_tuple(angles.z(), angles.y(), angles.x()); + }; transQType[sol::meta_function::multiplication] = sol::overload([](const TransformQ& a, const Vec3& b) { return a.mQ * b; }, @@ -243,6 +259,22 @@ namespace LuaUtil }; transQType["apply"] = [](const TransformQ& a, const Vec3& b) { return a.mQ * b; }, transQType["inverse"] = [](const TransformQ& q) { return TransformQ{ q.mQ.inverse() }; }; + transQType["getYaw"] = [](const TransformQ& q) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(q.mQ); + return angles.z(); + }; + transQType["getPitch"] = [](const TransformQ& q) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(q.mQ); + return angles.x(); + }; + transQType["getAnglesXZ"] = [](const TransformQ& q) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(q.mQ); + return std::make_tuple(angles.x(), angles.z()); + }; + transQType["getAnglesZYX"] = [](const TransformQ& q) { + osg::Vec3f angles = Misc::toEulerAnglesXZ(q.mQ); + return std::make_tuple(angles.z(), angles.y(), angles.x()); + }; // Utility functions util["clamp"] = [](double value, double from, double to) { return std::clamp(value, from, to); }; diff --git a/components/lua/utilpackage.hpp b/components/lua/utilpackage.hpp index 2b5da99346..e0ba5288ca 100644 --- a/components/lua/utilpackage.hpp +++ b/components/lua/utilpackage.hpp @@ -2,6 +2,7 @@ #define COMPONENTS_LUA_UTILPACKAGE_H #include +#include #include #include #include @@ -34,6 +35,11 @@ namespace LuaUtil return { q }; } + inline bool isTransform(const sol::object& obj) + { + return obj.is() || obj.is(); + } + sol::table initUtilPackage(lua_State*); } diff --git a/components/misc/mathutil.hpp b/components/misc/mathutil.hpp index 7c29352f44..8c9bff952c 100644 --- a/components/misc/mathutil.hpp +++ b/components/misc/mathutil.hpp @@ -2,7 +2,10 @@ #define MISC_MATHUTIL_H #include +#include +#include #include +#include namespace Misc { @@ -22,6 +25,44 @@ namespace Misc return osg::Vec2f(vec.x() * c + vec.y() * -s, vec.x() * s + vec.y() * c); } + inline osg::Vec3f toEulerAnglesXZ(osg::Vec3f forward) + { + float x = -asin(forward.z()); + float z = atan2(forward.x(), forward.y()); + return osg::Vec3f(x, 0, z); + } + inline osg::Vec3f toEulerAnglesXZ(osg::Quat quat) + { + return toEulerAnglesXZ(quat * osg::Vec3f(0, 1, 0)); + } + inline osg::Vec3f toEulerAnglesXZ(osg::Matrixf m) + { + osg::Vec3f forward(m(1, 0), m(1, 1), m(1, 2)); + forward.normalize(); + return toEulerAnglesXZ(forward); + } + + inline osg::Vec3f toEulerAnglesZYX(osg::Vec3f forward, osg::Vec3f up) + { + float y = -asin(up.x()); + float x = atan2(up.y(), up.z()); + osg::Vec3f forwardZ = (osg::Quat(x, osg::Vec3f(1, 0, 0)) * osg::Quat(y, osg::Vec3f(0, 1, 0))) * forward; + float z = atan2(forwardZ.x(), forwardZ.y()); + return osg::Vec3f(x, y, z); + } + inline osg::Vec3f toEulerAnglesZYX(osg::Quat quat) + { + return toEulerAnglesZYX(quat * osg::Vec3f(0, 1, 0), quat * osg::Vec3f(0, 0, 1)); + } + inline osg::Vec3f toEulerAnglesZYX(osg::Matrixf m) + { + osg::Vec3f forward(m(1, 0), m(1, 1), m(1, 2)); + osg::Vec3f up(m(2, 0), m(2, 1), m(2, 2)); + forward.normalize(); + up.normalize(); + return toEulerAnglesZYX(forward, up); + } + inline bool isPowerOfTwo(int x) { return ((x > 0) && ((x & (x - 1)) == 0)); diff --git a/files/data/scripts/omw/camera/first_person_auto_switch.lua b/files/data/scripts/omw/camera/first_person_auto_switch.lua index a1c0b863c9..9390b2e09d 100644 --- a/files/data/scripts/omw/camera/first_person_auto_switch.lua +++ b/files/data/scripts/omw/camera/first_person_auto_switch.lua @@ -34,7 +34,7 @@ function M.onUpdate(dt) return end if camera.getMode() == camera.MODE.ThirdPerson and camera.getThirdPersonDistance() < limitSwitch - and math.abs(util.normalizeAngle(camera.getYaw() - self.rotation.z)) < math.rad(10) then + and math.abs(util.normalizeAngle(camera.getYaw() - self.rotation:getYaw())) < math.rad(10) then if castRayBackward() <= limitSwitch then camera.setMode(camera.MODE.FirstPerson, true) forcedFirstPerson = true diff --git a/files/data/scripts/omw/camera/move360.lua b/files/data/scripts/omw/camera/move360.lua index e8bc761cfd..76ee3be9d6 100644 --- a/files/data/scripts/omw/camera/move360.lua +++ b/files/data/scripts/omw/camera/move360.lua @@ -43,11 +43,11 @@ function M.onFrame(dt) if camera.getMode() == MODE.Preview and not input.isActionPressed(input.ACTION.TogglePOV) then camera.showCrosshair(camera.getFocalPreferredOffset():length() > 5) local move = util.vector2(self.controls.sideMovement, self.controls.movement) - local yawDelta = camera.getYaw() - self.rotation.z + local yawDelta = camera.getYaw() - self.rotation:getYaw() move = move:rotate(-yawDelta) self.controls.sideMovement = move.x self.controls.movement = move.y - self.controls.pitchChange = camera.getPitch() * math.cos(yawDelta) - self.rotation.x + self.controls.pitchChange = camera.getPitch() * math.cos(yawDelta) - self.rotation:getPitch() if move:length() > 0.05 then local delta = math.atan2(move.x, move.y) local maxDelta = math.max(delta, 1) * M.turnSpeed * dt @@ -68,7 +68,7 @@ function M.onInputAction(action) end if action == input.ACTION.ZoomIn and camera.getMode() == MODE.Preview and I.Camera.getBaseThirdPersonDistance() == 30 then - self.controls.yawChange = camera.getYaw() - self.rotation.z + self.controls.yawChange = camera.getYaw() - self.rotation:getYaw() camera.setMode(MODE.FirstPerson) elseif action == input.ACTION.ZoomOut and camera.getMode() == MODE.FirstPerson then camera.setMode(MODE.Preview) diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 4fe9b082a5..d0efee7bc4 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -152,9 +152,9 @@ -- @field #boolean enabled Whether the object is enabled or disabled. Global scripts can set the value. Items in containers or inventories can't be disabled. -- @field openmw.util#Vector3 position Object position. -- @field #number scale Object scale. --- @field openmw.util#Vector3 rotation Object rotation (ZXY order). +-- @field openmw.util#Transform rotation Object rotation. -- @field openmw.util#Vector3 startingPosition The object original position --- @field openmw.util#Vector3 startingRotation The object original rotation +-- @field openmw.util#Transform startingRotation The object original rotation -- @field #string ownerRecordId NPC who owns the object (nil if missing). Global and self scripts can set the value. -- @field #string ownerFactionId Faction who owns the object (nil if missing). Global and self scripts can set the value. -- @field #number ownerFactionRank Rank required to be allowed to pick up the object. Global and self scripts can set the value. @@ -228,12 +228,12 @@ -- @param #any cellOrName A cell to define the destination worldspace; can be either #Cell, or cell name, or an empty string (empty string means the default exterior worldspace). -- If the worldspace has multiple cells (i.e. an exterior), the destination cell is calculated using `position`. -- @param openmw.util#Vector3 position New position. --- @param #TeleportOptions options (optional) Either table @{#TeleportOptions} or @{openmw.util#Vector3} rotation. +-- @param #TeleportOptions options (optional) Either table @{#TeleportOptions} or @{openmw.util#Transform} rotation. --- -- Either table with options or @{openmw.util#Vector3} rotation. -- @type TeleportOptions --- @field openmw.util#Vector3 rotation New rotation; if missing, then the current rotation is used. +-- @field openmw.util#Transform rotation New rotation; if missing, then the current rotation is used. -- @field #boolean onGround If true, adjust destination position to the ground. --- diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index ccb7c38897..47456de101 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -1333,7 +1333,7 @@ -- Destination rotation (only if a teleport door). -- @function [parent=#Door] destRotation -- @param openmw.core#GameObject object --- @return openmw.util#Vector3 +-- @return openmw.util#Transform --- -- Destination cell (only if a teleport door). @@ -1443,7 +1443,7 @@ -- Destination rotation (only if a teleport door). -- @function [parent=#ESM4Door] destRotation -- @param openmw.core#GameObject object --- @return openmw.util#Vector3 +-- @return openmw.util#Transform --- -- Destination cell (only if a teleport door). diff --git a/files/lua_api/openmw/util.lua b/files/lua_api/openmw/util.lua index b8c50bba2a..2bc2d2659a 100644 --- a/files/lua_api/openmw/util.lua +++ b/files/lua_api/openmw/util.lua @@ -501,6 +501,33 @@ -- @param #Vector3 v -- @return #Vector3 +--- +-- Get yaw angle (radians) +-- @function [parent=#Transform] getYaw +-- @param self +-- @return #number + +--- +-- Get pitch angle (radians) +-- @function [parent=#Transform] getPitch +-- @param self +-- @return #number + +--- +-- Get Euler angles for XZ rotation order (pitch and yaw; radians) +-- @function [parent=#Transform] getAnglesXZ +-- @param self +-- @return #number pitch (rotation around X axis) +-- @return #number yaw (rotation around Z axis) + +--- +-- Get Euler angles for ZYX rotation order (radians) +-- @function [parent=#Transform] getAnglesZYX +-- @param self +-- @return #number rotation around Z axis (first rotation) +-- @return #number rotation around Y axis (second rotation) +-- @return #number rotation around X axis (third rotation) + --- -- @type TRANSFORM -- @field [parent=#TRANSFORM] #Transform identity Empty transform. diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 10580ee507..110c24f1ac 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -21,10 +21,10 @@ testing.registerLocalTest('playerRotation', self.controls.run = true self.controls.movement = 0 self.controls.sideMovement = 0 - self.controls.yawChange = util.normalizeAngle(math.rad(90) - self.rotation.z) * 0.5 + self.controls.yawChange = util.normalizeAngle(math.rad(90) - self.rotation:getYaw()) * 0.5 coroutine.yield() end - testing.expectEqualWithDelta(self.rotation.z, math.rad(90), 0.05, 'Incorrect rotation') + testing.expectEqualWithDelta(self.rotation:getYaw(), math.rad(90), 0.05, 'Incorrect rotation') end) testing.registerLocalTest('playerForwardRunning', diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index 5929712a33..87df19faa0 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -36,23 +36,29 @@ local function testTimers() end local function testTeleport() - player:teleport('', util.vector3(100, 50, 0), util.vector3(0, 0, math.rad(-90))) + player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() testing.expect(player.cell.isExterior, 'teleport to exterior failed') testing.expectEqualWithDelta(player.position.x, 100, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.position.y, 50, 1, 'incorrect position after teleporting') - testing.expectEqualWithDelta(player.rotation.z, math.rad(-90), 0.05, 'incorrect rotation after teleporting') + testing.expectEqualWithDelta(player.position.z, 500, 1, 'incorrect position after teleporting') + testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(90), 0.05, 'incorrect rotation after teleporting') + + player:teleport('', player.position, {rotation=util.transform.rotateZ(math.rad(-90)), onGround=true}) + coroutine.yield() + testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'options.rotation is not working') + testing.expectLessOrEqual(player.position.z, 400, 'options.onGround is not working') player:teleport('', util.vector3(50, -100, 0)) coroutine.yield() testing.expect(player.cell.isExterior, 'teleport to exterior failed') testing.expectEqualWithDelta(player.position.x, 50, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.position.y, -100, 1, 'incorrect position after teleporting') - testing.expectEqualWithDelta(player.rotation.z, math.rad(-90), 0.05, 'teleporting changes rotation') + testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'teleporting changes rotation') end local function initPlayer() - player:teleport('', util.vector3(4096, 4096, 867.237), util.vector3(0, 0, 0)) + player:teleport('', util.vector3(4096, 4096, 867.237), util.transform.identity) coroutine.yield() end