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 a98a39eee..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 @@ -217,38 +218,21 @@ 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; + + // "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 // DRAW_MODE_line } diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index b7b161529..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,9 +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, 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 @@ -43,19 +48,18 @@ 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); + vec2 normalized = pointDiff / max(u_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;