Doom de SceneKit. Expérience Yandex avec les graphiques 3D dans iOS

- Je suis trop jeune pour mourir.


SceneKit est un cadre graphique 3D de haut niveau dans iOS qui permet de créer des scènes et des effets animés. Il comprend un moteur physique, un générateur de particules et un ensemble d'actions simples pour les objets 3D qui vous permettent de décrire la scène en termes de contenu - géométrie, matériaux, éclairage, caméras - et de l'animer à travers une description des changements pour ces objets.



Aujourd'hui, nous allons regarder SceneKit avec un regard attentif et légèrement sévère, mais d'abord, passons en revue les bases et voyons à quoi ressemble la scène 3D et ce qui doit être fait pour la créer.


La scène la plus simple de trois nœuds avec une géométrie en eux.
La scène la plus simple de trois nœuds avec une géométrie en eux


Vous devez d'abord créer la structure de base de la scène, qui se compose de nœuds ou de nœuds de la scène. Chaque nœud peut contenir à la fois la géométrie et d'autres nœuds. La géométrie peut être simple, comme une boule, un cube ou une pyramide, ou plus complexe, créée dans des éditeurs externes.


Matériaux de superposition
Matériaux de superposition


Ensuite, pour cette géométrie, vous devez spécifier les matériaux qui détermineront la représentation de base des objets. Chaque matériau définit lui-même son propre modèle d'éclairage et, selon lui, utilise un ensemble de propriétés différent . Chacune de ces propriétés est généralement une couleur ou une texture, mais en plus de ces options couramment utilisées, il existe également l'option d'utiliser CALayer , AVPlayer et SKScene .


Ajouter des sources d'éclairage
Ajouter des sources d'éclairage


Après cela, il est nécessaire d'ajouter des sources de lumière qui déterminent la visibilité des objets dans l'une ou l'autre partie de la scène. Ils, par analogie avec la géométrie, doivent se trouver à l'intérieur d'un nœud. SceneKit prend en charge de nombreux types d'éclairage différents , ainsi que plusieurs types d'ombres .


Effet Boke prêt à l'emploi
Effet Boke prêt à l'emploi


Ensuite, vous devez créer une caméra (et la placer dans un nœud séparé) et définir les paramètres de base pour elle. Il y en a beaucoup, mais avec l'aide d'eux, vous pouvez créer des effets sympas. Hors de la boîte, le bokeh (ou flou), le HDR avec l'adaptation, l'éclat, le SSAO et les modifications de teinte / saturation sont pris en charge.


Animations simples dans SceneKit
Animations simples dans SceneKit


Enfin, SceneKit comprend un ensemble simple d'actions pour les objets 3D qui vous permettent de définir des changements de scène au fil du temps. SceneKit prend également en charge les actions décrites en JavaScript , mais il s'agit d'un sujet pour un article séparé.


L'interaction d'un générateur de particules avec un moteur physique peut conduire à des tornades!
L'interaction d'un générateur de particules avec un moteur physique peut conduire à une tornade!


En plus des graphiques, les principales caractéristiques de SceneKit sont le générateur de particules et un moteur physique avancé qui vous permet de définir des propriétés physiques réelles pour les objets ordinaires et les particules du générateur.


Un grand nombre de tutoriels détaillés ont été écrits sur toutes ces fonctionnalités. Mais dans le processus de développement, nous n'avons pratiquement pas utilisé ces opportunités ...


Hé, pas trop rude


Une fois, j'ai écrit un modèle d'éclairage pour les jeux 3D mieux que la vraie lumière du soleil, donnant un FPS acceptable sur le Nvidia 8800, mais j'ai décidé de ne pas libérer le moteur, car Dieu est gentil avec moi et je ne veux pas montrer son incompétence en la matière.
- John Carmack

Nous allons commencer une étude détaillée avec une tâche assez simple qui se pose pour presque tous ceux qui travaillent très sérieusement avec SceneKit: comment charger un modèle avec une géométrie complexe et des matériaux connectés, un éclairage et même des animations?


