(flutter) provide nicer implementation of FixedOrbitCameraRotationDelegate

This commit is contained in:
Nick Fisher
2024-09-25 19:26:19 +08:00
parent 4b1d8ce729
commit 85d6946645
2 changed files with 86 additions and 84 deletions

View File

@@ -50,20 +50,17 @@ class DelegateGestureHandler implements ThermionGestureHandler {
} }
factory DelegateGestureHandler.fixedOrbit(ThermionViewer viewer, factory DelegateGestureHandler.fixedOrbit(ThermionViewer viewer,
{double? Function(Vector3)? getDistanceToTarget, {double minimumDistance = 10.0,
double rotationSensitivity = 0.001, double? Function(Vector3)? getDistanceToTarget,
double zoomSensitivity = 0.001,
double baseAnglePerMeterNumerator = 10000,
PickDelegate? pickDelegate}) => PickDelegate? pickDelegate}) =>
DelegateGestureHandler( DelegateGestureHandler(
viewer: viewer, viewer: viewer,
pickDelegate: pickDelegate, pickDelegate: pickDelegate,
cameraDelegate: FixedOrbitRotateCameraDelegate(viewer, cameraDelegate: FixedOrbitRotateCameraDelegate(viewer,
getDistanceToTarget: getDistanceToTarget, getDistanceToTarget: getDistanceToTarget,
rotationSensitivity: rotationSensitivity, minimumDistance: minimumDistance),
baseAnglePerMeterNumerator: baseAnglePerMeterNumerator,
zoomSensitivity: zoomSensitivity),
velocityDelegate: DefaultVelocityDelegate(), velocityDelegate: DefaultVelocityDelegate(),
actions: {GestureType.MMB_HOLD_AND_MOVE:GestureAction.ROTATE_CAMERA}
); );
factory DelegateGestureHandler.flight(ThermionViewer viewer, factory DelegateGestureHandler.flight(ThermionViewer viewer,
@@ -193,6 +190,7 @@ class DelegateGestureHandler implements ThermionGestureHandler {
} catch (e) { } catch (e) {
_logger.warning("Error during scroll accumulation: $e"); _logger.warning("Error during scroll accumulation: $e");
} }
await _applyAccumulatedUpdates();
} }
@override @override

View File

@@ -1,48 +1,36 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'package:flutter/services.dart';
import 'package:flutter/src/services/keyboard_key.g.dart';
import 'package:flutter/widgets.dart';
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
/// A camera delegate that rotates the camera around the origin.
/// Panning is not permitted; zooming is permitted (up to a minimum distance)
///
/// The rotation sensitivity will be automatically adjusted so that
/// 100 horizontal pixels equates to a geodetic distance of 1m when the camera
/// is 1m from the surface (denoted by distanceToSurface). This scales to 10m
/// geodetic distance when the camera is 100m from the surface, 100m when the
/// camera is 1000m from the surface, and so on.
///
///
class FixedOrbitRotateCameraDelegate implements CameraDelegate { class FixedOrbitRotateCameraDelegate implements CameraDelegate {
final ThermionViewer viewer; final ThermionViewer viewer;
final double minimumDistance;
double rotationSensitivity = 0.01; double? Function(Vector3)? getDistanceToTarget;
late DefaultZoomCameraDelegate _zoomCameraDelegate;
Offset _accumulatedRotationDelta = Offset.zero; Offset _accumulatedRotationDelta = Offset.zero;
double _accumulatedZoomDelta = 0.0; double _accumulatedZoomDelta = 0.0;
static final _up = Vector3(0, 1, 0); static final _up = Vector3(0, 1, 0);
Timer? _updateTimer; Timer? _updateTimer;
Vector3 _targetPosition = Vector3(0, 0, 0); FixedOrbitRotateCameraDelegate(
this.viewer, {
double? Function(Vector3)? getDistanceToTarget; this.getDistanceToTarget,
this.minimumDistance = 10.0,
FixedOrbitRotateCameraDelegate(this.viewer, });
{this.getDistanceToTarget,
double? rotationSensitivity,
double zoomSensitivity = 0.005}) {
_zoomCameraDelegate = DefaultZoomCameraDelegate(this.viewer,
zoomSensitivity: zoomSensitivity,
getDistanceToTarget: getDistanceToTarget);
this.rotationSensitivity = rotationSensitivity ?? 0.01;
_startUpdateTimer();
}
void _startUpdateTimer() {
_updateTimer = Timer.periodic(const Duration(milliseconds: 16), (_) {
_applyAccumulatedUpdates();
});
}
void dispose() { void dispose() {
_updateTimer?.cancel(); _updateTimer?.cancel();
@@ -51,6 +39,7 @@ class FixedOrbitRotateCameraDelegate implements CameraDelegate {
@override @override
Future<void> rotate(Offset delta, Vector2? velocity) async { Future<void> rotate(Offset delta, Vector2? velocity) async {
_accumulatedRotationDelta += delta; _accumulatedRotationDelta += delta;
await _applyAccumulatedUpdates();
} }
@override @override
@@ -60,11 +49,8 @@ class FixedOrbitRotateCameraDelegate implements CameraDelegate {
@override @override
Future<void> zoom(double yScrollDeltaInPixels, Vector2? velocity) async { Future<void> zoom(double yScrollDeltaInPixels, Vector2? velocity) async {
if (yScrollDeltaInPixels > 1) { _accumulatedZoomDelta += yScrollDeltaInPixels > 0 ? 1 : -1;
_accumulatedZoomDelta++; await _applyAccumulatedUpdates();
} else {
_accumulatedZoomDelta--;
}
} }
Future<void> _applyAccumulatedUpdates() async { Future<void> _applyAccumulatedUpdates() async {
@@ -73,64 +59,82 @@ class FixedOrbitRotateCameraDelegate implements CameraDelegate {
return; return;
} }
var viewMatrix = await viewer.getCameraViewMatrix();
var modelMatrix = await viewer.getCameraModelMatrix(); var modelMatrix = await viewer.getCameraModelMatrix();
Vector3 cameraPosition = modelMatrix.getTranslation(); var projectionMatrix = await viewer.getCameraProjectionMatrix();
var inverseProjectionMatrix = projectionMatrix.clone()..invert();
Vector3 currentPosition = modelMatrix.getTranslation();
final heightAboveSurface = getDistanceToTarget?.call(cameraPosition) ?? 1.0; Vector3 forward = -currentPosition.normalized();
Vector3 right = _up.cross(forward).normalized();
Vector3 up = forward.cross(right);
final sphereRadius = cameraPosition.length - heightAboveSurface; // first, we find the point in the sphere that intersects with the camera
// forward vector
// Apply rotation double radius = 0.0;
if (_accumulatedRotationDelta.distanceSquared > 0) { double? distanceToTarget = getDistanceToTarget?.call(currentPosition);
// Calculate the distance factor if (distanceToTarget != null) {
final distanceFactor = sqrt((heightAboveSurface / sphereRadius) + 1); radius = currentPosition.length - distanceToTarget;
} else {
// Adjust the base angle per meter radius = 1.0;
final baseAnglePerMeter = 10000 / sphereRadius;
final adjustedAnglePerMeter = baseAnglePerMeter * distanceFactor;
final metersOnSurface = _accumulatedRotationDelta;
final rotationX = metersOnSurface.dy * adjustedAnglePerMeter;
final rotationY = metersOnSurface.dx * adjustedAnglePerMeter;
Matrix4 rotation = Matrix4.rotationX(rotationX)..rotateY(rotationY);
Vector3 newPos = rotation.getRotation() * cameraPosition;
cameraPosition = newPos;
} }
Vector3 intersection = (-forward).scaled(radius);
// Normalize the position to maintain constant distance from center // next, calculate the depth value at that intersection point
cameraPosition = final intersectionInViewSpace = viewMatrix *
cameraPosition.normalized() * (sphereRadius + heightAboveSurface); Vector4(intersection.x, intersection.y, intersection.z, 1.0);
final intersectionInClipSpace = projectionMatrix * intersectionInViewSpace;
final intersectionInNdcSpace =
intersectionInClipSpace / intersectionInClipSpace.w;
// Apply zoom (modified to ensure minimum 10m distance) // using that depth value, find the world space position of the mouse
// note we flip the signs of the X and Y values
final ndcX = 2 *
((-_accumulatedRotationDelta.dx * viewer.pixelRatio) /
viewer.viewportDimensions.$1);
final ndcY = 2 *
((_accumulatedRotationDelta.dy * viewer.pixelRatio) /
viewer.viewportDimensions.$2);
final ndc = Vector4(ndcX, ndcY, intersectionInNdcSpace.z, 1.0);
var clipSpace = Vector4(
ndc.x * intersectionInClipSpace.w,
ndcY * intersectionInClipSpace.w,
ndc.z * intersectionInClipSpace.w,
intersectionInClipSpace.w);
Vector4 cameraSpace = inverseProjectionMatrix * clipSpace;
Vector4 worldSpace = modelMatrix * cameraSpace;
// the new camera world space position will be that position,
// scaled to the camera's current distance
var worldSpace3 = worldSpace.xyz.normalized() * currentPosition.length;
currentPosition = worldSpace3;
// Apply zoom
if (_accumulatedZoomDelta != 0.0) { if (_accumulatedZoomDelta != 0.0) {
var zoomFactor = -0.5 * _accumulatedZoomDelta; // double zoomFactor = 1.0 + ();
Vector3 toSurface = currentPosition - intersection;
double newHeight = heightAboveSurface * (1 - zoomFactor); currentPosition = currentPosition + toSurface.scaled(_accumulatedZoomDelta * 0.1);
newHeight = newHeight.clamp(
10.0, double.infinity); // Prevent getting closer than 10m to surface
cameraPosition = cameraPosition.normalized() * (sphereRadius + newHeight);
_accumulatedZoomDelta = 0.0; _accumulatedZoomDelta = 0.0;
} }
// Ensure minimum 10m distance even after rotation // Ensure minimum distance
final currentHeight = cameraPosition.length - sphereRadius; if (currentPosition.length < radius + minimumDistance) {
if (currentHeight < 10.0) { currentPosition =
cameraPosition = cameraPosition.normalized() * (sphereRadius + 10.0); (currentPosition.normalized() * (radius + minimumDistance));
} }
// Calculate view matrix (unchanged) // Calculate view matrix
Vector3 forward = cameraPosition.normalized(); forward = -currentPosition.normalized();
Vector3 up = Vector3(0, 1, 0); right = _up.cross(forward).normalized();
final right = up.cross(forward)..normalize();
up = forward.cross(right); up = forward.cross(right);
Matrix4 viewMatrix = makeViewMatrix(cameraPosition, Vector3.zero(), up); Matrix4 newViewMatrix = makeViewMatrix(currentPosition, Vector3.zero(), up);
viewMatrix.invert(); newViewMatrix.invert();
// Set the camera model matrix // Set the camera model matrix
await viewer.setCameraModelMatrix4(viewMatrix); await viewer.setCameraModelMatrix4(newViewMatrix);
_accumulatedRotationDelta = Offset.zero; _accumulatedRotationDelta = Offset.zero;
} }