Skip to content

External weapon model refactor: external models on turrets - #7752

Draft
Goober5000 wants to merge 6 commits into
scp-fs2open:masterfrom
Goober5000:feature/external_turret_model
Draft

External weapon model refactor: external models on turrets#7752
Goober5000 wants to merge 6 commits into
scp-fs2open:masterfrom
Goober5000:feature/external_turret_model

Conversation

@Goober5000

Copy link
Copy Markdown
Contributor

Follow-up to #7751. This adds the feature that motivated the whole series: external model support for turrets, complete with firing animations.

  • Add external weapon models on turrets. With the new "show external weapon model" subsystem flag, the turret's weapon model is rendered at each of the turret's firing points, transformed through the turret's submodel chain so it tracks base rotation and barrel elevation. When the turret fires, the fired point's model is hidden exactly as the live missile spawns at the same transformed position, making the swap seamless. The model stays hidden until the turret's turret-firing and turret-fired animations have fully completed. Destroyed turrets show nothing, and beam turrets never empty their points.

  • Add the weapon-reload animation type. After the post-firing animations complete, a reload animation plays if the ship defines one, and the weapon model reappears when it completes, since the animation may show the round being moved into place; without one, the model simply reappears. The type is dual-keyed: +Triggered By: takes the name of a turret subsystem, or a secondary bank index. The optional +Model Spawn Time: key lets the designer specify the exact moment of reappearance instead — for example the apex of an auto-reversing loader arm, or any point during its return leg.

  • Support weapon-reload on ship secondary banks. A reload animation keyed by bank index replaces the hardcoded slide-back reload visual for that bank: when a point fires, the animation starts and the point's missile stays hidden until it completes, then appears at its full position. Banks without one keep the classic slide-back. Applies to both the in-mission render and the HUD hardpoints gauge.

  • Multiplayer is supported. The server empties firing points when turrets fire (including the swarm path); clients do the same in the turret- and flak-fired packet handlers, cycling their own firing points. Reappear timing is exact on clients too, because it is derived from the turret's animation state (via a new AnimationList::anyActive() query rather than timestamps), and those animations already arrive via the existing animation sync. Banks need no extra plumbing, since ship_fire_secondary runs on every machine.

In draft until #7751 is merged.

Goober5000 and others added 6 commits August 28, 2026 17:52
…up queries

Three engine additions in preparation for migrating external weapon
model gun rotation to the animation system:

- "seamless forward shutdown" animation flag: while a "seamless with
  startup" animation plays its shutdown (or is otherwise reversed),
  its motion is mirrored about the pose where the reversal began, so
  the motion continues forward while winding down (a decelerating
  forward spin for gun barrels) instead of retracing backwards.  The
  mirror pivot is captured whenever an animation enters reverse; the
  mirrored pose is this animation's delta reflected about its pivot
  delta, composed onto the apply buffer in place of the normal
  calculation.

- $Spin Up: segment: rotates a submodel in PBH with constant
  acceleration up to a target angular velocity and ends while still
  moving at that velocity, unlike $Rotation:, which always brakes to
  hit its target angle.  Intended as the startup portion of a seamless
  looping animation, followed by a constant-velocity $Rotation:
  covering one full cycle, so pose and velocity are continuous at the
  loop seam.

- AnimationList::isFullyStarted() (true once every animation is "up to
  speed": playing forward within the seamless loop portion, or
  completed for non-looping animations) and
  AnimationList::startShutdown() (looping animations finish their
  current loop and stop -- entering the seamless shutdown -- while
  others play in reverse).  These are the firing-gate and
  trigger-release primitives for weapon warmup animations.

