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/
|
docs/.asciidoctor/
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
/util/perl/
|
|
||||||
|
|
||||||
# Extracted by the binary when it is run from here; never source.
|
# Extracted by the binary when it is run from here; never source.
|
||||||
/Singe/
|
/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
|
SSE4.1 on x86 (2008 and later); the engine's first C++ (one wrapper
|
||||||
file and the library), statically linked, no runtime to install.
|
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
|
- 3D scenes. A game can draw a 3D scene between the disc video and the
|
||||||
overlay: primitive meshes and script-built geometry, materials with
|
overlay: primitive meshes and script-built geometry, materials with
|
||||||
colour, textures from sprites, the disc or a loaded video, metallic and
|
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:
|
targets, placed any number of times. Everything is a node in one tree. New calls:
|
||||||
scene*, node*, mesh*, material*, light*, camera*, model*, animation*,
|
scene*, node*, mesh*, material*, light*, camera*, model*, animation*,
|
||||||
and the LIGHT_* constants; see the 3D Scenes chapter of the manual.
|
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
|
Needs a GPU with Vulkan, Direct3D 12 or Metal; the Raspberry Pi 4 is
|
||||||
later); 2D games run as before without one. The renderer now runs on
|
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.
|
SDL's GPU device where there is one.
|
||||||
|
|
||||||
- A game can be one file: singe --pack DIRECTORY GAME.game writes the game's
|
- 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/main.h
|
||||||
src/pack.c
|
src/pack.c
|
||||||
src/pack.h
|
src/pack.h
|
||||||
|
src/particles.c
|
||||||
|
src/particles.h
|
||||||
src/physics.h
|
src/physics.h
|
||||||
src/physicsJolt.cpp
|
src/physicsJolt.cpp
|
||||||
src/model.c
|
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.
|
that path yourself, or build it from source there.
|
||||||
|
|
||||||
The Raspberry Pi build (64-bit Raspberry Pi OS, glibc 2.31 or newer)
|
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,
|
Debian bookworm arm64 packages listed in cmake/zig/piPackages.cmake,
|
||||||
fetched from snapshot.debian.org and unpacked with dpkg-deb into
|
fetched from snapshot.debian.org and unpacked with dpkg-deb into
|
||||||
.builddir/toolchains/sysroot-aarch64-linux-gnu on first use:
|
.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
|
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
|
# 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.
|
# 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)
|
set(formats SPIRV DXIL MSL)
|
||||||
get_filename_component(sourceDir ${SOURCE} DIRECTORY)
|
get_filename_component(sourceDir ${SOURCE} DIRECTORY)
|
||||||
get_filename_component(outputDir ${OUTPUT} 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;
|
} 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 bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at);
|
||||||
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at);
|
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at);
|
||||||
bool bodyDelete(int32_t node);
|
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 bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c);
|
||||||
bool bodySetAngularVelocity(int32_t node, Vec3T velocity);
|
bool bodySetAngularVelocity(int32_t node, Vec3T velocity);
|
||||||
bool bodySetBounce(int32_t node, float bounce);
|
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 bodySetEnabled(int32_t node, bool enabled);
|
||||||
bool bodySetFriction(int32_t node, float friction);
|
bool bodySetFriction(int32_t node, float friction);
|
||||||
bool bodySetMass(int32_t node, float kilograms);
|
bool bodySetMass(int32_t node, float kilograms);
|
||||||
bool bodySetTrigger(int32_t node, bool trigger);
|
bool bodySetTrigger(int32_t node, bool trigger);
|
||||||
bool bodySetVelocity(int32_t node, Vec3T velocity);
|
bool bodySetVelocity(int32_t node, Vec3T velocity);
|
||||||
|
bool bodySetWater(int32_t node, float density, float linearDrag, float angularDrag);
|
||||||
bool jointDelete(int32_t joint);
|
bool jointDelete(int32_t joint);
|
||||||
int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Vec3T axis);
|
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 jointSetLimits(int32_t joint, float low, float high);
|
||||||
bool jointValid(int32_t joint);
|
bool jointValid(int32_t joint);
|
||||||
bool physicsAvailable(void);
|
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);
|
int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum);
|
||||||
bool physicsInit(void);
|
bool physicsInit(void);
|
||||||
void physicsQuit(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 meshCone(float radius, float height, int32_t segments);
|
||||||
int32_t meshCylinder(float radius, float height, int32_t segments);
|
int32_t meshCylinder(float radius, float height, int32_t segments);
|
||||||
bool meshDelete(int32_t mesh);
|
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);
|
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 meshFindMorph(int32_t mesh, const char *name);
|
||||||
int32_t meshGetMorphCount(int32_t mesh);
|
int32_t meshGetMorphCount(int32_t mesh);
|
||||||
const char *meshGetMorphName(int32_t mesh, int32_t target);
|
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 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 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 meshPlane(float width, float depth);
|
||||||
int32_t meshSphere(float radius, int32_t segments);
|
int32_t meshSphere(float radius, int32_t segments);
|
||||||
int32_t meshTorus(float radius, float tubeRadius, 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);
|
bool nodeMove(int32_t node, Vec3T delta);
|
||||||
int32_t nodeNew(int32_t parent);
|
int32_t nodeNew(int32_t parent);
|
||||||
bool nodeRotate(int32_t node, QuatT delta);
|
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 nodeSetMesh(int32_t node, int32_t mesh, int32_t material);
|
||||||
bool nodeSetMorphWeight(int32_t node, int32_t target, float weight);
|
bool nodeSetMorphWeight(int32_t node, int32_t target, float weight);
|
||||||
bool nodeSetName(int32_t node, const char *name);
|
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 nodeSetPosition(int32_t node, Vec3T position);
|
||||||
bool nodeSetRotation(int32_t node, QuatT rotation);
|
bool nodeSetRotation(int32_t node, QuatT rotation);
|
||||||
bool nodeSetScale(int32_t node, Vec3T scale);
|
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 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 nodeSetVisible(int32_t node, bool visible);
|
||||||
bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation);
|
bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation);
|
||||||
bool nodeValid(int32_t node);
|
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 =
|
// 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
|
// 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.
|
// projected depth is compared over five taps.
|
||||||
float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lightPosition) {
|
// One tap of a point light's shadow: the face is picked from the direction's major axis and the
|
||||||
float base = shadowInfo[slot].y;
|
// same frame the engine rendered it with is rebuilt, so a tap that crosses a face edge reads the
|
||||||
float near = shadowInfo[slot].z;
|
// neighbouring face instead of the wrong texels.
|
||||||
float far = shadowInfo[slot].w;
|
float cubeTap(float base, float near, float far, float bias, float3 dir) {
|
||||||
float3 dir = (worldPosition + normal * shadowParams.x * 8.0) - lightPosition;
|
|
||||||
float3 ad = abs(dir);
|
float3 ad = abs(dir);
|
||||||
float texel = shadowParams.y;
|
|
||||||
float ma;
|
float ma;
|
||||||
float face;
|
float face;
|
||||||
float3 forward;
|
float3 forward;
|
||||||
|
|
@ -191,7 +189,6 @@ float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lig
|
||||||
float3 right;
|
float3 right;
|
||||||
float2 uv;
|
float2 uv;
|
||||||
float depth;
|
float depth;
|
||||||
float lit;
|
|
||||||
|
|
||||||
if ((ad.x >= ad.y) && (ad.x >= ad.z)) {
|
if ((ad.x >= ad.y) && (ad.x >= ad.z)) {
|
||||||
ma = ad.x;
|
ma = ad.x;
|
||||||
|
|
@ -211,15 +208,39 @@ float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lig
|
||||||
}
|
}
|
||||||
right = cross(forward, up);
|
right = cross(forward, up);
|
||||||
uv = float2(0.5 + 0.5 * dot(dir, right) / ma, 0.5 - 0.5 * dot(dir, up) / ma);
|
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) {
|
if (depth > 1.0) {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
lit = (shadowMaps.Sample(shadowSampler, float3(uv, base + face)).r >= depth) ? 1.0 : 0.0;
|
return (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;
|
// 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;
|
return lit / 5.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,3 +325,48 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
|
||||||
}
|
}
|
||||||
return float4(result, albedo.a);
|
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
|
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
|
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
|
disc and the font are the engine's own, addressed as
|
||||||
Singe/menuBackground.mkv and Singe/FreeSansBold.ttf like any engine
|
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
|
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
|
.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
|
or pack it (singe --pack testScripts testScripts.game) and run an entry
|
||||||
with --entry N (the numbers follow games.dat). Every script takes a
|
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 |
|
| 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 |
|
| 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 |
|
| 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",
|
DEVELOPER = "Test",
|
||||||
PUBLISHER = "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;
|
my $i; my $i2; my $m; my $d; my $d2;
|
||||||
if ($unified_info{generate}->{$ddest}
|
if ($unified_info{generate}->{$ddest}
|
||||||
&& $f =~ m/^(.*?)\|(.*)$/) {
|
&& $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;
|
$m = $2;
|
||||||
$i = cleanfile($sourced, $i, $blddir);
|
$i = cleanfile($sourced, $inc, $blddir);
|
||||||
$i2 = cleanfile($buildd, $i, $blddir);
|
$i2 = cleanfile($buildd, $inc, $blddir);
|
||||||
$d = cleanfile($sourced, "$i/$m", $blddir);
|
$d = cleanfile($sourced, "$inc/$m", $blddir);
|
||||||
$d2 = cleanfile($buildd, "$i/$m", $blddir);
|
$d2 = cleanfile($buildd, "$inc/$m", $blddir);
|
||||||
} else {
|
} else {
|
||||||
$d = cleanfile($sourced, $f, $blddir);
|
$d = cleanfile($sourced, $f, $blddir);
|
||||||
$d2 = cleanfile($buildd, $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