- Ich bin zu jung, um zu sterben.
SceneKit ist ein 3D-Grafikframework auf hoher Ebene in iOS, mit dem animierte Szenen und Effekte erstellt werden können. Es enthält eine physische Engine, einen Partikelgenerator und eine Reihe einfacher Aktionen für 3D-Objekte, mit denen Sie die Szene inhaltlich beschreiben können - Geometrie, Materialien, Beleuchtung, Kameras - und sie durch eine Beschreibung der Änderungen für diese Objekte animieren können.

Heute werden wir SceneKit mit einem aufmerksamen, etwas strengen Blick betrachten, aber zuerst gehen wir die Grundlagen durch und sehen, wie die 3D-Szene aussieht und was getan werden muss, um sie zu erstellen.

Die einfachste Szene aus drei Knoten mit Geometrie
Zuerst müssen Sie die Grundstruktur der Szene erstellen, die aus Knoten oder Knoten der Szene besteht. Jeder Knoten kann sowohl Geometrie als auch andere Knoten enthalten. Die Geometrie kann entweder einfach wie eine Kugel, ein Würfel oder eine Pyramide oder komplexer sein und in externen Editoren erstellt werden.

Überlagerungsmaterialien
Für diese Geometrie müssen Sie dann die Materialien angeben, die die grundlegende Darstellung der Objekte bestimmen. Jedes Material selbst legt sein eigenes Beleuchtungsmodell fest und verwendet je nach Material unterschiedliche Eigenschaften . Jede dieser Eigenschaften ist normalerweise eine Farbe oder Textur, aber zusätzlich zu diesen häufig verwendeten Optionen gibt es auch die Option, CALayer , AVPlayer und SKScene zu verwenden .

Fügen Sie Lichtquellen hinzu
Danach müssen Lichtquellen hinzugefügt werden , die bestimmen, wie gut Objekte in dem einen oder anderen Teil der Szene sichtbar sind. Sie müssen analog zur Geometrie innerhalb eines Knotens liegen. SceneKit unterstützt viele verschiedene Beleuchtungsarten sowie verschiedene Arten von Schatten .

Out-of-the-Box-Boke-Effekt
Dann müssen Sie eine Kamera erstellen (und in einem separaten Knoten ablegen) und die grundlegenden Parameter dafür festlegen. Es gibt viele davon, aber mit ihrer Hilfe können Sie coole Effekte erstellen. Standardmäßig werden Bokeh (oder Unschärfe), HDR mit Anpassung, Glühen, SSAO und Farbton- / Sättigungsmodifikationen unterstützt.

Einfache Animationen in SceneKit
Schließlich enthält SceneKit eine einfache Reihe von Aktionen für 3D-Objekte, mit denen Sie Szenenänderungen im Laufe der Zeit festlegen können. SceneKit unterstützt auch in JavaScript beschriebene Aktionen , dies ist jedoch ein Thema für einen separaten Artikel.

Die Wechselwirkung eines Partikelgenerators mit einem physischen Motor kann zu einem Tornado führen!
Neben den Grafiken sind die Hauptmerkmale von SceneKit der Partikelgenerator und eine fortschrittliche physikalische Engine, mit der Sie reale physikalische Eigenschaften sowohl für normale Objekte als auch für Partikel aus dem Generator festlegen können.
Über all diese Chips wurde eine große Anzahl detaillierter Tutorials geschrieben. Aber im Entwicklungsprozess haben wir diese Möglichkeiten praktisch nicht genutzt ...
Hey, nicht zu rau
Einmal habe ich ein Beleuchtungsmodell für 3D-Spiele geschrieben, das besser als echtes Sonnenlicht ist und auf dem Nvidia 8800 akzeptable FPS liefert, aber ich habe beschlossen, die Engine nicht freizugeben, weil Gott nett zu mir ist und ich seine Inkompetenz in dieser Angelegenheit nicht zeigen möchte.
- John Carmack
Wir werden eine detaillierte Studie mit einer ziemlich einfachen Aufgabe beginnen, die sich für fast alle ergibt, die sehr ernsthaft mit SceneKit arbeiten: Wie lade ich ein Modell mit komplexer Geometrie und verbundenen Materialien, Beleuchtung und sogar Animationen?
Es gibt verschiedene Möglichkeiten, und alle haben ihre Vor- und Nachteile:
SCNScene (benannt :) - erhält eine Szene aus einem Bundle,
SCNScene (url: options :) - lädt die Szene per URL,
SCNScene (mdlAsset :) - konvertiert eine Szene aus verschiedenen Formaten,
SCNReferenceNode (url :) - lädt die Szene träge.
Holen Sie sich die Szene aus dem Bundle
Sie können die Standardmethode verwenden : Fügen Sie unser Modell im dae- oder scn-Format in das scnassets-Bundle ein und laden Sie es von dort in Analogie zu UIImage (benannt :).
Was aber, wenn Sie das Update von Modellen selbst steuern möchten, ohne jedes Mal ein Update im App Store zu veröffentlichen, wenn Sie einige Texturen ändern müssen? Oder nehmen Sie an, Sie müssen vom Benutzer erstellte Karten und Modelle unterstützen. Oder - dass Sie die Anwendung einfach nicht vergrößern möchten, da 3D-Grafiken nicht die Hauptfunktionalität sind.
Laden der Szene per URL
Sie können den Szenenkonstruktor über die URL der SCN-Datei verwenden. Diese Methode unterstützt das Herunterladen nicht nur aus dem Dateisystem, sondern auch aus dem Netzwerk. Im letzteren Fall können Sie die Komprimierung vergessen. Außerdem müssen Sie das Modell im Voraus in das SCN-Format konvertieren. Sie können natürlich dae verwenden, aber damit sind eine Reihe von Einschränkungen verbunden. Zum Beispiel das Fehlen eines physisch basierten Renderings.
Der Hauptvorteil dieser Methode besteht darin, dass Sie die Importeinstellungen flexibel konfigurieren können . Sie können beispielsweise den Lebenszyklus von Animationen ändern und sie endlos wiederholen lassen. Sie können die Quelle zum Laden externer Ressourcen wie Texturen explizit angeben, die Ausrichtung und den Maßstab der Szene konvertieren, fehlende Normalen für die Geometrie erstellen, die gesamte Geometrie der Szene zu einem großen Knoten zusammenführen oder alle Szenenelemente verwerfen, die nicht dem Formatstandard entsprechen.
Die dritte Option besteht darin, den Konstruktor mit MDLAsset zu verwenden . Das heißt, wir erstellen zuerst ein MDLAsset , das im ModelIO-Framework verfügbar ist, und übergeben es dann an den Konstruktor für die Szene.
Diese Option ist insofern gut, als Sie viele verschiedene Formate herunterladen können. Offiziell kann MDLAsset die Formate obj, ply, stl und usd laden, aber nachdem mir eine Liste aller möglichen Formate ausgegangen ist, zumindest in Bezug auf Computergrafiken, habe ich vier weitere gefunden: abc, bsp, vox und md3, aber sie werden möglicherweise nicht vollständig unterstützt oder nicht in allen Systemen, und für sie müssen Sie die Richtigkeit des Imports überprüfen.
Es ist auch zu berücksichtigen, dass diese Methode einen Overhead für die Konvertierung hat, und sie sehr vorsichtig zu verwenden.
Diese Methoden haben eine gemeinsame Gefahr: Sie geben SCNScene zurück, nicht SCNNode. Die einzige Möglichkeit, einer vorhandenen Szene Inhalte hinzuzufügen, besteht darin, alle untergeordneten Knoten zu kopieren und - Sie können diesen Schritt leicht überspringen - Animationen vom Stammknoten (z. B. können sie dort angezeigt werden, wenn Sie mit dae arbeiten). Darüber hinaus müssen Sie berücksichtigen, dass es in der Szene nur eine Texturumgebung geben kann (wenn Sie keine benutzerdefinierten Shader für Reflexionen verwenden).
Faul die Szene laden
Die vierte Option ist die Verwendung von SCNReferenceNode . Es wird keine Szene zurückgegeben, sondern ein Knoten, der selbst träge (oder auf Anfrage) die gesamte Hierarchie der Szene in sich laden kann. Somit ähnelt diese Methode der ersten, verbirgt jedoch alle Probleme beim Kopieren in sich.
Er hat aber eines: Die globalen Parameter der Szene gehen verloren.
Es stellt sich heraus, dass dies der einfachste und schnellste Weg ist, Ihr Modell herunterzuladen. Wenn Sie jedoch eine Dateioptimierung benötigen, ist die erste Methode besser.
Infolgedessen haben wir uns für die erste Option entschieden, da es für uns am bequemsten war, im scn-Format zu arbeiten, und für Designer, vom dae-Format in dieses zu konvertieren. Außerdem brauchten wir beim Booten Animationen zur Dateioptimierung.
Überhaupt keine vorzeitigen Optimierungen
Nachdem ich lange an diesem Prozess herumgebastelt habe, kann ich Ihnen einige Ratschläge geben.
Der wichtigste Tipp ist, Dateien im Voraus in scn zu konvertieren. Anschließend können Sie durch Öffnen der Datei im integrierten Szeneneditor in Xcode sehen, wie Ihr Objekt in SceneKit aussehen wird.
Darüber hinaus ist die scn-Datei nur eine binäre Darstellung der Szene, sodass das Laden aus der Szene am wenigsten Zeit in Anspruch nimmt. Aus demselben Grund müssen Sie zuerst die XML-Datei analysieren und dann alle Netze, Animationen und Materialien konvertieren. Darüber hinaus ist die Konvertierung von Animationen und Materialien eine potenzielle Problemquelle. Wir erinnern uns an den Mangel an PBR-Unterstützung in dae: Es stellt sich heraus, dass Sie, wenn Sie es verwenden möchten, die Art aller Materialien nach der Konvertierung ändern und die entsprechenden Texturen manuell ablegen müssen.
Mit dieser Operation können Sie einen sehr nützlichen Nebeneffekt erzielen: eine signifikante Texturkomprimierung. Es reicht aus, sie in der "Ansicht" zu öffnen und zu exportieren und das Format in heic zu ändern. Durch diese einfache Operation konnten durchschnittlich 5 Megabyte pro Modell eingespart werden.
Wenn Sie eine Szene aus dem Internet herunterladen, kann ich Ihnen auch empfehlen, sie im Archiv herunterzuladen, zu entpacken und die URL der entpackten scn-Datei zu übertragen. Dies spart Ihnen und dem Benutzer zusätzliche Megabyte - was wiederum den Download beschleunigt und auch die Anzahl der Fehlerstellen verringert. Einverstanden: Eine separate Anfrage für jede externe Ressource und sogar im mobilen Internet ist nicht der beste Weg, um die Zuverlässigkeit zu erhöhen.
Tut mir sehr weh
Wenn ich ein Auto fahre, höre ich oft die Festplatte des Universums knistern und die nächste Straße beladen.
- John Carmack
Wenn also das Laden und Importieren von Modellen in Betrieb genommen wird, entsteht eine neue Aufgabe: Hinzufügen verschiedener Effekte und Funktionen zur Szene. Und glauben Sie mir, es gibt etwas zu erzählen. Wir beginnen mit den verschiedenen Konstanten in SceneKit.

Einschränkungen in SceneKit werden unmittelbar nach der Physik berücksichtigt. Und bevor Sie den Rahmen rendern
Einschränkungen, sagst du? Was sind die Konstanten? Nur wenige Leute wissen es und sprechen noch mehr darüber, aber SceneKit hat seine eigenen Konstanten. Und obwohl sie nicht so flexibel sind wie die Konstanten in UIkit, können Sie mit ihnen dennoch viele interessante Dinge tun.

SCNReplicatorConstraint
Beginnen wir mit einer einfachen Konstante - SCNReplicatorConstraint . Er dupliziert lediglich die Position, Drehung und Größe eines anderen Objekts mit zusätzlichen Offsets. Wie bei allen anderen Konstanten kann er die Stärke ändern und das Flag der Inkrementalität setzen. Beide Parameter können am besten mit dieser Konstante angezeigt werden.

10-mal reduzierte Stärke
Die Stärke beeinflusst, wie viel Transformation auf das Objekt angewendet wird. Und da sich die Position des Zielobjekts in jedem Frame ändert, nähert sich das Schattenobjekt einem Zehntel der Entfernungsdifferenz. Aus diesem Grund tritt ein Verzögerungseffekt auf.

Erhöhtes Inkrement und reduzierte Stärke um das 10-fache
Die Inkrementalität beeinflusst wiederum, ob die Konstante nach dem Rendern gelöscht wird. Angenommen, wir haben es ausgeschaltet. Dann sehen wir, dass auf jedem Frame die Konstante vor dem Rendern angewendet wird und nach dem Rendern abgebrochen wird, sodass jeder Frame wiederholt wird. Wenn Sie diese beiden Parameter kombinieren, erhalten Sie einen ziemlich interessanten Effekt der Uhrzeiger.

