Le WCS (Système de Coordonées du "monde") est la base de votre application 3D. En effet, chaque objet peut être représenté dans l'univers par ses coordonées X, Y et Z. Si vous connaissez le plan XY en mathématique, c'est un peu identique dans l'espace : on rajoute juste une coordonnée Z. Vous avez surement remarqué en programmant, mappant ou modelant vous utilisez ce système de coordonées rigides. Pourquoi rigide ? Car les axes XYZ ne changent jamais et sont toujours positionés de la même façon. Ainsi, chaque objet que vous insérerez dans votre monde 3D devra l'être avec des coordonées X, Y et Z.

 

Pour définir les coordonées XYZ d'un point l'on utilise un vecteur. En informatique, on utilisera un array de 3 float ou double pour faire office de vecteur. Un vecteur (1,0,0) signifie que sa coordonées X vaut 1, Y vaut 0 et Z égal 0. De façon général on a : (x,y,z) donne X = x, Y = y, Z = z. Le vecteur proprement dit représente la droite qui joint la coordonées (0,0,0) c'est à dire, l'origine du système, et son point (x,y,z).

 

Pour additionner deux vecteurs (x1, y1, z1) et (x2, y2, z2) on additionne simplement les coordonées respectives tel que (x1+x2, y1+y2, z1+z2). Voici un exemple dans le plan.

V1 + V2 = (x1 + x2, y1 + y2, z1 + z2)

 

Pour soustraire un vecteur V1 d'un autre vecteur V2 l'on peut considérer que l'on additionne à V1, l'opposé de V2. Bref V1 - V2 = V1 + (-V2). Ceci nous permet de conclure que

V1 - V2 = (x1 - x2, y1 - y2, z1 - z2)

 

Pour multiplier un vecteur par un nombre réel (attention : pas un autre vecteur !!!) on multiplie chaque coordonées par ce réel.

r*V1 = (r*x1, r*y1, r*z1)

 

Si vous vous souvenez de Pythagore, vous vous rappelerez surement que a² = b² + c². Or, dans le graphique ci dessous a représente la longueur du vecteur, b sa coordonée X et c sa coordonée Y. La formule est donc sqrt (x² + y²). Dans l'espace cela donne : sqrt (x² + y² + z²). La longueur d'un vecteur V est notée |V|.

|V| = sqrt (xV² + yV² + zV²)

 

Le produit scalaire (ou dot product en anglais) correspond à V1.V2 et se calcule suivant la formule :

V1.V2 = cos (ß)*|V1|*|V2|
où ß est l'angle entre les deux vecteurs ou
V1.V2 = x1*x2 + y1*y2 + z1*z2

dés lors, on peut prouver que l'angle entre deux vecteurs est égal à

cos (ß) = (V1.V2)/(|V1|*|V2|)

où V1.V2 peut être calculé avec la seconde formule. Voici une représentation plus "graphique" du produit scalaire :

dotproduct.jpg (7773 octets)

 

Un vecteur unité est un vecteur dont sa longueur vaut 1. Pour transformer un vecteur quelconque en vecteur unité on divise chacune de ses coordonées par sa longueur.

U = (xV/|V|, yV/|V|, zV/|V|)

 

Le produit vectoriel (ou cross product en anglais) est à dire la multiplication d'un vecteur par un autre vecteur ce qui correspond à une troisième vecteur orthogonaux aux deux premiers.

xV3 = yV1*zV2 - zV1*zV2
yV3 = zV1*xV2 - xV1*zV2
yV3 = xV1*yV2 - yV1*xV2

 

Dans l'espace, le plan est représenté soit par son équation carthésienne soit par son équation vectorielle.

Equation vectorielle : ¶ = V1 + r*V2 + s*V3 où V1 est un point du plan, V2 et V3 sont des vecteurs directeurs appartenant au plan.
Equation carthésienne : ¶ = ax + by + cz + d = 0 où (a,b,c) est le vecteur directeur perpendiculaire au plan.

L'équation carthésienne est la plus utilisée. Le vecteur (a,b,c) est trouvé par vectoriel de deux vecteurs du plan ou, tout simplement, l'équation du plan est obtenue par résolution du détermiant :

où A, B et C sont trois points du plan.

 

