Qt Quick Introducción 3D con activos glTF
El ejemplo Qt Quick 3D - Introducción proporciona una rápida introducción a la creación de aplicaciones basadas en QML con Qt Quick 3D, pero lo hace utilizando sólo primitivas incorporadas, como esferas y cilindros. Esta página proporciona una introducción utilizando activos glTF 2.0, utilizando algunos de los modelos del repositorio Khronos glTF Sample Models.
Nuestro esqueleto de aplicación
Comencemos con la siguiente aplicación. Este fragmento de código se puede ejecutar tal cual con la herramienta de línea de comandos qml. El resultado es una vista 3D muy verde sin nada más en ella.
import QtQuick import QtQuick3D import QtQuick3D.Helpers Item { width: 1280 height: 720 View3D { anchors.fill: parent environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color clearColor: "green" } PerspectiveCamera { id: camera } WasdController { controlledObject: camera } } }

Importar un activo
Vamos a utilizar dos modelos glTF 2.0 del repositorio Sample Models: Sponza y Suzanne.
Estos modelos suelen venir con varios mapas de texturas y los datos de la malla (geometría) almacenados en un archivo binario aparte, además del archivo .gltf:

¿Cómo introducimos todo esto en nuestra escena 3D Qt Quick?
Existen varias opciones:
- Generar componentes QML que puedan instanciarse en la escena. La herramienta de línea de comandos para realizar esta conversión es la herramienta Balsam. Además de generar un archivo .qml, que es efectivamente una subescena, también reempaqueta los datos de la malla (geometría) en un formato optimizado y de carga rápida, y copia también los archivos de imagen del mapa de texturas.
- Haga lo mismo con
balsamui, una interfaz gráfica de usuario para Balsam. - Si se utiliza Qt Design Studioel proceso de importación de activos se integra en las herramientas de diseño visual. La importación puede activarse, por ejemplo, arrastrando y soltando el archivo .gltf en el panel correspondiente.
- Para los activos glTF 2.0 en particular, existe también una opción de tiempo de ejecución: el tipo RuntimeLoader. Esto permite cargar un archivo .gltf (y los archivos binarios y de datos de textura asociados) en tiempo de ejecución, sin realizar ningún procesamiento previo mediante herramientas como Balsam. Esto resulta muy práctico en aplicaciones que desean abrir y cargar activos proporcionados por el usuario. Por otro lado, este enfoque es significativamente menos eficiente cuando se trata de rendimiento. Por lo tanto, no nos centraremos en este enfoque en esta introducción. Consulte Qt Quick 3D - RuntimeLoader Example para ver un ejemplo de este enfoque.
Ambas aplicaciones balsam y balsamui se suministran con Qt, y deberían estar presentes en el directorio con otras herramientas ejecutables similares, asumiendo que Qt Quick 3D está instalado o construido. En muchos casos, basta con ejecutar balsam desde la línea de comandos en el archivo .gltf, sin tener que especificar argumentos adicionales. Sin embargo, conviene tener en cuenta las numerosas opciones de la línea de comandos, o interactivas si se utiliza balsamui o Qt Design Studio. Por ejemplo, cuando se trabaja con baked lightmaps para proporcionar iluminación global estática, es probable que uno quiera pasar --generateLightmapUV para obtener el canal UV lightmap adicional generado en el momento de la importación del asset, en lugar de realizar este proceso potencialmente consumidor en tiempo de ejecución. Del mismo modo, --generateMeshLevelsOfDetail es esencial cuando se desea tener versiones simplificadas de las mallas generadas con el fin de tener LOD automático habilitado en la escena. Otras opciones permiten generar datos que faltan (por ejemplo, --generateNormals) y realizar diversas optimizaciones.
En balsamui las opciones de la línea de comandos se asignan a elementos interactivos:

Importar mediante balsam
¡Empecemos! Asumiendo que el repositorio https://github.com/KhronosGroup/glTF-Sample-Models git está en algún lugar, podemos simplemente ejecutar balsam desde el directorio de nuestra aplicación de ejemplo, especificando una ruta absoluta a los archivos .gltf:
balsam c:\work\glTF-Sample-Models\2.0\Sponza\glTF\Sponza.gltf
Esto nos da un Sponza.qml, un archivo .mesh bajo el subdirectorio meshes, y los mapas de textura copiados bajo maps.
Nota: Este archivo qml no es ejecutable por sí mismo. Es un componente, que debe ser instanciado dentro de una escena 3D asociada a un View3D.
La estructura de nuestro proyecto es muy sencilla en este caso, ya que los archivos asset qml se encuentran junto a nuestra escena principal .qml. Esto nos permite instanciar simplemente el tipo Sponza utilizando el sistema estándar de componentes QML. (en tiempo de ejecución esto buscará Sponza.qml en el sistema de archivos)
Sin embargo, añadir simplemente el modelo (subescena) no tiene sentido, ya que por defecto los materiales presentan los cálculos de iluminación PBR completos, por lo que no se muestra nada de nuestra escena sin una luz como DirectionalLight, PointLight, o SpotLight, o teniendo activada la iluminación basada en imágenes a través de the environment.
Por ahora, elegimos añadir un DirectionalLight con la configuración por defecto. (lo que significa que el color es white, y la luz emite en la dirección del eje Z)
import QtQuick import QtQuick3D import QtQuick3D.Helpers Item { width: 1280 height: 720 View3D { anchors.fill: parent environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color clearColor: "green" } PerspectiveCamera { id: camera } DirectionalLight { } Sponza { } WasdController { controlledObject: camera } } }
Ejecutando esto con la herramienta qml se cargará y ejecutará, pero la escena está toda vacía por defecto ya que el modelo Sponza está detrás de la cámara. La escala tampoco es la ideal, por ejemplo, moverse con las teclas WASD y el ratón (activado por el WasdController) no se siente bien.
Para remediarlo, escalamos el modelo Sponza (subescena) en 100 a lo largo de los ejes X, Y y Z. Además, la posición inicial Y de la cámara se aumenta a 100.
import QtQuick import QtQuick3D import QtQuick3D.Helpers Item { width: 1280 height: 720 View3D { anchors.fill: parent environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color clearColor: "green" } PerspectiveCamera { id: camera y: 100 } DirectionalLight { } Sponza { scale: Qt.vector3d(100, 100, 100) } WasdController { controlledObject: camera } } }
Ejecutando esto nos da:

Con el ratón y las teclas WASDRF podemos movernos:


Nota: Hemos mencionado subscene varias veces como alternativa a "modelo". ¿Por qué? Aunque no es obvio con el asset Sponza, que en su forma glTF es un único modelo con 103 submallas, mapeando a un único objeto Model con 103 elementos en su materials list, un asset puede contener cualquier número de models, cada uno con múltiples submallas y materiales asociados. Estos modelos pueden formar relaciones padre-hijo y pueden combinarse con nodes adicionales para realizar transformaciones como trasladar, rotar o escalar. Por lo tanto, es más apropiado considerar el activo importado como una subescena completa, un árbol arbitrario de nodes, aunque el resultado renderizado se perciba visualmente como un único modelo. Abra el archivo Sponza.qml generado, o cualquier otro archivo QML generado a partir de este tipo de activos, en un editor de texto plano para hacerse una idea de la estructura (que, naturalmente, depende siempre de cómo se haya diseñado el activo de origen, en este caso el archivo glTF).
Importación mediante balsamui
Para nuestro segundo modelo, vamos a utilizar la interfaz gráfica de usuario de balsam.
Ejecutando balsamui se abre la herramienta:

Importemos el modelo Suzanne. Se trata de un modelo más sencillo con dos mapas de textura.

Como no hay necesidad de ninguna opción de configuración adicional, podemos simplemente Convertir. El resultado es el mismo que ejecutando balsam: un Suzanne.qml y algunos archivos adicionales generados en el directorio de salida específico.

A partir de este punto, el trabajo con los activos generados es el mismo que en la sección anterior.
import QtQuick import QtQuick3D import QtQuick3D.Helpers Item { width: 1280 height: 720 View3D { anchors.fill: parent environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color clearColor: "green" } PerspectiveCamera { id: camera y: 100 } DirectionalLight { } Sponza { scale: Qt.vector3d(100, 100, 100) } Suzanne { y: 100 scale: Qt.vector3d(50, 50, 50) eulerRotation.y: -90 } WasdController { controlledObject: camera } } }
De nuevo, se aplica una escala al nodo Suzanne instanciado y se modifica un poco la posición Y para que el modelo no acabe en el suelo del edificio Sponza.

Todas las propiedades pueden modificarse, vincularse y animarse, al igual que con Qt Quick. Por ejemplo, vamos a aplicar una rotación continua a nuestro modelo de Suzanne:
Suzanne { y: 100 scale: Qt.vector3d(50, 50, 50) NumberAnimation on eulerRotation.y { from: 0 to: 360 duration: 3000 loops: Animation.Infinite } }
Para que se vea mejor
Más luz
Ahora, nuestra escena está un poco oscura. Añadamos otra luz. Esta vez una PointLight, y una que proyecte sombra.
import QtQuick import QtQuick3D import QtQuick3D.Helpers Item { width: 1280 height: 720 View3D { anchors.fill: parent environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color clearColor: "green" } PerspectiveCamera { id: camera y: 100 } DirectionalLight { } Sponza { scale: Qt.vector3d(100, 100, 100) } PointLight { y: 200 color: "#d9c62b" brightness: 5 castsShadow: true shadowFactor: 75 } Suzanne { y: 100 scale: Qt.vector3d(50, 50, 50) NumberAnimation on eulerRotation.y { from: 0 to: 360 duration: 3000 loops: Animation.Infinite } } WasdController { controlledObject: camera } } }
Si iniciamos la escena y movemos un poco la cámara, veremos que empieza a verse mejor que antes:

Depuración de la luz
El PointLight está situado ligeramente por encima del modelo Suzanne. Cuando se diseña la escena utilizando herramientas visuales, como Qt Design Studio, esto es obvio, pero cuando se desarrolla sin ninguna herramienta de diseño puede resultar práctico poder visualizar rápidamente la ubicación de lights y otros nodes.
Esto lo podemos hacer añadiendo un nodo hijo, un Model al PointLight. La posición del nodo hijo es relativa al padre, por lo que el (0, 0, 0) por defecto es efectivamente la posición del PointLight en este caso. Encerrar la luz dentro de alguna geometría (el cubo incorporado en este caso) no es un problema para los cálculos estándar de iluminación en tiempo real, ya que este sistema no tiene el concepto de oclusión, lo que significa que la luz no tiene problemas con viajar a través de "paredes". Si utilizáramos lightmaps pre-cocinados, donde la iluminación se calcula utilizando raytracing, sería una historia diferente. En ese caso tendríamos que asegurarnos de que el cubo no bloquea la luz, quizás moviendo nuestro cubo de depuración un poco por encima de la luz.
PointLight { y: 200 color: "#d9c62b" brightness: 5 castsShadow: true shadowFactor: 75 Model { source: "#Cube" scale: Qt.vector3d(0.01, 0.01, 0.01) materials: PrincipledMaterial { lighting: PrincipledMaterial.NoLighting } } }
Otro truco que usamos aquí es desactivar la iluminación para el material usado con el cubo. Aparecerá usando el color base por defecto (blanco), sin ser afectado por la iluminación. Esto es útil para objetos utilizados para depurar y visualizar.
El resultado, observe el pequeño cubo blanco que aparece, visualizando la posición del PointLight:

Skybox e iluminación basada en imágenes
Otra mejora obvia es hacer algo con el fondo. Ese color verde claro no es del todo ideal. ¿Qué tal un entorno que también contribuya a la iluminación?
Como no disponemos necesariamente de una imagen panorámica HDRI adecuada, utilicemos una imagen del cielo de alto rango dinámico generada proceduralmente. Esto es fácil de hacer con la ayuda de ProceduralSkyTextureData y Texture's soporte para no basados en archivos, generados dinámicamente los datos de imagen. En lugar de especificar source, utilizaremos la propiedad textureData.
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.SkyBox
lightProbe: Texture {
textureData: ProceduralSkyTextureData {
}
}
}Nota: El código de ejemplo prefiere definir los objetos inline. Esto no es obligatorio, los objetos SceneEnvironment o ProceduralSkyTextureData también podrían haber sido definidos en otra parte del árbol de objetos, y luego referenciados por id.
Como resultado, tenemos tanto un skybox como una iluminación mejorada. (la primera se debe a que backgroundMode se ha definido como SkyBox y light probe se ha definido como un Texture válido; la segunda se debe a que light probe se ha definido como un Texture válido).


Investigaciones básicas sobre el rendimiento
Para obtener información básica sobre los recursos y aspectos de rendimiento de la escena, es una buena idea añadir una forma de mostrar un elemento interactivo DebugView al principio del proceso de desarrollo. Aquí elegimos añadir un Button que conmuta el DebugView, ambos anclados en la esquina superior derecha.
import QtQuick import QtQuick.Controls import QtQuick3D import QtQuick3D.Helpers Item { width: 1280 height: 720 View3D { id: view3D anchors.fill: parent environment: SceneEnvironment { backgroundMode: SceneEnvironment.SkyBox lightProbe: Texture { textureData: ProceduralSkyTextureData { } } } PerspectiveCamera { id: camera y: 100 } DirectionalLight { } Sponza { scale: Qt.vector3d(100, 100, 100) } PointLight { y: 200 color: "#d9c62b" brightness: 5 castsShadow: true shadowFactor: 75 Model { source: "#Cube" scale: Qt.vector3d(0.01, 0.01, 0.01) materials: PrincipledMaterial { lighting: PrincipledMaterial.NoLighting } } } Suzanne { y: 100 scale: Qt.vector3d(50, 50, 50) NumberAnimation on eulerRotation.y { from: 0 to: 360 duration: 3000 loops: Animation.Infinite } } WasdController { controlledObject: camera } } Button { anchors.right: parent.right text: "Toggle DebugView" onClicked: debugView.visible = !debugView.visible DebugView { id: debugView source: view3D visible: false anchors.top: parent.bottom anchors.right: parent.right } } }

Este panel muestra los tiempos en tiempo real, permite examinar la lista en tiempo real de mapas de texturas y mallas, y da una idea de los pases de render que hay que realizar antes de poder renderizar el búfer de color final.
Debido a que PointLight es una luz que proyecta sombras, hay múltiples pases de render involucrados:

En la sección Textures vemos los mapas de textura de los activos Suzanne y Sponza (este último tiene muchos), así como la textura del cielo generada proceduralmente.

La página Models no presenta sorpresas:

En la página Tools hay algunos controles interactivos para alternar entre wireframe mode y varios material overrides.
Aquí con el modo wireframe activado y forzando el renderizado para que sólo utilice el componente base color de los materiales:

Esto concluye nuestro recorrido por los fundamentos de la construcción de una escena 3DQt Quick con activos importados.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.