diff --git a/src/PenSkin.js b/src/PenSkin.js index fe5ae58a8..827248edb 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -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. @@ -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(); @@ -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; @@ -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 ] } }; @@ -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. @@ -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]; @@ -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); diff --git a/src/ShaderManager.js b/src/ShaderManager.js index a71ef2d2e..32f032f3d 100644 --- a/src/ShaderManager.js +++ b/src/ShaderManager.js @@ -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; diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index 13fe19deb..bd989ca92 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -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,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 } diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index a9bbe1507..c92468e11 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -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 }