diff --git a/src/penlayer.cpp b/src/penlayer.cpp index 0065f7f..bebcb0d 100644 --- a/src/penlayer.cpp +++ b/src/penlayer.cpp @@ -11,8 +11,17 @@ using namespace scratchcpprender; +static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 + std::unordered_map PenLayer::m_projectPenLayers; +// TODO: Move this to a separate class +template +short sgn(T x) +{ + return (T(0) < x) - (x < T(0)); +} + PenLayer::PenLayer(QNanoQuickItem *parent) : IPenLayer(parent) { @@ -24,10 +33,13 @@ PenLayer::~PenLayer() if (m_engine) m_projectPenLayers.erase(m_engine); - if (m_blitter.isCreated()) { + if (m_vao != 0) { // Delete vertex array and buffer m_glF->glDeleteVertexArrays(1, &m_vao); m_glF->glDeleteBuffers(1, &m_vbo); + + // Delete stamp FBO + m_glF->glDeleteFramebuffers(1, &m_stampFbo); } } @@ -68,13 +80,9 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) m_glF->initializeOpenGLFunctions(); } - if (!m_blitter.isCreated()) { - m_blitter.create(); - + if (m_vao == 0) { // Set up VBO and VAO - float vertices[] = { - -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, - }; + float vertices[] = { -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, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f }; m_glF->glGenVertexArrays(1, &m_vao); m_glF->glGenBuffers(1, &m_vbo); @@ -94,6 +102,9 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) m_glF->glBindVertexArray(0); m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Create stamp FBO + m_glF->glGenFramebuffers(1, &m_stampFbo); } clear(); @@ -195,98 +206,81 @@ void scratchcpprender::PenLayer::drawLine(const PenAttributes &penAttributes, do update(); } -/* - * A brief description of how stamping is implemented: - * 1. Get rotation, size and coordinates and translate them. - * 2. Draw the texture onto a temporary texture using shaders. - * 3. Blit the resulting texture to a FBO with a square texture (required for rotation). - * 4. Blit the resulting texture to the pen layer using QOpenGLTextureBlitter with transform. - * - * If you think this is too complicated, contributions are welcome! - */ void PenLayer::stamp(IRenderedTarget *target) { - if (!target || !m_fbo || !m_texture.isValid() || !m_blitter.isCreated()) + if (!target || !m_fbo || !m_texture.isValid() || m_vao == 0 || m_vbo == 0) return; - double x = 0; - double y = 0; - double angle = 0; - double scale = 1; - bool mirror = false; - std::shared_ptr costume; + const float stageWidth = m_engine->stageWidth() * m_scale; + const float stageHeight = m_engine->stageHeight() * m_scale; + float x = 0; + float y = 0; + float angle = 180; + float scaleX = 1; + float scaleY = 1; SpriteModel *spriteModel = target->spriteModel(); if (spriteModel) { libscratchcpp::Sprite *sprite = spriteModel->sprite(); - x = sprite->x(); - y = sprite->y(); switch (sprite->rotationStyle()) { case libscratchcpp::Sprite::RotationStyle::AllAround: - angle = 90 - sprite->direction(); + angle = 270 - sprite->direction(); break; case libscratchcpp::Sprite::RotationStyle::LeftRight: - mirror = (sprite->direction() < 0); + scaleX = sgn(sprite->direction()); break; default: break; } - scale = sprite->size() / 100; - costume = sprite->currentCostume(); - } else - costume = target->stageModel()->stage()->currentCostume(); - - // Apply scale (HQ pen) - scale *= m_scale; + scaleY = sprite->size() / 100; + scaleX *= scaleY; + } - const double bitmapRes = costume->bitmapResolution(); - const double centerX = costume->rotationCenterX() / bitmapRes; - const double centerY = costume->rotationCenterY() / bitmapRes; + scaleX *= m_scale; + scaleY *= m_scale; + libscratchcpp::Rect bounds = target->getFastBounds(); const Texture &texture = target->cpuTexture(); if (!texture.isValid()) return; - const double textureScale = texture.width() / static_cast(target->costumeWidth()); - - // Apply scale (HQ pen) - x *= m_scale; - y *= m_scale; - - // Translate the coordinates - x = std::floor(x + m_texture.width() / 2.0); - y = std::floor(-y + m_texture.height() / 2.0); - + const float textureScale = texture.width() / static_cast(target->costumeWidth()); + const float skinWidth = texture.width(); + const float skinHeight = texture.height(); + + // Projection matrix + QMatrix4x4 projectionMatrix; + const float aspectRatio = skinHeight / skinWidth; + projectionMatrix.ortho(1.0f, -1.0f, aspectRatio, -aspectRatio, 0.1f, 0.0f); + projectionMatrix.scale(skinWidth / bounds.width() / m_scale, skinHeight / bounds.height() / m_scale); + + // Model matrix + // TODO: This should be calculated and cached by targets + QMatrix4x4 modelMatrix; + modelMatrix.rotate(angle, 0, 0, 1); + modelMatrix.scale(scaleX / textureScale, aspectRatio * scaleY / textureScale); m_glF->glDisable(GL_SCISSOR_TEST); - - // For some reason nothing is rendered without this - // TODO: Find out why this is happening - m_painter->beginFrame(m_fbo->width(), m_fbo->height()); - m_painter->stroke(); - m_painter->endFrame(); - - // Create a temporary FBO for graphic effects - QOpenGLFramebufferObject tmpFbo(texture.size()); - m_painter->beginFrame(tmpFbo.width(), tmpFbo.height()); + m_glF->glDisable(GL_DEPTH_TEST); // Create a FBO for the current texture - unsigned int fbo; - m_glF->glGenFramebuffers(1, &fbo); - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, fbo); + m_glF->glBindFramebuffer(GL_FRAMEBUFFER, m_stampFbo); m_glF->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); if (m_glF->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { qWarning() << "error: framebuffer incomplete (stamp " + target->scratchTarget()->name() + ")"; - m_glF->glDeleteFramebuffers(1, &fbo); + m_glF->glBindFramebuffer(GL_FRAMEBUFFER, 0); return; } + // Set viewport + m_glF->glViewport((stageWidth / 2) + bounds.left() * m_scale, (stageHeight / 2) + bounds.bottom() * m_scale, bounds.width() * m_scale, bounds.height() * m_scale); + // Get the shader program for the current set of effects ShaderManager *shaderManager = ShaderManager::instance(); @@ -298,62 +292,24 @@ void PenLayer::stamp(IRenderedTarget *target) m_glF->glBindBuffer(GL_ARRAY_BUFFER, m_vbo); // Render to the target framebuffer - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, tmpFbo.handle()); + m_glF->glBindFramebuffer(GL_FRAMEBUFFER, m_fbo->handle()); shaderProgram->bind(); m_glF->glBindVertexArray(m_vao); m_glF->glActiveTexture(GL_TEXTURE0); m_glF->glBindTexture(GL_TEXTURE_2D, texture.handle()); shaderManager->setUniforms(shaderProgram, 0, texture.size(), effects); // set texture and effect uniforms - m_glF->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - m_painter->endFrame(); - - // Resize to square (for rotation) - const double dim = std::max(tmpFbo.width(), tmpFbo.height()); - QOpenGLFramebufferObject resizeFbo(dim, dim); - resizeFbo.bind(); - m_painter->beginFrame(dim, dim); - - const QRect resizeRect(QPoint(0, 0), tmpFbo.size()); - const QMatrix4x4 matrix = QOpenGLTextureBlitter::targetTransform(resizeRect, QRect(QPoint(0, 0), resizeFbo.size())); - m_glF->glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - m_glF->glClear(GL_COLOR_BUFFER_BIT); - m_blitter.bind(); - m_blitter.blit(tmpFbo.texture(), matrix, QOpenGLTextureBlitter::OriginBottomLeft); - m_blitter.release(); - - m_painter->endFrame(); - resizeFbo.release(); + shaderProgram->setUniformValue("u_projectionMatrix", projectionMatrix); + shaderProgram->setUniformValue("u_modelMatrix", modelMatrix); + m_glF->glDrawArrays(GL_TRIANGLES, 0, 6); // Cleanup shaderProgram->release(); m_glF->glBindVertexArray(0); m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0); m_glF->glBindFramebuffer(GL_FRAMEBUFFER, 0); - m_glF->glDeleteFramebuffers(1, &fbo); - - // Transform - const double width = resizeFbo.width() / textureScale; - const double height = resizeFbo.height() / textureScale; - QRectF targetRect(QPoint(x, y), QSizeF(width, height)); - QTransform transform = QOpenGLTextureBlitter::targetTransform(targetRect, QRect(QPoint(centerX, centerY), m_fbo->size())).toTransform(); - const double dx = 2 * (centerX - width / 2.0) / width; - const double dy = -2 * (centerY - height / 2.0) / height; - transform.translate(dx, dy); - transform.rotate(angle); - transform.scale(scale * (mirror ? -1 : 1), scale); - transform.translate(-dx, -dy); - - // Blit - m_fbo->bind(); - m_painter->beginFrame(m_fbo->width(), m_fbo->height()); - m_blitter.bind(); - m_blitter.blit(resizeFbo.texture(), transform, QOpenGLTextureBlitter::OriginBottomLeft); - m_blitter.release(); - m_painter->endFrame(); - m_fbo->release(); m_glF->glEnable(GL_SCISSOR_TEST); + m_glF->glEnable(GL_DEPTH_TEST); m_textureDirty = true; m_boundsDirty = true; diff --git a/src/penlayer.h b/src/penlayer.h index 2f93a9c..d17eadc 100644 --- a/src/penlayer.h +++ b/src/penlayer.h @@ -60,6 +60,7 @@ class PenLayer : public IPenLayer void updateTexture(); static std::unordered_map m_projectPenLayers; + static inline GLuint m_stampFbo = 0; bool m_antialiasingEnabled = true; libscratchcpp::IEngine *m_engine = nullptr; bool m_hqPen = false; @@ -72,7 +73,6 @@ class PenLayer : public IPenLayer mutable CpuTextureManager m_textureManager; mutable bool m_boundsDirty = true; mutable libscratchcpp::Rect m_bounds; - QOpenGLTextureBlitter m_blitter; GLuint m_vbo = 0; GLuint m_vao = 0; }; diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index d97a7c1..101e236 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -683,56 +683,52 @@ void RenderedTarget::calculatePos() if (!m_skin || !m_costume || !m_engine) return; - if (isVisible() || m_stageModel) { - double stageWidth = m_engine->stageWidth(); - double stageHeight = m_engine->stageHeight(); - setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); - setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution())); - qreal originX = m_costume->rotationCenterX() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); - qreal originY = m_costume->rotationCenterY() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); - setTransformOriginPoint(QPointF(originX, originY)); - - // Qt ignores the transform origin point if it's (0, 0), - // so set the transform origin to top left in this case. - if (originX == 0 && originY == 0) - setTransformOrigin(QQuickItem::TopLeft); - else - setTransformOrigin(QQuickItem::Center); - } + double stageWidth = m_engine->stageWidth(); + double stageHeight = m_engine->stageHeight(); + setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); + setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution())); + qreal originX = m_costume->rotationCenterX() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); + qreal originY = m_costume->rotationCenterY() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); + setTransformOriginPoint(QPointF(originX, originY)); + + // Qt ignores the transform origin point if it's (0, 0), + // so set the transform origin to top left in this case. + if (originX == 0 && originY == 0) + setTransformOrigin(QQuickItem::TopLeft); + else + setTransformOrigin(QQuickItem::Center); m_transformedHullDirty = true; } void RenderedTarget::calculateRotation() { - if (isVisible()) { - // Direction - bool oldMirrorHorizontally = m_mirrorHorizontally; + // Direction + bool oldMirrorHorizontally = m_mirrorHorizontally; - switch (m_rotationStyle) { - case Sprite::RotationStyle::AllAround: - setRotation(m_direction - 90); - m_mirrorHorizontally = (false); + switch (m_rotationStyle) { + case Sprite::RotationStyle::AllAround: + setRotation(m_direction - 90); + m_mirrorHorizontally = (false); - break; + break; - case Sprite::RotationStyle::LeftRight: { - setRotation(0); - m_mirrorHorizontally = (m_direction < 0); + case Sprite::RotationStyle::LeftRight: { + setRotation(0); + m_mirrorHorizontally = (m_direction < 0); - break; - } - - case Sprite::RotationStyle::DoNotRotate: - setRotation(0); - m_mirrorHorizontally = false; - break; + break; } - if (m_mirrorHorizontally != oldMirrorHorizontally) - emit mirrorHorizontallyChanged(); + case Sprite::RotationStyle::DoNotRotate: + setRotation(0); + m_mirrorHorizontally = false; + break; } + if (m_mirrorHorizontally != oldMirrorHorizontally) + emit mirrorHorizontallyChanged(); + m_transformedHullDirty = true; } diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index 08245ff..ef79415 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -1,9 +1,11 @@ +uniform mat4 u_projectionMatrix; +uniform mat4 u_modelMatrix; attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() { - gl_Position = vec4(a_position, 0.0, 1.0); + gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1); v_texCoord = a_texCoord; } diff --git a/src/skin.cpp b/src/skin.cpp index 4bc87b9..6ac4d6b 100644 --- a/src/skin.cpp +++ b/src/skin.cpp @@ -58,8 +58,12 @@ Texture Skin::createAndPaintTexture(int width, int height) // Create final texture from the image auto texture = std::make_shared(image); m_textures.push_back(texture); - texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear); - texture->setMagnificationFilter(QOpenGLTexture::Linear); + texture->setMinificationFilter(QOpenGLTexture::Nearest); + texture->setMagnificationFilter(QOpenGLTexture::Nearest); + texture->bind(); + glF.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glF.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + texture->release(); return Texture(texture->textureId(), width, height); } diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index a67f10e..7eb2dd7 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -67,9 +67,7 @@ void TargetPainter::paint(QNanoPainter *painter) Q_ASSERT(shaderProgram->isLinked()); // Set up vertex data and buffers for a quad - float vertices[] = { - -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, - }; + float vertices[] = { -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, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f }; GLuint VBO, VAO; glF.glGenVertexArrays(1, &VAO); @@ -95,7 +93,9 @@ void TargetPainter::paint(QNanoPainter *painter) glF.glActiveTexture(GL_TEXTURE0); glF.glBindTexture(GL_TEXTURE_2D, texture.handle()); shaderManager->setUniforms(shaderProgram, 0, QSize(m_target->costumeWidth(), m_target->costumeHeight()), effects); // set texture and effect uniforms - glF.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + shaderProgram->setUniformValue("u_projectionMatrix", QMatrix4x4()); + shaderProgram->setUniformValue("u_modelMatrix", QMatrix4x4()); + glF.glDrawArrays(GL_TRIANGLES, 0, 6); // Process the resulting texture // NOTE: This must happen now, not later, because the alpha channel can be used here diff --git a/test/penlayer/penlayer_test.cpp b/test/penlayer/penlayer_test.cpp index ba963e0..007ef81 100644 --- a/test/penlayer/penlayer_test.cpp +++ b/test/penlayer/penlayer_test.cpp @@ -30,6 +30,11 @@ class PenLayerTest : public testing::Test m_surface.create(); Q_ASSERT(m_surface.isValid()); m_context.makeCurrent(&m_surface); + + QOpenGLFunctions glF(&m_context); + glF.initializeOpenGLFunctions(); + glF.glEnable(GL_BLEND); + glF.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } void TearDown() override @@ -373,6 +378,8 @@ TEST_F(PenLayerTest, Stamp) loader.setFileName("stamp_env.sb3"); loader.start(); // wait until it loads + EXPECT_CALL(engine, stageWidth()).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillRepeatedly(Return(360)); std::vector> targets; StageModel *stage = loader.stage(); targets.push_back(std::make_unique()); @@ -406,12 +413,11 @@ TEST_F(PenLayerTest, Stamp) QOpenGLFramebufferObject *fbo = penLayer.framebufferObject(); QImage image = fbo->toImage().scaled(240, 180); QImage ref("stamp.png"); - ASSERT_LE(fuzzyCompareImages(image, ref), 0.18); + ASSERT_LE(fuzzyCompareImages(image, ref), 0.22); } // Test HQ pen penLayer.clear(); - EXPECT_CALL(engine, stageWidth()).Times(3).WillRepeatedly(Return(480)); penLayer.setHqPen(true); penLayer.setWidth(720); penLayer.setHeight(540); @@ -423,7 +429,7 @@ TEST_F(PenLayerTest, Stamp) QOpenGLFramebufferObject *fbo = penLayer.framebufferObject(); QImage image = fbo->toImage(); QImage ref("stamp_hq.png"); - ASSERT_LE(fuzzyCompareImages(image, ref), 0.33); + ASSERT_LE(fuzzyCompareImages(image, ref), 0.42); } } diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index f2ebac0..4d67461 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -138,8 +138,8 @@ TEST_F(RenderedTargetTest, UpdateMethods) SpriteModel spriteModel; sprite.setInterface(&spriteModel); - EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); - EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); + EXPECT_CALL(engine, stageWidth()).Times(3).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(3).WillRepeatedly(Return(360)); target.setSpriteModel(&spriteModel); target.loadCostumes(); target.beforeRedraw(); diff --git a/test/stamp.png b/test/stamp.png index 7bf9305..c9018b2 100644 Binary files a/test/stamp.png and b/test/stamp.png differ diff --git a/test/stamp_hq.png b/test/stamp_hq.png index c32caf9..ad21d34 100644 Binary files a/test/stamp_hq.png and b/test/stamp_hq.png differ