Skip to content

Commit

Permalink
Merge pull request #582 from LLK/revert-580-revert-559-revert-438-pen…
Browse files Browse the repository at this point in the history
…-shader

Revert "Revert "Revert "Draw pen lines via fragment shader"""
  • Loading branch information
fsih authored Apr 9, 2020
2 parents a94f238 + bf43ef3 commit 58bd9cb
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 95 deletions.
117 changes: 100 additions & 17 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 @@ -130,7 +135,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.line, NO_EFFECTS);
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS);

this._createLineGeometry();

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,
1, 0.5,

1, 0.5,
0, 0,
0, 1
0, 0.5
]
}
};
Expand Down Expand Up @@ -288,8 +344,6 @@ 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 @@ -303,6 +357,26 @@ 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 @@ -311,10 +385,19 @@ class PenSkin extends Skin {
__premultipliedColor[3] = penColor[3];

const uniforms = {
u_lineColor: __premultipliedColor,
u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
u_penPoints: [x0, -y0, x1, -y1],
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
4 changes: 2 additions & 2 deletions src/ShaderManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,9 @@ ShaderManager.DRAW_MODE = {
colorMask: 'colorMask',

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

module.exports = ShaderManager;
44 changes: 16 additions & 28 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_line
#ifdef DRAW_MODE_lineSample
uniform vec4 u_lineColor;
uniform float u_lineThickness;
uniform vec4 u_penPoints;
#endif // DRAW_MODE_line
uniform float u_capScale;
uniform float u_aliasAmount;
#endif // DRAW_MODE_lineSample

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_line
#ifndef DRAW_MODE_lineSample
vec2 texcoord0 = v_texCoord;

#ifdef ENABLE_mosaic
Expand Down Expand Up @@ -214,27 +214,15 @@ void main()
// Un-premultiply alpha.
gl_FragColor.rgb /= gl_FragColor.a + epsilon;
#endif

#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

#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
}
61 changes: 13 additions & 48 deletions src/shaders/sprite.vert
Original file line number Diff line number Diff line change
@@ -1,57 +1,22 @@
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;

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;

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;
#ifdef DRAW_MODE_lineSample
uniform float u_positionScalar;
#endif

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
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;
}

0 comments on commit 58bd9cb

Please sign in to comment.