/****************************************************************************** Copyright (c) 2008-2009 Ryan Juckett http://www.ryanjuckett.com/ This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ******************************************************************************/ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; namespace RJ_Demo_IK { /// /// 2D relaxation based inverse kinematics /// public partial class CalcIK_2D_ConstraintRelaxation : UserControl, INotifyPropertyChanged { #region Internal types // this class represents a bone in it's parent space public class BoneData : INotifyPropertyChanged { // actual bone data private double m_length = 0; private double m_angle = 0; private double m_weight = 0; // positions used to display the relaxation process private double m_relaxedX = 0; private double m_relaxedY = 0; #region INotifyPropertyChanged interface // event used by the user interface to bind to our properties public event PropertyChangedEventHandler PropertyChanged; // helper function to notify PropertyChanged subscribers protected void NotifyPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #endregion public double Length { get { return m_length; } set { m_length = value; NotifyPropertyChanged("Length"); } } public double Radians { get { return m_angle; } set { m_angle = value; NotifyPropertyChanged("Radians"); NotifyPropertyChanged("Degrees"); } } public double Degrees { get { return m_angle * 180.0 / Math.PI; } set { m_angle = value * Math.PI / 180.0; NotifyPropertyChanged("Radians"); NotifyPropertyChanged("Degrees"); } } public double Weight { get { return m_weight; } set { m_weight = value; NotifyPropertyChanged("Weight"); } } public double RelaxedX { get { return m_relaxedX; } set { m_relaxedX = value; NotifyPropertyChanged("RelaxedX"); } } public double RelaxedY { get { return m_relaxedY; } set { m_relaxedY = value; NotifyPropertyChanged("RelaxedY"); } } } #endregion #region Private data private static Brush[] s_boneLineColors = { Brushes.Black, Brushes.GreenYellow, Brushes.DarkBlue, Brushes.DarkRed, Brushes.MintCream, }; // number of iterations to perform on each update private int m_iterationsPerUpdate = 0; // bone lengths private ObservableCollection m_bones = new ObservableCollection(); // lines used to draw the bones class BoneLine { public Line m_actualBone = null; public Line m_relaxedBone = null; } private List m_boneLines = new List(); // target position to reach for private Point m_targetPos = new Point(0,0); // positions used to display the relaxation process private double m_relaxedTargetX = 0; private double m_relaxedTargetY = 0; private DispatcherTimer m_updateTimer; #endregion #region INotifyPropertyChanged interface // event used by the user interface to bind to our properties public event PropertyChangedEventHandler PropertyChanged; // helper function to notify PropertyChanged subscribers protected void NotifyPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #endregion #region Public properties public ObservableCollection Bones { get { return m_bones; } } public int IterationsPerUpdate { get { return m_iterationsPerUpdate; } set { m_iterationsPerUpdate = Math.Max(1,value); NotifyPropertyChanged("IterationsPerUpdate"); // update the bound UI } } public double TargetPosX { get { return m_targetPos.X; } set { m_targetPos.X = value; NotifyPropertyChanged("TargetPosX"); // update the bound UI UpdateDisplay(); // redraw } } public double TargetPosY { get { return m_targetPos.Y; } set { m_targetPos.Y = value; NotifyPropertyChanged("TargetPosY"); // update the bound UI UpdateDisplay(); // redraw } } #endregion #region Lifespan functions public CalcIK_2D_ConstraintRelaxation() { InitializeComponent(); // set the iteration number IterationsPerUpdate = 1; // create the timer m_updateTimer = new DispatcherTimer(DispatcherPriority.Normal); m_updateTimer.Tick += new EventHandler(UpdateTimer_Tick); m_updateTimer.Interval = new TimeSpan(0, 0, 0, 0, 100); // add the initial bones AddBone(); AddBone(); TargetPosX = 100; TargetPosY = 0; // update the display UpdateDisplay(); } #endregion #region Coordinate Conversion // compute the logical origin in _viewport coordinated private double ViewportWidth { get { return _viewportColumn.ActualWidth; } } private double ViewportHieght { get { return _mainGrid.ActualHeight; } } private double ViewportCenterX { get { return ViewportWidth / 2; } } private double ViewportCenterY { get { return ViewportHieght / 2; } } // convert logical coordinates to _viewport coordinates private double LogicalToViewportX(double logicalX) { return logicalX + ViewportCenterX; } private double LogicalToViewportY(double logicalY) { return -logicalY + ViewportCenterY; } // convert _viewport coordinates to logical coordinates private double ViewportToLogicalX(double viewportX) { return viewportX - ViewportCenterX; } private double ViewportToLogicalY(double viewportY) { return -viewportY + ViewportCenterY; } #endregion #region Logic Functions /// /// Add a new bone to the chain at the selected location or at the end if no location is selected. /// void AddBone() { BoneData newBone = new BoneData(); newBone.Length = 50; newBone.Weight = 1; newBone.PropertyChanged += BonePropertyChanged; // insert at the end if no bone is selected if( _boneList.SelectedIndex == -1 ) Bones.Add( newBone ); else Bones.Insert( _boneList.SelectedIndex, newBone ); } /// /// Remove a new bone from the chain at the selected location or from the end if no location is selected. /// private void RemoveBone() { if( Bones.Count == 0 ) return; // remove the end bone if no bone is selected int removeIdx = _boneList.SelectedIndex; if( removeIdx == -1 ) removeIdx = (Bones.Count - 1); Bones[removeIdx].PropertyChanged -= BonePropertyChanged; Bones.RemoveAt( removeIdx ); } /// /// Perform an iteration of IK /// private void UpdateIK() { int numBones = Bones.Count; if( numBones == 0 ) return; // calculate the bone angles List< IKSolver.Bone_2D_ConstraintRelaxation > relaxBones = new List< IKSolver.Bone_2D_ConstraintRelaxation >(); for( int boneIdx = 0; boneIdx < numBones; ++boneIdx ) { IKSolver.Bone_2D_ConstraintRelaxation newRelaxBone = new IKSolver.Bone_2D_ConstraintRelaxation(); newRelaxBone.angle = Bones[boneIdx].Radians; newRelaxBone.length = Bones[boneIdx].Length; newRelaxBone.weight = Bones[boneIdx].Weight; relaxBones.Add( newRelaxBone ); } // convert to worldspace bones List< IKSolver.Bone_2D_ConstraintRelaxation_World > worldBones; IKSolver.CalcIK_2D_ConstraintRelaxation_ConvertToWorld( out worldBones, relaxBones ); // iterate IK for( int itrCount = 0; itrCount < IterationsPerUpdate; ++itrCount ) { IKSolver.CalcIK_2D_ConstraintRelaxation( ref worldBones, TargetPosX, TargetPosY ); } // convert bones back to local space IKSolver.CalcIK_2D_ConstraintRelaxation_ConvertToLocal( ref relaxBones, worldBones, TargetPosX, TargetPosY ); // extract the new bone data from the results for( int boneIdx = 0; boneIdx < numBones; ++boneIdx ) { Bones[boneIdx].Radians = relaxBones[boneIdx].angle; Bones[boneIdx].RelaxedX = worldBones[boneIdx].x; Bones[boneIdx].RelaxedY = worldBones[boneIdx].y; } // track the target position we used for relaxation (this prevents the relaxed bone display from changing while paused) m_relaxedTargetX = TargetPosX; m_relaxedTargetY = TargetPosY; } /// /// Update the scene displayed in the viewport /// private void UpdateDisplay() { int numBones = Bones.Count; // resize the number of bone lines while( m_boneLines.Count > numBones ) { _viewport.Children.Remove( m_boneLines[m_boneLines.Count-1].m_actualBone ); _viewport.Children.Remove( m_boneLines[m_boneLines.Count-1].m_relaxedBone ); m_boneLines.RemoveAt( m_boneLines.Count-1 ); } while( m_boneLines.Count < numBones ) { BoneLine newBoneLine = new BoneLine(); newBoneLine.m_actualBone = new Line(); newBoneLine.m_actualBone.Stroke = s_boneLineColors[m_boneLines.Count % s_boneLineColors.Length]; newBoneLine.m_actualBone.StrokeThickness = 3; newBoneLine.m_actualBone.Opacity = 0.85; newBoneLine.m_actualBone.SetValue( Panel.ZIndexProperty, 100 ); newBoneLine.m_relaxedBone = new Line(); newBoneLine.m_relaxedBone.Stroke = s_boneLineColors[m_boneLines.Count % s_boneLineColors.Length]; newBoneLine.m_relaxedBone.StrokeThickness = 7; newBoneLine.m_relaxedBone.Opacity = 0.15; newBoneLine.m_relaxedBone.SetValue( Panel.ZIndexProperty, 99 ); m_boneLines.Add( newBoneLine ); _viewport.Children.Add( newBoneLine.m_actualBone ); _viewport.Children.Add( newBoneLine.m_relaxedBone ); } // compute the orientations of the bone lines in logical space double curAngle = 0; for( int boneIdx = 0; boneIdx < numBones; ++boneIdx ) { BoneData curBone = Bones[boneIdx]; curAngle += curBone.Radians; double cosAngle = Math.Cos( curAngle ); double sinAngle = Math.Sin( curAngle ); if( boneIdx > 0 ) { m_boneLines[boneIdx].m_actualBone.X1 = m_boneLines[boneIdx-1].m_actualBone.X2; m_boneLines[boneIdx].m_actualBone.Y1 = m_boneLines[boneIdx-1].m_actualBone.Y2; } else { m_boneLines[boneIdx].m_actualBone.X1 = 0; m_boneLines[boneIdx].m_actualBone.Y1 = 0; } m_boneLines[boneIdx].m_actualBone.X2 = m_boneLines[boneIdx].m_actualBone.X1 + cosAngle*curBone.Length; m_boneLines[boneIdx].m_actualBone.Y2 = m_boneLines[boneIdx].m_actualBone.Y1 + sinAngle*curBone.Length; m_boneLines[boneIdx].m_relaxedBone.X1 = curBone.RelaxedX; m_boneLines[boneIdx].m_relaxedBone.Y1 = curBone.RelaxedY; if( boneIdx < numBones-1 ) { m_boneLines[boneIdx].m_relaxedBone.X2 = Bones[boneIdx+1].RelaxedX; m_boneLines[boneIdx].m_relaxedBone.Y2 = Bones[boneIdx+1].RelaxedY; } else { m_boneLines[boneIdx].m_relaxedBone.X2 = m_relaxedTargetX; m_boneLines[boneIdx].m_relaxedBone.Y2 = m_relaxedTargetY; } } // convert the bone positions to viewport space foreach( BoneLine curLine in m_boneLines ) { curLine.m_actualBone.X1 = LogicalToViewportX(curLine.m_actualBone.X1); curLine.m_actualBone.Y1 = LogicalToViewportY(curLine.m_actualBone.Y1); curLine.m_actualBone.X2 = LogicalToViewportX(curLine.m_actualBone.X2); curLine.m_actualBone.Y2 = LogicalToViewportY(curLine.m_actualBone.Y2); curLine.m_relaxedBone.X1 = LogicalToViewportX(curLine.m_relaxedBone.X1); curLine.m_relaxedBone.Y1 = LogicalToViewportY(curLine.m_relaxedBone.Y1); curLine.m_relaxedBone.X2 = LogicalToViewportX(curLine.m_relaxedBone.X2); curLine.m_relaxedBone.Y2 = LogicalToViewportY(curLine.m_relaxedBone.Y2); } // draw the target Canvas.SetLeft( _targetEllipse, LogicalToViewportX(TargetPosX - _targetEllipse.Width/2) ); Canvas.SetTop( _targetEllipse, LogicalToViewportY(TargetPosY + _targetEllipse.Height/2) ); // draw the axes _xAxisLine.X1 = 0; _xAxisLine.Y1 = ViewportCenterY; _xAxisLine.X2 = ViewportWidth; _xAxisLine.Y2 = ViewportCenterY; _yAxisLine.X1 = ViewportCenterX; _yAxisLine.Y1 = 0; _yAxisLine.X2 = ViewportCenterX; _yAxisLine.Y2 = ViewportHieght; } /// /// Update logic at a set interval /// private void UpdateTimer_Tick(object sender, EventArgs e) { UpdateIK(); UpdateDisplay(); } #endregion #region Event Handlers private void BonePropertyChanged(object sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { case "Radians": UpdateDisplay(); break; case "Length": UpdateDisplay(); break; } } private void viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // capture the mouse to keep grabing MouseMove events if the user drags the // mouse outside of the _viewport bounds if (!_viewport.IsMouseCaptured) { _viewport.CaptureMouse(); } // update the target position Point viewportPos = e.GetPosition(_viewport); TargetPosX = ViewportToLogicalX( viewportPos.X ); TargetPosY = ViewportToLogicalY( viewportPos.Y ); } private void viewport_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { // release the captured mouse if (_viewport.IsMouseCaptured) { _viewport.ReleaseMouseCapture(); } } private void viewport_MouseMove(object sender, MouseEventArgs e) { // update the target position if we are still in a captured state // (i.e. the user has not released the mouse button) if (_viewport.IsMouseCaptured) { Point viewportPos = e.GetPosition(_viewport); TargetPosX = ViewportToLogicalX( viewportPos.X ); TargetPosY = ViewportToLogicalY( viewportPos.Y ); } } private void _thisWindow_SizeChanged(object sender, SizeChangedEventArgs e) { // update the display shapes based on the new window size UpdateDisplay(); } private void _thisWindow_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { if( m_updateTimer != null ) { if( this.IsVisible ) { if( _playRadioButton.IsChecked == true ) m_updateTimer.Start(); } else { m_updateTimer.Stop(); } } } private void _websiteLink_Click(object sender, RoutedEventArgs e) { System.Diagnostics.Process.Start( "http://www.ryanjuckett.com" ); } private void _addBoneButton_Click(object sender, RoutedEventArgs e) { AddBone(); UpdateDisplay(); } private void _removeBoneButton_Click(object sender, RoutedEventArgs e) { RemoveBone(); UpdateDisplay(); } private void _playRadioButton_Checked(object sender, RoutedEventArgs e) { if( m_updateTimer != null ) m_updateTimer.Start(); } private void _pauseRadioButton_Checked(object sender, RoutedEventArgs e) { if( m_updateTimer != null ) m_updateTimer.Stop(); } private void _singleUpdateButton_Click(object sender, RoutedEventArgs e) { _pauseRadioButton.IsChecked = true; UpdateIK(); UpdateDisplay(); } #endregion } }