Il y a plusieurs façons, et elles ont toutes leurs avantages et leurs inconvénients:


  1. SCNScene (nommé :) - obtient une scène d'un bundle,


  2. SCNScene (url: options :) - charge la scène par URL,


  3. SCNScene (mdlAsset :) - convertit une scène de différents formats,


  4. SCNReferenceNode (url :) - charge paresseusement la scène.



Obtenez la scène du bundle


Vous pouvez utiliser la méthode standard : mettre notre modèle au format dae ou scn dans le bundle scnassets et le charger à partir de là par analogie avec UIImage (nommé :).


Mais que faire si vous souhaitez contrôler vous-même la mise à jour des modèles sans publier de mise à jour dans l'App Store à chaque fois que vous devez modifier quelques textures? Ou supposez que vous devez prendre en charge les cartes et modèles créés par l'utilisateur. Ou - que vous ne voulez tout simplement pas augmenter la taille de l'application, car les graphiques 3D ne sont pas la fonctionnalité principale.


Chargement de la scène par URL


Vous pouvez utiliser le constructeur de scène à partir de l'URL du fichier scn. Cette méthode prend en charge le téléchargement non seulement à partir du système de fichiers, mais également à partir du réseau, mais dans ce dernier cas, vous pouvez oublier la compression. De plus, vous devez convertir le modèle au format scn à l'avance. Vous pouvez bien sûr utiliser dae, mais il s'accompagne d'un ensemble de restrictions. Par exemple, le manque de rendu physique.


Le principal avantage de cette méthode est qu'elle vous permet de configurer de manière flexible les paramètres d'importation . Vous pouvez, par exemple, modifier le cycle de vie des animations et les faire se répéter à l'infini. Vous pouvez spécifier explicitement la source de chargement des ressources externes telles que les textures, vous pouvez convertir l'orientation et l'échelle de la scène, créer des normales manquantes pour la géométrie, fusionner toute la géométrie de la scène en un seul grand nœud ou supprimer tous les éléments de la scène qui ne sont pas conformes à la norme de format.


Convertir une scène à partir de différents formats


La troisième option consiste à utiliser le constructeur avec MDLAsset . Autrement dit, nous créons d' abord un MDLAsset , disponible dans le framework ModelIO, puis le transmettons au constructeur de la scène.


Cette option est bonne car elle vous permet de télécharger de nombreux formats différents. Officiellement, MDLAsset peut charger les formats obj, ply, stl et usd, mais après avoir épuisé une liste de tous les formats possibles, au moins liés à l'infographie, j'en ai trouvé quatre autres: abc, bsp, vox et md3, mais ils peuvent ne pas être entièrement pris en charge ou non dans tous les systèmes, et pour eux, vous devez vérifier l'exactitude de l'importation.


Il est également nécessaire de considérer que cette méthode a une surcharge pour la conversion et de l'utiliser très soigneusement.


