Particle system, player controllers, and some more shadow tests.
This commit is contained in:
parent
9f5142738b
commit
33b45d0527
34 changed files with 8627 additions and 926 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,7 +17,6 @@ docs/Manual.pdf
|
|||
docs/.asciidoctor/
|
||||
.claude/settings.local.json
|
||||
**/__pycache__/
|
||||
/util/perl/
|
||||
|
||||
# Extracted by the binary when it is run from here; never source.
|
||||
/Singe/
|
||||
|
|
|
|||
65
CHANGELOG
65
CHANGELOG
|
|
@ -33,6 +33,66 @@ API Changes
|
|||
SSE4.1 on x86 (2008 and later); the engine's first C++ (one wrapper
|
||||
file and the library), statically linked, no runtime to install.
|
||||
|
||||
- Soft bodies. softNew makes cloth or a pressure body from a node's
|
||||
mesh (particles from its welded vertices, Jolt's stretch, shear and
|
||||
bend constraints, pressure for balloons) or a rope from the node to a
|
||||
point with a tube mesh the engine makes; they collide with everything
|
||||
rigid and the mesh is rewritten every frame. softPin holds particles
|
||||
in place or to nodes; stiffness, mass, damping and pressure are
|
||||
tunable. meshPlane takes subdivisions; nodeSetMaterial changes a
|
||||
material without the mesh. New calls: soft*, and the SOFT_*
|
||||
constants; see Soft bodies in the Physics chapter.
|
||||
|
||||
- Ragdolls. ragdollNew derives capsules and swing-twist joints from a
|
||||
skinned model's skeleton; ragdollActivate hands the skeleton to
|
||||
physics from its current pose and the mesh follows the bones as it
|
||||
falls; motors (ragdollSetStrength) pull it back toward that pose;
|
||||
ragdollApplyImpulse shoves a bone; ragdollDeactivate returns it to
|
||||
animation. New calls: ragdoll*; see Ragdolls in the Physics chapter.
|
||||
|
||||
- Water. bodySetWater fills a static body with water: dynamic bodies
|
||||
inside float, sink and drift by Jolt's buoyancy against its surface,
|
||||
with drag and a current (bodySetCurrent, bodySetBuoyancy per body);
|
||||
players swim in it (playerIsSwimming, playerSetSwim, three-axis
|
||||
playerMove); a buoyant hull with vehicleNew(node, VEHICLE_BOAT) gets
|
||||
a propeller and a rudder (vehicleSetThrust, vehicleSetRudder).
|
||||
|
||||
- Vehicles. A dynamic body becomes a car, a motorcycle or a tank on
|
||||
Jolt's vehicle constraint: wheels are nodes the engine poses (spin,
|
||||
steer, suspension), the script drives with throttle, steering, brake
|
||||
and hand brake, and engine, gears, suspension, steering, brakes and
|
||||
anti-roll bars are tunable. Motorcycles lean and balance with a spring
|
||||
scaled to the chassis; tanks pivot on the spot. New calls: vehicle*,
|
||||
and the VEHICLE_* constants; see Vehicles in the Physics chapter.
|
||||
|
||||
- Point lights inside geometry. A bulb in a closed room, under a shade
|
||||
or inside a cabinet now shadows correctly: cube faces render depth
|
||||
two-sided so closed meshes cast from inside, every shadow tap picks its
|
||||
own cube face so the seams along the cube edges are gone, the depth
|
||||
range fits the casters round the light instead of the scene centre,
|
||||
the bias grows with distance and grazing angle, and each face draws
|
||||
only what it can see. nodeSetShadow excuses a mesh from casting.
|
||||
|
||||
- Character controller. playerNew puts Jolt's CharacterVirtual on a
|
||||
node, as a capsule or any convex bodyNew shape: it walks where
|
||||
playerMove says, climbs steps, slides along walls, refuses steep
|
||||
slopes, falls, jumps from the ground, rides kinematic platforms and
|
||||
shoves light dynamic bodies; onCollision and onTrigger report it like
|
||||
a body. Works in 2D worlds as a platformer character, with tolerances
|
||||
scaled to the shape. New calls: player*; see Players in the Physics
|
||||
chapter.
|
||||
|
||||
- Particles. Emitters spawn, move and age particles from a recipe (rate,
|
||||
life, speed, direction and spread, gravity, drag, size, colour and spin
|
||||
over life, a birth radius, a pool cap) in the overlay and in the 3D
|
||||
scene alike. A 2D emitter draws on the GPU above or beneath the
|
||||
overlay when the script asks (emitterDraw); a 3D emitter sits on a node
|
||||
and draws camera-facing billboards after every mesh, depth tested, in
|
||||
world units. Any loaded sprite is the picture (a random frame per
|
||||
particle from an animated one); without one, a soft disc. Normal or
|
||||
additive blending. New calls: emitter*, and the PARTICLE_* constants;
|
||||
see the Particles chapter.
|
||||
|
||||
- 3D scenes. A game can draw a 3D scene between the disc video and the
|
||||
overlay: primitive meshes and script-built geometry, materials with
|
||||
colour, textures from sprites, the disc or a loaded video, metallic and
|
||||
|
|
@ -42,8 +102,9 @@ API Changes
|
|||
targets, placed any number of times. Everything is a node in one tree. New calls:
|
||||
scene*, node*, mesh*, material*, light*, camera*, model*, animation*,
|
||||
and the LIGHT_* constants; see the 3D Scenes chapter of the manual.
|
||||
Needs a GPU with Vulkan, Direct3D 12 or Metal (Raspberry Pi 4 or
|
||||
later); 2D games run as before without one. The renderer now runs on
|
||||
Needs a GPU with Vulkan, Direct3D 12 or Metal; the Raspberry Pi 4 is
|
||||
the minimum Pi (the Pi 3 has no Vulkan driver). 2D games run as
|
||||
before without one. The renderer now runs on
|
||||
SDL's GPU device where there is one.
|
||||
|
||||
- A game can be one file: singe --pack DIRECTORY GAME.game writes the game's
|
||||
|
|
|
|||
|
|
@ -273,6 +273,8 @@ set(SINGE_SOURCE
|
|||
src/main.h
|
||||
src/pack.c
|
||||
src/pack.h
|
||||
src/particles.c
|
||||
src/particles.h
|
||||
src/physics.h
|
||||
src/physicsJolt.cpp
|
||||
src/model.c
|
||||
|
|
|
|||
3
INSTALL
3
INSTALL
|
|
@ -106,7 +106,8 @@ release (lib/libdxcompiler.so, lib/libdxil.so, include/dxc/dxcapi.h) at
|
|||
that path yourself, or build it from source there.
|
||||
|
||||
The Raspberry Pi build (64-bit Raspberry Pi OS, glibc 2.31 or newer)
|
||||
also uses zig. The platform headers and libraries it links against are
|
||||
runs on any 64-bit Pi; 3D games need a Pi 4 or later, the Pi 3 has no
|
||||
Vulkan driver and plays 2D games only. The build also uses zig. The platform headers and libraries it links against are
|
||||
Debian bookworm arm64 packages listed in cmake/zig/piPackages.cmake,
|
||||
fetched from snapshot.debian.org and unpacked with dpkg-deb into
|
||||
.builddir/toolchains/sysroot-aarch64-linux-gnu on first use:
|
||||
|
|
|
|||
2
LICENSES
2
LICENSES
|
|
@ -46,5 +46,3 @@ Fonts
|
|||
-----
|
||||
|
||||
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
|
||||
BreatheFire (design-time only: text layer in singeLogo.xcf and indexing.xcf;
|
||||
the font file is not shipped and its license is not recorded)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# cmake -DSHADERCROSS=<tool> -DSOURCE=<scene.hlsl> -DOUTPUT=<sceneShaders.h> -P shaderHeader.cmake
|
||||
# so the header is generated into the build tree like the icon and the other embedded files.
|
||||
|
||||
set(entries vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment depthMain:fragment)
|
||||
set(entries vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment depthMain:fragment particleVertex:vertex particleFragment:fragment)
|
||||
set(formats SPIRV DXIL MSL)
|
||||
get_filename_component(sourceDir ${SOURCE} DIRECTORY)
|
||||
get_filename_component(outputDir ${OUTPUT} DIRECTORY)
|
||||
|
|
|
|||
1429
docs/Manual.adoc
1429
docs/Manual.adoc
File diff suppressed because it is too large
Load diff
|
|
@ -1,476 +0,0 @@
|
|||
dofile("Singe/Framework.singe")
|
||||
|
||||
stateStartup = 0
|
||||
stateSetup = 1
|
||||
stateTitle = 2
|
||||
stateMenu = 3
|
||||
stateIntro = 4
|
||||
statePlaying = 5
|
||||
stateGameOver = 6
|
||||
|
||||
gameStandard = 0
|
||||
gameLimited = 1
|
||||
|
||||
pixelLow = 0
|
||||
pixelHigh = 1
|
||||
pixelUnknown = 2
|
||||
|
||||
sprLightOn = spriteLoad(DIR .. "sprite_LightOn.png")
|
||||
sprLightOff = spriteLoad(DIR .. "sprite_LightOff.png")
|
||||
sprActionMax = spriteLoad(DIR .. "sprite_ActionMax.png")
|
||||
sprCrosshair = spriteLoad(DIR .. "sprite_Crosshair.png")
|
||||
sprBullet = spriteLoad(DIR .. "sprite_Bullet.png")
|
||||
sprBoxArt = spriteLoad(DIR .. "sprite_" .. gameID .. ".png")
|
||||
|
||||
sndActionMax = soundLoad(DIR .. "sound_ActionMax.wav")
|
||||
sndSteadyAim = soundLoad(DIR .. "sound_ASteadyAimIsCritical.wav")
|
||||
sndGetReady = soundLoad(DIR .. "sound_GetReadyForAction.wav")
|
||||
sndGunShot = soundLoad(DIR .. "sound_Gunshot.wav")
|
||||
sndGoodHit = soundLoad(DIR .. "sound_GoodHit.wav")
|
||||
sndBadHit = soundLoad(DIR .. "sound_BadHit.wav")
|
||||
sndGameOver = soundLoad(DIR .. "sound_GameOver.wav")
|
||||
|
||||
mouseX = 0
|
||||
mouseY = 0
|
||||
|
||||
halfWidth = 0
|
||||
|
||||
crosshairCenterX = spriteGetWidth(sprCrosshair) / 2
|
||||
crosshairCenterY = spriteGetHeight(sprCrosshair) / 2
|
||||
|
||||
currentState = stateStartup
|
||||
gameMode = gameStandard
|
||||
|
||||
shotsFired = 0 -- Total Fired
|
||||
shotsGood = 0 -- Hit our target
|
||||
shotsBad = 0 -- Hit our friends
|
||||
|
||||
scoreDisplay = 0 -- How long the score is yet to be displayed
|
||||
scoreTimer = 4 -- How long to display the score + 1
|
||||
|
||||
lightDisplay = 0 -- How long the light is yet to be displayed
|
||||
lightTimer = 2 -- How long to display the light + 1
|
||||
|
||||
triggerPulled = 0 -- This isn't quite a boolean. It counts down so we can get multiple pixel samples across frames.
|
||||
|
||||
ammoLeft = 0 -- Number of misses remaining in limited ammo mode
|
||||
ammoCount = 5 -- Number of misses allowed in limited ammo mode
|
||||
|
||||
gunLastState = pixelUnknown
|
||||
sensorLastState = pixelUnknown
|
||||
|
||||
thisSeconds = 0
|
||||
lastSeconds = 0
|
||||
heartbeat = false
|
||||
|
||||
fntBlueStone20 = fontLoad(DIR .. "font_BlueStone.ttf", 20)
|
||||
fntChemRea16 = fontLoad(DIR .. "font_chemrea.ttf", 16)
|
||||
fntChemRea32 = fontLoad(DIR .. "font_chemrea.ttf", 32)
|
||||
fntChemRea48 = fontLoad(DIR .. "font_chemrea.ttf", 48)
|
||||
fntLEDReal32 = fontLoad(DIR .. "font_LED_Real.ttf", 32)
|
||||
|
||||
colorBackground(0, 0, 0, 0)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
colorForeground(255, 255, 0)
|
||||
fontSelect(fntBlueStone20)
|
||||
sprPullToStart = fontToSprite("Pull Trigger to Start!")
|
||||
sprGetReady = fontToSprite("Get Ready!")
|
||||
|
||||
colorForeground(255, 255, 255)
|
||||
fontSelect(fntChemRea16)
|
||||
sprLastGame = fontToSprite("LAST GAME SCORE")
|
||||
|
||||
colorForeground(255, 255, 0)
|
||||
fontSelect(fntChemRea32)
|
||||
sprSelectGameType = fontToSprite("Select Game Type")
|
||||
sprStandard1 = fontToSprite("Standard")
|
||||
sprStandard2 = fontToSprite("Game")
|
||||
sprLimited1 = fontToSprite("Limited")
|
||||
sprLimited2 = fontToSprite("Ammo")
|
||||
|
||||
colorForeground(255, 0, 0)
|
||||
fontSelect(fntChemRea48)
|
||||
sprGameOver = fontToSprite("GAME OVER")
|
||||
|
||||
discSearch(backgroundFrame)
|
||||
|
||||
|
||||
|
||||
function getPixelState(intX, intY)
|
||||
|
||||
r1, g1, b1 = vldpGetPixel(intX, intY)
|
||||
|
||||
-- Determine pixel state
|
||||
if ((255 - r1 < highThreshold) and (255 - g1 < highThreshold) and (255 - b1 < highThreshold)) then
|
||||
pixelState = pixelHigh
|
||||
elseif ((r1 < lowThreshold) and (g1 < lowThreshold) and (b1 < lowThreshold)) then
|
||||
pixelState = pixelLow
|
||||
else
|
||||
pixelState = pixelUnknown
|
||||
end
|
||||
|
||||
return pixelState
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onInputPressed(intWhat)
|
||||
|
||||
--dr, dg, db = vldpGetPixel(mouseX, mouseY)
|
||||
--debugPrint("Current Frame: " .. discGetFrame() .. " X: " .. mouseX .. " Y: " .. mouseY .. " R: " .. dr .. " G: " .. dg .. " B: " .. db)
|
||||
|
||||
-- They fired!
|
||||
if (intWhat == SWITCH_BUTTON3) then
|
||||
|
||||
if (currentState == stateTitle) then
|
||||
|
||||
discSearch(lengthIntro + lengthGame + 2)
|
||||
discPlay()
|
||||
currentState = stateMenu
|
||||
|
||||
elseif (currentState == stateMenu) then
|
||||
|
||||
if (mouseX < halfWidth) then
|
||||
gameMode = gameStandard
|
||||
else
|
||||
ammoLeft = ammoCount
|
||||
gameMode = gameLimited
|
||||
end
|
||||
|
||||
shotsFired = 0
|
||||
shotsGood = 0
|
||||
shotsBad = 0
|
||||
colorForeground(255, 0, 0)
|
||||
fontSelect(fntLEDReal32)
|
||||
discSearch(1)
|
||||
discPlay()
|
||||
soundPlay(sndGetReady)
|
||||
currentState = stateIntro
|
||||
|
||||
elseif (currentState == statePlaying) then
|
||||
|
||||
-- Make gunshot noise
|
||||
soundPlay(sndGunShot)
|
||||
|
||||
-- We want to sample across two frames
|
||||
triggerPulled = 2
|
||||
|
||||
shotsFired = shotsFired + 1
|
||||
|
||||
-- No matter what, we subtract ammo. On good hits, we add it back to make it look like nothing changed.
|
||||
if (gameMode == gameLimited) then
|
||||
ammoLeft = ammoLeft - 1
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onMouseMoved(intX, intY, intXrel, intYrel)
|
||||
|
||||
-- Remember the mouse location for use later.
|
||||
mouseX = intX
|
||||
mouseY = intY
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
|
||||
overlayClear()
|
||||
|
||||
currentFrame = discGetFrame()
|
||||
|
||||
-- Give us a 1 second heartbeat for overlay element timing
|
||||
thisSeconds = os.time(os.date('*t'))
|
||||
if (thisSeconds ~= lastSeconds) then
|
||||
heartbeat = not heartbeat
|
||||
lastSeconds = thisSeconds
|
||||
-- Tick off floating score display
|
||||
if (scoreDisplay > 0) then
|
||||
scoreDisplay = scoreDisplay - 1
|
||||
end
|
||||
-- Tick off light timer
|
||||
if (lightDisplay > 0) then
|
||||
lightDisplay = lightDisplay - 1
|
||||
end
|
||||
end
|
||||
|
||||
if (currentState == stateStartup) then
|
||||
|
||||
-- We run this state to cause the overlay to update once before we use the dimensions to build the menu
|
||||
overlayClear()
|
||||
currentState = stateSetup
|
||||
|
||||
elseif (currentState == stateSetup) then
|
||||
|
||||
-- We do all the math to draw the menu here, one time.
|
||||
halfWidth = (overlayGetWidth() / 2)
|
||||
|
||||
logoTop = 5
|
||||
logoLeft = halfWidth - (spriteGetWidth(sprActionMax) / 2)
|
||||
logoHeight = logoTop + spriteGetHeight(sprActionMax)
|
||||
|
||||
pullToStartHeight = spriteGetHeight(sprPullToStart)
|
||||
pullToStartTop = overlayGetHeight() - pullToStartHeight
|
||||
pullToStartLeft = halfWidth - (spriteGetWidth(sprPullToStart) / 2)
|
||||
|
||||
getReadyHeight = spriteGetHeight(sprGetReady)
|
||||
getReadyTop = overlayGetHeight() - getReadyHeight
|
||||
getReadyLeft = halfWidth - (spriteGetWidth(sprGetReady) / 2)
|
||||
|
||||
boxLeft = (halfWidth / 2) - spriteGetWidth(sprBoxArt) / 2 + halfWidth
|
||||
boxTop = (overlayGetHeight() - (logoHeight + pullToStartHeight)) / 2 - spriteGetHeight(sprBoxArt) / 2 + logoHeight
|
||||
|
||||
lastGameLeft = (halfWidth / 2) - spriteGetWidth(sprLastGame) / 2
|
||||
lastGameTop = logoHeight + logoTop
|
||||
|
||||
scoreHeight = 13 -- Point size + 1 instead of spriteGetHeight(sprLastGame)
|
||||
scoreTop = lastGameTop + scoreHeight + 10
|
||||
scoreLeft = lastGameLeft
|
||||
|
||||
lightLeft = overlayGetWidth() - spriteGetWidth(sprLightOff)
|
||||
lightTop = overlayGetHeight() - spriteGetHeight(sprLightOff)
|
||||
|
||||
bulletWidth = spriteGetWidth(sprBullet)
|
||||
|
||||
selectGameTypeLeft = halfWidth - spriteGetWidth(sprSelectGameType) / 2
|
||||
selectGameTypeTop = 25
|
||||
|
||||
standard1Left = halfWidth / 2 - spriteGetWidth(sprStandard1) / 2
|
||||
standard2Left = halfWidth / 2 - spriteGetWidth(sprStandard2) / 2
|
||||
|
||||
limited1Left = halfWidth + (halfWidth / 2) - spriteGetWidth(sprLimited1) / 2
|
||||
limited2Left = halfWidth + (halfWidth / 2) - spriteGetWidth(sprLimited2) / 2
|
||||
|
||||
standardLimited1Top = (overlayGetHeight() / 2) - spriteGetHeight(sprStandard1) + 2
|
||||
standardLimited2Top = (overlayGetHeight() / 2) + 2
|
||||
|
||||
gameOverLeft = halfWidth - spriteGetWidth(sprGameOver) / 2
|
||||
gameOverTop = (overlayGetHeight() / 2) - spriteGetHeight(sprGameOver)
|
||||
|
||||
colorForeground(200, 200, 200)
|
||||
fontSelect(fntChemRea16)
|
||||
|
||||
hndActionMaxSound = soundPlay(sndActionMax)
|
||||
|
||||
currentState = stateTitle
|
||||
|
||||
elseif (currentState == stateTitle) then
|
||||
|
||||
spriteDraw(sprActionMax, logoLeft, logoTop)
|
||||
spriteDraw(sprBoxArt, boxLeft, boxTop)
|
||||
spriteDraw(sprLastGame, lastGameLeft, lastGameTop)
|
||||
y = scoreTop
|
||||
fontPrint(scoreLeft, y, " Shots Fired: " .. shotsFired)
|
||||
y = y + scoreHeight
|
||||
fontPrint(scoreLeft, y, " Good Hits: " .. shotsGood)
|
||||
y = y + scoreHeight
|
||||
fontPrint(scoreLeft, y, " Bad Hits: " .. shotsBad)
|
||||
y = y + scoreHeight
|
||||
fontPrint(scoreLeft, y, " Shot Score: " .. shotsGood - shotsBad)
|
||||
y = y + scoreHeight * 2
|
||||
if (shotsFired > 0) then
|
||||
scorePercent = math.floor((shotsGood - shotsBad) / shotsFired * 100)
|
||||
else
|
||||
scorePercent = 0
|
||||
end
|
||||
fontPrint(scoreLeft, y, " Game Score: " .. scorePercent .. "%")
|
||||
if (heartbeat) then
|
||||
spriteDraw(sprPullToStart, pullToStartLeft, pullToStartTop)
|
||||
end
|
||||
|
||||
elseif (currentState == stateMenu) then
|
||||
|
||||
spriteDraw(sprSelectGameType, selectGameTypeLeft, selectGameTypeTop)
|
||||
spriteDraw(sprStandard1, standard1Left, standardLimited1Top)
|
||||
spriteDraw(sprStandard2, standard2Left, standardLimited2Top)
|
||||
spriteDraw(sprLimited1, limited1Left, standardLimited1Top)
|
||||
spriteDraw(sprLimited2, limited2Left, standardLimited2Top)
|
||||
|
||||
if (currentFrame >= lengthIntro + lengthGame + lengthMenu) then
|
||||
discSearch(lengthIntro + lengthGame + 2)
|
||||
discPlay()
|
||||
end
|
||||
|
||||
elseif (currentState == stateIntro) then
|
||||
|
||||
if (heartbeat) then
|
||||
spriteDraw(sprGetReady, getReadyLeft, getReadyTop)
|
||||
end
|
||||
|
||||
-- Skip into game video when the intro is over.
|
||||
if (currentFrame >= lengthIntro) then
|
||||
discSearch(lengthIntro + 1)
|
||||
discPlay()
|
||||
currentState = statePlaying
|
||||
end
|
||||
|
||||
elseif (currentState == statePlaying) then
|
||||
|
||||
-- Read Sucton Cup Sensor.
|
||||
sensorLastState = sensorState
|
||||
sensorState = getPixelState(sensorX, sensorY)
|
||||
-- Are they firing?
|
||||
if (triggerPulled > 0) then
|
||||
-- New frame (according to the sensor)?
|
||||
if (sensorLastState ~= sensorState) then
|
||||
-- Did they aim outside the suction cup sensor area?
|
||||
if ((mouseX < sensorLeft) or (mouseY < sensorTop)) then
|
||||
-- Yes. Process shot.
|
||||
gunState = getPixelState(mouseX, mouseY)
|
||||
else
|
||||
-- No. They shot the sensor area. Bad user.
|
||||
gunState = pixelUnknown
|
||||
end
|
||||
-- Are we doing the first or second frame sample?
|
||||
if (triggerPulled == 1) then
|
||||
--debugPrint("S1: " .. sensorLastState .. " S2: " .. sensorState .. " G1: " .. gunLastState .. " G2: " .. gunState)
|
||||
-- Decide what happened. Do we have two good samples of each sensor?
|
||||
if ((gunState ~= pixelUnknown) and (sensorState ~= pixelUnknown) and (gunLastState ~= pixelUnknown) and (sensorLastState ~= pixelUnknown)) then
|
||||
-- If the sensor and gun states didn't change between frames, then this makes no sense.
|
||||
if (gunLastState ~= gunState) then
|
||||
-- Does the gun match the sensor?
|
||||
if (gunState == sensorState) then
|
||||
-- Yes! Hit bad guy!
|
||||
soundPlay(sndGoodHit)
|
||||
shotsGood = shotsGood + 1
|
||||
lightDisplay = lightTimer
|
||||
-- Add it back to make it look like nothing changed.
|
||||
if (gameMode == gameLimited) then
|
||||
ammoLeft = ammoLeft + 1
|
||||
end
|
||||
else
|
||||
-- No. Crap - shot a good guy!
|
||||
soundPlay(sndBadHit)
|
||||
shotsBad = shotsBad + 1
|
||||
end
|
||||
-- Float the score for a bit
|
||||
scoreDisplay = scoreTimer
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Remember this info for next pass
|
||||
gunLastState = gunState
|
||||
end
|
||||
-- Get ready for next pass
|
||||
triggerPulled = triggerPulled - 1
|
||||
end
|
||||
end
|
||||
|
||||
-- When the game is over, return to the menu.
|
||||
if (currentFrame >= lengthIntro + lengthGame) then
|
||||
discPause()
|
||||
discSearch(backgroundFrame)
|
||||
colorForeground(255, 255, 255)
|
||||
fontSelect(fntChemRea16)
|
||||
currentState = stateTitle
|
||||
end
|
||||
|
||||
-- Do we need to show the score?
|
||||
if (scoreDisplay > 0) then
|
||||
fontPrint(5, 5, "SCORE: " .. (shotsGood - shotsBad))
|
||||
end
|
||||
|
||||
-- Do we need to light the light?
|
||||
if (lightDisplay > 0) then
|
||||
spriteDraw(sprLightOn, lightLeft, lightTop)
|
||||
else
|
||||
spriteDraw(sprLightOff, lightLeft, lightTop)
|
||||
end
|
||||
|
||||
-- Do we need to draw the ammo display?
|
||||
if (gameMode == gameLimited) then
|
||||
-- Are they out of ammo?
|
||||
if (ammoLeft < 0) then
|
||||
discSearch(lengthIntro + lengthGame + 2)
|
||||
discPlay()
|
||||
soundPlay(sndGameOver)
|
||||
currentState = stateGameOver
|
||||
end
|
||||
if (ammoLeft > 0) then
|
||||
bulletStart = overlayGetWidth() - bulletWidth - 5
|
||||
for i=1,ammoLeft do
|
||||
spriteDraw(sprBullet, bulletStart, 0)
|
||||
bulletStart = bulletStart - bulletWidth
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
elseif (currentState == stateGameOver) then
|
||||
|
||||
spriteDraw(sprGameOver, gameOverLeft, gameOverTop)
|
||||
|
||||
if (currentFrame >= lengthIntro + lengthGame + lengthMenu) then
|
||||
discPause()
|
||||
discSearch(backgroundFrame)
|
||||
colorForeground(255, 255, 255)
|
||||
fontSelect(fntChemRea16)
|
||||
currentState = stateTitle
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Draw gun crosshair (This must be the last thing we draw so it's on top.)
|
||||
if (singeWantsCrosshairs()) then
|
||||
spriteDraw(sprCrosshair, mouseX - crosshairCenterX, mouseY - crosshairCenterY)
|
||||
end
|
||||
|
||||
return(OVERLAY_UPDATED)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onShutdown()
|
||||
|
||||
discStop()
|
||||
|
||||
-- Unload our resources.
|
||||
|
||||
fontUnload(fntBlueStone20)
|
||||
fontUnload(fntChemRea16)
|
||||
fontUnload(fntChemRea32)
|
||||
fontUnload(fntChemRea48)
|
||||
fontUnload(fntLEDReal32)
|
||||
|
||||
soundUnload(sndActionMax)
|
||||
soundUnload(sndSteadyAim)
|
||||
soundUnload(sndGetReady)
|
||||
soundUnload(sndGunShot)
|
||||
soundUnload(sndGoodHit)
|
||||
soundUnload(sndBadHit)
|
||||
soundUnload(sndGameOver)
|
||||
|
||||
spriteUnload(sprLightOn)
|
||||
spriteUnload(sprLightOff)
|
||||
spriteUnload(sprActionMax)
|
||||
spriteUnload(sprCrosshair)
|
||||
spriteUnload(sprBullet)
|
||||
spriteUnload(sprBoxArt)
|
||||
|
||||
spriteUnload(sprPullToStart)
|
||||
spriteUnload(sprGetReady)
|
||||
spriteUnload(sprLastGame)
|
||||
spriteUnload(sprSelectGameType)
|
||||
spriteUnload(sprStandard1)
|
||||
spriteUnload(sprStandard2)
|
||||
spriteUnload(sprLimited1)
|
||||
spriteUnload(sprLimited2)
|
||||
spriteUnload(sprGameOver)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onSoundCompleted(intWhich)
|
||||
|
||||
-- Play the "A Steady Aim is Critical" sound after "ActionMax" is finished.
|
||||
if (intWhich == hndActionMaxSound) then
|
||||
soundPlay(sndSteadyAim)
|
||||
hndActionMaxSound = SOUND_REMOVE_HANDLE
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
Game Patches
|
||||
============
|
||||
|
||||
These are corrected copies of scripts from third party Singe games. They
|
||||
are not part of the engine build; each one replaces the same named file
|
||||
inside an installed game. Package one as a ".patch" archive (see
|
||||
"Packaging Your Game" in the manual) or copy the file over the original.
|
||||
|
||||
ActionMax/Emulator.singe
|
||||
Shared emulator script used by every ActionMax title. Fixes sprite
|
||||
leaks in the original release and uses the Singe 3.00 sprite argument
|
||||
order. The per-game wrapper script must define gameID, backgroundFrame,
|
||||
lengthIntro, lengthGame, lengthMenu, highThreshold, lowThreshold,
|
||||
sensorX, sensorY, sensorLeft, and sensorTop before loading it.
|
||||
|
||||
The ActionMax games.dat is not part of this repository. Since Singe
|
||||
3.00 every laserdisc entry in it needs one added line, next to VIDEO:
|
||||
|
||||
DISC = true,
|
||||
|
||||
Entries without it are skipped by the menu with a message, and an entry
|
||||
that names a VIDEO without DISC = true is refused by the engine.
|
||||
|
||||
daitarn_3_singe/Script/toolbox.singe
|
||||
Helper library from "Daitarn 3" (Karis, 2020). The calling script must
|
||||
define OVLW, OVLH, and bPause.
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
--[[
|
||||
|
||||
PROGRAM NAME: LUA SINGE
|
||||
VERSION: 1.1
|
||||
AUTHOR: KARIS (2020)
|
||||
|
||||
This file is part of LUA SINGE.
|
||||
|
||||
LUA SINGE is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation.
|
||||
|
||||
LUA SINGE is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
Thanks to Scott Duensing, RDG.
|
||||
|
||||
]]--
|
||||
|
||||
iSecs = 0
|
||||
iLimit = 0
|
||||
lastSeconds = 0
|
||||
thisSeconds = 0
|
||||
|
||||
sprText = nil
|
||||
sprShadow = nil
|
||||
sLastText = nil
|
||||
iLastColor = -1
|
||||
iLastShadow = -1
|
||||
|
||||
tSecs = 0
|
||||
tLimit = 0
|
||||
tlastSeconds = 0
|
||||
tthisSeconds = 0
|
||||
bTommy = false
|
||||
|
||||
bGunMute = false
|
||||
iMuteFrames = 0
|
||||
MUTE_DELAY = 35
|
||||
|
||||
heartbeat = false
|
||||
blinkSecs = 0
|
||||
lastBlinkSecs = 0
|
||||
|
||||
iRevFrames = 0
|
||||
REV_DELAY = 10
|
||||
bReversePointer = false
|
||||
revsetx = 0; revsety = 0
|
||||
|
||||
iFrameStart = 0; iFrameEnd = 0
|
||||
|
||||
CHANNEL_LEFT = 1
|
||||
CHANNEL_RIGHT = 2
|
||||
ALL_CHANNELS = 3
|
||||
bMuteAttract = false
|
||||
|
||||
RED = 0
|
||||
BLUE = 1
|
||||
YELLOW = 2
|
||||
GREEN = 3
|
||||
ORANGE = 4
|
||||
WHITE = 5
|
||||
GREY = 6
|
||||
GRAY = 6
|
||||
PINK = 7
|
||||
LIGHTBLUE = 7
|
||||
BLACK = 8
|
||||
|
||||
function singeRandomize()
|
||||
|
||||
math.randomseed(os.time()) -- random initialize
|
||||
math.random(); math.random(); math.random() -- warming up
|
||||
|
||||
end
|
||||
|
||||
function blinkTimer(thisMS)
|
||||
|
||||
-- Function blinks every second.
|
||||
|
||||
blinkSecs = os.clock()
|
||||
|
||||
if bPause then
|
||||
|
||||
lastBlinkSecs = blinkSecs
|
||||
|
||||
else
|
||||
|
||||
if (blinkSecs - lastBlinkSecs > thisMS) then
|
||||
heartbeat = not heartbeat
|
||||
lastBlinkSecs = blinkSecs
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function goTimer(thisMS)
|
||||
|
||||
blinkSecs = os.clock()
|
||||
|
||||
if bPause then
|
||||
|
||||
lastBlinkSecs = blinkSecs
|
||||
|
||||
else
|
||||
|
||||
if (blinkSecs - lastBlinkSecs > thisMS) then
|
||||
heartbeat = true
|
||||
lastBlinkSecs = blinkSecs
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function clockRnd()
|
||||
|
||||
local j = 0
|
||||
local q = 0
|
||||
local w = 0
|
||||
local r = 0
|
||||
local b1 = true
|
||||
|
||||
j = os.clock()
|
||||
q, w = math.modf(j)
|
||||
|
||||
s2 = tostring(w)
|
||||
r = string.find(s2, ".", 1, true)
|
||||
|
||||
if (r == nil) then
|
||||
|
||||
s2 = tostring(q)
|
||||
s2 = string.sub(s2,string.len(s2), string.len(s2))
|
||||
|
||||
else
|
||||
|
||||
s2 = string.sub(s2, r + 1)
|
||||
r = string.len(s2)
|
||||
|
||||
if r == 0 then
|
||||
|
||||
s2 = tostring(q)
|
||||
s2 = string.sub(s2,string.len(s2), string.len(s2))
|
||||
|
||||
|
||||
elseif r == 2 then
|
||||
|
||||
s2 = string.sub(s2, 2, 2)
|
||||
|
||||
elseif r >= 3 then
|
||||
|
||||
s2 = string.sub(s2, 3, 3)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
w = tonumber(s2)
|
||||
return w
|
||||
|
||||
end
|
||||
|
||||
function timerOFF()
|
||||
|
||||
iSecs = 0
|
||||
iLimit = 0
|
||||
|
||||
end
|
||||
|
||||
function timerON(thisLong)
|
||||
|
||||
iSecs = 0
|
||||
iLimit = thisLong
|
||||
lastSeconds = os.clock()
|
||||
|
||||
end
|
||||
|
||||
function timerDue()
|
||||
|
||||
thisSeconds = os.clock()
|
||||
|
||||
if bPause then
|
||||
|
||||
lastSeconds = thisSeconds
|
||||
|
||||
else
|
||||
|
||||
if (thisSeconds ~= lastSeconds) then
|
||||
|
||||
iSecs = iSecs + thisSeconds - lastSeconds
|
||||
lastSeconds = thisSeconds
|
||||
|
||||
end
|
||||
|
||||
if (iSecs >= iLimit) then
|
||||
|
||||
timerOFF()
|
||||
return true
|
||||
|
||||
else
|
||||
|
||||
return false
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function muteSound()
|
||||
|
||||
iMuteFrames = 0
|
||||
bGunMute = true
|
||||
|
||||
end
|
||||
|
||||
function blinkRev()
|
||||
|
||||
iRevFrames = 0
|
||||
bReversePointer = true
|
||||
|
||||
end
|
||||
|
||||
function setupClip(thisA, thisB)
|
||||
|
||||
iFrameStart = thisA
|
||||
iFrameEnd = thisB
|
||||
|
||||
discSkipToFrame(thisA)
|
||||
|
||||
end
|
||||
|
||||
function monoAudio (thisChannel)
|
||||
|
||||
if thisChannel == CHANNEL_LEFT then
|
||||
|
||||
discAudio (2, false)
|
||||
discAudio (1, true)
|
||||
|
||||
elseif thisChannel == CHANNEL_RIGHT then
|
||||
|
||||
discAudio (1, false)
|
||||
discAudio (2, true)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function resetChannels()
|
||||
|
||||
discAudio(1, true)
|
||||
discAudio(2, true)
|
||||
|
||||
end
|
||||
|
||||
function muteAudio()
|
||||
|
||||
discAudio(1, false)
|
||||
discAudio(2, false)
|
||||
|
||||
end
|
||||
|
||||
function setFontColor(thisColor)
|
||||
|
||||
if thisColor == RED then
|
||||
|
||||
colorForeground(255, 0, 0)
|
||||
|
||||
elseif thisColor == BLUE then
|
||||
|
||||
colorForeground(0, 0, 255)
|
||||
|
||||
elseif thisColor == YELLOW then
|
||||
|
||||
colorForeground(255, 255, 0)
|
||||
|
||||
elseif thisColor == GREEN then
|
||||
|
||||
colorForeground(0, 255, 0)
|
||||
|
||||
elseif thisColor == ORANGE then
|
||||
|
||||
colorForeground(255, 150, 0)
|
||||
|
||||
elseif thisColor == WHITE then
|
||||
|
||||
colorForeground(255, 255, 255)
|
||||
|
||||
elseif thisColor == GREY or thisColor == GRAY then
|
||||
|
||||
colorForeground(128, 128, 128)
|
||||
|
||||
elseif thisColor == LIGHTBLUE then
|
||||
|
||||
colorForeground(30, 160, 250)
|
||||
|
||||
elseif thisColor == BLACK then
|
||||
|
||||
colorForeground(0,0,0)
|
||||
|
||||
elseif thisColor == PINK then
|
||||
|
||||
colorForeground(252,0,148)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function textPrint(thisMsg, thisx, thisy, thisFont, thisColor, thisShadow)
|
||||
|
||||
fontSelect(thisFont)
|
||||
setFontColor(thisColor)
|
||||
fontPrint(thisx,thisy,thisMsg)
|
||||
|
||||
end
|
||||
|
||||
function getMiddle(thisPhrase)
|
||||
|
||||
local sprite = fontToSprite(thisPhrase)
|
||||
local x = OVLW/2 - spriteGetWidth(sprite) * 0.5
|
||||
|
||||
spriteUnload(sprite)
|
||||
|
||||
return x
|
||||
|
||||
end
|
||||
784
src/particles.c
Normal file
784
src/particles.c
Normal file
|
|
@ -0,0 +1,784 @@
|
|||
/*
|
||||
* Singe - Particles: emitters that spawn, move and age particles for the 2D overlay and the 3D scene.
|
||||
* Copyright (C) 2026 Scott Duensing
|
||||
*
|
||||
* See LICENSE for details.
|
||||
*
|
||||
* One simulation serves both worlds. A 2D emitter keeps its own position in overlay pixels; a 3D
|
||||
* emitter is born at a node's world position. Particles live in structure-of-arrays pools, packed
|
||||
* so the live ones are always the first "count" entries; a dead one is swapped with the last. The
|
||||
* renderers (the overlay in singe.c, the scene in scene.c) read the pools through EmitterViewT.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include "particles.h"
|
||||
#include "scene.h"
|
||||
#include "../thirdparty/uthash/src/uthash.h"
|
||||
#include "util.h"
|
||||
|
||||
#define DEFAULT_MAX 1000
|
||||
#define MAX_STEP_SECONDS 0.1
|
||||
#define DISC_SIZE 64
|
||||
#define QUEUE_MAX 256
|
||||
#define DEGREES_TO_RADIANS 0.017453292519943295f
|
||||
|
||||
typedef struct EmitterS {
|
||||
int32_t id;
|
||||
int32_t node; // -1: 2D
|
||||
// The recipe
|
||||
float rate;
|
||||
float lifeMin;
|
||||
float lifeMax;
|
||||
float speedMin;
|
||||
float speedMax;
|
||||
Vec3T direction;
|
||||
float spread; // Degrees
|
||||
Vec3T gravity;
|
||||
float drag;
|
||||
float sizeStart;
|
||||
float sizeEnd;
|
||||
float sizeVariation;
|
||||
float colour0[4];
|
||||
float colour1[4];
|
||||
float spinMin;
|
||||
float spinMax;
|
||||
float radius;
|
||||
bool local;
|
||||
int32_t max;
|
||||
ParticleBlendE blend;
|
||||
ParticleLayerE layer;
|
||||
int32_t frameFirst;
|
||||
int32_t frameLast;
|
||||
SDL_Surface **frames;
|
||||
int32_t frameCount;
|
||||
uint32_t textureVersion;
|
||||
// The state
|
||||
bool emitting;
|
||||
float pending; // Fractional particles owed
|
||||
Vec3T position; // 2D position, or the last known 3D origin
|
||||
int32_t count;
|
||||
float *px;
|
||||
float *py;
|
||||
float *pz;
|
||||
float *vx;
|
||||
float *vy;
|
||||
float *vz;
|
||||
float *age;
|
||||
float *life;
|
||||
float *size0;
|
||||
float *size1;
|
||||
float *angle;
|
||||
float *spin;
|
||||
int32_t *frame;
|
||||
ParticleViewT *views;
|
||||
UT_hash_handle hh;
|
||||
} EmitterT;
|
||||
|
||||
static void _allocatePools(EmitterT *emitter, int32_t max);
|
||||
static EmitterT *_find(int32_t emitter);
|
||||
static void _freeFrames(EmitterT *emitter);
|
||||
static void _freePools(EmitterT *emitter);
|
||||
static void _kill(EmitterT *emitter, int32_t index);
|
||||
static Vec3T _origin(EmitterT *emitter);
|
||||
static float _random(float min, float max);
|
||||
static Vec3T _randomDirection(EmitterT *emitter);
|
||||
static Vec3T _randomOffset(EmitterT *emitter);
|
||||
static void _spawn(EmitterT *emitter, int32_t count);
|
||||
static void _step(EmitterT *emitter, float dt);
|
||||
static void _view(EmitterT *emitter, EmitterViewT *view);
|
||||
|
||||
static EmitterT *_emitters = NULL;
|
||||
static int32_t _nextId = 1;
|
||||
static uint64_t _lastTick = 0;
|
||||
static SDL_Surface *_disc = NULL;
|
||||
static int32_t _queue[QUEUE_MAX];
|
||||
static int32_t _queueCount = 0;
|
||||
|
||||
|
||||
static void _allocatePools(EmitterT *emitter, int32_t max) {
|
||||
_freePools(emitter);
|
||||
emitter->max = max;
|
||||
emitter->count = 0;
|
||||
emitter->px = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->py = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->pz = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->vx = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->vy = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->vz = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->age = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->life = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->size0 = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->size1 = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->angle = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->spin = SDL_calloc((size_t)max, sizeof(float));
|
||||
emitter->frame = SDL_calloc((size_t)max, sizeof(int32_t));
|
||||
emitter->views = SDL_calloc((size_t)max, sizeof(ParticleViewT));
|
||||
if ((emitter->px == NULL) || (emitter->py == NULL) || (emitter->pz == NULL) || (emitter->vx == NULL) || (emitter->vy == NULL) || (emitter->vz == NULL) || (emitter->age == NULL) || (emitter->life == NULL) || (emitter->size0 == NULL) || (emitter->size1 == NULL) || (emitter->angle == NULL) || (emitter->spin == NULL) || (emitter->frame == NULL) || (emitter->views == NULL)) {
|
||||
utilDie("Out of memory for %d particles.", max);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static EmitterT *_find(int32_t emitter) {
|
||||
EmitterT *found = NULL;
|
||||
|
||||
HASH_FIND_INT(_emitters, &emitter, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
static void _freeFrames(EmitterT *emitter) {
|
||||
int32_t x = 0;
|
||||
|
||||
for (x = 0; x < emitter->frameCount; x++) {
|
||||
SDL_DestroySurface(emitter->frames[x]);
|
||||
}
|
||||
SDL_free(emitter->frames);
|
||||
emitter->frames = NULL;
|
||||
emitter->frameCount = 0;
|
||||
}
|
||||
|
||||
|
||||
static void _freePools(EmitterT *emitter) {
|
||||
SDL_free(emitter->px);
|
||||
SDL_free(emitter->py);
|
||||
SDL_free(emitter->pz);
|
||||
SDL_free(emitter->vx);
|
||||
SDL_free(emitter->vy);
|
||||
SDL_free(emitter->vz);
|
||||
SDL_free(emitter->age);
|
||||
SDL_free(emitter->life);
|
||||
SDL_free(emitter->size0);
|
||||
SDL_free(emitter->size1);
|
||||
SDL_free(emitter->angle);
|
||||
SDL_free(emitter->spin);
|
||||
SDL_free(emitter->frame);
|
||||
SDL_free(emitter->views);
|
||||
emitter->px = NULL;
|
||||
emitter->py = NULL;
|
||||
emitter->pz = NULL;
|
||||
emitter->vx = NULL;
|
||||
emitter->vy = NULL;
|
||||
emitter->vz = NULL;
|
||||
emitter->age = NULL;
|
||||
emitter->life = NULL;
|
||||
emitter->size0 = NULL;
|
||||
emitter->size1 = NULL;
|
||||
emitter->angle = NULL;
|
||||
emitter->spin = NULL;
|
||||
emitter->frame = NULL;
|
||||
emitter->views = NULL;
|
||||
emitter->count = 0;
|
||||
}
|
||||
|
||||
|
||||
// Removes particle index by moving the last live one into its slot.
|
||||
static void _kill(EmitterT *emitter, int32_t index) {
|
||||
int32_t last = emitter->count - 1;
|
||||
|
||||
if (index != last) {
|
||||
emitter->px[index] = emitter->px[last];
|
||||
emitter->py[index] = emitter->py[last];
|
||||
emitter->pz[index] = emitter->pz[last];
|
||||
emitter->vx[index] = emitter->vx[last];
|
||||
emitter->vy[index] = emitter->vy[last];
|
||||
emitter->vz[index] = emitter->vz[last];
|
||||
emitter->age[index] = emitter->age[last];
|
||||
emitter->life[index] = emitter->life[last];
|
||||
emitter->size0[index] = emitter->size0[last];
|
||||
emitter->size1[index] = emitter->size1[last];
|
||||
emitter->angle[index] = emitter->angle[last];
|
||||
emitter->spin[index] = emitter->spin[last];
|
||||
emitter->frame[index] = emitter->frame[last];
|
||||
}
|
||||
emitter->count = last;
|
||||
}
|
||||
|
||||
|
||||
// Where particles are born: the 2D position, or the node's world position.
|
||||
static Vec3T _origin(EmitterT *emitter) {
|
||||
if (emitter->node >= 0) {
|
||||
if (nodeValid(emitter->node)) {
|
||||
emitter->position = nodeGetWorldPosition(emitter->node);
|
||||
}
|
||||
}
|
||||
return emitter->position;
|
||||
}
|
||||
|
||||
|
||||
static float _random(float min, float max) {
|
||||
return min + (max - min) * SDL_randf();
|
||||
}
|
||||
|
||||
|
||||
// A unit vector within the spread cone round the direction (in 2D the cone is a fan in the plane).
|
||||
static Vec3T _randomDirection(EmitterT *emitter) {
|
||||
Vec3T axis = emitter->direction;
|
||||
Vec3T other;
|
||||
Vec3T side;
|
||||
Vec3T up;
|
||||
float length = vec3Length(axis);
|
||||
float cosMax = SDL_cosf(emitter->spread * DEGREES_TO_RADIANS);
|
||||
float cosA;
|
||||
float sinA;
|
||||
float phi;
|
||||
|
||||
if (length < 1e-6f) {
|
||||
axis = vec3(0.0f, -1.0f, 0.0f);
|
||||
} else {
|
||||
axis = vec3Scale(axis, 1.0f / length);
|
||||
}
|
||||
if (emitter->node < 0) {
|
||||
// 2D: rotate within the plane by a random angle up to the spread.
|
||||
phi = _random(-emitter->spread, emitter->spread) * DEGREES_TO_RADIANS;
|
||||
return vec3(axis.x * SDL_cosf(phi) - axis.y * SDL_sinf(phi), axis.x * SDL_sinf(phi) + axis.y * SDL_cosf(phi), 0.0f);
|
||||
}
|
||||
// 3D: uniform over the spherical cap.
|
||||
cosA = _random(cosMax, 1.0f);
|
||||
sinA = SDL_sqrtf(SDL_max(0.0f, 1.0f - cosA * cosA));
|
||||
phi = _random(0.0f, 2.0f * SDL_PI_F);
|
||||
other = (SDL_fabsf(axis.y) < 0.9f) ? vec3(0.0f, 1.0f, 0.0f) : vec3(1.0f, 0.0f, 0.0f);
|
||||
side = vec3Normalize(vec3Cross(axis, other));
|
||||
up = vec3Cross(side, axis);
|
||||
return vec3Add(vec3Scale(axis, cosA), vec3Add(vec3Scale(side, sinA * SDL_cosf(phi)), vec3Scale(up, sinA * SDL_sinf(phi))));
|
||||
}
|
||||
|
||||
|
||||
// A random point in the birth disc (2D) or ball (3D) round the origin.
|
||||
static Vec3T _randomOffset(EmitterT *emitter) {
|
||||
Vec3T point;
|
||||
|
||||
if (emitter->radius <= 0.0f) {
|
||||
return vec3(0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
// Rejection sampling keeps it uniform.
|
||||
do {
|
||||
point = vec3(_random(-1.0f, 1.0f), _random(-1.0f, 1.0f), (emitter->node < 0) ? 0.0f : _random(-1.0f, 1.0f));
|
||||
} while (vec3Dot(point, point) > 1.0f);
|
||||
return vec3Scale(point, emitter->radius);
|
||||
}
|
||||
|
||||
|
||||
static void _spawn(EmitterT *emitter, int32_t count) {
|
||||
Vec3T origin = _origin(emitter);
|
||||
Vec3T base = emitter->local ? vec3(0.0f, 0.0f, 0.0f) : origin;
|
||||
Vec3T offset;
|
||||
Vec3T velocity;
|
||||
float variation;
|
||||
int32_t i;
|
||||
|
||||
while ((count > 0) && (emitter->count < emitter->max)) {
|
||||
i = emitter->count++;
|
||||
offset = vec3Add(base, _randomOffset(emitter));
|
||||
velocity = vec3Scale(_randomDirection(emitter), _random(emitter->speedMin, emitter->speedMax));
|
||||
variation = 1.0f + _random(-emitter->sizeVariation, emitter->sizeVariation);
|
||||
emitter->px[i] = offset.x;
|
||||
emitter->py[i] = offset.y;
|
||||
emitter->pz[i] = offset.z;
|
||||
emitter->vx[i] = velocity.x;
|
||||
emitter->vy[i] = velocity.y;
|
||||
emitter->vz[i] = velocity.z;
|
||||
emitter->age[i] = 0.0f;
|
||||
emitter->life[i] = SDL_max(0.001f, _random(emitter->lifeMin, emitter->lifeMax));
|
||||
emitter->size0[i] = emitter->sizeStart * variation;
|
||||
emitter->size1[i] = emitter->sizeEnd * variation;
|
||||
emitter->angle[i] = _random(0.0f, 360.0f);
|
||||
emitter->spin[i] = _random(emitter->spinMin, emitter->spinMax);
|
||||
emitter->frame[i] = (emitter->frameCount > 1) ? (int32_t)_random((float)emitter->frameFirst, (float)emitter->frameLast + 0.999f) : 0;
|
||||
count--;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void _step(EmitterT *emitter, float dt) {
|
||||
float keep = SDL_max(0.0f, 1.0f - emitter->drag * dt);
|
||||
int32_t i = 0;
|
||||
|
||||
if (emitter->emitting) {
|
||||
emitter->pending += emitter->rate * dt;
|
||||
if (emitter->pending >= 1.0f) {
|
||||
_spawn(emitter, (int32_t)emitter->pending);
|
||||
emitter->pending -= (float)(int32_t)emitter->pending;
|
||||
}
|
||||
}
|
||||
while (i < emitter->count) {
|
||||
emitter->age[i] += dt;
|
||||
if (emitter->age[i] >= emitter->life[i]) {
|
||||
_kill(emitter, i);
|
||||
continue;
|
||||
}
|
||||
emitter->vx[i] = (emitter->vx[i] + emitter->gravity.x * dt) * keep;
|
||||
emitter->vy[i] = (emitter->vy[i] + emitter->gravity.y * dt) * keep;
|
||||
emitter->vz[i] = (emitter->vz[i] + emitter->gravity.z * dt) * keep;
|
||||
emitter->px[i] += emitter->vx[i] * dt;
|
||||
emitter->py[i] += emitter->vy[i] * dt;
|
||||
emitter->pz[i] += emitter->vz[i] * dt;
|
||||
emitter->angle[i] += emitter->spin[i] * dt;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void _view(EmitterT *emitter, EmitterViewT *view) {
|
||||
Vec3T base = emitter->local ? _origin(emitter) : vec3(0.0f, 0.0f, 0.0f);
|
||||
float t;
|
||||
int32_t i;
|
||||
int32_t c;
|
||||
|
||||
for (i = 0; i < emitter->count; i++) {
|
||||
t = emitter->age[i] / emitter->life[i];
|
||||
emitter->views[i].position = vec3Add(base, vec3(emitter->px[i], emitter->py[i], emitter->pz[i]));
|
||||
emitter->views[i].size = emitter->size0[i] + (emitter->size1[i] - emitter->size0[i]) * t;
|
||||
emitter->views[i].angle = emitter->angle[i];
|
||||
emitter->views[i].frame = emitter->frame[i];
|
||||
for (c = 0; c < 4; c++) {
|
||||
emitter->views[i].colour[c] = emitter->colour0[c] + (emitter->colour1[c] - emitter->colour0[c]) * t;
|
||||
}
|
||||
}
|
||||
view->id = emitter->id;
|
||||
view->node = emitter->node;
|
||||
view->blend = emitter->blend;
|
||||
view->layer = emitter->layer;
|
||||
view->frameCount = (emitter->frameCount > 0) ? emitter->frameCount : 1;
|
||||
view->frames = (emitter->frameCount > 0) ? emitter->frames : &_disc;
|
||||
view->textureVersion = emitter->textureVersion;
|
||||
view->count = emitter->count;
|
||||
view->particles = emitter->views;
|
||||
}
|
||||
|
||||
|
||||
void emitterBurst(int32_t emitter, int32_t count) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
_spawn(found, count);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterClear(int32_t emitter) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->count = 0;
|
||||
found->pending = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterDelete(int32_t emitter) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
HASH_DEL(_emitters, found);
|
||||
_freePools(found);
|
||||
_freeFrames(found);
|
||||
SDL_free(found);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int32_t emitterGetCount(int32_t emitter) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
return (found != NULL) ? found->count : 0;
|
||||
}
|
||||
|
||||
|
||||
bool emitterIs3D(int32_t emitter) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
return (found != NULL) && (found->node >= 0);
|
||||
}
|
||||
|
||||
|
||||
bool emitterIsActive(int32_t emitter) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
return (found != NULL) && (found->emitting || (found->count > 0));
|
||||
}
|
||||
|
||||
|
||||
int32_t emitterNew(int32_t node) {
|
||||
EmitterT *emitter = SDL_calloc(1, sizeof(EmitterT));
|
||||
|
||||
if (emitter == NULL) {
|
||||
utilDie("Out of memory for an emitter.");
|
||||
}
|
||||
emitter->id = _nextId++;
|
||||
emitter->node = node;
|
||||
emitter->rate = 50.0f;
|
||||
emitter->lifeMin = 1.0f;
|
||||
emitter->lifeMax = 2.0f;
|
||||
emitter->speedMin = (node < 0) ? 50.0f : 1.0f;
|
||||
emitter->speedMax = (node < 0) ? 100.0f : 2.0f;
|
||||
emitter->direction = (node < 0) ? vec3(0.0f, -1.0f, 0.0f) : vec3(0.0f, 1.0f, 0.0f);
|
||||
emitter->spread = 30.0f;
|
||||
emitter->sizeStart = (node < 0) ? 16.0f : 0.2f;
|
||||
emitter->sizeEnd = (node < 0) ? 4.0f : 0.05f;
|
||||
emitter->colour0[0] = 1.0f;
|
||||
emitter->colour0[1] = 1.0f;
|
||||
emitter->colour0[2] = 1.0f;
|
||||
emitter->colour0[3] = 1.0f;
|
||||
emitter->colour1[0] = 1.0f;
|
||||
emitter->colour1[1] = 1.0f;
|
||||
emitter->colour1[2] = 1.0f;
|
||||
emitter->colour1[3] = 0.0f;
|
||||
emitter->textureVersion = 1;
|
||||
_allocatePools(emitter, DEFAULT_MAX);
|
||||
HASH_ADD_INT(_emitters, id, emitter);
|
||||
return emitter->id;
|
||||
}
|
||||
|
||||
|
||||
void emitterSetBlend(int32_t emitter, ParticleBlendE blend) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->blend = blend;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetColor(int32_t emitter, const float *start, const float *finish) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
memcpy(found->colour0, start, sizeof(found->colour0));
|
||||
memcpy(found->colour1, finish, sizeof(found->colour1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetDirection(int32_t emitter, Vec3T direction) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->direction = direction;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetDrag(int32_t emitter, float perSecond) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->drag = SDL_max(0.0f, perSecond);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetFrames(int32_t emitter, int32_t first, int32_t last) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->frameFirst = SDL_clamp(first, 0, SDL_max(0, found->frameCount - 1));
|
||||
found->frameLast = SDL_clamp(last, found->frameFirst, SDL_max(0, found->frameCount - 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetGravity(int32_t emitter, Vec3T acceleration) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->gravity = acceleration;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetLayer(int32_t emitter, ParticleLayerE layer) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->layer = layer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetLife(int32_t emitter, float minSeconds, float maxSeconds) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->lifeMin = SDL_max(0.001f, minSeconds);
|
||||
found->lifeMax = SDL_max(found->lifeMin, maxSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetLocal(int32_t emitter, bool local) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if ((found != NULL) && (found->local != local)) {
|
||||
// Keep the live particles where they are by moving them between frames of reference.
|
||||
Vec3T origin = _origin(found);
|
||||
float sign = local ? -1.0f : 1.0f;
|
||||
int32_t i;
|
||||
|
||||
for (i = 0; i < found->count; i++) {
|
||||
found->px[i] += sign * origin.x;
|
||||
found->py[i] += sign * origin.y;
|
||||
found->pz[i] += sign * origin.z;
|
||||
}
|
||||
found->local = local;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetMax(int32_t emitter, int32_t count) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if ((found != NULL) && (count > 0) && (count != found->max)) {
|
||||
_allocatePools(found, count);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetPosition(int32_t emitter, Vec3T position) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->position = position;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetRadius(int32_t emitter, float radius) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->radius = SDL_max(0.0f, radius);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetRate(int32_t emitter, float perSecond) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->rate = SDL_max(0.0f, perSecond);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetSize(int32_t emitter, float start, float finish, float variation) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->sizeStart = SDL_max(0.0f, start);
|
||||
found->sizeEnd = SDL_max(0.0f, finish);
|
||||
found->sizeVariation = SDL_clamp(variation, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetSpeed(int32_t emitter, float min, float max) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->speedMin = min;
|
||||
found->speedMax = SDL_max(min, max);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetSpin(int32_t emitter, float min, float max) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->spinMin = min;
|
||||
found->spinMax = SDL_max(min, max);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetSpread(int32_t emitter, float degrees) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->spread = SDL_clamp(degrees, 0.0f, 180.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterSetTexture(int32_t emitter, SDL_Surface **frames, int32_t frameCount) {
|
||||
EmitterT *found = _find(emitter);
|
||||
int32_t x = 0;
|
||||
|
||||
if (found == NULL) {
|
||||
return;
|
||||
}
|
||||
_freeFrames(found);
|
||||
if ((frames != NULL) && (frameCount > 0)) {
|
||||
found->frames = SDL_calloc((size_t)frameCount, sizeof(SDL_Surface *));
|
||||
if (found->frames == NULL) {
|
||||
utilDie("Out of memory for particle frames.");
|
||||
}
|
||||
for (x = 0; x < frameCount; x++) {
|
||||
found->frames[x] = SDL_DuplicateSurface(frames[x]);
|
||||
if (found->frames[x] == NULL) {
|
||||
utilDie("%s", SDL_GetError());
|
||||
}
|
||||
}
|
||||
found->frameCount = frameCount;
|
||||
}
|
||||
found->frameFirst = 0;
|
||||
found->frameLast = 0;
|
||||
found->textureVersion++;
|
||||
for (x = 0; x < found->count; x++) {
|
||||
found->frame[x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterStart(int32_t emitter) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->emitting = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void emitterStop(int32_t emitter) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found != NULL) {
|
||||
found->emitting = false;
|
||||
found->pending = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool emitterValid(int32_t emitter) {
|
||||
return _find(emitter) != NULL;
|
||||
}
|
||||
|
||||
|
||||
void particlesClearQueue2D(void) {
|
||||
_queueCount = 0;
|
||||
}
|
||||
|
||||
|
||||
int32_t particlesCount(void) {
|
||||
return (int32_t)HASH_COUNT(_emitters);
|
||||
}
|
||||
|
||||
|
||||
// The built-in picture: a soft disc, opaque in the middle fading to nothing at the edge.
|
||||
void particlesInit(void) {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
float dx;
|
||||
float dy;
|
||||
float d;
|
||||
uint8_t alpha;
|
||||
uint32_t *pixels;
|
||||
|
||||
_disc = SDL_CreateSurface(DISC_SIZE, DISC_SIZE, SDL_PIXELFORMAT_RGBA32);
|
||||
if (_disc == NULL) {
|
||||
utilDie("%s", SDL_GetError());
|
||||
}
|
||||
pixels = (uint32_t *)_disc->pixels;
|
||||
for (y = 0; y < DISC_SIZE; y++) {
|
||||
for (x = 0; x < DISC_SIZE; x++) {
|
||||
dx = ((float)x + 0.5f) / (DISC_SIZE / 2.0f) - 1.0f;
|
||||
dy = ((float)y + 0.5f) / (DISC_SIZE / 2.0f) - 1.0f;
|
||||
d = SDL_sqrtf(dx * dx + dy * dy);
|
||||
alpha = (uint8_t)(255.0f * SDL_clamp(1.0f - d, 0.0f, 1.0f) * SDL_clamp(1.0f - d, 0.0f, 1.0f));
|
||||
pixels[y * (_disc->pitch / 4) + x] = SDL_MapSurfaceRGBA(_disc, 255, 255, 255, alpha);
|
||||
}
|
||||
}
|
||||
_lastTick = 0;
|
||||
}
|
||||
|
||||
|
||||
void particlesQueue2D(int32_t emitter) {
|
||||
if ((_queueCount < QUEUE_MAX) && emitterValid(emitter)) {
|
||||
_queue[_queueCount++] = emitter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int32_t particlesQueued2D(ParticleLayerE layer, int32_t *emitters, int32_t max) {
|
||||
EmitterT *found;
|
||||
int32_t x;
|
||||
int32_t n = 0;
|
||||
|
||||
for (x = 0; (x < _queueCount) && (n < max); x++) {
|
||||
found = _find(_queue[x]);
|
||||
if ((found != NULL) && (found->layer == layer)) {
|
||||
emitters[n++] = _queue[x];
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
void particlesQuit(void) {
|
||||
EmitterT *emitter;
|
||||
EmitterT *next;
|
||||
|
||||
HASH_ITER(hh, _emitters, emitter, next) {
|
||||
emitterDelete(emitter->id);
|
||||
}
|
||||
if (_disc != NULL) {
|
||||
SDL_DestroySurface(_disc);
|
||||
_disc = NULL;
|
||||
}
|
||||
_queueCount = 0;
|
||||
}
|
||||
|
||||
|
||||
// Advances every emitter by the wall-clock time since the last call; a paused game passes no time.
|
||||
void particlesUpdate(bool advance) {
|
||||
EmitterT *emitter;
|
||||
EmitterT *next;
|
||||
uint64_t now = SDL_GetTicksNS();
|
||||
double dt = 0.0;
|
||||
|
||||
if (advance && (_lastTick != 0)) {
|
||||
dt = (double)(now - _lastTick) / 1e9;
|
||||
}
|
||||
_lastTick = now;
|
||||
if (dt > MAX_STEP_SECONDS) {
|
||||
dt = MAX_STEP_SECONDS;
|
||||
}
|
||||
HASH_ITER(hh, _emitters, emitter, next) {
|
||||
if ((emitter->node >= 0) && !nodeValid(emitter->node)) {
|
||||
emitterDelete(emitter->id);
|
||||
continue;
|
||||
}
|
||||
if (dt > 0.0) {
|
||||
_step(emitter, (float)dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool particlesView(int32_t index, EmitterViewT *view) {
|
||||
EmitterT *emitter;
|
||||
EmitterT *next;
|
||||
int32_t n = 0;
|
||||
|
||||
HASH_ITER(hh, _emitters, emitter, next) {
|
||||
if (n == index) {
|
||||
_view(emitter, view);
|
||||
return true;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool particlesViewEmitter(int32_t emitter, EmitterViewT *view) {
|
||||
EmitterT *found = _find(emitter);
|
||||
|
||||
if (found == NULL) {
|
||||
return false;
|
||||
}
|
||||
_view(found, view);
|
||||
return true;
|
||||
}
|
||||
87
src/particles.h
Normal file
87
src/particles.h
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Singe - Particles: emitters that spawn, move and age particles for the 2D overlay and the 3D scene.
|
||||
* Copyright (C) 2026 Scott Duensing
|
||||
*
|
||||
* See LICENSE for details.
|
||||
*/
|
||||
|
||||
#ifndef PARTICLES_H
|
||||
#define PARTICLES_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include "math3d.h"
|
||||
|
||||
typedef enum ParticleBlendE {
|
||||
PARTICLE_ALPHA = 0, // Normal alpha blending
|
||||
PARTICLE_ADD = 1 // Additive: light on light
|
||||
} ParticleBlendE;
|
||||
|
||||
typedef enum ParticleLayerE {
|
||||
PARTICLE_OVER = 0, // 2D: drawn above the overlay
|
||||
PARTICLE_UNDER = 1 // 2D: drawn beneath the overlay, above the video and the scene
|
||||
} ParticleLayerE;
|
||||
|
||||
// One particle, as handed to a renderer: world or overlay position, size, rotation, tint and frame.
|
||||
typedef struct ParticleViewT {
|
||||
Vec3T position;
|
||||
float size;
|
||||
float angle; // Degrees
|
||||
float colour[4]; // 0..1
|
||||
int32_t frame;
|
||||
} ParticleViewT;
|
||||
|
||||
// Renderers ask an emitter for its live particles; the texture per frame is theirs to keep.
|
||||
typedef struct EmitterViewT {
|
||||
int32_t id;
|
||||
int32_t node; // -1 for a 2D emitter
|
||||
ParticleBlendE blend;
|
||||
ParticleLayerE layer;
|
||||
int32_t frameCount;
|
||||
SDL_Surface **frames; // frameCount surfaces (the built-in disc when no sprite is set)
|
||||
uint32_t textureVersion; // Bumped whenever the frames change
|
||||
int32_t count;
|
||||
ParticleViewT *particles; // count entries, valid until the next update
|
||||
} EmitterViewT;
|
||||
|
||||
void particlesInit(void);
|
||||
void particlesQuit(void);
|
||||
void particlesUpdate(bool advance);
|
||||
int32_t particlesCount(void);
|
||||
bool particlesView(int32_t index, EmitterViewT *view); // Emitter number index of particlesCount
|
||||
bool particlesViewEmitter(int32_t emitter, EmitterViewT *view);
|
||||
void particlesQueue2D(int32_t emitter); // Draw this emitter this frame
|
||||
int32_t particlesQueued2D(ParticleLayerE layer, int32_t *emitters, int32_t max);
|
||||
void particlesClearQueue2D(void);
|
||||
|
||||
int32_t emitterNew(int32_t node);
|
||||
bool emitterValid(int32_t emitter);
|
||||
void emitterDelete(int32_t emitter);
|
||||
void emitterSetTexture(int32_t emitter, SDL_Surface **frames, int32_t frameCount); // Copies the surfaces; NULL restores the disc
|
||||
void emitterSetFrames(int32_t emitter, int32_t first, int32_t last);
|
||||
void emitterSetBlend(int32_t emitter, ParticleBlendE blend);
|
||||
void emitterSetLayer(int32_t emitter, ParticleLayerE layer);
|
||||
void emitterSetRate(int32_t emitter, float perSecond);
|
||||
void emitterSetLife(int32_t emitter, float minSeconds, float maxSeconds);
|
||||
void emitterSetSpeed(int32_t emitter, float min, float max);
|
||||
void emitterSetDirection(int32_t emitter, Vec3T direction);
|
||||
void emitterSetSpread(int32_t emitter, float degrees);
|
||||
void emitterSetGravity(int32_t emitter, Vec3T acceleration);
|
||||
void emitterSetDrag(int32_t emitter, float perSecond);
|
||||
void emitterSetSize(int32_t emitter, float start, float finish, float variation);
|
||||
void emitterSetColor(int32_t emitter, const float *start, const float *finish);
|
||||
void emitterSetSpin(int32_t emitter, float min, float max);
|
||||
void emitterSetRadius(int32_t emitter, float radius);
|
||||
void emitterSetLocal(int32_t emitter, bool local);
|
||||
void emitterSetMax(int32_t emitter, int32_t count);
|
||||
void emitterSetPosition(int32_t emitter, Vec3T position);
|
||||
void emitterStart(int32_t emitter);
|
||||
void emitterStop(int32_t emitter);
|
||||
void emitterBurst(int32_t emitter, int32_t count);
|
||||
void emitterClear(int32_t emitter);
|
||||
int32_t emitterGetCount(int32_t emitter);
|
||||
bool emitterIsActive(int32_t emitter);
|
||||
bool emitterIs3D(int32_t emitter);
|
||||
|
||||
#endif
|
||||
|
|
@ -76,6 +76,21 @@ typedef enum JointTypeE {
|
|||
} JointTypeE;
|
||||
|
||||
|
||||
typedef enum SoftKindE {
|
||||
SOFT_CLOTH = 0, // An open mesh: a flag, a curtain, a sheet
|
||||
SOFT_BODY = 1, // A closed mesh under pressure: a balloon, a jelly
|
||||
SOFT_ROPE = 2 // A chain of points with a tube drawn round it
|
||||
} SoftKindE;
|
||||
|
||||
|
||||
typedef enum VehicleKindE {
|
||||
VEHICLE_CAR = 0,
|
||||
VEHICLE_MOTORCYCLE = 1,
|
||||
VEHICLE_TANK = 2,
|
||||
VEHICLE_BOAT = 3
|
||||
} VehicleKindE;
|
||||
|
||||
|
||||
bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at);
|
||||
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at);
|
||||
bool bodyDelete(int32_t node);
|
||||
|
|
@ -86,16 +101,76 @@ bool bodyIsResting(int32_t node);
|
|||
bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c);
|
||||
bool bodySetAngularVelocity(int32_t node, Vec3T velocity);
|
||||
bool bodySetBounce(int32_t node, float bounce);
|
||||
bool bodySetBuoyancy(int32_t node, float factor);
|
||||
bool bodySetCurrent(int32_t node, Vec3T flow);
|
||||
bool bodySetEnabled(int32_t node, bool enabled);
|
||||
bool bodySetFriction(int32_t node, float friction);
|
||||
bool bodySetMass(int32_t node, float kilograms);
|
||||
bool bodySetTrigger(int32_t node, bool trigger);
|
||||
bool bodySetVelocity(int32_t node, Vec3T velocity);
|
||||
bool bodySetWater(int32_t node, float density, float linearDrag, float angularDrag);
|
||||
bool jointDelete(int32_t joint);
|
||||
int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Vec3T axis);
|
||||
bool jointSetLimits(int32_t joint, float low, float high);
|
||||
bool jointValid(int32_t joint);
|
||||
bool physicsAvailable(void);
|
||||
bool playerDelete(int32_t node);
|
||||
bool playerExists(int32_t node);
|
||||
int32_t playerGetGround(int32_t node, Vec3T *normal);
|
||||
Vec3T playerGetVelocity(int32_t node);
|
||||
bool playerIsOnGround(int32_t node);
|
||||
bool playerIsSwimming(int32_t node);
|
||||
bool playerJump(int32_t node, float speed);
|
||||
bool playerMove(int32_t node, Vec3T velocity);
|
||||
bool playerNew(int32_t node, ShapeTypeE shape, float a, float b, float c);
|
||||
bool playerSetEnabled(int32_t node, bool enabled);
|
||||
bool playerSetGravityScale(int32_t node, float scale);
|
||||
bool playerSetMass(int32_t node, float kilograms);
|
||||
bool playerSetPosition(int32_t node, Vec3T position);
|
||||
bool playerSetPush(int32_t node, float strength);
|
||||
bool playerSetSlope(int32_t node, float degrees);
|
||||
bool playerSetStep(int32_t node, float height);
|
||||
bool playerSetSwim(int32_t node, float sinkSpeed, float drag);
|
||||
bool playerSetVelocity(int32_t node, Vec3T velocity);
|
||||
bool ragdollActivate(int32_t node);
|
||||
bool ragdollApplyImpulse(int32_t node, const char *joint, Vec3T impulse);
|
||||
bool ragdollDeactivate(int32_t node);
|
||||
bool ragdollDelete(int32_t node);
|
||||
bool ragdollExists(int32_t node);
|
||||
bool ragdollIsActive(int32_t node);
|
||||
bool ragdollIsResting(int32_t node);
|
||||
bool ragdollNew(int32_t node);
|
||||
bool ragdollSetJoint(int32_t node, const char *joint, float radius, float swingDegrees, float twistDegrees);
|
||||
bool ragdollSetStrength(int32_t node, float strength);
|
||||
bool softDelete(int32_t node);
|
||||
bool softExists(int32_t node);
|
||||
bool softNew(int32_t node, SoftKindE kind);
|
||||
bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius);
|
||||
bool softPin(int32_t node, Vec3T point, int32_t follow);
|
||||
bool softSetDamping(int32_t node, float damping);
|
||||
bool softSetMass(int32_t node, float kilograms);
|
||||
bool softSetPressure(int32_t node, float pressure);
|
||||
bool softSetStiffness(int32_t node, float stretch, float bend);
|
||||
bool softUnpin(int32_t node, Vec3T point);
|
||||
int32_t vehicleAddWheel(int32_t node, int32_t wheelNode, float radius, float width, float suspension);
|
||||
bool vehicleDelete(int32_t node);
|
||||
bool vehicleDrive(int32_t node, float forward, float right, float brake, float handBrake);
|
||||
bool vehicleExists(int32_t node);
|
||||
int32_t vehicleGetGear(int32_t node);
|
||||
float vehicleGetRpm(int32_t node);
|
||||
float vehicleGetSpeed(int32_t node);
|
||||
float vehicleGetWheelSlip(int32_t node, int32_t index);
|
||||
bool vehicleIsWheelOnGround(int32_t node, int32_t index);
|
||||
bool vehicleNew(int32_t node, VehicleKindE kind);
|
||||
bool vehicleSetAntiRoll(int32_t node, float stiffness);
|
||||
bool vehicleSetBrakes(int32_t node, float brake, float handBrake);
|
||||
bool vehicleSetEngine(int32_t node, float maxTorque, float maxRpm, float minRpm);
|
||||
bool vehicleSetGears(int32_t node, const float *ratios, int32_t count, float reverse, bool automatic);
|
||||
bool vehicleSetSteering(int32_t node, float maxDegrees);
|
||||
bool vehicleSetRudder(int32_t node, float maxTorque);
|
||||
bool vehicleSetSuspension(int32_t node, float frequency, float damping);
|
||||
bool vehicleSetThrust(int32_t node, float maxForce, Vec3T point);
|
||||
bool vehicleSetWheel(int32_t node, int32_t index, bool steered, bool driven);
|
||||
int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum);
|
||||
bool physicsInit(void);
|
||||
void physicsQuit(void);
|
||||
|
|
|
|||
2764
src/physicsJolt.cpp
2764
src/physicsJolt.cpp
File diff suppressed because it is too large
Load diff
777
src/scene.c
777
src/scene.c
File diff suppressed because it is too large
Load diff
|
|
@ -97,12 +97,15 @@ int32_t meshBox(float width, float height, float depth);
|
|||
int32_t meshCone(float radius, float height, int32_t segments);
|
||||
int32_t meshCylinder(float radius, float height, int32_t segments);
|
||||
bool meshDelete(int32_t mesh);
|
||||
int32_t nodeGetMaterial(int32_t node);
|
||||
bool meshSetPositions(int32_t mesh, const float *positions);
|
||||
bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount);
|
||||
int32_t meshFindMorph(int32_t mesh, const char *name);
|
||||
int32_t meshGetMorphCount(int32_t mesh);
|
||||
const char *meshGetMorphName(int32_t mesh, int32_t target);
|
||||
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
|
||||
int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned);
|
||||
int32_t meshGrid(float width, float depth, int32_t columns, int32_t rows);
|
||||
int32_t meshPlane(float width, float depth);
|
||||
int32_t meshSphere(float radius, int32_t segments);
|
||||
int32_t meshTorus(float radius, float tubeRadius, int32_t segments);
|
||||
|
|
@ -128,6 +131,7 @@ bool nodeLookAt(int32_t node, Vec3T target);
|
|||
bool nodeMove(int32_t node, Vec3T delta);
|
||||
int32_t nodeNew(int32_t parent);
|
||||
bool nodeRotate(int32_t node, QuatT delta);
|
||||
bool nodeSetMaterial(int32_t node, int32_t material);
|
||||
bool nodeSetMesh(int32_t node, int32_t mesh, int32_t material);
|
||||
bool nodeSetMorphWeight(int32_t node, int32_t target, float weight);
|
||||
bool nodeSetName(int32_t node, const char *name);
|
||||
|
|
@ -135,7 +139,9 @@ bool nodeSetParent(int32_t node, int32_t parent);
|
|||
bool nodeSetPosition(int32_t node, Vec3T position);
|
||||
bool nodeSetRotation(int32_t node, QuatT rotation);
|
||||
bool nodeSetScale(int32_t node, Vec3T scale);
|
||||
int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints);
|
||||
bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, int32_t count);
|
||||
bool nodeSetShadow(int32_t node, bool casts);
|
||||
bool nodeSetVisible(int32_t node, bool visible);
|
||||
bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation);
|
||||
bool nodeValid(int32_t node);
|
||||
|
|
|
|||
|
|
@ -177,13 +177,11 @@ float shadowFactorMap(int slot, float3 worldPosition, float3 normal, float3 toLi
|
|||
// major axis and looked up with the same frame the engine rendered it with (forward, up, right =
|
||||
// forward x up, a 90 degree perspective), so no cube-map convention is involved; the point's own
|
||||
// projected depth is compared over five taps.
|
||||
float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lightPosition) {
|
||||
float base = shadowInfo[slot].y;
|
||||
float near = shadowInfo[slot].z;
|
||||
float far = shadowInfo[slot].w;
|
||||
float3 dir = (worldPosition + normal * shadowParams.x * 8.0) - lightPosition;
|
||||
// One tap of a point light's shadow: the face is picked from the direction's major axis and the
|
||||
// same frame the engine rendered it with is rebuilt, so a tap that crosses a face edge reads the
|
||||
// neighbouring face instead of the wrong texels.
|
||||
float cubeTap(float base, float near, float far, float bias, float3 dir) {
|
||||
float3 ad = abs(dir);
|
||||
float texel = shadowParams.y;
|
||||
float ma;
|
||||
float face;
|
||||
float3 forward;
|
||||
|
|
@ -191,7 +189,6 @@ float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lig
|
|||
float3 right;
|
||||
float2 uv;
|
||||
float depth;
|
||||
float lit;
|
||||
|
||||
if ((ad.x >= ad.y) && (ad.x >= ad.z)) {
|
||||
ma = ad.x;
|
||||
|
|
@ -211,15 +208,39 @@ float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lig
|
|||
}
|
||||
right = cross(forward, up);
|
||||
uv = float2(0.5 + 0.5 * dot(dir, right) / ma, 0.5 - 0.5 * dot(dir, up) / ma);
|
||||
depth = far / (far - near) - near * far / ((far - near) * ma) - shadowParams.x * 2.0;
|
||||
depth = far / (far - near) - near * far / ((far - near) * ma) - bias;
|
||||
if (depth > 1.0) {
|
||||
return 1.0;
|
||||
}
|
||||
lit = (shadowMaps.Sample(shadowSampler, float3(uv, base + face)).r >= depth) ? 1.0 : 0.0;
|
||||
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(texel, 0.0), base + face)).r >= depth) ? 1.0 : 0.0;
|
||||
lit += (shadowMaps.Sample(shadowSampler, float3(uv - float2(texel, 0.0), base + face)).r >= depth) ? 1.0 : 0.0;
|
||||
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(0.0, texel), base + face)).r >= depth) ? 1.0 : 0.0;
|
||||
lit += (shadowMaps.Sample(shadowSampler, float3(uv - float2(0.0, texel), base + face)).r >= depth) ? 1.0 : 0.0;
|
||||
return (shadowMaps.Sample(shadowSampler, float3(uv, base + face)).r >= depth) ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
|
||||
// How lit a point is by a point light's six shadow faces: five taps, each choosing its own face.
|
||||
// The normal offset grows with distance (a face's texel covers more world the farther it is) and
|
||||
// the depth bias with the grazing angle, so surfaces near the bulb stay acne-free.
|
||||
float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lightPosition) {
|
||||
float base = shadowInfo[slot].y;
|
||||
float near = shadowInfo[slot].z;
|
||||
float far = shadowInfo[slot].w;
|
||||
float texel = shadowParams.y;
|
||||
float3 toPoint = worldPosition - lightPosition;
|
||||
float distance = length(toPoint);
|
||||
float3 toLight = -toPoint / max(distance, 0.0001);
|
||||
float grazing = 1.0 - saturate(dot(normal, toLight));
|
||||
float texelWorld = 2.0 * distance * texel;
|
||||
float3 dir = toPoint + normal * texelWorld * (1.5 + 2.0 * grazing);
|
||||
float bias = shadowParams.x * (1.0 + 3.0 * grazing);
|
||||
float3 axisA = normalize(cross(dir, (abs(dir.y) < 0.9) ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0)));
|
||||
float3 axisB = normalize(cross(dir, axisA));
|
||||
float step = texelWorld;
|
||||
float lit;
|
||||
|
||||
lit = cubeTap(base, near, far, bias, dir);
|
||||
lit += cubeTap(base, near, far, bias, dir + axisA * step);
|
||||
lit += cubeTap(base, near, far, bias, dir - axisA * step);
|
||||
lit += cubeTap(base, near, far, bias, dir + axisB * step);
|
||||
lit += cubeTap(base, near, far, bias, dir - axisB * step);
|
||||
return lit / 5.0;
|
||||
}
|
||||
|
||||
|
|
@ -304,3 +325,48 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
|
|||
}
|
||||
return float4(result, albedo.a);
|
||||
}
|
||||
|
||||
|
||||
// ---- Particles: camera-facing billboards, unlit, one texture per draw ----
|
||||
|
||||
cbuffer ParticleUniforms : register(b0, space1) {
|
||||
float4x4 particleViewProjection;
|
||||
float4 cameraRight;
|
||||
float4 cameraUp;
|
||||
};
|
||||
|
||||
struct ParticleInput {
|
||||
float3 centre : TEXCOORD0;
|
||||
float2 corner : TEXCOORD1; // -1..1 across the quad
|
||||
float2 sizeAngle : TEXCOORD2; // x = size (world units), y = rotation in degrees
|
||||
float4 colour : TEXCOORD3;
|
||||
float2 uv : TEXCOORD4;
|
||||
};
|
||||
|
||||
struct ParticleOutput {
|
||||
float4 position : SV_Position;
|
||||
float4 colour : TEXCOORD0;
|
||||
float2 uv : TEXCOORD1;
|
||||
};
|
||||
|
||||
Texture2D<float4> particleTexture : register(t0, space2);
|
||||
SamplerState particleSampler : register(s0, space2);
|
||||
|
||||
ParticleOutput particleVertex(ParticleInput input) {
|
||||
ParticleOutput output;
|
||||
float radians = input.sizeAngle.y * 0.017453292;
|
||||
float c = cos(radians);
|
||||
float s = sin(radians);
|
||||
float2 offset = float2(input.corner.x * c - input.corner.y * s, input.corner.x * s + input.corner.y * c) * input.sizeAngle.x * 0.5;
|
||||
float3 world = input.centre + cameraRight.xyz * offset.x + cameraUp.xyz * offset.y;
|
||||
|
||||
output.position = mul(particleViewProjection, float4(world, 1.0));
|
||||
output.colour = input.colour;
|
||||
output.uv = input.uv;
|
||||
return output;
|
||||
}
|
||||
|
||||
float4 particleFragment(ParticleOutput input) : SV_Target {
|
||||
return particleTexture.Sample(particleSampler, input.uv) * input.colour;
|
||||
}
|
||||
|
||||
|
|
|
|||
1230
src/singe.c
1230
src/singe.c
File diff suppressed because it is too large
Load diff
BIN
testScripts/Models/Sponza.glb
(Stored with Git LFS)
Normal file
BIN
testScripts/Models/Sponza.glb
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
The Lua scripts used to verify the 3D scene and physics work, one per stage
|
||||
of PLAN.md sections 18 and 19, with everything they load: the Khronos
|
||||
Duck, Fox, BoxAnimated and AnimatedMorphCube sample models under Models/, Box.png and crate.png. The
|
||||
Duck, Fox, BoxAnimated, AnimatedMorphCube and Sponza sample models under Models/, Box.png and crate.png. The
|
||||
disc and the font are the engine's own, addressed as
|
||||
Singe/menuBackground.mkv and Singe/FreeSansBold.ttf like any engine
|
||||
asset. This directory is a complete Singe game directory. Run one from the singe directory with
|
||||
|
||||
.builddir/Singe-v3.00-Linux-x86_64 -w -d data -v Singe/menuBackground.mkv testScripts/scene6.singe
|
||||
|
||||
(scene12 has no disc: give it -C 720x480 instead of -v)
|
||||
(scene12 and scene20 have no disc: give them -C 720x480 instead of -v)
|
||||
|
||||
or pack it (singe --pack testScripts testScripts.game) and run an entry
|
||||
with --entry N (the numbers follow games.dat). Every script takes a
|
||||
|
|
@ -33,3 +33,14 @@ screenshot or two and quits by itself; the results are in screenshots/.
|
|||
| scene14.singe | 14 | - | The solid tangram dragon (Models/DragonModel.glb, a copy of assets/DragonModel.glb) turning under a sun with shadows |
|
||||
| scene15.singe | 15 | - | The Singe logotype as solid letters (Models/SingeText.glb, a copy of assets/SingeText.glb) turning under a sun with shadows |
|
||||
| scene16.singe | 16 | - | The dragon perched on the logotype, both models under one parent node, turning together |
|
||||
| scene17.singe | 17 | 20.1 | 2D particles: fireworks, a smoke trail, sparks off a 2D physics ball; runs with SDL_GPU_DRIVER=nothing |
|
||||
| scene18.singe | 18 | 20.2 | 3D particles: torch flame and smoke on a post, a fountain, dust, over the disc with shadows |
|
||||
| scene19.singe | 19 | 21.1 | Character controller: the Fox on a course of stairs, a ramp, a jump, a crate, a wall and a steep slope |
|
||||
| scene20.singe | 20 | 21.2 | Character controller in 2D: a sprite platformer (gap, step, wall, ledge); runs with SDL_GPU_DRIVER=nothing and -C 720x480 |
|
||||
| scene21.singe | 21 | 22.1 | Point-light shadows inside geometry: a bulb under a shade and a bulb in a cage, in a closed room of outward-facing boxes, inside and outside views |
|
||||
| scene22.singe | 22 | 23.1 | Vehicles: a car from primitives on a ramp track, driven by a scripted route with a chase camera |
|
||||
| scene23.singe | 23 | 23.2 | Vehicles: a tank that pivots and climbs, and a motorcycle that leans through a turn |
|
||||
| scene24.singe | 24 | 24.1 | Water: a pool and a river; crates float, sink and drift, the Fox swims across, a raft drives |
|
||||
| scene25.singe | 25 | 25.1 | Ragdolls: a running Fox crumples at a wall, one falls down stairs, one lies under motor strength |
|
||||
| scene26.singe | 26 | 26.1 | Soft bodies: a flag, a sheet catching a crate, a swinging rope, a balloon |
|
||||
| scene27.singe | 27 | - | The Khronos Sponza atrium (Models/Sponza.glb, 52 MB, packed with util/packGlb.py) under a sun with shadows, six braziers with flame, smoke and dust particles; no disc, -C 1280x720 |
|
||||
|
|
|
|||
|
|
@ -174,4 +174,123 @@ GAMES = {
|
|||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Particles2D",
|
||||
SCRIPT = "testScripts/scene17.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "2D particles: fireworks, a smoke trail and sparks off a physics ball; runs with no GPU.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Particles3D",
|
||||
SCRIPT = "testScripts/scene18.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "3D particles: a torch flame and smoke on a post, a fountain, dust in the air.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Player",
|
||||
SCRIPT = "testScripts/scene19.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "Character controller: the Fox walks stairs and a ramp, jumps, shoves a crate and stops at a steep slope.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Platformer",
|
||||
SCRIPT = "testScripts/scene20.singe",
|
||||
DESCRIPTION = "Character controller in 2D: a sprite platformer with a gap, a step, a wall and a ledge; no GPU needed.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "BulbInRoom",
|
||||
SCRIPT = "testScripts/scene21.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "Point-light shadows inside geometry: a bulb under a shade and a bulb in a cage, in a closed room with pillars, seen inside and out.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Car",
|
||||
SCRIPT = "testScripts/scene22.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "Vehicles: a car from primitives on a scripted route over a ramp, braking, turning and reversing.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "TankAndBike",
|
||||
SCRIPT = "testScripts/scene23.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "Vehicles: a six-wheeled tank that pivots on the spot and a motorcycle that leans through a turn.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Water",
|
||||
SCRIPT = "testScripts/scene24.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "Water: a pool and a river with a current; floating, sinking and drifting bodies, the Fox swimming, a raft under power.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Ragdolls",
|
||||
SCRIPT = "testScripts/scene25.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "Ragdolls: a running Fox crumples against a wall, another falls down stairs limp, a third lies under motor strength.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "SoftBodies",
|
||||
SCRIPT = "testScripts/scene26.singe",
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
DESCRIPTION = "Soft bodies: a flag on a pole, a sheet catching a crate, a swinging rope and a balloon under pressure.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
{
|
||||
TITLE = "Sponza",
|
||||
SCRIPT = "testScripts/scene27.singe",
|
||||
DESCRIPTION = "The Khronos Sponza atrium under a sun with shadows, on a camera walk.",
|
||||
YEAR = 2026,
|
||||
GENRE = "Test",
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Test",
|
||||
PUBLISHER = "Test",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
88
testScripts/scene17.singe
Normal file
88
testScripts/scene17.singe
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
-- Particles, 2D: fireworks bursting over the disc, a smoke trail following a moving point, and
|
||||
-- sparks off a bouncing 2D physics ball, all drawn with the overlay. Runs without a GPU too.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
local width = overlayGetWidth()
|
||||
local height = overlayGetHeight()
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
|
||||
-- Fireworks: a burst of bright additive sparks that fall and fade.
|
||||
local fireworks = emitterNew()
|
||||
emitterSetBlend(fireworks, PARTICLE_ADD)
|
||||
emitterSetLife(fireworks, 0.8, 1.6)
|
||||
emitterSetSpeed(fireworks, 120, 320)
|
||||
emitterSetSpread(fireworks, 180)
|
||||
emitterSetGravity(fireworks, 0, 220)
|
||||
emitterSetDrag(fireworks, 0.8)
|
||||
emitterSetSize(fireworks, 14, 3, 0.3)
|
||||
emitterSetColor(fireworks, 255, 230, 120, 255, 255, 60, 20, 0)
|
||||
emitterSetMax(fireworks, 2000)
|
||||
|
||||
-- Smoke: grey puffs drifting up from a moving point, drawn under the overlay text.
|
||||
local smoke = emitterNew()
|
||||
emitterSetLayer(smoke, PARTICLE_UNDER)
|
||||
emitterSetRate(smoke, 90)
|
||||
emitterSetLife(smoke, 1.2, 2.0)
|
||||
emitterSetSpeed(smoke, 30, 60)
|
||||
emitterSetDirection(smoke, 0, -1)
|
||||
emitterSetSpread(smoke, 25)
|
||||
emitterSetGravity(smoke, 0, -20)
|
||||
emitterSetSize(smoke, 12, 48, 0.2)
|
||||
emitterSetColor(smoke, 200, 200, 210, 160, 90, 90, 100, 0)
|
||||
emitterSetSpin(smoke, -60, 60)
|
||||
emitterSetRadius(smoke, 6)
|
||||
emitterStart(smoke)
|
||||
|
||||
-- Sparks: short hot streaks thrown off a ball bouncing in a 2D physics world.
|
||||
physicsSet2D(true)
|
||||
physicsSetGravity(0, 600, 0)
|
||||
local floor = nodeNew()
|
||||
nodeSetPosition(floor, width / 2, height - 10, 0)
|
||||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, width, 20, 50)
|
||||
local ball = nodeNew()
|
||||
nodeSetPosition(ball, width * 0.25, 80, 0)
|
||||
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 18)
|
||||
bodySetBounce(ball, 0.8)
|
||||
bodySetVelocity(ball, 140, 0, 0)
|
||||
local sparks = emitterNew()
|
||||
emitterSetBlend(sparks, PARTICLE_ADD)
|
||||
emitterSetRate(sparks, 160)
|
||||
emitterSetLife(sparks, 0.3, 0.7)
|
||||
emitterSetSpeed(sparks, 80, 200)
|
||||
emitterSetDirection(sparks, 0, -1)
|
||||
emitterSetSpread(sparks, 70)
|
||||
emitterSetGravity(sparks, 0, 500)
|
||||
emitterSetSize(sparks, 5, 1)
|
||||
emitterSetColor(sparks, 255, 255, 200, 255, 255, 120, 0, 0)
|
||||
emitterStart(sparks)
|
||||
|
||||
local ballSprite = spriteLoad("testScripts/crate.png")
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = frames / 60
|
||||
local bx = nodeGetPosition(ball)
|
||||
local by = select(2, nodeGetPosition(ball))
|
||||
frames = frames + 1
|
||||
overlayClear()
|
||||
-- The smoke source circles slowly.
|
||||
emitterSetPosition(smoke, width / 2 + 160 * math.cos(t), height / 2 + 60 * math.sin(t * 1.3))
|
||||
-- Sparks come from wherever the ball is.
|
||||
emitterSetPosition(sparks, bx, by + 14)
|
||||
if frames % 45 == 1 then
|
||||
emitterSetPosition(fireworks, 120 + (frames * 7) % (width - 240), 80 + (frames * 13) % 160)
|
||||
emitterBurst(fireworks, 260)
|
||||
end
|
||||
emitterDraw(smoke)
|
||||
emitterDraw(fireworks)
|
||||
emitterDraw(sparks)
|
||||
spriteDraw(ballSprite, bx - 18, by - 18, bx + 18, by + 18)
|
||||
fontPrint(20, 20, "Particles 2D, frame " .. frames .. " live " .. emitterGetCount(fireworks) + emitterGetCount(smoke) + emitterGetCount(sparks))
|
||||
if frames == 40 or frames == 110 or frames == 170 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
108
testScripts/scene18.singe
Normal file
108
testScripts/scene18.singe
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
-- Particles, 3D: a torch flame on a post, a fountain, and dust drifting in a sunbeam over the disc,
|
||||
-- with a crate to show the billboards depth-testing against geometry and casting no shadow.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(50, 50, 60)
|
||||
|
||||
local stone = materialNew()
|
||||
materialSetColor(stone, 110, 110, 120)
|
||||
materialSetRoughness(stone, 0.9)
|
||||
local wood = materialNew()
|
||||
materialSetColor(wood, 140, 90, 50)
|
||||
local floor = nodeNew()
|
||||
nodeSetMesh(floor, meshBox(12, 0.2, 8), stone)
|
||||
nodeSetPosition(floor, 0, -1.1, 0)
|
||||
local post = nodeNew()
|
||||
nodeSetMesh(post, meshCylinder(0.08, 2.2, 12), wood)
|
||||
nodeSetPosition(post, -2.5, 0, 0)
|
||||
local crate = nodeNew()
|
||||
nodeSetMesh(crate, meshBox(1, 1, 1), wood)
|
||||
nodeSetPosition(crate, 0.8, -0.5, 0.5)
|
||||
|
||||
-- The flame: additive, born in a small ball at the top of the post, rising and shrinking.
|
||||
local torchTip = nodeNew()
|
||||
nodeSetParent(torchTip, post)
|
||||
nodeSetPosition(torchTip, 0, 1.15, 0)
|
||||
local flame = emitterNew(torchTip)
|
||||
emitterSetBlend(flame, PARTICLE_ADD)
|
||||
emitterSetRate(flame, 140)
|
||||
emitterSetLife(flame, 0.4, 0.9)
|
||||
emitterSetSpeed(flame, 0.6, 1.4)
|
||||
emitterSetDirection(flame, 0, 1, 0)
|
||||
emitterSetSpread(flame, 15)
|
||||
emitterSetSize(flame, 0.35, 0.05, 0.3)
|
||||
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
|
||||
emitterSetRadius(flame, 0.06)
|
||||
emitterSetSpin(flame, -90, 90)
|
||||
emitterStart(flame)
|
||||
|
||||
-- Smoke above the flame, alpha blended and slow.
|
||||
local smoke = emitterNew(torchTip)
|
||||
emitterSetRate(smoke, 25)
|
||||
emitterSetLife(smoke, 1.5, 2.5)
|
||||
emitterSetSpeed(smoke, 0.5, 0.9)
|
||||
emitterSetDirection(smoke, 0.2, 1, 0)
|
||||
emitterSetSpread(smoke, 20)
|
||||
emitterSetSize(smoke, 0.2, 0.9, 0.2)
|
||||
emitterSetColor(smoke, 80, 80, 90, 120, 60, 60, 70, 0)
|
||||
emitterSetSpin(smoke, -30, 30)
|
||||
emitterStart(smoke)
|
||||
|
||||
-- A fountain: water drops thrown up and falling under gravity, staying where they were born.
|
||||
local spout = nodeNew()
|
||||
nodeSetPosition(spout, 2.5, -1, -0.5)
|
||||
local fountain = emitterNew(spout)
|
||||
emitterSetRate(fountain, 300)
|
||||
emitterSetLife(fountain, 1.0, 1.6)
|
||||
emitterSetSpeed(fountain, 4.0, 5.0)
|
||||
emitterSetDirection(fountain, 0, 1, 0)
|
||||
emitterSetSpread(fountain, 8)
|
||||
emitterSetGravity(fountain, 0, -9.8, 0)
|
||||
emitterSetSize(fountain, 0.08, 0.12)
|
||||
emitterSetColor(fountain, 150, 200, 255, 220, 150, 200, 255, 0)
|
||||
emitterSetMax(fountain, 1500)
|
||||
emitterStart(fountain)
|
||||
|
||||
-- Dust: a few slow motes in a box of air, following nothing.
|
||||
local air = nodeNew()
|
||||
nodeSetPosition(air, 0, 0.5, 0)
|
||||
local dust = emitterNew(air)
|
||||
emitterSetRate(dust, 20)
|
||||
emitterSetLife(dust, 4, 6)
|
||||
emitterSetSpeed(dust, 0.05, 0.2)
|
||||
emitterSetSpread(dust, 180)
|
||||
emitterSetSize(dust, 0.04, 0.04)
|
||||
emitterSetColor(dust, 255, 240, 200, 0, 255, 240, 200, 160)
|
||||
emitterSetRadius(dust, 2.5)
|
||||
emitterSetMax(dust, 200)
|
||||
emitterStart(dust)
|
||||
emitterBurst(dust, 120)
|
||||
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, -3, 6, 4)
|
||||
nodeLookAt(sun, 0, 0, 0)
|
||||
lightSetIntensity(sun, 1.4)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
nodeSetPosition(camera, 0, 1.2, 7)
|
||||
nodeLookAt(camera, 0, 0, 0)
|
||||
cameraSet(camera)
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
nodeRotate(post, 0, 0.5, 0)
|
||||
overlayClear()
|
||||
fontPrint(20, 20, "Particles 3D, frame " .. frames .. " flame " .. emitterGetCount(flame) .. " fountain " .. emitterGetCount(fountain))
|
||||
if frames == 60 or frames == 120 or frames == 170 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
121
testScripts/scene19.singe
Normal file
121
testScripts/scene19.singe
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
-- Character controller: the Fox as a player walking a course of stairs, ramps, a wall, a crate to
|
||||
-- shove and a moving platform, driven by a scripted route so it runs headless; the camera follows.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(70, 70, 80)
|
||||
|
||||
local stone = materialNew()
|
||||
materialSetColor(stone, 120, 120, 130)
|
||||
materialSetRoughness(stone, 0.9)
|
||||
local wood = materialNew()
|
||||
materialSetColor(wood, 150, 100, 50)
|
||||
local steel = materialNew()
|
||||
materialSetColor(steel, 90, 110, 150)
|
||||
materialSetMetallic(steel, 0.6)
|
||||
|
||||
local function block(x, y, z, w, h, d, material)
|
||||
local node = nodeNew()
|
||||
nodeSetMesh(node, meshBox(w, h, d), material)
|
||||
nodeSetPosition(node, x, y, z)
|
||||
bodyNew(node, BODY_STATIC, SHAPE_BOX, w, h, d)
|
||||
return node
|
||||
end
|
||||
|
||||
-- The course runs along +X. Floor, three stairs, a gentle ramp, a steep ramp, a wall to slide along.
|
||||
block(8, -0.1, 0, 30, 0.2, 8, stone)
|
||||
block(2.0, 0.075, 0, 0.6, 0.15, 4, wood)
|
||||
block(2.6, 0.15, 0, 0.6, 0.30, 4, wood)
|
||||
block(3.2, 0.225, 0, 0.6, 0.45, 4, wood)
|
||||
local ramp = nodeNew()
|
||||
nodeSetMesh(ramp, meshBox(3, 0.2, 4), stone)
|
||||
nodeSetPosition(ramp, 6.2, 0.7, 0)
|
||||
nodeSetRotation(ramp, 0, 0, 25)
|
||||
bodyNew(ramp, BODY_STATIC, SHAPE_BOX, 3, 0.2, 4)
|
||||
block(9.0, 0.7, 0, 2.6, 2.6, 4, stone) -- The top landing behind the ramp
|
||||
local steep = nodeNew()
|
||||
nodeSetMesh(steep, meshBox(2, 0.2, 4), stone)
|
||||
nodeSetPosition(steep, 11.0, 2.85, 0)
|
||||
nodeSetRotation(steep, 0, 0, 60)
|
||||
bodyNew(steep, BODY_STATIC, SHAPE_BOX, 2, 0.2, 4)
|
||||
block(9.0, 2.4, -2.5, 4, 1.2, 0.2, steel) -- A wall along the landing's far edge
|
||||
|
||||
-- A crate to shove and a platform that slides back and forth.
|
||||
local crate = nodeNew()
|
||||
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
|
||||
nodeSetPosition(crate, 4.6, 0.3, 0.15)
|
||||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||||
bodySetMass(crate, 5)
|
||||
local platform = nodeNew()
|
||||
nodeSetMesh(platform, meshBox(1.5, 0.2, 1.5), steel)
|
||||
nodeSetPosition(platform, 8.5, 2.1, 1.5)
|
||||
bodyNew(platform, BODY_KINEMATIC, SHAPE_BOX, 1.5, 0.2, 1.5)
|
||||
|
||||
-- The player: a capsule with the Fox model hanging off the node, facing +X.
|
||||
local player = nodeNew()
|
||||
nodeSetPosition(player, 0, 0.05, 0)
|
||||
playerNew(player, 0.3, 1.0)
|
||||
playerSetStep(player, 0.35)
|
||||
local foxModel = modelLoad("testScripts/Models/Fox.glb")
|
||||
local fox = modelInstance(foxModel)
|
||||
nodeSetParent(fox, player)
|
||||
nodeSetScale(fox, 0.01)
|
||||
nodeSetRotation(fox, 0, 90, 0)
|
||||
animationPlay(fox, "Walk", true)
|
||||
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, -3, 8, 5)
|
||||
nodeLookAt(sun, 6, 0, 0)
|
||||
lightSetIntensity(sun, 1.5)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
cameraSet(camera)
|
||||
|
||||
local jumped = false
|
||||
local landings = 0
|
||||
|
||||
function onCollision(a, b, x, y, z, speed)
|
||||
if a == player or b == player then
|
||||
landings = landings + 1
|
||||
end
|
||||
end
|
||||
|
||||
function onOverlayUpdate()
|
||||
local px, py, pz = nodeGetPosition(player)
|
||||
local t = frames / 60
|
||||
frames = frames + 1
|
||||
-- Route: walk east up the stairs and the ramp, veer to the wall, then onto the platform.
|
||||
local vz = 0
|
||||
if px > 7.5 and px < 10 then
|
||||
vz = (t < 6) and -1.2 or 1.2
|
||||
end
|
||||
playerMove(player, 3.0, vz)
|
||||
if px > 5.5 and not jumped and playerIsOnGround(player) then
|
||||
jumped = playerJump(player, 4.5)
|
||||
end
|
||||
nodeSetRotation(player, 0, math.deg(math.atan(vz, 3.0)) * -1, 0)
|
||||
-- The platform slides in Z.
|
||||
nodeSetPosition(platform, 8.5, 2.1, 1.5 + math.sin(t * 1.5) * 1.5)
|
||||
-- Camera behind and above the player.
|
||||
nodeSetPosition(camera, px - 4.5, py + 2.6, pz + 4.5)
|
||||
nodeLookAt(camera, px + 1, py + 0.6, pz)
|
||||
overlayClear()
|
||||
fontPrint(20, 20, string.format("Player, frame %d x %.1f y %.2f ground %s landings %d", frames, px, py, tostring(playerIsOnGround(player)), landings))
|
||||
if frames % 30 == 0 then
|
||||
local fx, fy, fz = nodeGetWorldPosition(fox)
|
||||
local vx, vy, vz = playerGetVelocity(player)
|
||||
local cx, cy, cz = nodeGetPosition(crate)
|
||||
debugPrint(string.format("frame %d player %.2f %.2f %.2f v %.2f %.2f %.2f ground %s jumped %s crate %.2f %.2f landings %d", frames, px, py, pz, vx, vy, vz, tostring(playerIsOnGround(player)), tostring(jumped), cx, cz, landings))
|
||||
end
|
||||
if frames == 40 or frames == 80 or frames == 120 or frames == 160 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
69
testScripts/scene20.singe
Normal file
69
testScripts/scene20.singe
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
-- Character controller in 2D: a platformer drawn on the overlay with sprites and boxes, no GPU
|
||||
-- needed. The player runs right, hops a gap, climbs a step, is stopped by a wall and jumps onto a
|
||||
-- ledge, on a scripted route; gravity points down the screen (+Y) like every 2D game here.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 24)
|
||||
local crate = spriteLoad("testScripts/crate.png")
|
||||
local frames = 0
|
||||
|
||||
overlaySetResolution(720, 480)
|
||||
local width, height = overlayGetWidth(), overlayGetHeight()
|
||||
|
||||
fontSelect(font)
|
||||
physicsSet2D(true)
|
||||
physicsSetGravity(0, 900, 0)
|
||||
|
||||
local blocks = {}
|
||||
local function block(x, y, w, h)
|
||||
local node = nodeNew()
|
||||
nodeSetPosition(node, x, y, 0)
|
||||
bodyNew(node, BODY_STATIC, SHAPE_BOX, w, h, 50)
|
||||
blocks[#blocks + 1] = { x = x, y = y, w = w, h = h }
|
||||
return node
|
||||
end
|
||||
|
||||
-- Ground with a gap, a low step, a wall, and a ledge to jump onto.
|
||||
local ground = height - 20
|
||||
block(width * 0.2, ground, width * 0.4, 20)
|
||||
block(width * 0.72, ground, width * 0.56, 20)
|
||||
block(width * 0.55, ground - 22, 60, 24)
|
||||
block(width * 0.85, ground - 60, 20, 100)
|
||||
block(width * 0.92, ground - 140, 140, 20)
|
||||
|
||||
local size = spriteGetWidth(crate)
|
||||
local player = nodeNew()
|
||||
nodeSetPosition(player, 40, ground - 10, 0)
|
||||
playerNew(player, size * 0.45, size * 1.4)
|
||||
playerSetStep(player, 26)
|
||||
|
||||
local hops = 0
|
||||
|
||||
function onOverlayUpdate()
|
||||
local px, py = nodeGetPosition(player)
|
||||
frames = frames + 1
|
||||
playerMove(player, (px < width * 0.9) and 220 or 0)
|
||||
-- Hop the gap, and later the wall, when the ground runs out ahead or the wall is near.
|
||||
if playerIsOnGround(player) and ((px > width * 0.36 and px < width * 0.42) or (px > width * 0.70 and px < width * 0.75)) then
|
||||
if playerJump(player, 560) then
|
||||
hops = hops + 1
|
||||
end
|
||||
end
|
||||
overlayClear()
|
||||
colorForeground(90, 90, 110)
|
||||
for _, b in ipairs(blocks) do
|
||||
overlayBox(b.x - b.w / 2, b.y - b.h / 2, b.x + b.w / 2, b.y + b.h / 2)
|
||||
end
|
||||
spriteDraw(crate, px - size / 2, py - size * 1.4, px + size / 2, py)
|
||||
colorForeground(255, 255, 255)
|
||||
fontPrint(20, 20, string.format("Platformer, frame %d x %.0f y %.0f ground %s hops %d", frames, px, py, tostring(playerIsOnGround(player)), hops))
|
||||
if frames % 10 == 0 then
|
||||
local vx, vy = playerGetVelocity(player)
|
||||
debugPrint(string.format("frame %d player %.0f %.0f v %.0f %.0f ground %s hops %d", frames, px, py, vx, vy, tostring(playerIsOnGround(player)), hops))
|
||||
end
|
||||
if frames == 40 or frames == 90 or frames == 140 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
return OVERLAY_UPDATED
|
||||
end
|
||||
131
testScripts/scene21.singe
Normal file
131
testScripts/scene21.singe
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
-- Point-light shadows inside geometry: two bulbs in a closed room built from ordinary boxes (their
|
||||
-- faces point outward, so from inside only back faces show), with pillars. One bulb hangs under a
|
||||
-- cone shade on a stand; the other sits inside a cage of bars and rings. Inside, the walls are lit
|
||||
-- with no seams, the shade throws its shadow on the ceiling and the cage its stripes across the
|
||||
-- corners; outside, the room stays dark.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(8, 8, 12)
|
||||
|
||||
local plaster = materialNew()
|
||||
materialSetColor(plaster, 200, 190, 170)
|
||||
materialSetRoughness(plaster, 0.95)
|
||||
materialSetDoubleSided(plaster, true)
|
||||
local brass = materialNew()
|
||||
materialSetColor(brass, 180, 140, 70)
|
||||
materialSetMetallic(brass, 0.7)
|
||||
materialSetRoughness(brass, 0.4)
|
||||
materialSetDoubleSided(brass, true)
|
||||
local glow = materialNew()
|
||||
materialSetColor(glow, 255, 240, 200)
|
||||
materialSetEmissive(glow, 255, 230, 170)
|
||||
materialSetUnlit(glow, true)
|
||||
|
||||
-- The room: six slabs 8 x 4 x 8 inside, all outward facing like any box.
|
||||
local function slab(x, y, z, w, h, d)
|
||||
local node = nodeNew()
|
||||
nodeSetMesh(node, meshBox(w, h, d), plaster)
|
||||
nodeSetPosition(node, x, y, z)
|
||||
return node
|
||||
end
|
||||
slab(0, -0.1, 0, 8.4, 0.2, 8.4)
|
||||
slab(0, 4.1, 0, 8.4, 0.2, 8.4)
|
||||
slab(-4.1, 2, 0, 0.2, 4.4, 8.4)
|
||||
slab(4.1, 2, 0, 0.2, 4.4, 8.4)
|
||||
slab(0, 2, -4.1, 8.4, 4.4, 0.2)
|
||||
slab(0, 2, 4.1, 8.4, 4.4, 0.2)
|
||||
|
||||
-- Pillars and a crate.
|
||||
for i, p in ipairs({ { -2.5, -2.5 }, { 2.5, -2.5 }, { -2.5, 2.5 }, { 2.5, 2.5 } }) do
|
||||
local pillar = nodeNew()
|
||||
nodeSetMesh(pillar, meshCylinder(0.25, 4, 16), plaster)
|
||||
nodeSetPosition(pillar, p[1], 2, p[2])
|
||||
end
|
||||
local crate = nodeNew()
|
||||
nodeSetMesh(crate, meshBox(0.8, 0.8, 0.8), brass)
|
||||
nodeSetPosition(crate, 2.2, 0.4, -1.8)
|
||||
|
||||
-- The lamp: a stand, a cone shade above the bulb, which hangs just below its rim so the shade
|
||||
-- throws a ring of shadow on the ceiling and the bulb lights everything below.
|
||||
local stand = nodeNew()
|
||||
nodeSetMesh(stand, meshCylinder(0.04, 1.4, 8), brass)
|
||||
nodeSetPosition(stand, -0.5, 0.7, -0.3)
|
||||
local shade = nodeNew()
|
||||
nodeSetMesh(shade, meshCone(0.45, 0.5, 24), brass)
|
||||
nodeSetPosition(shade, -0.5, 2.05, -0.3)
|
||||
local bulbMesh = nodeNew()
|
||||
nodeSetMesh(bulbMesh, meshSphere(0.08, 12), glow)
|
||||
nodeSetPosition(bulbMesh, -0.5, 1.55, -0.3)
|
||||
nodeSetShadow(bulbMesh, false) -- The bulb's own glass casts nothing.
|
||||
local bulb = lightNew(LIGHT_POINT)
|
||||
nodeSetPosition(bulb, -0.5, 1.55, -0.3)
|
||||
lightSetIntensity(bulb, 6)
|
||||
lightSetShadow(bulb, true)
|
||||
|
||||
-- The second lamp: a bulb inside a cage of eight bars and three rings of short segments, its own
|
||||
-- glass excused from casting. The bars are the harsh case: thin casters right beside the light.
|
||||
local cx, cy, cz = 2.2, 2.1, 1.6
|
||||
local function bar(x, y, z, rx, ry, rz, radius, h)
|
||||
local node = nodeNew()
|
||||
nodeSetMesh(node, meshCylinder(radius, h, 8), brass)
|
||||
nodeSetPosition(node, x, y, z)
|
||||
nodeSetRotation(node, rx, ry, rz)
|
||||
return node
|
||||
end
|
||||
for i = 0, 7 do
|
||||
local a = i / 8 * 2 * math.pi
|
||||
bar(cx + 0.35 * math.cos(a), cy, cz + 0.35 * math.sin(a), 0, 0, 0, 0.02, 1.0)
|
||||
end
|
||||
for _, dy in ipairs({ -0.5, 0, 0.5 }) do
|
||||
for i = 0, 7 do
|
||||
local a = (i + 0.5) / 8 * 2 * math.pi
|
||||
bar(cx + 0.35 * math.cos(a), cy + dy, cz + 0.35 * math.sin(a), 0, -math.deg(a), 90, 0.015, 0.28)
|
||||
end
|
||||
end
|
||||
local cagedMesh = nodeNew()
|
||||
nodeSetMesh(cagedMesh, meshSphere(0.06, 12), glow)
|
||||
nodeSetPosition(cagedMesh, cx, cy, cz)
|
||||
nodeSetShadow(cagedMesh, false)
|
||||
local caged = lightNew(LIGHT_POINT)
|
||||
nodeSetPosition(caged, cx, cy, cz)
|
||||
lightSetIntensity(caged, 4)
|
||||
lightSetColor(caged, 255, 230, 190)
|
||||
lightSetShadow(caged, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
cameraSet(camera)
|
||||
cameraSetPerspective(70, 0.05, 60)
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
overlayClear()
|
||||
if frames < 50 then
|
||||
-- Inside: from a corner, looking across the room at the shaded lamp, pillars and the far wall.
|
||||
nodeSetPosition(camera, 3.2 - frames * 0.006, 1.6, -3.2)
|
||||
nodeLookAt(camera, -1.5, 1.4, 1.5)
|
||||
elseif frames < 95 then
|
||||
-- Inside, looking up at the ceiling above the shade.
|
||||
nodeSetPosition(camera, 1.5, 0.8, -1.5)
|
||||
nodeLookAt(camera, -0.5, 3.8, -0.3)
|
||||
elseif frames < 140 then
|
||||
-- Under the cage, looking up at its stripes running over the ceiling corner.
|
||||
nodeSetPosition(camera, 1.6, 0.5, 1.0)
|
||||
nodeLookAt(camera, 3.6, 3.8, 3.6)
|
||||
else
|
||||
-- Outside: the closed room from above; nothing of either bulb may leak out.
|
||||
nodeSetPosition(camera, 9, 7, 9)
|
||||
nodeLookAt(camera, 0, 1.5, 0)
|
||||
end
|
||||
fontPrint(20, 20, "Bulbs in a room, frame " .. frames)
|
||||
if frames == 40 or frames == 85 or frames == 130 or frames == 175 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
110
testScripts/scene22.singe
Normal file
110
testScripts/scene22.singe
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
-- Vehicles: a car built from primitives (a box chassis with a dynamic body, four wheel nodes) on a
|
||||
-- track with a ramp, crates and a wall, driven by a scripted route: pull away, jump the ramp, brake,
|
||||
-- turn, and reverse into the crates. The camera chases.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
local turned = false
|
||||
local reversed = false
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(70, 70, 80)
|
||||
|
||||
local asphalt = materialNew()
|
||||
materialSetColor(asphalt, 70, 70, 75)
|
||||
materialSetRoughness(asphalt, 0.95)
|
||||
local paint = materialNew()
|
||||
materialSetColor(paint, 200, 40, 40)
|
||||
materialSetMetallic(paint, 0.5)
|
||||
materialSetRoughness(paint, 0.3)
|
||||
local rubber = materialNew()
|
||||
materialSetColor(rubber, 30, 30, 30)
|
||||
local wood = materialNew()
|
||||
materialSetColor(wood, 150, 100, 50)
|
||||
|
||||
local function block(x, y, z, w, h, d, material, rz)
|
||||
local node = nodeNew()
|
||||
nodeSetMesh(node, meshBox(w, h, d), material)
|
||||
nodeSetPosition(node, x, y, z)
|
||||
if rz then nodeSetRotation(node, rz, 0, 0) end
|
||||
bodyNew(node, BODY_STATIC, SHAPE_BOX, w, h, d)
|
||||
return node
|
||||
end
|
||||
|
||||
-- The track runs along -Z (the car's nose). A long floor, a ramp, a landing, crates and a wall.
|
||||
block(0, -0.1, -60, 40, 0.2, 200, asphalt)
|
||||
block(0, 0.45, -18, 6, 0.2, 5, asphalt, 12)
|
||||
block(0, 0.9, -23.4, 6, 0.2, 6, asphalt)
|
||||
for i = 0, 5 do
|
||||
local crate = nodeNew()
|
||||
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
|
||||
nodeSetPosition(crate, -3 + (i % 3) * 0.65, 0.3 + math.floor(i / 3) * 0.62, -36)
|
||||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||||
bodySetMass(crate, 4)
|
||||
end
|
||||
block(8, 1, -30, 0.3, 2, 12, asphalt)
|
||||
|
||||
-- The car: chassis 1.8 x 0.5 x 4.0, wheels as children whose X axis is the axle.
|
||||
local car = nodeNew()
|
||||
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
|
||||
nodeSetPosition(car, 0, 0.9, 0)
|
||||
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
|
||||
bodySetMass(car, 1500)
|
||||
vehicleNew(car, VEHICLE_CAR)
|
||||
local wheels = {}
|
||||
for i, p in ipairs({ { -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, { -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 } }) do
|
||||
local wheel = nodeNew()
|
||||
nodeSetParent(wheel, car)
|
||||
nodeSetPosition(wheel, p[1], p[2], p[3])
|
||||
local tyre = nodeNew()
|
||||
nodeSetParent(tyre, wheel)
|
||||
nodeSetMesh(tyre, meshCylinder(0.35, 0.25, 16), rubber)
|
||||
nodeSetRotation(tyre, 0, 0, 90)
|
||||
wheels[i] = vehicleAddWheel(car, wheel, 0.35, 0.25, 0.4)
|
||||
end
|
||||
vehicleSetEngine(car, 600, 6500, 1000)
|
||||
vehicleSetAntiRoll(car, 800)
|
||||
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, -5, 12, 6)
|
||||
nodeLookAt(sun, 0, 0, -15)
|
||||
lightSetIntensity(sun, 1.5)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
cameraSet(camera)
|
||||
cameraSetPerspective(60, 0.1, 200)
|
||||
|
||||
function onOverlayUpdate()
|
||||
local cx, cy, cz = nodeGetPosition(car)
|
||||
local t = frames / 60
|
||||
frames = frames + 1
|
||||
-- Route by distance, since physics runs on the clock: floor it over the ramp, then brake and
|
||||
-- steer, then reverse toward the crates, then stop.
|
||||
if cz > -30 and not turned then
|
||||
vehicleDrive(car, 1, 0, 0, 0)
|
||||
elseif not turned then
|
||||
vehicleDrive(car, 0, 0.7, 0.6, 0)
|
||||
if vehicleGetSpeed(car) < 2 then turned = true end
|
||||
elseif not reversed then
|
||||
vehicleDrive(car, -1, 0, 0, 0)
|
||||
if vehicleGetSpeed(car) < -6 then reversed = true end
|
||||
else
|
||||
vehicleDrive(car, 0, 0, 1, 1)
|
||||
end
|
||||
nodeSetPosition(camera, cx + 3, cy + 3, cz + 9)
|
||||
nodeLookAt(camera, cx, cy, cz - 3)
|
||||
overlayClear()
|
||||
fontPrint(20, 20, string.format("Car, frame %d speed %.1f m/s gear %d rpm %.0f", frames, vehicleGetSpeed(car), vehicleGetGear(car), vehicleGetRpm(car)))
|
||||
if frames % 30 == 0 then
|
||||
debugPrint(string.format("frame %d car %.1f %.2f %.1f speed %.1f gear %d rpm %.0f ground %s %s %s %s slip %.2f", frames, cx, cy, cz, vehicleGetSpeed(car), vehicleGetGear(car), vehicleGetRpm(car), tostring(vehicleIsWheelOnGround(car, 0)), tostring(vehicleIsWheelOnGround(car, 1)), tostring(vehicleIsWheelOnGround(car, 2)), tostring(vehicleIsWheelOnGround(car, 3)), vehicleGetWheelSlip(car, 2)))
|
||||
end
|
||||
if frames == 60 or frames == 120 or frames == 170 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
118
testScripts/scene23.singe
Normal file
118
testScripts/scene23.singe
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
-- Vehicles, second kind: a tank on six wheels that drives, pivots on the spot and climbs a slope,
|
||||
-- and a motorcycle that leans through a turn, both from primitives, side by side.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(70, 70, 80)
|
||||
|
||||
local ground = materialNew()
|
||||
materialSetColor(ground, 90, 110, 70)
|
||||
materialSetRoughness(ground, 0.95)
|
||||
local armour = materialNew()
|
||||
materialSetColor(armour, 80, 90, 70)
|
||||
local rubber = materialNew()
|
||||
materialSetColor(rubber, 30, 30, 30)
|
||||
local chrome = materialNew()
|
||||
materialSetColor(chrome, 200, 200, 210)
|
||||
materialSetMetallic(chrome, 0.8)
|
||||
materialSetRoughness(chrome, 0.2)
|
||||
|
||||
local floor = nodeNew()
|
||||
nodeSetMesh(floor, meshBox(160, 0.2, 160), ground)
|
||||
nodeSetPosition(floor, 0, -0.1, -30)
|
||||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 160, 0.2, 160)
|
||||
local slope = nodeNew()
|
||||
nodeSetMesh(slope, meshBox(8, 0.2, 8), ground)
|
||||
nodeSetPosition(slope, -6, 0.7, -24)
|
||||
nodeSetRotation(slope, 20, 0, 0)
|
||||
bodyNew(slope, BODY_STATIC, SHAPE_BOX, 8, 0.2, 8)
|
||||
|
||||
local function wheel(parent, x, y, z, radius, width)
|
||||
local node = nodeNew()
|
||||
nodeSetParent(node, parent)
|
||||
nodeSetPosition(node, x, y, z)
|
||||
local tyre = nodeNew()
|
||||
nodeSetParent(tyre, node)
|
||||
nodeSetMesh(tyre, meshCylinder(radius, width, 12), rubber)
|
||||
nodeSetRotation(tyre, 0, 0, 90)
|
||||
return node
|
||||
end
|
||||
|
||||
-- The tank: a heavy hull, three road wheels a side.
|
||||
local tank = nodeNew()
|
||||
nodeSetMesh(tank, meshBox(3, 1, 5), armour)
|
||||
nodeSetPosition(tank, -6, 1.2, 0)
|
||||
bodyNew(tank, BODY_DYNAMIC, SHAPE_BOX, 3, 1, 5)
|
||||
bodySetMass(tank, 8000)
|
||||
vehicleNew(tank, VEHICLE_TANK)
|
||||
for _, z in ipairs({ -1.8, 0, 1.8 }) do
|
||||
vehicleAddWheel(tank, wheel(tank, -1.7, -0.4, z, 0.5, 0.4), 0.5, 0.4, 0.5)
|
||||
vehicleAddWheel(tank, wheel(tank, 1.7, -0.4, z, 0.5, 0.4), 0.5, 0.4, 0.5)
|
||||
end
|
||||
vehicleSetEngine(tank, 1500, 4000, 800)
|
||||
|
||||
-- The motorcycle: a slim frame, one wheel each end, the rear driven.
|
||||
local bike = nodeNew()
|
||||
nodeSetMesh(bike, meshBox(0.3, 0.6, 1.6), chrome)
|
||||
nodeSetPosition(bike, 4, 0.9, 0)
|
||||
bodyNew(bike, BODY_DYNAMIC, SHAPE_BOX, 0.3, 0.6, 1.6)
|
||||
bodySetMass(bike, 250)
|
||||
vehicleNew(bike, VEHICLE_MOTORCYCLE)
|
||||
vehicleAddWheel(bike, wheel(bike, 0, -0.3, -0.8, 0.35, 0.12), 0.35, 0.12, 0.3)
|
||||
vehicleAddWheel(bike, wheel(bike, 0, -0.3, 0.8, 0.35, 0.12), 0.35, 0.12, 0.3)
|
||||
vehicleSetEngine(bike, 150, 9000, 1200)
|
||||
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, -5, 12, 6)
|
||||
nodeLookAt(sun, 0, 0, -15)
|
||||
lightSetIntensity(sun, 1.5)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
cameraSet(camera)
|
||||
cameraSetPerspective(60, 0.1, 200)
|
||||
|
||||
function onOverlayUpdate()
|
||||
local tx, ty, tz = nodeGetPosition(tank)
|
||||
local bx, by, bz = nodeGetPosition(bike)
|
||||
local t = frames / 60
|
||||
frames = frames + 1
|
||||
-- Tank: forward, then pivot, then on toward the slope. Bike: accelerate, then lean into a turn.
|
||||
if tz > -12 and frames < 100 then
|
||||
vehicleDrive(tank, 1, 0, 0, 0)
|
||||
elseif frames < 130 then
|
||||
vehicleDrive(tank, 0, 1, 0, 0)
|
||||
else
|
||||
vehicleDrive(tank, 1, 0, 0, 0)
|
||||
end
|
||||
if bz > -15 then
|
||||
vehicleDrive(bike, 1, 0, 0, 0)
|
||||
else
|
||||
vehicleDrive(bike, 0.4, 0.35, 0, 0)
|
||||
end
|
||||
-- The camera watches the tank for the first two thirds, then chases the bike.
|
||||
if frames < 125 then
|
||||
nodeSetPosition(camera, tx + 8, ty + 5, tz + 10)
|
||||
nodeLookAt(camera, tx, ty, tz)
|
||||
else
|
||||
nodeSetPosition(camera, bx + 6, by + 3, bz + 8)
|
||||
nodeLookAt(camera, bx, by, bz)
|
||||
end
|
||||
overlayClear()
|
||||
fontPrint(20, 20, string.format("Tank %.1f m/s bike %.1f m/s frame %d", vehicleGetSpeed(tank), vehicleGetSpeed(bike), frames))
|
||||
if frames % 30 == 0 then
|
||||
local _, ry = nodeGetRotation(tank)
|
||||
local _, _, roll = nodeGetRotation(bike)
|
||||
debugPrint(string.format("frame %d tank %.1f %.2f %.1f yaw %.0f speed %.1f | bike %.1f %.2f %.1f roll %.0f speed %.1f", frames, tx, ty, tz, ry, vehicleGetSpeed(tank), bx, by, bz, roll, vehicleGetSpeed(bike)))
|
||||
end
|
||||
if frames == 60 or frames == 120 or frames == 170 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
137
testScripts/scene24.singe
Normal file
137
testScripts/scene24.singe
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
-- Water: a pool that floats a light crate, sinks an anchor and bobs a ball; a river with a current
|
||||
-- that carries a crate; the Fox wading in and swimming across; a raft with a propeller and rudder.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(70, 70, 80)
|
||||
|
||||
local stone = materialNew()
|
||||
materialSetColor(stone, 130, 130, 140)
|
||||
materialSetRoughness(stone, 0.9)
|
||||
local wood = materialNew()
|
||||
materialSetColor(wood, 160, 110, 60)
|
||||
local iron = materialNew()
|
||||
materialSetColor(iron, 60, 60, 70)
|
||||
materialSetMetallic(iron, 0.8)
|
||||
local water = materialNew()
|
||||
materialSetColor(water, 60, 120, 200)
|
||||
materialSetBlend(water, true)
|
||||
materialSetRoughness(water, 0.2)
|
||||
local red = materialNew()
|
||||
materialSetColor(red, 220, 50, 50)
|
||||
|
||||
-- The ground, a pool cut into it (a deck round the water), and a river channel with a current.
|
||||
local ground = nodeNew()
|
||||
nodeSetMesh(ground, meshBox(30, 0.2, 30), stone)
|
||||
nodeSetPosition(ground, 0, -3.1, 0)
|
||||
bodyNew(ground, BODY_STATIC, SHAPE_BOX, 30, 0.2, 30)
|
||||
local deck = nodeNew()
|
||||
nodeSetMesh(deck, meshBox(4, 3, 12), stone)
|
||||
nodeSetPosition(deck, -6, -1.5, 0)
|
||||
bodyNew(deck, BODY_STATIC, SHAPE_BOX, 4, 3, 12)
|
||||
local pool = nodeNew()
|
||||
nodeSetMesh(pool, meshBox(8, 3, 12), water)
|
||||
nodeSetPosition(pool, 0, -1.5, 0)
|
||||
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
|
||||
bodySetWater(pool, 1.0, 0.6, 0.2)
|
||||
local river = nodeNew()
|
||||
nodeSetMesh(river, meshBox(6, 3, 12), water)
|
||||
nodeSetPosition(river, 7, -1.5, 0)
|
||||
bodyNew(river, BODY_STATIC, SHAPE_BOX, 6, 3, 12)
|
||||
bodySetWater(river, 1.0, 0.8, 0.2)
|
||||
bodySetCurrent(river, 0, 0, 2.5)
|
||||
|
||||
-- Things in the water: a crate that floats, an anchor that sinks, a ball, a crate in the river.
|
||||
local crate = nodeNew()
|
||||
nodeSetMesh(crate, meshBox(0.8, 0.8, 0.8), wood)
|
||||
nodeSetPosition(crate, -1, 1.5, 2.5)
|
||||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
||||
bodySetMass(crate, 20)
|
||||
bodySetBuoyancy(crate, 1.6)
|
||||
local anchor = nodeNew()
|
||||
nodeSetMesh(anchor, meshBox(0.5, 0.5, 0.5), iron)
|
||||
nodeSetPosition(anchor, 1.5, 1.5, -3)
|
||||
bodyNew(anchor, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
|
||||
bodySetMass(anchor, 200)
|
||||
bodySetBuoyancy(anchor, 0.3)
|
||||
local ball = nodeNew()
|
||||
nodeSetMesh(ball, meshSphere(0.4, 16), red)
|
||||
nodeSetPosition(ball, 2, 2.5, 2)
|
||||
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.4)
|
||||
bodySetMass(ball, 2)
|
||||
bodySetBuoyancy(ball, 2.0)
|
||||
local drift = nodeNew()
|
||||
nodeSetMesh(drift, meshBox(0.7, 0.7, 0.7), wood)
|
||||
nodeSetPosition(drift, 7, 0.5, -5)
|
||||
bodyNew(drift, BODY_DYNAMIC, SHAPE_BOX, 0.7, 0.7, 0.7)
|
||||
bodySetMass(drift, 10)
|
||||
bodySetBuoyancy(drift, 1.5)
|
||||
|
||||
-- The raft: a flat hull with the propeller at the stern.
|
||||
local raft = nodeNew()
|
||||
nodeSetMesh(raft, meshBox(1.6, 0.3, 2.4), wood)
|
||||
nodeSetPosition(raft, 1, 0.3, 4)
|
||||
bodyNew(raft, BODY_DYNAMIC, SHAPE_BOX, 1.6, 0.3, 2.4)
|
||||
bodySetMass(raft, 120)
|
||||
bodySetBuoyancy(raft, 3.0)
|
||||
vehicleNew(raft, VEHICLE_BOAT)
|
||||
vehicleSetThrust(raft, 600, 0, -0.1, 1.2)
|
||||
vehicleSetRudder(raft, 300)
|
||||
|
||||
-- The Fox wades in from the deck and swims across.
|
||||
local hero = nodeNew()
|
||||
nodeSetPosition(hero, -5, 0.05, -2)
|
||||
playerNew(hero, 0.3, 1.0)
|
||||
local fox = modelInstance(modelLoad("testScripts/Models/Fox.glb"))
|
||||
nodeSetParent(fox, hero)
|
||||
nodeSetScale(fox, 0.01)
|
||||
nodeSetRotation(fox, 0, 90, 0)
|
||||
animationPlay(fox, "Walk", true)
|
||||
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, -5, 10, 6)
|
||||
nodeLookAt(sun, 0, 0, 0)
|
||||
lightSetIntensity(sun, 1.5)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
nodeSetPosition(camera, 1, 6, 12)
|
||||
nodeLookAt(camera, 1, -0.5, 0)
|
||||
cameraSet(camera)
|
||||
|
||||
function onTrigger(trigger, other, entered)
|
||||
if other == hero then
|
||||
debugPrint(string.format("frame %d fox %s %s", frames, entered and "entered" or "left", (trigger == pool) and "the pool" or ((trigger == river) and "the river" or "a trigger")))
|
||||
end
|
||||
end
|
||||
|
||||
function onOverlayUpdate()
|
||||
local hx, hy, hz = nodeGetPosition(hero)
|
||||
frames = frames + 1
|
||||
if playerIsSwimming(hero) then
|
||||
playerMove(hero, 1.5, 0.6, 0)
|
||||
else
|
||||
playerMove(hero, 1.5, 0)
|
||||
end
|
||||
vehicleDrive(raft, 0.7, (frames > 30) and 0.9 or 0, 0)
|
||||
overlayClear()
|
||||
fontPrint(20, 20, string.format("Water, frame %d fox %s at %.1f %.2f raft %.1f m/s", frames, playerIsSwimming(hero) and "swimming" or "walking", hx, hy, vehicleGetSpeed(raft)))
|
||||
if frames % 30 == 0 then
|
||||
local cx, cy = nodeGetPosition(crate)
|
||||
local ax, ay = nodeGetPosition(anchor)
|
||||
local bx, by = nodeGetPosition(ball)
|
||||
local dx, dy, dz = nodeGetPosition(drift)
|
||||
local rx, ry, rz = nodeGetPosition(raft)
|
||||
debugPrint(string.format("frame %d fox %.1f %.2f %s | crate y %.2f anchor y %.2f ball y %.2f | drift z %.1f | raft %.1f %.2f %.1f speed %.1f", frames, hx, hy, tostring(playerIsSwimming(hero)), cy, ay, by, dz, rx, ry, rz, vehicleGetSpeed(raft)))
|
||||
end
|
||||
if frames == 50 or frames == 110 or frames == 170 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
105
testScripts/scene25.singe
Normal file
105
testScripts/scene25.singe
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
-- Ragdolls: one Fox runs at a wall and collapses on impact; another is dropped onto a flight of
|
||||
-- stairs limp; a third lies on the floor and gets up again under motor strength.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(70, 70, 80)
|
||||
|
||||
local stone = materialNew()
|
||||
materialSetColor(stone, 120, 120, 130)
|
||||
materialSetRoughness(stone, 0.9)
|
||||
local steel = materialNew()
|
||||
materialSetColor(steel, 90, 110, 150)
|
||||
|
||||
local function block(x, y, z, w, h, d, material)
|
||||
local node = nodeNew()
|
||||
nodeSetMesh(node, meshBox(w, h, d), material)
|
||||
nodeSetPosition(node, x, y, z)
|
||||
bodyNew(node, BODY_STATIC, SHAPE_BOX, w, h, d)
|
||||
return node
|
||||
end
|
||||
block(0, -0.1, 0, 20, 0.2, 14, stone)
|
||||
block(4, 0.75, -1, 0.3, 1.5, 3, steel) -- The wall the runner hits
|
||||
for i = 1, 5 do
|
||||
block(-4 - i * 0.5, i * 0.15, 3, 0.5, i * 0.3, 3, stone) -- Stairs going up to the left
|
||||
end
|
||||
|
||||
local foxModel = modelLoad("testScripts/Models/Fox.glb")
|
||||
|
||||
-- The runner: a player carrying the Fox; the ragdoll is made now and switched on at the crash.
|
||||
local runner = nodeNew()
|
||||
nodeSetPosition(runner, -1, 0.05, -1)
|
||||
playerNew(runner, 0.3, 0.9)
|
||||
local runnerFox = modelInstance(foxModel)
|
||||
nodeSetParent(runnerFox, runner)
|
||||
nodeSetScale(runnerFox, 0.01)
|
||||
nodeSetRotation(runnerFox, 0, 90, 0)
|
||||
animationPlay(runnerFox, "Run", true)
|
||||
ragdollNew(runnerFox)
|
||||
|
||||
-- The faller: dropped limp from above the stairs.
|
||||
local faller = modelInstance(foxModel)
|
||||
nodeSetPosition(faller, -6.5, 2.5, 3)
|
||||
nodeSetScale(faller, 0.01)
|
||||
nodeSetRotation(faller, 30, 40, 0)
|
||||
animationPlay(faller, "Survey", true)
|
||||
ragdollNew(faller)
|
||||
ragdollActivate(faller)
|
||||
|
||||
-- The riser: lying limp on the floor, then pulled back toward its standing pose.
|
||||
local riser = modelInstance(foxModel)
|
||||
nodeSetPosition(riser, 3, 0, 3.5)
|
||||
nodeSetScale(riser, 0.01)
|
||||
nodeSetRotation(riser, 0, -30, 0)
|
||||
animationPlay(riser, "Survey", true)
|
||||
ragdollNew(riser)
|
||||
ragdollActivate(riser)
|
||||
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, -4, 8, 6)
|
||||
nodeLookAt(sun, 0, 0, 0)
|
||||
lightSetIntensity(sun, 1.5)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
nodeSetPosition(camera, 0, 4, 9)
|
||||
nodeLookAt(camera, -1, 0.5, 0)
|
||||
cameraSet(camera)
|
||||
|
||||
local crashed = false
|
||||
|
||||
function onCollision(a, b, x, y, z, speed)
|
||||
if not crashed and (a == runner or b == runner) and speed > 1.5 then
|
||||
crashed = true
|
||||
playerSetEnabled(runner, false)
|
||||
ragdollActivate(runnerFox)
|
||||
ragdollApplyImpulse(runnerFox, "b_Spine02_03", 40, 30, 0)
|
||||
end
|
||||
end
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
if not crashed then
|
||||
playerMove(runner, 4, 0)
|
||||
end
|
||||
if frames == 100 then
|
||||
ragdollSetStrength(riser, 60)
|
||||
end
|
||||
overlayClear()
|
||||
fontPrint(20, 20, string.format("Ragdolls, frame %d %s faller %s riser %s", frames, crashed and "crashed" or "running", ragdollIsResting(faller) and "resting" or "moving", ragdollIsResting(riser) and "resting" or "moving"))
|
||||
if frames % 30 == 0 then
|
||||
local rx, ry, rz = nodeGetPosition(runner)
|
||||
local fx, fy, fz = nodeGetWorldPosition(faller)
|
||||
debugPrint(string.format("frame %d runner %.2f %.2f crashed %s | faller %.2f %.2f %.2f resting %s | riser resting %s", frames, rx, ry, tostring(crashed), fx, fy, fz, tostring(ragdollIsResting(faller)), tostring(ragdollIsResting(riser))))
|
||||
end
|
||||
if frames == 50 or frames == 110 or frames == 170 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
112
testScripts/scene26.singe
Normal file
112
testScripts/scene26.singe
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
-- Soft bodies: a flag pinned to a pole, a sheet pinned at four corners catching a crate, a rope
|
||||
-- swinging from a crane, and a balloon under pressure dropped onto the floor.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
discPlay()
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 0)
|
||||
sceneSetAmbient(70, 70, 80)
|
||||
|
||||
local stone = materialNew()
|
||||
materialSetColor(stone, 120, 120, 130)
|
||||
materialSetRoughness(stone, 0.9)
|
||||
local cloth = materialNew()
|
||||
materialSetColor(cloth, 220, 60, 60)
|
||||
materialSetDoubleSided(cloth, true)
|
||||
materialSetRoughness(cloth, 0.8)
|
||||
local linen = materialNew()
|
||||
materialSetColor(linen, 230, 225, 200)
|
||||
materialSetDoubleSided(linen, true)
|
||||
local wood = materialNew()
|
||||
materialSetColor(wood, 150, 100, 50)
|
||||
local hemp = materialNew()
|
||||
materialSetColor(hemp, 190, 160, 100)
|
||||
local rubber = materialNew()
|
||||
materialSetColor(rubber, 80, 180, 90)
|
||||
|
||||
local floor = nodeNew()
|
||||
nodeSetMesh(floor, meshBox(16, 0.2, 12), stone)
|
||||
nodeSetPosition(floor, 0, -0.1, 0)
|
||||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.2, 12)
|
||||
|
||||
-- The flag: a grid standing upright, pinned along its left edge to the pole.
|
||||
local pole = nodeNew()
|
||||
nodeSetMesh(pole, meshCylinder(0.04, 3, 8), wood)
|
||||
nodeSetPosition(pole, -5, 1.5, 0)
|
||||
bodyNew(pole, BODY_STATIC, SHAPE_CYLINDER, 0.04, 3)
|
||||
local flag = nodeNew()
|
||||
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), cloth)
|
||||
nodeSetPosition(flag, -4.16, 2.4, 0)
|
||||
nodeSetRotation(flag, 90, 0, 0)
|
||||
softNew(flag, SOFT_CLOTH)
|
||||
softSetStiffness(flag, 0.95, 0.1)
|
||||
softSetMass(flag, 0.5)
|
||||
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
|
||||
softPin(flag, -4.96, y, 0)
|
||||
end
|
||||
|
||||
-- The sheet: pinned at four corners, a crate dropped onto it.
|
||||
local sheet = nodeNew()
|
||||
nodeSetMesh(sheet, meshPlane(3, 3, 20, 20), linen)
|
||||
nodeSetPosition(sheet, 0, 1.6, 0)
|
||||
softNew(sheet, SOFT_CLOTH)
|
||||
softSetStiffness(sheet, 1.0, 0.05)
|
||||
softSetMass(sheet, 4)
|
||||
for _, c in ipairs({ { -1.5, -1.5 }, { 1.5, -1.5 }, { -1.5, 1.5 }, { 1.5, 1.5 } }) do
|
||||
softPin(sheet, c[1], 1.6, c[2])
|
||||
end
|
||||
local crate = nodeNew()
|
||||
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
|
||||
nodeSetPosition(crate, 0.2, 3.5, 0.1)
|
||||
nodeSetRotation(crate, 20, 30, 0)
|
||||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||||
bodySetMass(crate, 3)
|
||||
|
||||
-- The rope: from a crane arm out and down, swinging free from a horizontal start.
|
||||
local crane = nodeNew()
|
||||
nodeSetPosition(crane, 4, 4, -1)
|
||||
local rope = nodeNew()
|
||||
nodeSetPosition(rope, 4, 4, -1)
|
||||
softNew(rope, SOFT_ROPE, 6.5, 4, -1, 16, 0.04)
|
||||
nodeSetMaterial(rope, hemp)
|
||||
softSetMass(rope, 1.5)
|
||||
softPin(rope, 4, 4, -1, crane)
|
||||
|
||||
-- The balloon: a sphere kept round by pressure, dropped and squashed.
|
||||
local balloon = nodeNew()
|
||||
nodeSetMesh(balloon, meshSphere(0.5, 14), rubber)
|
||||
nodeSetPosition(balloon, 4, 2.5, 2)
|
||||
softNew(balloon, SOFT_BODY)
|
||||
softSetStiffness(balloon, 0.8, 0.3)
|
||||
softSetMass(balloon, 0.6)
|
||||
softSetPressure(balloon, 8)
|
||||
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, -4, 8, 6)
|
||||
nodeLookAt(sun, 0, 0, 0)
|
||||
lightSetIntensity(sun, 1.5)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
local camera = nodeNew()
|
||||
nodeSetPosition(camera, 0, 3.5, 9)
|
||||
nodeLookAt(camera, 0, 1.5, 0)
|
||||
cameraSet(camera)
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
overlayClear()
|
||||
fontPrint(20, 20, "Soft bodies, frame " .. frames)
|
||||
if frames % 30 == 0 then
|
||||
local cx, cy = nodeGetPosition(crate)
|
||||
local bx, by = nodeGetPosition(balloon)
|
||||
debugPrint(string.format("frame %d crate y %.2f balloon y %.2f", frames, cy, by))
|
||||
end
|
||||
if frames == 40 or frames == 100 or frames == 170 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
105
testScripts/scene27.singe
Normal file
105
testScripts/scene27.singe
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
-- The Khronos Sponza atrium, packed with util/packGlb.py from the glTF-Sample-Models 2.0 Sponza.
|
||||
-- A warm sun falls through the roof opening onto the atrium floor with hard shadows, braziers along the nave
|
||||
-- add pools of firelight with particle flames and smoke, and dust drifts in the sunbeams; the
|
||||
-- camera walks the ground floor, crosses the atrium, climbs to the gallery, then passes the columns.
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||
local frames = 0
|
||||
|
||||
fontSelect(font)
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(120, 150, 200, 255)
|
||||
sceneSetAmbient(52, 56, 74)
|
||||
sceneSetShadowSize(2048)
|
||||
|
||||
local model = modelLoad("testScripts/Models/Sponza.glb")
|
||||
local sponza = modelInstance(model)
|
||||
|
||||
-- The sun: high and warm so it falls through the roof opening onto the atrium floor.
|
||||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(sun, 5, 22, 4)
|
||||
nodeLookAt(sun, -2, 0, -1.5)
|
||||
lightSetIntensity(sun, 4.5)
|
||||
lightSetColor(sun, 255, 236, 205)
|
||||
lightSetShadow(sun, true)
|
||||
|
||||
-- Braziers: warm point lights along the nave, the two nearest the camera path casting shadows,
|
||||
-- each with a flame and a thread of smoke.
|
||||
local fire = materialNew()
|
||||
materialSetColor(fire, 60, 40, 30)
|
||||
local braziers = {}
|
||||
for i, p in ipairs({ { 6.5, 1.1, -2.2 }, { 6.5, 1.1, 2.2 }, { -6.5, 1.1, -2.2 }, { -6.5, 1.1, 2.2 }, { 0, 1.1, -2.4 }, { 0, 1.1, 2.4 } }) do
|
||||
local bowl = nodeNew()
|
||||
nodeSetMesh(bowl, meshCylinder(0.35, 0.25, 12), fire)
|
||||
nodeSetPosition(bowl, p[1], p[2] - 0.15, p[3])
|
||||
local glowNode = nodeNew()
|
||||
nodeSetPosition(glowNode, p[1], p[2] + 0.35, p[3])
|
||||
local lamp = lightNew(LIGHT_POINT)
|
||||
nodeSetParent(lamp, glowNode)
|
||||
lightSetColor(lamp, 255, 170, 80)
|
||||
lightSetIntensity(lamp, 3.4)
|
||||
lightSetRange(lamp, 12)
|
||||
lightSetShadow(lamp, i <= 2)
|
||||
local flame = emitterNew(glowNode)
|
||||
emitterSetBlend(flame, PARTICLE_ADD)
|
||||
emitterSetRate(flame, 60)
|
||||
emitterSetLife(flame, 0.4, 0.8)
|
||||
emitterSetSpeed(flame, 0.5, 1.1)
|
||||
emitterSetDirection(flame, 0, 1, 0)
|
||||
emitterSetSpread(flame, 18)
|
||||
emitterSetSize(flame, 0.22, 0.04, 0.15)
|
||||
emitterSetColor(flame, 255, 190, 90, 255, 255, 60, 0, 0)
|
||||
emitterSetRadius(flame, 0.1)
|
||||
emitterStart(flame)
|
||||
local smoke = emitterNew(glowNode)
|
||||
emitterSetRate(smoke, 12)
|
||||
emitterSetLife(smoke, 2.5, 4)
|
||||
emitterSetSpeed(smoke, 0.4, 0.7)
|
||||
emitterSetDirection(smoke, 0, 1, 0)
|
||||
emitterSetSpread(smoke, 12)
|
||||
emitterSetSize(smoke, 0.15, 0.8, 0.15)
|
||||
emitterSetColor(smoke, 90, 80, 75, 90, 50, 45, 45, 0)
|
||||
emitterSetSpin(smoke, -25, 25)
|
||||
emitterStart(smoke)
|
||||
braziers[i] = glowNode
|
||||
end
|
||||
|
||||
-- Dust in the sunbeams over the atrium floor.
|
||||
local air = nodeNew()
|
||||
nodeSetPosition(air, 0, 3, 0)
|
||||
local dust = emitterNew(air)
|
||||
emitterSetRate(dust, 40)
|
||||
emitterSetLife(dust, 5, 8)
|
||||
emitterSetSpeed(dust, 0.05, 0.25)
|
||||
emitterSetSpread(dust, 180)
|
||||
emitterSetSize(dust, 0.05, 0.05)
|
||||
emitterSetColor(dust, 255, 240, 210, 0, 255, 240, 210, 190)
|
||||
emitterSetRadius(dust, 5)
|
||||
emitterSetMax(dust, 400)
|
||||
emitterStart(dust)
|
||||
emitterBurst(dust, 300)
|
||||
|
||||
local camera = nodeNew()
|
||||
cameraSet(camera)
|
||||
cameraSetPerspective(65, 0.05, 100)
|
||||
|
||||
local stops = {
|
||||
{ 10, 1.6, 0, -10, 2.5, 0 }, -- Ground floor, along the nave past the braziers
|
||||
{ 1.2, 1.6, 5.2, -1, 3.5, -5 }, -- Across the atrium toward the far arcade
|
||||
{ -9, 5.5, -1, 6, 3, 1 }, -- From the upper gallery
|
||||
{ 5, 1.2, -1.5, -8, 2, 1.5 }, -- Low, past the columns and a brazier
|
||||
}
|
||||
|
||||
function onOverlayUpdate()
|
||||
local stop = stops[math.min(#stops, math.floor(frames / 45) + 1)]
|
||||
frames = frames + 1
|
||||
nodeSetPosition(camera, stop[1], stop[2], stop[3])
|
||||
nodeLookAt(camera, stop[4], stop[5], stop[6])
|
||||
overlayClear()
|
||||
fontPrint(20, 20, "Sponza, frame " .. frames)
|
||||
if frames == 40 or frames == 85 or frames == 130 or frames == 175 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 180 then
|
||||
singeQuit()
|
||||
end
|
||||
end
|
||||
14
thirdparty/openssl/Configure
vendored
14
thirdparty/openssl/Configure
vendored
|
|
@ -2469,12 +2469,16 @@ EOF
|
|||
my $i; my $i2; my $m; my $d; my $d2;
|
||||
if ($unified_info{generate}->{$ddest}
|
||||
&& $f =~ m/^(.*?)\|(.*)$/) {
|
||||
$i = $1;
|
||||
# Singe: resolve every path from the inclusion path as written. Reusing $i
|
||||
# after cleanfile() made it relative to the build directory sent the source
|
||||
# tree lookups through "../.." and created util/perl outside the tree when
|
||||
# the build directory is nested two levels inside it.
|
||||
my $inc = $1;
|
||||
$m = $2;
|
||||
$i = cleanfile($sourced, $i, $blddir);
|
||||
$i2 = cleanfile($buildd, $i, $blddir);
|
||||
$d = cleanfile($sourced, "$i/$m", $blddir);
|
||||
$d2 = cleanfile($buildd, "$i/$m", $blddir);
|
||||
$i = cleanfile($sourced, $inc, $blddir);
|
||||
$i2 = cleanfile($buildd, $inc, $blddir);
|
||||
$d = cleanfile($sourced, "$inc/$m", $blddir);
|
||||
$d2 = cleanfile($buildd, "$inc/$m", $blddir);
|
||||
} else {
|
||||
$d = cleanfile($sourced, $f, $blddir);
|
||||
$d2 = cleanfile($buildd, $f, $blddir);
|
||||
|
|
|
|||
37
util/packGlb.py
Normal file
37
util/packGlb.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Packs a .gltf with external buffers and images into one self-contained .glb, which is the only
|
||||
# model format Singe loads. Usage: python3 util/packGlb.py Model.gltf Model.glb
|
||||
import json, os, struct, sys
|
||||
src, out = sys.argv[1], sys.argv[2]
|
||||
base = os.path.dirname(src)
|
||||
g = json.load(open(src))
|
||||
binary = bytearray()
|
||||
views = g.setdefault('bufferViews', [])
|
||||
def add(data):
|
||||
while len(binary) % 4: binary.append(0)
|
||||
off = len(binary); binary.extend(data)
|
||||
return off
|
||||
# Buffers: concatenate, remapping views.
|
||||
offsets = []
|
||||
for b in g.get('buffers', []):
|
||||
data = open(os.path.join(base, b['uri']), 'rb').read()
|
||||
offsets.append(add(data))
|
||||
for v in views:
|
||||
v['byteOffset'] = v.get('byteOffset', 0) + offsets[v.get('buffer', 0)]
|
||||
v['buffer'] = 0
|
||||
# Images: each becomes a buffer view with a mime type.
|
||||
for img in g.get('images', []):
|
||||
uri = img.pop('uri')
|
||||
data = open(os.path.join(base, uri), 'rb').read()
|
||||
ext = uri.lower().rsplit('.', 1)[-1]
|
||||
views.append({'buffer': 0, 'byteOffset': add(data), 'byteLength': len(data)})
|
||||
img['bufferView'] = len(views) - 1
|
||||
img['mimeType'] = 'image/png' if ext == 'png' else 'image/jpeg'
|
||||
while len(binary) % 4: binary.append(0)
|
||||
g['buffers'] = [{'byteLength': len(binary)}]
|
||||
js = json.dumps(g, separators=(',', ':')).encode()
|
||||
while len(js) % 4: js += b' '
|
||||
with open(out, 'wb') as f:
|
||||
f.write(struct.pack('<4sII', b'glTF', 2, 12 + 8 + len(js) + 8 + len(binary)))
|
||||
f.write(struct.pack('<II', len(js), 0x4E4F534A)); f.write(js)
|
||||
f.write(struct.pack('<II', len(binary), 0x004E4942)); f.write(binary)
|
||||
print(out, os.path.getsize(out), 'bytes')
|
||||
Loading…
Add table
Reference in a new issue