Files
DramaticShapeVoxelMod/lib/VoxelState.lua
T
DramaticShape 702d8049f0 initial commit
2026-07-26 16:43:53 -04:00

112 lines
4.1 KiB
Lua

-- 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 Voxel = {}
Voxel.ANGLES_DEG = { 0, 15, 35, 50, 75 }
Voxel.ANGLE_LABELS = { "OFF", "15", "35", "50", "75" }
Voxel.MAX_LEVEL = #Voxel.ANGLES_DEG - 1
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
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 ~= 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
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