Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "qssgrhiparticles_p.h"
++ #include "qssgrhicontext_p.h"
++
++ #include <qfloat16.h>
++
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++
++ #include <QtQuick3DRuntimeRender/private/qssgrenderer_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendercamera_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderhelpers_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgshaderresourcemergecontext_p.h>
++
++ #include <ssg/qssgrendercontextcore.h>
++ #include "qssgrendershadercodegenerator_p.h"
++
++ #include "qtquick3d_tracepoints_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ static const QRhiShaderResourceBinding::StageFlags VISIBILITY_ALL =
++ QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
++
++ struct ParticleLightData
++ {
++ QVector4D pointLightPos[4];
++ float pointLightConstantAtt[4] = {1.0f, 1.0f, 1.0f, 1.0f};
++ float pointLightLinearAtt[4] = {0.0f};
++ float pointLightQuadAtt[4] = {0.0f};
++ QVector4D pointLightColor[4];
++ QVector4D spotLightPos[4];
++ float spotLightConstantAtt[4] = {1.0f, 1.0f, 1.0f, 1.0f};
++ float spotLightLinearAtt[4] = {0.0f};
++ float spotLightQuadAtt[4] = {0.0f};
++ QVector4D spotLightColor[4];
++ QVector4D spotLightDir[4];
++ float spotLightConeAngle[4] = {0.0f};
++ float spotLightInnerConeAngle[4] = {0.0f};
++ };
++
++ static bool s_shaderCacheEnabled = true;
++ void QSSGParticleRenderer::setShaderCacheEnabled(bool enabled)
++ {
++ s_shaderCacheEnabled = enabled;
++ }
++
++ void QSSGParticleRenderer::updateUniformsForParticles(const QSSGLayerRenderData &inData,
++ QSSGRhiShaderPipeline &shaders,
++ QSSGRhiContext *rhiCtx,
++ char *ubufData,
++ QSSGParticlesRenderable &renderable,
++ const QSSGRenderCameraList &cameras)
++ {
++ const QMatrix4x4 clipSpaceCorrMatrix = rhiCtx->rhi()->clipSpaceCorrMatrix();
++
++ QSSGRhiShaderPipeline::CommonUniformIndices &cui = shaders.commonUniformIndices;
++
++ const int viewCount = cameras.count();
++
++ // Pull the camera transforms from the render data once.
++ QMatrix4x4 camGlobalTransforms[2] { QMatrix4x4{Qt::Uninitialized}, QMatrix4x4{Qt::Uninitialized} };
++ if (viewCount < 2) {
++ camGlobalTransforms[0] = inData.getGlobalTransform(*cameras[0]);
++ } else {
++ for (size_t viewIndex = 0; viewIndex != std::size(camGlobalTransforms); ++viewIndex)
++ camGlobalTransforms[viewIndex] = inData.getGlobalTransform(*cameras[viewIndex]);
++ }
++
++ if (viewCount < 2) {
++ const QMatrix4x4 projection = clipSpaceCorrMatrix * cameras[0]->projection;
++ shaders.setUniform(ubufData, "qt_projectionMatrix", projection.constData(), 16 * sizeof(float), &cui.projectionMatrixIdx);
++ const QMatrix4x4 viewMatrix = camGlobalTransforms[0].inverted();
++ shaders.setUniform(ubufData, "qt_viewMatrix", viewMatrix.constData(), 16 * sizeof(float), &cui.viewMatrixIdx);
++ } else {
++ QVarLengthArray<QMatrix4x4, 2> projectionMatrices(viewCount);
++ QVarLengthArray<QMatrix4x4, 2> viewMatrices(viewCount);
++ for (int viewIndex = 0; viewIndex < viewCount; ++viewIndex) {
++ projectionMatrices[viewIndex] = clipSpaceCorrMatrix * cameras[viewIndex]->projection;
++ viewMatrices[viewIndex] = camGlobalTransforms[viewIndex].inverted();
++ }
++ shaders.setUniformArray(ubufData, "qt_projectionMatrix", projectionMatrices.constData(), viewCount, QSSGRenderShaderValue::Matrix4x4, &cui.projectionMatrixIdx);
++ shaders.setUniformArray(ubufData, "qt_viewMatrix", viewMatrices.constData(), viewCount, QSSGRenderShaderValue::Matrix4x4, &cui.viewMatrixIdx);
++ }
++
++ const QMatrix4x4 &modelMatrix = renderable.globalTransform;
++ shaders.setUniform(ubufData, "qt_modelMatrix", modelMatrix.constData(), 16 * sizeof(float), &cui.modelMatrixIdx);
++
++ const QVector2D camProperties(cameras[0]->clipPlanes);
++ shaders.setUniform(ubufData, "qt_cameraProperties", &camProperties, 2 * sizeof(float), &cui.cameraPropertiesIdx);
++
++ QVector2D oneOverSize = QVector2D(1.0f, 1.0f);
++ auto &particleBuffer = renderable.particles.m_particleBuffer;
++ const quint32 particlesPerSlice = particleBuffer.particlesPerSlice();
++ oneOverSize = QVector2D(1.0f / particleBuffer.size().width(), 1.0f / particleBuffer.size().height());
++ shaders.setUniform(ubufData, "qt_oneOverParticleImageSize", &oneOverSize, 2 * sizeof(float));
++ shaders.setUniform(ubufData, "qt_countPerSlice", &particlesPerSlice, 1 * sizeof(quint32));
++
++ // Global opacity of the particles node
++ shaders.setUniform(ubufData, "qt_opacity", &renderable.opacity, 1 * sizeof(float));
++
++ float blendImages = renderable.particles.m_blendImages ? 1.0f : 0.0f;
++ float imageCount = float(renderable.particles.m_spriteImageCount);
++ float ooImageCount = 1.0f / imageCount;
++
++ QVector4D spriteConfig(imageCount, ooImageCount, 0.0f, blendImages);
++ shaders.setUniform(ubufData, "qt_spriteConfig", &spriteConfig, 4 * sizeof(float));
++
++ const float billboard = renderable.particles.m_billboard ? 1.0f : 0.0f;
++ shaders.setUniform(ubufData, "qt_billboard", &billboard, 1 * sizeof(float));
++
++ // Lights
++ QVector3D theLightAmbientTotal;
++ bool hasLights = !renderable.particles.m_lights.isEmpty();
++ int pointLight = 0;
++ int spotLight = 0;
++ if (hasLights) {
++ ParticleLightData lightData;
++ auto &lights = renderable.lights;
++ for (quint32 lightIdx = 0, lightEnd = lights.size();
++ lightIdx < lightEnd && lightIdx < QSSG_MAX_NUM_LIGHTS; ++lightIdx) {
++ QSSGRenderLight *theLight(lights[lightIdx].light);
++ // Ignore lights which are not specified for the particle
++ if (!renderable.particles.m_lights.contains(theLight))
++ continue;
++
++ const QMatrix4x4 lightGlobalTransform = inData.getGlobalTransform(*theLight);
++
++ if (theLight->type == QSSGRenderLight::Type::DirectionalLight) {
++ theLightAmbientTotal += theLight->m_diffuseColor * theLight->m_brightness;
++ } else if (theLight->type == QSSGRenderLight::Type::PointLight && pointLight < 4) {
++ lightData.pointLightColor[pointLight] = QVector4D(theLight->m_diffuseColor * theLight->m_brightness, 1.0f);
++ lightData.pointLightPos[pointLight] = QVector4D(QSSGRenderNode::getGlobalPos(lightGlobalTransform), 1.0f);
++ lightData.pointLightConstantAtt[pointLight] = QSSGUtils::aux::translateConstantAttenuation(theLight->m_constantFade);
++ lightData.pointLightLinearAtt[pointLight] = QSSGUtils::aux::translateLinearAttenuation(theLight->m_linearFade);
++ lightData.pointLightQuadAtt[pointLight] = QSSGUtils::aux::translateQuadraticAttenuation(theLight->m_quadraticFade);
++ pointLight++;
++ } else if (theLight->type == QSSGRenderLight::Type::SpotLight && spotLight < 4) {
++ lightData.spotLightColor[spotLight] = QVector4D(theLight->m_diffuseColor * theLight->m_brightness, 1.0f);
++ lightData.spotLightPos[spotLight] = QVector4D(QSSGRenderNode::getGlobalPos(lightGlobalTransform), 1.0f);
++ lightData.spotLightDir[spotLight] = QVector4D(lights[lightIdx].direction, 0.0f);
++ lightData.spotLightConstantAtt[spotLight] = QSSGUtils::aux::translateConstantAttenuation(theLight->m_constantFade);
++ lightData.spotLightLinearAtt[spotLight] = QSSGUtils::aux::translateLinearAttenuation(theLight->m_linearFade);
++ lightData.spotLightQuadAtt[spotLight] = QSSGUtils::aux::translateQuadraticAttenuation(theLight->m_quadraticFade);
++ float coneAngle = theLight->m_coneAngle;
++ // Inner cone angle must always be < cone angle, to not have possible undefined behavior for shader smoothstep
++ float innerConeAngle = std::min(theLight->m_innerConeAngle, coneAngle - 0.01f);
++ lightData.spotLightConeAngle[spotLight] = qDegreesToRadians(coneAngle);
++ lightData.spotLightInnerConeAngle[spotLight] = qDegreesToRadians(innerConeAngle);
++ spotLight++;
++ }
++ theLightAmbientTotal += theLight->m_ambientColor;
++ }
++ // Copy light data
++ int lightOffset = shaders.offsetOfUniform("lightData");
++ if (lightOffset >= 0)
++ memcpy(ubufData + lightOffset, &lightData, sizeof(ParticleLightData));
++ }
++ shaders.setUniform(ubufData, "qt_light_ambient_total", &theLightAmbientTotal, 3 * sizeof(float), &cui.light_ambient_totalIdx);
++ int enablePointLights = pointLight > 0 ? 1 : 0;
++ int enableSpotLights = spotLight > 0 ? 1 : 0;
++ shaders.setUniform(ubufData, "qt_pointLights", &enablePointLights, sizeof(int));
++ shaders.setUniform(ubufData, "qt_spotLights", &enableSpotLights, sizeof(int));
++
++ // Line particle uniform
++ int segmentCount = particleBuffer.segments();
++ if (segmentCount) {
++ shaders.setUniform(ubufData, "qt_lineSegmentCount", &segmentCount, sizeof(int));
++ float alphaFade = renderable.particles.m_alphaFade;
++ float sizeModifier = renderable.particles.m_sizeModifier;
++ float texcoordScale = renderable.particles.m_texcoordScale;
++ auto image = renderable.firstImage;
++ if (image && image->m_texture.m_texture) {
++ const auto size = image->m_texture.m_texture->pixelSize();
++ texcoordScale *= float(size.height()) / float(size.width());
++ }
++ shaders.setUniform(ubufData, "qt_alphaFade", &alphaFade, sizeof(float));
++ shaders.setUniform(ubufData, "qt_sizeModifier", &sizeModifier, sizeof(float));
++ shaders.setUniform(ubufData, "qt_texcoordScale", &texcoordScale, sizeof(float));
++ }
++
++ #ifdef QSSG_OIT_USE_BUFFERS
++ shaders.setOITImages((QRhiTexture*)inData.oitRenderContext.aBuffer,
++ (QRhiTexture*)inData.oitRenderContext.auxBuffer,
++ (QRhiTexture*)inData.oitRenderContext.counterBuffer);
++ if (inData.oitRenderContext.aBuffer) {
++ int abufWidth = 0;
++ int listNodeCount = inData.layer.oitNodeCount;
++ #else
++ const QSSGRhiRenderableTexture *abuf = inData.getRenderResult(QSSGRenderResult::Key::ABufferImage);
++ const QSSGRhiRenderableTexture *aux = inData.getRenderResult(QSSGRenderResult::Key::AuxiliaryImage);
++ const QSSGRhiRenderableTexture *counter = inData.getRenderResult(QSSGRenderResult::Key::CounterImage);
++ shaders.setOITImages(abuf->texture, aux->texture, counter->texture);
++ if (abuf->texture) {
++ int abufWidth = RenderHelpers::rhiCalculateABufferSize(inData.layer.oitNodeCount);
++ int listNodeCount = abufWidth * abufWidth;
++ #endif
++ shaders.setUniform(ubufData, "qt_ABufImageWidth", &abufWidth, sizeof(int), &cui.abufImageWidth);
++ shaders.setUniform(ubufData, "qt_listNodeCount", &listNodeCount, sizeof(int), &cui.listNodeCount);
++ int viewSize[2] = {inData.layerPrepResult.textureDimensions().width(), inData.layerPrepResult.textureDimensions().height()};
++ shaders.setUniform(ubufData, "qt_viewSize", viewSize, sizeof(int) * 2, &cui.viewSize);
++ int samples = rhiCtx->mainPassSampleCount();
++ shaders.setUniform(ubufData, "qt_samples", &samples, sizeof(int), &cui.samples);
++ }
++
++ }
++
++ void QSSGParticleRenderer::updateUniformsForParticleModel(QSSGRhiShaderPipeline &shaderPipeline,
++ char *ubufData,
++ const QSSGRenderModel *model,
++ quint32 offset)
++ {
++ auto &particleBuffer = *model->particleBuffer;
++ const quint32 particlesPerSlice = particleBuffer.particlesPerSlice();
++ const QVector2D oneOverSize = QVector2D(1.0f / particleBuffer.size().width(), 1.0f / particleBuffer.size().height());
++ shaderPipeline.setUniform(ubufData, "qt_oneOverParticleImageSize", &oneOverSize, 2 * sizeof(float));
++ shaderPipeline.setUniform(ubufData, "qt_countPerSlice", &particlesPerSlice, sizeof(quint32));
++ const QMatrix4x4 &particleMatrix = model->particleMatrix;
++ shaderPipeline.setUniform(ubufData, "qt_particleMatrix", &particleMatrix, 16 * sizeof(float));
++ shaderPipeline.setUniform(ubufData, "qt_particleIndexOffset", &offset, sizeof(quint32));
++ }
++
++ static void fillTargetBlend(QRhiGraphicsPipeline::TargetBlend &targetBlend, QSSGRenderParticles::BlendMode mode)
++ {
++ switch (mode) {
++ case QSSGRenderParticles::BlendMode::Screen:
++ targetBlend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
++ targetBlend.dstColor = QRhiGraphicsPipeline::One;
++ targetBlend.srcAlpha = QRhiGraphicsPipeline::One;
++ targetBlend.dstAlpha = QRhiGraphicsPipeline::One;
++ break;
++ case QSSGRenderParticles::BlendMode::Multiply:
++ targetBlend.srcColor = QRhiGraphicsPipeline::DstColor;
++ targetBlend.dstColor = QRhiGraphicsPipeline::Zero;
++ targetBlend.srcAlpha = QRhiGraphicsPipeline::One;
++ targetBlend.dstAlpha = QRhiGraphicsPipeline::One;
++ break;
++ default:
++ // Source over as default
++ targetBlend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
++ targetBlend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
++ targetBlend.srcAlpha = QRhiGraphicsPipeline::One;
++ targetBlend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
++ break;
++
++ }
++ }
++
++ static void sortParticles(const QSSGLayerRenderData &renderData, QByteArray &result, QList<QSSGRhiSortData> &sortData,
++ const QSSGParticleBuffer &buffer, const QSSGRenderParticles &particles,
++ const QVector3D &cameraDirection, bool animatedParticles)
++ {
++ const QMatrix4x4 &modelMatrix = renderData.getGlobalTransform(particles);
++ const QMatrix4x4 &invModelMatrix = modelMatrix.inverted();
++ QVector3D dir = invModelMatrix.map(cameraDirection);
++ QVector3D n = dir.normalized();
++ const auto segments = buffer.segments();
++ auto particleCount = buffer.particleCount();
++ const bool lineParticles = segments > 0;
++ if (lineParticles)
++ particleCount /= segments;
++ sortData.resize(particleCount);
++ sortData.fill({});
++
++ const auto srcParticlePointer = [](int line, int segment, int sc, int ss, int pps, const char *source) -> const QSSGLineParticle * {
++ int pi = (line * sc + segment) / pps;
++ int i = (line * sc + segment) % pps;
++ const QSSGLineParticle *sp = reinterpret_cast<const QSSGLineParticle *>(source + pi * ss);
++ return sp + i;
++ };
++
++ // create sort data
++ {
++ const auto slices = buffer.sliceCount();
++ const auto ss = buffer.sliceStride();
++ const auto pps = buffer.particlesPerSlice();
++
++ QSSGRhiSortData *dst = sortData.data();
++ const char *source = buffer.pointer();
++ const char *begin = source;
++ int i = 0;
++ if (lineParticles) {
++ for (i = 0; i < particleCount; i++) {
++ QSSGRhiSortData lineData;
++ const QSSGLineParticle *lineBegin = srcParticlePointer(i, 0, segments, ss, pps, source);
++ lineData.indexOrOffset = i;
++ lineData.d = QVector3D::dotProduct(lineBegin->position, n);
++ for (int j = 1; j < buffer.segments(); j++) {
++ const QSSGLineParticle *p = srcParticlePointer(i, j, segments, ss, pps, source);
++ lineData.d = qMin(lineData.d, QVector3D::dotProduct(p->position, n));
++ }
++ *dst++ = lineData;
++ }
++ } else if (animatedParticles) {
++ for (int s = 0; s < slices; s++) {
++ const QSSGParticleAnimated *sp = reinterpret_cast<const QSSGParticleAnimated *>(source);
++ for (int p = 0; p < pps && i < particleCount; p++) {
++ *dst = { QVector3D::dotProduct(sp->position, n), int(reinterpret_cast<const char *>(sp) - begin)};
++ sp++;
++ dst++;
++ i++;
++ }
++ source += ss;
++ }
++ } else {
++ for (int s = 0; s < slices; s++) {
++ const QSSGParticleSimple *sp = reinterpret_cast<const QSSGParticleSimple *>(source);
++ for (int p = 0; p < pps && i < particleCount; p++) {
++ *dst = { QVector3D::dotProduct(sp->position, n), int(reinterpret_cast<const char *>(sp) - begin)};
++ sp++;
++ dst++;
++ i++;
++ }
++ source += ss;
++ }
++ }
++ }
++
++ // sort
++ result.resize(buffer.bufferSize());
++ std::sort(sortData.begin(), sortData.end(), [](const QSSGRhiSortData &a, const QSSGRhiSortData &b){
++ return a.d > b.d;
++ });
++
++ auto copyParticles = [&](QByteArray &dst, const QList<QSSGRhiSortData> &data, const QSSGParticleBuffer &buffer) {
++ const auto slices = buffer.sliceCount();
++ const auto ss = buffer.sliceStride();
++ const auto pps = buffer.particlesPerSlice();
++ const QSSGRhiSortData *sdata = data.data();
++ char *dest = dst.data();
++ const char *source = buffer.pointer();
++ int i = 0;
++ if (lineParticles) {
++ int seg = 0;
++ for (int s = 0; s < slices; s++) {
++ QSSGLineParticle *dp = reinterpret_cast<QSSGLineParticle *>(dest);
++ for (int p = 0; p < pps && i < particleCount; p++) {
++ *dp = *srcParticlePointer(sdata->indexOrOffset, seg, segments, ss, pps, source);
++ dp++;
++ seg++;
++ if (seg == segments) {
++ sdata++;
++ i++;
++ seg = 0;
++ }
++ }
++ dest += ss;
++ }
++ } else if (animatedParticles) {
++ for (int s = 0; s < slices; s++) {
++ QSSGParticleAnimated *dp = reinterpret_cast<QSSGParticleAnimated *>(dest);
++ for (int p = 0; p < pps && i < particleCount; p++) {
++ *dp = *reinterpret_cast<const QSSGParticleAnimated *>(source + sdata->indexOrOffset);
++ dp++;
++ sdata++;
++ i++;
++ }
++ dest += ss;
++ }
++ } else {
++ for (int s = 0; s < slices; s++) {
++ QSSGParticleSimple *dp = reinterpret_cast<QSSGParticleSimple *>(dest);
++ for (int p = 0; p < pps && i < particleCount; p++) {
++ *dp = *reinterpret_cast<const QSSGParticleSimple *>(source + sdata->indexOrOffset);
++ dp++;
++ sdata++;
++ i++;
++ }
++ dest += ss;
++ }
++ }
++ };
++
++ // write result
++ copyParticles(result, sortData, buffer);
++ }
++
++ static QByteArray convertParticleData(QByteArray &dest, const QByteArray &data, bool convert)
++ {
++ if (!convert)
++ return data;
++ int count = data.size() / 4;
++ if (dest.size() != count * 2)
++ dest.resize(2 * count);
++ qFloatToFloat16(reinterpret_cast<qfloat16 *>(dest.data()), reinterpret_cast<const float *>(data.constData()), count);
++ return dest;
++ }
++
++ void QSSGParticleRenderer::rhiPrepareRenderable(QSSGRhiShaderPipeline &shaderPipeline,
++ QSSGPassKey passKey,
++ QSSGRhiContext *rhiCtx,
++ QSSGRhiGraphicsPipelineState *ps,
++ QSSGParticlesRenderable &renderable,
++ const QSSGLayerRenderData &inData,
++ QRhiRenderPassDescriptor *renderPassDescriptor,
++ int samples,
++ int viewCount,
++ QSSGRenderCamera *alteredCamera,
++ QSSGRenderTextureCubeFace cubeFace,
++ QSSGReflectionMapEntry *entry,
++ bool oit)
++ {
++ const void *node = &renderable.particles;
++ const bool needsConversion = !rhiCtx->rhi()->isTextureFormatSupported(QRhiTexture::RGBA32F);
++
++ const auto cubeFaceIdx = QSSGBaseTypeHelpers::indexOfCubeFace(cubeFace);
++ QSSGRhiDrawCallData &dcd = QSSGRhiContextPrivate::get(rhiCtx)->drawCallData({ passKey, node, nullptr, entry,
++ cubeFaceIdx });
++ shaderPipeline.ensureUniformBuffer(&dcd.ubuf);
++
++ char *ubufData = dcd.ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
++ if (!alteredCamera) {
++ updateUniformsForParticles(inData, shaderPipeline, rhiCtx, ubufData, renderable, inData.renderedCameras);
++ } else {
++ QSSGRenderCameraList cameras({ alteredCamera });
++ updateUniformsForParticles(inData, shaderPipeline, rhiCtx, ubufData, renderable, cameras);
++ }
++ dcd.ubuf->endFullDynamicBufferUpdateForCurrentFrame();
++
++ QSSGRhiParticleData &particleData = QSSGRhiContextPrivate::get(rhiCtx)->particleData(&renderable.particles);
++ const QSSGParticleBuffer &particleBuffer = renderable.particles.m_particleBuffer;
++ int particleCount = particleBuffer.particleCount();
++ if (particleData.texture == nullptr || particleData.particleCount != particleCount) {
++ QSize size(particleBuffer.size());
++ if (!particleData.texture) {
++ particleData.texture = rhiCtx->rhi()->newTexture(needsConversion ? QRhiTexture::RGBA16F : QRhiTexture::RGBA32F, size);
++ particleData.texture->create();
++ } else {
++ particleData.texture->setPixelSize(size);
++ particleData.texture->create();
++ }
++ particleData.particleCount = particleCount;
++ }
++
++ bool sortingChanged = particleData.sorting != renderable.particles.m_depthSorting;
++ if (sortingChanged && !renderable.particles.m_depthSorting) {
++ particleData.sortData.clear();
++ particleData.sortedData.clear();
++ }
++ particleData.sorting = renderable.particles.m_depthSorting;
++
++ QByteArray uploadData;
++
++ if (renderable.particles.m_depthSorting) {
++ bool animatedParticles = renderable.particles.m_featureLevel == QSSGRenderParticles::FeatureLevel::Animated;
++ if (!alteredCamera) {
++ sortParticles(inData, particleData.sortedData, particleData.sortData, particleBuffer, renderable.particles, inData.renderedCameraData.value()[0].direction, animatedParticles);
++ } else {
++ const QMatrix4x4 globalTransform = inData.getGlobalTransform(*alteredCamera);
++ sortParticles(inData, particleData.sortedData, particleData.sortData, particleBuffer, renderable.particles, QSSGRenderNode::getScalingCorrectDirection(globalTransform), animatedParticles);
++ }
++ uploadData = convertParticleData(particleData.convertData, particleData.sortedData, needsConversion);
++ } else {
++ uploadData = convertParticleData(particleData.convertData, particleBuffer.data(), needsConversion);
++ }
++
++ QRhiResourceUpdateBatch *rub = rhiCtx->rhi()->nextResourceUpdateBatch();
++ QRhiTextureSubresourceUploadDescription upload;
++ upload.setData(uploadData);
++ QRhiTextureUploadDescription uploadDesc(QRhiTextureUploadEntry(0, 0, upload));
++ rub->uploadTexture(particleData.texture, uploadDesc);
++ rhiCtx->commandBuffer()->resourceUpdate(rub);
++
++ auto &ia = QSSGRhiInputAssemblerStatePrivate::get(*ps);
++ ia.topology = QRhiGraphicsPipeline::TriangleStrip;
++ ia.inputLayout = QRhiVertexInputLayout();
++ ia.inputs.clear();
++
++ ps->samples = samples;
++ ps->viewCount = viewCount;
++ ps->cullMode = QRhiGraphicsPipeline::None;
++
++ if (inData.orderIndependentTransparencyEnabled == false) {
++ if (renderable.renderableFlags.hasTransparency())
++ fillTargetBlend(ps->targetBlend[0], renderable.particles.m_blendMode);
++ else
++ ps->targetBlend[0] = QRhiGraphicsPipeline::TargetBlend();
++ }
++
++ QSSGRhiShaderResourceBindingList bindings;
++ bindings.addUniformBuffer(0, VISIBILITY_ALL, dcd.ubuf, 0, shaderPipeline.ub0Size());
++
++ // Texture maps
++ // we only have one image
++ QSSGRenderableImage *renderableImage = renderable.firstImage;
++
++ int samplerBinding = shaderPipeline.bindingForTexture("qt_sprite");
++ if (samplerBinding >= 0) {
++ QRhiTexture *texture = renderableImage ? renderableImage->m_texture.m_texture : nullptr;
++ if (samplerBinding >= 0 && texture) {
++ const bool mipmapped = texture->flags().testFlag(QRhiTexture::MipMapped);
++ QRhiSampler *sampler = rhiCtx->sampler({ QSSGRhiHelpers::toRhi(renderableImage->m_imageNode.m_minFilterType),
++ QSSGRhiHelpers::toRhi(renderableImage->m_imageNode.m_magFilterType),
++ mipmapped ? QSSGRhiHelpers::toRhi(renderableImage->m_imageNode.m_mipFilterType) : QRhiSampler::None,
++ QSSGRhiHelpers::toRhi(renderableImage->m_imageNode.m_horizontalTilingMode),
++ QSSGRhiHelpers::toRhi(renderableImage->m_imageNode.m_verticalTilingMode),
++ QSSGRhiHelpers::toRhi(renderableImage->m_imageNode.m_depthTilingMode)
++ });
++ bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::FragmentStage, texture, sampler);
++ } else {
++ QRhiResourceUpdateBatch *rub = rhiCtx->rhi()->nextResourceUpdateBatch();
++ QRhiTexture *texture = rhiCtx->dummyTexture({}, rub, QSize(4, 4), Qt::white);
++ rhiCtx->commandBuffer()->resourceUpdate(rub);
++ QRhiSampler *sampler = rhiCtx->sampler({ QRhiSampler::Nearest,
++ QRhiSampler::Nearest,
++ QRhiSampler::None,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::Repeat
++ });
++ bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::FragmentStage, texture, sampler);
++ }
++ }
++
++ samplerBinding = shaderPipeline.bindingForTexture("qt_particleTexture");
++ if (samplerBinding >= 0) {
++ QRhiTexture *texture = particleData.texture;
++ if (samplerBinding >= 0 && texture) {
++ QRhiSampler *sampler = rhiCtx->sampler({ QRhiSampler::Nearest,
++ QRhiSampler::Nearest,
++ QRhiSampler::None,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::Repeat
++ });
++ bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::VertexStage, texture, sampler);
++ }
++ }
++
++ samplerBinding = shaderPipeline.bindingForTexture("qt_colorTable");
++ if (samplerBinding >= 0) {
++ bool hasTexture = false;
++ if (renderable.colorTable) {
++ QRhiTexture *texture = renderable.colorTable->m_texture.m_texture;
++ if (texture) {
++ hasTexture = true;
++ QRhiSampler *sampler = rhiCtx->sampler({ QRhiSampler::Nearest,
++ QRhiSampler::Nearest,
++ QRhiSampler::None,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::Repeat
++ });
++ bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::FragmentStage, texture, sampler);
++ }
++ }
++
++ if (!hasTexture) {
++ QRhiResourceUpdateBatch *rub = rhiCtx->rhi()->nextResourceUpdateBatch();
++ QRhiTexture *texture = rhiCtx->dummyTexture({}, rub, QSize(4, 4), Qt::white);
++ rhiCtx->commandBuffer()->resourceUpdate(rub);
++ QRhiSampler *sampler = rhiCtx->sampler({ QRhiSampler::Nearest,
++ QRhiSampler::Nearest,
++ QRhiSampler::None,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::Repeat
++ });
++ bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::FragmentStage, texture, sampler);
++ }
++ }
++
++ if (oit && inData.layer.oitMethod == QSSGRenderLayer::OITMethod::LinkedList)
++ RenderHelpers::addAccumulatorImageBindings(&shaderPipeline, bindings);
++
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
++
++ // TODO: This is identical to other renderables. Make into a function?
++ QRhiShaderResourceBindings *&srb = dcd.srb;
++ bool srbChanged = false;
++ if (!srb || bindings != dcd.bindings) {
++ srb = rhiCtxD->srb(bindings);
++ rhiCtxD->releaseCachedSrb(dcd.bindings);
++ dcd.bindings = bindings;
++ srbChanged = true;
++ }
++
++ if (cubeFace == QSSGRenderTextureCubeFaceNone)
++ renderable.rhiRenderData.mainPass.srb = srb;
++ else
++ renderable.rhiRenderData.reflectionPass.srb[cubeFaceIdx] = srb;
++
++ const auto pipelineKey = QSSGGraphicsPipelineStateKey::create(*ps, renderPassDescriptor, srb);
++ if (dcd.pipeline
++ && !srbChanged
++ && dcd.renderTargetDescriptionHash == pipelineKey.extra.renderTargetDescriptionHash
++ && dcd.renderTargetDescription == pipelineKey.renderTargetDescription
++ && dcd.ps == *ps)
++ {
++ if (cubeFace == QSSGRenderTextureCubeFaceNone)
++ renderable.rhiRenderData.mainPass.pipeline = dcd.pipeline;
++ else
++ renderable.rhiRenderData.reflectionPass.pipeline = dcd.pipeline;
++ } else {
++ if (cubeFace == QSSGRenderTextureCubeFaceNone) {
++ renderable.rhiRenderData.mainPass.pipeline = rhiCtxD->pipeline(pipelineKey,
++ renderPassDescriptor,
++ srb);
++ dcd.pipeline = renderable.rhiRenderData.mainPass.pipeline;
++ } else {
++ renderable.rhiRenderData.reflectionPass.pipeline = rhiCtxD->pipeline(pipelineKey,
++ renderPassDescriptor,
++ srb);
++ dcd.pipeline = renderable.rhiRenderData.reflectionPass.pipeline;
++ }
++ dcd.renderTargetDescriptionHash = pipelineKey.extra.renderTargetDescriptionHash;
++ dcd.renderTargetDescription = pipelineKey.renderTargetDescription;
++ dcd.ps = *ps;
++ }
++ }
++
++ void QSSGParticleRenderer::prepareParticlesForModel(QSSGRhiShaderPipeline &shaderPipeline,
++ QSSGRhiContext *rhiCtx,
++ QSSGRhiShaderResourceBindingList &bindings,
++ const QSSGRenderModel *model)
++ {
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
++ QSSGRhiParticleData &particleData = rhiCtxD->particleData(model);
++ const QSSGParticleBuffer &particleBuffer = *model->particleBuffer;
++ int particleCount = particleBuffer.particleCount();
++ bool update = particleBuffer.serial() != particleData.serial;
++ const bool needsConversion = !rhiCtx->rhi()->isTextureFormatSupported(QRhiTexture::RGBA32F);
++ if (particleData.texture == nullptr || particleData.particleCount != particleCount) {
++ QSize size(particleBuffer.size());
++ if (!particleData.texture) {
++ particleData.texture = rhiCtx->rhi()->newTexture(needsConversion ? QRhiTexture::RGBA16F : QRhiTexture::RGBA32F, size);
++ particleData.texture->create();
++ } else {
++ particleData.texture->setPixelSize(size);
++ particleData.texture->create();
++ }
++ particleData.particleCount = particleCount;
++ update = true;
++ }
++
++ if (update) {
++ QRhiResourceUpdateBatch *rub = rhiCtx->rhi()->nextResourceUpdateBatch();
++ QRhiTextureSubresourceUploadDescription upload;
++ upload.setData(convertParticleData(particleData.convertData, particleBuffer.data(), needsConversion));
++ QRhiTextureUploadDescription uploadDesc(QRhiTextureUploadEntry(0, 0, upload));
++ rub->uploadTexture(particleData.texture, uploadDesc);
++ rhiCtx->commandBuffer()->resourceUpdate(rub);
++ }
++ particleData.serial = particleBuffer.serial();
++ int samplerBinding = shaderPipeline.bindingForTexture("qt_particleTexture");
++ if (samplerBinding >= 0) {
++ QRhiTexture *texture = particleData.texture;
++ if (samplerBinding >= 0 && texture) {
++ QRhiSampler *sampler = rhiCtx->sampler({ QRhiSampler::Nearest,
++ QRhiSampler::Nearest,
++ QRhiSampler::None,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::Repeat
++ });
++ bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::VertexStage, texture, sampler);
++ }
++ }
++ }
++
++ static QByteArray particlesLogPrefix() { return QByteArrayLiteral("particle material pipeline-- "); }
++
++ struct ShaderGeneratorCommon
++ {
++ QSSGStageGeneratorBase *vs;
++ QSSGStageGeneratorBase *fs;
++ ShaderGeneratorCommon(QSSGProgramGenerator &gen)
++ : vs(gen.getStage(QSSGShaderGeneratorStage::Vertex)), fs(gen.getStage(QSSGShaderGeneratorStage::Fragment))
++ {
++
++ }
++ void addUniform(const QByteArray &name, const QByteArray &type)
++ {
++ vs->addUniform(name, type);
++ fs->addUniform(name, type);
++ }
++ void addUniformArray(const QByteArray &name, const QByteArray &type, quint32 size)
++ {
++ vs->addUniformArray(name, type, size);
++ fs->addUniformArray(name, type, size);
++ }
++ void addDefinition(const QByteArray &name, const QByteArray &value)
++ {
++ vs->addDefinition(name, value);
++ fs->addDefinition(name, value);
++ }
++ void addInterpolant(const QByteArray &name, const QByteArray &type)
++ {
++ vs->addOutgoing(name, type);
++ fs->addIncoming(name, type);
++ }
++ void addFlatInterpolant(const QByteArray &name, const QByteArray &type)
++ {
++ vs->addFlatOutgoing(name, type);
++ fs->addFlatIncoming(name, type);
++ }
++ void addPrefix(const QByteArray &prefix)
++ {
++ vs->addPrefix(prefix);
++ fs->addPrefix(prefix);
++ }
++ };
++
++ struct AutoFormatGenerator
++ {
++ QSSGStageGeneratorBase &gen;
++ int indent = 0;
++ AutoFormatGenerator(QSSGStageGeneratorBase &g)
++ : gen(g)
++ {
++
++ }
++ AutoFormatGenerator &operator<<(const QByteArray &data)
++ {
++ for (int i = 0; i < indent; i++)
++ gen << " ";
++ gen << data << "\n";
++ return *this;
++ }
++ void incIndent()
++ {
++ indent++;
++ }
++ void decIndent()
++ {
++ indent--;
++ Q_ASSERT(indent >= 0);
++ }
++ };
++
++ static const char *s_lightPrefix = {
++ "struct ParticleLightData\n" \
++ "{\n"
++ " vec4 qt_pointLightPosition[4];\n" \
++ " vec4 qt_pointLightConstantAtt;\n" \
++ " vec4 qt_pointLightLinearAtt;\n" \
++ " vec4 qt_pointLightQuadAtt;\n" \
++ " vec4 qt_pointLightColor[4];\n" \
++ " vec4 qt_spotLightPosition[4];\n" \
++ " vec4 qt_spotLightConstantAtt;\n" \
++ " vec4 qt_spotLightLinearAtt;\n" \
++ " vec4 qt_spotLightQuadAtt;\n" \
++ " vec4 qt_spotLightColor[4];\n" \
++ " vec4 qt_spotLightDirection[4];\n" \
++ " vec4 qt_spotLightConeAngle;\n" \
++ " vec4 qt_spotLightInnerConeAngle;\n" \
++ "};\n"
++ };
++
++ QSSGShaderFeatures QSSGParticleRenderer::particleShaderFeatures(const QSSGShaderFeatures& features)
++ {
++ #define COPY_FEATURE(f) ret.set(f, features.isSet(f));
++ QSSGShaderFeatures ret;
++ COPY_FEATURE(QSSGShaderFeatures::Feature::LinearTonemapping);
++ COPY_FEATURE(QSSGShaderFeatures::Feature::AcesTonemapping);
++ COPY_FEATURE(QSSGShaderFeatures::Feature::HejlDawsonTonemapping);
++ COPY_FEATURE(QSSGShaderFeatures::Feature::FilmicTonemapping);
++ COPY_FEATURE(QSSGShaderFeatures::Feature::DisableMultiView);
++ #undef COPY_FEATURE
++ return ret;
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGParticleRenderer::generateRhiShaderPipeline(QSSGRenderer &renderer,
++ QSSGParticlesRenderable &inRenderable,
++ const QSSGShaderFeatures &inFeatureSet,
++ QByteArray &shaderString,
++ const QSSGShaderParticleMaterialKeyProperties &shaderKeyProperties)
++ {
++ const auto &m_contextInterface = renderer.m_contextInterface;
++ const auto &shaderCache = m_contextInterface->shaderCache();
++ const auto &shaderProgramGenerator = m_contextInterface->shaderProgramGenerator();
++ const auto &shaderLibraryManager = m_contextInterface->shaderLibraryManager();
++
++ shaderString = particlesLogPrefix();
++ QSSGShaderParticleMaterialKey theKey(inRenderable.shaderDescription);
++
++ theKey.toString(shaderString, shaderKeyProperties);
++
++ if (s_shaderCacheEnabled) {
++ if (const auto &maybePipeline = shaderCache->tryGetRhiShaderPipeline(shaderString, inFeatureSet))
++ return maybePipeline;
++
++ const QByteArray qsbcKey = QQsbCollection::EntryDesc::generateSha(shaderString, QQsbCollection::toFeatureSet(inFeatureSet));
++ // Check if there's a pre-built (offline generated) shader for available.
++ if (renderer.m_currentLayer) {
++ QQsbCollection::EntryMap &pregenEntries = renderer.m_currentLayer->m_particleShaderEntries;
++ if (pregenEntries.isEmpty()) {
++ renderer.m_currentLayer->m_particleShaderEntries = shaderLibraryManager->getParticleShaderEntries();
++ pregenEntries = renderer.m_currentLayer->m_particleShaderEntries;
++ }
++ if (!pregenEntries.isEmpty()) {
++ const auto foundIt = pregenEntries.constFind(QQsbCollection::Entry(qsbcKey));
++ if (foundIt != pregenEntries.cend())
++ return shaderCache->newPipelineFromPregenerated(shaderString, inFeatureSet, *foundIt, inRenderable.particles);
++ }
++ }
++
++ // Try the persistent (disk-based) cache then.
++ if (const auto &maybePipeline = shaderCache->tryNewPipelineFromPersistentCache(qsbcKey, shaderString, inFeatureSet))
++ return maybePipeline;
++ }
++
++ // generate shader code
++ shaderProgramGenerator->beginProgram();
++ auto &vertex = *shaderProgramGenerator->getStage(QSSGShaderGeneratorStage::Vertex);
++ auto &fragment = *shaderProgramGenerator->getStage(QSSGShaderGeneratorStage::Fragment);
++ ShaderGeneratorCommon common(*shaderProgramGenerator);
++ const bool isSpriteLinear = shaderKeyProperties.m_isSpriteLinear.getValue(theKey);
++ const bool isColorTableLinear = shaderKeyProperties.m_isColorTableLinear.getValue(theKey);
++ const bool lighting = shaderKeyProperties.m_hasLighting.getValue(theKey);
++ const bool isMapped = shaderKeyProperties.m_isMapped.getValue(theKey);
++ const bool isLineParticle = shaderKeyProperties.m_isLineParticle.getValue(theKey);
++ const bool isAnimated = shaderKeyProperties.m_isAnimated.getValue(theKey);
++ const int viewCount = inFeatureSet.isSet(QSSGShaderFeatures::Feature::DisableMultiView)
++ ? 1 : shaderKeyProperties.m_viewCount.getValue(theKey);
++ const int oit = shaderKeyProperties.m_orderIndependentTransparency.getValue(theKey);
++ const bool oitMSAA = shaderKeyProperties.m_oitMSAA.getValue(theKey);
++
++ if (isAnimated)
++ common.addDefinition("QSSG_PARTICLES_ENABLE_ANIMATED", "1");
++ if (isLineParticle)
++ common.addDefinition("QSSG_PARTICLES_ENABLE_LINE_PARTICLE", "1");
++ if (oit)
++ common.addDefinition("QSSG_OIT_METHOD", QStringLiteral("%1").arg(oit).toUtf8());
++ if (oitMSAA)
++ common.addDefinition("QSSG_MULTISAMPLE", "1");
++
++ common.addUniform("qt_modelMatrix", "mat4");
++ if (viewCount >= 2) {
++ common.addUniformArray("qt_viewMatrix", "mat4", viewCount);
++ common.addUniformArray("qt_projectionMatrix", "mat4", viewCount);
++ } else {
++ common.addUniform("qt_viewMatrix", "mat4");
++ common.addUniform("qt_projectionMatrix", "mat4");
++ }
++ if (lighting) {
++ common.addPrefix(s_lightPrefix);
++ common.addUniform("lightData", "ParticleLightData");
++ }
++ common.addUniform("qt_spriteConfig", "vec4");
++ common.addUniform("qt_light_ambient_total", "vec3");
++ common.addUniform("qt_oneOverParticleImageSize", "vec2");
++ common.addUniform("qt_countPerSlice", "uint");
++ if (isLineParticle)
++ common.addUniform("qt_lineSegmentCount", "uint");
++ common.addUniform("qt_billboard", "float");
++ common.addUniform("qt_opacity", "float");
++ if (isLineParticle) {
++ common.addUniform("qt_alphaFade", "float");
++ common.addUniform("qt_sizeModifier", "float");
++ common.addUniform("qt_texcoordScale", "float");
++ }
++ if (lighting) {
++ common.addUniform("qt_pointLights", "bool");
++ common.addUniform("qt_spotLights", "bool");
++ }
++ if (oit == (int)QSSGRenderLayer::OITMethod::WeightedBlended) {
++ common.addUniform("qt_cameraProperties", "vec2");
++ } else if (oit == (int)QSSGRenderLayer::OITMethod::LinkedList) {
++ common.addUniform("qt_viewSize", "ivec2");
++ common.addUniform("qt_ABufImageWidth", "uint");
++ common.addUniform("qt_listNodeCount", "uint");
++ #ifdef QSSG_OIT_USE_BUFFERS
++ common.addUniform("qt_samples", "uint");
++ #endif
++ }
++
++ if (lighting)
++ vertex.addInclude("particleLighting.glsllib");
++
++ vertex.addUniform("qt_particleTexture", "sampler2D");
++
++ vertex.addInclude("particleLoading.glsllib");
++
++ common.addInterpolant("color", "vec4");
++ common.addInterpolant("spriteData", "vec2");
++ common.addInterpolant("texcoord", "vec2");
++ common.addFlatInterpolant("instanceIndex", "uint");
++ if (oit == (int)QSSGRenderLayer::OITMethod::LinkedList && viewCount >= 2)
++ common.addFlatInterpolant("v_viewIndex", "uint");
++
++ AutoFormatGenerator vg(vertex);
++ if (!isLineParticle)
++ vg << "const vec2 corners[4] = {{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}};";
++
++ // vertex shader main
++ vg << "out gl_PerVertex {"
++ << " vec4 gl_Position;"
++ << "};";
++
++ vg << "void main() {";
++ vg.incIndent();
++ if (isLineParticle) {
++ vg << "uint segmentIndex = gl_VertexIndex / 2;"
++ << "uint particleIndex = segmentIndex + gl_InstanceIndex * qt_lineSegmentCount;";
++ } else {
++ vg << "uint particleIndex = gl_InstanceIndex;"
++ << "uint cornerIndex = gl_VertexIndex;"
++ << "vec2 corner = corners[cornerIndex];";
++ }
++ vg << "instanceIndex = particleIndex;"
++ << "Particle p = qt_loadParticle(particleIndex);";
++
++ if (isLineParticle) {
++ vg << "float side = float(mod(gl_VertexIndex, 2)) - 0.5;"
++ << "float size = p.size * qt_calcSizeFactor(segmentIndex);"
++ << "vec3 o = p.binormal * side * size;"
++ << "vec4 worldPos = qt_modelMatrix * vec4(p.position + (1.0 - qt_billboard) * o, 1.0);";
++ if (viewCount >= 2) {
++ vg << "vec4 viewPos = qt_viewMatrix[gl_ViewIndex] * worldPos;"
++ << "vec3 viewBN = (transpose(inverse(qt_viewMatrix[gl_ViewIndex] * qt_modelMatrix)) * vec4(p.binormal, 0.0)).xyz;";
++ } else {
++ vg << "vec4 viewPos = qt_viewMatrix * worldPos;"
++ << "vec3 viewBN = (transpose(inverse(qt_viewMatrix * qt_modelMatrix)) * vec4(p.binormal, 0.0)).xyz;";
++ }
++ vg << "viewBN.z = 0;"
++ << "viewBN = normalize(viewBN) * side * size;"
++ << "viewPos = viewPos + vec4(viewBN, 0.0) * qt_billboard;"
++ << "texcoord.x = p.segmentLength * qt_texcoordScale;"
++ << "texcoord.y = float(mod(gl_VertexIndex, 2));"
++ << "color = p.color;"
++ << "color.a *= qt_opacity * qt_calcAlphaFade(segmentIndex);";
++ } else {
++ vg << "mat3 rotMat = qt_fromEulerRotation(p.rotation);"
++ << "vec4 worldPos = qt_modelMatrix * vec4(p.position, 1.0);"
++ << "vec4 viewPos = worldPos;"
++ << "vec3 offset = (p.size * vec3(corner - vec2(0.5), 0.0));"
++ << "viewPos.xyz += offset * rotMat * (1.0 - qt_billboard);";
++ if (viewCount >= 2)
++ vg << "viewPos = qt_viewMatrix[gl_ViewIndex] * viewPos;";
++ else
++ vg << "viewPos = qt_viewMatrix * viewPos;";
++ vg << "viewPos.xyz += rotMat * offset * qt_billboard;"
++ << "texcoord = corner;"
++ << "color = p.color;"
++ << "color.a *= qt_opacity;";
++ }
++ if (lighting)
++ vg << "color.rgb *= qt_calcLightColor(worldPos.xyz);";
++ if (viewCount >= 2) {
++ if (oit == int(QSSGRenderLayer::OITMethod::LinkedList))
++ vg << "v_viewIndex = gl_ViewIndex;";
++ vg << "gl_Position = qt_projectionMatrix[gl_ViewIndex] * viewPos;";
++ } else {
++ vg << "gl_Position = qt_projectionMatrix * viewPos;";
++ }
++ if (isMapped)
++ vg << "spriteData.x = qt_ageToSpriteFactor(p.age);";
++ if (isAnimated)
++ vg << "spriteData.y = p.animationFrame;";
++ vg.decIndent();
++ vg << "}";
++
++ // fragment shader main
++ fragment.addInclude("tonemapping.glsllib");
++ fragment.addInclude("particleSprite.glsllib");
++ fragment.addUniform("qt_sprite", "sampler2D");
++
++ AutoFormatGenerator fg(fragment);
++
++ if (oit == int(QSSGRenderLayer::OITMethod::WeightedBlended)) {
++ fragment.addInclude("oitweightedblended.glsllib");
++ fg << "layout(location = 1) out vec4 revealageOutput;";
++ } else if (oit == int(QSSGRenderLayer::OITMethod::LinkedList)) {
++ #ifdef QSSG_OIT_USE_BUFFERS
++ fragment.addInclude("oitlinkedlist_buf.glsllib");
++ QSSGShaderResourceMergeContext::setAdditionalBufferAmount(3);
++ #else
++ fragment.addInclude("oitlinkedlist.glsllib");
++ #endif
++ }
++ if (isSpriteLinear)
++ fg << "#define spriteFunc";
++ else
++ fg << "#define spriteFunc qt_sRGBToLinear";
++ if (isMapped) {
++ fragment.addUniform("qt_colorTable", "sampler2D");
++ if (isColorTableLinear)
++ fg << "#define colorTableFunc";
++ else
++ fg << "#define colorTableFunc qt_sRGBToLinear";
++ }
++
++ fg << "vec4 qt_readSprite() {";
++ fg.incIndent();
++ if (isAnimated) {
++ fg << "vec2 coords[2];"
++ << "float factor = qt_spriteCoords(coords);"
++ << "return mix(spriteFunc(texture(qt_sprite, coords[0])), spriteFunc(texture(qt_sprite, coords[1])), factor);";
++ } else {
++ fg << "return spriteFunc(texture(qt_sprite, texcoord));";
++ }
++ fg.decIndent();
++ fg << "}";
++ fg << "vec4 qt_readColor() {";
++ fg.incIndent();
++ if (isMapped)
++ fg << "return color * colorTableFunc(texture(qt_colorTable, vec2(spriteData.x, qt_rand(vec2(instanceIndex, 0)))));";
++ else
++ fg << "return color;";
++ fg.decIndent();
++ fg << "}";
++ fg << "void main() {";
++ fg.incIndent();
++ fg << "vec4 ret = qt_readColor() * qt_readSprite();";
++ if (oit) {
++ if (oit == int(QSSGRenderLayer::OITMethod::WeightedBlended)) {
++ fg << "float z = abs(gl_FragCoord.z);"
++ << "float distWeight = qt_transparencyWeight(z, ret.a, qt_cameraProperties.y);"
++ << "fragOutput = distWeight * qt_tonemap(ret);"
++ << "revealageOutput = vec4(ret.a);";
++ } else if (oit == int(QSSGRenderLayer::OITMethod::LinkedList)) {
++ #ifdef QSSG_OIT_USE_BUFFERS
++ if (viewCount >= 2)
++ fg << "fragOutput = qt_oitLinkedList(ret, qt_listNodeCount, qt_ABufImageWidth, qt_viewSize, v_viewIndex, qt_samples);";
++ else
++ fg << "fragOutput = qt_oitLinkedList(ret, qt_listNodeCount, qt_ABufImageWidth, qt_viewSize, 0, qt_samples);";
++ #else
++ if (viewCount >= 2)
++ fg << "fragOutput = qt_oitLinkedList(ret, qt_listNodeCount, qt_ABufImageWidth, qt_viewSize, v_viewIndex);";
++ else
++ fg << "fragOutput = qt_oitLinkedList(ret, qt_listNodeCount, qt_ABufImageWidth, qt_viewSize, 0);";
++ #endif
++ }
++ } else {
++ fg << "fragOutput = qt_tonemap(ret);";
++ }
++ fg.decIndent();
++ fg << "}";
++
++
++ return shaderProgramGenerator->compileGeneratedRhiShader(shaderString,
++ inFeatureSet,
++ *shaderLibraryManager,
++ *shaderCache,
++ QSSGRhiShaderPipeline::UsedWithoutIa,
++ {},
++ shaderKeyProperties.m_viewCount.getValue(inRenderable.shaderDescription),
++ false);
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGParticleRenderer::getShaderPipelineParticles(QSSGRenderer &renderer,
++ QSSGParticlesRenderable &inRenderable,
++ const QSSGShaderFeatures &inFeatureSet)
++ {
++ const auto features = particleShaderFeatures(inFeatureSet);
++ auto *m_currentLayer = renderer.m_currentLayer;
++ QSSG_ASSERT(m_currentLayer != nullptr, return {});
++
++ auto &shaderMap = m_currentLayer->particleShaderMap;
++
++ QElapsedTimer timer;
++ timer.start();
++
++ QSSGRhiShaderPipelinePtr shaderPipeline;
++
++ // This just references inFeatureSet and inRenderable.shaderDescription -
++ // cheap to construct and is good enough for the find()
++ QSSGParticleShaderMapKey skey = QSSGParticleShaderMapKey(QByteArray(),
++ features,
++ inRenderable.shaderDescription);
++ auto it = shaderMap.find(skey);
++ if (it == shaderMap.end()) {
++ Q_TRACE_SCOPE(QSSG_generateShader);
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DGenerateShader);
++ auto &shaderString = renderer.m_currentLayer->generatedShaderString;
++ shaderPipeline = QSSGParticleRenderer::generateRhiShaderPipeline(renderer, inRenderable, features, shaderString, m_currentLayer->particleMaterialShaderKeyProperties);
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DGenerateShader, 0, inRenderable.particles.profilingId);
++ // make skey useable as a key for the QHash (makes a copy of the materialKey, instead of just referencing)
++ skey.detach();
++ // insert it no matter what, no point in trying over and over again
++ shaderMap.insert(skey, shaderPipeline);
++ } else {
++ shaderPipeline = it.value();
++ }
++
++ if (shaderPipeline != nullptr) {
++ if (m_currentLayer && !m_currentLayer->renderedCameras.isEmpty())
++ m_currentLayer->ensureCachedCameraDatas();
++ }
++
++ const auto &rhiContext = renderer.m_contextInterface->rhiContext();
++ QSSGRhiContextStats::get(*rhiContext).registerMaterialShaderGenerationTime(timer.elapsed());
++
++ return shaderPipeline;
++ }
++
++ void QSSGParticleRenderer::rhiRenderRenderable(QSSGRhiContext *rhiCtx,
++ QSSGParticlesRenderable &renderable,
++ bool *needsSetViewport,
++ QSSGRenderTextureCubeFace cubeFace,
++ const QSSGRhiGraphicsPipelineState &state)
++ {
++ QRhiGraphicsPipeline *ps = renderable.rhiRenderData.mainPass.pipeline;
++ QRhiShaderResourceBindings *srb = renderable.rhiRenderData.mainPass.srb;
++
++ if (cubeFace != QSSGRenderTextureCubeFaceNone) {
++ const auto cubeFaceIdx = QSSGBaseTypeHelpers::indexOfCubeFace(cubeFace);
++ ps = renderable.rhiRenderData.reflectionPass.pipeline;
++ srb = renderable.rhiRenderData.reflectionPass.srb[cubeFaceIdx];
++ }
++
++ if (!ps || !srb)
++ return;
++
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderCall);
++
++ QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
++ // QRhi optimizes out unnecessary binding of the same pipline
++ cb->setGraphicsPipeline(ps);
++ cb->setVertexInput(0, 0, nullptr);
++ cb->setShaderResources(srb);
++
++ if (needsSetViewport && *needsSetViewport) {
++ cb->setViewport(state.viewport);
++ *needsSetViewport = false;
++ }
++ if (renderable.particles.m_featureLevel >= QSSGRenderParticles::FeatureLevel::Line) {
++ // draw triangle strip with 2 * segmentCount vertices N times
++ int S = renderable.particles.m_particleBuffer.segments();
++ int N = renderable.particles.m_particleBuffer.particleCount() / S;
++ cb->draw(2 * S, N);
++ QSSGRHICTX_STAT(rhiCtx, draw(2 * S, N));
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DRenderCall, (2 * S | quint64(N) << 32), renderable.particles.profilingId);
++ } else {
++ // draw triangle strip with 2 triangles N times
++ cb->draw(4, renderable.particles.m_particleBuffer.particleCount());
++ QSSGRHICTX_STAT(rhiCtx, draw(4, renderable.particles.m_particleBuffer.particleCount()));
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DRenderCall, (4 | quint64(renderable.particles.m_particleBuffer.particleCount()) << 32), renderable.particles.profilingId);
++ }
++ }
++
++ QT_END_NAMESPACE