可编程材料、效果、几何和纹理数据
尽管 Qt Quick 3D 内置的材质,如 DefaultMaterial 和 PrincipledMaterial,允许通过其属性进行广泛的定制,但它们在顶点和片段着色器级别上不提供可编程性。为了实现这一点,提供了 CustomMaterial 类型。
带有 PrincipledMaterial 的模型 | 通过 CustomMaterial 转换顶点 |
---|---|
后期处理效果,其中在将 Qt Quick 的输出传递之前,对颜色缓冲区进行一次或多次处理,可选地考虑深度缓冲区,也存在两种类型
- 可以配置为扩展场景环境的内置后期处理步骤,例如发光/模糊、景深、渐晕、镜头光晕等
自定义
效果通过应用在片段着色器代码中以某种形式(Effect 对象)指定的处理过程的行为实现。
在实际中,还存在第三类后期处理效果:通过 Qt Quick 实现的 2D 效果,在 View3D 项的输出上操作,不涉及 3D 渲染器的任何参与。例如,要给一个 View3D 项应用模糊,最简单的方法是使用 Qt Quick 的新设施,如 MultiEffect。对于涉及深度缓冲区或屏幕纹理等 3D 场景概念的复杂效果,或需要处理 HDR 色调映射或需要多个带有中间缓冲区的步骤等,3D 后期处理系统变得有益。对于不需要了解 3D 场景和渲染器的简单 2D 效果,始终可以使用 ShaderEffect 或 MultiEffect 替代实现。
无效果的场景 | 应用自定义后期处理效果的同场景 |
---|---|
除了可编程材料和后期处理之外,还有两种通常以文件形式提供的数据(如 .mesh
文件或图像,如 .png
)
- 顶点数据,包括用于渲染的网格的几何形状、纹理坐标、法线、颜色和其他数据
- 作为渲染对象的纹理映射或与天空盒或基于图像的光照一起使用的纹理的内容
如果需要,应用程序可以从 C++ 以 QByteArray 的形式提供此类数据。此类数据也可以随时间变化,从而可以进程式生成并在以后更改数据,以用于 Model 或 Texture。
通过从 C++ 动态指定顶点数据渲染的网格 | 使用C++生成的图像数据纹理的立方体 |
---|---|
以下四种自定义和使材料、效果、几何形状和纹理动态化的方法,使得着色器可编程和着色器接收到的数据的生成过程可编程。以下部分提供了这些功能的概述。有关各自类型的完整参考,请参见文档页面。
材料的可编程
让我们从一个包含立方体的场景开始,并从默认的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 { } } } } |
这两者都会导致完全相同的结果,因为当没有添加顶点或片元着色器代码时,自定义材料实际上是一个PrincipledMaterial。
注意:例如,baseColor、metalness、baseColorMap等属性在CustomMaterial QML类型中没有等效属性。这是设计时有意的:自定义材料是通过着色器代码来实现的,而不仅仅是提供一些固定的值。
我们的第一个顶点着色器
让我们添加一个自定义顶点着色器片段。这是通过在vertexShader属性中引用一个文件来完成的。对于片元着色器,方法相同。这些引用就像Image.source或ShaderEffect.vertexShader一样工作:它们是本地或qrc
URL,并且相对路径是相对于.qml
文件的路径。因此,常规方法是放置.vert
和.frag
文件到Qt资源系统(使用CMake时为qt_add_resources
)中,并使用相对路径来引用它们。
从Qt 6.0开始,不再支持内联着色器字符串,无论是在Qt Quick还是在Qt Quick 3D中。(请注意,这些属性是URL,而不是字符串)但是,由于它们固有的动态性质,Qt Quick 3D中的自定义材料和后期处理效果仍然以源代码形式在引用的文件中提供着色器片段。这与ShaderEffect不同,其中的着色器是完整的,没有任何由引擎进一步修改,因此应作为先决条件的.qsb
着色器包提供。
注意:在Qt Quick 3D中,URL只能引用本地资源。不支持远程内容的方案。
注意: 所使用的着色语言是兼容Vulkan的GLSL。这些.vert
和.frag
文件本身并非完整的着色器,因此通常被称为片段
。这就是为什么这些片段不提供直接的统一块、输入和输出变量或采样统一变量。相反,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页面。
关键字 | 类型 | 描述 |
---|---|---|
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 | 当使用点拓扑渲染时(例如,因为自定义几何为网格提供了这样的几何形状),才相关。写入此值与在PrincipledMaterial的pointSize上设置等价。 |
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; } |
添加片段着色器
许多自定义材质也想要一个片段着色器。事实上,许多只有一个片段着色器。如果没有从顶点到片段阶段的额外数据需要传递,并且默认的顶点变换足够的话,可以省略从CustomMaterial设置的vertexShader
属性。
main.qml,material.frag中的更改 | 结果 |
---|---|
materials: CustomMaterial { fragmentShader: "material.frag" } void MAIN() { } |
我们的第一个片段着色器包含一个空的MAIN()函数。这与完全未指定片段着色器片段一样:得到的看起来就像默认的PrincipledMaterial。
让我们看看片段着色器中的一些常用关键字。这不是完整的列表,请参阅CustomMaterial文档以获取完整的参考。其中许多都是读写,这意味着它们有一个默认值,但着色器可以,并且通常想要,将不同的值分配给它们。
正如其名所示,其中许多与PrincipledMaterial的类似命名属性相对应,具有相同的含义和语义,遵循金属粗糙度材料模型。如何计算这些值的决定权在定制材料的实现上:例如,BASE_COLOR的值可以硬编码在着色器中,可以基于采样纹理,也可以基于作为uniforms暴露的QML属性或从顶点着色器传递的插值数据来计算。
关键字 | 类型 | 描述 |
---|---|---|
BASE_COLOR | vec4 | 基本颜色和Alpha值。对应于PrincipledMaterial::baseColor。片段的最终alpha值是模型不透明度与基本颜色alpha值的乘积。默认值是(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 | 在world空间中的插值法线,当禁用面舍入时,对双面性进行调整。只读。 |
UV0 | vec2 | 插值纹理坐标。只读。 |
VAR_WORLD_POSITION | vec3 | 在world空间中的插值顶点位置。只读。 |
让我们将立方体的基本颜色设置为红色
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); } |
我们不仅可以使用硬编码在着色器中的值,还可以使用作为uniforms暴露的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); } |
同时,为什么不可视化法线呢,这次在一个球体上。像UVs一样,如果自定义顶点着色器代码片段改变了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); } |
颜色
现在让我们转向茶壶模型,把材料做成金属和非金属的混合体,并尝试将其基础颜色设置为绿色。基于这个绿色
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设置为"green"并遵循金属度和其他属性,结果与我们第二种方法相同
main.qml中的更改 | 结果 |
---|---|
materials: PrincipledMaterial { baseColor: "green" metalness: 0.6 specularAmount: 0.4 roughness: 0.4 } |
如果将uColor
属性的 类型更改为vector4d
,或任何除color
之外的类型,结果会突然改变并变为与我们第一种方法相同。
这是为什么?
答案在于对DefaultMaterial、PrincipledMaterial和自定义材质color
类型进行的隐式RGB到线性的转换。这种转换不适用于其他任何值,因此如果着色器硬编码一个颜色值,或以类型不同于color
的QML属性为基础,在源值位于sRGB颜色空间的情况下,线性化将由着色器自己完成。将颜色转换为线性很重要,因为Qt Quick 3D会在着色器片段的结果上执行色调映射,并且该过程假设输入值位于sRGB空间。
内置的QColor
常数,如"green"
,都是以sRGB空间给出的。因此,如果我们想在sRGB空间中得到与RGB值(0, 128, 0)
匹配的结果,仅仅将vec4(0.0, 0.5, 0.0, 1.0)
分配给
混合
如果期望得到颜色混合,只写入一个小于1.0
的值到BASE_COLOR.a
是不够的。此类材质经常会更改
同时注意,组合透明度是
为了可视化,让我们使用将红色和alpha值为0.5
赋值给
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写入颜色alpha值,但由于颜色混合没有被启用,所以没有明显的效果。第二个立方体通过
在顶点和片段着色器之间传递数据
计算每个顶点的值(例如,假设一个三角形,用于三角形的三个顶点),然后将其传递到片段阶段,其中对于每个片段(例如,每个被光栅化的三角形所覆盖的片段),都会提供一个插值值。在自定义材质着色器代码片段中,这是通过VARYING
关键字实现的。这提供了一种类似于GLSL 120和GLSL ES 100的语法,但无论运行时的图形API如何,都会生效。引擎将负责将合适的变元声明重新写入。
让我们看看经典的以UV坐标进行纹理采样的样子。纹理将在后面的章节中进行讲解,现在让我们关注如何获取传递到着色器中texture()
函数的UV坐标。
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 坐标。
可以通过类似的方式将任何其他类型的数据传递到片段阶段。值得注意的是,在许多情况下,设置材料自己的 varyings 并非必需,因为有内置功能可以覆盖很多典型需求。这包括创建(插值)法线、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); } |
纹理
自定义材料没有内置的纹理映射,这意味着没有与,例如,PrincipledMaterial::baseColorMap 相等的等效项。这是因为实现这一点的通常很简单,同时也比 DefaultMaterial 和 PrincipledMaterial 内置的功能提供了更多的灵活性。除了简单地采样纹理外,自定义片段着色器片段可以自由地组合和混合来自不同来源的数据,以计算它们分配给 BASE_COLOR
、EMISSIVE_COLOR
、ROUGHNESS
等值的值。它们可以根据通过 QML 属性提供的数据、从顶点阶段发送的插值数据、从采样纹理检索的值,以及硬编码的值来基于这些计算。
如前例所示,向顶点、片段或两者着色器公开纹理与标量和矢量均匀值非常相似:具有 TextureInput 类型的 QML 属性将自动与着色器代码中的 sampler2D
相关联。始终无需在着色器代码中声明此采样器。
TextureInput 引用一个 Texture,并且还有一个额外的 enabled 属性。《a网址="qquick3dtexturedata.html"translate="no" QQuick3DTextureData 在 Texture 中以以下三种方式之一来源其数据:从一个图像文件,从一个具有实时 Qt Quick 内容的纹理,或可以通过QQuick3DTextureData 通过 C++ 提供。
注意:当涉及到 Texture 属性时,与源、平铺和过滤相关的属性是唯一在自定义材料中被隐含考虑的,因为其余部分(例如,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 子树(有两个孩子:另一个 Rectangle 和 Text 的 Rectangle)都会渲染到一个 512x512 的 2D 纹理中。然后,将该纹理以 someTextureMap
名称暴露给自定义材料。
请注意着色器中 V 坐标的翻转。如上所述,自定义材料,在其提供完全着色器级别的可编程性的情况下,不提供 Texture 和 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
没有足够的细分)为了获得更好的光照效果,我们将使用基于图像的光照,并使用一个360度的HDR图像。还将该图像设置为天空盒,以更清楚地了解正在发生的事情。
首先,我们从空的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
是可以采样带有场景中所有opaque
对象内容的深度缓冲区内容的sampler2D
。类似地,SCREEN_TEXTURE
是可以采样不包含任何透明材料或同时使用SCREEN_TEXTURE
的材料的场景内容的sampler2D
。可以用于需要其正在渲染到的帧缓冲区内容的材料的纹理。SCREEN_TEXTURE纹理使用与View3D相同的清除模式。这些纹理的大小与像素中的View3D大小相匹配。
让我们通过通过DEPTH_TEXTURE
可视化深度缓冲区内容来进行一个简单的展示。这里,将相机的far clip value
从默认的10000降低到2000,以便有一个较小的范围,并且可视化深度值差异更明显。结果是,在表面之上可视化场景深度缓冲区的矩形。
main.qml, material.frag | 结果 |
---|---|
import QtQuick import QtQuick3D import QtQuick3D.Helpers Rectangle { width: 400 height: 400 color: "black" View3D { anchors.fill: parent PerspectiveCamera { id: camera z: 600 clipNear: 1 clipFar: 2000 } DirectionalLight { } Model { source: "#Cube" scale: Qt.vector3d(2, 2, 2) position: Qt.vector3d(150, 200, -1000) eulerRotation.x: 60 eulerRotation.y: 20 materials: PrincipledMaterial { } } Model { source: "#Cylinder" scale: Qt.vector3d(2, 2, 2) position: Qt.vector3d(400, 200, -1000) materials: PrincipledMaterial { } opacity: 0.3 } Model { source: "#Sphere" scale: Qt.vector3d(2, 2, 2) position: Qt.vector3d(-150, 200, -600) materials: PrincipledMaterial { } } Model { source: "#Cone" scale: Qt.vector3d(2, 2, 2) position: Qt.vector3d(0, 400, -1200) materials: PrincipledMaterial { } } Model { source: "#Rectangle" scale: Qt.vector3d(3, 3, 3) y: -150 materials: CustomMaterial { fragmentShader: "material.frag" } } } WasdController { controlledObject: camera } } void MAIN() { float zNear = CAMERA_PROPERTIES.x; float zFar = CAMERA_PROPERTIES.y; float zRange = zFar - zNear; vec4 depthSample = texture(DEPTH_TEXTURE, vec2(UV0.x, 1.0 - UV0.y)); float zn = 2.0 * depthSample.r - 1.0; float d = 2.0 * zNear * zFar / (zFar + zNear - zn * zRange); d /= zFar; BASE_COLOR = vec4(d, d, d, 1.0); } |
注意由于圆柱体依赖于半透明度,圆柱体没有出现在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
进行纹理化,同时将透明像素替换为紫色。
光照处理函数
自定义材质的一个高级特性是可以定义片段着色器中的函数,这些函数会重新实现用于计算片段颜色的光照方程。当场景中存在光处理函数时,对于每个场景中的每个光照对象以及每个片段都会调用一次。针对不同类型的光源,有专门的函数,还有环境光和镜面反射的贡献。当不存在相应的光处理函数时,将使用标准计算方法,就像一个PrincipledMaterial会做的那样。当存在光处理函数但函数体为空时,意味着场景中某种类型的光将为零贡献。
有关函数的详细信息,例如 DIRECTIONAL_LIGHT
,POINT_LIGHT
,SPOT_LIGHT
,AMBIENT_LIGHT
和 SPECULAR_LIGHT
,请参阅CustomMaterial文档。
未着色自定义材质
还有一种自定义材质类型:名为 unshaded
的未着色自定义材质。迄今为止的所有示例都使用了 shaded
自定义材质,将 shadingMode 属性保留为其默认的 CustomMaterial 着色值。
如果我们把这一属性切换到 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
。相反,现在着色器代码必须计算并传递所需的所有内容,以确定最终片段颜色。
让我们查看一个具有顶点着色器和片段着色器的示例。修改后的顶点位置被传递给片段着色器,对每个片段都提供了插值值。
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 属性,那么它仍然将是有效的,但结果就像着色模式被设置为着色一样。因此,对于只包含顶点着色器的材质,切换着色模式几乎没有意义。
效果的可编程性
后处理效果将一个或多个片段着色器应用于View3D的结果。这些片段着色器的输出将显示为原始渲染结果。这与Qt Quick的ShaderEffect和ShaderEffectSource在概念上非常相似。
注意:后处理效果仅在将renderMode属性设置为View3D.Offscreen时,对View3D才可用。
还可以为效果指定自定义顶点着色器片段,但它们的使用价值有限,因此预计使用频率相对较低。后处理效果的顶点输入是一个四边形(两个三角形或三角形条),变换或移位该四边形的顶点通常没有帮助。然而,可以使用顶点着色器来计算并使用VARYING
关键字将数据传递给片段着色器。像往常一样,片段着色器将根据当前片段坐标接收一个插值值。
与Effect关联的着色器片段的语法与无纹理CustomMaterial的着色器相同。当涉及到内置特殊关键词时,VARYING
,MAIN
,FRAGCOLOR
(仅片段着色器),POSITION
(仅顶点着色器),VERTEX
(仅顶点着色器),和MODELVIEWPROJECTION_MATRIX
在CustomMaterial中的工作方式相同。
对于Effect片段着色器,最为重要的特殊关键词如下:
名称 | 类型 | 描述 |
---|---|---|
INPUT | sampler2D | 输入纹理的采样器。效果通常使用INPUT_UV 对其进行采样。 |
INPUT_UV | vec2 | 用于采样INPUT 的UV坐标。 |
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属性暴露为统一变量,效果相同。着色器以一行开始,这在编写效果片段着色器时非常常见:在UV坐标INPUT_UV
上采样INPUT
。然后,它执行所需的计算并分配最终片段颜色到FRAGCOLOR
中。
在示例中设置的大多数属性都是复数形式(效果、上映、着色器)。当只有一个元素时,可以省略列表[ ]
语法,但这些属性都是列表,可以包含多个元素。为什么是这样?
- effects是一个列表,因为View3D允许将多个效果链接在一起。效果按照它们添加到列表中的顺序应用。这允许轻松地将两个或更多效果应用于View3D,类似于在Qt Quick中嵌套ShaderEffect元素所能实现的效果。下一个效果的
INPUT
纹理始终是包含前一个效果输出的纹理。最后一个效果的输出是作为View3D的最终输出使用的。 - passes是一个列表,因为与ShaderEffect不同,效果具有内置的多个通过支持。多通过效果比在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; } |
现在一个可能会令人惊讶的问题:这 why what facts is这个问题这么 bad 坏的例子?
更确切地说,这并不 bad,但 rather rather rather显示了一个 often 经常 beneficial to 避免 avoid 避免的模式。
以这种方式连接效果可能 be 有用, but 但 important 重要的是要记住 performance polishing 的影响:做两个渲染 pass 通过 (一个用于生成具有调整 red 红色通道的纹理,然后另一个用于计算扭曲)相当浪费, one 当 when 一个就足够时。
从 C++ 定义网格和纹理数据
生成网格和纹理图像数据遵循类似的步骤
- 子类化 QQuick3DGeometry 或 QQuick3DTextureData
- 在构造时通过调用基类中的受保护成员函数设置所需的顶点或图像数据
- 如果以后某个时候需要动态更改,设置新数据并调用 update()
- 一旦实现完成,该类需要注册才能在 QML 中可见
- 在QML中的模型和纹理对象现在可以通过设置Model::geometry或Texture::textureData属性来使用自定义的顶点或图像数据提供程序
自定义顶点数据
顶点数据指的是组成网格的值的序列(通常是float
)。我们不再加载.mesh
文件,而是自定义几何形状提供程序负责提供相同的数据。顶点数据包括属性
,例如位置、纹理(UV)坐标或法线。属性的定义描述了所存在的属性类型,组件类型(例如,由x、y、z值组成的3组件float向量表示顶点位置),以及它们在提供的数据中开始的位置,以及步幅(需要添加到偏移量以指向同一属性中下一个元素的增量)。
如果您直接与图形API(例如OpenGL或Vulkan)工作过,这可能会很熟悉,因为使用这些API指定顶点输入的方式与.mesh
文件或QQuick3DGeometry实例定义的方式不太一样。
此外,必须指定网格拓扑(原语类型)。对于索引绘制,还必须提供索引缓冲区数据。
有一个内置的自定义几何形状实现:QtQuick3D Glover 模块包括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的点和非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 - 程序化纹理示例。
© 2024 Qt公司。本文件中的文档贡献版权归各自所有者。本文件提供的文档遵守由自由软件基金会发布的《GNU自由文档许可证版本1.3》的条款。Qt及其相应商标是Qt公司在芬兰和其他国家/地区的商标。所有其他商标是各自所有者的财产。