add (very rough) gizmo, restructure Dart package into library, add EntityListWidget

This commit is contained in:
Nick Fisher
2024-03-25 22:21:37 +08:00
parent 66ae0a4d08
commit 357c585d44
53 changed files with 2913 additions and 568 deletions

View File

@@ -0,0 +1,155 @@
import 'package:flutter_filament/filament/animations/animation_data.dart';
import 'package:vector_math/vector_math.dart';
class AnimationBuilder {
// BoneAnimationData? BoneAnimationData;
double _frameLengthInMs = 0;
double _duration = 0;
double? _interpMorphStart;
double? _interpMorphEnd;
double? _interpMorphStartValue;
double? _interpMorphEndValue;
// List<BoneAnimationData>? _BoneAnimationDatas = null;
String meshName;
late List<String> availableMorphs;
late List<int> _morphTargets;
AnimationBuilder(
{required this.availableMorphs,
required this.meshName,
required int framerate}) {
_frameLengthInMs = 1000 / framerate;
}
MorphAnimationData build() {
if (availableMorphs.isEmpty || _duration == 0 || _frameLengthInMs == 0) {
throw Exception();
}
int numFrames = _duration * 1000 ~/ _frameLengthInMs;
final morphData =
List<double>.filled((numFrames * _morphTargets.length).toInt(), 0.0);
var frameStart = (_interpMorphStart! * 1000) ~/ _frameLengthInMs;
var frameEnd = (_interpMorphEnd! * 1000) ~/ _frameLengthInMs;
for (int i = frameStart; i < frameEnd; i++) {
var linear = (i - frameStart) / frameEnd;
var val = ((1 - linear) * _interpMorphStartValue!) +
(linear * _interpMorphEndValue!);
for (int j = 0; j < _morphTargets.length; j++) {
morphData[(i * _morphTargets.length) + j] = val;
}
}
return MorphAnimationData(
[meshName],
morphData,
_morphTargets.map((i) => availableMorphs[i]).toList(),
_frameLengthInMs);
}
AnimationBuilder setDuration(double secs) {
_duration = secs;
return this;
}
AnimationBuilder setMorphTargets(List<String> names) {
_morphTargets = names.map((name) => availableMorphs.indexOf(name)).toList();
return this;
}
AnimationBuilder interpolateMorphWeights(
double start, double end, double startValue, double endValue) {
_interpMorphStart = start;
_interpMorphEnd = end;
_interpMorphStartValue = startValue;
_interpMorphEndValue = endValue;
return this;
}
AnimationBuilder interpolateBoneTransform(
String boneName,
String meshName,
double start,
double end,
Vector3 transStart,
Vector3 transEnd,
Quaternion quatStart,
Quaternion quatEnd) {
var translations = <Vector3>[];
var quats = <Quaternion>[];
var frameStart = (start * 1000) ~/ _frameLengthInMs;
var frameEnd = (end * 1000) ~/ _frameLengthInMs;
int numFrames = _duration * 1000 ~/ _frameLengthInMs;
if (frameEnd > numFrames) {
throw Exception();
}
for (int i = 0; i < numFrames; i++) {
if (i >= frameStart && i < frameEnd) {
var linear = (i - frameStart) / (frameEnd - frameStart);
translations.add(Vector3(
((1 - linear) * transStart.x) + (linear * transEnd.x),
((1 - linear) * transStart.y) + (linear * transEnd.y),
((1 - linear) * transStart.z) + (linear * transEnd.z),
));
quats.add(Quaternion(
((1 - linear) * quatStart.x) + (linear * quatEnd.x),
((1 - linear) * quatStart.y) + (linear * quatEnd.y),
((1 - linear) * quatStart.z) + (linear * quatEnd.z),
((1 - linear) * quatStart.w) + (linear * quatEnd.w),
));
} else {
translations.add(Vector3.zero());
quats.add(Quaternion.identity());
}
}
throw Exception();
// var boneFrameData = BoneTransformFrameData(translations, quats);
// _BoneAnimationDatas ??= <BoneAnimationData>[];
// var frameData = List<List<double>>.generate(
// numFrames, (index) => boneFrameData.getFrameData(index).toList());
// var animData = Float32List.fromList(frameData.expand((x) => x).toList());
// _BoneAnimationDatas!.add(DartBoneAnimationData([boneName], [meshName], animData));
// return this;
}
}
class BoneTransformFrameData {
final List<Vector3> translations;
final List<Quaternion> quaternions;
///
/// The length of [translations] and [quaternions] must be the same;
/// each entry represents the Vec3/Quaternion for the given frame.
///
BoneTransformFrameData(this.translations, this.quaternions) {
if (translations.length != quaternions.length) {
throw Exception("Length of translation/quaternion frames must match");
}
}
Iterable<double> getFrameData(int frame) sync* {
yield translations[frame].x;
yield translations[frame].y;
yield translations[frame].z;
yield quaternions[frame].x;
yield quaternions[frame].y;
yield quaternions[frame].z;
yield quaternions[frame].w;
}
}

View File

@@ -0,0 +1,61 @@
import 'package:vector_math/vector_math_64.dart';
///
/// Specifies frame data (i.e. weights) to animate the morph targets contained in [morphTargets] under a mesh named [mesh].
/// [data] is laid out as numFrames x numMorphTargets.
/// Each frame is [numMorphTargets] in length, where the index of each weight corresponds to the respective index in [morphTargets].
/// [morphTargets] must be some subset of the actual morph targets under [mesh] (though the order of these does not need to match).
///
class MorphAnimationData {
final List<String> meshNames;
final List<String> morphTargets;
final List<double> data;
MorphAnimationData(
this.meshNames, this.data, this.morphTargets, this.frameLengthInMs) {
assert(data.length == morphTargets.length * numFrames);
}
int get numMorphTargets => morphTargets.length;
int get numFrames => data.length ~/ numMorphTargets;
final double frameLengthInMs;
Iterable<double> getData(String morphName) sync* {
int index = morphTargets.indexOf(morphName);
if (index == -1) {
throw Exception("No data for morph $morphName");
}
for (int i = 0; i < numFrames; i++) {
yield data[(i * numMorphTargets) + index];
}
}
}
///
/// Model class for bone animation frame data.
/// To create dynamic/runtime bone animations (as distinct from animations embedded in a glTF asset), create an instance containing the relevant
/// data and pass to the [setBoneAnimation] method on a [FilamentController].
/// [frameData] is laid out as [locX, locY, locZ, rotW, rotX, rotY, rotZ]
///
class BoneAnimationData {
final List<String> bones;
final List<String> meshNames;
final List<List<Quaternion>> rotationFrameData;
final List<List<Vector3>> translationFrameData;
double frameLengthInMs;
final bool isModelSpace;
BoneAnimationData(this.bones, this.meshNames, this.rotationFrameData,
this.translationFrameData, this.frameLengthInMs,
{this.isModelSpace = false});
int get numFrames => rotationFrameData.length;
BoneAnimationData frame(int frame) {
return BoneAnimationData(bones, meshNames, [rotationFrameData[frame]],
[translationFrameData[frame]], frameLengthInMs,
isModelSpace: isModelSpace);
}
}

View File

@@ -0,0 +1,138 @@
import 'dart:math';
import 'package:flutter_filament/filament/animations/animation_data.dart';
import 'package:vector_math/vector_math_64.dart';
enum RotationMode { ZYX, XYZ }
enum Axes { Filament, Blender }
Map<RotationMode, Matrix3> _permutations = {
RotationMode.XYZ: Matrix3.identity()
// RotationMode.ZYX:Matrix3.columns([],[],[]),
};
class BVHParser {
static Map<String, String> parseARPRemap(String arpRemapData) {
final remap = <String, String>{};
var data = arpRemapData.split("\n");
for (int i = 0; i < data.length;) {
var srcBone = data[i].split("%")[0];
if (srcBone.isNotEmpty && srcBone != "None") {
var targetBone = data[i + 1].trim();
remap[targetBone] = srcBone;
}
i += 5;
}
return remap;
}
static BoneAnimationData parse(String data, List<String> meshNames,
{Map<String, String>? remap,
RegExp? boneRegex,
RotationMode rotationMode = RotationMode.ZYX,
Vector3? rootTranslationOffset,
axes = Axes.Filament}) {
// parse the list/hierarchy of bones
final bones = <String>[];
double frameLengthInMs = 0.0;
var iter = data.split("\n").iterator;
while (iter.moveNext()) {
final line = iter.current.trim();
if (line.startsWith('ROOT') || line.startsWith('JOINT')) {
var bone = line.split(' ')[1];
if (remap?.containsKey(bone) == true) {
print("Remapping $bone to ${remap![bone]!}");
bone = remap![bone]!;
}
bones.add(bone);
} else if (line.startsWith('Frame Time:')) {
var frameTime = line.split(' ').last.trim();
frameLengthInMs =
double.parse(frameTime) * 1000; // Convert to milliseconds
print("Got frame time $frameTime frameLengthInMs $frameLengthInMs");
break;
}
}
// filter out any bones that don't match the regexp (if provided)
final boneIndices = boneRegex == null
? List<int>.generate(bones.length, (index) => index)
: bones.indexed
.where((bone) => boneRegex.hasMatch(bone.$2))
.map((bone) => bone.$1);
// the remaining lines contain the actual animation data
// we assume the first six are LOCX LOCY LOCZ ROTZ ROTY ROTX for the root bone, then ROTZ ROTY ROTX for the remainder
final translationData = <List<Vector3>>[];
final rotationData = <List<Quaternion>>[];
while (iter.moveNext()) {
final line = iter.current;
if (line.isEmpty) {
break;
}
var parseResult = _parseFrameData(line,
axes: axes, rootTranslationOffset: rootTranslationOffset);
rotationData.add(
boneIndices.map((idx) => parseResult.rotationData[idx]).toList());
translationData.add(<Vector3>[parseResult.translationData] +
List<Vector3>.filled(bones.length - 1, Vector3.zero()));
}
return BoneAnimationData(boneIndices.map((idx) => bones[idx]).toList(),
meshNames, rotationData, translationData, frameLengthInMs,
isModelSpace: true);
}
static ({List<Quaternion> rotationData, Vector3 translationData})
_parseFrameData(String frameLine,
{Vector3? rootTranslationOffset, axes = Axes.Filament}) {
final frameValues = <double>[];
for (final entry in frameLine.split(RegExp(r'\s+'))) {
if (entry.isNotEmpty) {
frameValues.add(double.parse(entry));
}
}
// first 3 values are root node position (translation), remainder are ZYX rotatons
// this is hardcoded assumption for BVH files generated by momask only; won't work for any other animations in general
// Blender exports BVH using same coordinate system (i.e. Z is up, Y points into screen)
// but Filament uses Y-up/Z forward
late Vector3 rootTranslation;
late Vector3 Z, Y, X;
if (axes == Axes.Blender) {
rootTranslation =
Vector3(frameValues[0], frameValues[2], -frameValues[1]);
Z = Vector3(0, 1, 0);
Y = Vector3(0, 0, -1);
X = Vector3(1, 0, 0);
} else {
rootTranslation = Vector3(
frameValues[0],
frameValues[1],
frameValues[2],
);
Z = Vector3(0, 0, 1);
Y = Vector3(0, 1, 0);
X = Vector3(1, 0, 0);
}
if (rootTranslationOffset != null) {
rootTranslation -= rootTranslationOffset;
}
List<Quaternion> frameData = [];
for (int i = 3; i < frameValues.length; i += 3) {
var raw = frameValues.sublist(i, i + 3);
var z = Quaternion.axisAngle(Z, radians(raw[0]));
var y = Quaternion.axisAngle(Y, radians(raw[1]));
var x = Quaternion.axisAngle(X, radians(raw[2]));
var rotation = z * y * x;
frameData.add(rotation.normalized());
}
return (rotationData: frameData, translationData: rootTranslation);
}
static double radians(double degrees) => degrees * (pi / 180.0);
}

