Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8139afd

Browse files
authoredFeb 27, 2020
Merge pull request #438 from adroitwhiz/pen-shader
Draw pen lines via fragment shader
2 parents 2a36dad + 7d652b4 commit 8139afd

File tree

4 files changed

+94
-130
lines changed

4 files changed

+94
-130
lines changed
 

‎src/PenSkin.js

Lines changed: 17 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ const __projectionMatrix = twgl.m4.identity();
4444
*/
4545
const __modelTranslationMatrix = twgl.m4.identity();
4646

47-
/**
48-
* Reused memory location for rotation matrix for building a model matrix.
49-
* @type {FloatArray}
50-
*/
51-
const __modelRotationMatrix = twgl.m4.identity();
5247

5348
/**
5449
* Reused memory location for scaling matrix for building a model matrix.
@@ -129,7 +124,7 @@ class PenSkin extends Skin {
129124
this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.default, NO_EFFECTS);
130125

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

134129
this._createLineGeometry();
135130

@@ -215,10 +210,15 @@ class PenSkin extends Skin {
215210
* @param {number} y1 - the Y coordinate of the end of the line.
216211
*/
217212
drawLine (penAttributes, x0, y0, x1, y1) {
213+
// For compatibility with Scratch 2.0, offset pen lines of width 1 and 3 so they're pixel-aligned.
214+
// See https://github.com/LLK/scratch-render/pull/314
215+
const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
216+
const offset = (diameter === 1 || diameter === 3) ? 0.5 : 0;
217+
218218
this._drawLineOnBuffer(
219219
penAttributes,
220-
this._rotationCenter[0] + x0, this._rotationCenter[1] - y0,
221-
this._rotationCenter[0] + x1, this._rotationCenter[1] - y1
220+
x0 + offset, y0 + offset,
221+
x1 + offset, y1 + offset
222222
);
223223

224224
this._silhouetteDirty = true;
@@ -228,72 +228,16 @@ class PenSkin extends Skin {
228228
* Create 2D geometry for drawing lines to a framebuffer.
229229
*/
230230
_createLineGeometry () {
231-
// Create a set of triangulated quads that break up a line into 3 parts:
232-
// 2 caps and a body. The y component of these position vertices are
233-
// divided to bring a value of 1 down to 0.5 to 0. The large y values
234-
// are set so they will still be at least 0.5 after division. The
235-
// divisor is scaled based on the length of the line and the lines
236-
// width.
237-
//
238-
// Texture coordinates are based on a "generated" texture whose general
239-
// shape is a circle. The line caps set their texture values to define
240-
// there roundedness with the texture. The body has all of its texture
241-
// values set to the center of the texture so it's a solid block.
242231
const quads = {
243232
a_position: {
244233
numComponents: 2,
245234
data: [
246-
-0.5, 1,
247-
0.5, 1,
248-
-0.5, 100000,
249-
250-
-0.5, 100000,
251-
0.5, 1,
252-
0.5, 100000,
253-
254-
-0.5, 1,
255-
0.5, 1,
256-
-0.5, -1,
257-
258-
-0.5, -1,
259-
0.5, 1,
260-
0.5, -1,
261-
262-
-0.5, -100000,
263-
0.5, -100000,
264-
-0.5, -1,
265-
266-
-0.5, -1,
267-
0.5, -100000,
268-
0.5, -1
269-
]
270-
},
271-
a_texCoord: {
272-
numComponents: 2,
273-
data: [
274-
1, 0.5,
275-
0, 0.5,
276-
1, 0,
277-
278235
1, 0,
279-
0, 0.5,
280236
0, 0,
281-
282-
0.5, 0,
283-
0.5, 1,
284-
0.5, 0,
285-
286-
0.5, 0,
287-
0.5, 1,
288-
0.5, 1,
289-
290-
1, 0,
291-
0, 0,
292-
1, 0.5,
293-
294-
1, 0.5,
237+
1, 1,
238+
1, 1,
295239
0, 0,
296-
0, 0.5
240+
0, 1
297241
]
298242
}
299243
};
@@ -338,6 +282,8 @@ class PenSkin extends Skin {
338282

339283
/**
340284
* Draw a line on the framebuffer.
285+
* Note that the point coordinates are in the following coordinate space:
286+
* +y is down, (0, 0) is the center, and the coords range from (-width / 2, -height / 2) to (height / 2, width / 2).
341287
* @param {PenAttributes} penAttributes - how the line should be drawn.
342288
* @param {number} x0 - the X coordinate of the beginning of the line.
343289
* @param {number} y0 - the Y coordinate of the beginning of the line.
@@ -351,26 +297,6 @@ class PenSkin extends Skin {
351297

352298
this._renderer.enterDrawRegion(this._lineOnBufferDrawRegionId);
353299

354-
const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
355-
const length = Math.hypot(Math.abs(x1 - x0) - 0.001, Math.abs(y1 - y0) - 0.001);
356-
const avgX = (x0 + x1) / 2;
357-
const avgY = (y0 + y1) / 2;
358-
const theta = Math.atan2(y0 - y1, x0 - x1);
359-
const alias = 1;
360-
361-
// The line needs a bit of aliasing to look smooth. Add a small offset
362-
// and a small size boost to scaling to give a section to alias.
363-
const translationVector = __modelTranslationVector;
364-
translationVector[0] = avgX - (alias / 2);
365-
translationVector[1] = avgY + (alias / 4);
366-
367-
const scalingVector = __modelScalingVector;
368-
scalingVector[0] = diameter + alias;
369-
scalingVector[1] = length + diameter - (alias / 2);
370-
371-
const radius = diameter / 2;
372-
const yScalar = (0.50001 - (radius / (length + diameter)));
373-
374300
// Premultiply pen color by pen transparency
375301
const penColor = penAttributes.color4f || DefaultPenAttributes.color4f;
376302
__premultipliedColor[0] = penColor[0] * penColor[3];
@@ -379,19 +305,10 @@ class PenSkin extends Skin {
379305
__premultipliedColor[3] = penColor[3];
380306

381307
const uniforms = {
382-
u_positionScalar: yScalar,
383-
u_capScale: diameter,
384-
u_aliasAmount: alias,
385-
u_modelMatrix: twgl.m4.multiply(
386-
twgl.m4.multiply(
387-
twgl.m4.translation(translationVector, __modelTranslationMatrix),
388-
twgl.m4.rotationZ(theta - (Math.PI / 2), __modelRotationMatrix),
389-
__modelMatrix
390-
),
391-
twgl.m4.scaling(scalingVector, __modelScalingMatrix),
392-
__modelMatrix
393-
),
394-
u_lineColor: __premultipliedColor
308+
u_lineColor: __premultipliedColor,
309+
u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
310+
u_penPoints: [x0, -y0, x1, -y1],
311+
u_stageSize: this.size
395312
};
396313

397314
twgl.setUniforms(currentShader, uniforms);

‎src/ShaderManager.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ ShaderManager.DRAW_MODE = {
169169
colorMask: 'colorMask',
170170

171171
/**
172-
* Sample a "texture" to draw a line with caps.
172+
* Draw a line with caps.
173173
*/
174-
lineSample: 'lineSample'
174+
line: 'line'
175175
};
176176

177177
module.exports = ShaderManager;

‎src/shaders/sprite.frag

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ uniform float u_mosaic;
3333
uniform float u_ghost;
3434
#endif // ENABLE_ghost
3535

36-
#ifdef DRAW_MODE_lineSample
36+
#ifdef DRAW_MODE_line
3737
uniform vec4 u_lineColor;
38-
uniform float u_capScale;
39-
uniform float u_aliasAmount;
40-
#endif // DRAW_MODE_lineSample
38+
uniform float u_lineThickness;
39+
uniform vec4 u_penPoints;
40+
#endif // DRAW_MODE_line
4141

4242
uniform sampler2D u_skin;
4343

@@ -109,7 +109,7 @@ const vec2 kCenter = vec2(0.5, 0.5);
109109

110110
void main()
111111
{
112-
#ifndef DRAW_MODE_lineSample
112+
#ifndef DRAW_MODE_line
113113
vec2 texcoord0 = v_texCoord;
114114

115115
#ifdef ENABLE_mosaic
@@ -210,14 +210,26 @@ void main()
210210
#endif // DRAW_MODE_colorMask
211211
#endif // DRAW_MODE_silhouette
212212

213-
#else // DRAW_MODE_lineSample
214-
gl_FragColor = u_lineColor * clamp(
215-
// Scale the capScale a little to have an aliased region.
216-
(u_capScale + u_aliasAmount -
217-
u_capScale * 2.0 * distance(v_texCoord, vec2(0.5, 0.5))
218-
) / (u_aliasAmount + 1.0),
219-
0.0,
220-
1.0
221-
);
222-
#endif // DRAW_MODE_lineSample
213+
#else // DRAW_MODE_line
214+
// Maaaaagic antialiased-line-with-round-caps shader.
215+
// Adapted from Inigo Quilez' 2D distance function cheat sheet
216+
// https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
217+
218+
// The xy component of u_penPoints is the first point; the zw is the second point.
219+
// This is done to minimize the number of gl.uniform calls, which can add up.
220+
vec2 pa = v_texCoord - u_penPoints.xy, ba = u_penPoints.zw - u_penPoints.xy;
221+
// Magnitude of vector projection of this fragment onto the line (both relative to the line's start point).
222+
// This results in a "linear gradient" which goes from 0.0 at the start point to 1.0 at the end point.
223+
float projMagnitude = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
224+
225+
float lineDistance = length(pa - (ba * projMagnitude));
226+
227+
// The distance to the line allows us to create lines of any thickness.
228+
// Instead of checking whether this fragment's distance < the line thickness,
229+
// utilize the distance field to get some antialiasing. Fragments far away from the line are 0,
230+
// fragments close to the line are 1, and fragments that are within a 1-pixel border of the line are in between.
231+
float cappedLine = clamp((u_lineThickness + 1.0) * 0.5 - lineDistance, 0.0, 1.0);
232+
233+
gl_FragColor = u_lineColor * cappedLine;
234+
#endif // DRAW_MODE_line
223235
}

‎src/shaders/sprite.vert

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,57 @@
1+
precision mediump float;
2+
3+
#ifdef DRAW_MODE_line
4+
uniform vec2 u_stageSize;
5+
uniform float u_lineThickness;
6+
uniform vec4 u_penPoints;
7+
8+
// Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations.
9+
// Smaller values can cause problems on some mobile devices.
10+
const float epsilon = 1e-3;
11+
#endif
12+
13+
#ifndef DRAW_MODE_line
114
uniform mat4 u_projectionMatrix;
215
uniform mat4 u_modelMatrix;
16+
attribute vec2 a_texCoord;
17+
#endif
318

419
attribute vec2 a_position;
5-
attribute vec2 a_texCoord;
620

721
varying vec2 v_texCoord;
822

9-
#ifdef DRAW_MODE_lineSample
10-
uniform float u_positionScalar;
11-
#endif
12-
1323
void main() {
14-
#ifdef DRAW_MODE_lineSample
15-
vec2 position = a_position;
16-
position.y = clamp(position.y * u_positionScalar, -0.5, 0.5);
17-
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(position, 0, 1);
18-
#else
19-
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
20-
#endif
21-
v_texCoord = a_texCoord;
24+
#ifdef DRAW_MODE_line
25+
// Calculate a rotated ("tight") bounding box around the two pen points.
26+
// Yes, we're doing this 6 times (once per vertex), but on actual GPU hardware,
27+
// it's still faster than doing it in JS combined with the cost of uniformMatrix4fv.
28+
29+
// Expand line bounds by sqrt(2) / 2 each side-- this ensures that all antialiased pixels
30+
// fall within the quad, even at a 45-degree diagonal
31+
vec2 position = a_position;
32+
float expandedRadius = (u_lineThickness * 0.5) + 1.4142135623730951;
33+
34+
float lineLength = length(u_penPoints.zw - u_penPoints.xy);
35+
36+
position.x *= lineLength + (2.0 * expandedRadius);
37+
position.y *= 2.0 * expandedRadius;
38+
39+
// Center around first pen point
40+
position -= expandedRadius;
41+
42+
// Rotate quad to line angle
43+
vec2 normalized = (u_penPoints.zw - u_penPoints.xy + epsilon) / (lineLength + epsilon);
44+
position = mat2(normalized.x, normalized.y, -normalized.y, normalized.x) * position;
45+
// Translate quad
46+
position += u_penPoints.xy;
47+
48+
// Apply view transform
49+
position *= 2.0 / u_stageSize;
50+
51+
gl_Position = vec4(position, 0, 1);
52+
v_texCoord = position * 0.5 * u_stageSize;
53+
#else
54+
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
55+
v_texCoord = a_texCoord;
56+
#endif
2257
}

0 commit comments

Comments
 (0)
Please sign in to comment.