start method
void
start()
Starts gyroscope tracking. Calls calibrate automatically.
Implementation
void start() {
stop();
calibrate();
_lastTimestamp = null;
_prevYawFused = null;
_prevPitchFused = null;
_yawFused = 0.0;
_pitchFused = 0.0;
_gyroEventsCount = 0;
isGyroscopeActive = true;
_smoothPitch = 0.0;
_lastAccelPitch = null;
// Detect if gyroscope is present and active within 800ms
Future.delayed(const Duration(milliseconds: 800), () {
if (_gyroEventsCount == 0) {
isGyroscopeActive = false;
}
});
if (!useIsolate) {
// Main-thread Direct Low-Latency Fusion
_accelSubscription =
(accelerometerStreamOverride ??
accelerometerEventStream(
samplingPeriod: SensorInterval.fastestInterval,
))
.listen((event) {
_accelX = event.x;
_accelZ = event.z;
if (!isGyroscopeActive) {
_updateFromAccelerometerOnly(event.x, event.y, event.z);
}
});
_subscription =
(gyroscopeStreamOverride ??
gyroscopeEventStream(
samplingPeriod: SensorInterval.fastestInterval,
))
.listen((event) {
_gyroEventsCount++;
isGyroscopeActive = true;
if (_calibrating) {
_calibrationSumX += event.x;
_calibrationSumY += event.y;
_calibrationSamples++;
return;
}
// Dynamic Auto-Calibrating Anti-Drift:
// If the gyroscope velocity is extremely low, adaptively adjust the offsets
final double magnitude = sqrt(
event.x * event.x + event.y * event.y,
);
if (magnitude < 0.015) {
_offsetX = _offsetX * 0.995 + event.x * 0.005;
_offsetY = _offsetY * 0.995 + event.y * 0.005;
}
final adjustedX = event.x - _offsetX;
final adjustedY = event.y - _offsetY;
// Gravity reference for pitch channel in Landscape Left:
// Horizon (0°): accelX ≈ +9.8, accelZ ≈ 0 -> atan2(0, 9.8) = 0.0
// Look up (ceiling): atan2(+9.8, 0) = +1.57 rad
// Look down (feet): atan2(-9.8, 0) = -1.57 rad
double gravityPitch() {
final ax = _accelX.abs() < 0.01 ? 0.01 : _accelX;
return atan2(_accelZ, ax);
}
final now = DateTime.now();
if (_lastTimestamp == null) {
_lastTimestamp = now;
_yawFused = 0.0;
_pitchFused = gravityPitch();
_prevYawFused = _yawFused;
_prevPitchFused = _pitchFused;
return;
}
final dt =
now.difference(_lastTimestamp!).inMicroseconds / 1000000.0;
_lastTimestamp = now;
// Yaw in Landscape Left: turning head LEFT produces adjustedX > 0,
// so +adjustedX increases camera yaw (looking left).
_yawFused += adjustedX * dt;
// Pitch in Landscape Left: tilting head UP increases camera pitch (looking up).
_pitchFused =
_alpha * (_pitchFused + adjustedY * dt) +
(1 - _alpha) * gravityPitch();
final predictionTime = predictionMs / 1000.0;
final predictedYaw = _yawFused + adjustedX * predictionTime;
final predictedPitch = _pitchFused + adjustedY * predictionTime;
final dYaw = predictedYaw - (_prevYawFused ?? predictedYaw);
final dPitch =
predictedPitch - (_prevPitchFused ?? predictedPitch);
_prevYawFused = predictedYaw;
_prevPitchFused = predictedPitch;
if (jitterDamping) {
// Deadband for tiny sensor vibrations
final rawDYaw = dYaw.abs() < 0.00015 ? 0.0 : dYaw;
final rawDPitch = dPitch.abs() < 0.00015 ? 0.0 : dPitch;
// Fast exponential filter
_dampedDYaw = _dampedDYaw * 0.15 + rawDYaw * 0.85;
_dampedDPitch = _dampedDPitch * 0.15 + rawDPitch * 0.85;
target.rotate(
_dampedDYaw * sensitivity,
_dampedDPitch * sensitivity * pitchGain,
);
} else {
target.rotate(
dYaw * sensitivity,
dPitch * sensitivity * pitchGain,
);
}
});
return;
}
// Native Platform: Background Isolate Setup
final fusionIsolate = BackgroundIsolate.create();
_fusionIsolate = fusionIsolate;
fusionIsolate.messages.listen((message) {
if (message is List) {
final dYaw = (message[0] as num).toDouble();
final dPitch = (message[1] as num).toDouble();
target.rotate(dYaw, dPitch);
} else {
// First message: the worker's SendPort (two-way channel)
_isolateSendPort = message;
_isolateSendPort.send([
0,
_alpha,
sensitivity,
predictionMs,
_offsetX,
_offsetY,
]);
}
});
fusionIsolate.start(headTrackingFusionEntry);
_accelSubscription =
(accelerometerStreamOverride ??
accelerometerEventStream(
samplingPeriod: SensorInterval.fastestInterval,
))
.listen((event) {
_isolateSendPort?.send([1, event.x, event.y, event.z]);
if (!isGyroscopeActive) {
_updateFromAccelerometerOnly(event.x, event.y, event.z);
}
});
_subscription =
(gyroscopeStreamOverride ??
gyroscopeEventStream(
samplingPeriod: SensorInterval.fastestInterval,
))
.listen((event) {
_gyroEventsCount++;
isGyroscopeActive = true;
if (_calibrating) {
_calibrationSumX += event.x;
_calibrationSumY += event.y;
_calibrationSamples++;
return;
}
_isolateSendPort?.send([2, event.x, event.y, event.z]);
});
}