Weapon visuals + aim: beams render from the guns; pick self-target fixes
Renderer (dpl3-revive/patha): - Instance visibility honored at DRAW time (stored word 4 = live dpl_SetInstanceVisibility field): laser beams (4-frame pulses) and missile models now render; cache tags w3 0/1 instances 'gated' - PSFX world bursts (dcs=0 muzzle/impact/explosion) rendered; psfx storage list-based so one-shot bursts stop collapsing onto one key - fix_degenerate applied to INSTANCE chains (was camera-only): the beam chain rides four rank-2 rig DCSs; composed rank-2 drew beams away from the guns. Offline f1635 now shows both arm beams converging on the aim point - First-person canopy cull: the always-armed cockpit model at the head drew as a vertical line down screen center (user-confirmed fixed live) Device (vpx-device/vpxlog.cpp): - CAM backchannel on the fifosock: bridge streams its validated camera (+ vehicle root DCS); raycast_pick aims along the player's real look instead of the static view-node pose, which had locked every missile/beam onto one fixed wrong world point - raycast_pick skips hidden-until-armed instances and the player's own articulation subtree: picking own beams created a target feedback loop, picking own missiles retargeted salvos mid-flight, and a picked missile's deleted DCS froze the game (arena2 crash) Sound (vpx-device/vweawe.cpp): stereo-linked peak limiter replaces the hard output clamp (the "generator out" crackle); VWE_AWE_LOG reports pre-limit peaks + duck counts, enabled in launch_pod.ps1. Missile-fire crackle persists: per-voice EMU8000 clamp suspected. Eggs/confs: TESTAR2.EGG + gauge_arena2_sound.conf (map=arena2 city variant); cavern.egg (console-era reference); plasma display on COM2 in the gauge confs; gauge_arena_net_sound.conf (slirp net stack). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,9 @@ class VirtualBoard:
|
||||
self.morphs = {} # morphed geom -> (a, b, alpha)
|
||||
self.names = {} # 0x8000xxxx name id -> handle (0x22)
|
||||
self.sfx = {} # SPECIALFX defs by code (0x1c)
|
||||
self.psfx = [] # active PSFX effects (0x1d in
|
||||
# MUNGA): list of decoded bursts
|
||||
# = weapon bolts / muzzle / impacts
|
||||
self.dl2d = {} # 2D display lists (0x29/0x2b): handle -> [tag words]
|
||||
self._dl2d_acc = None # in-progress chunked 0x2b flush
|
||||
self.munga = False # game-build dialect (rpl4opt/btl4opt)
|
||||
@@ -147,11 +150,43 @@ class VirtualBoard:
|
||||
return self.do_add_connections(msg.payload)
|
||||
if msg.action == 0x1a:
|
||||
return self.do_texels(msg.payload)
|
||||
if msg.action == 0x1d and len(msg.payload) >= 56:
|
||||
# per-frame transform (shipped flush_artics):
|
||||
# [dcs_type=1 matrix][dcs handle][3x3 row-major][tx ty tz]
|
||||
h = struct.unpack_from('<I', msg.payload, 4)[0]
|
||||
self.anim[h] = struct.unpack_from('<12f', msg.payload, 8)
|
||||
if msg.action == 0x1d:
|
||||
p = msg.payload
|
||||
if self.munga and len(p) >= 160:
|
||||
# vr_psfx_action = dpl_Effect(particlestart|explosion, DCS,
|
||||
# &info): a glowing particle burst. Wire = [DCS handle]
|
||||
# [dpl_PARTICLESTART_EFFECT_INFO] (DPLTYPES.H), so struct
|
||||
# field K is at word K+1: [5..7]=px,py,pz release position,
|
||||
# [8]=pv, [9..11]=vel, [15]=rad, [17]=exp (expansion),
|
||||
# [29..31]=start INTERIOR rgb, [32]=start alpha, [45..47]=
|
||||
# end INTERIOR rgb. DCS!=0 -> emitter rides that projectile
|
||||
# DCS (weapon bolt, px,py,pz is a ~0 local offset); DCS==0
|
||||
# -> one-shot WORLD-space burst at (px,py,pz) (muzzle flash,
|
||||
# impact spark, explosion -- big rad). MUNGA articulates via
|
||||
# 0x1f, so a >=160B 0x1d is always a psfx, never a transform.
|
||||
n = len(p) // 4
|
||||
f = struct.unpack_from('<%df' % n, p)
|
||||
dcs = struct.unpack_from('<I', p, 0)[0]
|
||||
pos = (f[5], f[6], f[7])
|
||||
col = (max(0.0, f[29]), max(0.0, f[30]), max(0.0, f[31]))
|
||||
ecol = (max(0.0, f[45]), max(0.0, f[46]), max(0.0, f[47])) \
|
||||
if n > 47 else (0.0, 0.0, 0.0)
|
||||
rad = abs(f[15]) if n > 15 else 1.0
|
||||
exp = abs(f[17]) if n > 17 else 0.0
|
||||
alpha = min(1.0, abs(f[32])) if n > 32 else 1.0
|
||||
ttl = 30 if rad >= 3.0 else (14 if dcs == 0 else 48)
|
||||
self.psfx.append({'dcs': dcs, 'pos': pos, 'col': col,
|
||||
'ecol': ecol, 'rad': rad, 'exp': exp,
|
||||
'alpha': alpha, 'ttl': ttl, 'maxttl': ttl,
|
||||
'age': 0})
|
||||
if len(self.psfx) > 256: # bound it; renderer expires
|
||||
del self.psfx[:-256]
|
||||
return b''
|
||||
if len(p) >= 56:
|
||||
# FLYK per-frame transform (shipped flush_artics):
|
||||
# [dcs_type=1 matrix][dcs handle][3x3 row-major][tx ty tz]
|
||||
h = struct.unpack_from('<I', p, 4)[0]
|
||||
self.anim[h] = struct.unpack_from('<12f', p, 8)
|
||||
return b''
|
||||
if msg.action == 0x1f and len(msg.payload) >= 8:
|
||||
# MUNGA batched flush_artics: [n_records] then n records of
|
||||
|
||||
@@ -32,6 +32,18 @@ TYPE = {2:'zone',3:'view',4:'instance',5:'dcs',6:'lmodel',7:'object',8:'lod',
|
||||
0xe:'light'}
|
||||
|
||||
|
||||
def inst_visible(nodes, inst):
|
||||
"""Draw-time gate for 'gated' (hidden-until-armed, w3 0/1) instances:
|
||||
stored word 4 of the CURRENT instance body is the live visibility field
|
||||
(dpl_SetInstanceVisibility). Checked per draw because a flush bumps no
|
||||
node/edge counts, so the cache never rebuilds on a visibility pulse
|
||||
(BT laser beams arm for ~4 frames per shot)."""
|
||||
if not inst.get('gated'):
|
||||
return True
|
||||
b = nodes.get(inst['handle'], {}).get('body') or b''
|
||||
return len(b) >= 20 and struct.unpack_from('<I', b, 16)[0] != 0
|
||||
|
||||
|
||||
# NOTE: vrboard.do_flush stores the flush payload MINUS the leading remote word,
|
||||
# so stored-body word k = wire-payload word k+1. Offsets below are stored-body.
|
||||
def _mat_from_dcs(body):
|
||||
@@ -386,8 +398,14 @@ class SceneCache:
|
||||
# translocation, SHARKS' single w3=0 is the null-object 'fishes'.
|
||||
w3 = struct.unpack_from('<I', body, 12)[0]
|
||||
bboard = w3 == 2
|
||||
if w3 not in (2, 3):
|
||||
continue
|
||||
# w3 0/1 = HIDDEN-until-armed: stored word 4 is the live
|
||||
# visibility field (dpl_SetInstanceVisibility) -- BT pulses it
|
||||
# ~4 frames per laser shot (beam quad) and missile models fly
|
||||
# with it set. A flush bumps no node/edge counts so the cache
|
||||
# never rebuilds on the pulse: keep these instances in the list
|
||||
# tagged 'gated' and test the CURRENT body word at draw time.
|
||||
# w3 2/3 ignore the field (BT arena buildings sit at 0 yet draw).
|
||||
gated = w3 not in (2, 3)
|
||||
if tname(obj) != 'object':
|
||||
continue
|
||||
# SCHILD/DCHILD effect attachments (Trek shield bubbles) carry a
|
||||
@@ -436,12 +454,24 @@ class SceneCache:
|
||||
# the same dcs_link rig as the camera (torso 0x1f-animated) --
|
||||
# nest-only chains left the player mech rendering at world origin.
|
||||
# FLYK stays nest-only (links there are sibling rings; session 3).
|
||||
# gated instances carry their model-space radius so the draw can
|
||||
# cull the player's own cockpit canopy (a small always-armed
|
||||
# fixture AT the camera -- our eye is not at the true head node,
|
||||
# so it smears a sliver across the view). Beams/missiles are
|
||||
# gated too but extend far, so radius keeps them drawing.
|
||||
rad = 0.0
|
||||
if gated:
|
||||
for g in lods[0][2]:
|
||||
m = self._mesh(board, g)
|
||||
if m is not None and len(m.get('pos', [])):
|
||||
rad = max(rad, float(np.abs(m['pos']).max()))
|
||||
self.instances.append({'dcs': inst_dcs[h], 'handle': h,
|
||||
'chain': self._chain(
|
||||
board, inst_dcs[h],
|
||||
links=getattr(board, 'munga', False)),
|
||||
'geoms': lods[0][2], 'lods': lods,
|
||||
'billboard': bboard, 'hud': hud})
|
||||
'billboard': bboard, 'hud': hud,
|
||||
'gated': gated, 'radius': rad})
|
||||
|
||||
# vertex strides seen on the wire (SHARKS + SDEMO): stride -> field slices
|
||||
# 3 = [x y z] (vtype 0x01)
|
||||
@@ -688,7 +718,9 @@ class Renderer:
|
||||
d = d / dn
|
||||
best_t, best_h = np.inf, 0
|
||||
for inst in self.cache.instances:
|
||||
M = self.chain_matrix(board, inst['chain'])
|
||||
if not inst_visible(board.nodes, inst):
|
||||
continue
|
||||
M = self.chain_matrix(board, inst['chain'], fix_degenerate=True)
|
||||
R, T = M[:3, :3], M[3, :3]
|
||||
for gh in inst['geoms']:
|
||||
mesh = self.cache._mesh(board, gh)
|
||||
@@ -789,9 +821,19 @@ class Renderer:
|
||||
FIX[:3, :3] = [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
|
||||
|
||||
for inst in c.instances:
|
||||
Mw = self.chain_matrix(board, inst['chain'])
|
||||
if not inst_visible(board.nodes, inst):
|
||||
continue
|
||||
# fix_degenerate: BTL4 vehicle rigs contain pass-through DCSs
|
||||
# whose flushed rotation is rank-2 (shifted body variant); the
|
||||
# laser-beam chain rides four of them and composed rank-2 = beam
|
||||
# drawn away from the gun. Same treatment the camera chain gets.
|
||||
Mw = self.chain_matrix(board, inst['chain'], fix_degenerate=True)
|
||||
# NOVIEWMATRIX instances live in camera space: no view transform
|
||||
M = Mw if inst.get('hud') else Mw @ V
|
||||
# first-person canopy cull (see SceneCache radius note)
|
||||
if (inst.get('gated') and not inst.get('hud')
|
||||
and np.linalg.norm(M[3, :3]) + inst.get('radius', 0) < 10.0):
|
||||
continue
|
||||
if inst['chain'] and inst['chain'][0] in board.anim:
|
||||
M = FIX @ M
|
||||
if inst['billboard']:
|
||||
|
||||
@@ -463,8 +463,16 @@ class GLRenderer(vrview.Renderer):
|
||||
FIX[:3, :3] = [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
|
||||
|
||||
for inst in c.instances:
|
||||
Mw = self.chain_matrix(board, inst['chain'])
|
||||
if not vrview.inst_visible(board.nodes, inst):
|
||||
continue
|
||||
# fix_degenerate: see vrview draw loop (rank-2 rig DCSs skewed
|
||||
# the laser beam off the gun)
|
||||
Mw = self.chain_matrix(board, inst['chain'], fix_degenerate=True)
|
||||
M = Mw if inst.get('hud') else Mw @ V
|
||||
# first-person canopy cull (see SceneCache radius note in vrview)
|
||||
if (inst.get('gated') and not inst.get('hud')
|
||||
and np.linalg.norm(M[3, :3]) + inst.get('radius', 0) < 10.0):
|
||||
continue
|
||||
if inst['chain'] and inst['chain'][0] in board.anim:
|
||||
M = FIX @ M
|
||||
if inst['billboard']:
|
||||
@@ -488,6 +496,7 @@ class GLRenderer(vrview.Renderer):
|
||||
self._draw_geom(board, c, gh, mesh, M, Mw)
|
||||
|
||||
self._draw_particles(board, V, vp, self.skip / 60.0)
|
||||
self._draw_psfx(board, V)
|
||||
self._draw_hud2d_gl(board, vp)
|
||||
|
||||
# present: FBO -> window with the DAC gamma
|
||||
@@ -686,3 +695,82 @@ class GLRenderer(vrview.Renderer):
|
||||
self._points_vao(data).render(mgl.POINTS, vertices=len(data))
|
||||
self._fbo.depth_mask = True
|
||||
ctx.disable(mgl.BLEND)
|
||||
|
||||
def _draw_psfx(self, board, V):
|
||||
"""Triggered PSFX (0x1d) as glowing additive discs, two kinds:
|
||||
|
||||
- DCS-attached bolts (weapon fire): the emitter rides the flying
|
||||
projectile DCS, so the glow tracks the shot. We keep a short trail of
|
||||
recent world positions and draw a fading tail, so the continuous
|
||||
particle stream (rate ~300/s) reads as a streak, not a lone dot. The
|
||||
bolt lives until the projectile DCS is deleted (impact).
|
||||
- DCS=0 world bursts (muzzle flash, impact spark, explosion): a fixed
|
||||
world-space burst at (px,py,pz); big-radius explosions expand and the
|
||||
colour lerps from the start interior toward the end interior over the
|
||||
burst's ttl, then it fades out."""
|
||||
if not board.psfx:
|
||||
return
|
||||
cache = {}
|
||||
rows, live = [], []
|
||||
for e in board.psfx:
|
||||
dcs = e['dcs']
|
||||
if dcs:
|
||||
nd = board.nodes.get(dcs)
|
||||
if not nd or nd.get('type') != 5:
|
||||
continue # projectile consumed (impact)
|
||||
m = cache.get(dcs)
|
||||
if m is None:
|
||||
try:
|
||||
m = self.chain_matrix(board, self._chain(board, dcs, links=True))
|
||||
except Exception:
|
||||
m = self.dcs_matrix(board, dcs)
|
||||
cache[dcs] = m
|
||||
pos = np.asarray(m[3, :3], float)
|
||||
else:
|
||||
e['ttl'] -= 1
|
||||
if e['ttl'] <= 0:
|
||||
continue
|
||||
pos = np.asarray(e['pos'], float)
|
||||
if not np.all(np.isfinite(pos)):
|
||||
continue
|
||||
e['age'] += 1
|
||||
live.append(e)
|
||||
frac = e['age'] / max(e['maxttl'], 1) # 0..1 over life
|
||||
col = np.clip((1.0 - frac) * np.array(e['col'])
|
||||
+ frac * np.array(e['ecol']), 0.0, 2.0)
|
||||
a0 = min(e['alpha'], 1.0)
|
||||
if dcs: # bolt + trail
|
||||
tr = e.setdefault('trail', [])
|
||||
tr.append(pos)
|
||||
if len(tr) > 9:
|
||||
del tr[0]
|
||||
nt = len(tr)
|
||||
for k, tp in enumerate(tr):
|
||||
w = (k + 1) / nt # head brightest
|
||||
rows.append([tp[0], tp[1], tp[2],
|
||||
max(e['rad'] * (0.35 + 0.65 * w), 1.2),
|
||||
col[0], col[1], col[2], a0 * w * w])
|
||||
else: # world burst
|
||||
grow = 1.0 + min(e['exp'], 12.0) * 0.09 * frac # explosions swell
|
||||
fade = min(1.0, e['ttl'] / 10.0)
|
||||
rows.append([pos[0], pos[1], pos[2], max(e['rad'] * grow, 1.5),
|
||||
col[0], col[1], col[2], a0 * fade])
|
||||
board.psfx = live
|
||||
if not rows:
|
||||
return
|
||||
data = np.array(rows, '<f4')
|
||||
# back-to-front for the order-dependent additive-over blend
|
||||
pe_z = data[:, 0:3] @ V[:3, :3] + V[3, :3]
|
||||
data = data[np.argsort(pe_z[:, 2])]
|
||||
ctx, mgl = self.ctx, self.moderngl
|
||||
prog = self._point_prog
|
||||
self._mat(prog, 'u_mv', V)
|
||||
self._set(prog, 'u_minpr', 0.6)
|
||||
self._set(prog, 'u_maxpr', float(self.h * 0.22))
|
||||
self._set(prog, 'u_particle', 1)
|
||||
ctx.enable(mgl.BLEND)
|
||||
ctx.blend_func = (mgl.ONE, mgl.ONE_MINUS_SRC_ALPHA)
|
||||
self._fbo.depth_mask = False
|
||||
self._points_vao(data).render(mgl.POINTS, vertices=len(data))
|
||||
self._fbo.depth_mask = True
|
||||
ctx.disable(mgl.BLEND)
|
||||
|
||||
Reference in New Issue
Block a user