Skip to content

Rewrite stamp #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 60 additions & 104 deletions src/penlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<libscratchcpp::IEngine *, IPenLayer *> PenLayer::m_projectPenLayers;

// TODO: Move this to a separate class
template<typename T>
short sgn(T x)
{
return (T(0) < x) - (x < T(0));
}

PenLayer::PenLayer(QNanoQuickItem *parent) :
IPenLayer(parent)
{
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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<libscratchcpp::Costume> 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<double>(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<float>(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();

Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/penlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class PenLayer : public IPenLayer
void updateTexture();

static std::unordered_map<libscratchcpp::IEngine *, IPenLayer *> m_projectPenLayers;
static inline GLuint m_stampFbo = 0;
bool m_antialiasingEnabled = true;
libscratchcpp::IEngine *m_engine = nullptr;
bool m_hqPen = false;
Expand All @@ -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;
};
Expand Down
68 changes: 32 additions & 36 deletions src/renderedtarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
4 changes: 3 additions & 1 deletion src/shaders/sprite.vert
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 6 additions & 2 deletions src/skin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ Texture Skin::createAndPaintTexture(int width, int height)
// Create final texture from the image
auto texture = std::make_shared<QOpenGLTexture>(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);
}
Loading
Loading