Compare commits

...

15 Commits

Author SHA1 Message Date
greenkeeper[bot]
005e789df8 fix(package): update twgl.js to version 4.15.0
Closes #352
2020-04-27 14:10:05 +00:00
DD Liu
4b2e17186b Merge pull request #585 from adroitwhiz/update-silhouette-convex-hull
Update drawables' matrix + silhouette in _getConvexHullPointsForDrawable
2020-04-24 14:59:17 -04:00
DD Liu
6dd833a7f9 Merge pull request #586 from LLK/revert-582-revert-580-revert-559-revert-438-pen-shader
Revert "Revert "Revert "Revert "Draw pen lines via fragment shader""""
2020-04-22 19:45:49 -04:00
DD Liu
5bf185262a Revert "Revert "Revert "Revert "Draw pen lines via fragment shader"""" 2020-04-22 18:16:58 -04:00
adroitwhiz
e9d6c1d13f Update drawables' matrix+silhouette for hull calc. 2020-04-15 04:51:34 -04:00
DD Liu
58bd9cbe19 Merge pull request #582 from LLK/revert-580-revert-559-revert-438-pen-shader
Revert "Revert "Revert "Draw pen lines via fragment shader"""
2020-04-09 12:30:38 -04:00
DD Liu
bf43ef363a Revert "Revert "Revert "Draw pen lines via fragment shader""" 2020-04-09 12:27:19 -04:00
DD Liu
a94f238ea1 Merge pull request #580 from LLK/revert-559-revert-438-pen-shader
Revert "Revert "Draw pen lines via fragment shader""
2020-04-02 15:14:15 -04:00
DD Liu
4a521509da Merge branch 'develop' into revert-559-revert-438-pen-shader 2020-04-02 14:28:08 -04:00
DD Liu
d17066b745 Merge pull request #475 from adroitwhiz/penskin-silhouette-from-data
Set PenSkin silhouette data directly
2020-04-02 14:24:49 -04:00
DD Liu
6cde82d33d Merge pull request #556 from adroitwhiz/unmultiply-extracted-drawables
Un-premultiply extracted drawables
2020-04-02 14:24:34 -04:00
DD Liu
4958bb5c69 Revert "Revert "Draw pen lines via fragment shader"" 2020-04-02 14:14:06 -04:00
adroitwhiz
a427461467 Clarify draw mode comments 2020-03-26 23:01:18 -04:00
adroitwhiz
90b8f15d8c Un-premultiply extracted drawables 2020-02-04 22:32:04 -05:00
adroitwhiz
b194394cfd Set PenSkin silhouette data directly 2020-01-28 21:05:33 -05:00
6 changed files with 128 additions and 147 deletions

View File

@@ -54,6 +54,6 @@
"raw-loader": "^0.5.1",
"scratch-storage": "^1.0.0",
"scratch-svg-renderer": "0.2.0-prerelease.20200205003400",
"twgl.js": "4.4.0"
"twgl.js": "4.15.0"
}
}

View File

@@ -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.
@@ -109,6 +104,12 @@ class PenSkin extends Skin {
/** @type {boolean} */
this._silhouetteDirty = false;
/** @type {Uint8Array} */
this._silhouettePixels = null;
/** @type {ImageData} */
this._silhouetteImageData = null;
/** @type {object} */
this._lineOnBufferDrawRegionId = {
enter: () => this._enterDrawLineOnBuffer(),
@@ -129,7 +130,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();
@@ -215,10 +216,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;
@@ -228,72 +234,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
]
}
};
@@ -338,6 +288,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.
@@ -351,26 +303,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];
@@ -379,19 +311,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);
@@ -597,6 +520,9 @@ class PenSkin extends Skin {
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
this._silhouettePixels = new Uint8Array(Math.floor(width * height * 4));
this._silhouetteImageData = this._canvas.getContext('2d').createImageData(width, height);
this._silhouetteDirty = true;
}
@@ -634,24 +560,17 @@ class PenSkin extends Skin {
// Render export texture to another framebuffer
const gl = this._renderer.gl;
const bounds = this._bounds;
this._renderer.enterDrawRegion(this._toBufferDrawRegionId);
// Sample the framebuffer's pixels into the silhouette instance
const skinPixels = new Uint8Array(Math.floor(this._canvas.width * this._canvas.height * 4));
gl.readPixels(0, 0, this._canvas.width, this._canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, skinPixels);
gl.readPixels(
0, 0,
this._canvas.width, this._canvas.height,
gl.RGBA, gl.UNSIGNED_BYTE, this._silhouettePixels
);
const skinCanvas = this._canvas;
skinCanvas.width = bounds.width;
skinCanvas.height = bounds.height;
const skinContext = skinCanvas.getContext('2d');
const skinImageData = skinContext.createImageData(bounds.width, bounds.height);
skinImageData.data.set(skinPixels);
skinContext.putImageData(skinImageData, 0, 0);
this._silhouette.update(this._canvas, true /* isPremultiplied */);
this._silhouetteImageData.data.set(this._silhouettePixels);
this._silhouette.update(this._silhouetteImageData, true /* isPremultiplied */);
this._silhouetteDirty = false;
}

View File

@@ -1124,7 +1124,8 @@ class RenderWebGL extends EventEmitter {
gl.clear(gl.COLOR_BUFFER_BIT);
try {
gl.disable(gl.BLEND);
this._drawThese([drawableID], ShaderManager.DRAW_MODE.default, projection,
// ImageData objects store alpha un-premultiplied, so draw with the `straightAlpha` draw mode.
this._drawThese([drawableID], ShaderManager.DRAW_MODE.straightAlpha, projection,
{effectMask: ~ShaderManager.EFFECT_INFO.ghost.mask});
} finally {
gl.enable(gl.BLEND);
@@ -1761,6 +1762,10 @@ class RenderWebGL extends EventEmitter {
*/
_getConvexHullPointsForDrawable (drawableID) {
const drawable = this._allDrawables[drawableID];
drawable.updateMatrix();
drawable.skin.updateSilhouette(this._getDrawableScreenSpaceScale(drawable));
const [width, height] = drawable.skin.size;
// No points in the hull if invisible or size is 0.
if (!drawable.getVisible() || width === 0 || height === 0) {

View File

@@ -154,10 +154,15 @@ ShaderManager.EFFECTS = Object.keys(ShaderManager.EFFECT_INFO);
*/
ShaderManager.DRAW_MODE = {
/**
* Draw normally.
* Draw normally. Its output will use premultiplied alpha.
*/
default: 'default',
/**
* Draw with non-premultiplied alpha. Useful for reading pixels from GL into an ImageData object.
*/
straightAlpha: 'straightAlpha',
/**
* Draw a silhouette using a solid color.
*/
@@ -169,9 +174,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;

View File

@@ -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;
@@ -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
@@ -210,14 +210,31 @@ 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
#ifdef DRAW_MODE_straightAlpha
// 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
}

View File

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