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