View File

@@ -0,0 +1,203 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'package:flutter_filament/filament/utils/hardware_keyboard_listener.dart';
import 'package:vector_math/vector_math_64.dart' as v;
class EntityTransformController {
final FilamentController controller;
final FilamentEntity _entity;
late Timer _ticker;
double translationSpeed;
double rotationRadsPerSecond;
bool _forward = false;
bool _strafeLeft = false;
bool _strafeRight = false;
bool _back = false;
bool _rotateLeft = false;
bool _rotateRight = false;
double _rotY = 0;
int? forwardAnimationIndex;
int? backwardAnimationIndex;
int? strafeLeftAnimationIndex;
int? strafeRightAnimationIndex;
EntityTransformController(this.controller, this._entity,
{this.translationSpeed = 1,
this.rotationRadsPerSecond = pi / 2,
this.forwardAnimationIndex,
this.backwardAnimationIndex,
this.strafeLeftAnimationIndex,
this.strafeRightAnimationIndex}) {
var translationSpeedPerTick = translationSpeed / (1000 / 16.667);
var rotationRadsPerTick = rotationRadsPerSecond / (1000 / 16.667);
_ticker = Timer.periodic(const Duration(milliseconds: 16), (timer) {
_update(translationSpeedPerTick, rotationRadsPerTick);
});
}
bool _enabled = true;
void enable() {
_enabled = true;
}
void disable() {
_enabled = false;
}
void _update(
double translationSpeedPerTick, double rotationRadsPerTick) async {
if (!_enabled) {
return;
}
var _position = v.Vector3.zero();
bool updateTranslation = false;
if (_forward) {
_position.add(v.Vector3(0, 0, -translationSpeedPerTick));
updateTranslation = true;
}
if (_back) {
_position.add(v.Vector3(0, 0, translationSpeedPerTick));
updateTranslation = true;
}
if (_strafeLeft) {
_position.add(v.Vector3(-translationSpeedPerTick, 0, 0));
updateTranslation = true;
}
if (_strafeRight) {
_position.add(v.Vector3(translationSpeedPerTick, 0, 0));
updateTranslation = true;
}
// TODO - use pitch/yaw/roll
bool updateRotation = false;
var _rotation = v.Quaternion.identity();
double rads = 0.0;
if (_rotY != 0) {
rads = _rotY * pi / 1000;
var rotY = v.Quaternion.axisAngle(v.Vector3(0, 1, 0), rads).normalized();
_rotation = rotY;
updateRotation = true;
_rotY = 0;
}
if (updateTranslation) {
await controller.queuePositionUpdate(
_entity, _position.x, _position.y, _position.z,
relative: true);
}
if (updateRotation) {
await controller.queueRotationUpdateQuat(_entity, _rotation,
relative: true);
}
}
void look(double deltaX) async {
_rotY -= deltaX;
}
void dispose() {
_ticker.cancel();
}
bool _playingForwardAnimation = false;
bool _playingBackwardAnimation = false;
void forwardPressed() async {
_forward = true;
if (forwardAnimationIndex != null && !_playingForwardAnimation) {
await controller.playAnimation(_entity, forwardAnimationIndex!,
loop: true, replaceActive: false);
_playingForwardAnimation = true;
}
}
void forwardReleased() async {
_forward = false;
await Future.delayed(Duration(milliseconds: 50));
if (!_forward) {
_playingForwardAnimation = false;
if (forwardAnimationIndex != null) {
await controller.stopAnimation(_entity, forwardAnimationIndex!);
}
}
}
void backPressed() async {
_back = true;
if (forwardAnimationIndex != null) {
if (!_playingBackwardAnimation) {
await controller.playAnimation(_entity, forwardAnimationIndex!,
loop: true, replaceActive: false, reverse: true);
_playingBackwardAnimation = true;
}
}
}
void backReleased() async {
_back = false;
if (forwardAnimationIndex != null) {
await controller.stopAnimation(_entity, forwardAnimationIndex!);
}
_playingBackwardAnimation = false;
}
void strafeLeftPressed() {
_strafeLeft = true;
}
void strafeLeftReleased() async {
_strafeLeft = false;
}
void strafeRightPressed() {
_strafeRight = true;
}
void strafeRightReleased() async {
_strafeRight = false;
}
void Function()? _mouse1DownCallback;
void onMouse1Down(void Function() callback) {
_mouse1DownCallback = callback;
}
void mouse1Down() async {
_mouse1DownCallback?.call();
}
void mouse1Up() async {}
void mouse2Up() async {}
void mouse2Down() async {}
static HardwareKeyboardListener? _keyboardListener;
static Future<EntityTransformController> create(
FilamentController controller, FilamentEntity entity,
{double? translationSpeed, String? forwardAnimation}) async {
int? forwardAnimationIndex;
if (forwardAnimation != null) {
final animationNames = await controller.getAnimationNames(entity);
forwardAnimationIndex = animationNames.indexOf(forwardAnimation);
}
if (forwardAnimationIndex == -1) {
throw Exception("Invalid animation : $forwardAnimation");
}
_keyboardListener?.dispose();
var transformController = EntityTransformController(controller, entity,
translationSpeed: translationSpeed ?? 1.0,
forwardAnimationIndex: forwardAnimationIndex);
_keyboardListener = HardwareKeyboardListener(transformController);
return transformController;
}
}

View File

@@ -0,0 +1,72 @@
import 'dart:ui';
import 'package:vector_math/vector_math_64.dart';
import '../filament_controller.dart';
class Gizmo {
final FilamentEntity x;
Vector3 _x = Vector3(0.1, 0, 0);
final FilamentEntity y;
Vector3 _y = Vector3(0.0, 0.1, 0);
final FilamentEntity z;
Vector3 _z = Vector3(0.0, 0.0, 0.1);
final FilamentController controller;
FilamentEntity? _activeAxis;
FilamentEntity? _activeEntity;
bool get isActive => _activeAxis != null;
Gizmo(this.x, this.y, this.z, this.controller) {
controller.pickResult.listen(_onPickResult);
}
Future _reveal() async {
await controller.reveal(x, null);
await controller.reveal(y, null);
await controller.reveal(z, null);
}
void translate(Offset offset) async {
late Vector3 vec;
if (_activeAxis == x) {
vec = _x;
} else if (_activeAxis == y) {
vec = _y;
} else if (_activeAxis == z) {
vec = _z;
}
await controller.queuePositionUpdate(_activeEntity!, offset.dx * vec.x,
-offset.dy * vec.y, -offset.dx * vec.z,
relative: true);
}
void reset() {
_activeAxis = null;
}
void _onPickResult(FilamentPickResult result) async {
if (result.entity == x || result.entity == y || result.entity == z) {
_activeAxis = result.entity;
} else {
attach(result.entity);
}
}
void attach(FilamentEntity entity) async {
print("Attaching to $entity");
_activeAxis = null;
_activeEntity = entity;
await _reveal();
await controller.setParent(x, entity);
await controller.setParent(y, entity);
await controller.setParent(z, entity);
}
void detach() async {
await controller.hide(x, null);
await controller.hide(y, null);
await controller.hide(z, null);
}
}

View File

