Utiliser la puissance du Cloud Azure pour faire votre propre raytracer - Eternal Coding - HTML5 / Windows / Kinect / 3D development - Site Home - MSDN Blogs

Utiliser la puissance du Cloud Azure pour faire votre propre raytracer


 

Utiliser la puissance du Cloud Azure pour faire votre propre raytracer

  • Comments 2

6d594911-ad38-43af-ae46-2df304f020bb (1)Avec la multiplication de la puissance disponible dans le nuage, je me suis dit que nous pourrions mettre à profit tous ces CPUs pour écrire un générateur d’images de synthèse basé sur un lanceur de rayons (un raytracer).

Je ne suis certainement pas le premier à avoir eu l’idée puisque par exemple Pixar ou GreenButton utilise déjà Azure pour effectuer des rendus d’images.

Ce que je vous propose ici c’est de développer votre propre solution d’imagerie sur Azure pour peut être demain calculer le futur blockbuster!

Nous allons donc découper notre approche autour des axes suivants:

  1. Définition des pré-requis
  2. Architecture de la solution
  3. Déploiement vers Azure
  4. Définition d’une scène
  5. Le serveur web et les workers roles
  6. Explications sur le code du raytracer
  7. Le client JavaScript
  8. Conclusion
  9. Pour aller plus loin

La solution complète est téléchargeable ici et vous pouvez voir le résultat sur mon compte Azure : http://azureraytracer.cloudapp.net/

image

Vous pouvez utiliser une scène par défaut ou fournir votre propre description (nous verrons plus bas comment décrire une scène).

Les images produites sont limitées à une taille de 512x512 (vous pourrez bien sur changer ces limites dans le code).

Prérequis

Pour utiliser la solution, vous devez disposer :

De plus, il vous faudra un compte Azure. Vous pourrez en obtenir un gratuitement pour 90 jours pour tester juste ici : http://www.microsoft.com/france/windows-azure/Offres/Essai-90-jours.aspx

Architecture

Notre architecture peut être décrite par le schéma suivant :

image

Notre client va donc venir se connecter à un frontal web composé de un ou plusieurs Web Roles (dans mon cas il y en aura deux). Ces frontaux vont fournir le contenu web ainsi qu’un web service pour connaitre l’état d’une requête. Lorsqu’un ordre de rendu est envoyé, le frontal web va le transmettre à une ferme (composée de un ou plusieurs Worker Roles) de rendu via une file Azure transactionnelle (Azure Queue).

L’intérêt de passer par une file est que selon leur disponibilité un worker Role prendra la requête et effectuera le travail. Le site web ne sait pas qui traitera la requête ni quand. Il sait juste que cela sera fait. De plus comme les files sont transactionnelles, si un worker role plante lors d’un traitement et qu’il ne valide pas son travail, le message d’origine sera remit dans la file et pourra être traité par un autre worker role.

Au niveau de notre exemple, j’ai utilisé un sémaphore pour limiter le nombre de requêtes en parallèle traitées par un worker role à 4. En effet, j’ai préféré favoriser le fait que le lanceur de rayons va utiliser tous les CPUS de chaque worker role pour que chaque rendu soit bien parallélisé.

Déploiement de la solution sur Azure

Lorsque vous ouvrirez la solution, vous pourrez directement la lancer depuis Visual Studio dans l’émulateur intégré avec les outils de développement Azure. Cela permet de pouvoir débugger et de bien régler son code avant le déploiement en production.

Une fois que nous sommes prêts, nous allons pouvoir déployer notre raytracer sur notre compte Azure en suivant la procédure suivante :

  • Ouvrir la solution “AzureRaytracer.sln” dans Visual Studio
  • Configurer votre compte Azure sur le projet. Pour ce faire, il vous suffit de faire un clic droit sur le projet “AzureRaytracer” et choisir le menu “Publish” (ou Publier). A partir de là, vous allez obtenir l’écran suivant:

image

  • Sur cet écran, veuillez choisir l’option “Sign in to download credentials” qui va vous permettre de télécharger un fichier de configuration automatique sur votre compte Azure :

