En esta página

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
        }
    }
}

Vista 3D vacía con fondo verde

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:

Listado de archivos de activos Sponza

¿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:

Panel de configuración de Balsam UI

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:

Interior del palacio Sponza con iluminación direccional

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

El palacio Sponza visto desde otro ángulo

El palacio Sponza visto desde el exterior

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:

Ventana de inicio de Balsam UI

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

Navegador de archivos que muestra los archivos de activos de Suzanne

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.

La interfaz de usuario de Balsam muestra la conversión en curso

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.

Suzanne modelo cabeza de mono en escena oscura

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:

Suzanne con iluminación adicional

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:

Cabeza de mono Suzanne en el suelo del palacio Sponza

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

Palacio Sponza con iluminación basada en imágenes

Palacio Sponza con diferente orientación del IBL

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
        }
    }
}

Escena Sponza con panel de vista de depuración que muestra estadísticas de rendimiento

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:

Suzanne con panel de depuración que muestra los pases de renderizado

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.

Escena con panel de depuración que muestra las estadísticas de textura

La página Models no presenta sorpresas:

Escena con panel de depuración que muestra las estadísticas de la malla

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:

Escena Sponza en modo wireframe con panel de depuración

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.