Loupe

[C#] Donnez de la profondeur à vos jeux !

 

Dans cette article, on va s’amuser avec la profondeur de champ pour donner un côté dynamique à vos jeux ou applications. Cette première partie utilisera uniquement la souris ou le tactile, pour interagir avec le décors.

Voici ce que ça peut donner :

Un peu de théorie

Vous aurez remarqué que, dans la vidéo, les yeux bougent légèrement et les fantômes bougent beaucoup plus. Pourquoi ?

On va considérer un point virtuel de référence P0 au dessus de la scène et se situant au milieu de l’écran et un point virtuel P100, toujours au milieu de l’écran, qui se situe au niveau de la muraille et qui sera la profondeur maximale. Entre P0 et P100 peuvent se trouver différent calques/composants, vous avez pu le remarquer si vous avez déjà joué avec la propriété Z-index qui permet justement de pouvoir mettre un composant au dessus d’un autre afin de l’afficher au premier plan par exemple. Ici le principe est le même, sauf qu’ici notre point P0 ne sera pas statique mais dynamique !

explication_profondeur

Le schéma ci-contre, décrit le principe de mouvement en déplaçant le point P0 en P0’. On remarque bien que les deux vecteurs sont différents selon la profondeur (en bleu). Pour imager, quand le point P0 bouge, il “tire” les composants vers lui.

 

Les bases

D’après ce que l’on a vue dans le premier chapitre, on va avoir besoin de plusieurs choses :

  • Créer un composant spécifique de type conteneur
  • Pouvoir y ajouter des composants et leur fixer une profondeur
  • Paramétrer les vecteurs de translation et rotation sur l’axe X et Y et la profondeur maximale

Créer un composant spécifique

On va se baser sur le composant Grid :

SensorAnimatedGrid.cs
public class SensorAnimatedGrid : Grid
{
}

Ajouter des composants et leur fixer une profondeur

Nous allons avoir besoin d’une nouvelle classe afin de stocker le nom du composant et sa profondeur :

public class AnimatedElement : DependencyObject
{
    public static readonly DependencyProperty DepthProperty =
        DependencyProperty.Register("Depth", typeof(double), typeof(AnimatedElement), new PropertyMetadata(default(double)));

    public double Depth
    {
        get { return (double)GetValue(DepthProperty); }
        set { SetValue(DepthProperty, value); }
    }

    public static readonly DependencyProperty ElementNameProperty =
        DependencyProperty.Register("ElementName", typeof(string), typeof(AnimatedElement), new PropertyMetadata(default(string)));

    public string ElementName
    {
        get { return (string)GetValue(ElementNameProperty); }
        set { SetValue(ElementNameProperty, value); }
    }
}

Il ne reste plus qu’à créer un liste d’AnimatedElement dans la classe SensorAnimatedGrid :

public static List<AnimatedElement> GetAnimatedElements(DependencyObject obj)
{
    return (List<AnimatedElement>)obj.GetValue(AnimatedElementsProperty);
}

public static void SetAnimatedElements(DependencyObject obj, List<AnimatedElement> value)
{
    obj.SetValue(AnimatedElementsProperty, value);
}

public static readonly DependencyProperty AnimatedElementsProperty =
        DependencyProperty.RegisterAttached(
        "AnimatedElements",
        typeof(List<AnimatedElement>),
        typeof(SensorAnimatedGrid),
        new PropertyMetadata(new List<AnimatedElement>()));

Sans oublier d’initialiser la liste dans le constructeur :

public SensorAnimatedGrid()
{
    SetValue(AnimatedElementsProperty, new List<AnimatedElement>());
}

Paramétrage des vecteurs et de la profondeur maximale

private const double MAX_TRANSLATION_X = 50;
private const double MAX_TRANSLATION_Y = 50;
private const double MAX_ROTATION_X = 10;
private const double MAX_ROTATION_Y = 10;
private const double MAX_DEPTH = 100;

public static double GetMaxDepth(DependencyObject obj)
{
    return (double)obj.GetValue(MaxDepthProperty);
}

public static void SetMaxDepth(DependencyObject obj, double value)
{
    obj.SetValue(MaxDepthProperty, value);
}

public static readonly DependencyProperty MaxDepthProperty =
        DependencyProperty.RegisterAttached("MaxDepth", typeof(double),
        typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_DEPTH));

public static double GetMaxTranslationX(DependencyObject obj)
{
    return (double)obj.GetValue(MaxTranslationXProperty);
}

public static void SetMaxTranslationX(DependencyObject obj, double value)
{
    obj.SetValue(MaxTranslationXProperty, value);
}

public static readonly DependencyProperty MaxTranslationXProperty =
        DependencyProperty.RegisterAttached("MaxTranslationX", typeof(double),
        typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_TRANSLATION_X));