image

  • Une fois le fichier télécharger, nous allons l’importer dans notre projet :

image

  • Suite à l’importation des informations, Visual Studio va nous demander de donner un nom au service que nous allons créer sur Azure :

image

  • L’écran suivant résume ce qui va être créé (au passage j’ai coché les cases du bas de la fenêtre pour permettre le déploiement web et l’accès distant (on ne sait jamais Sourire)) :

image

  • Vous pouvez faire “Publish” pour créer le service. Toutefois, nous allons changer certains réglages de notre solution pour l’adapter à un déploiement en mode production. Tout d’abord nous allons nous connecter sur le portail Azure : http://windows.azure.com et venir au niveau des comptes de stockages pour récupérer les informations qui nous intéressent :

image

  • Sur la partie de droite nous voyons un tableau qui va nous donner notre clef d’accès et le nom du service :

image

  • Avec ces informations, nous allons venir sur les rôles de notre application :

image

  • Sur chacun des rôles nous allons faire bouton droit et choisir le menu propriétés et rentrer dans l’onglet ”Settings” pour venir basculer notre chaine de connexion de l’émulateur vers le compte de stockage sur Azure :

image

  • C’est donc la propriété AzureStorage qui doit être mise à jour en utilisant le bouton “…” en bout de ligne :

image

  • C’est sur cet écran que nous allons renseigner le nom du compte et la clef que l’on a récupérée au préalable.
  • Notez également que dans l’onglet Configuration, vous pourrez choisir la puissance et le nombre d’instances pour chaque rôle. Dans mon cas j’ai fais les choix suivants (attention la version d’essai ne permet pas d’utiliser autant de puissance) :

image

