Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++
++ #include "volumetricfogextension.h"
++
++ #include <memory>
++ #include <ssg/qssgrendercontextcore.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderer_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendercamera_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrhicontext_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendershadowmap_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderimage_p.h>
++ #include <QtQuick3D/private/qquick3dobject_p.h>
++ #include <QtCore/QFile>
++ #include <QtGui/QImage>
++ #include <QtQuick/QSGTexture>
++ #include <QHash>
++
++ static constexpr int MAX_LIGHTS = 8;
++ static constexpr int MAX_VOLUMES = 32;
++
++ struct alignas(16) LightBufferData {
++ float matrices[4][16];
++ QVector4D lightPos[MAX_LIGHTS];
++ QVector4D lightColor[MAX_LIGHTS];
++ QVector4D lightDirection[MAX_LIGHTS];
++ QVector4D coneAngles[MAX_LIGHTS];
++ QVector4D dirLightColor;
++ float csmSplits[4];
++ float csmActive[4];
++ float atlasLocations[4][4];
++ float dimensionsInverted[4][4];
++ float shadowBias;
++ float shadowFactor;
++ float padd2[2];
++ int numLights;
++ int padding[3];
++ float lightShadowMat[MAX_LIGHTS][16];
++ float lightShadowAtlas[MAX_LIGHTS][4];
++ float lightShadowAtlas2[MAX_LIGHTS][4];
++ QVector4D iesParams;
++ QVector4D iesIndices[2];
++ };
++
++ struct alignas(16) FroxelUniforms {
++ float invViewProjection[16];
++ float viewMatrix[16];
++ float gridDimensions[4];
++ float depthParams[4];
++ float cameraPos[4];
++ float qt_rhi_properties[4];
++ };
++
++ struct alignas(16) FogVolumes {
++ float matrices[MAX_VOLUMES][16];
++ QVector4D extents[MAX_VOLUMES];
++ QVector4D color[MAX_VOLUMES];
++ QVector4D heightParams[MAX_VOLUMES];
++ QVector4D noiseOffsetScale[MAX_VOLUMES];
++ int numVolumes;
++ int padding[3];
++ };
++
++ struct FogVolumeEntry {
++ QMatrix4x4 invTransform;
++ QVector3D extents;
++ int type = 0;
++ QColor color = Qt::white;
++ float density = 1.0f;
++ int heightEnabled = 0;
++ float leastIntenseY = 10.0f;
++ float mostIntenseY = 0.0f;
++ float heightCurve = 1.0f;
++ QVector3D noiseOffset;
++ float noiseScale = 0.5f;
++ };
++
++ struct IESLightEntry {
++ int index = -1;
++ float intensity = 1.0f;
++ };
++
++ struct IESConfig {
++ QSSGRenderImage *renderImage = nullptr;
++ int count = 1;
++ QHash<QSSGRenderLight *, IESLightEntry> lightMap;
++ };
++
++ struct FroxelConfig {
++ int width = 160;
++ int height = 90;
++ int depth = 64;
++ float nearPlane = 1.0f;
++ float farPlane = 2000.0f;
++ QList<FogVolumeEntry> fogVolumeData;
++ IESConfig ies;
++ };
++
++ class FroxelQSGTexture : public QSGTexture
++ {
++ public:
++ QRhiTexture *m_rhiTexture = nullptr;
++
++ QSize textureSize() const override { return m_rhiTexture ? m_rhiTexture->pixelSize() : QSize{}; }
++ bool hasAlphaChannel() const override { return true; }
++ bool hasMipmaps() const override { return false; }
++ qint64 comparisonKey() const override { return reinterpret_cast<quintptr>(m_rhiTexture); }
++ QRhiTexture *rhiTexture() const override { return m_rhiTexture; }
++ };
++
++ class FroxelRenderer : public QSSGRenderExtension
++ {
++ public:
++ FroxelConfig config;
++ bool resourcesDirty = true;
++ QQuick3DTexture *frontendTexture = nullptr;
++
++ bool prepareData(QSSGFrameData &data) override;
++ void prepareRender(QSSGFrameData &data) override;
++ void render(QSSGFrameData &data) override;
++ void resetForFrame() override;
++ RenderMode mode() const override { return RenderMode::Standalone; }
++ RenderStage stage() const override { return RenderStage::PostColor; }
++ bool m_pipelineDirty = true;
++
++ private:
++ void setupFroxelGrid(const QSSGRhiContext *rhiCtx);
++ void setupComputePipeline(const QSSGRhiContext *rhiCtx, QRhiTexture *shadowAtlas);
++ void updateUniforms(const QSSGRhiContext *rhiCtx, QSSGLayerRenderData *layer, QSSGRenderCamera *camera);
++
++ bool m_enabled = false;
++
++ std::unique_ptr<QRhiTexture> m_froxelRhiTexture;
++ std::unique_ptr<FroxelQSGTexture> m_qsgTexture;
++ std::unique_ptr<QRhiBuffer> m_froxelUniformBuffer;
++ std::unique_ptr<QRhiBuffer> m_lightDataBuffer;
++ std::unique_ptr<QRhiBuffer> m_volumeDataBuffer;
++
++ std::unique_ptr<QRhiComputePipeline> m_computePipeline;
++ std::unique_ptr<QRhiShaderResourceBindings> m_computeBindings;
++ std::unique_ptr<QRhiSampler> m_atlasSampler;
++ std::unique_ptr<QRhiTexture> m_blueNoiseTexture;
++ std::unique_ptr<QRhiSampler> m_blueNoiseSampler;
++ QRhiTexture *m_shadowAtlas = nullptr;
++
++ std::unique_ptr<QRhiTexture> m_ies1x1Fallback;
++ std::unique_ptr<QRhiSampler> m_iesSampler;
++ QRhiTexture *m_iesRhiTexture = nullptr;
++ };
++
++ void FroxelRenderer::setupFroxelGrid(const QSSGRhiContext *rhiCtx)
++ {
++ if (!resourcesDirty)
++ return;
++
++ QRhi *rhi = rhiCtx->rhi();
++
++ m_froxelRhiTexture.reset(rhi->newTexture(
++ QRhiTexture::RGBA16F,
++ config.width, config.height, config.depth,
++ 1,
++ QRhiTexture::ThreeDimensional | QRhiTexture::UsedWithLoadStore | QRhiTexture::UsedAsTransferSource));
++ m_froxelRhiTexture->setDepth(config.depth);
++ m_froxelRhiTexture->create();
++
++ if (!m_qsgTexture)
++ m_qsgTexture = std::make_unique<FroxelQSGTexture>();
++ m_qsgTexture->m_rhiTexture = m_froxelRhiTexture.get();
++
++ if (frontendTexture) {
++ auto *img = static_cast<QSSGRenderImage *>(
++ QQuick3DObjectPrivate::get(frontendTexture)->spatialNode);
++ if (img)
++ img->m_qsgTexture = m_qsgTexture.get();
++ }
++
++ if (!m_froxelUniformBuffer) {
++ m_froxelUniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(FroxelUniforms)));
++ m_froxelUniformBuffer->create();
++ }
++ if (!m_lightDataBuffer) {
++ m_lightDataBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(LightBufferData)));
++ m_lightDataBuffer->create();
++ }
++ if (!m_volumeDataBuffer) {
++ m_volumeDataBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(FogVolumes)));
++ m_volumeDataBuffer->create();
++ }
++ if (!m_blueNoiseTexture) {
++ QImage img(QStringLiteral(":/res/textures/blue_noise.png"));
++ img = img.convertToFormat(QImage::Format_RGBA8888);
++ m_blueNoiseTexture.reset(rhi->newTexture(QRhiTexture::RGBA8, img.size(), 1, QRhiTexture::Flags{}));
++ m_blueNoiseTexture->create();
++ QRhiResourceUpdateBatch *noiseRub = rhi->nextResourceUpdateBatch();
++ QRhiTextureUploadEntry entry(0, 0, QRhiTextureSubresourceUploadDescription(img.constBits(), img.sizeInBytes()));
++ noiseRub->uploadTexture(m_blueNoiseTexture.get(), QRhiTextureUploadDescription(entry));
++ rhiCtx->commandBuffer()->resourceUpdate(noiseRub);
++ m_blueNoiseSampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
++ QRhiSampler::Repeat, QRhiSampler::Repeat));
++ m_blueNoiseSampler->create();
++ }
++
++ if (!m_ies1x1Fallback) {
++ m_ies1x1Fallback.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1), 1, QRhiTexture::Flags{}));
++ m_ies1x1Fallback->create();
++ const quint8 white[4] = { 255, 255, 255, 255 };
++ QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
++ QRhiTextureUploadEntry entry(0, 0, QRhiTextureSubresourceUploadDescription(white, 4));
++ rub->uploadTexture(m_ies1x1Fallback.get(), QRhiTextureUploadDescription(entry));
++ rhiCtx->commandBuffer()->resourceUpdate(rub);
++
++ m_iesSampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
++ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
++ m_iesSampler->create();
++ }
++
++ resourcesDirty = false;
++ m_pipelineDirty = true;
++ }
++
++ void FroxelRenderer::setupComputePipeline(const QSSGRhiContext *rhiCtx, QRhiTexture *shadowAtlas)
++ {
++ if (shadowAtlas != m_shadowAtlas)
++ m_pipelineDirty = true;
++ if (!m_pipelineDirty)
++ return;
++
++ QRhi *rhi = rhiCtx->rhi();
++ if (!rhi->isFeatureSupported(QRhi::Compute)) {
++ qWarning("FroxelRenderer: compute shaders not supported on this backend");
++ return;
++ }
++
++ m_computePipeline.reset();
++ m_computeBindings.reset();
++ m_atlasSampler.reset();
++ m_shadowAtlas = nullptr;
++
++ QShader computeShader;
++ {
++ QFile f(QStringLiteral(":/shaders/froxellightinjection.comp.qsb"));
++ if (!f.open(QIODevice::ReadOnly)) {
++ qWarning("FroxelRenderer: could not open froxellightinjection.comp.qsb");
++ return;
++ }
++ computeShader = QShader::fromSerialized(f.readAll());
++ }
++ Q_ASSERT(computeShader.isValid());
++
++ m_shadowAtlas = shadowAtlas;
++
++ m_atlasSampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
++ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
++ m_atlasSampler->create();
++
++ QRhiTexture *iesForBinding = m_iesRhiTexture ? m_iesRhiTexture : m_ies1x1Fallback.get();
++
++ m_computeBindings.reset(rhi->newShaderResourceBindings());
++ m_computeBindings->setBindings({
++ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::ComputeStage, m_froxelUniformBuffer.get()),
++ QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::ComputeStage, m_lightDataBuffer.get()),
++ QRhiShaderResourceBinding::uniformBuffer(2, QRhiShaderResourceBinding::ComputeStage, m_volumeDataBuffer.get()),
++ QRhiShaderResourceBinding::imageStore(3, QRhiShaderResourceBinding::ComputeStage, m_froxelRhiTexture.get(), 0),
++ QRhiShaderResourceBinding::sampledTexture(4, QRhiShaderResourceBinding::ComputeStage, m_shadowAtlas, m_atlasSampler.get()),
++ QRhiShaderResourceBinding::sampledTexture(5, QRhiShaderResourceBinding::ComputeStage, m_blueNoiseTexture.get(), m_blueNoiseSampler.get()),
++ QRhiShaderResourceBinding::sampledTexture(6, QRhiShaderResourceBinding::ComputeStage, iesForBinding, m_iesSampler.get()),
++ });
++ if (!m_computeBindings->create()) {
++ qWarning("FroxelRenderer: failed to create compute shader resource bindings");
++ return;
++ }
++
++ m_computePipeline.reset(rhi->newComputePipeline());
++ m_computePipeline->setShaderResourceBindings(m_computeBindings.get());
++ m_computePipeline->setShaderStage({ QRhiShaderStage::Compute, computeShader });
++ if (!m_computePipeline->create()) {
++ qWarning("FroxelRenderer: failed to create compute pipeline");
++ return;
++ }
++
++ m_pipelineDirty = false;
++ }
++
++ void FroxelRenderer::updateUniforms(const QSSGRhiContext *rhiCtx, QSSGLayerRenderData *layer, QSSGRenderCamera *camera)
++ {
++ static_assert(alignof(FroxelUniforms) == 16, "FroxelUniforms alignment mismatch");
++ static_assert(alignof(LightBufferData) == 16, "LightBufferData alignment mismatch");
++
++ QRhiResourceUpdateBatch *rub = rhiCtx->rhi()->nextResourceUpdateBatch();
++
++ QMatrix4x4 cameraGlobalTransform = layer->getGlobalTransform(*camera);
++ QMatrix4x4 viewProjection(Qt::Uninitialized);
++ camera->calculateViewProjectionMatrix(cameraGlobalTransform, viewProjection);
++ QMatrix4x4 invViewProjection = viewProjection.inverted();
++
++ FroxelUniforms uniforms = {};
++ memcpy(uniforms.invViewProjection, invViewProjection.constData(), 64);
++ memcpy(uniforms.viewMatrix, cameraGlobalTransform.inverted().constData(), 64);
++
++ const auto globalRenderData = QSSGLayerRenderData::globalRenderProperties(*layer->contextInterface());
++ uniforms.qt_rhi_properties[0] = globalRenderData.isYUpInFramebuffer ? 1.0f : -1.0f;
++ uniforms.qt_rhi_properties[1] = globalRenderData.isYUpInNDC ? 1.0f : -1.0f;
++
++ uniforms.gridDimensions[0] = float(config.width);
++ uniforms.gridDimensions[1] = float(config.height);
++ uniforms.gridDimensions[2] = float(config.depth);
++
++ uniforms.depthParams[0] = config.nearPlane;
++ uniforms.depthParams[1] = config.farPlane;
++
++ const QVector3D camPos = cameraGlobalTransform.column(3).toVector3D();
++ uniforms.cameraPos[0] = camPos.x();
++ uniforms.cameraPos[1] = camPos.y();
++ uniforms.cameraPos[2] = camPos.z();
++
++ rub->updateDynamicBuffer(m_froxelUniformBuffer.get(), 0, sizeof(FroxelUniforms), &uniforms);
++
++ LightBufferData lightData = {};
++
++ lightData.iesIndices[0] = QVector4D(-1.f, -1.f, -1.f, -1.f);
++ lightData.iesIndices[1] = QVector4D(-1.f, -1.f, -1.f, -1.f);
++
++ const bool iesEnabled = config.ies.renderImage != nullptr && config.ies.count > 0;
++ lightData.iesParams = QVector4D(iesEnabled ? 1.0f / float(config.ies.count) : 0.0f,
++ 0.f, 0.f, 0.f);
++
++ int lightCount = 0;
++
++ for (quint32 i = 0, end = qMin(QSSG_MAX_NUM_LIGHTS, (int)layer->globalLights.size()); i < end; ++i) {
++ QSSGRenderLight *light = layer->globalLights[i].light;
++ const bool shadows = layer->globalLights[i].shadows;
++ if (light->m_bakingEnabled)
++ continue;
++
++ if (light->type == QSSGRenderGraphObject::Type::DirectionalLight) {
++ if (shadows) {
++ auto *shadowMap = layer->getShadowMapManager()->shadowMapEntry(i);
++ if (shadowMap) {
++ const bool noCascades = !(shadowMap->m_csmActive[0] || shadowMap->m_csmActive[1]
++ || shadowMap->m_csmActive[2] || shadowMap->m_csmActive[3]);
++ if (!noCascades) {
++ const quint32 layerCount = shadowMap->m_csmNumSplits + 1;
++ for (quint32 k = 0; k < layerCount; ++k)
++ memcpy(lightData.matrices[k], shadowMap->m_fixedScaleBiasMatrix[k].constData(), 64);
++ lightData.shadowBias = light->m_shadowBias;
++ lightData.shadowFactor = qBound(0.0f, light->m_shadowFactor, 100.0f) * 0.02f;
++ memcpy(lightData.csmSplits, shadowMap->m_csmSplits, 4 * sizeof(float));
++ memcpy(lightData.csmActive, shadowMap->m_csmActive, 4 * sizeof(float));
++ for (quint32 k = 0; k < layerCount; ++k) {
++ const auto &a = shadowMap->m_atlasInfo[k];
++ lightData.atlasLocations[k][0] = a.uOffset;
++ lightData.atlasLocations[k][1] = a.vOffset;
++ lightData.atlasLocations[k][2] = a.uvScale;
++ lightData.atlasLocations[k][3] = a.layerIndex;
++ memcpy(lightData.dimensionsInverted[k], &shadowMap->m_dimensionsInverted[k], 4 * sizeof(float));
++ }
++ }
++ }
++ }
++ lightData.dirLightColor = QVector4D(light->m_diffuseColor, light->m_brightness);
++
++ } else if (light->type == QSSGRenderGraphObject::Type::PointLight) {
++ const QMatrix4x4 lightTransform = layer->getGlobalTransform(*light);
++ const QVector3D pos = QSSGRenderNode::getGlobalPos(lightTransform);
++ const IESLightEntry iesEntry = iesEnabled ? config.ies.lightMap.value(light) : IESLightEntry{};
++ lightData.lightPos[lightCount] = QVector4D(pos, 1.0f);
++ lightData.lightColor[lightCount] = QVector4D(light->m_diffuseColor, light->m_brightness * iesEntry.intensity);
++ lightData.lightDirection[lightCount] = QVector4D(-lightTransform.column(1).toVector3D().normalized(), 0.0f);
++ if (shadows) {
++ auto *shadowMap = layer->getShadowMapManager()->shadowMapEntry(i);
++ if (shadowMap) {
++ lightData.shadowBias = light->m_shadowBias;
++ lightData.shadowFactor = qBound(0.0f, light->m_shadowFactor, 100.0f) * 0.02f;
++ memcpy(lightData.lightShadowMat[lightCount], shadowMap->m_lightView.constData(), 64);
++ const auto &af = shadowMap->m_atlasInfo[0];
++ lightData.lightShadowAtlas[lightCount][0] = af.uOffset;
++ lightData.lightShadowAtlas[lightCount][1] = af.vOffset;
++ lightData.lightShadowAtlas[lightCount][2] = af.uvScale;
++ lightData.lightShadowAtlas[lightCount][3] = float(af.layerIndex);
++ const auto &ab = shadowMap->m_atlasInfo[1];
++ lightData.lightShadowAtlas2[lightCount][0] = ab.uOffset;
++ lightData.lightShadowAtlas2[lightCount][1] = ab.vOffset;
++ lightData.lightShadowAtlas2[lightCount][2] = ab.uvScale;
++ lightData.lightShadowAtlas2[lightCount][3] = float(ab.layerIndex);
++ lightData.coneAngles[lightCount].setZ(2.0f);
++ lightData.coneAngles[lightCount].setW(shadowMap->m_shadowMapFar);
++ }
++ }
++ if (iesEnabled) {
++ if (lightCount < 4) lightData.iesIndices[0][lightCount] = float(iesEntry.index);
++ else lightData.iesIndices[1][lightCount - 4] = float(iesEntry.index);
++ }
++ ++lightCount;
++
++ } else {
++ const QVector3D direction = layer->globalLights[i].direction;
++ const QVector3D pos = QSSGRenderNode::getGlobalPos(layer->getGlobalTransform(*light));
++ const IESLightEntry iesEntry = iesEnabled ? config.ies.lightMap.value(light) : IESLightEntry{};
++ lightData.lightPos[lightCount] = QVector4D(pos, 1.0f);
++ lightData.lightColor[lightCount] = QVector4D(light->m_diffuseColor, light->m_brightness * iesEntry.intensity);
++ lightData.lightDirection[lightCount] = QVector4D(direction, 1.0f);
++ lightData.coneAngles[lightCount] = QVector4D(qCos(qDegreesToRadians(light->m_coneAngle)),
++ qCos(qDegreesToRadians(light->m_innerConeAngle)),
++ 0.0f, 0.0f);
++ if (shadows) {
++ auto *shadowMap = layer->getShadowMapManager()->shadowMapEntry(i);
++ if (shadowMap) {
++ memcpy(lightData.lightShadowMat[lightCount], shadowMap->m_fixedScaleBiasMatrix[0].constData(), 64);
++ const auto &a = shadowMap->m_atlasInfo[0];
++ lightData.shadowBias = light->m_shadowBias;
++ lightData.shadowFactor = qBound(0.0f, light->m_shadowFactor, 100.0f) * 0.02f;
++ lightData.lightShadowAtlas[lightCount][0] = a.uOffset;
++ lightData.lightShadowAtlas[lightCount][1] = a.vOffset;
++ lightData.lightShadowAtlas[lightCount][2] = a.uvScale;
++ lightData.lightShadowAtlas[lightCount][3] = float(a.layerIndex);
++ lightData.coneAngles[lightCount].setZ(1.0f);
++ lightData.coneAngles[lightCount].setW(shadowMap->m_shadowMapFar);
++ }
++ }
++ if (iesEnabled) {
++ if (lightCount < 4) lightData.iesIndices[0][lightCount] = float(iesEntry.index);
++ else lightData.iesIndices[1][lightCount - 4] = float(iesEntry.index);
++ }
++ ++lightCount;
++ }
++ }
++ lightData.numLights = lightCount;
++ rub->updateDynamicBuffer(m_lightDataBuffer.get(), 0, sizeof(LightBufferData), &lightData);
++
++ FogVolumes volumes = {};
++ int fvcount = 0;
++ for (const auto &fv : std::as_const(config.fogVolumeData)) {
++ if (fvcount >= MAX_VOLUMES) break;
++ memcpy(volumes.matrices[fvcount], fv.invTransform.constData(), 64);
++ volumes.extents[fvcount] = QVector4D(fv.extents, float(fv.type));
++ volumes.color[fvcount] = QVector4D(fv.color.redF(), fv.color.greenF(), fv.color.blueF(), fv.density);
++ volumes.heightParams[fvcount] = QVector4D(fv.leastIntenseY, fv.mostIntenseY, fv.heightCurve, fv.heightEnabled ? 1.0f : 0.0f);
++ volumes.noiseOffsetScale[fvcount] = QVector4D(fv.noiseOffset, fv.noiseScale);
++ ++fvcount;
++ }
++ volumes.numVolumes = fvcount;
++ rub->updateDynamicBuffer(m_volumeDataBuffer.get(), 0, sizeof(FogVolumes), &volumes);
++
++ rhiCtx->commandBuffer()->resourceUpdate(rub);
++ }
++
++ bool FroxelRenderer::prepareData(QSSGFrameData &data)
++ {
++ const auto ctx = data.contextInterface();
++ auto *layer = QSSGLayerRenderData::getCurrent(*ctx->renderer());
++
++ if (!layer || layer->renderedCameras.isEmpty()) {
++ m_enabled = false;
++ return false;
++ }
++
++ m_enabled = true;
++
++ const auto &rhiCtx = ctx->rhiContext();
++ setupFroxelGrid(rhiCtx.get());
++
++ {
++ QRhiTexture *iesRhiTex = m_ies1x1Fallback.get();
++ if (config.ies.renderImage) {
++ QSSGRenderImageTexture texData =
++ ctx->bufferManager()->loadRenderImage(config.ies.renderImage);
++ if (texData.m_texture)
++ iesRhiTex = texData.m_texture;
++ }
++ if (iesRhiTex != m_iesRhiTexture) {
++ m_iesRhiTexture = iesRhiTex;
++ m_pipelineDirty = true;
++ }
++ }
++
++ auto mgr = layer->getShadowMapManager();
++ setupComputePipeline(rhiCtx.get(), mgr ? mgr->shadowMapAtlasTexture() : nullptr);
++ updateUniforms(rhiCtx.get(), layer, layer->renderedCameras[0]);
++
++ return true;
++ }
++
++ void FroxelRenderer::prepareRender(QSSGFrameData &data)
++ {
++ Q_UNUSED(data)
++ }
++
++ void FroxelRenderer::render(QSSGFrameData &data)
++ {
++ if (!m_enabled || !m_computePipeline || !m_computeBindings)
++ return;
++
++ const auto &rhiCtx = data.contextInterface()->rhiContext();
++ QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
++
++ cb->debugMarkBegin(QByteArrayLiteral("Quick3D volumetric light (froxel injection)"));
++ cb->beginComputePass();
++ cb->setComputePipeline(m_computePipeline.get());
++ cb->setShaderResources(m_computeBindings.get());
++ cb->dispatch((config.width + 7) / 8,
++ (config.height + 7) / 8,
++ config.depth);
++ cb->endComputePass();
++ cb->debugMarkEnd();
++ }
++
++ void FroxelRenderer::resetForFrame()
++ {
++ m_enabled = false;
++ }
++
++ VolumetricFogExtension::VolumetricFogExtension(QQuick3DObject *parent)
++ : QQuick3DRenderExtension(parent)
++ {
++ m_froxelTexture = new QQuick3DTexture(this);
++ m_froxelTexture->setHorizontalTiling(QQuick3DTexture::ClampToEdge);
++ m_froxelTexture->setVerticalTiling(QQuick3DTexture::ClampToEdge);
++ m_froxelTexture->setDepthTiling(QQuick3DTexture::ClampToEdge);
++ m_froxelTexture->setTextureData(new QQuick3DTextureData(m_froxelTexture));
++ m_froxelTexture->textureData()->setDepth(1);
++ }
++
++ QSSGRenderGraphObject *VolumetricFogExtension::updateSpatialNode(QSSGRenderGraphObject *node)
++ {
++ if (!node)
++ node = new FroxelRenderer;
++
++ auto *renderer = static_cast<FroxelRenderer *>(node);
++ renderer->frontendTexture = m_froxelTexture;
++
++ if (m_dirtyFlag & Config) {
++ const bool gridChanged = renderer->config.width != m_froxelWidth
++ || renderer->config.height != m_froxelHeight
++ || renderer->config.depth != m_froxelDepth;
++ renderer->config.width = m_froxelWidth;
++ renderer->config.height = m_froxelHeight;
++ renderer->config.depth = m_froxelDepth;
++ renderer->config.nearPlane = m_nearPlane;
++ renderer->config.farPlane = m_farPlane;
++
++ if (gridChanged)
++ renderer->resourcesDirty = true;
++ m_dirtyFlag &= ~static_cast<DirtyT>(Config);
++ }
++ if (m_dirtyFlag & Volumes) {
++ renderer->config.fogVolumeData.clear();
++ for (auto *fv : std::as_const(m_fogVolumes)) {
++ FogVolumeEntry entry;
++ entry.invTransform = fv->sceneTransform().inverted();
++ entry.extents = fv->extents();
++ entry.type = int(fv->type());
++ entry.color = fv->color();
++ entry.density = fv->density();
++ entry.heightEnabled = fv->heightEnabled();
++ entry.leastIntenseY = fv->leastIntenseY();
++ entry.mostIntenseY = fv->mostIntenseY();
++ entry.heightCurve = fv->heightCurve();
++ entry.noiseOffset = fv->noiseOffset();
++ entry.noiseScale = fv->noiseScale();
++ renderer->config.fogVolumeData.append(entry);
++ }
++ m_dirtyFlag &= ~static_cast<DirtyT>(Volumes);
++ }
++ if (m_dirtyFlag & IES) {
++ bool nodeReady = true;
++ QSSGRenderImage *img = nullptr;
++ if (m_iesQmlTexture) {
++ auto *priv = QQuick3DObjectPrivate::get(m_iesQmlTexture);
++ if (priv && priv->spatialNode) {
++ img = static_cast<QSSGRenderImage *>(priv->spatialNode);
++ } else {
++ nodeReady = false;
++ update();
++ }
++ }
++ if (nodeReady) {
++ QHash<QSSGRenderLight *, IESLightEntry> lightMap;
++ for (auto *mapping : std::as_const(m_iesLightProfiles)) {
++ if (!mapping->light())
++ continue;
++ auto *quick3dObj = qobject_cast<QQuick3DObject *>(mapping->light());
++ if (!quick3dObj)
++ continue;
++ auto *lpriv = QQuick3DObjectPrivate::get(quick3dObj);
++ if (!lpriv || !lpriv->spatialNode) {
++ nodeReady = false;
++ update();
++ break;
++ }
++ lightMap.insert(static_cast<QSSGRenderLight *>(lpriv->spatialNode),
++ IESLightEntry{ mapping->index(), mapping->intensity() });
++ }
++ if (nodeReady) {
++ renderer->config.ies.renderImage = img;
++ renderer->config.ies.count = m_iesCount;
++ renderer->config.ies.lightMap = std::move(lightMap);
++ renderer->m_pipelineDirty = true;
++ m_dirtyFlag &= ~static_cast<DirtyT>(IES);
++ }
++ }
++ }
++
++ return node;
++ }
++
++ void VolumetricFogExtension::markDirty(Dirty v)
++ {
++ m_dirtyFlag |= v;
++ update();
++ }
++
++ QQuick3DTexture *VolumetricFogExtension::froxelTexture() const { return m_froxelTexture; }
++ int VolumetricFogExtension::froxelWidth() const { return m_froxelWidth; }
++ int VolumetricFogExtension::froxelHeight() const { return m_froxelHeight; }
++ int VolumetricFogExtension::froxelDepth() const { return m_froxelDepth; }
++ float VolumetricFogExtension::nearPlane() const { return m_nearPlane; }
++ float VolumetricFogExtension::farPlane() const { return m_farPlane; }
++
++ void VolumetricFogExtension::setFroxelWidth(int v)
++ {
++ if (m_froxelWidth == v) return;
++ m_froxelWidth = v; markDirty(Config); emit froxelWidthChanged();
++ }
++ void VolumetricFogExtension::setFroxelHeight(int v)
++ {
++ if (m_froxelHeight == v) return;
++ m_froxelHeight = v; markDirty(Config); emit froxelHeightChanged();
++ }
++ void VolumetricFogExtension::setFroxelDepth(int v)
++ {
++ if (m_froxelDepth == v) return;
++ m_froxelDepth = v; markDirty(Config); emit froxelDepthChanged();
++ }
++ void VolumetricFogExtension::setNearPlane(float v)
++ {
++ if (qFuzzyCompare(m_nearPlane, v)) return;
++ m_nearPlane = v; markDirty(Config); emit nearPlaneChanged();
++ }
++ void VolumetricFogExtension::setFarPlane(float v)
++ {
++ if (qFuzzyCompare(m_farPlane, v)) return;
++ m_farPlane = v; markDirty(Config); emit farPlaneChanged();
++ }
++
++ QQuick3DTexture *VolumetricFogExtension::iesTexture() const { return m_iesQmlTexture; }
++ void VolumetricFogExtension::setIesTexture(QQuick3DTexture *texture)
++ {
++ if (m_iesQmlTexture == texture) return;
++ m_iesQmlTexture = texture;
++ markDirty(IES);
++ emit iesTextureChanged();
++ }
++
++ int VolumetricFogExtension::iesCount() const { return m_iesCount; }
++ void VolumetricFogExtension::setIesCount(int count)
++ {
++ if (m_iesCount == count) return;
++ m_iesCount = qMax(1, count);
++ markDirty(IES);
++ emit iesCountChanged();
++ }
++
++ QQmlListProperty<IESLightProfileIndex> VolumetricFogExtension::iesLightProfiles()
++ {
++ return QQmlListProperty<IESLightProfileIndex>(this, nullptr,
++ appendIESLightProfile, iesLightProfileCount,
++ iesLightProfileAt, clearIESLightProfiles);
++ }
++
++ void VolumetricFogExtension::appendIESLightProfile(QQmlListProperty<IESLightProfileIndex> *list, IESLightProfileIndex *v)
++ {
++ auto *self = static_cast<VolumetricFogExtension *>(list->object);
++ self->m_iesLightProfiles.append(v);
++ connect(v, &IESLightProfileIndex::lightChanged, self, [self]{ self->markDirty(IES); });
++ connect(v, &IESLightProfileIndex::indexChanged, self, [self]{ self->markDirty(IES); });
++ connect(v, &IESLightProfileIndex::intensityChanged, self, [self]{ self->markDirty(IES); });
++ self->markDirty(IES);
++ emit self->iesLightProfilesChanged();
++ }
++
++ qsizetype VolumetricFogExtension::iesLightProfileCount(QQmlListProperty<IESLightProfileIndex> *list)
++ {
++ return static_cast<VolumetricFogExtension *>(list->object)->m_iesLightProfiles.size();
++ }
++
++ IESLightProfileIndex *VolumetricFogExtension::iesLightProfileAt(QQmlListProperty<IESLightProfileIndex> *list, qsizetype i)
++ {
++ return static_cast<VolumetricFogExtension *>(list->object)->m_iesLightProfiles.at(i);
++ }
++
++ void VolumetricFogExtension::clearIESLightProfiles(QQmlListProperty<IESLightProfileIndex> *list)
++ {
++ auto *self = static_cast<VolumetricFogExtension *>(list->object);
++ for (auto *v : std::as_const(self->m_iesLightProfiles))
++ disconnect(v, nullptr, self, nullptr);
++ self->m_iesLightProfiles.clear();
++ self->markDirty(IES);
++ emit self->iesLightProfilesChanged();
++ }
++
++ QQmlListProperty<Fog3DVolume> VolumetricFogExtension::fogVolumes()
++ {
++ return QQmlListProperty<Fog3DVolume>(this, nullptr,
++ appendFogVolume, fogVolumeCount,
++ fogVolumeAt, clearFogVolumes);
++ }
++
++ void VolumetricFogExtension::appendFogVolume(QQmlListProperty<Fog3DVolume> *list, Fog3DVolume *v)
++ {
++ auto *self = static_cast<VolumetricFogExtension *>(list->object);
++ self->m_fogVolumes.append(v);
++ connect(v, &Fog3DVolume::typeChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::extentsChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::colorChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::densityChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::heightEnabledChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::leastIntenseYChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::mostIntenseYChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::heightCurveChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::noiseOffsetChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &Fog3DVolume::noiseScaleChanged, self, [self]{ self->markDirty(Volumes); });
++ connect(v, &QQuick3DNode::sceneTransformChanged, self, [self]{ self->markDirty(Volumes); });
++ self->markDirty(Volumes);
++ emit self->fogVolumesChanged();
++ }
++
++ qsizetype VolumetricFogExtension::fogVolumeCount(QQmlListProperty<Fog3DVolume> *list)
++ {
++ return static_cast<VolumetricFogExtension *>(list->object)->m_fogVolumes.size();
++ }
++
++ Fog3DVolume *VolumetricFogExtension::fogVolumeAt(QQmlListProperty<Fog3DVolume> *list, qsizetype i)
++ {
++ return static_cast<VolumetricFogExtension *>(list->object)->m_fogVolumes.at(i);
++ }
++
++ void VolumetricFogExtension::clearFogVolumes(QQmlListProperty<Fog3DVolume> *list)
++ {
++ auto *self = static_cast<VolumetricFogExtension *>(list->object);
++ for (auto *v : std::as_const(self->m_fogVolumes))
++ disconnect(v, nullptr, self, nullptr);
++ self->m_fogVolumes.clear();
++ self->markDirty(Volumes);
++ emit self->fogVolumesChanged();
++ }