Revision control
Copy as Markdown
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qquick3deffect_p.h"
#include <ssg/qssgrendercontextcore.h>
#include <QtQuick3DRuntimeRender/private/qssgrendereffect_p.h>
#include <QtQuick3DRuntimeRender/private/qssgshadermaterialadapter_p.h>
#include <QtQuick3DUtils/private/qssgutils_p.h>
#include <QtQuick/qquickwindow.h>
#include <QtQuick3D/private/qquick3dobject_p.h>
#include <QtQuick3D/private/qquick3dscenemanager_p.h>
#include <QtCore/qfile.h>
#include <QtCore/qurl.h>
QT_BEGIN_NAMESPACE
/*!
\qmltype Effect
\inherits Object3D
\inqmlmodule QtQuick3D
\nativetype QQuick3DEffect
\brief Base component for creating a post-processing effect.
The Effect type allows the user to implement their own post-processing
effects for QtQuick3D.
\section1 Post-processing effects
A post-processing effect is conceptually very similar to Qt Quick's \l
ShaderEffect item. When an effect is present, the scene is rendered into a
separate texture first. The effect is then applied by drawing a textured
quad to the main render target, depending on the
\l{View3D::renderMode}{render mode} of the View3D. The effect can provide a
vertex shader, a fragment shader, or both. Effects are always applied on the
entire scene, per View3D.
Effects are associated with the \l SceneEnvironment in the
\l{SceneEnvironment::effects} property. The property is a list: effects can
be chained together; they are applied in the order they are in the list,
using the previous step's output as the input to the next one, with the last
effect's output defining the contents of the View3D.
\note \l SceneEnvironment and \l ExtendedSceneEnvironment provide a set of
built-in effects, such as depth of field, glow/bloom, lens flare, color
grading, and vignette. Always consider first if these are sufficient for
the application's needs, and prefer using the built-in facilities instead
of implementing a custom post-processing effect.
Effects are similar to \l{CustomMaterial}{custom materials} in many
ways. However, a custom material is associated with a model and is
responsible for the shading of that given mesh. Whereas an effect's vertex
shader always gets a quad (for example, two triangles) as its input, while
its fragment shader samples the texture with the scene's content.
Unlike custom materials, effects support multiple passes. For many effects
this it not necessary, and when there is a need to apply multiple effects,
identical results can often be achieved by chaining together multiple
effects in \l{SceneEnvironment::effects}{the SceneEnvironment}. This is
demonstrated by the \l{Qt Quick 3D - Custom Effect Example}{Custom Effect
example} as well. However, passes have the possibility to request additional
color buffers (texture), and specify which of these additional buffers they
output to. This allows implementing more complex image processing techniques
since subsequent passes can then use one or more of these additional
buffers, plus the original scene's content, as their input. If necessary,
these additional buffers can have an extended lifetime, meaning their
content is preserved between frames, which allows implementing effects that
rely on accumulating content from multiple frames, such as, motion blur.
When compared to Qt Quick's 2D ShaderEffect, the 3D post-processing effects
have the advantage of being able to work with depth buffer data, as well as
the ability to implement multiple passes with intermediate buffers. In
addition, the texture-related capabilities are extended: Qt Quick 3D allows
more fine-grained control over filtering modes, and allows effects to work
with texture formats other than RGBA8, for example, floating point formats.
\note Post-processing effects are currently available when the View3D
has its \l{View3D::renderMode}{renderMode} set to \c Offscreen,
\c Underlay or \c Overlay. Effects will not be rendered for \c Inline mode.
\note When using post-processing effects, the application-provided shaders
should expect linear color data without tonemapping applied. The
tonemapping that is performed during the main render pass (or during skybox
rendering, if there is a skybox) when
\l{SceneEnvironment::tonemapMode}{tonemapMode} is set to a value other than
\c SceneEnvironment.TonemapModeNone, is automatically disabled when there
is at least one post-processing effect specified in the SceneEnvironment.
The last effect in the chain (more precisely, the last pass of the last
effect in the chain) will automatically get its fragment shader amended to
perform the same tonemapping the main render pass would.
\note Effects that perform their own tonemapping should be used in a
SceneEnvironment that has the built-in tonemapping disabled by setting
\l{SceneEnvironment::tonemapMode}{tonemapMode} to \c
SceneEnvironment.TonemapModeNone.
\note By default the texture used as the effects' input is created with a
floating point texture format, such as 16-bit floating point RGBA. The
output texture's format is the same since by default it follows the input
format. This can be overridden using \l Buffer and an empty name. The
default RGBA16F is useful because it allows working with non-tonemapped
linear data without having the color values outside the 0-1 range clamped.
\section1 Exposing data to the shaders
Like with CustomMaterial or ShaderEffect, the dynamic properties of an
Effect object can be changed and animated using the usual QML and Qt Quick
facilities, and the values are exposed to the shaders automatically. The
following list shows how properties are mapped:
\list
\li bool, int, real -> bool, int, float
\li QColor, \l{QtQml::Qt::rgba()}{color} -> vec4, and the color gets
converted to linear, assuming sRGB space for the color value specified in
QML. The built-in Qt colors, such as \c{"green"} are in sRGB color space as
well, and the same conversion is performed for all color properties of
DefaultMaterial and PrincipledMaterial, so this behavior of Effect
matches those.
\li QRect, QRectF, \l{QtQml::Qt::rect()}{rect} -> vec4
\li QPoint, QPointF, \l{QtQml::Qt::point()}{point}, QSize, QSizeF, \l{QtQml::Qt::size()}{size} -> vec2
\li QVector2D, \l{QtQml::Qt::vector2d()}{vector2d} -> vec3
\li QVector3D, \l{QtQml::Qt::vector3d()}{vector3d} -> vec3
\li QVector4D, \l{QtQml::Qt::vector4d()}{vector4d} -> vec4
\li QMatrix4x4, \l{QtQml::Qt::matrix4x4()}{matrix4x4} -> mat4
\li QQuaternion, \l{QtQml::Qt::quaternion()}{quaternion} -> vec4, scalar value is \c w
\li TextureInput -> sampler2D or samplerCube, depending on whether \l
Texture or \l CubeMapTexture is used in the texture property of the
TextureInput. Setting the \l{TextureInput::enabled}{enabled} property to
false leads to exposing a dummy texture to the shader, meaning the shaders
are still functional but will sample a texture with opaque black image
content. Pay attention to the fact that properties for samplers must always
reference a \l TextureInput object, not a \l Texture directly. When it
comes to the \l Texture properties, the source, tiling, and filtering
related ones are the only ones that are taken into account implicitly with
effects, as the rest (such as, UV transformations) is up to the custom
shaders to implement as they see fit.
\endlist
\note When a uniform referenced in the shader code does not have a
corresponding property, it will cause a shader compilation error when
processing the effect at run time. There are some exceptions to this,
such as, sampler uniforms, that get a dummy texture bound when no
corresponding QML property is present, but as a general rule, all uniforms
and samplers must have a corresponding property declared in the
Effect object.
\section1 Getting started with user-defined effects
A custom post-processing effect involves at minimum an Effect object and a
fragment shader snippet. Some effects will also want a customized vertex
shader as well.
As a simple example, let's create an effect that combines the scene's
content with an image, while further altering the red channel's value in an
animated manner:
\table 70%
\row
\li \qml
Effect {
id: simpleEffect
property TextureInput tex: TextureInput {
texture: Texture { source: "image.png" }
}
property real redLevel
NumberAnimation on redLevel { from: 0; to: 1; duration: 5000; loops: -1 }
passes: Pass {
shaders: Shader {
stage: Shader.Fragment
shader: "effect.frag"
}
}
}
\endqml
\li \badcode
void MAIN()
{
vec4 c = texture(tex, TEXTURE_UV);
c.r *= redLevel;
FRAGCOLOR = c * texture(INPUT, INPUT_UV);
}
\endcode
\endtable
Here the texture with the image \c{image.png} is exposed to the shader under
the name \c tex. The value of redLevel is available in the shader in a \c
float uniform with the same name.
The fragment shader must contain a function called \c MAIN. The final
fragment color is determined by \c FRAGCOLOR. The main input texture, with
the contents of the View3D's scene, is accessible under a \c sampler2D with
the name \c INPUT. The UV coordinates from the quad are in \c
INPUT_UV. These UV values are always suitable for sampling \c INPUT,
regardless of the underlying graphics API at run time (and so regardless of
the Y axis direction in images since the necessary adjustments are applied
automatically by Qt Quick 3D). Sampling the texture with our external image
is done using \c TEXTURE_UV. \c INPUT_UV is not suitable in cross-platform
applications since V needs to be flipped to cater for the coordinate system
differences mentioned before, using a logic that is different for textures
based on images and textures used as render targets. Fortunately this is all
taken care of by the engine so the shader need no further logic for this.
Once simpleEffect is available, it can be associated with the effects list
of a the View3D's SceneEnvironment:
\qml
environment: SceneEnvironment {
effects: [ simpleEffect ]
}
\endqml
The results would look something like the following, with the original scene
on the left and with the effect applied on the right:
\table 70%
\row
\li \image effect_intro_1.png
{Three different 3D objects}
\li \image effect_intro_2.png
{Three different 3D objects with transparent Qt logo overlaid as
fullscreen effect}
\endtable
\note The \c shader property value in Shader is a URL, as is the custom in
QML and Qt Quick, referencing the file containing the shader snippet, and
works very similarly to ShaderEffect or
\l{Image::source}{Image.source}. Only the \c file and \c qrc schemes are
supported.. It is also possible to omit the \c file scheme, allowing to
specify a relative path in a convenient way. Such a path is resolved
relative to the component's (the \c{.qml} file's) location.
\note Shader code is always provided using Vulkan-style GLSL, regardless of
the graphics API used by Qt at run time.
\note The vertex and fragment shader code provided by the effect are not
full, complete GLSL shaders on their own. Rather, they provide a \c MAIN
function, and optionally a set of \c VARYING declarations, which are then
amended with further shader code by the engine.
\note The above example is not compatible with the optional multiview rendering mode that is used in some VR/AR applications.
To make it function both with and without multiview mode, change MAIN() like this:
\badcode
void MAIN()
{
vec4 c = texture(tex, TEXTURE_UV);
c.r *= redLevel;
#if QSHADER_VIEW_COUNT >= 2
FRAGCOLOR = c * texture(INPUT, vec3(INPUT_UV, VIEW_INDEX));
#else
FRAGCOLOR = c * texture(INPUT, INPUT_UV);
#endif
}
\endcode
\section1 Effects with vertex shaders
A vertex shader, when present, must provide a function called \c MAIN. In
the vast majority of cases the custom vertex shader will not want to provide
its own calculation of the homogenous vertex position, but it is possible
using \c POSITION, \c VERTEX, and \c MODELVIEWPROJECTION_MATRIX. When
\c POSITION is not present in the custom shader code, a statement equivalent to
\c{POSITION = MODELVIEWPROJECTION_MATRIX * vec4(VERTEX, 1.0);} will be
injected automatically by Qt Quick 3D.
To pass data between the vertex and fragment shaders, use the VARYING
keyword. Internally this will then be transformed into the appropriate
vertex output or fragment input declaration. The fragment shader can use the
same declaration, which then allows to read the interpolated value for the
current fragment.
Let's look at example, that is in effect very similar to the built-in
DistortionSpiral effect:
\table 70%
\row
\li \badcode
VARYING vec2 center_vec;
void MAIN()
{
center_vec = INPUT_UV - vec2(0.5, 0.5);
center_vec.y *= INPUT_SIZE.y / INPUT_SIZE.x;
}
\endcode
\li \badcode
VARYING vec2 center_vec;
void MAIN()
{
float radius = 0.25;
float dist_to_center = length(center_vec) / radius;
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 * FRAMEBUFFER_Y_UP;
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));
}
FRAGCOLOR = texture(INPUT, texcoord);
}
\endcode
\endtable
The Effect object's \c passes list should now specify both the vertex and
fragment snippets:
\qml
passes: Pass {
shaders: [
Shader {
stage: Shader.Vertex
shader: "effect.vert"
},
Shader {
stage: Shader.Fragment
shader: "effect.frag"
}
]
}
\endqml
The end result looks like the following:
\table 70%
\row
\li \image effect_intro_1.png
{Three different 3D objects}
\li \image effect_intro_3.png
{Warped view of three different 3D objects showing vertex shader
effect}
\endtable
\section1 Special keywords in effect shaders
\list
\li \c VARYING - Declares a vertex output or fragment input, depending on the type of the current shader.
\li \c MAIN - This function must always be present in an effect shader.
\li \c FRAGCOLOR - \c vec4 - The final fragment color; the output of the fragment shader. (fragment shader only)
\li \c POSITION - \c vec4 - The homogenous position calculated in the vertex shader. (vertex shader only)
\li \c MODELVIEWPROJECTION_MATRIX - \c mat4 - The transformation matrix for the screen quad.
\li \c VERTEX - \c vec3 - The vertices of the quad; the input to the vertex shader. (vertex shader only)
\li \c INPUT - \c sampler2D or \c sampler2DArray - The sampler for the input
texture with the scene rendered into it, unless a pass redirects its input
via a BufferInput object, in which case \c INPUT refers to the additional
color buffer's texture referenced by the BufferInput. With \l{Multiview
Rendering}{multiview rendering} enabled, which can be relevant for VR/AR
applications, this is a sampler2DArray, while the input texture becomes a 2D
texture array.
\li \c INPUT_UV - \c vec2 - UV coordinates for sampling \c INPUT.
\li \c TEXTURE_UV - \c vec2 - UV coordinates suitable for sampling a Texture
with contents loaded from an image file.
\li \c INPUT_SIZE - \c vec2 - The size of the \c INPUT texture, in pixels.
\li \c OUTPUT_SIZE - \c vec2 - The size of the output buffer, in
pixels. Often the same as \c INPUT_SIZE, unless the pass outputs to an extra
Buffer with a size multiplier on it.
\li \c FRAME - \c float - A frame counter, incremented after each frame in the View3D.
\li \c DEPTH_TEXTURE - \c sampler2D or \c sampler2DArray - A depth texture
with the depth buffer contents with the opaque objects in the scene. Like
with CustomMaterial, the presence of this keyword in the shader triggers
generating the depth texture automatically.
\li \c NORMAL_ROUGHNESS_TEXTURE - \c sampler2D - A texture with the
world-space normals and material roughness of the opaque objects in the
currently visible portion of the scene. Like with CustomMaterial, the
presence of this keyword in the shader implies an additional render pass to
generate the normal texture.
\li \c VIEW_INDEX - \c uint - With \l{Multiview Rendering}{multiview
rendering} enabled, this is the current view index, available in both vertex
and fragment shaders. Always 0 when multiview rendering is not used.
\li \c PROJECTION_MATRIX - \c mat4, the projection matrix. Note that with
\l{Multiview Rendering}{multiview rendering}, this is an array of matrices.
\li \c INVERSE_PROJECTION_MATRIX - \c mat4, the inverse projection matrix.
Note that with \l{Multiview Rendering}{multiview rendering}, this is an array
of matrices.
\li \c VIEW_MATRIX -> \c mat4, the view (camera) matrix.
Note that with \l{Multiview Rendering}{multiview rendering}, this is an array
of matrices.
\li float \c NDC_Y_UP - The value is \c 1 when the Y axis points up in
normalized device coordinate space, and \c{-1} when the Y axis points down.
Y pointing down is the case when rendering happens with Vulkan.
\li float \c FRAMEBUFFER_Y_UP - The value is \c 1 when the Y axis points up
in the coordinate system for framebuffers (textures), meaning \c{(0, 0)} is
the bottom-left corner. The value is \c{-1} when the Y axis points down,
\c{(0, 0)} being the top-left corner.
\li float \c NEAR_CLIP_VALUE - The value is \c -1 for when the clipping plane
range's starts at \c -1 and goes to \c 1. This is true when using OpenGL for
rendering. For other rendering backends the value of this property will be
\c 0 meaning the clipping plane range is \c 0 to \c 1.
\endlist
\section1 Building multi-pass effects
A multi-pass effect often uses more than one set of shaders, and takes the
\l{Pass::output}{output} and \l{Pass::commands}{commands} properties into
use. Each entry in the passes list translates to a render pass drawing a
quad into the pass's output texture, while sampling the effect's input texture
and optionally other textures as well.
The typical outline of a multi-pass Effect can look like the following:
\qml
passes: [
Pass {
shaders: [
Shader {
stage: Shader.Vertex
shader: "pass1.vert"
},
Shader {
stage: Shader.Fragment
shader: "pass1.frag"
}
// This pass outputs to the intermediate texture described
// by the Buffer object.
output: intermediateColorBuffer
],
},
Pass {
shaders: [
Shader {
stage: Shader.Vertex
shader: "pass2.vert"
},
Shader {
stage: Shader.Fragment
shader: "pass2.frag"
}
// The output of the last pass needs no redirection, it is
// the final result of the effect.
],
commands: [
// This pass reads from the intermediate texture, meaning
// INPUT in the shader will refer to the texture associated
// with the Buffer.
BufferInput {
buffer: intermediateColorBuffer
}
]
}
]
\endqml
What is \c intermediateColorBuffer?
\qml
Buffer {
id: intermediateColorBuffer
name: "tempBuffer"
// format: Buffer.RGBA8
// textureFilterOperation: Buffer.Linear
// textureCoordOperation: Buffer.ClampToEdge
}
\endqml
The commented properties are not necessary if the desired values match the
defaults.
Internally the presence of this Buffer object and referencing it from the \c
output property of a Pass leads to creating a texture with a size matching
the View3D, and so the size of the implicit input and output textures. When
this is not desired, the \l{Buffer::sizeMultiplier}{sizeMultiplier} property
can be used to get an intermediate texture with a different size. This can
lead to the \c INPUT_SIZE and \c OUTPUT_SIZE uniforms in the shader having
different values.
By default the Effect cannot count on textures preserving their contents
between frames. When a new intermediate texture is created, it is cleared to
\c{vec4(0.0)}. Afterwards, the same texture can be reused for another
purpose. Therefore, effect passes should always write to the entire texture,
without making assumptions about their content at the start of the pass.
There is an exception to this: Buffer objects with
\l{Buffer::bufferFlags}{bufferFlags} set to Buffer.SceneLifetime. This
indicates that the texture is permanently associated with a pass of the
effect and it will not be reused for other purposes. The contents of such
color buffers is preserved between frames. This is typically used in a
ping-pong fashion in effects like motion blur: the first pass takes the
persistent buffer as its input, in addition to the effects main input
texture, outputting to another intermediate buffer, while the second pass
outputs to the persistent buffer. This way in the first frame the first pass
samples an empty (transparent) texture, whereas in subsequent frames it
samples the output of the second pass from the previous frame. A third pass
can then blend the effect's input and the second pass' output together.
The BufferInput command type is used to expose custom texture buffers to the
render pass.
For instance, to access \c someBuffer in the render pass shaders under
the name, \c mySampler, the following can be added to its command list:
\qml
BufferInput { buffer: someBuffer; sampler: "mySampler" }
\endqml
If the \c sampler name is not specified, \c INPUT will be used as default.
Buffers can be useful to share intermediate results between render passes.
To expose preloaded textures to the effect, TextureInput should be used instead.
These can be defined as properties of the Effect itself, and will automatically
be accessible to the shaders by their property names.
\qml
property TextureInput tex: TextureInput {
texture: Texture { source: "image.png" }
}
\endqml
Here \c tex is a valid sampler in all shaders of all the passes of the
effect.
When it comes to uniform values from properties, all passes in the Effect
read the same values in their shaders. If necessary it is possible to
override the value of a uniform just for a given pass. This is achieved by
adding the \l SetUniformValue command to the list of commands for the pass.
\note The \l{SetUniformValue::target}{target} of the pass-specific uniform
value setter can only refer to a name that is the name of a property of the
effect. It can override the value for a property's corresponding uniform,
but it cannot introduce new uniforms.
\section1 Performance considerations
Be aware of the increased resource usage and potentially reduced performance
when using post-processing effects. Just like with Qt Quick layers and
ShaderEffect, rendering the scene into a texture and then using that to
texture a quad is not a cheap operation, especially on low-end hardware with
limited fragment processing power. The amount of additional graphics memory
needed, as well as the increase in GPU load both depend on the size of the
View3D (which, on embedded devices without a windowing system, may often be
as big as the screen resolution). Multi-pass effects, as well as applying
multiple effects increase the resource and performance requirements further.
Therefore, it is highly advisable to ensure early on in the development
lifecycle that the targeted device and graphics stack is able to cope with
the effects included in the design of the 3D scene at the final product's
screen resolution.
While unavoidable with techniques that need it, \c DEPTH_TEXTURE implies an
additional rendering pass to generate the contents of that texture, which
can also present a hit on less capable hardware. Therefore, use \c
DEPTH_TEXTURE in the effect's shaders only when essential.
The complexity of the operations in the shaders is also important. Just like
with CustomMaterial, a sub-optimal fragment shader can easily lead to
reduced rendering performance.
Be cautious with \l{Buffer::sizeMultiplier}{sizeMultiplier in Buffer} when
values larger than 1 are involved. For example, a multiplier of 4 means
creating and then rendering to a texture that is 4 times the size of the
View3D. Just like with shadow maps and multi- or supersampling, the
increased resource and performance costs can quickly outweigh the benefits
from better quality on systems with limited GPU power.
\section1 VR/AR considerations
When developing applications for virtual or augmented reality by using Qt
Quick 3D XR, postprocessing effects are functional and available to use.
However, designers are developers should take special care to understand
which and what kind of effects make sense in a virtual reality environment.
Some effects, including some of the built-in ones in
ExtendedSceneEnvironment or the deprecated Effects module, do not lead to a
good visual experience in a VR environment, possibly affecting the user
physically even (causing, for example, motion sickness or dizziness).
When the more efficient \l{Multiview Rendering}{multiview rendering mode} is
enabled in a VR/AR application, there is no separate render pass for the
left and right eye contents. Instead, it all happens in one pass, using a 2D
texture array with two layers instead of two independent 2D textures. This
also means that many intermediate buffers, meaning color or depth textures,
will need to become texture arrays in this mode. This then has implications
for custom materials and postprocessing effects. Textures such as the input
texture (\c INPUT) and the depth texture (\c DEPTH_TEXTURE) become 2D texture
arrays, exposed in the shader as a \c sampler2DArray instead of \c
sampler2D. This has implications for GLSL functions such as texture(),
textureLod(), or textureSize(). The UV coordinate is then a vec3, not a
vec2. Whereas textureSize() returns a vec3, not a vec2. Effects intended to
function regardless of the rendering mode, can be written with an
appropriate ifdef:
\badcode
#if QSHADER_VIEW_COUNT >= 2
vec4 c = texture(INPUT, vec3(INPUT_UV, VIEW_INDEX));
#else
vec4 c = texture(INPUT, INPUT_UV);
#endif
\endcode
It can also be useful to define macros that handle both cases. For example:
\badcode
#if QSHADER_VIEW_COUNT >= 2
#define SAMPLE_INPUT(uv) texture(INPUT, vec3(uv, VIEW_INDEX))
#define SAMPLE_DEPTH(uv) texture(DEPTH_TEXTURE, vec3(uv, VIEW_INDEX)).r
#define PROJECTION PROJECTION_MATRIX[VIEW_INDEX]
#define INVERSE_PROJECTION INVERSE_PROJECTION_MATRIX[VIEW_INDEX]
#else
#define SAMPLE_INPUT(uv) texture(INPUT, uv)
#define SAMPLE_DEPTH(uv) texture(DEPTH_TEXTURE, uv).r
#define PROJECTION PROJECTION_MATRIX
#define INVERSE_PROJECTION INVERSE_PROJECTION_MATRIX
#endif
\endcode
This does not apply to \c NORMAL_ROUGHNESS_TEXTURE that is always a 2D texture,
even when multiview rendering is active:
\badcode
#define SAMPLE_NORMAL(uv) normalize(texture(NORMAL_ROUGHNESS_TEXTURE, uv).rgb)
\endcode
\note The presence of keywords such as \c DEPTH_TEXTURE trigger additional
render passes, and uniforms such as \c INVERSE_PROJECTION_MATRIX are
calculated and set upon the presence of the keyword in the shader snippet
anywhere. This is more expensive, both when it comes to performance and
resource usage. Hence it is recommended to only add such #defines when the
textures and matrices will really be used in the effect.
\sa Shader, Pass, Buffer, BufferInput, {Qt Quick 3D - Custom Effect Example}
*/
/*!
\qmlproperty list Effect::passes
Contains a list of render \l {Pass}{passes} implemented by the effect.
*/
QQuick3DEffect::QQuick3DEffect(QQuick3DObject *parent)
: QQuick3DObject(*(new QQuick3DObjectPrivate(QQuick3DObjectPrivate::Type::Effect)), parent)
{
}
QQmlListProperty<QQuick3DShaderUtilsRenderPass> QQuick3DEffect::passes()
{
return QQmlListProperty<QQuick3DShaderUtilsRenderPass>(this,
nullptr,
QQuick3DEffect::qmlAppendPass,
QQuick3DEffect::qmlPassCount,
QQuick3DEffect::qmlPassAt,
QQuick3DEffect::qmlPassClear);
}
// Default vertex and fragment shader code that is used when no corresponding
// Shader is present in the Effect. These go through the usual processing so
// should use the user-facing builtins.
static const char *default_effect_vertex_shader =
"void MAIN()\n"
"{\n"
"}\n";
static const char *default_effect_fragment_shader =
"void MAIN()\n"
"{\n"
"#if QSHADER_VIEW_COUNT >= 2\n"
" FRAGCOLOR = texture(INPUT, vec3(INPUT_UV, VIEW_INDEX));\n"
"#else\n"
" FRAGCOLOR = texture(INPUT, INPUT_UV);\n"
"#endif\n"
"}\n";
static inline void insertVertexMainArgs(QByteArray &snippet)
{
static const char *argKey = "/*%QT_ARGS_MAIN%*/";
const int argKeyLen = int(strlen(argKey));
const int argKeyPos = snippet.indexOf(argKey);
if (argKeyPos >= 0)
snippet = snippet.left(argKeyPos) + QByteArrayLiteral("inout vec3 VERTEX") + snippet.mid(argKeyPos + argKeyLen);
}
static inline void resetShaderDependentEffectFlags(QSSGRenderEffect *effectNode)
{
effectNode->setFlag(QSSGRenderEffect::Flags::UsesDepthTexture, false);
effectNode->setFlag(QSSGRenderEffect::Flags::UsesProjectionMatrix, false);
effectNode->setFlag(QSSGRenderEffect::Flags::UsesInverseProjectionMatrix, false);
effectNode->setFlag(QSSGRenderEffect::Flags::UsesViewMatrix, false);
effectNode->setFlag(QSSGRenderEffect::Flags::UsesNormalTexture, false);
effectNode->setFlag(QSSGRenderEffect::Flags::UsesMotionVectorTexture, false);
}
static inline void accumulateEffectFlagsFromShader(QSSGRenderEffect *effectNode, const QSSGCustomShaderMetaData &meta)
{
if (meta.flags.testFlag(QSSGCustomShaderMetaData::UsesDepthTexture))
effectNode->setFlag(QSSGRenderEffect::Flags::UsesDepthTexture);
if (meta.flags.testFlag(QSSGCustomShaderMetaData::UsesProjectionMatrix))
effectNode->setFlag(QSSGRenderEffect::Flags::UsesProjectionMatrix);
if (meta.flags.testFlag(QSSGCustomShaderMetaData::UsesInverseProjectionMatrix))
effectNode->setFlag(QSSGRenderEffect::Flags::UsesInverseProjectionMatrix);
if (meta.flags.testFlag(QSSGCustomShaderMetaData::UsesViewMatrix))
effectNode->setFlag(QSSGRenderEffect::Flags::UsesViewMatrix);
if (meta.flags.testFlag(QSSGCustomShaderMetaData::UsesNormalTexture))
effectNode->setFlag(QSSGRenderEffect::Flags::UsesNormalTexture);
if (meta.flags.testFlag(QSSGCustomShaderMetaData::UsesMotionVectorTexture))
effectNode->setFlag(QSSGRenderEffect::Flags::UsesMotionVectorTexture);
}
QSSGRenderGraphObject *QQuick3DEffect::updateSpatialNode(QSSGRenderGraphObject *node)
{
using namespace QSSGShaderUtils;
const auto &renderContext = QQuick3DObjectPrivate::get(this)->sceneManager->wattached->rci();
if (!renderContext) {
qWarning("QQuick3DEffect: No render context interface?");
return nullptr;
}
QSSGRenderEffect *effectNode = static_cast<QSSGRenderEffect *>(node);
bool newBackendNode = false;
if (!effectNode) {
effectNode = new QSSGRenderEffect;
newBackendNode = true;
}
bool shadersOrBuffersMayChange = false;
if (m_dirtyAttributes & Dirty::EffectChainDirty)
shadersOrBuffersMayChange = true;
const bool fullUpdate = newBackendNode || effectNode->incompleteBuildTimeObject || (m_dirtyAttributes & Dirty::TextureDirty);
if (fullUpdate || shadersOrBuffersMayChange) {
markAllDirty();
// Need to clear the old list with properties and textures first.
effectNode->properties.clear();
effectNode->textureProperties.clear();
QMetaMethod propertyDirtyMethod;
const int idx = metaObject()->indexOfSlot("onPropertyDirty()");
if (idx != -1)
propertyDirtyMethod = metaObject()->method(idx);
// Properties -> uniforms
QSSGShaderCustomMaterialAdapter::StringPairList uniforms;
QSSGShaderCustomMaterialAdapter::StringPairList multiViewDependentSamplers;
const int propCount = metaObject()->propertyCount();
int propOffset = metaObject()->propertyOffset();
// Effect can have multilayered inheritance structure, so find the actual propOffset
const QMetaObject *superClass = metaObject()->superClass();
while (superClass && qstrcmp(superClass->className(), "QQuick3DEffect") != 0) {
propOffset = superClass->propertyOffset();
superClass = superClass->superClass();
}
using TextureInputProperty = QPair<QQuick3DShaderUtilsTextureInput *, const char *>;
QVector<TextureInputProperty> textureProperties; // We'll deal with these later
for (int i = propOffset; i != propCount; ++i) {
const QMetaProperty property = metaObject()->property(i);
if (Q_UNLIKELY(!property.isValid()))
continue;
const auto name = property.name();
QMetaType propType = property.metaType();
QVariant propValue = property.read(this);
if (propType == QMetaType(QMetaType::QVariant))
propType = propValue.metaType();
if (propType.id() >= QMetaType::User) {
if (propType.id() == qMetaTypeId<QQuick3DShaderUtilsTextureInput *>()) {
if (QQuick3DShaderUtilsTextureInput *texture = property.read(this).value<QQuick3DShaderUtilsTextureInput *>())
textureProperties.push_back({texture, name});
}
} else if (propType == QMetaType(QMetaType::QObjectStar)) {
if (QQuick3DShaderUtilsTextureInput *texture = qobject_cast<QQuick3DShaderUtilsTextureInput *>(propValue.value<QObject *>()))
textureProperties.push_back({texture, name});
} else {
const auto type = uniformType(propType);
if (type != QSSGRenderShaderValue::Unknown) {
uniforms.append({ uniformTypeName(propType), name });
effectNode->properties.push_back({ name, uniformTypeName(propType),
propValue, uniformType(propType), i});
// Track the property changes
if (fullUpdate) {
if (property.hasNotifySignal() && propertyDirtyMethod.isValid())
connect(this, property.notifySignal(), this, propertyDirtyMethod);
} // else already connected
} else {
// ### figure out how _not_ to warn when there are no dynamic
// properties defined (because warnings like Blah blah objectName etc. are not helpful)
//qWarning("No known uniform conversion found for effect property %s. Skipping", property.name());
}
}
}
const auto processTextureProperty = [&](QQuick3DShaderUtilsTextureInput &texture, const QByteArray &name) {
QSSGRenderEffect::TextureProperty texProp;
QQuick3DTexture *tex = texture.texture(); // may be null if the TextureInput has no 'texture' set
if (fullUpdate) {
connect(&texture, &QQuick3DShaderUtilsTextureInput::enabledChanged, this, &QQuick3DEffect::onTextureDirty);
connect(&texture, &QQuick3DShaderUtilsTextureInput::textureChanged, this, &QQuick3DEffect::onTextureDirty);
} // else already connected
texProp.name = name;
if (texture.enabled && tex)
texProp.texImage = tex->getRenderImage();
texProp.shaderDataType = QSSGRenderShaderValue::Texture;
if (tex) {
texProp.minFilterType = tex->minFilter() == QQuick3DTexture::Nearest ? QSSGRenderTextureFilterOp::Nearest
: QSSGRenderTextureFilterOp::Linear;
texProp.magFilterType = tex->magFilter() == QQuick3DTexture::Nearest ? QSSGRenderTextureFilterOp::Nearest
: QSSGRenderTextureFilterOp::Linear;
texProp.mipFilterType = tex->generateMipmaps() ? (tex->mipFilter() == QQuick3DTexture::Nearest ? QSSGRenderTextureFilterOp::Nearest
: QSSGRenderTextureFilterOp::Linear)
: QSSGRenderTextureFilterOp::None;
texProp.horizontalClampType = tex->horizontalTiling() == QQuick3DTexture::Repeat ? QSSGRenderTextureCoordOp::Repeat
: (tex->horizontalTiling() == QQuick3DTexture::ClampToEdge ? QSSGRenderTextureCoordOp::ClampToEdge
: QSSGRenderTextureCoordOp::MirroredRepeat);
texProp.verticalClampType = tex->verticalTiling() == QQuick3DTexture::Repeat ? QSSGRenderTextureCoordOp::Repeat
: (tex->verticalTiling() == QQuick3DTexture::ClampToEdge ? QSSGRenderTextureCoordOp::ClampToEdge
: QSSGRenderTextureCoordOp::MirroredRepeat);
texProp.zClampType = tex->depthTiling() == QQuick3DTexture::Repeat ? QSSGRenderTextureCoordOp::Repeat
: (tex->depthTiling() == QQuick3DTexture::ClampToEdge) ? QSSGRenderTextureCoordOp::ClampToEdge
: QSSGRenderTextureCoordOp::MirroredRepeat;
}
// Knowing upfront that a sampler2D needs to be a sampler2DArray in
// the multiview-compatible version of the shader is not trivial.
// Consider: we know the list of TextureInputs, without any
// knowledge about the usage of those textures. Intermediate buffers
// (textures) also have a default constructed (no source, no source
// item, no texture data) Texture set. What indicates that these are
// used as intermediate buffers, is the 'output' property of a Pass,
// referencing a Buffer object (which objects we otherwise do not
// track), the 'name' of which matches TextureInput property name.
// The list of passes may vary dynamically, and some Passes may not
// be listed at any point in time if the effect has an
// ubershader-ish design. Thus one can have TextureInputs that are
// not associated with a Buffer (when scanning through the Passes),
// and so we cannot just check the 'output'-referenced Buffers to
// decide if a TextureInput's Texture needs to be treated specially
// in the generated shader code. (and the type must be correct even
// for, from our perspective, "unused" samplers since they are still
// in the shader code, and will get a dummy texture bound)
//
// Therefore, in the absence of more sophisticated options, we just
// look at the TextureInput's texture, and if it is something along
// the lines of
// property TextureInput intermediateColorBuffer1: TextureInput { texture: Texture { } }
// then it is added to the special list, indicating the the type is
// sampler2D or sampler2DArray, depending on the rendering mode the
// shader is targeting.
if (tex && !tex->hasSourceData()) {
multiViewDependentSamplers.append({ QByteArrayLiteral("sampler2D"), name }); // the type may get adjusted later
} else {
if (tex && QQuick3DObjectPrivate::get(tex)->type == QQuick3DObjectPrivate::Type::ImageCube)
uniforms.append({ QByteArrayLiteral("samplerCube"), name });
else if (tex && tex->textureData() && tex->textureData()->depth() > 0)
uniforms.append({ QByteArrayLiteral("sampler3D"), name });
else
uniforms.append({ QByteArrayLiteral("sampler2D"), name });
}
effectNode->textureProperties.push_back(texProp);
};
// Textures
for (const auto &property : std::as_const(textureProperties))
processTextureProperty(*property.first, property.second);
if (effectNode->incompleteBuildTimeObject) { // This object came from the shadergen tool
const auto names = dynamicPropertyNames();
for (const auto &name : names) {
QVariant propValue = property(name.constData());
QMetaType propType = propValue.metaType();
if (propType == QMetaType(QMetaType::QVariant))
propType = propValue.metaType();
if (propType.id() >= QMetaType::User) {
if (propType.id() == qMetaTypeId<QQuick3DShaderUtilsTextureInput *>()) {
if (QQuick3DShaderUtilsTextureInput *texture = propValue.value<QQuick3DShaderUtilsTextureInput *>())
textureProperties.push_back({texture, name});
}
} else if (propType.id() == QMetaType::QObjectStar) {
if (QQuick3DShaderUtilsTextureInput *texture = qobject_cast<QQuick3DShaderUtilsTextureInput *>(propValue.value<QObject *>()))
textureProperties.push_back({texture, name});
} else {
const auto type = uniformType(propType);
if (type != QSSGRenderShaderValue::Unknown) {
uniforms.append({ uniformTypeName(propType), name });
effectNode->properties.push_back({ name, uniformTypeName(propType),
propValue, uniformType(propType), -1 /* aka. dynamic property */});
// We don't need to track property changes
} else {
// ### figure out how _not_ to warn when there are no dynamic
// properties defined (because warnings like Blah blah objectName etc. are not helpful)
qWarning("No known uniform conversion found for effect property %s. Skipping", name.constData());
}
}
}
for (const auto &property : std::as_const(textureProperties))
processTextureProperty(*property.first, property.second);
}
// built-ins
uniforms.append({ "mat4", "qt_modelViewProjection" });
uniforms.append({ "vec2", "qt_inputSize" });
uniforms.append({ "vec2", "qt_outputSize" });
uniforms.append({ "float", "qt_frame_num" });
uniforms.append({ "float", "qt_fps" });
uniforms.append({ "vec2", "qt_cameraProperties" });
uniforms.append({ "float", "qt_normalAdjustViewportFactor" });
uniforms.append({ "float", "qt_nearClipValue" });
uniforms.append({ "vec4", "qt_rhi_properties" });
// qt_inputTexture is not listed in uniforms, will be added by prepareCustomShader()
// since the name and type varies between non-multiview and multiview mode
QSSGShaderCustomMaterialAdapter::StringPairList builtinVertexInputs;
builtinVertexInputs.append({ "vec3", "attr_pos" });
builtinVertexInputs.append({ "vec2", "attr_uv" });
QSSGShaderCustomMaterialAdapter::StringPairList builtinVertexOutputs;
builtinVertexOutputs.append({ "vec2", "qt_inputUV" });
builtinVertexOutputs.append({ "vec2", "qt_textureUV" });
builtinVertexOutputs.append({ "flat uint", "qt_viewIndex" });
// fragOutput is added automatically by the program generator
resetShaderDependentEffectFlags(effectNode);
if (!m_passes.isEmpty()) {
const QQmlContext *context = qmlContext(this);
effectNode->resetCommands();
for (QQuick3DShaderUtilsRenderPass *pass : std::as_const(m_passes)) {
// Have a key composed more or less of the vertex and fragment filenames.
// The shaderLibraryManager uses stage+shaderPathKey as the key.
// Thus shaderPathKey is then sufficient to look up both the vertex and fragment shaders later on.
// Note that this key is not suitable as a unique key for the graphics resources because the same
// set of shader files can be used in multiple different passes, or in multiple active effects.
// But that's the effect system's problem.
QByteArray shaderPathKey("effect pipeline--");
QSSGRenderEffect::ShaderPrepPassData passData;
for (QQuick3DShaderUtilsShader::Stage stage : { QQuick3DShaderUtilsShader::Stage::Vertex, QQuick3DShaderUtilsShader::Stage::Fragment }) {
QQuick3DShaderUtilsShader *shader = nullptr;
for (QQuick3DShaderUtilsShader *s : pass->m_shaders) {
if (s->stage == stage) {
shader = s;
break;
}
}
// just how many enums does one need for the exact same thing...
QSSGShaderCache::ShaderType type = QSSGShaderCache::ShaderType::Vertex;
if (stage == QQuick3DShaderUtilsShader::Stage::Fragment)
type = QSSGShaderCache::ShaderType::Fragment;
// Will just use the custom material infrastructure. Some
// substitutions are common between custom materials and effects.
//
// Substitutions relevant to us here:
// MAIN -> qt_customMain
// FRAGCOLOR -> fragOutput
// POSITION -> gl_Position
// MODELVIEWPROJECTION_MATRIX -> qt_modelViewProjection
// DEPTH_TEXTURE -> qt_depthTexture
// ... other things shared with custom material
//
// INPUT -> qt_inputTexture
// INPUT_UV -> qt_inputUV
// ... other effect specifics
//
// Built-in uniforms, inputs and outputs will be baked into
// metadata comment blocks in the resulting source code.
// Same goes for inputs/outputs declared with VARYING.
QByteArray code;
if (shader) {
code = QSSGShaderUtils::resolveShader(shader->shader, context, shaderPathKey); // appends to shaderPathKey
} else {
if (!shaderPathKey.isEmpty())
shaderPathKey.append('>');
shaderPathKey += "DEFAULT";
if (type == QSSGShaderCache::ShaderType::Vertex)
code = default_effect_vertex_shader;
else
code = default_effect_fragment_shader;
}
for (auto pathKeyIndex : { QSSGRenderCustomMaterial::RegularShaderPathKeyIndex, QSSGRenderCustomMaterial::MultiViewShaderPathKeyIndex }) {
QSSGShaderCustomMaterialAdapter::ShaderCodeAndMetaData result;
QSSGShaderCustomMaterialAdapter::CustomShaderPrepWorkData scratch;
QSSGShaderCustomMaterialAdapter::beginPrepareCustomShader(
&scratch,
&result,
code,
type,
pathKeyIndex == QSSGRenderCustomMaterial::RegularShaderPathKeyIndex ? false : true);
QSSGShaderCustomMaterialAdapter::StringPairList multiViewDependentUniforms;
if (result.second.flags.testFlag(QSSGCustomShaderMetaData::UsesProjectionMatrix)
|| result.second.flags.testFlag(QSSGCustomShaderMetaData::UsesInverseProjectionMatrix))
{
multiViewDependentUniforms.append({ "mat4", "qt_projectionMatrix" });
multiViewDependentUniforms.append({ "mat4", "qt_inverseProjectionMatrix" });
}
multiViewDependentUniforms.append({ "mat4", "qt_viewMatrix" });
accumulateEffectFlagsFromShader(effectNode, result.second);
QSSGShaderCustomMaterialAdapter::finishPrepareCustomShader(
&result.first, // effectively appends the QQ3D_SHADER_META block
scratch,
result,
type,
pathKeyIndex == QSSGRenderCustomMaterial::RegularShaderPathKeyIndex ? false : true,
uniforms,
type == QSSGShaderCache::ShaderType::Vertex ? builtinVertexInputs : builtinVertexOutputs,
type == QSSGShaderCache::ShaderType::Vertex ? builtinVertexOutputs : QSSGShaderCustomMaterialAdapter::StringPairList(),
multiViewDependentSamplers,
multiViewDependentUniforms);
if (type == QSSGShaderCache::ShaderType::Vertex) {
// qt_customMain() has an argument list which gets injected here
insertVertexMainArgs(result.first);
passData.vertexShaderCode[pathKeyIndex] = result.first;
passData.vertexMetaData[pathKeyIndex] = result.second;
} else {
passData.fragmentShaderCode[pathKeyIndex] = result.first;
passData.fragmentMetaData[pathKeyIndex] = result.second;
}
}
}
effectNode->commands.push_back(nullptr); // will be changed to QSSGBindShader in finalizeShaders
passData.bindShaderCmdIndex = effectNode->commands.size() - 1;
// finalizing the shader code happens in a separate step later on by the backend node
passData.shaderPathKeyPrefix = shaderPathKey;
effectNode->shaderPrepData.passes.append(passData);
effectNode->shaderPrepData.valid = true; // trigger reprocessing the shader code later on
effectNode->commands.push_back(new QSSGApplyInstanceValue);
// Buffers
QQuick3DShaderUtilsBuffer *outputBuffer = pass->outputBuffer;
if (outputBuffer) {
const QByteArray &outBufferName = outputBuffer->name;
if (outBufferName.isEmpty()) {
// default output buffer (with settings)
auto outputFormat = QQuick3DShaderUtilsBuffer::mapTextureFormat(outputBuffer->format());
effectNode->commands.push_back(new QSSGBindTarget(outputFormat));
effectNode->outputFormat = outputFormat;
} else {
// Allocate buffer command
effectNode->commands.push_back(outputBuffer->cloneCommand());
connect(outputBuffer, &QQuick3DShaderUtilsBuffer::changed, this, &QQuick3DEffect::onPassDirty, Qt::UniqueConnection);
// bind buffer
effectNode->commands.push_back(new QSSGBindBuffer(outBufferName));
}
} else {
// Use the default output buffer, same format as the source buffer
effectNode->commands.push_back(new QSSGBindTarget(QSSGRenderTextureFormat::Unknown));
effectNode->outputFormat = QSSGRenderTextureFormat::Unknown;
}
// Other commands (BufferInput, Blending ... )
const auto &extraCommands = pass->m_commands;
for (const auto &command : extraCommands) {
const int bufferCount = command->bufferCount();
for (int i = 0; i != bufferCount; ++i) {
effectNode->commands.push_back(command->bufferAt(i)->cloneCommand());
connect(command->bufferAt(i), &QQuick3DShaderUtilsBuffer::changed, this, &QQuick3DEffect::onPassDirty, Qt::UniqueConnection);
}
effectNode->commands.push_back(command->cloneCommand());
}
effectNode->commands.push_back(new QSSGRender);
}
}
}
if (m_dirtyAttributes & Dirty::PropertyDirty) {
for (const auto &prop : std::as_const(effectNode->properties)) {
auto p = metaObject()->property(prop.pid);
if (Q_LIKELY(p.isValid()))
prop.value = p.read(this);
}
}
m_dirtyAttributes = 0;
DebugViewHelpers::ensureDebugObjectName(effectNode, this);
return effectNode;
}
void QQuick3DEffect::onPropertyDirty()
{
markDirty(Dirty::PropertyDirty);
}
void QQuick3DEffect::onTextureDirty()
{
markDirty(Dirty::TextureDirty);
}
void QQuick3DEffect::onPassDirty()
{
// changed() signals from not just Passes but Buffers are also hooked up to this.
// Any property change should lead to re-evaluating the whole effect in sync.
markDirty(Dirty::EffectChainDirty);
}
void QQuick3DEffect::effectChainDirty()
{
markDirty(Dirty::EffectChainDirty);
}
void QQuick3DEffect::markDirty(QQuick3DEffect::Dirty type)
{
if (!(m_dirtyAttributes & quint32(type))) {
m_dirtyAttributes |= quint32(type);
update();
}
}
void QQuick3DEffect::updateSceneManager(QQuick3DSceneManager *sceneManager)
{
if (sceneManager) {
for (const auto &it : std::as_const(m_dynamicTextureMaps)) {
if (auto tex = it->texture())
QQuick3DObjectPrivate::refSceneManager(tex, *sceneManager);
}
} else {
for (const auto &it : std::as_const(m_dynamicTextureMaps)) {
if (auto tex = it->texture())
QQuick3DObjectPrivate::derefSceneManager(tex);
}
}
}
void QQuick3DEffect::itemChange(QQuick3DObject::ItemChange change, const QQuick3DObject::ItemChangeData &value)
{
if (change == QQuick3DObject::ItemSceneChange)
updateSceneManager(value.sceneManager);
}
void QQuick3DEffect::qmlAppendPass(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list, QQuick3DShaderUtilsRenderPass *pass)
{
if (!pass)
return;
QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
that->m_passes.push_back(pass);
connect(pass, &QQuick3DShaderUtilsRenderPass::changed, that, &QQuick3DEffect::onPassDirty);
that->effectChainDirty();
}
QQuick3DShaderUtilsRenderPass *QQuick3DEffect::qmlPassAt(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list, qsizetype index)
{
QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
return that->m_passes.at(index);
}
qsizetype QQuick3DEffect::qmlPassCount(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list)
{
QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
return that->m_passes.size();
}
void QQuick3DEffect::qmlPassClear(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list)
{
QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
for (QQuick3DShaderUtilsRenderPass *pass : std::as_const(that->m_passes))
pass->disconnect(that);
that->m_passes.clear();
that->effectChainDirty();
}
void QQuick3DEffect::setDynamicTextureMap(QQuick3DShaderUtilsTextureInput *textureMap)
{
// There can only be one texture input per property, as the texture input is a combination
// of the texture used and the uniform name!
auto it = m_dynamicTextureMaps.constFind(textureMap);
if (it == m_dynamicTextureMaps.constEnd()) {
// Track the object, if it's destroyed we need to remove it from our table.
connect(textureMap, &QQuick3DShaderUtilsTextureInput::destroyed, this, [this, textureMap]() {
auto it = m_dynamicTextureMaps.constFind(textureMap);
if (it != m_dynamicTextureMaps.constEnd())
m_dynamicTextureMaps.erase(it);
});
m_dynamicTextureMaps.insert(textureMap);
update();
}
}
QT_END_NAMESPACE