Do you want to know why your app has z-fighting even when you're using a 24 bits zbuffer in your 3D app ? Or are you interested to know what is the internal representation of a Z-Buffer and aren't afraid of the gritty details ? Then read the following : The depth buffer the gritty details.

Après avoir répondu plusieurs fois à des questions similaires dans les forums du web, il est facile de se fatiguer à force et il devient difficile de répondre suffisamment bien pour éduquer les gens.

L'une des questions qui revient à tout coup est "pourquoi mes objets disparaissent, ont des artefacts liés au Z-Buffer (Z-Fighting)". Donc plutôt que de répéter une réponse incomplète ("il faut déplacer le plan proche un peu plus loin"), je préfère rédiger une réponse qui couvre largement le sujet à laquelle les gens peuvent se référer et s'ils sont vraiment intéressés peuvent jeter un oeil aux maths pour prendre une meilleur décision par eux-même. Cela n'est pas votre FAQ typique parce que ça ne répond pas à une question en particulier et parce que vous aurez plus de détails que vous pouviez demander.

Ceci dit, vous pouvez aller lire l'article là bas : Le depth buffer - ce qu'il faut savoir.

 

Si vous vous intéressez un peu au monde de l'électronique vous avez peut-être entendu parler du terme "yield" employé pour les microprocesseurs. Ce terme qui signifie en gros, le pourcentage de processeurs fonctionnant correctement rapporté à la capacité théorique de la ligne de production. Ce que je vais explorer ici, ce sont les modèles théoriques qui permettent de le calculer, les raisons des malfonctions et leur influence sur le coût d'un processeur.[...]

Qualité du process de production

Lorsque vous construisez des systèmes complexes avec des traits caractéristiques dont la taille ne dépasse pas le micromètre, voire approchent du nanomètre, vous savez probablement que des erreurs dans la construction de ces systèmes sont inévitables. La moindre poussière dont notre monde est rempli apparaitra de la taille d'un boing 747 ou d'une montagne une fois rapporté à ces échelles microscopiques. De même les défauts dans la structure atomique du substrat qui composent votre gaufrette de silicone (wafer) vont affecter négativement le fonctionnement de votre puce. Bien entendu les usines qui produisent ces puces ont des atmosphères filtrées et sont entièrement automatisées pour réduire l'impact des êtres humains pourvoyeurs de poussière. Mais même dans ces conditions les possibilités d'erreur sont là.

La possibilité de voir une erreur apparaître est dépendante de la qualité du process de production. Paradoxalement les process archaïques mais éprouvés ont une faible probabilité d'erreur. Au contraire les méthodes hi-tech qui poussent plus loin les limites de ce qui est faisable en terme de taille de gravure et de nombre de couches superposées vont être plus soumis aux risques d'erreur.

La formule du yield

