Merge branch 'lua_quat' into 'master'

Breaking change in Lua API: change obj.rotation from Euler angles to Quaternion

See merge request OpenMW/openmw!3123
revert-6246b479
psi29a 11 months ago
commit 68415a952e

@ -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();

@ -4,6 +4,9 @@
#include <components/esm3/loadnpc.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/shapes/box.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/misc/convert.hpp>
#include <components/misc/mathutil.hpp>
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
@ -141,6 +144,28 @@ namespace MWLua
listT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get<sol::function>();
}
osg::Vec3f toEulerRotation(const sol::object& transform, bool isActor)
{
if (transform.is<LuaUtil::TransformQ>())
{
const osg::Quat& q = transform.as<LuaUtil::TransformQ>().mQ;
return isActor ? Misc::toEulerAnglesXZ(q) : Misc::toEulerAnglesZYX(q);
}
else
{
const osg::Matrixf& m = LuaUtil::cast<LuaUtil::TransformM>(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 <class ObjectT>
void addBasicBindings(sol::usertype<ObjectT>& 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<osg::Vec3f>())
rot = options.as<osg::Vec3f>();
if (LuaUtil::isTransform(options))
rot = toEulerRotation(options, ptr.getClass().isActor());
else if (options != sol::nil)
{
sol::table t = LuaUtil::cast<sol::table>(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())

@ -2,6 +2,8 @@
#include <components/esm3/loaddoor.hpp>
#include <components/esm4/loaddoor.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/misc/convert.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -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())

@ -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)");

@ -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); };

@ -2,6 +2,7 @@
#define COMPONENTS_LUA_UTILPACKAGE_H
#include <osg/Matrix>
#include <osg/Quat>
#include <osg/Vec2>
#include <osg/Vec3>
#include <osg/Vec4>
@ -34,6 +35,11 @@ namespace LuaUtil
return { q };
}
inline bool isTransform(const sol::object& obj)
{
return obj.is<TransformM>() || obj.is<TransformQ>();
}
sol::table initUtilPackage(lua_State*);
}

@ -2,7 +2,10 @@
#define MISC_MATHUTIL_H
#include <osg/Math>
#include <osg/Matrixf>
#include <osg/Quat>
#include <osg/Vec2f>
#include <osg/Vec3f>
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));

@ -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

@ -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)

@ -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.
---

@ -1454,7 +1454,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).
@ -1606,7 +1606,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).

@ -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.

@ -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',

@ -36,19 +36,25 @@ 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 testGetGMST()
@ -59,7 +65,7 @@ local function testGetGMST()
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

Loading…
Cancel
Save