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 pathDiorama.lua
More file actions
339 lines (307 loc) · 13.9 KB
/
Copy pathDiorama.lua
File metadata and controls
339 lines (307 loc) · 13.9 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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
-- The DIORAMA modes: Kanto as a model you can pick up.
--
-- STANDARD VR presents whatever rung the player is on -- the orbit rungs
-- become a tabletop, 1ST stands you inside the world (see lib/VR.lua).
-- DIORAMA is a different promise, and it is one promise rather than a
-- ladder: the world is ALWAYS the model on the table, seen from outside,
-- and what the headset adds is that the model is a THING IN THE ROOM --
-- grab it, turn it, set it down somewhere else, decide how much of it you
-- want to be holding.
--
-- Two pieces make that read, and this file owns both.
--
-- THE VIEWPORT. Everything outside an invisible BOX centred on the view
-- is simply not drawn -- the Final Fantasy Tactics read, a square slab
-- of the world sitting in the air rather than a map running off to a
-- horizon. A square cut with a HARD edge, because a flat world is a
-- thing with sides and the sides are what say so.
--
-- V-CURVE is what changes its shape. With the bend on, the world is not
-- flat any more -- it is a little globe curling away over its own
-- horizon -- and a square cut through a globe is a lie about what is
-- being looked at. So the box becomes a BALL, and its rim becomes a
-- GRADIENT that dissolves into the sky rather than an edge that
-- guillotines it. One click of the left stick (which throws V-CURVE --
-- see lib/VR) swaps between the two readings of the same model.
--
-- A staged fight ignores both and cuts a vertical PILLAR about the
-- arena, which lifts the fight out of the map as a floating disc.
--
-- (A BASE was built under all this once -- the ground extruded a tile
-- deep, cut to the viewport's shape, wearing Mt Moon's cave floor down
-- its sides -- and it was REMOVED at the user's request. The cut ends at
-- the ground plane now; don't put a plinth back under it.)
--
-- THE GRIP. Squeeze one and the model follows that hand through the
-- room; squeeze both and it turns with them and the viewport resizes
-- to whatever you open your hands to. All of it is arithmetic on the
-- XR-to-world mapping lib/VRRig already had (an anchor, a yaw and a
-- scale), so nothing about the world's own geometry knows this is
-- happening.
--
-- DIORAMA-MR is the same mode with the background keyed pure green, for
-- a mixed-reality capture that composites the model into the room the
-- player is actually standing in.
--
-- Nothing here reaches the flat screen: every field is set by lib/VR for
-- the length of one headset frame and cleared with the session.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local Diorama = {}
-- ------- the viewport
--
-- The half-size at rest, as a fraction of the view height the flat screen
-- frames. Sized off the VIEW rather than fixed in world pixels so the zoom
-- rows keep meaning what they mean -- a zoomed-in rung frames less world
-- and gets a smaller model, exactly as it frames a smaller picture.
--
-- The BOX takes half of it, so the square is exactly the view the standard
-- rung would have shown, edge to edge. The BALL takes rather more: a ball
-- inscribed in that square holds noticeably less world (its corners are
-- the four biggest pieces of it), and the point of the V-CURVE throw is to
-- see the same model curl, not to lose a quarter of it.
Diorama.BOX_FRAC = 0.5
Diorama.BALL_FRAC = 0.62
-- How far the grips may open and close it, as a multiplier on that.
Diorama.SCALE_MIN = 0.3
Diorama.SCALE_MAX = 4
-- The rim under V-CURVE, as a fraction of the radius: where the world
-- starts fading and where it has finished. Wide enough to read as a
-- dissolve rather than an edge, narrow enough that the middle of the model
-- is solid. The BOX has no fade at all -- see fadeFor.
Diorama.FADE_FRAC = 0.16
-- The staged fight's disc: the arena's own half-length plus an apron, in
-- world pixels (a map cell is 16). The two mons stand three cells apart,
-- so this is a disc about seven cells across -- the fight, the ground it
-- is fought on, and nothing else.
Diorama.ARENA_APRON = 32
-- ------- what the live frame is
--
-- All three set by lib/VR for the length of one headset frame, and by
-- nothing else. `on` is the whole mode's gate; VoxelScene reads it once
-- per frame and every diorama-shaped thing hangs off that read.
Diorama.on = false
Diorama.keyed = false
Diorama.cull = nil -- { x, y, z, r, invFade, kind }
-- Chroma green, and PURE green deliberately: a keyer wants the one colour
-- nothing in the picture can accidentally be, and no palette this mod can
-- paint the world in reaches 0,255,0.
Diorama.KEY_COLOR = { 0, 1, 0 }
-- ------- what the grips have done to it
--
-- Kept across frames (this is where the model IS, as far as the player is
-- concerned) and cleared only when the session ends. `offset` is in LOCAL
-- metres and rides the mapping's anchor, `yaw` turns the mapping, `zoom`
-- multiplies the viewport's radius.
Diorama.offset = { 0, 0, 0 }
Diorama.yaw = 0
Diorama.zoom = 1
function Diorama.reset()
Diorama.on, Diorama.keyed, Diorama.cull = false, false, nil
Diorama.offset = { 0, 0, 0 }
Diorama.yaw, Diorama.zoom = 0, 1
Diorama.release()
end
-- ------- which way a staged fight lies on the table
--
-- The disc's bearing while a battle is up: the player's own hand-turn, with
-- the ARENA's quarter turn taken back out of it.
--
-- An arena may be laid down any of the four ways (BattleArena's `turn`), and
-- the promise that field makes everywhere else is that turning it changes the
-- GROUND under the fight and never the fight itself -- the two Pokemon land
-- on the same marks, seen the same way round. Every other camera keeps that
-- promise by construction: the flat shot and the standard VR mount are both
-- built from BattleCam's eye, which turns with the arena, so the composition
-- follows it round.
--
-- This one is not built from that eye. It is a disc of map lifted onto the
-- table, and its bearing is the arena's bearing in the WORLD -- so a fight
-- staged on a turned arena arrived on the table lying across the head that
-- was looking at it, while the same fight on an unturned one faced properly.
-- Same fight, same composition everywhere else, sideways here.
--
-- So the turn comes back out. Subtracted, matching the sign the standard
-- mount already lands on: its yaw is atan2 of (eye - focus), and rotating
-- that pair by +turn takes the bearing to (bearing - turn). One rule, two
-- seats.
--
-- The hand-turn stays on top of it, because that is the player moving the
-- model and is theirs to keep.
function Diorama.battleYaw(arena)
local turn = (arena and arena.turn) or 0
if turn == 0 then return Diorama.yaw end
local yaw = Diorama.yaw - math.rad(turn)
-- kept in (-pi, pi] like the grips leave it, so nothing downstream has to
-- care which way round it came
return (yaw + math.pi) % (2 * math.pi) - math.pi
end
-- Open a diorama frame. `mode` is VR.mode()'s answer; anything that is
-- not a diorama mode closes it.
function Diorama.begin(mode)
Diorama.on = (mode == "diorama" or mode == "diorama-mr")
Diorama.keyed = Diorama.on and mode == "diorama-mr"
if not Diorama.on then Diorama.cull = nil end
return Diorama.on
end
function Diorama.stop()
Diorama.on, Diorama.keyed, Diorama.cull = false, false, nil
end
-- What the world's background must be cleared to, or nil to leave the sky
-- alone. Only ever a colour in DIORAMA-MR, and only while a frame is open.
function Diorama.keyColor()
if not (Diorama.on and Diorama.keyed) then return nil end
return Diorama.KEY_COLOR
end
-- ------- the viewport, as the shaders take it
--
-- `kind` is the shader's own switch: 0 no cut, 1 the box, 2 the ball, 3
-- the fight's pillar. `invFade` is one over the fade band in world pixels,
-- so the rim is a single multiply out there -- and a hard edge is simply a
-- band under a pixel wide, which costs the shader no branch of its own.
Diorama.BOX = 1
Diorama.BALL = 2
Diorama.PILLAR = 3
-- The half-size the viewport stands at right now, for a view `vh` world
-- pixels tall -- the flat framing this rung would have shown -- and for
-- the shape it is currently in.
function Diorama.radius(vh, curved)
local frac = curved and Diorama.BALL_FRAC or Diorama.BOX_FRAC
return math.max(24, (vh or 288) * frac * Diorama.zoom)
end
-- Whether the world is BENT right now, which is the whole of what decides
-- the viewport's shape: a square cut suits a flat slab of map, and a
-- curved world rolling away over its own horizon wants a ball with a
-- dissolve. Asked of the row rather than remembered, so the V-CURVE the
-- stick click throws (and the "7" key, and the OPTIONS row) all reach it.
function Diorama.curved()
local ok, on = pcall(function()
return V.require("WorldCurve").active()
end)
return ok and on or false
end
-- The fade band for a cut of half-size `r`: the curve's dissolve, or a
-- hard edge (band 0) for the box.
function Diorama.fadeFor(r, curved)
if not curved then return 0 end
return math.max(1, r * Diorama.FADE_FRAC)
end
local function volume(kind, x, y, z, r, fade)
return { x = x, y = y, z = z, r = r,
-- a zero band is a hard edge: half a pixel of ramp, which is
-- one pixel of antialiasing rather than a stair
invFade = 1 / math.max(fade or 0, 0.5), kind = kind }
end
-- The viewport this frame, centred on the world point the model is pinned
-- by: the BOX ordinarily, and the BALL while the world is curved.
function Diorama.viewport(cx, cy, vh)
local curved = Diorama.curved()
local r = Diorama.radius(vh, curved)
Diorama.cull = volume(curved and Diorama.BALL or Diorama.BOX,
cx, 0, cy, r, Diorama.fadeFor(r, curved))
return Diorama.cull
end
-- The staged fight's disc: a vertical pillar about the arena's midpoint,
-- wide enough for both mons and their apron. Vertical means UNBOUNDED --
-- a tree standing on the disc keeps all of its height, which is what
-- makes the cut read as the ground having been lifted out rather than as
-- the world having been sliced through at eye level.
function Diorama.pillar(arena)
if not (arena and arena.mid) then return nil end
local mx, mz = arena.mid[1], arena.mid[2]
local r = Diorama.ARENA_APRON
if arena.player and arena.enemy then
local dx = arena.player[1] - mx
local dz = arena.player[2] - mz
r = r + math.sqrt(dx * dx + dz * dz)
end
-- Round whatever the curve is doing -- a fight is a disc, and a square
-- arena tile floating in the air is not the picture -- and ALWAYS
-- dissolved at the rim, curve or no curve. The box's hard edge is there
-- to say "this is a flat slab of map with sides"; a fight is a thing
-- lifted out of the world and hanging in the air, and a hard edge on it
-- reads as a cookie cutter rather than as a piece of ground.
Diorama.cull = volume(Diorama.PILLAR, mx, 0, mz, r,
Diorama.fadeFor(r, true))
return Diorama.cull
end
-- ------- the grips
--
-- One hand carries the model; two turn it and open the viewport. The
-- gesture is measured as a DELTA per frame rather than from where the
-- squeeze started, so letting go and taking hold again never snaps
-- anything -- the model simply stops following and starts again.
Diorama.GRIP = 0.6 -- squeezed past this counts as holding on
Diorama.SPREAD_MIN = 0.08 -- hands closer than this give no scale
local lastOne = nil -- the carrying hand's position, last frame
local lastMid = nil -- both hands' midpoint
local lastAngle = nil -- and the bearing of the line between them
local lastSpread = nil -- and its length
local function clearGrab()
lastOne, lastMid, lastAngle, lastSpread = nil, nil, nil, nil
end
Diorama.releaseGrab = clearGrab
-- Advance the grab from this frame's controller state (lib/VRXR's table:
-- gripL/gripR in 0..1, handl/handr as { pos, quat } when tracked).
-- Returns true while the model is being held.
function Diorama.gesture(ctl)
if not ctl then
clearGrab()
return false
end
local gl, gr = ctl.gripL or 0, ctl.gripR or 0
local hl = (gl > Diorama.GRIP) and ctl.handl or nil
local hr = (gr > Diorama.GRIP) and ctl.handr or nil
if hl and hr then
lastOne = nil
local lp, rp = hl.pos, hr.pos
local mid = { (lp[1] + rp[1]) / 2, (lp[2] + rp[2]) / 2,
(lp[3] + rp[3]) / 2 }
local dx, dy, dz = rp[1] - lp[1], rp[2] - lp[2], rp[3] - lp[3]
local spread = math.sqrt(dx * dx + dy * dy + dz * dz)
-- the bearing of the line between the hands, in the same convention
-- the mapping's yaw turns through (see VRRig.eyeCamera): atan2 of the
-- x component over the z one, so a hand-over-hand turn and the model's
-- turn are the same number
local angle = math.atan2(dx, dz)
if lastMid then
for i = 1, 3 do
Diorama.offset[i] = Diorama.offset[i] + (mid[i] - lastMid[i])
end
end
if lastAngle then
local d = (angle - lastAngle + math.pi) % (2 * math.pi) - math.pi
Diorama.yaw = (Diorama.yaw + d + math.pi) % (2 * math.pi) - math.pi
end
if lastSpread and lastSpread > Diorama.SPREAD_MIN
and spread > Diorama.SPREAD_MIN then
Diorama.zoom = math.max(Diorama.SCALE_MIN,
math.min(Diorama.SCALE_MAX,
Diorama.zoom * (spread / lastSpread)))
end
lastMid, lastAngle, lastSpread = mid, angle, spread
return true
end
lastMid, lastAngle, lastSpread = nil, nil, nil
local one = hl or hr
if one then
if lastOne then
for i = 1, 3 do
Diorama.offset[i] = Diorama.offset[i] + (one.pos[i] - lastOne[i])
end
end
lastOne = { one.pos[1], one.pos[2], one.pos[3] }
return true
end
lastOne = nil
return false
end
-- Nothing here owns a GPU object any more (the base did, and it is gone --
-- see the header), so this is only the grab's own hand-to-hand state: a
-- window resize or a hot reload should not leave the model following a
-- delta measured against a frame that no longer exists.
function Diorama.release()
clearGrab()
end
Diorama.invalidate = Diorama.release
return Diorama