@@ -0,0 +1,695 @@
// ignore_for_file: constant_identifier_names
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:flutter_filament/filament/entities/gizmo.dart';
import 'package:vector_math/vector_math_64.dart';
import 'animations/animation_data.dart';
// a handle that can be safely passed back to the rendering layer to manipulate an Entity
typedef FilamentEntity = int;
// "picking" means clicking/tapping on the viewport, and unprojecting the X/Y coordinate to determine whether any renderable entities were present at those coordinates.
typedef FilamentPickResult = ({FilamentEntity entity, double x, double y});
// copied from filament/backened/DriverEnums.h
enum PrimitiveType {
// don't change the enums values (made to match GL)
POINTS, //!< points
LINES, //!< lines
UNUSED1,
LINE_STRIP, //!< line strip
TRIANGLES, //!< triangles
TRIANGLE_STRIP, //!< triangle strip
}
enum ToneMapper { ACES, FILMIC, LINEAR }
// see filament Manipulator.h for more details
enum ManipulatorMode { ORBIT, MAP, FREE_FLIGHT }
class TextureDetails {
final int textureId;
// both width and height are in physical, not logical pixels
final int width;
final int height;
TextureDetails(
{required this.textureId, required this.width, required this.height});
}
abstract class FilamentController {
///
/// A [ValueNotifier] to indicate whether a FilamentViewer is currently available.
/// (FilamentViewer is a C++ type, hence why it is not referenced) here.
/// Call [createViewer]/[destroyViewer] to create/destroy a FilamentViewer.
///
ValueNotifier<bool> get hasViewer;
///
/// Whether a Flutter Texture widget should be inserted into the widget hierarchy.
/// This will be false on certain platforms where we use a transparent window underlay.
/// Used internally by [FilamentWidget]; you probably don't need to access this property directly.
///
bool get requiresTextureWidget;
///
/// The Flutter texture ID and dimensions for current texture in use.
/// This is only used by [FilamentWidget]; you shouldn't need to access directly yourself.
///
final textureDetails = ValueNotifier<TextureDetails?>(null);
///
/// The result(s) of calling [pick] (see below).
/// This may be a broadcast stream, so you should ensure you have subscribed to this stream before calling [pick].
/// If [pick] is called without an active subscription to this stream, the results will be silently discarded.
///
Stream<FilamentPickResult> get pickResult;
///
/// Whether the controller is currently rendering at [framerate].
///
bool get rendering;
///
/// Set to true to continuously render the scene at the framerate specified by [setFrameRate] (60 fps by default).
///
Future setRendering(bool render);
///
/// Render a single frame.
///
Future render();
///
/// Sets the framerate for continuous rendering when [setRendering] is enabled.
///
Future setFrameRate(int framerate);
///
/// Destroys the viewer and all backing textures. You can leave the FilamentWidget in the hierarchy after this is called, but you will need to manually call [createViewer] to
///
Future destroy();
///
/// Destroys the viewer only, leaving the texture intact. You probably want to call [destroy] instead of this; [destroyViewer] is exposed mostly for lifecycle changes which are handled by FilamentWidget.
///
Future destroyViewer();
///
/// Destroys the specified backing texture. You probably want to call [destroy] instead of this; this is exposed mostly for lifecycle changes which are handled by FilamentWidget.
///
Future destroyTexture();
///
/// Create a FilamentViewer. Must be called at least one frame after a [FilamentWidget] has been inserted into the rendering hierarchy.
///
/// Before a FilamentViewer is created, the FilamentWidget will only contain an empty Container (by default, with a solid red background).
/// FilamentWidget will then call [setDimensions] with dimensions/pixel ratio of the viewport
/// Calling [createViewer] will then dispatch a request to the native platform to create a hardware texture (Metal on iOS, OpenGL on Linux, GLES on Android and Windows) and a FilamentViewer (the main interface for manipulating the 3D scene) .
/// [FilamentWidget] will be notified that a texture is available and will replace the empty Container with a Texture widget
///
Future createViewer();
///
/// Sets the dimensions of the viewport and pixel ratio (obtained from [MediaQuery]) to be used the next time [resize] or [createViewer] is called.
/// This is called by FilamentWidget; you shouldn't need to invoke this manually.
///
Future setDimensions(ui.Rect rect, double pixelRatio);
///
/// Resize the viewport & backing texture to the current dimensions (as last set by [setDimensions]).
/// This is called by FilamentWidget; you shouldn't need to invoke this manually.
///
Future resize();
///
/// Set the background image to [path] (which should have a file extension .png, .jpg, or .ktx).
/// This will be rendered at the maximum depth (i.e. behind all other objects including the skybox).
/// If [fillHeight] is false, the image will be rendered at its original size. Note this may cause issues with pixel density so be sure to specify the correct resolution
/// If [fillHeight] is true, the image will be stretched/compressed to fit the height of the viewport.
///
Future setBackgroundImage(String path, {bool fillHeight = false});
///
/// Moves the background image to the relative offset from the origin (bottom-left) specified by [x] and [y].
/// If [clamp] is true, the image cannot be positioned outside the bounds of the viewport.
///
Future setBackgroundImagePosition(double x, double y, {bool clamp = false});
///
/// Removes the background image.
///
Future clearBackgroundImage();
///
/// Sets the color for the background plane (positioned at the maximum depth, i.e. behind all other objects including the skybox).
///
Future setBackgroundColor(Color color);
///
/// Load a skybox from [skyboxPath] (which must be a .ktx file)
///
Future loadSkybox(String skyboxPath);
///
/// Removes the skybox from the scene.
///
Future removeSkybox();
///
/// Loads an image-based light from the specified path at the given intensity.
/// Only one IBL can be active at any given time; if an IBL has already been loaded, it will be replaced.
///
Future loadIbl(String lightingPath, {double intensity = 30000});
///
/// Rotates the IBL & skybox.
///
Future rotateIbl(Matrix3 rotation);
///
/// Removes the image-based light from the scene.
///
Future removeIbl();
///
/// Adds a dynamic light to the scene.
/// copied from filament LightManager.h
/// enum class Type : uint8_t {
/// SUN, //!< Directional light that also draws a sun's disk in the sky.
/// DIRECTIONAL, //!< Directional light, emits light in a given direction.
/// POINT, //!< Point light, emits light from a position, in all directions.
/// FOCUSED_SPOT, //!< Physically correct spot light.
/// SPOT, //!< Spot light with coupling of outer cone and illumination disabled.
/// };
Future<FilamentEntity> addLight(
int type,
double colour,
double intensity,
double posX,
double posY,
double posZ,
double dirX,
double dirY,
double dirZ,
bool castShadows);
Future removeLight(FilamentEntity light);
///
/// Remove all lights (excluding IBL) from the scene.
///
Future clearLights();
///
/// Load the .glb asset at the given path and insert into the scene.
///
Future<FilamentEntity> loadGlb(String path, {int numInstances = 1});
///
/// Create a new instance of [entity].
///
Future<FilamentEntity> createInstance(FilamentEntity entity);
///
/// Returns the number of instances of the asset associated with [entity].
///
Future<int> getInstanceCount(FilamentEntity entity);
///
/// Returns all instances of [entity].
///
Future<List<FilamentEntity>> getInstances(FilamentEntity entity);
///
/// Load the .gltf asset at the given path and insert into the scene.
/// [relativeResourcePath] is the folder path where the glTF resources are stored;
/// this is usually the parent directory of the .gltf file itself.
///
Future<FilamentEntity> loadGltf(String path, String relativeResourcePath,
{bool force = false});
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future panStart(double x, double y);
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future panUpdate(double x, double y);
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future panEnd();
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future rotateStart(double x, double y);
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future rotateUpdate(double x, double y);
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future rotateEnd();
///
/// Set the weights for all morph targets under node [meshName] in [entity] to [weights].
/// Note that [weights] must contain values for ALL morph targets, but no exception will be thrown if you don't do so (you'll just get incorrect results).
/// If you only want to set one value, set all others to zero (check [getMorphTargetNames] if you need the get a list of all morph targets.)
///
Future setMorphTargetWeights(
FilamentEntity entity, String meshName, List<double> weights);
Future<List<String>> getMorphTargetNames(
FilamentEntity entity, String meshName);
Future<List<String>> getAnimationNames(FilamentEntity entity);
///
/// Returns the length (in seconds) of the animation at the given index.
///
Future<double> getAnimationDuration(
FilamentEntity entity, int animationIndex);
///
/// Animate the morph targets in [entity]. See [MorphTargetAnimation] for an explanation as to how to construct the animation frame data.
/// This method will check the morph target names specified in [animation] against the morph target names that actually exist exist under [meshName] in [entity],
/// throwing an exception if any cannot be found.
/// It is permissible for [animation] to omit any targets that do exist under [meshName]; these simply won't be animated.
///
Future setMorphAnimationData(
FilamentEntity entity, MorphAnimationData animation);
///
/// Resets all bones in the given entity to their rest pose.
/// This should be done before every call to addBoneAnimation.
///
Future resetBones(FilamentEntity entity);
///
/// Transforms the bone(s)/joint(s) according [animation].
/// To set the instantaneous transform, just use a single frame.
///
Future addBoneAnimation(FilamentEntity entity, BoneAnimationData animation);
///
/// Removes/destroys the specified entity from the scene.
/// [entity] will no longer be a valid handle after this method is called; ensure you immediately discard all references once this method is complete.
///
Future removeEntity(FilamentEntity entity);
///
/// Removes/destroys all renderable entities from the scene (including cameras).
/// All [FilamentEntity] handles will no longer be valid after this method is called; ensure you immediately discard all references to all entities once this method is complete.
///
Future clearEntities();
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future zoomBegin();
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future zoomUpdate(double x, double y, double z);
///
/// Called by `FilamentGestureDetector`. You probably don't want to call this yourself.
///
Future zoomEnd();
///
/// Schedules the glTF animation at [index] in [entity] to start playing on the next frame.
///
Future playAnimation(FilamentEntity entity, int index,
{bool loop = false,
bool reverse = false,
bool replaceActive = true,
double crossfade = 0.0});
///
/// Schedules the glTF animation at [index] in [entity] to start playing on the next frame.
///
Future playAnimationByName(FilamentEntity entity, String name,
{bool loop = false,
bool reverse = false,
bool replaceActive = true,
double crossfade = 0.0});
Future setAnimationFrame(
FilamentEntity entity, int index, int animationFrame);
Future stopAnimation(FilamentEntity entity, int animationIndex);
Future stopAnimationByName(FilamentEntity entity, String name);
///
/// Sets the current scene camera to the glTF camera under [name] in [entity].
///
Future setCamera(FilamentEntity entity, String? name);
///
/// Sets the current scene camera to the main camera (which is always available and added to every scene by default).
///
Future setMainCamera();
///
/// Returns the entity associated with the main camera.
///
Future<FilamentEntity> getMainCamera();
///
/// Sets the current scene camera to the glTF camera under [name] in [entity].
///
Future setCameraFov(double degrees);
///
/// Sets the tone mapping (requires postprocessing).
///
Future setToneMapping(ToneMapper mapper);
///
/// Sets the strength of the bloom.
///
Future setBloom(double bloom);
///
/// Sets the focal length of the camera. Default value is 28.0.
///
Future setCameraFocalLength(double focalLength);
///
/// Sets the distance (in world units) to the near/far planes for the active camera. Default values are 0.05/1000.0. See Camera.h for details.
///
Future setCameraCulling(double near, double far);
///
/// Get the distance (in world units) to the near culling plane for the active camera.
///
Future<double> getCameraCullingNear();
///
/// Get the distance (in world units) to the far culling plane for the active camera.
///
Future<double> getCameraCullingFar();
///
/// Sets the focus distance for the camera.
///
Future setCameraFocusDistance(double focusDistance);
///
/// Get the camera position in world space.
///
Future<Vector3> getCameraPosition();
///
/// Get the camera's model matrix.
///
Future<Matrix4> getCameraModelMatrix();
///
/// Get the camera's view matrix. See Camera.h for more details.
///
Future<Matrix4> getCameraViewMatrix();
///
/// Get the camera's projection matrix. See Camera.h for more details.
///
Future<Matrix4> getCameraProjectionMatrix();
///
/// Get the camera's culling projection matrix. See Camera.h for more details.
///
Future<Matrix4> getCameraCullingProjectionMatrix();
///
/// Get the camera's culling frustum in world space. Returns a (vector_math) [Frustum] instance where plane0-plane6 define the left, right, bottom, top, far and near planes respectively.
/// See Camera.h and (filament) Frustum.h for more details.
///
Future<Frustum> getCameraFrustum();
///
/// Set the camera position in world space. Note this is not persistent - any viewport navigation will reset the camera transform.
///
Future setCameraPosition(double x, double y, double z);
///
/// Get the camera rotation matrix.
///
Future<Matrix3> getCameraRotation();
///
/// Repositions the camera to the last vertex of the bounding box of [entity], looking at the penultimate vertex.
///
Future moveCameraToAsset(FilamentEntity entity);
///
/// Enables/disables frustum culling. Currently we don't expose a method for manipulating the camera projection/culling matrices so this is your only option to deal with unwanted near/far clipping.
///
Future setViewFrustumCulling(bool enabled);
///
/// Sets the camera exposure.
///
Future setCameraExposure(
double aperture, double shutterSpeed, double sensitivity);
///
/// Rotate the camera by [rads] around the given axis. Note this is not persistent - any viewport navigation will reset the camera transform.
///
Future setCameraRotation(Quaternion quaternion);
///
/// Sets the camera model matrix.
///
Future setCameraModelMatrix(List<double> matrix);
///
/// Sets the `baseColorFactor` property for the material at index [materialIndex] in [entity] under node [meshName] to [color].
///
Future setMaterialColor(
FilamentEntity entity, String meshName, int materialIndex, Color color);
///
/// Scale [entity] to fit within the unit cube.
///
Future transformToUnitCube(FilamentEntity entity);
///
/// Directly sets the world space position for [entity] to the given coordinates, skipping all collision detection.
///
Future setPosition(FilamentEntity entity, double x, double y, double z);
///
/// Directly sets the scale for [entity], skipping all collision detection.
///
Future setScale(FilamentEntity entity, double scale);
///
/// Directly sets the rotation for [entity] to [rads] around the axis {x,y,z}, skipping all collision detection.
///
Future setRotation(
FilamentEntity entity, double rads, double x, double y, double z);
///
/// Queues an update to the worldspace position for [entity] to {x,y,z}.
/// The actual update will occur on the next frame, and will be subject to collision detection.
///
Future queuePositionUpdate(
FilamentEntity entity, double x, double y, double z,
{bool relative = false});
///
/// Queues an update to the worldspace rotation for [entity].
/// The actual update will occur on the next frame, and will be subject to collision detection.
///
Future queueRotationUpdate(
FilamentEntity entity, double rads, double x, double y, double z,
{bool relative = false});
///
/// Same as [queueRotationUpdate].
///
Future queueRotationUpdateQuat(FilamentEntity entity, Quaternion quat,
{bool relative = false});
///
/// Enable/disable postprocessing.
///
Future setPostProcessing(bool enabled);
///
/// Set antialiasing options.
///
Future setAntiAliasing(bool msaa, bool fxaa, bool taa);
///
/// Sets the rotation for [entity] to the specified quaternion.
///
Future setRotationQuat(FilamentEntity entity, Quaternion rotation);
///
/// Reveal the node [meshName] under [entity]. Only applicable if [hide] had previously been called; this is a no-op otherwise.
///
Future reveal(FilamentEntity entity, String? meshName);
///
/// If [meshName] is provided, hide the node [meshName] under [entity], otherwise hide the root node for [entity].
/// The entity still exists in memory, but is no longer being rendered into the scene. Call [reveal] to re-commence rendering.
///
Future hide(FilamentEntity entity, String? meshName);
///
/// Used to select the entity in the scene at the given viewport coordinates.
/// Called by `FilamentGestureDetector` on a mouse/finger down event. You probably don't want to call this yourself.
/// This is asynchronous and will require 2-3 frames to complete - subscribe to the [pickResult] stream to receive the results of this method.
/// [x] and [y] must be in local logical coordinates (i.e. where 0,0 is at top-left of the FilamentWidget).
///
void pick(int x, int y);
///
/// Retrieves the name assigned to the given FilamentEntity (usually corresponds to the glTF mesh name).
///
String? getNameForEntity(FilamentEntity entity);
///
/// Sets the options for manipulating the camera via the viewport.
/// ManipulatorMode.FREE_FLIGHT and ManipulatorMode.MAP are currently unsupported and will throw an exception.
///
Future setCameraManipulatorOptions(
{ManipulatorMode mode = ManipulatorMode.ORBIT,
double orbitSpeedX = 0.01,
double orbitSpeedY = 0.01,
double zoomSpeed = 0.01});
///
/// Finds the child entity named [childName] associated with the given parent.
/// Usually, [parent] will be the return value from [loadGlb]/[loadGltf] and [childName] will be the name of a node/mesh.
///
Future<FilamentEntity> getChildEntity(
FilamentEntity parent, String childName);
///
/// List all child entities under the given entity.
///
Future<List<String>> getChildEntities(FilamentEntity entity,
{bool renderableOnly = true});
///
/// If [recording] is set to true, each frame the framebuffer/texture will be written to /tmp/output_*.png.
/// This will impact performance; handle with care.
///
Future setRecording(bool recording);
///
/// Sets the output directory where recorded PNGs will be placed.
///
Future setRecordingOutputDirectory(String outputDirectory);
///
/// An [entity] will only be animatable after an animation component is attached.
/// Any calls to [playAnimation]/[setBoneAnimation]/[setMorphAnimation] will have no visual effect until [addAnimationComponent] has been called on the instance.
///
Future addAnimationComponent(FilamentEntity entity);
///
/// Makes [entity] collidable.
/// This allows you to call [testCollisions] with any other entity ("entity B") to see if [entity] has collided with entity B. The callback will be invoked if so.
/// Alternatively, if [affectsTransform] is true and this entity collides with another entity, any queued position updates to the latter entity will be ignored.
///
Future addCollisionComponent(FilamentEntity entity,
{void Function(int entityId1, int entityId2)? callback,
bool affectsTransform = false});
///
/// Removes the collision component from [entity], meaning this will no longer be tested when [testCollisions] or [queuePositionUpdate] is called with another entity.
///
Future removeCollisionComponent(FilamentEntity entity);
///
/// Creates a (renderable) entity with the specified geometry and adds to the scene.
///
Future createGeometry(List<double> vertices, List<int> indices,
{String? materialPath,
PrimitiveType primitiveType = PrimitiveType.TRIANGLES});
///
/// Sets the parent transform of [child] to the transform of [parent].
///
Future setParent(FilamentEntity child, FilamentEntity parent);
///
/// Test all collidable entities against this entity to see if any have collided.
/// This method returns void; the relevant callback passed to [addCollisionComponent] will be fired if a collision is detected.
///
Future testCollisions(FilamentEntity entity);
///
/// Sets the draw priority for the given entity. See RenderableManager.h for more details.
///
Future setPriority(FilamentEntity entityId, int priority);
///
/// The Scene holds the transform gizmo and all loaded entities/lights.
///
Scene get scene;
}
///
/// For now, this class just holds the entities that have been loaded (though not necessarily visible in the Filament Scene).
///
abstract class Scene {
///
/// The last entity clicked/tapped in the viewport (internally, the result of calling pick);
FilamentEntity? selected;
///
/// A Stream updated whenever an entity is added/removed from the scene.
///
Stream<bool> get onUpdated;
///
/// A Stream containing every FilamentEntity added to the scene (i.e. via [loadGlb], [loadGltf] or [addLight]).
/// This is provided for convenience so you can set listeners in front-end widgets that can respond to entity loads without manually passing around the FilamentEntity returned from those methods.
///
Stream<FilamentEntity> get onLoad;
///
/// A Stream containing every FilamentEntity removed from the scene (i.e. via [removeEntity], [clearEntities], [removeLight] or [clearLights]).
Stream<FilamentEntity> get onUnload;
///
/// Lists all light entities currently loaded (not necessarily active in the scene). Does not account for instances.
///
Iterable<FilamentEntity> listLights();
///
/// Lists all entities currently loaded (not necessarily active in the scene). Does not account for instances.
///
Iterable<FilamentEntity> listEntities();
///
/// Attach the gizmo to the specified entity.
///
void select(FilamentEntity entity);
///
/// The transform gizmo.
///
Gizmo get gizmo;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
import 'dart:ffi';
import 'dart:developer' as dev;
class RenderingSurface {
final int flutterTextureId;
final Pointer<Void> surface;
final int textureHandle;
final int sharedContext;
factory RenderingSurface.from(dynamic platformMessage) {
var flutterTextureId = platformMessage[0];
// void* on iOS (pointer to pixel buffer), Android (pointer to native window), null on macOS/Windows
var surfaceAddress = platformMessage[1] as int? ?? 0;
// null on iOS/Android, void* on MacOS (pointer to metal texture), GLuid on Windows/Linux
var nativeTexture = platformMessage[2] as int? ?? 0;
if (nativeTexture != 0) {
assert(surfaceAddress == 0);
}
var sharedContext = platformMessage[3] as int? ?? 0;
dev.log(
"Using flutterTextureId $flutterTextureId, surface $surfaceAddress nativeTexture $nativeTexture and sharedContext $sharedContext");
return RenderingSurface(
sharedContext: sharedContext,
flutterTextureId: flutterTextureId,
surface: Pointer<Void>.fromAddress(surfaceAddress),
textureHandle: nativeTexture);
}
RenderingSurface(
{required this.sharedContext,
required this.flutterTextureId,
required this.surface,
required this.textureHandle});
}

118
lib/filament/scene.dart Normal file
View File

@@ -0,0 +1,118 @@
import 'dart:async';
import 'package:flutter_filament/filament/entities/gizmo.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
///
/// For now, this class just holds the entities that have been loaded (though not necessarily visible in the Filament Scene).
///
class SceneImpl extends Scene {
final Gizmo _gizmo;
Gizmo get gizmo => _gizmo;
SceneImpl(this._gizmo);
@override
FilamentEntity? selected;
final _onUpdatedController = StreamController<bool>.broadcast();
@override
Stream<bool> get onUpdated => _onUpdatedController.stream;
final _onLoadController = StreamController<FilamentEntity>.broadcast();
@override
Stream<FilamentEntity> get onLoad => _onLoadController.stream;
final _onUnloadController = StreamController<FilamentEntity>.broadcast();
@override
Stream<FilamentEntity> get onUnload => _onUnloadController.stream;
final _lights = <FilamentEntity>{};
final _entities = <FilamentEntity>{};
void registerLight(FilamentEntity entity) {
_lights.add(entity);
_onLoadController.sink.add(entity);
_onUpdatedController.add(true);
}
void unregisterLight(FilamentEntity entity) {
if (selected == entity) {
selected = null;
_gizmo.detach();
}
_lights.remove(entity);
_onUnloadController.add(entity);
_onUpdatedController.add(true);
}
void unregisterEntity(FilamentEntity entity) {
if (selected == entity) {
selected = null;
_gizmo.detach();
}
_entities.remove(entity);
_onUnloadController.add(entity);
_onUpdatedController.add(true);
}
void registerEntity(FilamentEntity entity) {
_entities.add(entity);
_entities.add(entity);
_onLoadController.sink.add(entity);
_onUpdatedController.add(true);
}
void clearLights() {
for (final light in _lights) {
if (selected == light) {
selected = null;
_gizmo.detach();
}
_onUnloadController.add(light);
}
_lights.clear();
_onUpdatedController.add(true);
}
void clearEntities() {
for (final entity in _entities) {
if (selected == entity) {
selected = null;
_gizmo.detach();
}
_onUnloadController.add(entity);
}
_entities.clear();
_onUpdatedController.add(true);
}
///
/// Lists all entities currently loaded (not necessarily active in the scene).
///
Iterable<FilamentEntity> listLights() {
return _lights;
}
@override
Iterable<FilamentEntity> listEntities() {
return _entities;
}
void registerSelected(FilamentEntity entity) {
selected = entity;
_onUpdatedController.add(true);
}
void unregisterSelected() {
selected = null;
_onUpdatedController.add(true);
}
@override
void select(FilamentEntity entity) {
selected = entity;
_gizmo.attach(entity);
_onUpdatedController.add(true);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:vector_math/vector_math_64.dart' as v;
class CameraOrientation {
v.Vector3 position = v.Vector3(0, 0, 0);
var rotationX = 0.0;
var rotationY = 0.0;
var rotationZ = 0.0;
v.Quaternion compose() {
return v.Quaternion.axisAngle(v.Vector3(0, 0, 1), rotationZ) *
v.Quaternion.axisAngle(v.Vector3(0, 1, 0), rotationY) *
v.Quaternion.axisAngle(v.Vector3(1, 0, 0), rotationX);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/services.dart';
import 'package:flutter_filament/filament/entities/entity_transform_controller.dart';
class HardwareKeyboardListener {
final EntityTransformController _controller;
var _listening = true;
HardwareKeyboardListener(this._controller) {
// Get the global handler.
final KeyMessageHandler? existing =
ServicesBinding.instance.keyEventManager.keyMessageHandler;
// The handler is guaranteed non-null since
// `FallbackKeyEventRegistrar.instance` is only called during
// `Focus.onFocusChange`, at which time `ServicesBinding.instance` must
// have been called somewhere.
assert(existing != null);
// Assign the global handler with a patched handler.
ServicesBinding.instance.keyEventManager.keyMessageHandler = (keyMessage) {
if (keyMessage.rawEvent == null) {
return false;
}
if (!_listening) {
return false;
}
var event = keyMessage.rawEvent!;
switch (event.logicalKey) {
case LogicalKeyboardKey.escape:
_listening = false;
break;
case LogicalKeyboardKey.keyW:
(event is RawKeyDownEvent)
? _controller.forwardPressed()
: _controller.forwardReleased();
break;
case LogicalKeyboardKey.keyA:
event is RawKeyDownEvent
? _controller.strafeLeftPressed()
: _controller.strafeLeftReleased();
break;
case LogicalKeyboardKey.keyS:
event is RawKeyDownEvent
? _controller.backPressed()
: _controller.backReleased();
break;
case LogicalKeyboardKey.keyD:
event is RawKeyDownEvent
? _controller.strafeRightPressed()
: _controller.strafeRightReleased();
break;
default:
break;
}
return true;
};
}
void dispose() {
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
_controller.dispose();
}
}

View File

@@ -0,0 +1,40 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_filament/filament/entities/entity_transform_controller.dart';
class HardwareKeyboardPoll {
final EntityTransformController _controller;
late Timer _timer;
HardwareKeyboardPoll(this._controller) {
_timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyW)) {
_controller.forwardPressed();
} else {
_controller.forwardReleased();
}
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyS)) {
_controller.backPressed();
} else {
_controller.backReleased();
}
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyA)) {
_controller.strafeLeftPressed();
} else {
_controller.strafeLeftReleased();
}
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyD)) {
_controller.strafeRightPressed();
} else {
_controller.strafeRightReleased();
}
});
}
void dispose() {
_timer.cancel();
}
}

