Skip to content

Commit 3dc61a4

Browse files
committed
Add stamp() method to PenLayer
1 parent d180546 commit 3dc61a4

File tree

7 files changed

+257
-3
lines changed

7 files changed

+257
-3
lines changed

src/ipenlayer.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace scratchcpprender
1616
{
1717

1818
struct PenAttributes;
19+
class IRenderedTarget;
1920

2021
class IPenLayer : public QNanoQuickItem
2122
{
@@ -36,6 +37,7 @@ class IPenLayer : public QNanoQuickItem
3637
virtual void clear() = 0;
3738
virtual void drawPoint(const PenAttributes &penAttributes, double x, double y) = 0;
3839
virtual void drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) = 0;
40+
virtual void stamp(IRenderedTarget *target) = 0;
3941

4042
virtual QOpenGLFramebufferObject *framebufferObject() const = 0;
4143
virtual QRgb colorAtScratchPoint(double x, double y) const = 0;

src/penlayer.cpp

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
// SPDX-License-Identifier: LGPL-3.0-or-later
22

3+
#include <scratchcpp/costume.h>
4+
35
#include "penlayer.h"
46
#include "penlayerpainter.h"
57
#include "penattributes.h"
8+
#include "irenderedtarget.h"
9+
#include "spritemodel.h"
10+
#include "stagemodel.h"
611

712
using namespace scratchcpprender;
813

@@ -18,6 +23,12 @@ PenLayer::~PenLayer()
1823
{
1924
if (m_engine)
2025
m_projectPenLayers.erase(m_engine);
26+
27+
if (m_blitter.isCreated()) {
28+
// Delete vertex array and buffer
29+
m_glF->glDeleteVertexArrays(1, &m_vao);
30+
m_glF->glDeleteBuffers(1, &m_vbo);
31+
}
2132
}
2233

2334
bool PenLayer::antialiasingEnabled() const
@@ -57,10 +68,38 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine)
5768
m_painter = std::make_unique<QNanoPainter>();
5869

5970
if (!m_glF) {
60-
m_glF = std::make_unique<QOpenGLFunctions>();
71+
m_glF = std::make_unique<QOpenGLExtraFunctions>();
6172
m_glF->initializeOpenGLFunctions();
6273
}
6374

75+
if (!m_blitter.isCreated()) {
76+
m_blitter.create();
77+
78+
// Set up VBO and VAO
79+
float vertices[] = {
80+
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
81+
};
82+
83+
m_glF->glGenVertexArrays(1, &m_vao);
84+
m_glF->glGenBuffers(1, &m_vbo);
85+
86+
m_glF->glBindVertexArray(m_vao);
87+
88+
m_glF->glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
89+
m_glF->glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
90+
91+
// Position attribute
92+
m_glF->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0);
93+
m_glF->glEnableVertexAttribArray(0);
94+
95+
// Texture coordinate attribute
96+
m_glF->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)(2 * sizeof(float)));
97+
m_glF->glEnableVertexAttribArray(1);
98+
99+
m_glF->glBindVertexArray(0);
100+
m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0);
101+
}
102+
64103
clear();
65104
}
66105

@@ -138,6 +177,165 @@ void scratchcpprender::PenLayer::drawLine(const PenAttributes &penAttributes, do
138177
update();
139178
}
140179

180+
/*
181+
* A brief description of how stamping is implemented:
182+
* 1. Get rotation, size and coordinates and translate them.
183+
* 2. Draw the texture onto a temporary texture using shaders.
184+
* 3. Blit the resulting texture to a FBO with a square texture (required for rotation).
185+
* 4. Blit the resulting texture to the pen layer using QOpenGLTextureBlitter with transform.
186+
*
187+
* If you think this is too complicated, contributions are welcome!
188+
*/
189+
void PenLayer::stamp(IRenderedTarget *target)
190+
{
191+
if (!target || !m_fbo || !m_texture.isValid() || !m_blitter.isCreated())
192+
return;
193+
194+
double x = 0;
195+
double y = 0;
196+
double angle = 0;
197+
double scale = 1;
198+
bool mirror = false;
199+
std::shared_ptr<libscratchcpp::Costume> costume;
200+
201+
SpriteModel *spriteModel = target->spriteModel();
202+
203+
if (spriteModel) {
204+
libscratchcpp::Sprite *sprite = spriteModel->sprite();
205+
x = sprite->x();
206+
y = sprite->y();
207+
208+
switch (sprite->rotationStyle()) {
209+
case libscratchcpp::Sprite::RotationStyle::AllAround:
210+
angle = 90 - sprite->direction();
211+
break;
212+
213+
case libscratchcpp::Sprite::RotationStyle::LeftRight:
214+
mirror = (sprite->direction() < 0);
215+
break;
216+
217+
default:
218+
break;
219+
}
220+
221+
scale = sprite->size() / 100;
222+
costume = sprite->currentCostume();
223+
} else
224+
costume = target->stageModel()->stage()->currentCostume();
225+
226+
const double bitmapRes = costume->bitmapResolution();
227+
const double centerX = costume->rotationCenterX() / bitmapRes;
228+
const double centerY = costume->rotationCenterY() / bitmapRes;
229+
230+
const Texture &texture = target->cpuTexture();
231+
232+
if (!texture.isValid())
233+
return;
234+
235+
const double textureScale = texture.width() / static_cast<double>(target->costumeWidth());
236+
237+
// Translate the coordinates
238+
// TODO: Apply scale (HQ pen)
239+
x = std::floor(x + m_texture.width() / 2.0);
240+
y = std::floor(-y + m_texture.height() / 2.0);
241+
242+
m_glF->glDisable(GL_SCISSOR_TEST);
243+
244+
// For some reason nothing is rendered without this
245+
// TODO: Find out why this is happening
246+
m_painter->beginFrame(m_fbo->width(), m_fbo->height());
247+
m_painter->stroke();
248+
m_painter->endFrame();
249+
250+
// Create a temporary FBO for graphic effects
251+
QOpenGLFramebufferObject tmpFbo(texture.size());
252+
m_painter->beginFrame(tmpFbo.width(), tmpFbo.height());
253+
254+
// Create a FBO for the current texture
255+
unsigned int fbo;
256+
m_glF->glGenFramebuffers(1, &fbo);
257+
m_glF->glBindFramebuffer(GL_FRAMEBUFFER, fbo);
258+
m_glF->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0);
259+
260+
if (m_glF->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
261+
qWarning() << "error: framebuffer incomplete (stamp " + target->scratchTarget()->name() + ")";
262+
m_glF->glDeleteFramebuffers(1, &fbo);
263+
return;
264+
}
265+
266+
// Get the shader program for the current set of effects
267+
ShaderManager *shaderManager = ShaderManager::instance();
268+
269+
const auto &effects = target->graphicEffects();
270+
QOpenGLShaderProgram *shaderProgram = shaderManager->getShaderProgram(effects);
271+
Q_ASSERT(shaderProgram);
272+
Q_ASSERT(shaderProgram->isLinked());
273+
274+
m_glF->glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
275+
276+
// Render to the target framebuffer
277+
m_glF->glBindFramebuffer(GL_FRAMEBUFFER, tmpFbo.handle());
278+
shaderProgram->bind();
279+
m_glF->glBindVertexArray(m_vao);
280+
m_glF->glActiveTexture(GL_TEXTURE0);
281+
m_glF->glBindTexture(GL_TEXTURE_2D, texture.handle());
282+
shaderManager->setUniforms(shaderProgram, 0, effects); // set texture and effect uniforms
283+
m_glF->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
284+
285+
m_painter->endFrame();
286+
287+
// Resize to square (for rotation)
288+
const double dim = std::max(tmpFbo.width(), tmpFbo.height());
289+
QOpenGLFramebufferObject resizeFbo(dim, dim);
290+
resizeFbo.bind();
291+
m_painter->beginFrame(dim, dim);
292+
293+
const QRect resizeRect(QPoint(0, 0), tmpFbo.size());
294+
const QMatrix4x4 matrix = QOpenGLTextureBlitter::targetTransform(resizeRect, QRect(QPoint(0, 0), resizeFbo.size()));
295+
m_glF->glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
296+
m_glF->glClear(GL_COLOR_BUFFER_BIT);
297+
m_blitter.bind();
298+
m_blitter.blit(tmpFbo.texture(), matrix, QOpenGLTextureBlitter::OriginBottomLeft);
299+
m_blitter.release();
300+
301+
m_painter->endFrame();
302+
resizeFbo.release();
303+
304+
// Cleanup
305+
shaderProgram->release();
306+
m_glF->glBindVertexArray(0);
307+
m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0);
308+
m_glF->glBindFramebuffer(GL_FRAMEBUFFER, 0);
309+
m_glF->glDeleteFramebuffers(1, &fbo);
310+
311+
// Transform
312+
const double width = resizeFbo.width() / textureScale;
313+
const double height = resizeFbo.height() / textureScale;
314+
QRectF targetRect(QPoint(x, y), QSizeF(width, height));
315+
QTransform transform = QOpenGLTextureBlitter::targetTransform(targetRect, QRect(QPoint(centerX, centerY), m_fbo->size())).toTransform();
316+
const double dx = 2 * (centerX - width / 2.0) / width;
317+
const double dy = -2 * (centerY - height / 2.0) / height;
318+
transform.translate(dx, dy);
319+
transform.rotate(angle);
320+
transform.scale(scale * (mirror ? -1 : 1), scale);
321+
transform.translate(-dx, -dy);
322+
323+
// Blit
324+
m_fbo->bind();
325+
m_painter->beginFrame(m_fbo->width(), m_fbo->height());
326+
m_blitter.bind();
327+
m_blitter.blit(resizeFbo.texture(), transform, QOpenGLTextureBlitter::OriginBottomLeft);
328+
m_blitter.release();
329+
m_painter->endFrame();
330+
m_fbo->release();
331+
332+
m_glF->glEnable(GL_SCISSOR_TEST);
333+
334+
m_textureDirty = true;
335+
m_boundsDirty = true;
336+
update();
337+
}
338+
141339
QOpenGLFramebufferObject *PenLayer::framebufferObject() const
142340
{
143341
return m_fbo.get();

src/penlayer.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#pragma once
44

55
#include <QOpenGLFramebufferObject>
6-
#include <QOpenGLFunctions>
6+
#include <QOpenGLExtraFunctions>
77
#include <qnanopainter.h>
88
#include <scratchcpp/iengine.h>
99

@@ -33,6 +33,7 @@ class PenLayer : public IPenLayer
3333
void clear() override;
3434
void drawPoint(const PenAttributes &penAttributes, double x, double y) override;
3535
void drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) override;
36+
void stamp(IRenderedTarget *target) override;
3637

