Sou jovem demais para morrer.
O SceneKit é uma estrutura de gráficos 3D de alto nível no iOS que ajuda a criar cenas e efeitos animados. Ele inclui um mecanismo físico, um gerador de partículas e um conjunto de ações simples para objetos 3D que permitem descrever a cena em termos de conteúdo - geometria, materiais, iluminação, câmeras - e animar através de uma descrição das alterações desses objetos.

Hoje, examinaremos o SceneKit com um olhar atento e um pouco severo, mas primeiro, vamos examinar o básico e ver como é a cena 3D e o que precisa ser feito para criá-la.

A cena mais simples de três nós com geometria neles
Primeiro, você precisa criar a estrutura básica da cena, que consiste em nós ou nós da cena. Cada nó pode conter geometria e outros nós. A geometria pode ser simples, como uma bola, cubo ou pirâmide, ou mais complexa, criada em editores externos.

Sobreposição de materiais
Então, para esta geometria, você precisa especificar os materiais que determinarão a representação básica dos objetos. Cada material em si define seu próprio modelo de iluminação e, dependendo dele, usa um conjunto diferente de propriedades . Cada uma dessas propriedades geralmente é uma cor ou textura, mas, além dessas opções comumente usadas, também há a opção de usar CALayer , AVPlayer e SKScene .

Adicionar fontes de iluminação
Depois disso, você precisa adicionar fontes de luz que determinem o quão bem os objetos são visíveis em uma ou outra parte da cena. Eles, por analogia com a geometria, devem estar dentro de um nó. O SceneKit suporta muitos tipos diferentes de iluminação , bem como vários tipos de sombras .

Efeito Boke pronto para uso
Então você precisa criar uma câmera (e colocá-la em um nó separado) e definir os parâmetros básicos para ela. Existem muitos deles, mas com a ajuda deles você pode criar efeitos interessantes. Fora da caixa, são aceitas modificações de bokeh (ou desfoque), HDR com adaptação, brilho, SSAO e matiz / saturação.

Animações simples no SceneKit
Por fim, o SceneKit inclui um conjunto simples de ações para objetos 3D que permitem definir alterações de cena ao longo do tempo. O SceneKit também suporta ações descritas em JavaScript , mas este é um tópico para um artigo separado.

