# 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. The modern billboard vertex-instance record carries a bindless texture handle, so adjacent particles with different textures are still one compatible ordered draw; blend mode is the only billboard state boundary. 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.