-- 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