|
@@ -0,0 +1,135 @@
|
|
|
+-- Some notes about the models used here:
|
|
|
+-- * They are MIT licensed, originally from here:
|
|
|
+-- https://github.com/immersive-web/webxr-input-profiles/tree/main/packages/assets/profiles/generic-hand
|
|
|
+-- * This particular model has all of the bones parented to a
|
|
|
+-- single root Armature node. Other approaches will need
|
|
|
+-- to be used for models that are rigged differently, like if
|
|
|
+-- the nodes are parented to each other in a hierarchy.
|
|
|
+-- * This model was rigged for the Quest. It works on other
|
|
|
+-- hand tracking systems, but differences in metacarpal bones
|
|
|
+-- might cause the wrist vertices to get scrunched up a bit.
|
|
|
+
|
|
|
+-- Rigging Process
|
|
|
+------------------
|
|
|
+-- The wrist joint/node is used as a shared reference point.
|
|
|
+-- There are 4 coordinate spaces:
|
|
|
+-- 1) world space (normal headset coordinate space)
|
|
|
+-- 2) grip space (relative to hand device pose)
|
|
|
+-- 3) wrist space (relative to wrist joint from getSkeleton)
|
|
|
+-- 4) model space (relative to the model root node)
|
|
|
+-- The steps to compute the local pose of a node in the model:
|
|
|
+-- * Start with the world-space joint from getSkeleton
|
|
|
+-- * Compute its pose relative to the wrist joint (wristFromWorld)
|
|
|
+-- * We need a pose relative to the root Armature, not relative
|
|
|
+-- to the wrist. To get this, apply the pose of the wrist node
|
|
|
+-- in the model (modelFromWrist).
|
|
|
+-- * So the final transform is (modelFromWrist * wristFromWorld)
|
|
|
+local function animateHand(device, skeleton, model, map)
|
|
|
+ model:resetNodeTransforms()
|
|
|
+
|
|
|
+ if not skeleton then return end
|
|
|
+
|
|
|
+ -- Get offset of wrist node in the model
|
|
|
+ local modelFromWrist = mat4(model:getNodeTransform(map[2]))
|
|
|
+ local wristFromModel = mat4(modelFromWrist):invert()
|
|
|
+
|
|
|
+ -- Get offset of wrist joint in the world
|
|
|
+ local x, y, z, _, angle, ax, ay, az = unpack(skeleton[2])
|
|
|
+ local worldFromWrist = mat4(x, y, z, angle, ax, ay, az)
|
|
|
+ local wristFromWorld = mat4(worldFromWrist):invert()
|
|
|
+
|
|
|
+ -- Combine the two into a matrix that will transform the
|
|
|
+ -- world-space hand joints into local node poses for the model
|
|
|
+ local modelFromWorld = modelFromWrist * wristFromWorld
|
|
|
+
|
|
|
+ -- Transform the nodes
|
|
|
+ for index, node in pairs(map) do
|
|
|
+ local x, y, z, _, angle, ax, ay, az = unpack(skeleton[index])
|
|
|
+
|
|
|
+ local jointWorld = mat4(x, y, z, angle, ax, ay, az)
|
|
|
+ local jointModel = modelFromWorld * jointWorld
|
|
|
+
|
|
|
+ model:setNodeTransform(node, jointModel)
|
|
|
+ end
|
|
|
+
|
|
|
+ -- This offsets the root node so the wrist poses line up when the
|
|
|
+ -- model is drawn at the hand pose. Instead of doing this, you
|
|
|
+ -- could just draw the model at worldFromWrist * wristFromModel
|
|
|
+ local worldFromGrip = mat4(lovr.headset.getPose(device))
|
|
|
+ local gripFromWorld = mat4(worldFromGrip):invert()
|
|
|
+ model:setNodeTransform(model:getRootNode(), gripFromWorld * worldFromWrist * wristFromModel)
|
|
|
+end
|
|
|
+
|
|
|
+function lovr.load()
|
|
|
+ hands = {}
|
|
|
+ for i, hand in ipairs({ 'left', 'right' }) do
|
|
|
+ hands[hand] = {
|
|
|
+ model = lovr.graphics.newModel(hand .. '.glb'),
|
|
|
+ skeleton = nil
|
|
|
+ }
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Maps skeleton joint index to node names in the model
|
|
|
+ map = {
|
|
|
+ [2] = 'wrist',
|
|
|
+ [3] = 'thumb-metacarpal',
|
|
|
+ [4] = 'thumb-phalanx-proximal',
|
|
|
+ [5] = 'thumb-phalanx-distal',
|
|
|
+
|
|
|
+ [7] = 'index-finger-metacarpal',
|
|
|
+ [8] = 'index-finger-phalanx-proximal',
|
|
|
+ [9] = 'index-finger-phalanx-intermediate',
|
|
|
+ [10] = 'index-finger-phalanx-distal',
|
|
|
+
|
|
|
+ [12] = 'middle-finger-metacarpal',
|
|
|
+ [13] = 'middle-finger-phalanx-proximal',
|
|
|
+ [14] = 'middle-finger-phalanx-intermediate',
|
|
|
+ [15] = 'middle-finger-phalanx-distal',
|
|
|
+
|
|
|
+ [17] = 'ring-finger-metacarpal',
|
|
|
+ [18] = 'ring-finger-phalanx-proximal',
|
|
|
+ [19] = 'ring-finger-phalanx-intermediate',
|
|
|
+ [20] = 'ring-finger-phalanx-distal',
|
|
|
+
|
|
|
+ [22] = 'pinky-finger-metacarpal',
|
|
|
+ [23] = 'pinky-finger-phalanx-proximal',
|
|
|
+ [24] = 'pinky-finger-phalanx-intermediate',
|
|
|
+ [25] = 'pinky-finger-phalanx-distal'
|
|
|
+ }
|
|
|
+end
|
|
|
+
|
|
|
+function lovr.update(dt)
|
|
|
+ for device, hand in pairs(hands) do
|
|
|
+ hand.skeleton = lovr.headset.getSkeleton(device)
|
|
|
+ animateHand(device, hand.skeleton, hand.model, map)
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+function lovr.draw(pass)
|
|
|
+ lovr.graphics.setBackgroundColor(0x202224)
|
|
|
+
|
|
|
+ if not hands.left.skeleton and not hands.right.skeleton then
|
|
|
+ pass:text('No skelly :(', 0, 1, -1, .1)
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ for device, hand in pairs(hands) do
|
|
|
+ if hand.skeleton then
|
|
|
+ -- Debug dots for joints
|
|
|
+ pass:setColor(0x8000ff)
|
|
|
+ pass:setDepthWrite(false)
|
|
|
+ for i = 1, #hand.skeleton do
|
|
|
+ local x, y, z, _, angle, ax, ay, az = unpack(hand.skeleton[i])
|
|
|
+ pass:sphere(mat4(x, y, z, angle, ax, ay, az):scale(.003))
|
|
|
+ end
|
|
|
+ pass:setDepthWrite(true)
|
|
|
+
|
|
|
+ -- Draw the (procedurally animated) wireframe hand model
|
|
|
+ local worldFromGrip = mat4(lovr.headset.getPose(device))
|
|
|
+ pass:setColor(0xffffff)
|
|
|
+ pass:setWireframe(true)
|
|
|
+ pass:draw(hand.model, worldFromGrip)
|
|
|
+ pass:setWireframe(false)
|
|
|
+ end
|
|
|
+ end
|
|
|
+end
|