Three.js: Carregadores de ativos sem bloqueio

Criado em 11 jul. 2017  ·  53Comentários  ·  Fonte: mrdoob/three.js

Conforme discutido em https://github.com/mrdoob/three.js/issues/11301, um dos principais problemas que temos no WebVR, embora seja irritante em experiências não VR também, é bloquear o thread principal ao carregar ativos.

Com a implementação recente do link traversal no navegador, o carregamento sem bloqueio é essencial para garantir uma experiência de usuário satisfatória. Se você pular de uma página para outra e a página de destino começar a carregar ativos bloqueando o mainthread, ela bloqueará a função de renderização para que nenhum quadro seja enviado ao fone de ouvido e, após um pequeno período de tolerância, o navegador nos expulsará da RV e será necessário que o usuário retire o fone de ouvido, clique em entrar em VR novamente (é necessário um gesto do usuário para fazê-lo) e volte para a experiência.

Atualmente, podemos ver duas implementações de carregamento sem bloqueio de arquivos OBJ:

  • (1) Usando webworkers para analisar o arquivo obj e, em seguida, retornar os dados de volta ao thread principal WWOBJLoader :
    Aqui, a análise é feita simultaneamente e você pode ter vários trabalhadores ao mesmo tempo. A principal desvantagem é que, depois de carregar os dados, você precisa enviar a carga de volta ao thread principal para reconstruir as TRÊS instâncias de objetos e essa parte pode bloquear o thread principal:
    https://github.com/kaisalmen/WWOBJLoader/blob/master/src/loaders/WWOBJLoader2.js#L312 -L423
  • (2) Promessa de thread principal com análise adiada usando setTimeOut: Oculus ReactVR : Este carregador continua lendo linhas usando pequenos intervalos de tempo para evitar o bloqueio do thread principal chamando setTimeout: https://github.com/facebook/react-vr/blob/master /ReactVR/js/Loaders/WavefrontOBJ/OBJParser.js#L281 -L298
    Com essa abordagem, o carregamento será mais lento, pois estamos apenas analisando algumas linhas em cada intervalo de tempo, mas a vantagem é que, assim que a análise for concluída, teremos os TRÊS objetos prontos para uso sem qualquer sobrecarga adicional.

Ambos têm seus prós e contras e, honestamente, não sou um especialista em webworkers para avaliar essa implementação, mas é uma discussão interessante que, idealmente, levaria a um módulo genérico que poderia ser usado para portar os carregadores para uma versão sem bloqueio.

Alguma sugestão?

/ cc @ mikearmstrong001 @kaisalmen @delapuente @spite

Suggestion

Comentários muito úteis

Lançamento da primeira proposta de THREE.UpdatableTexture

Idealmente, ele deve fazer parte de qualquer THREE.Texture, mas eu exploraria essa abordagem primeiro.

Todos 53 comentários

Você pode ter um carregador Promise + trabalhador + incremental (um pouco como uma mistura de ambos os pontos)

Passe a URL de origem para o script de trabalho, busque os recursos, retorne uma estrutura de objetos transferíveis com os buffers, estruturas e até ImageBitmaps necessários; deve ser direto o suficiente para não precisar de muita sobrecarga de processamento do three.js.

Os dados de upload para a GPU estarão bloqueando de qualquer maneira, mas você pode construir uma fila para distribuir os comandos em quadros diferentes, via display.rAF. Os comandos podem ser executados um de cada vez por quadro, ou calcular o tempo médio da operação e executar tantos quantos forem "seguros" para executar na movimentação do quadro atual (algo semelhante a requestIdleCallback seria bom, mas não é amplamente suportado e é problemático em sessões WebVR). Também pode ser melhorado usando bufferSubData, texSubImage2D, etc.

O suporte para trabalhadores e objetos transferíveis é bastante sólido agora, especialmente em navegadores compatíveis com WebVR.

Olá a todos, tenho um protótipo disponível que pode ser do seu interesse neste contexto. Veja o seguinte ramo:
https://github.com/kaisalmen/WWOBJLoader/tree/Commons
Aqui, a parte de provisionamento de malha foi completamente separada de WWOBJLoader2 :
https://github.com/kaisalmen/WWOBJLoader/blob/Commons/src/loaders/WWLoaderCommons.js

WWLoaderCommons facilita a implementação de outros provedores de malha (carregadores de formato de arquivo). Basicamente, ele define como uma implementação de trabalhador da web deve fornecer dados de malha de volta ao thread principal e processá-los / integrá-los na cena. Veja o fornecedor de lixo de triângulo aleatório 😉 que serve como um demonstrador de tecnologia:
https://github.com/kaisalmen/WWOBJLoader/tree/Commons/test/meshspray
https://kaisalmen.de/proto/test/meshspray/main.src.html

Mesmo na implementação atual, WWOBJLoader2 depende de objetos transferíveis (ArrayBuffers / ByteBuffers) para fornecer os dados BufferedGeometry brutos para Mesh do trabalhador para o thread principal. Em termos de tempo, a criação de Mesh partir dos ByteBuffers fornecidos é insignificante. Sempre que uma malha maior é integrada à cena, no entanto, a renderização é interrompida (cópias de dados, ajustes de gráfico de cena ...!?). Este é sempre o caso, independentemente da fonte (corrija-me se eu estiver errado).
O modo "stream" de WWOBJLoader2 suaviza essas paradas, mas se uma única peça de malha de seu modelo OBJ pesar 0,5 milhão de vértices, a renderização irá pausar por um longo período de tempo.

