Textures for 64k intro: comment c'est fait aujourd'hui

Cet article fait partie de notre série H - Immersion . La première partie peut être lue ici: Immersion in Immersion .

Lors de la création d'une animation de seulement 64 Ko, il est difficile d'utiliser des images prêtes à l'emploi. Nous ne pouvons pas les stocker de manière traditionnelle, car il n'est pas suffisamment efficace, même si vous appliquez une compression, par exemple JPEG. Une solution alternative est la génération procédurale, c'est-à-dire l'écriture de code qui décrit la création d'images pendant l'exécution du programme. Notre implémentation de cette solution était un générateur de texture - un élément fondamental de notre chaîne d'outils. Dans cet article, nous expliquerons comment nous l'avons développé et utilisé dans H-Immersion .


Des projecteurs sous-marins illuminent les détails des fonds marins.

Première version


La génération de texture a été l'un des tout premiers éléments de notre base de code: des textures procédurales étaient déjà utilisées dans notre première intro B-Incubation . Le code se composait d'un ensemble de fonctions qui remplissent, filtrent, transforment et combinent des textures, ainsi qu'une grande boucle qui contourne toutes les textures. Ces fonctions ont été écrites en C ++ pur, mais l'interaction de l'API C a été ajoutée par la suite afin qu'elles puissent être évaluées par l'interpréteur C PicoC . À cette époque, nous avons utilisé PicoC pour réduire le temps pris par chaque itération: de cette façon, nous avons pu changer et recharger les textures pendant l'exécution du programme. Le passage au sous-ensemble C était un petit sacrifice par rapport au fait que maintenant nous pouvions changer le code et voir le résultat immédiatement, sans se soucier de fermer, recompiler et recharger la démo entière.


En utilisant un motif simple, un peu de bruit et de déformation, nous pouvons obtenir une texture de bois stylisée.


Dans cette scène de l'atelier de F - Felix, différentes textures de bois ont été utilisées.

Pendant un certain temps, nous avons exploré les capacités de ce générateur et l'avons donc publié sur un serveur Web avec un petit script PHP et une interface Web simple. Nous pourrions écrire le code de texture dans un champ de texte, et le script l'a transmis au générateur, qui a ensuite vidé le résultat sous forme de fichier PNG à afficher sur la page. Très vite, nous avons commencé à dessiner au travail pendant la pause déjeuner et à partager nos petits chefs-d'œuvre avec les autres membres du groupe. Cette interaction nous a motivés au processus créatif.


Galerie Web de notre ancien générateur de texture. Toutes les textures peuvent être modifiées dans le navigateur.

Refonte complète


Pendant longtemps, le générateur de texture est resté presque inchangé; nous avons pensé que c'était bon, et notre efficacité a cessé d'augmenter. Mais une fois que nous avons découvert qu'il y avait beaucoup d' artistes sur les forums Internet démontrant leurs textures entièrement générées de manière procédurale, ainsi que l'organisation de défis sur divers sujets. Le contenu procédural était autrefois une caractéristique de la scène de démonstration, mais Allegorithmic , ShaderToy et des outils similaires l'ont rendu accessible au grand public. Nous n'y avons pas prêté attention et ils ont commencé à nous mettre facilement sur les omoplates. Inacceptable!


Canapé en tissu . Une texture de tissu entièrement procédurale créée dans Substance Designer. Publié par: Imanol Delgado. www.artstation.com/imanoldelgado

image

Sol forestier . Texture de sol forestier entièrement procédurale créée par Substance Designer. Publié par Daniel Thiger. www.artstation.com/dete

Nous avons depuis longtemps besoin de repenser nos outils. Heureusement, de nombreuses années de travail avec le même générateur de texture nous ont permis de reconnaître ses défauts. De plus, notre générateur de maillage naissant nous a également dit à quoi devrait ressembler le pipeline de contenu procédural.

L'erreur architecturale la plus importante a été l'implémentation de la génération comme un ensemble d'opérations avec des objets de texture. Du point de vue d'une perspective de haut niveau, cela peut être la bonne approche, mais du point de vue de la mise en œuvre, des fonctions telles que texture.DoSomething () ou Combine (textureA, textureB) présentent de sérieux inconvénients.

Tout d'abord, le style POO vous oblige à déclarer ces fonctions dans le cadre de l'API, peu importe leur simplicité. Il s'agit d'un problème grave car il n'évolue pas bien et, plus important encore, crée des frictions inutiles dans le processus créatif. Nous ne voulions pas changer l'API chaque fois que nous devions essayer quelque chose de nouveau. Cela complique l'expérimentation et limite la liberté de création.