Das Flugzeug ist immer der Kamera zugewandt.
Kommen wir zu einer interessanteren Konstante: der sogenannten Plakatwand.
Angenommen, es ist notwendig, dass ein Objekt uns immer „gegenübersteht“. Verwenden Sie dazu einfach SCNBillboardConstraint und geben Sie an , um welche Achsen sich das Objekt drehen kann. Vor der Berechnung jedes Frames (nach einem Schritt mit der Physik) werden die Positionen und Ausrichtungen aller Objekte aktualisiert, um alle Konstanten zu erfüllen.
Hier können Sie Look At Constraint erwähnen: Es ähnelt einer Werbetafel, nur das Objekt kann anstelle der aktuellen Kamera auf ein anderes Objekt in der Szene gerichtet werden.
Was kann mit ihrer Hilfe getan werden? Natürlich werden diese Konstanten meistens verwendet, um Bäume oder kleine Objekte zu zeichnen. Sie erzeugen auch Spezialeffekte wie Feuer oder Explosion. Darüber hinaus können Sie mit ihrer Hilfe die Kamera dazu bringen, dem Objekt auf der Bühne zu folgen.

Hält Abstand zwischen Objekten
Mit SCNDistanceConstraint können Sie den minimalen und / oder maximalen Abstand zur Position eines anderen Objekts festlegen . Und ja, du kannst damit eine Schlange machen. :) Diese Einschränkung kann auch verwendet werden, um die Kamera an den Charakter zu binden, obwohl die Position der Kamera normalerweise komplizierter ist und es keine leichte Aufgabe ist, sie nur mit Konstraten zu beschreiben. Der gleiche Effekt kann durch Hinzufügen einer Feder im physischen Motor erzielt werden. Diese Feder kann jedoch durch eine Belastung ergänzt werden, falls Sie Probleme mit übermäßigem Dehnen oder Zusammendrücken der Feder vermeiden müssen.
Viele haben in Hitman, Fallout oder Skyrim gesehen: Sie ziehen einen Körper mit sich, er berührt ein Hindernis - und beginnt sich so zu verhalten, als hätte ein Dämon ihn betreten. Diese Konstante würde helfen, solche Fehler zu vermeiden.

SCNSliderConstraint
Mit SCNSliderConstraint können Sie den Mindestabstand zwischen einem bestimmten Objekt und physischen Körpern mit einer geeigneten Kollisionsmaske festlegen. Ziemlich lustige Konstante, aber auch hier versuchen sie, sie durch physische Interaktion zu simulieren. Die Hauptidee besteht darin, den Radius der Totzone mit physischen Körpern für ein Objekt festzulegen, das keinen physischen Körper hat.

