// Copyright (C) 2020 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "randominstancing_p.h" #include #include QT_BEGIN_NAMESPACE /*! \qmltype InstanceRange \inherits Object3D \inqmlmodule QtQuick3D.Helpers \since 6.2 \brief Specifies a range for RandomInstancing. The InstanceRange QML type is used to specify the range of variation for RandomInstancing attributes. */ /*! \qmlproperty Variant InstanceRange::from This property specifies the lower bound of the range. The type needs to match the type of the attribute that this range is applied to. */ /*! \qmlproperty Variant InstanceRange::to This property specifies the upper bound of the range. The type needs to match the type of the attribute that this range is applied to. */ /*! \qmlproperty bool InstanceRange::proportional This property determines whether the components of the attribute vary proportionally or independently. The default value is \c true, meaning that all components are independent. For example, the following defines a scaling range that preserves the aspect ratio of the model: \qml InstanceRange { from: Qt.vector3d(1, 1, 1) to: Qt.vector3d(5, 5, 5) proportional: true } \endqml This defines a greyscale color range: \qml InstanceRange { from: "black" to: "white" proportional: true } \endqml While the following defines a range that covers all colors \qml InstanceRange { from: "black" to: "white" } \endqml */ /*! \qmltype RandomInstancing \inherits Instancing \inqmlmodule QtQuick3D.Helpers \since 6.2 \brief Generates a random instancing table. The RandomInstancing type provides an easy way to generate a large number of random instances within defined bounds. The number of instances is defined by the \l instanceCount property. The bounds are defined by the properties \l position, \l scale, \l rotation, \l color, and \l customData. \sa InstanceList */ /*! \qmlproperty int RandomInstancing::instanceCount The instanceCount property specifies the number of instances to generate. Changing this value will regenerate the whole table. \sa randomSeed */ /*! \qmlproperty int RandomInstancing::randomSeed This property defines the seed for the random number generator. Setting this to a value different from -1 guarantees that the instance table will have the same content each time it is generated. Note that adding or changing attributes may cause a completely different table to be generated. The default value is -1, causing the table to get a new random value each time it is generated. */ /*! \qmlproperty InstanceRange RandomInstancing::position The position property defines the geometrical bounds of the generated instances. The default value is empty, causing a generated position of \c{[0, 0, 0]}. \sa color, rotation, scale, customData */ /*! \qmlproperty vector3d RandomInstancing::gridSpacing \since 6.9 The gridSpacing property defines the minimum spacing between instances, ensuring they do not overlap. Each position will be separated by at least the value specified in \c gridSpacing. If the specified \c gridSpacing cannot accommodate the requested number of instances, the \c instanceCount property will be reduced to the number of instances that can be placed without overlap. \note The gridSpacing property affects only the position of instances. Rotation and scaling applied to instances are not considered in the spacing algorithm. The default value is \c{[0, 0, 0]}, which imposes no restriction on overlapping instances. \sa position */ /*! \qmlproperty InstanceRange RandomInstancing::scale The scale property defines the scaling limits for the generated instances. The type is \l vector3d. Set \l {InstanceRange::proportional}{InstanceRange.proportional} to \c true for uniform scaling. The default value is empty, causing no scaling to be applied. \sa position, color, rotation, scale, customData */ /*! \qmlproperty InstanceRange RandomInstancing::rotation The rotation property defines the rotation range for the generated instances. The type is \l vector3d, corresponding to a Euler rotation vector \c{[xRotation, yRotation, zRotation]}. The default value is empty, causing no rotation to be applied. \sa position, color, scale, customData */ /*! \qmlproperty InstanceRange RandomInstancing::color The color property defines the color variation range for the generated instances. The type is \l color. The default value is empty, causing the color to be white. Setting the colorModel property makes it possible to select only saturated colors, for example. \sa position, rotation, scale, customData */ /*! \qmlproperty InstanceRange RandomInstancing::customData The customData property defines the custom data variation range for the generated instances. The type is \l vector4d. The default value is empty, causing causing the generated data to be \c{[0, 0, 0, 0]}. \sa position, color, rotation, scale, customData */ /*! \qmlproperty enumeration RandomInstancing::colorModel This property controls how the color range is interpreted. The instance colors are generated component by component within the range determined by the \e from and \e to colors. The color model determines how those components are defined. \value RandomInstancing.RGB The components are red, green, blue, and alpha, according to the RGB color model. \value RandomInstancing.HSV The components are hue, saturation, value, and alpha, according to the \l{QColor#The HSV Color Model}{HSV Color Model}. \value RandomInstancing.HSL The components are hue, saturation, lightness, and alpha,, according to the \l{QColor#The HSL Color Model}{HSL Color Model}. As an example, the following color range \qml color: InstanceRange { from: Qt.hsva(0, 0.1, 0.8, 1) to: Qt.hsva(1, 0.3, 1, 1) } \endqml will generate a full range of pastel colors when using the \c HSV color model, but only shades of pink when using the \c RGB color model. The default value is \c RandomInstancing.RGB \sa RandomInstancing::color */ QQuick3DRandomInstancing::QQuick3DRandomInstancing(QQuick3DObject *parent) : QQuick3DInstancing(parent) { } QQuick3DRandomInstancing::~QQuick3DRandomInstancing() { } void QQuick3DRandomInstancing::setInstanceCount(int instanceCount) { if (instanceCount == m_randomCount) return; m_randomCount = instanceCount; emit instanceCountChanged(); m_dirty = true; markDirty(); } void QQuick3DRandomInstancing::setRandomSeed(int randomSeed) { if (m_randomSeed == randomSeed) return; m_randomSeed = randomSeed; emit randomSeedChanged(); m_dirty = true; markDirty(); } void QQuick3DRandomInstancing::setPosition(QQuick3DInstanceRange *position) { if (m_position == position) return; if (m_position) disconnect(m_position, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); m_position = position; emit positionChanged(); m_dirty = true; markDirty(); if (m_position) { connect(m_position, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); connect(m_position, &QObject::destroyed, this, [this](QObject *obj){ if (obj == m_position) m_position = nullptr; }); } } void QQuick3DRandomInstancing::setScale(QQuick3DInstanceRange *scale) { if (m_scale == scale) return; if (m_scale) disconnect(m_scale, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); m_scale = scale; emit scaleChanged(); m_dirty = true; markDirty(); if (m_scale) { connect(m_scale, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); connect(m_scale, &QObject::destroyed, this, [this](QObject *obj){ if (obj == m_scale) m_scale = nullptr; }); } } void QQuick3DRandomInstancing::setRotation(QQuick3DInstanceRange *rotation) { if (m_rotation == rotation) return; if (m_rotation) disconnect(m_rotation, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); m_rotation = rotation; emit rotationChanged(); m_dirty = true; markDirty(); if (m_rotation) { connect(m_rotation, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); connect(m_rotation, &QObject::destroyed, this, [this](QObject *obj){ if (obj == m_rotation) m_rotation = nullptr; }); } } void QQuick3DRandomInstancing::setColor(QQuick3DInstanceRange *color) { if (m_color == color) return; if (m_color) disconnect(m_color, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); m_color = color; emit colorChanged(); m_dirty = true; markDirty(); if (m_color) { connect(m_color, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); connect(m_color, &QObject::destroyed, this, [this](QObject *obj){ if (obj == m_color) m_color = nullptr; }); } } void QQuick3DRandomInstancing::setCustomData(QQuick3DInstanceRange *customData) { if (m_customData == customData) return; if (m_customData) disconnect(m_customData, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); m_customData = customData; emit customDataChanged(); m_dirty = true; markDirty(); if (m_customData) { connect(m_customData, &QQuick3DInstanceRange::changed, this, &QQuick3DRandomInstancing::handleChange); connect(m_customData, &QObject::destroyed, this, [this](QObject *obj){ if (obj == m_customData) m_customData = nullptr; }); } } void QQuick3DRandomInstancing::setColorModel(QQuick3DRandomInstancing::ColorModel colorModel) { if (m_colorModel == colorModel) return; m_colorModel = colorModel; emit colorModelChanged(); m_dirty = true; markDirty(); } void QQuick3DRandomInstancing::handleChange() { m_dirty = true; markDirty(); } static inline float genRandom(float from, float to, QRandomGenerator *rgen) { float c = rgen->bounded(1.0); return from + c * (to - from); } static QVector3D genRandom(const QVector3D &from, const QVector3D &to, bool proportional, QRandomGenerator *rgen) { if (proportional) { float c = rgen->bounded(1.0); return from + c * (to - from); } return { genRandom(from.x(), to.x(), rgen), genRandom(from.y(), to.y(), rgen), genRandom(from.z(), to.z(), rgen) }; } static QVector4D genRandom(const QVector4D &from, const QVector4D &to, bool proportional, QRandomGenerator *rgen) { if (proportional) { float c = rgen->bounded(1.0); return from + c * (to - from); } return { genRandom(from.x(), to.x(), rgen), genRandom(from.y(), to.y(), rgen), genRandom(from.z(), to.z(), rgen), genRandom(from.w(), to.w(), rgen) }; } static QColor genRandom(const QColor &from, const QColor &to, bool proportional, QQuick3DRandomInstancing::ColorModel colorModel, QRandomGenerator *rgen) { QVector4D v1, v2; switch (colorModel) { case QQuick3DRandomInstancing::ColorModel::HSL: from.getHslF(&v1[0], &v1[1], &v1[2], &v1[3]); to.getHslF(&v2[0], &v2[1], &v2[2], &v2[3]); break; case QQuick3DRandomInstancing::ColorModel::HSV: from.getHsvF(&v1[0], &v1[1], &v1[2], &v1[3]); to.getHsvF(&v2[0], &v2[1], &v2[2], &v2[3]); break; case QQuick3DRandomInstancing::ColorModel::RGB: default: from.getRgbF(&v1[0], &v1[1], &v1[2], &v1[3]); to.getRgbF(&v2[0], &v2[1], &v2[2], &v2[3]); break; } QVector4D r = genRandom(v1, v2, proportional, rgen); switch (colorModel) { case QQuick3DRandomInstancing::ColorModel::HSL: return QColor::fromHslF(r[0], r[1], r[2], r[3]); break; case QQuick3DRandomInstancing::ColorModel::HSV: return QColor::fromHsvF(r[0], r[1], r[2], r[3]); break; case QQuick3DRandomInstancing::ColorModel::RGB: default: return QColor::fromRgbF(r[0], r[1], r[2], r[3]); } } QByteArray QQuick3DRandomInstancing::getInstanceBuffer(int *instanceCount) { if (m_dirty) generateInstanceTable(); if (instanceCount) *instanceCount = m_randomCount; return m_instanceData; } namespace { struct GridPosition { int x, y, z; }; inline bool operator==(const GridPosition &e1, const GridPosition &e2) { return e1.x == e2.x && e1.y == e2.y && e1.z == e2.z; } inline size_t qHash(const GridPosition &key, size_t seed) { return qHashMulti(seed, key.x, key.y, key.z); } class PositionGenerator { public: void init(QVector3D fromPos, QVector3D toPos, bool proportional, bool gridMode, QVector3D gridSize) { m_from = fromPos; m_to = toPos; m_proportional = proportional; m_gridMode = gridMode; if (gridMode) { int nx, ny, nz; float cellWidth; float cellDepth; float cellHeight; float width = toPos.x() - fromPos.x(); float height = toPos.y() - fromPos.y(); float depth = toPos.z() - fromPos.z(); if (qFuzzyIsNull(width)) { cellWidth = 0; nx = 1; } else { cellWidth = gridSize.x() > 0 ? gridSize.x() : width; nx = width / cellWidth; } if (qFuzzyIsNull(height)) { cellHeight = 0; ny = 1; } else { cellHeight = gridSize.y() > 0 ? gridSize.y() : height; ny = height / cellHeight; } if (qFuzzyIsNull(depth)) { cellDepth = 0; nz = 1; } else { cellDepth = gridSize.z() > 0 ? gridSize.z() : depth; nz = depth / cellDepth; } m_xGrid = nx > 1; m_yGrid = ny > 1; m_zGrid = nz > 1; m_gridSize = {cellWidth, cellHeight, cellDepth}; } m_remainingAttempts = 1000000; } inline GridPosition gridPos(QVector3D pos) { int ix = m_xGrid ? int(pos.x() / m_gridSize.x()) : 0; int iy = m_yGrid ? int(pos.y() / m_gridSize.y()) : 0; int iz = m_zGrid ? int(pos.z() / m_gridSize.z()) : 0; return {ix, iy, iz}; } inline bool collision(const GridPosition &gp) { for (int x = gp.x - m_xGrid; x <= gp.x + m_xGrid; ++x) for (int y = gp.y - m_yGrid; y <= gp.y + m_yGrid; ++y) for (int z = gp.z - m_zGrid; z <= gp.z + m_zGrid; ++z ) if (m_occupiedCells.contains({x,y,z})) return true; return false; } QVector3D generate(QRandomGenerator *rgen) { if (m_gridMode) { while (m_remainingAttempts > 0) { auto pos = genRandom(m_from, m_to, m_proportional, rgen); auto gPos = gridPos(pos); if (!collision(gPos)) { m_occupiedCells.insert(gPos); return pos; } m_remainingAttempts--; } return {}; } return genRandom(m_from, m_to, m_proportional, rgen); } bool isFull() const { return m_remainingAttempts <= 0; } private: QVector3D m_from; QVector3D m_to; QVector3D m_gridSize; QSet m_occupiedCells; // TODO: We could use a Bloom filter (no, not the graphics effect;) to save memory int m_remainingAttempts; bool m_proportional = false; bool m_gridMode = false; bool m_xGrid = false; bool m_yGrid = false; bool m_zGrid = false; }; } void QQuick3DRandomInstancing::generateInstanceTable() { m_dirty = false; const int count = m_randomCount; QRandomGenerator rgen(m_randomSeed); if (m_randomSeed == -1) rgen.seed(QRandomGenerator::global()->generate()); qsizetype tableSize = count * sizeof(InstanceTableEntry); m_instanceData.resize(tableSize); auto *array = reinterpret_cast(m_instanceData.data()); PositionGenerator posGen; if (m_position) posGen.init(m_position->from().value(), m_position->to().value(), m_position->proportional(), m_gridMode, m_gridSpacing); for (int i = 0; i < count; ++i) { QVector3D pos; QVector3D scale{1, 1, 1}; QVector3D eulerRotation; QColor color(Qt::white); QVector4D customData; if (m_position) pos = posGen.generate(&rgen); if (m_scale) scale = genRandom(m_scale->from().value(), m_scale->to().value(), m_scale->proportional(), &rgen); if (m_rotation) //TODO: quaternion rotation??? eulerRotation = genRandom(m_rotation->from().value(), m_rotation->to().value(), m_rotation->proportional(), &rgen); if (m_color) color = genRandom(m_color->from().value(), m_color->to().value(), m_color->proportional(), m_colorModel, &rgen); if (m_customData) customData = genRandom(m_customData->from().value(), m_customData->to().value(), m_customData->proportional(), &rgen); if (Q_UNLIKELY(posGen.isFull())) { qWarning() << "RandomInstancing: Could not find free cell, truncating instance array" << i; qsizetype newSize = i * sizeof(InstanceTableEntry); m_instanceData.truncate(newSize); m_randomCount = i; emit instanceCountChanged(); break; } array[i] = calculateTableEntry(pos, scale, eulerRotation, color, customData); } } QQuick3DInstanceRange::QQuick3DInstanceRange(QQuick3DObject *parent) : QQuick3DObject(parent) { } void QQuick3DInstanceRange::setFrom(QVariant from) { if (m_from == from) return; m_from = from; emit fromChanged(); emit changed(); } void QQuick3DInstanceRange::setTo(QVariant to) { if (m_to == to) return; m_to = to; emit toChanged(); emit changed(); } void QQuick3DInstanceRange::setProportional(bool proportional) { if (m_proportional == proportional) return; m_proportional = proportional; emit proportionalChanged(); emit changed(); } QVector3D QQuick3DRandomInstancing::gridSpacing() const { return m_gridSpacing; } void QQuick3DRandomInstancing::setGridSpacing(const QVector3D &newGridSpacing) { if (m_gridSpacing == newGridSpacing) return; m_gridSpacing = newGridSpacing; emit gridSpacingChanged(); m_gridMode = (newGridSpacing.x() > 0 || newGridSpacing.y() > 0 || newGridSpacing.z() > 0) && !(newGridSpacing.x() < 0 || newGridSpacing.y() < 0 || newGridSpacing.z() < 0); m_dirty = true; markDirty(); } QT_END_NAMESPACE