Partager via


Cet article a fait l'objet d'une traduction automatique.

Facteur DirectX

Nuanciers Pixel et la réflexion de lumière

Charles Petzold

Télécharger l'exemple de code

Charles PetzoldSi vous pouviez voir photons... Eh bien, vous pouvez voir des photons, ou au moins certains d'entre eux. Photons sont les particules qui composent un rayonnement électromagnétique, et le œil est sensible aux photons de longueurs d'onde dans la gamme de lumière visible.

Mais vous ne pouvez pas voir les photons comme ils volent tout dans tous les sens. Ce serait sûrement intéressant. Parfois les photons aller droit à travers des objets ; ils sont parfois absorbées ; parfois ils sont consignées ; mais c'est souvent une combinaison de tous ces effets. Certains des photons qui rebondissent sur les objets éventuellement atteint vos yeux, en donnant à chaque objet sa couleur et sa texture.

Pour les graphiques 3D de très haute qualité, une technique appelée de traçage peut en fait tracer une simulation des chemins d'accès de ces innombrables photons pour imiter les effets de réflexion et d'ombres. Mais beaucoup plus simples techniques sont disponibles pour des besoins plus conventionnelles. C'est souvent le cas lors de l'utilisation de Direct3D — ou, dans mon cas, écrivant des effets personnalisés en Direct2D qui utilisent la 3D.

Réutiliser l'effet

Comme vous l'avez vu dans les précédents Articles de cette colonne, un effet de Direct2D est essentiellement un wrapper pour le code qui s'exécute sur le GPU. Ce code est connu comme un shader, et les plus importants sont le vertex shader et le nuanceur de pixels. Le code dans chacun de ces shaders est appelé à la vidéo de rafraîchissement de l'écran. Le nuanceur de sommets est appelé pour chacun des trois sommets dans chacun des triangles qui composent les objets graphiques affichés par l'effet, alors que le nuanceur de pixels est appelé pour chaque pixel dans ces triangles.

Évidemment, le nuanceur de pixels est appelé beaucoup plus souvent que le nuanceur de sommets, il est donc logique de garder autant de traitement possible dans le nuanceur de sommets, et non le nuanceur de pixels. Ce n'est pas toujours possible, cependant, et lorsque vous utilisez ces shaders pour simuler la réflexion de la lumière, c'est généralement l'équilibre et l'interaction entre ces deux shaders qui régit la sophistication et la souplesse de l'ombrage.

Dans le numéro d'août de ce magazine, j'ai présenté un effet de Direct2D appelé RotatingTriangleEffect qui construit un mémoire tampon de vertex consistant en points et les couleurs et autorisés à appliquer des transformations standards de modèle et de la caméra vers les sommets. J'ai utilisé cet effet pour faire tourner les trois triangles. Ce n'est pas une grande quantité de données. Trois triangles impliquent seulement un total de neuf sommets, et je l'ai mentionné à l'époque qui a le même effet pourrait être utilisé pour un beaucoup plus grand tampon de vertex.

Nous allons essayer : Le programme téléchargeable (msdn.microsoft.com/magazine/msdnmag1014) pour cette colonne est appelée ShadedCircularText, il utilise RotatingTriangleEffect sans un seul changement.

Le programme ShadedCircularText renvoie au problème, j'ai commencé à explorer plus tôt cette année de l'affichage du texte 2D le personnage en trois dimensions. Le constructeur de la classe ShadedCircularTextRenderer se charge dans un fichier de police, crée un type de police d'elle et puis appelle GetGlyphRunOutline pour obtenir une géométrie de la voie des contours caractère. La géométrie de ce chemin est pavée puis en utilisant une classe que j'ai créé, appelée InterrogableTessellationSink qui s'accumule les triangles réelles.

