Auteur Sujet: Effets Tunnels  (Lu 3852 fois)

0 Membres et 1 Invité sur ce sujet

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.htm

Sinon, 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. ;D

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 !

Citer
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.

Comme celui qu'on voit dans "Stars : Wonders of the world" the NoooN ?
J'avais essayé de le reproduire sur DS (dans Lack Of Disco), mais je crois que ça n'a sauté aux yeux de personne ^^

Pour info j'avais commencé par faire un voxel standard, et au lieu de l'afficher normalement je tirais des lignes vers le centre de l'écran, en partant du fond. C'était pas mal mais supra lent.

Finalement j'ai choisi de faire mon voxel puis de le "déformer en rond" avec une formule de conversion de coordonnées polaires. Beaucoup plus rapide, mais après j'ai dû foirer un truc dans les réglages. Et puis la texture et la heightmap jouent beaucoup à l'effet final je pense.

En tout cas le tunnel que tu nous montres, avec le normal mapping ça claque !!!

En tout cas le tunnel que tu nous montres, avec le normal mapping ça claque !!!

Il manque le flot d'ordures charriées par de l'eau verdâtre en bas, et quelques rats sur les côtés et ça sera parfait !


Tu aurais des plans pour des tunnels plus oldschool?
Je me suis demandé si c'était possible à faire, par exemple, sur GBA mais je me dis qu'étant donné que la machine ne pourrait plotter assez vite, il doit y avoir une combine bien oldschool pour y arriver!

Citer
la machine ne pourrait plotter assez vite
Elles disent toutes ça.

(EDIT : la vache le tuto, ca pourrait faire un article comme on avait prévu à la base)

C'est ce que disait mon ex :(

Tain pata impressionant !
Tu devrais repenser a une reconversion en prof ou faire un bouquin genre la "Demo scene pour les nuls"

Citer
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 ?


Haha toujours aussi créatif sur les noms aussi en tout cas cela me rappelle la demo stars de Noon



En tout cas continue et bonnes fêtes a tous les sceners de la place ouaiche ouaiche ! ;)

Tu aurais des plans pour des tunnels plus oldschool?
Je me suis demandé si c'était possible à faire, par exemple, sur GBA mais je me dis qu'étant donné que la machine ne pourrait plotter assez vite, il doit y avoir une combine bien oldschool pour y arriver!

Bah "old school", "old school", vous êtes bien gentils mais c'était les mêmes algos, sauf qu'on packait tout dans des tables comme on pouvait...
Là je vous vois en train de hausser les sourcils en vous disant : "Mais il est con ? Il fait des sqrt() à chaque pixel, comment je fais ça sur ma GBA moi, gros tocard ?!"

Ce à quoi je répondrai : coder old school c'est mettre ces formules dans des tables, ni plus ni moins !

Par exemple, l'algo d'éclairage.
Calcul de la table :
float		LightIntensity = 6.0f;	// On a vu que 6 c'était bien
float[,] LightTable = new float[256,256];
for ( int y=0; y < 256; y++ )
{ // Cette dimension code la position de la lampe radialement (0=centre du tubuluk, 255=bord)

Vector2 LightPosition( (float) y / 256, 0.0f ); // Position de la lampe sur l'axe X entre 0 (centre) et 1 (bord)

for ( int x=0; x < 256; x++ )
{ // Cette dimension code l'angle par rapport à la lampe (0=0°, 255=360°)

float Angle = 2.0f * PI * x / 256;;

Vector2 Position( cosf( Angle ), sinf( Angle ) ); // Position du bord du tunnel dans une tranche 2D
Vector2 Normal = -Position; // Normale au tubuklup à cette position

Vector2 ToLight = Position - LightPosition;
float SqDistance2Light = ToLight.LengthSquare();
float Distance2Light = sqrtf( SqDistance2Light );
ToLight /= Distance2Light;

// Et hop ! On enfourne le résultat !
LightTable[y,x] = LightIntensity * Vector2.Dot( ToLight, Normal ) / SqDistance2Light;
}
}

Maintenant, calcul de la table d'atténuation de la lampe quand on s'éloigne de la tranche 2D où elle se trouve :
float[]	AttenuationTable = new float[256];	// C'est une table 1D
for ( int x=0; x < 256; x++ )
{
float fDistance = 10.0f * x / 255.0f; // On va prendre une distance de 10 unités max
AttenuationTable[x] = 1.0f / (1.0f+fDistance*fDistance); // Atténuation en 1/d²
}

