可编程材质、特效、几何体和纹理数据
尽管Qt Quick 3D 、DefaultMaterial 和PrincipledMaterial 的内置材质可通过其属性进行高度自定义,但它们并不支持在顶点着色器和片段着色器层面的可编程性。为此,提供了CustomMaterial 类型。
| 一个包含PrincipledMaterial | 通过CustomMaterial 对顶点进行变换 |
|---|---|
|
|
后处理效果:在将View3D 的输出传递给Qt Quick 之前,会对颜色缓冲区执行一次或多次处理,并可选地考虑深度缓冲区。后处理效果也分为两种类型:
- 可通过ExtendedSceneEnvironment 配置的内置后处理步骤,例如光晕/光晕扩散、景深、暗角、镜头耀斑,
custom由应用程序以片段着色器代码形式实现的效果,以及在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++ 生成的图像数据进行贴图的立方体 |
|---|---|
|
|
这四种方法通过自定义和动态生成材质、特效、几何体及纹理,实现了着色器的可编程性,以及着色器输入数据的程序化生成。以下各节将概述这些功能。完整参考资料请参阅各相关类型的文档页面:
注意:若要 完全控制整个渲染管线(包括多重渲染通道和自定义渲染目标),请参阅 Qt Quick 3D 中的“用户定义渲染通道”。用户定义渲染通道可让您定义整个渲染管线架构,从而补充本页所述功能;而“CustomMaterial ”和“Effect”则侧重于自定义管线内的单个材质和后期处理步骤。
材质的可编程性
假设有一个包含立方体的场景,我们先使用默认的PrincipledMaterial 和CustomMaterial :
| PrincipledMaterial | CustomMaterial |
|---|---|
| |
这两者会产生完全相同的结果,因为当未添加顶点着色器或片段着色器代码时,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 文件本身并非完整的着色器,因此通常被称为snippets 。这就是为什么这些代码片段中没有直接提供的 uniform 块、输入和输出变量,也没有采样器 uniform。相反,Qt Quick 3D 引擎会根据需要对其进行适当修改。
| 在 main.qml 和 material.vert 中的更改 | 结果 |
|---|---|
|
|
自定义顶点或片段着色器代码片段应提供一个或多个预定义名称的函数,例如MAIN 、DIRECTIONAL_LIGHT 、POINT_LIGHT 、SPOT_LIGHT 、AMBIENT_LIGHT 、SPECULAR_LIGHT 。目前我们先关注MAIN 。
如图所示,当 MAIN() 为空时,最终结果与之前完全相同。
在深入探讨之前,让我们先概述一下自定义顶点着色器代码片段中最常用的特殊关键字。这并非完整列表。如需完整参考,请查阅CustomMaterial 页面。
| 关键字 | 类型 | 描述 |
|---|---|---|
| MAIN | void MAIN() 是入口点。自定义顶点着色器代码片段中必须包含此函数,否则编写该代码片段便毫无意义。 | |
| VERTEX | vec3 | 着色器作为输入接收的顶点位置。在自定义材质中,顶点着色器的常见用法是通过将值赋给整个向量或其部分分量,来更改(位移)该向量的 x、y 或 z 值。 |
| 法线 | vec3 | 来自输入网格数据的顶点法线,若未提供法线,则为全零向量。与 VERTEX 类似,着色器可根据需要自由修改该值。修改后的值随后将被管道的其余部分使用,包括片段阶段的光照计算。 |
| UV0 | vec2 | 来自输入网格数据的第一组纹理坐标,如果未提供 UV 值,则均为零。与 VERTEX 和 NORMAL 一样,该值可以被修改。 |
| MODELVIEWPROJECTION_MATRIX | mat4 | 模型-视图-投影矩阵。为了统一行为表现(无论使用何种图形API进行渲染),所有顶点数据和变换矩阵在此层级均遵循OpenGL规范。(Y轴向上,与OpenGL兼容的投影矩阵)只读。 |
| MODEL_MATRIX | mat4 | 模型(世界)矩阵。只读。 |
| 法线矩阵 | mat3 | 模型矩阵左上角 3×3 子矩阵的转置逆矩阵。只读。 |
| CAMERA_POSITION | vec3 | 世界坐标系中的摄像机位置。在本页的示例中,该值为(0, 0, 600) 。只读。 |
| CAMERA_DIRECTION | vec3 | 摄像机方向向量。在本页的示例中,其值为(0, 0, -1) 。只读。 |
| CAMERA_PROPERTIES | vec2 | 摄像机的近剪裁值和远剪裁值。在本页的示例中,其值为(10, 10000) 。只读。 |
| POINT_SIZE | float | 仅在使用点拓扑进行渲染时相关,例如当custom geometry 为网格提供此类几何体时。写入此值等同于设置pointSize on a PrincipledMaterial 。 |
| POSITION | vec4 | 与gl_Position 类似。若未指定,系统会自动生成默认赋值语句,使用MODELVIEWPROJECTION_MATRIX 和VERTEX 。这就是为什么空的MAIN()也能正常工作,而且在大多数情况下无需为其赋予自定义值。 |
让我们制作一个自定义材质,使其根据某种模式位移顶点。 为了让它更有趣,我们添加一些动画化的 QML 属性,这些属性的值最终会作为统一变量在着色器代码中暴露出来。(准确地说,大多数属性将被映射到统一块中的成员上,并在运行时由统一缓冲区支持,但Qt Quick 3D 巧妙地让这些细节对自定义材质作者而言变得透明)
| 在 main.qml 中修改 material.vert | 结果 |
|---|---|
|
来自 QML 属性的统一变量
CustomMaterial 对象中的自定义属性会被映射为统一变量。在上例中,这包括uAmplitude 和uTime 。每当值发生变化时,更新后的值就会在着色器中显示出来。这个概念可能在ShaderEffect 中已经很熟悉了。
QML 属性与 GLSL 变量的名称必须一致。着色器代码中无需为各个 uniform 进行单独声明,而是可以直接使用 QML 属性名称。这就是为什么在上例中,顶点着色器代码片段可以直接引用 `uTime ` 和 `uAmplitude `,而无需事先对它们进行声明。
下表列出了类型映射关系:
| QML 类型 | 着色器类型 | 备注 |
|---|---|---|
| real、int、bool | float、int、bool | |
| color | vec4 | sRGB 到线性空间的转换是隐式进行的 |
| vector2d | vec2 | |
| vector3d | vec3 | |
| vector4d | vec4 | |
| 4×4矩阵 | mat4 | |
| 四元数 | vec4 | 标量值是w |
| 矩形 | vec4 | |
| 点,大小 | vec2 | |
| TextureInput | sampler2D |
改进示例
在继续之前,让我们让这个示例看起来更美观一些。通过添加一个旋转的矩形网格,并让DirectionalLight 投射阴影,我们可以验证对立方体顶点的修改是否正确地反映在所有渲染阶段中,包括阴影贴图。 为了获得可见的阴影,现在将光源在Y轴上放置得稍高一些,并对其进行旋转,使其部分朝下照射。(由于这是directional 光源,旋转角度至关重要)
| main.qml,material.vert | 结果 |
|---|---|
|
添加片段着色器
许多自定义材质也会需要片段着色器。事实上,许多材质甚至仅需片段着色器。如果没有需要从顶点阶段传递到片段阶段的额外数据,且默认的顶点变换已足够,则可以省略在CustomMaterial 中设置vertexShader 属性。
| 在 main.qml 的 material.frag 中进行修改 | 结果 |
|---|---|
|
|
我们的第一个片段着色器包含一个空的 MAIN() 函数。这与完全不指定片段着色器代码片段并无二致:我们得到的效果与使用默认PrincipledMaterial 时完全相同。
让我们来看看片段着色器中一些常用的关键字。这并非完整列表,完整的参考请参阅CustomMaterial 文档。其中许多是可读写的,这意味着它们有默认值,但着色器可以(且通常会)为其赋予不同的值。
顾名思义,其中许多与名称相似的PrincipledMaterial 属性相对应,具有相同的含义和语义,遵循金属-粗糙度材质模型。 这些值的计算方式由自定义材质实现决定:例如,BASE_COLOR 的值可以在着色器中硬编码,也可以基于纹理采样,或者根据作为uniform暴露的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) 。 |
| 金属度 | float | Metalness 取值范围为0到1。默认值为0,表示该材质为介电材质(非金属)。 |
| 粗糙度 | float | Roughness 取值范围为 0-1。默认值为 0。数值越大,镜面高光越柔和,反射效果越模糊。 |
| 镜面反射强度 | 浮点数 | The strength of specularity 取值范围为0-1。默认值为0.5 。对于将metalness 设置为1 的金属物体,此值将不起作用。当SPECULAR_AMOUNT 和METALNESS 的值均大于0且小于1时,结果将是这两种材质模型的混合。 |
| 法线 | vec3 | 世界坐标系中的插值法线,当禁用面剔除时,该值会根据双面性进行调整。只读。 |
| UV0 | vec2 | 插值后的纹理坐标。只读。 |
| VAR_WORLD_POSITION | vec3 | 世界坐标系中的插值顶点位置。只读。 |
让我们将立方体的底色设为红色:
| 在 main.qml 文件中的 material.frag 部分进行修改 | 结果 |
|---|---|
|
|
现在稍微增强一下自发光强度:
| 在 main.qml 中修改 material.frag | 结果 |
|---|---|
|
|
与其在着色器中硬编码数值,我们也可以使用作为uniform暴露的QML属性,甚至包括带动画效果的属性:
| 在 main.qml 中修改 material.frag | 结果 |
|---|---|
|
让我们尝试一些更复杂的内容,即无法通过 `PrincipledMaterial ` 及其标准的内置属性来实现的功能。以下材质可直观展示立方体网格的纹理 UV 坐标。U 的取值范围为 0 到 1,因此颜色从黑色过渡到红色;而 V 的取值范围同样为 0 到 1,颜色从黑色过渡到绿色。
| 在 main.qml 中修改 material.frag | 结果 |
|---|---|
|
|
既然已经做到了这一步,不妨也可视化法线,这次在球体上展示。与 UV 坐标类似,如果自定义顶点着色器代码片段修改了 NORMAL 的值,那么片段着色器中按片段插值得出的值(同样以 NORMAL 为名暴露出来)也会反映这些调整。
| 在 main.qml 中修改 material.frag | 结果 |
|---|---|
|
|
颜色
让我们暂时切换到茶壶模型,将材质设置为金属与介电的混合材质,并尝试为其设置绿色底色。green QColor 的值映射到(0, 128, 0) ,基于此,我们的首次尝试可以是:
| main.qml, material.frag |
|---|
|

这看起来并不完全正确。与第二种方法相比:
| main.qml 中 material.frag 的修改 | 结果 |
|---|---|
|
|
切换到PrincipledMaterial 后,我们可以确认:将PrincipledMaterial::baseColor 设置为“green”,并遵循金属度及其他属性,结果与我们的第二种方法完全一致:
| main.qml中的修改 | 结果 |
|---|---|
|
|
如果将uColor 属性的类型改为vector4d ,或任何非color 的类型,结果会突然发生变化,并与我们的第一种方法完全一致。
这是为什么呢?
答案在于 DefaultMaterial 的 color 属性、PrincipledMaterial ,以及在CustomMaterial 中类型为color 的自定义属性,这些属性都会隐式地进行 sRGB 到线性空间的转换。 对于任何其他值都不会进行此类转换,因此,如果着色器硬编码了一个颜色值,或者基于类型与color 不同的QML属性,那么当源值位于sRGB颜色空间时,就由着色器负责进行线性化处理。 转换为线性非常重要,因为Qt Quick 3D 会对片段着色结果执行tonemapping ,而该过程假设输入值位于sRGB空间中。
内置的QColor 常量(例如"green" )均以sRGB色空间表示。因此,若要获得与sRGB色空间中RGB值(0, 128, 0) 相匹配的结果,仅在首次尝试时将vec4(0.0, 0.5, 0.0, 1.0) 赋值给BASE_COLOR 是不够的。 请参阅CustomMaterial 中的BASE_COLOR 文档,了解将此类颜色值线性化的公式。这同样适用于通过采样纹理获取的颜色值:如果源图像数据不在 sRGB 色彩空间中,则需要进行转换(除非禁用了tonemapping )。
混合
如果期望实现 alpha 混合,仅将小于1.0 的值写入BASE_COLOR.a 是不够的。此类材质通常会调整sourceBlend 和destinationBlend 属性的值以获得预期效果。
此外请注意,组合后的透明度值等于Node opacity 与材质透明度值的乘积。
为了直观展示,让我们使用一个着色器,该着色器将具有透明度0.5 的红色赋予BASE_COLOR :
| main.qml,material.frag | 结果 |
|---|---|
|
|
第一个立方体将 0.5 写入颜色的透明度值,但由于未启用透明度混合,因此不会产生可见效果。第二个立方体通过CustomMaterial 属性启用了简单的透明度混合。第三个立方体还为模型赋予了 0.5 的不透明度,这意味着有效不透明度为 0.25。
在顶点着色器与片段着色器之间传递数据
计算每个顶点的值(例如,假设有一个三角形,针对该三角形的三个顶点),然后将其传递到片段着色器阶段;在该阶段,对于每个片段(例如,被光栅化三角形覆盖的每个片段),都会提供一个插值后的值。 在自定义材质着色器代码片段中,这是通过VARYING 关键字实现的。它提供了与GLSL 120和GLSL ES 100类似的语法,但无论运行时使用何种图形API都能正常工作。引擎会负责根据实际情况重写varying声明。
让我们来看看采用 UV 坐标的经典纹理采样会是什么样子。纹理相关内容将在后续章节中详细介绍,目前我们先关注如何获取 UV 坐标,以便将其传递给着色器中的 `texture() ` 函数。
| main.qml、material.vert、material.frag |
|---|
|
| qt_logo_rect.png | 结果 |
|---|---|
|
|
请注意VARYING 的声明。名称和类型必须匹配,片段着色器中的uv 将暴露当前片段的插值UV坐标。
任何其他类型的数据都可以以类似的方式传递到片段阶段。值得注意的是,在许多情况下,没有必要设置材质自身的变量,因为系统提供了内置变量,可以满足许多典型需求。 这包括生成(插值后的)法线、UV、世界坐标(VAR_WORLD_POSITION )或指向摄像机的向量(VIEW_VECTOR )。
实际上,由于UV0 在片段渲染阶段也是自动可用的,因此上面的示例可以简化为以下形式:
| 在 main.qml 的 material.frag 中进行修改 | 结果 |
|---|---|
|
|
若要禁用某个变量的插值,请在顶点着色器和片段着色器代码片段中均使用flat 关键字。例如:
VARYING flat vec2 v;纹理
CustomMaterial 没有内置的纹理贴图,这意味着它没有类似于PrincipledMaterial::baseColorMap 的等效功能。这是因为实现相同功能通常非常简单,同时比 DefaultMaterial 和PrincipledMaterial 内置的功能提供了更大的灵活性。 除了简单地采样纹理外,自定义片段着色器代码片段在计算分配给BASE_COLOR 、EMISSIVE_COLOR 、ROUGHNESS 等属性的值时,可以自由地组合和混合来自各种来源的数据。这些计算可以基于通过QML属性提供的数据、从顶点阶段传来的插值数据、从采样纹理中获取的值,以及硬编码的值。
如前例所示,将纹理暴露给顶点着色器、片段着色器或两者,其机制与标量和向量统一变量非常相似:类型为TextureInput 的 QML 属性将自动与着色器代码中的sampler2D 建立关联。与往常一样,无需在着色器代码中声明该采样器。
TextureInput 引用一个Texture ,并额外具有enabled 属性。Texture 可以通过三种方式获取数据:from an image file 、 from a texture with live Qt Quick content,或通过QQuick3DTextureData 引用can be provided from C++ 。
注意:对于 Texture 属性,只有与源、平铺和过滤相关的属性会在自定义材质中被隐式考虑,其余属性(例如 UV 变换)则由自定义着色器根据需要自行实现。
下面我们来看一个示例,其中一个模型(本例中为一个球体)使用实时Qt Quick 内容进行贴图:
| main.qml、material.frag |
|---|
|
在此,每当该微场景发生变化时,该 2D 子树(一个带有两个子节点的矩形:另一个矩形和文本)都会被渲染到一张 512x512 的 2D 纹理中。随后,该纹理会以“someTextureMap ”为名暴露给自定义材质。
请注意着色器中对 V 坐标的翻转处理。如上所述,自定义材质在着色器层面具备完全的可编程性,但并不提供Texture 和PrincipledMaterial 所具备的“固定”特性。这意味着对 UV 坐标的任何变换都需要由着色器来完成。 这里我们知道纹理是通过Texture::sourceItem 生成的,因此需要翻转 V 坐标,以获得与我们所用网格的 UV 坐标集相匹配的结果。
这个示例展示了使用PrincipledMaterial 同样可以实现这一效果。让我们再添加一个简单的浮雕效果,使其更加生动:
| material.frag | 结果 |
|---|---|
|
借助迄今为止介绍的功能,我们可以创造出多种多样的材质,以视觉上令人印象深刻的方式为网格着色。为了结束这次基础巡礼,让我们看一个将高度图和法线贴图应用到平面网格的示例。 (此处使用专用的.mesh 文件,因为内置的#Rectangle 细分级别不足)为了获得更好的照明效果,我们将使用基于图像的照明技术,并采用一张360度HDR图像。该图像同时被设置为天空盒,以便更清晰地展示效果。
首先,让我们从一个空的CustomMaterial 文件开始:
| main.qml | 结果 |
|---|---|
|
|
现在,让我们编写一些着色器,将高度图和法线贴图应用到网格上:
| 高度贴图 | 法线贴图 |
|---|---|
|
|
| material.vert, material.frag |
|---|
|
| main.qml 中的更改 | 结果 |
|---|---|
|
|
注意: WasdController 对象 在开发和故障排除过程中非常有用,因为它允许用户以熟悉的方式使用键盘和鼠标在场景中导航和环顾四周。通过WasdController 控制摄像机非常简单,只需:
import QtQuick3D.Helpers
View3D {
PerspectiveCamera {
id: camera
}
// ...
}
WasdController {
controlledObject: camera
}深度和屏幕纹理
当自定义着色器片段使用DEPTH_TEXTURE 或SCREEN_TEXTURE 关键字时,它会选择在单独的渲染通道中生成相应的纹理。虽然这并非一项低成本的操作,但它允许实现多种技术,例如玻璃状材质的折射效果。
DEPTH_TEXTURE sampler2D 允许在渲染完场景中所有 对象后,从纹理中采样深度缓冲区的内容。同样, 作为 ,允许采样包含场景内容的纹理,其中不包含任何透明材质或同样使用 SCREEN_TEXTURE 的材质。 该纹理可用于需要渲染目标帧缓冲区内容材质。SCREEN_TEXTURE纹理使用与 相同的清除模式。这些纹理的大小与 的像素尺寸一致。opaque SCREEN_TEXTURE sampler2D View3D View3D
让我们通过DEPTH_TEXTURE 来可视化深度缓冲区的内容,以此进行一个简单的演示。此处将摄像机的far clip value 从默认值10000降低至2000,以便缩小范围,从而使可视化的深度值差异更加明显。 结果是一个矩形,它恰好将场景的深度缓冲区可视化并呈现在其表面上。
| main.qml, material.frag | 结果 |
|---|---|
|
请注意,圆柱体并未出现在DEPTH_TEXTURE 中,这是因为它依赖半透明属性,这使其与所有不透明的其他对象归入不同的类别。这些对象不会写入深度缓冲区,尽管它们会根据不透明对象写入的深度值进行测试,并且依赖于从后到前的渲染顺序。 因此,它们在DEPTH_TEXTURE 中也不存在。
如果我们将着色器改为采样SCREEN_TEXTURE ,会发生什么情况?
| material.frag | 结果 |
|---|---|
|
|
在此,矩形使用SCREEN_TEXTURE 进行纹理映射,同时将透明像素替换为紫色。
光照处理器函数
CustomMaterial 的一项高级功能是,可以在片段着色器中定义函数,以重新实现用于计算片段颜色的光照方程。如果存在光照处理器函数,则对于场景中的每盏灯,针对每个片段都会调用一次该函数。 针对不同的光源类型,以及环境光和镜面反射光,都有专门的函数。如果不存在对应的光源处理器函数,则使用标准计算方式,就像PrincipledMaterial 所做的那样。如果存在光源处理器,但函数体为空,则表示场景中该类型的光源不会产生任何贡献。
有关DIRECTIONAL_LIGHT 、POINT_LIGHT 、SPOT_LIGHT 、AMBIENT_LIGHT 和SPECULAR_LIGHT 等函数的详细信息,请参阅CustomMaterial 文档。
无阴影自定义材质
还有另一种CustomMaterial 类型:unshaded 自定义材质。到目前为止的所有示例都使用了shaded 自定义材质,其中shadingMode 属性保持其默认值CustomMaterial.Shaded。
如果我们将该属性切换为CustomMaterial.Unshaded,会发生什么情况?
首先,诸如BASE_COLOR 、EMISSIVE_COLOR 、METALNESS 等关键字将不再产生预期效果。这是因为无着色材质(Unshaded Material)顾名思义,不会自动应用大部分标准着色代码,因此会忽略场景中的光照、基于图像的光照、阴影以及环境光遮蔽。 相反,无着色材质通过FRAGCOLOR 关键字将完全控制权交给了着色器。这类似于gl_FragColor:分配给FRAGCOLOR 的颜色即为片段的结果和最终颜色,不会再受到Qt Quick 3D 的任何进一步调整。
| main.qml、material.frag、material2.frag | 结果 |
|---|---|
|
|
请注意,右侧圆柱体忽略了场景中的DirectionalLight 。其着色过程完全不考虑场景光照,最终的片段颜色为纯白。
未着色材质中的顶点着色器仍然可以使用典型的输入:VERTEX 、NORMAL 、MODELVIEWPROJECTION_MATRIX 等,并且可以写入POSITION 。然而,片段着色器不再拥有类似的便利功能:NORMAL 、UV0 或VAR_WORLD_POSITION 在未着色材质的片段着色器中均不可用。 取而代之的是,现在需要由着色器代码自行计算,并通过 `VARYING ` 传递所有用于确定最终片段颜色所需的数据。
让我们来看一个同时包含顶点着色器和片段着色器的示例。修改后的顶点位置会被传递给片段着色器,并且每个片段都能获得一个插值后的值。
| main.qml、material.vert、material.frag |
|---|
|
当无需或不希望与场景光照交互,且材质需要完全控制最终片段颜色时,无着色材质非常有用。请注意,上面的示例中既没有DirectionalLight ,也没有其他光源,但带有自定义材质的球体却如预期般显示出来。
注意:如果 无着色材质 仅包含顶点着色器片段,但未指定 fragmentShader 属性,该材质虽然 仍可正常工作,但渲染结果将等同于 shadingMode 被设置为 Shaded 的情况。因此,对于仅包含顶点着色器的材质,更改 shadingMode 并无实际意义。
效果的可编程性
后处理效果会将一个或多个片段着色器应用于View3D 的渲染结果。随后,这些片段着色器的输出将取代原始渲染结果进行显示。从概念上讲,这与Qt Quick 中的ShaderEffect 和ShaderEffectSource 非常相似。
注意: 仅当View3D 的renderMode 设置为View3D.Offscreen时,才可使用后处理 效果。
虽然也可以为某个效果指定自定义顶点着色器代码片段,但其实用性有限,因此预计使用频率较低。后处理效果的顶点输入是一个四边形(即两个三角形或一个三角形带),对该四边形的顶点进行变换或位移通常并无帮助。 不过,使用VARYING 关键字通过顶点着色器计算数据并将其传递给片段着色器,这种做法是有意义的。与通常情况一样,片段着色器随后将收到基于当前片段坐标的插值值。
与Effect 关联的着色器代码片段的语法,与未着色的CustomMaterial 的着色器完全相同。至于内置的特殊关键字,VARYING 、MAIN 、FRAGCOLOR (仅限片段着色器)、POSITION (仅限顶点着色器)、VERTEX (仅限顶点着色器)以及MODELVIEWPROJECTION_MATRIX 的工作方式与CustomMaterial 完全一致。
Effect 片段着色器中最重要的特殊关键字如下:
| 名称 | 类型 | 描述 |
|---|---|---|
| INPUT | sampler2D 或 sampler2DArray | 输入纹理的采样器。效果通常会使用INPUT_UV 对该采样器进行采样。 |
| INPUT_UV | vec2 | 用于采样INPUT 的UV坐标。 |
| INPUT_SIZE | vec2 | INPUT 纹理的大小(以像素为单位)。这是调用 textureSize() 的便捷替代方案。 |
| OUTPUT_SIZE | vec2 | 输出纹理的大小,单位为像素。在大多数情况下,该值与INPUT_SIZE 相同,但多通道效果中的某些通道可能会将数据输出到大小不同的中间纹理中。 |
| DEPTH_TEXTURE | sampler2D | 包含场景中不透明物体深度缓冲区内容的深度纹理。与CustomMaterial 类似,在着色器中包含此关键字会触发自动生成深度纹理。 |
注意:当 启用多视图渲染时, 输入纹理为一个 2D 纹理数组。此时,texture() 和 textureSize() 等 GLSL 函数将分别接受/返回 vec3/ivec3 类型参数。 请使用 `VIEW_INDEX ` 作为图层。对于希望在启用和禁用多视图渲染时均能正常运行的 VR/AR 应用程序,可采用以下可移植的着色器代码编写方式:
#if QSHADER_VIEW_COUNT >= 2
vec4 c = texture(INPUT, vec3(INPUT_UV, VIEW_INDEX));
#else
vec4 c = texture(INPUT, INPUT_UV);
#endif后处理效果
让我们从一个简单的场景开始,这次使用更多的对象,包括一个带纹理的矩形,该矩形使用棋盘格纹理作为其基础颜色贴图。
| main.qml | 结果 |
|---|---|
|
|
现在,让我们对整个场景应用一个效果。更准确地说,是对View3D 应用效果。当场景中存在多个View3D 项时,每个项都有自己的SceneEnvironment ,因此各自拥有独立的后处理效果链。在本例中,仅有一个View3D 覆盖了整个窗口。
| main.qml 中的更改 | effect.frag |
|---|---|
| |
这个简单的效果会改变红色通道的值。将 QML 属性作为统一变量(uniforms)暴露出来,在效果中的工作原理与自定义材质完全相同。着色器以一行代码开头,这在编写效果的片段着色器时非常常见:在 UV 坐标INPUT_UV 处采样INPUT 。随后,它执行所需的计算,并将最终的片段颜色赋值给FRAGCOLOR 。
示例中设置的许多属性(如效果、通道、着色器)均为复数形式。虽然当仅有一个元素时可以省略列表[ ] 的语法,但所有这些属性都是列表,可以包含多个元素。这是为什么呢?
- effects View3D 是一个列表,因为它允许将多个效果串联起来。这些效果将按照它们被添加到列表中的顺序依次应用。这使得可以轻松地将两个或多个效果同时应用于 ,其效果类似于在 中通过嵌套 项所能实现的效果。 下一个效果的 纹理始终是包含前一个效果输出的纹理。该列表中最后一个效果的输出将作为 的最终输出。View3D Qt Quick ShaderEffect
INPUTView3D - passes 是一个列表,因为与ShaderEffect 不同,Effect内置了对多通道的支持。多通道效果比在effects 中将多个独立效果串联起来更强大:一个通道不仅可以输出到效果的原始输入纹理,还可以输出到一个临时的中间纹理,该纹理随后可作为后续通道的输入。 这使得能够创建复杂的效果,通过计算、渲染并混合多个纹理来获得最终的片段颜色。本文将不涉及这一高级用例。详情请参阅Effect 文档页面。
- shaders 是一个列表,因为一个效果可能同时关联顶点着色器和片元着色器。
多个效果的串联
让我们看一个示例,其中前一个示例中的效果将通过另一个类似于内置的DistortionSpiral 效果的效果来补充。
| main.qml 中的修改 | effect2.frag |
|---|---|
| |
现在有一个可能令人惊讶的问题:为什么这是一个不好的例子?
更准确地说,这并非一个糟糕的示例,而是展示了一种通常最好避免的模式。
以这种方式链式连接效果虽然有用,但必须牢记其对性能的影响:进行两次渲染(一次用于生成调整过红色通道的纹理,另一次用于计算失真)是非常浪费的,因为一次就足够了。 如果将这些片段着色器代码片段合并,仅需一个效果就能实现相同的结果。
从 C++ 定义网格和纹理数据
程序化生成网格和纹理图像数据都遵循类似的步骤:
- 继承自QQuick3DGeometry ,或者QQuick3DTextureData
- 在构造时通过调用基类的受保护成员函数来设置所需的顶点或图像数据
- 如果后续某个时间点需要动态更改,请设置新数据并调用 update()
- 实现完成后,需要注册该类,使其在 QML 中可见
- Model 此时,QML 中的 `Texture ` 对象可通过设置 `Model::geometry ` 或 `Texture::textureData ` 属性,使用自定义的顶点或图像数据提供程序
自定义顶点数据
顶点数据是指构成网格的一系列(通常为float )值。自定义几何提供程序负责提供这些数据,而无需加载.mesh 文件。 顶点数据由attributes 组成,例如位置、纹理(UV)坐标或法线。属性的规范描述了存在哪些类型的属性、分量类型(例如,顶点位置是一个由x、 y、z 值))、它们在提供的数据中从哪个偏移量开始,以及步长(即为了指向同一属性的下一个元素,需要加到偏移量上的增量)是多少。
如果曾直接使用过 OpenGL 或 Vulkan 等图形 API,这可能看起来很熟悉,因为这些 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 实现):

