From 624451e23a26a8b0ff37bd69f78b56e9d8adb0e4 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Sun, 2 Aug 2020 18:07:07 -0400 Subject: [PATCH] Restore old pen line code --- src/PenSkin.js | 123 ++++++++++++++++++++++++++++++++-------- src/shaders/sprite.frag | 33 +++++------ src/shaders/sprite.vert | 62 +++----------------- 3 files changed, 120 insertions(+), 98 deletions(-) diff --git a/src/PenSkin.js b/src/PenSkin.js index f1ac7b1f7..84473b11a 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -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. @@ -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; @@ -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 ] } }; @@ -303,6 +359,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]; @@ -310,21 +386,20 @@ class PenSkin extends Skin { __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); diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index f13005901..f9428b7ed 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -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; @@ -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 } diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index ffc5150f0..edc3eedf3 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -1,24 +1,13 @@ 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; @@ -26,48 +15,13 @@ 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; }