View File

@@ -0,0 +1,29 @@
import 'package:vector_math/vector_math_64.dart' as v;
class LightOptions {
String? iblPath;
double iblIntensity;
int directionalType;
double directionalColor;
double directionalIntensity;
bool directionalCastShadows;
late v.Vector3 directionalPosition;
late v.Vector3 directionalDirection;
LightOptions(
{required this.iblPath,
required this.iblIntensity,
required this.directionalType,
required this.directionalColor,
required this.directionalIntensity,
required this.directionalCastShadows,
v.Vector3? directionalDirection,
v.Vector3? directionalPosition}) {
this.directionalDirection = directionalDirection == null
? v.Vector3(0, -1, 0)
: directionalDirection;
this.directionalPosition = directionalPosition == null
? v.Vector3(0, 100, 0)
: directionalPosition;
}
}

View File

@@ -0,0 +1,10 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
final allocator = calloc;
void using(Pointer ptr, Future Function(Pointer ptr) function) async {
await function.call(ptr);
allocator.free(ptr);
}

View File

@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'package:flutter_filament/filament/utils/camera_orientation.dart';
import 'dart:math';
import 'package:vector_math/vector_math_64.dart' as v64;
class CameraOptionsWidget extends StatefulWidget {
final FilamentController controller;
final CameraOrientation cameraOrientation;
final List<({FilamentEntity entity, String name})> cameras;
CameraOptionsWidget(
{super.key,
required this.controller,
required this.cameras,
required this.cameraOrientation}) {}
@override
State<StatefulWidget> createState() => _CameraOptionsWidgetState();
}
class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
final _apertureController = TextEditingController();
final _speedController = TextEditingController();
final _sensitivityController = TextEditingController();
@override
void initState() {
_apertureController.text = "0";
_speedController.text = "0";
_sensitivityController.text = "0";
_apertureController.addListener(() {
_set();
setState(() {});
});
_speedController.addListener(() {
_set();
setState(() {});
});
_sensitivityController.addListener(() {
_set();
setState(() {});
});
super.initState();
}
@override
void didUpdateWidget(CameraOptionsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.cameras.length != widget.cameras.length) {
setState(() {});
}
}
Future _set() async {
await widget.controller.setCameraExposure(
double.parse(_apertureController.text),
double.parse(_speedController.text),
double.parse(_sensitivityController.text));
await widget.controller.setCameraPosition(
widget.cameraOrientation.position.x,
widget.cameraOrientation.position.y,
widget.cameraOrientation.position.z);
var rotation = widget.cameraOrientation.compose();
await widget.controller.setCameraRotation(rotation);
print(
"Camera : ${widget.cameraOrientation.position} ${widget.cameraOrientation.rotationX} ${widget.cameraOrientation.rotationY} ${widget.cameraOrientation.rotationZ}");
setState(() {});
}
double _bloom = 0.0;
double _focalLength = 26.0;
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(platform: TargetPlatform.android),
child: Container(
decoration: BoxDecoration(color: Colors.white.withOpacity(0.5)),
child: SliderTheme(
data: SliderThemeData(
showValueIndicator: ShowValueIndicator.always,
valueIndicatorTextStyle: TextStyle(color: Colors.black)),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Row(children: [
Text("Aperture"),
Expanded(
child: TextField(
controller: _apertureController,
)),
Text("Speed"),
Expanded(child: TextField(controller: _speedController)),
Text("Sensitivity"),
Expanded(
child: TextField(controller: _sensitivityController)),
]),
Row(children: [
Text("Bloom: ${_bloom.toStringAsFixed(2)}"),
Slider(
value: _bloom,
min: 0.0,
max: 1.0,
onChanged: (v) async {
setState(() {
_bloom = v;
});
await widget.controller.setBloom(_bloom);
})
]),
Row(children: [
Text("Focal length"),
Slider(
label: _focalLength.toString(),
value: _focalLength,
min: 1.0,
max: 100.0,
onChanged: (v) async {
setState(() {
_focalLength = v;
});
await widget.controller
.setCameraFocalLength(_focalLength);
})
]),
Row(children: [
Text("X"),
Slider(
label: widget.cameraOrientation.position.x.toString(),
value: widget.cameraOrientation.position.x,
min: -100.0,
max: 100.0,
onChanged: (v) async {
setState(() {
widget.cameraOrientation.position.x = v;
});
_set();
})
]),
Row(children: [
Text("Y"),
Slider(
label: widget.cameraOrientation.position.y.toString(),
value: widget.cameraOrientation.position.y,
min: -100.0,
max: 100.0,
onChanged: (v) async {
setState(() {
widget.cameraOrientation.position.y = v;
});
_set();
})
]),
Row(children: [
Text("Z"),
Slider(
label: widget.cameraOrientation.position.z.toString(),
value: widget.cameraOrientation.position.z,
min: -100.0,
max: 100.0,
onChanged: (v) async {
setState(() {
widget.cameraOrientation.position.z = v;
});
_set();
})
]),
Row(children: [
Text("ROTX"),
Slider(
label: widget.cameraOrientation.rotationX.toString(),
value: widget.cameraOrientation.rotationX,
min: -pi,
max: pi,
onChanged: (value) async {
setState(() {
widget.cameraOrientation.rotationX = value;
});
_set();
})
]),
Row(children: [
Text("ROTY"),
Slider(
label: widget.cameraOrientation.rotationY.toString(),
value: widget.cameraOrientation.rotationY,
min: -pi,
max: pi,
onChanged: (v) async {
setState(() {
widget.cameraOrientation.rotationY = v;
});
_set();
}),
]),
Row(children: [
Text("ROTZ"),
Slider(
label: widget.cameraOrientation.rotationZ.toString(),
value: widget.cameraOrientation.rotationZ,
min: -pi,
max: pi,
onChanged: (v) async {
setState(() {
widget.cameraOrientation.rotationZ = v;
});
_set();
})
]),
Wrap(
children: [
GestureDetector(
child: Text("Main "),
onTap: () {
widget.controller.setMainCamera();
},
),
...widget.cameras
.map((camera) => GestureDetector(
onTap: () {
widget.controller
.setCamera(camera.entity, camera.name);
},
child: Text(camera.name)))
.toList()
],
)
]))));
}
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:flutter_filament/filament/entities/gizmo.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
class EntityListWidget extends StatefulWidget {
final FilamentController? controller;
const EntityListWidget({super.key, required this.controller});
@override
State<StatefulWidget> createState() => _EntityListWidget();
}
class _EntityListWidget extends State<EntityListWidget> {
@override
void didUpdateWidget(EntityListWidget oldWidget) {
super.didUpdateWidget(oldWidget);
}
Widget _entity(FilamentEntity entity) {
return FutureBuilder(
future: widget.controller!.getAnimationNames(entity),
builder: (_, animations) {
if (animations.data == null) {
return Container();
}
final menuController = MenuController();
return Row(children: [
Expanded(
child: GestureDetector(
onTap: () {
widget.controller!.scene.select(entity);
},
child: Text(entity.toString(),
style: TextStyle(
fontWeight:
entity == widget.controller!.scene.selected
? FontWeight.bold
: FontWeight.normal)))),
MenuAnchor(
controller: menuController,
child: Container(
color: Colors.transparent,
child: IconButton(
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black,
),
onPressed: () {
menuController.open();
},
)),
menuChildren: [
MenuItemButton(
child: const Text("Remove"),
onPressed: () async {
await widget.controller!.removeEntity(entity);
}),
MenuItemButton(
child: const Text("Transform to unit cube"),
onPressed: () async {
await widget.controller!.transformToUnitCube(entity);
}),
SubmenuButton(
child: const Text("Animations"),
menuChildren: animations.data!
.map((a) => MenuItemButton(
child: Text(a),
onPressed: () {
widget.controller!.playAnimation(
entity, animations.data!.indexOf(a));
},
))
.toList())
])
]);
});
}
Widget _light(FilamentEntity entity) {
final controller = MenuController();
return Row(children: [
GestureDetector(
onTap: () {
widget.controller!.scene.select(entity);
},
child: Container(
color: Colors.transparent,
child: Text("Light $entity",
style: TextStyle(
fontWeight: entity == widget.controller!.scene.selected
? FontWeight.bold
: FontWeight.normal)))),
MenuAnchor(
controller: controller,
child: Container(
color: Colors.transparent,
child: IconButton(
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black,
),
onPressed: () {
controller.open();
},
)),
menuChildren: [
MenuItemButton(
child: const Text("Remove"),
onPressed: () async {
await widget.controller!.removeLight(entity);
})
])
]);
}
@override
Widget build(BuildContext context) {
if (widget.controller == null) {
return Container();
}
return ValueListenableBuilder(
valueListenable: widget.controller!.hasViewer,
builder: (_, bool hasViewer, __) => !hasViewer
? Container()
: StreamBuilder(
stream: widget.controller!.scene.onUpdated,
builder: (_, __) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 30, vertical: 10),
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.white.withOpacity(0.25),
),
child: ListView(
reverse: true,
children: widget.controller!.scene
.listLights()
.map(_light)
.followedBy(widget.controller!.scene
.listEntities()
.map(_entity))
.cast<Widget>()
.toList()))));
}
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter_filament/filament/entities/entity_transform_controller.dart';
///
/// A widget that translates mouse gestures to zoom/pan/rotate actions.
///
class EntityTransformMouseControllerWidget extends StatelessWidget {
final EntityTransformController? transformController;
final Widget? child;
EntityTransformMouseControllerWidget(
{Key? key, required this.transformController, this.child})
: super(key: key);
Timer? _timer;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Listener(
onPointerDown: (event) {
if (kPrimaryMouseButton & event.buttons != 0) {
transformController?.mouse1Down();
}
},
onPointerUp: (event) {
if (kPrimaryMouseButton & event.buttons != 0) {
transformController?.mouse1Up();
}
},
onPointerHover: (event) {
_timer?.cancel();
if (event.position.dx < 10) {
_timer = Timer.periodic(const Duration(milliseconds: 17), (_) {
transformController?.look(-30);
});
} else if (event.position.dx > constraints.maxWidth - 10) {
_timer = Timer.periodic(const Duration(milliseconds: 17), (_) {
transformController?.look(30);
});
} else {
transformController?.look(event.delta.dx);
}
},
child: child);
});
}
}

