Intro Newton Protocol: ce qui peut tenir dans 4 kilo-octets

image

J'ai récemment participé à la scène de démonstration de Revision 2019 dans la catégorie d'introduction PC 4k, et mon intro a remporté la première place. J'ai fait du codage et des graphismes, et dixan a composé de la musique. La règle de base du concours est de créer un fichier exécutable ou un site Web qui ne fait que 4096 octets. Cela signifie que tout doit être généré à l'aide de mathématiques et d'algorithmes; Je ne peux en aucun cas compresser des images, des vidéos et du son dans une si petite quantité de mémoire. Dans cet article, je vais parler du pipeline de rendu de mon introduction à Newton. Ci-dessous, vous pouvez voir le résultat final, ou cliquez ici pour voir à quoi il ressemblait en direct sur Revision, ou allez sur pouet pour commenter et télécharger l'intro qui a participé au concours. Vous pouvez lire sur le travail et les corrections des concurrents ici .


La technique des champs de distance Ray marchant est très populaire dans la discipline d'introduction 4k car elle vous permet de spécifier des formes complexes en seulement quelques lignes de code. Cependant, l'inconvénient de cette approche est la vitesse d'exécution. Pour rendre la scène, vous devez trouver le point d'intersection des rayons avec la scène, déterminer d'abord ce que vous voyez, par exemple, un rayon de la caméra, puis les rayons suivants de l'objet vers les sources de lumière pour calculer l'éclairage. Lorsque vous travaillez avec la marche de rayons, ces intersections ne peuvent pas être trouvées en une seule étape, vous devez faire de nombreuses petites étapes le long du faisceau et évaluer tous les objets à chaque point. En revanche, lorsque vous utilisez le lancer de rayons, vous pouvez trouver l'intersection exacte en vérifiant chaque objet une seule fois, mais l'ensemble de formes qui peut être utilisé est très limité: vous devez avoir une formule pour chaque type pour calculer l'intersection avec le rayon.

Dans cette intro, je voulais simuler un éclairage très précis. Comme il était nécessaire de réfléchir des millions de rayons dans la scène, le traçage des rayons semblait un choix logique pour obtenir cet effet. Je me suis limité à une seule figure - une sphère, car l'intersection d'un rayon et d'une sphère est calculée tout simplement. Même les murs de l'intro sont en fait de très grandes sphères. De plus, cela a simplifié la simulation de la physique; il suffisait de ne prendre en compte que les conflits entre les sphères.

Pour illustrer la quantité de code qui tient dans 4096 octets, j'ai présenté ci-dessous le code source complet de l'intro terminée. Toutes les parties, à l'exception du code HTML à la fin, sont codées sous forme d'image PNG pour les compresser à une taille plus petite. Sans cette compression, le code aurait pris près de 8900 octets. La partie appelée Synth est une version allégée de SoundBox . Pour empaqueter le code dans ce format minimisé, j'ai utilisé le compilateur Google Closure et le Shader Minifier . Au final, presque tout est compressé en PNG à l'aide de JsExe . Le pipeline de compilation complet peut être vu dans le code source de ma précédente intro 4k Core Critical , car il correspond complètement à celui présenté ici.


La musique et le synthétiseur sont entièrement implémentés en Javascript. La partie sur WebGL est divisée en deux parties (surlignées en vert dans le code); elle configure le pipeline de rendu. Les éléments physiques et les traceurs de rayons sont des shaders GLSL. Le reste du code est codé dans une image PNG, et HTML est ajouté à la fin de l'image résultante inchangé. Le navigateur ignore les données d'image et exécute uniquement le code HTML, qui à son tour décode PNG en javascript et l'exécute.

Pipeline de rendu


L'image ci-dessous montre le pipeline de rendu. Il se compose de deux parties. La première partie du pipeline est un simulateur physique. La scène d'introduction contient 50 sphères entrant en collision les unes avec les autres à l'intérieur de la pièce. La salle elle-même est composée de six sphères, dont certaines sont plus petites que d'autres pour créer des murs plus incurvés. Deux sources de lumière verticales dans les coins sont également des sphères, c'est-à-dire qu'il y a 58 sphères dans la scène. La deuxième partie du pipeline est le traceur de rayons, qui rend la scène. Le diagramme ci-dessous montre le rendu d'une image au temps t. La simulation physique prend la trame précédente (t-1) et simule l'état actuel. Le traceur de rayons prend les positions actuelles et les positions de l'image précédente (pour le canal de vitesse) et rend la scène. Le post-traitement combine ensuite les 5 images précédentes et l'image actuelle pour réduire la distorsion et le bruit, puis crée un résultat fini.


Rendu d'une image à l'instant t.