Ces méthodes ont un écueil commun: elles renvoient SCNScene, pas SCNNode. La seule façon d'ajouter du contenu à une scène existante est de copier tous les nœuds enfants et - vous pouvez facilement ignorer cette étape - les animations du nœud racine (par exemple, elles peuvent y apparaître lorsque vous travaillez avec dae). De plus, vous devez considérer que dans la scène, il ne peut y avoir qu'un seul environnement de texture (si vous n'utilisez pas de shaders personnalisés pour les réflexions).


Chargement paresseux de la scène


La quatrième option consiste à utiliser SCNReferenceNode . Il ne renvoie pas une scène, mais un nœud, qui lui-même peut paresseusement (ou sur demande) charger en lui-même toute la hiérarchie de la scène. Ainsi, cette méthode est similaire à la première, mais elle cache en elle tous les problèmes de copie.


Il a une chose mais: les paramètres globaux de la scène sont perdus.


Il s'avère que c'est le moyen le plus simple et le plus rapide de télécharger votre modèle, mais si vous avez besoin d'un réglage de fichier, la première méthode sera meilleure.


En conséquence, nous avons opté pour la première option, car il était plus pratique pour nous de travailler au format scn et pour les concepteurs - de convertir du format dae en celui-ci. De plus, nous avions besoin d'animations de réglage des fichiers au démarrage.


Pas du tout d'optimisations prématurées


Ayant bricolé ce processus depuis longtemps, je peux vous donner quelques conseils.


L'astuce la plus importante est de convertir les fichiers en scn à l'avance. Ensuite, vous pouvez, en ouvrant le fichier dans l'éditeur de scène intégré dans Xcode, voir à quoi ressemblera votre objet dans SceneKit.


De plus, en fait, le fichier scn n'est qu'une représentation binaire de la scène, donc son chargement prendra le moins de temps. Pour le même dae, vous devez d'abord analyser le xml, puis convertir tous les maillages, animations et matériaux. De plus, la conversion des animations et des matériaux est une source potentielle de problèmes. Nous rappelons le manque de support PBR dans dae: il s'avère que si vous voulez l'utiliser, vous devrez changer le type de tous les matériaux après la conversion et déposer manuellement les textures appropriées.


Avec cette opération, vous pouvez obtenir un effet secondaire très utile: une compression de texture importante. Il suffit de les ouvrir dans la "Vue" et de les exporter, en changeant le format en heic. En moyenne, cette opération simple a permis d'économiser 5 mégaoctets par modèle.


De plus, si vous téléchargez une scène depuis Internet, je peux vous conseiller de la télécharger dans l'archive, de la décompresser et de transférer l'URL du fichier scn décompressé. Cela vous fera économiser, à vous et à l'utilisateur, des mégaoctets supplémentaires - ce qui, à son tour, accélérera le téléchargement et réduira également le nombre de points de défaillance. D'accord: faire une demande distincte pour chaque ressource externe, et même sur Internet mobile n'est pas le meilleur moyen d'augmenter la fiabilité.


Me fait beaucoup de mal


Quand je conduis une voiture, j'entends souvent le disque dur de l'univers grésiller, chargeant la prochaine rue.
- John Carmack

Ainsi, lorsque le travail de chargement et d'importation de modèles est mis en production, une nouvelle tâche se pose: ajouter divers effets et fonctionnalités à la scène. Et croyez-moi, il y a quelque chose à raconter. Nous commençons par parcourir les différentes constantes de SceneKit.


Les contraintes dans SceneKit sont prises en compte immédiatement après le physique.  Et avant de rendre le cadre
Les contraintes dans SceneKit sont prises en compte immédiatement après la physique. Et avant de rendre le cadre


Des contraintes, dites-vous? Quelles sont les constantes? Peu de gens le savent, et encore plus en parlent, mais SceneKit a son propre ensemble de constantes. Et bien qu'ils ne soient pas aussi flexibles que les constantes dans UIkit, vous pouvez toujours faire beaucoup de choses intéressantes avec eux.


SCNReplicatorConstraint
SCNReplicatorConstraint


Commençons par une constante simple - SCNReplicatorConstraint . Tout ce qu'il fait, c'est dupliquer la position, la rotation et la taille d'un autre objet avec des décalages supplémentaires. Comme pour toutes les autres constantes, il peut modifier la force et définir le drapeau d'incrémentalité. Les deux paramètres peuvent être mieux affichés sur cette constante.


Force réduite 10 fois
Force réduite 10 fois


La force affecte la quantité de transformation appliquée à l'objet. Et puisque la position de l'objet cible change à chaque image - l'objet ombre approche un dixième de la différence de distance. De ce fait, un effet de retard apparaît.


Incrément augmenté et force réduite de 10 fois
Incrément augmenté et force réduite de 10 fois


L'incrémentalité , à son tour, affecte si la constante est annulée après le rendu. Supposons que nous l'avons désactivé. Ensuite, nous voyons que sur chaque image, la constante est appliquée avant le rendu, et après le rendu, elle est annulée, et donc chaque image est répétée. Par conséquent, en combinant ces deux paramètres, vous pouvez obtenir un effet plutôt intéressant des aiguilles de l'horloge.


L'avion fait toujours face à la caméra.
L'avion fait toujours face à la caméra.


Passons à une constante plus intéressante: le soi-disant panneau d'affichage.


Supposons qu'il soit nécessaire qu'un objet nous «fasse face» toujours. Pour ce faire, utilisez simplement SCNBillboardConstraint , indiquez les axes autour desquels l'objet peut pivoter. De plus, avant de calculer chaque trame (après une étape avec la physique), les positions et orientations de tous les objets seront mises à jour pour satisfaire toutes les constantes.


Ici, vous pouvez mentionner Look At Constraint : il est similaire à un panneau d'affichage, seul l'objet peut être placé face à tout autre objet de la scène au lieu de la caméra actuelle.


Que peut-on faire avec leur aide? Bien sûr, le plus souvent, ces constantes sont utilisées pour dessiner des arbres ou de petits objets. Ils créent également des effets spéciaux comme un incendie ou une explosion. De plus, avec leur aide, vous pouvez faire en sorte que la caméra suive l'objet sur la scène.


Maintenir la distance entre les objets
Maintient la distance entre les objets


SCNDistanceConstraint vous permet de définir la distance minimale et / ou maximale à la position d'un autre objet. Et oui, vous pouvez l'utiliser pour faire un serpent. :) Cette contrainte peut également être utilisée pour lier la caméra au personnage, bien que la position de la caméra soit généralement plus compliquée, et la décrire avec des interprétations seules n'est pas une tâche facile. Le même effet peut être obtenu en ajoutant un ressort dans le moteur physique, mais ce ressort peut être complété par une tension au cas où vous auriez besoin d'éviter des problèmes d'étirement ou de compression excessifs du ressort.


