From ffca1edccec93b2629416997ae2401240c491f18 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Fri, 8 May 2020 15:15:33 -0400 Subject: [PATCH 1/2] Rewrite the pen line shader to be more numerically stable --- src/shaders/sprite.frag | 51 +++++++++++++++-------------------------- src/shaders/sprite.vert | 11 ++++++--- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index a98a39eee..945e62c40 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -217,38 +217,23 @@ void main() #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 - - // On (some?) devices with 16-bit float precision, sufficiently long lines will overflow the float's range. - // Avoid this by scaling these problematic values down to fit within (-1, 1) then scaling them back up later. - // TODO: Is this a problem on all drivers with 16-bit mediump floats, or just Mali? - vec2 pointDiff = abs(u_penPoints.zw - u_penPoints.xy); - float FLOAT_SCALING_INVERSE = max(1.0, max(pointDiff.x, pointDiff.y)); - float FLOAT_SCALING = 1.0 / FLOAT_SCALING_INVERSE; - - // 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; - vec2 pa = (v_texCoord - u_penPoints.xy) * FLOAT_SCALING, ba = (u_penPoints.zw - u_penPoints.xy) * FLOAT_SCALING; - - // Avoid division by zero - float baDot = dot(ba, ba); - // the dot product of a vector and itself is always positive - baDot = max(baDot, epsilon); - - // 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) / baDot, 0.0, 1.0); - - float lineDistance = length(pa - (ba * projMagnitude)) * FLOAT_SCALING_INVERSE; - - // 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; + + float lineLength = length(u_penPoints.zw - u_penPoints.xy); + + // "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, 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 // DRAW_MODE_line } diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index b7b161529..bc76f3f64 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -33,6 +33,12 @@ void main() { float lineLength = length(u_penPoints.zw - u_penPoints.xy); + // 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, 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 *= lineLength + (2.0 * expandedRadius); position.y *= 2.0 * expandedRadius; @@ -43,7 +49,8 @@ void main() { vec2 pointDiff = u_penPoints.zw - u_penPoints.xy; // Ensure line has a nonzero length so it's rendered properly // As long as either component is nonzero, the line length will be nonzero - pointDiff.x = abs(pointDiff.x) < epsilon ? epsilon : pointDiff.x; + // 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 vec2 normalized = pointDiff / max(lineLength, epsilon); @@ -53,9 +60,7 @@ void main() { // 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; From b5e27462d9229604f44e4dcdb98d0f81a8de3141 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 14 May 2020 09:39:43 -0400 Subject: [PATCH 2/2] Pass line length as a uniform HATE. LET ME TELL YOU HOW MUCH I'VE COME TO HATE GLSL FLOATS SINCE I BEGAN TO LIVE. THERE ARE 387.44 MILLION MILES OF PRINTED CIRCUITS IN WAFER THIN LAYERS ACROSS THE MILLIONS OF MOBILE GPUS ACROSS THE WORLD. IF THE WORD HATE WAS ENGRAVED ON EACH NANOANGSTROM OF THOSE HUNDREDS OF MILLIONS OF MILES IT WOULD NOT EQUAL ONE ONE-BILLIONTH OF THE HATE I FEEL FOR GLSL FLOATS AT THIS MICRO-INSTANT. HATE. HATE. --- src/PenSkin.js | 10 ++++++++++ src/shaders/sprite.frag | 5 ++--- src/shaders/sprite.vert | 9 ++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/PenSkin.js b/src/PenSkin.js index ddeea87da..0ed17fb18 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -310,9 +310,19 @@ 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, x1, -y1], u_stageSize: this.size }; diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index 945e62c40..b65fe11a0 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -36,6 +36,7 @@ uniform float u_ghost; #ifdef DRAW_MODE_line uniform vec4 u_lineColor; uniform float u_lineThickness; +uniform float u_lineLength; uniform vec4 u_penPoints; #endif // DRAW_MODE_line @@ -218,12 +219,10 @@ void main() #else // DRAW_MODE_line // Maaaaagic antialiased-line-with-round-caps shader. - float lineLength = length(u_penPoints.zw - u_penPoints.xy); - // "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, lineLength)) * 0.5) + 0.5; + 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. diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index bc76f3f64..90ae4ca5c 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -3,6 +3,7 @@ precision mediump float; #ifdef DRAW_MODE_line uniform vec2 u_stageSize; uniform float u_lineThickness; +uniform float u_lineLength; uniform vec4 u_penPoints; // Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations. @@ -31,15 +32,13 @@ void main() { vec2 position = a_position; float expandedRadius = (u_lineThickness * 0.5) + 1.4142135623730951; - float lineLength = length(u_penPoints.zw - u_penPoints.xy); - // 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, lineLength + (expandedRadius * 2.0), a_position.x) - expandedRadius; + 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 *= lineLength + (2.0 * expandedRadius); + position.x *= u_lineLength + (2.0 * expandedRadius); position.y *= 2.0 * expandedRadius; // Center around first pen point @@ -53,7 +52,7 @@ void main() { 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 - vec2 normalized = pointDiff / max(lineLength, epsilon); + vec2 normalized = pointDiff / max(u_lineLength, epsilon); position = mat2(normalized.x, normalized.y, -normalized.y, normalized.x) * position; // Translate quad position += u_penPoints.xy;