A nouveau, une droite peut être représentée soit par son équation carthésienne soit par son equation vectorielle.

Equation vectorielle : ¶ = V1 + r*V2
Equation carthésienne : ¶ = (X - Xa)/(Xb - Xa) = (Y - Ya)/(Yb - Ya) = (Z - Za)/(Zb - Za) où A et B sont des points appartenant à la droite.

On remarquera que V2 et (A - B) sont les vecteurs directeurs de la droite.

 

Pour que deux élements soient perpendiculaires il faut que leur produit scalaire soit nul. Attention : rappelez vous que (a,b,c) représente, dans l'équation du plan, un vecteur perpendiculaire à celui ci. Bref, si le produit scalaire d'une droite et d'un plan est nul, c'est que cette droite est parallèle au plan !

 

Comme vous le savez déjà, dans un jeu 3D le joueur peut se déplacer et, donc, la caméra bouge. Le système référenciel de la caméra est appelé VCS. On entend par là que ce qui se trouve devant votre ecran, sur la droite et vers le haut correspond aux nouveaux axes. Du coup, les vecteurs des axes ne sont plus

Ix = (1,0,0)
Iy = (0,1,0)
Iz = (0,0,1)

car ceci est uniquement valable dans le cas du WCS. Nous allons appliquer une nouvelle formule qui nous permet de trouver les coordonées WCS à partir d'un système VCS.