Je refais le code vu précédemment mais qui utilise les tables cette fois-ci :
Vector3	LightPos( 0.0f, 0.7f, 4.0f );	// Toujours la même position en 3D

// On calcule la distance de la lampe au centre
byte LightTableV = (byte) (255 * sqrtf( LightPos.x*LightPos.x + LightPos.y*LightPos.y ));

// On calcule l'angle de base de rotation
byte LightTableU (byte) (128 + 127 * atan2( LightPos.y, LightPos.x ) / PI);

for ( int y=0; y < height; y++ )
for ( int x=0; x < width; x++ )
{
ushort UV = Offsets[y,x]; // Fetch des offsets (maintenant ils sont en byte !)
float Distance = Position_Distances[y,x].w; // Fetch de la distance
Vector3 Color = Texture.Sample( UV ).xyz; // Fetch de la texture

// On calcule l'éclairage
byte NewLightTableU = (UV + LightTableUV) & 0xFF; // Angle entre le point courant (qui a déjà son propre U) et la lampe (qui a aussi son propre U), mais comme tout ce beau monde est entre 0 & 255, ça se AND bien...
float LightIntensity = LightTable[LightTableV,NewLightTableU];

// On calcule l'atténuation (un peu fausse quand même) avec la distance en Z par rapport à la position de la lampe
float Distance2Light = abs( Distance - LightPos.z );
Distance2Light = min( 10.0f, Distance2Light ); // On la clampe à 10 car on n'a codé que 10 unités de distance dans notre table
byte AttenuationU = (byte) (255 * Distance2Light);
float LightAttenuation = AttenuationTable[AttenuationU]; // Fetch !

LightIntensity *= LightAttenuation;

// Couleur finale
Color *= LightIntensity;

// Et le bisou tantrique
Display( Color );
}

Alors évidemment ça marche pas comme ça ! Il faut bien calibrer ses tables entre 0 et 255, en utiliser encore davantage, utiliser moins de floats voire aucun, limiter les casts, travailler en prémultiplié, etc.
Mais remarquez simplement qu'il n'y a déjà plus du tout d'instructions tabernacles comme sqrt et compagnie. Ou même de divisions, qu'on chassait à vue à l'époque !

L'idée étant de minimiser le nombre d'instructions dans la main loop pour arriver au "zenop" (le nop zen, qui fait tout bien qu'il ne fasse aucune opération).
Je m'en suis approché une fois, je l'ai vu qui fuyait dans la brume tel le dragon sauvage des steppes d'asie centrale. Pour qui sait précalculer ses tables, il sera peut-être possible de l'atteindre !

Bien le bisou.


Tu m'impressionneras toujours, Patapom !

Tu devrais vraiment faire une prod. :)

nystep

  • Invité
Cool, des tunnels  ;D
Joli screens...

Perso, le point sur lequel je bloque pour cet effet, c'est de savoir comment on fait pour enlever la ligne qui est due à la discontinuité dans atan2 (même problème pour le atan sur GPU en GLSL... que ce soit sur ATI ou NV..).

Si vous avez une idée... :)

nystep

  • Invité
Et je viens de retrouver un vieu screenshot de tunnel d'une démo jamais finie (encore une! lol)  ;)

(sur GPU la discontinuité atan fait une ligne verticale, assez bien masquée dans ce screenshot en fait...) :)

Perso, le point sur lequel je bloque pour cet effet, c'est de savoir comment on fait pour enlever la ligne qui est due à la discontinuité dans atan2 (même problème pour le atan sur GPU en GLSL... que ce soit sur ATI ou NV..).

Si vous avez une idée... :)

Bah là c'est du HLSL, j'utilise un atan2() comme indiqué dans le code filé + haut, j'ai pas de discontinuité pourtant.
Je suis en "Clamp" par contre, t'es sûr que t'es pas en wrap plutôt ?
Tu parles bien du rebouclage à l'envers de toute la texture ou j'ai pas compris ce que tu dis ?

nystep

  • Invité
Ah oui, exact, le clamp! Merci beaucoup.  :)

Nom: E-mail:
Vérification:
Quel est la couleur du cheval rouge d'Henri IV ?: