Head tracking: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: tracker (line 24), apply (line 95), turn (line 117).
1/// Where the head is, fed to a stereo rig every frame.
2///
3/// **`SensorHeadTracker`, the real implementation, reads a phone's rotation
4/// sensor through a platform channel** — nothing this test environment or a
5/// desktop browser has. This page implements the same `HeadTracker`
6/// interface by hand, driven by the pointer instead of a sensor, which is
7/// the honest stand-in: the rig on the other end cannot tell the difference,
8/// because all either one ever hands it is a `HeadPose`.
9///
10/// Quoted by `head_tracking.md` and shown whole in the Source tab.
11library;
13import 'dart:math' as math;
15import 'package:flutter/foundation.dart';
16import 'package:flutter/widgets.dart';
17import 'package:flutter3d/flutter3d.dart' hide Material;
18import 'package:flutter3d/flutter3d.dart' as f3d show Material;
19import 'package:flutter3d_showcase/src/demo/demo.dart';
20import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
21import 'package:flutter3d_stereo/flutter3d_stereo.dart';
22import 'package:vector_math/vector_math.dart';
24/// Turns pointer drags into a head pose, the same shape a sensor would.
25final class _PointerHeadTracker implements HeadTracker {
26 final ValueNotifier<HeadPose> _pose = ValueNotifier<HeadPose>(
27 HeadPose.still(),
28 );
29 double _yaw = 0.0;
32 ValueListenable<HeadPose> get pose => _pose;
34 void dragBy(double dx) {
35 _yaw -= dx * 0.01;
36 _pose.value = HeadPose(
37 rotation: Quaternion.axisAngle(Vector3(0.0, 1.0, 0.0), _yaw),
38 position: Vector3.zero(),
39 );
40 }
43 Future<void> start() async {}
46 Future<void> stop() async {}
47}
49final class HeadTrackingDemo extends ShowcaseDemo {
50 final _PointerHeadTracker _tracker = _PointerHeadTracker();
51 final StereoRig _rig = StereoRig();
52 late Scene _scene;
55 Scene build(DemoContext context) {
56 final material = f3d.Material(
57 name: 'ball',
58 baseColor: Vector4(0.8, 0.5, 0.3, 1.0),
59 );
60 final ball = MeshNode(
61 DeviceMesh.upload(context.device, SphereShape(segments: 24).build()),
62 material,
63 )..setPosition(0, 0, -2);
64 // A ring of posts round the viewer, each its own colour, so that turning
65 // the head visibly sweeps across something.
66 final List<SceneNode> posts = <SceneNode>[
67 for (var i = 0; i < 8; i++)
68 blockNode(
69 context,
70 'post $i',
71 Vector3(0.5, 1.0 + 0.4 * (i % 3), 0.5),
72 Vector4(
73 0.5 + 0.45 * math.sin(i * 0.8),
74 0.5 + 0.35 * math.sin(i * 1.6 + 1.0),
75 0.45 + 0.4 * math.sin(i * 2.4 + 2.0),
76 1.0,
77 ),
78 at: Vector3(
79 4.0 * math.sin(i * math.pi / 4),
80 (1.0 + 0.4 * (i % 3)) / 2 - 1.6,
81 -4.0 * math.cos(i * math.pi / 4),
82 ),
83 ),
84 ];
85 _scene = sceneOf(<SceneNode>[
86 _rig.stage,
87 ball,
88 floorNode(context, width: 14.0, depth: 14.0)
89 ..setPosition(0.0, -1.65, 0.0),
90 ...posts,
91 ]);
92 return _scene;
93 }
95 void _applyHead() => _rig.applyHead(_tracker.pose.value);
98 Widget? customBody(BuildContext buildContext, DemoContext context) {
99 _applyHead();
100 return GestureDetector(
101 onHorizontalDragUpdate: (DragUpdateDetails details) {
102 _tracker.dragBy(details.delta.dx);
103 _applyHead();
104 },
105 child: StereoSurface(
106 renderer: context.renderer,
107 scene: _scene,
108 rig: _rig,
109 settings: () => const RenderSettings().forStereo(),
110 onBeforeFrame: _applyHead,
111 ),
112 );
113 }
116 void verify(Scene scene, FrameResult frame) {
117 final before = _rig.gaze();
118 _tracker.dragBy(200.0);
119 _applyHead();
120 final after = _rig.gaze();
121 if ((before - after).length < 0.1) {
122 throw StateError(
123 'dragging should turn the head, and turning the head '
124 'should turn where the rig looks',
125 );
126 }
127 }
128}