public static double GetMaxTranslationY(DependencyObject obj)
{
    return (double)obj.GetValue(MaxTranslationYProperty);
}

public static void SetMaxTranslationY(DependencyObject obj, double value)
{
    obj.SetValue(MaxTranslationYProperty, value);
}

public static readonly DependencyProperty MaxTranslationYProperty =
        DependencyProperty.RegisterAttached("MaxTranslationY", typeof(double),
        typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_TRANSLATION_Y));

public static double GetMaxRotationX(DependencyObject obj)
{
    return (double)obj.GetValue(MaxRotationXProperty);
}

public static void SetMaxRotationX(DependencyObject obj, double value)
{
    obj.SetValue(MaxRotationXProperty, value);
}

public static readonly DependencyProperty MaxRotationXProperty =
        DependencyProperty.RegisterAttached("MaxRotationX", typeof(double),
        typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_ROTATION_X));

public static double GetMaxRotationY(DependencyObject obj)
{
    return (double)obj.GetValue(MaxRotationYProperty);
}

public static void SetMaxRotationY(DependencyObject obj, double value)
{
    obj.SetValue(MaxRotationYProperty, value);
}

public static readonly DependencyProperty MaxRotationYProperty =
        DependencyProperty.RegisterAttached("MaxRotationY", typeof(double),
        typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_ROTATION_Y));

Déplacement des composants

Tout le principe de déplacement va s’articuler autour de l’évènement CompositionTarget.Rendering qui sera la boucle principale de rendu (merci Jonathan pour l’astuce).

public SensorAnimatedGrid()
{
    SetValue(AnimatedElementsProperty, new List<AnimatedElement>());
    Loaded += OnLoaded;
}

void OnLoaded(object sender, RoutedEventArgs e)
{
    maxTranslationX = GetMaxTranslationX(this);
    maxTranslationY = GetMaxTranslationY(this);
    maxRotationX = GetMaxRotationX(this);
    maxRotationY = GetMaxRotationY(this);

    CompositionTarget.Rendering += CompositionTargetRendering;
}

void CompositionTargetRendering(object sender, object e)
{
    Animate();
}

Dans un premier temps, on récupère les coordonnées du point P100 puis la position du pointer de la souris par rapport à ce même point.

private void Animate()
{
    var maxGridX = Window.Current.Bounds.Width / 2;
    var maxGridY = Window.Current.Bounds.Height / 2;

    var animatedElements = GetAnimatedElements(this);
    var gridCenter = new Point(maxGridX, maxGridY);
    var pointer = GetPointerPosition();
    pointer.X = pointer.X - gridCenter.X;
    pointer.Y = pointer.Y - gridCenter.Y;

Ensuite, nous calculons les translations et rotations maximales d’un composant en fonction de la distance de la souris par rapport au centre :

var vectorX = pointer.X * maxTranslationX / maxGridX;
var vectorY = pointer.Y * maxTranslationY / maxGridY;
var rotationX = pointer.Y * maxRotationX / maxGridY;
var rotationY = pointer.X * maxRotationY / maxGridX;

La prochaine étape est de parcourir chaque élément de la liste et de calculer le déplacement/la rotation de chacun en fonction de sa profondeur et du déplacement/rotation maximales calculés auparavant :

foreach (var animatedElement in animatedElements)
    {
        var element = GetElementByName(animatedElement.ElementName);
        if (element == null)
            continue;

        element.RenderTransformOrigin = new Point(0.5, 0.5);

        TranslateTransform translate = new TranslateTransform();
        translate.X = GetVectorForDepth(animatedElement.Depth, vectorX);
        translate.Y = GetVectorForDepth(animatedElement.Depth, vectorY);

        PlaneProjection rotate = new PlaneProjection();
        rotate.RotationX = GetRotationForDepth(animatedElement.Depth, rotationX);
        rotate.RotationY = GetRotationForDepth(animatedElement.Depth, rotationY);

        element.RenderTransform = translate;
        element.Projection = rotate;
    }
}

private FrameworkElement GetElementByName(string elementName)
{
    FrameworkElement element;
    _elementsCache.TryGetValue(elementName, out element);
    if (element != null)
        return element;

    element = FindChild<FrameworkElement>(this, elementName);
    if (element == null)
        return null;

    _elementsCache.Add(elementName, element);
    return element;
}

private double GetVectorForDepth(double depth, double vector)
{
    if (depth > GetMaxDepth(this))
        return 0.0;

    return depth * vector / GetMaxDepth(this);
}

private double GetRotationForDepth(double depth, double rotation)
{
    if (depth > GetMaxDepth(this))
        return 0.0;

    return depth * rotation / GetMaxDepth(this);
}