Abri uma nova edição para detalhar o que exatamente fiz no ramo sob medida e por quê:
https://github.com/kaisalmen/WWOBJLoader/issues/11
O problema ainda é um esboço e os detalhes virão em breve.

Para oferecer alguns números, aqui está um perfil de desempenho de https://threejs.org/examples/webgl_loader_gltf2.html , carregando um modelo de 13 MB com texturas de 2048x2048.

screen shot 2017-07-11 at 10 11 42 pm

Nesse caso, a principal coisa que bloqueia o thread principal é o upload de texturas para a GPU e, pelo que eu sei, isso não pode ser feito em um WW .. ou o carregador deve adicionar texturas gradualmente ou o three.js deve lidar com isso internamente.

Para os curiosos, a parte final bloqueando o thread principal é a adição de um mapa de cubos do ambiente.

O principal objetivo do react-vr não é necessariamente ter o carregador mais otimizado em termos de tempo de relógio, mas não causar perdas de quadros repentinas e inesperadas à medida que o carregamento de novo conteúdo acontece. Qualquer coisa que possamos fazer para minimizar isso é benéfico para todos, mas especialmente para a RV.

As texturas são definitivamente um problema e um primeiro passo óbvio seria opcionalmente carregá-las de forma incremental - um conjunto de linhas por vez para uma textura grande. Como o upload está oculto para os programas clientes, será difícil para eles gerenciarem, mas eu faria tudo para que isso fosse exposto mais abertamente ao renderizador webgl para tirar a pressão do three.js

Para a análise gltf, normalmente vejo o bloqueio de 500 ms em meus testes, isso é significativo e eu prefiro uma abordagem incremental para todos os carregadores (que também devem ser clonáveis)

A premissa do React VR é encorajar conteúdo dinâmico fácil dirigido por um estilo web de modo a encorajar mais desenvolvedores, e isso dará mais ênfase ao aprimoramento do manuseio dinâmico. Na maioria das vezes, não sabemos quais ativos serão necessários no início de nossos aplicativos criados pelo usuário.

@kaisalmen Obrigado pelo link

No Elation Engine / JanusWeb, realmente fazemos toda a análise do nosso modelo usando um pool de threads de trabalho, o que funciona muito bem. Uma vez que os trabalhadores terminaram de carregar cada modelo, nós o serializamos usando object.toJSON() , enviamos para a thread principal com postMessage() , e então carregamos usando ObjectLoader.parse() . Isso remove a maioria das partes bloqueadoras do código do carregador - ainda há algum tempo gasto em ObjectLoader.parse() que provavelmente poderia ser otimizado, mas a interatividade geral e a velocidade de carregamento foram drasticamente aprimoradas. Como estamos usando um grupo de trabalhadores, também podemos analisar vários modelos em paralelo, o que é uma grande vitória em cenas complexas.

No lado da textura, sim, acho que algumas mudanças são necessárias na funcionalidade de upload de textura do three.js. Um uploader em partes usando texSubImage2D seria o ideal, então poderíamos fazer atualizações parciais de grandes texturas em vários quadros, como mencionado acima.

Eu ficaria mais do que feliz em colaborar nesta mudança, pois beneficiaria muitos projetos que usam Three.js como base

Acho que usar texSubImage2D é uma boa ideia.
Mas também porque o WebGL não carrega textura de forma assíncrona.
OpenGL e outras libs têm a mesma limitação?

E outra coisa que estou pensando é a compilação GLSL.
Isso vai derrubar o quadro? Ou rápido o suficiente e não precisamos nos preocupar?

Sim, isso também é um problema no OpenGL nativo - compilar sombreadores e enviar dados de imagem são operações síncronas / de bloqueio. É por isso que a maioria dos motores de jogo recomendam ou até mesmo forçam você a pré-carregar todo o conteúdo antes de iniciar o nível - geralmente é considerado um grande impacto no desempenho para carregar novos recursos, mesmo fora de um disco rígido, e aqui estamos tentando fazer de forma assíncrona na Internet ... na verdade, temos um problema mais difícil do que a maioria dos desenvolvedores de jogos, e teremos que recorrer ao uso de técnicas mais avançadas se quisermos ser capazes de transmitir novos conteúdos em tempo real.

Carregar texturas será menos problemático se usarmos a nova API ImageBitmap no futuro. Consulte https://youtu.be/wkDd-x0EkFU?t=82 .

BTW: Graças ao @spite , já temos um ImageBitmapLoader experimental no projeto.

@ Mugen87 na verdade já estou fazendo todas as minhas cargas de textura com ImageBitmap no Elation Engine / JanusWeb - definitivamente ajuda e vale a pena integrar no núcleo do Three.js, mas há duas despesas principais envolvidas no uso de texturas em WebGL - tempo de decodificação de imagem e tempo de upload da imagem - ImageBitmap só ajuda com o primeiro.

Isso reduz o tempo de bloqueio da CPU em cerca de 50% em meus testes, mas o upload de grandes texturas para a GPU, especialmente 2048x2048 e acima, pode facilmente levar um segundo ou mais.