Ceci dit en général on raisonne en terme de process donné. Tout simplement parce que le choix de ce process est fait pour des raisons externes. D'une part il n'y a pas tant de fondeurs que ça et d'autre part la pression du marché fait que l'on devra migrer tôt ou tard sur un process qui nous permettra de monter en fréquence et d'augmenter la taille de la puce. Et la plupart du temps la qualité du process n'est pas quelque chose qui est directement influençable (même s'il doit rentrer en ligne de compte dans les coûts du silicium).

La formule du yield est une mesure statistique, du nombre de chips défectueux en fonction du nombre d'erreurs par unité de surface. Elle considère d'une part que la disposition de ces erreurs est totalement aléatoire et uniforme sur la surface du wafer. Donc la distribution est totalement caractérisée par cette quantité (ou probabilité) d'erreurs par unité de surface et la gravité de ces erreurs est identique (on reviendra sur cela plus tard). Et donc le résultat d'une erreur sur la surface du chip est une puce défectueuse.

Il y a plusieurs modèles pour les défauts, celui que j'ai énoncé est celui d'une loi de Poisson et qui n'est pas forcément vraie dans la réalité mais suffira pour la démonstration. Dans ce modèle (distribution totalement aléatoire et uniforme), la probabilité qu'une puce ait au moins un défaut et donc soit défectueuse est 1 - exp(-AD). A étant l'aire couverte par la puce et D étant la probabilité d'erreur par unité de surface. Cette formule du yield va déterminer la quantité de puces que l'on peut produire par wafer.

Influence de la taille

On voit donc que si l'on construit une puce grosse on est doublement perdant, d'une part parce que pour une taille de wafer donnée on pourra mettre moins de grosses puces que de petites puces (et ce n'est pas une relation strictement linéaire à l'aire, parce qu'au fur et à mesure que la puce augmente, la quantité de surface non utilisable sur les bords augmente aussi), et que parmi cette quantité réduite de puces une plus grosse proportion sera non fonctionnelle. Cela se voit dans la formule lorsque l'aire devient très grande, l'exponentielle tend vers zèro et la proportion de puces défectueuse tend vers 1. La limite est bien entendu la taille du wafer et même s'il est théoriquement possible de faire une puce qui couvre toute la surface utile du wafer, on se ruinerait en silicium à vouloir en sortir ne serait-ce qu'une seule puce fonctionnelle.

Influence sur les coûts

Les systèmes de facturation sont variables d'un fondeur à un autre et d'un contrat à un autre. Le modèle économique du fondeur (qui fabrique la puce) n'est pas forcément le même que de celui de son client (qui a commandé la puce au fondeur). Ce qui fait qu'il n'est pas forcément évident d'établir des lois toutes faites pour extraire de la formule du yield, celle des coûts et des potentiels revenus issus de la vente de la puce.

Par contre ce qui est clair c'est que le yield influence directement la capacité de production et donc la capacité d'inonder le marché de puces fonctionnelles et de répondre à la demande. Ceci dit dans l'industrie du semi-conducteur il n'est pas rare qu'une meme entreprise ait deux modèles simultanés, l'un reposant sur une puce facile à produire en grande quantité et à faibles marges, et une autre puce reposant sur une construction plus complexe, avec un faible débit mais en contrepartie des marges à la vente plus élevées. Déterminer à l'avance quel modèle va créer le plus de revenu pour l'entreprise n'est pas forcément évident.

Utilité de la redondance

On a déterminé naïvement qu'une puce plus grosse va influencer négativement la capacité de production (et si ce n'est le coût, au moins la capacité à faire de l'argent). Mais cela n'est pas toujours vrai. Dans certains cas, faire des puces plus grandes va avoir des conséquences positives à la fois sur la capacité de production et sur les revenus.

Une puce aura toujours une taille minimale n�cessaire pour implémenter ses fonctionalités et pour atteindre une performance cible. Mais ceci dit on n'est pas forcément satisfait du yield initial d'une telle puce. On peut faire le choix de couper des fonctionalités ou de couper de la performance pour atteindre un yield plus raisonnable. Mais ce faisant on risque de changer de segment de marché, empiétant sur les plates bandes des produits des gammes inférieures ou d'être mis à mal par la concurrence. De même passer du temps pour optimiser le design pour réduire sa taille, peut signifier un retard important qui se traduira par une introduction tardive et une plus faible résistance aux actions de la concurrence. Ou on peut choisir de prendre le chemin de la redondance.

La redondance est le procédé qui consiste à rajouter des unités doublons, qui peuvent être désactivées sans invalider la puce. Le procédé est fréquemment utilisé pour les processeurs comme ceux d'Intel dont la moitié de la surface est prise par la mémoire cache. La mémoire cache est une unité relativement simple par rapport au reste de la puce mais qui prend une large place par simple duplication d'unité de base. Qu'est-ce qu'on y gagne ? direz-vous, on peut certes désactiver une unité défectueuse mais en contrepartie on doit payer le coût de cette unité superfétatoire.

Imaginez qu'une erreur fatale si elle arrive a 50% de chance d'arriver dans une de ces unités doublon si elles couvrent la moitié du chip. Le yield consistera donc comme ci dessus de la totalité des puces ayant zéro erreur, à cela s'ajoutera la moitié des puces qui auront une erreur, etc. Si il y avait typiquement une erreur par puce, cela peut donc faire passer une puce du statut de "non produisible", à une puce au yield acceptable et cela pour une inactivation de x% de ses transistors. Inversement si le yield était déjà à 90% sans redondance, ajouter de la redondance peut faire avancer le yield vers 95% mais en contrepartie de x% moins de puces par wafer, et donc ce n'est pas toujours un bon choix.

Là encore il y a un choix économique à faire. Multiplier les unités strictement par deux en pratique diminue le nombre maximal de puces produisibles par plus de deux (en comptant les déchets). Pour y gagner il faut donc que l'augmentation du yield soit également supérieure à deux.

Conclusion

Le sujet est vaste, il y aurait encore beaucoup à en dire, mais j'espère qu'il aura contribué à vous faire voir de manière un peu plus clair la notion de yield et ses conséquences sur la production de microprocesseurs. Envoyez vos questions à <>

 

Alors ce futur Direct3D 10, révolution ou évolution ?

Scoop : la version 10 de direct3d ne va pas révolutionner le gameplay des jeux. Mais ce n'est pas trés important puisque cette nouvelle release est entièrement et uniquement dédiée au graphisme ! Avec un peu de chance vous jouerez toujours aux mêmes FPS sous de nouveaux noms l'an prochain ;). Ce qui suit va donc uniquement parler du côté technique des jeux. Pour les tests, il y a les sites spécialisés. [...]

Un peu d'histoire : au fur et à mesure des release, DirectX s'est réduit comme une peau de chagrin. Le premier à passer à la trappe a été le fameux Direct3D retained mode (moteur de rendu de type "scene graph"), qui constituait pourtant le coeur de la stratégie jeu de Microsoft. Les développeurs de jeu n'aimaient pas qu'on leur mâche le travail et en plus le retained mode un peu rigide et reposant sur un rendu software super optimisé est vite devenu inadapté dans le monde ultra changeant des jeux en 3D. Puis est passée à la trappe l'interface DirectDraw (traitements 2D) dont l'évolution en parallèle ne permettait pas profiter des capacités des cartes accélératrices 3D. Puis on a vu arriver puis disparaître DirectPlay, une interface censée simplifier le jeu en réseau (modem, série, IPX) mais rendue obsolète par l'omniprésence de TCP/IP. Enfin, DirectInput est en train de mourir de manière similaire. La faute à une implantation sous XP qui n'a rien à apporter de plus face aux messages windows classiques pour la gestion du couple clavier/souris. Et en partie aussi du au nouveau focus de Microsoft sur les accessoires Xbox360 qu'ils essaient de promouvoir pour le jeu sous Windows. Ne restent donc plus que la 3D et l'audio. L'audio ne progressera pas dans sa prochaine version (ce qui pousse Creative Labs à pousser OpenAL comme alternative à DirectAudio).

Tout ça pour dire que seule la partie 3D changera de manière significative dans la prochaine itération de l'API de Microsoft. Mais quel changement !

Une nouvelle plateforme

Ceux qui ont connu le passage de Direct3D8 � Direct3D9, ne vont pas reconnaître leurs petits. D3D10 joue la rupture sur tous les plans.

Premièrement D3D10 ne sera disponible que sur Vista, le nouvel OS de Microsoft. Pas de rétro compatibilité avec les ordinateurs sous XP.

Deuxièmement D3D10 ne tournera que si vous avez une carte qui intègre toutes les features de Direct3D10. Pas d'exception. Si votre vendeur de hardware vous vend une carte graphique compatible D3D9 en vous disant qu'elle a des features qui vont au delà des specs de D3D10 vous pouvez lui rire au nez. Quoiqu'il arrive les derniers hardwares sortis ne feront pas tourner les jeux écrits exclusivement pour Direct3D10.

Enfin, l'interface de programmation change du tout au tout. Avec Direct3D8, vous pouviez faire un search/replace pour remplacer les noms des interfaces d3d8 et les remplacer par les noms d'interface d3d9. Ici ce n'est plus possible. Tout ce qui est legacy support est passé à la trappe. Il n'y a pas de hw transform and lighting, uniquement des shaders. Il n'y a pas de texture stages, uniquement des shaders et des texture samplers.

Pour résumer, programmer pour Direct3D10 c'est comme programmer pour une nouvelle plateforme. Votre base de client sera forcément réduite au départ et pour toucher un plus large public il faudra faire du "multiplateforme" : faire un jeu qui tourne de la même façon sous D3D9 et D3D10. Abstraire les similarités et simuler les parties manquantes.

Quoi de neuf en fait ?

Lister toutes les nouveautés risque de devenir une énumération à la Prévert. Mais bon tentons.

Unification des spécifications des shaders. Sous D3D8 et D3D9, on avait introduit des vertex shaders et pixel shaders dont la syntaxe était totalement différente avec possibilité de mixer les versions entre elles et des features qui étaient accessibles pour l'une et pas pour l'autre (texturing). D3D10 met fin à ça. Tous les shaders doivent être de la même version et ont accés aux mêmes instructions. Ils peuvent lire les mêmes formats de textures, ont les mêmes capacités de branchement. Seules certaines instructions très spécifiques (comme les gradients) pourraient garder un parfum de spécificité. Bien entendu, la séparation "logique" entre vertex et pixel shader subsiste, la forme du pipeline ne change que pour introduire un stade intermédiaire (geometry shaders) et la possibilité de rediriger la sortie d'un shader intermédiaire vers une zone mémoire.

Une nouveauté de taille est justement le geometry shader. Celui-ci s'intercale entre le vertex shader et le pixel shader et permet de modifier plusieurs sommets à la fois. En fait il peut lire plusieurs informations de sommets connexes et peut générer une nouvelle information qui peut être le même nombre de sommets ou des sommets en plus ou des sommets en moins. Les possibilités sur le papier peuvent être immenses : à partir d'un simple jeu de coordonnées et d'un programme vous pouvez générer des particules (similaires aux point sprites). à partir d'un mesh "normal" vous pouvez générer un shadow volume extrudé selon la direction de la lumière. À partir d'un mesh low poly vous pouvez subdiviser pour obtenir un mesh haut poly en fonction de la taille à l'écran et modifier le résultat avec une texture de déplacement. Bien entendu si on peut faire de nombreux plans avec ces specs, n'oublions pas que l'on attend encore le hardware qui implémentera ces features pour en voir les performances.

Le streaming output permet de sauvegarder le résultat d'un vertex ou geometry shader en mémoire sans passer par le pixel shader. Pour des raisons de performances et de gestion des accés concurrents, il n'est sans doute pas préférable d'autoriser les écritures aléatoires depuis n'importe où dans un shader (plusieurs shaders tournant en parallèle pouvant accéder à la même mémoire). Ces écritures "structurées" sont un compromis entre flexibilité et performance. On peut penser bien entendu à sauvegarder le résultat d'un calcul complexe en vue de le réutiliser plus tard dans un rendu qui nécessiterait de recalculer ces données plusieurs fois.

Les multiple render targets (MRT) sont étendues à la notion de rendertarget arrays. Autre nouveauté, le geometry shader peut rediriger les triangles vers une ou plusieurs "vues". Ce qui permet par exemple de rendre en une seule passe toutes les faces d'une cubemap utilisée pour les réflexions. Sous D3D9 il était nécessaire d'envoyer six fois la même géométrie, changer les états et la rendertarget cible six fois de suite avec une matrice différente. Le gain de temps est évident.

D3D10 introduit aussi la notion de "predication". C'est un mode de rendu conditionnel. Avec D3D9, si vous utilisiez une occlusion query pour savoir si votre objet est bien présent à l'écran ou pas, il fallait attendre le résultat de l'occlusion query qui arrivait au mieux à la fin de la frame ou une frame trop tard ce qui faisait que le rendu conditionnel est imprécis sous D3D9. Avec D3D10, vous pouvez envoyer des commandes dont l'éxecution ne sera effectu�e que si l'occlusion query précédente est passée. Bien entendu vous paierez toujours le coût de l'envoi de la commande jusqu'au moment où le résultat de la predication peut être utilisé. Vous pouvez bien sûr utiliser le résultat des queries précédentes pour savoir quel objet a plus de chance d'être caché (en cas de cohérence temporelle) et donc de maximiser le gain de la prédication.

Le langage des shaders a également évolué. Exit l'assembleur, tous les programmes seront écrits en HLSL (langages de shader de plus haut niveau). Le traitement sur les variables entières font leur apparition, mine de rien c'est un gros changement. Il faut savoir que jusqu'à présent il n'était pas possible de faire d'opérations bits à bits comme un simple AND ou un simple XOR. On a rajouté aussi l'accès indexé à des tableaux de constantes, l'accés indexé aux tableaux de textures (texture arrays).

L'instancing prend également une autre dimension avec D3D10. Sous D3D9, on pouvait rendre la même géométrie avec juste une propriété de transformation différente (pour les cartes qui supportent le shader model 3). Sous D3D10 on peut même utiliser des textures différentes pour les différentes instances via les texture arrays (cf plus haut). De même on peut tracer une géométrie sans connaitre à l'avance le nombre de ses sommets, ce qui est obligatoire si cette géométrie est issue d'un stream output (impossible de savoir à l'avance combien de sommets vont être écrits dans le buffer de destination).

