Hello !
Je commence le thread sur les tunnels demandé par Kaneel.
Wondersonic a déjà posté un lien très intéressant par Iñigo (comme tout ce qu'il écrit d'ailleurs) à lire avant d'aller plus loin :
http://iquilezles.org/www/articles/deform/deform.htmSinon, tout dépend de ce qu'on entend par "tunnel" : sur ST on mettait des points sur des cercles en perspective, on faisait bouger les cercles et on était content.

Là j'imagine évidemment que c'est plus des effets de déformation d'image dont on parle. La bonne vieille technique des tables d'offsets / tables d'indirections :

En shader, ça s'écrit :
float2 ImageUV = Offset.Sample( ScreenUV ).xy; // Indirection
float4 Color = Texture.Sample( ImageUV );
Tout le secret réside dans la capacité à coder une table d'offset sympa. Iñigo file plein d'exemples faciles à tester.
On peut évidemment pousser le concept plus loin en faisant plusieurs indirections, qui donne un effet kaléidoscope:
float2 OffsetUV = Offset0.Sample( ScreenUV ).xy; // 1ère Indirection
float2 ImageUV = Offset1.Sample( OffsetUV ).xy; // 2nde Indirection
float4 Color = Texture.Sample( ImageUV );
On peut aussi conserver le résultat de la frame précédente et s'en servir comme texture dans la frame suivante. Là ça file un effet reverb et un mal de tête assez costaud.
On peut aussi décider d'animer le tout, soit en déplaçant la table d'offset ou, plus généralement, en animant les coordonnées de texture.
Par exemple, on fait scroller la texture en faisant :
const float2 ScrollingSpeed = float2( 0.0, 0.1 ); // On avance d'un dixième de texture par seconde sur l'axe V
float2 ImageUV = Offset.Sample( ScreenUV ).xy;
ImageUV += Time * ScrollingSpeed;
float4 Color = Texture.Sample( ImageUV );
Et on la fait rotater comme ça :
const float RotationSpeed = 0.5 * PI; // On rotate d'un quart de cercle par seconde
float2 ImageUV = Offset.Sample( ScreenUV ).xy;
float Angle = Time * RotationSpeed;
float2 cs = float2( cos( Angle ), sin( angle ) );
float2 cs2 = float2( -cs.y, cs.x );
float2 RotatedImageUV = 0.5 + float2( dot( ImageUV-0.5, cs ), dot( ImageUV-0.5, cs2 ) );
float4 Color = Texture.Sample( RotatedImageUV );
Maintenant, le tunnel à proprement parler.
On va causer d'un tunnel droit, un bête tube pour commencer.
L'idée est de remapper l'image sur un tube infini dont l'axe est perpendiculaire à l'écran.
Vue de dessus en coupe, ça donne ça :

L'idée est de calculer l'intersection de tous les rayons de caméra (en orange sur l'image du dessus) pour chaque point de l'écran, avec un cylindre qui représente notre tounel.
Alors normalement, il faudrait faire du raytracing, delta = b*b - 4*a*c et tout le tintouin mais pour un cylindre comme le nôtre, c'est assez simple.
_ On fixe comme condition que la caméra ne bouge pas et se trouve en (0,0,0)
_ Le rayon du cylindre est fixe, genre 1, la caméra est donc à une unité de distance des bords du cylindre
_ Le cylindre est aligné sur l'axe Z
Pour générer nos rayons de caméra, on utilise la procédure :
float FOV = 0.5 * PI; // Field of view de 90°
float TanHalfFOV = tanf( 0.5 * FOV );
float AspectRatio = width / height;
for ( int y=0; y < height; y++ )
{
float fY = 1.0f - 2.0f * (float) y / height;
for ( int x=0; x < width; x++ )
{
float fX = 2.0f * (float) x / width - 1.0f;
Vector3 View( AspectRatio * fX, fY, 1.0f );
View.Normalize();
(do something with view)
(...)
}
}
Calculer l'intersection du rayon avec le cylindre revient à faire :
float HitDistance = CylinderRadius / (1.0f - View.Z); // Distance à laquelle le rayon touche le cylindre
Vector3 HitPosition = HitDistance * View; // Position du hit dans l'espace
(si vous voulez les détails du calcul, demandez-moi)
A noter une p'tite déformation sympa si on écrit :
float HitDistance = 1.0 / (1.0 - pow( View.z, 1.5 )); // Déformation amplifiée, on peut faire varier le POW pour d'autres effets
Voici une image de la distance des points du tunnel à la caméra (HitDistance), qu'on va utiliser comme coordonnée V dans la texture :

Maintenant qu'on a la distance (donc la coordonnée V), on a besoin de mapper la texture en U sur la surface du cylindre.
Il s'agit simplement de faire un mapping angulaire : l'angle du rayon de caméra indique une valeur entre 0 et 2PI qu'on normalise entre 0 et 1 pour obtenir U :
float NormalizedAngle = 1.0f + atan2( View.y, View.x ) / PI; // Là, on obtient une valeur entre 0 et 2 puisqu'atan2() renvoie toujours des valeurs entre -PI et +PI
Pour contraindre les UV entre 0 et 1, on applique un modulo :
Vector2 UV( (1.0 * Angle) % 1.0, (0.1 * HitDistance) % 1.0 );
Ici, j'ai utilisé l'angle entre 0 et 2 tel quel, la texture sera donc mappée 2 fois sur le tour du cylindre, et j'ai divisé la distance par 10.
La texture rebouclera donc toutes les 10 unités en profondeur.
NOTE: Là je calcule des UV pour les textures, comme dans un shader.
Si vous voulez du vrai old-school avec une texture 256x256 qui est drôlement pratique, vous calculez vos UV comme ça :
U16 UV = ((0.1 * HitDistance * 256) & 0xFF) << 8) | (((1.0 * Angle * 256) & 0xFF);
Et voici l'image des UV :

Le code final pour précalculer la table d'offsets est donc le suivant :
float FOV = 0.5 * PI; // Field of view de 90°
float TanHalfFOV = tanf( 0.5 * FOV );
float AspectRatio = width / height;
Vector2[,] Offsets = new Vector2[height,width];
for ( int y=0; y < height; y++ )
{
float fY = 1.0f - 2.0f * (float) y / height;
for ( int x=0; x < width; x++ )
{
float fX = 2.0f * (float) x / width - 1.0f;
Vector3 View( AspectRatio * fX, fY, 1.0f );
View.Normalize();
float HitDistance = CylinderRadius / (1.0f - View.Z);
Vector3 HitPosition = HitDistance * View;
float NormalizedAngle = 1.0f + atan2( View.y, View.x ) / PI;
Vector2 UV( (1.0 * Angle) % 1.0, (0.1 * HitDistance) % 1.0 );
Offsets[y,x] = UV;
}
}
Le code qui utilise la table d'offsets à chaque frame :
for ( int y=0; y < height; y++ )
for ( int x=0; x < width; x++ )
{
Vector2 UV = Offsets[y,x]; // Fetch des offsets
Vector3 Color = Texture.Sample( UV ).xyz; // Fetch de la texture
Display( Color ); // Bisou tantrique
}
Qui donne ça :

Et c'est pas beau ! Il faut embélir tout ce merdier maintenant.
NOTE: l'espèce de barre horizontale au milieu à gauche est dans la texture, c'est pas un bug rassurez-vous !
Pour préparer le terrain, on va conserver d'autres infos en + des UV pour chaque pixel.
Par exemple, on va vouloir réaliser un éclairage dynamique du tunnel comme pour les objets 3D classiques.
On va donc avoir besoin des normales, de la position et de la distance de chaque point...
On modifie donc le code de précalcul pour générer plusieurs tables :
float FOV = 0.5 * PI; // Field of view de 90°
float TanHalfFOV = tanf( 0.5 * FOV );
float AspectRatio = width / height;
Vector2[,] Offsets = new Vector2[height,width];
Vector4[,] Position_Distances = new Vector4[height,width];
Vector3[,] Normals = new Vector3[height,width];
for ( int y=0; y < height; y++ )
{
float fY = 1.0f - 2.0f * (float) y / height;
for ( int x=0; x < width; x++ )
{
float fX = 2.0f * (float) x / width - 1.0f;
Vector3 View( AspectRatio * fX, fY, 1.0f );
View.Normalize();
float HitDistance = CylinderRadius / (1.0f - View.Z);
Vector3 HitPosition = HitDistance * View;
float NormalizedAngle = 1.0f + atan2( View.y, View.x ) / PI;
Vector2 UV( (1.0 * Angle) % 1.0, (0.1 * HitDistance) % 1.0 );
Offsets[y,x] = UV;
Position_Distances[y,x] = Vector4( HitPosition, HitDistance ); // On stocke la position et la distance du point
// La normale est simplement -View qu'on aplatit en Z et qu'on renormalise derrière
Vector3 Normal( -View.xy, 0.0f );
Normal.Normalize();
Normals[y,x] = Normal;
}
}
Voici le code avec une simple attenuation de la couleur avec la distance pour cacher les pixels moches au fond :
for ( int y=0; y < height; y++ )
for ( int x=0; x < width; x++ )
{
Vector2 UV = Offsets[y,x]; // Fetch des offsets
Vector3 Color = Texture.Sample( UV ).xyz; // Fetch de la texture
Color /= max( 1.0f, 0.1f * Position_Distances[y,x].w ); // Atténuation avec la distance
Display( Color ); // Bisou tantrique
}
Ca donne ça :

Bon, c'est un peu mieux mais moche quand même.
Finalement, on va faire un bond de géant en calant une chouette lampe dynamique pour éclairer ce tube digestif.
On définit une position 3D pour la lampe et l'équation d'éclairage c'est :
I = I0 * max( 0, L.N ) / D²
I0 = intensité de la lampe
L direction du point éclairé jusqu'à la lampe
N normal au point éclairé
D distance entre la lamp et le point
Qu'on code comme ça :
Vector3 LightPos( 0.0f, 0.7f, 4.0f );
Vector3 LightColor( 6.0f, 6.0f, 6.0f );
for ( int y=0; y < height; y++ )
for ( int x=0; x < width; x++ )
{
Vector2 UV = Offsets[y,x]; // Fetch des offsets
Vector3 Position = Position_Distances[y,x].xyz; // Fetch de la position
Vector3 Normal = Normals[y,x].xyz; // Fetch de la normale
Vector3 Color = Texture.Sample( UV ).xyz; // Fetch de la texture
Color /= max( 1.0f, 0.1f * Position_Distances[y,x].w ); // Atténuation avec la distance
// Eclairage
Vector3 ToLight = LightPos - Position;
float SqD = ToLight.LengthSquared();
float D = sqrtf( SqD );
ToLight /= D; // Normalisation
float Dot = Vector3.Dot( ToLight, Normal );
// Dot = max( 0.0f, Dot ); // Inutile car la lampe est toujours dans le tube
Vector3 LightIntensity = LightColor * Dot / SqD;
Color *= LightIntensity;
Display( Color ); // Bisou tantrique
}
Et ça rend comme ça :

C'est quand même beaucoup mieux !
Dernière étape, on va rajouter une normal map.
Pour ça, on a besoin d'une normal map qui corresponde à notre image diffuse (ça tombe bien, j'ai ça sous la main tiens !).
Les normal maps définissent la normale d'une surface dans l'espace tangentiel à cette même surface.
Pour le cylindre, on a déjà sa normale qui pointe vers son axe central.
On a besoin de la tangente et de la bitangente (ou binormale selon les appelations).
Il y a la tangente qui est super simple à évaluer puisque c'est l'axe du cylindre :
T = (0,0,1) (i.e. selon l'axe Z)
Et finalement, la bitangente est simplement orthogonale à la normale en 2D, qu'on a déjà :
B = ( N.y, -N.x, 0.0 )
On écrit donc le code final :
Vector3 LightPos( 0.0f, 0.7f, 4.0f );
Vector3 LightColor( 6.0f, 6.0f, 6.0f );
for ( int y=0; y < height; y++ )
for ( int x=0; x < width; x++ )
{
Vector2 UV = Offsets[y,x]; // Fetch des offsets
Vector3 Position = Position_Distances[y,x].xyz; // Fetch de la position
Vector3 Normal = Normals[y,x].xyz; // Fetch de la normale
Vector3 Tangent( 0.0f, 0.0f, 1.0f ); // Facile !
Vector3 BiTangent( -Normal.Y, Normal.X, 0.0f ); // Facile aussi !
Vector3 Color = Texture.Sample( UV ).xyz; // Fetch de la texture
Color /= max( 1.0f, 0.1f * Position_Distances[y,x].w ); // Atténuation avec la distance
Vector3 NormalTS = NormalMap.Sample( UV ).xyz; // Fetch de la normale
NormalTS = 2.0f * NormalTS - 1.0f; // Qu'on réorganise pour avoir des composants entre -1 et +1
NormalTS.XY *= 2.0; // On amplifie un peu la normale (dans mon cas, elle était un peu mollassonne)
// On calcule la normale finale
Vector3 FinalNormal = NormalTS.X * BiTangent + NormalTS.Y * Tangent + NormalTS.Z * Normal;
// Eclairage
Vector3 ToLight = LightPos - Position;
float SqD = ToLight.LengthSquared();
float D = sqrtf( SqD );
ToLight /= D; // Normalisation
float Dot = Vector3.Dot( ToLight, FinalNormal );
// Dot = max( 0.0f, Dot ); // Inutile car la lampe est toujours dans le tube
Vector3 LightIntensity = LightColor * Dot / SqD;
Color *= LightIntensity;
Display( Color ); // Bisou tantrique
}
Et finalement, on obtient ça :

Je m'arrêterai là pour l'instant. Il faudrait encore parler des tunnels qui se déforment, de ceux qui tournent, de ceux qui sont faits en ray-trace temps-réel, etc.
Je sais pas trop ce qui vous intéresse parmi ces catégories ?
J'avais fait un truc y a longtemps que j'avais appelé le "vaginel" : un tunnel qui se déforme avec un terrain. C'était un mix entre les algos de terrains de l'époque en horizon flottant et les techniques de tunnel.
Chais pas si ça peut intéresser des gens ?
Bref, laissez-moi savoir (comme on dit en québéquie).
Bisous !