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>
6.8 KiB
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::EmitParticleat0x0051D010CPhysicsObj::AddPartToShadowCellscall at0x0051D126CPhysicsPart::UpdateViewerDistanceat0x0050E030CPartCell::add_partat0x0052E740RenderDeviceD3D::DrawObjCellForDummiesat0x005A0760CShadowPart::insertion_sortat0x006B5130CShadowPart::drawat0x006B50D0CPhysicsPart::Drawat0x0050D7A0D3DPolyRender::DrawMeshat0x0059D4A0D3DPolyRender::AddMeshToAlphaListat0x0059C230D3DPolyRender::FlushAlphaListat0x0059D2E0PView::DrawCellsat0x005A4840SmartBox::RenderNormalModeat0x00453AA0D3DPolyRender::s_AlphaDelayMask = 0xEat0x00820D88
The matching PDB header defines:
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
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 CPhysicsParts.
Viewer-distance key
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
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
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
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 liveCPartCell/CShadowPartownership 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
- Scene particles and translucent live/static GfxObj batches submit into one stage-scoped queue.
- Each submission carries the retail viewer-distance key derived from the transformed GfxObj sort center (particle billboards use their live center).
- 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-
CPartCelllists. AP-34 records that narrow architectural residual explicitly. - The queue itself does not reorder by renderer or material. Only adjacent compatible submissions may batch.
- Every flush keeps depth testing enabled and depth writes disabled.
- Each submission preserves its DAT blend mode, cull mode, transform, texture, lighting, opacity, and selection-lighting state.
- Flush before the outside-to-inside depth clear, then flush the final world scope before world-space overlays and UI.
- Sky-pre-scene and sky-post-scene particles remain in their dedicated sky passes; they do not enter the world alpha queue.
- 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.
- No lifestone-specific branch, transparent depth prepass, or particle depth-test suppression is permitted.