This repository was archived by the owner on Aug 14, 2026. It is now read-only.
forked from TeJota1337/DramaticShapeVoxelMod
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathVRRig.lua
More file actions
274 lines (251 loc) · 12.4 KB
/
Copy pathVRRig.lua
File metadata and controls
274 lines (251 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
-- VR: the pose arithmetic -- how a headset eye becomes one of this mod's
-- cameras. Pure math on purpose: no FFI, no OpenXR types, nothing a
-- headless test cannot hold still. Everything device-shaped stays in
-- VRXR/VRGL; everything world-shaped is here.
--
-- Two ways the world can sit around a headset, and they mirror the VOXEL
-- ladder exactly:
--
-- DIORAMA every orbit rung. The map is a tabletop miniature: a point
-- of the world (the view centre) is pinned VIEW_DIST away
-- along the rung's own viewing angle (dioramaAnchor), at the
-- scale that reproduces the flat screen's framing
-- (dioramaScale) -- so at rest the model presents exactly as
-- the standard view does, and the head moves freely around it
-- -- lean in and the town grows, walk around the table and
-- see the far side of the buildings honest occlusion has been
-- hiding.
--
-- FIRST_PERSON the 1ST rung. The player's head is pinned to where the
-- headset started, at FP_SCALE, so a 16-pixel person stands
-- about 1.6 m tall and a cell is a stride. The HMD's own
-- orientation becomes FirstPerson's yaw and pitch, so movement
-- stays "push forward, go where you look" through the same
-- FreeMove the flat screen uses.
--
-- SPACES AND UNITS. OpenXR LOCAL space is metres, +Y up, -Z the way the
-- head faced at session start. World space is world PIXELS, +Y up, +Z
-- south. The two are aligned axis-for-axis -- "away from you" is north --
-- so the whole mapping is one translate-and-scale:
--
-- worldFromXr(p) = pivot + s * (p - anchor)
--
-- with `pivot` a world point, `anchor` the LOCAL-space point pinned to it,
-- and `s` the scale in px/m. An eye's camera is then
--
-- worldFromEye = T(pivot) * S(s) * T(-anchor) * T(pose.pos) * R(pose.q)
-- view = the same chain inverted piece by rigid piece
--
-- and the VIEW deliberately ends in METRES: it un-scales the world, so eye
-- space -- where the projection's near and far live -- is real-world
-- metres whatever the mode's scale. Depth precision and clip planes stay
-- sane at both 10 px/m and 128 px/m.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local Mat4 = V.require("Mat4")
local VRRig = {}
-- first person's life size: 10 px/m makes a 16 px tile a 1.6 m stride
VRRig.FP_SCALE = 10
-- How far the diorama's pivot sits from the resting head, in metres --
-- the arm's-length viewing distance the anchor and the scale below are
-- both built around.
VRRig.VIEW_DIST = 0.95
-- Where, in LOCAL metres, the diorama's pivot sits: VIEW_DIST away along
-- the RUNG'S OWN viewing angle. The flat screen's camera looks at the
-- world `a` radians off vertical; putting the pivot at (-d cos a) below
-- and (-d sin a) ahead of the resting head reproduces exactly that line
-- of sight -- step onto the 35 rung and the table presents at 35 degrees,
-- onto 75 and it rises toward eye level, easing between them as the rung
-- tween runs.
--
-- `off` is the grab-drag adjustment, in metres of LOCAL travel -- where
-- the player has carried the model to. A bare number is the height alone,
-- which is what the standard mode's one-axis drag has always sent; the
-- DIORAMA modes hand over all three (see lib/Diorama). Positive Y drags
-- the world up: the anchor is the LOCAL point pinned to the pivot, so
-- moving it moves the model with the hand rather than against it.
function VRRig.dioramaAnchor(angleRad, off)
local d = VRRig.VIEW_DIST
local ox, oy, oz = 0, 0, 0
if type(off) == "table" then
ox, oy, oz = off[1] or 0, off[2] or 0, off[3] or 0
elseif type(off) == "number" then
oy = off
end
return { ox,
-d * math.cos(angleRad or 0) + oy,
-d * math.sin(angleRad or 0) + oz }
end
-- The diorama's scale, in world px per metre: the one that makes the
-- table subtend the same field the flat screen frames. The flat camera
-- fits `vh` world pixels in a lens of focal `focal` (Voxel.FOCAL); at
-- VIEW_DIST the same framing needs vh * focal / d pixels to the metre --
-- so the resting head sees the standard view's angle AND its apparent
-- size, and the zoom rows (which change vh) keep working in VR.
function VRRig.dioramaScale(vh, focal)
return math.max(16, (vh or 288) * (focal or 1) / VRRig.VIEW_DIST)
end
-- kept as the test suite's fixed example anchor, and as the fallback for
-- an angle nobody supplied
VRRig.TABLE = { 0, -0.45, -0.75 }
-- ------- the battle mount
--
-- A staged fight snaps the headset to an OVER-THE-SHOULDER seat: the same
-- line the flat battle camera stands on (eye through focus, so the player's
-- mon is near-left and the foe far-right exactly as the flat shot frames
-- them), but pulled in to BATTLE_DIST -- the flat rig is a long lens from
-- fifteen metres back, and a headset's lens is its own eyes, so keeping the
-- distance would shrink the fight to a stage seen from the back row. 66 px
-- is the wide rig's own standing distance: six and a half metres at life
-- scale, close enough to fill the view, far enough to hold both mons in it
-- -- and short enough to stay inside the small rooms the wide rig exists
-- for.
VRRig.BATTLE_DIST = 66
-- Where the head sits for a staged fight, and which way the mapping must
-- turn so that seat FACES it. Returns the pivot (world px -- pin the XR
-- origin here at FP_SCALE) and the yaw for eyeCamera: the flat camera
-- looks along focus - eye, the resting headset looks along XR -Z (world
-- north), and the yaw is what closes that gap.
function VRRig.battleMount(eye, focus)
local dx = eye[1] - focus[1]
local dy = eye[2] - focus[2]
local dz = eye[3] - focus[3]
local len = math.sqrt(dx * dx + dy * dy + dz * dz)
if len < 1e-6 then return { eye[1], eye[2], eye[3] }, 0 end
local k = VRRig.BATTLE_DIST / len
-- Ry(yaw) sends XR forward (0,0,-1) to (-sin yaw, 0, -cos yaw); aiming
-- that along the horizontal of focus - eye solves to atan2 of eye - focus
return { focus[1] + dx * k, focus[2] + dy * k, focus[3] + dz * k },
math.atan2(dx, dz)
end
-- eye-space clip planes, in metres (see the unit note above)
VRRig.NEAR = 0.05
VRRig.FAR = 400
-- ------- one eye's camera
-- Build the placed-camera record for one eye.
--
-- pose { pos = {x,y,z} metres, quat = {x,y,z,w} } (OpenXR LOCAL)
-- fov { angleLeft, angleRight, angleUp, angleDown } signed radians
-- pivot {x,y,z} world px pinned to `anchor`
-- anchor {x,y,z} LOCAL metres (VRRig.TABLE, or 0,0,0 for first person)
-- scale world px per metre
-- yaw optional turn of the whole mapping about +Y, radians: the
-- battle mount faces the resting head at the arena with it.
-- worldFromXr(p) becomes pivot + s * Ry(yaw) * (p - anchor).
-- curveK the world curve this eye is to be drawn with (see WorldCurve);
-- omitted is 0, the curve DECLINED.
--
-- Off by default because standing inside a bent world is what first person
-- already declines on the flat screen, and the battle mount is a placed
-- shot. The DIORAMA modes are the case that wants it and asks for it: the
-- model is a thing being looked AT, so bending it into a little globe is
-- the whole point rather than a broken tabletop -- and it is what the left
-- stick's click throws (see lib/VR). Passed in rather than read here
-- because a rig has no business deciding what a row means.
--
-- Beware the shape of the answer: Voxel3D reads `camera.curve` with `or`,
-- and 0 is TRUE in Lua, so a 0 here really does pin the bend off -- which
-- is exactly why the diorama's curve did nothing until this became a
-- parameter.
--
-- Returns a table shaped for Voxel3D.camera: raw view + proj, the world
-- eye and focus (for setLook, the water's lean, the sky), fov as a
-- vertical span, and that curve.
function VRRig.eyeCamera(pose, fov, pivot, anchor, scale, yaw, curveK)
local px, py, pz = pose.pos[1], pose.pos[2], pose.pos[3]
local q = pose.quat
local R = Mat4.fromQuat(q[1], q[2], q[3], q[4])
-- view = R^T * T(-pos) * T(anchor) * Ry(-yaw) * S(1/s) * T(-pivot)
local view = Mat4.mul(Mat4.transpose(R), Mat4.translate(-px, -py, -pz))
view = Mat4.mul(view, Mat4.translate(anchor[1], anchor[2], anchor[3]))
if yaw and yaw ~= 0 then
view = Mat4.mul(view, Mat4.rotateY(-yaw))
end
view = Mat4.mul(view, Mat4.scale(1 / scale, 1 / scale, 1 / scale))
view = Mat4.mul(view, Mat4.translate(-pivot[1], -pivot[2], -pivot[3]))
local proj = Mat4.fovProjection(fov.angleLeft, fov.angleRight,
fov.angleUp, fov.angleDown,
VRRig.NEAR, VRRig.FAR)
-- The eye's RAY FAN, in world axes: the direction a canvas point
-- (u, v in 0..1, left-to-right and top-to-bottom) looks along is
-- base + u * du + v * dv. The sky reads its per-pixel TRUE elevation
-- off this (a real skybox cannot be painted from any per-frame row
-- mapping -- that is exact only at the view's own azimuth and swims
-- everywhere else). Directions only, so the mapping's scale drops out;
-- the yaw must not (the battle mount and the snap turn swing the world).
local Rw = R
if yaw and yaw ~= 0 then Rw = Mat4.mul(Mat4.rotateY(yaw), R) end
local tl, tr = math.tan(fov.angleLeft), math.tan(fov.angleRight)
local tu, td = math.tan(fov.angleUp), math.tan(fov.angleDown)
-- world columns of the head's rotation: right (X), up (Y), forward (-Z)
local rxc, ryc, rzc = Rw[1], Rw[5], Rw[9]
local uxc, uyc, uzc = Rw[2], Rw[6], Rw[10]
local fxc, fyc, fzc = -Rw[3], -Rw[7], -Rw[11]
local skyRay = {
base = { fxc + rxc * tl + uxc * tu,
fyc + ryc * tl + uyc * tu,
fzc + rzc * tl + uzc * tu },
du = { rxc * (tr - tl), ryc * (tr - tl), rzc * (tr - tl) },
dv = { uxc * (td - tu), uyc * (td - tu), uzc * (td - tu) },
}
-- the eye and its forward, in world pixels: worldFromEye applied to the
-- origin and to -Z
local ax, ay, az = px - anchor[1], py - anchor[2], pz - anchor[3]
-- R's third column is the eye's +Z axis; forward is its negation
local fx, fy, fz = -R[3], -R[7], -R[11]
if yaw and yaw ~= 0 then
local c, s = math.cos(yaw), math.sin(yaw)
ax, az = c * ax + s * az, -s * ax + c * az
fx, fz = c * fx + s * fz, -s * fx + c * fz
end
local ex = pivot[1] + scale * ax
local ey = pivot[2] + scale * ay
local ez = pivot[3] + scale * az
return {
view = view,
proj = proj,
eye = { ex, ey, ez },
focus = { ex + fx * scale, ey + fy * scale, ez + fz * scale },
fov = fov.angleUp - fov.angleDown,
curve = curveK or 0,
skyRay = skyRay,
}
end
-- The WORLD model matrix a hand-held prop stands on: worldFromXr (the
-- same mapping the eyes use -- so the prop is exactly where the hand is,
-- whatever mode the mapping is in) composed with the hand's own tracked
-- pose. A mesh authored in METRES rides it straight: the mapping's scale
-- is what turns metres into world pixels, so the prop keeps its real
-- size in the hand at the diorama's scale and at life scale alike.
--
-- model = T(pivot) * S(s) * Ry(yaw) * T(-anchor) * T(hand.pos) * R(hand.quat)
function VRRig.propMatrix(pose, pivot, anchor, scale, yaw)
local m = Mat4.translate(pivot[1], pivot[2], pivot[3])
m = Mat4.mul(m, Mat4.scale(scale, scale, scale))
if yaw and yaw ~= 0 then m = Mat4.mul(m, Mat4.rotateY(yaw)) end
m = Mat4.mul(m, Mat4.translate(-anchor[1], -anchor[2], -anchor[3]))
m = Mat4.mul(m, Mat4.translate(pose.pos[1], pose.pos[2], pose.pos[3]))
local q = pose.quat
return Mat4.mul(m, Mat4.fromQuat(q[1], q[2], q[3], q[4]))
end
-- The flat compass numbers a head orientation implies, for driving
-- FirstPerson (and through it FreeMove) from the HMD: yaw in this mod's
-- convention (0 south, pi/2 east) and pitch positive-down.
function VRRig.headYawPitch(quat)
local R = Mat4.fromQuat(quat[1], quat[2], quat[3], quat[4])
local fx, fy, fz = -R[3], -R[7], -R[11]
local flat = math.sqrt(fx * fx + fz * fz)
local yaw = flat > 1e-6 and math.atan2(fx, fz) or 0
local pitch = -math.asin(math.max(-1, math.min(1, fy)))
return yaw, pitch
end
-- The two pivots. First person pins the player's head; the diorama pins
-- the view centre at the ground plane. `gh` is the ground height under
-- the player (VoxelScene.groundAt), `eyeH` FirstPerson.EYE_HEIGHT.
function VRRig.fpPivot(pxTopLeft, pyTopLeft, gh, eyeH)
return { pxTopLeft + 8, (gh or 0) + (eyeH or 13), pyTopLeft + 8 }
end
function VRRig.dioramaPivot(cx, cy)
return { cx, 0, cy }
end
return VRRig