Bug 1026023 - Part 2: Implement Physically Based Movement Model. r=mwoodrow
authorKearwood (Kip) Gilbert <kgilbert@mozilla.com>
Wed, 09 Jul 2014 10:02:29 -0700
changeset 218395 1290a934987f8a23f6c509b0a821323cd960e04f
parent 218394 619bf9ab665a91b474730d6208fcef7ee3b3afe8
child 218396 91ec42d7d9e8aaab80e32fa3765f47ffe05b3523
push idunknown
push userunknown
push dateunknown
reviewersmwoodrow
bugs1026023
milestone34.0a1
Bug 1026023 - Part 2: Implement Physically Based Movement Model. r=mwoodrow - Implemented the AxisPhysicsModel class, which encapsulates interpolation and integration of a 1-dimensional physics model in a frame-rate independent and stable manner. - Implemented the AxisPhysicsMSDModel class, which models a generic 1-dimensional Mass-Spring-Damper simulation. - This physical movement simulation code has been implemented separately from the existing momentum code in Axis.cpp so that it can be utilized by both the compositor (AsyncPanZoomController) and main thread (nsGfxScrollFrame).
gfx/layers/AxisPhysicsMSDModel.cpp
gfx/layers/AxisPhysicsMSDModel.h
gfx/layers/AxisPhysicsModel.cpp
gfx/layers/AxisPhysicsModel.h
gfx/layers/moz.build
new file mode 100644
--- /dev/null
+++ b/gfx/layers/AxisPhysicsMSDModel.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AxisPhysicsMSDModel.h"
+#include <math.h>                       // for sqrt and fabs
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Constructs an AxisPhysicsMSDModel with initial values for state.
+ *
+ * @param aInitialPosition sets the initial position of the simulated spring,
+ *        in AppUnits.
+ * @param aInitialDestination sets the resting position of the simulated spring,
+ *        in AppUnits.
+ * @param aInitialVelocity sets the initial velocity of the simulated spring,
+ *        in AppUnits / second.  Critically-damped and over-damped systems are
+ *        guaranteed not to overshoot aInitialDestination if this is set to 0;
+ *        however, it is possible to overshoot and oscillate if not set to 0 or
+ *        the system is under-damped.
+ * @param aSpringConstant sets the strength of the simulated spring.  Greater
+ *        values of mSpringConstant result in a stiffer / stronger spring.
+ * @param aDampingRatio controls the amount of dampening force and determines
+ *        if the system is under-damped, critically-damped, or over-damped.
+ */
+AxisPhysicsMSDModel::AxisPhysicsMSDModel(double aInitialPosition,
+                                         double aInitialDestination,
+                                         double aInitialVelocity,
+                                         double aSpringConstant,
+                                         double aDampingRatio)
+  : AxisPhysicsModel(aInitialPosition, aInitialVelocity)
+  , mDestination(aInitialDestination)
+  , mSpringConstant(aSpringConstant)
+  , mSpringConstantSqrtXTwo(sqrt(mSpringConstant) * 2.0)
+  , mDampingRatio(aDampingRatio)
+{
+}
+
+AxisPhysicsMSDModel::~AxisPhysicsMSDModel()
+{
+}
+
+double
+AxisPhysicsMSDModel::Acceleration(const State &aState)
+{
+  // Simulate a Mass-Damper-Spring Model; assume a unit mass
+
+  // Hooke’s Law: http://en.wikipedia.org/wiki/Hooke%27s_law
+  double spring_force = (mDestination - aState.p) * mSpringConstant;
+  double damp_force = -aState.v * mDampingRatio * mSpringConstantSqrtXTwo;
+
+  return spring_force + damp_force;
+}
+
+
+double
+AxisPhysicsMSDModel::GetDestination()
+{
+  return mDestination;
+}
+
+void
+AxisPhysicsMSDModel::SetDestination(double aDestination)
+{
+  mDestination = aDestination;
+}
+
+bool
+AxisPhysicsMSDModel::IsFinished()
+{
+  // In order to satisfy the condition of reaching the destination, the distance
+  // between the simulation position and the destination must be less than
+  // kFinishDistance while the speed is simultaneously less than
+  // kFinishVelocity.  This enables an under-damped system to overshoot the
+  // destination when desired without prematurely triggering the finished state.
+
+  // As the number of app units per css pixel is 60 and retina / HiDPI displays
+  // may display two pixels for every css pixel, setting kFinishDistance to 30.0
+  // ensures that there will be no perceptable shift in position at the end
+  // of the animation.
+  const double kFinishDistance = 30.0;
+
+  // If kFinishVelocity is set too low, the animation may end long after
+  // oscillation has finished, resulting in unnecessary processing.
+  // If set too high, the animation may prematurely terminate when expected
+  // to overshoot the destination in an under-damped system.
+  // 60.0 was selected through experimentation that revealed that a
+  // critically damped system will terminate within 100ms.
+  const double kFinishVelocity = 60.0;
+
+  return fabs(mDestination - GetPosition ()) < kFinishDistance
+    && fabs(GetVelocity()) <= kFinishVelocity;
+}
+
+}
+}
new file mode 100644
--- /dev/null
+++ b/gfx/layers/AxisPhysicsMSDModel.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AxisPhysicsMSDModel_h
+#define mozilla_layers_AxisPhysicsMSDModel_h
+
+#include "AxisPhysicsModel.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * AxisPhysicsMSDModel encapsulates a 1-dimensional MSD (Mass-Spring-Damper)
+ * model.  A unit mass is assumed.
+ */
+class AxisPhysicsMSDModel : public AxisPhysicsModel {
+public:
+  AxisPhysicsMSDModel(double aInitialPosition, double aInitialDestination,
+                      double aInitialVelocity, double aSpringConstant,
+                      double aDampingRatio);
+
+  ~AxisPhysicsMSDModel();
+
+  /**
+   * Gets the raw destination of this axis at this moment.
+   */
+  double GetDestination();
+
+  /**
+   * Sets the raw destination of this axis at this moment.
+   */
+  void SetDestination(double aDestination);
+
+  /**
+   * Returns true when the position is close to the destination and the
+   * velocity is low.
+   */
+  bool IsFinished();
+
+protected:
+  virtual double Acceleration(const State &aState);
+
+private:
+
+  /**
+   * mDestination represents the target position and the resting position of
+   * the simulated spring.
+   */
+  double mDestination;
+
+  /**
+   * Greater values of mSpringConstant result in a stiffer / stronger spring.
+   */
+  double mSpringConstant;
+
+  /**
+   * mSpringConstantSqrtTimesTwo is calculated from mSpringConstant to reduce
+   * calculations performed in the inner loop.
+   */
+  double mSpringConstantSqrtXTwo;
+
+  /**
+   * Damping Ratio: http://en.wikipedia.org/wiki/Damping_ratio
+   *
+   * When mDampingRatio < 1.0, this is an under damped system.
+   * - Overshoots destination and oscillates with the amplitude gradually
+   *   decreasing to zero.
+   *
+   * When mDampingRatio == 1.0, this is a critically damped system.
+   * - Reaches destination as quickly as possible without oscillating.
+   *
+   * When mDampingRatio > 1.0, this is an over damped system.
+   * - Reaches destination (exponentially decays) without oscillating.
+   */
+  double mDampingRatio;
+
+};
+
+
+}
+}
+
+#endif
new file mode 100644
--- /dev/null
+++ b/gfx/layers/AxisPhysicsModel.cpp
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AxisPhysicsModel.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The simulation is advanced forward in time with a fixed time step to ensure
+ * that it remains deterministic given variable framerates.  To determine the
+ * position at any variable time, two samples are interpolated.
+ *
+ * kFixedtimestep is set to 120hz in order to ensure that every frame in a
+ * common 60hz refresh rate display will have at least one physics simulation
+ * sample.  More accuracy can be obtained by reducing kFixedTimestep to smaller
+ * intervals, such as 240hz or 1000hz, at the cost of more CPU cycles.  If
+ * kFixedTimestep is increased to much longer intervals, interpolation will
+ * become less effective at reducing temporal jitter and the simulation will
+ * lose accuracy.
+ */
+const double AxisPhysicsModel::kFixedTimestep = 1.0 / 120.0; // 120hz
+
+/**
+ * Constructs an AxisPhysicsModel with initial values for state.
+ *
+ * @param aInitialPosition sets the initial position of the simulation,
+ *        in AppUnits.
+ * @param aInitialVelocity sets the initial velocity of the simulation,
+ *        in AppUnits / second.
+ */
+AxisPhysicsModel::AxisPhysicsModel(double aInitialPosition,
+                                   double aInitialVelocity)
+  : mProgress(1.0)
+  , mPrevState(aInitialPosition, aInitialVelocity)
+  , mNextState(aInitialPosition, aInitialVelocity)
+{
+
+}
+
+AxisPhysicsModel::~AxisPhysicsModel()
+{
+
+}
+
+double
+AxisPhysicsModel::GetVelocity()
+{
+  return LinearInterpolate(mPrevState.v, mNextState.v, mProgress);
+}
+
+double
+AxisPhysicsModel::GetPosition()
+{
+  return LinearInterpolate(mPrevState.p, mNextState.p, mProgress);
+}
+
+void
+AxisPhysicsModel::SetVelocity(double aVelocity)
+{
+  mNextState.v = aVelocity;
+  mNextState.p = GetPosition();
+  mProgress = 1.0;
+}
+
+void
+AxisPhysicsModel::SetPosition(double aPosition)
+{
+  mNextState.v = GetVelocity();
+  mNextState.p = aPosition;
+  mProgress = 1.0;
+}
+
+void
+AxisPhysicsModel::Simulate(const TimeDuration& aDeltaTime)
+{
+  for(mProgress += aDeltaTime.ToSeconds() / kFixedTimestep;
+      mProgress > 1.0; mProgress -= 1.0) {
+    Integrate(kFixedTimestep);
+  }
+}
+
+void
+AxisPhysicsModel::Integrate(double aDeltaTime)
+{
+  mPrevState = mNextState;
+
+  // RK4 (Runge-Kutta method) Integration
+  // http://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods
+  Derivative a = Evaluate( mNextState, 0.0, Derivative() );
+  Derivative b = Evaluate( mNextState, aDeltaTime * 0.5, a );
+  Derivative c = Evaluate( mNextState, aDeltaTime * 0.5, b );
+  Derivative d = Evaluate( mNextState, aDeltaTime, c );
+
+  double dpdt = 1.0 / 6.0 * (a.dp + 2.0 * (b.dp + c.dp) + d.dp);
+  double dvdt = 1.0 / 6.0 * (a.dv + 2.0 * (b.dv + c.dv) + d.dv);
+
+  mNextState.p += dpdt * aDeltaTime;
+  mNextState.v += dvdt * aDeltaTime;
+}
+
+AxisPhysicsModel::Derivative
+AxisPhysicsModel::Evaluate(const State &aInitState, double aDeltaTime,
+                           const Derivative &aDerivative)
+{
+  State state( aInitState.p + aDerivative.dp*aDeltaTime, aInitState.v + aDerivative.dv*aDeltaTime );
+
+  return Derivative( state.v, Acceleration(state) );
+}
+
+double
+AxisPhysicsModel::LinearInterpolate(double aV1, double aV2, double aBlend)
+{
+  return aV1 * (1.0 - aBlend) + aV2 * aBlend;
+}
+
+}
+}
new file mode 100644
--- /dev/null
+++ b/gfx/layers/AxisPhysicsModel.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AxisPhysicsModel_h
+#define mozilla_layers_AxisPhysicsModel_h
+
+#include "AxisPhysicsModel.h"
+#include <sys/types.h>                  // for int32_t
+#include "mozilla/TimeStamp.h"          // for TimeDuration
+
+namespace mozilla {
+namespace layers {
+
+
+/**
+ * AxisPhysicsModel encapsulates a generic 1-dimensional physically-based motion
+ * model.
+ *
+ * It performs frame-rate independent interpolation and RK4 integration for
+ * smooth animation with stable, deterministic behavior.
+ * Implementations are expected to subclass and override the Acceleration()
+ * method.
+ */
+class AxisPhysicsModel {
+public:
+  AxisPhysicsModel(double aInitialPosition, double aInitialVelocity);
+  ~AxisPhysicsModel();
+
+  /**
+   * Advance the physics simulation.
+   * |aDelta| is the time since the last sample.
+   */
+  void Simulate(const TimeDuration& aDeltaTime);
+
+  /**
+   * Gets the raw velocity of this axis at this moment.
+   */
+  double GetVelocity();
+
+  /**
+   * Sets the raw velocity of this axis at this moment.
+   */
+  void SetVelocity(double aVelocity);
+
+  /**
+   * Gets the raw position of this axis at this moment.
+   */
+  double GetPosition();
+
+  /**
+   * Sets the raw position of this axis at this moment.
+   */
+  void SetPosition(double aPosition);
+
+protected:
+
+  struct State
+  {
+    State(double ap, double av) : p(ap), v(av) {};
+    double p; // Position
+    double v; // Velocity
+  };
+
+  struct Derivative
+  {
+    Derivative() : dp(0.0), dv(0.0) {};
+    Derivative(double aDp, double aDv) : dp(aDp), dv(aDv) {};
+    double dp; // dp / delta time = Position
+    double dv; // dv / delta time = Velocity
+  };
+
+  /**
+   * Acceleration must be overridden and return the number of
+   * axis-position-units / second that should be added or removed from the
+   * velocity.
+   */
+  virtual double Acceleration(const State &aState) = 0;
+
+private:
+
+  /**
+   * Duration of fixed delta time step (seconds)
+   */
+  static const double kFixedTimestep;
+
+  /**
+   * 0.0 - 1.0 value indicating progress between current and next simulation
+   * sample.  Normalized to units of kFixedTimestep duration.
+   */
+  double mProgress;
+
+  /**
+   * Sample of simulation state as it existed
+   * (1.0 - mProgress) * kFixedTimestep seconds in the past.
+   */
+  State mPrevState;
+
+  /**
+   * Sample of simulation state as it will be in mProgress * kFixedTimestep
+   * seconds in the future.
+   */
+  State mNextState;
+
+  /**
+   * Perform RK4 (Runge-Kutta method) Integration to calculate the next
+   * simulation sample.
+   */
+  void Integrate(double aDeltaTime);
+
+  /**
+   * Apply delta velocity and position represented by aDerivative over
+   * aDeltaTime seconds, calculate new acceleration, and return new deltas.
+   */
+  Derivative Evaluate(const State &aInitState, double aDeltaTime,
+                      const Derivative &aDerivative);
+
+  /**
+   * Helper function for performing linear interpolation (lerp) of double's
+   */
+  static double LinearInterpolate(double aV1, double aV2, double aBlend);
+
+};
+
+
+}
+}
+
+#endif
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -110,16 +110,18 @@ EXPORTS.mozilla.layers += [
     'apz/src/AsyncPanZoomController.h',
     'apz/src/Axis.h',
     'apz/src/GestureEventListener.h',
     'apz/src/TaskThrottler.h',
     'apz/testutil/APZTestData.h',
     'apz/util/ActiveElementManager.h',
     'apz/util/APZCCallbackHelper.h',
     'AtomicRefCountedWithFinalize.h',
+    'AxisPhysicsModel.h',
+    'AxisPhysicsMSDModel.h',
     'basic/BasicCompositor.h',
     'basic/MacIOSurfaceTextureHostBasic.h',
     'basic/TextureHostBasic.h',
     'client/CanvasClient.h',
     'client/CompositableClient.h',
     'client/ContentClient.h',
     'client/ImageClient.h',
     'client/SimpleTextureClientPool.h',
@@ -236,16 +238,18 @@ UNIFIED_SOURCES += [
     'apz/src/AsyncPanZoomController.cpp',
     'apz/src/Axis.cpp',
     'apz/src/GestureEventListener.cpp',
     'apz/src/TaskThrottler.cpp',
     'apz/src/TouchBlockState.cpp',
     'apz/testutil/APZTestData.cpp',
     'apz/util/ActiveElementManager.cpp',
     'apz/util/APZCCallbackHelper.cpp',
+    'AxisPhysicsModel.cpp',
+    'AxisPhysicsMSDModel.cpp',
     'basic/BasicCanvasLayer.cpp',
     'basic/BasicColorLayer.cpp',
     'basic/BasicCompositor.cpp',
     'basic/BasicContainerLayer.cpp',
     'basic/BasicImages.cpp',
     'basic/BasicLayerManager.cpp',
     'basic/BasicLayersImpl.cpp',
     'basic/BasicThebesLayer.cpp',