Terre vue de l'espace

Modélisation géométrique d'une sphère

Petit défis persos (pour le fun) : calculer le rendu d'une sphère sans passer par les méthodes habituelles en ActionScript 3 (drawTriangle, segments, polygones…). La première chose qui m'est venue à l'esprit est un genre d'effet loupe avec un displacementmapfilter. Mais non ! Je veux faire les choses bien, avec une vraie sphère 3D, sans polygones et avec une texture. Une vraie belle sphère quoi... Si cette méthode est classe je me doute bien qu'elle sera extrêmement gourmande ^^

Première étape dessiner la sphère. La première fonction sert à convertir une coordonné sphérique en coordonné cartésienne. Pour cela une formule toute faîte est là pour nous aider :

x = r cos θ cos φ
y = r cos θ sin φ
z = r cos θ
Avec –π/2 ≤ θ ≤ π/2 et –π ≤ φ ≤ π

θ (thêta) correspond à la longitude et φ (phi) à la latitude. r est bien sur le rayon et pour finir nous retournons un Vector3D (c'est encore plus simple que retourner les coordonnés dans un tableau, en plus, ça nous servira pour la suite).

Terre rendu avec sphère NURBS et texture
Rendu avec le script décrit dans cet article (avec les variables _loop = 1600 et taille de rendu = 450*450px).
La texture est une vue satellite issue de la NASA
function sphere( theta:Number, phi:Number ):Vector3D
{
    const r:Number = 200;
    var pos:Vector3D = new Vector3D();
    pos.x = r * Math.cos(theta) * Math.cos(phi);
    pos.y = r * Math.cos(theta) * Math.sin(phi);
    pos.z = r * Math.sin(theta);
    return pos;
}

Deuxième fonction, trouver la couleur (parmi les pixels de notre texture) qui correspond à la coordonné de la sphère. Nous savons que –π/2 ≤ θ ≤ π/2 et –π ≤ φ ≤ π. Pour obtenir nos coordonnés de textures nous allons calculer u (qui, dans notre cas, correspond à la position du pixel dans la largeur de la texture) et v (pareil mais dans la hauteur) à partir de θ et φ. Si notre texture a pour taille 300 x 200 pixels (respectivement largeur puis hauteur) il faut que u et v soient compris entre 0 ≤ u ≤ 300 et 0 ≤ v ≤ 200. Pour passer de θ à u et de φ à v la manipulation est on ne peut plus simple. Voici un exemple avec θ : Valeur initiale de θ : –π/2 ≤ θ ≤ π/2 Nous ajoutons π/2 pour que 0 ≤ θ+π/2 ≤ π Nous divisons θ par π afin que son maximum soit égal à 1 : 0 ≤ (θ+π/2)/π ≤ 1 Et enfin nous multiplions par la largeur de notre image, c’est-à-dire 300. Ce qui nous donne : 0 ≤ 300*(θ+π/2)/π ≤ 300

Rendu de la sphère avec un rafraîchissement et une rotation
function uvTexture( theta:Number, phi:Number ):Point
{
    theta +=  Math.PI / 2;
    phi +=  Math.PI;
    var u:int = Math.round( ( phi * 300 ) / ( 2 * Math.PI ) );
    var v:int = Math.round( ( theta * 200 ) / ( Math.PI ) );
    return new Point( u, v );
}

Mais comment passer d'une coordonné en 3 dimensions en un affichage en 2 dimensions. Facile vous dira Thalès ! En passant cette méthode est facile à mettre en place pour placer des clips sur une scène tout en les laissant face à nous. La position x, y est toute trouvée et la variable scale correspond à l'échelle de notre objet.

function pos3Dto2D( pos3D:Vector3D ):Point
{
    const focal:Number = 1000;
    var scale:Number = focal / pos3D.z;
    var pos2D:Point = new Point();
    pos2D.x = scale * pos3D.x;
    pos2D.y = scale * pos3D.y;
    return pos2D;
}

Nous avons fait le tour, il ne manque plus qu'à réunir tout cela et lancer les calculs.

import flash.display.BitmapData;
import flash.display.Bitmap;
import flash.geom.Point;
import flash.geom.Vector3D;
import flash.geom.Matrix3D;


// Racine carré du nombre de boucles
// (plus y en a plus on calcule de points et plus ça rame)
const _loop:int = 300;


// Taille du Bitmap de rendu (plus il est grand plus il faudra de boucles)
const _widthRender:int = 450 / 4;
const _heightRender:int = 450 / 4;


// Bitmap qui accueille l'image de la sphère
var _render_bd:BitmapData = new BitmapData( _widthRender, _heightRender, false, 0x000000 );
var _render_b:Bitmap = new Bitmap( _render_bd, "auto", true );
_render_b.scaleX = _render_b.scaleY = 4;
addChild( _render_b );


// Bitmap de la texture
var _widthTexture:int = 800;
var _heightTexture:int = 400;
var _map_bd:BitmapData = new Map();


// rendu de notre sphère
render();
function render():void
{
    // Initialisation de nos variables
    var i:int,j:int;
    var pos3D:Vector3D;
    var pos2D:Point;

    var theta:Number, phi:Number;
    var color:uint;


    // Matrice pour infliger des rotations à notre sphère
    var m:Matrix3D = new Matrix3D();
    // Pour redresser la sphère et avoir le nord en haut ^^
    m.appendRotation( -90, Vector3D.X_AXIS );



    this._render_bd.lock();

    // Calcul de chaque pixel visible de la sphère
    for ( i = 0; i < this._loop; i++)
    {
        for ( j = 0 ; j < this._loop ; j++ )
        {
            // Récupération des coordonnés sphériques en prennant soin de les équilibrer à partir de la boucle
            theta = (i / this._loop) * Math.PI - Math.PI / 2;
            phi = (j / this._loop) * 2 * Math.PI - Math.PI;

            pos3D = sphere( theta, phi );
            pos3D = m.transformVector( pos3D );

            // Si le point fait parti de la face caché de notre sphère on arrête le calcul pour lui
            if (pos3D.z > 0) continue;

            pos2D = pos3Dto2D( pos3D );
            color = uvTexture( theta, phi );

            this._render_bd.setPixel( Math.round(pos2D.x), Math.round(pos2D.y), color );
        }
    }

    this._render_bd.unlock();
}


// passage d'une coordonée cartésienne 3D à une coordonnée cartésienne 2D
function pos3Dto2D( pos3D:Vector3D ):Point
{
    // Déplacement de la sphère afin quelle ne soit supperposé au point de vue
    pos3D.z +=  5000;

    var scale:Number = 1000 / pos3D.z;
    var pos2D:Point = new Point(scale * pos3D.x + this._widthRender /2, scale * pos3D.y + this._heightRender /2);

    return pos2D;
}


// Récupération de la couleur de la map en fonction de la coordonée sphérique
function uvTexture( theta:Number, phi:Number ):uint
{
    phi +=  Math.PI;
    theta +=  Math.PI / 2;
    var u:int = int( ( phi * this._widthTexture ) / ( 2 * Math.PI ) );
    var v:int = int( ( theta * this._heightTexture ) / ( Math.PI ) );

    return this._map_bd.getPixel( u , v );
}


// Passage de coordonées sphériques aux coordonées cartésiennes
function sphere( theta:Number, phi:Number ):Vector3D
{
    const r:Number = 200;
    var pos:Vector3D = new Vector3D();

    pos.x = r * Math.cos(theta) * Math.cos(phi);
    pos.y = r * Math.cos(theta) * Math.sin(phi);
    pos.z = r * Math.sin(theta);

    return pos;
}

Prochaine étape, calculer le rendu de ma sphère non plus à partir de chacun de ses points, mais plutôt en partant des coordonnés de ma scène afin de réduire les calculs inutiles. Affaire à suivre...