Inverse Kinematik bei der Arbeit
SCNIKConstraint ist die interessanteste, aber auch die komplexeste Konstante, die die sogenannte inverse Kinematik verwendet. Mithilfe einer Kette übergeordneter Knoten versucht die inverse Kinematik iterativ, die Knotenposition, auf die Sie diese Konstante anwenden, auf den erforderlichen Punkt zu bringen. Tatsächlich können Sie nicht überlegen, in welcher Position sich Schulter und Unterarm befinden sollen, sondern lediglich die Position der Hand und mögliche Drehwinkel der Verbindungsknoten festlegen. Der Rest wird für Sie gezählt. Der Hauptnachteil dieser Einschränkung besteht darin, dass Sie nur die Position der Hand einstellen können, nicht jedoch ihre Ausrichtung, und Winkelbeschränkungen können global festgelegt werden, ohne die Achsen zu beschädigen.
Also haben wir uns im Detail mit den Konstanten getroffen und mit dem, was sie zu tun wissen. Lassen Sie uns weiterhin interessante Effekte untersuchen. Wir werden uns mit der Wirkung von Schatten befassen.

Es gibt ein Flugzeug, aber es ist nicht
Es scheint, dass es in einer Engine, die Schatten unterstützt, einfacher sein könnte, als Schatten zu erzeugen? Aber manchmal müssen die Schatten auf eine völlig transparente Ebene geworfen werden. Dies ist in ARKit sehr nützlich, da das Kamerabild hinter der Ebene angezeigt wird und der Schatten irgendwo geworfen werden sollte. Der Trick stellt sich als recht einfach heraus: Zuerst müssen Sie verzögerte Schatten aktivieren und die Aufzeichnung in allen Komponenten der Ebene auf der Registerkarte Material deaktivieren, und der Schatten überlappt sie weiterhin. Das einzige Problem ist, dass diese Ebene die dahinter liegenden Objekte überlappt.
Schatten sind jedoch nicht der einzige schlecht untersuchte Effekt in SceneKit. Beschäftigen wir uns jetzt mit Spiegeln.

SCNFloor Spiegel - was könnte einfacher sein
Jeder, der mit SceneKit gespielt hat, kennt wahrscheinlich scnfloor, das dem Boden Spiegelreflexionen verleiht. Aber aus irgendeinem Grund verwenden nur sehr wenige es für ehrliche Spiegelreflexionen, weil Sie Ihr Modell auf die Bodengeometrie setzen, es ein wenig kippen und es ... in einen gewöhnlichen Spiegel verwandeln können.


Auf Glas und einen gebogenen Spiegel tropfen
Was jedoch noch weniger bekannt ist, kann eine normale Karte für dieses Geschlecht festgelegt werden. Auf diese Weise können Sie wiederum viele verschiedene interessante Effekte erzielen, z. B. den Effekt von Streifen oder einen gekrümmten Spiegel.
Ultraviolett
Einmal küsste ich ein Mädchen mit offenen Augen. Das Mädchen schnitt ihr Gesicht mit der nahen Ebene des Ausschnitts. Seitdem küsse ich nur mit geschlossenen Augen.
- John Carmack
Schatten, Spiegel - interessante Effekte. Es gibt jedoch einen Effekt, der sich bei geschickter Verwendung als noch interessanter herausstellen kann - Videotexturen.


Gewöhnliche und Höhenkartenvideos
Möglicherweise benötigen Sie sie nur, um das Video im Spiel zu zeigen. Viel interessanter ist jedoch, dass Sie mithilfe von Videotexturen die Geometrie ändern können. Dazu müssen Sie die Videotextur mit einer Höhenkarte in die Verschiebungseigenschaft Ihres Materials einfügen und das Material in einer Ebene mit einer ausreichend großen Anzahl von Segmenten verwenden . Es bleibt zu verstehen, wie man es dort platziert.
In der Beschreibung des Szenenerstellungsprozesses habe ich erwähnt, dass Sie SKScene als Materialeigenschaft verwenden können, und dies ist eine SpriteKit-Szene. SpriteKit ist wie SceneKit, jedoch für 2D-Grafiken. Es unterstützt die Anzeige von Videos mit SKVideoNode . Sie müssen nur SKVideoNode in SKScene und SKScene in SCNMaterialProperty einfügen, und fertig.
Nachdem Sie die resultierende 3D-Szene exportiert und an einer anderen Stelle geöffnet haben, wird ein schwarzes Quadrat angezeigt. Ich kramte in der scn-Datei und fand den Grund. Es stellt sich heraus, dass beim Speichern eines Videocodes die Video-URL nicht gespeichert wird. Es scheint, dass Sie nehmen und regieren. Aber nicht alles ist so einfach: Die scn-Datei ist eine sogenannte Binärliste, die das Ergebnis von NSKeyedArchiver enthält. Und das Material, das die SpriteKit-Szene darstellt, ist dieselbe Binärliste, die, wie sich herausstellt, bereits in einer anderen Binärliste liegt! Es ist gut, dass es nur zwei Verschachtelungsebenen gibt.
Nun gehen wir sogar zum Effekt über, aber zu einem Werkzeug, mit dem Sie jede Art von Effekten erstellen können. Dies sind Shader-Modifikatoren.
Bevor Sie etwas ändern, müssen Sie verstehen, was wir ändern. Ein Shader ist per Definition ein Programm für die GPU, das für jeden Scheitelpunkt und für jedes Pixel ausgeführt wird. Ein Shader ist also ein Programm, das bestimmt, wie ein Objekt auf dem Bildschirm aussieht.
Mit Shader-Modifikatoren können Sie die Ergebnisse von Standard-Shadern in GLSL oder Metal Shading Language ändern. Sie sind auch in einem visuellen Editor verfügbar, mit dem Sie Änderungen im Modifikator in Echtzeit sehen können.