image

  • Les deux instances “extra small” seront pour le frontal web et les deux “extra large” seront pour le Worker Role qui est le grand consommateur de puissance (pour plus d’infos sur les différents types d’instances: http://msdn.microsoft.com/fr-fr/library/ee814754.aspx)
  • Au final, nous pouvons lancer la publication définitive :

image

Notre service est donc en ligne! Il ne nous reste plus qu’à lui fournir des scènes à dessiner.

Définition d’une scène

Pour définir une scène à rendre sur le serveur nous allons passer par un fichier xml. Voici par exemple la scène par défaut que l’on peut utiliser sur le service :

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <scene FogStart="5" FogEnd="20" FogColor="0, 0, 0" ClearColor="0, 0, 0" AmbientColor="0.1, 0.1, 0.1">
  3.   <objects>
  4.     <sphere Name="Red Sphere" Center="0, 1, 0" Radius="1">
  5.       <defaultShader Diffuse="1, 0, 0" Specular="1, 1, 1" ReflectionLevel="0.6"/>
  6.     </sphere>
  7.     <sphere Name="Transparent Sphere" Center="-3, 0.5, 1.5" Radius="0.5">
  8.       <defaultShader Diffuse="0, 0, 1" Specular="1, 1, 1" OpacityLevel="0.4" RefractionIndex="2.8"/>
  9.     </sphere>
  10.     <sphere Name="Green Sphere" Center="-3, 2, 4" Radius="1">
  11.       <defaultShader Diffuse="0, 1, 0" Specular="1, 1, 1" ReflectionLevel="0.6" SpecularPower="10"/>
  12.     </sphere>
  13.     <sphere Name="Yellow Sphere" Center="-0.5, 0.3, -2" Radius="0.3">
  14.       <defaultShader Diffuse="1, 1, 0" Specular="1, 1, 1" Emissive="0.3, 0.3, 0.3" ReflectionLevel="0.6"/>
  15.     </sphere>
  16.     <sphere Name="Orange Sphere" Center="1.5, 2, -1" Radius="0.5">
  17.       <defaultShader Diffuse="1,0.5, 0" Specular="1, 1, 1" ReflectionLevel="0.6"/>
  18.     </sphere>
  19.     <sphere Name="Gray Sphere" Center="-2, 0.2, -0.5" Radius="0.2">
  20.       <defaultShader Diffuse="0.5, 0.5, 0.5" Specular="1, 1, 1" ReflectionLevel="0.6" SpecularPower="1"/>
  21.     </sphere>
  22.     <ground Name="Plane" Normal="0, 1, 0" Offset="0">
  23.       <checkerBoard WhiteDiffuse="1, 1, 1" BlackDiffuse="0.1, 0.1, 0.1" WhiteReflectionLevel="0.1" BlackReflectionLevel="0.5"/>
  24.     </ground>
  25.   </objects>
  26.   <lights>
  27.     <light Position="-2, 2.5, -1" Color="1, 1, 1"/>
  28.     <light Position="1.5, 2.5, 1.5" Color="0, 0, 1"/>
  29.   </lights>
  30.   <camera Position="0, 2, -6" Target="-0.5, 0.5, 0" />
  31. </scene>

La structure d’un fichier est la suivante :

  • Une balise [scene] délimite le fichier et permet de définir les paramètres suivants:
    • FogStart / FogEnd : Distance de départ et de fin du brouillard. Cette distance est évaluée depuis la caméra.
    • FogColor : Couleur du brouillard sous le format RGB (ou chaque composante varie de 0 à 1)
    • ClearColor : Couleur de fond sous le format RGB (ou chaque composante varie de 0 à 1)
    • AmbientColor : Couleur ambiante sous le format RGB (ou chaque composante varie de 0 à 1)
  • Une balise [objects] qui contient la liste des objets
  • Une balise [lights] qui contient la liste des lumières
  • Une balise [camera] qui définit la caméra de la scène. Il s’agit de notre point de vue. Elle est définit par les paramètres suivants :
    • Position : Position de la caméra sous la forme X,Y,Z
    • Target : Position de ce que regarde la caméra sous la forme X, Y, Z

Les objets portent tous un nom (Name) et peuvent être définis par les balises suivantes:

  • sphere : Sphere définie par la position de son centre (Center) et son rayon (Radius)
  • ground : Plan représentant le sol défini par sa hauteur (Offset) et la direction de sa normale (Normal)
  • mesh : Object complexe défini par une suite de vertices et de faces. Il peut être modifié via trois vecteurs : Position, Rotation et Scaling (zoom) :
  1. <mesh Name="Box" Position="-3, 0, 2" Rotation="0, 0.7, 0">
  2.   <vertices count="24">-1, -1, -1, -1, 0, 0,-1, -1, 1, -1, 0, 0,-1, 1, 1, -1, 0, 0,-1, 1, -1, -1, 0, 0,-1, 1, -1, 0, 1, 0,-1, 1, 1, 0, 1, 0,1, 1, 1, 0, 1, 0,1, 1, -1, 0, 1, 0,1, 1, -1, 1, 0, 0,1, 1, 1, 1, 0, 0,1, -1, 1, 1, 0, 0,1, -1, -1, 1, 0, 0,-1, -1, 1, 0, -1, 0,-1, -1, -1, 0, -1, 0,1, -1, -1, 0, -1, 0,1, -1, 1, 0, -1, 0,-1, -1, 1, 0, 0, 1,1, -1, 1, 0, 0, 1,1, 1, 1, 0, 0, 1,-1, 1, 1, 0, 0, 1,-1, -1, -1, 0, 0, -1,-1, 1, -1, 0, 0, -1,1, 1, -1, 0, 0, -1,1, -1, -1, 0, 0, -1,</vertices>
  3.   <indices count="36">0,1,2,2,3,0,4,5,6,6,7,4,8,9,10,10,11,8,12,13,14,14,15,12,16,17,18,18,19,16,20,21,22,22,23,20,</indices>
  4. </mesh>

Les faces sont des index vers les vertices sachant qu’une face contient trois vertices. Les vertices sont définis par une paire de coordonnées 3D: la position (X, Y, Z) et la normal (Nx, Ny, Nz).

Les objets peuvent porter un nœud enfant qui permet de définir le type de matériau que l’on va appliquer:

  • defaultShader : Matériau par défaut défini par les propriétés suivantes :
    • Diffuse : Couleur de base de l’objet au format RGB
    • Ambient : Couleur ambiante de l’objet au format RGB
    • Specular : Couleur du spéculaire de l’objet au format RGB
    • Emissive : Couleur émisse par l’objet au format RGB
    • SpecularPower : Niveau de dureté du spéculaire
    • RefractionIndex : Indice de réfraction (Vous devez définir le OpacityLevel également)
    • OpacityLevel : Niveau de transparence (Vous devez définir le RefractionIndex également)
    • ReflectionLevel : Indice de réflexion
  • checkerBoard : Matériau définissant un damier produit par les paramètres suivants :
    • WhiteDiffuse : Couleur de base des cases “blanches”
    • WhiteAmbient : Couleur ambiante des cases “blanches”
    • WhiteReflectionLevel : Niveau de réflexion des cases “blanches”
    • BlackDiffuse : Couleur de base des cases “noires”
    • BlackAmbient : Couleur ambiante des cases “noires”
    • BlackReflectionLevel : Niveau de réflexion des cases “noires”

Les lumières quand à elles sont définies via la balise [light] et via les attributs Position et Color (Couleur). Elles sont de types omnidirectionnelles (elles éclairent dans toutes les directions tels un soleil ponctuel).

Par exemple le fichier suivant :

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <scene FogStart="5" FogEnd="20" FogColor="0, 0, 0" ClearColor="0, 0, 0" AmbientColor="1, 1, 1">
  3.   <objects>
  4.     <ground Name="Plane" Normal="0, 1, 0" Offset="0">
  5.       <defaultShader Diffuse="0.4, 0.4, 0.4" Specular="1, 1, 1" ReflectionLevel="0.3" Ambient="0.5, 0.5, 0.5"/>
  6.     </ground>
  7.     <sphere Name="Sphere" Center="-0.5, 1.5, 0" Radius="1">
  8.       <defaultShader Diffuse="0, 0, 1" Specular="1, 1, 1" ReflectionLevel="0" Ambient="1, 1, 1"/>
  9.     </sphere>
  10.   </objects>
  11.   <lights>
  12.     <light Position="-0.5, 2.5, -2" Color="1, 1, 1"/>
  13.   </lights>
  14.   <camera Position="0, 2, -6" Target="-0.5, 0.5, 0" />
  15. </scene>

Produit le résultat suivant :

53ac53ad-b971-4d5e-8526-e7a4e39c3bb1

Le serveur web et les workers roles

Le serveur web tourne avec ASP.Net et fournit principalement deux fonctionnalités:

  • Lancement du rendu en envoyant un message dans la file d’attente des workers roles :
  1. void Render(string scene)
  2. {
  3.     try
  4.     {
  5.         InitializeStorage();
  6.         var guid = Guid.NewGuid();
  7.  
  8.         CloudBlob blob = Container.GetBlobReference(guid + ".xml");
  9.         blob.UploadText(scene);
  10.  
  11.         blob = Container.GetBlobReference(guid + ".progress");
  12.         blob.UploadText("-1");
  13.  
  14.         var message = new CloudQueueMessage(guid.ToString());
  15.         queue.AddMessage(message);
  16.  
  17.         guidField.Value = guid.ToString();
  18.     }
  19.     catch (Exception ex)
  20.     {
  21.         System.Diagnostics.Trace.WriteLine(ex.ToString());
  22.     }
  23. }

Comme nous pouvons le voir le principe est de générer pour chaque requête un GUID qui servira d’identifiant tout le long. Par la suite la description de la scène est mise dans un blob pour que les workers la récupèrent et un message est envoyé via la file d’attente (le message contient juste l’identifiant à traiter).

De plus un blob est également créé pour donner l’état d’avancement global (-1= Requête en attente).

  • Fournir un service web de gestion de l’état d’avancement pour que le client connaisse l’état courant :
  1. [OperationContract]
  2. [WebGet]
  3. public string GetProgress(string guid)
  4. {
  5.     try
  6.     {
  7.         CloudBlob blob = _Default.Container.GetBlobReference(guid + ".progress");
  8.         string result = blob.DownloadText();
  9.  
  10.         if (result == "101")
  11.             blob.Delete();
  12.  
  13.         return result;
  14.     }
  15.     catch (Exception ex)
  16.     {
  17.         return ex.Message;
  18.     }
  19. }

Le service requête juste le contenu du blob qui donne l’état et retourne le résultat. Si le rendu est fini (valeur=101), il supprime le blob en question.

Les workers quand à eux ont la charge de lire la file d’attente pour en récupérer du travail. Si un message est présent, le premier worker libre viendra prendre le message et se chargera d’effectuer le rendu :

  1. while (true)
  2. {
  3.     CloudQueueMessage msg = null;
  4.     semaphore.WaitOne();
  5.     try
  6.     {
  7.         msg = queue.GetMessage();
  8.         if (msg != null)
  9.         {
  10.             queue.DeleteMessage(msg);
  11.             string guid = msg.AsString;
  12.             CloudBlob blob = container.GetBlobReference(guid + ".xml");
  13.             string xml = blob.DownloadText();
  14.  
  15.             CloudBlob blobProgress = container.GetBlobReference(guid + ".progress");
  16.             blobProgress.UploadText("0");
  17.  
  18.             WorkingUnit unit = new WorkingUnit();
  19.  
  20.             unit.OnFinished += () =>
  21.                                    {
  22.                                        blob.Delete();
  23.                                        unit.Dispose();
  24.                                        semaphore.Release();
  25.                                    };
  26.  
  27.             unit.Launch(guid, xml, container);
  28.         }
  29.         else
  30.         {
  31.             semaphore.Release();
  32.         }
  33.         Thread.Sleep(1000);
  34.     }
  35.     catch (Exception ex)
  36.     {
  37.         semaphore.Release();
  38.         if (msg != null)
  39.         {
  40.             CloudQueueMessage newMessage = new CloudQueueMessage(msg.AsString);
  41.             queue.AddMessage(newMessage);
  42.         }
  43.         Trace.WriteLine(ex.ToString());
  44.     }
  45. }

Une fois que le contenu de la scène à produire est chargé, le worker met à jour l’état d’avancement et crée une WorkingUnit qui se chargera en asynchrone de gérer le rendu. Dès que le rendu sera terminé, la WorkingUnit lèvera un évènement OnFinished qui nous permettra de tout nettoyer.

Il est important de noter ici l’utilisation du sémaphore pour limiter le nombre de rendus qui sont effectués en parallèle.

La WorkingUnit fonctionne autour de la méthode Launch :

  1. public void Launch(string guid, string xml, CloudBlobContainer container)
  2. {
  3.     try
  4.     {
  5.         XmlDocument xmlDocument = new XmlDocument();
  6.         xmlDocument.LoadXml(xml);
  7.         XmlNode sceneNode = xmlDocument.SelectSingleNode("/scene");
  8.  
  9.         Scene scene = new Scene();
  10.         scene.Load(sceneNode);
  11.  
  12.         ParallelRayTracer renderer = new ParallelRayTracer();
  13.  
  14.         resultBitmap = new Bitmap(RenderWidth, RenderHeight, PixelFormat.Format32bppRgb);
  15.  
  16.         bitmapData = resultBitmap.LockBits(new Rectangle(0, 0, RenderWidth, RenderHeight), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
  17.         int bytes = Math.Abs(bitmapData.Stride) * bitmapData.Height;
  18.         byte[] rgbValues = new byte[bytes];
  19.         IntPtr ptr = bitmapData.Scan0;
  20.  
  21.         renderer.OnAfterRender += (obj, evt) =>
  22.                                       {
  23.                                           System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);
  24.  
  25.                                           resultBitmap.UnlockBits(bitmapData);
  26.                                           using (MemoryStream ms = new MemoryStream())
  27.                                           {
  28.                                               resultBitmap.Save(ms, ImageFormat.Png);
  29.                                               ms.Position = 0;
  30.                                               CloudBlob finalBlob = container.GetBlobReference(guid + ".png");
  31.                                               finalBlob.UploadFromStream(ms);
  32.                                               CloudBlob blob = container.GetBlobReference(guid + ".progress");
  33.                                               blob.UploadText("101");
  34.                                           }
  35.                                           OnFinished();
  36.                                       };
  37.  
  38.         int previousPercentage = -10;
  39.         renderer.OnLineRendered += (obj, evt) =>
  40.                                        {
  41.                                            if (evt.Percentage - previousPercentage < 10)
  42.                                                return;
  43.                                            previousPercentage = evt.Percentage;
  44.                                            CloudBlob blob = container.GetBlobReference(guid + ".progress");
  45.                                            blob.UploadText(evt.Percentage.ToString());
  46.                                        };
  47.  
  48.         renderer.Render(scene, RenderWidth, RenderHeight, (x, y, color) =>
  49.         {
  50.             var offset = x * 4 + y * bitmapData.Stride;
  51.             rgbValues[offset] = (byte)(color.B * 255);
  52.             rgbValues[offset + 1] = (byte)(color.G * 255);
  53.             rgbValues[offset + 2] = (byte)(color.R * 255);
  54.         });
  55.     }
  56.     catch (Exception ex)
  57.     {
  58.         CloudBlob blob = container.GetBlobReference(guid + ".progress");
  59.         blob.DeleteIfExists();
  60.         blob = container.GetBlobReference(guid + ".png");
  61.         blob.DeleteIfExists();
  62.         Trace.WriteLine(ex.ToString());
  63.     }
  64. }

Son fonctionnement peut être résumé ainsi:

  • Chargement de la scène
  • Création d’un raytracer
  • Génération d’une image cible et accès à son tableau d’octets
  • Lorsque le raytracer aura terminé, on sauve l’image dans un blob et on met à jour la progression pour indiquer la complétion du rendu
  • Définition de la manière de transférer les pixels vers l’image et lancement du rendu

Comme nous travaillons en mode mutli-instances, il est important qu’aucune dépendance ne soit établie entre le client, le serveur web et le worker role. C’est pour cela que tout passe par le GUID, la file et les blobs qui sont partagés par tous les acteurs.

Le raytracer

Le raytracer a été écrit entièrement en C# 4.0 et utilise la TPL (Task Parallel Libray) pour profiter de la puissance du développement parallèle.

Il gère les fonctionnalités suivantes (tout ne sera pas décrit ici, je vous encourage vivement à parcourir le code pour plus de détail. “Obvious is the code” comme dirait maitre Yoda) :

  • Brouillard
  • Diffuse
  • Ambiant
  • Transparence
  • Réflexion
  • Réfraction
  • Ombres
  • Objets complexes
  • Plusieurs sources lumineuses
  • Antialiasing
  • Rendu multi-cœurs
  • Octrees

L’avantage d’un raytracer c’est que son mode de fonctionnement est extrêmement parallélisable. En effet, un raytracer va pour produire son image lancer un rayon partant de chaque pixel de l’image et va remonter le faisceau lumineux qui a permis d’aboutir à ce pixel. Ainsi en partant de ce dernier, nous allons calculer la couleur du premier objet rencontré. Cette couleur viendra de son matériau et du niveau d’éclairage reçu de chaque lumière. Si l’objet est transparent ou réflectif ou réfractif, nous continuerons notre chemin en modifiant la direction du vecteur lumineux en fonction du type de matériau.

Le traitement en question est donc le même pour chaque pixel. De ce fait nous pouvons aisément paralléliser les traitements (jusqu’au niveau de chaque pixel si l’on a assez de processeurs). Dans notre cas et en attendant d’avoir plusieurs milliers de cœurs par machine, nous allons paralléliser au niveau de chaque ligne.

Ainsi le code central de notre raytracer est le suivant :

  1. Parallel.For(0, RenderHeight, y => ProcessLine(scene, y));

Nous allons donc parcourir l’ensemble des lignes et faire exécuter en parallèle sur l’ensemble des cœurs (merci TPL!) la méthode suivante :

  1. void ProcessLine(Scene scene, int line)
  2. {
  3.     for (int x = 0; x < RenderWidth; x++)
  4.     {
  5.         if (!renderInProgress)
  6.             return;
  7.         RGBColor color = RGBColor.Black;
  8.  
  9.         if (SuperSamplingLevel == 0)
  10.         {
  11.             color = TraceRay(new Ray { Start = scene.Camera.Position, Direction = GetPoint(x, line, scene.Camera) }, scene, 0);
  12.         }
  13.         else
  14.         {
  15.             int count = 0;
  16.             double size = 0.4 / SuperSamplingLevel;
  17.  
  18.             for (int sampleX = -SuperSamplingLevel; sampleX <= SuperSamplingLevel; sampleX += 2)
  19.             {
  20.                 for (int sampleY = -SuperSamplingLevel; sampleY <= SuperSamplingLevel; sampleY += 2)
  21.                 {
  22.                     color += TraceRay(new Ray { Start = scene.Camera.Position, Direction = GetPoint(x + sampleX * size, line + sampleY * size, scene.Camera) }, scene, 0);
  23.                     count++;
  24.                 }
  25.             }
  26.  
  27.             if (SuperSamplingLevel == 1)
  28.             {
  29.                 color += TraceRay(new Ray { Start = scene.Camera.Position, Direction = GetPoint(x, line, scene.Camera) }, scene, 0);
  30.                 count++;
  31.             }
  32.  
  33.             color = color / count;
  34.         }
  35.  
  36.         color.Clamp();
  37.  
  38.         storePixel(x, line, color);
  39.     }
  40.  
  41.     // Report progress
  42.     lock (this)
  43.     {
  44.         linesProcessed++;
  45.         if (OnLineRendered != null)
  46.             OnLineRendered(this, new LineRenderedEventArgs { Percentage = (linesProcessed * 100) / RenderHeight, LineRendered = line });
  47.     }
  48. }

La partie principale se passe dans la méthode TraceRay qui va donc pour chaque pixel de la ligne, envoyé un rayon depuis la caméra passant par le pixel en question :

  1. private RGBColor TraceRay(Ray ray, Scene scene, int depth, SceneObject excluded = null)
  2. {
  3.     List<Intersection> intersections;
  4.     
  5.     if (excluded == null)
  6.         intersections = IntersectionsOrdered(ray, scene).ToList();
  7.     else
  8.         intersections = IntersectionsOrdered(ray, scene).Where(intersection => intersection.Object != excluded).ToList();
  9.  
  10.     return intersections.Count == 0 ? scene.ClearColor : ComputeShading(intersections, scene, depth);
  11. }

Si le rayon ne trouve aucun objet sur sa route alors on retourne la couleur de la scène (ClearColor) sinon on va évaluer la couleur induite par la collision avec les objets trouvés (le calcul de cette collision est ici optimisé par l’utilisation d’octrees) :

  1. private RGBColor ComputeShading(List<Intersection> intersections, Scene scene, int depth)
  2. {
  3.     Intersection intersection = intersections[0];
  4.     intersections.RemoveAt(0);
  5.  
  6.     var direction = intersection.Ray.Direction;
  7.     var position = intersection.Position;
  8.     var normal = intersection.Normal;
  9.     var reflectionDirection = direction - 2 * Vector3.Dot(normal, direction) * normal;
  10.  
  11.     RGBColor result = GetBaseColor(intersection.Object, position, normal, reflectionDirection, scene, depth);
  12.  
  13.     // Opacity
  14.     if (IsOpacityEnabled && intersections.Count > 0)
  15.     {
  16.         double opacity = intersection.Object.Shader.GetOpacityLevelAt(position);
  17.         double refractionIndex = intersection.Object.Shader.GetRefractionIndexAt(position);
  18.  
  19.         if (opacity < 1.0)
  20.         {
  21.             if (refractionIndex == 1 || !IsRefractionEnabled)
  22.                 result = result * opacity + ComputeShading(intersections, scene, depth) * (1.0 - opacity);
  23.             else
  24.             {
  25.                 // Refraction
  26.                 result = result * opacity + GetRefractionColor(position, Utilities.Refract(direction, normal, refractionIndex), scene, depth, intersection.Object) * (1.0 - opacity);
  27.             }
  28.         }
  29.     }
  30.  
  31.     if (!IsFogEnabled)
  32.         return result;
  33.  
  34.     // Fog
  35.     double distance = (scene.Camera.Position - position).Length;
  36.  
  37.     if (distance < scene.FogStart)
  38.         return result;
  39.  
  40.     if (distance > scene.FogEnd)
  41.         return scene.FogColor;
  42.  
  43.     double fogLevel = (distance - scene.FogStart) / (scene.FogEnd - scene.FogStart);
  44.  
  45.     return result * (1.0 - fogLevel) + scene.FogColor * fogLevel;
  46. }

La méthode ComputeShading va établir la couleur de base de l’objet (en tenant compte des sources lumineuses bien sur). Si l’objet est opaque, réflectif ou réfractif, un nouveau rayon va être calculé pour établir la couleur induite. Cette couleur sera alors mitigée avec la couleur de base.

Une fois cette opération réussie, le brouillard est ajouté et la couleur finale est retournée.

Comme nous pouvons le voir, le calcul de chaque pixel est extrêmement gourmand et le fait de disposer d’une grande puissance brute améliore directement la vitesse de rendu.

Le client

La partie cliente s’appuie sur de l’HTML auquel j’ai rajouté un peu de JavaScript pour rendre le tout plus dynamique.

En effet, lors du lancement d’un rendu, je lance en parallèle un timer qui va se charger de requêter toutes les secondes l’état d’avancement :

  1. var checkState = function () {
  2.     $.getJSON("RenderStatusService.svc/GetProgress", { guid: guid, noCache: Math.random() }, function (result) {
  3.         var percentage = result.d;
  4.         var percentageAsNumber = parseInt(percentage);
  5.  
  6.         if (percentage == "-1") {
  7.             $("#progressMessage").text("Request queued");
  8.             setTimeout(checkState, 1000);
  9.             return;
  10.         }
  11.  
  12.         if (isNaN(percentageAsNumber)) {
  13.             window.localStorage.removeItem("currentGuid");
  14.             restartUI();
  15.             return;
  16.         }
  17.  
  18.         if (percentageAsNumber != 101) {
  19.             $("#progressBar").progressbar({ value: percentageAsNumber });
  20.             $("#progressMessage").text("Rendering in progress..." + result.d + "%");
  21.             setTimeout(checkState, 1000);
  22.         }
  23.         else {
  24.             $("#renderInProgressDiv").slideUp("fast");
  25.             $("#final").slideDown("fast");
  26.             $("#imageLoadingMessage").slideDown("fast");
  27.             $.getJSON("RenderStatusService.svc/GetImageUrl", { guid: guid, noCache: Math.random() }, function (url) {
  28.                 finalImage.src = url.d;
  29.                 document.getElementById("imageHref").href = url.d;
  30.             });
  31.             window.localStorage.removeItem("currentGuid");
  32.         }
  33.     });
  34. };

Si le service retourne –1, la requêtes est en attente. Si il retourne une valeur entre 0 et 100, il s’agit du taux d’avancement et si il retourne 101 c’est que l’image est prête et que nous pouvons l’afficher.

Au passage je me suis amusé à stocker le GUID de l’image en cours dans le localStorage pour permettre de garder une cohérence si l’utilisateur ferme le navigateur et revient par la suite sur la page.

Conclusion

Comme nous avons pu le voir tout au long de cet article, Azure me fournit tous les outils pour à la fois développer localement tout en disposant pour ma production d’une puissance adaptable à mes besoins.

Je vous invite à installer le SDK qui vous permettra de tester notre raytracer mais qui vous permettra surtout de vous préparer à utiliser tout ce que propose Azure.

Pour aller plus loin

Quelques liens intéressants autour d’Azure:

Leave a Comment
  • Please add 3 and 2 and type the answer here:
  • Post
  • Bonjour,

    intéressant mais comment générer les mesh ? existe-t-il un EDI pour les dessinet et récupérer les points?

  • VOus pouvez utiliser le format .ASC ou blender mais vous devrez un peu adapter le resultat

Page 1 of 1 (2 items)