// Filename: cLerpNodePathInterval.cxx
// Created by:  drose (27Aug02)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) Carnegie Mellon University.  All rights reserved.
//
// All use of this software is subject to the terms of the revised BSD
// license.  You should have received a copy of this license along
// with this source code in a file named "LICENSE."
//
////////////////////////////////////////////////////////////////////

#include "cLerpNodePathInterval.h"
#include "lerp_helpers.h"
#include "transformState.h"
#include "renderState.h"
#include "colorAttrib.h"
#include "colorScaleAttrib.h"
#include "texMatrixAttrib.h"
#include "dcast.h"
#include "config_interval.h"

TypeHandle CLerpNodePathInterval::_type_handle;

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::Constructor
//       Access: Published
//  Description: Constructs a lerp interval that will lerp some
//               properties on the indicated node, possibly relative
//               to the indicated other node (if other is nonempty).
//
//               You must call set_end_pos(), etc. for the various
//               properties you wish to lerp before the first call to
//               priv_initialize().  If you want to set a starting value
//               for any of the properties, you may call
//               set_start_pos(), etc.; otherwise, the starting value
//               is taken from the actual node's value at the time the
//               lerp is performed.
//
//               The starting values may be explicitly specified or
//               omitted.  The value of bake_in_start determines the
//               behavior if the starting values are omitted.  If
//               bake_in_start is true, the values are obtained the
//               first time the lerp runs, and thenceforth are stored
//               within the interval.  If bake_in_start is false, the
//               starting value is computed each frame, based on
//               assuming the current value represents the value set
//               from the last time the interval was run.  This
//               "smart" behavior allows code to manipulate the object
//               event while it is being lerped, and the lerp
//               continues to apply in a sensible way.
//
//               If fluid is true, the prev_transform is not adjusted
//               by the lerp; otherwise, it is reset.
////////////////////////////////////////////////////////////////////
CLerpNodePathInterval::
CLerpNodePathInterval(const string &name, double duration, 
                      CLerpInterval::BlendType blend_type,
                      bool bake_in_start, bool fluid,
                      const NodePath &node, const NodePath &other) :
  CLerpInterval(name, duration, blend_type),
  _node(node),
  _other(other),
  _flags(0),
  _texture_stage(TextureStage::get_default()),
  _override(0),
  _slerp(NULL)
{
  if (bake_in_start) {
    _flags |= F_bake_in_start;
  }
  if (fluid) {
    _flags |= F_fluid;
  }
  _prev_d = 0.0;
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::initialize
//       Access: Published, Virtual
//  Description: This replaces the first call to priv_step(), and indicates
//               that the interval has just begun.  This may be
//               overridden by derived classes that need to do some
//               explicit initialization on the first call.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
priv_initialize(double t) {
  check_stopped(get_class_type(), "priv_initialize");
  recompute();
  _prev_d = 0.0;
  _state = S_started;
  priv_step(t);
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::instant
//       Access: Published, Virtual
//  Description: This is called in lieu of priv_initialize() .. priv_step()
//               .. priv_finalize(), when everything is to happen within
//               one frame.  The interval should initialize itself,
//               then leave itself in the final state.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
priv_instant() {
  check_stopped(get_class_type(), "priv_instant");
  recompute();
  _prev_d = 0.0;
  _state = S_started;
  priv_step(get_duration());
  _state = S_final;
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::step
//       Access: Published, Virtual
//  Description: Advances the time on the interval.  The time may
//               either increase (the normal case) or decrease
//               (e.g. if the interval is being played by a slider).
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
priv_step(double t) {
  check_started(get_class_type(), "priv_step");
  _state = S_started;
  double d = compute_delta(t);

  // Save this in case we want to restore it later.
  CPT(TransformState) prev_transform = _node.get_prev_transform();

  if ((_flags & (F_end_pos | F_end_hpr | F_end_quat | F_end_scale | F_end_shear)) != 0) {
    // We have some transform lerp.
    CPT(TransformState) transform;

    if (_other.is_empty()) {
      // If there is no other node, it's a local transform lerp.
      transform = _node.get_transform();
    } else {
      // If there *is* another node, we get the transform relative to
      // that node.
      transform = _node.get_transform(_other);
    }
    
    LPoint3 pos;
    LVecBase3 hpr;
    LQuaternion quat;
    LVecBase3 scale;
    LVecBase3 shear;

    if ((_flags & F_end_pos) != 0) {
      if ((_flags & F_start_pos) != 0) {
        lerp_value(pos, d, _start_pos, _end_pos);

      } else if ((_flags & F_bake_in_start) != 0) {
        // Get the current starting pos, and bake it in.
        set_start_pos(transform->get_pos());
        lerp_value(pos, d, _start_pos, _end_pos);

      } else {
        // "smart" lerp from the current pos to the new pos.
        pos = transform->get_pos();
        lerp_value_from_prev(pos, d, _prev_d, pos, _end_pos);
      }
    }
    if ((_flags & F_end_hpr) != 0) {
      if ((_flags & F_start_hpr) != 0) {
        lerp_value(hpr, d, _start_hpr, _end_hpr);

      } else if ((_flags & F_start_quat) != 0) {
        _start_hpr = _start_quat.get_hpr();
        _flags |= F_start_hpr;
        lerp_value(hpr, d, _start_hpr, _end_hpr);

      } else if ((_flags & F_bake_in_start) != 0) {
        set_start_hpr(transform->get_hpr());
        lerp_value(hpr, d, _start_hpr, _end_hpr);

      } else {
        hpr = transform->get_hpr();
        lerp_value_from_prev(hpr, d, _prev_d, hpr, _end_hpr);
      }
    }
    if ((_flags & F_end_quat) != 0) {
      if ((_flags & F_slerp_setup) == 0) {
        if ((_flags & F_start_quat) != 0) {
          setup_slerp();

        } else if ((_flags & F_start_hpr) != 0) {
          _start_quat.set_hpr(_start_hpr);
          _flags |= F_start_quat;
          setup_slerp();

        } else if ((_flags & F_bake_in_start) != 0) {
          set_start_quat(transform->get_quat());
          setup_slerp();

        } else {
          if (_prev_d == 1.0) {
            _start_quat = _end_quat;
          } else {
            LQuaternion prev_value = transform->get_quat();
            _start_quat = (prev_value - _prev_d * _end_quat) / (1.0 - _prev_d);
          }
          setup_slerp();

          // In this case, clear the slerp_setup flag because we need
          // to re-setup the slerp each time.
          _flags &= ~F_slerp_setup;
        }
      }
      nassertv(_slerp != NULL);
      (this->*_slerp)(quat, d);
    }
    if ((_flags & F_end_scale) != 0) {
      if ((_flags & F_start_scale) != 0) {
        lerp_value(scale, d, _start_scale, _end_scale);

      } else if ((_flags & F_bake_in_start) != 0) {
        set_start_scale(transform->get_scale());
        lerp_value(scale, d, _start_scale, _end_scale);

      } else {
        scale = transform->get_scale();
        lerp_value_from_prev(scale, d, _prev_d, scale, _end_scale);
      }
    }
    if ((_flags & F_end_shear) != 0) {
      if ((_flags & F_start_shear) != 0) {
        lerp_value(shear, d, _start_shear, _end_shear);

      } else if ((_flags & F_bake_in_start) != 0) {
        set_start_shear(transform->get_shear());
        lerp_value(shear, d, _start_shear, _end_shear);

      } else {
        shear = transform->get_shear();
        lerp_value_from_prev(shear, d, _prev_d, shear, _end_shear);
      }
    }

    // Now apply the modifications back to the transform.  We want to
    // be a little careful here, because we don't want to assume the
    // transform has hpr/scale components if they're not needed.  And
    // in any case, we only want to apply the components that we
    // computed, above.
    unsigned int transform_flags = _flags & (F_end_pos | F_end_hpr | F_end_quat | F_end_scale);
    switch (transform_flags) {
    case 0:
      break;

    case F_end_pos:
      if (_other.is_empty()) {
        _node.set_pos(pos);
      } else {
        _node.set_pos(_other, pos);
      }
      break;

    case F_end_hpr:
      if (_other.is_empty()) {
        _node.set_hpr(hpr);
      } else {
        _node.set_hpr(_other, hpr);
      }
      break;

    case F_end_quat:
      if (_other.is_empty()) {
        _node.set_quat(quat);
      } else {
        _node.set_quat(_other, quat);
      }
      break;

    case F_end_scale:
      if (_other.is_empty()) {
        _node.set_scale(scale);
      } else {
        _node.set_scale(_other, scale);
      }
      break;

    case F_end_hpr | F_end_scale:
      if (_other.is_empty()) {
        _node.set_hpr_scale(hpr, scale);
      } else {
        _node.set_hpr_scale(hpr, scale);
      }
      break;

    case F_end_quat | F_end_scale:
      if (_other.is_empty()) {
        _node.set_quat_scale(quat, scale);
      } else {
        _node.set_quat_scale(quat, scale);
      }
      break;

    case F_end_pos | F_end_hpr:
      if (_other.is_empty()) {
        _node.set_pos_hpr(pos, hpr);
      } else {
        _node.set_pos_hpr(_other, pos, hpr);
      }
      break;

    case F_end_pos | F_end_quat:
      if (_other.is_empty()) {
        _node.set_pos_quat(pos, quat);
      } else {
        _node.set_pos_quat(_other, pos, quat);
      }
      break;

    case F_end_pos | F_end_scale:
      if (transform->quat_given()) {
        if (_other.is_empty()) {
          _node.set_pos_quat_scale(pos, transform->get_quat(), scale);
        } else {
          _node.set_pos_quat_scale(_other, pos, transform->get_quat(), scale);
        }
      } else {
        if (_other.is_empty()) {
          _node.set_pos_hpr_scale(pos, transform->get_hpr(), scale);
        } else {
          _node.set_pos_hpr_scale(_other, pos, transform->get_hpr(), scale);
        }
      }
      break;

    case F_end_pos | F_end_hpr | F_end_scale:
      if ((_flags & F_end_shear) != 0) {
        // Even better: we have all four components.
        if (_other.is_empty()) {
          _node.set_pos_hpr_scale_shear(pos, hpr, scale, shear);
        } else {
          _node.set_pos_hpr_scale_shear(_other, pos, hpr, scale, shear);
        }
      } else {
        // We have only the primary three components.
        if (_other.is_empty()) {
          _node.set_pos_hpr_scale(pos, hpr, scale);
        } else {
          _node.set_pos_hpr_scale(_other, pos, hpr, scale);
        }
      }
      break;

    case F_end_pos | F_end_quat | F_end_scale:
      if ((_flags & F_end_shear) != 0) {
        // Even better: we have all four components.
        if (_other.is_empty()) {
          _node.set_pos_quat_scale_shear(pos, quat, scale, shear);
        } else {
          _node.set_pos_quat_scale_shear(_other, pos, quat, scale, shear);
        }
      } else {
        // We have only the primary three components.
        if (_other.is_empty()) {
          _node.set_pos_quat_scale(pos, quat, scale);
        } else {
          _node.set_pos_quat_scale(_other, pos, quat, scale);
        }
      }
      break;

    default:
      // Some unhandled combination.  We should handle this.
      interval_cat.error()
        << "Internal error in CLerpNodePathInterval::priv_step().\n";
    }
    if ((_flags & F_end_shear) != 0) {
      // Also apply changes to shear.
      if (transform_flags == (F_end_pos | F_end_hpr | F_end_scale) ||
          transform_flags == (F_end_pos | F_end_quat | F_end_scale)) {
        // Actually, we already handled this case above.

      } else {
        if (_other.is_empty()) {
          _node.set_shear(shear);
        } else {
          _node.set_shear(_other, shear);
        }
      }
    }
  }

  if ((_flags & F_fluid) != 0) {
    // If we have the fluid flag set, we shouldn't mess with the prev
    // transform.  Therefore, restore it to what it was before we
    // started messing with it.
    _node.set_prev_transform(prev_transform);
  }

  if ((_flags & (F_end_color | F_end_color_scale | F_end_tex_offset | F_end_tex_rotate | F_end_tex_scale)) != 0) {
    // We have some render state lerp.
    CPT(RenderState) state;

    if (_other.is_empty()) {
      // If there is no other node, it's a local state lerp.  This is
      // most common.
      state = _node.get_state();
    } else {
      // If there *is* another node, we get the state relative to that
      // node.  This is weird, but you could lerp color (for instance)
      // relative to some other node's color.
      state = _node.get_state(_other);
    }
    
    // Unlike in the transform case above, we can go ahead and modify
    // the state immediately with each attribute change, since these
    // attributes don't interrelate.

    if ((_flags & F_end_color) != 0) {
      LColor color;

      if ((_flags & F_start_color) != 0) {
        lerp_value(color, d, _start_color, _end_color);

      } else {
        // Get the previous color.
        color.set(1.0f, 1.0f, 1.0f, 1.0f);
        const RenderAttrib *attrib =
          state->get_attrib(ColorAttrib::get_class_type());
        if (attrib != (const RenderAttrib *)NULL) {
          const ColorAttrib *ca = DCAST(ColorAttrib, attrib);
          if (ca->get_color_type() == ColorAttrib::T_flat) {
            color = ca->get_color();
          }
        }

        lerp_value_from_prev(color, d, _prev_d, color, _end_color);
      }

      state = state->add_attrib(ColorAttrib::make_flat(color), _override);
    }

    if ((_flags & F_end_color_scale) != 0) {
      LVecBase4 color_scale;

      if ((_flags & F_start_color_scale) != 0) {
        lerp_value(color_scale, d, _start_color_scale, _end_color_scale);

      } else {
        // Get the previous color scale.
        color_scale.set(1.0f, 1.0f, 1.0f, 1.0f);
        const RenderAttrib *attrib =
          state->get_attrib(ColorScaleAttrib::get_class_type());
        if (attrib != (const RenderAttrib *)NULL) {
          const ColorScaleAttrib *csa = DCAST(ColorScaleAttrib, attrib);
          color_scale = csa->get_scale();
        }

        lerp_value_from_prev(color_scale, d, _prev_d, color_scale, _end_color_scale);
      }

      state = state->add_attrib(ColorScaleAttrib::make(color_scale), _override);
    }    

    if ((_flags & (F_end_tex_offset | F_end_tex_rotate | F_end_tex_scale)) != 0) {
      // We have a UV lerp.
      CPT(TransformState) transform = TransformState::make_identity();

      const RenderAttrib *attrib =
        state->get_attrib(TexMatrixAttrib::get_class_type());
      CPT(TexMatrixAttrib) tma;
      if (attrib != (const TexMatrixAttrib *)NULL) {
        tma = DCAST(TexMatrixAttrib, attrib);
        transform = tma->get_transform(_texture_stage);
      } else {
        tma = DCAST(TexMatrixAttrib, TexMatrixAttrib::make());
      }

      if ((_flags & F_end_tex_offset) != 0) {
        LVecBase2 tex_offset;

        if ((_flags & F_start_tex_offset) != 0) {
          lerp_value(tex_offset, d, _start_tex_offset, _end_tex_offset);
        } else {
          tex_offset = transform->get_pos2d();
          lerp_value_from_prev(tex_offset, d, _prev_d, tex_offset, 
                               _end_tex_offset);
        }

        transform = transform->set_pos2d(tex_offset);
      }

      if ((_flags & F_end_tex_rotate) != 0) {
        PN_stdfloat tex_rotate;

        if ((_flags & F_start_tex_rotate) != 0) {
          lerp_value(tex_rotate, d, _start_tex_rotate, _end_tex_rotate);
        } else {
          tex_rotate = transform->get_rotate2d();
          lerp_value_from_prev(tex_rotate, d, _prev_d, tex_rotate, 
                               _end_tex_rotate);
        }

        transform = transform->set_rotate2d(tex_rotate);
      }

      if ((_flags & F_end_tex_scale) != 0) {
        LVecBase2 tex_scale;

        if ((_flags & F_start_tex_scale) != 0) {
          lerp_value(tex_scale, d, _start_tex_scale, _end_tex_scale);
        } else {
          tex_scale = transform->get_scale2d();
          lerp_value_from_prev(tex_scale, d, _prev_d, tex_scale, 
                               _end_tex_scale);
        }

        transform = transform->set_scale2d(tex_scale);
      }

      // Apply the modified transform back to the state.
      state = state->set_attrib(tma->add_stage(_texture_stage, transform, _override));
    }    


    // Now apply the new state back to the node.
    if (_other.is_empty()) {
      _node.set_state(state);
    } else {
      _node.set_state(_other, state);
    }
  }  _prev_d = d;
  _curr_t = t;
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::reverse_initialize
//       Access: Published, Virtual
//  Description: Similar to priv_initialize(), but this is called when the
//               interval is being played backwards; it indicates that
//               the interval should start at the finishing state and
//               undo any intervening intervals.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
priv_reverse_initialize(double t) {
  check_stopped(get_class_type(), "priv_reverse_initialize");
  recompute();
  _state = S_started;
  _prev_d = 1.0;
  priv_step(t);
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::reverse_instant
//       Access: Published, Virtual
//  Description: This is called in lieu of priv_reverse_initialize()
//               .. priv_step() .. priv_reverse_finalize(), when everything is
//               to happen within one frame.  The interval should
//               initialize itself, then leave itself in the initial
//               state.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
priv_reverse_instant() {
  check_stopped(get_class_type(), "priv_reverse_initialize");
  recompute();
  _state = S_started;
  _prev_d = 1.0;
  priv_step(0.0);
  _state = S_initial;
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::output
//       Access: Published, Virtual
//  Description: 
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
output(ostream &out) const {
  out << get_name() << ":";

  if ((_flags & F_end_pos) != 0) {
    out << " pos";
    if ((_flags & F_start_pos) != 0) {
      out << " from " << _start_pos;
    }
    out << " to " << _end_pos;
  }

  if ((_flags & F_end_hpr) != 0) {
    out << " hpr";
    if ((_flags & F_start_hpr) != 0) {
      out << " from " << _start_hpr;
    }
    out << " to " << _end_hpr;
  }

  if ((_flags & F_end_quat) != 0) {
    out << " quat";
    if ((_flags & F_start_quat) != 0) {
      out << " from " << _start_quat;
    }
    out << " to " << _end_quat;
  }

  if ((_flags & F_end_scale) != 0) {
    out << " scale";
    if ((_flags & F_start_scale) != 0) {
      out << " from " << _start_scale;
    }
    out << " to " << _end_scale;
  }

  if ((_flags & F_end_shear) != 0) {
    out << " shear";
    if ((_flags & F_start_shear) != 0) {
      out << " from " << _start_shear;
    }
    out << " to " << _end_shear;
  }

  if ((_flags & F_end_color) != 0) {
    out << " color";
    if ((_flags & F_start_color) != 0) {
      out << " from " << _start_color;
    }
    out << " to " << _end_color;
  }

  if ((_flags & F_end_color_scale) != 0) {
    out << " color_scale";
    if ((_flags & F_start_color_scale) != 0) {
      out << " from " << _start_color_scale;
    }
    out << " to " << _end_color_scale;
  }

  out << " dur " << get_duration();
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::setup_slerp
//       Access: Private
//  Description: Sets up a spherical lerp from _start_quat to
//               _end_quat.  This precomputes some important values
//               (like the angle between the quaternions) and sets up
//               the _slerp method pointer.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
setup_slerp() {
  if (_start_quat.dot(_end_quat) < 0.0f) {
    // Make sure both quaternions are on the same side.
    _start_quat = -_start_quat;
  }

  _slerp_angle = _start_quat.angle_rad(_end_quat);

  if (_slerp_angle < 0.1f) {
    // If the angle is small, use sin(angle)/angle as the denominator,
    // to provide better behavior with small divisors.  This is Don
    // Hatch's suggestion from http://www.hadron.org/~hatch/rightway.php .
    _slerp_denom = csin_over_x(_slerp_angle);
    _slerp = &CLerpNodePathInterval::slerp_angle_0;

  } else if (_slerp_angle > 3.14) {
    // If the angle is close to 180 degrees, the lerp is ambiguous.
    // which plane should we lerp through?  Better pick an
    // intermediate point to resolve the ambiguity up front.

    // We pick it by choosing a linear point between the quats and
    // normalizing it out; this will give an arbitrary point when the
    // angle is exactly 180, but will behave sanely as the angle
    // approaches 180.
    _slerp_c = (_start_quat + _end_quat);
    _slerp_c.normalize();
    _slerp_angle = _end_quat.angle_rad(_slerp_c);
    _slerp_denom = csin(_slerp_angle);

    _slerp = &CLerpNodePathInterval::slerp_angle_180;
    
  } else {
    // Otherwise, use the original Shoemake equation for spherical
    // lerp.
    _slerp_denom = csin(_slerp_angle);
    _slerp = &CLerpNodePathInterval::slerp_basic;
  }

  nassertv(_slerp_denom != 0.0f);
  _flags |= F_slerp_setup;
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::slerp_basic
//       Access: Private
//  Description: Implements Ken Shoemake's spherical lerp equation.
//               This is appropriate when the angle between the
//               quaternions is not near one extreme or the other.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
slerp_basic(LQuaternion &result, PN_stdfloat t) const {
  nassertv(_slerp_denom != 0.0f);
  PN_stdfloat ti = 1.0f - t;
  PN_stdfloat ta = t * _slerp_angle;
  PN_stdfloat tia = ti * _slerp_angle;

  if (interval_cat.is_spam()) {
    interval_cat.spam()
      << "slerp_basic, (t = " << t << "), angle = " << _slerp_angle << "\n"
      << "_start_quat = " << _start_quat << ", _end_quat = "
      << _end_quat << ", denom = " << _slerp_denom << "\n";
  }

  result = (csin(tia) * _start_quat + csin(ta) * _end_quat) / _slerp_denom;
  nassertv(!result.is_nan());
}

////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::slerp_angle_0
//       Access: Private
//  Description: Implements Don Hatch's modified spherical lerp
//               equation, appropriate for when the angle between the
//               quaternions approaches zero.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
slerp_angle_0(LQuaternion &result, PN_stdfloat t) const {
  nassertv(_slerp_denom != 0.0f);
  PN_stdfloat ti = 1.0f - t;
  PN_stdfloat ta = t * _slerp_angle;
  PN_stdfloat tia = ti * _slerp_angle;

  if (interval_cat.is_spam()) {
    interval_cat.spam()
      << "slerp_angle_0, (t = " << t << "), angle = " << _slerp_angle
      << "\n_start_quat = " << _start_quat << ", _end_quat = "
      << _end_quat << ", denom = " << _slerp_denom << "\n";
  }

  result = (csin_over_x(tia) * ti * _start_quat + csin_over_x(ta) * t * _end_quat) / _slerp_denom;
  nassertv(!result.is_nan());
}


////////////////////////////////////////////////////////////////////
//     Function: CLerpNodePathInterval::slerp_angle_180
//       Access: Private
//  Description: Implements a two-part slerp, to an intermediate point
//               and out again, appropriate for when the angle between
//               the quaternions approaches 180 degrees.
////////////////////////////////////////////////////////////////////
void CLerpNodePathInterval::
slerp_angle_180(LQuaternion &result, PN_stdfloat t) const {
  nassertv(_slerp_denom != 0.0f);
  if (t < 0.5) {
    // The first half of the lerp: _start_quat to _slerp_c.

    t *= 2.0f;

    PN_stdfloat ti = 1.0f - t;
    PN_stdfloat ta = t * _slerp_angle;
    PN_stdfloat tia = ti * _slerp_angle;

    if (interval_cat.is_spam()) {
      interval_cat.spam()
        << "slerp_angle_180, first half (t = " << t << "), angle = "
        << _slerp_angle << "\n_start_quat = " << _start_quat
        << ", _slerp_c = " << _slerp_c << ", denom = " 
        << _slerp_denom << "\n";
    }
    
    result = (csin(tia) * _start_quat + csin(ta) * _slerp_c) / _slerp_denom;

  } else {
    // The second half of the lerp: _slerp_c to _end_quat.
    t = t * 2.0f - 1.0f;

    PN_stdfloat ti = 1.0f - t;
    PN_stdfloat ta = t * _slerp_angle;
    PN_stdfloat tia = ti * _slerp_angle;

    if (interval_cat.is_spam()) {
      interval_cat.spam()
        << "slerp_angle_180, second half (t = " << t << "), angle = "
        << _slerp_angle << "\n_slerp_c = " << _slerp_c
        << ", _end_quat = " << _end_quat << ", denom = " 
        << _slerp_denom << "\n";
    }
    
    result = (csin(tia) * _slerp_c + csin(ta) * _end_quat) / _slerp_denom;
  }

  nassertv(!result.is_nan());
}