3738
QOpenGLFramebufferObject *framebufferObject() const override;
3839
QRgb colorAtScratchPoint(double x, double y) const override;
@@ -56,12 +57,15 @@ class PenLayer : public IPenLayer
5657
libscratchcpp::IEngine *m_engine = nullptr;
5758
std::unique_ptr<QOpenGLFramebufferObject> m_fbo;
5859
std::unique_ptr<QNanoPainter> m_painter;
59-
std::unique_ptr<QOpenGLFunctions> m_glF;
60+
std::unique_ptr<QOpenGLExtraFunctions> m_glF;
6061
Texture m_texture;
6162
bool m_textureDirty = true;
6263
mutable CpuTextureManager m_textureManager;
6364
mutable bool m_boundsDirty = true;
6465
mutable libscratchcpp::Rect m_bounds;
66+
QOpenGLTextureBlitter m_blitter;
67+
GLuint m_vbo = 0;
68+
GLuint m_vao = 0;
6569
};
6670

6771
} // namespace scratchcpprender

test/mocks/penlayermock.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class PenLayerMock : public IPenLayer
2121
MOCK_METHOD(void, clear, (), (override));
2222
MOCK_METHOD(void, drawPoint, (const PenAttributes &, double, double), (override));
2323
MOCK_METHOD(void, drawLine, (const PenAttributes &, double, double, double, double), (override));
24+
MOCK_METHOD(void, stamp, (IRenderedTarget *), (override));
2425

2526
MOCK_METHOD(QOpenGLFramebufferObject *, framebufferObject, (), (const, override));
2627
MOCK_METHOD(QRgb, colorAtScratchPoint, (double, double), (const, override));

test/penlayer/penlayer_test.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#include <QFile>
55
#include <penlayer.h>
66
#include <penattributes.h>
7+
#include <projectloader.h>
8+
#include <spritemodel.h>
9+
#include <renderedtarget.h>
710
#include <qnanopainter.h>
811
#include <enginemock.h>
912

@@ -259,6 +262,52 @@ TEST_F(PenLayerTest, DrawLine)
259262
ASSERT_EQ(ref.readAll(), buffer.readAll());
260263
}
261264

265+
TEST_F(PenLayerTest, Stamp)
266+
{
267+
PenLayer penLayer;
268+
EngineMock engine;
269+
EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
270+
EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
271+
penLayer.setEngine(&engine);
272+
273+
ProjectLoader loader;
274+
loader.setFileName("stamp_env.sb3");
275+
loader.start(); // wait until it loads
276+
277+
std::vector<std::unique_ptr<RenderedTarget>> targets;
278+
StageModel *stage = loader.stage();
279+
targets.push_back(std::make_unique<RenderedTarget>());
280+
targets.back()->setStageModel(stage);
281+
targets.back()->setEngine(loader.engine());
282+
targets.back()->loadCostumes();
283+
targets.back()->updateCostume(stage->stage()->currentCostume().get());
284+
targets.back()->setGraphicEffect(ShaderManager::Effect::Color, 25);
285+
stage->setRenderedTarget(targets.back().get());
286+
const auto &sprites = loader.spriteList();
287+
288+
int i = 0;
289+
290+
for (SpriteModel *sprite : sprites) {
291+
targets.push_back(std::make_unique<RenderedTarget>());
292+
targets.back()->setSpriteModel(sprite);
293+
targets.back()->setEngine(loader.engine());
294+
targets.back()->loadCostumes();
295+
targets.back()->updateCostume(sprite->sprite()->currentCostume().get());
296+
targets.back()->setGraphicEffect(ShaderManager::Effect::Color, i * 25);
297+
targets.back()->setGraphicEffect(ShaderManager::Effect::Ghost, i * 5);
298+
sprite->setRenderedTarget(targets.back().get());
299+
i++;
300+
}
301+
302+
for (const auto &target : targets)
303+
penLayer.stamp(target.get());
304+
305+
QOpenGLFramebufferObject *fbo = penLayer.framebufferObject();
306+
QImage image = fbo->toImage().scaled(240, 180);
307+
QImage ref("stamp.png");
308+
ASSERT_LE(fuzzyCompareImages(image, ref), 0.1668);
309+
}
310+
262311
TEST_F(PenLayerTest, TextureData)
263312
{
264313
PenLayer penLayer;

test/stamp.png

56.5 KB
Loading

test/stamp_env.sb3

175 KB
Binary file not shown.

0 commit comments

Comments
 (0)