注意:请 注意,根据底层图形 API 的不同,运行时可能不支持 1 以外的点大小和线宽。这并非 Qt 所能控制的。因此,可能需要实现替代技术,而不是依赖点和线的绘制。
自定义纹理数据
对于纹理,所需提供的数据在结构上要简单得多:它是原始像素数据,每个像素的字节数因纹理格式而异。例如,RGBA 纹理要求每个像素占用四个字节,而RGBA16F 则要求每个像素占用四个半精度浮点数。这与QImage 在内部存储的数据类似。 然而,Qt Quick 3D 纹理可能采用无法通过QImage 表示的数据格式,例如浮点 HDR 纹理或压缩纹理。因此,QQuick3DTextureData 的数据始终以原始字节序列的形式提供。如果曾直接使用过 OpenGL 或 Vulkan 等图形 API,这可能看起来很熟悉。
有关详细信息,请参阅QQuick3DGeometry 和QQuick3DTextureData 文档页面。
另请参阅 CustomMaterial 、Effect 、QQuick3DGeometry 、QQuick3DTextureData 、Qt Quick 3D - 自定义效果示例、Qt Quick 3D - 自定义着色器示例、Qt Quick 3D - 自定义材质示例、Qt Quick 3D - 自定义几何体示例,以及Qt Quick 3D - 过程纹理示例。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.























