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 pathVoxelState.lua
More file actions
235 lines (209 loc) · 9.13 KB
/
Copy pathVoxelState.lua
File metadata and controls
235 lines (209 loc) · 9.13 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
-- Voxel world mode: the camera angle and its tween.
--
-- Deliberately the same shape as src/render/Tilt.lua -- level 0 is off and
-- 1..3 are the same 15/35/50 degree ladder, eased the same way. What
-- differs is what the renderer does with the angle: tilt projects the flat
-- world canvas as one rigid plane, voxel mode drives a real 3D camera over
-- extruded terrain and voxel character models.
--
-- And because it is a real camera over real geometry, it has a rung tilt
-- could never have: 75 degrees, low enough to read as a diorama shot from
-- table height. Tilt's flat plane degenerates into a horizon line there,
-- but geometry only gets more of itself to show.
--
-- The LEVEL is not ours. The engine's render_pipelines plumbing owns it --
-- the options row, the hotkey, the ladder labels, persistence in
-- save.options.pipelines.voxel, and the mutual exclusion with tilt -- and
-- hands it to update() every frame. All this module keeps is the eased
-- ANGLE that level implies, because the tween is renderer state and only
-- the renderer knows what to do with a half-raised camera.
--
-- Purely presentational, like tilt and survey zoom: nothing here reaches
-- collision, movement, triggers or scripts.
local V = ...
local Perf = V.require("Perf")
local Voxel = {}
-- FULL is a PRESET, not another angle: one rung that puts the whole mode in
-- its intended state at once -- this camera, the miniature blur at full, the
-- horizon flat, the view fitted -- so a player who wants "the diorama" picks
-- it rather than assembling it from four rows. It sits directly after OFF
-- because that is the order those two get used in.
--
-- Its ANGLE is 35 degrees, the same as the rung of that name. The duplicate
-- in the table is deliberate: the ladder is a list of what each rung LOOKS
-- like, and two rungs may look the same while meaning different things.
--
-- 1ST and 3RD are the other rungs that are more than an angle: the camera
-- steps off its orbit entirely and stands with the player -- in their eyes
-- (lib/FirstPerson.lua), or on a boom behind their shoulder
-- (lib/ThirdPerson.lua) -- with free look and free movement on both. Their
-- ANGLE entries are 75 -- the orbit rung they hand over from -- because the
-- tween in and out starts from whatever the orbit shows, and the lowest rung
-- is the one a dive into a head should start from. Everything angle-derived
-- (the sky's fade, the billboard lean the blend eases away) reads that 75
-- while the free-roam rig owns the actual camera.
Voxel.ANGLES_DEG = { 0, 35, 15, 35, 50, 75, 75, 75 }
Voxel.ANGLE_LABELS = { "OFF", "FULL", "15", "35", "50", "75",
"1ST (EXPERIMENTAL)", "3RD (EXPERIMENTAL)" }
Voxel.MAX_LEVEL = #Voxel.ANGLES_DEG - 1
-- the rung FULL sits on, so nothing has to hunt for it by label
Voxel.FULL_LEVEL = 1
function Voxel.isFull(level)
return (level or Voxel.level) == Voxel.FULL_LEVEL
end
-- the rung the first-person camera sits on, likewise
Voxel.FP_LEVEL = 6
function Voxel.isFirstPerson(level)
return (level or Voxel.level) == Voxel.FP_LEVEL
end
-- and the third-person one, which is the same rig with the eye boomed off
-- the back of the head (lib/ThirdPerson.lua)
Voxel.TP_LEVEL = 7
function Voxel.isThirdPerson(level)
return (level or Voxel.level) == Voxel.TP_LEVEL
end
-- The two of them together: the rungs where the camera stands WITH the
-- player rather than orbiting the view centre, which is what decides that
-- the look inputs are read, the walk goes free and the cards turn to face
-- the eye. Everything that used to ask isFirstPerson for those asks this.
function Voxel.isFreeCam(level)
level = level or Voxel.level
return Voxel.isFirstPerson(level) or Voxel.isThirdPerson(level)
end
-- ------- what the hotkey walks
--
-- The ANGLE rungs only, with FULL left out. The key is a display-mode
-- cycler: pressing it should change the camera and nothing else, and FULL
-- reaches in and rewrites four other settings. Landing on it by accident,
-- mid-walk, would silently turn the blur to maximum and flatten the horizon
-- with no indication that a keypress had done so. FULL stays on the OPTIONS
-- row, which is where a preset that changes other rows belongs.
--
-- 1ST and 3RD are on the path: they change the camera and only the camera,
-- which is exactly what the key promises -- and the key is also the way back
-- OUT of them on a keyboard, where the mouse is captured and the OPTIONS
-- menu is a trip.
Voxel.HOTKEY_ORDER = { 0, 2, 3, 4, 5, 6, 7 } -- OFF,15,35,50,75,1ST,3RD
-- The rung a press moves to from `level`.
--
-- A level that is not on the key's path -- FULL, reached from the menu --
-- steps on from whichever rung shows the SAME camera it does. FULL is 35
-- degrees, so a press from it goes to 50 rather than back to 35, and the key
-- never appears to do nothing. Matched by ANGLE rather than by a hardcoded
-- rung, so retuning FULL moves the key's answer with it.
function Voxel.nextHotkeyLevel(level)
level = level or Voxel.level
local order = Voxel.HOTKEY_ORDER
local at = nil
for i, rung in ipairs(order) do
if rung == level then at = i break end
end
if not at then
local deg = Voxel.ANGLES_DEG[level + 1]
for i, rung in ipairs(order) do
if Voxel.ANGLES_DEG[rung + 1] == deg then at = i break end
end
end
if not at then return order[1] end
return order[at % #order + 1]
end
Voxel.level = 0
Voxel.angle = 0
Voxel.from = 0
Voxel.goal = 0
Voxel.t = 1
-- Whether the scene has terrain to show for the current map. VoxelScene
-- maintains it every frame; while the first mesh of a fresh toggle is
-- still building, update() holds the camera tween at flat -- the 2D
-- fallback IS the flat pose, so the switch waits invisibly instead of
-- tilting an empty stage (or, before builds went asynchronous, freezing
-- the whole frame for seconds).
Voxel.ready = true
-- A destination with no cached terrain must not leak the vanilla 2D map while
-- its first voxel meshes build. VoxelScene latches this state until every job
-- requested for the visible neighbourhood has landed; cached map crossings
-- stay playable while unfinished border fill appears. The pipeline gives the
-- cooperative mesher its covered budget meanwhile.
Voxel.loading = false
Voxel.loadingMap = nil
Voxel.loadingSince = 0
local clock = (love and love.timer and love.timer.getTime) or os.clock
local loadingPerfStart = nil
function Voxel.beginLoading(mapId)
if Voxel.loading and Voxel.loadingMap == mapId then return end
Voxel.loading = true
Voxel.loadingMap = mapId
Voxel.loadingSince = clock()
loadingPerfStart = Perf.now()
Voxel.ready = false
end
function Voxel.finishLoading(mapId)
if mapId and Voxel.loadingMap ~= mapId then return end
if Voxel.loading then Perf.add("VoxelLoading.total", loadingPerfStart) end
Voxel.loading = false
Voxel.loadingMap = nil
Voxel.loadingSince = 0
loadingPerfStart = nil
end
Voxel.TWEEN_TIME = 0.25
-- Camera distance as a multiple of the view height, and the matching field
-- of view. Kept equal to Tilt.FOCAL so a given angle frames the world the
-- same way in both modes; Voxel3D derives the FOV from it.
Voxel.FOCAL = 1.0
local function ease(t)
return t * t * (3 - 2 * t)
end
local function goalFor(level)
return math.rad(Voxel.ANGLES_DEG[level + 1] or 0)
end
function Voxel.setLevel(level)
level = math.floor(tonumber(level) or 0)
if level < 0 then level = 0 end
if level > Voxel.MAX_LEVEL then level = Voxel.MAX_LEVEL end
local goal = goalFor(level)
if goal <= 0 then Voxel.finishLoading() end
if goal ~= Voxel.goal or level ~= Voxel.level then
Voxel.from = Voxel.angle
Voxel.goal = goal
Voxel.t = 0
-- leaving flat: presume no terrain until the scene reports some, so
-- the tween's very first frame already waits instead of easing over
-- an empty stage (render() confirms readiness the same frame when
-- the meshes are already cached)
if Voxel.angle == 0 and goal > 0 then Voxel.ready = false end
end
Voxel.level = level
end
function Voxel.reset()
Voxel.level, Voxel.angle = 0, 0
Voxel.from, Voxel.goal, Voxel.t = 0, 0, 1
Voxel.finishLoading()
end
function Voxel.levelLabel(level)
return Voxel.ANGLE_LABELS[(level or Voxel.level) + 1] or "OFF"
end
-- The pipeline's per-frame tick. `level` is what the engine currently has
-- the mode set to, so a hotkey press or an options row lands here as a new
-- goal to ease toward rather than as a jump. dt is real frame time, so
-- fast-forward does not speed the camera up.
function Voxel.update(dt, level)
if level ~= nil and level ~= Voxel.level then Voxel.setLevel(level) end
-- hold at flat until there is geometry to tilt over; easing OUT (goal
-- below the current angle) never waits
if Voxel.angle == 0 and Voxel.goal > 0 and not Voxel.ready then
return
end
if Voxel.t < 1 then
Voxel.t = math.min(1, Voxel.t + dt / Voxel.TWEEN_TIME)
Voxel.angle = Voxel.from + (Voxel.goal - Voxel.from) * ease(Voxel.t)
else
Voxel.angle = Voxel.goal
end
end
-- True while voxel mode is on *or* still easing out -- i.e. whenever the
-- renderer must take the 3D path instead of the flat blit. Mirrors
-- Tilt.active so the two gate the same way.
function Voxel.active()
return Voxel.level > 0 or Voxel.angle > 0
end
return Voxel