Après s'être inscrit RotatingTriangleEffect, ShadedCircularText­Renderer crée un objet ID2D1Effect fondé sur cet effet. Il convertit ensuite les triangles du texte le personnage en sommets sur la surface d'une sphère, essentiellement le texte autour de l'Équateur et courbant vers les pôles. La couleur de chaque sommet est issue d'une teinte dérivée de la coordonnée X de la géométrie d'origine du texte. Cela crée un effet arc-en-ciel, et Figure 1 montre le résultat.

texte 3D arc-en-ciel de ShadedCircularText
Figure 1 texte 3D arc-en-ciel de ShadedCircularText

Comme vous pouvez le voir, un petit menu orne la partie supérieure. Le programme intègre en fait trois effets supplémentaires de Direct2D qui implémentent des modèles plus traditionnels d'ombrage. Tous d'entre eux utilisent les mêmes points, les mêmes transformations et les mêmes animations, ainsi vous pouvez commuter entre eux pour voir la différence. Les différences concernent uniquement l'ombrage de la couleur des triangles.

Le coin bas-droit dispose d'un écran de performance en images par seconde, mais vous découvrirez que rien dans ce programme cause que de laisser tomber beaucoup au-dessous de 60 sauf si quelque chose se passe.

Ombrage Gouraud

Comme les photons volent tout autour de nous, ils rebondissent souvent sur des molécules d'azote et d'oxygène dans l'air. Même sur une journée nuageuse avec aucune lumière directe du soleil, il y a toujours une quantité considérable de la lumière ambiante. Lumière ambiante tend à éclairer des objets de façon très uniforme.

Peut-être vous avez un objet qui est d'un bleu verdâtre, avec une valeur RGB (0, 0.5, 1.0). Si la lumière ambiante est un quart de pleine intensité de blanche, vous pouvez assigner une valeur RVB à la lumière de (0,25, 0,25 0,25). La couleur apparente de cet objet est le produit des composantes rouges, verts et bleus de ces numéros, ou (0, 0,125, 0,25). C'est encore un bleu verdâtre, mais beaucoup plus sombre.

Mais des scènes 3D simples ne vivent pas de la lumière ambiante seul. Dans la vraie vie, les objets normalement ont beaucoup de variation de couleur sur leurs surfaces, donc, même que si elles sont uniformément éclairés, les objets ont toujours textures visibles. Mais dans une scène 3D simple, un objet bleu-verdâtre, éclairé seulement par la lumière ambiante ne ressemblera à une dalle indifférenciée de couleur uniforme.

Pour cette raison, des scènes 3D simples bénéficient énormément de lumière directionnelle. Il est plus simple de supposer que cette lumière provient d'une distance lointaine (comme le soleil), donc la direction de la lumière est juste un vecteur unique qui s'applique à toute la scène. Si il n'y a qu'une seule source lumineuse, il est supposé généralement proviennent de plus de l'épaule gauche du spectateur, alors peut-être que le vecteur est (1, -1, -1) dans un système de coordonnées droit. Cette lumière directionnelle a aussi une couleur, peut-être (0,75, 0,75, 0,75), donc avec la lumière ambiante (0,25, 0,25 0,25), éclairement maximal est parfois atteint.

La quantité de lumière directionnelle que reflète une surface dépend de l'angle de que la lumière se fait avec la surface. (Il s'agit d'un concept étudié dans mon mai 2014 colonne DirectX facteur.) La réflexion maximale se produit lorsque la lumière directionnelle est perpendiculaire à la surface, et la lumière réfléchie descend à zéro si la lumière est tangent à la surface ou à venir de quelque part derrière la surface.

Le cosinus de Lambert — nommé d'après le mathématicien allemand et physicien Johann Heinrich Lambert (1728-1777), dit que la fraction de la lumière réfléchie par une surface est le négatif cosinus de l'angle entre la direction de la lumière et la direction d'un vecteur perpendiculaire à la surface, que l'on appelle une surface normale. Si ces deux vecteurs sont normalisés — autrement dit, s'ils ont une ampleur de 1 — ce cosinus de l'angle entre deux vecteurs est le même que le produit scalaire des vecteurs.

