197 lines
6.7 KiB
Text
197 lines
6.7 KiB
Text
-- Prototype: the menu's grid background drawn live instead of played from a video file.
|
|
|
|
local CELL = 6.0 -- Spacing between grid lines, world units.
|
|
local LINE_W = 0.12 -- Width of a line running away from the camera, at the near end.
|
|
local CROSS_W = 0.18 -- Width of a cross line, at the near end.
|
|
local SPREAD = 16.0 -- Distance over which a line doubles in width. See quad().
|
|
local HALF_X = 12 -- Lines each side of the centre.
|
|
local DEPTH = 14 -- Cross lines ahead of the camera, past where the fog ends.
|
|
local SPEED = 9.0 -- Units a second the grid moves toward the camera.
|
|
local FOG_R = 8
|
|
local FOG_G = 3
|
|
local FOG_B = 20
|
|
local FOG_NEAR = 12
|
|
local FOG_FAR = 72
|
|
local SUN_R = 10.5
|
|
local SUN_Y = 7.6
|
|
local SUN_Z = -34.0
|
|
local HORIZ_W = 70.0 -- Half the width of the horizon bar; it has to reach both edges.
|
|
local HORIZ_H = 0.5 -- Its height. Bloom makes it read thicker than this.
|
|
local HORIZ_Z = -38.0 -- Just beyond the sun, so the sun stands in front of it.
|
|
local HORIZ_D = 2.2 -- Degrees below eye level the horizon is drawn at. The true one is
|
|
-- at eye level, infinitely far away, and the grid fades into the fog
|
|
-- well before it; dropping the line to where the grid actually ends
|
|
-- closes the gap that would otherwise sit under the bar.
|
|
local CAM_Y = 2.7 -- Eye height.
|
|
local CAM_Z = 6.0
|
|
|
|
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
|
local frames = 0
|
|
|
|
fontSelect(font)
|
|
sceneEnable(true)
|
|
sceneSetBackground(FOG_R, FOG_G, FOG_B, 255)
|
|
sceneSetAmbient(0, 0, 0)
|
|
sceneSetFog(FOG_R, FOG_G, FOG_B, FOG_NEAR, FOG_FAR)
|
|
sceneSetBloom(0.25, 0.9)
|
|
sceneSetTonemap(TONEMAP_NEUTRAL)
|
|
|
|
|
|
-- One flat quad in the XZ plane, four corners in order, appended to a mesh being built.
|
|
local function quad(p, i, x0, z0, x1, z1, x2, z2, x3, z3)
|
|
local base = #p / 3
|
|
|
|
table.insert(p, x0) table.insert(p, 0) table.insert(p, z0)
|
|
table.insert(p, x1) table.insert(p, 0) table.insert(p, z1)
|
|
table.insert(p, x2) table.insert(p, 0) table.insert(p, z2)
|
|
table.insert(p, x3) table.insert(p, 0) table.insert(p, z3)
|
|
for _, n in ipairs({ 1, 2, 3, 1, 3, 4 }) do
|
|
table.insert(i, base + n)
|
|
end
|
|
end
|
|
|
|
|
|
-- The height at z that lies on the horizon line, so the bar and the sun's cut land on the same row
|
|
-- of the screen even though they are at different depths.
|
|
local function horizonY(z)
|
|
return CAM_Y - (CAM_Z - z) * math.tan(math.rad(HORIZ_D))
|
|
end
|
|
|
|
|
|
-- How much wider than its near end a line is at z, so that it holds its width on screen. A line of
|
|
-- constant width in the world thins as it recedes, and once it covers less than a pixel it breaks
|
|
-- into a dotted mess that crawls as the grid moves; widening it with distance is what stops that.
|
|
local function spread(z)
|
|
return 1 + (-z) / SPREAD
|
|
end
|
|
|
|
|
|
-- The grid: lines running away from the camera, and cross lines one cell beyond each end so the
|
|
-- whole thing can be slid by one cell and wrapped without a seam.
|
|
local function gridMesh()
|
|
local p = {}
|
|
local i = {}
|
|
local far = -DEPTH * CELL
|
|
local wide = HALF_X * CELL
|
|
local hN = LINE_W / 2
|
|
local hF = hN * spread(far)
|
|
|
|
for n = -HALF_X, HALF_X do
|
|
local x = n * CELL
|
|
quad(p, i, x - hN, CELL, x + hN, CELL, x + hF, far, x - hF, far)
|
|
end
|
|
for n = -1, DEPTH do
|
|
local z = -n * CELL
|
|
local h = CROSS_W / 2 * spread(z)
|
|
quad(p, i, -wide, z + h, wide, z + h, wide, z - h, -wide, z - h)
|
|
end
|
|
|
|
return meshNew(p, nil, nil, i)
|
|
end
|
|
|
|
|
|
-- The sun: the part of a disc above yClip, as a fan from the middle of the chord. Cutting the
|
|
-- geometry is what puts the sun behind the horizon; letting the ground plane hide its lower half
|
|
-- instead only works while the ground reaches far enough, and the grid's gaps show through it.
|
|
local function sunMesh(radius, segments, yClip)
|
|
local p = { 0, yClip, 0 }
|
|
local i = {}
|
|
local a0 = math.asin(math.max(math.min(yClip / radius, 1), -1))
|
|
|
|
for n = 0, segments do
|
|
local a = a0 + (n / segments) * (math.pi - a0 * 2)
|
|
table.insert(p, math.cos(a) * radius)
|
|
table.insert(p, math.sin(a) * radius)
|
|
table.insert(p, 0)
|
|
end
|
|
for n = 1, segments do
|
|
table.insert(i, 1)
|
|
table.insert(i, n + 1)
|
|
table.insert(i, n + 2)
|
|
end
|
|
|
|
return meshNew(p, nil, nil, i)
|
|
end
|
|
|
|
|
|
-- Half the width of the sun at height y above its middle.
|
|
local function sunChord(radius, y)
|
|
return math.sqrt(math.max(radius * radius - y * y, 0))
|
|
end
|
|
|
|
|
|
local neon = materialNew()
|
|
materialSetColor(neon, 0, 0, 0)
|
|
materialSetEmissive(neon, 255, 45, 190)
|
|
materialSetDoubleSided(neon, true)
|
|
|
|
local sunMat = materialNew()
|
|
materialSetColor(sunMat, 0, 0, 0)
|
|
materialSetEmissive(sunMat, 255, 130, 55)
|
|
materialSetDoubleSided(sunMat, true)
|
|
|
|
local slotMat = materialNew()
|
|
materialSetColor(slotMat, FOG_R, FOG_G, FOG_B)
|
|
materialSetUnlit(slotMat, true)
|
|
materialSetDoubleSided(slotMat, true)
|
|
|
|
local grid = nodeNew()
|
|
nodeSetMesh(grid, gridMesh(), neon)
|
|
|
|
-- The glowing line the grid runs to. It is hotter than the grid's own magenta because the fog it
|
|
-- sits in halves it: at the grid's colour it sinks into the far rows instead of capping them.
|
|
local horizMat = materialNew()
|
|
materialSetColor(horizMat, 0, 0, 0)
|
|
materialSetEmissive(horizMat, 255, 150, 235)
|
|
materialSetDoubleSided(horizMat, true)
|
|
|
|
local horizon = nodeNew()
|
|
nodeSetMesh(horizon, meshPlane(HORIZ_W * 2, HORIZ_H), horizMat)
|
|
nodeSetRotation(horizon, 90, 0, 0)
|
|
nodeSetPosition(horizon, 0, horizonY(HORIZ_Z), HORIZ_Z)
|
|
|
|
-- The sun is cut off at the horizon line, so it stands on the bar rather than floating over it.
|
|
local sunCut = horizonY(SUN_Z) - SUN_Y
|
|
|
|
local sun = nodeNew()
|
|
nodeSetMesh(sun, sunMesh(SUN_R, 72, sunCut), sunMat)
|
|
nodeSetPosition(sun, 0, SUN_Y, SUN_Z)
|
|
|
|
-- The sun's horizontal slices, widest and thickest at the bottom. Each is cut to the chord of the
|
|
-- disc at its own height: a slice wider than the sun shows its corners, and being opaque it also
|
|
-- punches a rectangular hole in the horizon bar passing behind.
|
|
for n = 1, 6 do
|
|
local slot = nodeNew()
|
|
local h = 0.62 - (n - 1) * 0.07
|
|
local y = sunCut + 0.5 + (n - 1) * 1.3
|
|
local w = sunChord(SUN_R, math.abs(y) + h / 2)
|
|
|
|
nodeSetMesh(slot, meshPlane(w * 2, h), slotMat)
|
|
nodeSetRotation(slot, 90, 0, 0)
|
|
nodeSetPosition(slot, 0, SUN_Y + y, SUN_Z + 0.4)
|
|
end
|
|
|
|
local camera = nodeNew()
|
|
nodeSetPosition(camera, 0, CAM_Y, CAM_Z)
|
|
nodeSetRotation(camera, -2.0, 0, 0)
|
|
cameraSet(camera)
|
|
|
|
|
|
function onOverlayUpdate()
|
|
local t = singeGetTicks() / 1000.0
|
|
|
|
frames = frames + 1
|
|
nodeSetPosition(grid, 0, 0, (t * SPEED) % CELL)
|
|
|
|
overlayClear()
|
|
-- Nothing is drawn into the overlay; the menu draws its own.
|
|
|
|
if frames == 30 or frames == 48 then
|
|
singeScreenshot()
|
|
end
|
|
if frames == 60 then
|
|
singeQuit()
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|