Beaucoup ont vu dans certains Hitman, Fallout ou Skyrim: vous traînez un corps avec vous, il touche un obstacle - et commence à se comporter comme si un démon y était entré. Cette constante aiderait à éviter de tels bugs.


SCNSliderConstraint
SCNSliderConstraint


SCNSliderConstraint vous permet de définir la distance minimale entre un objet donné et des corps physiques avec un masque de collision approprié. Constante assez drôle, mais encore une fois, ils essaient de le simuler en utilisant une interaction physique. L'idée principale est de définir le rayon de la zone morte avec des corps physiques pour un objet qui n'a pas de corps physique.


Cinématique inverse au travail
Cinématique inverse au travail


SCNIKConstraint est la constante la plus intéressante, mais aussi la plus complexe, qui utilise la cinématique dite inverse. En utilisant une chaîne de nœuds parents, la cinématique inverse tente de manière itérative d'amener la position du nœud à laquelle vous appliquez cette constante au point nécessaire. En fait, cela vous permet de ne pas penser dans quelle position l'épaule et l'avant-bras doivent être, mais simplement de définir la position de la main et les angles de rotation possibles des nœuds de connexion. Le reste sera compté pour vous. Le principal inconvénient de cette contrainte est qu'elle vous permet de définir uniquement la position de la main, mais pas son orientation, et les restrictions d'angles peuvent être globalisées, sans décomposer les axes.


Nous avons donc rencontré en détail les constantes et ce qu'elles savent faire. Continuons à explorer les effets intéressants. Nous traiterons de l'effet des ombres.


Il y a un avion, mais ce n'est pas
Il y a un avion, mais ce n'est pas


Il semblerait que cela pourrait être plus facile dans un moteur qui prend en charge les ombres que de créer des ombres? Mais parfois, les ombres doivent être projetées sur un plan complètement transparent. Ceci est très utile dans ARKit, car l'image de la caméra est affichée derrière l'avion et l'ombre doit être projetée quelque part. L'astuce s'avère assez simple: vous devez d'abord activer les ombres différées et désactiver l'enregistrement dans tous les composants du plan dans l'onglet Matériel, et l'ombre continuera de la chevaucher. Le seul problème est que ce plan chevauchera les objets derrière lui.


