mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 23:10:55 +02:00
119 lines
5.1 KiB
Lua
119 lines
5.1 KiB
Lua
-- Voxel world mode: the curved world -- the Animal Crossing horizon.
|
|
--
|
|
-- Every vertex is pushed DOWN by the square of its horizontal distance from
|
|
-- the camera's focus:
|
|
--
|
|
-- y' = y - k * ((x - cx)^2 + (z - cz)^2)
|
|
--
|
|
-- and that is the whole effect. The ground you are standing on stays flat
|
|
-- (a quadratic is nearly zero near its vertex -- at half a tile out the drop
|
|
-- is a hundredth of a pixel), and the falloff accelerates with distance, so
|
|
-- the far edge of the map bends away and rolls over a near horizon. The
|
|
-- town reads as sitting on top of a small sphere.
|
|
--
|
|
-- WHAT IT IS NOT is a fisheye. A fisheye is a LENS -- a screen-space
|
|
-- warp -- and it bends straight lines everywhere, including right in front
|
|
-- of the player, and resamples every pixel to do it. On pixel art that
|
|
-- means a blurred, crawling image and a bent HUD. This bends the WORLD
|
|
-- instead: one line in the vertex shader, straight lines near the camera
|
|
-- stay straight, and not a single pixel is resampled, so the art stays as
|
|
-- crisp as it was.
|
|
--
|
|
-- Two properties are what keep it readable rather than nauseating, and both
|
|
-- fall out of displacing along Y only:
|
|
--
|
|
-- Things stay UPRIGHT. The drop depends on where a column stands, not on
|
|
-- how tall it is, so a building's whole column moves together -- the
|
|
-- world tips away, the buildings on it do not lean.
|
|
-- Shadows and the voxel grid RIDE ALONG. Both are worked out before the
|
|
-- bend -- the shadow map in flat world space, the wireframe in model
|
|
-- space -- so the bend carries them with it exactly as if it had been
|
|
-- painted on. Nothing has to be recomputed and the light frustum never
|
|
-- moves.
|
|
--
|
|
-- The strength is scaled by the VIEW HEIGHT, so a rung looks the same at
|
|
-- every zoom: `amount` is the drop, in view-heights, one view-height out.
|
|
|
|
-- the mod namespace (see main.lua): V.require loads a sibling module
|
|
local V = ...
|
|
|
|
local ModSetting = V.require("ModSetting")
|
|
|
|
local WorldCurve = {}
|
|
|
|
WorldCurve.KEY = "curve"
|
|
WorldCurve.LABEL = "V-CURVE"
|
|
|
|
-- The ladder, calibrated against the FAR EDGE of the visible ground -- a
|
|
-- little over two view-heights out at the low camera rungs. Since the drop
|
|
-- goes as the square, that edge falls `amount * 4` view-heights while the
|
|
-- ground within a screen of the player barely moves, which is the whole
|
|
-- shape of the effect: flat where you are playing, rolling where you are
|
|
-- only looking.
|
|
--
|
|
-- 1 is a hint of roll at the frame edges, 2 is the Animal Crossing read,
|
|
-- 3 is as far as it goes before the horizon closes inside the next block
|
|
-- of the town -- which stops being a look and starts being an occlusion
|
|
-- bug, since what has rolled away is still there to walk into. (The first
|
|
-- cut ran 0.18/0.35/0.60 and every rung of it was a marble.)
|
|
--
|
|
-- 4 AND 5 ARE PAST THAT LINE ON PURPOSE, and they are for the DIORAMA:
|
|
-- once the world is a model being looked at from outside rather than a
|
|
-- place being walked around in, "the horizon has closed over the next
|
|
-- block" stops being a bug and becomes the entire effect -- the town on
|
|
-- top of a little planet.
|
|
--
|
|
-- 5 is the HALF SPHERE, and it is not eyeballed. The drop is a parabola,
|
|
-- y = k d^2 with k = amount / vh, and the parabola that osculates a sphere
|
|
-- of radius R at its pole is y = d^2 / 2R -- so k = 1 / 2R, and an amount
|
|
-- of 1.0 gives R = vh / 2. The diorama's box is cut at exactly half a view
|
|
-- height (Diorama.BOX_FRAC), so at amount 1.0 the model's own rim is that
|
|
-- sphere's EQUATOR: the ground turns 45 degrees by the edge of the cut and
|
|
-- is falling vertically a view-height out. A dome, ending where the model
|
|
-- ends. 4 is the step between it and 3, geometrically rather than
|
|
-- arithmetically -- the effect goes as the square of distance, so even
|
|
-- steps in `amount` would bunch the whole ladder at the bottom.
|
|
WorldCurve.AMOUNTS = { 0, 0.05, 0.10, 0.18, 0.42, 1.00 }
|
|
|
|
WorldCurve.setting = ModSetting.new(WorldCurve.KEY, WorldCurve.LABEL,
|
|
{ 0, 1, 2, 3, 4, 5 },
|
|
{ "OFF", "1", "2", "3", "4", "5" })
|
|
|
|
function WorldCurve.level()
|
|
return WorldCurve.setting:get() or 0
|
|
end
|
|
|
|
function WorldCurve.active()
|
|
return WorldCurve.level() > 0
|
|
end
|
|
|
|
-- The quadratic's coefficient for a view `vh` world pixels tall: the drop
|
|
-- in world pixels per squared world pixel of distance. Zero when off, which
|
|
-- is also the shader's "skip it" signal.
|
|
function WorldCurve.k(vh)
|
|
local amount = WorldCurve.AMOUNTS[WorldCurve.level() + 1] or 0
|
|
if amount <= 0 or not vh or vh <= 0 then return 0 end
|
|
return amount / vh
|
|
end
|
|
|
|
-- The same displacement the vertex shader applies, for the callers that
|
|
-- have to agree with it on the CPU -- Voxel3D.project, which anchors the
|
|
-- overworld's 2D field FX (the "!" bubble, the fishing rod, the Fly bird)
|
|
-- to their ground points. Miss this and every one of them floats off its
|
|
-- own feet the moment the ground under it bends.
|
|
function WorldCurve.drop(k, cx, cy, wx, wz)
|
|
if k <= 0 then return 0 end
|
|
local dx, dz = wx - cx, wz - cy
|
|
return (dx * dx + dz * dz) * k
|
|
end
|
|
|
|
function WorldCurve.row()
|
|
return WorldCurve.setting:row()
|
|
end
|
|
|
|
function WorldCurve.sync(value)
|
|
WorldCurve.setting:sync(value)
|
|
end
|
|
|
|
return WorldCurve
|