Deuxièmement, en termes de performances, cette approche vous oblige à traiter les données de texture en cycles autant de fois qu'il y a d'opérations. Cela ne serait pas particulièrement important si ces opérations étaient coûteuses par rapport au coût d'accès à de gros fragments de mémoire, mais ce n'est généralement pas le cas. À l'exception d'une très petite fraction des opérations, par exemple la génération de bruit Perlin ou le remplissage , elles sont fondamentalement très simples et ne nécessitent que quelques instructions sur le point de texture. Autrement dit, nous avons contourné les données de texture pour effectuer des opérations triviales, ce qui est extrêmement inefficace du point de vue de la mise en cache.

La nouvelle structure résout ces problèmes grâce à la réorganisation de la logique. Dans la pratique, la plupart des fonctions effectuent indépendamment la même opération pour chaque élément de texture. Par conséquent, au lieu d'écrire une fonction texture.DoSomething () qui contourne tous les éléments, nous pouvons écrire texture.ApplyFunction (f) , où f (element) ne fonctionne que pour un seul élément de texture. Ensuite, f (élément) peut être écrit selon une texture spécifique.

Cela semble être un changement mineur. Cependant, cette structure simplifie l'API, rend le code de génération plus flexible et expressif, plus convivial pour le cache et permet un traitement parallèle en toute simplicité. De nombreux lecteurs ont déjà réalisé qu'il s'agissait essentiellement d'un shader. Cependant, l'implémentation réelle reste le code C ++ exécuté sur le processeur. Nous conservons toujours la possibilité d'effectuer des opérations en dehors de la boucle, mais n'utilisons cette option qu'en cas de besoin, par exemple par convolution.

C'était:


//     . // API . //    -  API. //      . class ProceduralTexture { void DoSomething(parameters) { for (int i = 0; i < size; ++i) { //   . (*this)[i] = … } } void PerlinNoise(parameters) { … } void Voronoi(parameters) { … } void Filter(parameters) { … } void GenerateNormalMap() { … } }; void GenerateSomeTexture(texture t) { t.PerlinNoise(someParameter); t.Filter(someOtherParameter); … //  .. t.GenerateNormalMap(); } 

C'est devenu:


 //       . // API . //     . //      . class ProceduralTexture { void ApplyFunction(functionPointer f) { for (int i = 0; i < size; ++i) { //    . (*this)[i] = f((*this)[i]); } } }; void GenerateNormalMap(ProceduralTexture t) { … } void SomeTextureGenerationPass(void* out, PixelInfo in) { result = PerlinNoise(in); result = Filter(result); … //  .. *out = result; } void GenerateSomeTexture(texture t) { t.ApplyFunction(SomeTextureGenerationPass); GenerateNormalMap(t); } 

Parallélisation


La génération de texture prend du temps, et un candidat évident pour réduire ce temps est l'exécution de code parallèle. À tout le moins, vous pouvez apprendre à générer plusieurs textures à la fois. C'est exactement ce que nous avons fait pour l'atelier F - Felix , ce qui a considérablement réduit le temps de chargement.

Cependant, cela ne fait pas gagner de temps là où il est le plus nécessaire. Il faut encore beaucoup de temps pour générer une texture. Cela s'applique au changement car nous continuons de recharger la texture encore et encore avant chaque modification. Au lieu de cela, il est préférable de paralléliser le code de génération de texture interne. Puisque maintenant le code consiste essentiellement en une grande fonction appliquée en boucle à chaque texel, la parallélisation devient simple et efficace. Réduit le coût des expériences, des réglages et des brouillons, ce qui affecte directement le processus créatif.




Illustration d'une idée que nous avons explorée et rejetée pour H - Immersion : une décoration en mosaïque avec une doublure en orichalcon. Ici, il est montré dans notre outil d'édition interactif.

Génération côté GPU


Si ce n'est toujours pas évident, alors je dirai que la génération de texture est complètement effectuée dans le CPU. Peut-être que certains d'entre vous lisent ces lignes maintenant et sont perplexes "mais pourquoi?!". Il semble que l'étape évidente soit la génération de texture dans le processeur vidéo. Pour commencer, il augmentera le taux de génération d'un ordre de grandeur. Alors pourquoi ne l'utilisons-nous pas?

La raison principale est que l'objectif de notre petite refonte était de rester sur le CPU. Passer à un GPU signifierait beaucoup plus de travail. Il nous faudrait résoudre des problèmes supplémentaires pour lesquels nous n'avons pas encore suffisamment d'expérience. En travaillant avec le CPU, nous avons une compréhension claire de ce que nous voulons et nous savons comment corriger les erreurs précédentes.

Cependant, la bonne nouvelle est que, grâce à la nouvelle structure, expérimenter avec le GPU semble maintenant assez trivial. Tester des combinaisons des deux types de processeurs sera une expérience intéressante pour l'avenir.

Génération de texture et ombrage physiquement précis


Une autre limitation de l'ancien design était que la texture n'était considérée que comme une image RVB. Si nous avions besoin de générer plus d'informations, disons la texture diffuse et la texture des normales pour la même surface, alors rien ne nous a empêché de le faire, mais l'API n'a pas beaucoup aidé. Cela est devenu particulièrement important dans le contexte de l'ombrage à base physique (PBR).

