OK, now that go.get_world_rotation always gives the result you expect (thanks @jhonny.goransson!), my code just works! I spent a bit cleaning things up and testing and put together a little module, in case anyone else needs it.
It will be very nice and probably quite a bit faster when a go.get_world_transform function is added, but for now this will get the job done.
local M = {}
-- NOTE: All of these functions modify the input vector, so if you want to
-- keep the original vector, input a copy of it to the transform function.
-- Convenience function to convert to vector4 and back while multiplying.
function M.v3_by_matrix(v3, m)
local v4 = m * vmath.vector4(v3.x, v3.y, v3.z, 1)
v3.x, v3.y, v3.z = v4.x, v4.y, v4.z
return v3
end
-- If you want the world-to-local matrix, just invert the result with vmath.inv().
function M.get_to_world_matrix(wp, wr, ws)
-- Make matrix4 from rotation quaternion.
local m = vmath.matrix4_from_quat(wr)
-- Multiply axes by world scale.
m.m00, m.m01, m.m02 = m.m00 * ws.x, m.m01 * ws.y, m.m02 * ws.z
m.m10, m.m11, m.m12 = m.m10 * ws.x, m.m11 * ws.y, m.m12 * ws.z
m.m20, m.m21, m.m22 = m.m20 * ws.x, m.m21 * ws.y, m.m22 * ws.z
-- Plug in world position.
m.m03, m.m13, m.m23 = wp.x, wp.y, wp.z
return m
end
function M.local_to_world(v3, wp, wr, ws)
-- Scale.
v3.x = v3.x * ws.x; v3.y = v3.y * ws.y; v3.z = v3.z * ws.z
-- Rotation.
v3 = vmath.rotate(wr, v3)
-- Translation.
v3.x = v3.x + wp.x; v3.y = v3.y + wp.y; v3.z = v3.z + wp.z
return v3
end
function M.world_to_local(v3, wp, wr, ws)
-- Translation inverse.
v3.x = v3.x - wp.x; v3.y = v3.y - wp.y; v3.z = v3.z - wp.z
-- Rotation inverse.
local r = vmath.conj(wr)
v3 = vmath.rotate(r, v3)
-- Scale inverse.
v3.x = v3.x / ws.x; v3.y = v3.y / ws.y; v3.z = v3.z / ws.z
return v3
end
return M
Example usage:
local transform = require "transform" -- The module shown above.
-- Move a child object to the world position of the mouse cursor:
local wpos = rendercam.screen_to_world_2d(action.screen_x, action.screen_y)
local wp = go.get_world_position("parent")
local wr = go.get_world_rotation("parent")
local ws = go.get_world_scale("parent")
local lpos = transform.world_to_local(vmath.vector3(wpos), wp, wr, ws)
go.set_position(lpos, "child")
Caveats:
This won’t work with skew.
In other words, if you have a parent object with non-uniform scale and a child object with rotation, transforming to and from the child’s local space won’t give a correct result. We really need to be able to get the object’s world transform directly to do this (but I don’t think it’s a very common use case).
Some interesting performance benchmarks:
I ran each of these 100,000 times and checked how long it took:
- Empty function with return value — 1 ms
- Convert vector3 to vector4 or vice versa — 25 ms
- vmath.inv — 21 ms
- vmath.ortho_inv — 20 ms
- M.v3_by_matrix — 81 ms
- Multiply vector4 * matrix — 30 ms
- M.get_to_world_matrix — 185 ms
- M.local_to_world — 98 ms
- M.world_to_local — 116 ms
So if you’re doing more than about 5 transforms with the same object at once, then it makes sense to use M.get_to_world_matrix() and multiply yourself, otherwise it’s faster to use the transform functions. These times do not include the original get_world_position, get_world_rotation, and get_world_scale calls for the module’s transform functions, just the functions themselves.
I tried a few other methods for transforming coordinates, and the ones I put here were by far the fastest (not using matrices at all).