Mais les ombres ne sont pas le seul effet mal étudié dans SceneKit. Traitons maintenant des miroirs.


Miroir SCNFloor - quoi de plus simple
Miroir SCNFloor - quoi de plus simple


Tous ceux qui ont joué avec SceneKit connaissent probablement scnfloor, qui ajoute des reflets miroir au sol. Mais pour une raison quelconque, très peu l'utilisent pour des réflexions honnêtes sur les miroirs, car vous pouvez placer votre modèle sur la géométrie du sol, l'incliner un peu et le transformer ... en un miroir ordinaire.



Goutte sur verre et miroir incurvé
Goutte sur verre et miroir incurvé


Mais, ce qui est encore moins connu, une carte normale peut être établie pour ce sexe. Pour cette raison, à son tour, vous pouvez créer de nombreux effets intéressants différents, tels que l'effet de stries ou d'un miroir incurvé.


Ultra violet


Une fois, j'ai embrassé une fille aux yeux ouverts. La jeune fille a coupé son visage avec le plan proche de l'écrêtage. Depuis, je ne m'embrasse que les yeux fermés.
- John Carmack

Ombres, miroirs - effets intéressants. Mais il y a un effet qui, lorsqu'il est utilisé avec habileté, peut s'avérer encore plus intéressant: les textures vidéo.



Vidos ordinaires et en hauteur
Vidéos ordinaires et en hauteur


Vous en aurez peut-être besoin juste pour montrer la vidéo dans le jeu. Mais il est beaucoup plus intéressant qu'avec l'aide de textures vidéo, vous pouvez modifier la géométrie. Pour ce faire, vous devez mettre la texture vidéo avec une carte de hauteur dans la propriété de déplacement de votre matériau et utiliser le matériau sur un plan avec un nombre suffisamment important de segments . Reste à comprendre comment le mettre là.


J'ai mentionné dans la description du processus de création de scène que vous pouvez utiliser SKScene comme propriété matérielle, et il s'agit d'une scène SpriteKit. SpriteKit est comme SceneKit, mais pour les graphiques 2D. Il prend en charge l'affichage de vidéos à l'aide de SKVideoNode . Il vous suffit de mettre SKVideoNode dans SKScene et SKScene dans SCNMaterialProperty, et vous avez terminé.


Mais après avoir exporté la scène 3D résultante et l'ouvrir ailleurs, nous verrons un carré noir. En fouillant dans le fichier scn, j'ai trouvé la raison. Il s'avère que lors de l'enregistrement d'un code vidéo, il n'enregistre pas l'URL de la vidéo. Il semblerait que vous prenez et régnez. Mais tout n'est pas si simple: le fichier scn est un soi-disant pliste binaire, qui contient le résultat de NSKeyedArchiver. Et le matériau, qui est la scène SpriteKit, est le même plist binaire, qui, il s'avère, se trouve déjà dans un autre plist binaire! C'est bien qu'il n'y ait que deux niveaux d'imbrication.


Eh bien, maintenant, nous allons même passer à l'effet, mais à un outil qui vous permet de créer tout type d'effets. Ce sont des modificateurs de shader.


Avant de modifier quelque chose, vous devez comprendre ce que nous modifions. Un shader, par définition, est un programme pour le GPU qui s'exécute pour chaque sommet et pour chaque pixel. Ainsi, un shader est un programme qui détermine l'apparence d'un objet à l'écran.


Eh bien, les modificateurs de shaders vous permettent de changer les résultats des shaders standard en GLSL ou Metal Shading Language. Ils sont également disponibles dans un éditeur visuel, qui vous permet de voir les changements dans le modificateur en temps réel.



Cartographie de la fourrure et de la parallaxe
Cartographie de la fourrure et de la parallaxe


À l'aide de modificateurs de shader, vous pouvez créer des effets visuels complexes. Par exemple, quelques-uns des effets les plus célèbres: Fur et Parallax Mapping .


