Three.js: Cargadores de activos sin bloqueo

Creado en 11 jul. 2017  ·  53Comentarios  ·  Fuente: mrdoob/three.js

Como se comenta en https://github.com/mrdoob/three.js/issues/11301, uno de los principales problemas que tenemos en WebVR, aunque también es molesto en las experiencias que no son de realidad virtual, es bloquear el hilo principal mientras se cargan los activos.

Con la reciente implementación del cruce de enlaces en el navegador, la carga sin bloqueo es imprescindible para garantizar una experiencia de usuario satisfactoria. Si salta de una página a otra y la página de destino comienza a cargar activos que bloquean el hilo principal, bloqueará la función de renderizado, por lo que no se enviarán marcos a los auriculares y, después de un pequeño período de gracia, el navegador nos sacará de la realidad virtual. y requerirá que el usuario se quite los auriculares, haga clic en Enter VR nuevamente (se requiere un gesto de usuario para hacerlo) y regrese a la experiencia.

Actualmente podemos ver dos implementaciones de carga sin bloqueo de archivos OBJ:

  • (1) Uso de webworkers para analizar el archivo obj y luego devolver los datos al hilo principal WWOBJLoader :
    Aquí, el análisis se realiza simultáneamente y podría tener varios trabajadores al mismo tiempo. El principal inconveniente es que una vez que haya cargado los datos, debe enviar la carga útil al hilo principal para reconstruir las TRES instancias de objetos y esa parte podría bloquear el hilo principal:
    https://github.com/kaisalmen/WWOBJLoader/blob/master/src/loaders/WWOBJLoader2.js#L312 -L423
  • (2) Promesa del hilo principal con análisis diferido usando setTimeOut: Oculus ReactVR : este cargador sigue leyendo líneas usando espacios de tiempo pequeños para evitar bloquear el hilo principal llamando a setTimeout: https://github.com/facebook/react-vr/blob/master /ReactVR/js/Loaders/WavefrontOBJ/OBJParser.js#L281 -L298
    Con este enfoque, la carga será más lenta ya que solo estamos analizando algunas líneas en cada intervalo de tiempo, pero la ventaja es que una vez que se complete el análisis, tendremos los TRES objetos listos para usar sin ningún gasto adicional.

Ambos tienen sus pros y sus contras y, sinceramente, no soy un experto en webworkers para evaluar esa implementación, pero es una discusión interesante que idealmente conduciría a un módulo genérico que podría usarse para portar los cargadores a una versión sin bloqueo.

¿Alguna sugerencia?

/ cc @ mikearmstrong001 @kaisalmen @delapuente @spite

Suggestion

Comentario más útil

Primera propuesta de lanzamiento de THREE.UpdatableTexture

Idealmente, debería ser parte de cualquier TRES Textura, pero primero exploraría este enfoque.

Todos 53 comentarios

Puede tener un cargador basado en Promise + worker + incremental (un poco como una mezcla de ambos puntos)

Pase la URL de origen al script de trabajo, obtenga los recursos, devuelva una estructura de objetos transferibles con los búferes, estructuras e incluso ImageBitmaps necesarios; debería ser lo suficientemente sencillo como para no necesitar una gran cantidad de sobrecarga de procesamiento de three.js.

Los datos de carga a la GPU se bloquearán independientemente, pero puede crear una cola para distribuir los comandos en diferentes marcos, a través de display.rAF. Los comandos se pueden ejecutar uno a la vez por cuadro, o calcular el tiempo promedio de la operación y ejecutar tantos como sean "seguros" para ejecutar en el marco actual (algo similar a requestIdleCallback sería bueno, pero no es ampliamente compatible , y es problemático en las sesiones de WebVR). También se puede mejorar usando bufferSubData, texSubImage2D, etc.

El soporte para trabajadores y objetos transferibles es bastante sólido en este momento, especialmente en navegadores compatibles con WebVR.

Hola a todos, tengo disponible un prototipo que puede ser de su interés en este contexto. Vea la siguiente rama:
https://github.com/kaisalmen/WWOBJLoader/tree/Commons
Aquí, la parte de aprovisionamiento de malla se ha separado completamente de WWOBJLoader2 :
https://github.com/kaisalmen/WWOBJLoader/blob/Commons/src/loaders/WWLoaderCommons.js

WWLoaderCommons facilita la implementación de otros proveedores de malla (cargadores de formato de archivo). Básicamente, define cómo la implementación de un trabajador web debe proporcionar datos de malla al hilo principal y los procesa / integra en la escena. Vea el proveedor de basura de triángulo aleatorio 😉 que sirve como demostrador de tecnología:
https://github.com/kaisalmen/WWOBJLoader/tree/Commons/test/meshspray
https://kaisalmen.de/proto/test/meshspray/main.src.html

Incluso en la implementación actual, WWOBJLoader2 basa en objetos transferibles (ArrayBuffers / ByteBuffers) para proporcionar los datos sin procesar BufferedGeometry para el Mesh del trabajador al hilo principal. En cuanto al tiempo, la creación de Mesh partir de los ByteBuffers proporcionados es insignificante. Siempre que se integra una malla más grande en la escena, sin embargo, el renderizado se detiene (copias de datos, ajustes de gráficos de escena ...!?). Este es siempre el caso independientemente de la fuente (corríjame si me equivoco).
El modo de "flujo" de WWOBJLoader2 suaviza estas paradas, pero si una sola pieza de malla de su modelo OBJ pesa 0,5 millones de vértices, la renderización se detendrá durante un período de tiempo más largo.

Abrí un nuevo número para detallar qué hice exactamente en la rama personalizada y por qué:
https://github.com/kaisalmen/WWOBJLoader/issues/11
El problema sigue siendo un fragmento y los detalles se darán a conocer pronto.

Para ofrecer algunos números, aquí hay un perfil de rendimiento de https://threejs.org/examples/webgl_loader_gltf2.html , cargando un modelo de 13 MB con texturas de 2048x2048.

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

En este caso, lo principal que bloquea el hilo principal es cargar texturas a la GPU, y hasta donde yo sé, eso no se puede hacer desde un WW ... o el cargador debe agregar texturas gradualmente, o three.js debe manejarlo internamente.

Para los curiosos, el bloque final que bloquea el hilo principal es la adición de un mapa de cubos de entorno.

El objetivo principal de react-vr no es necesariamente tener el cargador más óptimo en términos de tiempo de reloj de pared, sino no causar salidas de marco repentinas e inesperadas a medida que se carga contenido nuevo. Todo lo que podamos hacer para minimizar esto es beneficioso para todos, pero especialmente para la realidad virtual.

Las texturas son definitivamente un problema y un primer paso obvio sería cargarlas opcionalmente de forma incremental: un conjunto de líneas a la vez para obtener una textura grande. Como la carga está oculta para los programas cliente, será difícil para ellos administrarla, pero estaría a favor de que esto se exponga más abiertamente al renderizador webgl para aliviar la presión de three.js

Para el análisis de gltf, comúnmente veo el bloqueo de 500ms en mis pruebas, esto es significativo y preferiría un enfoque incremental para todos los cargadores (que también debería ser clonable)

La premisa de React VR es fomentar el contenido dinámico fácil impulsado por un estilo web para alentar a más desarrolladores, y esto hará más énfasis en mejorar el manejo dinámico. La mayoría de las veces no sabemos qué activos serán necesarios al comienzo de nuestras aplicaciones creadas por el usuario.

@kaisalmen Gracias por el enlace

En Elation Engine / JanusWeb, en realidad hacemos todo nuestro análisis de modelos utilizando un grupo de subprocesos de trabajo, lo que funciona bastante bien. Una vez que los trabajadores han terminado de cargar cada modelo, lo serializamos usando object.toJSON() , lo enviamos al hilo principal con postMessage() , y luego lo cargamos usando ObjectLoader.parse() . Esto elimina la mayoría de las porciones de bloqueo del código del cargador; todavía hay algo de tiempo invertido en ObjectLoader.parse() que probablemente podría optimizarse, pero la interactividad general y la velocidad de carga mejoran drásticamente. Dado que estamos usando un grupo de trabajadores, también podemos analizar varios modelos en paralelo, lo cual es una gran ventaja en escenas complejas.

En el lado de la textura, sí, creo que se necesitan algunos cambios en la funcionalidad de carga de texturas de three.js. Un cargador fragmentado usando texSubImage2D sería ideal, entonces podríamos hacer actualizaciones parciales de texturas grandes en múltiples marcos, como se mencionó anteriormente.

Estaría más que feliz de colaborar en este cambio, ya que beneficiaría a muchos proyectos que usan Three.js como base.

Creo que usar texSubImage2D es una buena idea.
Pero también por qué WebGL no carga la textura de forma asincrónica.
¿OpenGL y otras bibliotecas tienen la misma limitación?

Y otra cosa que estoy pensando es la compilación GLSL.
¿Dejará caer el marco? ¿O lo suficientemente rápido y no necesitamos preocuparnos?

Sí, esto también es un problema en OpenGL nativo: compilar sombreadores y cargar datos de imágenes son operaciones sincrónicas / de bloqueo. Esta es la razón por la que la mayoría de los motores de juegos recomiendan o incluso te obligan a precargar todo el contenido antes de comenzar el nivel; generalmente se considera un impacto de rendimiento demasiado alto para cargar nuevos recursos incluso desde un disco duro, y aquí estamos tratando de hacerlo de forma asincrónica. a través de Internet ... en realidad tenemos un problema más difícil que la mayoría de los desarrolladores de juegos, y tendremos que recurrir al uso de técnicas más avanzadas si queremos poder transmitir contenido nuevo sobre la marcha.

Cargar texturas será menos problemático si usamos la nueva API ImageBitmap en el futuro. Consulte https://youtu.be/wkDd-x0EkFU?t=82 .

Por cierto: Gracias a @spite , ya tenemos un ImageBitmapLoader experimental en el proyecto.

@ Mugen87 en realidad ya estoy haciendo todas mis cargas de texturas con ImageBitmap en Elation Engine / JanusWeb; definitivamente ayuda y vale la pena integrarlo en el núcleo de Three.js, pero hay dos gastos principales relacionados con el uso de texturas en WebGL: tiempo de decodificación de imágenes y el tiempo de carga de la imagen: ImageBitmap solo ayuda con el primero.

Esto reduce el tiempo de bloqueo de la CPU en aproximadamente un 50% en mis pruebas, pero cargar texturas grandes a la GPU, especialmente 2048x2048 y superiores, puede llevar fácilmente un segundo o más.

Sería conveniente probar lo que sugiere @jbaicoianu . De todos modos, si opta por la alternativa del hilo principal, esto parece una combinación perfecta para requestIdleCallback en lugar de setTimeout.

Estoy de acuerdo con todos ustedes, creo que el enfoque para cargar y analizar todo en el trabajador, crear los objetos necesarios nuevamente en el hilo principal (si es muy costoso, podría hacerse en varios pasos) y luego incluir una carga incremental en el renderizador.
Para un MVP, podríamos definir un maxTexturesUploadPerFrame (por defecto infinito), y el render se encargará de cargar desde el grupo de acuerdo con ese número.
En las siguientes iteraciones podríamos agregar una lógica, como comentó @spite , para medir el promedio y subirlos automáticamente en base a un rango de tiempo seguro antes del bloqueo. Esto podría hacerse inicialmente para cada texturas como una unidad, pero luego podría mejorarse para subir trozos de manera incremental para obtener texturas más grandes.

requestIdleCallback sería bueno, pero no es ampliamente compatible y es problemático en las sesiones de WebVR

@spite Tengo curiosidad por tu frase, ¿a qué te refieres con problemática?

Tengo THREE.UpdatableTexture para actualizar texturas incrementalmente usando texSubImage2D, pero necesito un poco de ajuste de three.js. La idea es preparar un PR para agregar soporte.

Con respecto a requestIdleCallback (rIC):

  • En primer lugar, es compatible con Chrome y Firefox, y aunque se puede rellenar con polietileno fácilmente, la versión con relleno de polietileno podría frustrar un poco el propósito.

  • segundo: de la misma manera que se debe llamar a vrDisplay.requestAnimationFrame (rAF) en lugar de window.rAF cuando se presenta, lo mismo se aplica para rIC, como se explica en este crbug . Eso significa que el cargador debe estar al tanto de la pantalla activa actual en todo momento, o dejará de disparar dependiendo de lo que se presente. No es terriblemente complicado, solo agrega más complejidad al cableado de los cargadores (que idealmente deberían hacer su trabajo, independientemente del estado de presentación). Otra opción es tener la parte en threejs que ejecuta trabajos incrementales en el hilo principal para compartir la pantalla actual; Creo que ahora es mucho más fácil de hacer con los últimos cambios en la realidad virtual en threejs.

Otra consideración: para poder cargar una textura grande en varios pasos usando texSubImage2D (256x256 o 512x512), necesitamos un contexto WebGL2 para tener funciones de compensación y recorte. De lo contrario, las imágenes deben recortarse previamente a través de un lienzo, básicamente en mosaico del lado del cliente antes de cargarlas.

@spite Buen punto, no pensé en que no se llamara a rIC al presentar, al principio pensé que deberíamos necesitar un display.rIC pero creo que el .rIC debería estar adjunto a la ventana y ser llamado cuando la ventana o la pantalla están ambos inactivos.
Creo que no escuché nada relacionado con esto en las discusiones de especificaciones de webvr. @Kearwood tal vez tenga más información, pero definitivamente es un problema que debemos abordar.

¡Esperamos ver sus relaciones públicas de UpdatableTexture! :) Incluso si es solo un WIP, podríamos mover parte de la discusión allí.

Quizás los cargadores podrían convertirse en algo como esto ...

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 );
        }
    }
} )();

Primera propuesta de lanzamiento de THREE.UpdatableTexture

Idealmente, debería ser parte de cualquier TRES Textura, pero primero exploraría este enfoque.

@mrdoob veo el mérito de tener exactamente el mismo código canalizado al trabajador, simplemente se siente tan mal 😄. Me pregunto cuál sería el impacto de serializar, aplicar manchas y reevaluar el guión; nada demasiado terrible, pero no creo que el navegador esté optimizado para estas peculiaridades 🙃

Además, lo ideal sería que la recuperación del recurso en sí se produjera en el trabajador. Y creo que el método parser () en el navegador necesitaría un importScripts de three.js.

¡Pero un solo punto para definir cargadores sincronizados / asíncronos sería increíble!

@mrdoob, la función builder podría ser completamente genérica y común para todos los cargadores (WIP: https://github.com/kaisalmen/WWOBJLoader/blob/Commons/src/loaders/support/WWMeshProvider.js#LL215- LL367; Actualización: aún no aislado en una función). Si los datos de entrada están restringidos a objetos js puros sin referencia a ningún objeto THREE (eso es lo que tiene en mente, ¿verdad?), Podríamos construir código de trabajador serializable sin necesidad de importaciones en el trabajador (¿qué WWOBJLoader hace). Esto es fácil para Geometry, pero los materiales / sombreadores (si se definen en el archivo) solo se pueden crear en el constructor y solo se pueden describir como JSON antes mediante parser .
Un trabajador debería señalar cada nueva malla y su finalización, creo. Podría modificarse así:

// 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;
},

Una utilidad de constructor de trabajadores es útil + un protocolo de comunicación genérico que no contradice su idea de usar el analizador tal como está, pero creo que necesita algunos ajustes. Estado actual de la evolución de WWOBJLoader: https://github.com/kaisalmen/WWOBJLoader/blob/Commons/src/loaders/support/WWMeshProvider.js#LL40 -LL133, mientras que las llamadas de front-end son report_progress, meshData y completas.

Actualización2:

  • ¿Crees que el analizador debería ser apátrida? Está bien para el builder , pero podría tener sentido poder establecer algunos parámetros para ajustar el comportamiento del parser . Esto también implica que los parámetros de configuración deben ser transferibles al trabajador independientemente del análisis
  • Sería genial tener algo así como una función de ejecución que se come un objeto de configuración genérico. Un director genérico podría alimentar cualquier cargador con instrucciones (esto ahora funciona en la rama Commons personalizada de WWOBJLoader , por cierto)
  • desarrollo en curso: WWOBJLoader2 ahora extiende OBJLoader y anula el análisis. Entonces, tenemos ambos mayúsculas de análisis, pero en diferentes clases. Se acerca a la propuesta, pero todavía no está en la línea. Es necesario unificar parte del código del analizador y, finalmente, ambas clases deben fusionarse

Eso es todo por ahora. Comentarios bienvenidos 😄

@mrdoob Me gusta la idea de componer el trabajador a partir del código del cargador sobre la marcha. Mi enfoque actual solo carga toda la aplicación combinada js y solo usa un punto de entrada diferente del hilo principal, definitivamente no es tan eficiente como tener trabajadores compuestos solo con el código que necesitan.

Me gusta el enfoque de usar un formato de transmisión recortado para pasar entre trabajadores, porque es fácil marcar esos TypedArrays como transferibles cuando se pasa de nuevo al hilo principal. En mi enfoque actual, estoy usando el método .toJSON() en el trabajador, pero luego reviso y reemplazo las matrices JS para vértices, UV, etc.con el tipo TypedArray apropiado, y las marco como transferibles al llamar postMessage. Esto hace que el análisis / uso de memoria sea un poco más liviano en el hilo principal, a costa de un poco más de procesamiento / uso de memoria en el trabajador; es una buena compensación, pero podría hacerse más eficiente introduciendo un nuevo formato de transmisión que proponga, o modificando .toJSON() para darnos opcionalmente TypedArrays en lugar de matrices JS.

Las dos desventajas que veo en este enfoque simplificado son:

  • Necesita una reescritura de los cargadores de modelos existentes. Muchos cargadores usan TRES clases y funciones integradas con espacios de nombres para realizar su tarea, por lo que probablemente tengamos que introducir un conjunto mínimo de código three.js, y con los trabajadores esto se vuelve complicado
  • El formato de transmisión debe capturar correctamente una jerarquía de objetos, así como diferentes tipos de objetos (Malla, Malla de piel, Luz, Cámara, Object3D, Línea, etc.)

@spite Con respecto a "Además, lo ideal sería que la recuperación del recurso en sí se produjera en el trabajador". - esto fue lo que pensé cuando implementé por primera vez el cargador de activos basado en trabajadores para Elation Engine - tenía un grupo de 4 u 8 trabajadores, y les pasaba trabajos a medida que estaban disponibles, y luego los trabajadores buscaban los archivos, analizaban y devuélvalos al hilo principal. Sin embargo, en la práctica, lo que esto significaba era que las descargas bloquearían el análisis y perdería los beneficios que obtendría de la canalización, etc. si las solicitara todas a la vez.

Una vez que nos dimos cuenta de esto, agregamos otra capa para administrar todas nuestras descargas de activos, y luego el descargador de activos activa eventos para informarnos cuando los activos están disponibles. Luego, los pasamos al grupo de trabajadores, utilizando transferibles en los datos del archivo binario para que el trabajador los entregue de manera eficiente. Con este cambio, todas las descargas ocurren más rápido a pesar de que están en el hilo principal, y los analizadores pueden ejecutar el procesamiento a fondo, en lugar de jugar con sus pulgares esperando datos. En general, esta resultó ser una de las mejores optimizaciones que hicimos en términos de velocidad de carga de activos.

Sobre el tema de la carga de texturas, he creado una prueba de concepto de una nueva clase FramebufferTexture , que viene con un compañero FramebufferTextureLoader . Este tipo de textura se extiende a WebGLRenderTarget , y su cargador se puede configurar para cargar texturas en mosaicos fragmentados de un tamaño determinado y componerlos en el framebuffer usando requestIdleCallback() .

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

En este ejemplo, simplemente seleccione un tamaño de imagen y un tamaño de mosaico y comenzará el proceso de carga. Primero inicializamos la textura a rojo puro. Comenzamos la descarga de las imágenes (son unos 10mb, así que dale un poco), y cuando se completan cambiamos el fondo a azul. En este punto, comenzamos a analizar la imagen con createImageBitmap() para analizar el archivo, y cuando está hecho, configuramos una serie de devoluciones de llamada inactivas que contienen más llamadas a createImageBitmap() que dividen eficientemente la imagen en mosaicos. . Estos mosaicos se procesan en el búfer de cuadros en varios cuadros y tienen un impacto significativamente menor en los tiempos de los cuadros que hacerlo todos a la vez.

NOTA: FireFox actualmente no parece implementar todas las versiones de createImageBitmap , y actualmente me arroja un error cuando intenta dividirse en mosaicos. Como resultado, esta demostración actualmente solo funciona en Chrome. ¿Alguien tiene una referencia para una hoja de ruta de soporte createImageBitmap en FireFox?

Hay algo de limpieza que debo hacer, este prototipo está un poco desordenado, pero estoy muy contento con los resultados y una vez que pueda encontrar una forma de solucionar los problemas entre navegadores (retroceso del lienzo, etc.), estoy considerando usar esto como predeterminado para todas las texturas en JanusWeb. El efecto de aparición gradual también es bastante bueno, e incluso podríamos ponernos elegantes y hacer una versión reducida primero, y luego cargar progresivamente los mosaicos de mayor detalle.

¿Hay alguna razón relacionada con el rendimiento o la función en la que alguien pueda pensar por qué podría ser una mala idea tener un framebuffer para cada textura en la escena, en lugar de una referencia de textura estándar? No pude encontrar nada sobre max. framebuffers por escena, por lo que puedo decir una vez que se ha configurado un framebuffer, si no lo está renderizando, entonces es lo mismo que cualquier otra referencia de textura, pero tengo la sensación de que me estoy perdiendo algo obvio en cuanto a por qué esto sería una muy mala idea :)

@jbaicoianu re: createImageBitmap de firefox, la razón es que no admiten el parámetro del diccionario, por lo que no admite la orientación de la imagen o la conversión del espacio de color. hace que la mayoría de las aplicaciones de la API sean bastante inútiles. Presenté dos errores relacionados con el problema: https://bugzilla.mozilla.org/show_bug.cgi?id=1367251 y https://bugzilla.mozilla.org/show_bug.cgi?id=1335594

@spite, eso es lo que pensé también, había visto este error sobre no admitir el diccionario de opciones, pero en este caso ni siquiera lo estoy usando, solo estoy tratando de usar las opciones x, y, w, h. El error específico que recibo es:

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

Lo cual es confuso, porque no veo ninguna versión de createImageBitmap en la especificación que toma un ImageBitmapFormat como argumento.

¿Hay alguna razón relacionada con el rendimiento o la función en la que alguien pueda pensar por qué podría ser una mala idea tener un framebuffer para cada textura en la escena, en lugar de una referencia de textura estándar? No pude encontrar nada sobre max. framebuffers por escena, por lo que puedo decir una vez que se ha configurado un framebuffer, si no lo está renderizando, entonces es lo mismo que cualquier otra referencia de textura, pero tengo la sensación de que me estoy perdiendo algo obvio en cuanto a por qué esto sería una muy mala idea :)

@jbaicoianu THREE.WebGLRenderTarget mantiene un búfer THREE.WebGLRenderTarget fotogramas , una textura y un búfer de renderizado. Cuando tenga la textura ensamblada, puede eliminar el búfer de fotogramas y el búfer de procesamiento y solo conservar la textura. Algo como esto debería hacer esto (no probado):

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

@wrr, es bueno saberlo, gracias. Definitivamente también tengo que pasar la eficiencia de la memoria en esto; inevitablemente, se bloquea en algún momento si cambias los parámetros lo suficiente, así que sé que todavía no estoy haciendo una limpieza. Cualquier otra sugerencia como esta sería muy apreciada.

@mrdoob y @jbaicoianu Olvidé mencionar que también me gusta la idea. 😄
He despejado el código (reinicio modificado, objeto de instrucciones de trabajador, manejo de devolución de llamada múltiple de basura, descripción de recurso común, etc.) de OBJLoader y WWOBJLoader y todos los ejemplos ( código ). Ambos cargadores están ahora listos para combinarse. Estarán de acuerdo con su plano, con suerte, en algún momento de la próxima semana, dependiendo de mi tiempo libre:
Prueba dirigida WWOBJLoader2:
https://kaisalmen.de/proto/test/wwparallels/main.src.html
Usuario dirigido del genérico WorkerSupport :
https://kaisalmen.de/proto/test/meshspray/main.src.html
La gran prueba de archivo OBJ comprimido:
https://kaisalmen.de/proto/test/wwobjloader2stage/main.src.html

Actualizaré los ejemplos anteriores con un código más nuevo cuando esté disponible y te lo haré saber.
Actualización 2017-07-30: OBJLoader2 y WWOBJLoader2 ahora usan analizadores idénticos. Pasan datos a la función de constructor común directamente o desde el trabajador.
Actualización 31-07-2017: WWOBJLoader2 se ha ido. OBJLoader2 proporciona parse y parseAsync , load y run (alimentar por LoaderDirector o manualmente)

Actualización 2017-08-09:
Actualización movida a nueva publicación.

OBJLoader2 es la firma y el comportamiento compatibles nuevamente con OBJLoader (rompí esto durante la evolución), OBJLoader2 proporciona parseAsync y load con useAsync bandera además. Creo que ya está listo para llamarse V2.0.0-Beta. Aquí puede encontrar el estado actual de los desarrolladores:
https://github.com/kaisalmen/WWOBJLoader/tree/V2.0.0-Beta/src/loaders

He extraído clases de LoaderSupport (independientes de OBJ) que sirven como utilidades y herramientas de soporte necesarias. Podrían reutilizarse para otros cargadores potenciales basados ​​en trabajadores. Todo el código a continuación, lo puse en el espacio THREE.LoaderSupport nombres OBJLoader2 :

  • Builder : Para construcción de malla general
  • WorkerDirector : Crea cargadores a través de la reflexión, procesa PrepData en cola con la cantidad configurada de trabajadores. Se utiliza para automatizar completamente los cargadores (demostración de MeshSpray y Parallels)
  • WorkerSupport : clase de utilidad para crear trabajadores a partir del código existente y establecer un protocolo de comunicación simple
  • PrepData + ResourceDescriptor : Descripción usada para automatización o simplemente para descripción unificada entre ejemplos
  • Commons : Posible clase base para cargadores (agrupa los parámetros comunes)
  • Callbacks : (onProgress, onMeshAlter, onLoad) se usa para automatización y dirección y LoadedMeshUserOverride se usa para proporcionar información desde onMeshAlter (adición normal en la prueba objloader2 a continuación)
  • Validator : comprobaciones de variables nulas / indefinidas

@mrdoob @jbaicoianu OBJLoader2 ahora envuelve un analizador como se sugiere (está configurado con parámetros establecidos globalmente o recibidos por PrepData para su ejecución). El Builder recibe cada malla sin formato y el analizador devuelve el nodo base, pero aparte de eso, coincide con el modelo.
Todavía hay un código auxiliar en OBJLoader2 para la serialización del analizador que probablemente no sea necesario.
El constructor necesita una limpieza ya que el objeto de contrato / parámetro para la función buildMeshes todavía está muy influenciado por la carga de OBJ y, por lo tanto, todavía se considera en construcción.

El código necesita un poco de pulido, pero luego está listo para comentarios, discusiones, críticas, etc. 😄

Ejemplos y pruebas

Cargador OBJ usando ejecutar y cargar:
https://kaisalmen.de/proto/test/objloader2/main.src.html
Cargador OBJ usando ejecutar async y parseAsync:
https://kaisalmen.de/proto/test/wwobjloader2/main.src.html
Uso dirigido de ejecutar async OBJLoader2:
https://kaisalmen.de/proto/test/wwparallels/main.src.html
Uso dirigido de WorkerSupport genérico:
https://kaisalmen.de/proto/test/meshspray/main.src.html
La gran prueba de archivo OBJ comprimido:
https://kaisalmen.de/proto/test/wwobjloader2stage/main.src.html

¡Luciendo bien! ¿Conoce estos cambios en OBJLoader ? # 11871 565c6fd0f3d9b146b9434e5fccfa2345a90a3842

Sí, necesito portar esto. Propuse algunas medidas de rendimiento reproducibles. Empezará a trabajar en ambos este fin de semana. ¿Cuándo planeas lanzar r87? El soporte de N-gon podría hacerlo dependiendo de la fecha.

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

Actualización de estado ( código ):
Los trabajadores creados ahora pueden configurar cualquier analizador dentro del trabajador a través de los parámetros recibidos por un mensaje. WorkerSupport proporciona una implementación de ejecución de trabajador de referencia ( código ) que podría ser reemplazado por completo por su propio código si se desea o si se requiere.
El trabajador creará y ejecutará el analizador en el método run del WorkerRunnerRefImpl ( Parser está disponible dentro del alcance del trabajador; this.applyProperties llama a establecedores o propiedades de el analizador):

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 );

    }
};

El mensaje de OBJLoader2.parseAsync ve así:

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

El objeto de mensaje depende del cargador, pero la configuración del analizador en el trabajador es genérica.
El código utilizado por los ejemplos vinculados en la publicación anterior se ha actualizado con el código más reciente.

Creo que la evolución de OBJLoader2 y la extracción de funciones de soporte ha llegado a un punto en el que se requieren sus comentarios. Cuando todos los ejemplos se hayan transferido de su repositorio a la rama anterior, abriré un PR con un resumen completo y luego solicitaré comentarios

Para su información, aquí hay un trabajo en progreso para que ImageBitmapLoader use un trabajador como se discutió anteriormente. Quizás más interesante, algunos números concretos sobre los resultados: https://github.com/mrdoob/three.js/pull/12456

createImageBitmap de firefox, la razón es que no admiten el parámetro del diccionario, por lo que no admite la orientación de la imagen o la conversión del espacio de color. hace que la mayoría de las aplicaciones de la API sean bastante inútiles.

Esto es desafortunado. ☹️

@mrdoob ¿Tiene un plan para cambiar ImageLoader a ImageBitmapLoader en TextureLoader porque ImageBitmap debería ser menos bloqueante para subir a la textura? createImageBitmap() parece funcionar en FireFox hasta ahora si pasamos solo el primer argumento. (¿Quizás no necesitemos pasar un segundo y más argumentos a través de TextureLoader ?)

return createImageBitmap( blob );

De hecho, es importante que createImageBitmap () admita el diccionario de opciones. De lo contrario, no puede cambiar cosas como la orientación de la imagen (flip-Y) o indicar alfa premultiplicado. Lo que pasa es que no puedes usar WebGLRenderingContext.pixelStorei por ImageBitmap . De la especificación :

_Si TexImageSource es un ImageBitmap, estos tres parámetros (UNPACK_FLIP_Y_WEBGL, UNPACK_PREMULTIPLY_ALPHA_WEBGL, UNPACK_COLORSPACE_CONVERSION_WEBGL) serán ignorados. En su lugar, se deben usar las ImageBitmapOptions equivalentes para crear un ImageBitmap con el formato deseado.

Así que creo que solo podemos cambiar a ImageBitmapLoader si FF admite el diccionario de opciones. Además, propiedades como Texture.premultiplyAlpha y Texture.flipY no funcionan con ImageBitmap este momento. Quiero decir, si los usuarios los configuran, no afectarán una textura basada en ImageBitmap cual es algo desafortunado.

Ah, vale. Me perdí esa especificación.

La importancia del diccionario de opciones también se discute aquí:

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

Los errores en bugzilla (https://bugzilla.mozilla.org/show_bug.cgi?id=1367251, https://bugzilla.mozilla.org/show_bug.cgi?id=1335594) han estado allí intactos durante ... dos años ahora? No pensé que les tomaría tanto tiempo arreglarlo.

Entonces, el problema es que "técnicamente" la función es compatible con FF, pero en la práctica es inútil. Para usarlo, podríamos tener una ruta para Chrome que lo usa y otra para los otros navegadores que no lo usan. El problema es que, dado que Firefox tiene la función, tendríamos que hacer un rastreo de UA, lo cual apesta.

La solución práctica es realizar la detección de características: construya una imagen de 2x2 usando cIB con la bandera invertida, y luego vuelva a leer y asegúrese de que los valores sean correctos.

Acerca de los errores de FireFox, también los contactaré internamente. Veamos si necesitamos una solución alternativa después de escuchar su plan.

Los errores en bugzilla (https://bugzilla.mozilla.org/show_bug.cgi?id=1367251, https://bugzilla.mozilla.org/show_bug.cgi?id=1335594) han estado allí intactos durante ... dos años ahora? No pensé que les tomaría tanto tiempo arreglarlo.

Sí, lo siento, realmente no lo seguí por un tiempo -_-

Entonces, el problema es que "técnicamente" la función es compatible con FF, pero en la práctica es inútil. Para usarlo, podríamos tener una ruta para Chrome que lo usa y otra para los otros navegadores que no lo usan. El problema es que, dado que Firefox tiene la función, tendríamos que hacer un rastreo de UA, lo cual apesta.

La solución práctica es realizar la detección de características: construya una imagen de 2x2 usando cIB con la bandera invertida, y luego vuelva a leer y asegúrese de que los valores sean correctos.

Sí, estoy de acuerdo en que ambas soluciones realmente apestan y deberíamos tratar de evitarlas, así que antes de profundizar en cualquiera de estas, veamos si podemos desbloquearlas de nuestro lado.

Hice ImageBitmap prueba de rendimiento de carga. Subiendo textura cada 5 segundos.

Puede comparar Regular Image con ImageBitmap.

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

En mis ventanas veo

| Navegador | 8192x4096 JPG 4.4MB | 2048x2048 PNG 4.5MB |
| ---- | ---- | ---- |
| Imagen de Chrome | 500ms | 140ms |
| ImageBitmap de Chrome | 165ms | 35ms |
| Imagen de Firefox | 500ms | 40ms |
| FireFox ImageBitmap | 500ms | 60ms |

( texture.generateMipmaps es true )

Mis pensamientos

  1. 3 veces mejor rendimiento con ImageBitmap en Chrome. Muy buena mejora.
  2. ¿FireFox tiene ahora un problema de rendimiento de ImageBitmap? ¿También puedes probarlo en tu computadora? Probar en el móvil también es bienvenido.
  3. Incluso con ImageBitmap, la carga de texturas parece bloquearse para texturas grandes. Tal vez necesitemos una técnica de carga incrementalmente parcial o algo para no bloquear.

Incluso con ImageBitmap, la carga de texturas parece bloquearse para texturas grandes. Tal vez necesitemos una técnica de carga parcial o algo para no bloquear.

Supongo que una solución para este problema podría ser el uso de un formato de compresión de textura y evitar JPG o PNG (y por lo tanto ImageBitmap ). Sería interesante ver algunos datos de rendimiento en este contexto.

Sí, estoy de acuerdo. Pero supongo que probablemente todavía veamos el bloqueo para texturas grandes, especialmente en dispositivos de bajo consumo como el móvil. De todos modos, evalúe el desempeño primero.

O use programado / requestIdleCallback texSubImage2D

rIC = requestIdleCallback?

sí, hice una edición ninja

está bien. Sí, estoy de acuerdo.

Por cierto, todavía no estoy familiarizado con la textura comprimida. Permíteme confirmar mi entendimiento. No podemos usar la textura comprimida con ImageBitmap porque compressedTexImage2D no acepta ImageBitmap , ¿correcto?

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

Volví a revisar mis viejos experimentos de TiledTextureLoader; parece que ahora están causando que mi controlador de video se bloquee y se reinicie :(

(editar: en realidad, parece que incluso cargar la textura más grande (16k x 16k - https://baicoianu.com/~bai/three.js/examples/textures/dotamap1_25.jpg) directamente en Chrome es lo que está causando el bloqueo. Esto solía funcionar bien, por lo que parece haber una regresión en el manejo de imágenes de Chrome)

Hice algunos experimentos utilizando los generadores requestIdleCallback, ImageBitmap y ES6 para dividir una textura grande en varios fragmentos para cargarla en la GPU. Usé un framebuffer en lugar de una Texture regular, porque incluso si está usando texSubimage2D para completar los datos de la imagen, aún necesita preasignar la memoria, lo que requiere cargar un montón de datos vacíos a la GPU, mientras que se puede crear un framebuffer e inicializado con una sola llamada GL.

El repositorio de esos cambios todavía está disponible aquí https://github.com/jbaicoianu/THREE.TiledTexture/

Algunas notas de lo que recuerdo de los experimentos:

  • requestIdleCallback definitivamente ayudó a reducir el jank al cargar texturas, a expensas de aumentar en gran medida el tiempo total de carga
  • Con un poco de trabajo adicional, esto podría mitigarse cargando primero una versión reducida de la textura y luego completando los datos de resolución completa a un ritmo más pausado.
  • Los generadores ES6 ayudaron a que el código fuera más fácil de entender y más fácil de escribir sin desperdiciar memoria, pero probablemente no sean realmente necesarios para esto.

Mis resultados fueron similares: hubo una compensación entre la velocidad de carga y la jankiness. (Por cierto, creé este https://github.com/spite/THREE.UpdatableTexture).

Creo que para que la segunda opción funcione en WebGL 1, en realidad necesitaría dos texturas, o al menos modificadores de las coordenadas UV. En WebGL 2, creo que es más fácil copiar fuentes que tienen un tamaño diferente al de la textura de destino.

Sí, con texSubImage2D creo que ese tipo de cambio de tamaño no sería posible, pero cuando uso un framebuffer, estoy usando una OrthographicCamera para renderizar un plano con el fragmento de textura, por lo que es solo una cuestión de cambiar la escala del plano para esa llamada de sorteo.

Sobre el problema de rendimiento de ImageBItmap en FireFox, abrí un error en bugzilla

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

He estado buscando intentar comprender mejor cuándo los datos asociados con una textura se cargan realmente en la GPU y se encuentran con este hilo. En mi caso de uso particular, NO me preocupa la carga y decodificación de archivos jpeg / gif locales en texturas, solo me preocupa intentar precargar datos de textura en la GPU. Después de leer este hilo, debo confesar que no estoy del todo seguro si aborda ambos problemas o solo el primero. Dado que solo me importa lo último, ¿necesito buscar una solución diferente o hay algo aquí que ayudará a forzar la carga de los datos de textura en la GPU?

¿Fue útil esta página
0 / 5 - 0 calificaciones