On pourrait continuer la description mais je vais m'arrêter là. Pour ceux qui veulent se rendre compte des nouveautés. La documentation (temporaire) ainsi que des échantillons de code ( et des vidéos des échantillons pour ceux qui n'ont pas windows Vista) sont disponibles dans le dernier SDK (february 2006 sdk with D3d10 technology preview).

Nul doute que les jeux qui exploiteront toutes ces capacités (en supposant que le support hardware suive au niveau des perfs) seront bien intéressants visuellement.

Des images ?

Il n'y a pas vraiment d'images des nouveaux jeux D3D10 qui ne sont pas encore écrits, mais des vidéos que fournit Microsoft à ceux qui n'ont pas Vista (le hardware D3D10 y est émulé de manière software grâce au reference rasterizer).

Ce premier exemple génère du motion blur en extrudant les polygones en fonction de leur vélocité et les fond avec l'arrière plan avec l'alpha to coverage.

Ce deuxième exemple utilise l'instancing, en rajoutant la possibilité de varier la texture de l'herbe, des feuilles grâce aux arrays de texture indexables.

Ce dernier exemple utilise le geometry shader pour faire évoluer la plante avec une intervention minimale du CPU.

Voilà, c'est tout pour aujourd'hui.

 

Here comes a short article (compared to the months long articles about raytracing) about the design of a voxel terrain engine that I conducted back in the days of 1999 : the main inspiration was the game Outcast which looked gorgeous at that time when polygons only started to become interesting.

