User Story #64 » 0002-fix-ui-sample-the-interpolation-at-the-wall-advanced.patch
| androidApp/src/main/java/com/aether/mofe/viewmodel/HudViewModel.kt | ||
|---|---|---|
|
import com.aether.mofe.data.*
|
||
|
import com.aether.mofe.engine.netcode.MeshAimTracker
|
||
|
import com.aether.mofe.engine.netcode.NetcodeSession
|
||
|
import com.aether.mofe.engine.netcode.RenderClock
|
||
|
import com.aether.mofe.engine.netcode.toTemporalPose
|
||
|
import com.aether.mofe.model.DeviceId
|
||
|
import com.aether.mofe.model.Timestamp
|
||
| ... | ... | |
|
// the reticle from entity-interpolated poses, so the aim reads self-consistent regardless of
|
||
|
// absolute-heading accuracy. Local single-observer ⇒ the mesh clock is identity, so each
|
||
|
// FusedState's own timestamp is its mesh time.
|
||
|
private val netcode = NetcodeSession(baseInterpDelayMicros = 60_000L)
|
||
|
// Interp delay ≥ one ~10 Hz solve interval (was 60 ms — BELOW one interval, so even a correctly
|
||
|
// advancing query clock couldn't straddle two samples). 120 ms keeps render-time between the two
|
||
|
// newest samples with ~20 ms of slack for solve jitter, at ~20 ms extra visual latency.
|
||
|
private val netcode = NetcodeSession(baseInterpDelayMicros = 120_000L)
|
||
|
private val aim = MeshAimTracker(netcode)
|
||
|
@Volatile private var latestMeshMicros: Long = 0L
|
||
|
// Monotonic wall-time (µs) captured when the newest fused sample was recorded. Paired with
|
||
|
// [latestMeshMicros] so [smoothQueryMicros] can advance the interpolation query clock by REAL
|
||
|
// elapsed time between the ~10 Hz solves — otherwise the query freezes between solves and the 60 Hz
|
||
|
// view re-reads one point ~6× then jumps (nodes look as jumpy as the raw solve). See RenderClock.
|
||
|
@Volatile private var lastSampleWallMicros: Long = 0L
|
||
|
private fun nowWallMicros(): Long = System.nanoTime() / 1_000L
|
||
|
/** Mesh-time to sample the interpolation at NOW, advanced smoothly off the wall clock (F1). */
|
||
|
private fun smoothQueryMicros(): Long =
|
||
|
RenderClock.queryMicros(latestMeshMicros, lastSampleWallMicros, nowWallMicros())
|
||
|
init {
|
||
|
viewModelScope.launch {
|
||
| ... | ... | |
|
// Feed the aim tracker from the engine's fused world poses (~10 Hz solve).
|
||
|
viewModelScope.launch {
|
||
|
app.mofeEngineHost.fusedStates.collect { states ->
|
||
|
val arrivedWall = nowWallMicros() // one wall stamp for this ~10 Hz solve batch
|
||
|
for ((id, state) in states) {
|
||
|
netcode.recordSample(DeviceId(id), state.timestamp, state.toTemporalPose())
|
||
|
val us = state.timestamp.microseconds
|
||
|
if (us > latestMeshMicros) latestMeshMicros = us
|
||
|
if (us > latestMeshMicros) { latestMeshMicros = us; lastSampleWallMicros = arrivedWall }
|
||
|
}
|
||
|
}
|
||
|
}
|
||
| ... | ... | |
|
* mirroring [latestFacing]; empty until a self pose + facing exist. Observer position is the
|
||
|
* engine's WORLD-frame self pose (the same frame as the recorded fused poses), so a track's
|
||
|
* (0,0) reticle offset coincides with the crosshair by construction. [coneRadians] is the
|
||
|
* on-reticle "locked" half-angle. Queried at the freshest fused mesh time, so renderPose
|
||
|
* interpolates one interp-delay behind the latest solve.
|
||
|
* on-reticle "locked" half-angle. Queried at [smoothQueryMicros] (wall-advanced, F1), so the
|
||
|
* markers interpolate smoothly between the ~10 Hz solves instead of stepping with them.
|
||
|
*/
|
||
|
fun aimTracks(coneRadians: Double): List<MeshAimTracker.Track> {
|
||
|
val observer = app.mofeEngineHost.latestSelfPose()?.position ?: return emptyList()
|
||
|
val facing = latestFacing() ?: return emptyList()
|
||
|
val up = latestTopEdge() ?: Vector3D(0.0, 1.0, 0.0)
|
||
|
return aim.tracks(observer, facing, up, Timestamp(latestMeshMicros), coneRadians)
|
||
|
return aim.tracks(observer, facing, up, Timestamp(smoothQueryMicros()), coneRadians)
|
||
|
}
|
||
|
/**
|
||
| ... | ... | |
|
*/
|
||
|
fun renderNodePositions(): Map<String, Vector3D> {
|
||
|
val selfWorld = app.mofeEngineHost.latestSelfPose()?.position ?: return emptyMap()
|
||
|
return netcode.renderPositions(Timestamp(latestMeshMicros))
|
||
|
return netcode.renderPositions(Timestamp(smoothQueryMicros()))
|
||
|
.entries.associate { (id, p) -> id.value to (p - selfWorld) }
|
||
|
}
|
||
- « Previous
- 1
- 2
- Next »