Pelz- und Parallaxenkartierung
Mit Hilfe von Shader-Modifikatoren können Sie komplexe visuelle Effekte erstellen. Zum Beispiel einige der bekanntesten Effekte: Fur and 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 mit Echtzeit-Caustics
Interessanter ist, dass sich niemand die Mühe macht, die Ergebnisse ihrer Arbeit vollständig wegzuwerfen und einen eigenen Renderer zu schreiben. Sie können beispielsweise versuchen, Ray Casting in Shadern zu implementieren. Und das alles schnell genug, um selbst bei solch komplexen Berechnungen 30 FPS zu liefern. Dies ist jedoch ein Thema für einen separaten Bericht. Komm schon Mobius !
Albtraum!
Ich blinke nicht gern, weil geschlossene Augenlider die GPU für BDPT wegen mangelnder Beleuchtung stark belasten.
- John Carmack
Wir haben also eine Reihe von Objekten mit coolen Effekten. Jetzt bleibt zu lernen, wie man sie aufzeichnet. Lassen Sie uns dazu zu einem komplexeren Thema übergehen: Wie wir gelernt haben, Videos direkt aus SceneKit ohne externe Benutzeroberfläche aufzunehmen, und wie wir diese Aufnahme zehnmal optimiert haben.
Wenden wir uns zunächst der einfachsten Lösung zu: ReplayKit . Finden Sie heraus, warum es nicht passt. Im Allgemeinen können Sie mit dieser Lösung einen Bildschirmeintrag in mehreren Codezeilen erstellen und über die Systemvorschau speichern. Aber. Es hat ein großes Minus - es zeichnet alles auf, die gesamte Benutzeroberfläche, einschließlich aller Schaltflächen auf dem Bildschirm. Dies war unsere erste Entscheidung, aber aus offensichtlichen Gründen war es unmöglich, sie in die Produktion zu lassen: Benutzer mussten das Video freigeben und nicht aus der Systemvorschau.
Wir befanden uns in einer Situation, in der die Lösung von Grund auf neu geschrieben werden musste. Absolut von Grund auf neu. Lassen Sie uns sehen, wie Sie in iOS Ihr eigenes Video erstellen und Ihre Bilder dort aufnehmen können. Alles ist ganz einfach:

