Spec-Zone.ru › Qt

Данные о программируемых материалах, эффектах, геометрии и текстурах

Хотя встроенные материалы Qt Quick 3D, DefaultMaterial и PrincipledMaterial, позволяют в широкой степени настраивать свойства, они не предоставляют возможности программирования на уровне вершинного и фрагментного шейдеров. Для этого предоставляется тип CustomMaterial.

Модель с PrincipledMaterial С CustomMaterial, преобразующим вершины

Также существуют эффекты пост-обработки, где выполняется один или несколько проходов обработки буфера цвета, при необходимости учитывая буфер глубины, прежде чем вывод View3D передаётся Qt Quick, также существующие в двух вариантах:

  • набор встроенных, предварительно созданных эффектов, таких как размытие, глубина резкости, искажение и другие,
  • custom эффекты, реализованные приложением в виде кода фрагментного шейдера и спецификации проходов обработки в объекте Effect.
Сцена без эффекта Та же сцена с применённым пользовательским эффектом пост-обработки

Помимо программируемых материалов и пост-обработки, существуют два типа данных, обычно предоставляемых в виде файлов (.mesh файлы или изображения, такие как .png):

  • данные вершин, включая геометрию рендерируемой сетки, координаты текстур, нормали, цвета и другие данные,
  • содержимое для текстур, которые затем используются в качестве текстурных карт для рендерируемых объектов или используются с небосводами или освещением на основе изображений.

Если приложение пожелает, оно может предоставить такие данные из C++ в виде QByteArray. Такие данные также могут меняться со временем, позволяя динамически генерировать и изменять данные для Model или Texture.

Сетка, рендерируемая путём динамического задания данных вершин из C++ Куб, текстурированный данными изображений, сгенерированными из C++

Эти четыре подхода к настройке и динамизации материалов, эффектов, геометрии и текстур позволяют программировать затенение и динамическое генерирование данных, которые шейдеры получают как входные данные. Следующие разделы предоставляют обзор этих функций. Полная справка доступна на страницах документации для соответствующих типов:

Функция Документация по справке Соответствующие примеры
Пользовательские материалы CustomMaterial Qt Quick 3D - Пример пользовательских шейдеров, Qt Quick 3D - Пример пользовательских материалов
Пользовательские эффекты пост-обработки Effect Qt Quick 3D - Пример пользовательского эффекта
Пользовательская геометрия QQuick3DGeometry, Model::geometry Qt Quick 3D - Пример пользовательской геометрии
Пользовательские данные текстуры QQuick3DTextureData, Texture::textureData Qt Quick 3D - Пример текстуры процедурного типа

Программируемость для материалов

Рассмотрим сцену с кубом, начав со стандартного PrincipledMaterial и CustomMaterial:

PrincipledMaterial CustomMaterial
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 { }
         }
    }
}

Оба результата будут идентичными, поскольку CustomMaterial фактически является PrincipledMaterial, если не добавлен код вершинного или фрагментного шейдера.

Примечание: Свойства, такие как baseColor, metalness, baseColorMap и многие другие, не имеют эквивалентов в типе QML CustomMaterial. Это сделано намеренно: настройка материала осуществляется через код шейдера, а не просто путём задания нескольких фиксированных значений.

Наш первый вершинный шейдер

Добавим фрагмент пользовательского вершинного шейдера. Это делается путём ссылки на файл в свойстве vertexShader. Подход будет аналогичным для фрагментных шейдеров. Эти ссылки работают как Image.source или ShaderEffect.vertexShader: они являются локальными или qrc URL-адресами, а относительный путь обрабатывается относительно расположения файла .qml. Поэтому общий подход заключается в размещении файлов .vert и .frag в системе ресурсов Qt (qt_add_resources при использовании CMake) и ссылке на них с помощью относительного пути.

В Qt 6.0 встроенные строки шейдеров больше не поддерживаются, ни в Qt Quick, ни в Qt Quick 3D. (обратите внимание на то, что эти свойства являются URL-адресами, а не строками) Однако из-за их сущностной динамичности пользовательские материалы и эффекты пост-обработки в Qt Quick 3D по-прежнему предоставляют фрагменты шейдеров в исходной форме в ссылаемых файлах. Это отличие от ShaderEffect, где шейдеры являются завершенными сами по себе, без дальнейшего изменения движком, и поэтому ожидается, что они будут предоставлены в виде предварительно подготовленных .qsb пакетов шейдеров.

Примечание: В Qt Quick 3D URL-адреса могут ссылаться только на локальные ресурсы. Схемы для удаленного контента не поддерживаются.