Dans un pipeline traditionnel sans PBR, des textures de couleur sont généralement utilisées, dans lesquelles de nombreuses informations sont cuites. De telles textures représentent souvent l'aspect final de la surface: elles ont déjà un certain volume, les fissures sont assombries et il peut même y avoir des reflets. Si plusieurs textures sont utilisées en même temps, les détails à grande et à petite échelle sont généralement combinés pour ajouter des cartes normales ou une réflectivité de surface.

Les convoyeurs PBR de surface utilisent généralement plusieurs ensembles de textures représentant des valeurs physiques plutôt que le résultat artistique souhaité. La texture de couleur diffuse, qui se rapproche le plus de ce que l'on appelle souvent la «couleur» de la surface, est généralement plate et sans intérêt. La couleur spéculaire est déterminée par l'indice de réfraction de la surface. La plupart des détails et de la variabilité proviennent des textures des normales et de la rugosité (rugosité) (que quelqu'un peut considérer comme identiques, mais avec deux échelles différentes). La réflectivité perçue d'une surface devient une conséquence de son niveau de rugosité. A ce stade, il sera plus logique de penser non pas en termes de matériaux, mais de matériaux.










La nouvelle structure nous permet de déclarer des formats de pixels arbitraires pour les textures. L'ayant fait partie de l'API, nous lui permettons de traiter tout le code standard. Après avoir déclaré le format de pixel, nous pouvons nous concentrer sur le code de la création sans dépenser trop d'efforts pour traiter ces données. Au moment de l'exécution, il générera plusieurs textures et les transférera de manière transparente vers le GPU.

Dans certains pipelines PBR, les couleurs diffuses et spéculaires ne sont pas transmises directement. Au lieu de cela, les paramètres «couleur de base» et «métal» sont utilisés, ce qui a ses avantages et ses inconvénients. Dans H - Immersion, nous utilisons le modèle diffus + spéculaire, et le matériau se compose généralement de cinq couches:

  1. Couleur diffuse (RVB; 0: Vantablack ; 1: neige fraîche ).
  2. Couleur spéculaire (RVB: fraction de lumière réfléchie à 90 °, également appelée F0 ou R0 ).
  3. Rugosité (A; 0: parfaitement lisse; 1: caoutchouteux).
  4. Normal (XYZ; vecteur unitaire).
  5. Élévation du terrain (A; utilisé pour la cartographie d'occlusion de parallaxe).

Lors de l'utilisation, les informations d'émission lumineuse ont été ajoutées directement au shader. Nous n'avons pas jugé nécessaire d'avoir une occlusion ambiante, car dans la plupart des scènes, il n'y a aucun éclairage ambiant. Cependant, je ne serai pas surpris que nous ayons des couches supplémentaires ou d'autres types d'informations, par exemple l'anisotropie ou l'opacité.



Les images ci-dessus montrent une expérience récente de génération d'occlusion ambiante locale basée sur l'altitude. Pour chaque direction, nous parcourons une distance prédéterminée et maintenons la plus grande pente (différence de hauteur divisée par la distance). Ensuite, nous calculons l'occlusion à partir de la pente moyenne.

Contraintes et travaux futurs


Comme vous pouvez le voir, la nouvelle structure est devenue une amélioration majeure par rapport à l'ancienne. De plus, elle encourage l'expression créative. Cependant, elle a encore des limites que nous voulons éliminer à l'avenir.

Par exemple, bien qu'il n'y ait eu aucun problème dans cette introduction, nous avons remarqué que l'allocation de mémoire pouvait être un obstacle. Lors de la génération de textures, un tableau de valeurs flottantes est utilisé. Avec de grandes textures avec plusieurs couches, vous pouvez rapidement rencontrer un problème d'allocation de mémoire. Il existe différentes façons de le résoudre, mais ils ont tous leurs inconvénients. Par exemple, nous pouvons générer des textures tuile par tuile, tandis que l'évolutivité sera meilleure, cependant, la mise en œuvre de certaines opérations, telles que la convolution, devient moins évidente.

De plus, dans cet article, malgré le mot «matériaux» utilisé, nous ne parlions que de textures, mais pas de shaders. Cependant, l'utilisation de matériaux devrait également conduire à des shaders. Cette contradiction reflète les limites de la structure existante: la génération de texture et l'ombrage sont deux parties distinctes séparées par un pont. Nous avons essayé de faciliter la traversée de ce pont, mais en fait, nous voulons que ces pièces ne fassent plus qu'un. Par exemple, si un matériau a à la fois des paramètres statiques et dynamiques, nous voulons les décrire en un seul endroit. C'est un sujet complexe et nous ne savons pas encore s'il existe une bonne solution, mais n'allons pas de l'avant.

image

Une expérience pour créer une texture de tissu similaire au travail d'Imadol Delgado montré ci-dessus.

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


All Articles