Skip to content

Commit cab76f6

Browse files
committed
Implement CPU rendering for shape-changing effects
1 parent b855d00 commit cab76f6

File tree

9 files changed

+367
-62
lines changed

9 files changed

+367
-62
lines changed

src/cputexturemanager.cpp

Lines changed: 166 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,40 @@ GLubyte *CpuTextureManager::getTextureData(const Texture &texture)
3333
return it->second;
3434
}
3535

36-
const std::vector<QPoint> &CpuTextureManager::getTextureConvexHullPoints(const Texture &texture)
36+
void CpuTextureManager::getTextureConvexHullPoints(
37+
const Texture &texture,
38+
const QSize &skinSize,
39+
ShaderManager::Effect effectMask,
40+
const std::unordered_map<ShaderManager::Effect, double> &effects,
41+
std::vector<QPoint> &dst)
3742
{
38-
static const std::vector<QPoint> empty;
43+
dst.clear();
3944

4045
if (!texture.isValid())
41-
return empty;
46+
return;
4247

43-
const GLuint handle = texture.handle();
44-
auto it = m_convexHullPoints.find(handle);
48+
// Remove effects that don't change shape
49+
if (effectMask != 0) {
50+
const auto &allEffects = ShaderManager::effects();
4551

46-
if (it == m_convexHullPoints.cend()) {
47-
if (addTexture(texture))
48-
return m_convexHullPoints[handle];
49-
else
50-
return empty;
52+
for (ShaderManager::Effect effect : allEffects) {
53+
if ((effectMask & effect) != 0 && !ShaderManager::effectShapeChanges(effect))
54+
effectMask &= ~effect;
55+
}
56+
}
57+
58+
// If there are no shape-changing effects, use cached hull points
59+
if (effectMask == 0) {
60+
const GLuint handle = texture.handle();
61+
auto it = m_convexHullPoints.find(handle);
62+
63+
if (it == m_convexHullPoints.cend()) {
64+
if (addTexture(texture))
65+
dst = m_convexHullPoints[handle];
66+
} else
67+
dst = it->second;
5168
} else
52-
return it->second;
69+
readTexture(texture, skinSize, effectMask, effects, nullptr, dst);
5370
}
5471

5572
QRgb CpuTextureManager::getPointColor(const Texture &texture, int x, int y, ShaderManager::Effect effectMask, const std::unordered_map<ShaderManager::Effect, double> &effects)
@@ -61,7 +78,7 @@ QRgb CpuTextureManager::getPointColor(const Texture &texture, int x, int y, Shad
6178
// Get local position with effect transform
6279
QVector2D transformedCoords;
6380
const QVector2D localCoords(x / static_cast<float>(width), y / static_cast<float>(height));
64-
EffectTransform::transformPoint(effectMask, effects, localCoords, transformedCoords);
81+
EffectTransform::transformPoint(effectMask, effects, texture.size(), localCoords, transformedCoords);
6582
x = transformedCoords.x() * width;
6683
y = transformedCoords.y() * height;
6784
}
@@ -90,7 +107,7 @@ bool CpuTextureManager::textureContainsPoint(const Texture &texture, const QPoin
90107
// Get local position with effect transform
91108
QVector2D transformedCoords;
92109
const QVector2D localCoords(x / static_cast<float>(width), y / static_cast<float>(height));
93-
EffectTransform::transformPoint(effectMask, effects, localCoords, transformedCoords);
110+
EffectTransform::transformPoint(effectMask, effects, texture.size(), localCoords, transformedCoords);
94111
x = transformedCoords.x() * width;
95112
y = transformedCoords.y() * height;
96113
}
@@ -118,7 +135,24 @@ void CpuTextureManager::removeTexture(const Texture &texture)
118135
}
119136
}
120137

