Skip to content
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

Restore old pen line code #672

Closed
wants to merge 1 commit into from
Closed
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
123 changes: 99 additions & 24 deletions src/PenSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ const __projectionMatrix = twgl.m4.identity();
*/
const __modelTranslationMatrix = twgl.m4.identity();

/**
* Reused memory location for rotation matrix for building a model matrix.
* @type {FloatArray}
*/
const __modelRotationMatrix = twgl.m4.identity();

/**
* Reused memory location for scaling matrix for building a model matrix.
Expand Down Expand Up @@ -216,15 +221,10 @@ class PenSkin extends Skin {
* @param {number} y1 - the Y coordinate of the end of the line.
*/
drawLine (penAttributes, x0, y0, x1, y1) {
// For compatibility with Scratch 2.0, offset pen lines of width 1 and 3 so they're pixel-aligned.
// See https://github.com/LLK/scratch-render/pull/314
const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
const offset = (diameter === 1 || diameter === 3) ? 0.5 : 0;

this._drawLineOnBuffer(
penAttributes,
x0 + offset, y0 + offset,
x1 + offset, y1 + offset
this._rotationCenter[0] + x0, this._rotationCenter[1] - y0,
this._rotationCenter[0] + x1, this._rotationCenter[1] - y1
);

this._silhouetteDirty = true;
Expand All @@ -234,16 +234,72 @@ class PenSkin extends Skin {
* Create 2D geometry for drawing lines to a framebuffer.
*/
_createLineGeometry () {
// Create a set of triangulated quads that break up a line into 3 parts:
// 2 caps and a body. The y component of these position vertices are
// divided to bring a value of 1 down to 0.5 to 0. The large y values
// are set so they will still be at least 0.5 after division. The
// divisor is scaled based on the length of the line and the lines
// width.
//
// Texture coordinates are based on a "generated" texture whose general
// shape is a circle. The line caps set their texture values to define
// there roundedness with the texture. The body has all of its texture
// values set to the center of the texture so it's a solid block.
const quads = {
a_position: {
numComponents: 2,
data: [
-0.5, 1,
0.5, 1,
-0.5, 100000,

-0.5, 100000,
0.5, 1,
0.5, 100000,

-0.5, 1,
0.5, 1,
-0.5, -1,

-0.5, -1,
0.5, 1,
0.5, -1,

-0.5, -100000,
0.5, -100000,
-0.5, -1,

-0.5, -1,
0.5, -100000,
0.5, -1
]
},
a_texCoord: {
numComponents: 2,
data: [
1, 0.5,
0, 0.5,
1, 0,

1, 0,
0, 0.5,
0, 0,
1, 1,
1, 1,

0.5, 0,
0.5, 1,
0.5, 0,

0.5, 0,
0.5, 1,
0.5, 1,

1, 0,
0, 0,
0, 1
1, 0.5,

1, 0.5,
0, 0,
0, 0.5
]
}
};
Expand Down Expand Up @@ -303,28 +359,47 @@ class PenSkin extends Skin {

this._renderer.enterDrawRegion(this._lineOnBufferDrawRegionId);

const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
const length = Math.hypot(Math.abs(x1 - x0) - 0.001, Math.abs(y1 - y0) - 0.001);
const avgX = (x0 + x1) / 2;
const avgY = (y0 + y1) / 2;
const theta = Math.atan2(y0 - y1, x0 - x1);
const alias = 1;

// The line needs a bit of aliasing to look smooth. Add a small offset
// and a small size boost to scaling to give a section to alias.
const translationVector = __modelTranslationVector;
translationVector[0] = avgX - (alias / 2);
translationVector[1] = avgY + (alias / 4);

const scalingVector = __modelScalingVector;
scalingVector[0] = diameter + alias;
scalingVector[1] = length + diameter - (alias / 2);

const radius = diameter / 2;
const yScalar = (0.50001 - (radius / (length + diameter)));

// Premultiply pen color by pen transparency
const penColor = penAttributes.color4f || DefaultPenAttributes.color4f;
__premultipliedColor[0] = penColor[0] * penColor[3];
__premultipliedColor[1] = penColor[1] * penColor[3];
__premultipliedColor[2] = penColor[2] * penColor[3];
__premultipliedColor[3] = penColor[3];

// Fun fact: Doing this calculation in the shader has the potential to overflow the floating-point range.
// 'mediump' precision is only required to have a range up to 2^14 (16384), so any lines longer than 2^7 (128)
// can overflow that, because you're squaring the operands, and they could end up as "infinity".
// Even GLSL's `length` function won't save us here:
// https://asawicki.info/news_1596_watch_out_for_reduced_precision_normalizelength_in_opengl_es
const lineDiffX = x1 - x0;
const lineDiffY = y1 - y0;
const lineLength = Math.sqrt((lineDiffX * lineDiffX) + (lineDiffY * lineDiffY));

const uniforms = {
u_lineColor: __premultipliedColor,
u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
u_lineLength: lineLength,
u_penPoints: [x0, -y0, lineDiffX, -lineDiffY],
u_stageSize: this.size
u_positionScalar: yScalar,
u_capScale: diameter,
u_aliasAmount: alias,
u_modelMatrix: twgl.m4.multiply(
twgl.m4.multiply(
twgl.m4.translation(translationVector, __modelTranslationMatrix),
twgl.m4.rotationZ(theta - (Math.PI / 2), __modelRotationMatrix),
__modelMatrix
),
twgl.m4.scaling(scalingVector, __modelScalingMatrix),
__modelMatrix
),
u_lineColor: __premultipliedColor
};

twgl.setUniforms(currentShader, uniforms);
Expand Down
33 changes: 13 additions & 20 deletions src/shaders/sprite.frag
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ uniform float u_ghost;

#ifdef DRAW_MODE_line
uniform vec4 u_lineColor;
uniform float u_lineThickness;
uniform float u_lineLength;
uniform float u_capScale;
uniform float u_aliasAmount;
#endif // DRAW_MODE_line

uniform sampler2D u_skin;
Expand Down Expand Up @@ -215,23 +215,16 @@ void main()
gl_FragColor.rgb /= gl_FragColor.a + epsilon;
#endif

#else // DRAW_MODE_line
// Maaaaagic antialiased-line-with-round-caps shader.

// "along-the-lineness". This increases parallel to the line.
// It goes from negative before the start point, to 0.5 through the start to the end, then ramps up again
// past the end point.
float d = ((v_texCoord.x - clamp(v_texCoord.x, 0.0, u_lineLength)) * 0.5) + 0.5;

// Distance from (0.5, 0.5) to (d, the perpendicular coordinate). When we're in the middle of the line,
// d will be 0.5, so the distance will be 0 at points close to the line and will grow at points further from it.
// For the "caps", d will ramp down/up, giving us rounding.
// See https://www.youtube.com/watch?v=PMltMdi1Wzg for a rough outline of the technique used to round the lines.
float line = distance(vec2(0.5), vec2(d, v_texCoord.y)) * 2.0;
// Expand out the line by its thickness.
line -= ((u_lineThickness - 1.0) * 0.5);
// Because "distance to the center of the line" decreases the closer we get to the line, but we want more opacity
// the closer we are to the line, invert it.
gl_FragColor = u_lineColor * clamp(1.0 - line, 0.0, 1.0);
#endif // !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background))

#ifdef DRAW_MODE_line
gl_FragColor = u_lineColor * clamp(
// Scale the capScale a little to have an aliased region.
(u_capScale + u_aliasAmount -
u_capScale * 2.0 * distance(v_texCoord, vec2(0.5, 0.5))
) / (u_aliasAmount + 1.0),
0.0,
1.0
);
#endif // DRAW_MODE_line
}
62 changes: 8 additions & 54 deletions src/shaders/sprite.vert
Original file line number Diff line number Diff line change
@@ -1,73 +1,27 @@
precision mediump float;

#ifdef DRAW_MODE_line
uniform vec2 u_stageSize;
uniform float u_lineThickness;
uniform float u_lineLength;
// The X and Y components of u_penPoints hold the first pen point. The Z and W components hold the difference between
// the second pen point and the first. This is done because calculating the difference in the shader leads to floating-
// point error when both points have large-ish coordinates.
uniform vec4 u_penPoints;

// Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations.
// Smaller values can cause problems on some mobile devices.
const float epsilon = 1e-3;
uniform float u_positionScalar;
#endif

#ifndef DRAW_MODE_line
uniform mat4 u_projectionMatrix;
uniform mat4 u_modelMatrix;

attribute vec2 a_texCoord;
#endif

attribute vec2 a_position;

varying vec2 v_texCoord;

void main() {
#ifdef DRAW_MODE_line
// Calculate a rotated ("tight") bounding box around the two pen points.
// Yes, we're doing this 6 times (once per vertex), but on actual GPU hardware,
// it's still faster than doing it in JS combined with the cost of uniformMatrix4fv.

// Expand line bounds by sqrt(2) / 2 each side-- this ensures that all antialiased pixels
// fall within the quad, even at a 45-degree diagonal
vec2 position = a_position;
float expandedRadius = (u_lineThickness * 0.5) + 1.4142135623730951;

// The X coordinate increases along the length of the line. It's 0 at the center of the origin point
// and is in pixel-space (so at n pixels along the line, its value is n).
v_texCoord.x = mix(0.0, u_lineLength + (expandedRadius * 2.0), a_position.x) - expandedRadius;
// The Y coordinate is perpendicular to the line. It's also in pixel-space.
v_texCoord.y = ((a_position.y - 0.5) * expandedRadius) + 0.5;

position.x *= u_lineLength + (2.0 * expandedRadius);
position.y *= 2.0 * expandedRadius;

// 1. Center around first pen point
position -= expandedRadius;

// 2. Rotate quad to line angle
vec2 pointDiff = u_penPoints.zw;
// Ensure line has a nonzero length so it's rendered properly
// As long as either component is nonzero, the line length will be nonzero
// If the line is zero-length, give it a bit of horizontal length
pointDiff.x = (abs(pointDiff.x) < epsilon && abs(pointDiff.y) < epsilon) ? epsilon : pointDiff.x;
// The `normalized` vector holds rotational values equivalent to sine/cosine
// We're applying the standard rotation matrix formula to the position to rotate the quad to the line angle
// pointDiff can hold large values so we must divide by u_lineLength instead of calling GLSL's normalize function:
// https://asawicki.info/news_1596_watch_out_for_reduced_precision_normalizelength_in_opengl_es
vec2 normalized = pointDiff / max(u_lineLength, epsilon);
position = mat2(normalized.x, normalized.y, -normalized.y, normalized.x) * position;

// 3. Translate quad
position += u_penPoints.xy;

// 4. Apply view transform
position *= 2.0 / u_stageSize;
gl_Position = vec4(position, 0, 1);
vec2 position = a_position;
position.y = clamp(position.y * u_positionScalar, -0.5, 0.5);
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(position, 0, 1);
#elif defined(DRAW_MODE_background)
gl_Position = vec4(a_position * 2.0, 0, 1);
#else
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
v_texCoord = a_texCoord;
#endif
v_texCoord = a_texCoord;
}