En esta página

Materiales, efectos, geometría y datos de textura programables

Aunque los materiales incorporados de Qt Quick 3D, DefaultMaterial y PrincipledMaterial, permiten un amplio grado de personalización a través de sus propiedades, no proporcionan programabilidad en el nivel de sombreado de vértices y fragmentos. Para permitirlo, se proporciona el tipo CustomMaterial.

Un modelo con PrincipledMaterialCon un CustomMaterial que transforma los vértices

Tetera con material estándar

Tetera con vértices transformados por material personalizado

Los efectos de postprocesamiento, en los que se realizan una o más pasadas de procesamiento en el búfer de color, teniendo en cuenta opcionalmente el búfer de profundidad, antes de que la salida de View3D se pase a Qt Quick, también existen en dos variedades:

  • pasos de posprocesamiento integrados que pueden configurarse a través de ExtendedSceneEnvironment, como resplandor/floración, profundidad de campo, viñeta, destello de lente,
  • custom efectos implementados por la aplicación en forma de código fragment shader y una especificación de los pasos de procesamiento en un objeto Effect.

En la práctica, existe una tercera categoría de efectos de postprocesamiento: los efectos 2D implementados a través de Qt Quick, que operan sobre la salida del objeto View3D sin ninguna participación del renderizador 3D. Por ejemplo, para aplicar un desenfoque a un elemento View3D, lo más sencillo es utilizar las funciones existentes en Qt Quick, como MultiEffect. El sistema de postprocesamiento 3D resulta beneficioso para los efectos complejos que implican conceptos de escena 3D como el búfer de profundidad o la textura de la pantalla, o que necesitan tratar con el mapeado tonal HDR o necesitan múltiples pasadas con búferes intermedios, etc. Los efectos 2D sencillos que no requieren ningún conocimiento de la escena 3D y el renderizador siempre pueden implementarse con ShaderEffect o MultiEffect en su lugar.

Escena sin efectoLa misma escena con un efecto de postprocesado personalizado aplicado

Escena con esfera, cono y cubo sin efecto de postprocesado

Escena con geometría deformada por un efecto de postprocesado personalizado

Además de los materiales programables y el post-procesado, hay dos tipos de datos que normalmente se proporcionan en forma de archivos ( archivos.mesh o imágenes como .png):

  • datos de vértices, que incluyen la geometría de la malla que se va a renderizar, coordenadas de textura, normales, colores y otros datos,
  • el contenido de las texturas que luego se utilizan como mapas de texturas para los objetos renderizados, o se utilizan con la iluminación basada en skybox o en imágenes.

Si lo desean, las aplicaciones pueden proporcionar esos datos desde C++ en forma de QByteArray. Esos datos también pueden modificarse con el tiempo, lo que permite generar proceduralmente y alterar posteriormente los datos de un Model o Texture.

Una rejilla, renderizada especificando datos de vértices dinámicamente desde C++Un cubo texturizado con datos de imagen generados desde C++.

Cuadrícula generada a partir de datos geométricos personalizados

Cubo con textura degradada generada procedimentalmente

Estos cuatro enfoques para personalizar y hacer dinámicos los materiales, efectos, geometría y texturas permiten programar el sombreado y la generación procedimental de los datos que los sombreadores reciben como entrada. Las secciones siguientes ofrecen una visión general de estas características. La referencia completa está disponible en las páginas de documentación de los tipos respectivos:

Programabilidad para Materiales

Tengamos una escena con un cubo, y empecemos con un PrincipledMaterial y un CustomMaterial por defecto:

PrincipledMaterialCustomMaterial
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            eulerRotation.x: 30
            materials: PrincipledMaterial { }
         }
    }
}
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            eulerRotation.x: 30
            materials: CustomMaterial { }
         }
    }
}

Ambos conducen exactamente al mismo resultado, porque un CustomMaterial es efectivamente un PrincipledMaterial, cuando no se le añade código vertex o fragment shader.

Cubo blanco con material por defecto

Nota: Las propiedades, tales como, baseColor, metalness, baseColorMap, y muchas otras, no tienen propiedades equivalentes en el tipo QML CustomMaterial. Esto es por diseño: la personalización del material se hace a través del código del shader, no simplemente proporcionando algunos valores fijos.

Nuestro primer sombreador de vértices

Vamos a añadir un fragmento de sombreador de vértices personalizado. Esto se hace haciendo referencia a un archivo en la propiedad vertexShader. El enfoque será el mismo para los fragment shaders. Estas referencias funcionan como Image.source o ShaderEffect.vertexShader: son URLs locales o qrc, y una ruta relativa es tratada en relación a la localización del archivo .qml. El enfoque común es, por tanto, colocar los archivos .vert y .frag en el sistema de recursos Qt (qt_add_resources cuando se utiliza CMake) y hacer referencia a ellos utilizando una ruta relativa.

En Qt 6.0 las cadenas de sombreado en línea ya no están soportadas, ni en Qt Quick ni en Qt Quick 3D. (Sin embargo, debido a su naturaleza intrínsecamente dinámica, los materiales personalizados y los efectos de post-procesado en Qt Quick 3D siguen proporcionando fragmentos de shaders en formato fuente en los archivos referenciados. Esto supone una diferencia con respecto a ShaderEffect, donde los shaders están completos por sí solos, sin más modificaciones por parte del motor, por lo que se espera que se proporcionen como paquetes de shaders preconfigurados de .qsb.

Nota: En Qt Quick 3D, las URL sólo pueden referirse a recursos locales. No se admiten esquemas para contenidos remotos.

Nota: El lenguaje de sombreado utilizado es GLSL compatible con Vulkan. Los archivos .vert y .frag no son shaders completos por sí mismos, de ahí que a menudo se les llame snippets. Por eso no hay bloques uniformes, variables de entrada y salida o uniformes de muestreo proporcionados directamente por estos fragmentos. Más bien, el motor 3D de Qt Quick los modificará según convenga.

Cambio en main.qml, material.vertResultado
materials: CustomMaterial {
    vertexShader: "material.vert"
}
void MAIN()
{
}

Cubo blanco con material por defecto

Se espera que un fragmento de sombreado de vértices o fragmentos personalizado proporcione una o más funciones con nombres predefinidos, como MAIN, DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT, SPECULAR_LIGHT. Por ahora vamos a centrarnos en MAIN.

Como se muestra aquí, el resultado final con un MAIN() vacío es exactamente el mismo que antes.

Antes de hacerlo más interesante, veamos un resumen de las palabras clave especiales más utilizadas en los fragmentos de sombreadores de vértices personalizados. Esta no es la lista completa. Para una referencia completa, consulte la página CustomMaterial.

Palabra claveTipoDescripción
MAINvoid MAIN() es el punto de entrada. Esta función debe estar siempre presente en un sombreador de vértices personalizado, no tiene sentido proporcionar uno de otra manera.
VERTEXvec3La posición del vértice que el shader recibe como entrada. Un caso de uso común para los sombreadores de vértices en materiales personalizados es cambiar (desplazar) los valores x, y, o z de este vector, simplemente asignando un valor a todo el vector, o a algunos de sus componentes.
NORMALvec3La normal del vértice de los datos de la malla de entrada, o todos los ceros si no se han proporcionado normales. Como con VERTEX, el shader es libre de alterar el valor como crea conveniente. El valor alterado es utilizado por el resto del pipeline, incluyendo los cálculos de iluminación en la fase de fragmentos.
UV0vec2El primer conjunto de coordenadas de textura de los datos de la malla de entrada, o todos los ceros si no hay valores UV proporcionados. Como con VERTEX y NORMAL, el valor puede ser alterado.
MODELVIEWPROJECTION_MATRIXmat4La matriz de proyección de la vista del modelo. Para unificar el comportamiento independientemente de la API gráfica con la que se renderice, todos los datos de vértices y matrices de transformación siguen las convenciones de OpenGL en este nivel. (Eje Y apuntando hacia arriba, matriz de proyección compatible con OpenGL) Sólo lectura.
MATRIZ_MODELOmat4La matriz del modelo (mundo). Sólo lectura.
NORMAL_MATRIXmat3La inversa transpuesta del corte superior izquierdo 3x3 de la matriz modelo. Sólo lectura.
POSICIÓN_CÁMARAvec3La posición de la cámara en el espacio global. En los ejemplos de esta página es (0, 0, 600). Sólo lectura.
DIRECCIÓN_DE_LA_CÁMARAvec3El vector de dirección de la cámara. En los ejemplos de esta página es (0, 0, -1). Sólo lectura.
PROPIEDADES_DE_LA_CÁMARAvec2Los valores del clip cercano y lejano de la cámara. En los ejemplos de esta página es (10, 10000). Sólo lectura.
TAMAÑO_PUNTOfloatRelevante sólo cuando se renderiza con una topología de puntos, por ejemplo porque custom geometry proporciona dicha geometría para la malla. Escribir este valor es equivalente a poner pointSize on a PrincipledMaterial.
POSICIÓNvec4Como gl_Position. Cuando no está presente, se genera automáticamente una sentencia de asignación por defecto utilizando MODELVIEWPROJECTION_MATRIX y VERTEX. Esta es la razón por la que un MAIN() vacío es funcional, y en la mayoría de los casos no habrá necesidad de asignarle un valor personalizado.

Hagamos un material personalizado que desplace los vértices de acuerdo a algún patrón. Para hacerlo más interesante, tengamos algunas propiedades QML animadas, cuyos valores acaben siendo expuestos como uniformes en el código del shader. (para ser precisos, la mayoría de las propiedades van a ser mapeadas a miembros en un bloque uniforme, respaldado por un buffer uniforme en tiempo de ejecución, pero Qt Quick 3D convenientemente hace tales detalles transparentes al autor del material personalizado)

Cambio en main.qml, material.vertResultado
materials: CustomMaterial {
   vertexShader: "material.vert"
   property real uAmplitude: 0
   NumberAnimation on uAmplitude {
       from: 0; to: 100; duration: 5000; loops: -1
   }
   property real uTime: 0
   NumberAnimation on uTime {
       from: 0; to: 100; duration: 10000; loops: -1
   }
}
void MAIN()
{
    VERTEX.x += sin(uTime + VERTEX.y) * uAmplitude;
}

Uniformes a partir de propiedades QML

Las propiedades personalizadas del objeto CustomMaterial se asignan a uniformes. En el ejemplo anterior esto incluye uAmplitude y uTime. Cada vez que los valores cambien, el valor actualizado será visible en el shader. Puede que este concepto ya te resulte familiar de ShaderEffect.

El nombre de la propiedad QML y la variable GLSL deben coincidir. No hay una declaración separada en el código del shader para los uniformes individuales. En su lugar, el nombre de la propiedad QML puede utilizarse tal cual. Por este motivo, en el ejemplo anterior se puede hacer referencia a uTime y uAmplitude en el fragmento del sombreador de vértices sin ninguna declaración previa.

La siguiente tabla muestra cómo se asignan los tipos:

QML TipoTipo de sombreadorNotas
real, int, boolfloat, int, bool
colorvec4la conversión de sRGB a lineal se realiza implícitamente
vector2dvec2
vector3dvec3
vector4dvec4
matriz4x4mat4
cuaterniónvec4valor escalar es w
rectvec4
punto, tamañovec2
TextureInputmuestreador2D

Mejorar el ejemplo

Antes de seguir adelante, vamos a mejorar un poco el aspecto del ejemplo. Añadiendo una malla rectangular rotada y haciendo que DirectionalLight proyecte sombras, podemos comprobar que la alteración de los vértices del cubo se refleja correctamente en todos los pases de renderizado, incluidos los mapas de sombras. Para conseguir una sombra visible, la luz se coloca ahora un poco más arriba en el eje Y, y se le aplica una rotación para que apunte parcialmente hacia abajo. (al tratarse de una luz directional, la rotación es importante)

main.qml, material.vertResultado
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight {
            y: 200
            eulerRotation.x: -45
            castsShadow: true
        }
        Model {
            source: "#Rectangle"
            y: -250
            scale: Qt.vector3d(5, 5, 5)
            eulerRotation.x: -45
            materials: PrincipledMaterial { baseColor: "lightBlue" }
        }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            eulerRotation.x: 30
            materials: CustomMaterial {
                vertexShader: "material.vert"
                property real uAmplitude: 0
                NumberAnimation on uAmplitude {
                    from: 0; to: 100; duration: 5000; loops: -1
                }
                property real uTime: 0
                NumberAnimation on uTime {
                    from: 0; to: 100; duration: 10000; loops: -1
                }
            }
        }
    }
}
void MAIN()
{
    VERTEX.x += sin(uTime + VERTEX.y) * uAmplitude;
}

Añadir un fragment shader

Muchos materiales personalizados querrán tener también un fragment shader. De hecho, muchos querrán sólo un fragment shader. Si no hay datos adicionales que pasar de la etapa de vértices a la de fragmentos, y la transformación de vértices por defecto es suficiente, la configuración de la propiedad vertexShader puede omitirse en CustomMaterial.

Cambio en main.qml, material.fragResultado
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
}

Cubo blanco con fragment shader vacío

Nuestro primer fragment shader contiene una función MAIN() vacía. Esto no es diferente de no especificar un fragmento de fragment shader en absoluto: lo que obtenemos se parece a lo que obtenemos con un PrincipledMaterial por defecto.

Veamos algunas de las palabras clave más utilizadas en los fragment shaders. Esta no es la lista completa, consulte la documentación de CustomMaterial para obtener una referencia completa. Muchas de ellas son de lectura-escritura, lo que significa que tienen un valor por defecto, pero el sombreador puede, y a menudo querrá, asignarles un valor diferente.