Aufnahmevorgang
Wir müssen eine Entität erstellen, die Dateien aufzeichnet - AVAssetWriter , einen Videostream - AVAssetWriterInput hinzufügen und einen Adapter für diesen Stream erstellen, der unseren Pixelpuffer in das vom Stream benötigte Format konvertiert - AVAssetWriterPixelBufferAdaptor .
Nur für den Fall, ich erinnere Sie daran, dass der Pixelpuffer eine Entität ist, die ein Stück Speicher ist, in dem die Daten für Pixel irgendwie geschrieben sind. Dies ist im Wesentlichen eine Darstellung des Bildes auf niedriger Ebene.
Aber wie bekommt man diesen Pixelpuffer? Die Lösung ist einfach. SCNView hat eine wunderbare .snapshot () - Funktion, die ein UIImage zurückgibt. Wir müssen nur einen Pixelpuffer aus diesem UIImage erstellen.
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)
Wir weisen einfach einen Platz im Speicher zu, beschreiben, welches Format diese Pixel haben, blockieren den Puffer zum Ändern, rufen die Speicheradresse ab, erstellen einen Kontext an der empfangenen Adresse, wo wir beschreiben, wie die Pixel gepackt sind, wie viele Zeilen im Bild sind und welchen Farbraum wir verwenden. Dann kopieren wir die Pixel von UIImage dort, kennen das endgültige Format und entsperren die Änderung.

Jetzt müssen Sie dies in jedem Frame tun. Zu diesem Zweck erstellen wir einen Anzeigelink, der für jeden Frame einen Rückruf aufruft, wobei wir wiederum die Snapshot-Methode aufrufen und aus dem Bild einen Pixelpuffer erstellen. Alles ist einfach!

Aber nein. Eine solche Lösung verursacht selbst bei leistungsstarken Telefonen schreckliche Verzögerungen und FPS-Verluste. Lassen Sie uns die Optimierung durchführen.

Nehmen wir an, wir brauchen keine 60 FPS. Wir werden uns sogar über den 25. freuen. Aber was ist der einfachste Weg, um dieses Ergebnis zu erzielen? Natürlich müssen Sie dies alles nur in einen Hintergrund-Thread einfügen. Darüber hinaus ist diese Funktion laut Entwicklern threadsicher.

Hmm, die Verzögerung ist geringer geworden, aber das Video hat die Aufnahme gestoppt ...
Alles ist einfach. Wie sie sagen, wenn Sie ein Problem haben und es mit Hilfe mehrerer Threads lösen, werden Sie 2 Probleme haben.
Wenn Sie versuchen, einen Pixelpuffer mit einem Zeitstempel aufzuzeichnen, der niedriger als der zuletzt aufgezeichnete ist, ist das gesamte Video ungültig.

Schreiben wir dann keinen neuen Puffer, bis der vorherige Schreibvorgang beendet ist.

Hmm, es wurde viel besser. Aber warum sind die Verzögerungen anfangs aufgetreten?

Es stellt sich heraus, dass die Funktion .snapshot () , mit der wir ein Bild vom Bildschirm erhalten, für jeden Aufruf einen neuen Renderer erstellt, einen Frame von Grund auf neu zeichnet und zurückgibt, nicht das Bild, das auf dem Bildschirm angezeigt wird. Dies führt zu lustigen Effekten. Beispielsweise ist die physikalische Simulation doppelt so schnell.
Aber warten Sie - warum versuchen wir jedes Mal, einen neuen Frame zu rendern? Sicherlich finden Sie irgendwo den Puffer, der auf dem Bildschirm angezeigt wird. Es gibt zwar Zugriff auf einen solchen Puffer, dieser ist jedoch nicht trivial. Wir müssen CAMetalDrawable von Metal bekommen.
Leider ist es aus einem ziemlich verständlichen Grund nicht so einfach, direkt von SCNView zu Metal zu gelangen. In SceneKit können Sie den API-Typ selbst auswählen. Wenn Sie jedoch unter die Haube schauen und die Ebene betrachten , können Sie feststellen, dass sie im Fall von Metal, CAMetalLayer , so funktioniert .
Aber auch hier erwartet uns ein Fehler: In CAMetalLayer ist die einzige Möglichkeit, mit der Ansicht zu interagieren, die nextDrawable-Funktion, die eine nicht belegte CAMetalDrawable zurückgibt. Es versteht sich, dass Sie Daten in das Programm schreiben und die aktuelle Funktion darauf aufrufen, die es auf dem Bildschirm anzeigt.
Die Lösung existiert tatsächlich. Tatsache ist, dass der Puffer nach dem Verschwinden vom Bildschirm nicht freigegeben, sondern nur wieder in den Pool gestellt wird. Warum sollte jedes Mal Speicher zugewiesen werden, wenn zwei oder drei Puffer ausreichen? Einer wird auf dem Bildschirm angezeigt, der zweite zum Rendern und der dritte beispielsweise zum Nachbearbeiten, falls Sie einen haben.
Es stellt sich heraus, dass nach dem Anzeigen des Puffers die Daten aus dem Puffer nirgendwo verschwinden und Sie sicher darauf zugreifen können.
Und wenn wir im Nachfolger als Antwort auf jeden Aufruf von nextDrawable () beginnen, um ihn zu speichern, erhalten wir fast das, was wir brauchen. Das Problem ist, dass das gespeicherte CAMetalDrawable dasjenige ist, in dem das Bild gerade gezeichnet wird.
Der Sprung zur eigentlichen Lösung ist sehr einfach - wir speichern sowohl das aktuelle Drawable als auch das vorherige.
Und hier ist es fertig - direkter Zugriff auf den Speicher über 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)
Jetzt erstellen wir keinen Kontext und zeichnen kein UIImage darin, sondern kopieren ein Stück Speicher in ein anderes. Es stellt sich die Frage: Was ist mit dem Pixelformat?
Es stimmt nicht mit deviceColorSpace überein ... Und es stimmt nicht mit häufig verwendeten Farbräumen überein ...
Dies ist genau der Punkt, an dem der Autor eines der öffentlichen Herde, der die gleiche Aufgabe ausführt, zusammenbrach . Alle anderen sind nicht einmal hierher gekommen.