(x',y',z') = x*Ix + y*Iy + z*Iz

On s'apperçoit immédiatement que si nous nous trouvons dans le WCS, Nous avons : (x',y',z') = x*(1,0,0) + y*(0,1,0) +z*(0,0,1) = (x,0,0) + (0,y,0) + (0,z,0) = (x,y,z). Donc dans le WCS, (x',y',z') = (x,y,z) plutôt normal !

Les formules pour trouver les veteurs forward (droit devant), right (à droite) et up (en haut) sont assez compliquées si vous ne connaissez pas bien les maths mais très simple dans le cas inverse. Il n'y a pas grand chose à expliquer mis à part qu'on utilise les traditionnels sinus et cosinus. Heureusement, dans beaucoup de moteurs 3D des fonctions sont là pour tout nous simplifier la vie. Voici un exemple dans Half-Life :

float length;
float right[3], up[3], forward[3], angles[3], d[3];

d[0] = dest[0] - origin[0];
d[1] = dest[1] - origin[1];
d[2] = dest[2] - origin[2];

length = sqrt (d[0]*d[0] + d[1]*d[1] + d[2]*d[2]);

VectorAngles (d, angles);
angles[0] *= -1;
(*gEngfuncs.pfnAngleVectors) (angles, forward, right, up);

 

Bref, pour calculer les coordonées WCS à partir de coordonées VCS on fait :

right*x + up*y + forward*z

Nous allons voir ici un exemple plus concret d'utilisation du VCS.

 

Vous connaissez le railgun ? C'est une arme qui tire des projectiles en utilisant la force de lorenz (F = BI*sin(alpha) - électromagnétisme). Ceci ne nous intéresse pas tellement. Ce sur quoi nous allons nous pencher est l'effet de l'arme. Un beau tire en spirale fais avec des particules et qui, en plus, bouge !

Dans le WCS, l'équation généralisée de l'hélice est très très très compliquée. Je n'ai même pas essayé de la résoudre. Je me suis donc basé, beaucoup plus simplement, sur ce système VCS. En VCS l'équation de l'hélice est donnée par :

x = cos (i)
y = sin (i)
z = i

où l'on fait incrémenter i.

Donc, l'équation de l'hélice dans le WCS vaut donc :

cos (i)*right + sin (i)*up + i*forward

wow ! C'est simple non ? Il ne reste qu'à faire une petite boucle et incrémenter i. Le code complet sera donné juste après car nous devons d'abord voir un point, très important : le système de particules ! Celui d'half-life ne supporte pas plus de 500 entités, il nous faut donc concevoir le notre, plus puissant !

 

Nous allons concevoir ici un système de particules simple. Vous pourrez l'amméliorer par la suite si vous le désirez.

typedef struct
{
    float origin[3]; // l'origine de notre particule
    float velocity[3]; // la destination de notre particule
    float life; // la vie de notre particule, en seconde
    float emit_time; // date de création
    float size; // la taille en pixel
    bool active; // cette particule est elle libre ou non ?
    struct model_s *pSpriteModel; // le pointeur vers la texture
} oparticle_t;

Ceci est notre structure. Nous allons définir un array de 10000 particules.

#define MAX_PARTICLES    10000

oparticle_t particles[MAX_PARTICLES];

Nous allons devoir initialiser tout à zéro pour être sur de ne pas avoir de problèmes par la suite. Cette fonction doit être appelée dans le HUD_VidInit.

void reset_particles (void)
{
    memset (particles, 0, sizeof (oparticle_t)*MAX_PARTICLES);
}

Dans la fonction suivante nous allons créer notre particule. Nous prenons toute la liste et regardons si une particule est inactive. Si tel est la cas alors nous inscrivons notre particule dessus.

void emit_particle (char *pSpriteName, float origin[3], float velocity[3], float life, float size)
{
    oparticle_t *p;
    int i;
   
    for (i=0;i<MAX_PARTICLES;i++)
    {
        p = &particles[i];

        if (p->active)
            continue;

        p->active = true;
        p->life = life;
        p->size = size;
        p->emit_time = gEngfuncs.GetClientTime ();
        p->pSpriteModel = (struct model_s *)gEngfuncs.GetSpritePointer (SPR_Load (pSpriteName));

        memcpy (p->origin, origin, sizeof (float)*3);
        memcpy (p->velocity, velocity, sizeof (float)*3);

        break;
    }
}

Nous allons maintenant calculer la position actuelle de la particule de façon linéaire. C'est à dire qu'elle se situera à x% de la distance origin/destination. x etant calculé avec le temps de vie. Nous regardons egalement si la particule se situe bien dans le vide. Si la particule touche qqchose ou dépasse sont temps de vie, nous l'éffaçons.

void update_particle (oparticle_t *p, float m_flTime, float forward[3], float right[3], float up[3])
{
    float origin[3];
    float color[4];

    float percent = ((m_flTime - p->emit_time)/p->life);

    origin[0] = p->origin[0] + p->velocity[0]*percent;
    origin[1] = p->origin[1] + p->velocity[1]*percent;
    origin[2] = p->origin[2] + p->velocity[2]*percent;

    color[0] = 1.0f;
    color[1] = 1.0f;
    color[2] = 1.0f;
    color[3] = 1.0f - percent;

    draw_particle (p->pSpriteModel, origin, color, p->size, forward, right, up);

    if (gEngfuncs.PM_PointContents (origin, NULL) != CONTENTS_EMPTY)
        p->active = false;

    if (m_flTime - p->emit_time >= p->life)
        p->active = false;
}

void RenderAllParticles (float m_flTime)
{
    int i;
    float forward[3], right[3], up[3], angles[3];

    (*gEngfuncs.GetViewAngles) (angles);
    (*gEngfuncs.pfnAngleVectors) (angles, forward, right, up);

    for (i=0;i<MAX_PARTICLES;i++)
        if (particles[i].active)
            update_particle (&particles[i], m_flTime, forward, right, up);
}

Nous allons maintenant dessiner la particule. Si nous affichons juste un quad nous aurons un problème lorsque le joueur se déplacera. En effet, au bout d'un court moment celui ci vera la "tranche" du quad. Nous allons à nouveau utiliser le système VCS pour placer notre quad perpendiculairement à la vue du joueur.

void draw_particle (struct model_s *pSpriteModel, float origin[3], float color[4], float size, float forward[3], float right[3], float up[3])
{
    float vec[3];

    gEngfuncs.pTriAPI->SpriteTexture (pSpriteModel, 0 );

    gEngfuncs.pTriAPI->RenderMode (kRenderTransAdd);
    gEngfuncs.pTriAPI->CullFace (TRI_FRONT);
    gEngfuncs.pTriAPI->Begin (TRI_QUADS);

// Haut à gauche
    vec[0] = -right[0]*size + up[0]*size;
    vec[1] = -right[1]*size + up[1]*size;
    vec[2] = -right[2]*size + up[2]*size;

    gEngfuncs.pTriAPI->Color4f (color[0], color[1], color[2], color[3]);
    gEngfuncs.pTriAPI->Brightness (1);
    gEngfuncs.pTriAPI->TexCoord2f (0.0f, 0.0f);
    gEngfuncs.pTriAPI->Vertex3f (origin[0] + vec[0], origin[1] + vec[1], origin[2] + vec[2]);

// haut à droite
    vec[0] = right[0]*size + up[0]*size;
    vec[1] = right[1]*size + up[1]*size;
    vec[2] = right[2]*size + up[2]*size;

    gEngfuncs.pTriAPI->Color4f (color[0], color[1], color[2], color[3]);
    gEngfuncs.pTriAPI->Brightness (1);
    gEngfuncs.pTriAPI->TexCoord2f (1.0f, 0.0f);
    gEngfuncs.pTriAPI->Vertex3f (origin[0] + vec[0], origin[1] + vec[1], origin[2] + vec[2]);

// bas à droite
    vec[0] = right[0]*size - up[0]*size;
    vec[1] = right[1]*size - up[1]*size;
    vec[2] = right[2]*size - up[2]*size;

    gEngfuncs.pTriAPI->Color4f (color[0], color[1], color[2], color[3]);
    gEngfuncs.pTriAPI->Brightness (1);
    gEngfuncs.pTriAPI->TexCoord2f (1.0f, 1.0f);
    gEngfuncs.pTriAPI->Vertex3f (origin[0] + vec[0], origin[1] + vec[1], origin[2] + vec[2]);

// bas à gauche
    vec[0] = -right[0]*size - up[0]*size;
    vec[1] = -right[1]*size - up[1]*size;
    vec[2] = -right[2]*size - up[2]*size;

    gEngfuncs.pTriAPI->Color4f (color[0], color[1], color[2], color[3]);
    gEngfuncs.pTriAPI->Brightness (1);
    gEngfuncs.pTriAPI->TexCoord2f (0.0f, 1.0f);
    gEngfuncs.pTriAPI->Vertex3f (origin[0] + vec[0], origin[1] + vec[1], origin[2] + vec[2]);

    gEngfuncs.pTriAPI->End ();
    gEngfuncs.pTriAPI->RenderMode (kRenderNormal);
}

Il ne reste plus qu'à appeler la fonction RenderAllParticules dans le fichier tri.cpp, fonction RenderTransparentTriangles.

 

Voilà donc la fonction tant attendue que vous pourrez dés à présent comprendre sans aucun problèmes si vous avez bien suivis ce tutorial. J'ai ajouté beaucoup de paramètres à mon code pour vous otpimaliser ma fonction empiriquement.

void OgGiZ_DrawRailEffect (float* origin, float* dest)
{
    float length;
    int i;
    float d[3], v[3], angles[3], n[3];
    double fsin, fcos, alpha;

    const int jump = 10;
    const float s1 = 1.0f;
    const float s2 = 12.5f;
    const float period = 0.16f;

    float right[3], up[3], forward[3];
   
    d[0] = dest[0] - origin[0];
    d[1] = dest[1] - origin[1];
    d[2] = dest[2] - origin[2];

    length = sqrt (d[0]*d[0] + d[1]*d[1] + d[2]*d[2]);

    VectorAngles (d, angles);
    angles[0] *= -1;

    (*gEngfuncs.pfnAngleVectors) (angles, forward, right, up);

    for (i=0;i<length;i+=jump)
    {
        alpha = (i/jump)*period;
        fsin = sin (alpha);
        fcos = cos (alpha);

        v[0] = s1*right[0]*fcos + s1*up[0]*fsin + origin[0] + i*forward[0];
        v[1] = s1*right[1]*fcos + s1*up[1]*fsin + origin[1] + i*forward[1];
        v[2] = s1*right[2]*fcos + s1*up[2]*fsin + origin[2] + i*forward[2];

        n[0] = s2*right[0]*fcos + s2*up[0]*fsin;
        n[1] = s2*right[1]*fcos + s2*up[1]*fsin;
        n[2] = s2*right[2]*fcos + s2*up[2]*fsin;

        emit_particle ("sprites/railgun.spr", v, n, 2.5f, 2.0f);
    }
}

 

InSaNe^WaRl0rD *OgGiZ*
ogfrit@hotmail.com