121-
bool CpuTextureManager::addTexture(const Texture &texture)
138+
bool CpuTextureManager::addTexture(const Texture &tex)
139+
{
140+
if (!tex.isValid())
141+
return false;
142+
143+
const GLuint handle = tex.handle();
144+
m_textureData[handle] = nullptr;
145+
m_convexHullPoints[handle] = {};
146+
return readTexture(tex, QSize(), ShaderManager::Effect::NoEffect, {}, &m_textureData[handle], m_convexHullPoints[handle]);
147+
}
148+
149+
bool CpuTextureManager::readTexture(
150+
const Texture &texture,
151+
const QSize &skinSize,
152+
ShaderManager::Effect effectMask,
153+
const std::unordered_map<ShaderManager::Effect, double> &effects,
154+
GLubyte **data,
155+
std::vector<QPoint> &points) const
122156
{
123157
if (!texture.isValid())
124158
return false;
@@ -146,37 +180,134 @@ bool CpuTextureManager::addTexture(const Texture &texture)
146180
GLubyte *pixels = new GLubyte[width * height * 4]; // 4 channels (RGBA)
147181
glF.glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
148182

149-
// Flip vertically
150-
int rowSize = width * 4;
151-
GLubyte *tempRow = new GLubyte[rowSize];
152-
153-
for (size_t i = 0; i < height / 2; ++i) {
154-
size_t topRowIndex = i * rowSize;
155-
size_t bottomRowIndex = (height - 1 - i) * rowSize;
183+
std::vector<QPoint> leftHull;
184+
std::vector<QPoint> rightHull;
185+
leftHull.reserve(height);
186+
rightHull.reserve(height);
156187

157-
// Swap rows
158-
memcpy(tempRow, &pixels[topRowIndex], rowSize);
159-
memcpy(&pixels[topRowIndex], &pixels[bottomRowIndex], rowSize);
160-
memcpy(&pixels[bottomRowIndex], tempRow, rowSize);
188+
for (int x = 0; x < height; x++) {
189+
leftHull.push_back(QPoint(-1, -1));
190+
rightHull.push_back(QPoint(-1, -1));
161191
}
162192

163-
delete[] tempRow;
193+
int leftEndPointIndex = -1;
194+
int rightEndPointIndex = -1;
164195

165-
m_textureData[handle] = pixels;
166-
m_convexHullPoints[handle] = {};
167-
std::vector<QPoint> &hullPoints = m_convexHullPoints[handle];
196+
auto determinant = [](const QPoint &A, const QPoint &B, const QPoint &C) { return (B.x() - A.x()) * (C.y() - A.y()) - (B.y() - A.y()) * (C.x() - A.x()); };
168197

169-
// Get convex hull points
198+
// Get convex hull points (flipped vertically)
199+
// https://github.com/scratchfoundation/scratch-render/blob/0f6663f3148b4f994d58e19590e14c152f1cc2f8/src/RenderWebGL.js#L1829-L1955
170200
for (int y = 0; y < height; y++) {
171-
for (int x = 0; x < width; x++) {
172-
int index = (y * width + x) * 4; // 4 channels (RGBA)
201+
QPoint currentPoint;
202+
int x;
203+
const int flippedY = height - 1 - y;
204+
205+
for (x = 0; x < width; x++) {
206+
int transformedX = x;
207+
int transformedY = flippedY;
208+
209+
if (effectMask != 0) {
210+
// Get local position with effect transform
211+
QVector2D transformedCoords;
212+
const QVector2D localCoords(transformedX / static_cast<float>(width), transformedY / static_cast<float>(height));
213+
EffectTransform::transformPoint(effectMask, effects, skinSize, localCoords, transformedCoords);
214+
transformedX = transformedCoords.x() * width;
215+
transformedY = transformedCoords.y() * height;
216+
}
217+
218+
if ((transformedX >= 0 && transformedX < width) && (transformedY >= 0 && transformedY < height)) {
219+
int index = (transformedY * width + transformedX) * 4;
220+
221+
if (pixels[index + 3] > 0) {
222+
currentPoint.setX(x);
223+
currentPoint.setY(y);
224+
break;
225+
}
226+
}
227+
}
173228

174-
// Check alpha channel
175-
if (pixels[index + 3] > 0)
176-
hullPoints.push_back(QPoint(x, y));
229+
if (x >= width)
230+
continue;
231+
232+
while (leftEndPointIndex > 0) {
233+
if (determinant(leftHull[leftEndPointIndex], leftHull[leftEndPointIndex - 1], currentPoint) > 0)
234+
break;
235+
else {
236+
leftEndPointIndex--;
237+
}
238+
}
239+
240+
leftHull[++leftEndPointIndex] = currentPoint;
241+
242+
for (x = width - 1; x >= 0; x--) {
243+
int transformedX = x;
244+
int transformedY = flippedY;
245+
246+
if (effectMask != 0) {
247+
// Get local position with effect transform
248+
QVector2D transformedCoords;
249+
const QVector2D localCoords(transformedX / static_cast<float>(width), transformedY / static_cast<float>(height));
250+
EffectTransform::transformPoint(effectMask, effects, skinSize, localCoords, transformedCoords);
251+
transformedX = transformedCoords.x() * width;
252+
transformedY = transformedCoords.y() * height;
253+
}
254+
255+
if ((transformedX >= 0 && transformedX < width) && (transformedY >= 0 && transformedY < height)) {
256+
int index = (transformedY * width + transformedX) * 4;
257+
258+
if (pixels[index + 3] > 0) {
259+
currentPoint.setX(x);
260+
currentPoint.setY(y);
261+
break;
262+
}
263+
}
264+
}
265+
266+
while (rightEndPointIndex > 0) {
267+
if (determinant(rightHull[rightEndPointIndex], rightHull[rightEndPointIndex - 1], currentPoint) < 0)
268+
break;
269+
else
270+
rightEndPointIndex--;
177271
}
272+
273+
rightHull[++rightEndPointIndex] = currentPoint;
274+
}
275+
276+
points.clear();
277+
points.reserve((leftEndPointIndex + 1) + (rightEndPointIndex + 1));
278+
279+
long i;
280+
281+
for (i = 0; i < leftHull.size(); i++) {
282+
if (leftHull[i].x() >= 0)
283+
points.push_back(leftHull[i]);
178284
}
179285

286+
for (i = rightEndPointIndex; i >= 0; --i)
287+
if (rightHull[i].x() >= 0)
288+
points.push_back(rightHull[i]);
289+
290+
if (data) {
291+
// Flip vertically
292+
int rowSize = width * 4;
293+
GLubyte *tempRow = new GLubyte[rowSize];
294+
295+
for (size_t i = 0; i < height / 2; ++i) {
296+
size_t topRowIndex = i * rowSize;
297+
size_t bottomRowIndex = (height - 1 - i) * rowSize;
298+
299+
// Swap rows
300+
memcpy(tempRow, &pixels[topRowIndex], rowSize);
301+
memcpy(&pixels[topRowIndex], &pixels[bottomRowIndex], rowSize);
302+
memcpy(&pixels[bottomRowIndex], tempRow, rowSize);
303+
}
304+
305+
delete[] tempRow;
306+
307+
*data = pixels;
308+
} else
309+
delete[] pixels;
310+
180311
// Cleanup
181312
glF.glBindFramebuffer(GL_FRAMEBUFFER, 0);
182313
glF.glDeleteFramebuffers(1, &fbo);

src/cputexturemanager.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,27 @@ class CpuTextureManager
2020
~CpuTextureManager();
2121

2222
GLubyte *getTextureData(const Texture &texture);
23-
const std::vector<QPoint> &getTextureConvexHullPoints(const Texture &texture);
23+
void getTextureConvexHullPoints(
24+
const Texture &texture,
25+
const QSize &skinSize,
26+
ShaderManager::Effect effectMask,
27+
const std::unordered_map<ShaderManager::Effect, double> &effects,
28+
std::vector<QPoint> &dst);
2429

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

2833
void removeTexture(const Texture &texture);
2934

3035
private:
31-
bool addTexture(const Texture &texture);
36+
bool addTexture(const Texture &tex);
37+
bool readTexture(
38+
const Texture &texture,
39+
const QSize &skinSize,
40+
ShaderManager::Effect effectMask,
41+
const std::unordered_map<ShaderManager::Effect, double> &effects,
42+
GLubyte **data,
43+
std::vector<QPoint> &points) const;
3244

3345
std::unordered_map<GLuint, GLubyte *> m_textureData;
3446
std::unordered_map<GLuint, std::vector<QPoint>> m_convexHullPoints;

src/effecttransform.cpp

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66

77
using namespace scratchcpprender;
88

9+
// A texture coordinate is between 0 and 1, 0.5 is the center position
10+
static const float CENTER_X = 0.5f;
11+
static const float CENTER_Y = 0.5f;
12+
13+
inline float fract(float x)
14+
{
15+
// https://registry.khronos.org/OpenGL-Refpages/gl4/html/fract.xhtml
16+
return x - std::floor(x);
17+
}
18+
919
QRgb EffectTransform::transformColor(ShaderManager::Effect effectMask, const std::unordered_map<ShaderManager::Effect, double> &effectValues, QRgb color)
1020
{
1121
// https://github.com/scratchfoundation/scratch-render/blob/e075e5f5ebc95dec4a2718551624ad587c56f0a6/src/EffectTransform.js#L40-L119
@@ -99,8 +109,77 @@ QRgb EffectTransform::transformColor(ShaderManager::Effect effectMask, const std
99109
return inOutColor.rgba();
100110
}
101111

102-
void EffectTransform::transformPoint(ShaderManager::Effect effectMask, const std::unordered_map<ShaderManager::Effect, double> &effectValues, const QVector2D &vec, QVector2D &dst)
112+
void EffectTransform::transformPoint(ShaderManager::Effect effectMask, const std::unordered_map<ShaderManager::Effect, double> &effectValues, const QSize &size, const QVector2D &vec, QVector2D &dst)
103113
{
104-
// TODO: Implement remaining effects
114+
// https://github.com/scratchfoundation/scratch-render/blob/e075e5f5ebc95dec4a2718551624ad587c56f0a6/src/EffectTransform.js#L128-L194
105115
dst = vec;
116+
117+
std::unordered_map<ShaderManager::Effect, float> uniforms;
118+
ShaderManager::getUniformValuesForEffects(effectValues, uniforms);
119+
120+
if ((effectMask & ShaderManager::Effect::Mosaic) != 0) {
121+
// texcoord0 = fract(u_mosaic * texcoord0);
122+
const float mosaic = uniforms[ShaderManager::Effect::Mosaic];
123+
dst.setX(fract(mosaic * dst.x()));
124+
dst.setY(fract(mosaic * dst.y()));
125+
}
126+
127+
if ((effectMask & ShaderManager::Effect::Pixelate) != 0) {
128+
// vec2 pixelTexelSize = u_skinSize / u_pixelate;
129+
const float pixelate = uniforms[ShaderManager::Effect::Pixelate];
130+
const float texelX = size.width() / pixelate;
131+
const float texelY = size.height() / pixelate;
132+
// texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) /
133+
// pixelTexelSize;
134+
dst.setX((std::floor(dst.x() * texelX) + CENTER_X) / texelX);
135+
dst.setY((std::floor(dst.y() * texelY) + CENTER_Y) / texelY);
136+
}
137+
138+
if ((effectMask & ShaderManager::Effect::Whirl) != 0) {
139+
const float whirl = uniforms[ShaderManager::Effect::Whirl];
140+
// const float kRadius = 0.5;
141+
const float RADIUS = 0.5f;
142+
// vec2 offset = texcoord0 - kCenter;
143+
const float offsetX = dst.x() - CENTER_X;
144+
const float offsetY = dst.y() - CENTER_Y;
145+
// float offsetMagnitude = length(offset);
146+
const float offsetMagnitude = std::sqrt(std::pow(offsetX, 2.0f) + std::pow(offsetY, 2.0f));
147+
// float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0);
148+
const float whirlFactor = std::max(1.0f - (offsetMagnitude / RADIUS), 0.0f);
149+
// float whirlActual = u_whirl * whirlFactor * whirlFactor;
150+
const float whirlActual = whirl * whirlFactor * whirlFactor;
151+
// float sinWhirl = sin(whirlActual);
152+
const float sinWhirl = std::sin(whirlActual);
153+
// float cosWhirl = cos(whirlActual);
154+
const float cosWhirl = std::cos(whirlActual);
155+
// mat2 rotationMatrix = mat2(
156+
// cosWhirl, -sinWhirl,
157+
// sinWhirl, cosWhirl
158+
// );
159+
const float rot1 = cosWhirl;
160+
const float rot2 = -sinWhirl;
161+
const float rot3 = sinWhirl;
162+
const float rot4 = cosWhirl;
163+
164+
// texcoord0 = rotationMatrix * offset + kCenter;
165+
dst.setX((rot1 * offsetX) + (rot3 * offsetY) + CENTER_X);
166+
dst.setY((rot2 * offsetX) + (rot4 * offsetY) + CENTER_Y);
167+
}
168+
169+
if ((effectMask & ShaderManager::Effect::Fisheye) != 0) {
170+
const float fisheye = uniforms[ShaderManager::Effect::Fisheye];
171+
// vec2 vec = (texcoord0 - kCenter) / kCenter;
172+
const float vX = (dst.x() - CENTER_X) / CENTER_X;
173+
const float vY = (dst.y() - CENTER_Y) / CENTER_Y;
174+
// float vecLength = length(vec);
175+
const float vLength = std::sqrt((vX * vX) + (vY * vY));
176+
// float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength);
177+
const float r = std::pow(std::min(vLength, 1.0f), fisheye) * std::max(1.0f, vLength);
178+
// vec2 unit = vec / vecLength;
179+
const float unitX = vX / vLength;
180+
const float unitY = vY / vLength;
181+
// texcoord0 = kCenter + r * unit * kCenter;
182+
dst.setX(CENTER_X + (r * unitX * CENTER_X));
183+
dst.setY(CENTER_Y + (r * unitY * CENTER_Y));
184+
}
106185
}

src/effecttransform.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class EffectTransform
1515
EffectTransform() = delete;
1616

1717
static QRgb transformColor(ShaderManager::Effect effectMask, const std::unordered_map<ShaderManager::Effect, double> &effectValues, QRgb color);
18-
static void transformPoint(ShaderManager::Effect effectMask, const std::unordered_map<ShaderManager::Effect, double> &effectValues, const QVector2D &vec, QVector2D &dst);
18+
static void transformPoint(ShaderManager::Effect effectMask, const std::unordered_map<ShaderManager::Effect, double> &effectValues, const QSize &size, const QVector2D &vec, QVector2D &dst);
1919
};
2020

2121
} // namespace scratchcpprender

src/penlayer.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,8 @@ const libscratchcpp::Rect &PenLayer::getBounds() const
412412
double bottom = std::numeric_limits<double>::infinity();
413413
const double width = m_texture.width();
414414
const double height = m_texture.height();
415-
const std::vector<QPoint> &points = m_textureManager.getTextureConvexHullPoints(m_texture);
415+
std::vector<QPoint> points;
416+
m_textureManager.getTextureConvexHullPoints(m_texture, QSize(), ShaderManager::Effect::NoEffect, {}, points);
416417

417418
if (points.empty()) {
418419
m_bounds = libscratchcpp::Rect();

0 commit comments

Comments
 (0)