Seria conveniente tentar o que @jbaicoianu está sugerindo. De qualquer forma, se optar pela alternativa do thread principal, essa parece uma combinação perfeita para requestIdleCallback em vez de setTimeout.

Eu concordo com todos vocês, acredito que a abordagem para carregar e analisar tudo no trabalhador, criar os objetos necessários de volta no thread principal (se for muito caro, pode ser feito em várias etapas) e, em seguida, incluir um carregamento incremental no renderizador.
Para um MVP, poderíamos definir um maxTexturesUploadPerFrame (por padrão infinito), e a renderização cuidará do carregamento do pool de acordo com esse número.
Nas iterações a seguir, poderíamos adicionar uma lógica, como @spite comentou, para medir a média e carregá-los automaticamente com base em um intervalo de tempo seguro antes do bloqueio. Isso pode ser feito inicialmente para cada textura como uma unidade, mas depois pode ser melhorado para fazer upload de blocos de forma incremental para texturas maiores.

requestIdleCallback seria bom, mas não é amplamente suportado e é problemático em sessões WebVR

Apesar de estar curioso sobre sua frase, o que você quer dizer com problemático?

Eu tenho um THREE.UpdatableTexture para atualizar texturas incrementalmente usando texSubImage2D, mas precisa de alguns ajustes de three.js. A ideia é preparar um PR para agregar suporte.

Em relação a requestIdleCallback (rIC):

  • primeiro, é compatível com Chrome e Firefox e, embora possa ser polyfilled facilmente, a versão polyfilled pode prejudicar um pouco o propósito.

  • segundo: da mesma forma que vrDisplay.requestAnimationFrame (rAF) precisa ser chamado em vez de window.rAF durante a apresentação, o mesmo se aplica a rIC, conforme discutido neste crbug . Isso significa que o carregador precisa estar ciente do display ativo atual o tempo todo, ou ele irá parar de disparar dependendo do que está sendo apresentado. Não é terrivelmente complicado, apenas adiciona mais complexidade à fiação dos carregadores (que, idealmente, deveriam apenas fazer seu trabalho, independentemente do estado de apresentação). Outra opção é ter a parte em threejs que executa tarefas incrementais no encadeamento principal para compartilhar a exibição atual; Acho que é muito mais fácil fazer agora com as últimas mudanças na RV em três meses.

Outra consideração: para poder fazer upload de uma grande textura em várias etapas usando texSubImage2D (256x256 ou 512x512), precisamos de um contexto WebGL2 para ter recursos de deslocamento e recorte. Caso contrário, as imagens devem ser pré-cortadas por meio de tela, basicamente lado a lado do lado do cliente antes de fazer o upload.

@Apesar de um bom ponto, não pensei em rIC não ser chamado durante a apresentação, a princípio pensei que deveríamos precisar de um display.rIC, mas acredito que o .rIC deve ser anexado à janela e ser chamado quando a janela ou o display forem ambos ociosos.
Eu acredito que não ouvi nada relacionado a isso nas discussões de especificações do webvr. @Kearwood talvez tenha mais informações, mas definitivamente é um problema que devemos abordar.

Ansioso para ver o seu PR UpdatableTexture! :) Mesmo que seja apenas um WIP, poderíamos mover parte da discussão para lá.

Talvez os loaders possam se tornar algo assim ...

THREE.MyLoader = ( function () {

    // parse file and output js object
    function parser( text ) {
        return { 'vertices': new Float32Array() }
    }

    // convert js object to THREE objects.
    function builder( data ) {
        var geometry = new THREE.BufferGeometry();
        geometry.addAttribute( new THREE.BufferAttribute( data.vertices, 3 );
        return geometry;
    }

    function MyLoader( manager ) {}
    MyLoader.prototype = {
        constructor: MyLoader,
        load: function ( url, onLoad, onProgress, onError  ) {},
        parse: function ( text ) {
            return builder( parser( text ) );
        },
        parseAsync: function ( text, onParse ) {
            var code = parser.toString() + '\nonmessage = function ( e ) { postMessage( parser( e.data ) ); }';
            var blob = new Blob( [ code ], { type: 'text/plain' } );
            var worker = new Worker( window.URL.createObjectURL( blob ) );
            worker.addEventListener( 'message', function ( e ) {
                onParse( builder( e.data ) );
            } );
            worker.postMessage( text );
        }
    }
} )();

Lançamento da primeira proposta de THREE.UpdatableTexture

Idealmente, ele deve fazer parte de qualquer THREE.Texture, mas eu exploraria essa abordagem primeiro.

@mrdoob vejo o mérito em ter exatamente o mesmo código canalizado para o trabalhador, parece tããão errado 😄. Eu me pergunto qual seria o impacto de serializar, borrar e reavaliar o roteiro; nada muito terrível, mas não acho que o navegador esteja otimizado para essas peculiaridades 🙃

Além disso, idealmente, a busca do próprio recurso aconteceria no trabalhador. E acho que o método parser () no navegador precisaria de um importScripts do próprio three.js.

Mas um único ponto para definir carregadores de sincronização / assíncrona seria arrasar!

@mrdoob a função builder pode ser completamente genérica e comum a todos os carregadores (WIP: https://github.com/kaisalmen/WWOBJLoader/blob/Commons/src/loaders/support/WWMeshProvider.js#LL215- LL367; Atualização: ainda não isolado em uma função). Se os dados de entrada forem restritos a objetos js puros sem referência a quaisquer objetos THREE (é isso que você tem em mente, certo?), Poderíamos construir código de trabalho serializável sem a necessidade de importações no trabalhador (que WWOBJLoader faz). Isso é fácil para geometria, mas os materiais / sombreadores (se definidos no arquivo) só poderiam ser criados no construtor e apenas ser descritos como JSON antes pelo parser .
Um trabalhador deve sinalizar cada nova malha e sua conclusão, eu acho. Pode ser alterado assim:

// parse file and output js object
function parser( text, onMeshLoaded, onComplete ) {
    ....
}
parse: function ( text ) {
    var node = new THREE.Object3d();
    var onMeshLoaded = function ( data ) {
        node.add( builder( data ) );
    };

    // onComplete as second callbackonly provided in async case
    parser( text, onMeshLoaded ) );
    return node;
},

Um utilitário de construtor de trabalhadores é útil + algum protocolo de comunicação genérico que não está contradizendo sua ideia de usar o analisador como está, mas precisa ser embrulhado, eu acho. Estado atual na evolução do WWOBJLoader: https://github.com/kaisalmen/WWOBJLoader/blob/Commons/src/loaders/support/WWMeshProvider.js#LL40 -LL133, enquanto as chamadas front-end são report_progress, meshData e complete.

Update2:

  • Você acha que o analisador deve ser sem estado? Está tudo bem para builder , mas pode fazer sentido definir alguns parâmetros para ajustar o comportamento de parser . Isso também implica que os parâmetros de configuração devem ser transferíveis para o trabalhador, independentemente da análise
  • Seria legal ter algo como uma função de execução que come um objeto de configuração genérico. Um diretor genérico poderia, então, alimentar qualquer carregador com instruções (isso agora está funcionando no branch Commons personalizado de WWOBJLoader , entre)
  • desenvolvimento contínuo: WWOBJLoader2 agora estende OBJLoader e substitui a análise. Portanto, temos ambos os limites de análise, mas em classes diferentes. Chega perto da proposta, mas ainda não está na linha. Algum código do analisador precisa ser unificado e, eventualmente, ambas as classes precisam ser fundidas

Por enquanto é isso. Feedback bem-vindo 😄

@mrdoob Gosto da ideia de compor o trabalhador a partir do código do carregador imediatamente. Minha abordagem atual apenas carrega todo o aplicativo combinado js e usa apenas diferentes pontos de entrada do thread principal, definitivamente não tão eficiente quanto ter workers compostos apenas com o código de que precisam.

Gosto da abordagem de usar um formato de transmissão reduzido para passar entre os workers, porque é fácil marcar esses TypedArrays como transferíveis ao passar de volta para o thread principal. Na minha abordagem atual, estou usando o método .toJSON() no trabalhador, mas então passo e substituo os arrays JS para vértices, UVs, etc. pelo tipo TypedArray apropriado e os marco como transferíveis ao chamar postar mensagem. Isso torna a análise / uso de memória um pouco mais leve no thread principal, ao custo de um pouco mais de processamento / uso de memória no trabalhador - é uma boa troca de se fazer, mas poderia ser mais eficiente introduzindo um novo formato de transmissão conforme você propõe, ou modificando .toJSON() para opcionalmente nos fornecer TypedArrays em vez de arranjos JS.

As duas desvantagens que vejo nessa abordagem simplificada são:

  • Precisa reescrever os carregadores de modelo existentes. Muitos carregadores usam TRÊS classes e funções com espaço de nomes integradas para realizar suas tarefas, então provavelmente teríamos que puxar algum conjunto mínimo de código three.js - e com trabalhadores isso fica complicado
  • O formato de transmissão deve capturar adequadamente uma hierarquia de objeto, bem como diferentes tipos de objeto (Malha, SkinnedMesh, Luz, Câmera, Object3D, Linha, etc.)

@spite Referente a "Além disso, o ideal é que a busca do próprio recurso aconteceria no trabalhador." - era esse o meu pensamento quando implementei pela primeira vez o carregador de ativos baseado em trabalhador para Elation Engine - Eu tinha um grupo de 4 ou 8 trabalhadores, e passaria a eles trabalhos assim que se tornassem disponíveis e, em seguida, os trabalhadores buscariam os arquivos, analisariam -los e devolvê-los ao tópico principal. No entanto, na prática, isso significava que os downloads bloqueariam a análise e você perderia os benefícios que obteria com o pipelining etc. se solicitasse todos de uma vez.

Assim que percebemos isso, adicionamos outra camada para gerenciar todos os nossos downloads de ativos e, em seguida, o downloader de ativos dispara eventos para nos informar quando os ativos ficam disponíveis. Em seguida, passamos esses dados para o pool de workers, usando transferíveis nos dados do arquivo binário para colocá-los no worker de forma eficiente. Com essa mudança, todos os downloads acontecem mais rápido, embora estejam no thread principal, e os analisadores podem executar o processamento completo, em vez de girar o dedo à espera dos dados. No geral, esta acabou sendo uma das melhores otimizações que fizemos em termos de velocidade de carregamento de ativos.

No tópico de carregamento de textura, eu construí uma prova de conceito de uma nova classe FramebufferTexture , que vem com um companheiro FramebufferTextureLoader . Este tipo de textura estende WebGLRenderTarget , e seu carregador pode ser configurado para carregar texturas em blocos fragmentados de um determinado tamanho e compô-los no framebuffer usando requestIdleCallback() .

https://baicoianu.com/~bai/three.js/examples/webgl_texture_framebuffer.html

Neste exemplo, basta selecionar um tamanho de imagem e um tamanho de tiles para iniciar o processo de carregamento. Primeiro, inicializamos a textura para vermelho puro. Começamos o download das imagens (têm cerca de 10 MB, então espere um pouco) e, quando terminarem, mudamos o fundo para azul. Neste ponto, começamos a analisar a imagem com createImageBitmap() para analisar o arquivo e, quando isso for feito, configuramos uma série de retornos de chamada inativos que contêm chamadas adicionais para createImageBitmap() que dividem a imagem em blocos de maneira eficiente . Esses blocos são renderizados no framebuffer ao longo de vários quadros e têm um impacto significativamente menor nos tempos de quadro do que fazer tudo de uma vez.

NOTA - Atualmente, o FireFox não parece implementar todas as versões de createImageBitmap e, no momento, está gerando um erro para mim ao tentar se dividir em blocos. Como resultado, esta demonstração atualmente só funciona no Chrome. Alguém tem uma referência para um roteiro de suporte de createImageBitmap no FireFox?

Há uma limpeza que preciso fazer, este protótipo é um pouco bagunçado, mas estou muito feliz com os resultados e assim que puder descobrir uma maneira de contornar os problemas entre navegadores (fallback de tela, etc), estou considerando usar isso como o padrão para todas as texturas em JanusWeb. O efeito de fade-in é bem legal também, e poderíamos até mesmo criar uma versão reduzida primeiro e, em seguida, carregar progressivamente os blocos de detalhes mais altos.

Há algum motivo relacionado ao desempenho ou ao recurso que alguém possa imaginar porque pode ser uma má ideia ter um framebuffer para cada textura na cena, ao invés de uma referência de textura padrão? Não consegui encontrar nada sobre max. framebuffers por cena, pelo que eu posso dizer uma vez que um framebuffer foi configurado, se você não estiver renderizando para ele, então é o mesmo que qualquer outra referência de textura, mas tenho a sensação de que estou perdendo algo óbvio quanto a por que isso seria uma ideia realmente ruim :)

@jbaicoianu re: createImageBitmap do firefox, a razão é que eles não suportam o parâmetro de dicionário, então não suportam orientação de imagem ou conversão de espaço de cores. isso torna a maioria dos aplicativos da API bastante inúteis. Registrei dois bugs relacionados ao problema: https://bugzilla.mozilla.org/show_bug.cgi?id=1367251 e https://bugzilla.mozilla.org/show_bug.cgi?id=1335594

@apesar de ter sido o que pensei também, vi esse bug sobre não oferecer suporte ao dicionário de opções - mas, neste caso, nem estou usando isso, estou apenas tentando usar as opções x, y, w, h. O erro específico que estou recebendo é:

Argument 4 of Window.createImageBitmap '1024' is not a valid value for enumeration ImageBitmapFormat.

O que é confuso, porque não vejo nenhuma versão de createImageBitmap na especificação que leva ImageBitmapFormat como argumento.

Há algum motivo relacionado ao desempenho ou ao recurso que alguém possa imaginar porque pode ser uma má ideia ter um framebuffer para cada textura na cena, ao invés de uma referência de textura padrão? Não consegui encontrar nada sobre max. framebuffers por cena, pelo que eu posso dizer uma vez que um framebuffer foi configurado, se você não estiver renderizando para ele, então é o mesmo que qualquer outra referência de textura, mas tenho a sensação de que estou perdendo algo óbvio quanto a por que isso seria uma ideia realmente ruim :)

@jbaicoianu THREE.WebGLRenderTarget mantém um framebuffer, uma textura e um buffer de renderização. Quando você tiver a textura montada, você pode deletar o framebuffer e o buffer de renderização e apenas manter a textura. Algo assim deve fazer isso (não testado):

texture = target.texture;
target.texture = null; // so the webgl texture is not deleted by dispose()
target.dispose();

@wrr é bom saber, obrigado. Eu definitivamente tenho que fazer uma transferência de eficiência de memória nisso também - ela inevitavelmente trava em algum ponto se você alterar os parâmetros o suficiente, então eu sei que há alguma limpeza que não estou fazendo ainda. Quaisquer outras dicas como esta seriam muito apreciadas.

@mrdoob e @jbaicoianu Esqueci de dizer que também gostei da ideia. 😄
Organizei o código (init retrabalhado, objeto de instruções do trabalhador, manipulação de multi-callback lixo substituído, descrição de recurso comum, etc.) de OBJLoader e WWOBJLoader e todos os exemplos ( código ). Os dois carregadores agora estão prontos para serem combinados. Eles estarão de acordo com o seu plano, espero que em algum momento da próxima semana, dependendo do meu tempo livre:
Teste WWOBJLoader2 direcionado:
https://kaisalmen.de/proto/test/wwparallels/main.src.html
Usuário direcionado do genérico WorkerSupport :
https://kaisalmen.de/proto/test/meshspray/main.src.html
O grande teste de arquivo OBJ compactado:
https://kaisalmen.de/proto/test/wwobjloader2stage/main.src.html

Vou atualizar os exemplos acima com o código mais recente quando disponível e informá-lo.
Atualização 30-07-2017: OBJLoader2 e WWOBJLoader2 agora usam analisadores idênticos. Eles passam dados para a função de construtor comum diretamente ou do trabalhador.
Atualização 31/07/2017: WWOBJLoader2 se foi. OBJLoader2 fornece parse e parseAsync , load e run (alimentação por LoaderDirector ou manualmente)

Atualização 09/08/2017:
Atualização movida para uma nova postagem.

OBJLoader2 é assinatura e comportamento compatível novamente com OBJLoader (eu quebrei isso durante a evolução), OBJLoader2 fornece parseAsync e load com useAsync Além disso, a bandeira
https://github.com/kaisalmen/WWOBJLoader/tree/V2.0.0-Beta/src/loaders

Extraí as classes LoaderSupport (independentes do OBJ) que servem como utilitários e ferramentas de suporte necessárias. Eles podem ser reutilizados para carregadores baseados em outros trabalhadores em potencial. Todo o código abaixo, coloquei no namespace THREE.LoaderSupport para destacar sua dependência de OBJLoader2 :

  • Builder : Para construção de malha geral
  • WorkerDirector : Cria carregadores via reflexão, processa PrepData na fila com a quantidade configurada de trabalhadores. Usado para automatizar totalmente os carregadores (demonstração MeshSpray e Parallels)
  • WorkerSupport : classe de utilitário para criar trabalhadores a partir do código existente e estabelecer um protocolo de comunicação simples
  • PrepData + ResourceDescriptor : Descrição usada para automação ou simplesmente para descrição unificada entre exemplos
  • Commons : Possível classe base para carregadores (agrupa parâmetros comuns)
  • Callbacks : (onProgress, onMeshAlter, onLoad) usado para automação e direção e LoadedMeshUserOverride é usado para fornecer informações de onMeshAlter (adição de normais no teste objloader2 abaixo)
  • Validator : verificações de variáveis ​​nulas / indefinidas

@mrdoob @jbaicoianu OBJLoader2 agora envolve um analisador conforme sugerido (ele é configurado com parâmetros definidos globalmente ou recebidos por PrepData para execução). O Builder recebe cada malha bruta e o analisador retorna o nó base, mas fora isso ele corresponde ao blueprint.
Ainda existe algum código auxiliar em OBJLoader2 para serialização do analisador que provavelmente não é necessário.
O Construtor precisa de limpeza, pois o objeto de contrato / parâmetro para a função buildMeshes ainda é fortemente influenciado pelo carregamento do OBJ e, portanto, ainda é considerado em construção.

O código precisa de algum polimento, mas está pronto para feedback, discussão, crítica, etc ... 😄

Exemplos e testes

OBJ Loader usando run and load:
https://kaisalmen.de/proto/test/objloader2/main.src.html
OBJ Loader usando run async e parseAsync:
https://kaisalmen.de/proto/test/wwobjloader2/main.src.html
Uso direcionado de execução assíncrona OBJLoader2:
https://kaisalmen.de/proto/test/wwparallels/main.src.html
Uso direcionado de WorkerSupport genérico:
https://kaisalmen.de/proto/test/meshspray/main.src.html
O grande teste de arquivo OBJ compactado:
https://kaisalmen.de/proto/test/wwobjloader2stage/main.src.html

Parece bom! Você está ciente dessas mudanças em OBJLoader ? # 11871 565c6fd0f3d9b146b9434e5fccfa2345a90a3842

Sim, eu preciso portar isso. Propus algumas medições de desempenho reproduzíveis. Começará a trabalhar em ambos neste fim de semana. Quando você planeja lançar o r87? O suporte N-gon pode fazer isso dependendo da data.

@mrdoob e voila: https://github.com/mrdoob/three.js/pull/11928 suporte n-gon 😄

Atualização de status ( código ):
Os trabalhadores criados agora podem configurar qualquer analisador dentro do trabalhador por meio de parâmetros recebidos por uma mensagem. WorkerSupport fornece uma implementação de executor de trabalho de referência ( código ) que pode ser completamente substituída por seu próprio código, se desejado ou se for necessário.
O trabalhador irá criar e executar o analisador no método run do WorkerRunnerRefImpl ( Parser está disponível dentro do escopo do trabalhador; this.applyProperties chama setters ou propriedades de o analisador):

WorkerRunnerRefImpl.prototype.run = function ( payload ) {
    if ( payload.cmd === 'run' ) {

        console.log( 'WorkerRunner: Starting Run...' );

        var callbacks = {
            callbackBuilder: function ( payload ) {
                self.postMessage( payload );
            },
            callbackProgress: function ( message ) {
                console.log( 'WorkerRunner: progress: ' + message );
            }
        };

        // Parser is expected to be named as such
        var parser = new Parser();
        this.applyProperties( parser, payload.params );
        this.applyProperties( parser, payload.materials );
        this.applyProperties( parser, callbacks );
        parser.parse( payload.buffers.input );

        console.log( 'WorkerRunner: Run complete!' );

        callbacks.callbackBuilder( {
            cmd: 'complete',
            msg: 'WorkerRunner completed run.'
        } );

    } else {

        console.error( 'WorkerRunner: Received unknown command: ' + payload.cmd );

    }
};

A mensagem de OBJLoader2.parseAsync tem esta aparência:

this.workerSupport.run(
    {
        cmd: 'run',
        params: {
            debug: this.debug,
            materialPerSmoothingGroup: this.materialPerSmoothingGroup
        },
        materials: {
            materialNames: this.materialNames
        },
        buffers: {
            input: content
        }
    },
    [ content.buffer ]
);

O objeto de mensagem é dependente do Loader, mas a configuração do Parser no trabalhador é genérica.
O código usado por exemplos vinculados na postagem anterior foi atualizado com o código mais recente.

Acho que a evolução do OBJLoader2 e a extração das funções de suporte chegaram a um ponto em que seu feedback é necessário. Quando todos os exemplos tiverem sido transferidos de seu repo para a filial acima, abrirei um PR com um resumo completo e solicitarei feedback

Para sua informação, aqui está um trabalho em andamento para que o ImageBitmapLoader use um trabalhador conforme discutido acima. Talvez mais interessante, alguns números rígidos sobre os resultados: https://github.com/mrdoob/three.js/pull/12456

createImageBitmap do firefox, a razão é que eles não suportam o parâmetro de dicionário, então não suporta orientação de imagem ou conversão de espaço de cor. isso torna a maioria dos aplicativos da API bastante inúteis.

Isto é um infortúnio. ☹️

@mrdoob Você tem um plano para mudar ImageLoader para ImageBitmapLoader em TextureLoader porque o ImageBitmap deve bloquear menos para fazer o upload para a textura? createImageBitmap() parece funcionar no FireFox até agora se passarmos apenas o primeiro argumento. (Talvez não precisemos passar o segundo e mais argumentos por meio de TextureLoader ?)

return createImageBitmap( blob );

Na verdade, é importante que createImageBitmap () suporte o dicionário de opções. Caso contrário, você não pode alterar coisas como a orientação da imagem (flip-Y) ou indicar alfa pré-multiplicado. O problema é que você não pode usar WebGLRenderingContext.pixelStorei para ImageBitmap . Das especificações :

_Se o TexImageSource for um ImageBitmap, esses três parâmetros (UNPACK_FLIP_Y_WEBGL, UNPACK_PREMULTIPLY_ALPHA_WEBGL, UNPACK_COLORSPACE_CONVERSION_WEBGL) serão ignorados. Em vez disso, o ImageBitmapOptions equivalente deve ser usado para criar um ImageBitmap com o formato desejado.

Portanto, acho que só podemos mudar para ImageBitmapLoader se o FF suportar o dicionário de opções. Além disso, propriedades como Texture.premultiplyAlpha e Texture.flipY não funcionam com ImageBitmap agora. Quero dizer, se os usuários os definirem, eles não afetarão uma textura baseada em ImageBitmap que é um tanto infeliz.

Ah ok. Eu perdi essa especificação.

A importância do dicionário de opções também é discutida aqui:

https://bugzilla.mozilla.org/show_bug.cgi?id=1335594

Os bugs no bugzilla (https://bugzilla.mozilla.org/show_bug.cgi?id=1367251, https://bugzilla.mozilla.org/show_bug.cgi?id=1335594) não foram alterados por ... dois anos agora? Eu não acho que eles levariam tanto tempo para consertar.

Portanto, o problema é que "tecnicamente" o recurso é compatível com o FF, mas na prática é inútil. Para usá-lo, poderíamos ter um caminho para o Chrome que o usa e outro para os outros navegadores que não. O problema é que, como o Firefox tem esse recurso, teríamos que fazer um sniffing de UA, o que é uma merda.

A solução prática é realizar a detecção de recursos: construir uma imagem 2x2 usando cIB com o sinalizador de flip e, em seguida, ler novamente e certificar-se de que os valores estão corretos.

Sobre os bugs do FireFox, também irei contatá-los internamente. Vamos ver se precisamos de uma solução alternativa depois de ouvirmos seu plano.

Os bugs no bugzilla (https://bugzilla.mozilla.org/show_bug.cgi?id=1367251, https://bugzilla.mozilla.org/show_bug.cgi?id=1335594) não foram alterados por ... dois anos agora? Eu não acho que eles levariam tanto tempo para consertar.

Sim, desculpe por isso, eu realmente não segui com isso por um tempo -_-

Portanto, o problema é que "tecnicamente" o recurso é compatível com o FF, mas na prática é inútil. Para usá-lo, poderíamos ter um caminho para o Chrome que o usa e outro para os outros navegadores que não. O problema é que, como o Firefox tem esse recurso, teríamos que fazer um sniffing de UA, o que é uma merda.

A solução prática é realizar a detecção de recursos: construir uma imagem 2x2 usando cIB com o sinalizador de flip e, em seguida, ler novamente e certificar-se de que os valores estão corretos.

Sim, eu concordo que ambas as soluções são realmente ruins e devemos tentar evitá-las, então antes de nos aprofundarmos em qualquer uma dessas, vamos ver se podemos desbloqueá-la do nosso lado

Fiz ImageBitmap teste de desempenho de upload. Enviando textura a cada 5 segundos.

Você pode comparar Regular Image vs ImageBitmap.

https://rawgit.com/takahirox/three.js/ImageBitmapTest/examples/webgl_texture_upload.html (imagem normal)
https://rawgit.com/takahirox/three.js/ImageBitmapTest/examples/webgl_texture_upload.html?imagebitmap (ImageBitmap)

Nas minhas janelas eu vejo

| Navegador | 8192x4096 JPG 4,4 MB | 2048x2048 PNG 4,5 MB |
| ---- | ---- | ---- |
| Imagem do Chrome | 500ms | 140ms |
| Chrome ImageBitmap | 165ms | 35ms |
| Imagem FireFox | 500ms | 40ms |
| FireFox ImageBitmap | 500ms | 60ms |

( texture.generateMipmaps é true )

Meus pensamentos

  1. Desempenho 3x melhor com ImageBitmap no Chrome. Melhoria muito boa.
  2. FireFox tem problema de desempenho de ImageBitmap agora? Você também pode tentar no seu computador? Testar no celular também é bem-vindo.
  3. Mesmo com ImageBitmap, o upload de textura parece ainda bloquear para texturas grandes. Talvez precisemos de uma técnica de upload parcial incremental ou algo para não bloquear.

Mesmo com ImageBitmap, o upload de textura parece ainda bloquear para texturas grandes. Talvez precisemos de uma técnica de upload parcial ou algo para não bloquear.

Acho que uma solução para esse problema pode ser o uso de um formato de compressão de textura e evitar JPG ou PNG (e, portanto, ImageBitmap ). Seria interessante ver alguns dados de desempenho neste contexto.

Sim combinado. Mas eu acho que provavelmente ainda vemos bloqueio para textura grande, especialmente em dispositivos de baixo consumo de energia, como os móveis. De qualquer forma, avalie o desempenho primeiro.

Ou use agendado / requestIdleCallback texSubImage2D

rIC = requestIdleCallback?

sim, eu fiz uma edição ninja

OK. Sim combinado.

Aliás, não estou familiarizado com textura compactada ainda. Deixe-me confirmar meu entendimento. Não podemos usar textura compactada com ImageBitmap porque compressedTexImage2D não aceita ImageBitmap , correto?

https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/compressedTexImage2D

Voltei para revisitar meus antigos experimentos do TiledTextureLoader - parece que agora estão fazendo com que meu driver de vídeo trave e reinicie :(

(editar: na verdade, parece que carregar a textura maior (16k x 16k - https://baicoianu.com/~bai/three.js/examples/textures/dotamap1_25.jpg) diretamente no cromo é o que está causando o travamento. Isso costumava funcionar bem, então parece haver alguma regressão no tratamento de imagens do Chrome)

Eu fiz alguns experimentos usando os geradores requestIdleCallback, ImageBitmap e ES6 para dividir uma grande textura em vários pedaços para enviar para a GPU. Eu usei um framebuffer em vez de uma textura normal, porque mesmo se você estiver usando texSubimage2D para preencher os dados da imagem, você ainda precisa pré-alocar a memória, o que requer o upload de um monte de dados vazios para a GPU, enquanto um framebuffer pode ser criado e inicializado com uma única chamada GL.

O repositório para essas mudanças ainda está disponível aqui https://github.com/jbaicoianu/THREE.TiledTexture/

Algumas notas do que me lembro dos experimentos:

  • requestIdleCallback definitivamente ajudou a reduzir o jank durante o carregamento de texturas, à custa de aumentar muito o tempo total de carregamento
  • Com um pouco de trabalho extra, isso poderia ser atenuado enviando primeiro uma versão reduzida da textura e, em seguida, preenchendo os dados de resolução plena em um ritmo mais lento
  • Os geradores ES6 ajudaram a tornar o código mais fácil de entender e mais fácil de escrever sem desperdiçar memória, mas provavelmente não são realmente necessários para isso

Meus resultados foram semelhantes: houve uma troca entre velocidade de upload e jankiness. (BTW eu criei este https://github.com/spite/THREE.UpdatableTexture).

Eu acho que para a segunda opção funcionar no WebGL 1, você realmente precisaria de duas texturas, ou pelo menos modificadores para as coordenadas UV. No WebGL 2, acho mais fácil copiar fontes com tamanhos diferentes da textura de destino.

Sim, com texSubImage2D acho que esse tipo de redimensionamento não seria possível, mas ao usar um framebuffer, estou usando uma OrthographicCamera para renderizar um plano com o fragmento de textura, então é só uma questão de mudar a escala do plano para essa chamada de empate.

Sobre o problema de desempenho do ImageBItmap no FireFox, abri um bug no bugzilla

https://bugzilla.mozilla.org/show_bug.cgi?id=1486454

Tenho tentado entender melhor quando os dados associados a uma textura são realmente carregados na GPU e se deparam com este tópico. Em meu caso de uso específico, NÃO estou preocupado com o carregamento e decodificação de arquivos jpeg / gif locais em texturas, estou apenas preocupado em tentar pré-carregar dados de textura na GPU. Depois de ler este tópico, devo confessar que não tenho certeza se trata de ambos os problemas ou apenas do primeiro? Visto que só me preocupo com o último, preciso procurar uma solução diferente ou há algo aqui que ajudará a forçar o carregamento dos dados de textura na GPU?

Esta página foi útil?
0 / 5 - 0 avaliações