Par exemple, si la lumière frappe une surface particulière à un angle de 45 degrés, le cosinus est environ 0,7, donc qui se multiplient par le direc­tional lumière couleur de (0,75, 0,75 0,75) et la couleur de l'objet (0, 0.5, 1.0), de déduire la couleur de l'objet lumière directionnelle de (0, 0,26, 0,53). Ajouter que, pour la couleur de la lumière ambiante.

Cependant, gardez à l'esprit qui des courbes des objets dans des scènes 3D ne sont pas réellement courbé. Tout dans la scène se compose de triangles plats. Si l'éclairage de chaque triangle repose sur une surface perpendiculaire normale au triangle lui-même, chaque triangle aura une couleur uniforme différente. C'est très bien pour les solides de Platon comme celles affichées dans ma colonne de mai 2014, mais pas très bon pour les surfaces courbes. Vous recherchez des surfaces courbes, les couleurs des triangles pour se fondre avec l'autre.

Autrement dit, qu'il est nécessaire que chaque triangle avoir une couleur graduée plutôt que d'une couleur uniforme. La couleur de la lumière directionnelle ne peut reposer sur une seule surface normale pour le triangle. Au lieu de cela, chaque sommet du triangle doit avoir une couleur différente, basée sur la surface normale à ce sommet. Ces couleurs de vertex peuvent ensuite être interpolées sur tous les pixels du triangle. Triangles adjacents se fondre puis dans l'autre pour ressembler à une surface courbe.

Ce type d'ombrage a été inventé par informaticien Français Henri Gouraud (b. 1944) dans un document publié en 1971 et est donc connue comme ombrage Gouraud.

Ombrage Gouraud est la deuxième option, mis en place dans le programme ShadedCircularText. L'effet lui-même est appelé GouraudShadingEffect, et il requiert un mémoire tampon de vertex avec un peu plus de données :

struct PositionNormalColorVertex
{
  DirectX::XMFLOAT3 position;
  DirectX::XMFLOAT3 normal;
  DirectX::XMFLOAT3 color;
  DirectX::XMFLOAT3 backColor;
};

Il est intéressant, parce que le texte est effectivement enveloppée autour d'une sphère centrée sur le point (0, 0, 0), la surface normale à chaque sommet est identique à la position, mais normalisé pour avoir une ampleur de 1. L'effet permet de couleurs uniques pour chaque sommet, mais dans ce programme, chaque vertex obtient la même couleur, qui est (0, 0,5, 1) et le même backColor de (0.5, 0.5, 0.5), qui est la couleur à utiliser si le spectateur est confronté à l'arrière d'une surface.

La GouraudShadingEffect exige également plusieurs propriétés de l'effet. Il doit être possible de définir la couleur de la lumière ambiante, couleur de la lumière directionnelle et la direction du vecteur de la lumière directionnelle. Le GouraudShadingEffect transfère toutes ces valeurs à une mémoire tampon plus grande constante pour le vertex shader. Le nuanceur de sommets s'est montré en Figure 2.

Figure 2 le nuanceur de sommets pour l'ombrage Gouraud

// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 normal : NORMAL;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Constant buffer provided by effect.
cbuffer VertexShaderConstantBuffer : register(b1)
{
  float4x4 modelMatrix;
  float4x4 viewMatrix;
  float4x4 projectionMatrix;
  float4 ambientLight;
  float4 directionalLight;
  float4 lightDirection;
};
// Called for each vertex.
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Get the input vertex, and include a W coordinate
  float4 pos = float4(input.position.xyz, 1.0f);
  // Pass through the resultant scene space output value
  //  (not necessary -- can be removed from both shaders)
  output.sceneSpaceOutput = pos;
  // Apply transforms to that vertex
  pos = mul(pos, modelMatrix);
  pos = mul(pos, viewMatrix);
  pos = mul(pos, projectionMatrix);
  // The result is clip space output
  output.clipSpaceOutput = pos;
  // Apply model transform to normal
  float4 normal = float4(input.normal, 0);
  normal = mul(normal, modelMatrix);
  // Find angle between light and normal
  float3 lightDir = normalize(lightDirection.xyz);
  float cosine = -dot(normal.xyz, lightDir);
  cosine = max(cosine, 0);
  // Apply view transform to normal
  normal = mul(normal, viewMatrix);
  // Check if normal pointing at viewer
  if (normal.z > 0)
  {
    output.color = (ambientLight.xyz + cosine *
                    directionalLight.xyz) * input.color;
  }
  else
  {
    output.color = input.backColor;
  }
  return output;
}

