Free-look and walking: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: freelook (line 45), walk (line 109).
1/// Turning the head from where the camera stands, and walking it there, as
2/// opposed to orbiting a point somebody else chose. The posts are laid out
3/// again around the camera every frame, so there is always something to walk
4/// past.
5///
6/// Quoted by `free_look.md` and shown whole in the Source tab.
7library;
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13final class FreeLookDemo extends ShowcaseDemo {
14 bool walkingForward = true;
15 double turn = 0.3;
17 late final FreeLook _freeLook;
18 late Vector3 _targetBefore;
19 double _stepSeconds = 0.0;
21 late final MeshNode _floor;
22 final List<MeshNode> _posts = <MeshNode>[];
23 final List<Material> _stones = <Material>[];
25 /// Posts a side of the square laid out around the camera, and how far
26 /// apart they stand.
27 static const int _reach = 4;
28 static const double _spacing = 4.0;
30 /// Where the eyes are, in metres. Walking goes along the view axis, so a
31 /// camera tilted down walks down: level, and at this height, it walks along
32 /// the ground.
33 static const double _eyeHeight = 1.6;
36 void configureView(DemoContext context) {
37 context.orbit
38 ..distance = 4.0
39 ..pitch = 0.0;
40 context.orbit.target.y = _eyeHeight;
41 }
44 Scene build(DemoContext context) {
45 _freeLook = FreeLook(context.orbit);
47 Material stone(String name, double r, double g, double b) =>
48 Material(name: name, baseColor: Vector4(r, g, b, 1.0), roughness: 0.8);
49 _stones.addAll(<Material>[
50 stone('grey', 0.65, 0.62, 0.58),
51 stone('sand', 0.85, 0.75, 0.5),
52 stone('clay', 0.8, 0.45, 0.35),
53 stone('moss', 0.45, 0.7, 0.45),
54 ]);
56 final Scene scene = Scene()
57 ..ambientColor = Vector3(0.5, 0.55, 0.65)
58 ..ambientIntensity = 0.3;
59 _floor = MeshNode(
60 DeviceMesh.upload(
61 context.device,
62 const PlaneShape(width: 80.0, depth: 80.0).build(),
63 ),
64 stone('floor', 0.3, 0.34, 0.32),
65 name: 'floor',
66 );
67 scene.add(_floor);
69 final DeviceMesh post = DeviceMesh.upload(
70 context.device,
71 CuboidShape(size: Vector3(0.5, 2.5, 0.5)).build(),
72 );
73 for (var i = 0; i < (2 * _reach + 1) * (2 * _reach + 1); i++) {
74 final MeshNode node = MeshNode(post, _stones.first, name: 'post $i');
75 _posts.add(node);
76 scene.add(node);
77 }
78 scene.add(
79 LightNode(name: 'sun', intensity: 3.0)
80 ..setLocalForward(Vector3(-0.3, -1.0, -0.4)),
81 );
82 _layOut(context);
83 return scene;
84 }
86 /// Puts every post on the lattice point nearest the camera and beyond, and
87 /// colours it by which point that is, so a post keeps its colour as the
88 /// camera passes it and a new one arrives ahead.
89 void _layOut(DemoContext context) {
90 final Vector3 eye = context.camera.readPosition();
91 final int cellX = (eye.x / _spacing).round();
92 final int cellZ = (eye.z / _spacing).round();
93 _floor.setPosition(cellX * _spacing, 0.0, cellZ * _spacing);
94 var index = 0;
95 for (var i = -_reach; i <= _reach; i++) {
96 for (var j = -_reach; j <= _reach; j++) {
97 final int x = cellX + i;
98 final int z = cellZ + j;
99 final MeshNode node = _posts[index++];
100 node.setPosition(x * _spacing, 1.25, z * _spacing);
101 node.material = _stones[(x * 7 + z * 13).abs() % _stones.length];
102 // One the camera is standing inside would fill the frame.
103 node.visible =
104 (node.readPosition() - Vector3(eye.x, 1.25, eye.z)).length > 1.2;
105 }
106 }
107 }
110 void update(DemoContext context, double dt) {
111 // Back to eye height first: a drag that tilted the view would otherwise
112 // have the walk climb or sink, and the camera would leave the floor.
113 context.orbit.target.y = _eyeHeight;
114 // The head turns with the eye held still; the walk then carries the eye
115 // the way the head now faces.
116 _freeLook.look(turn * 75.0 * dt, 0.0);
117 _targetBefore = context.orbit.target.clone();
118 _stepSeconds = dt;
119 if (walkingForward) {
120 _freeLook.walk(forward: 1.0, seconds: dt);
121 }
122 _layOut(context);
123 }
126 List<DemoControl> controls(DemoContext context) => <DemoControl>[
127 ToggleControl(
128 'Walk forward',
129 value: () => walkingForward,
130 onChanged: (bool v) => walkingForward = v,
131 ),
132 SliderControl(
133 'Speed',
134 min: 0,
135 max: 8,
136 value: () => _freeLook.metresPerSecond,
137 onChanged: (double v) => _freeLook.metresPerSecond = v,
138 format: (double v) => '${v.toStringAsFixed(1)} m/s',
139 ),
140 SliderControl(
141 'Turn the head',
142 min: -1,
143 max: 1,
144 value: () => turn,
145 onChanged: (double v) => turn = v,
146 format: (double v) => v.toStringAsFixed(2),
147 ),
148 ];
151 void verify(Scene scene, FrameResult frame) {
152 if (frame.drawCalls < 1) {
153 throw StateError('the posts were not drawn');
154 }
155 if (!walkingForward) return;
156 // walk moves the target by metresPerSecond * seconds along the view
157 // axis. The distance actually covered has to match that number exactly,
158 // rather than only be nonzero.
159 final double moved = (_freeLook.orbit.target - _targetBefore).length;
160 final double expected = _freeLook.metresPerSecond * _stepSeconds;
161 if ((moved - expected).abs() > 1e-6) {
162 throw StateError('walking moved $moved, expected $expected');
163 }
164 final int standing = _posts.where((MeshNode p) => p.visible).length;
165 if (standing < _posts.length - 2) {
166 throw StateError('the camera should be standing among the posts');
167 }
168 }
169}