Also, an explicit forward start now cancels a pending or in-progress
seamless shutdown, so a re-triggered animation spins back up from its
current state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the "weapon-warmup" animation trigger type for weapon-owned
animation sets ($Animations: in weapons.tbl, which already existed for
the in-flight model's on-spawn and scripted triggers).  Warmup
animations target the weapon's external display model: they start when
the bank tries to fire, the bank holds fire until they are fully
started, and they wind down when the bank stops firing.  Weapon-owned
primary-fired and secondary-fired animations now also trigger on the
bank's external model with each shot, enabling recoil animations.

Wiring:
- ship_get_external_weapon_model_instance() is generalized to any bank
  (secondaries can now have instances) and creates an instance whenever
  the weapon has animations, not just for Gun_rotation submodels.  It
  also stops running animations before deleting a stale instance, as
  does ship_delete().
- update_external_weapon_animations() (formerly ..._spin) ensures the
  bank instance exists even for ships that are never rendered, starts
  or shuts down warmup animations on firing-state transitions, and
  steps each bank instance's animations -- these instances belong to no
  object, so the object-loop stepping never reaches them.
- ship_fire_primary() gates firing on
  AnimationList::isFullyStarted() for the weapon's warmup animations,
  parallel to the legacy submodel-rotation gate.

Also, ModelAnimation::start() no longer takes the multiplayer
early-return path for model instances that belong to no object
(external weapon models, skybox, cockpit): such animations cannot be
synced by object and now simply run locally on each machine;
previously multiplayer clients would never start them at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
$Submodel Rotation Speed: / $Submodel Rotation Acceleration: plus the
Gun_rotation submodel flag are now a parse-time shim: at weapon page-in
the tabled values are converted into a synthesized weapon-warmup
animation on the weapon's display model, unless the modder already
supplied one.  Each Gun_rotation submodel gets a $Spin Up: ramp to the
tabled velocity about its bank axis followed by one full revolution at
speed, looping seamlessly, with the forward-shutdown flag so barrels
decelerate forward on release -- reproducing the old hardcoded
behavior, including the firing gate.  If the display model has no
Gun_rotation submodels, the old system still delayed firing by the
spin-up time, so a segmentless wait animation preserves that gate.

The hand-rolled spin machinery is deleted: the per-bank rotate
rate/angle state, the integration in update_external_weapon_spin, the
firing gate on rotate_rate, and the render-time bashing of
canonical_orient on Gun_rotation submodels are all replaced by the
warmup animation path added in the previous commit.

One deliberate behavior change: a weapon tabled with rotation velocity
but zero acceleration could never fire under the old system (its spin
rate never increased); it now spins up instantly and fires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New subsystem flag "show external weapon model": the turret's weapon
model (first weapon with a displayable model, secondaries preferred;
external model with fallback to the weapon's own model) is rendered at
each of the turret's firing points, transformed through the turret's
submodel chain so it tracks base rotation and barrel elevation.  This
makes a missile visible in its launcher while a turret-firing (trap
door) animation plays, per-firing-point:

- When the turret fires, the fired point's model is hidden exactly as
  the live missile spawns at the same transformed position, making the
  swap seamless (also orientation-seamless for "fire down normals"
  turrets).
- The model stays hidden until the turret's firing and post-firing
  (turret-fired) animations have fully completed, tracked with the new
  AnimationList::anyActive() query rather than timestamps -- which
  also gives multiplayer clients exact timing, since those animations
  arrive via the existing animation sync.
- The new "weapon-reload" animation type then plays if the ship
  defines one; the weapon model reappears when it completes, since the
  animation may show the round being moved into place.  Without one,
  the model simply reappears.

The weapon-reload type is dual-keyed: +Triggered By: takes either the
name of a turret subsystem, or an integer, reserved for a secondary
bank index for reload animations on ship weapon banks.

Setting the subsystem flag implies the ship-level Draw_weapon_models
flag so the render path activates.  Destroyed turrets show nothing.
Beam turrets never empty their points (no projectile to display).

Multiplayer: the server empties firing points in turret_fire_weapon
and turret_swarm_fire_from_turret; clients do the same in the turret-
and flak-fired packet handlers, cycling their own firing points --
visually consistent even if the cycle drifts from the server's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A weapon-reload animation keyed by secondary bank index (+Triggered
By: <bank>) now replaces the hardcoded slide-back reload visual for
that bank's external weapon models: when a point fires, the animation
starts and the point's missile stays hidden until it completes, then
appears at its full position.  Banks without a reload animation keep
the classic slide-back.  Applies to both the in-mission render and the
HUD hardpoints gauge.

The animation starts immediately on firing (banks have no door or
post-firing phase to wait for, unlike turrets).  If it is longer than
the bank's fire wait, a fast-firing bank can fire from a still-hidden
point -- coordinating those times is up to the ship designer, matching
the stance taken for weapon-warmup.  Since a bank's points share one
animation, simultaneously reloading points restart it; staggered
single fire looks correct.

Multiplayer works without extra plumbing: ship_fire_secondary runs on
every machine via the existing secondary-fired packets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
For an auto-reversing reload animation (e.g. a loader arm that extends
with the round and retracts empty), the external weapon model should
reappear at the moment the round is placed -- mid-animation -- rather
than at the end.  The optional +Model Spawn Time: key (seconds, after
$Flags:) lets the designer specify that moment; it is stored in the
animation's flag data alongside loopsFrom.

AnimationList::getModelSpawnTime() honors the key, falling back to the
animation's duration when unset, and both reload consumers (turrets
and secondary banks) now use it for their reappear stamps.  The spawn
time is deliberately not capped to the duration, since an
auto-reversing animation's return leg plays past the one-way duration
that getTime() reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Goober5000
Goober5000 force-pushed the feature/external_turret_model branch from 157b3a9 to 64261d8 Compare August 28, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant