Working on new menus and intros.
This commit is contained in:
parent
ffc1d54a89
commit
263409a743
12 changed files with 885 additions and 20 deletions
|
|
@ -1111,6 +1111,12 @@ Fixes
|
|||
version header, and the manual are generated by CMake. The manual is
|
||||
embedded again and extracted as Singe/Manual.pdf.
|
||||
|
||||
- The licence Singe is under and the notices its libraries and assets
|
||||
require are embedded and extracted as Singe/COPYING and
|
||||
Singe/LICENSES, the way the manual is. Nothing in the build copied
|
||||
them beside the binary, so a download carried the program without the
|
||||
terms it is offered under.
|
||||
|
||||
- One command builds Singe from a clean checkout on a Debian based
|
||||
system: ./build-all.sh <platform> <arch> installs the host packages,
|
||||
then CMake fetches zig (the compiler for every target), the Pi's
|
||||
|
|
|
|||
|
|
@ -239,6 +239,11 @@ add_custom_command(
|
|||
)
|
||||
singeEmbed(${MANUAL_DIR}/Manual.pdf ${GENERATED_DIR}/Manual_pdf.h "")
|
||||
|
||||
# The licence Singe is under and the notices its third-party libraries and assets require. Both
|
||||
# have to reach whoever gets the binary, so they travel inside it like the manual does.
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/COPYING ${GENERATED_DIR}/COPYING.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/LICENSES ${GENERATED_DIR}/LICENSES.h "")
|
||||
|
||||
# Lua libraries
|
||||
singeEmbedLua(thirdparty/luasocket/src/ftp.lua "")
|
||||
singeEmbedLua(thirdparty/luasocket/src/headers.lua "")
|
||||
|
|
|
|||
3
INSTALL
3
INSTALL
|
|
@ -2,7 +2,8 @@ SINGE 3.00
|
|||
==========
|
||||
|
||||
(For the latest version of this document, visit https://kangaroopunch.com!
|
||||
The full manual is extracted to Singe/Manual.pdf on first run.)
|
||||
The full manual is extracted to Singe/Manual.pdf on first run, beside
|
||||
Singe/COPYING and Singe/LICENSES.)
|
||||
|
||||
Welcome to Singe! SINGE Is Not a Game Emulator!
|
||||
|
||||
|
|
|
|||
|
|
@ -91,9 +91,9 @@ function loadGameAssets()
|
|||
end
|
||||
|
||||
|
||||
-- The game page takes input once the disc has reached the grid behind it.
|
||||
-- The game page takes input once the backdrop has finished the intro and settled on the grid.
|
||||
function menuActive()
|
||||
return (not toolsActive()) and (discGetFrame() >= DISC_GRID_START)
|
||||
return (not toolsActive()) and MENU_RENDER.backdropReady()
|
||||
end
|
||||
|
||||
|
||||
|
|
@ -147,17 +147,8 @@ end
|
|||
|
||||
function onOverlayUpdate()
|
||||
|
||||
-- Loop the grid. A machine too slow to show every disc frame can step over the last one, after
|
||||
-- which the engine starts the disc again from the top; once the intro has played, that is a loop.
|
||||
if discGetFrame() >= DISC_LAST_FRAME or (INTRO_DONE and discGetFrame() < DISC_GRID_START) then
|
||||
discSkipToFrame(DISC_GRID_START)
|
||||
end
|
||||
if discGetFrame() >= DISC_GRID_START then
|
||||
INTRO_DONE = true
|
||||
end
|
||||
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
-- The intro, then the grid behind the menu, and the overlay cleared ready to be drawn into.
|
||||
MENU_RENDER.backdrop()
|
||||
|
||||
-- The network never blocks the frame; it makes progress here instead.
|
||||
masterPump()
|
||||
|
|
@ -272,10 +263,12 @@ if GAME_COUNT == 0 then
|
|||
else
|
||||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||||
|
||||
-- Where the engine intro ends and the grid section begins in Singe/menuBackground.mkv, and its
|
||||
-- last frame. A renderer that draws its own grid uses the first as the end of the intro and
|
||||
-- never reaches the second.
|
||||
DISC_GRID_START = 180
|
||||
DISC_LAST_FRAME = 359
|
||||
|
||||
INTRO_DONE = false
|
||||
SHUTDOWN_FROM_PUSH = false
|
||||
|
||||
-- The service tools share this click with the audio delay tool.
|
||||
|
|
@ -310,10 +303,7 @@ else
|
|||
MENU_RENDER.layout()
|
||||
MENU_RENDER.buildList()
|
||||
|
||||
discPlay()
|
||||
if (not SHOW_INTRO) then
|
||||
discSkipToFrame(DISC_GRID_START)
|
||||
end
|
||||
MENU_RENDER.backdropBegin(SHOW_INTRO)
|
||||
|
||||
-- Prime the pump
|
||||
gameSelect(GAME_SELECTED)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@
|
|||
--
|
||||
-- begin() build what drawing needs
|
||||
-- finish() give it back
|
||||
-- backdropBegin(showIntro) start the background
|
||||
-- backdrop() advance it, and clear the overlay ready to be drawn into
|
||||
-- backdropReady() the intro is over and the menu may take input
|
||||
-- layout() place the regions, from the overlay size
|
||||
-- buildList() the whole game list is known
|
||||
-- showGame(game) this is the selection now
|
||||
|
|
@ -53,8 +56,228 @@ local MARQUEE_ASPECT_H = 75
|
|||
local VIDEO_ASPECT_W = 320
|
||||
local VIDEO_ASPECT_H = 200
|
||||
|
||||
-- The backdrop: a neon grid running to a horizon under a sliced sun, drawn by the 3D scene. The
|
||||
-- numbers are world units; the fog colour is also the background, so the grid fades into it rather
|
||||
-- than stopping at an edge.
|
||||
local GRID_CELL = 6.0 -- Spacing between grid lines.
|
||||
local GRID_LINE_W = 0.12 -- Width of a line running away from the camera, at the near end.
|
||||
local GRID_CROSS_W = 0.18 -- Width of a cross line, at the near end.
|
||||
local GRID_SPREAD = 16.0 -- Distance over which a line doubles in width. See backdropQuad().
|
||||
local GRID_HALF_X = 12 -- Lines each side of the centre.
|
||||
local GRID_DEPTH = 14 -- Cross lines ahead of the camera, past where the fog ends.
|
||||
local GRID_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 SUN_SLICES = 6 -- Horizontal bands cut out of the sun, widest at the bottom.
|
||||
local SUN_SEGMENTS = 72 -- Segments around the sun's arc.
|
||||
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 BLOOM_LEVEL = 0.25
|
||||
local BLOOM_AMOUNT = 0.9
|
||||
|
||||
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
||||
local scrollPending = false
|
||||
local backdropRoot = nil
|
||||
local backdropGrid = nil
|
||||
local introPlaying = false
|
||||
|
||||
|
||||
-- A vertex on the ground plane, appended to the positions of a mesh being built.
|
||||
local function backdropVertex(p, x, z)
|
||||
p[#p + 1] = x
|
||||
p[#p + 1] = 0
|
||||
p[#p + 1] = z
|
||||
end
|
||||
|
||||
|
||||
-- One flat quad in the XZ plane, four corners in order, appended to a mesh being built.
|
||||
local function backdropQuad(p, i, x0, z0, x1, z1, x2, z2, x3, z3)
|
||||
local base = #p / 3
|
||||
|
||||
backdropVertex(p, x0, z0)
|
||||
backdropVertex(p, x1, z1)
|
||||
backdropVertex(p, x2, z2)
|
||||
backdropVertex(p, x3, z3)
|
||||
for _, n in ipairs({ 1, 2, 3, 1, 3, 4 }) do
|
||||
i[#i + 1] = base + n
|
||||
end
|
||||
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 backdropSpread(z)
|
||||
return 1 + (-z) / GRID_SPREAD
|
||||
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 backdropHorizonY(z)
|
||||
return CAM_Y - (CAM_Z - z) * math.tan(math.rad(HORIZ_D))
|
||||
end
|
||||
|
||||
|
||||
-- Half the width of the sun at height y above its middle.
|
||||
local function backdropChord(y)
|
||||
return math.sqrt(math.max(SUN_R * SUN_R - y * y, 0))
|
||||
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 backdropGridMesh()
|
||||
local p = {}
|
||||
local i = {}
|
||||
local far = -GRID_DEPTH * GRID_CELL
|
||||
local wide = GRID_HALF_X * GRID_CELL
|
||||
local hN = GRID_LINE_W / 2
|
||||
local hF = hN * backdropSpread(far)
|
||||
|
||||
for n = -GRID_HALF_X, GRID_HALF_X do
|
||||
local x = n * GRID_CELL
|
||||
|
||||
backdropQuad(p, i, x - hN, GRID_CELL, x + hN, GRID_CELL, x + hF, far, x - hF, far)
|
||||
end
|
||||
for n = -1, GRID_DEPTH do
|
||||
local z = -n * GRID_CELL
|
||||
local h = GRID_CROSS_W / 2 * backdropSpread(z)
|
||||
|
||||
backdropQuad(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 a 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 backdropSunMesh(yClip)
|
||||
local p = { 0, yClip, 0 }
|
||||
local i = {}
|
||||
local a0 = math.asin(math.max(math.min(yClip / SUN_R, 1), -1))
|
||||
|
||||
for n = 0, SUN_SEGMENTS do
|
||||
local a = a0 + (n / SUN_SEGMENTS) * (math.pi - a0 * 2)
|
||||
|
||||
p[#p + 1] = math.cos(a) * SUN_R
|
||||
p[#p + 1] = math.sin(a) * SUN_R
|
||||
p[#p + 1] = 0
|
||||
end
|
||||
for n = 1, SUN_SEGMENTS do
|
||||
i[#i + 1] = 1
|
||||
i[#i + 1] = n + 1
|
||||
i[#i + 1] = n + 2
|
||||
end
|
||||
|
||||
return meshNew(p, nil, nil, i)
|
||||
end
|
||||
|
||||
|
||||
-- Builds the backdrop. Everything but the camera hangs off one node, so the whole thing can be
|
||||
-- hidden with a single call while the disc plays the intro in front of it.
|
||||
local function backdropBuild()
|
||||
local sunCut = backdropHorizonY(SUN_Z) - SUN_Y
|
||||
local neon
|
||||
local sunMat
|
||||
local sliceMat
|
||||
local horizMat
|
||||
local camera
|
||||
local sun
|
||||
local horizon
|
||||
|
||||
sceneEnable(true)
|
||||
sceneSetAmbient(0, 0, 0)
|
||||
sceneSetFog(FOG_R, FOG_G, FOG_B, FOG_NEAR, FOG_FAR)
|
||||
sceneSetBloom(BLOOM_LEVEL, BLOOM_AMOUNT)
|
||||
sceneSetTonemap(TONEMAP_NEUTRAL)
|
||||
|
||||
-- Transparent until the intro is over, so the disc is what shows through the 3D layer.
|
||||
sceneSetBackground(FOG_R, FOG_G, FOG_B, 0)
|
||||
|
||||
neon = materialNew()
|
||||
materialSetColor(neon, 0, 0, 0)
|
||||
materialSetEmissive(neon, 255, 45, 190)
|
||||
materialSetDoubleSided(neon, true)
|
||||
|
||||
sunMat = materialNew()
|
||||
materialSetColor(sunMat, 0, 0, 0)
|
||||
materialSetEmissive(sunMat, 255, 130, 55)
|
||||
materialSetDoubleSided(sunMat, true)
|
||||
|
||||
-- The slices are holes cut in the sun, so they are painted in the colour behind it.
|
||||
sliceMat = materialNew()
|
||||
materialSetColor(sliceMat, FOG_R, FOG_G, FOG_B)
|
||||
materialSetUnlit(sliceMat, true)
|
||||
materialSetDoubleSided(sliceMat, true)
|
||||
|
||||
-- Hotter than the grid's own magenta because the fog it sits in halves it: at the grid's
|
||||
-- colour the bar sinks into the far rows instead of capping them.
|
||||
horizMat = materialNew()
|
||||
materialSetColor(horizMat, 0, 0, 0)
|
||||
materialSetEmissive(horizMat, 255, 150, 235)
|
||||
materialSetDoubleSided(horizMat, true)
|
||||
|
||||
backdropRoot = nodeNew()
|
||||
nodeSetVisible(backdropRoot, false)
|
||||
|
||||
backdropGrid = nodeNew()
|
||||
nodeSetMesh(backdropGrid, backdropGridMesh(), neon)
|
||||
nodeSetParent(backdropGrid, backdropRoot)
|
||||
|
||||
horizon = nodeNew()
|
||||
nodeSetMesh(horizon, meshPlane(HORIZ_W * 2, HORIZ_H), horizMat)
|
||||
nodeSetRotation(horizon, 90, 0, 0)
|
||||
nodeSetPosition(horizon, 0, backdropHorizonY(HORIZ_Z), HORIZ_Z)
|
||||
nodeSetParent(horizon, backdropRoot)
|
||||
|
||||
sun = nodeNew()
|
||||
nodeSetMesh(sun, backdropSunMesh(sunCut), sunMat)
|
||||
nodeSetPosition(sun, 0, SUN_Y, SUN_Z)
|
||||
nodeSetParent(sun, backdropRoot)
|
||||
|
||||
-- Each slice is cut to the chord of the disc at its own height: one wider than the sun shows
|
||||
-- its corners, and being opaque it also punches a rectangular hole in the horizon bar behind.
|
||||
for n = 1, SUN_SLICES do
|
||||
local slice = nodeNew()
|
||||
local height = 0.62 - (n - 1) * 0.07
|
||||
local y = sunCut + 0.5 + (n - 1) * 1.3
|
||||
|
||||
nodeSetMesh(slice, meshPlane(backdropChord(math.abs(y) + height / 2) * 2, height), sliceMat)
|
||||
nodeSetRotation(slice, 90, 0, 0)
|
||||
nodeSetPosition(slice, 0, SUN_Y + y, SUN_Z + 0.4)
|
||||
nodeSetParent(slice, backdropRoot)
|
||||
end
|
||||
|
||||
camera = nodeNew()
|
||||
nodeSetPosition(camera, 0, CAM_Y, CAM_Z)
|
||||
nodeSetRotation(camera, -2.0, 0, 0)
|
||||
cameraSet(camera)
|
||||
end
|
||||
|
||||
|
||||
-- The intro is over: stop the disc and let the grid through.
|
||||
local function backdropShow()
|
||||
introPlaying = false
|
||||
discPause()
|
||||
sceneSetBackground(FOG_R, FOG_G, FOG_B, 255)
|
||||
nodeSetVisible(backdropRoot, true)
|
||||
end
|
||||
|
||||
|
||||
function menuElement(id)
|
||||
|
|
@ -189,6 +412,40 @@ MENU_RENDER.finish = function()
|
|||
guiDelete(GUI)
|
||||
GUI = nil
|
||||
end
|
||||
sceneEnable(false)
|
||||
end
|
||||
|
||||
|
||||
-- The backdrop is the disc for the intro and the 3D scene after it. The grid section of
|
||||
-- Singe/menuBackground.mkv is never reached: this renderer has the GPU device the document needs,
|
||||
-- which is the same one the scene draws with, so the grid is drawn rather than played.
|
||||
MENU_RENDER.backdropBegin = function(showIntro)
|
||||
backdropBuild()
|
||||
introPlaying = showIntro
|
||||
if showIntro then
|
||||
discPlay()
|
||||
else
|
||||
backdropShow()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdrop = function()
|
||||
if introPlaying and discGetFrame() >= DISC_GRID_START then
|
||||
backdropShow()
|
||||
end
|
||||
if not introPlaying then
|
||||
nodeSetPosition(backdropGrid, 0, 0, (singeGetTicks() / 1000.0 * GRID_SPEED) % GRID_CELL)
|
||||
end
|
||||
|
||||
-- Transparent, so the scene behind the overlay is what shows.
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdropReady = function()
|
||||
return not introPlaying
|
||||
end
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ local PAGE_ROWS = 10
|
|||
local WRAP_BREAK = " [!wb!] "
|
||||
|
||||
local font = nil
|
||||
local introDone = false
|
||||
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
||||
local marqueeX, marqueeY, marqueeW = 0, 0, 0
|
||||
local textX, textY, textW, textH = 0, 0, 0, 0
|
||||
|
|
@ -205,6 +206,39 @@ MENU_RENDER.finish = function()
|
|||
end
|
||||
|
||||
|
||||
-- The backdrop is the disc: the engine intro, then the grid section played over and over. A
|
||||
-- machine with no GPU device has nothing to draw a grid with, so this renderer plays the one in
|
||||
-- Singe/menuBackground.mkv; Singe/MenuDocument.singe draws its own instead.
|
||||
MENU_RENDER.backdropBegin = function(showIntro)
|
||||
introDone = not showIntro
|
||||
discPlay()
|
||||
if not showIntro then
|
||||
discSkipToFrame(DISC_GRID_START)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdrop = function()
|
||||
-- Loop the grid. A machine too slow to show every disc frame can step over the last one, after
|
||||
-- which the engine starts the disc again from the top; once the intro has played, that is a loop.
|
||||
if discGetFrame() >= DISC_LAST_FRAME or (introDone and discGetFrame() < DISC_GRID_START) then
|
||||
discSkipToFrame(DISC_GRID_START)
|
||||
end
|
||||
if discGetFrame() >= DISC_GRID_START then
|
||||
introDone = true
|
||||
end
|
||||
|
||||
-- Transparent, so the disc behind the overlay is what shows.
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdropReady = function()
|
||||
return discGetFrame() >= DISC_GRID_START
|
||||
end
|
||||
|
||||
|
||||
-- The cabinet fills the left; the marquee, the attract video and the details stack down the right.
|
||||
MENU_RENDER.layout = function()
|
||||
local width = overlayGetWidth()
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,8 @@ Singe/ Support files extracted by the engine
|
|||
controls.cfg.example Template for input mappings
|
||||
click.wav Used by the menu's audio delay and sound tests
|
||||
Manual.pdf This manual
|
||||
COPYING The GNU General Public License, version 3
|
||||
LICENSES Third-party notices and asset credits
|
||||
ActionMax/ One game
|
||||
games.dat Menu entries for the games in this directory
|
||||
38AmbushAlley.singe A script
|
||||
|
|
@ -14385,7 +14387,8 @@ present = singeHasGpu()
|
|||
----
|
||||
|
||||
Whether the GPU device came up. Everything drawn through it is unavailable
|
||||
without one: the 3D scene, the particles, and every GUI document. Ask this
|
||||
without one: the 3D scene, 3D particles, and every GUI document. 2D
|
||||
particles are drawn with the overlay and need no device. Ask this
|
||||
before asking for any of them, because <<guinew,guiNew>> and the scene calls
|
||||
end the script rather than answering `false` -- there is no way to try and
|
||||
recover.
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@
|
|||
#include "generated/menuBackground_mkv.h"
|
||||
#include "generated/click_wav.h"
|
||||
#include "generated/Manual_pdf.h"
|
||||
#include "generated/COPYING.h"
|
||||
#include "generated/LICENSES.h"
|
||||
|
||||
// LuaSocket
|
||||
#include "generated/ftp_lua.h"
|
||||
|
|
|
|||
|
|
@ -1818,6 +1818,8 @@ static void _unpackData(const char *exePath, bool absolute) {
|
|||
{ "menuBackground.mkv", menuBackground_mkv, menuBackground_mkv_len },
|
||||
{ "click.wav", click_wav, click_wav_len },
|
||||
{ "Manual.pdf", Manual_pdf, Manual_pdf_len },
|
||||
{ "COPYING", COPYING, COPYING_len },
|
||||
{ "LICENSES", LICENSES, LICENSES_len },
|
||||
{ "gui.rcss", gui_rcss, gui_rcss_len },
|
||||
{ "scoreBezel.rml", scoreBezel_rml, scoreBezel_rml_len },
|
||||
{ "scoreBezel.rcss", scoreBezel_rcss, scoreBezel_rcss_len },
|
||||
|
|
|
|||
197
testScripts/gridBackground.singe
Normal file
197
testScripts/gridBackground.singe
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
-- 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
|
||||
301
testScripts/menuIntro.singe
Normal file
301
testScripts/menuIntro.singe
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
-- Prototype: the menu intro. An explosion, the Singe dragon and name flying out of it toward the
|
||||
-- camera, and the tagline fading in underneath.
|
||||
--
|
||||
-- The fire is 3D emitters rather than the 2D ones the fireball prototype used, because the logo has
|
||||
-- to come out THROUGH it: 2D particles are drawn with the overlay, on top of the whole scene, so
|
||||
-- the logo would have flown out behind its own explosion.
|
||||
|
||||
local BLAST_Z = -30.0 -- Where the charge goes off.
|
||||
local REST_Z = -8.0 -- Where the logo ends up.
|
||||
local CAM_Z = 2.0
|
||||
local FLY_START = 0.30 -- Seconds: the logo leaves the blast.
|
||||
local FLY_END = 1.90 -- and has settled.
|
||||
local TAG_START = 2.10 -- The tagline begins to appear.
|
||||
local TAG_END = 3.00
|
||||
local SPINS = 2.0 -- Turns the logo makes on the way out.
|
||||
local IDLE_TURN = 7.0 -- Degrees it drifts either side once it has settled.
|
||||
local IDLE_RATE = 0.9 -- Radians a second of that drift.
|
||||
|
||||
-- The dragon carries no animation, but util/dragonModel.py builds it out of named parts -- head,
|
||||
-- neck, wingL, wingR and the rest -- and every one of them is its own node. They all sit at the
|
||||
-- model's origin though, so turning a wing node would swing the wing round the dragon's middle
|
||||
-- rather than its shoulder. hinge() gives a part the joint it is missing. Coordinates are the
|
||||
-- model's own: it faces +X, +Y is up, and the wings reach out along +/-Z.
|
||||
local WING_JOINT = { 0.30, 4.50, 0.45 }
|
||||
local NECK_JOINT = { 0.30, 2.70, 0.00 }
|
||||
local HEAD_JOINT = { 1.00, 5.20, 0.00 }
|
||||
local MOUTH = { 3.55, 5.60, 0.00 }
|
||||
local FLAP_MAX = 34.0 -- Degrees a wing rises and falls.
|
||||
local FLAP_FAST = 13.0 -- Radians a second while it is flying out of the blast.
|
||||
local FLAP_SLOW = 3.4 -- and once it has settled.
|
||||
local REAR_BACK = 3.05 -- Seconds: the head starts to rear.
|
||||
local REAR_TOP = 3.40 -- fully back.
|
||||
local REAR_STRIKE = 3.58 -- and thrown forward.
|
||||
local BREATH_START = 3.46
|
||||
local BREATH_END = 4.35
|
||||
local REAR_DONE = 4.90 -- Back to where it started.
|
||||
local REAR_ANGLE = 34.0 -- Degrees the head tips back.
|
||||
local STRIKE_ANGLE = -14.0 -- and forward through the strike.
|
||||
local DRAGON_YAW = -32.0 -- The dragon alone is turned toward the camera, so the wingbeat is
|
||||
-- seen three quarters on rather than edge on; the name stays square
|
||||
-- to the camera because it has to be read.
|
||||
local TAGLINE = "SINGE Is Not a Game Emulator"
|
||||
|
||||
local SHOT_TIMES = { 0.05, 0.25, 0.55, 0.95, 1.45, 2.00, 2.60, 3.20, 3.50, 3.70, 4.00, 4.60, 5.20 }
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 30)
|
||||
local frames = 0
|
||||
local shotAt = 1
|
||||
|
||||
overlaySetResolution(720, 480)
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 255)
|
||||
sceneSetAmbient(14, 12, 16)
|
||||
sceneSetBloom(1.2, 0.22)
|
||||
sceneSetTonemap(TONEMAP_ACES)
|
||||
|
||||
|
||||
-- Gives a part the joint the model does not have: a hinge node at the joint, with the part hung
|
||||
-- off it and pushed back by the same amount, so the geometry stays where it was and turning the
|
||||
-- hinge turns the part about the joint. Returns the hinge to turn.
|
||||
local function hinge(root, name, joint, parent, parentJoint)
|
||||
local part = nodeFind(name, root)
|
||||
local pivot = nodeNew()
|
||||
|
||||
nodeSetParent(pivot, parent or nodeGetParent(part))
|
||||
nodeSetPosition(pivot, joint[1] - parentJoint[1], joint[2] - parentJoint[2], joint[3] - parentJoint[3])
|
||||
nodeSetParent(part, pivot)
|
||||
nodeSetPosition(part, -joint[1], -joint[2], -joint[3])
|
||||
|
||||
return pivot
|
||||
end
|
||||
|
||||
|
||||
-- Eases out: fast off the mark, slow into the stop.
|
||||
local function easeOut(t)
|
||||
return 1 - (1 - t) * (1 - t) * (1 - t)
|
||||
end
|
||||
|
||||
|
||||
-- How far through a span the time is, 0 before it and 1 after.
|
||||
local function span(t, from, to)
|
||||
return math.max(math.min((t - from) / (to - from), 1), 0)
|
||||
end
|
||||
|
||||
|
||||
local blast = nodeNew()
|
||||
nodeSetPosition(blast, 0, 0, BLAST_Z)
|
||||
|
||||
local smoke = emitterNew(blast)
|
||||
emitterSetMax(smoke, 400)
|
||||
emitterSetBlend(smoke, PARTICLE_ALPHA)
|
||||
emitterSetRadius(smoke, 1.6)
|
||||
emitterSetSpread(smoke, 360)
|
||||
emitterSetSpeed(smoke, 2.5, 9.0)
|
||||
emitterSetDrag(smoke, 1.3)
|
||||
emitterSetGravity(smoke, 0, 3.2, 0)
|
||||
emitterSetLife(smoke, 1.2, 2.4)
|
||||
emitterSetSize(smoke, 3.2, 12.0)
|
||||
emitterSetSpin(smoke, -40, 40)
|
||||
emitterSetColor(smoke, 64, 46, 38, 170, 10, 9, 12, 0)
|
||||
|
||||
local core = emitterNew(blast)
|
||||
emitterSetMax(core, 500)
|
||||
emitterSetBlend(core, PARTICLE_ADD)
|
||||
emitterSetRadius(core, 1.1)
|
||||
emitterSetSpread(core, 360)
|
||||
emitterSetSpeed(core, 5.0, 20.0)
|
||||
emitterSetDrag(core, 1.7)
|
||||
emitterSetGravity(core, 0, 5.0, 0)
|
||||
emitterSetLife(core, 0.6, 1.3)
|
||||
emitterSetSize(core, 2.0, 8.2)
|
||||
emitterSetColor(core, 255, 232, 150, 255, 190, 35, 0, 0)
|
||||
|
||||
local flash = emitterNew(blast)
|
||||
emitterSetMax(flash, 20)
|
||||
emitterSetBlend(flash, PARTICLE_ADD)
|
||||
emitterSetRadius(flash, 0.4)
|
||||
emitterSetSpread(flash, 360)
|
||||
emitterSetSpeed(flash, 0, 1.5)
|
||||
emitterSetLife(flash, 0.10, 0.22)
|
||||
emitterSetSize(flash, 6.0, 15.0)
|
||||
emitterSetColor(flash, 255, 255, 235, 255, 255, 150, 40, 0)
|
||||
|
||||
local sparks = emitterNew(blast)
|
||||
emitterSetMax(sparks, 300)
|
||||
emitterSetBlend(sparks, PARTICLE_ADD)
|
||||
emitterSetRadius(sparks, 0.5)
|
||||
emitterSetSpread(sparks, 360)
|
||||
emitterSetSpeed(sparks, 12.0, 34.0)
|
||||
emitterSetDrag(sparks, 0.25)
|
||||
emitterSetGravity(sparks, 0, -15.0, 0)
|
||||
emitterSetLife(sparks, 0.5, 1.5)
|
||||
emitterSetSize(sparks, 0.30, 0.05)
|
||||
emitterSetSpin(sparks, -200, 200)
|
||||
emitterSetTrail(sparks, 8, 0.10)
|
||||
emitterSetColor(sparks, 255, 226, 160, 255, 255, 80, 15, 0)
|
||||
|
||||
-- The blast's own light, so the logo is lit by the fire it comes out of.
|
||||
local fireLight = lightNew(LIGHT_POINT)
|
||||
nodeSetParent(fireLight, blast)
|
||||
lightSetColor(fireLight, 255, 150, 60)
|
||||
lightSetRange(fireLight, 60)
|
||||
lightSetIntensity(fireLight, 0)
|
||||
|
||||
-- A key light for the logo once the fire has died, from over the camera's shoulder.
|
||||
local key = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(key, 4, 6, 10)
|
||||
nodeLookAt(key, 0, 0, REST_Z)
|
||||
lightSetIntensity(key, 1.15)
|
||||
|
||||
-- A magenta rim from behind and to the left, the colour the grid behind the menu is drawn in, so
|
||||
-- the logo is not a flat white cut-out and the intro and the menu share a palette.
|
||||
local rim = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(rim, -7, 3, REST_Z - 9)
|
||||
nodeLookAt(rim, 0, 1.5, REST_Z)
|
||||
lightSetColor(rim, 255, 70, 200)
|
||||
lightSetIntensity(rim, 2.6)
|
||||
|
||||
-- The dragon over the name, assembled the way testScripts/scene30.singe does it.
|
||||
local logo = nodeNew()
|
||||
local text = modelInstance(modelLoad("testScripts/Models/SingeText.glb"))
|
||||
nodeSetParent(text, logo)
|
||||
local dragon = modelInstance(modelLoad("testScripts/Models/DragonModel.glb"))
|
||||
nodeSetParent(dragon, logo)
|
||||
nodeSetPosition(dragon, 0, 4.85, 0)
|
||||
nodeSetRotation(dragon, 0, DRAGON_YAW, 0)
|
||||
nodeSetScale(dragon, 0.9)
|
||||
nodeSetScale(logo, 0.5)
|
||||
nodeSetVisible(logo, false)
|
||||
|
||||
-- The rig. The head hangs off the neck so that rearing the neck carries the head with it.
|
||||
local ORIGIN = { 0, 0, 0 }
|
||||
local wingLeft = hinge(dragon, "wingL", { WING_JOINT[1], WING_JOINT[2], -WING_JOINT[3] }, nil, ORIGIN)
|
||||
local wingRite = hinge(dragon, "wingR", WING_JOINT, nil, ORIGIN)
|
||||
local neck = hinge(dragon, "neck", NECK_JOINT, nil, ORIGIN)
|
||||
local head = hinge(dragon, "head", HEAD_JOINT, neck, NECK_JOINT)
|
||||
|
||||
-- The flame, on a node at the dragon's mouth so it follows the head round.
|
||||
local jet = nodeNew()
|
||||
|
||||
nodeSetParent(jet, head)
|
||||
nodeSetPosition(jet, MOUTH[1] - HEAD_JOINT[1], MOUTH[2] - HEAD_JOINT[2], MOUTH[3] - HEAD_JOINT[3])
|
||||
|
||||
-- The flame's own light, so the dragon is lit by what it is breathing.
|
||||
local breathLight = lightNew(LIGHT_POINT)
|
||||
|
||||
nodeSetParent(breathLight, jet)
|
||||
lightSetColor(breathLight, 255, 160, 60)
|
||||
lightSetRange(breathLight, 14)
|
||||
lightSetIntensity(breathLight, 0)
|
||||
|
||||
local breath = emitterNew(jet)
|
||||
emitterSetMax(breath, 400)
|
||||
emitterSetBlend(breath, PARTICLE_ADD)
|
||||
emitterSetRadius(breath, 0.05)
|
||||
emitterSetSpread(breath, 13)
|
||||
emitterSetSpeed(breath, 4.5, 9.5)
|
||||
emitterSetDrag(breath, 3.0)
|
||||
emitterSetLife(breath, 0.28, 0.55)
|
||||
emitterSetSize(breath, 0.10, 1.30)
|
||||
emitterSetRate(breath, 260)
|
||||
emitterSetColor(breath, 255, 244, 200, 255, 235, 60, 0, 0)
|
||||
|
||||
-- fontPrint has no centring, so the tagline is measured once through a throwaway sprite and drawn
|
||||
-- by hand from there; it is drawn rather than kept as a sprite because a sprite cannot be tinted,
|
||||
-- and the fade needs the foreground colour's alpha.
|
||||
local measure = fontToSprite(TAGLINE)
|
||||
local tagX = math.floor((overlayGetWidth() - spriteGetWidth(measure)) / 2)
|
||||
local tagY = math.floor(overlayGetHeight() * 0.72)
|
||||
|
||||
spriteUnload(measure)
|
||||
|
||||
local camera = nodeNew()
|
||||
nodeSetPosition(camera, 0, 1.2, CAM_Z)
|
||||
cameraSet(camera)
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = singeGetTicks() / 1000.0
|
||||
local fly = span(t, FLY_START, FLY_END)
|
||||
local tag = span(t, TAG_START, TAG_END)
|
||||
|
||||
frames = frames + 1
|
||||
if frames == 2 then
|
||||
emitterBurst(smoke, 85)
|
||||
emitterBurst(core, 110)
|
||||
emitterBurst(flash, 5)
|
||||
emitterBurst(sparks, 130)
|
||||
lightSetIntensity(fireLight, 400)
|
||||
end
|
||||
|
||||
-- The fire's light dies over the first second and a half.
|
||||
lightSetIntensity(fireLight, 400 * (1 - span(t, 0.05, 1.5)))
|
||||
|
||||
if fly > 0 then
|
||||
local e = easeOut(fly)
|
||||
-- Once it has arrived it keeps turning slowly, so the held frame is not a still picture.
|
||||
local idle = IDLE_TURN * math.sin((t - FLY_END) * IDLE_RATE) * fly
|
||||
local yaw = 360 * SPINS * (1 - e) + idle
|
||||
|
||||
nodeSetVisible(logo, true)
|
||||
nodeSetPosition(logo, 0, 0.4 * e, BLAST_Z + (REST_Z - BLAST_Z) * e)
|
||||
nodeSetRotation(logo, 0, yaw, 0)
|
||||
|
||||
-- Wings: beating hard on the way out, easing to a hover once it has arrived.
|
||||
local rate = FLAP_FAST + (FLAP_SLOW - FLAP_FAST) * e
|
||||
local flap = FLAP_MAX * math.sin(t * rate)
|
||||
|
||||
nodeSetRotation(wingLeft, -flap, 0, 0)
|
||||
nodeSetRotation(wingRite, flap, 0, 0)
|
||||
|
||||
-- The head rears back, snaps forward, and settles; the flame goes with the strike.
|
||||
local pitch = 0
|
||||
|
||||
if t < REAR_TOP then
|
||||
pitch = REAR_ANGLE * span(t, REAR_BACK, REAR_TOP)
|
||||
elseif t < REAR_STRIKE then
|
||||
local k = span(t, REAR_TOP, REAR_STRIKE)
|
||||
|
||||
pitch = REAR_ANGLE + (STRIKE_ANGLE - REAR_ANGLE) * k
|
||||
else
|
||||
pitch = STRIKE_ANGLE * (1 - easeOut(span(t, REAR_STRIKE, REAR_DONE)))
|
||||
end
|
||||
nodeSetRotation(head, 0, 0, pitch)
|
||||
nodeSetRotation(neck, 0, 0, pitch * 0.35)
|
||||
|
||||
-- The flame leaves the mouth along the dragon's own forward, which the whole logo has been
|
||||
-- turning; a 3D emitter takes its direction in world axes, so it is turned here by hand.
|
||||
local aim = math.rad(pitch)
|
||||
local spin = math.rad(yaw + DRAGON_YAW)
|
||||
|
||||
emitterSetDirection(breath, math.cos(aim) * math.cos(spin), math.sin(aim), -math.cos(aim) * math.sin(spin))
|
||||
if t >= BREATH_START and t < BREATH_END then
|
||||
emitterStart(breath)
|
||||
lightSetIntensity(breathLight, 26)
|
||||
else
|
||||
emitterStop(breath)
|
||||
lightSetIntensity(breathLight, 0)
|
||||
end
|
||||
end
|
||||
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
if tag > 0 then
|
||||
colorForeground(235, 225, 240, math.floor(255 * tag))
|
||||
fontPrint(tagX, tagY, TAGLINE)
|
||||
end
|
||||
|
||||
if shotAt <= #SHOT_TIMES and t >= SHOT_TIMES[shotAt] then
|
||||
shotAt = shotAt + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if t > 5.6 then
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return OVERLAY_UPDATED
|
||||
end
|
||||
67
util/embedGlbImages.py
Normal file
67
util/embedGlbImages.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Embeds a .glb's externally referenced images into the file itself, which is what Singe's loader
|
||||
# needs: it reads images from buffer views and will not go looking for a file beside the model.
|
||||
# Downloaded packs often ship a .glb that still points at a shared texture atlas (Kenney's arcade
|
||||
# models name "Textures/colormap.png"), and those load untextured until this is run over them.
|
||||
# packGlb.py does the same job for a .gltf with external buffers; this one leaves the buffer alone
|
||||
# and only pulls the images in.
|
||||
# Usage: python3 util/embedGlbImages.py in.glb out.glb [--base DIRECTORY]
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
mimeTypes = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'ktx2': 'image/ktx2', 'webp': 'image/webp', 'basis': 'image/basis'}
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('source')
|
||||
parser.add_argument('output')
|
||||
parser.add_argument('--base', default=None, help='where the external images live (default: beside the source)')
|
||||
args = parser.parse_args()
|
||||
base = args.base if args.base else os.path.dirname(args.source)
|
||||
|
||||
with open(args.source, 'rb') as f:
|
||||
magic, version, length = struct.unpack('<III', f.read(12))
|
||||
if magic != 0x46546C67:
|
||||
sys.exit('%s is not a .glb' % args.source)
|
||||
jsonLength, jsonType = struct.unpack('<II', f.read(8))
|
||||
gltf = json.loads(f.read(jsonLength).decode('utf-8'))
|
||||
binary = bytearray()
|
||||
rest = f.read()
|
||||
if len(rest) >= 8:
|
||||
binLength, binType = struct.unpack('<II', rest[:8])
|
||||
binary = bytearray(rest[8:8 + binLength])
|
||||
|
||||
views = gltf.setdefault('bufferViews', [])
|
||||
embedded = 0
|
||||
for image in gltf.get('images', []):
|
||||
uri = image.get('uri')
|
||||
if uri is None or uri.startswith('data:'):
|
||||
continue
|
||||
path = os.path.join(base, uri)
|
||||
if not os.path.exists(path):
|
||||
sys.exit('%s references %s, which is not beside it' % (args.source, uri))
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
while len(binary) % 4:
|
||||
binary.append(0)
|
||||
views.append({'buffer': 0, 'byteOffset': len(binary), 'byteLength': len(data)})
|
||||
binary.extend(data)
|
||||
image['bufferView'] = len(views) - 1
|
||||
image['mimeType'] = mimeTypes.get(uri.rsplit('.', 1)[-1].lower(), 'image/png')
|
||||
del image['uri']
|
||||
embedded += 1
|
||||
|
||||
gltf['buffers'] = [{'byteLength': len(binary)}]
|
||||
jsonChunk = json.dumps(gltf, separators=(',', ':')).encode('utf-8')
|
||||
while len(jsonChunk) % 4:
|
||||
jsonChunk += b' '
|
||||
while len(binary) % 4:
|
||||
binary.append(0)
|
||||
with open(args.output, 'wb') as f:
|
||||
f.write(struct.pack('<III', 0x46546C67, 2, 12 + 8 + len(jsonChunk) + 8 + len(binary)))
|
||||
f.write(struct.pack('<II', len(jsonChunk), 0x4E4F534A))
|
||||
f.write(jsonChunk)
|
||||
f.write(struct.pack('<II', len(binary), 0x004E4942))
|
||||
f.write(binary)
|
||||
print('%s: %d image(s) embedded, %d bytes' % (args.output, embedded, os.path.getsize(args.output)))
|
||||
Loading…
Add table
Reference in a new issue