Данные программируемых материалов, эффектов, геометрии и текстур
Хотя встроенные материалы 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 | Актуально только при рендеринге с топологией точек, например, потому что пользовательская геометрия (custom geometry) предоставляет такую геометрию для сетки. Запись в это значение эквивалентна установке 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::emissiveColor. Значение по умолчанию — (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 в шейдере. Как отмечалось выше, пользовательские материалы, где имеется полная программируемость на уровне шейдеров, не предоставляют "фиксированных" функций текстуры и PrincipledMaterial. Это означает, что любые преобразования координат UV должны применяться шейдером. Здесь мы знаем, что текстура генерируется через Texture::sourceItem, и поэтому V необходимо инвертировать, чтобы получить соответствие набору UV, который мы используем.
То же самое можно сделать и с PrincipledMaterial. Давайте сделаем его более интересным, добавив простой эффект рельефа:
| 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 градусов. Изображение также установлено в качестве небосвода, чтобы было ясно, что происходит.
Сначала создадим пустой CustomMaterial:
| 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, которое позволяет сэмплировать текстуру с содержимым буфера глубины с рендерингом всех sampler2D объектов сцены. Аналогично, SCREEN_TEXTURE - это sampler2D, которое позволяет сэмплировать текстуру, на которую рендерятся все opaque объекты сцены без чего-либо еще, т. е. без небосвода, теней, эффектов и объектов, имеющих какую-либо прозрачность (значение Node::opacity меньше 1, текстурные карты материалов из изображений с полупрозрачными пикселями и т. д.). Текстура очищается до vec4(0.0), т. е. имеет значение альфа 0 для фрагментов, не покрытых объектами сцены. Размер этих текстур соответствует размеру View3D в пикселях.
Давайте продемонстрируем это визуализацией содержимого буфера глубины через DEPTH_TEXTURE. Здесь значение отсечения глубины камеры clipFar уменьшено с 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, заменяя прозрачные пиксели фиолетовым.
Функции обработчика света
Дополнительно в CustomMaterial есть возможность определять функции в фрагментном шейдере, переопределяющие уравнения освещения, используемые для вычисления цвета фрагмента. Функция обработчика света, если она присутствует, вызывается один раз для каждого источника света в сцене для каждого фрагмента. Есть специальные функции для различных типов источников света, а также для рассеянного и зеркального света. Если соответствующая функция обработчика света отсутствует, используются стандартные вычисления, как в PrincipledMaterial. Если функция обработчика света есть, но ее тело пустое, это означает, что вклад данного типа источников света в сцене отсутствует.
Подробные сведения о функциях, таких как DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT, и SPECULAR_LIGHT, см. в документации к CustomMaterial.
Незатененные пользовательские материалы
Есть ещё один тип CustomMaterial: unshaded пользовательские материалы. Все примеры до сих пор использовали shaded пользовательские материалы, при этом свойство shadingMode оставалось по умолчанию CustomMaterial.Затененное значение.
Что произойдет, если мы изменим это свойство на CustomMaterial.Незатененный?
Во-первых, ключевые слова, такие как 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, passes, shaders). Хотя синтаксис списка [ ] можно опустить, если есть только один элемент, все эти свойства являются списками и могут содержать более одного элемента. Почему?
-
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.1/qtquick3d-custom.html