Como sugieren sus nombres, muchas de ellas corresponden a propiedades de nombre similar de PrincipledMaterial, con el mismo significado y semántica, siguiendo el modelo de material de rugosidad metálica. Depende de la implementación del material personalizado decidir cómo se calculan estos valores: por ejemplo, un valor para BASE_COLOR puede estar codificado en el sombreador, puede basarse en el muestreo de una textura, o puede calcularse basándose en propiedades QML expuestas como uniformes o en datos interpolados pasados desde el sombreador de vértices.

Palabra claveTipoDescripción
BASE_COLORvec4El color base y el valor alfa. Corresponde a PrincipledMaterial::baseColor. El valor alfa final del fragmento es la opacidad del modelo multiplicada por el color base alfa. El valor por defecto es (1.0, 1.0, 1.0, 1.0).
EMISSIVE_COLORvec3El color de la autoiluminación. Corresponde a PrincipledMaterial::emissiveFactor. El valor por defecto es (0.0, 0.0, 0.0).
METALNESSfloatMetalness Valor en el rango 0-1. Por defecto es 0, lo que significa que el material es dieléctrico (no metálico).
ROUGHNESSfloatRoughness valor en el rango 0-1. El valor por defecto es 0. Valores mayores suavizan los brillos especulares y difuminan los reflejos.
CANTIDAD_ESPECULARfloatThe strength of specularity en el rango 0-1. El valor por defecto es 0.5. Para objetos metálicos con metalness ajustado a 1 este valor no tendrá efecto. Cuando SPECULAR_AMOUNT y METALNESS tienen valores mayores que 0 pero menores que 1, el resultado es una mezcla entre los dos modelos de material.
NORMALvec3La normal interpolada en el espacio global, ajustada para la doble cara cuando la eliminación de caras está desactivada. Solo lectura.
UV0vec2Las coordenadas interpoladas de la textura. Sólo lectura.
VAR_WORLD_POSITIONvec3Posición interpolada del vértice en el espacio del mundo. Solo lectura.

Hagamos el color base del cubo rojo:

Cambio en main.qml, material.fragResultado
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
}

Cubo rojo con color base personalizado

Ahora reforcemos un poco el nivel de autoiluminación:

Cambio en main.qml, material.fragResultado
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
    EMISSIVE_COLOR = vec3(0.4);
}

Cubo rojo brillante de color emisivo

En lugar de tener valores hardcodeados en el shader, también podríamos usar propiedades QML expuestas como uniformes, incluso animadas:

Cambio en main.qml, material.fragResultado
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property color baseColor: "black"
    ColorAnimation on baseColor {
        from: "black"; to: "purple"; duration: 5000; loops: -1
    }
}
void MAIN()
{
    BASE_COLOR = vec4(baseColor.rgb, 1.0);
    EMISSIVE_COLOR = vec3(0.4);
}

Hagamos algo menos trivial, algo que no es implementable con un PrincipledMaterial y sus propiedades estándar incorporadas. El siguiente material visualiza las coordenadas UV de la textura de la malla del cubo. U va de 0 a 1, es decir, de negro a rojo, mientras que V es también de 0 a 1, de negro a verde.

Cambio en main.qml, material.fragResultado
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(UV0, 0.0, 1.0);
}

Cubo que muestra las coordenadas UV como gradiente de color rojo y verde

Ya que estamos, por qué no visualizar también las normales, esta vez sobre una esfera. Al igual que con los UVs, si un fragmento de sombreador de vértices personalizado alterara el valor de NORMAL, el valor interpolado por fragmento en el sombreador de fragmentos, también expuesto bajo el nombre NORMAL, reflejaría esos ajustes.

Cambio en main.qml, material.fragResultado
Model {
    source: "#Sphere"
    scale: Qt.vector3d(2, 2, 2)
    materials: CustomMaterial {
        fragmentShader: "material.frag"
    }
}
void MAIN()
{
    BASE_COLOR = vec4(NORMAL, 1.0);
}

Esfera que visualiza las normales de superficie como colores RGB

Colores

Cambiemos por un momento a un modelo de tetera, hagamos que el material sea una mezcla de metálico y dieléctrico, e intentemos establecerle un color base verde. El valor green QColor mapea a (0, 128, 0), en base al cual podría ser nuestro primer intento:

main.qml, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "teapot.mesh"
            scale: Qt.vector3d(60, 60, 60)
            eulerRotation.x: 30
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
    }
}
void MAIN()
{
    BASE_COLOR = vec4(0.0, 0.5, 0.0, 1.0);
    METALNESS = 0.6;
    SPECULAR_AMOUNT = 0.4;
    ROUGHNESS = 0.4;
}

Tetera verde con valores de color RGB directos

Esto no parece del todo correcto. Compárelo con el segundo enfoque:

Cambio en main.qml, material.fragResultado
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property color uColor: "green"
}
void MAIN()
{
    BASE_COLOR = vec4(uColor.rgb, 1.0);
    METALNESS = 0.6;
    SPECULAR_AMOUNT = 0.4;
    ROUGHNESS = 0.4;
}

Tetera verde con propiedad de color QML y conversión sRGB

Cambiando a un PrincipledMaterial, podemos confirmar que estableciendo el PrincipledMaterial::baseColor a "verde" y siguiendo la metalidad y otras propiedades, el resultado es idéntico a nuestro segundo enfoque:

Cambio en main.qmlResultado
materials: PrincipledMaterial {
    baseColor: "green"
    metalness: 0.6
    specularAmount: 0.4
    roughness: 0.4
}

Tetera verde con material estándar que muestra el color correcto

Si el tipo de la propiedad uColor se cambiara a vector4d, o a cualquier otro tipo distinto de color, los resultados cambiarían repentinamente y pasarían a ser idénticos a nuestro primer enfoque.

¿A qué se debe esto?

La respuesta está en la conversión de sRGB a lineal que se realiza implícitamente para las propiedades de color de DefaultMaterial, PrincipledMaterial, y también para las propiedades personalizadas con un tipo color en un CustomMaterial. Dicha conversión no se realiza para ningún otro valor, por lo que si el shader hardcodifica un valor de color, o lo basa en una propiedad QML con un tipo distinto de color, será el shader el que deba realizar la linealización en caso de que el valor de origen estuviera en espacio de color sRGB. La conversión a lineal es importante ya que Qt Quick 3D realiza tonemapping sobre los resultados del sombreado de fragmentos, y ese proceso asume como entrada valores en el espacio sRGB.

Las constantes incorporadas en QColor, tales como, "green", están todas dadas en el espacio sRGB. Por lo tanto, sólo asignar vec4(0.0, 0.5, 0.0, 1.0) a BASE_COLOR en el primer intento es insuficiente si queremos un resultado que coincida con un valor RGB (0, 128, 0) en el espacio sRGB. Consulte la documentación de BASE_COLOR en CustomMaterial para obtener una fórmula para linealizar dichos valores de color. Lo mismo se aplica a los valores de color recuperados mediante el muestreo de texturas: si los datos de la imagen de origen no están en el espacio de color sRGB, se necesita una conversión (a menos que tonemapping esté desactivado).

Mezcla