Le nuanceur de pixels est le même que pour le RotatingTriangleEffect et est montré dans Figure 3. L'interpolation des couleurs sur l'ensemble triangle vertex se produit dans les coulisses entre le vertex shader et le nuanceur de pixels, donc le nuanceur de pixels ne fait que transmettre la couleur à afficher.

Figure 3 le nuanceur de pixels pour l'ombrage Gouraud

// Per-pixel data input to the pixel shader
struct PixelShaderInput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Called for each pixel
float4 main(PixelShaderInput input) : SV_TARGET
{
  // Simply return color with opacity of 1
  return float4(input.color, 1);
}

Le résultat s'affiche dans Figure 4, cette fois sur Windows Phone 8.1 plutôt que Windows 8.1. La solution de ShadedCircularText a été créée dans le Visual Studio avec le nouveau modèle d'application universelle et peut être compilée sur une autre plate-forme. Tout le code est partagé entre les deux plates-formes à l'exception des classes App et DirectXPage. La différence dans les schémas des deux programmes suggère pourquoi avoir page différentes définitions est souvent une bonne idée, même si la fonctionnalité du programme est fondamentalement le même.

l'affichage de la Gouraud Shading modèle
Figure 4 l'affichage de la Gouraud Shading modèle

Comme vous pouvez le voir, la figure est plus léger dans sa zone en haut à gauche, clairement montrant l'effet de la lumière directionnelle et d'aider à l'illusion d'un aspect arrondi à la surface.

Les améliorations de Phong

Ombrage Gouraud est une technique séculaire, mais il a un défaut fondamental : Ombrage Gouraud, la quantité de lumière directionnelle dans le centre d'un triangle est une valeur interpolée de la lumière réfléchie sur les sommets. La lumière réfléchie sur les sommets est basée sur le cosinus de l'angle entre la direction de la lumière et les surfaces normales de ces sommets.

Mais la lumière réfléchie dans le centre du triangle a vraiment devrait se fonder sur la surface normale à ce moment-là. En d'autres termes, les couleurs ne devraient pas être interpolées dans le triangle ; au lieu de cela, les normales de surface devraient être interpolées sur la surface du triangle, et la lumière réfléchie est calculé pour chaque pixel issu de cette normale.

Entrez origine vietnamienne informaticien Pui Tuong Phong (1942-1975), qui est mort de leucémie à l'âge de 32 ans. Dans sa thèse de doctorat de 1973, Phong décrit un algorithme d'ombrage quelque peu différente. Plutôt que d'interpoler les couleurs de vertex dans le triangle, les normales de vertex sont interpolées dans le triangle, et puis la lumière réfléchie est calculée à partir de celles.

Dans un sens pratique, Phong shading nécessite le calcul de la lumière réfléchie à quittent le nuanceur vertex pour le nuanceur de pixels, ainsi que la section du tampon constant consacré à cette tâche. Cela augmente la quantité de traitement immensément par pixel, mais, heureusement, cela se fait sur le GPU où vous espérez qu'il ne semble pas faire beaucoup de différence.

Le nuanceur de sommets pour le modèle d'ombrage Phong est montré dans Figure 5. Certaines des données d'entrée — telles que la couleur et la couleur d'arrière-plan — sont simplement passés sur le nuanceur de pixels. Mais il est toujours utile d'appliquer toutes les transformations ici. La transformation du monde et les deux transformations de l'appareil photo doivent être appliquées aux positions, tandis que deux normales sont également calculés — un avec seulement la transformation de modèle pour la lumière réfléchie et l'autre avec la transformation d'affichage pour déterminer si une surface est confronté vers ou loin le spectateur.

Figure 5 le nuanceur Vertex pour le Phong Shading modèle

// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 normal : NORMAL;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 normalModel : NORMAL0;
  float3 normalView : NORMAL1;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Constant buffer provided by effect
cbuffer VertexShaderConstantBuffer : register(b1)
{
  float4x4 modelMatrix;
  float4x4 viewMatrix;
  float4x4 projectionMatrix;
};
// Called for each vertex
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Get the input vertex, and include a W coordinate
  float4 pos = float4(input.position.xyz, 1.0f);
  // Pass through the resultant scene space output value
  // (not necessary — can be removed from both shaders)
  output.sceneSpaceOutput = pos;
  // Apply transforms to that vertex
  pos = mul(pos, modelMatrix);
  pos = mul(pos, viewMatrix);
  pos = mul(pos, projectionMatrix);
  // The result is clip space output
  output.clipSpaceOutput = pos;
  // Apply model transform to normal
  float4 normal = float4(input.normal, 0);
  normal = mul(normal, modelMatrix);
  output.normalModel = normal.xyz;
  // Apply view transform to normal
  normal = mul(normal, viewMatrix);
  output.normalView = normal.xyz;
  // Transfer colors
  output.color = input.color;
  output.backColor = input.backColor;
  return output;
}

Comme la sortie du nuanceur vertex devient entrée vers le nuanceur de pixels, ces normales sont interpolés sur la surface du triangle. Le nuanceur de pixels peut alors finir le travail en calculant la lumière réfléchie, comme le montre Figure 6.

Figure 6 le nuanceur de pixels pour le Phong Shading modèle

// Per-pixel data input to the pixel shader
struct PixelShaderInput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 normalModel : NORMAL0;
  float3 normalView : NORMAL1;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Constant buffer provided by effect
cbuffer PixelShaderConstantBuffer : register(b0)
{
  float4 ambientLight;
  float4 directionalLight;
  float4 lightDirection;
};
// Called for each pixel
float4 main(PixelShaderInput input) : SV_TARGET
{
  // Find angle between light and normal
  float3 lightDir = normalize(lightDirection.xyz);
  float cosine = -dot(input.normalModel, lightDir);
  cosine = max(cosine, 0);
  float3 color;
  // Check if normal pointing at viewer
  if (input.normalView.z > 0)
  {
    color = (ambientLight.xyz + cosine *
      directionalLight.xyz) * input.color;
  }
  else
  {
    color = input.backColor;
  }
  // Return color with opacity of 1
  return float4(color, 1);
}

Cependant, je ne vais pas vous montrer une capture d'écran du résultat. Il est assez bien visuellement identique à l'ombrage Gouraud. Ombrage Gouraud est vraiment une bonne approximation.

Spéculaires

L'importance réelle de Phong shading, c'est qu'il rend possible autres fonctionnalités qui reposent sur une normale à la surface plus précise.

Jusqu'ici, dans cet article, vous avez vu ombrage qui convient aux surfaces diffuses. Ce sont des surfaces qui sont plutôt rugueux et mat et qui ont tendance à la lumière de dispersion réfléchie par les surfaces.

Une surface qui est un peu brillante réfléchit la lumière un peu différemment. Si une surface est inclinée tellement lumière directionnelle pourrait rebondir et aller directement à le œil du spectateur. Cela est généralement perçu comme une lumière blanche brillante, et il est connu comme une surbrillance spéculaire. Vous pouvez voir l'effet plutôt exagérée dans Figure 7. Si le chiffre a des courbes plus nettes, la lumière blanche serait plus localisée.

l'affichage de la surbrillance spéculaire
Figure 7 l'affichage de la surbrillance spéculaire

Obtenir cet effet semble dans un premier temps comme s'il pourrait être mathématiquement complexe, mais c'est seulement quelques lignes de code dans le nuanceur de pixels. Cette technique a été développée par maven de graphiques de la NASA Jim Blinn (b. 1949).

