flutter3d
Showcase Changelog 38 packages API reference

Off-axis frustum: the source

since 0.7.0 Views, picking and input

Regions the guide quotes: symmetric (line 32), skew (line 37).

1/// A frustum whose axis is not down the middle of the view: what a headset
2/// lens or a portal window needs instead of an ordinary field of view.
3///
4/// Quoted by `off_axis_projection.md` and shown whole in the Source tab.
5library;
6
7import 'package:flutter3d/flutter3d.dart';
8import 'package:flutter3d_showcase/src/demo/demo.dart';
9import 'package:vector_math/vector_math.dart';
11final class OffAxisProjectionDemo extends ShowcaseDemo {
12 double shift = 0.5;
14 late OffAxisProjection _off;
16 @override
17 Scene build(DemoContext context) {
18 final Material stone = Material(
19 name: 'stone',
20 baseColor: Vector4(0.68, 0.72, 0.78, 1.0),
21 roughness: 0.7,
22 );
23 final MeshNode ball = MeshNode(
24 DeviceMesh.upload(
25 context.device,
26 SphereShape(segments: 32, rings: 16).build(),
27 ),
28 stone,
29 name: 'ball',
30 );
32 final OffAxisProjection symmetric = OffAxisProjection.symmetric(
33 fovYRadians: 0.9,
34 aspect: 16 / 9,
35 );
37 _off = symmetric.copyWith(
38 tanLeft: symmetric.tanLeft - shift,
39 tanRight: symmetric.tanRight - shift,
40 );
41 context.camera.projection = _off;
43 return Scene()
44 ..add(ball)
45 ..add(
46 LightNode(name: 'sun', intensity: 3.0)
47 ..setLocalForward(Vector3(-0.3, -1.0, -0.4)),
48 );
49 }
51 @override
52 void update(DemoContext context, double dt) {
53 final OffAxisProjection symmetric = OffAxisProjection.symmetric(
54 fovYRadians: 0.9,
55 aspect: 16 / 9,
56 );
57 _off = symmetric.copyWith(
58 tanLeft: symmetric.tanLeft - shift,
59 tanRight: symmetric.tanRight - shift,
60 );
61 context.camera.projection = _off;
62 }
64 @override
65 List<DemoControl> controls(DemoContext context) => <DemoControl>[
66 SliderControl(
67 'Shift',
68 min: -0.6,
69 max: 0.6,
70 value: () => shift,
71 onChanged: (double v) => shift = v,
72 ),
73 ];
75 @override
76 void verify(Scene scene, FrameResult frame) {
77 if (frame.drawCalls < 1) {
78 throw StateError('the ball was not drawn');
79 }
80 // The off-centre term in column 2 of the matrix is what shears the
81 // frustum. It is zero only when the frustum is symmetric: away from
82 // shift == 0 it must be non-zero, or the shift did nothing.
83 final Matrix4 matrix = _off.toMatrix(16 / 9);
84 final double offCentre = matrix.storage[8];
85 if (shift != 0.0 && offCentre == 0.0) {
86 throw StateError('a shifted frustum should not be symmetric');
87 }
88 }