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>
191 lines
6.8 KiB
Markdown
191 lines
6.8 KiB
Markdown
# 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.
|