La partie physique est assez simple, sur Internet vous pouvez trouver de nombreux tutoriels sur la création de simulations primitives pour les sphères. La position, le rayon, la vitesse et la masse sont stockés dans deux textures avec une résolution de 1 x 58. J'ai utilisé la fonctionnalité Webgl 2, qui permet le rendu sur plusieurs cibles de rendu, de sorte que les données de deux textures sont enregistrées simultanément. La même fonctionnalité est utilisée par le traceur de rayons pour créer trois textures. Webgl ne fournit aucun accès aux API de traçage de rayons NVidia RTX ou DirectX Raytracing (DXR), donc tout est fait à partir de zéro.

Ray tracer


Le lancer de rayons lui-même est une technique assez primitive. Nous libérons un rayon dans la scène, il est réfléchi 4 fois, et s'il pénètre dans la source lumineuse, la couleur des reflets s'accumule; sinon, on devient noir. En 4096 octets (qui incluent la musique, le synthétiseur, la physique et le rendu), il n'y a pas de place pour créer des structures complexes de traçage de rayons accélérateurs. Par conséquent, nous utilisons la méthode de la force brute, c'est-à-dire que nous vérifions les 57 sphères (la paroi avant est exclue) pour chaque rayon, sans faire aucune optimisation pour exclure une partie des sphères. Cela signifie que pour fournir 60 images par seconde en résolution 1080p, vous ne pouvez émettre que 2 à 6 rayons ou échantillons par pixel. C'est assez proche pour créer un éclairage doux.


1 échantillon par pixel.


6 échantillons par pixel.

Comment y faire face? Au début, j'ai étudié l'algorithme de lancer de rayons, mais il était déjà simplifié au point. J'ai réussi à augmenter légèrement les performances en éliminant les cas où le rayon commence à l'intérieur de la sphère, car de tels cas ne sont applicables qu'en présence d'effets de transparence, et seuls des objets opaques étaient présents dans notre scène. Après cela, j'ai combiné chaque condition if dans une instruction distincte pour éviter les branchements inutiles: malgré les calculs «redondants», cette approche est toujours plus rapide qu'un tas d'instructions conditionnelles. Il a également été possible d'améliorer le modèle d'échantillonnage: au lieu d'émettre des rayons au hasard, nous pourrions les répartir sur la scène selon un modèle plus uniforme. Malheureusement, cela n'a pas aidé et a conduit à des artefacts ondulés dans chaque algorithme que j'ai essayé. Cependant, cette approche a donné de bons résultats pour les images fixes. En conséquence, j'ai recommencé à utiliser une distribution complètement aléatoire.

Les pixels voisins devraient avoir un éclairage très similaire, alors pourquoi ne pas les utiliser pour calculer l'éclairage d'un seul pixel? Nous ne voulons pas rendre les textures floues, seulement l'éclairage, nous devons donc les rendre dans des canaux séparés. Nous ne voulons pas non plus flouter les objets, nous devons donc prendre en compte les identifiants des objets afin de savoir quels pixels peuvent être facilement flous. Puisque nous avons des objets réfléchissant la lumière et que nous avons besoin de reflets clairs, il ne suffit pas simplement de trouver l'ID du premier objet avec lequel le faisceau entre en collision. J'ai utilisé un cas spécial pour les matériaux réfléchissants purs pour inclure également les identifiants des premier et deuxième objets visibles dans les réflexions dans le canal identificateur d'objet. Dans ce cas, le flou peut atténuer l'éclairage des objets dans les réflexions, tout en conservant les limites des objets.


Canal de texture, nous n'avons pas besoin de le flouter.


Ici, dans le canal rouge contient l'ID du premier objet, en vert - le deuxième, et en bleu - le troisième. En pratique, ils sont tous codés en une seule valeur flottante, dans laquelle la partie entière stocke les identifiants des objets, et la fraction indique la rugosité: 332211.RR.

Puisqu'il y a des objets avec différentes rugosités dans la scène (certaines zones sont rugueuses, la lumière est diffusée sur d'autres, dans la troisième il y a un reflet miroir), je stocke la rugosité pour contrôler le rayon de flou. Il n'y a pas de petits détails dans la scène, j'ai donc utilisé un grand noyau 50 x 50 avec les poids sous forme de carrés inversés pour le flou. Il ne tient pas compte de l'espace mondial (cela pourrait être réalisé afin d'obtenir des résultats plus précis), car sur des surfaces situées à un angle dans certaines directions, il érode une zone plus grande. Un tel flou crée une image assez lisse, mais les artefacts sont clairement visibles, en particulier en mouvement.