A interação de um gerador de partículas com um motor físico pode levar a um tornado!
Além dos gráficos, os principais recursos do SceneKit são o gerador de partículas e um mecanismo físico avançado que permite definir propriedades físicas reais para objetos e partículas comuns do gerador.
Um grande número de tutoriais detalhados foi escrito sobre todos esses recursos. Mas no processo de desenvolvimento, praticamente não aproveitamos essas oportunidades ...
Ei, não muito áspero
Uma vez eu escrevi um modelo de iluminação para jogos 3D melhor do que a luz do sol real, fornecendo um FPS aceitável na Nvidia 8800, mas decidi não liberar o mecanismo, porque Deus é bom comigo e não quero mostrar sua incompetência nesse assunto.
- John Carmack
Começaremos um estudo detalhado com uma tarefa bastante simples que surge para quase todo mundo que trabalha com seriedade com o SceneKit: como carregar um modelo com geometria complexa e materiais conectados, materiais, iluminação e até animações?
Existem várias maneiras, e todas elas têm seus prós e contras:
SCNScene (nomeado :) - obtém uma cena de um pacote,
SCNScene (url: options :) - carrega a cena por URL,
SCNScene (mdlAsset :) - converte uma cena de diferentes formatos,
SCNReferenceNode (url :) - carrega preguiçosamente a cena.
Obtenha a cena do pacote
Você pode usar o método padrão : coloque nosso modelo no formato dae ou scn no pacote scnassets e carregue-o a partir daí por analogia com UIImage (nomeado :).
Mas e se você quiser controlar a atualização dos modelos sem liberar uma atualização na App Store toda vez que precisar alterar algumas texturas? Ou suponha que você precise oferecer suporte a mapas e modelos criados pelo usuário. Ou - você simplesmente não deseja aumentar o tamanho do aplicativo, pois os gráficos 3D não são a principal funcionalidade.
Carregando a cena por URL
Você pode usar o construtor de cena a partir da URL do arquivo scn. Esse método suporta o download não apenas do sistema de arquivos, mas também da rede, mas, no último caso, você pode esquecer a compactação. Além disso, você precisa converter o modelo para o formato scn antecipadamente. É claro que você pode usar dae, mas com isso vem um conjunto de restrições. Por exemplo, a falta de renderização com base fisicamente.
A principal vantagem desse método é que ele permite que você defina com flexibilidade as configurações de importação . Você pode, por exemplo, modificar o ciclo de vida das animações e fazê-las repetir sem parar. Você pode especificar explicitamente a fonte para carregar recursos externos, como texturas, converter a orientação e a escala da cena, criar normais ausentes para a geometria, mesclar toda a geometria da cena em um nó grande ou descartar todos os elementos da cena que não estejam em conformidade com o padrão de formato.
A terceira opção é usar o construtor com o MDLAsset . Ou seja, primeiro criamos um conjunto de MDLAs , disponível na estrutura ModelIO, e depois passamos para o construtor da cena.
Essa opção é boa, pois permite baixar muitos formatos diferentes. Oficialmente, o MDLAsset pode carregar os formatos obj, ply, stl e usd, mas, após ter executado uma lista de todos os formatos possíveis, pelo menos de alguma forma relacionados à computação gráfica, encontrei mais quatro: abc, bsp, vox e md3, mas eles podem não ser totalmente suportados ou não em todos os sistemas e, para eles, é necessário verificar a correção da importação.
Também é necessário considerar que esse método possui uma sobrecarga para conversão e usá-lo com muito cuidado.
Esses métodos têm uma armadilha comum: eles retornam SCNScene, não SCNNode. A única maneira de adicionar conteúdo a uma cena existente é copiar todos os nós filhos e - você pode pular facilmente esta etapa - animações do nó raiz (por exemplo, elas podem aparecer lá ao trabalhar com dae). Além disso, é necessário considerar que na cena só pode haver um ambiente de textura (se você não usar shaders personalizados para reflexões).
Preguiçosamente carregando a cena
A quarta opção é usar SCNReferenceNode . Ele não retorna uma cena, mas um nó, que pode preguiçosamente (ou mediante solicitação) carregar em si toda a hierarquia da cena. Portanto, esse método é semelhante ao primeiro, mas oculta dentro de si todos os problemas de cópia.
Ele tem uma coisa, mas: os parâmetros globais da cena estão perdidos.
Acontece que esta é a maneira mais fácil e rápida de baixar seu modelo, mas se você precisar de ajuste de arquivo, o primeiro método será melhor.
Como resultado, decidimos pela primeira opção, pois era mais conveniente trabalhar no formato scn e para designers - converter do formato dae para ele. Além disso, precisávamos de animações de ajuste de arquivo na inicialização.
Otimizações nada precoces
Tendo mexido nesse processo por um longo tempo, posso lhe dar alguns conselhos.
A dica mais importante é converter arquivos para scn com antecedência. Então, você pode, ao abrir o arquivo no editor de cenas embutido no Xcode, ver como o seu objeto ficará no SceneKit.
Além disso, de fato, o arquivo scn é apenas uma representação binária da cena; portanto, o carregamento dele levará menos tempo. Para o mesmo dia, você deve primeiro analisar o xml e depois converter todas as malhas, animações e materiais. Além disso, a conversão de animações e materiais é uma fonte potencial de problemas. Recordamos a falta de suporte à PBR em dae: acontece que, se você quiser usá-lo, precisará alterar o tipo de todos os materiais após a conversão e colocar manualmente as texturas apropriadas.
Com esta operação, você pode obter um efeito colateral muito útil: compactação significativa de textura. Basta abri-los no "View" e exportar, alterando o formato para heic. Em média, essa operação simples economizou 5 megabytes por modelo.
Além disso, se você estiver baixando uma cena da Internet, posso aconselhá-lo a fazer o download no arquivo, descompactá-lo e transferir o URL do arquivo scn descompactado. Isso economizará a você e ao usuário megabytes extras - o que, por sua vez, acelerará o download e também reduzirá o número de pontos de falha. Concordo: fazer uma solicitação separada para cada recurso externo e mesmo na Internet móvel não é a melhor maneira de aumentar a confiabilidade.
Me machucou bastante
Quando dirijo um carro, ouço frequentemente o disco rígido do universo estalar, carregando a próxima rua.
- John Carmack
Assim, quando o trabalho de carregar e importar modelos é iniciado, surge uma nova tarefa: adicionar vários efeitos e recursos à cena. E acredite, há algo a dizer. Começamos examinando as várias constantes no SceneKit.

As restrições no SceneKit são consideradas imediatamente após a física. E antes de renderizar o quadro
Restrições, você diz? Quais são as constantes? Poucas pessoas sabem, e mais ainda falam sobre isso, mas o SceneKit tem seu próprio conjunto de constantes. E, embora não sejam tão flexíveis quanto as constantes no UIkit, você ainda pode fazer muitas coisas interessantes com elas.

SCNReplicatorConstraint
Vamos começar com uma constante simples - SCNReplicatorConstraint . Tudo o que ele faz é duplicar a posição, rotação e tamanho de outro objeto com compensações adicionais. Como em todas as outras constantes, ele pode alterar a força e definir a bandeira da incrementalidade. Ambos os parâmetros podem ser melhor exibidos nesta constante.

Força reduzida 10 vezes
A força afeta quanta transformação é aplicada ao objeto. E como a posição do objeto alvo muda a cada quadro - o objeto sombra se aproxima de um décimo da diferença de distância. Por esse motivo, um efeito de atraso aparece.

Incremento aumentado e força reduzida em 10 vezes
A incrementalidade , por sua vez, afeta se a constante é cancelada após a renderização. Suponha que desligamos. Então, vemos que em cada quadro, a constante é aplicada antes da renderização e, após a renderização, é cancelada e, portanto, cada quadro é repetido. Como resultado, combinando esses dois parâmetros, você pode obter um efeito bastante interessante dos ponteiros do relógio.

O avião sempre fica de frente para a câmera.
Vamos para uma constante mais interessante: o chamado outdoor.
Suponhamos, é necessário que algum objeto esteja sempre "nos encarando". Para fazer isso, basta usar SCNBillboardConstraint , indicar em quais eixos o objeto pode girar. Além disso, antes de calcular cada quadro (após um passo com a física), as posições e orientações de todos os objetos serão atualizadas para satisfazer todas as constantes.
Aqui você pode mencionar a restrição de aparência : é semelhante a um outdoor, apenas o objeto pode ser definido de frente para qualquer outro objeto na cena, em vez da câmera atual.
O que pode ser feito com a ajuda deles? Obviamente, na maioria das vezes essas constantes são usadas para desenhar árvores ou objetos pequenos. Eles também criam efeitos especiais como fogo ou explosão. Além disso, com a ajuda deles, você pode fazer a câmera seguir o objeto no palco.

Mantém a distância entre os objetos
SCNDistanceConstraint permite definir a distância mínima e / ou máxima para a posição de outro objeto. E sim, você pode usá-lo para fazer uma cobra. :) Essa restrição também pode ser usada para ligar a câmera ao personagem, embora a posição da câmera seja geralmente mais complicada, e descrevê-la apenas com construções não é uma tarefa fácil. O mesmo efeito pode ser alcançado adicionando uma mola no mecanismo físico, mas essa mola pode ser suplementada com uma tensão, caso você precise evitar problemas com esticamento ou compressão excessiva da mola.
Muitos viram em alguns Hitman, Fallout ou Skyrim: você arrasta um corpo com você, ele toca em um obstáculo - e começa a se comportar como se um demônio tivesse entrado nele. Essa constante ajudaria a evitar esses erros.

SCNSliderConstraint
SCNSliderConstraint permite definir a distância mínima entre um determinado objeto e os corpos físicos com uma máscara de colisão adequada. Constante bastante engraçada, mas, novamente, eles tentam simulá-lo usando interação física. A idéia principal é definir o raio da zona morta com corpos físicos para um objeto que não possui um corpo físico.

