CgFX - Des shaders temps réel dans le viewport Maya! - Part 2
Par Narann le jeudi, 27 mai 2010, 23:20 - Script et code - Lien permanent
Maintenant que nous savons comment fonctionnent les shaders CgFX dans Maya, il est temps de faire le notre :sourit: . Je vous propose donc une série de billets sur l'écriture d'un shader CgFX pour Maya de A à Z! Nous verront comment faire une illumination de base, ajouter de l'ambiant (avec une option de falloff) et du bump, avec deux lights. C'est maintenant qu'on va bidouiller! Maintenant qu'il va falloir éveiller votre curiosité et ne pas hésiter à expérimenter.
A la fin de cette série de tutos, vous devriez être capable d'ajouter d'autres effets vous même (speculaire, reflection, etc...), en fouillant dans différents codes! :siffle:
Sommaire:
- Avant de commencer
- Quoi c'est qu'on va faire exactement?
- Le code de base
- On commence à modifier: L'illumination de base
- Ajout de la seconde pointLight
Avant de commencer
Bien entendu, il est fortement conseillé d'avoir lu (et compris) la partie 1 de ce tutorial. Autrement, il y a de fortes chances pour que vous soyez largué assez vite... :baffed:
Deuxième chose: je ne prétends pas ici vous apprendre à coder un shader pour le jeu vidéo. L'écriture de ce code est "Maya oriented". Ainsi, la méthode n'est pas très rigoureuse (au niveau du passage des argument dans les shader par exemple) mais simplifiée au possible pour ne pas passer de temps sur des détails techniques et avoir le plus rapidement possible quelque chose dans sont viewport.
Quoi c'est qu'on va faire exactement?
Le code de base
Voici le code minimal duquel nous allons partir:
/*--- Matrice ---*/ float4x4 wvpMatrix : WorldViewProjection; // On récupère la matrice de la projection de la vue dans le monde /*--- Structures ---*/ struct vIN { float3 Position : POSITION; // La position du vertex en entrée }; struct vOUT { float4 Position : POSITION; // La position du vertex en sortie (doit être de type float4) }; /*--- Vertex Shader ---*/ vOUT mainVS(vIN IN) { // Création de la strucure de sortie vOUT OUT; // Convertion du float3 IN.Position en un float4 float4 Position = float4(IN.Position, 1); // On multiplie la nouvelle position par la matrice de la vue. Puis on remplis la structure de sortie OUT.Position = mul(wvpMatrix, Position); // On renvoit la nouvelle structure de sortie return OUT; } /*--- Pixel Shader ---*/ float4 mainPS(vOUT IN) : COLOR { // Renvoit la couleur du pixel: RGBA return float4(1, 0, 0, 1); } /*--- Techniques ---*/ technique Main { pass p0 // Une seule passe { // Compilation des shaders VertexProgram = compile arbvp1 mainVS(); FragmentProgram = compile arbfp1 mainPS(); } }
La matrice
float4x4 wvpMatrix : WorldViewProjection; // On récupère la matrice de la projection de la vue dans le monde
Ici nous initialisons la matrice qui va nous permettre de calculer la position du vertex en fonction de notre vue.
Les structures d'entrées et de sorties
struct vIN { float3 Position : POSITION; // La position du vertex en entrée }; struct vOUT { float4 Position : POSITION; // La position du vertex en sortie (doit être de type float4) };
- vIN, ce sont les informations qu'on a besoin en entrée.
- vOUT c'est ce qu'on à en sortie du Vertex Shader. Notre vertex "modifié" en fait.
Pour l'instant, nous n'utilisons que les positions des vertex. C'est pourquoi vIN et vOUT contiennent les mêmes choses. Le mot POSITION (en majuscule spécifie le "buffer" dans lequel on stock les informations).
Le vertex shader
vOUT mainVS(vIN IN) { // Création de la strucure de sortie vOUT OUT; // Convertion du float3 IN.Position en un float4 float4 Position = float4(IN.Position, 1); // On multiplie la nouvelle position par la matrice de la vue. Puis on remplis la structure de sortie OUT.Position = mul(wvpMatrix, Position); // On renvoit la nouvelle structure de sortie return OUT; }
C'est le shader (par shader, entendez "program") qui modifie la position (et éventuellement d'autres choses: Normal, UV, vecteur de la lumière, etc..) des vertex. Il prend comme argument la structure d'entrée (vIN) et renvoie la structure de sortie modifiée (vOUT). Si ce shader est très utile pour le jeu vidéo (notamment pour calculer du skinning en temps réel), il l'est beaucoup moins dans notre cas. Il servira surtout à calculer les matrices.
Le pixel shader
float4 mainPS(vOUT IN) : COLOR { // Renvoit la couleur du pixel: RGBA return float4(1, 0, 0, 1); }
Ce shader renvoie une couleur de type float4 (je ne sais pas à quoi sert le dernier composant. Je suppose que c'est l'alpha mais il ne semble pas très bien supporter par Maya). Ici il est très simple (On renvoie du rouge) mais c'est là que beaucoup de choses vont se passer.
Les techniques et les passes
technique Main { pass p0 // Une seule passe { // Compilation des shaders VertexProgram = compile arbvp1 mainVS(); FragmentProgram = compile arbfp1 mainPS(); } }
Les "techniques" permettent de coder plusieurs comportements dans un seul shader. Ici, il n'y en aura qu'une seule. Les passes permettent de calculer plusieurs passes dans un shader (je suppose que pour des choses comme la réfraction ça peut être utile). Idem, nous n'utiliserons qu'une seule passe.
On commence à modifier: L'illumination de base
Bon, le rouge ça a beau être sexy sur une femme, ça l'est beaucoup moins dans un viewport. :sourit: Nous allons "illuminer" tout ça. Pour pouvoir illuminer, il faut une pointLight. Il n'y a pas (à ma connaissance) de moyen de reconnaitre automatiquement les pointLights de vos scènes Maya. Il faut les déterminer dans le shader. Nous allons donc créer un attribut "pointLightPos0":
Création des attributs
La convention veut que les attributs soit créés juste en dessous des matrices et en au dessus des structures.
//Light 0 float4 pointLightPos0 : POSITION < string UIName = "PointLight0 Position"; string Space = "World"; >; float3 pointLightColor0 < string type = "color"; string UIName = "PointLight0 Color"; > = {1.0, 1.0, 1.0}; float pointLightIntensity0 < string UIName = "PointLight0 Intensity"; string UIWidget = "slider"; float UIMin = 0.0; float UIMax = 1.0; float UIStep = 0.001; > = 1; bool bUsePointLight0 < string UIName = "Use PointLight0"; string UIWidget = "RadioButton"; > = true;
On s'arrête un peu sur les différents paramètres utilisés lors des déclarations des attributs (Je tiens à vous prévenir, je ne suis pas hyper calé là dessus):
string UIName = "PointLight0 Position";
C'est le nom que prendra l'attribut dans Maya.
string Space = "World";
J'ai une idée mais impossible de bien l'expliquer. En gros c'est l'espace utilisé pour ladite position.
string type = "color";
Si vous ne mettez pas ça, dans Maya, vous aurez 3 valeurs à rentrer à la main pour la couleur au lieu d'un widget de Color de Maya. C'est pour simplifier disons.
> = {1.0, 1.0, 1.0};
Vous l'aurez compris, c'est la valeur par défaut.
string UIWidget = "slider"; float UIMin = 0.0; float UIMax = 1.0; float UIStep = 0.001;
Le type de widget, ainsi que ses paramètres, toujours dans l'optique d'avoir quelque chose d'assez facile à manipuler dans Maya. :siffle:
Voila, les attributs ont été créés.
Ajout de déclaration de matrices
Pour la suite, nous allons avoir besoin de déclarer deux nouvelles matrices dont nous allons avoir besoin:
float4x4 wMatrix : World; float4x4 wvMatrixIT : WorldInverseTranspose;
Maintenant, direction les déclarations des structures!
Modification des déclarations des structures
Pour vIN, on précise qu'on va avoir besoin de la normal des vertex:
struct vIN { float3 Position : POSITION; // La position du vertex en entrée float4 Normal : NORMAL; // La normal du vertex en entrée };
Et on calculera la normal du vertex par rapport au monde (WorldNormal) ainsi que le vecteur de la pointLight (PointLight0Vec). On doit alors aussi modifier la structure de sortie du vertex:
struct vOUT { float4 Position : POSITION; // La position du vertex en sortie (doit être de type float4) float3 PointLight0Vec : TEXCOORD0; float3 WorldNormal : TEXCOORD1; };
C'est maintenant que ça ce gâte! Direction le vertex shader!
Modification du vertex shader
On commence par le plus simple. Juste après avoir créé la structure de sortie (vOUT), remplissez la WorldNormal:
// Création de la strucure de sortie vOUT OUT; OUT.WorldNormal = mul(wvMatrixIT, IN.Normal).xyz;
On est en plein dans les maths: On multiplie la wvMatrixIT par la normal du vertex. La pointLights maintenant:
// Convertion du float3 IN.Position en un float4 float4 Position = float4(IN.Position, 1); // Convertion en "world" space de la position du vertex float4 PositionWorld = mul(wMatrix, Position);
Et enfin
// Calcul du vecteur de la pointLight OUT.PointLight0Vec = pointLightPos0.xyz - PositionWorld.xyz;
On s'accroche et on va maintenant au pixel shader!
Modification du pixel shader
Dès le début du shader, on normalise les vecteurs en entrée:
float4 mainPS(vOUT IN) : COLOR { // On normalise les vecteurs float3 Nn = normalize(IN.WorldNormal); float3 pL0n = normalize(IN.PointLight0Vec); // Renvoit la couleur du pixel: RGBA return float4(1, 0, 0, 1); }
pL0n pour "pointLight0 normalisé".
Et maintenant, c'est les opérations de calcul de l'illumination:
//Illumination Declarations float3 illumPointLight0 = 0; if(bUsePointLight0) { float pL0dn = dot(pL0n,Nn); illumPointLight0 = max(0, pL0dn); illumPointLight0 *= pointLightColor0*pointLightIntensity0; } float3 result = illumPointLight0; // Renvoit la couleur du pixel: RGBA return float4(result, 1);
Sauvegardez votre fichier .cgfx et rechargez le:
Vous verrez qu'il vous faut remplir un paramètre pour que tout fonctionne bien:
Créez une point light, copiez collez le nom du node de transform de cette pointLight:
Et paf!
Bougez votre pointLight, changez la couleur, l'illumination devrait suivre!
Si à ce stade rien ne fonctionne, la meilleure méthode est de prendre mon shader, vérifier qu'il fonctionne bien chez vous et comparer mon fichier avec le votre (via un logiciel comme winmerge pour voir ou il y a des différences). N'hésitez surtout pas à laisser un commentaire pour me prévenir si un de mes fichiers sources ne fonctionnent pas.
Ajout de la seconde pointLight
Bon, maintenant qu'on à une pointLight, ça serait bien d'en mettre une autre! :hehe:
Vous allez voir que ça n'a rien de bien compliqué.
Ajout des attributs
La c'est de la logique pure. Dans:
/* --- Déclarations des paramètres */
On ajoute:
//Light 1 float4 pointLightPos1 : POSITION < string UIName = "PointLight1 Position"; string Space = "World"; >; float3 pointLightColor1 < string type = "color"; string UIName = "PointLight1 Color"; > = {1.0, 1.0, 1.0}; float pointLightIntensity1 < string UIName = "PointLight1 Intensity"; string UIWidget = "slider"; float UIMin = 0.0; float UIMax = 1.0; float UIStep = 0.001; > = 1; bool bUsePointLight1 < string UIName = "Use PointLight1"; string UIWidget = "RadioButton"; > = true;
Vous l'aurez compris, on copie tous les attributs de la light déjà existante (zero) et on remplace les zéros par des un...
Modification de la structure
Idem pour la structure vOUT:
struct vOUT { float4 Position : POSITION; // La position du vertex en sortie (doit être de type float4) float3 PointLight0Vec : TEXCOORD0; float3 PointLight1Vec : TEXCOORD1; float3 WorldNormal : TEXCOORD2; };
Notez le petit changement de numéro sur les TEXCOORD pour garder un truc clean.
Modification du vertex shader
Ici rien de compliqué non plus, dans:
/*--- Vertex Shader ---*/
// Calcul du vecteur de la pointLight OUT.PointLight0Vec = pointLightPos0.xyz - PositionWorld.xyz; OUT.PointLight1Vec = pointLightPos1.xyz - PositionWorld.xyz;
Modification du pixel shader
C'est là que c'est le plus compliqué (et vous allez voir que ça ne l'est pas non plus réellement). Dans:
/*--- Pixel Shader ---*/ float4 mainPS(vOUT IN) : COLOR { // On normalise les vecteurs float3 Nn = normalize(IN.WorldNormal); float3 pL0n = normalize(IN.PointLight0Vec); float3 pL1n = normalize(IN.PointLight1Vec); //Illumination Declarations float3 illumPointLight0 = 0; float3 illumPointLight1 = 0; if(bUsePointLight0) { float pL0dn = dot(pL0n,Nn); illumPointLight0 = max(0, pL0dn); illumPointLight0 *= pointLightColor0*pointLightIntensity0; } if(bUsePointLight1) { float pL1dn = dot(pL1n,Nn); illumPointLight1 = max(0, pL1dn); illumPointLight1 *= pointLightColor1*pointLightIntensity1; } float3 result = illumPointLight0 + illumPointLight1; // Renvoit la couleur du pixel: RGBA return float4(result, 1); return float4(1,0,0, 1); }
En fait, on calcule les valeurs d'illumination de chaque pointLight séparément (illumPointLight0 et illumPointLight1).
Puis on les additionne:
float3 result = illumPointLight0 + illumPointLight1;
Rechargez:
Rien de bien compliqué, vous en conviendrez. :hihi:
Voici le code source, au cas où:
Fin de ce billet. N'hésitez pas à laisser un commentaire si vous avez des questions ou si je n'ai pas été assez clair sur certains points
Quand vous vous sentez prêt, passez à la suite!
Commentaires
ahhhh !
en rendu les conditions sont a bannir absolument 1 if c'est la mort de ta cg !!!
prefere mélanger des valeur via un lerp, c'est énormément moins couteux.
Je l'attendais celle là! :)
Je me suis douté que du conditionnel n'était pas bon (Comme je l'ai expliqué, je suis un gros noob en RT mais je trouvais ça intéressant d'en parler "aux graphistes").
Comme tu dis, je suppose qu'un blend des couleurs est plus "clean" en terme d'optimisation. Mais je ne connaissais pas cette approche en écrivant mon tuto (en fait, tu me l'apprends! ^^ ).
Je suis preneur de toutes les idées. Pourrait tu m'expliquer ce que tu entend par "c'est la mort de ta cg"? Tu parle en terme de perf (je suppose) ou en terme physique (vu le ton, j'ai douté, même si ça me semble improbable).
Si j'avais eu à utiliser lerp? Comment (grossièrement), j'aurai du m'y prendre avec mon bool "use light"?
Merci pour cette remarque et vos éventuelles réponses.
Dorian
EDIT: Je viens de voir ta réponse sur Mayalounge:
http://mayalounge.com/viewtopic.php?f=5&t=2892&p=50410&e=50410
Merci!
Merci pour toutes ces explications !^c' est très intéressant ! Y aura t-il une suite ? J 'ai aussi une carte ATI, mais je pense prendre une nvidia également.
Hello!
J'aimerai beaucoup continuer ces articles mais le temps me manque en ce moment. Dès que j'en ai, j'avance! :)
Dorian