Данные о программируемых материалах, эффектах, геометрии и текстурах
Хотя встроенные материалы 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