Skip to content

Draw pen lines via fragment shader #438

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 5 commits into from
Feb 27, 2020
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
117 changes: 17 additions & 100 deletions src/PenSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ 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 @@ -129,7 +124,7 @@ class PenSkin extends Skin {
this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.default, NO_EFFECTS);

/** @type {twgl.ProgramInfo} */
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS);
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.line, NO_EFFECTS);

this._createLineGeometry();

Expand Down Expand Up @@ -215,10 +210,15 @@ 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,
this._rotationCenter[0] + x0, this._rotationCenter[1] - y0,
this._rotationCenter[0] + x1, this._rotationCenter[1] - y1
x0 + offset, y0 + offset,
x1 + offset, y1 + offset
);

this._silhouetteDirty = true;
Expand All @@ -228,72 +228,16 @@ 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,

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

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

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

1, 0.5,
1, 1,
1, 1,
0, 0,
0, 0.5
0, 1
]
}
};
Expand Down Expand Up @@ -338,6 +282,8 @@ class PenSkin extends Skin {

/**
* Draw a line on the framebuffer.
* Note that the point coordinates are in the following coordinate space:
* +y is down, (0, 0) is the center, and the coords range from (-width / 2, -height / 2) to (height / 2, width / 2).
* @param {PenAttributes} penAttributes - how the line should be drawn.
* @param {number} x0 - the X coordinate of the beginning of the line.
* @param {number} y0 - the Y coordinate of the beginning of the line.
Expand All @@ -351,26 +297,6 @@ 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];
Expand All @@ -379,19 +305,10 @@ class PenSkin extends Skin {
__premultipliedColor[3] = penColor[3];

const uniforms = {
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
u_lineColor: __premultipliedColor,
u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
u_penPoints: [x0, -y0, x1, -y1],
u_stageSize: this.size
};

twgl.setUniforms(currentShader, uniforms);
Expand Down
4 changes: 2 additions & 2 deletions src/ShaderManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ ShaderManager.DRAW_MODE = {
colorMask: 'colorMask',

/**
* Sample a "texture" to draw a line with caps.
* Draw a line with caps.
*/
lineSample: 'lineSample'
line: 'line'
};

module.exports = ShaderManager;
42 changes: 27 additions & 15 deletions src/shaders/sprite.frag
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ uniform float u_mosaic;
uniform float u_ghost;
#endif // ENABLE_ghost

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

uniform sampler2D u_skin;

Expand Down Expand Up @@ -109,7 +109,7 @@ const vec2 kCenter = vec2(0.5, 0.5);

void main()
{
#ifndef DRAW_MODE_lineSample
#ifndef DRAW_MODE_line
vec2 texcoord0 = v_texCoord;

#ifdef ENABLE_mosaic
Expand Down Expand Up @@ -210,14 +210,26 @@ void main()
#endif // DRAW_MODE_colorMask
#endif // DRAW_MODE_silhouette

#else // DRAW_MODE_lineSample
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_lineSample
#else // DRAW_MODE_line
// Maaaaagic antialiased-line-with-round-caps shader.
// Adapted from Inigo Quilez' 2D distance function cheat sheet
// https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm

// The xy component of u_penPoints is the first point; the zw is the second point.
// This is done to minimize the number of gl.uniform calls, which can add up.
vec2 pa = v_texCoord - u_penPoints.xy, ba = u_penPoints.zw - u_penPoints.xy;
// Magnitude of vector projection of this fragment onto the line (both relative to the line's start point).
// This results in a "linear gradient" which goes from 0.0 at the start point to 1.0 at the end point.
float projMagnitude = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);

float lineDistance = length(pa - (ba * projMagnitude));

// The distance to the line allows us to create lines of any thickness.
// Instead of checking whether this fragment's distance < the line thickness,
// utilize the distance field to get some antialiasing. Fragments far away from the line are 0,
// fragments close to the line are 1, and fragments that are within a 1-pixel border of the line are in between.
float cappedLine = clamp((u_lineThickness + 1.0) * 0.5 - lineDistance, 0.0, 1.0);

gl_FragColor = u_lineColor * cappedLine;
#endif // DRAW_MODE_line
}
61 changes: 48 additions & 13 deletions src/shaders/sprite.vert
Original file line number Diff line number Diff line change
@@ -1,22 +1,57 @@
precision mediump float;

#ifdef DRAW_MODE_line
uniform vec2 u_stageSize;
uniform float u_lineThickness;
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;
#endif

#ifndef DRAW_MODE_line
uniform mat4 u_projectionMatrix;
uniform mat4 u_modelMatrix;
attribute vec2 a_texCoord;
#endif

attribute vec2 a_position;
attribute vec2 a_texCoord;

varying vec2 v_texCoord;

#ifdef DRAW_MODE_lineSample
uniform float u_positionScalar;
#endif

void main() {
#ifdef DRAW_MODE_lineSample
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);
#else
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
#endif
v_texCoord = a_texCoord;
#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;

float lineLength = length(u_penPoints.zw - u_penPoints.xy);

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

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

// Rotate quad to line angle
vec2 normalized = (u_penPoints.zw - u_penPoints.xy + epsilon) / (lineLength + epsilon);
position = mat2(normalized.x, normalized.y, -normalized.y, normalized.x) * position;
// Translate quad
position += u_penPoints.xy;

// Apply view transform
position *= 2.0 / u_stageSize;

gl_Position = vec4(position, 0, 1);
v_texCoord = position * 0.5 * u_stageSize;
#else
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
v_texCoord = a_texCoord;
#endif
}