Port the retail high/low material-lighting cadence for successful world clicks, keep it instance-scoped in the modern renderer, and restore authored lighting after 0.8 seconds. Correct the selection oracle and pin timing plus per-frame buffer lifecycle with tests. Co-authored-by: Codex <codex@openai.com>
202 lines
7.4 KiB
Markdown
202 lines
7.4 KiB
Markdown
# Retail world selection and vivid target indicator
|
|
|
|
Oracle: September 2013 EoR named retail client. Cross-checks: the extracted
|
|
WorldBuilder mesh path (DAT geometry and current part transforms) and ACE object
|
|
identity/qualities. Neither reference implementation supplies the client picker;
|
|
the named retail functions below are authoritative.
|
|
|
|
## Mouse selection
|
|
|
|
`SmartBox::find_object @ 0x00451C60`
|
|
|
|
```text
|
|
remember the clicked viewport pixel
|
|
clear the previous per-frame selection accumulator
|
|
arm mouse selection for the next world draw
|
|
```
|
|
|
|
`CPhysicsPart::Draw @ 0x0050D7A0`, `RenderDeviceD3D::DrawMesh @ 0x005A0860`,
|
|
and `Render::GfxObjUnderSelectionRay @ 0x0054C740`
|
|
|
|
```text
|
|
for each physics part which reaches the normal visible-mesh draw path:
|
|
reject parts which do not belong to a server object
|
|
transform the one world selection ray into this part's local coordinates
|
|
divide by the part's GfxObj scale (do not change the ray parameter units)
|
|
|
|
sphere = gfxObj.drawing_sphere
|
|
if sphere does not intersect the local ray:
|
|
continue
|
|
|
|
if a polygon has already won globally and sphereDistance is farther:
|
|
continue
|
|
|
|
if sphereDistance is nearer than the current sphere fallback:
|
|
remember this object's id, part index, and sphereDistance
|
|
|
|
for polygons in the GfxObj's stored flat DAT order:
|
|
if polygon_hits_ray(polygon, localRay, out t):
|
|
remember only this FIRST polygon hit for this part
|
|
if t is nearer than the current global polygon winner:
|
|
remember this object's id, part index, and t
|
|
break
|
|
```
|
|
|
|
`CPolygon::polygon_hits_ray @ 0x005395E0`
|
|
|
|
```text
|
|
if polygon.sides_type == ST_SINGLE (raw value 0)
|
|
and dot(ray.direction, polygon.plane.normal) > 0:
|
|
miss // back face
|
|
|
|
denominator = dot(ray.direction, polygon.plane.normal)
|
|
if abs(denominator) < 0.0002:
|
|
miss // parallel to the plane
|
|
|
|
t = plane/ray intersection time
|
|
if t < 0:
|
|
miss // behind the viewer
|
|
|
|
point = ray.origin + t * ray.direction
|
|
normal = normalize(sum(cross(vertex[i] - vertex[0],
|
|
vertex[i+1] - vertex[0])))
|
|
for each ordered edge (previous -> current):
|
|
inward = cross(normal, current - previous)
|
|
if dot(point - previous, inward) < 0:
|
|
miss
|
|
return hit
|
|
```
|
|
|
|
The broad-phase helper is retail `CSphere::sphere_intersects_ray @
|
|
0x005377A0`: it rejects rays beginning in/on the sphere, requires squared ray
|
|
direction length at least `0.0002`, solves the quadratic in double precision,
|
|
and chooses the near root when non-negative or the far root otherwise.
|
|
The ray origin is `Render::viewpoint` (the camera eye), not the unprojected
|
|
near-plane point; `Render::pick_ray @ 0x0054B610` supplies only its direction.
|
|
|
|
`Render::GetMouseSelectionObjectID @ 0x0054C950`
|
|
|
|
```text
|
|
if any polygon hit exists anywhere in the visible draw:
|
|
return the closest polygon winner
|
|
if any drawing-sphere hit exists:
|
|
return the closest sphere fallback
|
|
return no object
|
|
```
|
|
|
|
Important consequences:
|
|
|
|
- The broad phase is each visible **GfxObj part's drawing sphere**, not a Setup
|
|
selection sphere, physics collision shape, fixed creature hit box, or expanded
|
|
screen rectangle.
|
|
- A polygon hit on any visible part beats every sphere-only fallback.
|
|
- Retail does not ray-test world-cell walls as a second, independent occluder.
|
|
Occlusion is inherited from the same portal/viewcone draw traversal that decides
|
|
which parts may call `GfxObjUnderSelectionRay`.
|
|
- `BSPTREE::GetSphere @ 0x005397E0` returns the drawing BSP root sphere exactly;
|
|
the DAT source is `GfxObj.DrawingBSP.Root.BoundingSphere`.
|
|
|
|
## Vivid target indicator
|
|
|
|
`VividTargetIndicator::SetSelected @ 0x004F5CE0`
|
|
|
|
```text
|
|
if selected id is the player: clear it
|
|
if object is player-owned or IN_CONTAINER: clear it
|
|
publish the remaining id to SmartBox
|
|
if enabled and an id remains:
|
|
color = gmRadarUI::GetBlipColor(selected id)
|
|
else:
|
|
hide both indicator roots
|
|
```
|
|
|
|
`VividTargetIndicator::Initialized @ 0x004F6C60`
|
|
|
|
```text
|
|
onscreenRoot = child 0x10000038
|
|
corners = children 0x10000039, 0x1000003A, 0x1000003B, 0x1000003C
|
|
offscreenRoot = child 0x10000045
|
|
```
|
|
|
|
The constructor resolves twelve source surfaces by calling `DBObj::GetByEnum`
|
|
with RenderSurface DBO type `0x0C`, client-enum category `0x10000009`, and
|
|
enum values `1..12`. Values `1..4` are the four on-screen corners.
|
|
`CopyImage @ 0x004F5DD0`
|
|
uses the active retail blit mode (`s_BlitMethod == 4`) to colorize the source
|
|
art with the same color as the object's radar blip.
|
|
|
|
`VividTargetIndicator::OnDraw @ 0x004F62B0` (on-screen branch)
|
|
|
|
```text
|
|
if disabled, no selected id, or selected part was not reported by this world draw:
|
|
hide indicator
|
|
else:
|
|
colorize the four DAT corner images with the selected radar-blip color
|
|
obtain the selected object's SmartBox screen rectangle
|
|
use the actual DAT corner width and height
|
|
place the corner images just outside the rectangle
|
|
clamp the assembled indicator to an 8-pixel viewport margin
|
|
show the on-screen root and hide the off-screen root
|
|
```
|
|
|
|
The indicator is persistent selection presentation. Retail also has a separate,
|
|
short-lived click confirmation owned by SmartBox; it is not implemented in
|
|
`CPhysicsPart::Draw` or `ACCWeenieObject::SetSelected`.
|
|
|
|
## SmartBox click lighting pulse
|
|
|
|
`UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`
|
|
|
|
```text
|
|
when FindObject succeeds for Select, Examine, Use, Drop, Drag, or TargetedUse:
|
|
if another object is currently pulsing:
|
|
RestoreLighting(previous object)
|
|
SetLighting(found object, luminosity=0.99, diffuse=1.0) // HIGH
|
|
flipCount = 1
|
|
nextFlip = Timer::cur_time + 0.2 seconds
|
|
|
|
continue with the reason-specific select/examine/use/drop behavior
|
|
```
|
|
|
|
`UIElement_SmartBoxWrapper::Global_Loop @ 0x004E5620`
|
|
|
|
```text
|
|
if flipCount != 0 and Timer::cur_time >= nextFlip:
|
|
flipCount += 1
|
|
if flipCount < 5:
|
|
lightingMode = ((flipCount & 1) != 0) + 1
|
|
nextFlip = Timer::cur_time + 0.2 seconds
|
|
else:
|
|
flipCount = 0
|
|
lightingMode = RESTORE
|
|
ApplyLighting(pulsing object, lightingMode)
|
|
```
|
|
|
|
`UIElement_SmartBoxWrapper::ApplyLighting @ 0x004E5320`
|
|
|
|
```text
|
|
RESTORE: CPhysicsObj::RestoreLighting(object)
|
|
LOW: CPhysicsObj::SetLighting(object, luminosity=0.0, diffuse=0.35)
|
|
HIGH: CPhysicsObj::SetLighting(object, luminosity=0.99, diffuse=1.0)
|
|
```
|
|
|
|
The complete cadence is therefore HIGH immediately, LOW at 0.2 seconds, HIGH
|
|
at 0.4, LOW at 0.6, and authored material lighting restored at 0.8. A delayed
|
|
frame advances only one step because retail schedules the next flip from the
|
|
current `Timer::cur_time`; it does not drain missed intervals.
|
|
|
|
`CPhysicsObj::SetLighting @ 0x00511A80` forwards to every physics part through
|
|
`CPartArray::SetLightingInternal @ 0x00518490`. `CPhysicsPart::SetLighting @
|
|
0x0050E400` copies the material when needed, then calls
|
|
`CMaterial::SetLuminositySimple` followed by `CMaterial::SetDiffuseSimple`.
|
|
The modern renderer's equivalent is an instance-scoped material replacement:
|
|
|
|
```text
|
|
lit = luminosity + diffuse * sceneLighting
|
|
rgb = sampledTexture.rgb * clamp(lit, 0, 1)
|
|
```
|
|
|
|
Normal/restored rendering uses `(luminosity=0, diffuse=1)` in the current object
|
|
shader. The pulse is distinct from the persistent four-corner indicator and
|
|
must restart on every successfully resolved SmartBox click, even when the click
|
|
is consumed by targeted use instead of changing the selected object.
|