Nous devons tout d'abord un vecteur indiquant la direction dans laquelle le spectateur de la scène 3D est à la recherche. C'est très facile parce que la transformation d'affichage caméra a réglé toutes les coordonnées et le spectateur est à la recherche vers le bas de l'axe Z :

float3 viewVector = float3(0, 0, -1);

Ensuite, calculer un vecteur qui est à mi-chemin entre ce vecteur d'affichage et de la direction de la lumière :

float3 halfway = -normalize(viewVector + lightDirection.xyz);

Notez le signe négatif. Cela rend le vecteur pointent dans la direction opposée, à mi-chemin entre la source de la lumière et le spectateur.

Si un triangle particulier contient une surface normale qui correspond exactement à ce vecteur à mi-chemin, cela signifie que lumière est rebondit sur la surface directement dans le œil du spectateur. Cela se traduit par la mise en surbrillance spéculaire maximale.

Mettant en évidence moindre résulte d'angles non nulle entre le vecteur à mi-chemin et la normale à la surface. Il s'agit d'une autre application pour le cosinus entre deux vecteurs, qui est le même que le produit scalaire si les deux vecteurs sont normalisées :

float dotProduct = max(0.0f, dot(input.normalView, halfway));

Cette valeur de dotProduct varie de 1 pour accentuer spéculaire maximale lorsque l'angle entre deux vecteurs est 0, et 0 pour aucune mise en surbrillance spéculaire, qui se produit lorsque les deux vecteurs sont perpendiculaires.

Toutefois, il ne devrait pas être visible pour tous les angles entre 0 et 90 degrés de mise en surbrillance spéculaire. Elle doit être localisée. Il doit être présent uniquement pour les très petits angles entre ces deux vecteurs. Vous avez besoin d'une fonction qui n'affectera pas un produit scalaire de 1, mais provoquera des valeurs inférieures à 1 pour devenir beaucoup plus faible. C'est la fonction pow :

float specularLuminance = pow(dotProduct, 20);

Cette fonction pow prend le produit scalaire de la 20ème puissance. Si le produit scalaire est 1, la fonction pow retourne 1. Si le produit scalaire est 0,7 (qui résulte d'un angle de 45 degrés entre les deux vecteurs), alors la fonction pow retourne 0,0008, qui est effectivement 0 en ce qui concerne l'éclairage goes. Utiliser des valeurs plus élevées exposants pour rendre l'effet encore plus localisée.

Maintenant, tout ce qui est nécessaire est de multiplier ce facteur par la couleur de la lumière directionnelle et ajoutez-le à la couleur déjà calculée de la lumière ambiante et directionnelle lumière :

color += specularLuminance * directionalLight.xyz;

Qui crée une tache de lumière blanche comme l'animation tourne la figure.

Adieu

Et avec cela, la colonne de DirectX facteur tire à sa fin. Cette plongée dans DirectX a été un des emplois plus difficiles de ma carrière, mais donc aussi la plus enrichissante, et j'espère avoir l'occasion un jour revenir à cette technologie puissante.


Charles Petzold est un contributeur de longue date à MSDN Magazine et l'auteur de « Programmation Windows, 6e édition » (Microsoft Press, 2013), un livre sur l'écriture d'applications pour Windows 8. Son site Web est charlespetzold.com.

Merci à l'expert technique Microsoft suivant d'avoir relu cet article : Doug Erickson

Cette question marque de Charles Petzold dernière comme une chronique régulière dans le MSDN Magazine. Charles quitte pour rejoindre l'équipe à Novell, un fournisseur leader d'outils multi-plateforme, s'appuyant sur Microsoft .NET Framework. Charles a été associée de MSDN Magazine pendant des décennies et est l'auteur de nombreuses chroniques régulières, y compris les fondations, les frontières de l'interface utilisateur et les facteur de DirectX. Nous lui souhaitons bien sur ses nouveaux efforts.