#pragma arguments texture2d bg; texture2d height; float depth; float layers; #pragma transparent #pragma body constexpr sampler sm = sampler(filter::linear, s_address::repeat, t_address::repeat); float3 bitangent = cross(_surface.tangent, _surface.normal); float2 direction = float2(-dot(_surface.view.rgb, _surface.tangent), dot(_surface.view.rgb, _surface.bitangent)); _output.color.rgba = float4(0); for(int i = 0; i < int(floor(layers)); i++) { float coeff = float(i) / floor(layers); float2 defaultCoords = _surface.diffuseTexcoord + direction * (1 - coeff) * depth; float2 adjustment = float2(scn_frame.sinTime + defaultCoords.x, scn_frame.cosTime) * depth * coeff * 0.1; float2 coords = defaultCoords + adjustment; _output.color.rgb += bg.sample(sm, coords).rgb * coeff * (height.sample(sm, coords).r + 0.1) * (1.0 - coeff); _output.color.a += (height.sample(sm, coords).r + 0.1) * (1.0 - coeff); } return _output; 

Ray Casting avec caustiques en temps réel.
Lancer de rayons avec des caustiques en temps réel


Plus intéressant, personne ne prend la peine de jeter complètement les résultats de son travail et d'écrire son propre rendu. Par exemple, vous pouvez essayer d'implémenter Ray Casting dans les shaders. Et tout cela fonctionne assez rapidement pour fournir 30 FPS même sur des calculs aussi complexes. Mais c'est un sujet pour un rapport séparé. Allez Mobius !


Cauchemar!


Je n'aime pas cligner des yeux, car les paupières fermées chargent fortement le GPU pour BDPT en raison d'un manque d'éclairage.
- John Carmack

Donc, nous avons un tas d'objets avec des effets sympas. Reste maintenant à apprendre à les enregistrer. Pour ce faire, passons à un sujet plus complexe: comment nous avons appris à enregistrer des vidéos directement à partir de SceneKit sans interface utilisateur externe et comment nous avons optimisé cet enregistrement des dizaines de fois.


Voyons d'abord la solution la plus simple: ReplayKit . Découvrez pourquoi cela ne convient pas. De manière générale, cette solution vous permet de créer une entrée d'écran sur plusieurs lignes de code et de l'enregistrer via l'aperçu du système. Mais. Il a un gros inconvénient - il enregistre tout, l'interface utilisateur entière, y compris tous les boutons sur l'écran. C'était notre première décision, mais pour des raisons évidentes, il était impossible de la laisser en production: les utilisateurs devaient partager la vidéo, et non pas à partir de l'aperçu du système.


Nous nous sommes retrouvés dans une situation où la solution devait être écrite à partir de zéro. Absolument à partir de zéro. Voyons donc comment sur iOS vous pouvez créer votre propre vidéo et y enregistrer vos images. Tout est assez simple:


Processus d'enregistrement
Processus d'enregistrement


Nous devons créer une entité qui enregistrera les fichiers - AVAssetWriter , y ajoutera un flux vidéo - AVAssetWriterInput et créera un adaptateur pour ce flux qui convertira notre tampon de pixels au format requis par le flux - AVAssetWriterPixelBufferAdaptor .


Au cas où, je vous rappelle que le tampon de pixels est une entité, qui est un morceau de mémoire où les données pour les pixels sont en quelque sorte écrites. Il s'agit essentiellement d'une représentation de bas niveau de l'image.


Mais comment obtenir ce tampon de pixels? La solution est simple. SCNView a une merveilleuse fonction .snapshot () qui retourne un UIImage. Nous avons juste besoin de créer un tampon de pixels à partir de cette UIImage.


 var unsafePixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(NULL, self.pixelBufferPool, &unsafePixelBuffer) guard let pixelBuffer = maybePixelBuffer else { return } CVPixelBufferLockBaseAddress(pixelBuffer, 0) let data = CVPixelBufferGetBaseAddress(pixelBuffer) let rgbColorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) let rowBytes = NSUInteger(CVPixelBufferGetBytesPerRow(pixelBuffer)) let context = CGContext( data: data, width: image.width, height: image.height, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: bitmapInfo.rawValue ) context?.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) CVPixelBufferUnlockBaseAddress(pixelBuffer, 0) self.appendPixelBuffer(pixelBuffer, withPresentationTime: presentationTime) 

