fix(rendering): port retail shared alpha list
Queue translucent world GfxObj batches and scene particles in one stable far-to-near stream using transformed DAT sort centers, then drain it at retail's landscape and final-world boundaries. Preserve authored blend, cull, lighting, opacity, and adjacent-only batching so particles behind lifestones are composited through the crystal instead of overpainting it. Release build succeeds and all 5,914 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
ec1bb19609
commit
6b0472ee32
14 changed files with 1083 additions and 34 deletions
191
docs/research/2026-07-18-retail-shared-alpha-list-pseudocode.md
Normal file
191
docs/research/2026-07-18-retail-shared-alpha-list-pseudocode.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Retail shared alpha-list and particle ordering
|
||||
|
||||
## Scope
|
||||
|
||||
This note records the Sept. 2013 retail mechanism that orders translucent
|
||||
physics-object parts and particle parts. It is the oracle for replacing
|
||||
acdream's independent `WbDrawDispatcher` and `ParticleRenderer` transparent
|
||||
passes.
|
||||
|
||||
The motivating failure is a translucent lifestone crystal drawn before a
|
||||
later scene-particle pass. Both passes depth-test and disable depth writes, so
|
||||
a smoke or flame particle behind the crystal passes the opaque depth buffer
|
||||
and composites at full strength over the already-drawn crystal.
|
||||
|
||||
## Named-retail sources
|
||||
|
||||
- `ParticleEmitter::EmitParticle` at `0x0051D010`
|
||||
- `CPhysicsObj::AddPartToShadowCells` call at `0x0051D126`
|
||||
- `CPhysicsPart::UpdateViewerDistance` at `0x0050E030`
|
||||
- `CPartCell::add_part` at `0x0052E740`
|
||||
- `RenderDeviceD3D::DrawObjCellForDummies` at `0x005A0760`
|
||||
- `CShadowPart::insertion_sort` at `0x006B5130`
|
||||
- `CShadowPart::draw` at `0x006B50D0`
|
||||
- `CPhysicsPart::Draw` at `0x0050D7A0`
|
||||
- `D3DPolyRender::DrawMesh` at `0x0059D4A0`
|
||||
- `D3DPolyRender::AddMeshToAlphaList` at `0x0059C230`
|
||||
- `D3DPolyRender::FlushAlphaList` at `0x0059D2E0`
|
||||
- `PView::DrawCells` at `0x005A4840`
|
||||
- `SmartBox::RenderNormalMode` at `0x00453AA0`
|
||||
- `D3DPolyRender::s_AlphaDelayMask = 0xE` at `0x00820D88`
|
||||
|
||||
The matching PDB header defines:
|
||||
|
||||
```text
|
||||
CShadowPart {
|
||||
uint num_planes;
|
||||
ClipPlaneList** planes;
|
||||
Frame* frame;
|
||||
CPhysicsPart* part;
|
||||
}
|
||||
|
||||
CPhysicsPart {
|
||||
float CYpt;
|
||||
Vector3 viewer_heading;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`CYpt` is the distance from the viewer to the transformed GfxObj
|
||||
`sort_center`, not merely the object's origin.
|
||||
|
||||
## Retail pseudocode
|
||||
|
||||
### Particle materialization
|
||||
|
||||
```text
|
||||
EmitParticle(emitter):
|
||||
particlePart = initialize one particle as a CPhysicsPart
|
||||
parentPhysicsObject.AddPartToShadowCells(particlePart)
|
||||
```
|
||||
|
||||
A particle is therefore not a special post-process overlay. It participates
|
||||
in the same cell-owned part list as an object's ordinary `CPhysicsPart`s.
|
||||
|
||||
### Viewer-distance key
|
||||
|
||||
```text
|
||||
UpdateViewerDistance(part):
|
||||
localCenter = part.gfxobj_scale * part.gfxobj.sort_center
|
||||
viewerOffset = viewer.position offset to (part.position + localCenter)
|
||||
part.CYpt = length(viewerOffset)
|
||||
part.viewer_heading = normalize(viewerOffset), or +Z when almost zero
|
||||
update degradation from CYpt
|
||||
calculate final draw frame
|
||||
```
|
||||
|
||||
### Cell part ordering
|
||||
|
||||
```text
|
||||
DrawObjCellForDummies(cell):
|
||||
update all object/part viewer distances in the cell
|
||||
if cell.shadow_parts.count > 1:
|
||||
CShadowPart.insertion_sort(cell.shadow_parts)
|
||||
DrawObjCell(cell)
|
||||
|
||||
CShadowPart.insertion_sort(cell.parts):
|
||||
stable insertion sort using part.CYpt
|
||||
arrange that cell's parts in descending CYpt order
|
||||
|
||||
DrawPartCell(cell):
|
||||
for shadowPart in cell.shadow_parts:
|
||||
shadowPart.draw()
|
||||
|
||||
CShadowPart.draw(shadowPart):
|
||||
shadowPart.part.Draw(force = false)
|
||||
```
|
||||
|
||||
The sort covers ordinary object parts and particle parts together **within one
|
||||
`CPartCell`**. Equal distance retains the established list order. The later
|
||||
alpha-list append also preserves the order in which retail traverses cells;
|
||||
it does not globally re-sort entries from different cells.
|
||||
|
||||
### Deferred translucent subsets
|
||||
|
||||
```text
|
||||
CPhysicsPart.Draw(part):
|
||||
install material, surface array, object scale, and draw frame
|
||||
DrawMesh(part.gfxobj)
|
||||
|
||||
DrawMesh(mesh):
|
||||
for each surface subset in authored order:
|
||||
if subset matches AlphaDelayMask (retail default 0xE):
|
||||
AddMeshToAlphaList(mesh, subset, surface,
|
||||
captureCurrentWorldAndMaterialState)
|
||||
else:
|
||||
RenderMeshSubset immediately
|
||||
|
||||
AddMeshToAlphaList(...):
|
||||
append one entry to the selected fixed alpha list
|
||||
preserve append order
|
||||
optionally snapshot world matrix and material state
|
||||
|
||||
FlushAlphaList():
|
||||
render every queued clip-list entry in append order
|
||||
clear clip list
|
||||
render every queued alpha-list entry in append order
|
||||
clear alpha list
|
||||
```
|
||||
|
||||
`FlushAlphaList` does not invent a second distance sort. It preserves the
|
||||
order established by the cell's `CShadowPart` list and the authored part /
|
||||
surface traversal.
|
||||
|
||||
### Flush scopes
|
||||
|
||||
```text
|
||||
PView.DrawCells():
|
||||
if outside view exists:
|
||||
LScape.draw() // queues delayed alpha
|
||||
FlushAlphaList()
|
||||
conditionally clear depth
|
||||
draw indoor cell shells and object lists
|
||||
|
||||
SmartBox.RenderNormalMode():
|
||||
DrawInside(viewerCell) // PView path above
|
||||
FlushAlphaList() // final world-object alpha scope
|
||||
```
|
||||
|
||||
The landscape flush must occur before retail's outside-to-inside depth clear.
|
||||
The final flush drains delayed alpha produced after that boundary.
|
||||
|
||||
## Cross-reference findings
|
||||
|
||||
- acdream's extracted WorldBuilder mesh path correctly preserves DAT surface
|
||||
translucency metadata and a GfxObj `SortCenter`, but WorldBuilder renders
|
||||
its editor content in renderer-local passes. It has no live `CPartCell` /
|
||||
`CShadowPart` ownership and therefore cannot supply retail's mixed
|
||||
object-particle queue.
|
||||
- ACViewer is useful for DAT geometry and particle asset interpretation but is
|
||||
a viewer rather than the retail cell renderer. It does not supersede the
|
||||
named-retail ordering above.
|
||||
- ACE is authoritative for particle/gameplay events but is server-side and has
|
||||
no client alpha compositor. It corroborates asset and object identity, not
|
||||
draw ordering.
|
||||
|
||||
The named retail client remains the behavioral oracle.
|
||||
|
||||
## Port contract
|
||||
|
||||
1. Scene particles and translucent live/static GfxObj batches submit into one
|
||||
stage-scoped queue.
|
||||
2. Each submission carries the retail viewer-distance key derived from the
|
||||
transformed GfxObj sort center (particle billboards use their live center).
|
||||
3. In the modern renderer's stage-scoped reconstruction, sort far-to-near with
|
||||
stable submission sequence as the equal-distance tiebreak. This recreates
|
||||
mixed ordinary/particle part ordering, but it is scope-global rather than
|
||||
retaining retail's missing per-`CPartCell` lists. AP-34 records that narrow
|
||||
architectural residual explicitly.
|
||||
4. The queue itself does not reorder by renderer or material. Only adjacent
|
||||
compatible submissions may batch.
|
||||
5. Every flush keeps depth testing enabled and depth writes disabled.
|
||||
6. Each submission preserves its DAT blend mode, cull mode, transform,
|
||||
texture, lighting, opacity, and selection-lighting state.
|
||||
7. Flush before the outside-to-inside depth clear, then flush the final world
|
||||
scope before world-space overlays and UI.
|
||||
8. Sky-pre-scene and sky-post-scene particles remain in their dedicated sky
|
||||
passes; they do not enter the world alpha queue.
|
||||
9. A particle behind translucent crystal draws first and is then attenuated by
|
||||
the crystal. A particle in front draws after the crystal and remains bright.
|
||||
10. No lifestone-specific branch, transparent depth prepass, or particle
|
||||
depth-test suppression is permitted.
|
||||
Loading…
Add table
Add a link
Reference in a new issue