Cinemática inversa no trabalho
SCNIKConstraint é a constante mais interessante, mas também a mais complexa, que usa a chamada cinemática inversa. Usando uma cadeia de nós pais, a cinemática inversa tenta iterativamente trazer a posição do nó à qual você aplica essa constante ao ponto necessário. De fato, permite que você não pense em qual posição o ombro e o antebraço devem estar, mas simplesmente defina a posição da mão e os possíveis ângulos de rotação dos nós de conexão. O resto será contado para você. A principal desvantagem dessa restrição é que ela permite definir apenas a posição da mão, mas não sua orientação, e as restrições nos ângulos podem ser globalizadas, sem quebrar os eixos.
Então, nos reunimos em detalhes com as constantes e com o que elas sabem fazer. Vamos continuar a explorar efeitos interessantes. Lidaremos com o efeito das sombras.

Existe um avião, mas não é
Parece que poderia ser mais fácil em um mecanismo que suporta sombras do que criar sombras? Mas, às vezes, as sombras precisam ser projetadas em um plano completamente transparente. Isso é muito útil no ARKit, pois a imagem da câmera é exibida atrás do avião e a sombra deve ser projetada em algum lugar. O truque acaba sendo bastante simples: primeiro você precisa ativar sombras adiadas e desativar a gravação em todos os componentes do plano na guia material, e a sombra continuará sobrepondo-a. O único problema é que este plano irá sobrepor os objetos por trás dele.
Mas as sombras não são o único efeito pouco estudado no SceneKit. Vamos lidar com espelhos agora.

Espelho SCNFloor - o que poderia ser mais simples
Todo mundo que brincou com o SceneKit provavelmente conhece o scnfloor, que adiciona reflexos no espelho ao chão. Mas, por alguma razão, muito poucos o usam para reflexões honestas, porque você pode colocar seu modelo na geometria do piso, inclinar um pouco e transformá-lo ... em um espelho comum.


Gotejamento em vidro e um espelho curvo
Mas, o que é ainda menos conhecido, um mapa normal pode ser definido para esse gênero. Por esse motivo, por sua vez, você pode criar muitos efeitos interessantes diferentes, como o efeito de listras ou um espelho curvo.
Ultra-violeta
Certa vez, beijei uma garota de olhos abertos. A menina cortou o rosto com o plano de recorte próximo. Desde então, beijo apenas com os olhos fechados.
- John Carmack
Sombras, espelhos - efeitos interessantes. Mas há um efeito que, quando usado com habilidade, pode se tornar ainda mais interessante - texturas de vídeo.


Vídeos comuns e mapas de altura
Você pode precisar deles apenas para mostrar o vídeo dentro do jogo. Mas é muito mais interessante que, com a ajuda das texturas de vídeo, você pode modificar a geometria. Para fazer isso, você precisa colocar a textura do vídeo com um mapa de altura na propriedade de deslocamento do seu material e usar o material em um plano com um número suficientemente grande de segmentos . Resta entender como colocá-lo lá.
Mencionei na descrição do processo de criação de cena que você pode usar o SKScene como uma propriedade material, e esta é uma cena do SpriteKit. SpriteKit é como SceneKit, mas para gráficos 2D. Ele suporta a exibição de vídeos usando o SKVideoNode . Você só precisa colocar o SKVideoNode no SKScene e o SKScene no SCNMaterialProperty e pronto.
Mas depois de exportar a cena 3D resultante e abri-la em outro lugar, veremos um quadrado preto. Vasculhando o arquivo scn, encontrei o motivo. Acontece que, ao salvar um código de vídeo, ele não salva o URL do vídeo. Parece que você toma e governa. Mas nem tudo é tão simples: o arquivo scn é o chamado plist binário, que contém o resultado do NSKeyedArchiver. E o material, que é a cena do SpriteKit, é o mesmo código binário, que, ao que parece, já está dentro de outro código binário! É bom que haja apenas dois níveis de aninhamento.
Bem, agora vamos até o efeito, mas para uma ferramenta que permite criar qualquer tipo de efeito. Estes são modificadores de sombreador.
Antes de modificar algo, você precisa entender o que estamos modificando. Um sombreador, por definição, é um programa para a GPU que é executado para cada vértice e para cada pixel. Assim, um sombreador é um programa que determina a aparência de um objeto na tela.
Bem, modificadores de shader permitem alterar os resultados dos shaders padrão para GLSL ou Metal Shading Language. Eles também estão disponíveis em um editor visual, que permite ver alterações no modificador em tempo real.


Mapeamento de peles e paralaxe
Com a ajuda de modificadores de sombreador, você pode criar efeitos visuais complexos. Por exemplo, alguns dos efeitos mais famosos: mapeamento de peles e paralaxe .
#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;

Fundição de raios com cáusticos em tempo real
Mais interessante, ninguém se incomoda em jogar completamente fora os resultados de seu trabalho e escrever seu próprio renderizador. Por exemplo, você pode tentar implementar o Ray Casting em shaders. E tudo isso funciona rápido o suficiente para fornecer 30 FPS, mesmo em cálculos tão complexos. Mas este é um tópico para um relatório separado. Vamos Mobius !
Pesadelo!
Eu não gosto de piscar, porque as pálpebras fechadas carregam fortemente a GPU para BDPT devido à falta de iluminação.
- John Carmack
Então, temos um monte de objetos com efeitos interessantes. Agora resta aprender a gravá-las. Para fazer isso, vamos para um tópico mais complexo: como aprendemos a gravar vídeos diretamente do SceneKit sem uma interface de usuário externa e como otimizamos essa gravação dezenas de vezes.
Vamos primeiro procurar a solução mais simples: ReplayKit . Descubra por que ele não se encaixa. De um modo geral, esta solução permite criar uma entrada de tela em várias linhas de código e salvá-la através da visualização do sistema. Mas Ele tem muito menos - ele grava tudo, toda a interface do usuário, incluindo todos os botões na tela. Esta foi a nossa primeira decisão, mas por razões óbvias, era impossível deixá-lo em produção: os usuários tinham que compartilhar o vídeo e não da visualização do sistema.
Nos encontramos em uma situação em que a solução precisava ser escrita do zero. Absolutamente do zero. Então, vamos ver como no iOS você pode criar seu próprio vídeo e gravar seus quadros lá. Tudo é bem simples:

Processo de gravação
Precisamos criar uma entidade que registre arquivos - AVAssetWriter , adicione um fluxo de vídeo - AVAssetWriterInput a ele e crie um adaptador para esse fluxo que converta nosso buffer de pixel no formato exigido pelo fluxo - AVAssetWriterPixelBufferAdaptor .
Só por precaução, lembro que o buffer de pixels é uma entidade, que é um pedaço de memória em que os dados dos pixels são de alguma forma gravados. Esta é essencialmente uma representação de baixo nível da imagem.
Mas como obter esse buffer de pixel? A solução é simples. O SCNView possui uma maravilhosa função .snapshot () que retorna uma UIImage. Nós apenas precisamos criar um buffer de pixel a partir desta 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)
Nós apenas alocamos um lugar na memória, descrevemos qual o formato desses pixels, bloqueamos o buffer para alterar, obtemos o endereço de memória, criamos um contexto no endereço recebido, onde descrevemos como os pixels são compactados, quantas linhas existem na imagem e que espaço de cores usamos. Em seguida, copiamos os pixels da UIImage lá, conhecendo o formato final e desbloqueamos a alteração.

Agora você precisa fazer isso em todos os quadros. Para fazer isso, criamos um link de exibição que chamará um retorno de chamada para cada quadro, onde, por sua vez, chamaremos o método snapshot e criaremos um buffer de pixel a partir da imagem. Tudo é simples!

Mas não. Essa solução, mesmo em telefones poderosos, causa atrasos terríveis e rebaixamentos de FPS. Vamos fazer a otimização.

Digamos que não precisamos de 60 FPS. Nós ficaremos satisfeitos com o dia 25. Mas qual é a maneira mais fácil de alcançar esse resultado? Obviamente, você só precisa colocar tudo isso em um thread de segundo plano. Além disso, de acordo com os desenvolvedores, essa função é segura para threads.

Hmm, o atraso ficou menor, mas o vídeo parou de gravar ...
Tudo é simples. Como se costuma dizer, se você tiver um problema e o resolverá com a ajuda de vários threads, terá 2 problemas.
Se você tentar gravar um buffer de pixels com um carimbo de data e hora menor que o último gravado, o vídeo inteiro será inválido.

Não vamos escrever um novo buffer até que a gravação anterior termine.

Hmm, ficou muito melhor Mas, mesmo assim, por que os atrasos apareceram inicialmente?

Acontece que a função .snapshot () , com a qual obtemos uma imagem da tela, cria um novo renderizador para cada chamada, desenha um quadro a partir do zero e o devolve, não a imagem que está na tela. Isso leva a efeitos divertidos. Por exemplo, a simulação física é duas vezes mais rápida.
Mas espere - por que tentamos renderizar um novo quadro toda vez? Certamente em algum lugar você pode encontrar o buffer que é exibido na tela. De fato, existe acesso a esse buffer, mas é muito discreto. Precisamos obter o CAMetalDrawable do Metal.
Infelizmente, chegar ao Metal diretamente do SCNView não é tão fácil por uma razão bastante compreensível - no SceneKit, você pode escolher o tipo de API, mas se você olhar por baixo do capô e ver a camada , poderá ver que ele age como ele, no caso do Metal, CAMetalLayer .
Mas aqui também a falha nos espera: no CAMetalLayer, a única maneira de interagir com a visualização é a função nextDrawable, que retorna um CAMetalDrawable desocupado. Entende-se que você gravará dados nele e chamará a função atual, que os exibirá na tela.
A solução realmente existe. O fato é que, depois de desaparecer da tela, o buffer não é desalocado, mas apenas colocado de volta no pool. De fato, por que alocar memória toda vez que dois ou três buffers são suficientes: um é mostrado na tela, o segundo para renderização e o terceiro, por exemplo, para pós-processamento, se você tiver um.
Acontece que, após exibir o buffer, os dados dele não desaparecem em lugar algum e você pode acessá-los com segurança.
E se nós, no sucessor, começarmos em resposta a cada chamada para nextDrawable () para salvá-lo, obteremos quase o que precisamos. O problema é que o CAMetalDrawable salvo é aquele em que a imagem está sendo desenhada agora.
O salto para a solução real é muito simples - salvamos o Drawable atual e o anterior.
E aqui está, pronto - acesso direto à memória através do 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)
Portanto, agora não criamos um contexto e desenhamos uma UIImage nele, mas copiamos um pedaço de memória para outro. Surge a pergunta: e o formato de pixel?
Não corresponde ao deviceColorSpace ... E não corresponde aos espaços de cores mais usados ...
Este é exatamente o ponto em que o autor de um dos lares públicos, que executa a mesma tarefa, quebrou . Todo mundo nem chegou aqui.

Bem, todos esses truques - por um filtro assustador?
Bem, não! No artigo sobre o ARKit, é possível mencionar que a imagem da câmera não usa o espaço de cores padrão, mas expandida. E até uma matriz de transformação do espaço de cores é apresentada. Mas por que se envolver na transformação, se você pode tentar gravar diretamente nesse formato? Resta descobrir qual é o formato dos 60 disponíveis ...

E então eu comecei a rebentar. Gravei três vídeos em diferentes transmissões com diferentes formatos, substituindo-os a cada gravação.
Como resultado, no formato quadragésimo, obtemos o nome. Acontece que não é outro senão kCVPixelFormatType_30RGBLEPackedWideGamut . Como eu não adivinhei?

Mas minha alegria durou até o primeiro testador. Eu não tinha palavras. Como Passei muito tempo procurando o formato certo. É bom que o problema seja localizado rapidamente - o bug foi reproduzido de forma estável e apenas no 6s e 6s Plus. Quase imediatamente depois disso, lembrei-me de que os monitores com suporte de ampla gama começaram a ser instalados apenas nos sétimos iPhones.
Mudando a gama ampla para o bom e velho 32RGBA, recebo um recorde de trabalho! Resta entender como determinar se o dispositivo suporta ampla gama. Existem iPads com diferentes tipos de tela e achei que com certeza você pode obter o tipo de tela ENUM do sistema. Revendo a documentação, eu achei - este é displayGamut no UITraitCollection .
Tendo entregue a montagem aos testadores, recebi notícias agradáveis deles - tudo funcionou sem atrasos, mesmo em dispositivos antigos!
Em conclusão, quero lhe dizer - faça gráficos 3D! Em nosso aplicativo, para o qual a realidade aumentada não é o principal caso de uso, no fim de semana, as pessoas percorreram mais de 2.000 quilômetros, assistiram a mais de 3.000 objetos e gravaram mais de 1.000 vídeos com eles! Imagine o que você pode fazer se você mesmo.