Escribir un valor inferior a 1.0 en BASE_COLOR.a no es suficiente si se desea obtener una mezcla alfa. Tales materiales cambiarán muy a menudo los valores de las propiedades sourceBlend y destinationBlend para obtener los resultados deseados.

También hay que tener en cuenta que el valor alfa combinado es el Node opacity multiplicado por el alfa del material.

Para visualizarlo, usemos un shader que asigna rojo con alpha 0.5 a BASE_COLOR:

main.qml, material.fragResultado
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "white"
        }
        PerspectiveCamera {
            id: camera
            z: 600
        }
        DirectionalLight { }
        Model {
            source: "#Cube"
            x: -150
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
        Model {
            source: "#Cube"
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: CustomMaterial {
                sourceBlend: CustomMaterial.SrcAlpha
                destinationBlend: CustomMaterial.OneMinusSrcAlpha
                fragmentShader: "material.frag"
            }
        }
        Model {
            source: "#Cube"
            x: 150
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: CustomMaterial {
                sourceBlend: CustomMaterial.SrcAlpha
                destinationBlend: CustomMaterial.OneMinusSrcAlpha
                fragmentShader: "material.frag"
            }
            opacity: 0.5
        }
    }
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 0.5);
}

Tres cubos que muestran las variaciones de opacidad de los modos de fusión

El primer cubo está escribiendo 0.5 al valor alfa del color pero no trae resultados visibles ya que la mezcla alfa no está habilitada. El segundo cubo habilita la mezcla alfa simple a través de las propiedades de CustomMaterial. El tercero también asigna una opacidad de 0.5 al Modelo, lo que significa que la opacidad efectiva es de 0.25.

Paso de datos entre el sombreador de vértices y el de fragmentos

Calculando un valor por vértice (por ejemplo, suponiendo un solo triángulo, para las 3 esquinas del triángulo), y luego pasándolo a la fase de fragmentos, donde para cada fragmento (por ejemplo, cada fragmento cubierto por el triángulo rasterizado) se hace accesible un valor interpolado. En los fragmentos de sombreado de materiales personalizados esto es posible gracias a la palabra clave VARYING. Esto proporciona una sintaxis similar a GLSL 120 y GLSL ES 100, pero funcionará independientemente de la API gráfica utilizada en tiempo de ejecución. El motor se encargará de reescribir la declaración variable según corresponda.

Veamos cómo sería el muestreo clásico de texturas con coordenadas UV. Las texturas van a ser cubiertas en una próxima sección, por ahora vamos a centrarnos en cómo obtenemos las coordenadas UV que se pueden pasar a la función texture() en el shader.

main.qml, material.vert, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(4, 4, 4)
            eulerRotation.x: 30
            materials: CustomMaterial {
                vertexShader: "material.vert"
                fragmentShader: "material.frag"
                property TextureInput someTextureMap: TextureInput {
                    texture: Texture {
                        source: "qt_logo_rect.png"
                    }
                }
            }
        }
    }
}
VARYING vec2 uv;
void MAIN()
{
    uv = UV0;
}
VARYING vec2 uv;
void MAIN()
{
    BASE_COLOR = texture(someTextureMap, uv);
}
qt_logo_rect.pngResultado

Textura del logotipo de Qt utilizado como datos de origen

Cubo con la textura del logotipo Qt aplicada a las caras

Tenga en cuenta que VARYING declaraciones. El nombre y el tipo deben coincidir, uv en el fragment shader expondrá la coordenada UV interpolada para el fragmento actual.

Cualquier otro tipo de datos se pueden pasar a la etapa de fragmento de una manera similar. Vale la pena señalar que en muchos casos no es necesario configurar las propias variaciones del material, ya que hay builtins que cubren muchas de las necesidades típicas. Esto incluye hacer las normales (interpoladas), UVs, posición del mundo (VAR_WORLD_POSITION), o el vector que apunta hacia la cámara (VIEW_VECTOR).

De hecho, el ejemplo anterior puede simplificarse a lo siguiente, ya que UV0 también está disponible automáticamente en la fase de fragmentos:

Cambio en main.qml, material.fragResultado
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property TextureInput someTextureMap: TextureInput {
        texture: Texture {
        source: "qt_logo_rect.png"
    }
}
void MAIN()
{
    BASE_COLOR = texture(someTextureMap, UV0);
}

Cubo con la textura del logotipo Qt aplicada a las caras

Para desactivar la interpolación de una variable, utilice la palabra clave flat tanto en el fragmento de sombreado de vértices como en el de fragmentos. Por ejemplo

VARYING flat vec2 v;

Texturas

Un CustomMaterial no tiene mapas de textura incorporados, lo que significa que no existe un equivalente de, por ejemplo, PrincipledMaterial::baseColorMap. Esto se debe a que la implementación de los mismos es a menudo trivial, mientras que da mucha más flexibilidad que lo que DefaultMaterial y PrincipledMaterial tiene incorporado. Además de simplemente muestrear una textura, los fragmentos de sombreado de fragmentos personalizados son libres de combinar y mezclar datos de varias fuentes cuando calculan los valores que asignan a BASE_COLOR, EMISSIVE_COLOR, ROUGHNESS, etc. Pueden basar estos cálculos en datos proporcionados a través de propiedades QML, datos interpolados enviados desde la etapa de vértices, valores recuperados del muestreo de texturas y valores codificados.

Como muestra el ejemplo anterior, exponer una textura a los sombreadores de vértices, de fragmentos o a ambos es muy similar a los valores uniformes escalares y vectoriales: una propiedad QML con el tipo TextureInput se asociará automáticamente a un sampler2D en el código del sombreador. Como siempre, no es necesario declarar este muestreador en el código del sombreador.

Un TextureInput hace referencia a un Texture, con una propiedad enabled adicional. Un Texture puede obtener sus datos de tres maneras: from an image file, from a texture with live Qt Quick contento can be provided from C++ a través de QQuick3DTextureData.

Nota: En lo que respecta a las propiedades de Texture, las relacionadas con la fuente, el mosaico y el filtrado son las únicas que se tienen en cuenta de forma implícita con los materiales personalizados, ya que el resto (como las transformaciones UV) depende de los sombreadores personalizados que las implementen como consideren oportuno.

Veamos un ejemplo en el que un modelo, una esfera en este caso, se texturiza utilizando contenido vivo de Qt Quick:

main.qml, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(4, 4, 4)
            eulerRotation.x: 30
            materials: CustomMaterial {
                fragmentShader: "material.frag"
                property TextureInput someTextureMap: TextureInput {
                    texture: Texture {
                        sourceItem: Rectangle {
                            width: 512; height: 512
                            color: "red"
                            Rectangle {
                                width: 32; height: 32
                                anchors.horizontalCenter: parent.horizontalCenter
                                y: 150
                                color: "gray";
                                NumberAnimation on rotation { from: 0; to: 360; duration: 3000; loops: -1 }
                            }
                            Text {
                                anchors.centerIn: parent
                                text: "Texture Map"
                                font.pointSize: 16
                            }
                        }
                    }
                }
            }
        }
    }
}
void MAIN()
{
    vec2 uv = vec2(UV0.x, 1.0 - UV0.y);
    vec4 c = texture(someTextureMap, uv);
    BASE_COLOR = c;
}

Aquí el subárbol 2D (Rectángulo con dos hijos: otro Rectángulo y el Texto) es renderizado en una textura 2D de 512x512 cada vez que esta mini-escena cambia. La textura es entonces expuesta al material personalizado bajo el nombre de someTextureMap.

Nótese la inversión de la coordenada V en el shader. Como se ha indicado anteriormente, los materiales personalizados, en los que existe una total programabilidad a nivel de shader, no ofrecen las características "fijas" de Texture y PrincipledMaterial. Esto significa que cualquier transformación de las coordenadas UV tendrá que ser aplicada por el shader. En este caso, sabemos que la textura se genera a través de Texture::sourceItem y, por tanto, es necesario voltear V para obtener algo que coincida con el conjunto de UV de la malla que estamos utilizando.

Lo que este ejemplo muestra es posible hacerlo con un PrincipledMaterial también. Vamos a hacerlo más interesante haciendo además un simple efecto de relieve:

material.fragResultado
void MAIN()
{
    vec2 uv = vec2(UV0.x, 1.0 - UV0.y);
    vec2 size = vec2(textureSize(someTextureMap, 0));
    vec2 d = vec2(1.0 / size.x, 1.0 / size.y);
    vec4 diff = texture(someTextureMap, uv + d) - texture(someTextureMap, uv - d);
    float c = (diff.x + diff.y + diff.z) + 0.5;
    BASE_COLOR = vec4(c, c, c, 1.0);
}

Con las características cubiertas hasta ahora se abre un amplio abanico de posibilidades para crear materiales que sombreen las mallas de formas visualmente impresionantes. Para terminar el recorrido básico, veamos un ejemplo que aplica mapas de altura y normales a una malla plana. (aquí se utiliza un archivo dedicado .mesh porque el incorporado #Rectangle no tiene suficientes subdivisiones) Para obtener mejores resultados de iluminación, utilizaremos iluminación basada en imágenes con una imagen HDR de 360 grados. La imagen también se establece como el skybox para que sea más claro lo que está sucediendo.

En primer lugar vamos a empezar con un CustomMaterial vacío :

main.qmlResultado
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.SkyBox
            lightProbe: Texture {
                source: "00489_OpenfootageNET_snowfield_low.hdr"
            }
        }
        PerspectiveCamera {
            z: 600
        }
        Model {
            source: "plane.mesh"
            scale: Qt.vector3d(400, 400, 400)
            z: 400
            y: -50
            eulerRotation.x: -90
            materials: CustomMaterial { }
        }
    }
}

Plano con paisaje invernal a partir de datos de textura personalizados

Ahora vamos a hacer algunos shaders que aplican un mapa de altura y normal a la malla:

Mapa de alturaMapa de normales

Textura de mapa de altura en escala de grises

Plano con mapa de alturas aplicado como desplazamiento

material.vert, material.frag
float getHeight(vec2 pos)
{
    return texture(heightMap, pos).r;
}

void MAIN()
{
    const float offset = 0.004;
    VERTEX.y += getHeight(UV0);
    TANGENT = normalize(vec3(0.0, getHeight(UV0 + vec2(0.0, offset)) - getHeight(UV0 + vec2(0.0, -offset)), offset * 2.0));
    BINORMAL = normalize(vec3(offset * 2.0, getHeight(UV0 + vec2(offset, 0.0)) - getHeight(UV0 + vec2(-offset, 0.0)), 0.0));
    NORMAL = cross(TANGENT, BINORMAL);
}
void MAIN()
{
    vec3 normalValue = texture(normalMap, UV0).rgb;
    normalValue.xy = normalValue.xy * 2.0 - 1.0;
    normalValue.z = sqrt(max(0.0, 1.0 - dot(normalValue.xy, normalValue.xy)));
    NORMAL = normalize(mix(NORMAL, TANGENT * normalValue.x + BINORMAL * normalValue.y + NORMAL * normalValue.z, 1.0));
}
Cambio en main.qmlResultado
materials: CustomMaterial {
    vertexShader: "material.vert"
    fragmentShader: "material.frag"
    property TextureInput normalMap: TextureInput {
        texture: Texture { source: "normalmap.jpg" }
    }
    property TextureInput heightMap: TextureInput {
        texture: Texture { source: "heightmap.png" }
    }
}

Plano con mapa de normales que muestra los detalles de la superficie

Nota: El objeto WasdController puede ser inmensamente útil durante el desarrollo y la solución de problemas, ya que permite navegar y mirar alrededor en la escena con el teclado y el ratón de una manera familiar. Tener una cámara controlada por el WasdController es tan simple como:

import QtQuick3D.Helpers
View3D {
    PerspectiveCamera {
        id: camera
    }
    // ...
}
WasdController {
    controlledObject: camera
}

Profundidad y texturas de pantalla

Cuando un fragmento de sombreador personalizado utiliza las palabras clave DEPTH_TEXTURE o SCREEN_TEXTURE, opta por generar las texturas correspondientes en una pasada de render separada, lo que no es necesariamente una operación barata, pero permite implementar diversas técnicas, como la refracción para materiales similares al vidrio.

DEPTH_TEXTURE es un sampler2D que permite muestrear una textura con el contenido del búfer de profundidad con todos los objetos opaque de la escena renderizados. De forma similar, SCREEN_TEXTURE es un sampler2D que permite muestrear una textura con el contenido de la escena excluyendo cualquier material transparente o cualquier material que también utilice la SCREEN_TEXTURE. La textura puede usarse para materiales que requieren el contenido del framebuffer en el que están siendo renderizados. La textura SCREEN_TEXTURE utiliza el mismo modo transparente que View3D. El tamaño de estas texturas coincide con el tamaño de View3D en píxeles.

Hagamos una demostración sencilla visualizando el contenido del búfer de profundidad a través de DEPTH_TEXTURE. El far clip value de la cámara se reduce aquí del 10000 por defecto a 2000, para tener un rango más pequeño, y así tener las diferencias de valor de profundidad visualizadas más obvias. El resultado es un rectángulo que pasa a visualizar el buffer de profundidad de la escena sobre su superficie.

main.qml, material.fragResultado
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Rectangle {
    width: 400
    height: 400
    color: "black"
    View3D {
        anchors.fill: parent
        PerspectiveCamera {
            id: camera
            z: 600
            clipNear: 1
            clipFar: 2000
        }
        DirectionalLight { }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(150, 200, -1000)
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: PrincipledMaterial { }
        }
        Model {
            source: "#Cylinder"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(400, 200, -1000)
            materials: PrincipledMaterial { }
            opacity: 0.3
        }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(-150, 200, -600)
            materials: PrincipledMaterial { }
        }
        Model {
            source: "#Cone"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(0, 400, -1200)
            materials: PrincipledMaterial { }
        }
        Model {
            source: "#Rectangle"
            scale: Qt.vector3d(3, 3, 3)
            y: -150
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
    }
    WasdController {
        controlledObject: camera
    }
}
void MAIN()
{
    float zNear = CAMERA_PROPERTIES.x;
    float zFar = CAMERA_PROPERTIES.y;
    float zRange = zFar - zNear;
    vec4 depthSample = texture(DEPTH_TEXTURE, vec2(UV0.x, 1.0 - UV0.y));
    float zn = 2.0 * depthSample.r - 1.0;
    float d = 2.0 * zNear * zFar / (zFar + zNear - zn * zRange);
    d /= zFar;
    BASE_COLOR = vec4(d, d, d, 1.0);
}

Observe cómo el cilindro no está presente en DEPTH_TEXTURE debido a su dependencia de la semitransparencia, lo que lo sitúa en una categoría diferente a la de los otros objetos que son todos opacos. Estos objetos no escriben en el buffer de profundidad, aunque comprueban los valores de profundidad escritos por los objetos opacos, y dependen de ser renderizados en orden de atrás hacia delante. Por lo tanto, tampoco están presentes en DEPTH_TEXTURE.

¿Qué ocurre si cambiamos el shader a la muestra SCREEN_TEXTURE?

material.fragResultado
void MAIN()
{
    vec4 c = texture(SCREEN_TEXTURE, vec2(UV0.x, 1.0 - UV0.y));
    if (c.a == 0.0)
        c.rgb = vec3(0.2, 0.1, 0.3);
    BASE_COLOR = c;
}

Escena que muestra la textura de la pantalla con efecto de distorsión

Aquí el rectángulo se texturiza con SCREEN_TEXTURE, reemplazando los píxeles transparentes por púrpura.

Funciones del procesador de luz

Una característica avanzada de CustomMaterial es la capacidad de definir funciones en el fragment shader que reimplementan las ecuaciones de iluminación que se utilizan para calcular el color del fragmento. Una función de procesador de luz, cuando está presente, se llama una vez por cada luz en la escena, para cada fragmento. Existe una función específica para cada tipo de luz, así como para la contribución ambiental y especular. Cuando no hay una función de procesador de luz correspondiente, se utilizan los cálculos estándar, como haría un PrincipledMaterial. Cuando un procesador de luz está presente, pero el cuerpo de la función está vacío, significa que no habrá contribución de un determinado tipo de luces en la escena.

Consulte la documentación de CustomMaterial para más detalles sobre funciones como DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT, y SPECULAR_LIGHT.

Materiales personalizados sin sombreado

Existe otro tipo de CustomMaterial: unshaded materiales personalizados. En todos los ejemplos anteriores se han utilizado materiales personalizados shaded, dejando la propiedad shadingMode en su valor por defecto CustomMaterial.Shaded.

¿Qué ocurre si cambiamos esta propiedad a CustomMaterial.Unshaded?

En primer lugar, palabras clave como BASE_COLOR, EMISSIVE_COLOR, METALNESS, etc. ya no tienen el efecto deseado. Esto se debe a que un material sin sombreado, como su nombre indica, no se modifica automáticamente con gran parte del código de sombreado estándar, ignorando así las luces, la iluminación basada en imágenes, las sombras y la oclusión ambiental en la escena. Más bien, un material sin sombreado da el control total al sombreador a través de la palabra clave FRAGCOLOR. Esto es similar a gl_FragColor: el color asignado a FRAGCOLOR es el resultado y el color final del fragmento, sin más ajustes por parte de Qt Quick 3D.

main.qml, material.frag, material2.fragResultado
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Cylinder"
            x: -100
            eulerRotation.x: 30
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
        Model {
            source: "#Cylinder"
            x: 100
            eulerRotation.x: 30
            materials: CustomMaterial {
                shadingMode: CustomMaterial.Unshaded
                fragmentShader: "material2.frag"
            }
        }
    }
}
void MAIN()
{
    BASE_COLOR = vec4(1.0);
}
void MAIN()
{
    FRAGCOLOR = vec4(1.0);
}

Cubo y cilindro con material personalizado sin sombrear

Observe cómo el cilindro derecho ignora el DirectionalLight de la escena. Su sombreado no sabe nada de la iluminación de la escena, el color final del fragmento es todo blanco.

El sombreador de vértices en un material sin sombreado todavía tiene las entradas típicas disponibles: VERTEX NORMAL , MODELVIEWPROJECTION_MATRIX, etc. y puede escribir en POSITION. Sin embargo, el sombreador de fragmentos ya no dispone de las mismas comodidades: NORMAL, UV0, o VAR_WORLD_POSITION no están disponibles en el sombreador de fragmentos de un material sin sombreado. Más bien, ahora depende del código del sombreador calcular y pasar usando VARYING todo lo que necesita para determinar el color final del fragmento.

Veamos un ejemplo que tiene tanto un sombreador de vértices como de fragmentos. La posición alterada del vértice se pasa al fragment shader, con un valor interpolado disponible para cada fragmento.

main.qml, material.vert, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(3, 3, 3)
            materials: CustomMaterial {
                property real time: 0.0
                NumberAnimation on time { from: 0; to: 100; duration: 20000; loops: -1 }
                property real amplitude: 10.0
                shadingMode: CustomMaterial.Unshaded
                vertexShader: "material.vert"
                fragmentShader: "material.frag"
            }
        }
    }
}
VARYING vec3 pos;
void MAIN()
{
    pos = VERTEX;
    pos.x += sin(time * 4.0 + pos.y) * amplitude;
    POSITION = MODELVIEWPROJECTION_MATRIX * vec4(pos, 1.0);
}
VARYING vec3 pos;
void MAIN()
{
    FRAGCOLOR = vec4(vec3(pos.x * 0.02, pos.y * 0.02, pos.z * 0.02), 1.0);
}

Los materiales sin sombreado son útiles cuando no es necesario o deseado interactuar con la iluminación de la escena, y el material necesita un control total sobre el color final del fragmento. Observe cómo el ejemplo anterior no tiene ni DirectionalLight ni ninguna otra luz, pero la esfera con el material personalizado se muestra como se esperaba.

Nota: Un material sin sombreado que sólo tiene un fragmento de sombreado de vértices, pero no especifica la propiedad fragmentShader, seguirá siendo funcional, pero los resultados son como si el shadingMode se hubiera establecido en Shaded. Por lo tanto, no tiene mucho sentido cambiar shadingMode para materiales que sólo tienen un vertex shader.

Programación de efectos

Los efectos de post-procesado aplican uno o más fragment shaders al resultado de un View3D. La salida de estos fragment shaders se muestra entonces en lugar de los resultados del renderizado original. Esto es conceptualmente muy similar a Qt Quick's ShaderEffect y ShaderEffectSource.

Nota: Los efectos de post-procesamiento sólo están disponibles cuando el renderMode para el View3D se establece en View3D.Offscreen.

También se pueden especificar fragmentos de sombreado de vértices personalizados para un efecto, pero su utilidad es limitada y, por lo tanto, se espera que se utilicen relativamente poco. La entrada de vértices para un efecto de post-procesado es un quad (ya sean dos triángulos o una tira de triángulos), transformar o desplazar los vértices de eso no suele ser útil. Sin embargo, puede tener sentido tener un sombreador de vértices para calcular y pasar datos al sombreador de fragmentos usando la palabra clave VARYING. Como es habitual, el fragment shader recibirá entonces un valor interpolado basado en la coordenada actual del fragmento.

La sintaxis de los fragmentos de sombreado asociados a un Effect es idéntica a la de los sombreadores para un CustomMaterial sin sombreado. En lo que respecta a las palabras clave especiales incorporadas, VARYING, MAIN, FRAGCOLOR (sólo sombreador de fragmentos), POSITION (sólo sombreador de vértices), VERTEX (sólo sombreador de vértices) y MODELVIEWPROJECTION_MATRIX funcionan de forma idéntica a CustomMaterial.

Las palabras clave especiales más importantes para los sombreadores de fragmentos Effect son las siguientes:

NombreTipoDescripción
ENTRADAsampler2D o sampler2DArrayEl muestreador para la textura de entrada. Un efecto típicamente muestreará esto usando INPUT_UV.
ENTRADA_UVvec2Coordenadas UV para el muestreo INPUT.
TAMAÑO_ENTRADAvec2El tamaño de la textura INPUT, en píxeles. Esta es una alternativa conveniente a llamar a textureSize().
TAMAÑO_SALIDAvec2El tamaño de la textura de salida, en píxeles. Igual a INPUT_SIZE en muchos casos, pero un efecto multipase puede tener pases que den salida a texturas intermedias con diferentes tamaños.
PROFUNDIDAD_TEXTURAsampler2DTextura de profundidad con el contenido del buffer de profundidad con los objetos opacos de la escena. Al igual que con CustomMaterial, la presencia de esta palabra clave en el shader desencadena la generación de la textura de profundidad automáticamente.

Nota: Cuando el renderizado multivista está activado, la textura de entrada es un array de texturas 2D. Las funciones GLSL como texture() y textureSize() toman/devuelven un vec3/ivec3, respectivamente, entonces. Utiliza VIEW_INDEX para la capa. En aplicaciones VR/AR que deseen funcionar tanto con como sin renderizado multivista, el enfoque portable es escribir el código del shader así:

#if QSHADER_VIEW_COUNT >= 2
    vec4 c = texture(INPUT, vec3(INPUT_UV, VIEW_INDEX));
#else
    vec4 c = texture(INPUT, INPUT_UV);
#endif

Un efecto de post-procesado

Empecemos con una escena simple, esta vez usando algunos objetos más, incluyendo un rectángulo texturizado que usa una textura de tablero de ajedrez como su mapa de color base.

main.qmlResultado
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }

        PerspectiveCamera { z: 400 }

        DirectionalLight { }

        Texture {
            id: checkerboard
            source: "checkerboard.png"
            scaleU: 20
            scaleV: 20
            tilingModeHorizontal: Texture.Repeat
            tilingModeVertical: Texture.Repeat
        }

        Model {
            source: "#Rectangle"
            scale: Qt.vector3d(10, 10, 1)
            eulerRotation.x: -45
            materials: PrincipledMaterial {
                baseColorMap: checkerboard
            }
        }

        Model {
            source: "#Cone"
            position: Qt.vector3d(100, -50, 100)
            materials: PrincipledMaterial { }
        }

        Model {
            source: "#Cube"
            position.y: 100
            eulerRotation.y: 20
            materials: PrincipledMaterial { }
        }

        Model {
            source: "#Sphere"
            position: Qt.vector3d(-150, 200, -100)
            materials: PrincipledMaterial { }
        }
    }
}

Escena de referencia con esfera, cono y cubo

Ahora vamos a aplicar un efecto a toda la escena. Más precisamente, al View3D. Cuando hay múltiples elementos View3D en la escena, cada uno tiene su propio SceneEnvironment y por lo tanto tienen su propia cadena de efectos de post-procesamiento. En el ejemplo hay un único View3D que cubre toda la ventana.

Cambio en main.qmlefecto.frag
environment: SceneEnvironment {
    backgroundMode: SceneEnvironment.Color
    clearColor: "black"
    effects: redEffect
}

Effect {
    id: redEffect
    property real uRed: 1.0
    NumberAnimation on uRed { from: 1; to: 0; duration: 5000; loops: -1 }
    passes: Pass {
        shaders: Shader {
            stage: Shader.Fragment
            shader: "effect.frag"
        }
    }
}
void MAIN()
{
    vec4 c = texture(INPUT, INPUT_UV);
    c.r = uRed;
    FRAGCOLOR = c;
}

Este sencillo efecto altera el valor del canal de color rojo. La exposición de propiedades QML como uniformes funciona de la misma manera con los efectos que con los materiales personalizados. El shader comienza con una línea que va a ser muy común al escribir fragment shaders para efectos: muestreo INPUT en las coordenadas UV INPUT_UV. A continuación, realiza los cálculos deseados, y asigna el color final del fragmento a FRAGCOLOR.

Muchas de las propiedades establecidas en el ejemplo están en plural (efectos, pases, shaders). Mientras que la sintaxis de la lista [ ] puede omitirse cuando tiene un solo elemento, todas estas propiedades son listas, y pueden contener más de un elemento. ¿Por qué?

  • effects es una lista, porque View3D permite encadenar múltiples efectos. Los efectos se aplican en el orden en que se añaden a la lista. Esto permite aplicar fácilmente dos o más efectos juntos en View3D, y es similar a lo que se puede conseguir en Qt Quick anidando elementos de ShaderEffect. La textura INPUT del siguiente efecto es siempre una textura que contiene la salida del efecto anterior. La salida del último efecto en lo que se utiliza como salida final del View3D.
  • passes es una lista, porque a diferencia de ShaderEffect, Effect tiene soporte incorporado para múltiples pasadas. Un efecto multipase es más potente que encadenar varios efectos independientes en effects: un pase puede tener como salida una textura intermedia temporal, que puede utilizarse como entrada para pases posteriores, además de la textura de entrada original del efecto. Esto permite crear efectos complejos que calculan, renderizan y mezclan múltiples texturas para llegar al color final del fragmento. Este caso de uso avanzado no va a ser cubierto aquí. Consulte la página de documentación Effect para más detalles.
  • shaders es una lista, porque un efecto puede tener asociados tanto un sombreador de vértices como uno de fragmentos.

Encadenamiento de múltiples efectos

Veamos un ejemplo en el que el efecto del ejemplo anterior se complementa con otro efecto similar al incorporado en DistortionSpiral.

Cambio en main.qmlefecto2.frag
environment: SceneEnvironment {
    backgroundMode: SceneEnvironment.Color
    clearColor: "black"
    effects: [redEffect, distortEffect]
}

Effect {
    id: redEffect
    property real uRed: 1.0
    NumberAnimation on uRed { from: 1; to: 0; duration: 5000; loops: -1 }
    passes: Pass {
        shaders: Shader {
            stage: Shader.Fragment
            shader: "effect.frag"
        }
    }
}

Effect {
    id: distortEffect
    property real uRadius: 0.1
    NumberAnimation on uRadius { from: 0.1; to: 1.0; duration: 5000; loops: -1 }
    passes: Pass {
        shaders: Shader {
            stage: Shader.Fragment
            shader: "effect2.frag"
        }
    }
}
void MAIN()
{
    vec2 center_vec = INPUT_UV - vec2(0.5, 0.5);
    center_vec.y *= INPUT_SIZE.y / INPUT_SIZE.x;
    float dist_to_center = length(center_vec) / uRadius;
    vec2 texcoord = INPUT_UV;
    if (dist_to_center <= 1.0) {
        float rotation_amount = (1.0 - dist_to_center) * (1.0 - dist_to_center);
        float r = radians(360.0) * rotation_amount / 4.0;
        float cos_r = cos(r);
        float sin_r = sin(r);
        mat2 rotation = mat2(cos_r, sin_r, -sin_r, cos_r);
        texcoord = vec2(0.5, 0.5) + rotation * (INPUT_UV - vec2(0.5, 0.5));
    }
    vec4 c = texture(INPUT, texcoord);
    FRAGCOLOR = c;
}

Ahora la pregunta quizás sorprendente: ¿por qué es un mal ejemplo?

Más concretamente, no es malo, sino que muestra un patrón que a menudo puede ser beneficioso evitar.

Encadenar efectos de esta manera puede ser útil, pero es importante tener en cuenta las implicaciones de rendimiento: hacer dos pases de render (uno para generar una textura con el canal de color rojo ajustado, y luego otro dos calcular la distorsión) es bastante derrochador cuando uno sería suficiente. Si se combinaran los fragmentos de fragment shader, se podría haber conseguido el mismo resultado con un solo efecto.

Definición de datos de malla y textura desde C++

La generación procedimental de datos de malla e imagen de textura sigue pasos similares:

  • Subclase QQuick3DGeometry o QQuick3DTextureData
  • Establecer los datos de vértice o imagen deseados en la construcción llamando a las funciones miembro protegidas de la clase base.
  • Si en algún momento se necesitan cambios dinámicos, establecer los nuevos datos y llamar a update()
  • Una vez realizada la implementación, la clase necesita ser registrada para hacerla visible en QML
  • Model y los objetos Texture en QML pueden ahora utilizar el proveedor de datos de vértice o imagen personalizado estableciendo la propiedad Model::geometry o Texture::textureData

Datos de vértices personalizados

Los datos de vértices se refieren a la secuencia de valores (normalmente float) que componen una malla. En lugar de cargar archivos .mesh, un proveedor de geometría personalizado se encarga de proporcionar los mismos datos. Los datos de vértice consisten en attributes, como la posición, las coordenadas de textura (UV) o las normales. La especificación de los atributos describe qué tipo de atributos están presentes, el tipo de componente (por ejemplo, un vector flotante de 3 componentes para la posición del vértice que consta de valores x, y, z), en qué desplazamiento comienzan en los datos proporcionados, y cuál es el stride (el incremento que hay que añadir al desplazamiento para apuntar al siguiente elemento para el mismo atributo).

Esto puede parecer familiar si se ha trabajado con APIs gráficas, como OpenGL o Vulkan directamente, porque la forma en que se especifica la entrada de vértices con esas APIs se asemeja vagamente a lo que define un archivo .mesh o una instancia de QQuick3DGeometry.

Además, también debe especificarse la topología de la malla (tipo de primitiva). Para el dibujo indexado, también deben proporcionarse los datos para un búfer de índice.

Existe una implementación de geometría personalizada incorporada: el módulo QtQuick3D.Helpers incluye un tipo GridGeometry. Esto permite renderizar una rejilla en la escena con primitivas de línea, sin tener que implementar una subclase personalizada de QQuick3DGeometry.

Otro caso de uso común es el renderizado de puntos. Esto es bastante sencillo de hacer ya que la especificación de atributos va a ser mínima: proporcionamos tres floats (x, y, z) para cada vértice, nada más. Una subclase de QQuick3DGeometry podría implementar una geometría consistente en 2000 puntos de forma similar a la siguiente:

clear();
const int N = 2000;
const int stride = 3 * sizeof(float);
QByteArray v;
v.resize(N * stride);
float *p = reinterpret_cast<float *>(v.data());
QRandomGenerator *rg = QRandomGenerator::global();
for (int i = 0; i < N; ++i) {
    const float x = float(rg->bounded(200.0f) - 100.0f) / 20.0f;
    const float y = float(rg->bounded(200.0f) - 100.0f) / 20.0f;
    *p++ = x;
    *p++ = y;
    *p++ = 0.0f;
}
setVertexData(v);
setStride(stride);
setPrimitiveType(QQuick3DGeometry::PrimitiveType::Points);
addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::F32Type);

Combinada con un material de

DefaultMaterial {
    lighting: DefaultMaterial.NoLighting
    cullMode: DefaultMaterial.NoCulling
    diffuseColor: "yellow"
    pointSize: 4
}

el resultado final es similar a este (aquí visto desde un ángulo de cámara alterado, con la ayuda de WasdController):

Geometría de nubes de puntos representada como puntos individuales

Nota: Tenga en cuenta que los tamaños de punto y anchos de línea distintos de 1 pueden no ser compatibles en tiempo de ejecución, dependiendo de la API de gráficos subyacente. Esto no es algo sobre lo que Qt tenga control. Por lo tanto, puede ser necesario implementar técnicas alternativas en lugar de confiar en el dibujo de puntos y líneas.

Datos de textura personalizados

Con las texturas, los datos que hay que proporcionar son mucho más simples estructuralmente: son los datos de píxel en bruto, con un número variable de bytes por píxel, dependiendo del formato de la textura. Por ejemplo, una textura RGBA espera cuatro bytes por píxel, mientras que RGBA16F tiene cuatro medios floats por píxel. Esto es similar a lo que QImage almacena internamente. Sin embargo, las texturas 3D de Qt Quick pueden tener formatos cuyos datos no pueden ser representados por un QImage. Por ejemplo, texturas HDR de coma flotante, o texturas comprimidas. Por lo tanto, los datos de QQuick3DTextureData se proporcionan siempre como una secuencia de bytes sin procesar. Esto puede resultar familiar si se ha trabajado directamente con API de gráficos, como OpenGL o Vulkan.

Para más detalles, consulte las páginas de documentación QQuick3DGeometry y QQuick3DTextureData.

Ver también CustomMaterial, Effect, QQuick3DGeometry, QQuick3DTextureData, Qt Quick 3D - Ejemplo de Efecto Personalizado, Qt Quick 3D - Ejemplo de Shaders Personalizados, Qt Quick 3D - Ejemplo de Materiales Personalizados, Qt Quick 3D - Ejemplo de Geometría Personalizada, y Qt Quick 3D - Ejemplo de Textura Procedural.

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