View File

@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_filament/filament/widgets/filament_gesture_detector_desktop.dart';
import 'package:flutter_filament/filament/widgets/filament_gesture_detector_mobile.dart';
import '../filament_controller.dart';
enum GestureType { rotateCamera, panCamera, panBackground }
///
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
///
class FilamentGestureDetector extends StatelessWidget {
///
/// The content to display below the gesture detector/listener widget.
/// This will usually be a FilamentWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentController].
///
final Widget? child;
///
/// The [controller] attached to the [FilamentWidget] you wish to control.
///
final FilamentController controller;
///
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
/// 1) rotate or a pan (mobile only),
/// 2) moving the camera or the background image (TODO).
///
final bool showControlOverlay;
///
/// If false, gestures will not manipulate the active camera.
///
final bool enableCamera;
///
/// If false, pointer down events will not trigger hit-testing (picking).
///
final bool enablePicking;
final void Function(ScaleStartDetails)? onScaleStart;
final void Function(ScaleUpdateDetails)? onScaleUpdate;
final void Function(ScaleEndDetails)? onScaleEnd;
const FilamentGestureDetector(
{Key? key,
required this.controller,
this.child,
this.showControlOverlay = false,
this.enableCamera = true,
this.enablePicking = true,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd})
: super(key: key);
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller.hasViewer,
builder: (_, bool hasViewer, __) {
if (!hasViewer) {
return Container(child: child);
}
if (kIsWeb) {
throw Exception("TODO");
} else if (Platform.isLinux ||
Platform.isWindows ||
Platform.isMacOS) {
return FilamentGestureDetectorDesktop(
controller: controller,
child: child,
showControlOverlay: showControlOverlay,
enableCamera: enableCamera,
enablePicking: enablePicking,
);
} else {
return FilamentGestureDetectorMobile(
controller: controller,
child: child,
showControlOverlay: showControlOverlay,
enableCamera: enableCamera,
enablePicking: enablePicking,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd);
}
});
}
}

View File

@@ -0,0 +1,185 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_filament/filament/entities/gizmo.dart';
import '../filament_controller.dart';
///
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
///
class FilamentGestureDetectorDesktop extends StatefulWidget {
///
/// The content to display below the gesture detector/listener widget.
/// This will usually be a FilamentWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentController].
///
final Widget? child;
///
/// The [controller] attached to the [FilamentWidget] you wish to control.
///
final FilamentController controller;
///
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
/// 1) rotate or a pan (mobile only),
/// 2) moving the camera or the background image (TODO).
///
final bool showControlOverlay;
///
/// If false, gestures will not manipulate the active camera.
///
final bool enableCamera;
///
/// If false, pointer down events will not trigger hit-testing (picking).
///
final bool enablePicking;
const FilamentGestureDetectorDesktop(
{Key? key,
required this.controller,
this.child,
this.showControlOverlay = false,
this.enableCamera = true,
this.enablePicking = true})
: super(key: key);
@override
State<StatefulWidget> createState() => _FilamentGestureDetectorDesktopState();
}
class _FilamentGestureDetectorDesktopState
extends State<FilamentGestureDetectorDesktop> {
///
///
///
// ignore: unused_field
final bool _scaling = false;
bool _pointerMoving = false;
Gizmo get _gizmo => widget.controller.scene.gizmo;
@override
void initState() {
super.initState();
}
@override
void didUpdateWidget(FilamentGestureDetectorDesktop oldWidget) {
if (widget.showControlOverlay != oldWidget.showControlOverlay ||
widget.enableCamera != oldWidget.enableCamera ||
widget.enablePicking != oldWidget.enablePicking) {
setState(() {});
}
super.didUpdateWidget(oldWidget);
}
Timer? _scrollTimer;
///
/// Scroll-wheel on desktop, interpreted as zoom
///
void _zoom(PointerScrollEvent pointerSignal) async {
_scrollTimer?.cancel();
await widget.controller.zoomBegin();
await widget.controller.zoomUpdate(
pointerSignal.localPosition.dx,
pointerSignal.localPosition.dy,
pointerSignal.scrollDelta.dy > 0 ? 1 : -1);
// we don't want to end the zoom in the same frame, because this will destroy the camera manipulator (and cancel the zoom update).
// here, we just defer calling [zoomEnd] for 100ms to ensure the update is propagated through.
_scrollTimer = Timer(const Duration(milliseconds: 100), () async {
await widget.controller.zoomEnd();
});
}
Timer? _pickTimer;
@override
Widget build(BuildContext context) {
return Listener(
// onPointerHover: (event) async {
// if (_gizmo.isActive) {
// return;
// }
// _pickTimer?.cancel();
// _pickTimer = Timer(const Duration(milliseconds: 100), () async {
// widget.controller
// .pick(event.position.dx.toInt(), event.position.dy.toInt());
// });
// },
onPointerSignal: (PointerSignalEvent pointerSignal) async {
if (pointerSignal is PointerScrollEvent) {
if (widget.enableCamera) {
_zoom(pointerSignal);
}
} else {
throw Exception("TODO");
}
},
onPointerPanZoomStart: (pzs) {
throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
},
onPointerDown: (d) async {
if (_gizmo.isActive) {
return;
}
if (d.buttons != kTertiaryButton && widget.enablePicking) {
widget.controller
.pick(d.localPosition.dx.toInt(), d.localPosition.dy.toInt());
}
_pointerMoving = false;
},
// holding/moving the left mouse button is interpreted as a pan, middle mouse button as a rotate
onPointerMove: (PointerMoveEvent d) async {
if (_gizmo.isActive) {
_gizmo.translate(d.delta);
return;
}
// if this is the first move event, we need to call rotateStart/panStart to set the first coordinates
if (!_pointerMoving) {
if (d.buttons == kTertiaryButton && widget.enableCamera) {
widget.controller
.rotateStart(d.localPosition.dx, d.localPosition.dy);
} else if (widget.enableCamera) {
widget.controller
.panStart(d.localPosition.dx, d.localPosition.dy);
}
}
// set the _pointerMoving flag so we don't call rotateStart/panStart on future move events
_pointerMoving = true;
if (d.buttons == kTertiaryButton && widget.enableCamera) {
widget.controller
.rotateUpdate(d.localPosition.dx, d.localPosition.dy);
} else if (widget.enableCamera) {
widget.controller.panUpdate(d.localPosition.dx, d.localPosition.dy);
}
},
// when the left mouse button is released:
// 1) if _pointerMoving is true, this completes the pan
// 2) if _pointerMoving is false, this is interpreted as a pick
// same applies to middle mouse button, but this is ignored as a pick
onPointerUp: (PointerUpEvent d) async {
if (_gizmo.isActive) {
_gizmo.reset();
return;
}
if (d.buttons == kTertiaryButton && widget.enableCamera) {
widget.controller.rotateEnd();
} else {
if (_pointerMoving && widget.enableCamera) {
widget.controller.panEnd();
}
}
_pointerMoving = false;
},
child: widget.child);
}
}

View File

@@ -0,0 +1,234 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../filament_controller.dart';
enum GestureType { rotateCamera, panCamera, panBackground }
///
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
///
class FilamentGestureDetectorMobile extends StatefulWidget {
///
/// The content to display below the gesture detector/listener widget.
/// This will usually be a FilamentWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentController].
///
final Widget? child;
///
/// The [controller] attached to the [FilamentWidget] you wish to control.
///
final FilamentController controller;
///
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
/// 1) rotate or a pan (mobile only),
/// 2) moving the camera or the background image (TODO).
///
final bool showControlOverlay;
///
/// If false, gestures will not manipulate the active camera.
///
final bool enableCamera;
///
/// If false, pointer down events will not trigger hit-testing (picking).
///
final bool enablePicking;
final double zoomDelta;
final void Function(ScaleStartDetails)? onScaleStart;
final void Function(ScaleUpdateDetails)? onScaleUpdate;
final void Function(ScaleEndDetails)? onScaleEnd;
const FilamentGestureDetectorMobile(
{Key? key,
required this.controller,
this.child,
this.showControlOverlay = false,
this.enableCamera = true,
this.enablePicking = true,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.zoomDelta = 1})
: super(key: key);
@override
State<StatefulWidget> createState() => _FilamentGestureDetectorMobileState();
}
class _FilamentGestureDetectorMobileState
extends State<FilamentGestureDetectorMobile> {
GestureType gestureType = GestureType.panCamera;
final _icons = {
GestureType.panBackground: Icons.image,
GestureType.panCamera: Icons.pan_tool,
GestureType.rotateCamera: Icons.rotate_90_degrees_ccw
};
// on mobile, we can't differentiate between pointer down events like we do on desktop with primary/secondary/tertiary buttons
// we allow the user to toggle between panning and rotating by double-tapping the widget
bool _rotateOnPointerMove = false;
//
//
//
bool _scaling = false;
// to avoid duplicating code for pan/rotate (panStart, panUpdate, panEnd, rotateStart, rotateUpdate etc)
// we have only a single function for start/update/end.
// when the gesture type is changed, these properties are updated to point to the correct function.
// ignore: unused_field
late Function(double x, double y) _functionStart;
// ignore: unused_field
late Function(double x, double y) _functionUpdate;
// ignore: unused_field
late Function() _functionEnd;
@override
void initState() {
_setFunction();
super.initState();
}
void _setFunction() {
switch (gestureType) {
case GestureType.rotateCamera:
_functionStart = widget.controller.rotateStart;
_functionUpdate = widget.controller.rotateUpdate;
_functionEnd = widget.controller.rotateEnd;
break;
case GestureType.panCamera:
_functionStart = widget.controller.panStart;
_functionUpdate = widget.controller.panUpdate;
_functionEnd = widget.controller.panEnd;
break;
// TODO
case GestureType.panBackground:
_functionStart = (x, y) async {};
_functionUpdate = (x, y) async {};
_functionEnd = () async {};
}
}
@override
void didUpdateWidget(FilamentGestureDetectorMobile oldWidget) {
if (widget.showControlOverlay != oldWidget.showControlOverlay ||
widget.enableCamera != oldWidget.enableCamera ||
widget.enablePicking != oldWidget.enablePicking) {
setState(() {});
}
super.didUpdateWidget(oldWidget);
}
// ignore: unused_field
Timer? _scrollTimer;
double _lastScale = 0;
// pinch zoom on mobile
// couldn't find any equivalent for pointerCount in Listener (?) so we use a GestureDetector
@override
Widget build(BuildContext context) {
return Stack(children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (d) {
if (!widget.enablePicking) {
return;
}
widget.controller.pick(
d.globalPosition.dx.toInt(), d.globalPosition.dy.toInt());
},
onDoubleTap: () {
setState(() {
_rotateOnPointerMove = !_rotateOnPointerMove;
});
},
onScaleStart: (d) async {
if (widget.onScaleStart != null) {
widget.onScaleStart!.call(d);
return;
}
if (d.pointerCount == 2 && widget.enableCamera) {
_scaling = true;
await widget.controller.zoomBegin();
} else if (!_scaling && widget.enableCamera) {
if (_rotateOnPointerMove) {
widget.controller.rotateStart(
d.localFocalPoint.dx, d.localFocalPoint.dy);
} else {
widget.controller
.panStart(d.localFocalPoint.dx, d.localFocalPoint.dy);
}
}
},
onScaleUpdate: (ScaleUpdateDetails d) async {
if (widget.onScaleUpdate != null) {
widget.onScaleUpdate!.call(d);
return;
}
if (d.pointerCount == 2 && widget.enableCamera) {
if (d.horizontalScale != _lastScale) {
widget.controller.zoomUpdate(
d.localFocalPoint.dx,
d.localFocalPoint.dy,
d.horizontalScale > _lastScale ? 0.1 : -0.1);
_lastScale = d.horizontalScale;
}
} else if (!_scaling && widget.enableCamera) {
if (_rotateOnPointerMove) {
widget.controller
.rotateUpdate(d.focalPoint.dx, d.focalPoint.dy);
} else {
widget.controller
.panUpdate(d.focalPoint.dx, d.focalPoint.dy);
}
}
},
onScaleEnd: (d) async {
if (widget.onScaleEnd != null) {
widget.onScaleEnd!.call(d);
return;
}
if (d.pointerCount == 2 && widget.enableCamera) {
widget.controller.zoomEnd();
} else if (!_scaling && widget.enableCamera) {
if (_rotateOnPointerMove) {
widget.controller.rotateEnd();
} else {
widget.controller.panEnd();
}
}
_scaling = false;
},
child: widget.child)),
widget.showControlOverlay
? Align(
alignment: Alignment.bottomRight,
child: GestureDetector(
onTap: () {
setState(() {
var curIdx = GestureType.values.indexOf(gestureType);
var nextIdx = curIdx == GestureType.values.length - 1
? 0
: curIdx + 1;
gestureType = GestureType.values[nextIdx];
_setFunction();
});
},
child: Container(
padding: const EdgeInsets.all(50),
child: Icon(_icons[gestureType], color: Colors.green)),
))
: Container()
]);
}
}

View File