Vous remarquerez que les seuls calculs qui sont faits sont de simples règles de 3 car tout est une question de proportions.

Code final

using System;
using System.Collections.Generic;
using Windows.Devices.Sensors;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace SensorW8.Framework.UI
{
    public class SensorAnimatedGrid : Grid
    {
        #region Dependency Properties

        public static List<AnimatedElement> GetAnimatedElements(DependencyObject obj)
        {
            return (List<AnimatedElement>)obj.GetValue(AnimatedElementsProperty);
        }

        public static void SetAnimatedElements(DependencyObject obj, List<AnimatedElement> value)
        {
            obj.SetValue(AnimatedElementsProperty, value);
        }

        public static readonly DependencyProperty AnimatedElementsProperty =
                DependencyProperty.RegisterAttached(
                "AnimatedElements",
                typeof(List<AnimatedElement>),
                typeof(SensorAnimatedGrid),
                new PropertyMetadata(new List<AnimatedElement>()));

        public static double GetMaxDepth(DependencyObject obj)
        {
            return (double)obj.GetValue(MaxDepthProperty);
        }

        public static void SetMaxDepth(DependencyObject obj, double value)
        {
            obj.SetValue(MaxDepthProperty, value);
        }

        public static readonly DependencyProperty MaxDepthProperty =
                DependencyProperty.RegisterAttached("MaxDepth", typeof(double),
                typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_DEPTH));

        public static double GetMaxTranslationX(DependencyObject obj)
        {
            return (double)obj.GetValue(MaxTranslationXProperty);
        }

        public static void SetMaxTranslationX(DependencyObject obj, double value)
        {
            obj.SetValue(MaxTranslationXProperty, value);
        }

        public static readonly DependencyProperty MaxTranslationXProperty =
                DependencyProperty.RegisterAttached("MaxTranslationX", typeof(double),
                typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_TRANSLATION_X));

        public static double GetMaxTranslationY(DependencyObject obj)
        {
            return (double)obj.GetValue(MaxTranslationYProperty);
        }

        public static void SetMaxTranslationY(DependencyObject obj, double value)
        {
            obj.SetValue(MaxTranslationYProperty, value);
        }

        public static readonly DependencyProperty MaxTranslationYProperty =
                DependencyProperty.RegisterAttached("MaxTranslationY", typeof(double),
                typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_TRANSLATION_Y));

        public static double GetMaxRotationX(DependencyObject obj)
        {
            return (double)obj.GetValue(MaxRotationXProperty);
        }

        public static void SetMaxRotationX(DependencyObject obj, double value)
        {
            obj.SetValue(MaxRotationXProperty, value);
        }

        public static readonly DependencyProperty MaxRotationXProperty =
                DependencyProperty.RegisterAttached("MaxRotationX", typeof(double),
                typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_ROTATION_X));

        public static double GetMaxRotationY(DependencyObject obj)
        {
            return (double)obj.GetValue(MaxRotationYProperty);
        }

        public static void SetMaxRotationY(DependencyObject obj, double value)
        {
            obj.SetValue(MaxRotationYProperty, value);
        }

        public static readonly DependencyProperty MaxRotationYProperty =
                DependencyProperty.RegisterAttached("MaxRotationY", typeof(double),
                typeof(SensorAnimatedGrid), new PropertyMetadata(MAX_ROTATION_Y));

        #endregion

        #region Properties

        private const double MAX_TRANSLATION_X = 50;
        private const double MAX_TRANSLATION_Y = 50;
        private const double MAX_ROTATION_X = 10;
        private const double MAX_ROTATION_Y = 10;
        private const double MAX_DEPTH = 100;

        private double maxTranslationX;
        private double maxTranslationY;
        private double maxRotationX;
        private double maxRotationY;

        private readonly Inclinometer _inclinometer;
        private readonly Dictionary<string, FrameworkElement> _elementsCache = new Dictionary<string, FrameworkElement>();

        #endregion

        #region Constructor

        public SensorAnimatedGrid()
        {
            SetValue(AnimatedElementsProperty, new List<AnimatedElement>());
            Loaded += OnLoaded;
        }

        void OnLoaded(object sender, RoutedEventArgs e)
        {
            maxTranslationX = GetMaxTranslationX(this);
            maxTranslationY = GetMaxTranslationY(this);
            maxRotationX = GetMaxRotationX(this);
            maxRotationY = GetMaxRotationY(this);

            CompositionTarget.Rendering += CompositionTargetRendering;
        }

        void CompositionTargetRendering(object sender, object e)
        {
            Animate();
        }

        private void Animate()
        {
            var maxGridX = Window.Current.Bounds.Width / 2;
            var maxGridY = Window.Current.Bounds.Height / 2;

            var animatedElements = GetAnimatedElements(this);
            var gridCenter = new Point(maxGridX, maxGridY);
            var pointer = GetPointerPosition();
            pointer.X = pointer.X - gridCenter.X;
            pointer.Y = pointer.Y - gridCenter.Y;

            var vectorX = pointer.X * maxTranslationX / maxGridX;
            var vectorY = pointer.Y * maxTranslationY / maxGridY;
            var rotationX = pointer.Y * maxRotationX / maxGridY;
            var rotationY = pointer.X * maxRotationY / maxGridX;

            foreach (var animatedElement in animatedElements)
            {
                var element = GetElementByName(animatedElement.ElementName);
                if (element == null)
                    continue;

                element.RenderTransformOrigin = new Point(0.5, 0.5);

                TranslateTransform translate = new TranslateTransform();
                translate.X = GetVectorForDepth(animatedElement.Depth, vectorX);
                translate.Y = GetVectorForDepth(animatedElement.Depth, vectorY);

                PlaneProjection rotate = new PlaneProjection();
                rotate.RotationX = GetRotationForDepth(animatedElement.Depth, rotationX);
                rotate.RotationY = GetRotationForDepth(animatedElement.Depth, rotationY);

                element.RenderTransform = translate;
                element.Projection = rotate;
            }
        }

        private FrameworkElement GetElementByName(string elementName)
        {
            FrameworkElement element;
            _elementsCache.TryGetValue(elementName, out element);
            if (element != null)
                return element;

            element = FindChild<FrameworkElement>(this, elementName);
            if (element == null)
                return null;

            _elementsCache.Add(elementName, element);
            return element;
        }

        private double GetVectorForDepth(double depth, double vector)
        {
            if (depth > GetMaxDepth(this))
                return 0.0;

            return depth * vector / GetMaxDepth(this);
        }

        private double GetRotationForDepth(double depth, double rotation)
        {
            if (depth > GetMaxDepth(this))
                return 0.0;

            return depth * rotation / GetMaxDepth(this);
        }

        #endregion

        #region VisualTreeHelper

        public static T FindChild<T>(DependencyObject parent, string childName)
        where T : DependencyObject
        {
            // Confirm parent and childName are valid. 
            if (parent == null)
            {
                return null;
            }

            T foundChild = null;

            int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < childrenCount; i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(parent, i);
                // If the child is not of the request child type child
                var childType = child as T;
                if (childType == null)
                {
                    // recursively drill down the tree
                    foundChild = FindChild<T>(child, childName);

                    // If the child is found, break so we do not overwrite the found child. 
                    if (foundChild != null)
                    {
                        break;
                    }
                }
                else if (!string.IsNullOrEmpty(childName))
                {
                    var frameworkElement = child as FrameworkElement;
                    // If the child's name is set for search
                    if (frameworkElement != null && frameworkElement.Name == childName)
                    {
                        // if the child's name is of the request name
                        foundChild = (T)child;
                        break;
                    }

                    // Need this in case the element we want is nested
                    // in another element of the same type
                    foundChild = FindChild<T>(child, childName);
                }
                else
                {
                    // child element found.
                    foundChild = (T)child;
                    break;
                }
            }

            return foundChild;
        }

        #endregion

        #region PointerUtil

        public Point GetPointerPosition()
        {
            Window currentWindow = Window.Current;

            Point point;

            try
            {
                point = currentWindow.CoreWindow.PointerPosition;
            }
            catch (UnauthorizedAccessException)
            {
                return new Point(double.NegativeInfinity, double.NegativeInfinity);
            }

            Rect bounds = currentWindow.Bounds;

            return new Point(DipToPixel(point.X - bounds.X), DipToPixel(point.Y - bounds.Y));
        }

        private double DipToPixel(double dip)
        {
            return (dip * DisplayProperties.LogicalDpi) / 96.0;
        }

        #endregion
    }

    public class AnimatedElement : DependencyObject
    {
        public static readonly DependencyProperty DepthProperty =
            DependencyProperty.Register("Depth", typeof(double), typeof(AnimatedElement), new PropertyMetadata(default(double)));

        public double Depth
        {
            get { return (double)GetValue(DepthProperty); }
            set { SetValue(DepthProperty, value); }
        }

        public static readonly DependencyProperty ElementNameProperty =
            DependencyProperty.Register("ElementName", typeof(string), typeof(AnimatedElement), new PropertyMetadata(default(string)));

        public string ElementName
        {
            get { return (string)GetValue(ElementNameProperty); }
            set { SetValue(ElementNameProperty, value); }
        }
    }
}

Et voilà ! Sourire

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus