flutter3d_stereo 0.7.1 copy "flutter3d_stereo: ^0.7.1" to clipboard
flutter3d_stereo: ^0.7.1 copied to clipboard

PlatformAndroid

Stereo rendering for flutter3d: a rig of two eyes under a head, the widget that draws the pair into one frame, and the settings a pair can actually have.

example/lib/main.dart

/// A room in stereo, pointed by the device's own rotation sensor.
///
///     flutter run -d <a phone>
///
/// **What this is for is the half of a headset that a phone already has.** Two
/// eyes a fixed distance apart, two views into one frame, the settings a pair
/// can actually have, and a head that moves when you move — all of that is the
/// same on a phone held up to the face as it is in a headset, and all of it can
/// be got wrong long before there is a runtime to blame. What a phone cannot
/// show is the rest: a compositor's predicted pose, seventy-two hertz, and a
/// swapchain owned by somebody else.
///
/// So this example is deliberately not a demo of VR. It is the acceptance tool
/// for `StereoRig`, `StereoSurface` and `OffAxisProjection` on real hardware.
library;

import 'dart:math' as math;

import 'package:flutter/material.dart' hide Material;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter3d/flutter3d.dart' hide Material;
import 'package:flutter3d/flutter3d.dart' as engine show Material;
import 'package:flutter3d_app/flutter3d_app.dart';
import 'package:flutter3d_cpu/flutter3d_cpu.dart';
import 'package:flutter3d_stereo/flutter3d_stereo.dart';
import 'package:vector_math/vector_math.dart' hide Colors;

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  // Landscape and nothing else on screen: a phone in a holder has no room for
  // a status bar, and a picture that reflows halfway through a turn of the head
  // is worse than one that never rotates.
  unawaitedChrome();
  runApp(const StereoExampleApp());
}

void unawaitedChrome() {
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
  SystemChrome.setPreferredOrientations(<DeviceOrientation>[
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.landscapeRight,
  ]);
}

class StereoExampleApp extends StatelessWidget {
  const StereoExampleApp({super.key});

  @override
  Widget build(BuildContext context) => const MaterialApp(
    title: 'flutter3d in stereo',
    debugShowCheckedModeBanner: false,
    home: StereoScreen(),
  );
}

class StereoScreen extends StatefulWidget {
  const StereoScreen({super.key});

  @override
  State<StereoScreen> createState() => _StereoScreenState();
}

class _StereoScreenState extends State<StereoScreen>
    with SingleTickerProviderStateMixin {
  final StereoRig _rig = StereoRig();
  final SensorHeadTracker _tracker = SensorHeadTracker();
  Scene? _scene;
  Renderer? _renderer;
  Object? _error;

  @override
  void initState() {
    super.initState();
    _open();
  }

  Future<void> _open() async {
    try {
      // The size is what the software fallback would draw at; the hardware
      // backends size themselves to the surface and ignore it.
      // `--dart-define=software=true` draws in Dart instead, at a size a
      // rasteriser without a GPU can keep up with. It is for a device whose
      // driver will not run the engine — the picture is what is being checked,
      // not the frame rate.
      final device = const bool.fromEnvironment('software')
          ? CpuDevice(
              width: 400,
              height: 200,
              shaders: CpuShaderLibrary(builtinCpuShaders()),
            )
          : await openDevice(width: 1920, height: 1080);
      if (!mounted) return;
      final scene = _room(device);
      scene.add(_rig.stage);
      // Standing height, so the floor is where a floor is rather than at the
      // eyes. The rig's stage is the only thing an application moves.
      _rig.stage.setPosition(0.0, 1.6, 0.0);
      setState(() {
        _renderer = Renderer.create(device: device);
        _scene = scene;
      });
      await _tracker.start();
      // **A ticker rather than the sensor drives the frame.** The sensor
      // arrives about fifty times a second whatever the renderer can manage,
      // and a rebuild per reading on the software rasteriser is a queue of
      // frames the platform thread never gets out of — which Android reports
      // as an application not responding. One frame at a time, each reading
      // the latest pose, is the honest arrangement on any backend.
      _ticker = createTicker(_onTick)..start();
    } catch (error) {
      if (mounted) setState(() => _error = error);
    }
  }

  Ticker? _ticker;
  final Stopwatch _clock = Stopwatch();
  int _frames = 0;

  void _onTick(Duration _) {
    if (!mounted) return;
    if (!_clock.isRunning) _clock.start();
    _frames++;
    if (_clock.elapsedMilliseconds >= 2000) {
      debugPrint(
        '[stereo] ${(_frames * 1000 / _clock.elapsedMilliseconds).toStringAsFixed(1)} fps '
        '(${(_clock.elapsedMilliseconds / _frames).toStringAsFixed(1)} ms a '
        'frame)',
      );
      _frames = 0;
      _clock
        ..reset()
        ..start();
    }
    setState(() {});
  }

  @override
  void dispose() {
    _ticker?.dispose();
    _tracker.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final error = _error;
    if (error != null) {
      return Scaffold(
        backgroundColor: Colors.black,
        body: Center(
          child: Text(
            'No device: $error',
            style: const TextStyle(color: Colors.white),
          ),
        ),
      );
    }
    final renderer = _renderer;
    final scene = _scene;
    if (renderer == null || scene == null) {
      return const Scaffold(
        backgroundColor: Colors.black,
        body: Center(child: CircularProgressIndicator()),
      );
    }
    // A one-eyed variant, for telling "the stereo pair is the problem" from
    // "this device and this engine are the problem":
    //     flutter run --dart-define=mono=true
    if (const bool.fromEnvironment('mono')) {
      return Scaffold(
        backgroundColor: Colors.black,
        body: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            final dpr = MediaQuery.devicePixelRatioOf(context);
            _rig.applyHead(_tracker.pose.value);
            final width = (constraints.maxWidth * dpr).round().clamp(2, 8192);
            final height = (constraints.maxHeight * dpr).round().clamp(1, 8192);
            _rig.fitToViewport(width: width * 2, height: height);
            final frame = renderer.render(
              width: width,
              height: height,
              scene: scene,
              views: <RenderView>[RenderView(camera: _rig.camera(Eye.left))],
              settings: const RenderSettings(exposure: 1.2).forStereo(),
            );
            return presentFrame(renderer.device, frame.frame);
          },
        ),
      );
    }

    return Scaffold(
      backgroundColor: Colors.black,
      body: StereoSurface(
        renderer: renderer,
        scene: scene,
        rig: _rig,
        onBeforeFrame: () => _rig.applyHead(_tracker.pose.value),
        settings: () => const RenderSettings(exposure: 1.2),
      ),
    );
  }
}