Nun, all diese Tricks - um eines gruseligen Filters willen?
Nun, nein! In dem Artikel über ARKit finden Sie eine Erwähnung, dass das Bild von der Kamera nicht den Standardfarbraum verwendet, sondern erweitert wird. Und sogar eine Matrix der Farbraumtransformation wird vorgestellt. Aber warum sich auf eine Transformation einlassen, wenn Sie versuchen können, direkt in diesem Format aufzunehmen? Es bleibt abzuwarten, welches Format von 60 verfügbar ist ...

Und dann fing ich an zu platzen. Ich habe drei Videos in verschiedenen Streams mit unterschiedlichen Formaten aufgenommen und sie bei jeder Aufnahme ersetzt.
Infolgedessen erhalten wir im etwa vierzigsten Format seinen Namen. Es stellt sich heraus, dass es kein anderer als kCVPixelFormatType_30RGBLEPackedWideGamut ist . Wie habe ich nicht geraten?

Aber meine Freude hielt bis zum ersten Tester an. Ich hatte keine Worte. Wie? Ich habe gerade viel Zeit damit verbracht, nach dem richtigen Format zu suchen. Es ist gut, dass sich das Problem schnell lokalisiert hat - der Fehler wurde stabil und nur auf 6s und 6s Plus reproduziert. Fast unmittelbar danach fiel mir ein, dass Displays mit breitem Farbumfang erst auf siebten iPhones installiert wurden.
Wenn ich den weiten Bereich auf den guten alten 32RGBA umstelle, bekomme ich eine Arbeitsaufzeichnung! Es bleibt zu verstehen, wie festgestellt werden kann, dass das Gerät einen weiten Bereich unterstützt. Es gibt iPads mit verschiedenen Anzeigetypen, und ich dachte, dass Sie mit Sicherheit den ENUM-Anzeigetyp vom System erhalten können. Beim Durchstöbern der Dokumentation habe ich sie gefunden - dies ist displayGamut in UITraitCollection .
Nachdem ich den Testern die Montage gegeben hatte, erhielt ich angenehme Neuigkeiten von ihnen - alles funktionierte ohne Verzögerungen, auch auf alten Geräten!
Abschließend möchte ich Ihnen sagen - machen Sie 3D-Grafiken! In unserer Anwendung, für die Augmented Reality nicht der Hauptanwendungsfall ist, haben Menschen am Wochenende mehr als 2.000 Kilometer zurückgelegt, mehr als 3.000 Objekte angesehen und mehr als 1.000 Videos mit ihnen aufgenommen! Stellen Sie sich vor, was Sie tun können, wenn Sie es selbst tun.