Nous allouons simplement une place dans la mémoire, décrivons le format de ces pixels, bloquons le tampon pour le changement, obtenons l'adresse mémoire, créons un contexte à l'adresse reçue, où nous décrivons comment les pixels sont compressés, combien de lignes sont dans l'image et quel espace colorimétrique nous utilisons. Ensuite, nous copions les pixels de UIImage là-bas, connaissant le format final, et débloquons le changement.



Vous devez maintenant effectuer cette opération à chaque image. Pour ce faire, nous créons un lien d'affichage qui appellera un rappel pour chaque image, où nous, à notre tour, appellerons la méthode de l'instantané et créerons un tampon de pixels à partir de l'image. Tout est simple!



Mais non. Une telle solution, même sur des téléphones puissants, entraîne des retards terribles et des baisses de FPS. Faisons l'optimisation.



Disons que nous n'avons pas besoin de 60 FPS. Nous serons même ravis du 25. Mais quelle est la manière la plus simple d'atteindre ce résultat? Bien sûr, il vous suffit de mettre tout cela sur un fil d'arrière-plan. De plus, selon les développeurs, cette fonction est thread-safe.



Hmm, le décalage est devenu moins important, mais la vidéo a cessé d'enregistrer ...


Tout est simple. Comme on dit, si vous avez un problème et que vous le résolvez à l'aide de plusieurs threads, vous aurez 2 problèmes.


Si vous essayez d'enregistrer un tampon de pixels avec un horodatage inférieur au dernier enregistré, la vidéo entière sera invalide.



N'écrivons donc pas un nouveau tampon avant la fin de l'écriture précédente.



Hmm, ça s'est beaucoup amélioré. Mais tout de même, pourquoi les décalages sont-ils apparus au départ?



Il s'avère que la fonction .snapshot () , avec laquelle nous obtenons une image de l'écran, crée un nouveau rendu pour chaque appel, dessine un cadre à partir de zéro et le renvoie, pas l'image qui est à l'écran. Cela conduit à des effets amusants. Par exemple, la simulation physique est deux fois plus rapide.


Mais attendez - pourquoi essayons-nous de rendre un nouveau cadre à chaque fois? Vous pouvez sûrement trouver quelque part le tampon affiché à l'écran. En effet, il y a accès à un tel tampon, mais c'est très simple. Nous devons obtenir CAMetalDrawable à partir de Metal.


Malheureusement, accéder à Metal directement depuis SCNView n'est pas si facile pour une raison assez compréhensible - dans SceneKit, vous pouvez choisir le type d'API vous-même, mais si vous regardez sous le capot et regardez le calque , vous pouvez voir qu'il agit comme tel, dans le cas de Metal, CAMetalLayer .


Mais ici aussi, l'échec nous attend: dans CAMetalLayer, la seule façon d'interagir avec la vue est la fonction nextDrawable, qui renvoie un CAMetalDrawable inutilisé. Il est entendu que vous allez y écrire des données et y appeler la fonction actuelle, qui les affichera à l'écran.


La solution existe réellement. Le fait est qu'après avoir disparu de l'écran, le tampon n'est pas désalloué, mais seulement replacé dans le pool. En effet, pourquoi allouer de la mémoire à chaque fois si deux ou trois tampons suffisent: un est affiché à l'écran, le second pour le rendu et le troisième, par exemple, pour le post-traitement, si vous en avez un.


Il s'avère qu'après avoir affiché le tampon, les données ne disparaissent nulle part et vous pouvez y accéder en toute sécurité.