/// A room with a floor, four walls' worth of pillars, and something to look up
/// at — enough for parallax to have work to do at several distances.
Scene _room(GraphicsDevice device) {
  final scene = Scene();

  final tile = DeviceMesh.upload(
    device,
    CuboidShape(size: Vector3(0.98, 0.08, 0.98)).build(),
  );
  final pillar = DeviceMesh.upload(
    device,
    CuboidShape(size: Vector3(0.35, 3.0, 0.35)).build(),
  );
  final crate = DeviceMesh.upload(
    device,
    CuboidShape(size: Vector3(0.6, 0.6, 0.6)).build(),
  );

  // A checkerboard rather than one big quad: the seams are what the eye reads
  // distance from when there is no other texture.
  for (var x = -4; x <= 4; x++) {
    for (var z = -4; z <= 4; z++) {
      final light = (x + z).isEven;
      scene.add(
        MeshNode(
          tile,
          engine.Material(
            name: 'tile',
            baseColor: light
                ? Vector4(0.42, 0.44, 0.50, 1.0)
                : Vector4(0.24, 0.26, 0.32, 1.0),
            lighting: LightingModel.unlit,
          ),
          name: 'tile.$x.$z',
        )..setPosition(x.toDouble(), 0.0, z.toDouble()),
      );
    }
  }

  const colours = <(double, double, double)>[
    (0.92, 0.35, 0.28),
    (0.35, 0.78, 0.45),
    (0.36, 0.55, 0.95),
    (0.95, 0.78, 0.30),
  ];
  for (var i = 0; i < 4; i++) {
    final angle = i * math.pi / 2.0;
    final colour = colours[i % colours.length];
    scene.add(
      MeshNode(
        pillar,
        engine.Material(
          name: 'pillar',
          baseColor: Vector4(colour.$1, colour.$2, colour.$3, 1.0),
          lighting: LightingModel.unlit,
        ),
        name: 'pillar.$i',
      )..setPosition(math.sin(angle) * 4.5, 1.5, math.cos(angle) * 4.5),
    );
  }

  // Near, so that the difference between the two eyes is obvious: at half a
  // metre the parallax is a good fraction of the frame.
  for (var i = 0; i < 2; i++) {
    scene.add(
      MeshNode(
        crate,
        engine.Material(
          name: 'crate',
          baseColor: Vector4(0.85, 0.85, 0.88, 1.0),
          lighting: LightingModel.unlit,
        ),
        name: 'crate.$i',
      )..setPosition(-0.6 + i * 0.6, 0.45 + i * 0.1, -0.9 - i * 0.7),
    );
  }

  return scene;
}
0
likes
160
points
422
downloads

Documentation

API reference

Publisher

verified publisherpleion.dev

Weekly Downloads

Stereo rendering for flutter3d: a rig of two eyes under a head, the widget that draws the pair into one frame, and the settings a pair can actually have.

Homepage
Repository (GitHub)
View/report issues

Topics

#vr #xr #stereo #game-development

License

MIT (license)

Dependencies

flutter, flutter3d, flutter3d_app, flutter3d_sim, vector_math

More

Packages that depend on flutter3d_stereo

Packages that implement flutter3d_stereo