There have been some improvements since then, explanations on this page (in english): A voxel terrain engine design in C++

Voxel terrain engine design in C++

Le lien ci-dessus pointe vers un nouvel article, plutot court (comparé au marathon de plusieurs mois consacré au raytracing), consacré comme l'indique le titre au rendu de terrain par voxel. La principale source d'inspiration étant Outcast qui m'avait beaucoup impressionné par son rendu lorsque les polygones étaient encore balbutiants. Le design de ce moteur avait commencé en 1999. Vous pouvez lire l'article en français à l'adresse suivante : Design d'un moteur de terrain par voxel en C++.

 

J'ai mis en ligne une série d'articles sur le raytracing, avec l'implémentation d'un exemple de raytracer en C++. Il s'agit d'une compilation de posts écrits sur les forums de hardware.fr et qui ont grandement bénéficié d'une remise en forme.

Vous pouvez accéder à la première page (ainsi qu'aux suivantes) ici: Premier pas - raytracer en C++

Les notions abordées sont aussi diverses que : balayage et écriture d'un fichier TGA, intersection rayon/sphère, lecture d'un fichier config, éclairage de lambert, phong, blinn-phong, correction gamma et format sRGB, formule de Snell-Descartes, formule de Fresnel, réfraction, réflexion, bump mapping, surfaces isopotentielles (blobs), texture d'environnement cubique, adressage cubique, filtrage bilinéaire, bruit de Perlin, textures procédurales, roulette russe, importance sampling, suréchantillonnage (oversampling et supersampling), loi de Beer, aberration chromatique, format HDR et tone mapping, profondeur de champ (depth of field), kd-tree, illumination globale, photon tracing/photon mapping, caustiques et interréflexions et un raton laveur.

You can find on the link above an old article on the raytracing that I reformated for my main web page (it was originally published as a serie of posts on a discussion board). The articles and the source code comments are in french.

 

On part généralement du principe que les pixels sont carrés une fois représentés à l'écran. Ce qui est généralement le cas lorsque l'on est dans la résolution native de cet écran. En pratique on pourrait proposer des options un peu plus avancées que ce qui existe à l'heure actuelle parce qu'il est parfois délicat d'avoir une résolution native qui reste jouable. [...]

    Option 1 : Square pixels (default)
    Option 2 : Force ratio 4/3 (1024x768)
    Option 3 : Force ratio 5/4 (1280x1024)
    Option 4 : Force ratio 16/10 (1600x1050)
    Option 5 : Force ratio 16/9 (1600x720)

Les nombres entre parenthèse sont la résolution native équivalente.

Si par exemple le joueur sélectionne la résolution de jeu 1024x768 avec un force pixel ration de 5/4 (pour jouer sur son écran LCD), alors le jeu devra compenser en considérant que les pixel sont étirés verticalement. Cela peut se faire en jouant sur les matrices de transformation dans le pipeline graphique.

Le mode portrait (écran tourné à 90 degré par rapport au mode paysage) n'est pas très courant mais peut-être pris en compte également. Lorsque ces modes exotiques (portrait, 16/9) sont utilisés, se posent des questions douloureuses de gameplay; ou comment éviter que l'une ou l'autre catégorie de joueur se retrouve avantagée, handicapée par le nombre de choses affichées à l'écran. Mais c'est un autre problème.

 

(drop the hyphen! comme dirait D. Knuth)

Une question qui semble souvent bloquer sans raison les débutants en programmation est le tri sur plusieurs critères. [...]

En général la première des choses que l'on apprend en cours d'algorithmique est la fonction qsort. Et en général on trie des entiers n selon une simple fonction compare qui retourne qui de n1 ou n2 est le plus grand.

Maintenant là où ça se complique c'est si l'on introduit plusieurs critères de tri comme le nom, puis le prénom puis la date de naissance et le lieu de naissance. C'est facile à faire dans Excel mais comment faire pour le programmer ?

La chose essentielle à comprendre c'est que tri multicritère == tri à un seul critère. Et oui si l'on sait faire l'un on sait faire l'autre.

struct foo { int a; int b; int c; };

Imaginons qu'on ait un tableau de struct foo que l'on désire trier selon a puis selon b puis selon c sans devoir réécrire la fonction qsort, introduisons la fonction compare ci dessous :

int compare (void *foo1, void * foo2) {
  foo *truefoo1 = (foo *)foo1;
  foo *truefoo2 = (foo *)foo2;
  if (truefoo1->a - truefoo2->a != 0)
    return truefoo1->a - truefoo2->a;
  else if (truefoo1->b - truefoo2->b != 0)
    return truefoo1->b - truefoo2->b;
  else
    return truefoo1->c - truefoo2->c;
}

et ensuite il suffit d'appeler qsort sur notre tableau de valeur:

qsort(arrayoffoo, n, sizeof(foo), compare);

Et notre tableau sera trié.

Voilà : l'algorithme ne change pas, seule la fonction compare doit maintenant prendre en compte trois critères pour définir l'ordre des éléments entre eux.

Digression: une autre manière de trier des objets sur plusieurs critères a été mis à profit par les machines à trier des premiers recensements. Il s'agit de trier les objets dans des seaux (buckets) en plusieurs étapes et de maintenir l'ordre relatif des objets à l'intérieur d'un même seau. Si l'on trie selon prénom, alors on triera selon nom en faisant en sorte que pour un même nom l'ordre des prénoms soit maintenu. En bout de chaine on aura donc une liste triée correctement selon les deux critères. Si c'est particulièrement adapté au tri mécanique, cela a aussi donné naissance à un tri super rapide des entiers sur ordinateur : le tri radix.

 

I'm not sure what's the equivalent name for stereoscopy when you have actually three (or more) different views. Anyway this project was called stéréovision and I struggled because it was my first project in C++. (I guess nowaday I would find it sooo easy to redo it).

http://www.massal.net/article/stereovision.pdf (forgive the quality of the paper which is equally bad. I just keep it for curiosity purpose.)

Je ne suis pas sûr qu'il y ait un mot différent pour trois vues différentes (ou plus) mais ce papier s'appelle stéréovision tout de même. Bien évidemment comme c'était mon premier vrai projet en C++, je me suis battu en même temps pour apprendre le langage (introduction à la STL et tout ça). J'imagine que si je le faisais aujourd'hui je trouverai tout cela tellement facile.. Le papier est gardé comme objet de curiosité principalement même s'il y a quelques idées à glaner si vous débutez.


Copyright © Grégory Massal 1976-2005

Partner websites : LEGREG | GRAPHICS | GRAPHISME | PHOTOGRAPHY | OUT OF MY MIND | ANIMATION MENTOR | GREEN LIVING | VOXEL | RAY TRACING | DEALS