可编程材质、效果、几何图形和纹理数据
虽然Qt Quick 3D 、DefaultMaterial 和PrincipledMaterial 的内置材质可通过其属性进行广泛的自定义,但它们不提供顶点和片段着色器级别的可编程性。为此,我们提供了CustomMaterial 类型。
模型具有PrincipledMaterial | CustomMaterial 对顶点进行变换的模型 |
---|---|
后处理效果,即在View3D 的输出传递到Qt Quick 之前,对色彩缓冲区进行一次或多次处理,并可选择将深度缓冲区考虑在内,也有两种类型:
- 内置后期处理步骤,可通过ExtendedSceneEnvironment 进行配置,如发光/绽放、景深、虚化、镜头耀斑等、
custom
由应用程序以片段着色器代码形式实现的效果,以及在 对象中指定的处理步骤。Effect
在实践中,还有第三类后期处理效果:通过Qt Quick 实现的 2D 效果,在View3D 项目的输出上运行,无需 3D 渲染器的任何参与。例如,要对View3D 项目进行模糊处理,最简单的方法是使用Qt Quick 的现有设施,如MultiEffect 。三维后处理系统适用于涉及三维场景概念的复杂特效,如深度缓冲区或屏幕纹理,或需要处理 HDR 色调映射,或需要使用中间缓冲区进行多次处理等。对于不需要深入了解 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 { } } } } |
由于CustomMaterial 在未添加顶点或片段着色器代码的情况下,实际上就是一个PrincipledMaterial ,因此两者的结果完全相同。
注意: 诸如baseColor,metalness,baseColorMap 等属性在CustomMaterial QML 类型中没有对应的属性。这是由设计决定的:自定义材质是通过着色器代码完成的,而不仅仅是提供几个固定值。
我们的第一个顶点着色器
让我们添加一个自定义顶点着色器片段。方法是在vertexShader 属性中引用一个文件。片段着色器的方法与此相同。这些引用的工作原理与Image.source 或ShaderEffect.vertexShader 类似:它们是本地 URL 或qrc
URL,而相对路径是相对于.qml
文件位置处理的。因此,常用的方法是将.vert
和.frag
文件放入 Qt 资源系统(使用 CMake 时为qt_add_resources
),并使用相对路径引用它们。
在 Qt XML 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 页面。
关键字 | 类型 | 说明 |
---|---|---|
MAIN | void MAIN() 是入口点。自定义顶点着色器片段中必须始终包含该函数,否则就没有意义。 | |
顶点 | vec3 | 顶点着色器作为输入接收的顶点位置。自定义材质中顶点着色器的一个常见用例是改变(置换)该向量的 x、y 或 z 值,只需给整个向量或其部分分量赋值即可。 |
正常 | vec3 | 输入网格数据中的顶点法线,如果没有提供法线,则全部为零。与 VERTEX 一样,着色器可以随意更改该值。更改后的值将用于管道的其他部分,包括片段阶段的光照计算。 |
UV0 | vec2 | 输入网格数据中的第一组纹理坐标,如果没有提供 UV 值,则全部为零。与 VERTEX 和 NORMAL 一样,该值可以更改。 |
模型视图投影矩阵 | mat4 | 模型视图投影矩阵。为了统一行为,无论使用哪种图形应用程序接口进行渲染,所有顶点数据和变换矩阵都在这一层遵循 OpenGL 惯例。(Y 轴向上,OpenGL 兼容的投影矩阵),只读。 |
模型矩阵 | mat4 | 模型(世界)矩阵。只读。 |
NORMAL_MATRIX | mat3 | 模型矩阵左上方 3x3 片的转置逆矩阵。只读。 |
摄像机位置 | vec3 | 摄像机在世界空间中的位置。在本页的示例中为(0, 0, 600) 。只读。 |
摄像机方向 | vec3 | 摄像机方向矢量。在本页的示例中,它是(0, 0, -1) 。只读。 |
摄像机属性 | vec2 | 摄像机的远近剪辑值。在本页的示例中,它是(10, 10000) 。只读。 |
点尺寸 | 浮点数 | 仅在使用点拓扑进行渲染时相关,例如custom geometry 会为网格提供这样的几何体。写入此值等同于设置pointSize on a PrincipledMaterial 。 |
位置 | 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 类型 | 着色器类型 | 注释 |
---|---|---|
实型、整型、布尔型 | 浮点、int、bool | |
颜色 | vec4 | 隐式执行 sRGB 到线性的转换 |
vector2d | vec2 | |
向量3d | vec3 | |
向量4d | 向量4 | |
矩阵4x4 | mat4 | |
四元数 | vec4 | 标量值为w |
矩形 | 矢量 | |
点,大小 | vec2 | |
TextureInput | 采样器 2D |
改进示例
在继续前进之前,让我们先把示例做得更好看一些。通过添加一个旋转矩形网格并使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 属性,其含义和语义与金属粗糙度材质模型相同。自定义材质实现可自行决定如何计算这些值:例如,BASE_COLOR 的值可以硬编码在着色器中,也可以根据纹理采样计算,还可以根据 QML 属性计算,如制服或顶点着色器传递的内插数据。
关键字 | 类型 | 描述 |
---|---|---|
BASE_COLOR | vec4 | 基色和 alpha 值。对应于PrincipledMaterial::baseColor 。片段的最终 alpha 值是模型不透明度乘以基色 alpha。默认值为(1.0, 1.0, 1.0, 1.0) 。 |
动态颜色 | vec3 | 自发光的颜色。对应于PrincipledMaterial::emissiveFactor 。默认值为(0.0, 0.0, 0.0) 。 |
金属度 | 浮点数 | Metalness 数值范围 0-1。默认值为 0,表示材料是介电质(非金属)。 |
粗糙度 | 浮点 | Roughness 值,取值范围 0-1。默认值为 0。数值越大,镜面反射越柔和,反射越模糊。 |
镜面反射量 | 浮点数 | The strength of specularity 范围为 0-1。默认值为 。对于 设置为 的金属物体,该值不会有任何影响。当 和 的值都大于 0 但小于 1 时,结果将是两种材质模型的混合。0.5 metalness 1 SPECULAR_AMOUNT METALNESS |
正常 | 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 设置为 "绿色 "并遵循金属度和其他属性,结果与第二种方法完全相同:
更改 main.qml | 结果 |
---|---|
materials: PrincipledMaterial { baseColor: "green" metalness: 0.6 specularAmount: 0.4 roughness: 0.4 } |
如果将uColor
属性的类型改为vector4d
或color
以外的任何类型,结果会突然发生变化,变得与我们的第一种方法完全相同。
为什么会这样呢?
答案就在于默认材质(DefaultMaterial)的颜色属性、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 是不够的。有关将此类颜色值线性化的公式,请参阅BASE_COLOR
文档CustomMaterial 。这同样适用于通过纹理采样获取的颜色值:如果源图像数据不在 sRGB 颜色空间中,则需要进行转换(除非禁用tonemapping )。
混合
如果希望获得 alpha 混合效果,仅将小于1.0
的值写入BASE_COLOR.a
是不够的。此类材质通常会更改sourceBlend 和destinationBlend 属性的值,以获得理想的效果。
此外,请记住,综合 alpha 值是Node opacity 乘以材质 alpha 值。
为直观起见,让我们使用一个着色器,将带有 alpha0.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 写入颜色的 alpha 值,但由于未启用 alpha 混合,因此不会产生明显的效果。第二个立方体通过CustomMaterial 属性启用了简单的 alpha 混合。第三个立方体也为模型指定了 0.5 的不透明度,这意味着有效不透明度为 0.25。
在顶点着色器和片段着色器之间传递数据
计算每个顶点的数值(例如,假设是一个三角形,则计算三角形 3 个角的数值),然后将其传递到片段阶段,在此阶段,每个片段(例如,光栅化三角形覆盖的每个片段)都可以获得一个插值。在自定义材质着色器片段中,VARYING
关键字可以实现这一点。该关键字提供了一种类似于 GLSL 120 和 GLSL ES 100 的语法,但无论运行时使用何种图形 API,它都能正常工作。引擎会根据情况重写不同的声明。
让我们看看使用 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); } |
要禁用变量插值,请在顶点和片段着色器片段中使用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 |
---|
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 子树(矩形,有两个子树:另一个矩形和文本)在这个迷你场景每次变化时都会渲染为 512x512 的 2D 纹理。然后,该纹理会以someTextureMap
的名称显示在自定义材质中。
请注意着色器中 V 坐标的翻转。如上所述,自定义材质在着色器层面具有完全的可编程性,但并不具备Texture 和PrincipledMaterial 的 "固定 "特性。这意味着 UV 坐标的任何变换都需要由着色器应用。在这里,我们知道纹理是通过Texture::sourceItem 生成的,因此需要翻转 V,以获得与我们使用的网格的 UV 集相匹配的纹理。
本示例所展示的内容也可以通过PrincipledMaterial 实现。让我们再做一个简单的浮雕效果,让它变得更有趣:
材质.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 { } } } } |
现在让我们制作一些着色器,将高度贴图和法线贴图应用到网格上:
高度贴图 | 法线贴图 |
---|---|
材质.vert、材质.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
是一个 ,允许使用深度缓冲区的内容对纹理进行采样,同时渲染场景中的所有 物体。同样, 是一个 ,允许对包含场景内容的纹理进行采样,但不包括任何透明材质或同样使用 SCREEN_TEXTURE 的材质。该纹理可用于需要渲染帧缓冲内容的材质。SCREEN_TEXTURE 纹理使用与 相同的透明模式。这些纹理的大小与 的像素大小相匹配。sampler2D
opaque
SCREEN_TEXTURE
sampler2D
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
制作的纹理,同时用紫色替换了透明像素。
光处理器功能
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
等不再具有预期效果。这是因为未着色材质,顾名思义,不会自动修正大部分标准着色代码,从而忽略场景中的灯光、基于图像的照明、阴影和环境遮蔽。相反,无着色材质通过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 的意义不大。
特效的可编程性
后处理特效对View3D 的结果应用一个或多个片段着色器。这些片段着色器的输出结果将代替原始渲染结果显示出来。这在概念上与Qt Quick 的ShaderEffect 和ShaderEffectSource 非常相似。
注: 只有当View3D 的renderMode 设置为View3D.Offscreen 时,后处理效果才可用。
也可以为效果指定自定义顶点着色器片段,但这些片段的作用有限,因此预计很少使用。后处理效果的顶点输入是一个四边形(两个三角形或一个三角形条带),变换或移动其顶点通常不会有帮助。不过,使用VARYING
关键字计算顶点着色器的数据并将其传递给片段着色器也是有意义的。像往常一样,片段着色器将收到基于当前片段坐标的插值。
与Effect 相关联的着色器片段的语法与未着色的CustomMaterial 的着色器完全相同。在内置特殊关键字方面,VARYING
、MAIN
、FRAGCOLOR
(仅片段着色器)、POSITION
(仅顶点着色器)、VERTEX
(仅顶点着色器)和MODELVIEWPROJECTION_MATRIX
的工作原理与CustomMaterial 相同。
Effect 片段着色器最重要的特殊关键字如下:
名称 | 类型 | 描述 |
---|---|---|
输入 | sampler2D 或 sampler2DArray | 输入纹理的采样器。特效通常会使用INPUT_UV 对其进行采样。 |
INPUT_UV | vec2 | 用于采样的 UV 坐标INPUT 。 |
INPUT_SIZE | vec2 | INPUT 纹理的大小(像素)。这是调用 textureSize() 的便捷替代方法。 |
输出尺寸 | vec2 | 输出纹理的大小(像素)。在很多情况下等于INPUT_SIZE ,但多通道效果可能会输出不同大小的中间纹理。 |
深度纹理 | 采样器 2D | 深度纹理,包含场景中不透明物体的深度缓冲区内容。与CustomMaterial 一样,着色器中出现此关键字会自动生成深度纹理。 |
注: 启用多视图渲染时,输入纹理是一个二维纹理数组。GLSL 函数(如 texture() 和 textureSize())将分别获取/返回一个 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 | 结果 |
---|---|
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 因为与 不同,Effect 内置支持多通道。多通道特效比在 中将多个独立特效串联在一起的功能更强大:一个通道可以输出到一个临时的中间纹理,除了特效的原始输入纹理外,该纹理还可以用作后续通道的输入。这样就可以创建计算、渲染和混合多种纹理的复杂效果,以获得最终的片段颜色。这里不涉及这种高级用例。详情请参考 文档页面。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; } |
现在有一个也许令人惊讶的问题:为什么这个例子不好?
更确切地说,这并不是一个坏例子,相反,它显示了一种可以避免的模式。
以这种方式连锁效果可能很有用,但重要的是要记住对性能的影响:进行两次渲染(一次是用调整后的红色通道生成纹理,另一次是计算失真)是相当浪费的,而一次渲染就足够了。如果将片段着色器片段合并,只需一个效果就能达到同样的效果。
用 C++ 定义网格和纹理数据
程序化生成网格和纹理图像数据的步骤相似:
- 子类QQuick3DGeometry 或QQuick3DTextureData
- 通过调用基类的受保护成员函数,在构建时设置所需的顶点或图像数据
- 如果之后需要动态更改,则设置新数据并调用 update()
- 实现完成后,需要注册该类,使其在 QML 中可见
- Model 和 对象现在可以通过设置 或 属性来使用自定义顶点或图像数据提供程序。Texture Model::geometry Texture::textureData
自定义顶点数据
顶点数据是指组成网格的一系列数值(通常是float
)。自定义几何体提供程序负责提供相同的数据,而不是加载.mesh
文件。顶点数据包括attributes
,如位置、纹理(UV)坐标或法线。属性规范描述了属性的种类、组件类型(例如,由 x、y、z 值组成的顶点位置三组件浮点矢量)、它们在所提供数据中的起始偏移量以及跨距(为指向同一属性的下一个元素而需要添加到偏移量的增量)。
如果直接使用过 OpenGL 或 Vulkan 等图形应用程序接口,可能会对这些内容感到熟悉,因为这些应用程序接口指定顶点输入的方式与.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
纹理希望每个像素有 4 个字节,而RGBA16F
则是每个像素有 4 个半浮点。这与QImage 内部存储的数据类似。但是,Qt Quick 3D 纹理可能具有无法用QImage 表示的数据格式。例如,浮点 HDR 纹理或压缩纹理。因此,QQuick3DTextureData 的数据总是以原始字节序列的形式提供。如果曾直接使用过 OpenGL 或 Vulkan 等图形应用程序接口,可能会对这一点有所了解。
有关详情,请参阅QQuick3DGeometry 和QQuick3DTextureData 文档页面。
另请参阅 CustomMaterial,Effect,QQuick3DGeometry,QQuick3DTextureData,Qt Quick 3D - 自定义效果示例、Qt Quick 3D - 自定义着色器示例、Qt Quick 3D - 自定义材质示例、Qt Quick 3D - 自定义几何图形示例和Qt Quick 3D - 程序纹理示例。
© 2025 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.