Skip to content

Implement CPU graphic effects #142

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 4 commits into from
Sep 8, 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
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ qt_add_qml_module(scratchcpp-render
textbubblepainter.h
cputexturemanager.cpp
cputexturemanager.h
effecttransform.cpp
effecttransform.h
blocks/penextension.cpp
blocks/penextension.h
blocks/penblocks.cpp
Expand Down
40 changes: 29 additions & 11 deletions src/cputexturemanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "cputexturemanager.h"
#include "texture.h"
#include "effecttransform.h"

using namespace scratchcpprender;

Expand Down Expand Up @@ -51,10 +52,36 @@ const std::vector<QPoint> &CpuTextureManager::getTextureConvexHullPoints(const T
return it->second;
}

bool CpuTextureManager::textureContainsPoint(const Texture &texture, const QPointF &localPoint)
QRgb CpuTextureManager::getPointColor(const Texture &texture, int x, int y, const std::unordered_map<ShaderManager::Effect, double> &effects)
{
const int width = texture.width();
const int height = texture.height();

if (!effects.empty()) {
// Get local position with effect transform
QVector2D transformedCoords;
const QVector2D localCoords(x / static_cast<float>(width), y / static_cast<float>(height));
EffectTransform::transformPoint(effects, localCoords, transformedCoords);
x = transformedCoords.x() * width;
y = transformedCoords.y() * height;
}

if ((x < 0 || x >= width) || (y < 0 || y >= height))
return qRgba(0, 0, 0, 0);

GLubyte *pixels = getTextureData(texture);
QRgb color = qRgba(pixels[(y * width + x) * 4], pixels[(y * width + x) * 4 + 1], pixels[(y * width + x) * 4 + 2], pixels[(y * width + x) * 4 + 3]);

if (effects.empty())
return color;
else
return EffectTransform::transformColor(effects, color);
}

bool CpuTextureManager::textureContainsPoint(const Texture &texture, const QPointF &localPoint, const std::unordered_map<ShaderManager::Effect, double> &effects)
{
// https://github.com/scratchfoundation/scratch-render/blob/7b823985bc6fe92f572cc3276a8915e550f7c5e6/src/Silhouette.js#L219-L226
return getPointAlpha(texture, localPoint.x(), localPoint.y()) > 0;
return qAlpha(getPointColor(texture, localPoint.x(), localPoint.y(), effects)) > 0;
}

void CpuTextureManager::removeTexture(const Texture &texture)
Expand Down Expand Up @@ -137,12 +164,3 @@ bool CpuTextureManager::addTexture(const Texture &texture)

return true;
}

int CpuTextureManager::getPointAlpha(const Texture &texture, int x, int y)
{
if ((x < 0 || x >= texture.width()) || (y < 0 || y >= texture.height()))
return 0;

GLubyte *pixels = getTextureData(texture);
return pixels[(y * texture.width() + x) * 4 + 3];
}
6 changes: 4 additions & 2 deletions src/cputexturemanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <QtOpenGL>
#include <unordered_map>

#include "shadermanager.h"

