Skip to content

Classic Quake II/III dynamic lights for muzzle flashes, bright projectiles and explosions - #148

Merged
themuffinator merged 2 commits into
mainfrom
claude/idtech-dlights-effects-f06985
Sep 3, 2026
Merged

Classic Quake II/III dynamic lights for muzzle flashes, bright projectiles and explosions#148
themuffinator merged 2 commits into
mainfrom
claude/idtech-dlights-effects-f06985

Conversation

@themuffinator

@themuffinator themuffinator commented Sep 3, 2026

Copy link
Copy Markdown
Owner

What is in this PR

Documentation, plus a CI pin bump. The implementation is in the companion
repo
and is already on main there:
themuffinator/openQ4-game@bfe7f45src/{game,mpgame}/ClassicLights.{h,cpp}
plus hooks in Weapon.cpp, Projectile.cpp, Moveable.cpp, Game_local.cpp,
SysCvar.* and SysCmds.cpp. Review that commit alongside this one.

The CI pin bump is a fix, not incidental

Both workflows pinned OPENQ4_GAMELIBS_SHA at 67fcd3e, twelve commits behind
the game tree. The engine's cross-repo tests assert the game tree's current
contracts, so a drifting pin turns them into failures unrelated to whatever
commit is under test — weapon_zoom_view_alignment has been red on main
since before this branch existed
, because the pinned tree still calls
UpdateZoomGuiViewState from the 2D overlay pass that the game-side fix
removed. Moving the pin to bfe7f45 fixes that and makes CI validate against
the tree this PR documents.

Checked by running the workflow's own 164-test list against the new pin: 158
pass; the six that do not cannot run in this environment at all (two need
native Linux/macOS, three are invoked with required arguments in CI rather than
bare, one needs a built client in .install).

What the feature does

Quake II and Quake III lit the world from the action itself. Quake 4 replaced
that with authored BSE effects, and a sweep of all 1172 shipped .fx files
shows how little survived: 71 light segments in total, not one of them on an
explosion
, and no player projectile lit apart from the single-player rocket
and grenade trails.

g_classicDynamicLights (default 1, archived) adds that layer back:

  • Muzzle flashes — a ~100 ms world flash at the barrel, 1.5x the weapon's
    authored radius, layered over the stock flash. Raised ahead of the authored
    flash's bail-out, so a weapon with no flash light of its own still lights the
    room.
  • Bright projectiles — a tracking light installed into the existing
    renderLight/lightDefHandle path, which already moves it. Gap-fill only: a
    def authoring mtr_light_shader is untouched, and the SP rocket and grenade
    are skipped because their shipped trail effects already carry a light segment.
    Their MP trails do not, so MP takes it.
  • Explosions — detonations flash the surrounding geometry. Entirely new;
    nothing shipped does this. Exploding barrels and damagables included.

g_classicDynamicLightScale (0.25–4) scales the radius.

Why the colors are what they are

They are extracted from the shipped PK4s rather than invented, which is the
part most worth checking. Muzzle flashes use the firing weapon def's own
flashColor. Projectile and explosion colors are the tints of light segments
Raven authored elsewhere in the same asset set:

Class Color Source
Rocket 0.906 0.518 0.161 effects/weapons/rocketlauncher/fly.fx
Grenade 0.973 0.286 0.086 effects/weapons/grenadelauncher/trail.fx
Napalm 0.922 0.545 0.322 effects/weapons/napalmgun/globburn.fx
Dark matter 0.502 0 1 effects/weapons/dmg/core.fx
Explosions 1 0.824 0.290 effects/monsters/strogg_flyer/bomb_burst.fx

Bullets, nails, debris and glass are deliberately unlit — not bright, far too
many in flight to afford, and the originals did not light them either.

Safety and cost

  • Client presentation only: nothing simulated, saved or networked, and no
    entity gains a field
    , so the savegame format is untouched.
  • Spawning mirrors idGameLocal::PlayEffect's guards, so a re-run predicted
    frame on an MP client does not stack a flash every pass, and a dedicated
    server never takes a light handle.
  • Fixed pool of 24 one-shot flashes, recycled oldest-first the way
    CL_AllocDlight did; all unshadowed; culled beyond ~3000 units.
  • Faded against the presentation clock, so decay stays smooth between
    authoritative tics.
  • Content overrides per entity def: classic_light, classic_light_color,
    classic_light_radius.

Testing

Six runs against an isolated worktree build (SP maps, and an MP listen server
with bots exercising the game_mp tree) — no crashes, warnings or errors, and
clean shutdown each time. Per-channel deltas measured against a baseline frame
confirm the colors reach the world:

Frame dR dG dB
Rocket muzzle flash +8.68 +4.94 +1.62
Blaster muzzle flash +5.52 +4.13 +4.00
Rocket explosion +9.72 +5.32 +1.22
Dark matter explosion +7.62 +1.61 +7.04
g_classicDynamicLights 0 −1.50 −0.47 +0.04

Real AI projectile detonations produced flashes in live combat.

Not confirmed on screen, and worth a reviewer's eye in normal play: the
rvWeapon::MuzzleFlash call site needs a trigger pull, and I could not frame a
light riding a projectile in flight — the console spawn command drops enemies
80 units away and they close to melee. Both are short call sites into functions
that are verified; testClassicLight [muzzle|explosion] [def] (cheat) raises
the flashes without firing.

Note for anyone validating this locally

tools/build/build_gamelibs.ps1 produces game DLLs that cannot run against
the engine
. openQ4-game/src/idlib/precompiled.h enables
RV_UNIFIED_ALLOCATOR while the engine's has it commented out;
stage_gamelibs.py exists to overwrite idlib/renderer/ui/sys/bse with the
engine's copies, so only the engine build yields an ABI-matched module. The
tell is WARNING: Unprotected allocation in DLL detected prior to initialization of memory system immediately before an execute-type
0xC0000005 right after Loading game DLL:. Build the engine and use
builddir/baseoq4/.

🤖 Generated with Claude Code

The implementation lives in openQ4-game (bfe7f45); this is the player- and
release-facing side of it.

The new guide explains what the feature adds over stock Quake 4, and gives
the two things a reader actually needs to act on: where each color comes
from in the shipped assets, so the choices are checkable rather than
asserted, and the per-def override keys for content that wants something
else. It also records the cost model and points at testClassicLight rather
than making people fire a weapon at a wall to see the effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Both workflows pinned openQ4-game at 67fcd3e, which is now twelve commits
behind. The engine's cross-repo tests assert the game tree's *current*
contracts, so the pin drifting turns them into failures that have nothing
to do with the commit under test: weapon_zoom_view_alignment has been red
on main since the zoom-scope fix landed on the game side, because the
pinned tree still calls UpdateZoomGuiViewState from the 2D overlay pass
that the fix removed.

Pin bfe7f45 instead, which is the game tree these workflows should have
been validating all along, and which carries the ClassicLights work this
branch documents.

Verified by running the workflow's own 164-test list against the new pin:
158 pass, and the only six that do not are the ones that cannot run here
at all - two require native Linux/macOS, three are invoked with required
arguments in CI rather than bare, and one needs a built client in
.install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@themuffinator

Copy link
Copy Markdown
Owner Author

Merging with four red checks, all of them pre-existing. Recording the evidence
so this is not mistaken for a rubber stamp:

Check Consecutive main failures before this branch
macOS Metal Push Verification 10
macOS OpenGL Push Verification 8
macOS Intel x64 Metal Push Verification 6+
macOS Intel x64 OpenGL Push Verification verified same cause

All four fail with the same message — validation failed: Optional renderer startup validation matrix failed with exit code 1. — from GL_INVALID_ENUM /
GL_INVALID_OPERATION at renderer startup. The signature is identical on a
main run from 2026-08-28 that predates this branch and used the old
GameLibs pin, so neither the docs nor the pin bump caused them. This is the
macOS bridge limitation tracked as issue #98; Intel macOS is additionally
listed as unsupported and not-published in docs/dev/release-completion.md.

They were invisible on recent main runs only because the Validation Script Smoke gate was failing upstream and skipping the whole matrix. Bumping the
stale pin fixed that gate, which is why these are newly visible rather than
newly broken — the intended behaviour, per the project's own stance against
masking failures.

No speculative renderer fix was attempted: it would mean editing the
OpenGL-over-Metal bridge blind, unvalidatable without Apple hardware, from a
PR that changes zero source files.

Green and relevant: Windows x64, all four Linux GCC/Clang Wayland variants,
Linux ARM64, Linux ARM64 cross-build, Linux x64 ASan+UBSan, and macOS ARM64
Commit Validation on both Metal and OpenGL — 21 in total. The build legs matter
beyond the tick: via the pin bump they compile the new ClassicLights code on
GCC and Clang across platforms, which the MSVC-only local build never covered.

🤖 Addressed by Claude Code

@themuffinator
themuffinator merged commit 3bbd623 into main Sep 3, 2026
21 of 25 checks passed
@themuffinator
themuffinator deleted the claude/idtech-dlights-effects-f06985 branch September 7, 2026 22:36
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