Et si nous, dans le successeur, commençons en réponse à chaque appel à nextDrawable () pour l'enregistrer, nous obtenons presque ce dont nous avons besoin. Le problème est que le CAMetalDrawable enregistré est celui dans lequel l'image est dessinée en ce moment.


Le passage à la vraie solution est très simple - nous enregistrons à la fois le Drawable actuel et le précédent.


Et voilà, prêt - accès direct à la mémoire via CAMetalDrawable.


 var unsafePixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(NULL, self.pixelBufferPool, &unsafePixelBuffer) guard let pixelBuffer = maybePixelBuffer else { return } CVPixelBufferLockBaseAddress(pixelBuffer, 0) let data = CVPixelBufferGetBaseAddress(pixelBuffer) let width: NSUInteger = lastDrawable.texture.width let height: NSUInteger = lastDrawable.texture.height let rowBytes: NSUInteger = NSUInteger(CVPixelBufferGetBytesPerRow(pixelBuffer) lastDrawable.texture.getBytes( data, bytesPerRow: rowBytes, fromRegion: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0 ) CVPixelBufferUnlockBaseAddress(pixelBuffer, 0) self.appendPixelBuffer(pixelBuffer, withPresentationTime: presentationTime) 

Donc, maintenant, nous ne créons pas de contexte et n'y dessinons pas d'image UII, mais copions un morceau de mémoire dans un autre. La question se pose: qu'en est-il du format pixel? ..


Il ne correspond pas à deviceColorSpace ... Et il ne correspond pas aux espaces colorimétriques couramment utilisés ...


C'est exactement le point où l' auteur de l'un des foyers publics, qui effectue la même tâche, est tombé en panne . Tout le monde n'était même pas venu ici.



Eh bien, toutes ces astuces - pour un filtre effrayant?


Et bien non! Dans l'article sur ARKit, vous pouvez trouver une mention que l'image de la caméra n'utilise pas l'espace colorimétrique standard, mais agrandi. Et même une matrice de transformation de l'espace colorimétrique est présentée. Mais pourquoi s'engager dans la transformation, si vous pouvez essayer d'enregistrer directement dans ce format? Reste à savoir de quel format il s'agit sur 60 disponibles ...



Et puis j'ai commencé à éclater. J'ai enregistré trois vidéos dans différents flux avec différents formats, en les remplaçant à chaque enregistrement.


En conséquence, dans environ le quarantième format, nous obtenons son nom. Il s'avère que ce n'est autre que kCVPixelFormatType_30RGBLEPackedWideGamut . Comment n'ai-je pas deviné?



Mais ma joie a duré jusqu'au premier testeur. Je n'avais pas de mots. Comment? Je viens de passer beaucoup de temps à chercher le bon format. Il est bon que le problème se soit localisé rapidement - le bug s'est reproduit de manière stable et uniquement sur 6s et 6s Plus. Presque immédiatement après cela, je me suis souvenu que les écrans avec prise en charge à large gamme ont commencé à être installés uniquement sur les septièmes iPhones.


En changeant la gamme large en bon vieux 32RGBA, j'obtiens un record de travail! Il reste à comprendre comment déterminer que l'appareil prend en charge la large gamme. Il existe des iPads avec différents types d'affichage, et je pensais que vous pouvez certainement obtenir le type d'affichage ENUM du système. En fouillant dans la documentation, je l'ai trouvée - c'est displayGamut dans UITraitCollection .


Après avoir donné l'assemblage aux testeurs, j'ai reçu de bonnes nouvelles de leur part - tout fonctionnait sans aucun retard, même sur les anciens appareils!


En conclusion, je veux vous dire - faites des graphiques 3D! Dans notre application, pour laquelle la réalité augmentée n'est pas le cas d'utilisation principal, au cours du week-end, les gens ont parcouru plus de 2 000 kilomètres, regardé plus de 3 000 objets et enregistré plus de 1 000 vidéos avec eux! Imaginez ce que vous pouvez faire si vous le faites vous-même.

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


All Articles