@@ -0,0 +1,309 @@
import 'dart:io';
import 'dart:math';
import 'dart:developer' as dev;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'dart:async';
typedef ResizeCallback = void Function(Size newSize);
class ResizeObserver extends SingleChildRenderObjectWidget {
final ResizeCallback onResized;
const ResizeObserver({
Key? key,
required this.onResized,
Widget? child,
}) : super(
key: key,
child: child,
);
@override
RenderObject createRenderObject(BuildContext context) =>
_RenderResizeObserver(onLayoutChangedCallback: onResized);
}
class _RenderResizeObserver extends RenderProxyBox {
final ResizeCallback onLayoutChangedCallback;
_RenderResizeObserver({
RenderBox? child,
required this.onLayoutChangedCallback,
}) : super(child);
Size _oldSize = Size.zero;
@override
void performLayout() async {
super.performLayout();
if (size.width != _oldSize.width || size.height != _oldSize.height) {
onLayoutChangedCallback(size);
_oldSize = Size(size.width, size.height);
}
}
}
class FilamentWidget extends StatefulWidget {
final FilamentController controller;
///
/// The content to render before the texture widget is available.
/// The default is a solid red Container, intentionally chosen to make it clear that there will be at least one frame where the Texture widget is not being rendered.
///
final Widget? initial;
const FilamentWidget({Key? key, required this.controller, this.initial})
: super(key: key);
@override
_FilamentWidgetState createState() => _FilamentWidgetState();
}
class _FilamentWidgetState extends State<FilamentWidget> {
int? _width;
int? _height;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
var size = ((context.findRenderObject()) as RenderBox).size;
_width = size.width.ceil();
_height = size.height.ceil();
setState(() {});
});
super.initState();
}
@override
Widget build(BuildContext context) {
if (_width == null || _height == null) {
return widget.initial ?? Container(color: Colors.red);
}
return ResizeObserver(
onResized: (newSize) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
_width = newSize.width.ceil();
_height = newSize.height.ceil();
});
});
},
child: _SizedFilamentWidget(
initial: widget.initial,
width: _width!,
height: _height!,
controller: widget.controller,
));
}
}
class _SizedFilamentWidget extends StatefulWidget {
final int width;
final int height;
final Widget? initial;
final FilamentController controller;
const _SizedFilamentWidget(
{required this.width,
required this.height,
this.initial,
required this.controller});
@override
State<StatefulWidget> createState() => _SizedFilamentWidgetState();
}
class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
String? _error;
late final AppLifecycleListener _appLifecycleListener;
Rect get _rect {
final renderBox = (context.findRenderObject()) as RenderBox;
final size = renderBox.size;
final translation = renderBox.getTransformTo(null).getTranslation();
return Rect.fromLTWH(translation.x, translation.y, size.width, size.height);
}
@override
void initState() {
_appLifecycleListener = AppLifecycleListener(
onStateChange: _handleStateChange,
);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
try {
widget.controller
.setDimensions(_rect, MediaQuery.of(context).devicePixelRatio);
} catch (err) {
dev.log("Fatal error : $err");
_error = err.toString();
}
});
super.initState();
}
Timer? _resizeTimer;
bool _resizing = false;
Future _resize() async {
final completer = Completer();
// resizing the window can be sluggish (particular in debug mode), exacerbated when simultaneously recreating the swapchain and resize the window.
// to address this, whenever the widget is resized, we set a timer for Xms in the future.
// this timer will call [resize] with the widget size at that point in time.
// any subsequent widget resizes will cancel the timer and replace with a new one.
// debug mode does need a longer timeout.
_resizeTimer?.cancel();
await widget.controller
.setDimensions(_rect, MediaQuery.of(context).devicePixelRatio);
_resizeTimer = Timer(
Duration(milliseconds: (kReleaseMode || Platform.isWindows) ? 10 : 100),
() async {
try {
while (_resizing) {
await Future.delayed(const Duration(milliseconds: 20));
}
_resizing = true;
await widget.controller
.setDimensions(_rect, MediaQuery.of(context).devicePixelRatio);
await widget.controller.resize();
_resizeTimer = null;
setState(() {});
} catch (err) {
dev.log("Error resizing FilamentWidget: $err");
} finally {
_resizing = false;
completer.complete();
_resizeTimer?.cancel();
}
});
return completer.future;
}
@override
void didUpdateWidget(_SizedFilamentWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.height != oldWidget.height || widget.width != oldWidget.width) {
_resize();
}
}
@override
void dispose() {
_appLifecycleListener.dispose();
super.dispose();
}
bool _wasRenderingOnInactive = true;
void _handleStateChange(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.detached:
dev.log("Detached");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.hidden:
dev.log("Hidden");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.inactive:
dev.log("Inactive");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
// on Windows in particular, restoring a window after minimizing stalls the renderer (and the whole application) for a considerable length of time.
// disabling rendering on minimize seems to fix the issue (so I wonder if there's some kind of command buffer that's filling up while the window is minimized).
await widget.controller.setRendering(false);
break;
case AppLifecycleState.paused:
dev.log("Paused");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.resumed:
dev.log("Resumed");
await widget.controller.setRendering(_wasRenderingOnInactive);
break;
}
}
@override
Widget build(BuildContext context) {
// if an error was encountered in creating a viewer, display the error message and don't even try to display a Texture widget.
if (_error != null) {
return Container(
color: Colors.white,
child: Column(children: [
const Text("A fatal error was encountered"),
Text(_error!)
]));
}
if (!widget.controller.requiresTextureWidget) {
return Stack(children: [
Positioned.fill(child: CustomPaint(painter: TransparencyPainter()))
]);
}
return ListenableBuilder(
listenable: widget.controller.textureDetails,
builder: (BuildContext ctx, Widget? wdgt) {
if (widget.controller.textureDetails.value == null) {
return Stack(children: [
Positioned.fill(
child: widget.initial ?? Container(color: Colors.red))
]);
}
// see [FilamentControllerFFI.resize] for an explanation of how we deal with resizing
var texture = Texture(
key: ObjectKey(
"texture_${widget.controller.textureDetails.value!.textureId}"),
textureId: widget.controller.textureDetails.value!.textureId,
filterQuality: FilterQuality.none,
freeze: false,
);
return Stack(children: [
Positioned.fill(
child: Platform.isLinux || Platform.isWindows
? Transform(
alignment: Alignment.center,
transform: Matrix4.rotationX(
pi), // TODO - this rotation is due to OpenGL texture coordinate working in a different space from Flutter, can we move this to the C++ side somewhere?
child: texture)
: texture)
]);
});
}
}
class TransparencyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()
..blendMode = BlendMode.clear
..color = const Color(0x00000000),
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -0,0 +1,31 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'package:vector_math/vector_math_64.dart' as v;
class IblRotationSliderWidget extends StatefulWidget {
final FilamentController controller;
const IblRotationSliderWidget({super.key, required this.controller});
@override
State<StatefulWidget> createState() => _IblRotationSliderWidgetState();
}
class _IblRotationSliderWidgetState extends State<IblRotationSliderWidget> {
double _iblRotation = 0;
@override
Widget build(BuildContext context) {
return Slider(
value: _iblRotation,
onChanged: (value) {
_iblRotation = value;
setState(() {});
print(value);
var rotation = v.Matrix3.identity();
Matrix4.rotationY(value * 2 * pi).copyRotation(rotation);
widget.controller.rotateIbl(rotation);
});
}
}

View File

@@ -0,0 +1,193 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'package:flutter_filament/filament/utils/light_options.dart';
import 'package:vector_math/vector_math_64.dart' as v;
class LightSliderWidget extends StatefulWidget {
final FilamentController controller;
final LightOptions options;
final bool showControls;
LightSliderWidget(
{super.key,
required this.controller,
this.showControls = false,
required this.options});
@override
State<StatefulWidget> createState() => _LightSliderWidgetState();
}
class _LightSliderWidgetState extends State<LightSliderWidget> {
FilamentEntity? _light;
@override
void initState() {
_set();
super.initState();
}
Future _set() async {
await widget.controller.clearLights();
if (widget.options.iblPath != null) {
_light = await widget.controller.loadIbl(widget.options.iblPath!,
intensity: widget.options.iblIntensity);
}
_light = await widget.controller.addLight(
widget.options.directionalType,
widget.options.directionalColor,
widget.options.directionalIntensity,
widget.options.directionalPosition.x,
widget.options.directionalPosition.y,
widget.options.directionalPosition.z,
widget.options.directionalDirection.x,
widget.options.directionalDirection.y,
widget.options.directionalDirection.z,
widget.options.directionalCastShadows);
setState(() {});
}
@override
Widget build(BuildContext context) {
if (_light == null || !widget.showControls) {
return Container();
}
return Theme(
data: ThemeData(platform: TargetPlatform.android),
child: Container(
decoration: BoxDecoration(color: Colors.white.withOpacity(0.5)),
child: SliderTheme(
data: SliderThemeData(
showValueIndicator: ShowValueIndicator.always,
valueIndicatorTextStyle: TextStyle(color: Colors.black)),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text("Directional"),
Row(children: [
Expanded(
child: Slider(
label:
"POSX ${widget.options.directionalPosition.x}",
value: widget.options.directionalPosition.x,
min: -10.0,
max: 10.0,
onChanged: (value) {
widget.options.directionalPosition.x = value;
_set();
})),
Expanded(
child: Slider(
label:
"POSY ${widget.options.directionalPosition.y}",
value: widget.options.directionalPosition.y,
min: -100.0,
max: 100.0,
onChanged: (value) {
widget.options.directionalPosition.y = value;
_set();
})),
Expanded(
child: Slider(
label:
"POSZ ${widget.options.directionalPosition.z}",
value: widget.options.directionalPosition.z,
min: -100.0,
max: 100.0,
onChanged: (value) {
widget.options.directionalPosition.z = value;
_set();
}))
]),
Row(children: [
Expanded(
child: Slider(
label: "DIRX",
value: widget.options.directionalDirection.x,
min: -1.0,
max: 1.0,
onChanged: (value) {
widget.options.directionalDirection.x = value;
_set();
})),
Expanded(
child: Slider(
label: "DIRY",
value: widget.options.directionalDirection.y,
min: -1.0,
max: 1.0,
onChanged: (value) {
widget.options.directionalDirection.y = value;
_set();
})),
Expanded(
child: Slider(
label: "DIRZ",
value: widget.options.directionalDirection.z,
min: -1.0,
max: 1.0,
onChanged: (value) {
widget.options.directionalDirection.z = value;
_set();
}))
]),
Slider(
label: "Color",
value: widget.options.directionalColor,
min: 0,
max: 16000,
onChanged: (value) {
widget.options.directionalColor = value;
_set();
}),
Slider(
label: "Intensity ${widget.options.directionalIntensity}",
value: widget.options.directionalIntensity,
min: 0,
max: 1000000,
onChanged: (value) {
widget.options.directionalIntensity = value;
_set();
}),
DropdownButton(
onChanged: (v) {
this.widget.options.directionalType = v;
_set();
},
value: this.widget.options.directionalType,
items: List<DropdownMenuItem>.generate(
5,
(idx) => DropdownMenuItem(
value: idx,
child: Text("$idx"),
))),
Row(children: [
Text(
"Shadows: ${this.widget.options.directionalCastShadows}"),
Checkbox(
value: widget.options.directionalCastShadows,
onChanged: (v) {
this.widget.options.directionalCastShadows = v!;
_set();
})
]),
Text("Indirect"),
Row(children: [
Expanded(
child: Slider(
label: "Intensity ${widget.options.iblIntensity}",
value: widget.options.iblIntensity,
min: 0.0,
max: 200000,
onChanged: (value) {
widget.options.iblIntensity = value;
_set();
})),
])
]))));
}
}