namespace scratchcpprender
{

Expand All @@ -20,13 +22,13 @@ class CpuTextureManager
GLubyte *getTextureData(const Texture &texture);
const std::vector<QPoint> &getTextureConvexHullPoints(const Texture &texture);

bool textureContainsPoint(const Texture &texture, const QPointF &localPoint);
QRgb getPointColor(const Texture &texture, int x, int y, const std::unordered_map<ShaderManager::Effect, double> &effects);
bool textureContainsPoint(const Texture &texture, const QPointF &localPoint, const std::unordered_map<ShaderManager::Effect, double> &effects);

void removeTexture(const Texture &texture);

private:
bool addTexture(const Texture &texture);
int getPointAlpha(const Texture &texture, int x, int y);

std::unordered_map<GLuint, GLubyte *> m_textureData;
std::unordered_map<GLuint, std::vector<QPoint>> m_convexHullPoints;
Expand Down
106 changes: 106 additions & 0 deletions src/effecttransform.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: LGPL-3.0-or-later

#include <QVector2D>

#include "effecttransform.h"

using namespace scratchcpprender;

QRgb EffectTransform::transformColor(const std::unordered_map<ShaderManager::Effect, double> &effectValues, QRgb color)
{
// https://github.com/scratchfoundation/scratch-render/blob/e075e5f5ebc95dec4a2718551624ad587c56f0a6/src/EffectTransform.js#L40-L119
// If the color is fully transparent, don't bother attempting any transformations.
if (qAlpha(color) == 0)
return color;

QColor inOutColor = QColor::fromRgba(color);

std::unordered_map<ShaderManager::Effect, float> uniforms;
ShaderManager::getUniformValuesForEffects(effectValues, uniforms);

const bool enableColor = uniforms[ShaderManager::Effect::Color] != 0;
const bool enableBrightness = uniforms[ShaderManager::Effect::Brightness] != 0;

if (enableColor || enableBrightness) {
// gl_FragColor.rgb /= gl_FragColor.a + epsilon;
// Here, we're dividing by the (previously pre-multiplied) alpha to ensure HSV is properly calculated
// for partially transparent pixels.
const float alpha = inOutColor.alphaF();

if (alpha == 0) {
inOutColor.setRed(255);
inOutColor.setGreen(255);
inOutColor.setBlue(255);
} else {
inOutColor.setRedF(inOutColor.redF() / alpha);
inOutColor.setGreenF(inOutColor.greenF() / alpha);
inOutColor.setBlueF(inOutColor.blueF() / alpha);
}

if (enableColor) {
// vec3 hsv = convertRGB2HSV(gl_FragColor.xyz);
QColor hsv = inOutColor.toHsv();

// this code forces grayscale values to be slightly saturated
// so that some slight change of hue will be visible
// const float minLightness = 0.11 / 2.0;
const float minV = 0.11f / 2.0f;
// const float minSaturation = 0.09;
const float minS = 0.09f;
// if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness);
if (hsv.valueF() < minV) {
hsv.setHsvF(0.0f, 1.0f, minV);
// else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z);
} else if (hsv.saturationF() < minS) {
hsv.setHsvF(0.0f, minS, hsv.valueF());
}

// hsv.x = mod(hsv.x + u_color, 1.0);
// if (hsv.x < 0.0) hsv.x += 1.0;
float hue = std::fmod(uniforms[ShaderManager::Effect::Color] + hsv.hueF(), 1.0f);

if (hue < 0.0f)
hue += 1.0f;

hsv.setHsvF(hue, hsv.saturationF(), hsv.valueF());

// gl_FragColor.rgb = convertHSV2RGB(hsl);
inOutColor = hsv.toRgb();
}

if (enableBrightness) {
const float brightness = uniforms[ShaderManager::Effect::Brightness] * 255.0f;
// gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1));
inOutColor.setRed(std::clamp(inOutColor.red() + brightness, 0.0f, 255.0f));
inOutColor.setGreen(std::clamp(inOutColor.green() + brightness, 0.0f, 255.0f));
inOutColor.setBlue(std::clamp(inOutColor.blue() + brightness, 0.0f, 255.0f));
}

// gl_FragColor.rgb *= gl_FragColor.a + epsilon;
// Now we're doing the reverse, premultiplying by the alpha once again.
inOutColor.setRedF(inOutColor.redF() * alpha);
inOutColor.setGreenF(inOutColor.greenF() * alpha);
inOutColor.setBlueF(inOutColor.blueF() * alpha);

// Restore alpha
inOutColor.setAlphaF(alpha);
}

const float ghost = uniforms[ShaderManager::Effect::Ghost];

if (ghost != 1) {
// gl_FragColor *= u_ghost
inOutColor.setRedF(inOutColor.redF() * ghost);
inOutColor.setGreenF(inOutColor.greenF() * ghost);
inOutColor.setBlueF(inOutColor.blueF() * ghost);
inOutColor.setAlphaF(inOutColor.alphaF() * ghost);
}

return inOutColor.rgba();
}

void EffectTransform::transformPoint(const std::unordered_map<ShaderManager::Effect, double> &effectValues, const QVector2D &vec, QVector2D &dst)
{
// TODO: Implement remaining effects
dst = vec;
}
21 changes: 21 additions & 0 deletions src/effecttransform.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: LGPL-3.0-or-later

#pragma once

#include <QColor>

#include "shadermanager.h"

namespace scratchcpprender
{

class EffectTransform
{
public:
EffectTransform() = delete;

static QRgb transformColor(const std::unordered_map<ShaderManager::Effect, double> &effectValues, QRgb color);
static void transformPoint(const std::unordered_map<ShaderManager::Effect, double> &effectValues, const QVector2D &vec, QVector2D &dst);
};

} // namespace scratchcpprender
8 changes: 2 additions & 6 deletions src/renderedtarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,11 +636,7 @@ QRgb RenderedTarget::colorAtScratchPoint(double x, double y) const
if ((x < 0 || x >= width) || (y < 0 || y >= height))
return qRgba(0, 0, 0, 0);

GLubyte *data = textureManager()->getTextureData(m_cpuTexture);
const int index = (y * width + x) * 4; // RGBA channels
Q_ASSERT(index >= 0 && index < width * height * 4);
// TODO: Apply graphic effects (#117)
return qRgba(data[index], data[index + 1], data[index + 2], data[index + 3]);
return textureManager()->getPointColor(m_cpuTexture, x, y, m_graphicEffects);
}

bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const
Expand Down Expand Up @@ -817,7 +813,7 @@ void RenderedTarget::updateHullPoints()

bool RenderedTarget::containsLocalPoint(const QPointF &point) const
{
return textureManager()->textureContainsPoint(m_cpuTexture, point);
return textureManager()->textureContainsPoint(m_cpuTexture, point, m_graphicEffects);
}

QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const
Expand Down
21 changes: 16 additions & 5 deletions src/shadermanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,10 @@ QOpenGLShaderProgram *ShaderManager::getShaderProgram(const std::unordered_map<E
return it->second;
}

void ShaderManager::setUniforms(QOpenGLShaderProgram *program, int textureUnit, const std::unordered_map<Effect, double> &effectValues)
void ShaderManager::getUniformValuesForEffects(const std::unordered_map<Effect, double> &effectValues, std::unordered_map<Effect, float> &dst)
{
// Set the texture unit
program->setUniformValue(TEXTURE_UNIT_UNIFORM, textureUnit);
dst.clear();

// Set the uniform values for the enabled effects and reset the other effects
for (const auto &[effect, name] : EFFECT_TO_NAME) {
const auto it = effectValues.find(effect);
double value;
Expand All @@ -117,10 +115,23 @@ void ShaderManager::setUniforms(QOpenGLShaderProgram *program, int textureUnit,
value = it->second;

auto converter = EFFECT_CONVERTER.at(effect);
program->setUniformValue(EFFECT_UNIFORM_NAME.at(effect), converter(value));
dst[effect] = converter(value);
}
}

void ShaderManager::setUniforms(QOpenGLShaderProgram *program, int textureUnit, const std::unordered_map<Effect, double> &effectValues)
{
// Set the texture unit
program->setUniformValue(TEXTURE_UNIT_UNIFORM, textureUnit);

// Set uniform values
std::unordered_map<Effect, float> values;
getUniformValuesForEffects(effectValues, values);

for (const auto &[effect, value] : values)
program->setUniformValue(EFFECT_UNIFORM_NAME.at(effect), value);
}

void ShaderManager::registerEffects()
{
// Register graphic effects in libscratchcpp
Expand Down
1 change: 1 addition & 0 deletions src/shadermanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ShaderManager : public QObject
static ShaderManager *instance();

QOpenGLShaderProgram *getShaderProgram(const std::unordered_map<Effect, double> &effectValues);
static void getUniformValuesForEffects(const std::unordered_map<Effect, double> &effectValues, std::unordered_map<Effect, float> &dst);
void setUniforms(QOpenGLShaderProgram *program, int textureUnit, const std::unordered_map<Effect, double> &effectValues);

private:
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ add_subdirectory(graphicseffect)
add_subdirectory(shadermanager)
add_subdirectory(textbubbleshape)
add_subdirectory(textbubblepainter)
add_subdirectory(effecttransform)
14 changes: 14 additions & 0 deletions test/effecttransform/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
add_executable(
effecttransform_test
effecttransform_test.cpp
)

target_link_libraries(
effecttransform_test
GTest::gtest_main
scratchcpp-render
qnanopainter
)

add_test(effecttransform_test)
gtest_discover_tests(effecttransform_test)
Loading
Loading