Примечание: Язык затенения используемый совместим с Vulkan-совместимым GLSL. Файлы .vert и .frag не являются полными шейдерами сами по себе, поэтому часто называются snippets. Поэтому в этих фрагментах не предусмотрены блоки униформ, входные и выходные переменные или униформы-сэмплеры. Вместо этого движок Qt Quick 3D будет их соответствующим образом изменять.

Изменения в main.qml, material.vert Результат
materials: CustomMaterial {
    vertexShader: "material.vert"
}
void MAIN()
{
}

Ожидается, что пользовательский фрагмент вершинного или фрагментного шейдера предоставит одну или несколько функций с предварительно определенными именами, такими как MAIN, DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT, SPECULAR_LIGHT. Сейчас сосредоточимся на MAIN.

Как показано здесь, конечный результат с пустым MAIN() точно такой же, как и прежде.

Прежде чем сделать его более интересным, давайте рассмотрим обзор наиболее часто используемых специальных ключевых слов в пользовательских фрагментах вершинного шейдера. Это не полный список. Для получения полной справки см. страницу CustomMaterial.

END_OF_DOCUMENT_MARKER
Ключевое слово Тип Описание
MAIN void MAIN() — точка входа. Эта функция должна всегда присутствовать в фрагменте пользовательской вершинной шейдерной программы; в противном случае её предоставление не имеет смысла.
VERTEX vec3 Позиция вершины, получаемая шейдером в качестве входных данных. Типичный случай использования вершинных шейдеров в пользовательских материалах — изменение (смещение) значений x, y или z этого вектора путём присвоения значения всему вектору или некоторым его компонентам.
NORMAL vec3 Нормаль вершины из входных данных сетки, либо все нули, если данные о нормалях отсутствуют. Как и в случае с VERTEX, шейдеру предоставлена свобода изменения значения по своему усмотрению. Изменённое значение затем используется остальной частью конвейера, включая вычисления освещения на стадии фрагмента.
UV0 vec2 Первый набор координат текстуры из входных данных сетки, либо все нули, если данные о UV-координатах отсутствуют. Как и в случаях с VERTEX и NORMAL, значение может быть изменено.
MODELVIEWPROJECTION_MATRIX mat4 Матрица модель-вида-проекции. Чтобы унифицировать поведение независимо от используемого графического API для отрисовки, все вершинные данные и матрицы преобразования следуют конвенциям OpenGL на этом уровне. (Ось Y направлена вверх, матрица проекции совместима с OpenGL). Только для чтения.
MODEL_MATRIX mat4 Матрица модели (мира). Только для чтения.
NORMAL_MATRIX mat3 Транспонированная обратная верхне-левая 3x3 матрица модели. Только для чтения.
CAMERA_POSITION vec3 Позиция камеры в мировом пространстве. В примерах на этой странице это (0, 0, 600). Только для чтения.
CAMERA_DIRECTION vec3 Вектор направления камеры. В примерах на этой странице это (0, 0, -1). Только для чтения.
CAMERA_PROPERTIES vec2 Значения ближней и дальней плоскостей обрезки камеры. В примерах на этой странице это (10, 10000). Только для чтения.
POINT_SIZE float Актуально только при отрисовке с топологией точек, например, когда пользовательская геометрия сетки предоставляет такую геометрию. Запись в это значение эквивалентна установке pointSize в материале PrincipledMaterial.
POSITION vec4 Аналогично gl_Position. Если отсутствует, автоматически генерируется оператор присваивания по умолчанию, использующий MODELVIEWPROJECTION_MATRIX и VERTEX. Именно поэтому пустая функция MAIN() функциональна, и в большинстве случаев нет необходимости присваивать ей пользовательское значение.

Давайте создадим пользовательский материал, который смещает вершины в соответствии с каким-либо шаблоном. Чтобы сделать его более интересным, добавим некоторые анимированные свойства QML, значения которых в итоге будут экспонированы как униформы в коде шейдера. (точнее, большинство свойств будет сопоставлено с членами в блоке униформ, поддерживаемом буфером униформ во время выполнения, но Qt Quick 3D удобно делает такие детали прозрачными для разработчика пользовательского материала)

Изменения в main.qml, material.vert Результат
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;
}

Униформы из свойств QML

Пользовательские свойства в объекте CustomMaterial отображаются в униформы. В приведённом примере это включает uAmplitude и uTime. Всякий раз, когда значения изменяются, обновлённое значение станет видимым в шейдере. Этот концепт может быть уже знаком по ShaderEffect.

Имя свойства QML и переменной GLSL должны совпадать. Нет отдельного объявления в коде шейдера для отдельных униформ. Вместо этого имя свойства QML может быть использовано напрямую. Вот почему в приведённом выше примере можно просто использовать uTime и uAmplitude в фрагменте вершинного шейдера без предварительного объявления для них.

В следующей таблице показано, как типы отображаются:

Тип QML Тип шейдера Примечания
real, int, bool float, int, bool
color vec4 Неявное преобразование sRGB в линейное
vector2d vec2
vector3d vec3
vector4d vec4
matrix4x4 mat4
quaternion vec4 вещественная часть — w
rect vec4
point, size vec2
TextureInput sampler2D

Улучшение примера

Прежде чем переходить дальше, сделаем пример немного более привлекательным. Добавив повернутый прямоугольный объект и заставив DirectionalLight отбрасывать тени, мы можем проверить, что изменение вершин куба правильно отражается во всех проходах отрисовки, включая карты теней. Для видимой тени свет теперь расположен немного выше по оси Y, и ему присвоено вращение, чтобы он был направлен частично вниз. (этот свет — directional , вращение имеет значение)

main.qml, material.vert Результат
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;
}

Добавление фрагментного шейдера

Многие пользовательские материалы захотят иметь также фрагментный шейдер. На самом деле, многие захотят только фрагментный шейдер. Если нет дополнительных данных, передаваемых из вершинного в фрагментный этап, и стандартное вершинное преобразование достаточно, можно опустить свойство vertexShader из CustomMaterial.

Изменение в main.qml, material.frag Результат
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
}

Наш первый фрагментный шейдер содержит пустую функцию MAIN(). Это ничем не отличается от того, чтобы вообще не указывать фрагментный шейдерный фрагмент: результат выглядит так же, как и с использованием стандартного PrincipledMaterial.

Давайте рассмотрим некоторые часто используемые ключевые слова в фрагментных шейдерах. Это неполный список, обратитесь к документации CustomMaterial для получения полной справки. Многие из них являются читаемыми и записываемыми, т.е. имеют значение по умолчанию, но шейдер может и, скорее всего, захочет присвоить им другое значение.

Как подсказывают названия, многие из них сопоставляются с аналогично названными свойствами PrincipledMaterial с тем же значением и семантикой, следуя модели материала metallic-roughness. За реализацией пользовательского материала остается решение о том, как рассчитываются эти значения: например, значение для BASE_COLOR можно задать в шейдере, получить путем сэмплирования текстуры или вычислить на основе свойств QML, экспонированных как униформы, или на основе интерполированных данных, переданных из вершинного шейдера.

Ключевое слово Тип Описание
BASE_COLOR vec4 Основной цвет и альфа-значение. Соответствует PrincipledMaterial::baseColor. Окончательное альфа-значение фрагмента — это непрозрачность модели, умноженная на альфа-значение базового цвета. Значение по умолчанию — (1.0, 1.0, 1.0, 1.0).
EMISSIVE_COLOR vec3 Цвет собственного излучения. Соответствует PrincipledMaterial::emissiveFactor. Значение по умолчанию — (0.0, 0.0, 0.0).
METALNESS float Значение Металличности в диапазоне 0-1. По умолчанию 0, что означает, что материал диэлектрический (неметаллический).
ROUGHNESS float Значение Шероховатости в диапазоне 0-1. Значение по умолчанию — 0. Большие значения смягчают блики и размывают отражения.
SPECULAR_AMOUNT float Сила зеркальности в диапазоне 0-1. Значение по умолчанию — 0.5. Для металлических объектов, с metalness равным 1, это значение не повлияет. Когда оба SPECULAR_AMOUNT и METALNESS имеют значения больше 0, но меньше 1, результат — это смесь двух моделей материалов.
NORMAL vec3 Интерполированная нормаль в мировом пространстве, скорректированная на двусторонность, когда отсечение граней отключено. Только для чтения.
UV0 vec2 Интерполированные текстурные координаты. Только для чтения.
VAR_WORLD_POSITION vec3 Интерполированная позиция вершины в мировом пространстве. Только для чтения.

Давайте сделаем базовый цвет куба красным:

Изменение в main.qml, material.frag Результат
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
}

Теперь немного усилим уровень самоосвещения:

Изменение в main.qml, material.frag Результат
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
    EMISSIVE_COLOR = vec3(0.4);
}

Вместо жёстко заданных значений в шейдере, мы также можем использовать свойства QML, экспонированные как униформы, даже анимированные:

Изменение в main.qml, material.frag Результат
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);
}

Давайте сделаем что-то менее тривиальное, что не реализуется с помощью PrincipledMaterial и его стандартных встроенных свойств. Следующий материал визуализирует текстурные UV-координаты куба. U изменяется от 0 до 1, от чёрного до красного, а V также от 0 до 1, от чёрного до зелёного.

Изменение в main.qml, material.frag Результат
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(UV0, 0.0, 1.0);
}

Пока мы при этом, почему бы не визуализировать нормали тоже, на этот раз на сфере. Как и с UV, если фрагмент пользовательского вершинного шейдера должен изменить значение NORMAL, интерполированное значение каждого фрагмента в фрагментном шейдере, также экспонированное под именем NORMAL, будет отражать эти изменения.

Изменение в main.qml, material.frag Результат
Model {
    source: "#Sphere"
    scale: Qt.vector3d(2, 2, 2)
    materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(NORMAL, 1.0);
}

Цвета

Давайте переключимся на модель чайника на мгновение, сделаем материал смесью металлического и диэлектрического и попробуем установить для него зелёный основной цвет. Значение green QColor отображается как (0, 128, 0), на основе чего наша первая попытка может быть такой:

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

Это не совсем правильно. Сравните со вторым подходом:

Изменение в main.qml, material.frag Результат
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;
}

Переключаясь на PrincipledMaterial, мы можем подтвердить, что установка PrincipledMaterial::baseColor на «зелёный» и следование за metalness и другими свойствами, результат идентичен нашему второму подходу:

Изменение в main.qml Результат
materials: PrincipledMaterial {
    baseColor: "green"
    metalness: 0.6
    specularAmount: 0.4
    roughness: 0.4
}

Если тип свойства uColor был изменён на vector4d, или любой тип, кроме color, результаты внезапно изменятся и станут идентичными нашему первому подходу.

Почему так происходит?

Ответ заключается в неявном преобразовании sRGB в линейный, которое выполняется для цветовых свойств DefaultMaterial, PrincipledMaterial, а также для пользовательских свойств с типом color в CustomMaterial. Такое преобразование не выполняется для каких-либо других значений, поэтому, если шейдер жёстко задаёт цветовое значение или основывает его на свойстве QML с типом, отличным от color, ему предстоит выполнить линеаризацию в случае, если исходное значение было в цветовом пространстве sRGB. Преобразование в линейное важно, так как Qt Quick 3D выполняет тонемаппинг результатов фрагментной затенения, и этот процесс предполагает значения в пространстве sRGB в качестве входных данных.

Встроенные константы QColor, такие как "green", все задаются в пространстве sRGB. Поэтому просто назначение vec4(0.0, 0.5, 0.0, 1.0) в BASE_COLOR в первой попытке недостаточно, если мы хотим результат, который соответствует значению RGB (0, 128, 0) в пространстве sRGB. См. документацию BASE_COLOR в CustomMaterial для формулы линеаризации таких цветовых значений. То же самое относится к значениям цветов, полученным путём выборки текстур: если данные исходного изображения не находятся в цветовом пространстве sRGB, требуется преобразование (если тонемаппинг не отключен).

Смешивание

Простое запись значения меньше чем 1.0 в BASE_COLOR.a недостаточно, если ожидается получить альфа-смешивание. Такие материалы очень часто изменяют значения свойств sourceBlend и destinationBlend, чтобы получить желаемые результаты.

Также имейте в виду, что комбинированное значение альфа — это значение непрозрачности узла Node opacity, умноженное на альфа-значение материала.

Для визуализации давайте воспользуемся шейдером, который назначает красный цвет с альфой 0.5 в BASE_COLOR:

main.qml, material.frag Результат
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);
}

Первый куб записывает 0,5 в значение альфы цвета, но это не приносит видимых результатов, так как альфа-смешивание не включено. Второй куб включает простое альфа-смешивание через свойства CustomMaterial. Третий также назначает непрозрачность 0,5 модели, что означает, что эффективная непрозрачность равна 0,25.

Передача данных между вершинным и фрагментным шейдерами

Вычисление значения за вершину (например, предположим один треугольник, для 3 вершин треугольника), а затем передача его на этап фрагмента, где для каждого фрагмента (например, каждый фрагмент, покрытый растровым треугольником), доступно интерполированное значение. В фрагментах пользовательских материалов это достигается с помощью ключевого слова VARYING. Это обеспечивает синтаксис, подобный GLSL 120 и GLSL ES 100, но будет работать независимо от используемой графической API во время выполнения. Двигатель позаботится о переписывании объявления varying соответствующим образом.

Давайте посмотрим, как будет выглядеть классическая выборка текстуры с UV-координатами. Текстуры будут рассмотрены в последующем разделе, сейчас давайте сосредоточимся на том, как получить UV-координаты, которые можно передать в функцию texture() в шейдере.

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.png Результат

Обратите внимание на объявления VARYING. Имя и тип должны совпадать, uv в фрагментном шейдере экспонирует интерполированную UV-координату для текущего фрагмента.