Canal d'éclairage avec flou et artefacts encore visibles. Sur cette image, des points flous sur le mur arrière sont visibles, causés par un petit bug avec les identifiants du deuxième objet réfléchi (les rayons quittent la scène). Sur l'image finie, ce n'est pas très visible, car des réflexions claires sont prises à partir du canal de texture. Les sources d'éclairage deviennent également floues, mais j'ai aimé cet effet et je l'ai laissé. Si vous le souhaitez, cela peut être évité en modifiant les identifiants des objets en fonction du matériau.

Lorsque des objets se trouvent dans la scène et que l'appareil photo prend la scène en mouvement lentement, l'éclairage de chaque image doit rester constant. Par conséquent, nous pouvons effectuer un flou non seulement dans les coordonnées XY de l'écran; on peut s'estomper avec le temps. Si nous supposons que l'éclairage ne change pas trop en 100 ms, nous pouvons le calculer en moyenne pour 6 images. Mais pendant cette fenêtre temporelle, les objets et la caméra vont encore parcourir une certaine distance, donc un simple calcul de la moyenne pour 6 images créera une image très floue. Cependant, nous savons où tous les objets et la caméra se trouvaient sur la carte précédente, nous pouvons donc calculer les vecteurs de vitesse dans l'espace d'écran. C'est ce qu'on appelle la reprojection temporaire. Si j'ai un pixel au temps t, je peux prendre la vitesse de ce pixel et calculer où il était au temps t-1, puis calculer où le pixel au temps t-1 est au temps t-2, etc. 5 images en arrière. Contrairement au flou dans l'espace de l'écran, j'ai utilisé le même poids pour chaque image, c'est-à-dire juste la moyenne de la couleur entre toutes les images pour un "flou" temporaire.


Un canal de vitesse de pixel indiquant où le pixel était dans la dernière image en fonction du mouvement de l'objet et de la caméra.


Pour éviter le flou commun des objets, nous utiliserons à nouveau le canal des identifiants d'objets. Dans ce cas, nous considérons uniquement le premier objet avec lequel le faisceau est entré en collision. Cela fournit un anti-aliasing au sein de l'objet, c'est-à-dire en reflets.

Bien sûr, le pixel n'était peut-être pas visible dans l'image précédente; il pourrait être caché par un autre objet ou être hors du champ de vision de la caméra. Dans de tels cas, nous ne pouvons pas utiliser les informations précédentes. Cette vérification est effectuée séparément pour chaque image, nous obtenons donc de 1 à 6 échantillons ou images par pixel et utilisons ceux qui sont possibles. La figure ci-dessous montre que pour les objets lents, ce n'est pas un problème très grave.


Lorsque des objets se déplacent et ouvrent de nouvelles parties de la scène, nous n'avons pas 6 cadres d'informations pour faire la moyenne de ces parties. Cette image montre les zones qui ont 6 cadres (blanc), ainsi que celles qui en manquent (nuances progressivement assombries). L'apparition des contours est causée par la randomisation des emplacements d'échantillonnage pour le pixel dans chaque image et le fait que nous prenons l'identifiant de l'objet du premier échantillon.


L'éclairage flou est calculé en moyenne sur six images. Les artefacts sont presque invisibles et le résultat est stable dans le temps, car dans chaque image, une seule image sur six change dans laquelle l'éclairage est pris en compte.

En combinant tout cela, nous obtenons une image finie. L'éclairage est flou sur les pixels voisins, tandis que les textures et les reflets restent clairs. Ensuite, tout cela est en moyenne entre six images pour créer une image encore plus lisse et plus stable dans le temps.


L'image finie.

Les artefacts d'atténuation sont toujours visibles, car j'ai fait la moyenne de plusieurs échantillons par pixel, bien que j'ai pris le canal de l'identifiant de l'objet et la vitesse pour la première intersection. Vous pouvez essayer de résoudre ce problème et d'obtenir un lissage dans les réflexions en éliminant les échantillons s'ils ne coïncident pas avec le premier, ou du moins si la première collision ne coïncide pas dans l'ordre. En pratique, les traces sont presque invisibles, donc je n'ai pas pris la peine de les éliminer. Les limites des objets sont également déformées, car les canaux de vitesse et les identificateurs d'objet ne peuvent pas être lissés. J'envisageais la possibilité de rendre l'image entière à 2160p avec une réduction supplémentaire de l'échelle à 1080p, mais mon NVidia GTX 980ti n'est pas capable de traiter de telles résolutions à 60fps, j'ai donc décidé d'abandonner cette idée.

En général, je suis très content du résultat de l'intro. J'ai réussi à y insérer tout ce que j'avais à l'esprit, et malgré des bugs mineurs, le résultat final était de très haute qualité. À l'avenir, vous pouvez essayer de corriger les bogues et d'améliorer l'anti-aliasing. Il convient également d'expérimenter des fonctionnalités telles que la transparence, le flou de mouvement, diverses formes et les transformations d'objets.

Source: https://habr.com/ru/post/fr450612/


All Articles