// Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qquick3dtexture_p.h" #include #include #include #include #include #include #include #include #include #include "qquick3dobject_p.h" #include "qquick3dscenemanager_p.h" #include "qquick3dutils_p.h" #include "qquick3drenderextensions.h" #include "qquick3dviewport_p.h" QT_BEGIN_NAMESPACE /*! \qmltype Texture \inherits Object3D \inqmlmodule QtQuick3D \brief Defines a texture for use in 3D scenes. A texture is technically any array of pixels (1D, 2D or 3D) and its related settings, such as minification and magnification filters, scaling and UV transformations. The Texture type in Qt Quick 3D represents a two-dimensional image. Its use is typically to map onto / wrap around three-dimensional geometry to emulate additional detail which cannot be efficiently modelled in 3D. It can also be used to emulate other lighting effects, such as reflections. While Texture itself always represents a 2D texture, other kinds of textures are available as well via subclasses of Texture. For example, to create a cube map texture with 6 faces, use the \l CubeMapTexture type. When the geometry is being rendered, each location on its surface will be transformed to a corresponding location in the texture by transforming and interpolating the UV coordinates (texture coordinate) that have been set for the mesh's vertexes. The fragment shader program that is being used to render the active material will then typically sample the material's texture(s) at the given coordinates and use the sampled data in its light calculations. \note A Material may use multiple textures to give the desired interaction with light in the 3D scene. It can represent the color of each texel on the geometry surface, but also other attributes of the surface. For instance, a "normal map" can represent the deviation from the geometry normals for each texel on the surface, emulating light interaction with finer details on the surface, such as cracks or bumps. See \l{Qt Quick 3D - Principled Material Example}{the principled material example} for a demonstration of a material with multiple texture maps. Texture objects can source image data from: \list \li an image or texture file by using the \l source property, \li a Qt Quick \l Item by using the sourceItem property, \li or by setting the \l textureData property to a \l TextureData item subclass for defining the custom texture contents. \endlist The following example maps the image "madewithqt.png" onto the default sphere mesh, and scales the UV coordinates to tile the image on the sphere surface. \qml Model { source: "#Sphere" materials: [ PrincipledMaterial { baseColorMap: Texture { source: "madewithqt.png" scaleU: 4.0 scaleV: 4.0 } } ] } \endqml The result looks as follows: \table \header \li Original image \li Mapped onto a sphere \row \li \image madewithqt.png \li \image spheremap.png \endtable \sa {Qt Quick 3D - Procedural Texture Example} */ QQuick3DTexture::QQuick3DTexture(QQuick3DObject *parent) : QQuick3DTexture(*(new QQuick3DObjectPrivate(QQuick3DObjectPrivate::Type::Image2D)), parent) { } QQuick3DTexture::QQuick3DTexture(QQuick3DObjectPrivate &dd, QQuick3DObject *parent) : QQuick3DObject(dd, parent) { } QQuick3DTexture::~QQuick3DTexture() { if (m_layer) { if (m_sceneManagerForLayer) m_sceneManagerForLayer->qsgDynamicTextures.removeAll(m_layer); m_layer->deleteLater(); // uhh... } if (m_sourceItem) { QQuickItemPrivate *sourcePrivate = QQuickItemPrivate::get(m_sourceItem); sourcePrivate->removeItemChangeListener(this, QQuickItemPrivate::Geometry); } } /*! \qmlproperty url QtQuick3D::Texture::source This property holds the location of an image or texture file containing the data used by the texture. The property is a URL, with the same rules as other source properties, such as \l{Image::source}{Image.source}. With Texture, only the \c qrc and \c file schemes are supported. When no scheme is present and the value is a relative path, it is assumed to be relative to the component's (i.e. the \c{.qml} file's) location. The source file can have any conventional image file format \l{QImageReader::supportedImageFormats()}{supported by Qt}. In addition, Texture supports the same \l [QtQuick]{Compressed Texture Files}{compressed texture file types} as QtQuick::Image. \note Texture data read from image files such as .png or .jpg involves storing the rows of pixels within the texture in an order defined the Qt Quick 3D rendering engine. When the source file is a container for - possibly compressed - texture data, such transformations cannot happen on the pixel data level. Examples of this are .ktx or .pkm files. Instead, the Texture implicitly enables vertical flipping in the fragment shader code in order to get identical on-screen results. This is controlled by the \l autoOrientation property and can be disabled, if desired. \note Some texture compression tools may apply automatic vertical mirroring (flipping) on the image data. In modern tools this is often an opt-in setting. It is important to be aware of the settings used in the asset conditioning pipeline, because an unexpectedly flipped texture, and thus incorrect texturing of objects, can have its root cause in the asset itself, outside the application's and rendering engine's control. When the asset requires it, applications can always set the \l flipV property themselves. \sa sourceItem, textureData, autoOrientation, flipV */ QUrl QQuick3DTexture::source() const { return m_source; } /*! \qmlproperty Item QtQuick3D::Texture::sourceItem This property defines a Item to be used as the source of the texture. Using this property allows any 2D Qt Quick content to be used as a texture source by rendering that item as an offscreen layer. If the item is a \l{QQuickItem::textureProvider()}{texture provider}, no additional texture is used. If this property is set, then the value of \l source will be ignored. A Texture should use one method to provide image data, and set only one of source, \l sourceItem, or \l textureData. \note Currently input events are forwarded to the Item used as a texture source only if the user is limited to interacting with one sourceItem instance at a time. In other words: you can share the same Item between multiple Textures, but then you cannot have multi-touch interaction with the same item on multiple textures at the same time. So it's best to use a separate 2D subscene instance for each Texture instance, if you expect to manipulate interactive items inside. \note Using this property in a Texture that is referenced from multiple windows is strongly discouraged. This includes usage via \l{View3D::importScene}. As the source texture created by this property is only accessible by one render thread, attempting to share it between multiple QQuickWindow instances is going to fail, unless the \c basic render loop of Qt Quick is used instead of the default \c threaded one. See \l{Qt Quick Scene Graph} on more information about the Qt Quick render loops. \note A Texture that contains the results of a Qt Quick offscreen render pass will in effect have an Y axis orientation that is different from what a Texture that receives its content via the source property uses. When used in combination with DefaultMaterial or PrincipledMaterial, this is all transparent to the application as the necessary UV transformations are applied automatically as long as the autoOrientation property is set to true, and so no further action is needed, regardless of how the texture was sourced. However, when developing \l{QtQuick3D::CustomMaterial}{custom materials} this needs to be kept in mind by the shader code author when sampling the texture and working with UV coordinates. \sa source, textureData, autoOrientation */ QQuickItem *QQuick3DTexture::sourceItem() const { return m_sourceItem; } /*! \qmlproperty real QtQuick3D::Texture::scaleU This property defines how to scale the U texture coordinate when mapping to a mesh's UV coordinates. Scaling the U value when using horizontal tiling will define how many times the texture is repeated from left to right. The default is 1.0. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa tilingModeHorizontal */ float QQuick3DTexture::scaleU() const { return m_scaleU; } /*! \qmlproperty real QtQuick3D::Texture::scaleV This property defines how to scale the V texture coordinate when mapping to a mesh's UV coordinates. Scaling the V value when using vertical tiling will define how many times a texture is repeated from bottom to top. The default is 1.0. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa tilingModeVertical */ float QQuick3DTexture::scaleV() const { return m_scaleV; } /*! \qmlproperty enumeration QtQuick3D::Texture::mappingMode This property defines which method of mapping to use when sampling this texture. \value Texture.UV The default value. Suitable for base color, diffuse, opacity, and most other texture maps. Performs standard UV mapping. The same portion of the image will always appear on the same vertex, unless the UV coordinates are transformed and animated. \value Texture.Environment Used for \l{PrincipledMaterial::specularReflectionMap}{specular reflection}, this causes the image to be projected onto the material as though it was being reflected. Using this mode for other type of texture maps provides a mirror effect. \value Texture.LightProbe The default for HDRI sphere maps used by light probes. This mode does not need to be manually set for Texture objects associated with the \l{SceneEnvironment::lightProbe}{lightProbe} property, because it is implied automatically. */ QQuick3DTexture::MappingMode QQuick3DTexture::mappingMode() const { return m_mappingMode; } /*! \qmlproperty enumeration QtQuick3D::Texture::tilingModeHorizontal Controls how the texture is mapped when the U scaling value is greater than 1. By default, this property is set to \c{Texture.Repeat}. \value Texture.ClampToEdge Texture is not tiled, but the value on the edge is used instead. \value Texture.MirroredRepeat Texture is repeated and mirrored over the X axis. \value Texture.Repeat Texture is repeated over the X axis. \sa scaleU */ QQuick3DTexture::TilingMode QQuick3DTexture::horizontalTiling() const { return m_tilingModeHorizontal; } /*! \qmlproperty enumeration QtQuick3D::Texture::tilingModeVertical This property controls how the texture is mapped when the V scaling value is greater than 1. By default, this property is set to \c{Texture.Repeat}. \value Texture.ClampToEdge Texture is not tiled, but the value on the edge is used instead. \value Texture.MirroredRepeat Texture is repeated and mirrored over the Y axis. \value Texture.Repeat Texture is repeated over the Y axis. \sa scaleV */ QQuick3DTexture::TilingMode QQuick3DTexture::verticalTiling() const { return m_tilingModeVertical; } /*! \qmlproperty enumeration QtQuick3D::Texture::tilingModeDepth This property controls how the texture is mapped when the Z scaling value is greater than 1. By default, this property is set to \c{Texture.Repeat}. \value Texture.ClampToEdge Texture is not tiled, but the value on the edge is used instead. \value Texture.MirroredRepeat Texture is repeated and mirrored over the Z axis. \value Texture.Repeat Texture is repeated over the Z axis. */ QQuick3DTexture::TilingMode QQuick3DTexture::depthTiling() const { return m_tilingModeDepth; } /*! \qmlproperty real QtQuick3D::Texture::rotationUV This property rotates the texture around the pivot point. This is defined using euler angles and for a positive value rotation is clockwise. The default is 0.0. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa pivotU, pivotV */ float QQuick3DTexture::rotationUV() const { return m_rotationUV; } /*! \qmlproperty real QtQuick3D::Texture::positionU This property offsets the U coordinate mapping from left to right. The default is 0.0. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa positionV */ float QQuick3DTexture::positionU() const { return m_positionU; } /*! \qmlproperty real QtQuick3D::Texture::positionV This property offsets the V coordinate mapping from bottom to top. The default is 0.0. \note Qt Quick 3D uses OpenGL-style vertex data, regardless of the graphics API used at run time. The UV position \c{(0, 0)} is therefore referring to the bottom-left corner of the image data. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa positionU */ float QQuick3DTexture::positionV() const { return m_positionV; } /*! \qmlproperty real QtQuick3D::Texture::pivotU This property sets the pivot U position which is used when applying a \l{QtQuick3D::Texture::rotationUV}{rotationUV}. The default is 0.0. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa rotationUV */ float QQuick3DTexture::pivotU() const { return m_pivotU; } /*! \qmlproperty real QtQuick3D::Texture::pivotV This property sets the pivot V position which is used when applying a \l{QtQuick3D::Texture::rotationUV}{rotationUV}. The default is 0.0. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa pivotU, rotationUV */ float QQuick3DTexture::pivotV() const { return m_pivotV; } /*! \qmlproperty bool QtQuick3D::Texture::flipU This property sets the use of the horizontally flipped texture coordinates. The default is false. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa flipV */ bool QQuick3DTexture::flipU() const { return m_flipU; } /*! \qmlproperty bool QtQuick3D::Texture::flipV This property sets the use of the vertically flipped texture coordinates. The default is false. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \sa flipU */ bool QQuick3DTexture::flipV() const { return m_flipV; } /*! \qmlproperty int QtQuick3D::Texture::indexUV This property sets the UV coordinate index used by this texture. Since QtQuick3D supports 2 UV sets(0 or 1) for now, the value will be saturated to the range. The default is 0. */ int QQuick3DTexture::indexUV() const { return m_indexUV; } /*! \qmlproperty enumeration QtQuick3D::Texture::magFilter This property determines how the texture is sampled when it is "magnified", i.e. a texel covers \e more than one pixel in screen space. The default value is \c{Texture.Linear}. \value Texture.Nearest uses the value of the closest texel. \value Texture.Linear takes the four closest texels and bilinearly interpolates them. \note Using \c Texture.None here will default to \c Texture.Linear instead. \sa minFilter, mipFilter */ QQuick3DTexture::Filter QQuick3DTexture::magFilter() const { return m_magFilter; } /*! \qmlproperty enumeration QtQuick3D::Texture::minFilter This property determines how the texture is sampled when it is "minimized", i.e. a texel covers \e less than one pixel in screen space. The default value is \c{Texture.Linear}. \value Texture.Nearest uses the value of the closest texel. \value Texture.Linear takes the four closest texels and bilinearly interpolates them. \note Using \c Texture.None here will default to \c Texture.Linear instead. \sa magFilter, mipFilter */ QQuick3DTexture::Filter QQuick3DTexture::minFilter() const { return m_minFilter; } /*! \qmlproperty enumeration QtQuick3D::Texture::mipFilter This property determines how the texture mipmaps are sampled when a texel covers less than one pixel. The default value is \c{Texture.None}. \value Texture.None disables the usage of mipmap sampling. \value Texture.Nearest uses mipmapping and samples the value of the closest texel. \value Texture.Linear uses mipmapping and interpolates between multiple texel values. \note This property will have no effect on Textures that do not have mipmaps. \sa minFilter, magFilter */ QQuick3DTexture::Filter QQuick3DTexture::mipFilter() const { return m_mipFilter; } /*! \qmlproperty TextureData QtQuick3D::Texture::textureData This property holds a reference to a \l TextureData component which defines the contents and properties of raw texture data. If this property is used, then the value of \l source will be ignored. A Texture should use one method to provide image data, and set only one of source, \l sourceItem, or \l textureData. \sa source, sourceItem, {Qt Quick 3D - Procedural Texture Example} */ QQuick3DTextureData *QQuick3DTexture::textureData() const { return m_textureData; } /*! \qmlproperty bool QtQuick3D::Texture::generateMipmaps This property determines if mipmaps are generated for textures that do not provide mipmap levels themselves. Using mipmaps along with mip filtering gives better visual quality when viewing textures at a distance compared rendering without them, but it may come at a performance cost (both when initializing the image and during rendering). By default, this property is set to false. \note It is necessary to set a \l{QtQuick3D::Texture::mipFilter}{mipFilter} mode for the generated mipmaps to be be used. \note This property is not applicable when the texture content is based on a Qt Quick item referenced by the \l sourceItem property. Mipmap generation for dynamic textures is not feasible due to the performance implications. Therefore, the value of this property is ignored for such textures. \sa mipFilter */ bool QQuick3DTexture::generateMipmaps() const { return m_generateMipmaps; } /*! \qmlproperty bool QtQuick3D::Texture::autoOrientation This property determines if a texture transformation, such as flipping the V texture coordinate, is applied automatically for textures where this is typically relevant. By default, this property is set to true. Certain type of texture data, such as compressed textures loaded via the \l source property from a .ktx or .pkm file, or textures generated by rendering a Qt Quick scene via the \l sourceItem property, often have a different Y axis orientation when compared to textures loaded from image files, such as, .png or .jpg. Therefore, such a Texture would appear "upside down" compared to a Texture with its source set to a regular image file. To remedy this, any qualifying Texture gets an implicit UV transformation as if the flipV property was set to true. If this is not desired, set this property to false. \note This property is effective when the Texture is used in combination with a DefaultMaterial or PrincipledMaterial. \l{QtQuick3D::CustomMaterial}{Custom materials} provide their own shader code, and so transformations such as the one configured by this property are ignored and are up to the application-provided shader code to implement. \since 6.2 \sa flipV */ bool QQuick3DTexture::autoOrientation() const { return m_autoOrientation; } /*! \qmlproperty RenderExtension QtQuick3D::Texture::textureProvider This property holds the RenderExtension that will provide the \l QRhiTexture that will be used by this item. \note The texture created by RenderExtension needs to be made available by \l{QSSGRenderExtensionHelpers::registerRenderResult}{registering} it with the engine. \since 6.7 \sa RenderExtension, QSSGRenderExtensionHelpers */ QQuick3DRenderExtension *QQuick3DTexture::textureProvider() const { return m_renderExtension; } void QQuick3DTexture::setSource(const QUrl &source) { if (m_source == source) return; m_source = source; m_dirtyFlags.setFlag(DirtyFlag::SourceDirty); m_dirtyFlags.setFlag(DirtyFlag::SourceItemDirty); m_dirtyFlags.setFlag(DirtyFlag::TextureDataDirty); emit sourceChanged(); update(); } void QQuick3DTexture::trySetSourceParent() { if (m_sourceItem->parentItem() && m_sourceItemRefed) return; auto *sourcePrivate = QQuickItemPrivate::get(m_sourceItem); if (!m_sourceItem->parentItem()) { if (const auto &manager = QQuick3DObjectPrivate::get(this)->sceneManager) { if (auto *window = manager->window()) { if (m_sourceItemRefed) { // Item was already refed but probably with hide set to false... // so we need to deref before we ref again below. const bool hide = m_sourceItemReparented; sourcePrivate->derefFromEffectItem(hide); m_sourceItemRefed = false; } m_sourceItem->setParentItem(window->contentItem()); m_sourceItemReparented = true; update(); } } } if (!m_sourceItemRefed) { const bool hide = m_sourceItemReparented; sourcePrivate->refFromEffectItem(hide); } } void QQuick3DTexture::setSourceItem(QQuickItem *sourceItem) { if (m_sourceItem == sourceItem) return; disconnect(m_textureProviderConnection); disconnect(m_textureUpdateConnection); if (m_sourceItem) { QQuickItemPrivate *sourcePrivate = QQuickItemPrivate::get(m_sourceItem); const bool hide = m_sourceItemReparented; sourcePrivate->derefFromEffectItem(hide); m_sourceItemRefed = false; sourcePrivate->removeItemChangeListener(this, QQuickItemPrivate::Geometry); disconnect(m_sourceItem, SIGNAL(destroyed(QObject*)), this, SLOT(sourceItemDestroyed(QObject*))); if (m_sourceItemReparented) { m_sourceItem->setParentItem(nullptr); m_sourceItemReparented = false; } } m_sourceItem = sourceItem; if (sourceItem) { trySetSourceParent(); QQuickItemPrivate *sourcePrivate = QQuickItemPrivate::get(m_sourceItem); sourcePrivate->addItemChangeListener(this, QQuickItemPrivate::Geometry); connect(m_sourceItem, SIGNAL(destroyed(QObject*)), this, SLOT(sourceItemDestroyed(QObject*))); sourcePrivate->ensureSubsceneDeliveryAgent(); } if (m_layer) { const auto &manager = QQuick3DObjectPrivate::get(this)->sceneManager; manager->qsgDynamicTextures.removeAll(m_layer); m_sceneManagerForLayer = nullptr; // cannot touch m_layer here } m_initializedSourceItem = nullptr; m_initializedSourceItemSize = QSize(); m_dirtyFlags.setFlag(DirtyFlag::SourceDirty); m_dirtyFlags.setFlag(DirtyFlag::SourceItemDirty); m_dirtyFlags.setFlag(DirtyFlag::TextureDataDirty); emit sourceItemChanged(); update(); } void QQuick3DTexture::setScaleU(float scaleU) { if (qFuzzyCompare(m_scaleU, scaleU)) return; m_scaleU = scaleU; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit scaleUChanged(); update(); } void QQuick3DTexture::setScaleV(float scaleV) { if (qFuzzyCompare(m_scaleV, scaleV)) return; m_scaleV = scaleV; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit scaleVChanged(); update(); } void QQuick3DTexture::setMappingMode(QQuick3DTexture::MappingMode mappingMode) { if (m_mappingMode == mappingMode) return; m_mappingMode = mappingMode; emit mappingModeChanged(); update(); } void QQuick3DTexture::setHorizontalTiling(QQuick3DTexture::TilingMode tilingModeHorizontal) { if (m_tilingModeHorizontal == tilingModeHorizontal) return; m_tilingModeHorizontal = tilingModeHorizontal; emit horizontalTilingChanged(); update(); } void QQuick3DTexture::setVerticalTiling(QQuick3DTexture::TilingMode tilingModeVertical) { if (m_tilingModeVertical == tilingModeVertical) return; m_tilingModeVertical = tilingModeVertical; emit verticalTilingChanged(); update(); } void QQuick3DTexture::setDepthTiling(QQuick3DTexture::TilingMode tilingModeDepth) { if (m_tilingModeDepth == tilingModeDepth) return; m_tilingModeDepth = tilingModeDepth; emit depthTilingChanged(); update(); } void QQuick3DTexture::setRotationUV(float rotationUV) { if (qFuzzyCompare(m_rotationUV, rotationUV)) return; m_rotationUV = rotationUV; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit rotationUVChanged(); update(); } void QQuick3DTexture::setPositionU(float positionU) { if (qFuzzyCompare(m_positionU, positionU)) return; m_positionU = positionU; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit positionUChanged(); update(); } void QQuick3DTexture::setPositionV(float positionV) { if (qFuzzyCompare(m_positionV, positionV)) return; m_positionV = positionV; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit positionVChanged(); update(); } void QQuick3DTexture::setPivotU(float pivotU) { if (qFuzzyCompare(m_pivotU, pivotU)) return; m_pivotU = pivotU; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit pivotUChanged(); update(); } void QQuick3DTexture::setPivotV(float pivotV) { if (qFuzzyCompare(m_pivotV, pivotV)) return; m_pivotV = pivotV; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit pivotVChanged(); update(); } void QQuick3DTexture::setFlipU(bool flipU) { if (m_flipU == flipU) return; m_flipU = flipU; m_dirtyFlags.setFlag(DirtyFlag::TransformDirty); emit flipUChanged(); update(); } void QQuick3DTexture::setFlipV(bool flipV) { if (m_flipV == flipV) return; m_flipV = flipV; m_dirtyFlags.setFlag(DirtyFlag::FlipVDirty); emit flipVChanged(); update(); } void QQuick3DTexture::setIndexUV(int indexUV) { if (m_indexUV == indexUV) return; if (indexUV < 0) m_indexUV = 0; else if (indexUV > 1) m_indexUV = 1; else m_indexUV = indexUV; m_dirtyFlags.setFlag(DirtyFlag::IndexUVDirty); emit indexUVChanged(); update(); } void QQuick3DTexture::setTextureData(QQuick3DTextureData *textureData) { if (m_textureData == textureData) return; // Make sure to disconnect if the geometry gets deleted out from under us QQuick3DObjectPrivate::attachWatcher(this, &QQuick3DTexture::setTextureData, textureData, m_textureData); if (m_textureData) QObject::disconnect(m_textureDataConnection); m_textureData = textureData; if (m_textureData) { m_textureDataConnection = QObject::connect(m_textureData, &QQuick3DTextureData::textureDataNodeDirty, this, [this]() { markDirty(DirtyFlag::TextureDataDirty); }); } m_dirtyFlags.setFlag(DirtyFlag::SourceDirty); m_dirtyFlags.setFlag(DirtyFlag::SourceItemDirty); m_dirtyFlags.setFlag(DirtyFlag::TextureDataDirty); emit textureDataChanged(); update(); } void QQuick3DTexture::setGenerateMipmaps(bool generateMipmaps) { if (m_generateMipmaps == generateMipmaps) return; m_generateMipmaps = generateMipmaps; m_dirtyFlags.setFlag(DirtyFlag::SamplerDirty); emit generateMipmapsChanged(); update(); } void QQuick3DTexture::setAutoOrientation(bool autoOrientation) { if (m_autoOrientation == autoOrientation) return; m_autoOrientation = autoOrientation; m_dirtyFlags.setFlag(DirtyFlag::FlipVDirty); emit autoOrientationChanged(); update(); } void QQuick3DTexture::setMagFilter(QQuick3DTexture::Filter magFilter) { if (m_magFilter == magFilter) return; m_magFilter = magFilter; m_dirtyFlags.setFlag(DirtyFlag::SamplerDirty); emit magFilterChanged(); update(); } void QQuick3DTexture::setMinFilter(QQuick3DTexture::Filter minFilter) { if (m_minFilter == minFilter) return; m_minFilter = minFilter; m_dirtyFlags.setFlag(DirtyFlag::SamplerDirty); emit minFilterChanged(); update(); } void QQuick3DTexture::setMipFilter(QQuick3DTexture::Filter mipFilter) { if (m_mipFilter == mipFilter) return; m_mipFilter = mipFilter; m_dirtyFlags.setFlag(DirtyFlag::SamplerDirty); emit mipFilterChanged(); update(); } // this function may involve file system access and hence can be expensive bool QQuick3DTexture::effectiveFlipV(const QSSGRenderImage &imageNode) const { // No magic when autoOrientation is false. if (!m_autoOrientation) return m_flipV; // Keep the same order as in QSSGBufferManager: sourceItem > textureData > source // Using sourceItem implies inverting (the effective, internal) flipV, // transparently to the user. Otherwise two #Rectangle models textured with // two Textures where one has its content loaded from an image file via // QImage while the other is generated by Qt Quick rendering into the // texture would appear upside-down relative to each other, and that // discrepancy is not ideal. (that said, this won't help CustomMaterial, as // documented for flipV and co.) if (m_sourceItem) return !m_flipV; // With textureData and renderExtension we assume the application knows what it is doing, // because there the application is controlling the content itself. if (m_textureData || m_renderExtension) return m_flipV; // Compressed textures (or any texture that is coming from the associated // container formats, such as KTX, i.e. not via QImage but through // QTextureFileReader) get the implicit flip, like sourceItem. This is done // mainly for parity with Qt Quick's Image, see QTBUG-93972. if (!m_source.isEmpty()) { const QString filePath = imageNode.m_imagePath.path(); if (!filePath.isEmpty()) { QSSGInputUtil::FileType fileType = QSSGInputUtil::UnknownFile; if (QSSGInputUtil::getStreamForTextureFile(filePath, true, nullptr, &fileType)) { if (fileType == QSSGInputUtil::TextureFile) return !m_flipV; } } } return m_flipV; } static QSSGRenderPath resolveImagePath(const QUrl &url, const QQmlContext *context) { if (context && url.isRelative()) { QString path = url.path(); QChar separator = QChar::fromLatin1(';'); if (path.contains(separator)) { QString resolvedPath; const QStringList paths = path.split(separator); bool first = true; for (auto &s : paths) { auto mapped = QQmlFile::urlToLocalFileOrQrc(context->resolvedUrl(s)); if (!first) resolvedPath.append(separator); resolvedPath.append(mapped); first = false; } return QSSGRenderPath(resolvedPath); } } return QSSGRenderPath(QQmlFile::urlToLocalFileOrQrc(context ? context->resolvedUrl(url) : url)); } QSSGRenderGraphObject *QQuick3DTexture::updateSpatialNode(QSSGRenderGraphObject *node) { if (!node) { markAllDirty(); node = new QSSGRenderImage(QQuick3DObjectPrivate::get(this)->type); } QQuick3DObject::updateSpatialNode(node); auto imageNode = static_cast(node); if (m_dirtyFlags.testFlag(DirtyFlag::TransformDirty)) { m_dirtyFlags.setFlag(DirtyFlag::TransformDirty, false); // flipV and indexUV have their own dirty flags, handled separately below imageNode->m_flipU = m_flipU; imageNode->m_scale = QVector2D(m_scaleU, m_scaleV); imageNode->m_pivot = QVector2D(m_pivotU, m_pivotV); imageNode->m_rotation = m_rotationUV; imageNode->m_position = QVector2D(m_positionU, m_positionV); imageNode->m_flags.setFlag(QSSGRenderImage::Flag::TransformDirty); } bool nodeChanged = false; if (m_dirtyFlags.testFlag(DirtyFlag::SourceDirty)) { m_dirtyFlags.setFlag(DirtyFlag::SourceDirty, false); m_dirtyFlags.setFlag(DirtyFlag::FlipVDirty, true); if (!m_source.isEmpty()) { const QQmlContext *context = qmlContext(this); imageNode->m_imagePath = resolveImagePath(m_source, context); } else { imageNode->m_imagePath = QSSGRenderPath(); } nodeChanged = true; } if (m_dirtyFlags.testFlag(DirtyFlag::IndexUVDirty)) { m_dirtyFlags.setFlag(DirtyFlag::IndexUVDirty, false); imageNode->m_indexUV = m_indexUV; } nodeChanged |= qUpdateIfNeeded(imageNode->m_mappingMode, QSSGRenderImage::MappingModes(m_mappingMode)); nodeChanged |= qUpdateIfNeeded(imageNode->m_horizontalTilingMode, QSSGRenderTextureCoordOp(m_tilingModeHorizontal)); nodeChanged |= qUpdateIfNeeded(imageNode->m_verticalTilingMode, QSSGRenderTextureCoordOp(m_tilingModeVertical)); nodeChanged |= qUpdateIfNeeded(imageNode->m_depthTilingMode, QSSGRenderTextureCoordOp(m_tilingModeDepth)); if (m_dirtyFlags.testFlag(DirtyFlag::SamplerDirty)) { m_dirtyFlags.setFlag(DirtyFlag::SamplerDirty, false); nodeChanged |= qUpdateIfNeeded(imageNode->m_minFilterType, QSSGRenderTextureFilterOp(m_minFilter)); nodeChanged |= qUpdateIfNeeded(imageNode->m_magFilterType, QSSGRenderTextureFilterOp(m_magFilter)); nodeChanged |= qUpdateIfNeeded(imageNode->m_mipFilterType, QSSGRenderTextureFilterOp(m_mipFilter)); nodeChanged |= qUpdateIfNeeded(imageNode->m_generateMipmaps, m_generateMipmaps); } if (m_dirtyFlags.testFlag(DirtyFlag::TextureDataDirty)) { m_dirtyFlags.setFlag(DirtyFlag::TextureDataDirty, false); m_dirtyFlags.setFlag(DirtyFlag::FlipVDirty, true); if (m_textureData) imageNode->m_rawTextureData = static_cast(QQuick3DObjectPrivate::get(m_textureData)->spatialNode); else imageNode->m_rawTextureData = nullptr; nodeChanged = true; } if (m_dirtyFlags.testFlag(DirtyFlag::ExtensionDirty)) { bool extDirty = false; if (m_renderExtension) { auto *sn = QQuick3DObjectPrivate::get(m_renderExtension)->spatialNode; // NOTE: We don't clear if we haven't gotten the spatial node yet, as // we'll be called once _again_ when the extensions have been processed. extDirty = (sn == nullptr); if (sn && QSSG_GUARD(sn->type == QSSGRenderGraphObject::Type::RenderExtension)) imageNode->m_extensionsSource = static_cast(sn); } m_dirtyFlags.setFlag(DirtyFlag::ExtensionDirty, extDirty); m_dirtyFlags.setFlag(DirtyFlag::FlipVDirty, true); nodeChanged = true; } if (m_dirtyFlags.testFlag(DirtyFlag::SourceItemDirty)) { m_dirtyFlags.setFlag(DirtyFlag::SourceItemDirty, false); m_dirtyFlags.setFlag(DirtyFlag::FlipVDirty, true); if (m_sourceItem) { QQuickWindow *window = m_sourceItem->window(); // If it was an inline declared item (very common, e.g. Texture { // sourceItem: Rectangle { ... } } then it is likely it won't be // associated with a window (Qt Quick scene) unless we help it to // one via refWindow. However, this here is only the last resort, // ideally there is a refWindow upon ItemSceneChange already. if (!window) { window = QQuick3DObjectPrivate::get(this)->sceneManager->window(); if (window) QQuickItemPrivate::get(m_sourceItem)->refWindow(window); else qWarning("Unable to get window, this will probably not work"); } // Workaround: Due to limitation in how the texture provider works in View3D we can't use it in the // special case when a non-visible View3D is used as a sourceItem, at least not in its current incarnation. // There's also a drawback here, since we now have an extra indirection by rendering to the layer texture first.. const bool isHiddenView3D = !m_sourceItem->isVisible() && (qobject_cast(m_sourceItem) != nullptr); // This assumes that the QSGTextureProvider returned never changes, // which is hopefully the case for both Image and Item layers. if (QSGTextureProvider *provider = m_sourceItem->textureProvider(); provider != nullptr && !isHiddenView3D) { imageNode->m_qsgTexture = provider->texture(); disconnect(m_textureProviderConnection); m_textureProviderConnection = connect(provider, &QSGTextureProvider::textureChanged, this, [this, provider] () { // called on the render thread, if there is one; the gui // thread may or may not be blocked (e.g. if the source is // a View3D, that emits textureChanged() from preprocess, // so after sync, whereas an Image emits in // updatePaintNode() where gui is blocked) auto imageNode = static_cast(QQuick3DObjectPrivate::get(this)->spatialNode); if (!imageNode) return; imageNode->m_qsgTexture = provider->texture(); // the QSGTexture may be different now, go through loadRenderImage() again imageNode->m_flags.setFlag(QSSGRenderImage::Flag::Dirty); // Call update() on the main thread - otherwise we could // end up in a situation where the 3D scene does not update // due to nothing else changing, even though the source // texture is now different. QMetaObject::invokeMethod(this, &QQuick3DTexture::update, Qt::AutoConnection); }, Qt::DirectConnection); disconnect(m_textureUpdateConnection); auto *sourcePrivate = QQuickItemPrivate::get(m_sourceItem); if (sourcePrivate->window) { QQuickItem *sourceItem = m_sourceItem; // for capturing, recognizing in the lambda that m_sourceItem has changed is essential // Why after, not beforeSynchronizing? Consider the case of an Item layer: // if the View3D gets to sync (updatePaintNode) first, doing an // updateTexture() is futile, the QSGLayer is not yet initialized (not // associated with an Item, has no size, etc.). That happens only once the // underlying QQuickShaderEffectSource hits its updatePaintNode. And that // may well happen happen only after the View3D has finished with its sync // step. By connecting to afterSynchronizing, we still get a chance to // trigger a layer texture update and so have a QSGTexture with real // content ready by the time the View3D prepares/renders the 3D scene upon // the scenegraph's preprocess step (Offscreen) or before/after the // scenegraph rendering (if Underlay/Overlay). // // This eliminates, or in the worst case reduces, the ugly effects of not // having a texture ready when rendering the 3D scene. m_textureUpdateConnection = connect(sourcePrivate->window, &QQuickWindow::afterSynchronizing, this, [this, sourceItem]() { // Called on the render thread with gui blocked (if there is a render thread, that is). if (m_sourceItem != sourceItem) { disconnect(m_textureProviderConnection); disconnect(m_textureUpdateConnection); return; } auto imageNode = static_cast(QQuick3DObjectPrivate::get(this)->spatialNode); if (!imageNode) return; if (QSGDynamicTexture *t = qobject_cast(imageNode->m_qsgTexture)) { if (t->updateTexture()) update(); // safe because the gui thread is blocked } }, Qt::DirectConnection); } else { qWarning("No window for item, texture updates are doomed"); } if (m_layer) { delete m_layer; m_layer = nullptr; } } else { // Not a texture provider, so not an Image or an Item with // layer.enabled: true, create our own QSGLayer. if (m_initializedSourceItem != m_sourceItem || m_initializedSourceItemSize != m_sourceItem->size()) { // If there was a previous sourceItem and m_layer is valid // then set its content to null until we get to // afterSynchronizing, otherwise things can blow up. if (m_layer) m_layer->setItem(nullptr); m_initializedSourceItem = m_sourceItem; m_initializedSourceItemSize = m_sourceItem->size(); // The earliest next point where we can do anything is // after the scenegraph's QQuickItem sync round has completed. connect(window, &QQuickWindow::afterSynchronizing, this, [this, window]() { auto imageNode = static_cast(QQuick3DObjectPrivate::get(this)->spatialNode); if (!imageNode) return; // Called on the render thread with gui blocked (if there is a render thread, that is). disconnect(window, &QQuickWindow::afterSynchronizing, this, nullptr); if (m_layer) { const auto &manager = QQuick3DObjectPrivate::get(this)->sceneManager; manager->qsgDynamicTextures.removeAll(m_layer); delete m_layer; m_layer = nullptr; } QQuickItemPrivate *sourcePrivate = QQuickItemPrivate::get(m_sourceItem); QSGRenderContext *rc = sourcePrivate->sceneGraphRenderContext(); Q_ASSERT(QThread::currentThread() == rc->thread()); // must be on the render thread QSGLayer *layer = rc->sceneGraphContext()->createLayer(rc); connect(sourcePrivate->window, SIGNAL(sceneGraphInvalidated()), layer, SLOT(invalidated()), Qt::DirectConnection); QQuick3DSceneManager *manager = QQuick3DObjectPrivate::get(this)->sceneManager; manager->qsgDynamicTextures << layer; m_sceneManagerForLayer = manager; connect(layer, &QObject::destroyed, manager, [manager, layer]() { // this is on the render thread so all borked threading-wise (all data here is gui thread stuff...) but will survive manager->qsgDynamicTextures.removeAll(layer); }, Qt::DirectConnection); QQuickItem *sourceItem = m_sourceItem; // for capturing, recognizing in the lambda that m_sourceItem has changed is essential connect(layer, &QObject::destroyed, this, [this, sourceItem]() { // just as dubious as the previous connection if (m_initializedSourceItem == sourceItem) { m_sceneManagerForLayer = nullptr; m_initializedSourceItem = nullptr; } }, Qt::DirectConnection); // With every frame try to update the texture. Use // afterSynchronizing like in the other branch. (why // after: a property changing something in the 2D // subtree leading to updates in the content will only // be "visible" after the (2D item) sync, not before) // // If updateTexture() returns false, content hasn't // changed. This complements qsgDynamicTextures and // QQuick3DViewport::updateDynamicTextures(). m_textureUpdateConnection = connect(sourcePrivate->window, &QQuickWindow::afterSynchronizing, this, [this, sourceItem]() { // Called on the render thread with gui blocked (if there is a render thread, that is). if (!m_layer) return; if (m_sourceItem != sourceItem) { disconnect(m_textureUpdateConnection); return; } if (m_layer->updateTexture()) update(); }, Qt::DirectConnection); m_layer = layer; m_layer->setItem(QQuickItemPrivate::get(m_sourceItem)->itemNode()); QRectF sourceRect = QRectF(0, 0, m_sourceItem->width(), m_sourceItem->height()); if (qFuzzyIsNull(sourceRect.width())) sourceRect.setWidth(256); if (qFuzzyIsNull(sourceRect.height())) sourceRect.setHeight(256); m_layer->setRect(sourceRect); QSize textureSize(qCeil(qAbs(sourceRect.width())), qCeil(qAbs(sourceRect.height()))); const QSize minTextureSize = sourcePrivate->sceneGraphContext()->minimumFBOSize(); while (textureSize.width() < minTextureSize.width()) textureSize.rwidth() *= 2; while (textureSize.height() < minTextureSize.height()) textureSize.rheight() *= 2; m_layer->setSize(textureSize); // now that the layer has an item and a size, it can render into the texture m_layer->updateTexture(); imageNode->m_qsgTexture = m_layer; imageNode->m_flags.setFlag(QSSGRenderImage::Flag::Dirty); }, Qt::DirectConnection); } } } else { if (m_layer) { m_layer->setItem(nullptr); delete m_layer; m_layer = nullptr; } imageNode->m_qsgTexture = nullptr; } nodeChanged = true; } if (m_dirtyFlags.testFlag(DirtyFlag::FlipVDirty)) { m_dirtyFlags.setFlag(DirtyFlag::FlipVDirty, false); imageNode->m_flipV = effectiveFlipV(*imageNode); imageNode->m_flags.setFlag(QSSGRenderImage::Flag::TransformDirty); } if (nodeChanged) imageNode->m_flags.setFlag(QSSGRenderImage::Flag::Dirty); return imageNode; } void QQuick3DTexture::itemChange(QQuick3DObject::ItemChange change, const QQuick3DObject::ItemChangeData &value) { QQuick3DObject::itemChange(change, value); if (change == QQuick3DObject::ItemChange::ItemSceneChange) { // Source item if (m_sourceItem) { disconnect(m_sceneManagerWindowChangeConnection); if (m_sceneManagerForLayer) { m_sceneManagerForLayer->qsgDynamicTextures.removeOne(m_layer); m_sceneManagerForLayer = nullptr; } trySetSourceParent(); const auto &sceneManager = value.sceneManager; Q_ASSERT(QQuick3DObjectPrivate::get(this)->sceneManager == sceneManager); if (m_layer) { if (sceneManager) sceneManager->qsgDynamicTextures << m_layer; m_sceneManagerForLayer = sceneManager; } // If m_sourceItem was an inline declared item (very common, e.g. // Texture { sourceItem: Rectangle { ... } } then it is highly // likely it won't be associated with a window (Qt Quick scene) // yet. Associate with one as soon as possible, do not leave it to // updateSpatialNode, because that, while safe, would defer // rendering into the texture to a future frame (adding a 2 frame // lag for the first rendering of the mesh textured with the 2D // item content), since a refWindow needs to be followed by a // scenegraph sync round to get QSGNodes created (updatePaintNode), // whereas updateSpatialNode is in the middle of a sync round, so // would need to wait for another one, etc. if (sceneManager && m_sourceItem && !m_sourceItem->window()) { if (sceneManager->window()) { QQuickItemPrivate::get(m_sourceItem)->refWindow(sceneManager->window()); } else { m_sceneManagerWindowChangeConnection = connect(sceneManager, &QQuick3DSceneManager::windowChanged, this, [this, sceneManager] { if (m_sourceItem && !m_sourceItem->window() && sceneManager->window()) QQuickItemPrivate::get(m_sourceItem)->refWindow(sceneManager->window()); }); } } } // TextureData if (m_textureData) { const auto &sceneManager = value.sceneManager; if (sceneManager) QQuick3DObjectPrivate::refSceneManager(m_textureData, *sceneManager); else QQuick3DObjectPrivate::derefSceneManager(m_textureData); } } } void QQuick3DTexture::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &geometry) { Q_ASSERT(item == m_sourceItem); Q_UNUSED(item); Q_UNUSED(geometry); if (change.sizeChange()) { m_dirtyFlags.setFlag(DirtyFlag::SourceItemDirty); update(); } } void QQuick3DTexture::sourceItemDestroyed(QObject *item) { Q_ASSERT(item == m_sourceItem); Q_UNUSED(item); m_sourceItem = nullptr; m_dirtyFlags.setFlag(DirtyFlag::SourceDirty); m_dirtyFlags.setFlag(DirtyFlag::SourceItemDirty); m_dirtyFlags.setFlag(DirtyFlag::TextureDataDirty); emit sourceItemChanged(); update(); } void QQuick3DTexture::markDirty(QQuick3DTexture::DirtyFlag type) { if (!m_dirtyFlags.testFlag(type)) { m_dirtyFlags.setFlag(type, true); update(); } } QSSGRenderImage *QQuick3DTexture::getRenderImage() { QQuick3DObjectPrivate *p = QQuick3DObjectPrivate::get(this); return static_cast(p->spatialNode); } void QQuick3DTexture::markAllDirty() { m_dirtyFlags = DirtyFlags(0xFFFF); QQuick3DObject::markAllDirty(); } void QQuick3DTexture::setTextureProvider(QQuick3DRenderExtension *textureProvider) { if (m_renderExtension == textureProvider) return; QQuick3DObjectPrivate::attachWatcher(this, &QQuick3DTexture::setTextureProvider, textureProvider, m_renderExtension); m_renderExtension = textureProvider; m_dirtyFlags.setFlag(DirtyFlag::SourceDirty); m_dirtyFlags.setFlag(DirtyFlag::SourceItemDirty); m_dirtyFlags.setFlag(DirtyFlag::TextureDataDirty); m_dirtyFlags.setFlag(DirtyFlag::ExtensionDirty); emit textureProviderChanged(); update(); } QT_END_NAMESPACE