Любые другие типы данных могут быть переданы на этап фрагмента аналогичным образом. Стоит отметить, что во многих случаях настройка собственных переменных материала не требуется, потому что существуют встроенные решения, охватывающие многие типичные потребности. Это включает получение (интерполированных) нормалей, UV, мировой позиции (VAR_WORLD_POSITION) или вектора, направленного на камеру (VIEW_VECTOR).

Вышеприведённый пример фактически может быть упрощён следующим образом, так как UV0 также автоматически доступен на этапе фрагмента:

Изменение в main.qml, material.frag Результат
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property TextureInput someTextureMap: TextureInput {
        texture: Texture {
        source: "qt_logo_rect.png"
    }
}
void MAIN()
{
    BASE_COLOR = texture(someTextureMap, UV0);
}

Текстуры

CustomMaterial не имеет встроенных текстурных карт, что означает отсутствие эквивалента, например, PrincipledMaterial::baseColorMap. Это потому, что реализация того же часто тривиальна, при этом давая гораздо больше гибкости, чем у DefaultMaterial и PrincipledMaterial. Помимо простого сэмплирования текстуры, пользовательские фрагментные шейдеры свободны объединять и смешивать данные из различных источников при вычислении значений, которые они присваивают BASE_COLOR, EMISSIVE_COLOR, ROUGHNESS, и т.д. Они могут основывать эти вычисления на данных, предоставленных через свойства QML, интерполированных данных, отправленных со стадии вершин, значений, полученных из выборки текстур, и жёстко заданных значениях.

Как показывает предыдущий пример, экспонирование текстуры для вершинного, фрагментного или обоих шейдеров очень похоже на скалярные и векторные униформные значения: свойство QML с типом TextureInput автоматически связывается с sampler2D в коде шейдера. Как всегда, нет необходимости объявлять этот сэмплер в коде шейдера.

TextureInput ссылается на Texture с дополнительным свойством enabled. Texture может получать свои данные тремя способами: из файла изображения, из текстуры с живым содержимым Qt Quick или из предоставленных данными C++ через QQuick3DTextureData.

Примечание: Что касается свойств Текстуры, то при использовании пользовательских материалов учитываются только те, что связаны с источником, выкладкой и фильтрацией. Остальное (например, преобразования UV) реализуется пользовательскими шейдерами по своему усмотрению.

Давайте рассмотрим пример, где модель, в данном случае сфера, текстурируется с помощью живого содержимого 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;
}

Здесь 2D-поддерево (Прямоугольник с двумя дочерними элементами: ещё одним Прямоугольником и текстом) отображается в 2D-текстуре размером 512x512 каждый раз, когда изменяется эта мини-сцена. Затем текстура отображается в пользовательском материале под именем someTextureMap.

Обратите внимание на инверсию координаты V в шейдере. Как отмечалось выше, в пользовательских материалах, где доступна полная программируемость на уровне шейдеров, отсутствуют «фиксированные» возможности текстуры и материала Principled. Это означает, что любые преобразования координат UV должны применяться шейдером. Здесь мы знаем, что текстура генерируется через Texture::sourceItem, поэтому V необходимо инвертировать, чтобы получить соответствие набору UV используемой нами сетки.

Этот пример показывает, что можно сделать и с материалом Principled. Давайте сделаем его более интересным, добавив простой эффект выемки:

material.frag Результат
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);
}

Используя возможности, рассмотренные до сих пор, открывается широкий спектр возможностей для создания материалов, которые визуально эффектно затеняют сетки. Чтобы завершить базовое ознакомление, давайте рассмотрим пример применения высотных и нормальных карт к сетке плоскости. (Здесь используется отдельный .mesh файл, так как у встроенного #Rectangle недостаточно подразделений) Для улучшения результатов освещения мы будем использовать освещение на основе изображения с HDR-изображением на 360 градусов. Также изображение установлено в качестве небосвода, чтобы было понятнее, что происходит.

Начнём с пустого пользовательского материала:

main.qml Результат
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 { }
        }
    }
}

Теперь давайте создадим шейдеры, которые применяют высотную и нормальную карту к сетке:

Высотная карта Нормальная карта

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));
}
Изменение в main.qml Результат
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" }
    }
}

Примечание: Объект WasdController может быть очень полезен при разработке и устранении неполадок, так как он позволяет перемещаться и осматривать сцену с помощью клавиатуры и мыши привычным образом. Настройка камеры с помощью WasdController осуществляется просто:

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

Глубина и экранные текстуры

Когда фрагмент пользовательского шейдера использует ключевые слова DEPTH_TEXTURE или SCREEN_TEXTURE, он выбирает создание соответствующих текстур в отдельном проходе рендеринга, что не обязательно является быстрой операцией, но позволяет реализовать различные техники, такие как рефракция для материалов, похожих на стекло.

DEPTH_TEXTURE — это sampler2D, который позволяет брать образцы из текстуры с содержимым буфера глубины со всеми opaque объектами сцены, отрендеренными. Аналогично, SCREEN_TEXTURE — это sampler2D объект, который позволяет брать образцы из текстуры, содержащей все opaque объекты сцены, без чего-либо ещё, что означает отсутствие небосвода, теней, эффектов и объектов с любой формой прозрачности (Node::opacity меньше 1, карты текстур материала из изображений с полупрозрачными пикселями и т. д.). Текстура очищается до vec4(0.0), что означает, что у фрагментов, не покрытых объектами в сцене, значение альфа равно 0. Размер этих текстур соответствует размеру View3D в пикселях.

Давайте проведем простую демонстрацию, визуализируя содержимое буфера глубины с помощью DEPTH_TEXTURE. Здесь значение дальнего обреза камеры (far clip value) уменьшено с значения по умолчанию 10000 до 2000, чтобы обеспечить меньший диапазон и, следовательно, более очевидные различия визуализированных значений глубины. В результате получается прямоугольник, который визуализирует буфер глубины сцены на своей поверхности.

main.qml, material.frag Результат
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        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);
}

Обратите внимание, что цилиндр отсутствует в DEPTH_TEXTURE из-за его зависимости от полупрозрачности, что помещает его в другую категорию, чем другие объекты, которые все непрозрачны. Эти объекты не записывают значения в буфер глубины, хотя они и проверяют значения глубины, записанные непрозрачными объектами, и полагаются на рендеринг в порядке «сзади вперёд». Поэтому они также отсутствуют в DEPTH_TEXTURE.

Что произойдёт, если мы изменим шейдер на образец SCREEN_TEXTURE?

material.frag Результат
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;
}

Здесь прямоугольник текстурирован с помощью SCREEN_TEXTURE, при этом прозрачные пиксели заменяются фиолетовым цветом.

Функции обработки света

Дополнительно в пользовательском материале есть возможность определять функции в фрагментном шейдере, которые повторно реализуют уравнения освещения, используемые для расчета цвета фрагмента. Функция обработки света, если она есть, вызывается один раз для каждого источника света в сцене для каждого фрагмента. Есть специализированные функции для разных типов света, а также для окружающего и зеркального освещения. Если функция обработки света отсутствует, используются стандартные вычисления, как и в материале Principled. Если функция обработки света присутствует, но тело функции пустое, это означает, что в сцене не будет никакого вклада от данного типа источников света.

Обратитесь к документации пользовательского материала за подробностями о функциях, таких как DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT, и SPECULAR_LIGHT.

Незатенённые пользовательские материалы

Есть ещё один тип пользовательского материала: незатенённые пользовательские материалы. Все примеры до сих пор использовали shaded пользовательские материалы, причём свойство shadingMode оставлено в значении по умолчанию CustomMaterial.Shaded.

Что произойдёт, если мы изменим это свойство на CustomMaterial.Unshaded?

Прежде всего, ключевые слова, такие как BASE_COLOR, EMISSIVE_COLOR, METALNESS и т. д., больше не имеют желаемого эффекта. Это связано с тем, что незатенённый материал, как следует из названия, не получает автоматических дополнений стандартного кода затенения, игнорируя источники света, освещение на основе изображений, тени и окружающее затемнение в сцене. Скорее, незатенённый материал даёт полный контроль над шейдером через ключевое слово FRAGCOLOR. Это похоже на gl_FragColor: цвет, назначенный FRAGCOLOR, является результатом и окончательным цветом фрагмента без дальнейших корректировок Qt Quick 3D.

main.qml, material.frag, material2.frag Результат
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);
}

Заметьте, как правый цилиндр игнорирует DirectionalLight в сцене. Его затенение не учитывает освещение сцены, конечный цвет фрагмента — полностью белый.

В вершинном шейдере незатенённого материала всё ещё доступны типичные входные данные: VERTEX, NORMAL, MODELVIEWPROJECTION_MATRIX и т. д., и он может записывать в POSITION. Однако во фрагментном шейдере подобных удобств больше нет: NORMAL, UV0 или VAR_WORLD_POSITION недоступны во фрагментном шейдере незатенённого материала. Вместо этого теперь код шейдера должен вычислять и передавать через VARYING всё необходимое для определения конечного цвета фрагмента.

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

Незатенённые материалы полезны, когда взаимодействие со светом сцены не требуется или нежелательно, и материал нуждается в полном контроле над конечным цветом фрагмента. Обратите внимание, что в приведённом выше примере нет DirectionalLight или других источников света, но сфера с настраиваемым материалом отображается как ожидалось.

