Vous voulez apprendre à ajouter des textures, des éclairages, des ombres, des cartes normales, des objets lumineux, une occlusion ambiante et d'autres effets à votre jeu 3D? Super! Cet article présente un ensemble de techniques d'ombrage qui peuvent élever le niveau des graphismes de votre jeu à de nouveaux sommets. J'explique chaque technique de telle manière que vous pouvez appliquer / porter ces informations sur n'importe quelle pile d'outils, que ce soit Godot, Unity ou autre.
Comme «colle» entre les shaders, j'ai décidé d'utiliser le magnifique moteur de jeu Panda3D et OpenGL Shading Language (GLSL). Si vous utilisez la même pile, vous obtiendrez un avantage supplémentaire - vous apprendrez à utiliser des techniques d'ombrage spécifiquement dans Panda3D et OpenGL.
La préparation
Vous trouverez ci-dessous le système que j'ai utilisé pour développer et tester l'exemple de code.
Mercredi
L'exemple de code a été développé et testé dans l'environnement suivant:
- Linux manjaro 4.9.135-1-MANJARO
- Chaîne de rendu OpenGL: GeForce GTX 970 / PCIe / SSE2
- Chaîne de version OpenGL: 4.6.0 NVIDIA 410.73
- g ++ (GCC) 8.2.1 20180831
- Panda3D 1.10.1-1
Matériaux
Chacun des matériaux
Blender utilisés pour créer
mill-scene.egg
a deux textures.
La première texture est une carte normale, la seconde est une carte diffuse. Si un objet utilise les normales de ses sommets, une carte normale «bleu clair» est utilisée. Du fait que tous les modèles ont les mêmes cartes dans les mêmes positions, les shaders peuvent être généralisés et appliqués au nœud racine du graphe de la scène.
Notez que le graphique de scène est une
fonctionnalité de l'
implémentation du moteur Panda3D.
Voici une carte normale à une couleur contenant uniquement la couleur
[red = 128, green = 128, blue = 255]
.
Cette couleur indique l'unité normale, indiquant dans le sens positif de l'axe z
[0, 0, 1]
.
[0, 0, 1] = [ round((0 * 0.5 + 0.5) * 255) , round((0 * 0.5 + 0.5) * 255) , round((1 * 0.5 + 0.5) * 255) ] = [128, 128, 255] = [ round(128 / 255 * 2 - 1) , round(128 / 255 * 2 - 1) , round(255 / 255 * 2 - 1) ] = [0, 0, 1]
Ici, nous voyons l'unité normale
[0, 0, 1]
convertie en bleu clair
[128, 128, 255]
, et le bleu uni converti en unité normale.
Ceci est décrit plus en détail dans la section sur les techniques normales de superposition de carte.
Panda3d
Dans cet exemple de code,
Panda3D est utilisé comme «colle» entre les shaders. Cela n'affecte pas les techniques décrites ci-dessous, c'est-à-dire que vous pouvez utiliser les informations étudiées ici dans n'importe quelle pile ou moteur de jeu sélectionné. Panda3D offre certaines commodités. Dans l'article dont j'ai parlé, vous pouvez soit trouver leur homologue dans votre pile, soit les recréer vous-même s'ils ne sont pas sur la pile.
Il convient de noter que le
gl-coordinate-system default
,
textures-power-2 down
et
textures-auto-power-2 1
ont été ajoutés à
config.prc
. Ils ne sont pas contenus dans la
configuration Panda3D standard.
Par défaut, Panda3D utilise un système de coordonnées droitier avec un axe z vers le haut, tandis qu'OpenGL utilise un système de coordonnées droitier avec un axe y vers le haut.
gl-coordinate-system default
vous permet de vous débarrasser des transformations entre deux systèmes de coordonnées à l'intérieur des shaders.
textures-auto-power-2 1
nous permet d'utiliser des tailles de texture qui ne sont pas des puissances de deux, si le système les prend en charge.
Cela est pratique lors de l'exécution de SSAO ou de la mise en œuvre d'autres techniques dans un écran / fenêtre, car la taille de l'écran / fenêtre n'est généralement pas une puissance de deux.
textures-power-2 down
réduit la taille des textures à une puissance de deux si le système ne prend en charge que les textures de tailles égales à deux.
Exemple de code de génération
Si vous souhaitez exécuter l'exemple de code, vous devez d'abord le générer.
Panda3D fonctionne sur Linux, Mac et Windows.
Linux
Commencez par
installer le SDK Panda3D pour votre distribution.
Trouvez où se trouvent les en-têtes et les bibliothèques Panda3D. Ils se trouvent très probablement dans
/usr/include/panda3d/
et dans
/usr/lib/panda3d/
.
Ensuite, clonez ce référentiel et accédez à son répertoire.
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
Compilez maintenant le code source dans un fichier de sortie.
g++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-O2 \
-I/usr/include/python2.7/ \
-I/usr/include/panda3d/
Après avoir créé le fichier de sortie, créez un fichier exécutable en associant le fichier de sortie à ses dépendances.
g++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/usr/lib/panda3d \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread
Voir le
manuel Panda3D pour plus d'informations.
Mac
Commencez par installer le
SDK Panda3D pour Mac.
Trouvez où se trouvent les en-têtes et les bibliothèques de Panda3D.
Ensuite, clonez le référentiel et accédez à son répertoire.
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
Compilez maintenant le code source dans un fichier de sortie. Vous devez trouver où se trouvent les répertoires include dans Python 2.7 et Panda3D.
clang++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-g \
-O2 \
-I/usr/include/python2.7/ \
-I/Developer/Panda3D/include/
Après avoir créé le fichier de sortie, créez un fichier exécutable en associant le fichier de sortie à ses dépendances.
Vous devez trouver où se trouvent les bibliothèques Panda3D.
clang++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/Developer/Panda3D/lib \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread
Voir le
manuel Panda3D pour plus d'informations.
Windows
Commencez par
installer le SDK Panda3D pour Windows.
Trouvez où se trouvent les en-têtes et les bibliothèques Panda3D.
Clonez ce référentiel et accédez à son répertoire.
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
Voir le
manuel Panda3D pour plus d'informations.
Lancer la démo
Après avoir créé l'exemple de code, vous pouvez exécuter le fichier exécutable ou la démo. C'est ainsi qu'ils fonctionnent sous Linux ou Mac.
./3d-game-shaders-for-beginners
Et donc ils fonctionnent sous Windows:
3d-game-shaders-for-beginners.exe
Contrôle clavier
La démo dispose d'un contrôle au clavier qui vous permet de déplacer la caméra et de changer l'état de divers effets.
Mouvement
w
- pénétrer profondément dans la scène.a
- faites pivoter la scène dans le sens des aiguilles d'une montre.s
- éloignez-vous de la scène.d
- faire pivoter la scène dans le sens antihoraire.
Effets commutables
y
- activer SSAO.Shift
+ y
- désactiver SSAO.u
- inclusion de circuits.Shift
+ u
- désactiver les contours.i
- activer la floraison.Shift
+ i
- désactiver la floraison.o
- activer les cartes normales.Shift
+ o
- désactiver les cartes normales.p
- inclusion de brouillard.Shift
+ p
- désactiver le brouillard.h
- l'inclusion de la profondeur de champ.Shift
+ h
- désactiver la profondeur de champ.j
- activer la postérisation.Shift
+ j
- désactiver la postérisationk
- activer la pixellisation.Shift
+ k
- désactiver la pixelisation.l
- affûtage.Shift
+ l
- désactiver la netteté.n
inclusion de grain de film.Shift
+ n
- désactive le grain du film.
Système de référence
Avant de commencer à écrire des shaders, vous devez vous familiariser avec les systèmes de référence ou systèmes de coordonnées suivants. Tous se résument à la provenance des coordonnées actuelles de l'origine de la référence
(0, 0, 0)
. Dès que nous le découvrons, nous pouvons les transformer en utilisant une sorte de matrice ou un autre espace vectoriel. En règle générale, si la sortie d'un shader ne semble pas correcte, la cause est la confusion des systèmes de coordonnées.
Modèle
Le système de coordonnées du modèle ou de l'objet est relatif à l'origine du modèle. Dans les programmes de modélisation tridimensionnelle, par exemple, dans Blender, il est généralement placé au centre du modèle.
Le monde
L'espace mondial est relatif à l'origine de la scène / niveau / univers que vous avez créé.
Revue
L'espace de coordonnées de la vue est relatif à la position de la caméra active.
Coupure
Espace de détourage par rapport au centre du cadre de la caméra. Toutes les coordonnées sont homogènes et sont dans l'intervalle
(-1, 1)
. X et y sont parallèles au film de la caméra et la coordonnée z est la profondeur.
Tous les sommets qui ne se trouvent pas dans les limites de la pyramide de visibilité ou du volume de visibilité de la caméra sont coupés ou supprimés. Nous voyons comment cela se produit avec un cube tronqué derrière par le plan éloigné de la caméra, et avec un cube situé sur le côté.
Écran
L'espace d'écran est (généralement) relatif au coin inférieur gauche de l'écran. X passe de zéro à la largeur de l'écran. Y passe de zéro à la hauteur de l'écran.
GLSL
Au lieu de travailler avec un pipeline de fonctions fixes, nous utiliserons un pipeline de rendu GPU programmable. Puisqu'il est programmable, nous devons lui-même lui passer le code du programme sous forme de shaders. Un shader est un programme (généralement petit) créé avec une syntaxe ressemblant au langage C. Un pipeline de rendu GPU programmable se compose de différentes étapes qui peuvent être programmées à l'aide de shaders. Différents types de shaders comprennent les vertex shaders, les tessellation shaders, les shaders géométriques, fragmentaires et informatiques. Pour utiliser les techniques décrites dans l'article, il nous suffit d'utiliser vertex et fragment
étapes.
#version 140 void main() {}
Voici le shader GLSL minimal, composé du numéro de version GLSL et de la fonction principale.
#version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec4 p3d_Vertex; void main() { gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }
Voici le vertex shader GLSL tronqué, qui transforme le sommet d'entrée en espace de découpage et affiche cette nouvelle position comme une position de sommet uniforme.
La procédure
main
ne renvoie rien, car elle est
void
, et la variable
gl_Position
est la sortie en ligne.
Deux mots clés méritent d'être mentionnés:
uniform
et
in
.
Le mot-clé
uniform
signifie que cette variable globale est la même pour tous les sommets. Panda3D définit lui-même
p3d_ModelViewProjectionMatrix
et pour chaque sommet, il s'agit de la même matrice.
Le mot clé
in
signifie que cette variable globale est passée au shader. Un vertex shader obtient chaque vertex composé de la géométrie, auquel un vertex shader est attaché.
#version 140 out vec4 fragColor; void main() { fragColor = vec4(0, 1, 0, 1); }
Voici le shader de fragment GLSL découpé, affichant le vert opaque comme couleur du fragment.
N'oubliez pas qu'un fragment n'affecte qu'un seul pixel d'écran, mais plusieurs fragments peuvent affecter un pixel.
Faites attention au mot clé out.
Le mot clé
out
signifie que cette variable globale est définie par le shader.
Le nom
fragColor
facultatif, vous pouvez donc en choisir un autre.
Voici la sortie des deux shaders illustrés ci-dessus.
Rendu de texture
Au lieu de rendre / dessiner directement sur l'écran, l'exemple de code utilise une technique pour
le nom "rendu en texture" (rendu en texture). Pour effectuer un rendu sur une texture, vous devez configurer le tampon d'image et y lier la texture. Vous pouvez lier plusieurs textures à un seul tampon d'image.
Les textures liées au tampon de trame stockent les vecteurs renvoyés par le fragment shader. Habituellement, ces vecteurs sont des vecteurs de couleur
(r, g, b, a)
, mais ils peuvent être des positions ou des vecteurs normaux
(x, y, z, w)
. Pour chaque texture liée, un ombrage de fragment peut générer un vecteur distinct. Par exemple, on peut déduire en un seul passage la position et la normale du sommet.
La majeure partie de l'exemple de code qui fonctionne avec Panda3D est liée à la définition
des textures de tampon de trame . Pour simplifier les choses, chaque fragment de shader dans l'exemple de code n'a qu'une seule sortie. Cependant, pour garantir une fréquence d'images élevée (FPS), nous devons produire autant d'informations que possible à chaque passage de rendu.
Voici deux structures de texture pour le tampon de trame de l'exemple de code.
La première structure transforme une scène de moulin à eau en une texture de tampon de trame à l'aide d'une variété de vertex et de shaders de fragments. Cette structure passe par chacun des sommets de la scène avec le moulin et le long des fragments correspondants.
Dans cette structure, l'exemple de code fonctionne comme suit.
- Enregistre les données de géométrie (par exemple, la position ou le sommet normal) pour une utilisation future.
- Enregistre les données de matériau (par exemple, couleur diffuse) pour une utilisation future.
- Crée une liaison UV de différentes textures (diffuses, normales, ombrées, etc.).
- Calcule l'éclairage ambiant, diffus, réfléchi et émis.
- Rend le brouillard.
La deuxième structure est une caméra orthogonale visant un rectangle en forme d'écran.
Cette structure ne traverse que quatre pics et leurs fragments correspondants.
Dans la deuxième structure, l'exemple de code effectue les actions suivantes:
- Traite la sortie d'une autre texture de tampon de trame.
- Combine différentes textures de tampon de trame en une seule.
Dans l'exemple de code, nous pouvons voir la sortie d'une texture de tampon d'image, définissant l'image correspondante sur true et false sur toutes les autres.
Texturation
La texturation est la liaison d'une couleur ou d'un autre vecteur à un fragment en utilisant les coordonnées UV. Les valeurs de U et V varient de zéro à un. Chaque sommet reçoit une coordonnée UV et elle est affichée dans le vertex shader.
Le fragment shader obtient la coordonnée UV interpolée. L'interpolation signifie que la coordonnée UV du fragment se situe quelque part entre les coordonnées UV des sommets qui composent la face du triangle.
Vertex shader
#version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec2 p3d_MultiTexCoord0; in vec4 p3d_Vertex; out vec2 texCoord; void main() { texCoord = p3d_MultiTexCoord0; gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }
Ici, nous voyons que le vertex shader délivre les coordonnées de la texture au fragment shader. Notez qu'il s'agit d'un vecteur à deux dimensions: une valeur pour U et une pour V.
Shader de fragment
#version 140 uniform sampler2D p3d_Texture0; in vec2 texCoord; out vec2 fragColor; void main() { texColor = texture(p3d_Texture0, texCoord); fragColor = texColor; }
Ici, nous voyons que le shader de fragment recherche la couleur dans sa coordonnée UV et l'affiche comme la couleur du fragment.
Texture de remplissage d'écran
#version 140 uniform sampler2D screenSizedTexture; out vec2 fragColor; void main() { vec2 texSize = textureSize(texture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; texColor = texture(screenSizedTexture, texCoord); fragColor = texColor; }
Lors du rendu d'une texture, le maillage est un rectangle plat avec le même rapport hauteur / largeur que l'écran. Par conséquent, nous pouvons calculer les coordonnées UV, en ne connaissant que
A) la largeur et la hauteur de la texture avec la taille de l'écran superposée au rectangle en utilisant les coordonnées UV, et
B) les coordonnées x et y du fragment.
Pour lier x à U, vous devez diviser x par la largeur de la texture entrante. De même, pour lier y à V, vous devez diviser y par la hauteur de la texture entrante. Vous verrez que cette technique est utilisée dans l'exemple de code.
Eclairage
Pour déterminer l'éclairage, il est nécessaire de calculer et de combiner les aspects de l'éclairage ambiant, diffus, réfléchi et émis. L'exemple de code utilise l'éclairage Phong.
Vertex shader
Pour chaque source de lumière, à l'exception de la lumière ambiante, Panda3D nous fournit une structure pratique qui est disponible pour les vertex et les shaders de fragments. La chose la plus pratique est une carte d'ombres et une matrice pour visualiser les ombres afin de convertir les sommets en un espace d'ombres ou d'éclairage.
En commençant par le vertex shader, nous devons transformer et supprimer le sommet de l'espace de visualisation en ombre ou espace d'éclairage pour chaque source de lumière dans la scène. Cela sera utile à l'avenir pour que le fragment shader rende les ombres. Un espace d'ombre ou d'éclairage est un espace dans lequel chaque coordonnée est relative à la position de la source lumineuse (l'origine est la source lumineuse).
Shader de fragment
Le fragment shader fait la majeure partie du calcul d'éclairage.
Matière
Panda3D nous fournit du matériel (sous la forme d'une structure) pour le maillage ou le modèle que nous rendons actuellement.
Sources d'éclairage multiples
Avant de faire le tour des sources d'éclairage de la scène, nous allons créer un lecteur qui contiendra à la fois des couleurs diffuses et réfléchies.
Nous pouvons maintenant faire le tour des sources lumineuses dans un cycle, en calculant les couleurs diffuses et réfléchies pour chacune.
Vecteurs liés à l'éclairage
Voici quatre vecteurs de base nécessaires pour calculer les couleurs diffuses et réfléchies introduites par chaque source de lumière. Le vecteur de direction d'éclairage est une flèche bleue pointant vers la source de lumière. Le vecteur normal est une flèche verte pointant verticalement vers le haut. Le vecteur de réflexion est une flèche bleue qui reflète le vecteur de direction de la lumière. Le vecteur œil ou vue est la flèche orange pointant vers la caméra.
La direction de l'éclairage est le vecteur de la position du sommet à la position de la source lumineuse.
S'il s'agit d'un éclairage directionnel, Panda3D met
p3d_LightSource[i].position.w
zéro
p3d_LightSource[i].position.w
. L'éclairage directionnel n'a pas de position, seulement une direction. Par conséquent, s'il s'agit d'un éclairage directionnel, la direction de l'éclairage sera une direction négative ou opposée à la source, car pour l'éclairage directionnel, Panda3D définit
p3d_LightSource[i].position.xyz
sur
-direction
.
La normale au sommet doit être un vecteur unitaire. Les vecteurs unitaires ont une valeur égale à un.
Ensuite, nous avons besoin de trois vecteurs supplémentaires.
Nous avons besoin d'un produit scalaire avec la participation de la direction d'éclairage, il est donc préférable de le normaliser. Cela nous donne une distance ou une magnitude égale à l'unité (vecteur unitaire).
La direction de vue est opposée à la position du sommet / fragment, car la position du sommet / fragment est relative à la position de la caméra. N'oubliez pas que la position du sommet / fragment est dans l'espace de visualisation. Par conséquent, au lieu de passer de la caméra (œil) au sommet / fragment, nous passons du sommet / fragment à la caméra (œil).
Le vecteur de réflexion est une réflexion de la direction d'éclairage normale à la surface. Lorsque le "rayon" de lumière touche la surface, il se reflète sous le même angle auquel il est tombé. L'angle entre le vecteur de direction de l'illumination et la normale est appelé "angle d'incidence". L'angle entre le vecteur de réflexion et la normale est appelé "angle de réflexion".
Vous devez changer le signe du vecteur de lumière réfléchie, car il doit pointer dans la même direction que le vecteur de l'œil. N'oubliez pas que la direction de l'œil va du haut / fragment à la position de la caméra. Nous utiliserons le vecteur de réflexion pour calculer la luminosité de la lumière réfléchie.
Éclairage diffus
La luminosité de l'éclairage diffus est le produit scalaire de la normale à la surface et de la direction d'éclairage d'un vecteur unique. Le produit scalaire peut aller de moins un à un. Si les deux vecteurs pointent dans la même direction, alors la luminosité est l'unité. Dans tous les autres cas, ce sera inférieur à l'unité.
Si le vecteur d'éclairage se rapproche de la même direction que la normale, alors la luminosité de l'éclairage diffus tend à l'unité.
Si la luminosité de l'éclairage diffus est inférieure ou égale à zéro, vous devez passer à la source de lumière suivante.
Nous pouvons maintenant calculer la couleur diffuse introduite par cette source.
Si la luminosité de l'éclairage diffus est égale à l'unité, alors la couleur diffuse sera un mélange de la couleur de la texture diffuse et de la couleur de l'éclairage. À toute autre luminosité, la couleur diffuse sera plus sombre.Notez que je limite la couleur diffuse afin qu'elle ne soit pas plus lumineuse que la couleur de la texture diffuse. Cela évitera une surexposition de la scène.Lumière réfléchie
Après un éclairage diffus, la réflexion est calculée.
La luminosité de la lumière réfléchie est le produit scalaire entre le vecteur œil et le vecteur réflexion. Comme dans le cas de la luminosité d'un éclairage diffus, si deux vecteurs pointent dans la même direction, alors la luminosité de l'éclairage réfléchi est égale à l'unité. Toute autre luminosité réduira la quantité de couleur réfléchie introduite par cette source de lumière.Le lustre du matériau détermine la quantité de diffusion de la lumière réfléchie. Il est généralement défini dans un programme de simulation, par exemple dans Blender. Dans Blender, on parle de dureté spéculaire.Projecteurs
Ce code ne permet pas à l'éclairage d'affecter les fragments à l'extérieur du cône ou de la pyramide du projecteur. Heureusement, Panda3D peut définir spotDirection
et spotCosCutoff
travailler avec des lumières directionnelles et ponctuelles. Les projecteurs ont à la fois une position et une direction. Cependant, l'éclairage directionnel n'a qu'une direction et les sources ponctuelles n'ont qu'une position. Cependant, ce code fonctionne pour les trois types d'éclairage sans qu'il soit nécessaire de confondre les instructions if. spotCosCutoff = cosine(0.5 * spotlightLensFovAngle);
Si dans le cas d'un éclairage de projection, le produit scalaire du vecteur "fragment-source d'éclairage" et du vecteur de direction du projecteur est inférieur au cosinus de la moitié de l'angle du champ de vision duprojecteur, le shader ne prend pas en compte l'influence de cette source.Notez que vous devez changer le signe unitLightDirection
. unitLightDirection
va du fragment au projecteur, et nous devons passer du projecteur au fragment, car il spotDirection
va directement au centre de la pyramide du projecteur à une certaine distance de la position du projecteur.Dans le cas d'un éclairage directionnel et ponctuel, Panda3D définit la spotCosCutoff
valeur sur -1. Rappelons que le produit scalaire varie dans la plage de -1 à 1. Par conséquent, peu importe ce qu'il sera unitLightDirectionDelta
, car il est toujours supérieur ou égal à -1.
Comme le code unitLightDirectionDelta
, ce code fonctionne également pour les trois types de sources lumineuses. Dans le cas des projecteurs, il rendra les fragments plus brillants à l'approche du centre de la pyramide des projecteurs. Pour les sources de lumière directionnelles et ponctuelles, la valeur spotExponent
est nulle. Rappelons que toute valeur à la puissance de zéro est égale à l'unité, de sorte que la couleur diffuse est égale à elle-même, multipliée par un, c'est-à-dire qu'elle ne change pas.Ombres
Panda3D simplifie l'utilisation des ombres car il crée une carte des ombres et une matrice de transformation des ombres pour chaque source de lumière dans la scène. Pour créer vous-même une matrice de transformation, vous devez collecter une matrice qui convertit les coordonnées de l'espace de visualisation en espace d'éclairage (les coordonnées sont relatives à la position de la source lumineuse). Pour créer vous-même une carte d'ombre, vous devez rendre la scène du point de vue de la source de lumière dans la texture du tampon d'image. La texture du tampon de trame doit contenir la distance entre la source de lumière et les fragments. C'est ce qu'on appelle une «carte de profondeur». Enfin, vous devez transférer manuellement vers le shader votre carte de profondeur maison en tant que uniform sampler2DShadow
, et la matrice de transformation d'ombre en tant que uniform mat4
. Nous allons donc recréer ce que Panda3D fait automatiquement pour nous.L'extrait de code illustré est utilisé textureProj
, ce qui est différent de la fonction indiquée ci-dessus texture
. textureProj
divise d'abord vertexInShadowSpaces[i].xyz
par vertexInShadowSpaces[i].w
. Elle l'utilise ensuite vertexInShadowSpaces[i].xy
pour trouver la profondeur stockée dans la carte d'ombre. Ensuite, elle utilise vertexInShadowSpaces[i].z
pour comparer la profondeur du haut avec la profondeur de la carte d'ombre dans vertexInShadowSpaces[i].xy
. Si la comparaison réussit, elle en textureProj
renvoie un. Sinon, il renvoie zéro. Zéro signifie que ce sommet / fragment est dans l'ombre et un signifie que le sommet / fragment n'est pas dans l'ombre.Notez textureProj
qu'il peut également renvoyer une valeur de zéro à un, selon la configuration de la carte fantôme. Dans cet exempletextureProj
Effectue plusieurs tests de profondeur basés sur des profondeurs adjacentes et renvoie une moyenne pondérée. Cette moyenne pondérée peut donner de la douceur aux ombres.Atténuation
La distance à la source lumineuse est simplement la grandeur ou la longueur du vecteur de direction d'éclairage. Notez que nous n'utilisons pas la direction d'éclairement normalisée, car une telle distance serait égale à l'unité.La distance à la source lumineuse est nécessaire pour calculer l'atténuation. L'atténuation signifie que l'effet de la lumière loin de la source diminue.Paramètres constantAttenuation
, linearAttenuation
et quadraticAttenuation
vous pouvez définir toutes les valeurs. Cela vaut la peine de commencer par constantAttenuation = 1
, linearAttenuation = 0
et quadraticAttenuation = 1
. Avec de tels paramètres, la position de la source lumineuse est égale à l'unité et tend à zéro en s'éloignant d'elle.Éclairage couleur final
Pour calculer la couleur finale de l'éclairage, vous devez ajouter la couleur diffuse et réfléchie. Il est nécessaire d'ajouter cela au lecteur dans un cycle de contournement des sources lumineuses dans la scène.Ambiant
Le composant d'éclairage ambiant dans le modèle d'éclairage est basé sur la couleur ambiante du matériau, la couleur de l'éclairage ambiant et la couleur de la texture diffuse.Il ne doit jamais y avoir plus d'une source de lumière ambiante, ce calcul ne doit donc être effectué qu'une seule fois, contrairement aux calculs des couleurs diffuses et réfléchies accumulées pour chaque source de lumière.Veuillez noter que la couleur de la lumière ambiante est utile lors de l'exécution de SSAO.Tout mettre ensemble
La couleur finale est la somme de la couleur ambiante, de la couleur diffuse, de la couleur réfléchie et de la couleur émise.Code source
Cartes normales
L'utilisation de cartes normales vous permet d'ajouter de nouvelles pièces à la surface sans géométrie supplémentaire. En règle générale, lorsque vous travaillez dans un programme de modélisation 3D, des versions haute et basse poly du maillage sont créées. Ensuite, les normales des sommets du maillage poly élevé sont prises et intégrées dans la texture. Cette texture est une carte normale. Ensuite, à l'intérieur du shader de fragment, nous remplaçons les normales des sommets du maillage poly faible par les normales du maillage poly élevé cuites dans la carte normale. Pour cette raison, lors de l'éclairage d'un maillage, il semblera qu'il a plus de polygones qu'il ne l'est réellement. Cela vous permet de maintenir un FPS élevé, tout en transmettant la plupart des détails de la version haute poly.Ici, nous voyons la transition d'un modèle poly élevé à un modèle poly faible, puis à un modèle poly faible avec une carte normale superposée.Cependant, n'oubliez pas que superposer une carte normale n'est qu'une illusion. Sous un certain angle, la surface redevient plate.Vertex shader
En commençant par le vertex shader, nous devons sortir le vecteur normal, le vecteur binormal et le vecteur tangent vers le fragment shader. Ces vecteurs sont utilisés dans le fragment shader pour transformer la normale de la carte normale de l'espace tangent à l'espace de visualisation.p3d_NormalMatrix
convertit les vecteurs normaux du vecteur sommet, binormal et tangent en espace de visualisation. N'oubliez pas que dans l'espace de visualisation, toutes les coordonnées sont relatives à la position de la caméra.[p3d_NormalMatrix] sont les principaux éléments de transposition inverse 3x3 de ModelViewMatrix. Cette structure est utilisée pour convertir le vecteur normal en coordonnées de l'espace d'observation.
Source
Nous devons également sortir les coordonnées UV de la carte normale dans le fragment shader.Shader de fragment
Rappelons que le sommet normal a été utilisé pour calculer l'éclairage. Cependant, pour calculer l'éclairage, la carte normale nous donne d'autres normales. Dans le fragment shader, nous devons remplacer les normales des sommets par les normales situées dans la carte normale.
En utilisant les coordonnées de la carte normale transférées par le vertex shader, nous extrayons la normale correspondante de la carte.
Ci-dessus, j'ai montré comment les normales sont converties en couleurs pour créer des cartes normales. Maintenant, nous devons inverser ce processus afin que les normales d'origine soient intégrées à la carte. [ r, g, b] = [ r * 2 - 1, g * 2 - 1, b * 2 - 1] = [ x, y, z]
Voici à quoi ressemble le processus de décompression des normales de la carte normale.
Les normales obtenues à partir de la carte normale sont généralement dans l'espace tangent. Cependant, ils peuvent être dans un autre espace. Par exemple, Blender vous permet de faire cuire des normales dans l'espace tangent, l'espace objet, l'espace monde et l'espace caméra.Pour transférer la normale de la carte normale de l'espace tangent à l'espace de visualisation, créez une matrice 3x3 basée sur le vecteur tangent, les vecteurs binormaux et la normale du sommet. Multipliez la normale par cette matrice et normalisez-la. C'est là que nous nous sommes retrouvés avec les normales. Tous les autres calculs d'éclairage sont toujours effectués.Code source