Utiliser les UV pour déplacer un objet.

Utiliser les UV pour déplacer un objet.

Utiliser les UV pour déplacer un objet.

L’idée est d’utiliser les UV d’un maillage pour appliquer une force sur un objet.

Dans l’application suivante, préparée pour ce billet, on peut voir que les billes sont accélérées par des canaux.

Application (1200×680)

Les logiciels utilisés sont Blender, Gimp, Substance Painter, Unity3D et Visual Studio 2017.

Pour ce cour l’objet qui subira l’accélération sera une bille et le circuit sera un ensemble de canaux.

Gestion des collisions:

Pour commencer on cré un script qui va détecter les collisions. La fonction à surchargée est OnCollisionStay. Pour chaque bille qui touche la surface d’un canal semi-cylindrique il y a très souvent plusieurs points d’impacts.
Je vous propose donc de faire:

  1. la moyenne des directions obtenue qui seront valides.
  2. l’affichage de la normale et des coordonnées de textures.
  3. l’affichage du produit vectoriel résultant.
  4. afficher les triangles touchés par la bille.
using UnityEngine;
using System.Collections;

namespace su
{
    struct PointDir
    {
        public Vector3 pos;
        public Vector3 dir;
        public bool available;
    };

    Vector3 ToLocal(Vector3 inPosition, Transform inTransform)
    {
        return inTransform.TransformVector(inPosition) + inTransform.position;
    }

    public class Accelerator3D : MonoBehaviour
    {
        public float m_Magnitude = 1.0F;

        void Start()
        {}

        void Update()
        {}

        PointDir GetTangent( RaycastHit inRayCast, Vector3 inSphereCenter, Collision inCollision, Vector3 inNormal )
        {
            PointDir result = new PointDir
            {
                dir = new Vector3(),
                pos = new Vector3(),
                available = false
            };

            //* Pour comprendre que le produit vectoriel de la normale et des coordonnées de texture ne suffiront pas pour donner la bonne direction.
            Debug.DrawRay( inRayCast.point, inRayCast.normal, Color.white );  
            Debug.DrawRay( inRayCast.point, inRayCast.textureCoord, Color.red);                              
            Debug.DrawRay( inRayCast.point, Vector3.Cross( inRayCast.normal, inRayCast.textureCoord ), Color.yellow );
            //*/

            MeshCollider meshCollider = inRayCast.collider as MeshCollider;

            // On récupère la géométrie pour afficher les triangles impactés.
            if( meshCollider )
            {  
                Mesh mesh = meshCollider.sharedMesh; // meshCollider.sharedMesh // GetComponent<MeshFilter>().mesh

                Vector3 [] vertices = mesh.vertices;
                int [] triangles = mesh.triangles;

                Vector3 v0 = vertices[triangles[inRayCast.triangleIndex * 3 + 0]],
                        v1 = vertices[triangles[inRayCast.triangleIndex * 3 + 1]],
                        v2 = vertices[triangles[inRayCast.triangleIndex * 3 + 2]];

                //* Dessiner les triangles qui ont été touché.
                Debug.DrawLine( ToLocal(v0, meshCollider.transform), ToLocal(v1, meshCollider.transform), Color.green );  
                Debug.DrawLine( ToLocal(v1, meshCollider.transform), ToLocal(v2, meshCollider.transform), Color.green );  
                Debug.DrawLine( ToLocal(v2, meshCollider.transform), ToLocal(v0, meshCollider.transform), Color.green );
                //*/

                // CODE A venir pour déterminer la direction de l'accélération.
            }
            return result;

        }

        void OnCollisionStay( Collision inCollision )
        {
            if( inCollision.gameObject.tag == "Balles" )
            {
                PointDir results = new PointDir
                {
                    dir = new Vector3(),
                    pos = new Vector3(),
                    available = true
                };
                uint results_count = 0;

                foreach( ContactPoint aHit in inCollision.contacts )          
                {
                    Vector3 aHitPoint = aHit.point,
                            aSphereCenter = inCollision.transform.position;  

                    RaycastHit [] aRayCastHits = Physics.RaycastAll( aSphereCenter, aHit.normal, LayerMask.NameToLayer( "Accelerateur" ) );
                    RaycastHit aRaycastHit = new RaycastHit();

                    float aDistance = Mathf.Infinity;        

                    foreach( RaycastHit aIt in aRayCastHits )
                    {
                        if( aDistance > aIt.distance )
                        {
                            aDistance = aIt.distance;
                            aRaycastHit = aIt;
                        }
                    }

                    if( aDistance != Mathf.Infinity )
                    {
                        PointDir result = GetTangent( aRaycastHit, aSphereCenter, inCollision, aHit.normal );

                        if(result.available)
                        {
                            results.pos += result.pos;
                            results.dir += result.dir;
                            results_count += 1;
                        }
                    }
                }

                if(results_count > 0)
                {
                    Vector3 dir = results.dir / results_count;
                    Vector3 pos = results.pos / results_count;

                    dir *= m_Magnitude;

                    inCollision.rigidbody.AddForceAtPosition( dir, pos, ForceMode.Impulse );

                }
            }        
        }
    }
} // namespace su

Voilà les informations qu’on obtient.

  1. Il peux y avoir plusieurs impacts en même temps.
  2. Le produit vectoriel entre la normale et les coordonnées de textures ne suffisent pas pour déterminer la direction de l’accélération.
  3. En vert: les triangles touchés.
  4. En blanc: les normales de la surface aux points de contacts.
  5. En rouge: les coordonnées de textures de la surface de contacts.
  6. En jaune: le résultat des produits vectoriels.



Choix de l’orientation de l’accélération dans le repère UV:

Du coup l’objectif va être de récupérer manuellement les coordonnées du vecteur qui donne la direction de l’accélération. Pour ce faire on utilise les UV des sommets du triangle impacté comme des poids. Ces poids vont permettre de retrouver la position du barycentre dans ce triangle.
Ici l’explication qui m’a permis de retrouver les poids d’un point 2D quelconque dans un triangle.
Calculate-uv-coordinates-of-3d-point-on-plane-of-m

Premièrement il faut choisir l’orientation de l’accélération dans le repère UV.

J’ai donc choisi le vecteur bas. Qui dans Unity s’obtient ainsi:

Vector2.down

Voici comment on obtient les coordonnées UV de la somme du point d’impact et du vecteur Vector.down.

Vector2 uv0 = texcoord[triangles[inRayCast.triangleIndex * 3 + 0]],
        uv1 = texcoord[triangles[inRayCast.triangleIndex * 3 + 1]],
        uv2 = texcoord[triangles[inRayCast.triangleIndex * 3 + 2]];

Vector2 aTexCoord = inRayCast.textureCoord + Vector2.down * 0.001F;

Il faut remarquer ce 0.01. Il a été obtenue de manière empirique.
Pour une valeur à 0.1 les billes se bloquent aux bords des maillages.
Plus petit vous risquer de perdre en précision.
Si vous avez des problèmes sur les bords de vos maillages descendez la valeur.
C’est la seul partie du code qui est un peu ésotérique et qu’il faudrait améliorer ou du moins comprendre pourquoi.

Déterminer les coordonnées 3D du vecteur accélération:

Une fois l’orientation de l’accélération choisie il faut déterminé le barycentre de la somme du point d’impact et de ce vecteur Vector.down. Pour ce faire on calcul l’air du triangle puis celles des trois triangles formées par le point: aTexCoord. Cela permet d’avoir un rapport des airs et d’établir les poids au point: aTexCoord. Ce qui permet de déterminer sa position 3D à partir des positions des trois sommet du triangle.

float a = TriangleAera( uv0, uv1, uv2 );

if(a > 0)
{
    float a1 = TriangleAera( uv1, uv2, aTexCoord ) / a,
          a2 = TriangleAera( uv2, uv0, aTexCoord ) / a,
          a3 = TriangleAera( uv0, uv1, aTexCoord ) / a;

    Vector3 aBarycentre = inRayCast.barycentricCoordinate,                        
            aForward2 = ( a1 * v0 ) + ( a2 * v1 ) + ( a3 * v2 ),
            aForward = aForward2 - ( ( aBarycentre.x * v0 ) + ( aBarycentre.y * v1 ) + ( aBarycentre.z * v2 ) );

Le code pour calculer l’air d’un triangle est le suivant:

float TriangleAera( Vector2 inPointA, Vector2 inPointB, Vector2 inPointC )
{
    // https://fr.wikipedia.org/wiki/Aire_d'un_triangle
    float aDet = ( inPointB.x - inPointA.x ) * ( inPointC.y - inPointA.y ) - ( inPointC.x - inPointA.x ) * ( inPointB.y - inPointA.y );
    return 0.5F * Mathf.Abs( aDet );
}

Je vous met ici la documentation de la fonction RaycastHit::BarycentricCoordinate, elle évite de calculer les poids du barycentre du point d’impact.

Gestion des transformations:

Ensuite il faut traiter les transformations et surtout l’échelle. L’échelle des objets peut amèner à des résultats où l’accélération fait réellement n’importe quoi, à tel point qu’il est difficile de comprendre que cela provient de l’échelle des objets.

if(aForward.magnitude > 0.00001)
{  
    Vector3 aTangent = this.transform.localToWorldMatrix * aForward.normalized;

    // L'échelle des maillages joue un rôle qui modifie les valeurs des tangente. Il faut donc
    // quitter l'effet des échelles des maillage, afin d'avoir une direction homogène pour toutes les surfaces.
    // Attention ne pas utiliser d'échelle négative.
    aTangent = ScaleVectorImpulse(aTangent, meshCollider.transform);
    aTangent.Normalize();
    //Debug.DrawRay( inRayCast.point, aTangent, Color.yellow );

    result.available = true;
    result.pos = inRayCast.point;
    result.dir = aTangent;
}

Et la fonction «ScaleVectorImpoluse»

Vector3 ScaleVectorImpulse(Vector3 inPos, Transform inTransform)
{
    Vector3 result = inPos;

    result.x /= inTransform.lossyScale.x;
    result.y /= inTransform.lossyScale.y;
    result.z /= inTransform.lossyScale.z;

    return result;
}

Et voilà.

Le code dans son intégralité:

using UnityEngine;
using System.Collections;


namespace Su
{
    struct PointDir
    {
        public Vector3 pos;
        public Vector3 dir;
        public bool available;
    };

    public class Accelerator3D : MonoBehaviour
    {
        public float m_Magnitude = 1.0F;

        // Use this for initialization
        void Start()
        {}

        // Update is called once per frame
        void Update()
        {}

        float TriangleAera( Vector2 inPointA, Vector2 inPointB, Vector2 inPointC )
        {
            // https://fr.wikipedia.org/wiki/Aire_d'un_triangle
            float aDet = ( inPointB.x - inPointA.x ) * ( inPointC.y - inPointA.y ) - ( inPointC.x - inPointA.x ) * ( inPointB.y - inPointA.y );
            return 0.5F * Mathf.Abs( aDet );
        }

        Vector3 ToLocal(Vector3 inPosition, Transform inTransform)
        {
            return inTransform.TransformVector(inPosition) + inTransform.position;
        }

        Vector3 ScaleVectorImpulse(Vector3 inPos, Transform inTransform)
        {
            Vector3 result = inPos;

            result.x /= inTransform.lossyScale.x;
            result.y /= inTransform.lossyScale.y;
            result.z /= inTransform.lossyScale.z;

            return result;
        }

        PointDir GetTangent( RaycastHit inRayCast, Vector3 inSphereCenter, Collision inCollision, Vector3 inNormal )
        {
            PointDir result = new PointDir
            {
                dir = new Vector3(),
                pos = new Vector3(),
                available = false
            };

            /* Pour comprendre que le produit vectoriel de la normale et des coordonnées de texture ne suffiront pas pour donner la bonne direction.
            Debug.DrawRay( inRayCast.point, inRayCast.normal, Color.white );  
            Debug.DrawRay( inRayCast.point, inRayCast.textureCoord, Color.red );                              
            Debug.DrawRay( inRayCast.point, Vector3.Cross( inRayCast.normal, inRayCast.textureCoord ), Color.yellow );
            //*/

            MeshCollider meshCollider = inRayCast.collider as MeshCollider;


            if( meshCollider )
            {        
                // On récupère la géométrie
                Mesh mesh = meshCollider.sharedMesh;

                Vector3 [] vertices = mesh.vertices;
                Vector2 [] texcoord = mesh.uv;
                int [] triangles = mesh.triangles;

                Vector3 v0 = vertices[triangles[inRayCast.triangleIndex * 3 + 0]],
                        v1 = vertices[triangles[inRayCast.triangleIndex * 3 + 1]],
                        v2 = vertices[triangles[inRayCast.triangleIndex * 3 + 2]];
                /* Dessiner les triangles qui ont été touché.
                Debug.DrawLine( ToLocal(v0, meshCollider.transform), ToLocal(v1, meshCollider.transform), Color.green );  
                Debug.DrawLine( ToLocal(v1, meshCollider.transform), ToLocal(v2, meshCollider.transform), Color.green );  
                Debug.DrawLine( ToLocal(v2, meshCollider.transform), ToLocal(v0, meshCollider.transform), Color.green );
                //*/

                Vector2 uv0 = texcoord[triangles[inRayCast.triangleIndex * 3 + 0]],
                        uv1 = texcoord[triangles[inRayCast.triangleIndex * 3 + 1]],
                        uv2 = texcoord[triangles[inRayCast.triangleIndex * 3 + 2]];

                Vector2 aTexCoord = inRayCast.textureCoord + Vector2.down * 0.001F;

                // http://answers.unity3d.com/questions/383804/calculate-uv-coordinates-of-3d-point-on-plane-of-m.html

                float a = TriangleAera( uv0, uv1, uv2 );

                if(a > 0)
                {
                    float a1 = TriangleAera( uv1, uv2, aTexCoord ) / a,
                          a2 = TriangleAera( uv2, uv0, aTexCoord ) / a,
                          a3 = TriangleAera( uv0, uv1, aTexCoord ) / a;

                    //Debug.DrawLine( uv0, aTexCoord, Color.red );  
                    //Debug.DrawLine( uv1, aTexCoord, Color.green );  
                    //Debug.DrawLine( uv2, aTexCoord, Color.blue );  

                    Vector3 aBarycentre = inRayCast.barycentricCoordinate,                        
                            aForward2 = ( a1 * v0 ) + ( a2 * v1 ) + ( a3 * v2 ),
                            aForward = aForward2 - ( ( aBarycentre.x * v0 ) + ( aBarycentre.y * v1 ) + ( aBarycentre.z * v2 ) );

                    /*
                    Debug.DrawLine( ToLocal(a1 * v0, meshCollider.transform), ToLocal(v0, meshCollider.transform), Color.red );  
                    Debug.DrawLine( ToLocal(a2 * v1, meshCollider.transform), ToLocal(v1, meshCollider.transform), Color.green );  
                    Debug.DrawLine( ToLocal(a3 * v2, meshCollider.transform), ToLocal(v2, meshCollider.transform), Color.blue );
                    //*/
                    /*
                    Debug.DrawRay( inRayCast.point, aForward.normalized, Color.red );  
                    Debug.DrawRay( inRayCast.point, aForward2.normalized, Color.magenta );  
                    Debug.DrawLine( ToLocal(aForward2, meshCollider.transform), inRayCast.point, Color.blue );  
                    Debug.DrawLine( ToLocal(aForward, meshCollider.transform), inRayCast.point, Color.cyan );  
                    //*/    

                    if(aForward.magnitude > 0.00001)
                    {  
                        Vector3 aTangent = this.transform.localToWorldMatrix * aForward.normalized;

                        // L'échelle des maillages joue un rôle qui modifie les valeurs des tangente. Il faut donc
                        // quitter l'effet des échelles des maillage, afin d'avoir une direction homogène pour toutes les surfaces.
                        // Attention ne pas utiliser d'échelle négative.
                        aTangent = ScaleVectorImpulse(aTangent, meshCollider.transform);
                        aTangent.Normalize();
                        //Debug.DrawRay( inRayCast.point, aTangent, Color.yellow );

                        result.available = true;
                        result.pos = inRayCast.point;
                        result.dir = aTangent;
                    }
                }
            }  

            return result;

        }

        void OnCollisionStay( Collision inCollision )
        {
            if( inCollision.gameObject.tag == "Balles" )
            {
                PointDir results = new PointDir
                {
                    dir = new Vector3(),
                    pos = new Vector3(),
                    available = true
                };
                uint results_count = 0;

                foreach( ContactPoint aHit in inCollision.contacts )          
                {
                    Vector3 aHitPoint = aHit.point,
                            aSphereCenter = inCollision.transform.position;  

                    //Debug.DrawRay( aSphereCenter, aHit.normal, Color.red );  

                    RaycastHit [] aRayCastHits = Physics.RaycastAll( aSphereCenter, aHit.normal, LayerMask.NameToLayer( "Accelerateur" ) );//, LayerMask.NameToLayer( "Accelerateur" ) ); //  Mathf.Infinity    
                    RaycastHit aRaycastHit = new RaycastHit();

                    float aDistance = Mathf.Infinity;        

                    foreach( RaycastHit aIt in aRayCastHits )
                    {
                        if( aDistance > aIt.distance )
                        {
                            aDistance = aIt.distance;
                            aRaycastHit = aIt;
                        }
                    }

                    if( aDistance != Mathf.Infinity )
                    {
                        PointDir result = GetTangent( aRaycastHit, aSphereCenter, inCollision, aHit.normal );

                        if(result.available)
                        {
                            results.pos += result.pos;
                            results.dir += result.dir;
                            results_count += 1;
                        }
                    }
                }

                if(results_count > 0)
                {
                    Vector3 dir = results.dir / results_count;
                    Vector3 pos = results.pos / results_count;

                    dir *= m_Magnitude;

                    inCollision.rigidbody.AddForceAtPosition( dir, pos, ForceMode.Impulse );

                    //Debug.DrawRay( pos, dir.normalized, Color.yellow );                
                }
            }

        }
    }
}
suryavarman