Примечание: Незатенённый материал, содержащий только фрагмент вершинного шейдера, но не определяющий свойство fragmentShader, всё ещё будет функциональным, но результаты будут такими же, как если бы свойство shadingMode было установлено в Shaded. Поэтому менять shadingMode для материалов, содержащих только вершинный шейдер, не имеет смысла.

Программируемость эффектов

Эффекты пост-обработки применяют один или несколько фрагментных шейдеров к результату View3D. Выход из этих фрагментных шейдеров отображается вместо исходных результатов рендеринга. Это концептуально очень похоже на ShaderEffect и ShaderEffectSource в Qt Quick.

Примечание: Эффекты пост-обработки доступны только тогда, когда свойство renderMode для View3D установлено в View3D.Offscreen.

Для эффекта также можно указать фрагменты вершинного шейдера, но они имеют ограниченную полезность и, следовательно, предполагается, что используются относительно редко. Входные данные вершины для эффекта пост-обработки — это квадрирование (либо две треугольные, либо треугольная полоса), преобразование или смещение вершин часто не полезно. Однако вершинный шейдер может быть полезен для вычисления и передачи данных фрагментному шейдеру с использованием ключевого слова VARYING. Как обычно, фрагментный шейдер затем получит интерполированное значение, основанное на текущих координатах фрагмента.

Синтаксис фрагментов шейдера, связанных с Effect, идентичен шейдерам для незатенённого CustomMaterial. Что касается встроенных специальных ключевых слов, VARYING, MAIN, FRAGCOLOR (только фрагментный шейдер), POSITION (только вершинный шейдер), VERTEX (только вершинный шейдер) и MODELVIEWPROJECTION_MATRIX работают так же, как и для CustomMaterial.

Самые важные специальные ключевые слова для фрагментных шейдеров Effect следующие:

Имя Тип Описание
INPUT sampler2D Образец входной текстуры. Эффект обычно использует этот образец с использованием INPUT_UV.
INPUT_UV vec2 Координаты UV для выборки INPUT.
INPUT_SIZE vec2 Размер текстуры INPUT, в пикселях. Это удобная альтернатива вызову textureSize().
OUTPUT_SIZE vec2 Размер выходной текстуры, в пикселях. В большинстве случаев равно INPUT_SIZE, но многопроходной эффект может иметь проходы, выводящие данные в промежуточные текстуры с разными размерами.
DEPTH_TEXTURE sampler2D Текстура глубины с содержимым буфера глубины с непрозрачными объектами в сцене. Как и с CustomMaterial, наличие этого ключевого слова в шейдере запускает автоматическое создание текстуры глубины.

Эффект пост-обработки

Начнём с простой сцены, на этот раз используя несколько объектов, включая текстурированный прямоугольник, использующий шахматную текстуру в качестве карты базового цвета.

main.qml Результат
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 { }
        }
    }
}

Теперь применим эффект ко всей сцене. Точнее, к View3D. Когда в сцене несколько элементов View3D, каждый имеет свою SceneEnvironment и, следовательно, свою собственную цепочку эффектов пост-обработки. В примере один элемент View3D покрывает всё окно.

Изменение в main.qml effect.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;
}

Этот простой эффект изменяет значение канала красного цвета. Экспонирование свойств QML в качестве униформ работает с эффектами так же, как и с настраиваемыми материалами. Шейдер начинается со строки, которая будет очень распространённой при написании фрагментных шейдеров для эффектов: выборка INPUT в координатах UV INPUT_UV. Затем он выполняет необходимые вычисления и присваивает окончательный цвет фрагмента FRAGCOLOR.

Многие свойства, установленные в примере, находятся во множественном числе (эффекты, проходы, шейдеры). Хотя синтаксис списка [ ] может быть опущен, когда имеется только один элемент, все эти свойства являются списками и могут содержать более одного элемента. Почему?

  • effects — список, потому что View3D позволяет объединять несколько эффектов. Эффекты применяются в порядке добавления в список. Это позволяет легко применять два или более эффектов к View3D, что аналогично тому, что можно добиться в Qt Quick, вложенными элементами ShaderEffect. Текстура INPUT следующего эффекта всегда является текстурой, содержащей выходной результат предыдущего эффекта. Результат последнего эффекта используется в качестве конечного результата View3D.
  • passes — список, потому что, в отличие от ShaderEffect, Effect имеет встроенную поддержку нескольких проходов. Многопроходной эффект более мощный, чем объединение нескольких независимых эффектов в effects: проход может выводить данные в временную промежуточную текстуру, которая затем может использоваться в качестве входных данных для последующих проходов, помимо исходной входной текстуры эффекта. Это позволяет создавать сложные эффекты, вычисляющие, рендерящие и комбинирующие несколько текстур для получения конечного цвета фрагмента. Этот продвинутый случай использования не рассматривается здесь. Обратитесь к странице документации Effect для получения подробностей.
  • shaders — список, потому что эффект может иметь как вершинный, так и фрагментный шейдер.

Цепочки нескольких эффектов

Рассмотрим пример, где эффект из предыдущего примера дополняется другим эффектом, похожим на встроенный эффект DistortionSpiral.

Изменение в main.qml effect2.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;
}

Теперь, возможно, неожиданный вопрос: почему это плохой пример?

Точнее, он не плохой, а скорее демонстрирует паттерн, которому часто следует избегать.

Цепочки эффектов таким образом могут быть полезными, но важно помнить о производительности: выполнение двух проходов рендеринга (один для создания текстуры с изменённым каналом красного цвета, а затем другой для расчёта искажения) довольно расточительно, когда одного было бы достаточно. Если бы фрагментные шейдеры были объединены, тот же результат мог быть достигнут с одним единственным эффектом.

Определение данных сетки и текстур из C++

Процедурное создание данных сетки и изображений текстур следуют похожим шагам:

  • Наследуйте от QQuick3DGeometry или QQuick3DTextureData
  • Установите желаемые данные вершин или изображения при создании, вызвав защищённые член-функции базового класса
  • Если в дальнейшем потребуются динамические изменения, установите новые данные и вызовите update()
  • После завершения реализации необходимо зарегистрировать класс, чтобы сделать его видимым в QML
  • Объекты Model и Texture в QML теперь могут использовать поставщика настраиваемых данных вершин или изображений, установив свойство Model::geometry или Texture::textureData

Настраиваемые данные вершин

Данные вершин относятся к последовательности (обычно float) значений, которые образуют сетку. Вместо загрузки файлов .mesh, пользовательский поставщик геометрии отвечает за предоставление тех же данных. Данные вершин состоят из attributes, таких как координаты позиции, текстуры (UV) или нормали.

Это может показаться знакомым, если вы работали с графическими API, такими как OpenGL или Vulkan напрямую, поскольку способ определения входных данных вершин в этих API примерно соответствует тому, что определяет файл .mesh или экземпляр QQuick3DGeometry.

Кроме того, необходимо указать топологию сетки (тип примитива). Для индексированного рисования также необходимо предоставить данные для буфера индексов.

Существует одно встроенное реализованное пользовательское геометрическое описание: модуль QtQuick3D.Helpers содержит тип GridGeometry. Это позволяет отрисовать сетку в сцене с помощью примитивов линий, не прибегая к реализации пользовательского подкласса QQuick3DGeometry.

Еще один распространенный случай использования — отрисовка точек. Это довольно просто, так как спецификация атрибутов будет минимальной: мы предоставляем три числа с плавающей точкой (x, y, z) для каждой вершины, ничего больше. Подкласс QQuick3DGeometry может реализовать геометрию, состоящую из 2000 точек, аналогично следующему:

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

В сочетании с материалом

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

конечный результат похож на этот (здесь вид изменённого угла камеры, с помощью WasdController):

Примечание: Обратите внимание, что размеры точек и толщины линий, отличные от 1, могут не поддерживаться во время выполнения в зависимости от базового графического API. Qt не контролирует этот аспект. Поэтому может потребоваться реализовать альтернативные методы вместо использования рисования точек и линий.

Пользовательские данные текстур

Что касается текстур, данные, которые нужно предоставить, структурно намного проще: это сырые пиксельные данные с различным количеством байтов на пиксель в зависимости от формата текстуры. Например, текстура RGBA ожидает четыре байта на пиксель, а RGBA16F — четыре полуточных числа с плавающей точкой на пиксель. Это аналогично тому, как хранится QImage. Однако текстуры Qt Quick 3D могут иметь форматы, данные которых не могут быть представлены QImage. Например, текстуры с плавающей точкой HDR или сжатые текстуры. Поэтому данные для QQuick3DTextureData всегда предоставляются как последовательность сырых байтов. Это может показаться знакомым, если вы работали с графическими API, такими как OpenGL или Vulkan напрямую.

Для получения подробной информации обратитесь к страницам документации QQuick3DGeometry и QQuick3DTextureData.

См. также CustomMaterial, Effect, QQuick3DGeometry, QQuick3DTextureData, Qt Quick 3D - Пример пользовательского эффекта, Qt Quick 3D - Пример пользовательских шейдеров, Qt Quick 3D - Пример пользовательских материалов, Qt Quick 3D - Пример пользовательской геометрии и Qt Quick 3D - Пример текстуры процедурного типа.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.2/qtquick3d-custom.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API