mirror of
https://github.com/Qz3rK/tdesktop.git
synced 2026-06-02 03:53:42 +02:00
[thanos] Added GPU-accelerated particle dissolution effect for messages.
This commit is contained in:
@@ -1827,6 +1827,10 @@ PRIVATE
|
||||
ui/effects/message_sending_animation_controller.h
|
||||
ui/effects/reaction_fly_animation.cpp
|
||||
ui/effects/reaction_fly_animation.h
|
||||
ui/effects/thanos_effect.cpp
|
||||
ui/effects/thanos_effect.h
|
||||
ui/effects/thanos_effect_renderer.cpp
|
||||
ui/effects/thanos_effect_renderer.h
|
||||
ui/effects/send_action_animations.cpp
|
||||
ui/effects/send_action_animations.h
|
||||
ui/image/image.cpp
|
||||
|
||||
@@ -2182,6 +2182,16 @@ rpl::producer<not_null<const HistoryItem*>> Session::itemRemoved(
|
||||
});
|
||||
}
|
||||
|
||||
void Session::notifyViewAboutToBeRemoved(
|
||||
not_null<const ViewElement*> view) {
|
||||
_viewAboutToBeRemoved.fire_copy(view);
|
||||
}
|
||||
|
||||
rpl::producer<not_null<const ViewElement*>>
|
||||
Session::viewAboutToBeRemoved() const {
|
||||
return _viewAboutToBeRemoved.events();
|
||||
}
|
||||
|
||||
void Session::notifyViewRemoved(not_null<const ViewElement*> view) {
|
||||
_viewRemoved.fire_copy(view);
|
||||
}
|
||||
|
||||
@@ -427,6 +427,8 @@ public:
|
||||
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRemoved() const;
|
||||
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRemoved(
|
||||
FullMsgId itemId) const;
|
||||
void notifyViewAboutToBeRemoved(not_null<const ViewElement*> view);
|
||||
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewAboutToBeRemoved() const;
|
||||
void notifyViewRemoved(not_null<const ViewElement*> view);
|
||||
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewRemoved() const;
|
||||
void notifyHistoryCleared(not_null<const History*> history);
|
||||
@@ -1171,6 +1173,7 @@ private:
|
||||
rpl::event_stream<not_null<HistoryItem*>> _itemDataChanges;
|
||||
rpl::event_stream<ReactionsRemoved> _reactionsRemoved;
|
||||
rpl::event_stream<not_null<const HistoryItem*>> _itemRemoved;
|
||||
rpl::event_stream<not_null<const ViewElement*>> _viewAboutToBeRemoved;
|
||||
rpl::event_stream<not_null<const ViewElement*>> _viewRemoved;
|
||||
rpl::event_stream<not_null<const ViewElement*>> _viewPaidReactionSent;
|
||||
rpl::event_stream<not_null<Calls::GroupCall*>> _callPaidReactionSent;
|
||||
|
||||
@@ -4395,6 +4395,7 @@ int HistoryBlock::resizeGetHeight(int newWidth, ResizeRequest request) {
|
||||
void HistoryBlock::remove(not_null<Element*> view) {
|
||||
Expects(view->block() == this);
|
||||
|
||||
_history->owner().notifyViewAboutToBeRemoved(view);
|
||||
_history->mainViewRemoved(this, view);
|
||||
|
||||
const auto blockIndex = indexInHistory();
|
||||
|
||||
@@ -435,6 +435,10 @@ HistoryInner::HistoryInner(
|
||||
) | rpl::on_next(
|
||||
[this](auto item) { itemRemoved(item); },
|
||||
lifetime());
|
||||
session().data().viewAboutToBeRemoved(
|
||||
) | rpl::on_next(
|
||||
[this](auto view) { captureViewForThanosEffect(view); },
|
||||
lifetime());
|
||||
session().data().viewRemoved(
|
||||
) | rpl::on_next(
|
||||
[this](auto view) { viewRemoved(view); },
|
||||
@@ -4164,6 +4168,58 @@ void HistoryInner::leaveEventHook(QEvent *e) {
|
||||
return RpWidget::leaveEventHook(e);
|
||||
}
|
||||
|
||||
void HistoryInner::captureViewForThanosEffect(
|
||||
not_null<const Element*> view) {
|
||||
if (!Ui::ThanosEffect::Supported()) {
|
||||
return;
|
||||
}
|
||||
if (view->data()->history() != _history
|
||||
&& view->data()->history() != _migrated) {
|
||||
return;
|
||||
}
|
||||
const auto top = itemTop(view);
|
||||
if (top < 0) {
|
||||
return;
|
||||
}
|
||||
const auto viewHeight = view->height();
|
||||
const auto viewWidth = width();
|
||||
if (viewWidth <= 0 || viewHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto screenTop = top - _visibleAreaTop;
|
||||
if (screenTop + viewHeight <= 0
|
||||
|| screenTop >= (_visibleAreaBottom - _visibleAreaTop)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto dpr = style::DevicePixelRatio();
|
||||
auto image = QImage(
|
||||
QSize(viewWidth, viewHeight) * dpr,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(dpr);
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&image);
|
||||
auto clip = QRect(0, 0, viewWidth, viewHeight);
|
||||
auto context = preparePaintContext(clip);
|
||||
context.clip = clip;
|
||||
context.outbg = view->hasOutLayout();
|
||||
p.translate(0, -top);
|
||||
p.translate(0, top);
|
||||
view->draw(p, context);
|
||||
}
|
||||
|
||||
if (!_thanosEffect) {
|
||||
_thanosEffect = std::make_unique<Ui::ThanosEffect>(this);
|
||||
}
|
||||
_thanosEffect->setGeometry(
|
||||
QRect(0, 0, viewWidth, _visibleAreaBottom - _visibleAreaTop));
|
||||
_thanosEffect->raise();
|
||||
_thanosEffect->addItem(
|
||||
std::move(image),
|
||||
QRect(0, screenTop, viewWidth, viewHeight));
|
||||
}
|
||||
|
||||
HistoryInner::~HistoryInner() {
|
||||
if (_overlayHost) {
|
||||
_overlayHost->hide();
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/controls/swipe_handler_data.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/thanos_effect.h"
|
||||
#include "ui/dragging_scroll_manager.h"
|
||||
#include "ui/widgets/middle_click_autoscroll.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
@@ -609,6 +610,9 @@ private:
|
||||
[[nodiscard]] HistoryView::ElementOverlayHost &ensureOverlayHost();
|
||||
std::unique_ptr<HistoryView::ElementOverlayHost> _overlayHost;
|
||||
|
||||
void captureViewForThanosEffect(not_null<const Element*> view);
|
||||
std::unique_ptr<Ui::ThanosEffect> _thanosEffect;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] bool CanSendReply(not_null<const HistoryItem*> item);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/effects/thanos_effect.h"
|
||||
|
||||
#include "ui/effects/thanos_effect_renderer.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/debug_log.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
#include <rhi/qrhi.h>
|
||||
#endif
|
||||
|
||||
namespace Ui {
|
||||
|
||||
bool ThanosEffect::Supported() {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
return !PowerSaving::On(PowerSaving::kChatEffects);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
ThanosEffect::ThanosEffect(not_null<RpWidget*> parent)
|
||||
: _parent(parent) {
|
||||
}
|
||||
|
||||
ThanosEffect::~ThanosEffect() {
|
||||
stopUpdateTimer();
|
||||
}
|
||||
|
||||
void ThanosEffect::ensureSurface() {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
if (_surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto renderer = std::make_unique<ThanosEffectRenderer>();
|
||||
_renderer = renderer.get();
|
||||
|
||||
_renderer->allDone() | rpl::on_next([=] {
|
||||
stopUpdateTimer();
|
||||
_allDone.fire({});
|
||||
}, _lifetime);
|
||||
|
||||
_surface = GL::CreateSurface(
|
||||
_parent,
|
||||
GL::ChosenRenderer{
|
||||
.renderer = std::move(renderer),
|
||||
.backend = GL::Backend::QRhi,
|
||||
});
|
||||
|
||||
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
|
||||
w->setGeometry(_parent->rect());
|
||||
w->show();
|
||||
w->raise();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ThanosEffect::addItem(QImage snapshot, QRect rect) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
ensureSurface();
|
||||
if (!_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
_renderer->addItem({
|
||||
.snapshot = std::move(snapshot),
|
||||
.rect = QRectF(rect),
|
||||
});
|
||||
|
||||
startUpdateTimer();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ThanosEffect::animating() const {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
return _renderer && _renderer->hasActiveItems();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
rpl::producer<> ThanosEffect::allDone() const {
|
||||
return _allDone.events();
|
||||
}
|
||||
|
||||
void ThanosEffect::setGeometry(QRect rect) {
|
||||
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
|
||||
w->setGeometry(rect);
|
||||
}
|
||||
}
|
||||
|
||||
void ThanosEffect::raise() {
|
||||
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
|
||||
w->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void ThanosEffect::startUpdateTimer() {
|
||||
if (_updateTimer) {
|
||||
return;
|
||||
}
|
||||
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
|
||||
_updateTimer = new QTimer(w);
|
||||
_updateTimer->setInterval(16);
|
||||
QObject::connect(_updateTimer, &QTimer::timeout, w, [w] {
|
||||
w->update();
|
||||
});
|
||||
_updateTimer->start();
|
||||
}
|
||||
}
|
||||
|
||||
void ThanosEffect::stopUpdateTimer() {
|
||||
if (_updateTimer) {
|
||||
_updateTimer->stop();
|
||||
delete _updateTimer;
|
||||
_updateTimer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
#include <QImage>
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class RpWidgetWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ThanosEffectRenderer;
|
||||
|
||||
class ThanosEffect final {
|
||||
public:
|
||||
explicit ThanosEffect(not_null<RpWidget*> parent);
|
||||
~ThanosEffect();
|
||||
|
||||
void addItem(QImage snapshot, QRect rect);
|
||||
|
||||
[[nodiscard]] bool animating() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> allDone() const;
|
||||
|
||||
void setGeometry(QRect rect);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] static bool Supported();
|
||||
|
||||
private:
|
||||
void ensureSurface();
|
||||
void startUpdateTimer();
|
||||
void stopUpdateTimer();
|
||||
|
||||
const not_null<RpWidget*> _parent;
|
||||
|
||||
std::unique_ptr<RpWidgetWrap> _surface;
|
||||
[[maybe_unused]] ThanosEffectRenderer *_renderer = nullptr;
|
||||
|
||||
QTimer *_updateTimer = nullptr;
|
||||
|
||||
rpl::event_stream<> _allDone;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,586 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/effects/thanos_effect_renderer.h"
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
|
||||
#include "ui/rhi/rhi_shader.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "base/debug_log.h"
|
||||
|
||||
#include <rhi/qrhi.h>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kParticleStride = int(24);
|
||||
constexpr auto kQuadVertexCount = int(6);
|
||||
constexpr auto kQuadVertexStride = int(2 * sizeof(float));
|
||||
constexpr auto kComputeWorkgroupSize = int(64);
|
||||
constexpr auto kMaxPhaseDuration = 4.0f;
|
||||
|
||||
const float kQuadVertices[kQuadVertexCount * 2] = {
|
||||
0.f, 0.f,
|
||||
1.f, 0.f,
|
||||
0.f, 1.f,
|
||||
1.f, 0.f,
|
||||
0.f, 1.f,
|
||||
1.f, 1.f,
|
||||
};
|
||||
|
||||
struct alignas(16) ComputeInitUniforms {
|
||||
uint32_t particleCountX;
|
||||
uint32_t particleCountY;
|
||||
uint32_t seed;
|
||||
uint32_t _pad;
|
||||
};
|
||||
static_assert(sizeof(ComputeInitUniforms) % 16 == 0);
|
||||
|
||||
struct alignas(16) ComputeUpdateUniforms {
|
||||
uint32_t particleCountX;
|
||||
uint32_t particleCountY;
|
||||
float phase;
|
||||
float timeStep;
|
||||
};
|
||||
static_assert(sizeof(ComputeUpdateUniforms) % 16 == 0);
|
||||
|
||||
struct alignas(16) RenderUniforms {
|
||||
float rect[4];
|
||||
float size[2];
|
||||
uint32_t particleResolution[2];
|
||||
};
|
||||
static_assert(sizeof(RenderUniforms) % 16 == 0);
|
||||
|
||||
[[nodiscard]] QShader LoadShader(const QString &name) {
|
||||
return Rhi::ShaderFromFile(u":/shaders/"_q + name + u".qsb"_q);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ThanosEffectRenderer::ThanosEffectRenderer() {
|
||||
_elapsed.start();
|
||||
}
|
||||
|
||||
ThanosEffectRenderer::~ThanosEffectRenderer() {
|
||||
releaseResources();
|
||||
}
|
||||
|
||||
void ThanosEffectRenderer::initialize(
|
||||
QRhi *rhi,
|
||||
QRhiRenderTarget *rt,
|
||||
QRhiCommandBuffer *cb) {
|
||||
if (_initialized && _rhi == rhi) {
|
||||
return;
|
||||
}
|
||||
releaseResources();
|
||||
_rhi = rhi;
|
||||
|
||||
if (!rhi->isFeatureSupported(QRhi::Compute)) {
|
||||
LOG(("ThanosEffect: Compute shaders not supported, disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
_quadVertexBuffer = rhi->newBuffer(
|
||||
QRhiBuffer::Immutable,
|
||||
QRhiBuffer::VertexBuffer,
|
||||
sizeof(kQuadVertices));
|
||||
_quadVertexBuffer->create();
|
||||
|
||||
_computeInitUniformBuffer = rhi->newBuffer(
|
||||
QRhiBuffer::Dynamic,
|
||||
QRhiBuffer::UniformBuffer,
|
||||
sizeof(ComputeInitUniforms));
|
||||
_computeInitUniformBuffer->create();
|
||||
|
||||
_computeUpdateUniformBuffer = rhi->newBuffer(
|
||||
QRhiBuffer::Dynamic,
|
||||
QRhiBuffer::UniformBuffer,
|
||||
sizeof(ComputeUpdateUniforms));
|
||||
_computeUpdateUniformBuffer->create();
|
||||
|
||||
_renderUniformBuffer = rhi->newBuffer(
|
||||
QRhiBuffer::Dynamic,
|
||||
QRhiBuffer::UniformBuffer,
|
||||
sizeof(RenderUniforms));
|
||||
_renderUniformBuffer->create();
|
||||
|
||||
_placeholderParticleBuffer = rhi->newBuffer(
|
||||
QRhiBuffer::Immutable,
|
||||
QRhiBuffer::VertexBuffer | QRhiBuffer::StorageBuffer,
|
||||
kParticleStride);
|
||||
_placeholderParticleBuffer->create();
|
||||
|
||||
_placeholderTexture = rhi->newTexture(
|
||||
QRhiTexture::RGBA8,
|
||||
QSize(1, 1));
|
||||
_placeholderTexture->create();
|
||||
|
||||
_placeholderSampler = rhi->newSampler(
|
||||
QRhiSampler::Linear,
|
||||
QRhiSampler::Linear,
|
||||
QRhiSampler::None,
|
||||
QRhiSampler::ClampToEdge,
|
||||
QRhiSampler::ClampToEdge);
|
||||
_placeholderSampler->create();
|
||||
|
||||
createPipelines(rt);
|
||||
|
||||
auto *rub = rhi->nextResourceUpdateBatch();
|
||||
rub->uploadStaticBuffer(_quadVertexBuffer, kQuadVertices);
|
||||
cb->resourceUpdate(rub);
|
||||
|
||||
_initialized = true;
|
||||
_lastFrameTime = double(_elapsed.elapsed()) / 1000.0;
|
||||
|
||||
LOG(("ThanosEffect: initialized, backend=%1 device=%2")
|
||||
.arg(rhi->backendName())
|
||||
.arg(rhi->driverInfo().deviceName));
|
||||
}
|
||||
|
||||
void ThanosEffectRenderer::createPipelines(QRhiRenderTarget *rt) {
|
||||
const auto initShader = LoadShader(u"thanos_init.comp"_q);
|
||||
const auto updateShader = LoadShader(u"thanos_update.comp"_q);
|
||||
const auto vertShader = LoadShader(u"thanos.vert"_q);
|
||||
const auto fragShader = LoadShader(u"thanos.frag"_q);
|
||||
|
||||
_computeInitSrbLayout = _rhi->newShaderResourceBindings();
|
||||
_computeInitSrbLayout->setBindings({
|
||||
QRhiShaderResourceBinding::bufferLoadStore(
|
||||
0,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
_placeholderParticleBuffer),
|
||||
QRhiShaderResourceBinding::uniformBuffer(
|
||||
1,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
_computeInitUniformBuffer),
|
||||
});
|
||||
_computeInitSrbLayout->create();
|
||||
|
||||
_computeInitPipeline = _rhi->newComputePipeline();
|
||||
_computeInitPipeline->setShaderStage(
|
||||
{ QRhiShaderStage::Compute, initShader });
|
||||
_computeInitPipeline->setShaderResourceBindings(_computeInitSrbLayout);
|
||||
_computeInitPipeline->create();
|
||||
|
||||
_computeUpdateSrbLayout = _rhi->newShaderResourceBindings();
|
||||
_computeUpdateSrbLayout->setBindings({
|
||||
QRhiShaderResourceBinding::bufferLoadStore(
|
||||
0,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
_placeholderParticleBuffer),
|
||||
QRhiShaderResourceBinding::uniformBuffer(
|
||||
1,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
_computeUpdateUniformBuffer),
|
||||
});
|
||||
_computeUpdateSrbLayout->create();
|
||||
|
||||
_computeUpdatePipeline = _rhi->newComputePipeline();
|
||||
_computeUpdatePipeline->setShaderStage(
|
||||
{ QRhiShaderStage::Compute, updateShader });
|
||||
_computeUpdatePipeline->setShaderResourceBindings(
|
||||
_computeUpdateSrbLayout);
|
||||
_computeUpdatePipeline->create();
|
||||
|
||||
_renderSrbLayout = _rhi->newShaderResourceBindings();
|
||||
_renderSrbLayout->setBindings({
|
||||
QRhiShaderResourceBinding::uniformBuffer(
|
||||
0,
|
||||
QRhiShaderResourceBinding::VertexStage,
|
||||
_renderUniformBuffer),
|
||||
QRhiShaderResourceBinding::sampledTexture(
|
||||
1,
|
||||
QRhiShaderResourceBinding::FragmentStage,
|
||||
_placeholderTexture,
|
||||
_placeholderSampler),
|
||||
});
|
||||
_renderSrbLayout->create();
|
||||
|
||||
_renderPipeline = _rhi->newGraphicsPipeline();
|
||||
_renderPipeline->setShaderStages({
|
||||
{ QRhiShaderStage::Vertex, vertShader },
|
||||
{ QRhiShaderStage::Fragment, fragShader },
|
||||
});
|
||||
|
||||
QRhiVertexInputLayout inputLayout;
|
||||
inputLayout.setBindings({
|
||||
{ quint32(kQuadVertexStride) },
|
||||
{ quint32(kParticleStride),
|
||||
QRhiVertexInputBinding::PerInstance },
|
||||
});
|
||||
inputLayout.setAttributes({
|
||||
{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },
|
||||
{ 1, 1, QRhiVertexInputAttribute::Float2, 0 },
|
||||
{ 1, 2, QRhiVertexInputAttribute::Float, 16 },
|
||||
});
|
||||
_renderPipeline->setVertexInputLayout(inputLayout);
|
||||
|
||||
QRhiGraphicsPipeline::TargetBlend blend;
|
||||
blend.enable = true;
|
||||
blend.srcColor = QRhiGraphicsPipeline::One;
|
||||
blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
|
||||
blend.srcAlpha = QRhiGraphicsPipeline::One;
|
||||
blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
|
||||
_renderPipeline->setTargetBlends({ blend });
|
||||
|
||||
_renderPipeline->setTopology(QRhiGraphicsPipeline::Triangles);
|
||||
_renderPipeline->setShaderResourceBindings(_renderSrbLayout);
|
||||
_renderPipeline->setRenderPassDescriptor(
|
||||
rt->renderPassDescriptor());
|
||||
_renderPipeline->create();
|
||||
}
|
||||
|
||||
void ThanosEffectRenderer::render(
|
||||
QRhi *rhi,
|
||||
QRhiRenderTarget *rt,
|
||||
QRhiCommandBuffer *cb) {
|
||||
if (!_initialized || !rhi->isFeatureSupported(QRhi::Compute)) {
|
||||
return;
|
||||
}
|
||||
_rhi = rhi;
|
||||
|
||||
const auto now = double(_elapsed.elapsed()) / 1000.0;
|
||||
const auto dt = float(std::clamp(now - _lastFrameTime, 0.001, 0.1));
|
||||
_lastFrameTime = now;
|
||||
|
||||
addPendingItems(cb);
|
||||
|
||||
if (_items.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pixelSize = rt->pixelSize();
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto viewW = float(pixelSize.width()) / factor;
|
||||
const auto viewH = float(pixelSize.height()) / factor;
|
||||
|
||||
bool needsInit = false;
|
||||
for (auto &item : _items) {
|
||||
item.phase += dt * 2.0f;
|
||||
if (!item.particlesInitialized) {
|
||||
needsInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsInit) {
|
||||
auto *rub = rhi->nextResourceUpdateBatch();
|
||||
for (auto &item : _items) {
|
||||
if (!item.particlesInitialized) {
|
||||
item.particlesInitialized = true;
|
||||
|
||||
ComputeInitUniforms uni;
|
||||
uni.particleCountX = item.particleCountX;
|
||||
uni.particleCountY = item.particleCountY;
|
||||
uni.seed = _seedCounter++;
|
||||
uni._pad = 0;
|
||||
rub->updateDynamicBuffer(
|
||||
_computeInitUniformBuffer,
|
||||
0,
|
||||
sizeof(uni),
|
||||
&uni);
|
||||
}
|
||||
}
|
||||
cb->beginComputePass(rub);
|
||||
for (auto &item : _items) {
|
||||
if (item.phase <= dt * 2.1f) {
|
||||
cb->setComputePipeline(_computeInitPipeline);
|
||||
cb->setShaderResources(item.computeInitSrb);
|
||||
const auto count = item.particleCountX * item.particleCountY;
|
||||
const auto groups = (count + kComputeWorkgroupSize - 1)
|
||||
/ kComputeWorkgroupSize;
|
||||
cb->dispatch(int(groups), 1, 1);
|
||||
}
|
||||
}
|
||||
cb->endComputePass();
|
||||
}
|
||||
|
||||
{
|
||||
auto *rub = rhi->nextResourceUpdateBatch();
|
||||
for (auto &item : _items) {
|
||||
ComputeUpdateUniforms uni;
|
||||
uni.particleCountX = item.particleCountX;
|
||||
uni.particleCountY = item.particleCountY;
|
||||
uni.phase = item.phase;
|
||||
uni.timeStep = dt * 2.0f;
|
||||
rub->updateDynamicBuffer(
|
||||
_computeUpdateUniformBuffer,
|
||||
0,
|
||||
sizeof(uni),
|
||||
&uni);
|
||||
}
|
||||
cb->beginComputePass(rub);
|
||||
for (auto &item : _items) {
|
||||
if (item.phase >= kMaxPhaseDuration) {
|
||||
continue;
|
||||
}
|
||||
cb->setComputePipeline(_computeUpdatePipeline);
|
||||
cb->setShaderResources(item.computeUpdateSrb);
|
||||
const auto count = item.particleCountX * item.particleCountY;
|
||||
const auto groups = (count + kComputeWorkgroupSize - 1)
|
||||
/ kComputeWorkgroupSize;
|
||||
cb->dispatch(int(groups), 1, 1);
|
||||
}
|
||||
cb->endComputePass();
|
||||
}
|
||||
|
||||
{
|
||||
const auto bg = QColor(0, 0, 0, 0);
|
||||
cb->beginPass(rt, bg, { 1.0f, 0 });
|
||||
|
||||
for (auto &item : _items) {
|
||||
if (item.phase >= kMaxPhaseDuration) {
|
||||
continue;
|
||||
}
|
||||
RenderUniforms uni;
|
||||
uni.rect[0] = float(item.rect.x()) / viewW;
|
||||
uni.rect[1] = float(item.rect.y()) / viewH;
|
||||
uni.rect[2] = float(item.rect.width()) / viewW;
|
||||
uni.rect[3] = float(item.rect.height()) / viewH;
|
||||
uni.size[0] = float(item.rect.width());
|
||||
uni.size[1] = float(item.rect.height());
|
||||
uni.particleResolution[0] = item.particleCountX;
|
||||
uni.particleResolution[1] = item.particleCountY;
|
||||
|
||||
auto *rub = rhi->nextResourceUpdateBatch();
|
||||
rub->updateDynamicBuffer(
|
||||
_renderUniformBuffer,
|
||||
0,
|
||||
sizeof(uni),
|
||||
&uni);
|
||||
cb->resourceUpdate(rub);
|
||||
|
||||
cb->setGraphicsPipeline(_renderPipeline);
|
||||
cb->setShaderResources(item.renderSrb);
|
||||
cb->setViewport({
|
||||
0, 0,
|
||||
float(pixelSize.width()),
|
||||
float(pixelSize.height()) });
|
||||
|
||||
const QRhiCommandBuffer::VertexInput vbufs[] = {
|
||||
{ _quadVertexBuffer, 0 },
|
||||
{ item.particleBuffer, 0 },
|
||||
};
|
||||
cb->setVertexInput(0, 2, vbufs);
|
||||
|
||||
const auto instanceCount =
|
||||
item.particleCountX * item.particleCountY;
|
||||
cb->draw(kQuadVertexCount, instanceCount);
|
||||
}
|
||||
|
||||
cb->endPass();
|
||||
}
|
||||
|
||||
// Remove finished items using deleteLater() so QRhi resources
|
||||
// survive until the command buffer is fully submitted.
|
||||
auto hadItems = !_items.empty();
|
||||
_items.erase(
|
||||
std::remove_if(_items.begin(), _items.end(), [&](auto &item) {
|
||||
if (item.phase >= kMaxPhaseDuration) {
|
||||
if (item.renderSrb) item.renderSrb->deleteLater();
|
||||
if (item.computeUpdateSrb) item.computeUpdateSrb->deleteLater();
|
||||
if (item.computeInitSrb) item.computeInitSrb->deleteLater();
|
||||
if (item.renderUniformBuffer) item.renderUniformBuffer->deleteLater();
|
||||
if (item.computeUpdateUniformBuffer) item.computeUpdateUniformBuffer->deleteLater();
|
||||
if (item.computeInitUniformBuffer) item.computeInitUniformBuffer->deleteLater();
|
||||
if (item.particleBuffer) item.particleBuffer->deleteLater();
|
||||
if (item.sampler) item.sampler->deleteLater();
|
||||
if (item.texture) item.texture->deleteLater();
|
||||
item = {};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
_items.end());
|
||||
|
||||
if (hadItems && _items.empty()) {
|
||||
_allDone.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void ThanosEffectRenderer::addItem(ThanosItem item) {
|
||||
_pendingItems.push_back(std::move(item));
|
||||
}
|
||||
|
||||
bool ThanosEffectRenderer::hasActiveItems() const {
|
||||
return !_items.empty() || !_pendingItems.empty();
|
||||
}
|
||||
|
||||
rpl::producer<> ThanosEffectRenderer::allDone() const {
|
||||
return _allDone.events();
|
||||
}
|
||||
|
||||
void ThanosEffectRenderer::addPendingItems(QRhiCommandBuffer *cb) {
|
||||
if (_pendingItems.empty() || !_rhi) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *rub = _rhi->nextResourceUpdateBatch();
|
||||
|
||||
for (auto &pending : _pendingItems) {
|
||||
auto animating = createAnimatingItem(std::move(pending));
|
||||
if (animating.texture) {
|
||||
auto image = animating.uploadImage;
|
||||
if (!image.isNull()) {
|
||||
rub->uploadTexture(
|
||||
animating.texture,
|
||||
QRhiTextureUploadDescription(
|
||||
QRhiTextureUploadEntry(
|
||||
0, 0,
|
||||
QRhiTextureSubresourceUploadDescription(
|
||||
image))));
|
||||
}
|
||||
animating.uploadImage = QImage();
|
||||
_items.push_back(std::move(animating));
|
||||
}
|
||||
}
|
||||
_pendingItems.clear();
|
||||
|
||||
if (hasUploads) {
|
||||
cb->resourceUpdate(rub);
|
||||
} else {
|
||||
delete rub;
|
||||
}
|
||||
}
|
||||
|
||||
ThanosEffectRenderer::AnimatingItem ThanosEffectRenderer::createAnimatingItem(
|
||||
ThanosItem &&item) {
|
||||
AnimatingItem result;
|
||||
result.rect = item.rect;
|
||||
|
||||
const auto w = int(item.rect.width());
|
||||
const auto h = int(item.rect.height());
|
||||
if (w <= 0 || h <= 0 || item.snapshot.isNull()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.particleCountX = uint32_t(w);
|
||||
result.particleCountY = uint32_t(h);
|
||||
const auto particleCount =
|
||||
result.particleCountX * result.particleCountY;
|
||||
|
||||
auto *tex = _rhi->newTexture(
|
||||
QRhiTexture::RGBA8,
|
||||
QSize(item.snapshot.width(), item.snapshot.height()));
|
||||
tex->create();
|
||||
result.texture = tex;
|
||||
|
||||
result.uploadImage = item.snapshot.convertToFormat(
|
||||
QImage::Format_RGBA8888_Premultiplied);
|
||||
|
||||
auto *sampler = _rhi->newSampler(
|
||||
QRhiSampler::Linear,
|
||||
QRhiSampler::Linear,
|
||||
QRhiSampler::None,
|
||||
QRhiSampler::ClampToEdge,
|
||||
QRhiSampler::ClampToEdge);
|
||||
sampler->create();
|
||||
result.sampler = sampler;
|
||||
|
||||
auto *particleBuf = _rhi->newBuffer(
|
||||
QRhiBuffer::Immutable,
|
||||
QRhiBuffer::VertexBuffer | QRhiBuffer::StorageBuffer,
|
||||
particleCount * kParticleStride);
|
||||
particleBuf->create();
|
||||
result.particleBuffer = particleBuf;
|
||||
|
||||
result.computeInitSrb = _rhi->newShaderResourceBindings();
|
||||
result.computeInitSrb->setBindings({
|
||||
QRhiShaderResourceBinding::bufferLoadStore(
|
||||
0,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
particleBuf),
|
||||
QRhiShaderResourceBinding::uniformBuffer(
|
||||
1,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
_computeInitUniformBuffer),
|
||||
});
|
||||
result.computeInitSrb->create();
|
||||
|
||||
result.computeUpdateSrb = _rhi->newShaderResourceBindings();
|
||||
result.computeUpdateSrb->setBindings({
|
||||
QRhiShaderResourceBinding::bufferLoadStore(
|
||||
0,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
particleBuf),
|
||||
QRhiShaderResourceBinding::uniformBuffer(
|
||||
1,
|
||||
QRhiShaderResourceBinding::ComputeStage,
|
||||
_computeUpdateUniformBuffer),
|
||||
});
|
||||
result.computeUpdateSrb->create();
|
||||
|
||||
result.renderSrb = _rhi->newShaderResourceBindings();
|
||||
result.renderSrb->setBindings({
|
||||
QRhiShaderResourceBinding::uniformBuffer(
|
||||
0,
|
||||
QRhiShaderResourceBinding::VertexStage,
|
||||
_renderUniformBuffer),
|
||||
QRhiShaderResourceBinding::sampledTexture(
|
||||
1,
|
||||
QRhiShaderResourceBinding::FragmentStage,
|
||||
tex,
|
||||
sampler),
|
||||
});
|
||||
result.renderSrb->create();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ThanosEffectRenderer::destroyAnimatingItem(AnimatingItem &item) {
|
||||
delete item.renderSrb;
|
||||
delete item.computeUpdateSrb;
|
||||
delete item.computeInitSrb;
|
||||
delete item.particleBuffer;
|
||||
delete item.sampler;
|
||||
delete item.texture;
|
||||
item = {};
|
||||
}
|
||||
|
||||
void ThanosEffectRenderer::releaseResources() {
|
||||
for (auto &item : _items) {
|
||||
destroyAnimatingItem(item);
|
||||
}
|
||||
_items.clear();
|
||||
|
||||
delete _renderPipeline;
|
||||
_renderPipeline = nullptr;
|
||||
delete _renderSrbLayout;
|
||||
_renderSrbLayout = nullptr;
|
||||
delete _computeUpdatePipeline;
|
||||
_computeUpdatePipeline = nullptr;
|
||||
delete _computeUpdateSrbLayout;
|
||||
_computeUpdateSrbLayout = nullptr;
|
||||
delete _computeInitPipeline;
|
||||
_computeInitPipeline = nullptr;
|
||||
delete _computeInitSrbLayout;
|
||||
_computeInitSrbLayout = nullptr;
|
||||
|
||||
delete _placeholderSampler;
|
||||
_placeholderSampler = nullptr;
|
||||
delete _placeholderTexture;
|
||||
_placeholderTexture = nullptr;
|
||||
delete _placeholderParticleBuffer;
|
||||
_placeholderParticleBuffer = nullptr;
|
||||
|
||||
delete _renderUniformBuffer;
|
||||
_renderUniformBuffer = nullptr;
|
||||
delete _computeUpdateUniformBuffer;
|
||||
_computeUpdateUniformBuffer = nullptr;
|
||||
delete _computeInitUniformBuffer;
|
||||
_computeInitUniformBuffer = nullptr;
|
||||
delete _quadVertexBuffer;
|
||||
_quadVertexBuffer = nullptr;
|
||||
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
|
||||
#endif // Qt >= 6.7
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rhi/rhi_renderer.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QImage>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
|
||||
class QRhi;
|
||||
class QRhiBuffer;
|
||||
class QRhiTexture;
|
||||
class QRhiSampler;
|
||||
class QRhiGraphicsPipeline;
|
||||
class QRhiComputePipeline;
|
||||
class QRhiShaderResourceBindings;
|
||||
class QRhiRenderTarget;
|
||||
class QRhiCommandBuffer;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct ThanosItem {
|
||||
QImage snapshot;
|
||||
QRectF rect;
|
||||
};
|
||||
|
||||
class ThanosEffectRenderer final
|
||||
: public GL::Renderer
|
||||
, public Rhi::Renderer {
|
||||
public:
|
||||
ThanosEffectRenderer();
|
||||
~ThanosEffectRenderer();
|
||||
|
||||
void initialize(
|
||||
QRhi *rhi,
|
||||
QRhiRenderTarget *rt,
|
||||
QRhiCommandBuffer *cb) override;
|
||||
void render(
|
||||
QRhi *rhi,
|
||||
QRhiRenderTarget *rt,
|
||||
QRhiCommandBuffer *cb) override;
|
||||
void releaseResources() override;
|
||||
|
||||
QColor rhiClearColor() override {
|
||||
return QColor(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
std::optional<QColor> clearColor() override {
|
||||
return QColor(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
void addItem(ThanosItem item);
|
||||
[[nodiscard]] bool hasActiveItems() const;
|
||||
|
||||
rpl::producer<> allDone() const;
|
||||
|
||||
private:
|
||||
struct AnimatingItem {
|
||||
QRhiTexture *texture = nullptr;
|
||||
QRhiSampler *sampler = nullptr;
|
||||
QRhiBuffer *particleBuffer = nullptr;
|
||||
QRhiShaderResourceBindings *computeInitSrb = nullptr;
|
||||
QRhiShaderResourceBindings *computeUpdateSrb = nullptr;
|
||||
QRhiShaderResourceBindings *renderSrb = nullptr;
|
||||
QImage uploadImage;
|
||||
QRectF rect;
|
||||
uint32_t particleCountX = 0;
|
||||
uint32_t particleCountY = 0;
|
||||
float phase = 0.f;
|
||||
bool particlesInitialized = false;
|
||||
};
|
||||
|
||||
void createPipelines(QRhiRenderTarget *rt);
|
||||
void addPendingItems(QRhiCommandBuffer *cb);
|
||||
AnimatingItem createAnimatingItem(ThanosItem &&item);
|
||||
void destroyAnimatingItem(AnimatingItem &item);
|
||||
|
||||
QRhi *_rhi = nullptr;
|
||||
|
||||
QRhiBuffer *_quadVertexBuffer = nullptr;
|
||||
QRhiBuffer *_computeInitUniformBuffer = nullptr;
|
||||
QRhiBuffer *_computeUpdateUniformBuffer = nullptr;
|
||||
QRhiBuffer *_renderUniformBuffer = nullptr;
|
||||
|
||||
QRhiBuffer *_placeholderParticleBuffer = nullptr;
|
||||
QRhiTexture *_placeholderTexture = nullptr;
|
||||
QRhiSampler *_placeholderSampler = nullptr;
|
||||
|
||||
QRhiShaderResourceBindings *_computeInitSrbLayout = nullptr;
|
||||
QRhiShaderResourceBindings *_computeUpdateSrbLayout = nullptr;
|
||||
QRhiShaderResourceBindings *_renderSrbLayout = nullptr;
|
||||
|
||||
QRhiComputePipeline *_computeInitPipeline = nullptr;
|
||||
QRhiComputePipeline *_computeUpdatePipeline = nullptr;
|
||||
QRhiGraphicsPipeline *_renderPipeline = nullptr;
|
||||
|
||||
std::vector<AnimatingItem> _items;
|
||||
std::vector<ThanosItem> _pendingItems;
|
||||
|
||||
QElapsedTimer _elapsed;
|
||||
double _lastFrameTime = 0.;
|
||||
bool _initialized = false;
|
||||
uint32_t _seedCounter = 0;
|
||||
|
||||
rpl::event_stream<> _allDone;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
|
||||
#endif // Qt >= 6.7
|
||||
@@ -1,4 +1,4 @@
|
||||
# Compile QRhi shaders (.vert/.frag -> .qsb) at build time.
|
||||
# Compile QRhi shaders (.vert/.frag/.comp -> .qsb) at build time.
|
||||
#
|
||||
# Usage: include(cmake/qrhi_shaders.cmake)
|
||||
# Requires: target "Telegram" and function "nice_target_sources" to exist.
|
||||
@@ -15,16 +15,27 @@ if (QSB_EXECUTABLE)
|
||||
set(_shader_dir "${CMAKE_CURRENT_SOURCE_DIR}/shaders")
|
||||
set(_qsb_out_dir "${CMAKE_CURRENT_BINARY_DIR}/shaders")
|
||||
file(MAKE_DIRECTORY ${_qsb_out_dir})
|
||||
file(GLOB _shader_sources "${_shader_dir}/*.vert" "${_shader_dir}/*.frag")
|
||||
file(GLOB _shader_sources
|
||||
"${_shader_dir}/*.vert"
|
||||
"${_shader_dir}/*.frag"
|
||||
"${_shader_dir}/*.comp")
|
||||
set(_qsb_outputs)
|
||||
set(_qrc_entries)
|
||||
foreach(_src ${_shader_sources})
|
||||
get_filename_component(_name ${_src} NAME)
|
||||
get_filename_component(_ext ${_src} LAST_EXT)
|
||||
set(_qsb "${_qsb_out_dir}/${_name}.qsb")
|
||||
|
||||
if("${_ext}" STREQUAL ".comp")
|
||||
set(_glsl_ver "310es,430")
|
||||
else()
|
||||
set(_glsl_ver "100es,120,150")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${_qsb}
|
||||
COMMAND ${QSB_EXECUTABLE}
|
||||
--glsl "100es,120,150"
|
||||
--glsl "${_glsl_ver}"
|
||||
--hlsl 50
|
||||
--msl 12
|
||||
-o ${_qsb}
|
||||
@@ -45,7 +56,7 @@ if (QSB_EXECUTABLE)
|
||||
shaders.qrc
|
||||
)
|
||||
add_dependencies(Telegram compile_shaders)
|
||||
message(STATUS "QSB: found ${QSB_EXECUTABLE}, will compile ${_shader_dir}/*.vert/*.frag")
|
||||
message(STATUS "QSB: found ${QSB_EXECUTABLE}, will compile ${_shader_dir}/*.vert/*.frag/*.comp")
|
||||
else()
|
||||
message(STATUS "QSB: not found, shaders will not be compiled")
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 v_texcoord;
|
||||
layout(location = 1) in float v_alpha;
|
||||
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(binding = 1) uniform sampler2D tex;
|
||||
|
||||
void main() {
|
||||
if (v_alpha <= 0.0) {
|
||||
discard;
|
||||
}
|
||||
vec4 color = texture(tex, vec2(v_texcoord.x, 1.0 - v_texcoord.y));
|
||||
fragColor = color * v_alpha;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 inQuadPos;
|
||||
|
||||
layout(location = 1) in vec2 inOffset;
|
||||
layout(location = 2) in float inLifetime;
|
||||
|
||||
layout(location = 0) out vec2 v_texcoord;
|
||||
layout(location = 1) out float v_alpha;
|
||||
|
||||
layout(std140, binding = 0) uniform Params {
|
||||
vec4 rect;
|
||||
vec2 size;
|
||||
uvec2 particleResolution;
|
||||
};
|
||||
|
||||
void main() {
|
||||
uint particleId = uint(gl_InstanceIndex);
|
||||
uint pX = particleId % particleResolution.x;
|
||||
uint pY = particleId / particleResolution.x;
|
||||
|
||||
vec2 particleSize = size / vec2(particleResolution);
|
||||
|
||||
vec2 topLeft = vec2(float(pX) * particleSize.x, float(pY) * particleSize.y);
|
||||
v_texcoord = (topLeft + inQuadPos * particleSize) / size;
|
||||
|
||||
topLeft += inOffset;
|
||||
vec2 position = topLeft + inQuadPos * particleSize;
|
||||
|
||||
vec2 ndc;
|
||||
ndc.x = rect.x + position.x / size.x * rect.z;
|
||||
ndc.y = rect.y + position.y / size.y * rect.w;
|
||||
ndc.x = -1.0 + ndc.x * 2.0;
|
||||
ndc.y = -1.0 + ndc.y * 2.0;
|
||||
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
|
||||
v_alpha = clamp(inLifetime / 0.3, 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#version 450
|
||||
|
||||
layout(local_size_x = 64) in;
|
||||
|
||||
struct Particle {
|
||||
vec2 offset;
|
||||
vec2 velocity;
|
||||
float lifetime;
|
||||
float _pad;
|
||||
};
|
||||
|
||||
layout(std430, binding = 0) buffer Particles {
|
||||
Particle particles[];
|
||||
};
|
||||
|
||||
layout(std140, binding = 1) uniform Params {
|
||||
uint particleCountX;
|
||||
uint particleCountY;
|
||||
uint seed;
|
||||
uint _pad;
|
||||
};
|
||||
|
||||
uint hash(uint x) {
|
||||
x ^= x >> 16u;
|
||||
x *= 0x45d9f3bu;
|
||||
x ^= x >> 16u;
|
||||
x *= 0x45d9f3bu;
|
||||
x ^= x >> 16u;
|
||||
return x;
|
||||
}
|
||||
|
||||
float hashFloat(uint x) {
|
||||
return float(hash(x)) / float(0xFFFFFFFFu);
|
||||
}
|
||||
|
||||
void main() {
|
||||
uint gid = gl_GlobalInvocationID.x;
|
||||
uint count = particleCountX * particleCountY;
|
||||
if (gid >= count) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint s = gid * 3u + seed;
|
||||
|
||||
float direction = hashFloat(s) * (3.14159265 * 2.0);
|
||||
float speed = (0.1 + hashFloat(s + 1u) * 0.1) * 420.0;
|
||||
|
||||
Particle p;
|
||||
p.offset = vec2(0.0, 0.0);
|
||||
p.velocity = vec2(cos(direction) * speed, sin(direction) * speed);
|
||||
p.lifetime = 0.7 + hashFloat(s + 2u) * 0.8;
|
||||
p._pad = 0.0;
|
||||
|
||||
particles[gid] = p;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#version 450
|
||||
|
||||
layout(local_size_x = 64) in;
|
||||
|
||||
struct Particle {
|
||||
vec2 offset;
|
||||
vec2 velocity;
|
||||
float lifetime;
|
||||
float _pad;
|
||||
};
|
||||
|
||||
layout(std430, binding = 0) buffer Particles {
|
||||
Particle particles[];
|
||||
};
|
||||
|
||||
layout(std140, binding = 1) uniform Params {
|
||||
uint particleCountX;
|
||||
uint particleCountY;
|
||||
float phase;
|
||||
float timeStep;
|
||||
};
|
||||
|
||||
float easeInWindow(float fraction, float t) {
|
||||
float windowSize = 0.8;
|
||||
float windowStart = -windowSize;
|
||||
float windowEnd = 1.0;
|
||||
float windowPos = (1.0 - fraction) * windowStart + fraction * windowEnd;
|
||||
float windowT = clamp(t - windowPos, 0.0, windowSize) / windowSize;
|
||||
return 1.0 - windowT;
|
||||
}
|
||||
|
||||
void main() {
|
||||
uint gid = gl_GlobalInvocationID.x;
|
||||
uint count = particleCountX * particleCountY;
|
||||
if (gid >= count) {
|
||||
return;
|
||||
}
|
||||
|
||||
float easeInDuration = 0.8;
|
||||
float effectFraction = clamp(phase, 0.0, easeInDuration) / easeInDuration;
|
||||
|
||||
uint particleX = gid % particleCountX;
|
||||
float particleXFraction = float(particleX) / float(particleCountX);
|
||||
float particleFraction = easeInWindow(effectFraction, particleXFraction);
|
||||
|
||||
Particle p = particles[gid];
|
||||
|
||||
p.offset += p.velocity * timeStep * particleFraction;
|
||||
p.velocity.y += 120.0 * timeStep * particleFraction;
|
||||
p.lifetime = max(0.0, p.lifetime - timeStep * particleFraction);
|
||||
|
||||
particles[gid] = p;
|
||||
}
|
||||
Reference in New Issue
Block a user