diff --git a/CHANGELOG b/CHANGELOG index eec3f485e..30e014040 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -135,7 +135,8 @@ API Changes threshold glow, blurred over a half-size chain before the tone curve. viewNew renders a second camera to a texture every frame (viewSetCamera points it) that materialSetView shows on any surface: monitors, - mirrors, portals. + mirrors, portals. Each view turns billboards to its own camera and + draws transparency back to front from it. - Particle extras. Trails (emitterSetTrail: a fading ribbon behind each particle, 2D and 3D), collisions with a floor or, by ray cast, diff --git a/docs/Manual.adoc b/docs/Manual.adoc index 65796c2a2..831535300 100644 --- a/docs/Manual.adoc +++ b/docs/Manual.adoc @@ -1023,9 +1023,10 @@ a material: a security monitor, a rear-view mirror, a portal, a picture in picture. `viewNew(width, height)` makes one at that resolution, `viewSetCamera` gives it a camera node, and `materialSetView` puts its picture on any mesh. Views share the frame's shadows and skip bloom, and -each one renders the scene again, so keep them few and small. A billboard -(see <>) faces the window's -camera, so a view sees it from the side. +each one renders the scene again, so keep them few and small. Each view +turns billboards (see <>) to +its own camera and draws transparent materials back to front from it, so +a monitor sees name tags face on and glass in the right order. [source,lua] ---- @@ -7820,8 +7821,8 @@ world up only, so the node stays upright (trees, health bars); `BILLBOARD_NONE`, the default, does not turn at all. The node's world position and scale are kept and its own rotation is ignored while a mode is set. Any other value aborts the script. It works on meshes as well as -sprites and text, and follows the window's camera: a view sees the -billboard from the side. +sprites and text, and turns to whichever camera is drawing: a view sees +the billboard face on too, while its shadow follows the window's camera. *Since:* 3.00. *See also:* <>, <>, <> @@ -12541,7 +12542,7 @@ end [#view] === View -A view is a second camera rendered to a texture every frame, for a monitor, a mirror or a portal in the scene: `viewNew` makes one and returns an integer handle, `viewSetCamera` points it at a node, and `materialSetView` shows it on a material. Up to four exist at once; each renders the whole scene again at its own size, in overlay-independent pixels, with the main camera's projection, the frame's shadows and no bloom, so keep them few and small. Billboards (`nodeSetBillboard`) are turned to face the window's camera and blended draws are sorted back to front from it, once for every view, so a view whose camera looks from elsewhere sees billboards side on and may see transparent objects overlap in the wrong order. A bad handle raises an error. See <>. +A view is a second camera rendered to a texture every frame, for a monitor, a mirror or a portal in the scene: `viewNew` makes one and returns an integer handle, `viewSetCamera` points it at a node, and `materialSetView` shows it on a material. Up to four exist at once; each renders the whole scene again at its own size, in overlay-independent pixels, with the main camera's projection, the frame's shadows and no bloom, so keep them few and small. Every view turns billboards (`nodeSetBillboard`) to its own camera and sorts blended draws back to front from it, so a view whose camera looks from elsewhere still sees billboards face on and transparent objects in the right order. A bad handle raises an error. See <>. [#viewdelete] ==== viewDelete diff --git a/src/hdr.c b/src/hdr.c index f31752922..f93ca41fc 100644 --- a/src/hdr.c +++ b/src/hdr.c @@ -41,9 +41,9 @@ static bool _isRadiance(const uint8_t *bytes, size_t size); +static float _linear(uint8_t value); static float *_loadRadiance(const uint8_t *bytes, size_t size, int32_t *width, int32_t *height); static float *_loadSurface(SDL_Surface *surface, int32_t *width, int32_t *height); -static float _linear(uint8_t value); static bool _readScanline(const uint8_t *bytes, size_t size, size_t *offset, uint8_t *rgbe, int32_t width); static void _rgbeToFloat(const uint8_t *rgbe, float *out); diff --git a/src/ktx2.h b/src/ktx2.h index 1f884106a..8ca899852 100644 --- a/src/ktx2.h +++ b/src/ktx2.h @@ -54,8 +54,8 @@ typedef struct Ktx2ImageS { bool srgb; // The file says its colours are sRGB } Ktx2ImageT; +void ktx2Free(Ktx2ImageT *image); bool ktx2Is(const void *bytes, size_t size); // Starts with the KTX2 identifier bool ktx2Transcode(const void *bytes, size_t size, Ktx2FormatE wanted, Ktx2ImageT *out); // Every level; free with ktx2Free -void ktx2Free(Ktx2ImageT *image); #endif diff --git a/src/main.c b/src/main.c index fe5256019..4a7ebf7b4 100644 --- a/src/main.c +++ b/src/main.c @@ -185,12 +185,12 @@ static void _crashHandler(int signalNumber); #endif static void _launcher(const char *exeName, ConfigT *conf); static void _mainTrace(const ConfigT *conf, const char *fmt, ...) __attribute__((format(printf, 2, 3))); -static bool _runTool(const ConfigT *conf); static bool _modeMatchesRatio(int32_t index, int32_t ratioIndex); static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]); static bool _parseInteger(const char *text, int32_t *value); static void _requireRange(const char *exeName, int32_t value, int32_t min, int32_t max, const char *what, const char *unit); static void _resolveFiles(const char *exeName, ConfigT *conf); +static bool _runTool(const ConfigT *conf); static void _showHeader(void); static void _showUsage(const char *name, const char *message) __attribute__((noreturn)); static void _startSDL(void); @@ -207,6 +207,22 @@ static char *_cloneString(const char *string) { } +// Last words on a crash: where it happened, so a report can name the line. Async-signal-unsafe +// calls are acceptable here; the process is already lost. +#ifndef _WIN32 +static void _crashHandler(int signalNumber) { + void *frames[CRASH_FRAMES_MAX]; + int32_t count = backtrace(frames, CRASH_FRAMES_MAX); + + fprintf(stderr, "\nSinge crashed (signal %d). Backtrace:\n", signalNumber); + backtrace_symbols_fd(frames, count, STDERR_FILENO); + fprintf(stderr, "Run with --program and send trace.txt with this.\n"); + signal(signalNumber, SIG_DFL); + raise(signalNumber); +} +#endif + + // Writes an embedded support file, or rewrites it when the installed copy differs from this build's. static bool _extractFile(const char *filename, const uint8_t *data, size_t length) { FILE *out = NULL; @@ -278,22 +294,6 @@ static char *_findVideoFile(const char *baseName) { } -// Last words on a crash: where it happened, so a report can name the line. Async-signal-unsafe -// calls are acceptable here; the process is already lost. -#ifndef _WIN32 -static void _crashHandler(int signalNumber) { - void *frames[CRASH_FRAMES_MAX]; - int32_t count = backtrace(frames, CRASH_FRAMES_MAX); - - fprintf(stderr, "\nSinge crashed (signal %d). Backtrace:\n", signalNumber); - backtrace_symbols_fd(frames, count, STDERR_FILENO); - fprintf(stderr, "Run with --program and send trace.txt with this.\n"); - signal(signalNumber, SIG_DFL); - raise(signalNumber); -} -#endif - - static void _launcher(const char *exeName, ConfigT *conf) { int32_t x = 0; int32_t bestResIndex = -1; @@ -887,23 +887,6 @@ static void _resolveFiles(const char *exeName, ConfigT *conf) { } -static void _showHeader(void) { - static bool shown = false; - - if (!shown) { - utilRedirectConsole(); - // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 - // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 - utilSay(" ___ ___ _ _ ___ ___"); - utilSay("/ __|_ _| \\| |/ __| __| SINGE Is Not a Game Emulator %s", VERSION_STRING); - utilSay("\\__ \\| || .` | (_ | _| Copyright (c) 2006-%s Scott C. Duensing", COPYRIGHT_END_YEAR); - utilSay("|___/___|_|\\_|\\___|___| https://KangarooPunch.com https://SingeEngine.com"); - utilNewline(); - shown = true; - } -} - - // --pack, --unpack, and --patch: the option carries the source, the script argument the destination. static bool _runTool(const ConfigT *conf) { switch (conf->toolMode) { @@ -922,6 +905,23 @@ static bool _runTool(const ConfigT *conf) { } +static void _showHeader(void) { + static bool shown = false; + + if (!shown) { + utilRedirectConsole(); + // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 + // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + utilSay(" ___ ___ _ _ ___ ___"); + utilSay("/ __|_ _| \\| |/ __| __| SINGE Is Not a Game Emulator %s", VERSION_STRING); + utilSay("\\__ \\| || .` | (_ | _| Copyright (c) 2006-%s Scott C. Duensing", COPYRIGHT_END_YEAR); + utilSay("|___/___|_|\\_|\\___|___| https://KangarooPunch.com https://SingeEngine.com"); + utilNewline(); + shown = true; + } +} + + static void _showUsage(const char *name, const char *message) { const int32_t helpColumn = 6 + USAGE_OPTION_WIDTH; // " -x, " plus the padded long form const int32_t helpWidth = USAGE_LINE_WIDTH - helpColumn; diff --git a/src/nav.h b/src/nav.h index 6bba3a0b2..254c9f6c5 100644 --- a/src/nav.h +++ b/src/nav.h @@ -42,32 +42,32 @@ extern "C" { #define NAV_MAX_CROWD_AGENTS 128 // Agents one mesh's crowd can steer void navInit(void); +int32_t navPollArrived(int32_t *agents, int32_t max); // Agents that reached their targets since last asked void navQuit(void); void navUpdate(bool advance); // Once per frame: moves the agents -int32_t navPollArrived(int32_t *agents, int32_t max); // Agents that reached their targets since last asked -int32_t navNew(float agentRadius, float agentHeight, float maxSlopeDegrees, float maxStep); bool navAddNode(int32_t nav, int32_t node); // The node's mesh and its children's, where they stand now bool navBuild(int32_t nav); // Bakes what was added; may take a moment bool navDelete(int32_t nav); -bool navValid(int32_t nav); -int32_t navPath(int32_t nav, Vec3T from, Vec3T to, Vec3T *points, int32_t max); // Corners of the path, or -1 for none -bool navNearest(int32_t nav, Vec3T point, Vec3T *out); // The closest point on the mesh -bool navRaycast(int32_t nav, Vec3T from, Vec3T to, Vec3T *hit); // true when the walk from from to to is blocked, hit where -bool navRandomPoint(int32_t nav, Vec3T *out); -bool navSave(int32_t nav, const char *path); // The baked mesh, to reload with navLoad -int32_t navLoad(const void *data, size_t size, float agentRadius, float agentHeight); int32_t navGetPolygons(int32_t nav, Vec3T *vertices, int32_t max); // Triangles of the baked mesh (3 per), for debug drawing +int32_t navLoad(const void *data, size_t size, float agentRadius, float agentHeight); +bool navNearest(int32_t nav, Vec3T point, Vec3T *out); // The closest point on the mesh +int32_t navNew(float agentRadius, float agentHeight, float maxSlopeDegrees, float maxStep); +int32_t navPath(int32_t nav, Vec3T from, Vec3T to, Vec3T *points, int32_t max); // Corners of the path, or -1 for none +bool navRandomPoint(int32_t nav, Vec3T *out); +bool navRaycast(int32_t nav, Vec3T from, Vec3T to, Vec3T *hit); // true when the walk from from to to is blocked, hit where +bool navSave(int32_t nav, const char *path); // The baked mesh, to reload with navLoad +bool navValid(int32_t nav); -int32_t navAgentNew(int32_t nav, int32_t node, float radius, float height, float speed); bool navAgentDelete(int32_t agent); -bool navAgentMoveTo(int32_t agent, Vec3T target); -bool navAgentStop(int32_t agent); +int32_t navAgentGetNode(int32_t agent); bool navAgentGetVelocity(int32_t agent, Vec3T *velocity); bool navAgentIsArrived(int32_t agent); +bool navAgentMoveTo(int32_t agent, Vec3T target); +int32_t navAgentNew(int32_t nav, int32_t node, float radius, float height, float speed); bool navAgentSetPlayer(int32_t agent, bool player); // Steer a player controller on the node instead of placing it +bool navAgentStop(int32_t agent); bool navAgentValid(int32_t agent); -int32_t navAgentGetNode(int32_t agent); #ifdef __cplusplus } diff --git a/src/particles.h b/src/particles.h index a4054e2e9..9c6efc1f9 100644 --- a/src/particles.h +++ b/src/particles.h @@ -77,46 +77,46 @@ typedef struct EmitterViewT { Vec3T trailOffset; // Added to trail points (a local emitter's origin) } EmitterViewT; +void particlesClearQueue2D(void); void particlesInit(void); +void particlesQueue2D(int32_t emitter); // Draw this emitter this frame +int32_t particlesQueued2D(ParticleLayerE layer, int32_t *emitters, int32_t max); void particlesQuit(void); void particlesUpdate(bool advance); int32_t particlesView3D(EmitterViewT *views, int32_t max); // Every 3D emitter with live particles, up to max; returns how many 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 emitterBurst(int32_t emitter, int32_t count); +void emitterClear(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 emitterSetTrail(int32_t emitter, int32_t length, float width); -void emitterSetFrames(int32_t emitter, int32_t first, int32_t last); +int32_t emitterGetCount(int32_t emitter); +bool emitterIs3D(int32_t emitter); +bool emitterIsActive(int32_t emitter); +int32_t emitterNew(int32_t node); void emitterSetBlend(int32_t emitter, ParticleBlendE blend); void emitterSetCollide(int32_t emitter, ParticleCollideE mode, float bounce, float friction, float floor); +void emitterSetColor(int32_t emitter, const float *start, const float *finish); +void emitterSetDirection(int32_t emitter, Vec3T direction); +void emitterSetDrag(int32_t emitter, float perSecond); +void emitterSetFrames(int32_t emitter, int32_t first, int32_t last); +void emitterSetGravity(int32_t emitter, Vec3T acceleration); void emitterSetLayer(int32_t emitter, ParticleLayerE layer); -void emitterSetRate(int32_t emitter, float perSecond); void emitterSetLife(int32_t emitter, float minSeconds, float maxSeconds); void emitterSetLit(int32_t emitter, bool lit); -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 emitterSetSoftness(int32_t emitter, float distance); -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 emitterSetRadius(int32_t emitter, float radius); +void emitterSetRate(int32_t emitter, float perSecond); +void emitterSetSize(int32_t emitter, float start, float finish, float variation); +void emitterSetSoftness(int32_t emitter, float distance); +void emitterSetSpeed(int32_t emitter, float min, float max); +void emitterSetSpin(int32_t emitter, float min, float max); +void emitterSetSpread(int32_t emitter, float degrees); +void emitterSetTexture(int32_t emitter, SDL_Surface **frames, int32_t frameCount); // Copies the surfaces; NULL restores the disc +void emitterSetTrail(int32_t emitter, int32_t length, float width); 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); +bool emitterValid(int32_t emitter); #endif diff --git a/src/physics.h b/src/physics.h index 7690537d8..6430a3099 100644 --- a/src/physics.h +++ b/src/physics.h @@ -132,6 +132,15 @@ int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Ve bool jointSetLimits(int32_t joint, float low, float high); bool jointValid(int32_t joint); bool physicsAvailable(void); +int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum); +bool physicsInit(void); +void physicsQuit(void); +bool physicsRaycast(Vec3T origin, Vec3T direction, float maxDistance, int32_t *node, Vec3T *point, Vec3T *normal); +void physicsSet2D(bool planar); +void physicsSetDebug(uint32_t mask); +void physicsSetEnabled(bool enabled); +void physicsSetGravity(Vec3T gravity); +void physicsUpdate(bool advance); bool playerDelete(int32_t node); bool playerExists(int32_t node); int32_t playerGetGround(int32_t node, Vec3T *normal); @@ -184,20 +193,11 @@ 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 vehicleSetSteering(int32_t node, float maxDegrees); bool vehicleSetSuspension(int32_t node, float frequency, float damping); bool vehicleSetThrust(int32_t node, float maxForce, Vec3T point); bool vehicleSetWheel(int32_t node, int32_t index, bool steered, bool driven); -int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum); -bool physicsInit(void); -void physicsQuit(void); -bool physicsRaycast(Vec3T origin, Vec3T direction, float maxDistance, int32_t *node, Vec3T *point, Vec3T *normal); -void physicsSet2D(bool planar); -void physicsSetDebug(uint32_t mask); -void physicsSetEnabled(bool enabled); -void physicsSetGravity(Vec3T gravity); -void physicsUpdate(bool advance); #ifdef __cplusplus diff --git a/src/physicsJolt.cpp b/src/physicsJolt.cpp index 8f58f89a9..58d89462a 100644 --- a/src/physicsJolt.cpp +++ b/src/physicsJolt.cpp @@ -2251,8 +2251,6 @@ namespace { } -// ===== Bodies ===== - // A force for this step, at the centre of mass or a world point. bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at) { BodyRecordT *record = _find(node); @@ -2419,6 +2417,28 @@ bool bodySetBounce(int32_t node, float bounce) { } +bool bodySetBuoyancy(int32_t node, float factor) { + BodyRecordT *record = _find(node); + + if (record == nullptr) { + return false; + } + record->buoyancy = SDL_max(0.0f, factor); + return true; +} + + +bool bodySetCurrent(int32_t node, Vec3T flow) { + BodyRecordT *record = _find(node); + + if (record == nullptr) { + return false; + } + record->current = flow; + return true; +} + + // Takes the body out of the world (it stops colliding and moving) and puts it back. bool bodySetEnabled(int32_t node, bool enabled) { BodyRecordT *record = _find(node); @@ -2500,7 +2520,24 @@ bool bodySetVelocity(int32_t node, Vec3T velocity) { } -// ===== Joints ===== +// Fills a static trigger with water: bodies inside float, sink and drift; players swim. +bool bodySetWater(int32_t node, float density, float linearDrag, float angularDrag) { + BodyRecordT *record = _find(node); + + if ((record == nullptr) || (record->type != BODY_STATIC)) { + utilTrace("Physics: water needs a static body on node %d.", node); + return false; + } + if (!record->trigger) { + bodySetTrigger(node, true); + } + record->water = true; + record->waterDensity = SDL_max(0.0f, density); + record->waterLinearDrag = SDL_max(0.0f, linearDrag); + record->waterAngularDrag = SDL_max(0.0f, angularDrag); + return true; +} + bool jointDelete(int32_t joint) { JointRecordT *record; @@ -2621,8 +2658,6 @@ bool jointValid(int32_t joint) { } -// ===== World ===== - bool physicsAvailable(void) { return _world != nullptr; } @@ -2914,6 +2949,13 @@ bool playerIsOnGround(int32_t node) { } +bool playerIsSwimming(int32_t node) { + PlayerRecordT *record = _findPlayer(node); + + return (record != nullptr) && record->swimming; +} + + // Asks for a jump at the next step; only granted with ground underfoot. bool playerJump(int32_t node, float speed) { PlayerRecordT *record = _findPlayer(node); @@ -3121,322 +3163,6 @@ bool playerSetStep(int32_t node, float height) { } -bool playerSetVelocity(int32_t node, Vec3T velocity) { - PlayerRecordT *record = _findPlayer(node); - - if (record == nullptr) { - return false; - } - record->character->SetLinearVelocity(JPH::Vec3(velocity.x, velocity.y, velocity.z)); - record->intent = vec3(0.0f, 0.0f, 0.0f); - return true; -} - - -// A wheel at the wheel node's position relative to the chassis; returns its index. -int32_t vehicleAddWheel(int32_t node, int32_t wheelNode, float radius, float width, float suspension) { - VehicleRecordT *record = _findVehicle(node); - WheelRecordT *wheel; - Vec3T chassisPosition; - QuatT chassisRotation; - Vec3T chassisScale; - - if ((record == nullptr) || !nodeValid(wheelNode) || (record->wheelCount >= MAX_WHEELS)) { - return -1; - } - // The attachment point is the wheel node's place in the chassis' frame right now (the body's - // frame is unscaled: the chassis' scale is baked into its shape); the engine poses the node - // with suspension travel from here on, so a later rebuild must not read it back. - sceneUpdateTransforms(); - nodeGetWorldTransform(node, &chassisPosition, &chassisRotation, &chassisScale); - wheel = &record->wheels[record->wheelCount]; - wheel->node = wheelNode; - wheel->generation = nodeGetGeneration(wheelNode); - wheel->rest = quatRotate(quatInverse(chassisRotation), vec3Subtract(nodeGetWorldPosition(wheelNode), chassisPosition)); - wheel->radius = SDL_max(radius, MIN_DIMENSION); - wheel->width = SDL_max(width, MIN_DIMENSION); - wheel->suspension = SDL_max(suspension, MIN_DIMENSION); - wheel->steered = false; - wheel->driven = true; - wheel->steeredSet = false; - record->dirty = true; - return record->wheelCount++; -} - - -bool vehicleDelete(int32_t node) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - _releaseVehicle(record); - return true; -} - - -bool vehicleDrive(int32_t node, float forward, float right, float brake, float handBrake) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - record->inputForward = SDL_clamp(forward, -1.0f, 1.0f); - record->inputRight = SDL_clamp(right, -1.0f, 1.0f); - record->inputBrake = SDL_clamp(brake, 0.0f, 1.0f); - record->inputHandBrake = SDL_clamp(handBrake, 0.0f, 1.0f); - return true; -} - - -bool vehicleExists(int32_t node) { - return _findVehicle(node) != nullptr; -} - - -int32_t vehicleGetGear(int32_t node) { - VehicleRecordT *record = _findVehicle(node); - - if ((record == nullptr) || (record->constraint == nullptr)) { - return 0; - } - if (record->kind == VEHICLE_TANK) { - return static_cast(record->constraint->GetController())->GetTransmission().GetCurrentGear(); - } - return static_cast(record->constraint->GetController())->GetTransmission().GetCurrentGear(); -} - - -float vehicleGetRpm(int32_t node) { - VehicleRecordT *record = _findVehicle(node); - - if ((record == nullptr) || (record->constraint == nullptr)) { - return 0.0f; - } - if (record->kind == VEHICLE_TANK) { - return static_cast(record->constraint->GetController())->GetEngine().GetCurrentRPM(); - } - return static_cast(record->constraint->GetController())->GetEngine().GetCurrentRPM(); -} - - -// Metres a second along the chassis' nose, negative in reverse. -float vehicleGetSpeed(int32_t node) { - VehicleRecordT *record = _findVehicle(node); - BodyRecordT *body; - JPH::Vec3 velocity; - JPH::Quat rotation; - - if ((record == nullptr) || ((body = _find(record->node)) == nullptr)) { - return 0.0f; - } - velocity = _world->system->GetBodyInterface().GetLinearVelocity(body->id); - rotation = _world->system->GetBodyInterface().GetRotation(body->id); - return velocity.Dot(rotation * JPH::Vec3(0.0f, 0.0f, -1.0f)); -} - - -// Longitudinal slip of a wheel, 0 gripping to about 1 spinning or locked. -float vehicleGetWheelSlip(int32_t node, int32_t index) { - VehicleRecordT *record = _findVehicle(node); - - if ((record == nullptr) || (record->constraint == nullptr) || (index < 0) || (index >= record->wheelCount) || (record->kind == VEHICLE_TANK)) { - return 0.0f; - } - return SDL_min(fabsf(static_cast(record->constraint->GetWheel((JPH::uint)index))->mLongitudinalSlip), 1.0f); -} - - -bool vehicleIsWheelOnGround(int32_t node, int32_t index) { - VehicleRecordT *record = _findVehicle(node); - - if ((record == nullptr) || (record->constraint == nullptr) || (index < 0) || (index >= record->wheelCount)) { - return false; - } - return record->constraint->GetWheel((JPH::uint)index)->HasContact(); -} - - -// A vehicle on the node, whose dynamic body is the chassis; add wheels before driving it. -bool vehicleNew(int32_t node, VehicleKindE kind) { - VehicleRecordT *record = nullptr; - BodyRecordT *body = _find(node); - int32_t x; - - if ((_world == nullptr) || (body == nullptr) || (body->type != BODY_DYNAMIC)) { - utilTrace("Physics: a vehicle needs a dynamic body on node %d first.", node); - return false; - } - if (_world->planar && (kind != VEHICLE_BOAT)) { - utilTrace("Physics: vehicles need a 3D world; a 2D car is a body with hinged wheels."); - return false; - } - vehicleDelete(node); - for (x = 0; x < _world->vehicleCount; x++) { - if (!_world->vehicles[x].used) { - record = &_world->vehicles[x]; - break; - } - } - if (record == nullptr) { - utilTrace("Physics: no room for another vehicle (%d already).", MAX_VEHICLES); - return false; - } - _resetVehicle(record); - record->node = node; - record->generation = nodeGetGeneration(node); - record->kind = kind; - record->dirty = (kind != VEHICLE_BOAT); - record->used = true; - _indexSet(_world->vehicleOfNode, node, x); - return true; -} - - -bool vehicleSetAntiRoll(int32_t node, float stiffness) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - record->antiRoll = SDL_max(0.0f, stiffness); - record->dirty = true; - return true; -} - - -bool vehicleSetBrakes(int32_t node, float brake, float handBrake) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - record->brakeTorque = SDL_max(0.0f, brake); - record->handBrakeTorque = SDL_max(0.0f, handBrake); - record->dirty = true; - return true; -} - - -bool vehicleSetEngine(int32_t node, float maxTorque, float maxRpm, float minRpm) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - record->maxTorque = SDL_max(MIN_ENGINE_TORQUE, maxTorque); - record->maxRpm = SDL_max(MIN_ENGINE_MAX_RPM, maxRpm); - record->minRpm = SDL_clamp(minRpm, MIN_ENGINE_RPM, record->maxRpm); - record->dirty = true; - return true; -} - - -bool vehicleSetGears(int32_t node, const float *ratios, int32_t count, float reverse, bool automatic) { - VehicleRecordT *record = _findVehicle(node); - int32_t x; - - if ((record == nullptr) || (count < 1) || (count > VEHICLE_MAX_GEARS)) { - return false; - } - for (x = 0; x < count; x++) { - record->gears[x] = ratios[x]; - } - record->gearCount = count; - record->reverseGear = (reverse > 0.0f) ? -reverse : reverse; - record->automatic = automatic; - record->dirty = true; - return true; -} - - -bool vehicleSetSteering(int32_t node, float maxDegrees) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - record->maxSteer = SDL_clamp(maxDegrees, 0.0f, MAX_STEER_DEGREES); - record->dirty = true; - return true; -} - - -bool vehicleSetSuspension(int32_t node, float frequency, float damping) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - record->suspensionHz = SDL_max(MIN_SUSPENSION_HZ, frequency); - record->suspensionDamping = SDL_max(0.0f, damping); - record->dirty = true; - return true; -} - - -bool vehicleSetWheel(int32_t node, int32_t index, bool steered, bool driven) { - VehicleRecordT *record = _findVehicle(node); - - if ((record == nullptr) || (index < 0) || (index >= record->wheelCount)) { - return false; - } - record->wheels[index].steered = steered; - record->wheels[index].driven = driven; - record->wheels[index].steeredSet = true; - record->dirty = true; - return true; -} - - -bool bodySetBuoyancy(int32_t node, float factor) { - BodyRecordT *record = _find(node); - - if (record == nullptr) { - return false; - } - record->buoyancy = SDL_max(0.0f, factor); - return true; -} - - -bool bodySetCurrent(int32_t node, Vec3T flow) { - BodyRecordT *record = _find(node); - - if (record == nullptr) { - return false; - } - record->current = flow; - return true; -} - - -// Fills a static trigger with water: bodies inside float, sink and drift; players swim. -bool bodySetWater(int32_t node, float density, float linearDrag, float angularDrag) { - BodyRecordT *record = _find(node); - - if ((record == nullptr) || (record->type != BODY_STATIC)) { - utilTrace("Physics: water needs a static body on node %d.", node); - return false; - } - if (!record->trigger) { - bodySetTrigger(node, true); - } - record->water = true; - record->waterDensity = SDL_max(0.0f, density); - record->waterLinearDrag = SDL_max(0.0f, linearDrag); - record->waterAngularDrag = SDL_max(0.0f, angularDrag); - return true; -} - - -bool playerIsSwimming(int32_t node) { - PlayerRecordT *record = _findPlayer(node); - - return (record != nullptr) && record->swimming; -} - - bool playerSetSwim(int32_t node, float sinkSpeed, float drag) { PlayerRecordT *record = _findPlayer(node); @@ -3449,25 +3175,14 @@ bool playerSetSwim(int32_t node, float sinkSpeed, float drag) { } -bool vehicleSetRudder(int32_t node, float maxTorque) { - VehicleRecordT *record = _findVehicle(node); +bool playerSetVelocity(int32_t node, Vec3T velocity) { + PlayerRecordT *record = _findPlayer(node); if (record == nullptr) { return false; } - record->rudder = SDL_max(0.0f, maxTorque); - return true; -} - - -bool vehicleSetThrust(int32_t node, float maxForce, Vec3T point) { - VehicleRecordT *record = _findVehicle(node); - - if (record == nullptr) { - return false; - } - record->thrust = SDL_max(0.0f, maxForce); - record->thrustPoint = point; + record->character->SetLinearVelocity(JPH::Vec3(velocity.x, velocity.y, velocity.z)); + record->intent = vec3(0.0f, 0.0f, 0.0f); return true; } @@ -4132,3 +3847,281 @@ bool softUnpin(int32_t node, Vec3T point) { return false; } + +// A wheel at the wheel node's position relative to the chassis; returns its index. +int32_t vehicleAddWheel(int32_t node, int32_t wheelNode, float radius, float width, float suspension) { + VehicleRecordT *record = _findVehicle(node); + WheelRecordT *wheel; + Vec3T chassisPosition; + QuatT chassisRotation; + Vec3T chassisScale; + + if ((record == nullptr) || !nodeValid(wheelNode) || (record->wheelCount >= MAX_WHEELS)) { + return -1; + } + // The attachment point is the wheel node's place in the chassis' frame right now (the body's + // frame is unscaled: the chassis' scale is baked into its shape); the engine poses the node + // with suspension travel from here on, so a later rebuild must not read it back. + sceneUpdateTransforms(); + nodeGetWorldTransform(node, &chassisPosition, &chassisRotation, &chassisScale); + wheel = &record->wheels[record->wheelCount]; + wheel->node = wheelNode; + wheel->generation = nodeGetGeneration(wheelNode); + wheel->rest = quatRotate(quatInverse(chassisRotation), vec3Subtract(nodeGetWorldPosition(wheelNode), chassisPosition)); + wheel->radius = SDL_max(radius, MIN_DIMENSION); + wheel->width = SDL_max(width, MIN_DIMENSION); + wheel->suspension = SDL_max(suspension, MIN_DIMENSION); + wheel->steered = false; + wheel->driven = true; + wheel->steeredSet = false; + record->dirty = true; + return record->wheelCount++; +} + + +bool vehicleDelete(int32_t node) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + _releaseVehicle(record); + return true; +} + + +bool vehicleDrive(int32_t node, float forward, float right, float brake, float handBrake) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->inputForward = SDL_clamp(forward, -1.0f, 1.0f); + record->inputRight = SDL_clamp(right, -1.0f, 1.0f); + record->inputBrake = SDL_clamp(brake, 0.0f, 1.0f); + record->inputHandBrake = SDL_clamp(handBrake, 0.0f, 1.0f); + return true; +} + + +bool vehicleExists(int32_t node) { + return _findVehicle(node) != nullptr; +} + + +int32_t vehicleGetGear(int32_t node) { + VehicleRecordT *record = _findVehicle(node); + + if ((record == nullptr) || (record->constraint == nullptr)) { + return 0; + } + if (record->kind == VEHICLE_TANK) { + return static_cast(record->constraint->GetController())->GetTransmission().GetCurrentGear(); + } + return static_cast(record->constraint->GetController())->GetTransmission().GetCurrentGear(); +} + + +float vehicleGetRpm(int32_t node) { + VehicleRecordT *record = _findVehicle(node); + + if ((record == nullptr) || (record->constraint == nullptr)) { + return 0.0f; + } + if (record->kind == VEHICLE_TANK) { + return static_cast(record->constraint->GetController())->GetEngine().GetCurrentRPM(); + } + return static_cast(record->constraint->GetController())->GetEngine().GetCurrentRPM(); +} + + +// Metres a second along the chassis' nose, negative in reverse. +float vehicleGetSpeed(int32_t node) { + VehicleRecordT *record = _findVehicle(node); + BodyRecordT *body; + JPH::Vec3 velocity; + JPH::Quat rotation; + + if ((record == nullptr) || ((body = _find(record->node)) == nullptr)) { + return 0.0f; + } + velocity = _world->system->GetBodyInterface().GetLinearVelocity(body->id); + rotation = _world->system->GetBodyInterface().GetRotation(body->id); + return velocity.Dot(rotation * JPH::Vec3(0.0f, 0.0f, -1.0f)); +} + + +// Longitudinal slip of a wheel, 0 gripping to about 1 spinning or locked. +float vehicleGetWheelSlip(int32_t node, int32_t index) { + VehicleRecordT *record = _findVehicle(node); + + if ((record == nullptr) || (record->constraint == nullptr) || (index < 0) || (index >= record->wheelCount) || (record->kind == VEHICLE_TANK)) { + return 0.0f; + } + return SDL_min(fabsf(static_cast(record->constraint->GetWheel((JPH::uint)index))->mLongitudinalSlip), 1.0f); +} + + +bool vehicleIsWheelOnGround(int32_t node, int32_t index) { + VehicleRecordT *record = _findVehicle(node); + + if ((record == nullptr) || (record->constraint == nullptr) || (index < 0) || (index >= record->wheelCount)) { + return false; + } + return record->constraint->GetWheel((JPH::uint)index)->HasContact(); +} + + +// A vehicle on the node, whose dynamic body is the chassis; add wheels before driving it. +bool vehicleNew(int32_t node, VehicleKindE kind) { + VehicleRecordT *record = nullptr; + BodyRecordT *body = _find(node); + int32_t x; + + if ((_world == nullptr) || (body == nullptr) || (body->type != BODY_DYNAMIC)) { + utilTrace("Physics: a vehicle needs a dynamic body on node %d first.", node); + return false; + } + if (_world->planar && (kind != VEHICLE_BOAT)) { + utilTrace("Physics: vehicles need a 3D world; a 2D car is a body with hinged wheels."); + return false; + } + vehicleDelete(node); + for (x = 0; x < _world->vehicleCount; x++) { + if (!_world->vehicles[x].used) { + record = &_world->vehicles[x]; + break; + } + } + if (record == nullptr) { + utilTrace("Physics: no room for another vehicle (%d already).", MAX_VEHICLES); + return false; + } + _resetVehicle(record); + record->node = node; + record->generation = nodeGetGeneration(node); + record->kind = kind; + record->dirty = (kind != VEHICLE_BOAT); + record->used = true; + _indexSet(_world->vehicleOfNode, node, x); + return true; +} + + +bool vehicleSetAntiRoll(int32_t node, float stiffness) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->antiRoll = SDL_max(0.0f, stiffness); + record->dirty = true; + return true; +} + + +bool vehicleSetBrakes(int32_t node, float brake, float handBrake) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->brakeTorque = SDL_max(0.0f, brake); + record->handBrakeTorque = SDL_max(0.0f, handBrake); + record->dirty = true; + return true; +} + + +bool vehicleSetEngine(int32_t node, float maxTorque, float maxRpm, float minRpm) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->maxTorque = SDL_max(MIN_ENGINE_TORQUE, maxTorque); + record->maxRpm = SDL_max(MIN_ENGINE_MAX_RPM, maxRpm); + record->minRpm = SDL_clamp(minRpm, MIN_ENGINE_RPM, record->maxRpm); + record->dirty = true; + return true; +} + + +bool vehicleSetGears(int32_t node, const float *ratios, int32_t count, float reverse, bool automatic) { + VehicleRecordT *record = _findVehicle(node); + int32_t x; + + if ((record == nullptr) || (count < 1) || (count > VEHICLE_MAX_GEARS)) { + return false; + } + for (x = 0; x < count; x++) { + record->gears[x] = ratios[x]; + } + record->gearCount = count; + record->reverseGear = (reverse > 0.0f) ? -reverse : reverse; + record->automatic = automatic; + record->dirty = true; + return true; +} + + +bool vehicleSetRudder(int32_t node, float maxTorque) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->rudder = SDL_max(0.0f, maxTorque); + return true; +} + + +bool vehicleSetSteering(int32_t node, float maxDegrees) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->maxSteer = SDL_clamp(maxDegrees, 0.0f, MAX_STEER_DEGREES); + record->dirty = true; + return true; +} + + +bool vehicleSetSuspension(int32_t node, float frequency, float damping) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->suspensionHz = SDL_max(MIN_SUSPENSION_HZ, frequency); + record->suspensionDamping = SDL_max(0.0f, damping); + record->dirty = true; + return true; +} + + +bool vehicleSetThrust(int32_t node, float maxForce, Vec3T point) { + VehicleRecordT *record = _findVehicle(node); + + if (record == nullptr) { + return false; + } + record->thrust = SDL_max(0.0f, maxForce); + record->thrustPoint = point; + return true; +} + + +bool vehicleSetWheel(int32_t node, int32_t index, bool steered, bool driven) { + VehicleRecordT *record = _findVehicle(node); + + if ((record == nullptr) || (index < 0) || (index >= record->wheelCount)) { + return false; + } + record->wheels[index].steered = steered; + record->wheels[index].driven = driven; + record->wheels[index].steeredSet = true; + record->dirty = true; + return true; +} diff --git a/src/scene.c b/src/scene.c index c6cc464fc..8354dd9a7 100644 --- a/src/scene.c +++ b/src/scene.c @@ -112,6 +112,7 @@ #define CASCADE_NEAR 0.01f #define SKIN_BOUNDS_GROW 1.5f // A skinned mesh moves beyond its bind pose #define MORPH_FLOATS 8 // Per target per vertex: position delta xyz + pad, normal delta xyz + pad +#define BILLBOARD_LANE 15 // The normal matrix's spare lane (the shader's _m33) carrying a draw's BILLBOARD_MODE_* // Matches DrawUniforms in scene.hlsl. @@ -120,12 +121,15 @@ typedef struct DrawUniformsS { float morphWeights[MAX_MORPHS]; int32_t morphTargets[MAX_MORPHS]; int32_t morphInfo[4]; // x = active targets, y = vertices per target, z = first instance in the matrix buffer + float billboardRight[4]; // The axes and eye billboards turn to + float billboardUp[4]; + float billboardEye[4]; } DrawUniformsT; // One draw's matrices in the per-frame instance buffer the vertex shaders read. typedef struct InstanceMatricesS { Mat4T model; - Mat4T normal; // Inverse transpose, for normals under non-uniform scale + Mat4T normal; // Inverse transpose, for normals under non-uniform scale; m[BILLBOARD_LANE] the billboard mode } InstanceMatricesT; // Matches Light and FragmentUniforms in scene.hlsl. @@ -267,7 +271,7 @@ typedef struct NodeS { bool visible; bool worldVisible; // Own flag and every ancestor's bool shadowCaster; // Drawn into shadow maps (nodeSetShadow) - BillboardE billboard; // Turned to face the window's camera when its matrices are built + BillboardE billboard; // Turned to face each camera by the vertex shader int32_t spriteSlot; // Into the sprite records when the node shows a sprite or text, else NO_HANDLE bool used; } NodeT; @@ -286,7 +290,6 @@ typedef struct SpriteNodeS { typedef struct DrawS { int32_t node; - float depth; // View-space distance, for sorting blended draws Vec3T centre; // World bounding sphere, filled by _boundDraws float radius; int32_t skin; // Into the frame's skin matrices (_fillInstances), NO_HANDLE unskinned @@ -551,8 +554,11 @@ typedef struct SceneS { int32_t materialCount; FeedT *feeds; int32_t feedCount; - DrawT *draws; + DrawT *draws; // This frame's draws: the opaque ones first, sorted for batching, then the blended ... int32_t drawCapacity; + int32_t opaqueCount; // ... how many are opaque ... + DepthOrderT *blendedOrder; // ... and the blended ones back to front from the camera being rendered + int32_t blendedOrderRoom; int32_t cameraNode; // NO_HANDLE for the built-in default view bool perspective; float fov; @@ -576,7 +582,6 @@ static void _boundDraws(int32_t drawCount); static void _cameraFrame(int32_t camera, int32_t width, int32_t height, CameraFrameT *frame); static int32_t _compareDepth(float a, float b); static int32_t _compareDepthOrder(const void *a, const void *b); -static int32_t _compareDraws(const void *a, const void *b); static int32_t _compareOpaque(const void *a, const void *b); static void _computeSh(const float *rgb, int32_t width, int32_t height); static bool _createBloomPipelines(void); @@ -604,12 +609,12 @@ static void _destroyTargets(void); static void _detach(int32_t node); static void _drawBloom(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame); static void _drawLines(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame); -static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, bool twoSided, const bool *skip, const FragmentUniformsT *fragmentUniforms, int32_t sampleSet); +static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, const CameraFrameT *axes, bool shadowPass, bool twoSided, const bool *skip, const FragmentUniformsT *fragmentUniforms, int32_t sampleSet); static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame, const FragmentUniformsT *fragmentUniforms); static void _drawPost(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame); static void _drawSky(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame); static Vec3T _faceDirection(int32_t face, float s, float t); -static void _fillInstances(int32_t drawCount, const CameraFrameT *frame); +static void _fillInstances(int32_t drawCount); static void _fillLights(FragmentUniformsT *uniforms); static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms); static void _fitShadows(int32_t drawCount, const CameraFrameT *camera); @@ -635,6 +640,7 @@ static void _materialDefaults(MaterialT *material); static void _materialPlace(MaterialT *material, MaterialMapE map, SDL_GPUTexture *texture, float strength); static SDL_GPUTexture *_materialTexture(const MaterialT *material); static uint32_t _mipLevels(int32_t width, int32_t height); +static void _orderBlended(Vec3T eye, int32_t drawCount); static ParticleTexturesT *_particleTextures(const EmitterViewT *view); static SDL_GPUTextureFormat _pickDepthFormat(const SDL_GPUTextureFormat *wanted, int32_t count, SDL_GPUTextureUsageFlags usage); static int32_t _pipelineVariant(int32_t node); @@ -857,6 +863,24 @@ static void _alphaBlendState(SDL_GPUColorTargetDescription *colour, bool additiv } +// Links node under parent as its last child. +static void _attach(int32_t node, int32_t parent) { + int32_t last; + + _scene.nodes[node].parent = parent; + _scene.nodes[node].nextSibling = NO_HANDLE; + if (_scene.nodes[parent].firstChild == NO_HANDLE) { + _scene.nodes[parent].firstChild = node; + return; + } + last = _scene.nodes[parent].firstChild; + while (_scene.nodes[last].nextSibling != NO_HANDLE) { + last = _scene.nodes[last].nextSibling; + } + _scene.nodes[last].nextSibling = node; +} + + // A world bounding sphere per draw, for culling and for fitting shadows. static void _boundDraws(int32_t drawCount) { int32_t x; @@ -907,24 +931,6 @@ static void _cameraFrame(int32_t camera, int32_t width, int32_t height, CameraFr } -// Links node under parent as its last child. -static void _attach(int32_t node, int32_t parent) { - int32_t last; - - _scene.nodes[node].parent = parent; - _scene.nodes[node].nextSibling = NO_HANDLE; - if (_scene.nodes[parent].firstChild == NO_HANDLE) { - _scene.nodes[parent].firstChild = node; - return; - } - last = _scene.nodes[parent].firstChild; - while (_scene.nodes[last].nextSibling != NO_HANDLE) { - last = _scene.nodes[last].nextSibling; - } - _scene.nodes[last].nextSibling = node; -} - - // Far to near. static int32_t _compareDepth(float a, float b) { if (a > b) { @@ -943,12 +949,6 @@ static int32_t _compareDepthOrder(const void *a, const void *b) { } -// Blended draws go back to front; opaque ones keep their order. -static int32_t _compareDraws(const void *a, const void *b) { - return _compareDepth(((const DrawT *)a)->depth, ((const DrawT *)b)->depth); -} - - // Opaque draws sort by mesh then material so that copies of one thing sit together and batch. static int32_t _compareOpaque(const void *a, const void *b) { const NodeT *na = &_scene.nodes[((const DrawT *)a)->node]; @@ -964,6 +964,123 @@ static int32_t _compareOpaque(const void *a, const void *b) { } +// The sky's diffuse light as nine spherical harmonic coefficients per channel, integrated over +// a coarse sampling of the equirect weighted by each sample's solid angle. +static void _computeSh(const float *rgb, int32_t width, int32_t height) { + int32_t stepX = SDL_max(width / SH_SAMPLES_ACROSS, 1); + int32_t stepY = SDL_max(height / (SH_SAMPLES_ACROSS / 2), 1); + int32_t x; + int32_t y; + int32_t c; + + for (c = 0; c < SH_COEFFICIENTS; c++) { + _scene.sh[c] = vec3(0.0f, 0.0f, 0.0f); + } + for (y = stepY / 2; y < height; y += stepY) { + float theta = SDL_PI_F * (y + 0.5f) / (float)height; + float solid = (2.0f * SDL_PI_F / (float)width * stepX) * (SDL_PI_F / (float)height * stepY) * SDL_sinf(theta); + + for (x = stepX / 2; x < width; x += stepX) { + float phi = 2.0f * SDL_PI_F * ((x + 0.5f) / (float)width - 0.5f); + Vec3T d = vec3(SDL_sinf(theta) * SDL_sinf(phi), SDL_cosf(theta), -SDL_sinf(theta) * SDL_cosf(phi)); + const float *pixel = rgb + ((size_t)y * (size_t)width + (size_t)x) * 3; + Vec3T colour = vec3Scale(vec3(pixel[0], pixel[1], pixel[2]), solid); + float basis[SH_COEFFICIENTS]; + + basis[0] = 0.282095f; + basis[1] = 0.488603f * d.y; + basis[2] = 0.488603f * d.z; + basis[3] = 0.488603f * d.x; + basis[4] = 1.092548f * d.x * d.y; + basis[5] = 1.092548f * d.y * d.z; + basis[6] = 0.315392f * (3.0f * d.z * d.z - 1.0f); + basis[7] = 1.092548f * d.x * d.z; + basis[8] = 0.546274f * (d.x * d.x - d.y * d.y); + for (c = 0; c < SH_COEFFICIENTS; c++) { + _scene.sh[c] = vec3Add(_scene.sh[c], vec3Scale(colour, basis[c])); + } + } + } +} + + +// The bloom pipelines: the post vertex shader with the downsample and upsample fragments, into +// 16-bit float levels. +static bool _createBloomPipelines(void) { + SDL_GPUGraphicsPipelineCreateInfo info; + SDL_GPUColorTargetDescription colour; + + memset(&info, 0, sizeof(info)); + memset(&colour, 0, sizeof(colour)); + colour.format = _scene.hdrFormat; + info.vertex_shader = _scene.postVertex; + info.fragment_shader = _scene.bloomDownFragment; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; + info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; + info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; + info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; + info.target_info.color_target_descriptions = &colour; + info.target_info.num_color_targets = 1; + _scene.bloomDownPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.bloomDownPipeline == NULL) { + utilTrace("Scene: bloom pipeline: %s", SDL_GetError()); + return false; + } + info.fragment_shader = _scene.bloomUpFragment; + _scene.bloomUpPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.bloomUpPipeline == NULL) { + utilTrace("Scene: bloom pipeline: %s", SDL_GetError()); + SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.bloomDownPipeline); + _scene.bloomDownPipeline = NULL; + return false; + } + return true; +} + + +// The bloom chain for a target of this size: half-size levels halving again down to +// BLOOM_MIN_SIZE, one texture per level each way so no pass reads the texture it writes. +static bool _createBloomTargets(int32_t width, int32_t height) { + SDL_GPUTextureCreateInfo info; + int32_t w = SDL_max(width / 2, 1); + int32_t h = SDL_max(height / 2, 1); + int32_t level; + + if ((_scene.bloomWidth == w) && (_scene.bloomHeight == h) && (_scene.bloomLevels > 0)) { + return true; + } + _destroyBloomTargets(); + _scene.bloomWidth = w; + _scene.bloomHeight = h; + memset(&info, 0, sizeof(info)); + info.type = SDL_GPU_TEXTURETYPE_2D; + info.format = _scene.hdrFormat; + info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; + info.layer_count_or_depth = 1; + info.num_levels = 1; + info.sample_count = SDL_GPU_SAMPLECOUNT_1; + for (level = 0; level < BLOOM_LEVELS; level++) { + if ((level > 0) && ((w < BLOOM_MIN_SIZE) || (h < BLOOM_MIN_SIZE))) { + break; + } + info.width = (Uint32)w; + info.height = (Uint32)h; + _scene.bloomDown[level] = SDL_CreateGPUTexture(_scene.device, &info); + _scene.bloomUp[level] = SDL_CreateGPUTexture(_scene.device, &info); + if ((_scene.bloomDown[level] == NULL) || (_scene.bloomUp[level] == NULL)) { + utilTrace("Scene: bloom targets: %s", SDL_GetError()); + _destroyBloomTargets(); + return false; + } + w = SDL_max(w / 2, 1); + h = SDL_max(h / 2, 1); + } + _scene.bloomLevels = level; + return level > 0; +} + + // Debug lines: unlit, alpha blended, depth tested against the scene but never writing it. static bool _createLinePipeline(int32_t sampleSet) { SDL_GPUGraphicsPipelineCreateInfo info; @@ -1112,83 +1229,6 @@ static bool _createPipeline(int32_t sampleSet, int32_t variant) { } -// The bloom pipelines: the post vertex shader with the downsample and upsample fragments, into -// 16-bit float levels. -static bool _createBloomPipelines(void) { - SDL_GPUGraphicsPipelineCreateInfo info; - SDL_GPUColorTargetDescription colour; - - memset(&info, 0, sizeof(info)); - memset(&colour, 0, sizeof(colour)); - colour.format = _scene.hdrFormat; - info.vertex_shader = _scene.postVertex; - info.fragment_shader = _scene.bloomDownFragment; - info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; - info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; - info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; - info.target_info.color_target_descriptions = &colour; - info.target_info.num_color_targets = 1; - _scene.bloomDownPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if (_scene.bloomDownPipeline == NULL) { - utilTrace("Scene: bloom pipeline: %s", SDL_GetError()); - return false; - } - info.fragment_shader = _scene.bloomUpFragment; - _scene.bloomUpPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if (_scene.bloomUpPipeline == NULL) { - utilTrace("Scene: bloom pipeline: %s", SDL_GetError()); - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.bloomDownPipeline); - _scene.bloomDownPipeline = NULL; - return false; - } - return true; -} - - -// The bloom chain for a target of this size: half-size levels halving again down to -// BLOOM_MIN_SIZE, one texture per level each way so no pass reads the texture it writes. -static bool _createBloomTargets(int32_t width, int32_t height) { - SDL_GPUTextureCreateInfo info; - int32_t w = SDL_max(width / 2, 1); - int32_t h = SDL_max(height / 2, 1); - int32_t level; - - if ((_scene.bloomWidth == w) && (_scene.bloomHeight == h) && (_scene.bloomLevels > 0)) { - return true; - } - _destroyBloomTargets(); - _scene.bloomWidth = w; - _scene.bloomHeight = h; - memset(&info, 0, sizeof(info)); - info.type = SDL_GPU_TEXTURETYPE_2D; - info.format = _scene.hdrFormat; - info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; - info.layer_count_or_depth = 1; - info.num_levels = 1; - info.sample_count = SDL_GPU_SAMPLECOUNT_1; - for (level = 0; level < BLOOM_LEVELS; level++) { - if ((level > 0) && ((w < BLOOM_MIN_SIZE) || (h < BLOOM_MIN_SIZE))) { - break; - } - info.width = (Uint32)w; - info.height = (Uint32)h; - _scene.bloomDown[level] = SDL_CreateGPUTexture(_scene.device, &info); - _scene.bloomUp[level] = SDL_CreateGPUTexture(_scene.device, &info); - if ((_scene.bloomDown[level] == NULL) || (_scene.bloomUp[level] == NULL)) { - utilTrace("Scene: bloom targets: %s", SDL_GetError()); - _destroyBloomTargets(); - return false; - } - w = SDL_max(w / 2, 1); - h = SDL_max(h / 2, 1); - } - _scene.bloomLevels = level; - return level > 0; -} - - // The post pipeline: one triangle from the HDR target to the display texture, no vertex buffer. static bool _createPostPipeline(void) { SDL_GPUGraphicsPipelineCreateInfo info; @@ -1215,77 +1255,6 @@ static bool _createPostPipeline(void) { } -// The sky pipeline: the post pass's screen triangle with the sky fragment shader, drawn first -// into the main pass under everything (no depth test or write), so it must match the pass's -// multisampling and depth format. -static bool _createSkyPipeline(int32_t sampleSet) { - SDL_GPUGraphicsPipelineCreateInfo info; - SDL_GPUColorTargetDescription colour; - - memset(&info, 0, sizeof(info)); - memset(&colour, 0, sizeof(colour)); - colour.format = _scene.hdrFormat; - info.vertex_shader = _scene.postVertex; - info.fragment_shader = _scene.skyFragment; - info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; - info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; - info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = _sampleCountOf(sampleSet); - info.depth_stencil_state.enable_depth_test = false; - info.target_info.color_target_descriptions = &colour; - info.target_info.num_color_targets = 1; - info.target_info.depth_stencil_format = _scene.depthFormat; - info.target_info.has_depth_stencil_target = true; - _scene.skyPipeline[sampleSet] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if (_scene.skyPipeline[sampleSet] == NULL) { - utilTrace("Scene: sky pipeline: %s", SDL_GetError()); - return false; - } - return true; -} - - -// The sky's diffuse light as nine spherical harmonic coefficients per channel, integrated over -// a coarse sampling of the equirect weighted by each sample's solid angle. -static void _computeSh(const float *rgb, int32_t width, int32_t height) { - int32_t stepX = SDL_max(width / SH_SAMPLES_ACROSS, 1); - int32_t stepY = SDL_max(height / (SH_SAMPLES_ACROSS / 2), 1); - int32_t x; - int32_t y; - int32_t c; - - for (c = 0; c < SH_COEFFICIENTS; c++) { - _scene.sh[c] = vec3(0.0f, 0.0f, 0.0f); - } - for (y = stepY / 2; y < height; y += stepY) { - float theta = SDL_PI_F * (y + 0.5f) / (float)height; - float solid = (2.0f * SDL_PI_F / (float)width * stepX) * (SDL_PI_F / (float)height * stepY) * SDL_sinf(theta); - - for (x = stepX / 2; x < width; x += stepX) { - float phi = 2.0f * SDL_PI_F * ((x + 0.5f) / (float)width - 0.5f); - Vec3T d = vec3(SDL_sinf(theta) * SDL_sinf(phi), SDL_cosf(theta), -SDL_sinf(theta) * SDL_cosf(phi)); - const float *pixel = rgb + ((size_t)y * (size_t)width + (size_t)x) * 3; - Vec3T colour = vec3Scale(vec3(pixel[0], pixel[1], pixel[2]), solid); - float basis[SH_COEFFICIENTS]; - - basis[0] = 0.282095f; - basis[1] = 0.488603f * d.y; - basis[2] = 0.488603f * d.z; - basis[3] = 0.488603f * d.x; - basis[4] = 1.092548f * d.x * d.y; - basis[5] = 1.092548f * d.y * d.z; - basis[6] = 0.315392f * (3.0f * d.z * d.z - 1.0f); - basis[7] = 1.092548f * d.x * d.z; - basis[8] = 0.546274f * (d.x * d.x - d.y * d.y); - for (c = 0; c < SH_COEFFICIENTS; c++) { - _scene.sh[c] = vec3Add(_scene.sh[c], vec3Scale(colour, basis[c])); - } - } - } -} - - // Picks the blob for the format the device accepts. static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderStage stage, uint32_t samplers, uint32_t uniforms, uint32_t storageBuffers) { SDL_GPUShaderCreateInfo info; @@ -1415,6 +1384,141 @@ static bool _createShadowPipeline(int32_t variant) { } +// The sky pipeline: the post pass's screen triangle with the sky fragment shader, drawn first +// into the main pass under everything (no depth test or write), so it must match the pass's +// multisampling and depth format. +static bool _createSkyPipeline(int32_t sampleSet) { + SDL_GPUGraphicsPipelineCreateInfo info; + SDL_GPUColorTargetDescription colour; + + memset(&info, 0, sizeof(info)); + memset(&colour, 0, sizeof(colour)); + colour.format = _scene.hdrFormat; + info.vertex_shader = _scene.postVertex; + info.fragment_shader = _scene.skyFragment; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; + info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; + info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; + info.multisample_state.sample_count = _sampleCountOf(sampleSet); + info.depth_stencil_state.enable_depth_test = false; + info.target_info.color_target_descriptions = &colour; + info.target_info.num_color_targets = 1; + info.target_info.depth_stencil_format = _scene.depthFormat; + info.target_info.has_depth_stencil_target = true; + _scene.skyPipeline[sampleSet] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.skyPipeline[sampleSet] == NULL) { + utilTrace("Scene: sky pipeline: %s", SDL_GetError()); + return false; + } + return true; +} + + +// A fingerprint of everything a point light's faces depend on: the light, its range, and every +// caster's transform; skinned and morphing casters count as always changed. +static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) { + uint64_t hash = 1469598103934665603ULL; + int32_t x; + int32_t b; + const uint8_t *bytes; + + bytes = (const uint8_t *)&_scene.nodes[shadow->node].world; + for (b = 0; b < (int32_t)sizeof(Mat4T); b++) { + hash = (hash ^ bytes[b]) * 1099511628211ULL; + } + bytes = (const uint8_t *)&shadow->far; + for (b = 0; b < (int32_t)sizeof(float); b++) { + hash = (hash ^ bytes[b]) * 1099511628211ULL; + } + for (x = 0; x < drawCount; x++) { + NodeT *node = &_scene.nodes[_scene.draws[x].node]; + MeshT *mesh = &_scene.meshes[node->mesh]; + + if (!node->shadowCaster) { + continue; + } + if (_isSkinned(node, mesh) || ((mesh->morphBuffer != NULL) && (node->morphCount > 0))) { + return 0; + } + hash = (hash ^ (uint64_t)(uint32_t)_scene.draws[x].node) * 1099511628211ULL; + bytes = (const uint8_t *)&node->world; + for (b = 0; b < (int32_t)sizeof(Mat4T); b++) { + hash = (hash ^ bytes[b]) * 1099511628211ULL; + } + } + return (hash == 0) ? 1 : hash; +} + + +// Draws outside one cascade's box (in the cascade's light view, looking down -Z) are skipped. +static void _cullCascade(const ShadowT *shadow, int32_t cascade, int32_t drawCount, bool *skip) { + int32_t x; + float half = shadow->radius[cascade]; + + for (x = 0; x < drawCount; x++) { + Vec3T local = mat4TransformPoint(shadow->faceViews[cascade], _scene.draws[x].centre); + float radius = _scene.draws[x].radius; + + skip[x] = ((fabsf(local.x) - radius > half) || (fabsf(local.y) - radius > half) || (-local.z - radius > shadow->depth[cascade]) || (-local.z + radius < 0.0f)); + } +} + + +// Draws whose bounding sphere lies wholly outside the camera frustum are skipped in the main pass +// (they still cast shadows). The six planes come straight from the view-projection rows. +static void _cullDraws(const Mat4T *viewProjection, int32_t drawCount, bool *skip) { + const float *m = viewProjection->m; + float planes[6][4]; + int32_t p; + int32_t x; + + for (p = 0; p < 6; p++) { + int32_t row = (p < 2) ? 0 : ((p < 4) ? 1 : 2); + float sign = (p & 1) ? -1.0f : 1.0f; + float length; + int32_t k; + + for (k = 0; k < 4; k++) { + // Row 3 plus or minus row 0, 1, 2 for the sides; the near plane is row 2 alone. + planes[p][k] = ((p == 4) ? 0.0f : m[k * 4 + 3]) + sign * m[k * 4 + row]; + } + length = SDL_sqrtf(planes[p][0] * planes[p][0] + planes[p][1] * planes[p][1] + planes[p][2] * planes[p][2]); + if (length > 0.0f) { + for (k = 0; k < 4; k++) { + planes[p][k] /= length; + } + } + } + for (x = 0; x < drawCount; x++) { + const DrawT *draw = &_scene.draws[x]; + + skip[x] = false; + for (p = 0; p < 6; p++) { + if (planes[p][0] * draw->centre.x + planes[p][1] * draw->centre.y + planes[p][2] * draw->centre.z + planes[p][3] < -draw->radius) { + skip[x] = true; + break; + } + } + } +} + + +// Marks the draws a point light's face cannot see: bounding sphere against the 90 degree frustum +// (whose side planes are at 45 degrees, hence the root two on the radius). +static void _cullFace(const ShadowT *shadow, int32_t face, int32_t drawCount, bool *skip) { + int32_t x; + + for (x = 0; x < drawCount; x++) { + Vec3T local = mat4TransformPoint(shadow->faceViews[face], _scene.draws[x].centre); + float radius = _scene.draws[x].radius; + float ahead = -local.z; + + skip[x] = ((ahead + radius < shadow->near) || (ahead - radius > shadow->far) || (fabsf(local.x) - radius * SQRT2 > ahead + radius) || (fabsf(local.y) - radius * SQRT2 > ahead + radius)); + } +} + + // The best depth format the device offers for the camera's depth target. static SDL_GPUTextureFormat _depthFormat(void) { static const SDL_GPUTextureFormat wanted[] = { SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D16_UNORM }; @@ -1524,6 +1628,30 @@ static void _destroyTargets(void) { } +// Unlinks node from its parent's child list. +static void _detach(int32_t node) { + int32_t parent = _scene.nodes[node].parent; + int32_t child; + + if (parent == NO_HANDLE) { + return; + } + if (_scene.nodes[parent].firstChild == node) { + _scene.nodes[parent].firstChild = _scene.nodes[node].nextSibling; + } else { + child = _scene.nodes[parent].firstChild; + while ((child != NO_HANDLE) && (_scene.nodes[child].nextSibling != node)) { + child = _scene.nodes[child].nextSibling; + } + if (child != NO_HANDLE) { + _scene.nodes[child].nextSibling = _scene.nodes[node].nextSibling; + } + } + _scene.nodes[node].parent = NO_HANDLE; + _scene.nodes[node].nextSibling = NO_HANDLE; +} + + // The glow: the bright part of the HDR frame taken down a half-size chain with a 13-tap filter // and brought back up with a tent filter, each level adding to the one above, into bloomUp[0] // for the post pass to add. @@ -1589,11 +1717,34 @@ static void _drawBloom(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame } +// Draws this frame's debug lines, after everything else in the pass. +static void _drawLines(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame) { + SDL_GPUBufferBinding binding; + LineUniformsT uniforms; + + if ((_scene.lineVertexCount == 0) || (_scene.lineBuffer == NULL)) { + return; + } + if ((_scene.linePipeline[frame->sampleSet] == NULL) && !_createLinePipeline(frame->sampleSet)) { + return; + } + uniforms.viewProjection = frame->viewProjection; + memset(&binding, 0, sizeof(binding)); + binding.buffer = _scene.lineBuffer; + SDL_BindGPUGraphicsPipeline(pass, _scene.linePipeline[frame->sampleSet]); + SDL_PushGPUVertexUniformData(commands, 0, &uniforms, sizeof(uniforms)); + SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); + SDL_DrawGPUPrimitives(pass, (Uint32)_scene.lineVertexCount, 1, 0, 0); +} + + // Issues the collected draws into a pass: the shadow pass with the light's view-projection and -// depth-only pipelines (blended meshes cast nothing), or the main pass with the camera's and the -// full material, from the pipelines of the target's sample set. The frame's fragment uniforms go +// depth-only pipelines (the opaque range only; blended meshes cast nothing), or the main pass with +// the camera's and the full material, from the pipelines of the target's sample set, the blended +// range in the order _orderBlended left. A draw's instance index is its own place in the list +// whatever order it goes in. Billboards turn to the axes given. The frame's fragment uniforms go // up once per pipeline, the material's per batch. -static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, bool twoSided, const bool *skip, const FragmentUniformsT *fragmentUniforms, int32_t sampleSet) { +static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, const CameraFrameT *axes, bool shadowPass, bool twoSided, const bool *skip, const FragmentUniformsT *fragmentUniforms, int32_t sampleSet) { SDL_GPUBufferBinding binding; SDL_GPUTextureSamplerBinding samplerBindings[MATERIAL_SAMPLERS]; SDL_GPUSampler *materialSampler; @@ -1603,6 +1754,8 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i SDL_GPUBuffer *storage[2]; int32_t x; int32_t end; + int32_t index; + int32_t count = shadowPass ? _scene.opaqueCount : drawCount; int32_t lastPipeline = NO_HANDLE; int32_t lastMesh = NO_HANDLE; int32_t variant; @@ -1613,17 +1766,29 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i SDL_GPUGraphicsPipeline *pipeline; _materialDefaults(&defaultMaterial); - for (x = 0; x < drawCount; x = end) { + memset(&drawUniforms, 0, sizeof(drawUniforms)); + drawUniforms.viewProjection = *viewProjection; + drawUniforms.billboardRight[0] = axes->right.x; + drawUniforms.billboardRight[1] = axes->right.y; + drawUniforms.billboardRight[2] = axes->right.z; + drawUniforms.billboardUp[0] = axes->up.x; + drawUniforms.billboardUp[1] = axes->up.y; + drawUniforms.billboardUp[2] = axes->up.z; + drawUniforms.billboardEye[0] = axes->eye.x; + drawUniforms.billboardEye[1] = axes->eye.y; + drawUniforms.billboardEye[2] = axes->eye.z; + for (x = 0; x < count; x = end) { end = x + 1; - node = &_scene.nodes[_scene.draws[x].node]; + index = (x < _scene.opaqueCount) ? x : _scene.blendedOrder[x - _scene.opaqueCount].index; + node = &_scene.nodes[_scene.draws[index].node]; mesh = &_scene.meshes[node->mesh]; material = (node->material != NO_HANDLE) ? &_scene.materials[node->material] : &defaultMaterial; - variant = _pipelineVariant(_scene.draws[x].node); - if ((skip != NULL) && skip[x]) { + variant = _pipelineVariant(_scene.draws[index].node); + if ((skip != NULL) && skip[index]) { continue; } if (shadowPass) { - if (material->blend || (!node->shadowCaster && !_scene.depthPrepass)) { + if (!node->shadowCaster && !_scene.depthPrepass) { continue; } // A bulb inside a closed mesh sees only its back faces; they must still cast. @@ -1646,12 +1811,13 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i lastPipeline = variant; lastMesh = NO_HANDLE; } - memset(&drawUniforms, 0, sizeof(drawUniforms)); - drawUniforms.viewProjection = *viewProjection; - drawUniforms.morphInfo[2] = x; - // Copies of the same thing after this one ride along as instances. - if (!_isSkinned(node, mesh) && !_hasMorphs(node, mesh)) { - while ((end < drawCount) && _sameBatch(x, end, shadowPass, skip)) { + drawUniforms.morphInfo[0] = 0; + drawUniforms.morphInfo[1] = 0; + drawUniforms.morphInfo[2] = index; + // Copies of the same thing after this one ride along as instances: opaque draws only, whose + // instances sit in list order. + if ((x < _scene.opaqueCount) && !_isSkinned(node, mesh) && !_hasMorphs(node, mesh)) { + while ((end < _scene.opaqueCount) && _sameBatch(x, end, shadowPass, skip)) { end++; } } @@ -1700,8 +1866,8 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i SDL_BindGPUIndexBuffer(pass, &binding, SDL_GPU_INDEXELEMENTSIZE_32BIT); lastMesh = node->mesh; } - if (_scene.draws[x].skin != NO_HANDLE) { - SDL_PushGPUVertexUniformData(commands, SKIN_UNIFORMS, &_scene.skins[_scene.draws[x].skin], sizeof(SkinUniformsT)); + if (_scene.draws[index].skin != NO_HANDLE) { + SDL_PushGPUVertexUniformData(commands, SKIN_UNIFORMS, &_scene.skins[_scene.draws[index].skin], sizeof(SkinUniformsT)); } if (!shadowPass) { baseTexture = _materialTexture(material); @@ -1747,27 +1913,6 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i } -// Draws this frame's debug lines, after everything else in the pass. -static void _drawLines(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame) { - SDL_GPUBufferBinding binding; - LineUniformsT uniforms; - - if ((_scene.lineVertexCount == 0) || (_scene.lineBuffer == NULL)) { - return; - } - if ((_scene.linePipeline[frame->sampleSet] == NULL) && !_createLinePipeline(frame->sampleSet)) { - return; - } - uniforms.viewProjection = frame->viewProjection; - memset(&binding, 0, sizeof(binding)); - binding.buffer = _scene.lineBuffer; - SDL_BindGPUGraphicsPipeline(pass, _scene.linePipeline[frame->sampleSet]); - SDL_PushGPUVertexUniformData(commands, 0, &uniforms, sizeof(uniforms)); - SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); - SDL_DrawGPUPrimitives(pass, (Uint32)_scene.lineVertexCount, 1, 0, 0); -} - - // Draws this frame's particle runs, after every mesh, with the camera's axes for the billboards. static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame, const FragmentUniformsT *fragmentUniforms) { SDL_GPUBufferBinding binding; @@ -1923,137 +2068,11 @@ static Vec3T _faceDirection(int32_t face, float s, float t) { } -// A fingerprint of everything a point light's faces depend on: the light, its range, and every -// caster's transform; skinned and morphing casters count as always changed. -static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) { - uint64_t hash = 1469598103934665603ULL; - int32_t x; - int32_t b; - const uint8_t *bytes; - - bytes = (const uint8_t *)&_scene.nodes[shadow->node].world; - for (b = 0; b < (int32_t)sizeof(Mat4T); b++) { - hash = (hash ^ bytes[b]) * 1099511628211ULL; - } - bytes = (const uint8_t *)&shadow->far; - for (b = 0; b < (int32_t)sizeof(float); b++) { - hash = (hash ^ bytes[b]) * 1099511628211ULL; - } - for (x = 0; x < drawCount; x++) { - NodeT *node = &_scene.nodes[_scene.draws[x].node]; - MeshT *mesh = &_scene.meshes[node->mesh]; - - if (!node->shadowCaster) { - continue; - } - if (_isSkinned(node, mesh) || ((mesh->morphBuffer != NULL) && (node->morphCount > 0))) { - return 0; - } - hash = (hash ^ (uint64_t)(uint32_t)_scene.draws[x].node) * 1099511628211ULL; - bytes = (const uint8_t *)&node->world; - for (b = 0; b < (int32_t)sizeof(Mat4T); b++) { - hash = (hash ^ bytes[b]) * 1099511628211ULL; - } - } - return (hash == 0) ? 1 : hash; -} - - -// Draws outside one cascade's box (in the cascade's light view, looking down -Z) are skipped. -static void _cullCascade(const ShadowT *shadow, int32_t cascade, int32_t drawCount, bool *skip) { - int32_t x; - float half = shadow->radius[cascade]; - - for (x = 0; x < drawCount; x++) { - Vec3T local = mat4TransformPoint(shadow->faceViews[cascade], _scene.draws[x].centre); - float radius = _scene.draws[x].radius; - - skip[x] = ((fabsf(local.x) - radius > half) || (fabsf(local.y) - radius > half) || (-local.z - radius > shadow->depth[cascade]) || (-local.z + radius < 0.0f)); - } -} - - -// Draws whose bounding sphere lies wholly outside the camera frustum are skipped in the main pass -// (they still cast shadows). The six planes come straight from the view-projection rows. -static void _cullDraws(const Mat4T *viewProjection, int32_t drawCount, bool *skip) { - const float *m = viewProjection->m; - float planes[6][4]; - int32_t p; - int32_t x; - - for (p = 0; p < 6; p++) { - int32_t row = (p < 2) ? 0 : ((p < 4) ? 1 : 2); - float sign = (p & 1) ? -1.0f : 1.0f; - float length; - int32_t k; - - for (k = 0; k < 4; k++) { - // Row 3 plus or minus row 0, 1, 2 for the sides; the near plane is row 2 alone. - planes[p][k] = ((p == 4) ? 0.0f : m[k * 4 + 3]) + sign * m[k * 4 + row]; - } - length = SDL_sqrtf(planes[p][0] * planes[p][0] + planes[p][1] * planes[p][1] + planes[p][2] * planes[p][2]); - if (length > 0.0f) { - for (k = 0; k < 4; k++) { - planes[p][k] /= length; - } - } - } - for (x = 0; x < drawCount; x++) { - const DrawT *draw = &_scene.draws[x]; - - skip[x] = false; - for (p = 0; p < 6; p++) { - if (planes[p][0] * draw->centre.x + planes[p][1] * draw->centre.y + planes[p][2] * draw->centre.z + planes[p][3] < -draw->radius) { - skip[x] = true; - break; - } - } - } -} - - -// Marks the draws a point light's face cannot see: bounding sphere against the 90 degree frustum -// (whose side planes are at 45 degrees, hence the root two on the radius). -static void _cullFace(const ShadowT *shadow, int32_t face, int32_t drawCount, bool *skip) { - int32_t x; - - for (x = 0; x < drawCount; x++) { - Vec3T local = mat4TransformPoint(shadow->faceViews[face], _scene.draws[x].centre); - float radius = _scene.draws[x].radius; - float ahead = -local.z; - - skip[x] = ((ahead + radius < shadow->near) || (ahead - radius > shadow->far) || (fabsf(local.x) - radius * SQRT2 > ahead + radius) || (fabsf(local.y) - radius * SQRT2 > ahead + radius)); - } -} - - -// Unlinks node from its parent's child list. -static void _detach(int32_t node) { - int32_t parent = _scene.nodes[node].parent; - int32_t child; - - if (parent == NO_HANDLE) { - return; - } - if (_scene.nodes[parent].firstChild == node) { - _scene.nodes[parent].firstChild = _scene.nodes[node].nextSibling; - } else { - child = _scene.nodes[parent].firstChild; - while ((child != NO_HANDLE) && (_scene.nodes[child].nextSibling != node)) { - child = _scene.nodes[child].nextSibling; - } - if (child != NO_HANDLE) { - _scene.nodes[child].nextSibling = _scene.nodes[node].nextSibling; - } - } - _scene.nodes[node].parent = NO_HANDLE; - _scene.nodes[node].nextSibling = NO_HANDLE; -} - - // The frame's model and normal matrices, one pair per draw in draw order, and the joint matrices -// of every skinned draw, posed once here for every pass that draws it. -static void _fillInstances(int32_t drawCount, const CameraFrameT *frame) { +// of every skinned draw, posed once here for every pass that draws it. A billboard's pair is the +// node's own with its mode in the normal matrix's spare lane; the vertex shader turns both to +// whichever camera draws it. +static void _fillInstances(int32_t drawCount) { int32_t x; int32_t skins = 0; @@ -2069,52 +2088,17 @@ static void _fillInstances(int32_t drawCount, const CameraFrameT *frame) { const NodeT *node = &_scene.nodes[_scene.draws[x].node]; Mat4T model = node->world; - // A billboard keeps its position and scale but takes its axes from the camera: squarely, - // or turning about its own up only. - if (node->billboard != BILLBOARD_NONE) { - Vec3T position = mat4TransformPoint(node->world, vec3(0.0f, 0.0f, 0.0f)); - Vec3T scale = vec3(vec3Length(vec3(node->world.m[0], node->world.m[1], node->world.m[2])), vec3Length(vec3(node->world.m[4], node->world.m[5], node->world.m[6])), vec3Length(vec3(node->world.m[8], node->world.m[9], node->world.m[10]))); - Vec3T right; - Vec3T up; - Vec3T toward; - - if (node->billboard == BILLBOARD_ALL) { - right = frame->right; - up = frame->up; - toward = vec3Scale(frame->forward, -1.0f); - } else { - toward = vec3Subtract(frame->eye, position); - toward.y = 0.0f; - if (vec3Length(toward) < MATH_EPSILON) { - toward = vec3(0.0f, 0.0f, 1.0f); - } - toward = vec3Normalize(toward); - up = vec3(0.0f, 1.0f, 0.0f); - right = vec3Cross(up, toward); - } - model = mat4Identity(); - model.m[0] = right.x * scale.x; - model.m[1] = right.y * scale.x; - model.m[2] = right.z * scale.x; - model.m[4] = up.x * scale.y; - model.m[5] = up.y * scale.y; - model.m[6] = up.z * scale.y; - model.m[8] = toward.x * scale.z; - model.m[9] = toward.y * scale.z; - model.m[10] = toward.z * scale.z; - model.m[12] = position.x; - model.m[13] = position.y; - model.m[14] = position.z; - } // A sprite's quad is a unit square: its size goes in here so the node's own scale stays free. if (node->spriteSlot != NO_HANDLE) { const SpriteNodeT *sprite = &_scene.spriteNodes[node->spriteSlot]; model = mat4Multiply(model, mat4Compose(vec3(0.0f, 0.0f, 0.0f), quatIdentity(), vec3(sprite->width, sprite->height, 1.0f))); } - _scene.instances[x].model = model; - _scene.instances[x].normal = mat4NormalMatrix(model); - _scene.draws[x].skin = NO_HANDLE; + // The shader makes a billboard's normal matrix from the axes it turns to. + _scene.instances[x].model = model; + _scene.instances[x].normal = (node->billboard == BILLBOARD_NONE) ? mat4NormalMatrix(model) : mat4Identity(); + _scene.instances[x].normal.m[BILLBOARD_LANE] = (float)node->billboard; + _scene.draws[x].skin = NO_HANDLE; if (_isSkinned(node, &_scene.meshes[node->mesh])) { if (skins == _scene.skinRoom) { _scene.skinRoom = SDL_max(skins + 1, _scene.skinRoom * 2); @@ -2377,6 +2361,92 @@ static void _fitShadows(int32_t drawCount, const CameraFrameT *camera) { } +static void _freeFeed(FeedT *feed) { + if (feed->target != NULL) { + SDL_DestroyTexture(feed->target); + } + memset(feed, 0, sizeof(*feed)); +} + + +// Every texture a material owns. +static void _freeMaterialTexture(MaterialT *material) { + _releaseMaterialBase(material); + _releaseTexture(&material->normalMap); + _releaseTexture(&material->occlusionMap); + _releaseTexture(&material->metallicRoughnessMap); + _releaseTexture(&material->emissiveMap); +} + + +static void _freeMorphs(MeshT *mesh) { + int32_t x; + + if (mesh->morphBuffer != NULL) { + SDL_ReleaseGPUBuffer(_scene.device, mesh->morphBuffer); + mesh->morphBuffer = NULL; + } + for (x = 0; x < mesh->morphCount; x++) { + SDL_free(mesh->morphNames[x]); + } + SDL_free(mesh->morphNames); + mesh->morphNames = NULL; + mesh->morphCount = 0; +} + + +static void _freeMorphWeights(NodeT *node) { + SDL_free(node->morphWeights); + node->morphWeights = NULL; + node->morphCount = 0; +} + + +static void _freeSkin(NodeT *node) { + SDL_free(node->skinJoints); + SDL_free(node->skinInverseBind); + node->skinJoints = NULL; + node->skinInverseBind = NULL; + node->skinCount = 0; +} + + +// Drops a node's sprite: its textures, its private material, the quad off the node. +static void _freeSpriteNode(int32_t node) { + NodeT *n = &_scene.nodes[node]; + SpriteNodeT *sprite; + int32_t x; + + if (n->spriteSlot == NO_HANDLE) { + return; + } + sprite = &_scene.spriteNodes[n->spriteSlot]; + for (x = 0; x < sprite->count; x++) { + _releaseTexture(&sprite->frames[x]); + } + SDL_free(sprite->frames); + if (materialValid(sprite->material)) { + _scene.materials[sprite->material].texture = NULL; + _scene.materials[sprite->material].textureBorrowed = false; + materialDelete(sprite->material); + } + memset(sprite, 0, sizeof(*sprite)); + n->spriteSlot = NO_HANDLE; + if (n->mesh == _scene.quadMesh) { + n->mesh = NO_HANDLE; + n->material = NO_HANDLE; + } +} + + +static void _freeView(ViewT *view) { + _releaseTexture(&view->colour); + _releaseTexture(&view->depth); + _releaseTexture(&view->output); + memset(view, 0, sizeof(*view)); +} + + // Collects every 3D emitter's live particles into this frame's vertex list: emitters far to near, // alpha-blended particles far to near within each, one run per frame texture. static void _gatherParticles(Vec3T eye, Vec3T forward) { @@ -2546,92 +2616,6 @@ static int32_t _gridMesh(const float *heights, int32_t columns, int32_t rows, fl } -static void _freeFeed(FeedT *feed) { - if (feed->target != NULL) { - SDL_DestroyTexture(feed->target); - } - memset(feed, 0, sizeof(*feed)); -} - - -// Drops a node's sprite: its textures, its private material, the quad off the node. -static void _freeSpriteNode(int32_t node) { - NodeT *n = &_scene.nodes[node]; - SpriteNodeT *sprite; - int32_t x; - - if (n->spriteSlot == NO_HANDLE) { - return; - } - sprite = &_scene.spriteNodes[n->spriteSlot]; - for (x = 0; x < sprite->count; x++) { - _releaseTexture(&sprite->frames[x]); - } - SDL_free(sprite->frames); - if (materialValid(sprite->material)) { - _scene.materials[sprite->material].texture = NULL; - _scene.materials[sprite->material].textureBorrowed = false; - materialDelete(sprite->material); - } - memset(sprite, 0, sizeof(*sprite)); - n->spriteSlot = NO_HANDLE; - if (n->mesh == _scene.quadMesh) { - n->mesh = NO_HANDLE; - n->material = NO_HANDLE; - } -} - - -static void _freeView(ViewT *view) { - _releaseTexture(&view->colour); - _releaseTexture(&view->depth); - _releaseTexture(&view->output); - memset(view, 0, sizeof(*view)); -} - - -// Every texture a material owns. -static void _freeMaterialTexture(MaterialT *material) { - _releaseMaterialBase(material); - _releaseTexture(&material->normalMap); - _releaseTexture(&material->occlusionMap); - _releaseTexture(&material->metallicRoughnessMap); - _releaseTexture(&material->emissiveMap); -} - - -static void _freeMorphs(MeshT *mesh) { - int32_t x; - - if (mesh->morphBuffer != NULL) { - SDL_ReleaseGPUBuffer(_scene.device, mesh->morphBuffer); - mesh->morphBuffer = NULL; - } - for (x = 0; x < mesh->morphCount; x++) { - SDL_free(mesh->morphNames[x]); - } - SDL_free(mesh->morphNames); - mesh->morphNames = NULL; - mesh->morphCount = 0; -} - - -static void _freeMorphWeights(NodeT *node) { - SDL_free(node->morphWeights); - node->morphWeights = NULL; - node->morphCount = 0; -} - - -static void _freeSkin(NodeT *node) { - SDL_free(node->skinJoints); - SDL_free(node->skinInverseBind); - node->skinJoints = NULL; - node->skinInverseBind = NULL; - node->skinCount = 0; -} - - // A float as a 16-bit float (round toward zero; denormals flush to zero). static uint16_t _half(float value) { uint32_t bits; @@ -2757,6 +2741,27 @@ static void _lathe(SceneVertexT **vertices, int32_t *vertexCount, uint32_t **ind } +// An sRGB byte as linear light. +static float _linear(uint8_t value) { + return _linearF(value / COLOUR_MAX); +} + + +// An sRGB fraction as linear light. +static float _linearF(float value) { + if (value <= 0.04045f) { + return value / 12.92f; + } + return SDL_powf((value + 0.055f) / 1.055f, 2.4f); +} + + +// Colour maps are sRGB (the sampler decodes them); data maps are not. +static bool _mapIsColour(MaterialMapE map) { + return (map == MAP_BASE) || (map == MAP_EMISSIVE); +} + + // Sizes the node's weight list to its mesh's targets (weights start at 0). static void _matchMorphWeights(NodeT *node) { int32_t count = meshValid(node->mesh) ? _scene.meshes[node->mesh].morphCount : 0; @@ -2826,19 +2831,6 @@ static void _materialPlace(MaterialT *material, MaterialMapE map, SDL_GPUTexture } -// Levels in a full mipmap chain down to 1x1. -static uint32_t _mipLevels(int32_t width, int32_t height) { - uint32_t levels = 1; - int32_t size = SDL_max(width, height); - - while (size > 1) { - size >>= 1; - levels++; - } - return levels; -} - - // What the fragment shader samples for a material: its video feed, its rendered view, its image, // or NULL. static SDL_GPUTexture *_materialTexture(const MaterialT *material) { @@ -2852,24 +2844,39 @@ static SDL_GPUTexture *_materialTexture(const MaterialT *material) { } -// An sRGB byte as linear light. -static float _linear(uint8_t value) { - return _linearF(value / COLOUR_MAX); -} +// Levels in a full mipmap chain down to 1x1. +static uint32_t _mipLevels(int32_t width, int32_t height) { + uint32_t levels = 1; + int32_t size = SDL_max(width, height); - -// An sRGB fraction as linear light. -static float _linearF(float value) { - if (value <= 0.04045f) { - return value / 12.92f; + while (size > 1) { + size >>= 1; + levels++; } - return SDL_powf((value + 0.055f) / 1.055f, 2.4f); + return levels; } -// Colour maps are sRGB (the sampler decodes them); data maps are not. -static bool _mapIsColour(MaterialMapE map) { - return (map == MAP_BASE) || (map == MAP_EMISSIVE); +// The blended draws (those after the opaque ones) back to front from an eye, as the order the +// camera's pass draws them in; the draws and their instances stay where they are. +static void _orderBlended(Vec3T eye, int32_t drawCount) { + int32_t count = drawCount - _scene.opaqueCount; + int32_t x; + + if (_scene.blendedOrderRoom < count) { + _scene.blendedOrderRoom = SDL_max(count, _scene.blendedOrderRoom * 2); + _scene.blendedOrder = SDL_realloc(_scene.blendedOrder, sizeof(DepthOrderT) * (size_t)_scene.blendedOrderRoom); + if (_scene.blendedOrder == NULL) { + utilDie("Out of memory ordering blended draws."); + } + } + for (x = 0; x < count; x++) { + const float *m = _scene.nodes[_scene.draws[_scene.opaqueCount + x].node].world.m; + + _scene.blendedOrder[x].index = _scene.opaqueCount + x; + _scene.blendedOrder[x].depth = vec3Length(vec3Subtract(vec3(m[12], m[13], m[14]), eye)); + } + qsort(_scene.blendedOrder, (size_t)count, sizeof(DepthOrderT), _compareDepthOrder); } @@ -2992,6 +2999,37 @@ static int32_t _quadMesh(float width, float height, Vec3T down, Vec3T normal) { } +// Keeps a texture's size so releasing it can take it off the total. +static void _recordTexture(SDL_GPUTexture *texture, size_t bytes) { + if (texture == NULL) { + return; + } + if (_scene.sizedCount == _scene.sizedCapacity) { + _scene.sizedCapacity += TEXTURE_SIZES_STEP; + _scene.sizedTextures = SDL_realloc(_scene.sizedTextures, sizeof(SDL_GPUTexture *) * (size_t)_scene.sizedCapacity); + _scene.sizedBytes = SDL_realloc(_scene.sizedBytes, sizeof(size_t) * (size_t)_scene.sizedCapacity); + if ((_scene.sizedTextures == NULL) || (_scene.sizedBytes == NULL)) { + utilDie("Out of memory tracking textures."); + } + } + _scene.sizedTextures[_scene.sizedCount] = texture; + _scene.sizedBytes[_scene.sizedCount] = bytes; + _scene.sizedCount++; + _scene.textureBytes += (int64_t)bytes; +} + + +// A material's base texture, unless it is a sprite node's (which keeps it). +static void _releaseMaterialBase(MaterialT *material) { + if (material->textureBorrowed) { + material->texture = NULL; + material->textureBorrowed = false; + return; + } + _releaseTexture(&material->texture); +} + + // Drops the textures of emitters that no longer exist, or all of them. static void _releaseParticleTextures(bool all) { int32_t x = 0; @@ -3018,37 +3056,6 @@ static void _releaseParticleTextures(bool all) { } -// A material's base texture, unless it is a sprite node's (which keeps it). -static void _releaseMaterialBase(MaterialT *material) { - if (material->textureBorrowed) { - material->texture = NULL; - material->textureBorrowed = false; - return; - } - _releaseTexture(&material->texture); -} - - -// Keeps a texture's size so releasing it can take it off the total. -static void _recordTexture(SDL_GPUTexture *texture, size_t bytes) { - if (texture == NULL) { - return; - } - if (_scene.sizedCount == _scene.sizedCapacity) { - _scene.sizedCapacity += TEXTURE_SIZES_STEP; - _scene.sizedTextures = SDL_realloc(_scene.sizedTextures, sizeof(SDL_GPUTexture *) * (size_t)_scene.sizedCapacity); - _scene.sizedBytes = SDL_realloc(_scene.sizedBytes, sizeof(size_t) * (size_t)_scene.sizedCapacity); - if ((_scene.sizedTextures == NULL) || (_scene.sizedBytes == NULL)) { - utilDie("Out of memory tracking textures."); - } - } - _scene.sizedTextures[_scene.sizedCount] = texture; - _scene.sizedBytes[_scene.sizedCount] = bytes; - _scene.sizedCount++; - _scene.textureBytes += (int64_t)bytes; -} - - static void _releasePipeline(SDL_GPUGraphicsPipeline **pipeline) { if (*pipeline != NULL) { SDL_ReleaseGPUGraphicsPipeline(_scene.device, *pipeline); @@ -3077,9 +3084,9 @@ static void _releaseTexture(SDL_GPUTexture **texture) { } -// One camera's render: its particles gathered and uploaded, draws outside its view culled, the -// depth copy for soft particles when wanted, the main pass (sky, meshes, particles) into its HDR -// target and the post pass into its display texture. +// One camera's render: its particles gathered and uploaded, draws outside its view culled and +// the blended ones ordered from its eye, the depth copy for soft particles when wanted, the main +// pass (sky, meshes, particles) into its HDR target and the post pass into its display texture. static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame, FragmentUniformsT *uniforms, int32_t drawCount) { SDL_GPUColorTargetInfo colour; SDL_GPUDepthStencilTargetInfo depth; @@ -3097,6 +3104,7 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr _gatherParticles(frame->eye, frame->forward); _uploadParticles(commands); _cullDraws(&frame->viewProjection, drawCount, culled); + _orderBlended(frame->eye, drawCount); if (frame->main) { _scene.statTotal = drawCount; _scene.statDrawn = 0; @@ -3119,7 +3127,7 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; _scene.depthPrepass = true; pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &frame->viewProjection, true, false, culled, NULL, SAMPLE_SET_SINGLE); + _drawList(commands, pass, drawCount, &frame->viewProjection, frame, true, false, culled, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); _scene.depthPrepass = false; } @@ -3144,7 +3152,7 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; pass = SDL_BeginGPURenderPass(commands, &colour, 1, &depth); _drawSky(commands, pass, frame); - _drawList(commands, pass, drawCount, &frame->viewProjection, false, false, culled, uniforms, frame->sampleSet); + _drawList(commands, pass, drawCount, &frame->viewProjection, frame, false, false, culled, uniforms, frame->sampleSet); _drawParticles(commands, pass, frame, uniforms); _drawLines(commands, pass, frame); SDL_EndGPURenderPass(pass); @@ -3217,13 +3225,13 @@ static void _ribbon(const EmitterViewT *view, int32_t index, Vec3T eye) { } -// Whether draw b can ride in draw a's instanced batch: the same mesh and material (so the same -// pipeline), nothing per-draw beyond the matrices (no skin, no morphs), and not skipped. +// Whether opaque draw b can ride in draw a's instanced batch: the same mesh and material (so the +// same pipeline), nothing per-draw beyond the matrices (no skin, no morphs), not skipped, and in +// a shadow pass a caster. static bool _sameBatch(int32_t a, int32_t b, bool shadowPass, const bool *skip) { - const NodeT *na = &_scene.nodes[_scene.draws[a].node]; - const NodeT *nb = &_scene.nodes[_scene.draws[b].node]; - const MeshT *mesh; - const MaterialT *material; + const NodeT *na = &_scene.nodes[_scene.draws[a].node]; + const NodeT *nb = &_scene.nodes[_scene.draws[b].node]; + const MeshT *mesh; if ((na->mesh != nb->mesh) || (na->material != nb->material) || ((skip != NULL) && skip[b])) { return false; @@ -3232,11 +3240,8 @@ static bool _sameBatch(int32_t a, int32_t b, bool shadowPass, const bool *skip) if (_isSkinned(nb, mesh) || _hasMorphs(nb, mesh)) { return false; } - if (shadowPass) { - material = (nb->material != NO_HANDLE) ? &_scene.materials[nb->material] : NULL; - if (((material != NULL) && material->blend) || (!nb->shadowCaster && !_scene.depthPrepass)) { - return false; - } + if (shadowPass && !nb->shadowCaster && !_scene.depthPrepass) { + return false; } return true; } @@ -3400,6 +3405,55 @@ static void _stageTexture(const StagingT *staging, uint32_t offset, SDL_GPUTextu } +// World matrices and inherited visibility, depth first. +static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) { + NodeT *n = &_scene.nodes[node]; + int32_t child; + + n->world = mat4Multiply(*parentWorld, mat4Compose(n->translation, n->rotation, n->scale)); + n->worldVisible = parentVisible && n->visible; + for (child = n->firstChild; child != NO_HANDLE; child = _scene.nodes[child].nextSibling) { + _updateWorld(child, &n->world, n->worldVisible); + } +} + + +// Copies data into a new GPU buffer through a transfer buffer. +static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size) { + SDL_GPUBufferCreateInfo info; + SDL_GPUTransferBufferLocation source; + SDL_GPUBufferRegion region; + SDL_GPUBuffer *buffer; + StagingT staging; + + memset(&info, 0, sizeof(info)); + info.usage = usage; + info.size = size; + buffer = SDL_CreateGPUBuffer(_scene.device, &info); + if (buffer == NULL) { + utilTrace("Scene: %s", SDL_GetError()); + return NULL; + } + if (!_stageBegin(&staging, size)) { + SDL_ReleaseGPUBuffer(_scene.device, buffer); + return NULL; + } + memcpy(staging.mapped, data, size); + if (!_stageCopy(&staging)) { + SDL_ReleaseGPUBuffer(_scene.device, buffer); + return NULL; + } + memset(&source, 0, sizeof(source)); + memset(®ion, 0, sizeof(region)); + source.transfer_buffer = staging.transfer; + region.buffer = buffer; + region.size = size; + SDL_UploadToGPUBuffer(staging.pass, &source, ®ion, false); + _stageEnd(&staging, NULL); + return buffer; +} + + // A block-compressed (or, as the fallback, RGBA) texture from a transcoded KTX2 image, every mip // level uploaded as it came (no generation: compressed formats cannot be rendered into). static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb) { @@ -3505,63 +3559,6 @@ static SDL_GPUTexture *_uploadCube(const uint16_t *pixels, int32_t face) { } -// World matrices and inherited visibility, depth first. -static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) { - NodeT *n = &_scene.nodes[node]; - int32_t child; - - n->world = mat4Multiply(*parentWorld, mat4Compose(n->translation, n->rotation, n->scale)); - n->worldVisible = parentVisible && n->visible; - for (child = n->firstChild; child != NO_HANDLE; child = _scene.nodes[child].nextSibling) { - _updateWorld(child, &n->world, n->worldVisible); - } -} - - -// Copies data into a new GPU buffer through a transfer buffer. -static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size) { - SDL_GPUBufferCreateInfo info; - SDL_GPUTransferBufferLocation source; - SDL_GPUBufferRegion region; - SDL_GPUBuffer *buffer; - StagingT staging; - - memset(&info, 0, sizeof(info)); - info.usage = usage; - info.size = size; - buffer = SDL_CreateGPUBuffer(_scene.device, &info); - if (buffer == NULL) { - utilTrace("Scene: %s", SDL_GetError()); - return NULL; - } - if (!_stageBegin(&staging, size)) { - SDL_ReleaseGPUBuffer(_scene.device, buffer); - return NULL; - } - memcpy(staging.mapped, data, size); - if (!_stageCopy(&staging)) { - SDL_ReleaseGPUBuffer(_scene.device, buffer); - return NULL; - } - memset(&source, 0, sizeof(source)); - memset(®ion, 0, sizeof(region)); - source.transfer_buffer = staging.transfer; - region.buffer = buffer; - region.size = size; - SDL_UploadToGPUBuffer(staging.pass, &source, ®ion, false); - _stageEnd(&staging, NULL); - return buffer; -} - - -// This frame's matrices into the storage buffer the vertex shaders index by instance. -static void _uploadInstances(SDL_GPUCommandBuffer *commands, int32_t drawCount) { - if (!_uploadDynamic(commands, SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, &_scene.instanceBuffer, &_scene.instanceTransfer, &_scene.instanceCapacity, _scene.instances, (uint32_t)drawCount * (uint32_t)sizeof(InstanceMatricesT), "instance")) { - utilDie("Unable to upload the scene's matrices."); - } -} - - // A per-frame vertex buffer (and the transfer buffer that fills it), grown when a frame needs more, // with this frame's data uploaded. False when the GPU refused the buffers. static bool _uploadDynamic(SDL_GPUCommandBuffer *commands, SDL_GPUBufferUsageFlags usage, SDL_GPUBuffer **buffer, SDL_GPUTransferBuffer **transfer, uint32_t *capacity, const void *data, uint32_t bytes, const char *what) { @@ -3625,6 +3622,14 @@ static bool _uploadDynamic(SDL_GPUCommandBuffer *commands, SDL_GPUBufferUsageFla } +// This frame's matrices into the storage buffer the vertex shaders index by instance. +static void _uploadInstances(SDL_GPUCommandBuffer *commands, int32_t drawCount) { + if (!_uploadDynamic(commands, SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, &_scene.instanceBuffer, &_scene.instanceTransfer, &_scene.instanceCapacity, _scene.instances, (uint32_t)drawCount * (uint32_t)sizeof(InstanceMatricesT), "instance")) { + utilDie("Unable to upload the scene's matrices."); + } +} + + static void _uploadLines(SDL_GPUCommandBuffer *commands) { if (!_uploadDynamic(commands, SDL_GPU_BUFFERUSAGE_VERTEX, &_scene.lineBuffer, &_scene.lineTransfer, &_scene.lineCapacity, _scene.lineVertices, (uint32_t)_scene.lineVertexCount * (uint32_t)sizeof(LineVertexT), "line")) { _scene.lineVertexCount = 0; @@ -4031,19 +4036,6 @@ bool materialSetUnlit(int32_t material, bool unlit) { } -// A rendered view as the base colour texture; NO_HANDLE goes back to the material's own texture. -bool materialSetView(int32_t material, int32_t view) { - if (!materialValid(material) || ((view != NO_HANDLE) && !viewValid(view))) { - return false; - } - _scene.materials[material].view = view; - if (view != NO_HANDLE) { - _scene.materials[material].feed = NO_HANDLE; - } - return true; -} - - // A video player's frames as the base colour texture; replaces any image. The frames arrive // through sceneUpdateVideo each frame. bool materialSetVideo(int32_t material, int32_t player) { @@ -4057,6 +4049,19 @@ bool materialSetVideo(int32_t material, int32_t player) { } +// A rendered view as the base colour texture; NO_HANDLE goes back to the material's own texture. +bool materialSetView(int32_t material, int32_t view) { + if (!materialValid(material) || ((view != NO_HANDLE) && !viewValid(view))) { + return false; + } + _scene.materials[material].view = view; + if (view != NO_HANDLE) { + _scene.materials[material].feed = NO_HANDLE; + } + return true; +} + + bool materialValid(int32_t material) { return (material >= 0) && (material < _scene.materialCount) && _scene.materials[material].used; } @@ -4179,6 +4184,196 @@ int32_t meshFindMorph(int32_t mesh, const char *name) { } +// The mesh's geometry as kept on the CPU: x, y, z per vertex and triangle indices. +bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount) { + if (!meshValid(mesh)) { + return false; + } + *positions = _scene.meshes[mesh].positions; + *vertexCount = _scene.meshes[mesh].vertexCount; + *indices = _scene.meshes[mesh].indices; + *indexCount = (int32_t)_scene.meshes[mesh].indexCount; + return true; +} + + +// A heightmap mesh's samples and size; false for any other mesh. +bool meshGetHeights(int32_t mesh, const float **heights, int32_t *columns, int32_t *rows, float *sizeX, float *sizeY, float *sizeZ) { + if (!meshValid(mesh) || (_scene.meshes[mesh].heights == NULL)) { + return false; + } + *heights = _scene.meshes[mesh].heights; + *columns = _scene.meshes[mesh].heightColumns; + *rows = _scene.meshes[mesh].heightRows; + *sizeX = _scene.meshes[mesh].sizeX; + *sizeY = _scene.meshes[mesh].sizeY; + *sizeZ = _scene.meshes[mesh].sizeZ; + return true; +} + + +int32_t meshGetMorphCount(int32_t mesh) { + if (!meshValid(mesh)) { + return 0; + } + return _scene.meshes[mesh].morphCount; +} + + +const char *meshGetMorphName(int32_t mesh, int32_t target) { + if (!meshValid(mesh) || (target < 0) || (target >= _scene.meshes[mesh].morphCount) || (_scene.meshes[mesh].morphNames[target] == NULL)) { + return ""; + } + return _scene.meshes[mesh].morphNames[target]; +} + + +// A plane of columns x rows quads, for cloth and terrain that bends; the first row is the near +// (+Z) edge. +int32_t meshGrid(float width, float depth, int32_t columns, int32_t rows) { + return _gridMesh(NULL, columns, rows, width, 0.0f, depth, false); +} + + +// A grid of columns x rows cells across sizeX by sizeZ, each vertex lifted by its sample (0 to 1) +// times sizeY, centred on the origin with the first row of samples at the far (-Z) edge and UVs +// 0 to 1 across the whole (materialSetTiling repeats a texture over it). Normals come from the +// slopes. The samples are kept for the height field body and terrainGetHeight. +int32_t meshHeightmap(const float *heights, int32_t columns, int32_t rows, float sizeX, float sizeY, float sizeZ) { + int32_t vertexCount = (columns + 1) * (rows + 1); + int32_t mesh; + MeshT *m; + + if (heights == NULL) { + return NO_HANDLE; + } + mesh = _gridMesh(heights, columns, rows, sizeX, sizeY, sizeZ, true); + if (mesh == NO_HANDLE) { + return NO_HANDLE; + } + m = &_scene.meshes[mesh]; + m->heights = SDL_malloc(sizeof(float) * (size_t)vertexCount); + if (m->heights == NULL) { + utilDie("Out of memory keeping a heightmap."); + } + memcpy(m->heights, heights, sizeof(float) * (size_t)vertexCount); + m->heightColumns = columns; + m->heightRows = rows; + m->sizeX = sizeX; + m->sizeY = sizeY; + m->sizeZ = sizeZ; + return mesh; +} + + +// Raw geometry from a script: positions (3 per vertex), normals (3, may be NULL for flat +// shading computed here), uvs (2, may be NULL), and triangle indices. +int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) { + SceneVertexT *vertices; + int32_t x; + int32_t mesh; + + if ((positions == NULL) || (indices == NULL) || (vertexCount <= 0) || (indexCount < 3)) { + return NO_HANDLE; + } + for (x = 0; x < indexCount; x++) { + if (indices[x] >= (uint32_t)vertexCount) { + return NO_HANDLE; + } + } + vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT)); + if (vertices == NULL) { + utilDie("Out of memory building a mesh."); + } + for (x = 0; x < vertexCount; x++) { + vertices[x] = _vertex(positions[x * 3], positions[x * 3 + 1], positions[x * 3 + 2], 0.0f, 0.0f, 0.0f, uvs ? uvs[x * 2] : 0.0f, uvs ? uvs[x * 2 + 1] : 0.0f); + if (normals != NULL) { + vertices[x].normal[0] = normals[x * 3]; + vertices[x].normal[1] = normals[x * 3 + 1]; + vertices[x].normal[2] = normals[x * 3 + 2]; + } + } + if (normals == NULL) { + sceneComputeNormals(vertices, vertexCount, indices, indexCount); + } + mesh = _addMesh(vertices, vertexCount, indices, indexCount, false); + SDL_free(vertices); + return mesh; +} + + +// Geometry a loader has already laid out in GPU form. +int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned) { + int32_t x; + + if ((vertices == NULL) || (indices == NULL) || (vertexCount <= 0) || (indexCount < 3)) { + return NO_HANDLE; + } + for (x = 0; x < indexCount; x++) { + if (indices[x] >= (uint32_t)vertexCount) { + return NO_HANDLE; + } + } + return _addMesh(vertices, vertexCount, indices, indexCount, skinned); +} + + +// A quad in the XZ plane facing +Y. +int32_t meshPlane(float width, float depth) { + return _quadMesh(width, depth, vec3(0.0f, 0.0f, 1.0f), vec3(0.0f, 1.0f, 0.0f)); +} + + +// Gives the mesh morph targets: deltas holds, per target, per vertex, a position delta (x, y, z) +// and a normal delta (x, y, z), six floats; names may be NULL or hold NULL entries. Nodes using +// the mesh get a weight per target, all 0. +bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, const char **names) { + MeshT *m; + float *packed; + int32_t count; + int32_t x; + + if (!meshValid(mesh) || (deltas == NULL) || (targetCount <= 0)) { + return false; + } + m = &_scene.meshes[mesh]; + count = targetCount * m->vertexCount; + packed = SDL_calloc((size_t)count * MORPH_FLOATS, sizeof(float)); + if (packed == NULL) { + utilDie("Out of memory packing morph targets."); + } + // float4 pairs for the shader: xyz0 position delta, xyz0 normal delta. + for (x = 0; x < count; x++) { + packed[x * MORPH_FLOATS] = deltas[x * 6]; + packed[x * MORPH_FLOATS + 1] = deltas[x * 6 + 1]; + packed[x * MORPH_FLOATS + 2] = deltas[x * 6 + 2]; + packed[x * MORPH_FLOATS + 4] = deltas[x * 6 + 3]; + packed[x * MORPH_FLOATS + 5] = deltas[x * 6 + 4]; + packed[x * MORPH_FLOATS + 6] = deltas[x * 6 + 5]; + } + _freeMorphs(m); + m->morphBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, packed, (uint32_t)((size_t)count * MORPH_FLOATS * sizeof(float))); + SDL_free(packed); + if (m->morphBuffer == NULL) { + return false; + } + m->morphNames = SDL_calloc((size_t)targetCount, sizeof(char *)); + if (m->morphNames == NULL) { + utilDie("Out of memory naming morph targets."); + } + for (x = 0; x < targetCount; x++) { + m->morphNames[x] = ((names != NULL) && (names[x] != NULL)) ? SDL_strdup(names[x]) : NULL; + } + m->morphCount = targetCount; + for (x = 0; x < _scene.nodeCount; x++) { + if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) { + _matchMorphWeights(&_scene.nodes[x]); + } + } + return true; +} + + // Rewrites a mesh's vertex positions (x, y, z per vertex, in the mesh's space), recomputing normals // and bounds and uploading in place, for meshes a soft body drives. bool meshSetPositions(int32_t mesh, const float *positions) { @@ -4249,196 +4444,6 @@ bool meshSetPositions(int32_t mesh, const float *positions) { } -// The mesh's geometry as kept on the CPU: x, y, z per vertex and triangle indices. -bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount) { - if (!meshValid(mesh)) { - return false; - } - *positions = _scene.meshes[mesh].positions; - *vertexCount = _scene.meshes[mesh].vertexCount; - *indices = _scene.meshes[mesh].indices; - *indexCount = (int32_t)_scene.meshes[mesh].indexCount; - return true; -} - - -// A heightmap mesh's samples and size; false for any other mesh. -bool meshGetHeights(int32_t mesh, const float **heights, int32_t *columns, int32_t *rows, float *sizeX, float *sizeY, float *sizeZ) { - if (!meshValid(mesh) || (_scene.meshes[mesh].heights == NULL)) { - return false; - } - *heights = _scene.meshes[mesh].heights; - *columns = _scene.meshes[mesh].heightColumns; - *rows = _scene.meshes[mesh].heightRows; - *sizeX = _scene.meshes[mesh].sizeX; - *sizeY = _scene.meshes[mesh].sizeY; - *sizeZ = _scene.meshes[mesh].sizeZ; - return true; -} - - -int32_t meshGetMorphCount(int32_t mesh) { - if (!meshValid(mesh)) { - return 0; - } - return _scene.meshes[mesh].morphCount; -} - - -const char *meshGetMorphName(int32_t mesh, int32_t target) { - if (!meshValid(mesh) || (target < 0) || (target >= _scene.meshes[mesh].morphCount) || (_scene.meshes[mesh].morphNames[target] == NULL)) { - return ""; - } - return _scene.meshes[mesh].morphNames[target]; -} - - -// Raw geometry from a script: positions (3 per vertex), normals (3, may be NULL for flat -// shading computed here), uvs (2, may be NULL), and triangle indices. -int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) { - SceneVertexT *vertices; - int32_t x; - int32_t mesh; - - if ((positions == NULL) || (indices == NULL) || (vertexCount <= 0) || (indexCount < 3)) { - return NO_HANDLE; - } - for (x = 0; x < indexCount; x++) { - if (indices[x] >= (uint32_t)vertexCount) { - return NO_HANDLE; - } - } - vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT)); - if (vertices == NULL) { - utilDie("Out of memory building a mesh."); - } - for (x = 0; x < vertexCount; x++) { - vertices[x] = _vertex(positions[x * 3], positions[x * 3 + 1], positions[x * 3 + 2], 0.0f, 0.0f, 0.0f, uvs ? uvs[x * 2] : 0.0f, uvs ? uvs[x * 2 + 1] : 0.0f); - if (normals != NULL) { - vertices[x].normal[0] = normals[x * 3]; - vertices[x].normal[1] = normals[x * 3 + 1]; - vertices[x].normal[2] = normals[x * 3 + 2]; - } - } - if (normals == NULL) { - sceneComputeNormals(vertices, vertexCount, indices, indexCount); - } - mesh = _addMesh(vertices, vertexCount, indices, indexCount, false); - SDL_free(vertices); - return mesh; -} - - -// Geometry a loader has already laid out in GPU form. -int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned) { - int32_t x; - - if ((vertices == NULL) || (indices == NULL) || (vertexCount <= 0) || (indexCount < 3)) { - return NO_HANDLE; - } - for (x = 0; x < indexCount; x++) { - if (indices[x] >= (uint32_t)vertexCount) { - return NO_HANDLE; - } - } - return _addMesh(vertices, vertexCount, indices, indexCount, skinned); -} - - -// A plane of columns x rows quads, for cloth and terrain that bends; the first row is the near -// (+Z) edge. -int32_t meshGrid(float width, float depth, int32_t columns, int32_t rows) { - return _gridMesh(NULL, columns, rows, width, 0.0f, depth, false); -} - - -// A grid of columns x rows cells across sizeX by sizeZ, each vertex lifted by its sample (0 to 1) -// times sizeY, centred on the origin with the first row of samples at the far (-Z) edge and UVs -// 0 to 1 across the whole (materialSetTiling repeats a texture over it). Normals come from the -// slopes. The samples are kept for the height field body and terrainGetHeight. -int32_t meshHeightmap(const float *heights, int32_t columns, int32_t rows, float sizeX, float sizeY, float sizeZ) { - int32_t vertexCount = (columns + 1) * (rows + 1); - int32_t mesh; - MeshT *m; - - if (heights == NULL) { - return NO_HANDLE; - } - mesh = _gridMesh(heights, columns, rows, sizeX, sizeY, sizeZ, true); - if (mesh == NO_HANDLE) { - return NO_HANDLE; - } - m = &_scene.meshes[mesh]; - m->heights = SDL_malloc(sizeof(float) * (size_t)vertexCount); - if (m->heights == NULL) { - utilDie("Out of memory keeping a heightmap."); - } - memcpy(m->heights, heights, sizeof(float) * (size_t)vertexCount); - m->heightColumns = columns; - m->heightRows = rows; - m->sizeX = sizeX; - m->sizeY = sizeY; - m->sizeZ = sizeZ; - return mesh; -} - - -// A quad in the XZ plane facing +Y. -int32_t meshPlane(float width, float depth) { - return _quadMesh(width, depth, vec3(0.0f, 0.0f, 1.0f), vec3(0.0f, 1.0f, 0.0f)); -} - - -// Gives the mesh morph targets: deltas holds, per target, per vertex, a position delta (x, y, z) -// and a normal delta (x, y, z), six floats; names may be NULL or hold NULL entries. Nodes using -// the mesh get a weight per target, all 0. -bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, const char **names) { - MeshT *m; - float *packed; - int32_t count; - int32_t x; - - if (!meshValid(mesh) || (deltas == NULL) || (targetCount <= 0)) { - return false; - } - m = &_scene.meshes[mesh]; - count = targetCount * m->vertexCount; - packed = SDL_calloc((size_t)count * MORPH_FLOATS, sizeof(float)); - if (packed == NULL) { - utilDie("Out of memory packing morph targets."); - } - // float4 pairs for the shader: xyz0 position delta, xyz0 normal delta. - for (x = 0; x < count; x++) { - packed[x * MORPH_FLOATS] = deltas[x * 6]; - packed[x * MORPH_FLOATS + 1] = deltas[x * 6 + 1]; - packed[x * MORPH_FLOATS + 2] = deltas[x * 6 + 2]; - packed[x * MORPH_FLOATS + 4] = deltas[x * 6 + 3]; - packed[x * MORPH_FLOATS + 5] = deltas[x * 6 + 4]; - packed[x * MORPH_FLOATS + 6] = deltas[x * 6 + 5]; - } - _freeMorphs(m); - m->morphBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, packed, (uint32_t)((size_t)count * MORPH_FLOATS * sizeof(float))); - SDL_free(packed); - if (m->morphBuffer == NULL) { - return false; - } - m->morphNames = SDL_calloc((size_t)targetCount, sizeof(char *)); - if (m->morphNames == NULL) { - utilDie("Out of memory naming morph targets."); - } - for (x = 0; x < targetCount; x++) { - m->morphNames[x] = ((names != NULL) && (names[x] != NULL)) ? SDL_strdup(names[x]) : NULL; - } - m->morphCount = targetCount; - for (x = 0; x < _scene.nodeCount; x++) { - if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) { - _matchMorphWeights(&_scene.nodes[x]); - } - } - return true; -} - - // Latitude/longitude sphere; segments around, half as many from pole to pole. int32_t meshSphere(float radius, int32_t segments) { SceneVertexT *vertices; @@ -4712,6 +4717,18 @@ Vec3T nodeGetScale(int32_t node) { } +// The joint nodes of a skinned node's skin, or 0 with none. +int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints) { + if (!nodeValid(node) || (_scene.nodes[node].skinCount == 0)) { + return 0; + } + if (joints != NULL) { + *joints = _scene.nodes[node].skinJoints; + } + return _scene.nodes[node].skinCount; +} + + // From the last rendered frame's matrices (this frame's edits show after the next render). Vec3T nodeGetWorldPosition(int32_t node) { if (!nodeValid(node)) { @@ -4901,15 +4918,13 @@ bool nodeSetScale(int32_t node, Vec3T scale) { } -// The joint nodes of a skinned node's skin, or 0 with none. -int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints) { - if (!nodeValid(node) || (_scene.nodes[node].skinCount == 0)) { - return 0; +// Whether the node's mesh is drawn into shadow maps; a bulb's own mesh or a glowing sign is not. +bool nodeSetShadow(int32_t node, bool casts) { + if (!nodeValid(node)) { + return false; } - if (joints != NULL) { - *joints = _scene.nodes[node].skinJoints; - } - return _scene.nodes[node].skinCount; + _scene.nodes[node].shadowCaster = casts; + return true; } @@ -4938,16 +4953,6 @@ bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, } -// Whether the node's mesh is drawn into shadow maps; a bulb's own mesh or a glowing sign is not. -bool nodeSetShadow(int32_t node, bool casts) { - if (!nodeValid(node)) { - return false; - } - _scene.nodes[node].shadowCaster = casts; - return true; -} - - // A picture on the shared unit quad (width by height world units, the node's scale on top): the // frames become textures, a private blended double-sided material shows the current one, and the // node's mesh becomes the quad. NULL frames clears it. @@ -5091,13 +5096,44 @@ bool sceneAvailable(void) { } -// Turns the layer on or off. Fails only when the machine has no GPU device. -bool sceneEnable(bool enabled) { - if (enabled && (_scene.device == NULL)) { - return false; +Ktx2FormatE sceneCompressedFormat(void) { + return _scene.compressedFormat; +} + + +// Smooth normals from the triangles: face normals accumulated per vertex, then normalised. +void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) { + int32_t x; + + for (x = 0; x < vertexCount; x++) { + vertices[x].normal[0] = 0.0f; + vertices[x].normal[1] = 0.0f; + vertices[x].normal[2] = 0.0f; + } + for (x = 0; x + 2 < indexCount; x += 3) { + SceneVertexT *a = &vertices[indices[x]]; + SceneVertexT *b = &vertices[indices[x + 1]]; + SceneVertexT *c = &vertices[indices[x + 2]]; + SceneVertexT *corners[3] = { a, b, c }; + Vec3T pa = vec3(a->position[0], a->position[1], a->position[2]); + Vec3T pb = vec3(b->position[0], b->position[1], b->position[2]); + Vec3T pc = vec3(c->position[0], c->position[1], c->position[2]); + Vec3T n = vec3Cross(vec3Subtract(pb, pa), vec3Subtract(pc, pa)); + int32_t k; + + for (k = 0; k < 3; k++) { + corners[k]->normal[0] += n.x; + corners[k]->normal[1] += n.y; + corners[k]->normal[2] += n.z; + } + } + for (x = 0; x < vertexCount; x++) { + Vec3T n = vec3Normalize(vec3(vertices[x].normal[0], vertices[x].normal[1], vertices[x].normal[2])); + + vertices[x].normal[0] = n.x; + vertices[x].normal[1] = n.y; + vertices[x].normal[2] = n.z; } - _scene.enabled = enabled; - return true; } @@ -5198,14 +5234,19 @@ void sceneDrawLine(Vec3T from, Vec3T to, uint8_t r, uint8_t g, uint8_t b) { } -void sceneGetSize(int32_t *width, int32_t *height) { - *width = _scene.width; - *height = _scene.height; +// Turns the layer on or off. Fails only when the machine has no GPU device. +bool sceneEnable(bool enabled) { + if (enabled && (_scene.device == NULL)) { + return false; + } + _scene.enabled = enabled; + return true; } -Ktx2FormatE sceneCompressedFormat(void) { - return _scene.compressedFormat; +void sceneGetSize(int32_t *width, int32_t *height) { + *width = _scene.width; + *height = _scene.height; } @@ -5482,6 +5523,7 @@ void sceneQuit(void) { SDL_free(_scene.skins); SDL_free(_scene.skip); SDL_free(_scene.particleOrder); + SDL_free(_scene.blendedOrder); SDL_free(_scene.spriteNodes); SDL_free(_scene.sizedTextures); SDL_free(_scene.sizedBytes); @@ -5546,8 +5588,8 @@ SDL_Texture *sceneRender(void) { _scene.shadowCount = 0; _fillLights(&fragmentUniforms); // Collect what to draw in one walk: opaque draws from the front of the array, sorted for - // batching, blended ones from the back, then moved up behind them and sorted back to front - // from the window's camera (a view sees them in that order too). + // batching, blended ones from the back, then moved up behind them; each camera orders those + // back to front from its own eye as it renders (_orderBlended). if (_scene.drawCapacity < _scene.nodeCount) { _scene.draws = SDL_realloc(_scene.draws, sizeof(DrawT) * (size_t)_scene.nodeCount); if (_scene.draws == NULL) { @@ -5563,19 +5605,17 @@ SDL_Texture *sceneRender(void) { } if ((node->material != NO_HANDLE) && _scene.materials[node->material].blend) { blendedStart--; - _scene.draws[blendedStart].node = x; - _scene.draws[blendedStart].depth = vec3Length(vec3Subtract(mat4TransformPoint(node->world, vec3(0.0f, 0.0f, 0.0f)), main.eye)); + _scene.draws[blendedStart].node = x; } else { - _scene.draws[opaqueCount].node = x; - _scene.draws[opaqueCount].depth = 0.0f; + _scene.draws[opaqueCount].node = x; opaqueCount++; } } - drawCount = opaqueCount + (_scene.nodeCount - blendedStart); + drawCount = opaqueCount + (_scene.nodeCount - blendedStart); + _scene.opaqueCount = opaqueCount; qsort(_scene.draws, (size_t)opaqueCount, sizeof(DrawT), _compareOpaque); if (drawCount > opaqueCount) { memmove(&_scene.draws[opaqueCount], &_scene.draws[blendedStart], sizeof(DrawT) * (size_t)(drawCount - opaqueCount)); - qsort(&_scene.draws[opaqueCount], (size_t)(drawCount - opaqueCount), sizeof(DrawT), _compareDraws); } commands = SDL_AcquireGPUCommandBuffer(_scene.device); if (commands == NULL) { @@ -5584,10 +5624,10 @@ SDL_Texture *sceneRender(void) { } // Bounds and matrices for every draw. _boundDraws(drawCount); - _fillInstances(drawCount, &main); + _fillInstances(drawCount); _uploadInstances(commands, drawCount); // The shadow passes: depth from every shadow light into its map layer, or its six cube faces, - // fitted to the window's camera and shared by every view this frame. + // fitted to the window's camera (billboards turned to it) and shared by every view this frame. for (slot = 0; slot < _scene.shadowCount; slot++) { layers += (_scene.shadows[slot].type == SHADOW_CUBE) ? CUBE_FACES : _scene.shadows[slot].cascades; } @@ -5627,7 +5667,7 @@ SDL_Texture *sceneRender(void) { depth.texture = _scene.shadowMaps; depth.layer = (Uint8)(shadow->layer + face); pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &shadow->faces[face], true, true, skip, NULL, SAMPLE_SET_SINGLE); + _drawList(commands, pass, drawCount, &shadow->faces[face], &main, true, true, skip, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); } } else if (shadow->type == SHADOW_CASCADE) { @@ -5641,7 +5681,7 @@ SDL_Texture *sceneRender(void) { depth.texture = _scene.shadowMaps; depth.layer = (Uint8)(shadow->layer + k); pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &shadow->faces[k], true, false, skip, NULL, SAMPLE_SET_SINGLE); + _drawList(commands, pass, drawCount, &shadow->faces[k], &main, true, false, skip, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); } } else { @@ -5649,7 +5689,7 @@ SDL_Texture *sceneRender(void) { depth.texture = _scene.shadowMaps; depth.layer = (Uint8)shadow->layer; pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &shadow->matrix, true, false, NULL, NULL, SAMPLE_SET_SINGLE); + _drawList(commands, pass, drawCount, &shadow->matrix, &main, true, false, NULL, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); } } @@ -5761,42 +5801,6 @@ bool sceneResize(int32_t width, int32_t height) { } -// Smooth normals from the triangles: face normals accumulated per vertex, then normalised. -void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) { - int32_t x; - - for (x = 0; x < vertexCount; x++) { - vertices[x].normal[0] = 0.0f; - vertices[x].normal[1] = 0.0f; - vertices[x].normal[2] = 0.0f; - } - for (x = 0; x + 2 < indexCount; x += 3) { - SceneVertexT *a = &vertices[indices[x]]; - SceneVertexT *b = &vertices[indices[x + 1]]; - SceneVertexT *c = &vertices[indices[x + 2]]; - SceneVertexT *corners[3] = { a, b, c }; - Vec3T pa = vec3(a->position[0], a->position[1], a->position[2]); - Vec3T pb = vec3(b->position[0], b->position[1], b->position[2]); - Vec3T pc = vec3(c->position[0], c->position[1], c->position[2]); - Vec3T n = vec3Cross(vec3Subtract(pb, pa), vec3Subtract(pc, pa)); - int32_t k; - - for (k = 0; k < 3; k++) { - corners[k]->normal[0] += n.x; - corners[k]->normal[1] += n.y; - corners[k]->normal[2] += n.z; - } - } - for (x = 0; x < vertexCount; x++) { - Vec3T n = vec3Normalize(vec3(vertices[x].normal[0], vertices[x].normal[1], vertices[x].normal[2])); - - vertices[x].normal[0] = n.x; - vertices[x].normal[1] = n.y; - vertices[x].normal[2] = n.z; - } -} - - void sceneSetAmbient(uint8_t r, uint8_t g, uint8_t b) { _scene.ambient = vec3(_linear(r), _linear(g), _linear(b)); } @@ -5873,26 +5877,6 @@ void sceneSetShadowSize(int32_t size) { } -// Overlay coordinates back to a world point: the point on the ray through that pixel at the given -// distance from the camera's near plane, using the last rendered frame's camera. Two distances -// give a ray for picking. -Vec3T sceneUnproject(float x, float y, float distance) { - Mat4T inverse; - Vec3T ndc; - Vec3T near; - Vec3T far; - - if ((_scene.width <= 0) || !mat4Invert(_scene.viewProjection, &inverse)) { - return vec3(0.0f, 0.0f, 0.0f); - } - ndc = vec3(x / (float)_scene.width * 2.0f - 1.0f, 1.0f - y / (float)_scene.height * 2.0f, 0.0f); - near = mat4TransformPoint(inverse, ndc); - ndc.z = 1.0f; - far = mat4TransformPoint(inverse, ndc); - return vec3Add(near, vec3Scale(vec3Normalize(vec3Subtract(far, near)), distance)); -} - - // The sky from an equirectangular image of linear RGB floats (NULL removes it): six cube faces // resampled from it, a mip chain for reflections by roughness, and the spherical harmonics of its // diffuse light. @@ -5952,6 +5936,26 @@ void sceneSetTonemap(SceneTonemapE tonemap) { } +// Overlay coordinates back to a world point: the point on the ray through that pixel at the given +// distance from the camera's near plane, using the last rendered frame's camera. Two distances +// give a ray for picking. +Vec3T sceneUnproject(float x, float y, float distance) { + Mat4T inverse; + Vec3T ndc; + Vec3T near; + Vec3T far; + + if ((_scene.width <= 0) || !mat4Invert(_scene.viewProjection, &inverse)) { + return vec3(0.0f, 0.0f, 0.0f); + } + ndc = vec3(x / (float)_scene.width * 2.0f - 1.0f, 1.0f - y / (float)_scene.height * 2.0f, 0.0f); + near = mat4TransformPoint(inverse, ndc); + ndc.z = 1.0f; + far = mat4TransformPoint(inverse, ndc); + return vec3Add(near, vec3Scale(vec3Normalize(vec3Subtract(far, near)), distance)); +} + + // Rebuilds every node's world matrix now (sceneRender does it too); physics needs them before the // step, after animation has moved the nodes. void sceneUpdateTransforms(void) { @@ -6003,6 +6007,54 @@ void sceneUpdateVideo(SceneVideoSourceFn source) { } +// ===== Terrain ===== + +// The world height of a heightmap mesh's node at world x, z (the node's position and scale apply; +// its rotation is ignored, terrains lying flat), bilinear between samples; false off the mesh. +bool terrainGetHeight(int32_t node, float x, float z, float *height) { + const float *heights; + int32_t columns; + int32_t rows; + float sizeX; + float sizeY; + float sizeZ; + Vec3T position; + QuatT rotation; + Vec3T scale; + float u; + float v; + int32_t x0; + int32_t y0; + float fx; + float fy; + float h00; + float h10; + float h01; + float h11; + + if (!nodeValid(node) || !meshGetHeights(_scene.nodes[node].mesh, &heights, &columns, &rows, &sizeX, &sizeY, &sizeZ) || !nodeGetWorldTransform(node, &position, &rotation, &scale)) { + return false; + } + u = ((x - position.x) / SDL_max(scale.x, 0.0001f) + sizeX / 2.0f) / sizeX; + v = ((z - position.z) / SDL_max(scale.z, 0.0001f) + sizeZ / 2.0f) / sizeZ; + if ((u < 0.0f) || (u > 1.0f) || (v < 0.0f) || (v > 1.0f)) { + return false; + } + u *= (float)columns; + v *= (float)rows; + x0 = SDL_min((int32_t)u, columns - 1); + y0 = SDL_min((int32_t)v, rows - 1); + fx = u - (float)x0; + fy = v - (float)y0; + h00 = heights[y0 * (columns + 1) + x0]; + h10 = heights[y0 * (columns + 1) + x0 + 1]; + h01 = heights[(y0 + 1) * (columns + 1) + x0]; + h11 = heights[(y0 + 1) * (columns + 1) + x0 + 1]; + *height = position.y + scale.y * sizeY * ((h00 * (1.0f - fx) + h10 * fx) * (1.0f - fy) + (h01 * (1.0f - fx) + h11 * fx) * fy); + return true; +} + + // ===== Views: cameras rendered to textures ===== bool viewDelete(int32_t view) { @@ -6081,51 +6133,3 @@ bool viewSetCamera(int32_t view, int32_t camera) { bool viewValid(int32_t view) { return (view >= 0) && (view < MAX_VIEWS) && _scene.views[view].used; } - - -// ===== Terrain ===== - -// The world height of a heightmap mesh's node at world x, z (the node's position and scale apply; -// its rotation is ignored, terrains lying flat), bilinear between samples; false off the mesh. -bool terrainGetHeight(int32_t node, float x, float z, float *height) { - const float *heights; - int32_t columns; - int32_t rows; - float sizeX; - float sizeY; - float sizeZ; - Vec3T position; - QuatT rotation; - Vec3T scale; - float u; - float v; - int32_t x0; - int32_t y0; - float fx; - float fy; - float h00; - float h10; - float h01; - float h11; - - if (!nodeValid(node) || !meshGetHeights(_scene.nodes[node].mesh, &heights, &columns, &rows, &sizeX, &sizeY, &sizeZ) || !nodeGetWorldTransform(node, &position, &rotation, &scale)) { - return false; - } - u = ((x - position.x) / SDL_max(scale.x, 0.0001f) + sizeX / 2.0f) / sizeX; - v = ((z - position.z) / SDL_max(scale.z, 0.0001f) + sizeZ / 2.0f) / sizeZ; - if ((u < 0.0f) || (u > 1.0f) || (v < 0.0f) || (v > 1.0f)) { - return false; - } - u *= (float)columns; - v *= (float)rows; - x0 = SDL_min((int32_t)u, columns - 1); - y0 = SDL_min((int32_t)v, rows - 1); - fx = u - (float)x0; - fy = v - (float)y0; - h00 = heights[y0 * (columns + 1) + x0]; - h10 = heights[y0 * (columns + 1) + x0 + 1]; - h01 = heights[(y0 + 1) * (columns + 1) + x0]; - h11 = heights[(y0 + 1) * (columns + 1) + x0 + 1]; - *height = position.y + scale.y * sizeY * ((h00 * (1.0f - fx) + h10 * fx) * (1.0f - fy) + (h01 * (1.0f - fx) + h11 * fx) * fy); - return true; -} diff --git a/src/scene.h b/src/scene.h index 58e63b3fe..85cbb0ab7 100644 --- a/src/scene.h +++ b/src/scene.h @@ -60,11 +60,11 @@ typedef enum MaterialFilterE { FILTER_NEAREST = 1 } MaterialFilterE; -// How a node turns to face the camera. +// How a node turns to face the camera (the codes are the shader's, sceneShared.h). typedef enum BillboardE { - BILLBOARD_NONE = 0, - BILLBOARD_ALL = 1, // Faces the camera squarely - BILLBOARD_Y = 2 // Turns about its own up axis only (trees, health bars) + BILLBOARD_NONE = BILLBOARD_MODE_NONE, + BILLBOARD_ALL = BILLBOARD_MODE_ALL, // Faces the camera squarely + BILLBOARD_Y = BILLBOARD_MODE_Y // Turns about world up only (trees, health bars) } BillboardE; // A material's textures, for the compressed path. diff --git a/src/sceneShared.h b/src/sceneShared.h new file mode 100644 index 000000000..dd8ae16cc --- /dev/null +++ b/src/sceneShared.h @@ -0,0 +1,65 @@ +/* + * + * Singe 3 + * Copyright (C) 2006-2026 Scott Duensing + * + * This program 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; either version 3 + * of the License, or (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + */ + +#ifndef SCENE_SHARED_H +#define SCENE_SHARED_H + +// The limits and codes the scene's uniform blocks are laid out with, shared by the C side +// (scene.h, scene.c) and the shaders (shaders/scene.hlsl), so the two cannot drift apart. +// Preprocessor definitions only: this file is read by DXC as well as the C compiler. + +// Sizes of the uniform arrays. +#define MAX_LIGHTS 8 // Lights the fragment shader sees per frame +#define MAX_JOINTS 128 // Joint matrices per skinned draw +#define MAX_SHADOWS MAX_LIGHTS // Every light may cast; the arrays are sized to the lights in use +#define MAX_MORPHS 8 // Active morph targets per draw +#define MAX_CASCADES 4 // Cascades of a directional light's shadow +#define SH_COEFFICIENTS 9 // Spherical harmonics to second order + +// A shadow slot's kind (shadowInfo[slot].x). +#define SHADOW_NONE 0 +#define SHADOW_MAP 1 // One map: a spot light, or a directional light without cascades +#define SHADOW_CUBE 2 // Six faces: a point light +#define SHADOW_CASCADE 3 // A directional light's shadow split along the camera's view + +// A light's kind (positionType.w). +#define LIGHT_TYPE_DIRECTIONAL 0 +#define LIGHT_TYPE_POINT 1 +#define LIGHT_TYPE_SPOT 2 + +// The post pass's tone curve (postParams.y). +#define TONEMAP_CURVE_NONE 0 +#define TONEMAP_CURVE_NEUTRAL 1 +#define TONEMAP_CURVE_ACES 2 + +// What material.w says about the base texture. +#define TEXTURE_NONE 0 +#define TEXTURE_SRGB 1 // A texture the sampler decodes to linear +#define TEXTURE_FEED 2 // A video frame or rendered view, sRGB, decoded in the shader + +// How a draw turns to face the camera (the spare lane of its normal matrix, instanceMatrices). +#define BILLBOARD_MODE_NONE 0 +#define BILLBOARD_MODE_ALL 1 // Its axes are the camera's +#define BILLBOARD_MODE_Y 2 // Turned about world +Y toward the eye only + + +#endif diff --git a/src/shaders/scene.hlsl b/src/shaders/scene.hlsl index 20758bfdc..9b6cb3d92 100644 --- a/src/shaders/scene.hlsl +++ b/src/shaders/scene.hlsl @@ -40,6 +40,7 @@ #define PI 3.14159265 #define DEGREES_TO_RADIANS 0.017453292 #define MIN_ROUGHNESS 0.045 // Below this the GGX lobe is narrower than a pixel and sparkles +#define BILLBOARD_EPSILON 0.000001 // Below this a BILLBOARD_MODE_Y node is under the eye and faces +Z // ----- Vertex ----- @@ -49,6 +50,9 @@ cbuffer DrawUniforms : register(b0, space1) { float4 morphWeights[2]; // Up to MAX_MORPHS active targets ... int4 morphTargets[2]; // ... and which targets they are int4 morphInfo; // x = active count, y = vertices per target, z = the draw's first pair in instanceMatrices + float4 billboardRight; // The axes and eye billboards turn to: the pass's camera (a shadow pass gives the window's) + float4 billboardUp; + float4 billboardEye; }; @@ -56,7 +60,8 @@ cbuffer DrawUniforms : register(b0, space1) { StructuredBuffer morphDeltas : register(t0, space0); // Per draw, in draw order: the model matrix then its inverse transpose (for normals under -// non-uniform scale); an instanced batch reads consecutive pairs. +// non-uniform scale); an instanced batch reads consecutive pairs. The inverse transpose's spare +// lane (_m33) holds the draw's BILLBOARD_MODE_*; a billboard's pair is rebuilt here per camera. StructuredBuffer instanceMatrices : register(t1, space0); cbuffer SkinUniforms : register(b1, space1) { @@ -102,6 +107,44 @@ void morph(uint vertex, inout float3 position, inout float3 normal) { } +// Turns a billboard's model matrix (and its normal matrix) to this pass's camera: squarely, its +// axes the camera's, or about world +Y only toward the eye. The node's own position and scale +// stay; the normal matrix of an orthonormal frame under scale is the frame over the scale. +void billboard(inout float4x4 model, inout float4x4 normalMatrix) { + int mode = (int)normalMatrix._m33; + float3 position = model._m03_m13_m23; + float3 scale = float3(length(model._m00_m10_m20), length(model._m01_m11_m21), length(model._m02_m12_m22)); + float3 right; + float3 up; + float3 toward; + + if (mode == BILLBOARD_MODE_NONE) { + return; + } + if (mode == BILLBOARD_MODE_ALL) { + right = billboardRight.xyz; + up = billboardUp.xyz; + toward = cross(right, up); + } else { + toward = billboardEye.xyz - position; + toward.y = 0.0; + if (length(toward) < BILLBOARD_EPSILON) { + toward = float3(0.0, 0.0, 1.0); + } + toward = normalize(toward); + up = float3(0.0, 1.0, 0.0); + right = cross(up, toward); + } + scale = max(scale, BILLBOARD_EPSILON); + model._m00_m10_m20 = right * scale.x; + model._m01_m11_m21 = up * scale.y; + model._m02_m12_m22 = toward * scale.z; + normalMatrix._m00_m10_m20 = right / scale.x; + normalMatrix._m01_m11_m21 = up / scale.y; + normalMatrix._m02_m12_m22 = toward / scale.z; +} + + VertexOutput vertexStatic(VertexInput input, uint vertex : SV_VertexID, uint instance : SV_InstanceID) { VertexOutput output; float3 position = input.position; @@ -109,6 +152,7 @@ VertexOutput vertexStatic(VertexInput input, uint vertex : SV_VertexID, uint ins float4x4 model = instanceMatrices[(morphInfo.z + instance) * 2]; float4x4 normalMatrix = instanceMatrices[(morphInfo.z + instance) * 2 + 1]; + billboard(model, normalMatrix); morph(vertex, position, normal); output.worldPosition = mul(model, float4(position, 1.0)).xyz; output.position = mul(viewProjection, float4(output.worldPosition, 1.0)); @@ -130,6 +174,7 @@ VertexOutput vertexSkinned(VertexInput input, uint vertex : SV_VertexID, uint in float3 skinnedNormal; float3 skinnedTangent; + billboard(model, normalMatrix); morph(vertex, morphed, normal); position = float4(morphed, 1.0); diff --git a/src/singe.c b/src/singe.c index 70c025f39..b37859928 100644 --- a/src/singe.c +++ b/src/singe.c @@ -461,45 +461,41 @@ static const int32_t _sdlMouseButtonToCode[] = { 0, 0, 2, 1, 3, 4 }; static int32_t _apiUnimplemented(lua_State *L, const char *method); static int32_t _argAnimation(lua_State *L, const char *method, int32_t model, int32_t index); static int32_t _argAnimationLayer(lua_State *L, const char *method, int32_t index); +static int32_t _argBody(lua_State *L, const char *method, int32_t index); static bool _argBoolean(lua_State *L, const char *method, int32_t index); static int32_t _argChannel(lua_State *L, const char *method, int32_t index); -static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index); static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum); +static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index); +static int32_t _argEmitter(lua_State *L, const char *method, int32_t index); static QuatT _argEuler(lua_State *L, const char *method, int32_t index); static float *_argFloatTable(lua_State *L, const char *method, int32_t index, int32_t *count); static FontT *_argFont(lua_State *L, const char *method, int32_t index); static int32_t _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun); static int32_t _argInteger(lua_State *L, const char *method, int32_t index); -static int32_t _argBody(lua_State *L, const char *method, int32_t index); -static int32_t _argEmitter(lua_State *L, const char *method, int32_t index); +static int64_t _argInteger64(lua_State *L, const char *method, int32_t index); static bool _argMapImage(lua_State *L, const char *method, int32_t index, SDL_Surface **surface, Ktx2ImageT *ktx2); static int32_t _argMaterial(lua_State *L, const char *method, int32_t index); -static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index); static int32_t _argMesh(lua_State *L, const char *method, int32_t index); +static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index); static int32_t _argNav(lua_State *L, const char *method, int32_t index); static int32_t _argNavAgent(lua_State *L, const char *method, int32_t index); static int32_t _argNode(lua_State *L, const char *method, int32_t index); static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, bool (*exists)(int32_t), const char *noun); +static double _argNumber(lua_State *L, const char *method, int32_t index); static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b); static int32_t _argPlayer(lua_State *L, const char *method, int32_t index); static int32_t _argRagdoll(lua_State *L, const char *method, int32_t index); static int32_t _argSoft(lua_State *L, const char *method, int32_t index); -static int32_t _argVehicle(lua_State *L, const char *method, int32_t index); -static int64_t _argInteger64(lua_State *L, const char *method, int32_t index); -static double _argNumber(lua_State *L, const char *method, int32_t index); static SoundT *_argSound(lua_State *L, const char *method, int32_t index); static SpriteT *_argSprite(lua_State *L, const char *method, int32_t index); static const char *_argString(lua_State *L, const char *method, int32_t index); static Vec3T _argVec3(lua_State *L, const char *method, int32_t index); -static int32_t _argView(lua_State *L, const char *method, int32_t index); +static int32_t _argVehicle(lua_State *L, const char *method, int32_t index); static VideoT *_argVideo(lua_State *L, const char *method, int32_t index); +static int32_t _argView(lua_State *L, const char *method, int32_t index); static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base); static void _callLua(const char *func, const char *sig, ...); static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2); -static int32_t _effectTrackFree(void); -static float _effectGain(const EffectT *effect, float distance); -static void _effectReset(int32_t channel); -static void _effectStopped(void *userdata, MIX_Track *track); static int32_t _controllerSlot(SDL_JoystickID which); static bool _delayAndPump(uint32_t ms); static void _deliverKey(bool down, int32_t keysym, int32_t scancode); @@ -508,8 +504,12 @@ static void _discSeek(int64_t frame); static void _doLogos(void); static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel); static void _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target); -static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY); static void _drawPauseIndicator(const SDL_FRect *target); +static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY); +static float _effectGain(const EffectT *effect, float distance); +static void _effectReset(int32_t channel); +static void _effectStopped(void *userdata, MIX_Track *track); +static int32_t _effectTrackFree(void); static InputE _engineSwitch(int32_t scancode); static void _fireMouseMoved(int32_t device, int32_t x, int32_t y, int32_t xr, int32_t yr); static void _fitRect(int32_t width, int32_t height, int32_t spaceWidth, int32_t spaceHeight, int32_t margin, SDL_FRect *rect); @@ -543,33 +543,33 @@ static int32_t _luaTraceback(lua_State *L); static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength); static float _mixerGain(int32_t effectsVolume); static int32_t _mouseCode(int32_t device, int32_t button); +static void _navCallbacks(void); static uint32_t _overlayColor(const SDL_Color *color); static void _overlayResize(int32_t width, int32_t height); static void _overlayTouched(void); -static void _pauseAllVideos(bool pause); -static void _processKey(bool down, int32_t keysym, int32_t scancode); -static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2))); -static int32_t _pushVec3(lua_State *L, Vec3T v); -static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad); -static void _quadScratch(int32_t quads, int32_t frames); -static void _navCallbacks(void); -static void _physicsCallbacks(void); static void _particleTexturesDestroy(ParticleTexturesT *cache); static void _particleTexturesFree(int32_t emitter); static void _particleTexturesFreeAll(void); static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view); +static void _pauseAllVideos(bool pause); +static void _physicsCallbacks(void); +static void _processKey(bool down, int32_t keysym, int32_t scancode); +static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +static void _pushConstants(lua_State *L); +static int32_t _pushVec3(lua_State *L, Vec3T v); +static void _putPixel(int32_t x, int32_t y, uint32_t pixel); +static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad); +static void _quadScratch(int32_t quads, int32_t frames); +static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha); static void _registerApi(lua_State *L); +static void _releaseAxis(int32_t axisIndex); static void _reloadScript(void); +static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message); static void _resetScriptState(void); static void _runScript(bool fatal); +static void _saveAudioCalibration(int32_t milliseconds); static SDL_Texture *_sceneVideoSource(int32_t player); static ConfigT *_scriptConfFromTable(lua_State *L, const char *method); -static void _pushConstants(lua_State *L); -static void _putPixel(int32_t x, int32_t y, uint32_t pixel); -static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha); -static void _releaseAxis(int32_t axisIndex); -static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message); -static void _saveAudioCalibration(int32_t milliseconds); static void _selectDefaultAudioTrack(int32_t handle); static void _setMouseCaptured(bool captured); static void _setPause(bool paused, bool fromKey); @@ -591,8 +591,8 @@ static void _unloadScriptResources(void); static void _updatePauseState(void); static void _updateSounds(void); static void _videoDestroy(VideoT *video); -static void _watchFile(const char *name); static bool _watchedChanged(void); +static void _watchFile(const char *name); static int32_t apiAnimationGetTime(lua_State *L); static int32_t apiAnimationIsPlaying(lua_State *L); @@ -925,8 +925,8 @@ static int32_t apiVehicleSetAntiRoll(lua_State *L); static int32_t apiVehicleSetBrakes(lua_State *L); static int32_t apiVehicleSetEngine(lua_State *L); static int32_t apiVehicleSetGears(lua_State *L); -static int32_t apiVehicleSetSteering(lua_State *L); static int32_t apiVehicleSetRudder(lua_State *L); +static int32_t apiVehicleSetSteering(lua_State *L); static int32_t apiVehicleSetSuspension(lua_State *L); static int32_t apiVehicleSetThrust(lua_State *L); static int32_t apiVehicleSetWheel(lua_State *L); @@ -1051,6 +1051,12 @@ static int32_t _argAnimationLayer(lua_State *L, const char *method, int32_t inde } +// A node that carries a physics body, checked. +static int32_t _argBody(lua_State *L, const char *method, int32_t index) { + return _argNodeWith(L, method, index, bodyExists, "body"); +} + + static bool _argBoolean(lua_State *L, const char *method, int32_t index) { if (!lua_isboolean(L, index)) { _luaDie(L, method, "Argument %d must be a boolean.", index); @@ -1071,12 +1077,6 @@ static int32_t _argChannel(lua_State *L, const char *method, int32_t index) { } -// A colour component argument, clamped to 0..255. -static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index) { - return (uint8_t)SDL_clamp(_argInteger(L, method, index), 0, COLOR_BYTE_MAX); -} - - // Dies unless the argument count is within [minimum, maximum]. static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum) { int32_t n = lua_gettop(L); @@ -1090,16 +1090,15 @@ static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t } -static FontT *_argFont(lua_State *L, const char *method, int32_t index) { - int32_t id = _argInteger(L, method, index); - FontT *font = NULL; +// A colour component argument, clamped to 0..255. +static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index) { + return (uint8_t)SDL_clamp(_argInteger(L, method, index), 0, COLOR_BYTE_MAX); +} - HASH_FIND_INT(_global.fontList, &id, font); - if (!font) { - _luaDie(L, method, "No font at index %d.", id); - } - return font; +// An emitter handle argument, checked. +static int32_t _argEmitter(lua_State *L, const char *method, int32_t index) { + return _argHandle(L, method, index, emitterValid, "emitter"); } @@ -1139,6 +1138,19 @@ static float *_argFloatTable(lua_State *L, const char *method, int32_t index, in } +static FontT *_argFont(lua_State *L, const char *method, int32_t index) { + int32_t id = _argInteger(L, method, index); + FontT *font = NULL; + + HASH_FIND_INT(_global.fontList, &id, font); + if (!font) { + _luaDie(L, method, "No font at index %d.", id); + } + + return font; +} + + // A handle argument the given test accepts, named for the message: "No material 7." static int32_t _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun) { int32_t handle = _argInteger(L, method, index); @@ -1155,34 +1167,8 @@ static int32_t _argInteger(lua_State *L, const char *method, int32_t index) { } -// A morph target of a node's mesh, by name or by number from 1, checked. -static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index) { - int32_t target; - - if (lua_type(L, index) == LUA_TSTRING) { - target = meshFindMorph(nodeGetMesh(node), lua_tostring(L, index)); - if (target < 0) { - _luaDie(L, method, "Node %d has no morph target named %s.", node, lua_tostring(L, index)); - } - return target; - } - target = _argInteger(L, method, index) - 1; - if ((target < 0) || (target >= nodeGetMorphCount(node))) { - _luaDie(L, method, "Node %d has no morph target %d.", node, target + 1); - } - return target; -} - - -// A node that carries a physics body, checked. -static int32_t _argBody(lua_State *L, const char *method, int32_t index) { - return _argNodeWith(L, method, index, bodyExists, "body"); -} - - -// An emitter handle argument, checked. -static int32_t _argEmitter(lua_State *L, const char *method, int32_t index) { - return _argHandle(L, method, index, emitterValid, "emitter"); +static int64_t _argInteger64(lua_State *L, const char *method, int32_t index) { + return (int64_t)_argNumber(L, method, index); } @@ -1230,6 +1216,25 @@ static int32_t _argMesh(lua_State *L, const char *method, int32_t index) { } +// A morph target of a node's mesh, by name or by number from 1, checked. +static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index) { + int32_t target; + + if (lua_type(L, index) == LUA_TSTRING) { + target = meshFindMorph(nodeGetMesh(node), lua_tostring(L, index)); + if (target < 0) { + _luaDie(L, method, "Node %d has no morph target named %s.", node, lua_tostring(L, index)); + } + return target; + } + target = _argInteger(L, method, index) - 1; + if ((target < 0) || (target >= nodeGetMorphCount(node))) { + _luaDie(L, method, "Node %d has no morph target %d.", node, target + 1); + } + return target; +} + + // A navigation mesh handle from navNew or navLoad. static int32_t _argNav(lua_State *L, const char *method, int32_t index) { return _argHandle(L, method, index, navValid, "navigation mesh"); @@ -1259,6 +1264,15 @@ static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, boo } +static double _argNumber(lua_State *L, const char *method, int32_t index) { + if (!lua_isnumber(L, index)) { + _luaDie(L, method, "Argument %d must be a number.", index); + } + + return lua_tonumber(L, index); +} + + // r, g, b at index onwards when the script gave them; the caller's defaults otherwise. static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b) { if (lua_gettop(L) < index + 2) { @@ -1288,26 +1302,6 @@ static int32_t _argSoft(lua_State *L, const char *method, int32_t index) { } -// A node carrying a vehicle, checked. -static int32_t _argVehicle(lua_State *L, const char *method, int32_t index) { - return _argNodeWith(L, method, index, vehicleExists, "vehicle"); -} - - -static int64_t _argInteger64(lua_State *L, const char *method, int32_t index) { - return (int64_t)_argNumber(L, method, index); -} - - -static double _argNumber(lua_State *L, const char *method, int32_t index) { - if (!lua_isnumber(L, index)) { - _luaDie(L, method, "Argument %d must be a number.", index); - } - - return lua_tonumber(L, index); -} - - static SoundT *_argSound(lua_State *L, const char *method, int32_t index) { int32_t id = _argInteger(L, method, index); SoundT *sound = NULL; @@ -1349,9 +1343,9 @@ static Vec3T _argVec3(lua_State *L, const char *method, int32_t index) { } -// A view handle from viewNew. -static int32_t _argView(lua_State *L, const char *method, int32_t index) { - return _argHandle(L, method, index, viewValid, "view"); +// A node carrying a vehicle, checked. +static int32_t _argVehicle(lua_State *L, const char *method, int32_t index) { + return _argNodeWith(L, method, index, vehicleExists, "vehicle"); } @@ -1368,6 +1362,12 @@ static VideoT *_argVideo(lua_State *L, const char *method, int32_t index) { } +// A view handle from viewNew. +static int32_t _argView(lua_State *L, const char *method, int32_t index) { + return _argHandle(L, method, index, viewValid, "view"); +} + + // Builds a config for scriptExecute/scriptPush from the games.dat style table at stack index 1. static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) { const char *confKey = NULL; @@ -1573,60 +1573,6 @@ static void _callLua(const char *func, const char *sig, ...) { } -// First effect track that is neither playing nor paused, or SOUND_CHANNEL_NONE when all are busy. -static int32_t _effectTrackFree(void) { - int32_t x = 0; - - for (x = 0; x < EFFECT_TRACKS; x++) { - if (!MIX_TrackPlaying(_effectTracks[x]) && !MIX_TrackPaused(_effectTracks[x])) { - return x; - } - } - - return SOUND_CHANNEL_NONE; -} - - -// The volume of a positioned sound at a distance: full within near, inverse distance beyond it, -// and fading out over the last fifth before far. -static float _effectGain(const EffectT *effect, float distance) { - float gain = 1.0f; - float fadeStart; - - if (distance > effect->nearBy) { - gain = effect->nearBy / distance; - } - fadeStart = effect->farOff * (1.0f - SOUND_FADE_FRACTION); - if (distance > fadeStart) { - gain *= SDL_clamp((effect->farOff - distance) / (effect->farOff - fadeStart), 0.0f, 1.0f); - } - return gain; -} - - -// A channel about to play afresh: no position, no pan, the default range. -static void _effectReset(int32_t channel) { - EffectT *effect = &_effects[channel]; - - memset(effect, 0, sizeof(*effect)); - effect->node = LISTENER_CAMERA; - effect->nearBy = SOUND_DEFAULT_NEAR; - effect->farOff = SOUND_DEFAULT_FAR; - effect->gain = 1.0f; - MIX_SetTrack3DPosition(_effectTracks[channel], NULL); -} - - -// SDL_mixer calls this from its mixing thread. Just queue the channel; the game loop reads it under the mixer lock. -static void _effectStopped(void *userdata, MIX_Track *track) { - (void)track; - - if (_global.soundQueueCount < SOUND_QUEUE_SIZE) { - _global.soundQueue[_global.soundQueueCount++] = (int32_t)(intptr_t)userdata; - } -} - - // Liang-Barsky clip of a line to the overlay. False when none of it is on the overlay; otherwise // the ends are moved onto it. static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2) { @@ -1692,6 +1638,15 @@ static int32_t _controllerSlot(SDL_JoystickID which) { } +// A fresh Lua state with the standard libraries, the vfs hooks, the constants and the whole API. +static void _createScriptContext(void) { + _progTrace("Creating Lua context for script"); + _global.luaContext = luaL_newstate(); + _startLuaContext(_global.luaContext); + _registerApi(_global.luaContext); +} + + // Sleeps while keeping the window responsive. Returns false if the user asked to quit. static bool _delayAndPump(uint32_t ms) { SDL_Event event; @@ -1969,6 +1924,47 @@ static void _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target) { } +// Draws the PAUSED indicator, built from the console font on first use, centered on the target. +static void _drawPauseIndicator(const SDL_FRect *target) { + SDL_Surface *text = NULL; + SDL_Rect src; + SDL_Rect dest; + SDL_FRect where; + int32_t i = 0; + + if (_global.pauseTexture == NULL) { + _global.pauseTextureWidth = (int32_t)strlen(PAUSE_TEXT) * _global.consoleFontWidth; + _global.pauseTextureHeight = _global.consoleFontHeight; + text = SDL_CreateSurface(_global.pauseTextureWidth, _global.pauseTextureHeight, SDL_PIXELFORMAT_BGRA32); + if (text == NULL) { + utilDie("%s", SDL_GetError()); + } + src.y = 0; + src.w = _global.consoleFontWidth; + src.h = _global.consoleFontHeight; + dest.y = 0; + dest.w = _global.consoleFontWidth; + dest.h = _global.consoleFontHeight; + for (i = 0; PAUSE_TEXT[i] != 0; i++) { + src.x = (uint8_t)PAUSE_TEXT[i] * _global.consoleFontWidth; + dest.x = i * _global.consoleFontWidth; + SDL_BlitSurface(_global.consoleFontSurface, &src, text, &dest); + } + _global.pauseTexture = SDL_CreateTextureFromSurface(_global.renderer, text); + SDL_DestroySurface(text); + if (_global.pauseTexture == NULL) { + utilDie("%s", SDL_GetError()); + } + } + + where.w = (float)(_global.pauseTextureWidth * PAUSE_TEXT_SCALE); + where.h = (float)(_global.pauseTextureHeight * PAUSE_TEXT_SCALE); + where.x = target->x + (target->w - where.w) / 2.0f; + where.y = target->y + (target->h - where.h) / 2.0f; + SDL_RenderTexture(_global.renderer, _global.pauseTexture, NULL, &where); +} + + // The 2D emitter's trails: a ribbon of quads through each particle's recorded points, fading // toward the tail, textured by the middle column of the first frame so the disc's edge softens it. static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY) { @@ -2038,44 +2034,57 @@ static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cac } -// Draws the PAUSED indicator, built from the console font on first use, centered on the target. -static void _drawPauseIndicator(const SDL_FRect *target) { - SDL_Surface *text = NULL; - SDL_Rect src; - SDL_Rect dest; - SDL_FRect where; - int32_t i = 0; +// The volume of a positioned sound at a distance: full within near, inverse distance beyond it, +// and fading out over the last fifth before far. +static float _effectGain(const EffectT *effect, float distance) { + float gain = 1.0f; + float fadeStart; - if (_global.pauseTexture == NULL) { - _global.pauseTextureWidth = (int32_t)strlen(PAUSE_TEXT) * _global.consoleFontWidth; - _global.pauseTextureHeight = _global.consoleFontHeight; - text = SDL_CreateSurface(_global.pauseTextureWidth, _global.pauseTextureHeight, SDL_PIXELFORMAT_BGRA32); - if (text == NULL) { - utilDie("%s", SDL_GetError()); - } - src.y = 0; - src.w = _global.consoleFontWidth; - src.h = _global.consoleFontHeight; - dest.y = 0; - dest.w = _global.consoleFontWidth; - dest.h = _global.consoleFontHeight; - for (i = 0; PAUSE_TEXT[i] != 0; i++) { - src.x = (uint8_t)PAUSE_TEXT[i] * _global.consoleFontWidth; - dest.x = i * _global.consoleFontWidth; - SDL_BlitSurface(_global.consoleFontSurface, &src, text, &dest); - } - _global.pauseTexture = SDL_CreateTextureFromSurface(_global.renderer, text); - SDL_DestroySurface(text); - if (_global.pauseTexture == NULL) { - utilDie("%s", SDL_GetError()); + if (distance > effect->nearBy) { + gain = effect->nearBy / distance; + } + fadeStart = effect->farOff * (1.0f - SOUND_FADE_FRACTION); + if (distance > fadeStart) { + gain *= SDL_clamp((effect->farOff - distance) / (effect->farOff - fadeStart), 0.0f, 1.0f); + } + return gain; +} + + +// A channel about to play afresh: no position, no pan, the default range. +static void _effectReset(int32_t channel) { + EffectT *effect = &_effects[channel]; + + memset(effect, 0, sizeof(*effect)); + effect->node = LISTENER_CAMERA; + effect->nearBy = SOUND_DEFAULT_NEAR; + effect->farOff = SOUND_DEFAULT_FAR; + effect->gain = 1.0f; + MIX_SetTrack3DPosition(_effectTracks[channel], NULL); +} + + +// SDL_mixer calls this from its mixing thread. Just queue the channel; the game loop reads it under the mixer lock. +static void _effectStopped(void *userdata, MIX_Track *track) { + (void)track; + + if (_global.soundQueueCount < SOUND_QUEUE_SIZE) { + _global.soundQueue[_global.soundQueueCount++] = (int32_t)(intptr_t)userdata; + } +} + + +// First effect track that is neither playing nor paused, or SOUND_CHANNEL_NONE when all are busy. +static int32_t _effectTrackFree(void) { + int32_t x = 0; + + for (x = 0; x < EFFECT_TRACKS; x++) { + if (!MIX_TrackPlaying(_effectTracks[x]) && !MIX_TrackPaused(_effectTracks[x])) { + return x; } } - where.w = (float)(_global.pauseTextureWidth * PAUSE_TEXT_SCALE); - where.h = (float)(_global.pauseTextureHeight * PAUSE_TEXT_SCALE); - where.x = target->x + (target->w - where.w) / 2.0f; - where.y = target->y + (target->h - where.h) / 2.0f; - SDL_RenderTexture(_global.renderer, _global.pauseTexture, NULL, &where); + return SOUND_CHANNEL_NONE; } @@ -2686,15 +2695,6 @@ static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode, bo } -// A fresh Lua state with the standard libraries, the vfs hooks, the constants and the whole API. -static void _createScriptContext(void) { - _progTrace("Creating Lua context for script"); - _global.luaContext = luaL_newstate(); - _startLuaContext(_global.luaContext); - _registerApi(_global.luaContext); -} - - // require("lfs") with dir and attributes routed through the vfs, so a packed game can list itself. static int32_t _luaopenLfs(lua_State *L) { luaopen_lfs(L); @@ -2739,8 +2739,6 @@ static int32_t _luaPanic(lua_State *L) { // package.searchers entry serving the embedded Lua modules. // https://leiradel.github.io/2020/03/01/Embedding-Lua-Modules.html - - static int32_t _luaSearcher(lua_State *L) { const char *modname = lua_tostring(L, 1); size_t i = 0; @@ -2830,18 +2828,30 @@ static float _mixerGain(int32_t effectsVolume) { } -// A colour as the overlay surface stores it. -static uint32_t _overlayColor(const SDL_Color *color) { - return SDL_MapRGBA(SDL_GetPixelFormatDetails(_global.overlay->format), NULL, color->r, color->g, color->b, color->a); -} - - // Input code for a mouse button (0 = left, 1 = right, 2 = middle, ...) or wheel offset. static int32_t _mouseCode(int32_t device, int32_t button) { return CODE_MOUSE_BASE + device * CODE_MOUSE_STRIDE + button; } +// onNavArrived(agent) for every agent that reached its target this frame. +static void _navCallbacks(void) { + int32_t agents[NAV_ARRIVAL_QUEUE]; + int32_t count = navPollArrived(agents, (int32_t)SDL_arraysize(agents)); + int32_t x = 0; + + for (x = 0; x < count; x++) { + _callLua("onNavArrived", "i", agents[x]); + } +} + + +// A colour as the overlay surface stores it. +static uint32_t _overlayColor(const SDL_Color *color) { + return SDL_MapRGBA(SDL_GetPixelFormatDetails(_global.overlay->format), NULL, color->r, color->g, color->b, color->a); +} + + // Replaces the overlay surface and texture (and the scene's targets) at a new size; its contents are lost. static void _overlayResize(int32_t width, int32_t height) { SDL_DestroySurface(_global.overlay); @@ -2869,6 +2879,79 @@ static void _overlayTouched(void) { } +// Frees the renderer textures kept for an emitter's 2D drawing. +static void _particleTexturesDestroy(ParticleTexturesT *cache) { + int32_t f = 0; + + HASH_DEL(_global.particleTextures, cache); + for (f = 0; f < cache->count; f++) { + SDL_DestroyTexture(cache->textures[f]); + } + SDL_free(cache->textures); + SDL_free(cache); +} + + +static void _particleTexturesFree(int32_t emitter) { + ParticleTexturesT *cache = NULL; + + HASH_FIND_INT(_global.particleTextures, &emitter, cache); + if (cache != NULL) { + _particleTexturesDestroy(cache); + } +} + + +// Every emitter's textures; the emitters themselves go with particlesQuit. +static void _particleTexturesFreeAll(void) { + ParticleTexturesT *cache = NULL; + ParticleTexturesT *temp = NULL; + + HASH_ITER(hh, _global.particleTextures, cache, temp) { + _particleTexturesDestroy(cache); + } +} + + +// The textures for an emitter's frames, made on first use and remade when the frames change. +static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view) { + ParticleTexturesT *cache = NULL; + int32_t f = 0; + + HASH_FIND_INT(_global.particleTextures, &view->id, cache); + if ((cache != NULL) && (cache->version != view->textureVersion)) { + _particleTexturesDestroy(cache); + cache = NULL; + } + if (cache != NULL) { + return cache; + } + cache = SDL_calloc(1, sizeof(ParticleTexturesT)); + if (cache == NULL) { + utilDie("Out of memory for particle textures."); + } + cache->id = view->id; + cache->version = view->textureVersion; + cache->count = view->frameCount; + cache->blend = view->blend; + cache->textures = SDL_calloc((size_t)view->frameCount, sizeof(SDL_Texture *)); + if (cache->textures == NULL) { + utilDie("Out of memory for particle textures."); + } + for (f = 0; f < view->frameCount; f++) { + cache->textures[f] = SDL_CreateTextureFromSurface(_global.renderer, view->frames[f]); + if (cache->textures[f] == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_SetTextureScaleMode(cache->textures[f], SDL_SCALEMODE_LINEAR); + SDL_SetTextureBlendMode(cache->textures[f], (view->blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND); + } + HASH_ADD_INT(_global.particleTextures, id, cache); + + return cache; +} + + static void _pauseAllVideos(bool pause) { VideoT *video = NULL; VideoT *temp = NULL; @@ -2889,6 +2972,28 @@ static void _pauseAllVideos(bool pause) { } +// Hands the step's contacts and trigger overlaps to the script: onCollision(nodeA, nodeB, x, y, z, +// speed) and onTrigger(trigger, other, entered), when the script defines them. Physics keeps what +// one batch cannot hold, so the queue is drained until empty. +static void _physicsCallbacks(void) { + PhysicsEventT events[PHYSICS_MAX_EVENTS]; + PhysicsEventT *event = NULL; + int32_t count = 0; + int32_t x = 0; + + while ((count = physicsGetEvents(events, (int32_t)SDL_arraysize(events))) > 0) { + for (x = 0; x < count; x++) { + event = &events[x]; + if (event->type == PHYSICS_EVENT_COLLISION) { + _callLua("onCollision", "iidddd", event->nodeA, event->nodeB, (double)event->point.x, (double)event->point.y, (double)event->point.z, (double)event->speed); + } else { + _callLua("onTrigger", "iib", event->nodeA, event->nodeB, (event->type == PHYSICS_EVENT_ENTER) ? 1 : 0); + } + } + } +} + + // Routes a key, button, or axis direction code: engine switches first, then the script. static void _processKey(bool down, int32_t keysym, int32_t scancode) { InputE engine = INPUT_COUNT; @@ -2960,6 +3065,193 @@ static void _progTrace(const char *fmt, ...) { } +// Constants every script (and controls.cfg) can rely on. These are the single source of truth. +static void _pushConstants(lua_State *L) { + int32_t x = 0; + + for (x = 0; x < INPUT_COUNT; x++) { + lua_pushinteger(L, x); + lua_setglobal(L, _inputNames[x].switchName); + } + + lua_pushinteger(L, FONT_QUALITY_SOLID); + lua_setglobal(L, "FONT_QUALITY_SOLID"); + lua_pushinteger(L, FONT_QUALITY_SHADED); + lua_setglobal(L, "FONT_QUALITY_SHADED"); + lua_pushinteger(L, FONT_QUALITY_BLENDED); + lua_setglobal(L, "FONT_QUALITY_BLENDED"); + + lua_pushinteger(L, KEYBOARD_NORMAL); + lua_setglobal(L, "MODE_NORMAL"); + lua_pushinteger(L, KEYBOARD_FULL); + lua_setglobal(L, "MODE_FULL"); + + lua_pushinteger(L, MOUSE_SINGLE); + lua_setglobal(L, "MOUSE_SINGLE"); + lua_pushinteger(L, MOUSE_MANY); + lua_setglobal(L, "MOUSE_MANY"); + lua_pushinteger(L, MOUSE_SINGLE); + lua_setglobal(L, "SINGLE_MOUSE"); + lua_pushinteger(L, MOUSE_MANY); + lua_setglobal(L, "MANY_MOUSE"); + + lua_pushinteger(L, OVERLAY_NOT_UPDATED); + lua_setglobal(L, "OVERLAY_NOT_UPDATED"); + lua_pushinteger(L, OVERLAY_UPDATED); + lua_setglobal(L, "OVERLAY_UPDATED"); + + lua_pushinteger(L, RENDER_PIXELATED); + lua_setglobal(L, "RENDER_PIXELATED"); + lua_pushinteger(L, RENDER_SMOOTH); + lua_setglobal(L, "RENDER_SMOOTH"); + + lua_pushinteger(L, DISC_STOPPED); + lua_setglobal(L, "DISC_STOPPED"); + lua_pushinteger(L, DISC_PLAYING); + lua_setglobal(L, "DISC_PLAYING"); + lua_pushinteger(L, DISC_PAUSED); + lua_setglobal(L, "DISC_PAUSED"); + lua_pushinteger(L, DISC_EJECTED); + lua_setglobal(L, "DISC_EJECTED"); + // 3D light types + lua_pushinteger(L, LIGHT_DIRECTIONAL); + lua_setglobal(L, "LIGHT_DIRECTIONAL"); + lua_pushinteger(L, LIGHT_POINT); + lua_setglobal(L, "LIGHT_POINT"); + lua_pushinteger(L, LIGHT_SPOT); + lua_setglobal(L, "LIGHT_SPOT"); + // Physics bodies and shapes + lua_pushinteger(L, BODY_STATIC); + lua_setglobal(L, "BODY_STATIC"); + lua_pushinteger(L, BODY_DYNAMIC); + lua_setglobal(L, "BODY_DYNAMIC"); + lua_pushinteger(L, BODY_KINEMATIC); + lua_setglobal(L, "BODY_KINEMATIC"); + lua_pushinteger(L, SHAPE_BOX); + lua_setglobal(L, "SHAPE_BOX"); + lua_pushinteger(L, SHAPE_SPHERE); + lua_setglobal(L, "SHAPE_SPHERE"); + lua_pushinteger(L, SHAPE_CAPSULE); + lua_setglobal(L, "SHAPE_CAPSULE"); + lua_pushinteger(L, SHAPE_CYLINDER); + lua_setglobal(L, "SHAPE_CYLINDER"); + lua_pushinteger(L, SHAPE_HULL); + lua_setglobal(L, "SHAPE_HULL"); + lua_pushinteger(L, SHAPE_MESH); + lua_setglobal(L, "SHAPE_MESH"); + lua_pushinteger(L, JOINT_HINGE); + lua_setglobal(L, "JOINT_HINGE"); + lua_pushinteger(L, JOINT_BALL); + lua_setglobal(L, "JOINT_BALL"); + lua_pushinteger(L, JOINT_SLIDER); + lua_setglobal(L, "JOINT_SLIDER"); + lua_pushinteger(L, DEBUG_NONE); + lua_setglobal(L, "DEBUG_NONE"); + lua_pushinteger(L, DEBUG_SHAPES); + lua_setglobal(L, "DEBUG_SHAPES"); + lua_pushinteger(L, DEBUG_CONSTRAINTS); + lua_setglobal(L, "DEBUG_CONSTRAINTS"); + lua_pushinteger(L, DEBUG_CONTACTS); + lua_setglobal(L, "DEBUG_CONTACTS"); + lua_pushinteger(L, DEBUG_VELOCITIES); + lua_setglobal(L, "DEBUG_VELOCITIES"); + lua_pushinteger(L, DEBUG_STATIC); + lua_setglobal(L, "DEBUG_STATIC"); + lua_pushinteger(L, DEBUG_ALL); + lua_setglobal(L, "DEBUG_ALL"); + // Particles + lua_pushinteger(L, PARTICLE_ALPHA); + lua_setglobal(L, "PARTICLE_ALPHA"); + lua_pushinteger(L, PARTICLE_ADD); + lua_setglobal(L, "PARTICLE_ADD"); + lua_pushinteger(L, PARTICLE_OVER); + lua_setglobal(L, "PARTICLE_OVER"); + lua_pushinteger(L, PARTICLE_UNDER); + lua_setglobal(L, "PARTICLE_UNDER"); + // Billboards + lua_pushinteger(L, BILLBOARD_NONE); + lua_setglobal(L, "BILLBOARD_NONE"); + lua_pushinteger(L, BILLBOARD_ALL); + lua_setglobal(L, "BILLBOARD_ALL"); + lua_pushinteger(L, BILLBOARD_Y); + lua_setglobal(L, "BILLBOARD_Y"); + // Particle collisions + lua_pushinteger(L, COLLIDE_NONE); + lua_setglobal(L, "COLLIDE_NONE"); + lua_pushinteger(L, COLLIDE_FLOOR); + lua_setglobal(L, "COLLIDE_FLOOR"); + lua_pushinteger(L, COLLIDE_SCENE); + lua_setglobal(L, "COLLIDE_SCENE"); + // Texture filtering + lua_pushinteger(L, FILTER_LINEAR); + lua_setglobal(L, "FILTER_LINEAR"); + lua_pushinteger(L, FILTER_NEAREST); + lua_setglobal(L, "FILTER_NEAREST"); + // Tone curves + lua_pushinteger(L, TONEMAP_NONE); + lua_setglobal(L, "TONEMAP_NONE"); + lua_pushinteger(L, TONEMAP_NEUTRAL); + lua_setglobal(L, "TONEMAP_NEUTRAL"); + lua_pushinteger(L, TONEMAP_ACES); + lua_setglobal(L, "TONEMAP_ACES"); + // Vehicles + lua_pushinteger(L, VEHICLE_CAR); + lua_setglobal(L, "VEHICLE_CAR"); + lua_pushinteger(L, VEHICLE_MOTORCYCLE); + lua_setglobal(L, "VEHICLE_MOTORCYCLE"); + lua_pushinteger(L, VEHICLE_TANK); + lua_setglobal(L, "VEHICLE_TANK"); + lua_pushinteger(L, VEHICLE_BOAT); + lua_setglobal(L, "VEHICLE_BOAT"); + // Soft bodies + lua_pushinteger(L, SOFT_CLOTH); + lua_setglobal(L, "SOFT_CLOTH"); + lua_pushinteger(L, SOFT_BODY); + lua_setglobal(L, "SOFT_BODY"); + lua_pushinteger(L, SOFT_ROPE); + lua_setglobal(L, "SOFT_ROPE"); + + lua_pushinteger(L, SOUND_CHANNEL_NONE); + lua_setglobal(L, "SOUND_ERROR_INVALID"); + lua_pushinteger(L, SOUND_CHANNEL_NONE); + lua_setglobal(L, "SOUND_REMOVE_HANDLE"); + + // Input code layout so Framework.singe can build the GAMEPAD_N and MOUSE_N tables. + lua_pushinteger(L, CODE_GAMEPAD_BASE); + lua_setglobal(L, "SINGE_GAMEPAD_BASE"); + lua_pushinteger(L, CODE_GAMEPAD_STRIDE); + lua_setglobal(L, "SINGE_GAMEPAD_STRIDE"); + lua_pushinteger(L, CODE_AXIS_STRIDE); + lua_setglobal(L, "SINGE_AXIS_STRIDE"); + lua_pushinteger(L, CODE_GAMEPAD_BUTTON_OFFSET); + lua_setglobal(L, "SINGE_GAMEPAD_BUTTON_OFFSET"); + lua_pushinteger(L, CODE_MOUSE_BASE); + lua_setglobal(L, "SINGE_MOUSE_BASE"); + lua_pushinteger(L, CODE_MOUSE_STRIDE); + lua_setglobal(L, "SINGE_MOUSE_STRIDE"); + lua_pushinteger(L, MAX_CONTROLLERS); + lua_setglobal(L, "SINGE_MAX_CONTROLLERS"); + lua_pushinteger(L, MAX_MICE); + lua_setglobal(L, "SINGE_MAX_MICE"); + + lua_pushinteger(L, SINGE_VERSION_MAJOR); + lua_setglobal(L, "SINGE_VERSION_MAJOR"); + lua_pushinteger(L, SINGE_VERSION_MINOR); + lua_setglobal(L, "SINGE_VERSION_MINOR"); + lua_pushstring(L, VERSION_STRING); + lua_setglobal(L, "SINGE_VERSION_STRING"); + lua_pushnumber(L, SINGE_VERSION); + lua_setglobal(L, "SINGE_FRAMEWORK_VERSION"); + + lua_pushinteger(L, _global.controllerDeadZone); + lua_setglobal(L, "SINGE_DEAD_ZONE"); + lua_pushboolean(L, _global.conf->legacySpriteArgs); + lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS"); + lua_pushboolean(L, _global.conf->disc); + lua_setglobal(L, "SINGE_DISC"); +} + + // Pushes a vector as three results. static int32_t _pushVec3(lua_State *L, Vec3T v) { lua_pushnumber(L, v.x); @@ -2969,6 +3261,20 @@ static int32_t _pushVec3(lua_State *L, Vec3T v) { } +// Writes one overlay pixel. The overlay is always BGRA32, one uint32_t per pixel (_overlayResize +// makes it so), and must be locked by the caller. +static void _putPixel(int32_t x, int32_t y, uint32_t pixel) { + SDL_Surface *surface = _global.overlay; + uint32_t *row = NULL; + + if ((x < 0) || (x >= surface->w) || (y < 0) || (y >= surface->h)) { + return; + } + row = (uint32_t *)((uint8_t *)surface->pixels + (size_t)y * (size_t)surface->pitch); + row[x] = pixel; +} + + // Writes the two triangles of quad number quad (corners quad * 4 onwards) at index slot. static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad) { int32_t *out = indices + slot * QUAD_INDICES; @@ -3004,110 +3310,21 @@ static void _quadScratch(int32_t quads, int32_t frames) { } -// Frees the renderer textures kept for an emitter's 2D drawing. -static void _particleTexturesDestroy(ParticleTexturesT *cache) { - int32_t f = 0; +// colorXxx(r, g, b[, a]) with components clamped to 0..255. +static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha) { + int32_t n = lua_gettop(L); + uint8_t value[COLOR_COMPONENTS] = { 0, 0, 0, defaultAlpha }; + int32_t x = 0; - HASH_DEL(_global.particleTextures, cache); - for (f = 0; f < cache->count; f++) { - SDL_DestroyTexture(cache->textures[f]); - } - SDL_free(cache->textures); - SDL_free(cache); -} - - -static void _particleTexturesFree(int32_t emitter) { - ParticleTexturesT *cache = NULL; - - HASH_FIND_INT(_global.particleTextures, &emitter, cache); - if (cache != NULL) { - _particleTexturesDestroy(cache); - } -} - - -// Every emitter's textures; the emitters themselves go with particlesQuit. -static void _particleTexturesFreeAll(void) { - ParticleTexturesT *cache = NULL; - ParticleTexturesT *temp = NULL; - - HASH_ITER(hh, _global.particleTextures, cache, temp) { - _particleTexturesDestroy(cache); - } -} - - -// The textures for an emitter's frames, made on first use and remade when the frames change. -static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view) { - ParticleTexturesT *cache = NULL; - int32_t f = 0; - - HASH_FIND_INT(_global.particleTextures, &view->id, cache); - if ((cache != NULL) && (cache->version != view->textureVersion)) { - _particleTexturesDestroy(cache); - cache = NULL; - } - if (cache != NULL) { - return cache; - } - cache = SDL_calloc(1, sizeof(ParticleTexturesT)); - if (cache == NULL) { - utilDie("Out of memory for particle textures."); - } - cache->id = view->id; - cache->version = view->textureVersion; - cache->count = view->frameCount; - cache->blend = view->blend; - cache->textures = SDL_calloc((size_t)view->frameCount, sizeof(SDL_Texture *)); - if (cache->textures == NULL) { - utilDie("Out of memory for particle textures."); - } - for (f = 0; f < view->frameCount; f++) { - cache->textures[f] = SDL_CreateTextureFromSurface(_global.renderer, view->frames[f]); - if (cache->textures[f] == NULL) { - utilDie("%s", SDL_GetError()); - } - SDL_SetTextureScaleMode(cache->textures[f], SDL_SCALEMODE_LINEAR); - SDL_SetTextureBlendMode(cache->textures[f], (view->blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND); - } - HASH_ADD_INT(_global.particleTextures, id, cache); - - return cache; -} - - -// onNavArrived(agent) for every agent that reached its target this frame. -static void _navCallbacks(void) { - int32_t agents[NAV_ARRIVAL_QUEUE]; - int32_t count = navPollArrived(agents, (int32_t)SDL_arraysize(agents)); - int32_t x = 0; - - for (x = 0; x < count; x++) { - _callLua("onNavArrived", "i", agents[x]); - } -} - - -// Hands the step's contacts and trigger overlaps to the script: onCollision(nodeA, nodeB, x, y, z, -// speed) and onTrigger(trigger, other, entered), when the script defines them. Physics keeps what -// one batch cannot hold, so the queue is drained until empty. -static void _physicsCallbacks(void) { - PhysicsEventT events[PHYSICS_MAX_EVENTS]; - PhysicsEventT *event = NULL; - int32_t count = 0; - int32_t x = 0; - - while ((count = physicsGetEvents(events, (int32_t)SDL_arraysize(events))) > 0) { - for (x = 0; x < count; x++) { - event = &events[x]; - if (event->type == PHYSICS_EVENT_COLLISION) { - _callLua("onCollision", "iidddd", event->nodeA, event->nodeB, (double)event->point.x, (double)event->point.y, (double)event->point.z, (double)event->speed); - } else { - _callLua("onTrigger", "iib", event->nodeA, event->nodeB, (event->type == PHYSICS_EVENT_ENTER) ? 1 : 0); - } - } + _argCheck(L, method, 3, COLOR_COMPONENTS); + for (x = 0; x < n; x++) { + value[x] = _argColorByte(L, method, x + 1); } + color->r = value[0]; + color->g = value[1]; + color->b = value[2]; + color->a = value[3]; + _luaTrace(L, method, "%d %d %d %d", color->r, color->g, color->b, color->a); } @@ -3493,6 +3710,15 @@ static void _registerApi(lua_State *L) { } +// Releases whatever direction code an axis was holding. +static void _releaseAxis(int32_t axisIndex) { + if (_global.axisCode[axisIndex] != 0) { + _processKey(false, 0, _global.axisCode[axisIndex]); + _global.axisCode[axisIndex] = 0; + } +} + + // Runs the game again from its script without leaving: every script-owned thing goes (sounds, // the Lua state, fonts, sprites, videos, the scene, physics, navigation, particles, the overlay // back to its default size), the engine (window, GPU device, disc, controllers) stays, and the @@ -3513,283 +3739,35 @@ static void _reloadScript(void) { } -// Loads and runs the game script under the traceback handler. A failure is fatal when the game -// starts; on a reload it is reported and the game sits empty until the next one. -static void _runScript(bool fatal) { - _progTrace("Running %s", _global.conf->scriptFile); - lua_pushcfunction(_global.luaContext, _luaTraceback); - if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL, _global.conf->reload) || lua_pcall(_global.luaContext, 0, 0, -2)) { - if (fatal) { - utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1)); - } - utilSay("Error running script: %s", lua_tostring(_global.luaContext, -1)); +// Renders text with the current font, quality, and colors. +static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message) { + SDL_Surface *surface = NULL; + + if (_global.fontCurrent == NULL) { + _luaDie(L, method, "No font selected."); } - lua_settop(_global.luaContext, 0); -} + switch (_global.fontQuality) { + case FONT_QUALITY_SOLID: + surface = TTF_RenderText_Solid(_global.fontCurrent->font, message, 0, _global.colorForeground); + break; + case FONT_QUALITY_SHADED: + surface = TTF_RenderText_Shaded(_global.fontCurrent->font, message, 0, _global.colorForeground, _global.colorBackground); + break; -// A player's current frame for the 3D scene's video materials: the disc's texture is already -// updated for this frame; a loaded video is advanced here (drawing it on the overlay too is harmless). -static SDL_Texture *_sceneVideoSource(int32_t player) { - VideoT *video; + case FONT_QUALITY_BLENDED: + surface = TTF_RenderText_Blended(_global.fontCurrent->font, message, 0, _global.colorForeground); + break; - if ((player == _global.videoHandle) && (player >= 0)) { - return _global.videoTexture; + default: + _luaDie(L, method, "Unknown font quality!"); } - for (video = _global.videoList; video != NULL; video = video->hh.next) { - if (video->handle == player) { - videoUpdate(video->handle, &video->texture); - return video->texture; - } + if (!surface) { + _luaDie(L, method, "%s", SDL_GetError()); } - return NULL; -} + SDL_SetSurfaceColorKey(surface, true, COLOR_KEY_VALUE); - -// scriptExecute and scriptPush: the games.dat style table at argument 1 as a config, with the data -// directory the launched game will write to. Caller destroys it. -static ConfigT *_scriptConfFromTable(lua_State *L, const char *method) { - ConfigT *conf = NULL; - - _argCheck(L, method, 1, 1); - if (!lua_istable(L, 1)) { - _luaDie(L, method, "Argument 1 must be a table."); - } - conf = _buildConfFromTable(L, _global.conf); - conf->dataDir = resolveDataDir(conf); - if (conf->dataDir == NULL) { - _luaDie(L, method, "Unable to create the data directory for %s.", conf->scriptFile); - } - - return conf; -} - - -// Constants every script (and controls.cfg) can rely on. These are the single source of truth. -static void _pushConstants(lua_State *L) { - int32_t x = 0; - - for (x = 0; x < INPUT_COUNT; x++) { - lua_pushinteger(L, x); - lua_setglobal(L, _inputNames[x].switchName); - } - - lua_pushinteger(L, FONT_QUALITY_SOLID); - lua_setglobal(L, "FONT_QUALITY_SOLID"); - lua_pushinteger(L, FONT_QUALITY_SHADED); - lua_setglobal(L, "FONT_QUALITY_SHADED"); - lua_pushinteger(L, FONT_QUALITY_BLENDED); - lua_setglobal(L, "FONT_QUALITY_BLENDED"); - - lua_pushinteger(L, KEYBOARD_NORMAL); - lua_setglobal(L, "MODE_NORMAL"); - lua_pushinteger(L, KEYBOARD_FULL); - lua_setglobal(L, "MODE_FULL"); - - lua_pushinteger(L, MOUSE_SINGLE); - lua_setglobal(L, "MOUSE_SINGLE"); - lua_pushinteger(L, MOUSE_MANY); - lua_setglobal(L, "MOUSE_MANY"); - lua_pushinteger(L, MOUSE_SINGLE); - lua_setglobal(L, "SINGLE_MOUSE"); - lua_pushinteger(L, MOUSE_MANY); - lua_setglobal(L, "MANY_MOUSE"); - - lua_pushinteger(L, OVERLAY_NOT_UPDATED); - lua_setglobal(L, "OVERLAY_NOT_UPDATED"); - lua_pushinteger(L, OVERLAY_UPDATED); - lua_setglobal(L, "OVERLAY_UPDATED"); - - lua_pushinteger(L, RENDER_PIXELATED); - lua_setglobal(L, "RENDER_PIXELATED"); - lua_pushinteger(L, RENDER_SMOOTH); - lua_setglobal(L, "RENDER_SMOOTH"); - - lua_pushinteger(L, DISC_STOPPED); - lua_setglobal(L, "DISC_STOPPED"); - lua_pushinteger(L, DISC_PLAYING); - lua_setglobal(L, "DISC_PLAYING"); - lua_pushinteger(L, DISC_PAUSED); - lua_setglobal(L, "DISC_PAUSED"); - lua_pushinteger(L, DISC_EJECTED); - lua_setglobal(L, "DISC_EJECTED"); - // 3D light types - lua_pushinteger(L, LIGHT_DIRECTIONAL); - lua_setglobal(L, "LIGHT_DIRECTIONAL"); - lua_pushinteger(L, LIGHT_POINT); - lua_setglobal(L, "LIGHT_POINT"); - lua_pushinteger(L, LIGHT_SPOT); - lua_setglobal(L, "LIGHT_SPOT"); - // Physics bodies and shapes - lua_pushinteger(L, BODY_STATIC); - lua_setglobal(L, "BODY_STATIC"); - lua_pushinteger(L, BODY_DYNAMIC); - lua_setglobal(L, "BODY_DYNAMIC"); - lua_pushinteger(L, BODY_KINEMATIC); - lua_setglobal(L, "BODY_KINEMATIC"); - lua_pushinteger(L, SHAPE_BOX); - lua_setglobal(L, "SHAPE_BOX"); - lua_pushinteger(L, SHAPE_SPHERE); - lua_setglobal(L, "SHAPE_SPHERE"); - lua_pushinteger(L, SHAPE_CAPSULE); - lua_setglobal(L, "SHAPE_CAPSULE"); - lua_pushinteger(L, SHAPE_CYLINDER); - lua_setglobal(L, "SHAPE_CYLINDER"); - lua_pushinteger(L, SHAPE_HULL); - lua_setglobal(L, "SHAPE_HULL"); - lua_pushinteger(L, SHAPE_MESH); - lua_setglobal(L, "SHAPE_MESH"); - lua_pushinteger(L, JOINT_HINGE); - lua_setglobal(L, "JOINT_HINGE"); - lua_pushinteger(L, JOINT_BALL); - lua_setglobal(L, "JOINT_BALL"); - lua_pushinteger(L, JOINT_SLIDER); - lua_setglobal(L, "JOINT_SLIDER"); - lua_pushinteger(L, DEBUG_NONE); - lua_setglobal(L, "DEBUG_NONE"); - lua_pushinteger(L, DEBUG_SHAPES); - lua_setglobal(L, "DEBUG_SHAPES"); - lua_pushinteger(L, DEBUG_CONSTRAINTS); - lua_setglobal(L, "DEBUG_CONSTRAINTS"); - lua_pushinteger(L, DEBUG_CONTACTS); - lua_setglobal(L, "DEBUG_CONTACTS"); - lua_pushinteger(L, DEBUG_VELOCITIES); - lua_setglobal(L, "DEBUG_VELOCITIES"); - lua_pushinteger(L, DEBUG_STATIC); - lua_setglobal(L, "DEBUG_STATIC"); - lua_pushinteger(L, DEBUG_ALL); - lua_setglobal(L, "DEBUG_ALL"); - // Particles - lua_pushinteger(L, PARTICLE_ALPHA); - lua_setglobal(L, "PARTICLE_ALPHA"); - lua_pushinteger(L, PARTICLE_ADD); - lua_setglobal(L, "PARTICLE_ADD"); - lua_pushinteger(L, PARTICLE_OVER); - lua_setglobal(L, "PARTICLE_OVER"); - lua_pushinteger(L, PARTICLE_UNDER); - lua_setglobal(L, "PARTICLE_UNDER"); - // Billboards - lua_pushinteger(L, BILLBOARD_NONE); - lua_setglobal(L, "BILLBOARD_NONE"); - lua_pushinteger(L, BILLBOARD_ALL); - lua_setglobal(L, "BILLBOARD_ALL"); - lua_pushinteger(L, BILLBOARD_Y); - lua_setglobal(L, "BILLBOARD_Y"); - // Particle collisions - lua_pushinteger(L, COLLIDE_NONE); - lua_setglobal(L, "COLLIDE_NONE"); - lua_pushinteger(L, COLLIDE_FLOOR); - lua_setglobal(L, "COLLIDE_FLOOR"); - lua_pushinteger(L, COLLIDE_SCENE); - lua_setglobal(L, "COLLIDE_SCENE"); - // Texture filtering - lua_pushinteger(L, FILTER_LINEAR); - lua_setglobal(L, "FILTER_LINEAR"); - lua_pushinteger(L, FILTER_NEAREST); - lua_setglobal(L, "FILTER_NEAREST"); - // Tone curves - lua_pushinteger(L, TONEMAP_NONE); - lua_setglobal(L, "TONEMAP_NONE"); - lua_pushinteger(L, TONEMAP_NEUTRAL); - lua_setglobal(L, "TONEMAP_NEUTRAL"); - lua_pushinteger(L, TONEMAP_ACES); - lua_setglobal(L, "TONEMAP_ACES"); - // Vehicles - lua_pushinteger(L, VEHICLE_CAR); - lua_setglobal(L, "VEHICLE_CAR"); - lua_pushinteger(L, VEHICLE_MOTORCYCLE); - lua_setglobal(L, "VEHICLE_MOTORCYCLE"); - lua_pushinteger(L, VEHICLE_TANK); - lua_setglobal(L, "VEHICLE_TANK"); - lua_pushinteger(L, VEHICLE_BOAT); - lua_setglobal(L, "VEHICLE_BOAT"); - // Soft bodies - lua_pushinteger(L, SOFT_CLOTH); - lua_setglobal(L, "SOFT_CLOTH"); - lua_pushinteger(L, SOFT_BODY); - lua_setglobal(L, "SOFT_BODY"); - lua_pushinteger(L, SOFT_ROPE); - lua_setglobal(L, "SOFT_ROPE"); - - lua_pushinteger(L, SOUND_CHANNEL_NONE); - lua_setglobal(L, "SOUND_ERROR_INVALID"); - lua_pushinteger(L, SOUND_CHANNEL_NONE); - lua_setglobal(L, "SOUND_REMOVE_HANDLE"); - - // Input code layout so Framework.singe can build the GAMEPAD_N and MOUSE_N tables. - lua_pushinteger(L, CODE_GAMEPAD_BASE); - lua_setglobal(L, "SINGE_GAMEPAD_BASE"); - lua_pushinteger(L, CODE_GAMEPAD_STRIDE); - lua_setglobal(L, "SINGE_GAMEPAD_STRIDE"); - lua_pushinteger(L, CODE_AXIS_STRIDE); - lua_setglobal(L, "SINGE_AXIS_STRIDE"); - lua_pushinteger(L, CODE_GAMEPAD_BUTTON_OFFSET); - lua_setglobal(L, "SINGE_GAMEPAD_BUTTON_OFFSET"); - lua_pushinteger(L, CODE_MOUSE_BASE); - lua_setglobal(L, "SINGE_MOUSE_BASE"); - lua_pushinteger(L, CODE_MOUSE_STRIDE); - lua_setglobal(L, "SINGE_MOUSE_STRIDE"); - lua_pushinteger(L, MAX_CONTROLLERS); - lua_setglobal(L, "SINGE_MAX_CONTROLLERS"); - lua_pushinteger(L, MAX_MICE); - lua_setglobal(L, "SINGE_MAX_MICE"); - - lua_pushinteger(L, SINGE_VERSION_MAJOR); - lua_setglobal(L, "SINGE_VERSION_MAJOR"); - lua_pushinteger(L, SINGE_VERSION_MINOR); - lua_setglobal(L, "SINGE_VERSION_MINOR"); - lua_pushstring(L, VERSION_STRING); - lua_setglobal(L, "SINGE_VERSION_STRING"); - lua_pushnumber(L, SINGE_VERSION); - lua_setglobal(L, "SINGE_FRAMEWORK_VERSION"); - - lua_pushinteger(L, _global.controllerDeadZone); - lua_setglobal(L, "SINGE_DEAD_ZONE"); - lua_pushboolean(L, _global.conf->legacySpriteArgs); - lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS"); - lua_pushboolean(L, _global.conf->disc); - lua_setglobal(L, "SINGE_DISC"); -} - - -// Writes one overlay pixel. The overlay is always BGRA32, one uint32_t per pixel (_overlayResize -// makes it so), and must be locked by the caller. -static void _putPixel(int32_t x, int32_t y, uint32_t pixel) { - SDL_Surface *surface = _global.overlay; - uint32_t *row = NULL; - - if ((x < 0) || (x >= surface->w) || (y < 0) || (y >= surface->h)) { - return; - } - row = (uint32_t *)((uint8_t *)surface->pixels + (size_t)y * (size_t)surface->pitch); - row[x] = pixel; -} - - -// colorXxx(r, g, b[, a]) with components clamped to 0..255. -static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha) { - int32_t n = lua_gettop(L); - uint8_t value[COLOR_COMPONENTS] = { 0, 0, 0, defaultAlpha }; - int32_t x = 0; - - _argCheck(L, method, 3, COLOR_COMPONENTS); - for (x = 0; x < n; x++) { - value[x] = _argColorByte(L, method, x + 1); - } - color->r = value[0]; - color->g = value[1]; - color->b = value[2]; - color->a = value[3]; - _luaTrace(L, method, "%d %d %d %d", color->r, color->g, color->b, color->a); -} - - -// Releases whatever direction code an axis was holding. -static void _releaseAxis(int32_t axisIndex) { - if (_global.axisCode[axisIndex] != 0) { - _processKey(false, 0, _global.axisCode[axisIndex]); - _global.axisCode[axisIndex] = 0; - } + return surface; } @@ -3836,35 +3814,18 @@ static void _resetScriptState(void) { } -// Renders text with the current font, quality, and colors. -static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message) { - SDL_Surface *surface = NULL; - - if (_global.fontCurrent == NULL) { - _luaDie(L, method, "No font selected."); +// Loads and runs the game script under the traceback handler. A failure is fatal when the game +// starts; on a reload it is reported and the game sits empty until the next one. +static void _runScript(bool fatal) { + _progTrace("Running %s", _global.conf->scriptFile); + lua_pushcfunction(_global.luaContext, _luaTraceback); + if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL, _global.conf->reload) || lua_pcall(_global.luaContext, 0, 0, -2)) { + if (fatal) { + utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1)); + } + utilSay("Error running script: %s", lua_tostring(_global.luaContext, -1)); } - switch (_global.fontQuality) { - case FONT_QUALITY_SOLID: - surface = TTF_RenderText_Solid(_global.fontCurrent->font, message, 0, _global.colorForeground); - break; - - case FONT_QUALITY_SHADED: - surface = TTF_RenderText_Shaded(_global.fontCurrent->font, message, 0, _global.colorForeground, _global.colorBackground); - break; - - case FONT_QUALITY_BLENDED: - surface = TTF_RenderText_Blended(_global.fontCurrent->font, message, 0, _global.colorForeground); - break; - - default: - _luaDie(L, method, "Unknown font quality!"); - } - if (!surface) { - _luaDie(L, method, "%s", SDL_GetError()); - } - SDL_SetSurfaceColorKey(surface, true, COLOR_KEY_VALUE); - - return surface; + lua_settop(_global.luaContext, 0); } @@ -3882,6 +3843,43 @@ static void _saveAudioCalibration(int32_t milliseconds) { } +// A player's current frame for the 3D scene's video materials: the disc's texture is already +// updated for this frame; a loaded video is advanced here (drawing it on the overlay too is harmless). +static SDL_Texture *_sceneVideoSource(int32_t player) { + VideoT *video; + + if ((player == _global.videoHandle) && (player >= 0)) { + return _global.videoTexture; + } + for (video = _global.videoList; video != NULL; video = video->hh.next) { + if (video->handle == player) { + videoUpdate(video->handle, &video->texture); + return video->texture; + } + } + return NULL; +} + + +// scriptExecute and scriptPush: the games.dat style table at argument 1 as a config, with the data +// directory the launched game will write to. Caller destroys it. +static ConfigT *_scriptConfFromTable(lua_State *L, const char *method) { + ConfigT *conf = NULL; + + _argCheck(L, method, 1, 1); + if (!lua_istable(L, 1)) { + _luaDie(L, method, "Argument 1 must be a table."); + } + conf = _buildConfFromTable(L, _global.conf); + conf->dataDir = resolveDataDir(conf); + if (conf->dataDir == NULL) { + _luaDie(L, method, "Unable to create the data directory for %s.", conf->scriptFile); + } + + return conf; +} + + // Applies the command line audio track to a freshly loaded video, when it has one. static void _selectDefaultAudioTrack(int32_t handle) { if ((_global.conf->audioOutputTrack >= 0) && (_global.conf->audioOutputTrack < videoGetAudioTracks(handle))) { @@ -4301,6 +4299,28 @@ static void _videoDestroy(VideoT *video) { } +// Once a second: whether any watched file's modification time moved. +static bool _watchedChanged(void) { + uint64_t now = SDL_GetTicks(); + int32_t x; + + if (now < _global.watchTick + WATCH_INTERVAL_MS) { + return false; + } + _global.watchTick = now; + for (x = 0; x < _global.watchedCount; x++) { + int64_t size = 0; + int64_t modified = 0; + + if (vfsStat(_global.watched[x].name, &size, &modified) && (modified != _global.watched[x].modified)) { + _progTrace("%s changed", _global.watched[x].name); + return true; + } + } + return false; +} + + // --reload keeps the modification time of every loose script file the game loads. static void _watchFile(const char *name) { int64_t size = 0; @@ -4328,28 +4348,6 @@ static void _watchFile(const char *name) { } -// Once a second: whether any watched file's modification time moved. -static bool _watchedChanged(void) { - uint64_t now = SDL_GetTicks(); - int32_t x; - - if (now < _global.watchTick + WATCH_INTERVAL_MS) { - return false; - } - _global.watchTick = now; - for (x = 0; x < _global.watchedCount; x++) { - int64_t size = 0; - int64_t modified = 0; - - if (vfsStat(_global.watched[x].name, &size, &modified) && (modified != _global.watched[x].modified)) { - _progTrace("%s changed", _global.watched[x].name); - return true; - } - } - return false; -} - - // ===== Lua API ===== @@ -8081,6 +8079,22 @@ static int32_t apiSingeSetAudioCalibration(lua_State *L) { } +// singeSetAudioDelay(milliseconds) Positive when the audio device is heard later than it reports. +static int32_t apiSingeSetAudioDelay(lua_State *L) { + int32_t delay = 0; + + _argCheck(L, "singeSetAudioDelay", 1, 1); + delay = _argInteger(L, "singeSetAudioDelay", 1); + if ((delay < -VIDEO_AUDIO_DELAY_MAX) || (delay > VIDEO_AUDIO_DELAY_MAX)) { + _luaDie(L, "singeSetAudioDelay", "Audio delay must be between %d and %d milliseconds: %d", -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, delay); + } + videoSetAudioDelay(delay); + _luaTrace(L, "singeSetAudioDelay", "%d", delay); + + return 0; +} + + // singeSetGameName(title) static int32_t apiSingeSetGameName(lua_State *L) { const char *title = NULL; @@ -8104,22 +8118,6 @@ static int32_t apiSingeSetPauseFlag(lua_State *L) { } -// singeSetAudioDelay(milliseconds) Positive when the audio device is heard later than it reports. -static int32_t apiSingeSetAudioDelay(lua_State *L) { - int32_t delay = 0; - - _argCheck(L, "singeSetAudioDelay", 1, 1); - delay = _argInteger(L, "singeSetAudioDelay", 1); - if ((delay < -VIDEO_AUDIO_DELAY_MAX) || (delay > VIDEO_AUDIO_DELAY_MAX)) { - _luaDie(L, "singeSetAudioDelay", "Audio delay must be between %d and %d milliseconds: %d", -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, delay); - } - videoSetAudioDelay(delay); - _luaTrace(L, "singeSetAudioDelay", "%d", delay); - - return 0; -} - - // singeSetPauseKeyEnabled(enabled) Framework.singe aliases singeEnablePauseKey()/singeDisablePauseKey(). static int32_t apiSingeSetPauseKeyEnabled(lua_State *L) { _argCheck(L, "singeSetPauseKeyEnabled", 1, 1); @@ -9022,13 +9020,6 @@ static int32_t apiVehicleSetGears(lua_State *L) { } -static int32_t apiVehicleSetSteering(lua_State *L) { - _argCheck(L, "vehicleSetSteering", 2, 2); - vehicleSetSteering(_argVehicle(L, "vehicleSetSteering", 1), (float)_argNumber(L, "vehicleSetSteering", 2)); - return 0; -} - - // vehicleSetRudder(boat, maxTorque) static int32_t apiVehicleSetRudder(lua_State *L) { _argCheck(L, "vehicleSetRudder", 2, 2); @@ -9037,6 +9028,13 @@ static int32_t apiVehicleSetRudder(lua_State *L) { } +static int32_t apiVehicleSetSteering(lua_State *L) { + _argCheck(L, "vehicleSetSteering", 2, 2); + vehicleSetSteering(_argVehicle(L, "vehicleSetSteering", 1), (float)_argNumber(L, "vehicleSetSteering", 2)); + return 0; +} + + static int32_t apiVehicleSetSuspension(lua_State *L) { _argCheck(L, "vehicleSetSuspension", 3, 3); vehicleSetSuspension(_argVehicle(L, "vehicleSetSuspension", 1), (float)_argNumber(L, "vehicleSetSuspension", 2), (float)_argNumber(L, "vehicleSetSuspension", 3)); diff --git a/src/vfs.c b/src/vfs.c index 4c12d9dad..f14201321 100644 --- a/src/vfs.c +++ b/src/vfs.c @@ -104,8 +104,8 @@ struct VfsStreamS { }; -static bool _assetExists(DatabaseT *db, const char *key); static bool _assetDirectory(DatabaseT *db, const char *key); +static bool _assetExists(DatabaseT *db, const char *key); static bool _assetSize(DatabaseT *db, const char *key, int64_t *size); static char *_bindListRange(DatabaseT *db, const char *key); static bool _cacheCurrent(const TargetT *target); @@ -115,12 +115,12 @@ static void _databasesClose(void); static bool _fileModified(const char *path, int64_t *size, int64_t *modified); static bool _hasDatabaseExtension(const char *path); static bool _hasParentComponent(const char *norm); +static bool _isAbsolute(const char *name); +static bool _isEngineName(const char *name); static void _listAdd(ListT *list, const char *name); static int _listCompare(const void *a, const void *b); // qsort callback. Not changing int. static void _listDirectory(const char *path, ListT *list); static char **_listFinish(ListT *list, int32_t *count); -static bool _isAbsolute(const char *name); -static bool _isEngineName(const char *name); static char *_normalise(const char *name); static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer, const char *directory); static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory); @@ -137,13 +137,6 @@ static char *_dataDir = NULL; static char *_dataDirKey = NULL; // _dataDirBase normalised without trailing slashes, for _isEngineName -static bool _assetExists(DatabaseT *db, const char *key) { - int64_t size = 0; - - return _assetSize(db, key, &size); -} - - // True when any asset lives below the key, which is what a directory is in a database. static bool _assetDirectory(DatabaseT *db, const char *key) { char *from = _bindListRange(db, key); @@ -156,6 +149,13 @@ static bool _assetDirectory(DatabaseT *db, const char *key) { } +static bool _assetExists(DatabaseT *db, const char *key) { + int64_t size = 0; + + return _assetSize(db, key, &size); +} + + static bool _assetSize(DatabaseT *db, const char *key, int64_t *size) { bool found = false; diff --git a/src/videoPlayer.c b/src/videoPlayer.c index b02ebacbf..fd3f32c67 100644 --- a/src/videoPlayer.c +++ b/src/videoPlayer.c @@ -1484,12 +1484,6 @@ bool videoIsPlaying(int32_t playerHandle) { } -// The mixer's audio thread runs the sound and video callbacks; hold this to read what they write. -void videoLockAudio(void) { - MIX_LockMixer(_mixer); -} - - // audioFilename may be NULL when the audio lives in the video file. rgb players decode to BGRA so // scripts can read the pixels; everything else stays YUV and is converted by the GPU. int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb) { @@ -1572,6 +1566,12 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch } +// The mixer's audio thread runs the sound and video callbacks; hold this to read what they write. +void videoLockAudio(void) { + MIX_LockMixer(_mixer); +} + + void videoPause(int32_t playerHandle) { VideoPlayerT *v = _getPlayer(playerHandle, "videoPause"); @@ -1664,11 +1664,6 @@ void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPerc } -void videoUnlockAudio(void) { - MIX_UnlockMixer(_mixer); -} - - void videoUnload(int32_t playerHandle) { VideoPlayerT *v = _getPlayer(playerHandle, "videoUnload"); int32_t x = 0; @@ -1717,6 +1712,11 @@ void videoUnload(int32_t playerHandle) { } +void videoUnlockAudio(void) { + MIX_UnlockMixer(_mixer); +} + + // Advances playback to match the audio clock (or the wall clock for silent videos). Returns the frame now on the texture. int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) { VideoPlayerT *v = _getPlayer(playerHandle, "videoUpdate"); diff --git a/src/videoPlayer.h b/src/videoPlayer.h index eca71c9e7..5fff638c3 100644 --- a/src/videoPlayer.h +++ b/src/videoPlayer.h @@ -52,8 +52,8 @@ void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t * int32_t videoGetWidth(int32_t playerHandle); void videoInit(MIX_Mixer *mixer); bool videoIsPlaying(int32_t playerHandle); -void videoLockAudio(void); int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb); +void videoLockAudio(void); void videoPause(int32_t playerHandle); void videoPlay(int32_t playerHandle); void videoQuit(void); @@ -63,8 +63,8 @@ void videoSetAudioDelay(int32_t milliseconds); void videoSetAudioTrack(int32_t playerHandle, int32_t track); void videoSetHardwareDecoding(bool enabled); void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent); -void videoUnlockAudio(void); void videoUnload(int32_t playerHandle); +void videoUnlockAudio(void); int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture);