From 2bcd369b6e0e6d3a5961d0af42111b9be1469505 Mon Sep 17 00:00:00 2001 From: Ricky Reusser <572717+rreusser@users.noreply.github.com> Date: Fri, 19 Nov 2021 11:16:52 -0800 Subject: [PATCH] v0.0.23 --- CHANGELOG.md | 11 ++ dist/regl-gpu-lines.compat.js | 61 ++++++---- dist/regl-gpu-lines.compat.min.js | 2 +- dist/regl-gpu-lines.js | 189 +++++++++++++++++------------- dist/regl-gpu-lines.min.js | 2 +- package.json | 2 +- 6 files changed, 159 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 103cc8c..9090e43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.0.23 + +### Features + +- Turning an integer index into a position was somewhat badly done. This release completely renumbers all of the indices. Instead of modifying geometry and flipping it in order to get winding order correct, it now shifts vertex indices by one while preserving geometry. At the cost of one extra wasted vertex, this has the effect of flipping winding order when needed in order to make it consistent--but without modifying geometry. The resuing code is easier to follow, shorter, cleaner, and shows better results. + +### Bugfixes + +- As a result of renumbering, winding order is now consistent. +- Collapsed triangle vertices are now repeated at the first and last points only, rather than scattered throughout the instances. + ## 0.0.22 ### Features diff --git a/dist/regl-gpu-lines.compat.js b/dist/regl-gpu-lines.compat.js index 7bed56c..6e45214 100644 --- a/dist/regl-gpu-lines.compat.js +++ b/dist/regl-gpu-lines.compat.js @@ -213,31 +213,35 @@ var spec = isEndpoints ? endpointSpec : segmentSpec; var verts = ['B', 'C', 'D']; if (!isEndpoints) verts.unshift('A'); - - function computeCount(props) { - return insertCaps ? isEndpoints // Cap has fixed number, join could either be a cap or a join - ? [props.capResolution, Math.max(props.capResolution, props.joinResolution)] // Both could be either a cap or a join - : [Math.max(props.capResolution, props.joinResolution), Math.max(props.capResolution, props.joinResolution)] : isEndpoints // Draw a cap - ? [props.capResolution, props.joinResolution] // Draw two joins - : [props.joinResolution, props.joinResolution]; - } - + var computeCount = insertCaps ? isEndpoints // Cap has fixed number, join could either be a cap or a join + ? function (props) { + return [props.capRes2, Math.max(props.capRes2, props.joinRes2)]; + } // Both could be either a cap or a join + : function (props) { + return [Math.max(props.capRes2, props.joinRes2), Math.max(props.capRes2, props.joinRes2)]; + } : isEndpoints // Draw a cap + ? function (props) { + return [props.capRes2, props.joinRes2]; + } // Draw two joins + : function (props) { + return [props.joinRes2, props.joinRes2]; + }; return regl({ - vert: "".concat(meta.glsl, "\nconst float CAP_START = ").concat(ORIENTATION$2.CAP_START, ".0;\nconst float CAP_END = ").concat(ORIENTATION$2.CAP_END, ".0;\n\n").concat(spec.glsl, "\n\nattribute float index;\n").concat(debug ? 'attribute float debugInstanceID;' : '', "\n\nuniform vec2 vertexCount, capJoinRes;\nuniform vec2 resolution, capScale;\nuniform float miterLimit;\n").concat(meta.orientation || !isEndpoints ? '' : 'uniform float orientation;', "\n\nvarying vec3 lineCoord;\nvarying float dir;\n").concat(debug ? 'varying vec2 triStripCoord;' : '', "\n").concat(debug ? 'varying float instanceID;' : '', "\n\n// bool isnan(float val) {\n// return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;\n// }\n\nbool invalid(vec4 p) {\n return p.w == 0.0;\n}\n\nconst bool isRound = ").concat(isRound ? 'true' : 'false', ";\nconst float pi = 3.141592653589793;\n\nvoid main() {\n lineCoord = vec3(0);\n\n ").concat(debug ? "instanceID = ".concat(isEndpoints ? '-1.0' : 'debugInstanceID', ";") : '', "\n ").concat(debug ? 'triStripCoord = vec2(floor(index / 2.0), mod(index, 2.0));' : '', "\n\n ").concat(verts.map(function (vert) { + vert: "".concat(meta.glsl, "\nconst float CAP_START = ").concat(ORIENTATION$2.CAP_START, ".0;\nconst float CAP_END = ").concat(ORIENTATION$2.CAP_END, ".0;\n\n").concat(spec.glsl, "\n\nattribute float index;\n").concat(debug ? 'attribute float debugInstanceID;' : '', "\n\nuniform vec2 vertCnt2, capJoinRes2;\nuniform vec2 resolution, capScale;\nuniform float miterLimit;\n").concat(meta.orientation || !isEndpoints ? '' : 'uniform float orientation;', "\n\nvarying vec3 lineCoord;\nvarying float dir;\n").concat(debug ? 'varying vec2 triStripCoord;' : '', "\n").concat(debug ? 'varying float instanceID;' : '', "\n").concat(debug ? 'varying float vertexIndex;' : '', "\n\n// This turns out not to work very well\n// bool isnan(float val) {\n// return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;\n// }\n\nbool invalid(vec4 p) {\n return p.w == 0.0;\n}\n\nvoid main() {\n const float pi = 3.141592653589793;\n\n bool isRound = ").concat(isRound ? 'true' : 'false', ";\n ").concat(debug ? 'vertexIndex = index;' : '', "\n lineCoord = vec3(0);\n\n ").concat(debug ? "instanceID = ".concat(isEndpoints ? '-1.0' : 'debugInstanceID', ";") : '', "\n ").concat(debug ? 'triStripCoord = vec2(floor(index / 2.0), mod(index, 2.0));' : '', "\n\n ").concat(verts.map(function (vert) { return "vec4 p".concat(vert, " = ").concat(meta.position.generate(vert), ";"); - }).join('\n'), "\n\n bool aInvalid = ").concat(isEndpoints ? 'false' : 'invalid(pA)', ";\n bool bInvalid = invalid(pB);\n bool cInvalid = invalid(pC);\n bool dInvalid = invalid(pD);\n\n float mirrorIndex = 2.0 * vertexCount.x + 3.0;\n float totalVertexCount = mirrorIndex + 2.0 + 2.0 * vertexCount.y;\n\n // If we're past the first half-join and half of the segment, then we swap all vertices and start\n // over from the opposite end.\n bool isMirrored = index > mirrorIndex;\n\n // When rendering dedicated endoints, this allows us to insert an end cap *alone* (without the attached\n // segment and join)\n ").concat(isEndpoints ? "if (dInvalid && isMirrored) {\n gl_Position = pB;\n return;\n }" : '', "\n\n // Convert to screen-pixel coordinates\n // Save w so we can perspective re-multiply at the end to get varyings depth-correct\n float pw = isMirrored ? pC.w : pB.w;\n ").concat(verts.map(function (v) { + }).join('\n'), "\n\n // A sensible default for early returns\n gl_Position = pB;\n\n bool aInvalid = ").concat(isEndpoints ? 'false' : 'invalid(pA)', ";\n bool bInvalid = invalid(pB);\n bool cInvalid = invalid(pC);\n bool dInvalid = invalid(pD);\n\n // Vertex count for each part (first half of join, second (mirrored) half). Note that not all of\n // these vertices may be used, for example if we have enough for a round cap but only draw a miter\n // join.\n vec2 v = vertCnt2 + 3.0;\n\n // Total vertex count\n float N = dot(v, vec2(1));\n\n // If we're past the first half-join and half of the segment, then we swap all vertices and start\n // over from the opposite end.\n bool mirror = index >= v.x;\n\n // When rendering dedicated endoints, this allows us to insert an end cap *alone* (without the attached\n // segment and join)\n ").concat(isEndpoints ? "if (dInvalid && mirror) return;" : '', "\n\n // Convert to screen-pixel coordinates\n // Save w so we can perspective re-multiply at the end to get varyings depth-correct\n float pw = mirror ? pC.w : pB.w;\n ").concat(verts.map(function (v) { return "p".concat(v, " = vec4(vec3(p").concat(v, ".xy * resolution, p").concat(v, ".z) / p").concat(v, ".w, 1);"); - }).join('\n'), "\n\n // If it's a cap, mirror A back onto C to accomplish a round\n ").concat(isEndpoints ? "vec4 pA = pC;" : '', "\n\n if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) {\n gl_Position = pB;\n return;\n }\n\n float mirrorSign = isMirrored ? -1.0 : 1.0;\n if (isMirrored) {\n vec4 vTmp = pC; pC = pB; pB = vTmp;\n vTmp = pD; pD = pA; pA = vTmp;\n bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;\n }\n\n ").concat(isEndpoints ? "bool isCap = !isMirrored;" : "bool isCap = false;", ";\n\n if (aInvalid) { ").concat(insertCaps ? 'pA = pC; isCap = true;' : 'pA = 2.0 * pB - pC;', " }\n if (dInvalid) { ").concat(insertCaps ? 'pD = pB;' : 'pD = 2.0 * pC - pB;', " }\n\n float width = isMirrored ? ").concat(meta.width.generate('C'), " : ").concat(meta.width.generate('B'), ";\n\n // Tangent and normal vectors\n vec2 tBC = pC.xy - pB.xy;\n float lBC = length(tBC);\n tBC /= lBC;\n vec2 nBC = vec2(-tBC.y, tBC.x);\n\n vec2 tAB = pB.xy - pA.xy;\n float lAB = length(tAB);\n if (lAB > 0.0) tAB /= lAB;\n vec2 nAB = vec2(-tAB.y, tAB.x);\n\n vec2 tCD = pD.xy - pC.xy;\n float lCD = length(tCD);\n if (lCD > 0.0) tCD /= lCD;\n vec2 nCD = vec2(-tCD.y, tCD.x);\n\n float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);\n\n // This section is very fragile. When lines are collinear, signs flip randomly and break orientation\n // of the middle segment. The fix appears straightforward, but this took a few hours to get right.\n const float tol = 1e-4;\n float dirB = -dot(tBC, nAB);\n float dirC = dot(tBC, nCD);\n bool bCollinear = abs(dirB) < tol;\n bool cCollinear = abs(dirC) < tol;\n bool bIsHairpin = bCollinear && cosB < 0.0;\n bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;\n dirB = bCollinear ? -mirrorSign : sign(dirB);\n dirC = cCollinear ? -mirrorSign : sign(dirC);\n\n vec2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;\n\n // The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!\n float i = index <= mirrorIndex ? index : totalVertexCount - index;\n\n // Chop off the join to get at the segment part index\n float iSeg = i - 2.0 * (isMirrored ? vertexCount.y : vertexCount.x);\n\n // After the first half-join, repeat two vertices of the segment strip in order to get the orientation correct\n // for the next join. These are wasted vertices, but they enable using a triangle strip. for two joins which\n // might be oriented differently.\n if (iSeg > 1.0 && iSeg <= 3.0) {\n iSeg -= 2.0;\n if (dirB * dirC >= 0.0) iSeg += iSeg == 0.0 ? 1.0 : -1.0;\n }\n\n vec2 xBasis = tBC;\n vec2 yBasis = nBC * dirB;\n vec2 xy = vec2(0, 1);\n\n lineCoord.y = dirB * mirrorSign;\n\n if (iSeg < 0.0) {\n // Draw half of a join\n float m2 = dot(miter, miter);\n float lm = length(miter);\n float tBCm = dot(tBC, miter);\n yBasis = miter / lm;\n bool isBevel = 1.0 > miterLimit * m2;\n\n if (mod(i, 2.0) == 0.0) {\n // Outer joint points\n if (isRound || isCap) {\n // Round joins\n xBasis = dirB * vec2(yBasis.y, -yBasis.x);\n float cnt = (isCap ? capJoinRes.x : capJoinRes.y) * 2.0;\n float theta = -0.5 * (acos(cosB) * (min(i, cnt) / cnt) - pi) * (isCap ? 2.0 : 1.0);\n xy = vec2(cos(theta), sin(theta));\n\n if (isCap) {\n if (xy.y > 0.001) xy *= capScale;\n lineCoord.xy = xy.yx * lineCoord.y;\n }\n } else {\n // Miter joins\n yBasis = bIsHairpin ? vec2(0) : miter;\n if (!isBevel) xy.y /= m2;\n }\n } else {\n // Repeat vertex B to create a triangle fan\n lineCoord.y = 0.0;\n xy = vec2(0);\n\n // Offset the center vertex position to get bevel SDF correct\n if (!isRound && isBevel && !isCap) {\n xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);\n }\n }\n //} else if (iSeg == 0.0) { // No op: vertex B + line B-C normal\n } else if (iSeg > 0.0) {\n // vertex B + inner miter\n lineCoord.y = -lineCoord.y;\n\n float miterExt = 0.0;\n if (cosB > -0.9999) {\n float sinB = tAB.x * tBC.y - tAB.y * tBC.x;\n miterExt = sinB / (1.0 + cosB);\n }\n float m = abs(miterExt);\n m = min(m, min(lBC, lAB) / width);\n xy = vec2(m, -1);\n }\n\n ").concat(isEndpoints ? "float orientation = ".concat(meta.orientation ? meta.orientation.generate('') : 'mod(orientation,2.0)', ";") : '', ";\n ").concat(isEndpoints ? "if (orientation == CAP_END) lineCoord.xy = -lineCoord.xy;" : '', "\n\n vec2 dP = mat2(xBasis, yBasis) * xy;\n float dC = dot(dP, tBC) * mirrorSign;\n\n float useC = (isMirrored ? 1.0 : 0.0) + dC * (width / lBC);\n lineCoord.z = useC < 0.0 || useC > 1.0 ? 1.0 : 0.0;\n ").concat(_toConsumableArray(meta.varyings.values()).map(function (varying) { + }).join('\n'), "\n\n // If it's a cap, mirror A back onto C to accomplish a round\n ").concat(isEndpoints ? "vec4 pA = pC;" : '', "\n\n // Reject if invalid or if outside viewing planes\n if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) return;\n\n // Swap everything computed so far if computing mirrored half\n if (mirror) {\n vec4 vTmp = pC; pC = pB; pB = vTmp;\n vTmp = pD; pD = pA; pA = vTmp;\n bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;\n }\n\n ").concat(isEndpoints ? "bool isCap = !mirror;" : "".concat(insertCaps ? '' : 'const ', "bool isCap = false"), ";\n\n // Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a\n // degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging.\n if (aInvalid) { ").concat(insertCaps ? 'pA = pC; isCap = true;' : 'pA = 2.0 * pB - pC;', " }\n if (dInvalid) { ").concat(insertCaps ? 'pD = pB;' : 'pD = 2.0 * pC - pB;', " }\n isRound = isRound || isCap;\n\n // TODO: swap inputs rather than computing both and discarding one\n float width = mirror ? ").concat(meta.width.generate('C'), " : ").concat(meta.width.generate('B'), ";\n\n // Tangent and normal vectors\n vec2 tBC = pC.xy - pB.xy;\n float lBC = length(tBC);\n tBC /= lBC;\n vec2 nBC = vec2(-tBC.y, tBC.x);\n\n vec2 tAB = pB.xy - pA.xy;\n float lAB = length(tAB);\n if (lAB > 0.0) tAB /= lAB;\n vec2 nAB = vec2(-tAB.y, tAB.x);\n\n vec2 tCD = pD.xy - pC.xy;\n float lCD = length(tCD);\n if (lCD > 0.0) tCD /= lCD;\n vec2 nCD = vec2(-tCD.y, tCD.x);\n\n // Clamp for safety, since we take the arccos\n float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);\n\n // This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation\n // of the middle segment. The fix appears straightforward, but this took a few hours to get right.\n const float tol = 1e-4;\n float mirrorSign = mirror ? -1.0 : 1.0;\n float dirB = -dot(tBC, nAB);\n float dirC = dot(tBC, nCD);\n bool bCollinear = abs(dirB) < tol;\n bool cCollinear = abs(dirC) < tol;\n bool bIsHairpin = bCollinear && cosB < 0.0;\n // bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;\n dirB = bCollinear ? -mirrorSign : sign(dirB);\n dirC = cCollinear ? -mirrorSign : sign(dirC);\n\n vec2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;\n\n // Compute our primary \"join index\", that is, the index starting at the very first point of the join.\n // The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!\n float i = mirror ? N - index : index;\n\n // Decide the resolution of whichever feature we're drawing. n is twice the number of points used since\n // that's the only form in which we use this number.\n float res = (isCap ? capJoinRes2.x : capJoinRes2.y);\n\n // Shift the index to send unused vertices to an index below zero, which will then just get clamped to\n // zero and result in repeated points, i.e. degenerate triangles.\n i -= max(0.0, (mirror ? vertCnt2.y : vertCnt2.x) - res);\n\n // Use the direction to offset the index by one. This has the effect of flipping the winding number so\n // that it's always consistent no matter which direction the join turns.\n i += (dirB < 0.0 ? -1.0 : 0.0);\n\n // Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly\n // in the middle, where the mirrored and unmirrored halves meet.\n i -= mirror ? 1.0 : 0.0;\n\n // Clamp to zero and repeat unused excess vertices.\n i = max(0.0, i);\n\n // Start with a default basis pointing along the segment with normal vector outward\n vec2 xBasis = tBC;\n vec2 yBasis = nBC * dirB;\n\n // Default point is 0 along the segment, 1 (width unit) normal to it\n vec2 xy = vec2(0);\n\n lineCoord.y = dirB * mirrorSign;\n\n if (i == res + 1.0) {\n // pick off this one specific index to be the interior miter point\n // If not div-by-zero, then sinB / (1 + cosB)\n float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0;\n xy = vec2(min(abs(m), min(lBC, lAB) / width), -1);\n lineCoord.y = -lineCoord.y;\n } else {\n // Draw half of a join\n float m2 = dot(miter, miter);\n float lm = sqrt(m2);\n yBasis = miter / lm;\n xBasis = dirB * vec2(yBasis.y, -yBasis.x);\n bool isBevel = 1.0 > miterLimit * m2;\n\n if (mod(i, 2.0) == 0.0) {\n // Outer joint points\n if (isRound || i != 0.0) {\n // Round joins\n float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0);\n xy = vec2(cos(theta), sin(theta));\n\n if (isCap) {\n // A special multiplier factor for turning 3-point rounds into square caps (but leave the\n // y == 0.0 point unaffected)\n if (xy.y > 0.001) xy *= capScale;\n lineCoord.xy = xy.yx * lineCoord.y;\n }\n } else {\n // Miter joins\n yBasis = bIsHairpin ? vec2(0) : miter;\n xy.y = isBevel ? 1.0 : 1.0 / m2;\n }\n } else {\n // Center of the fan\n lineCoord.y = 0.0;\n\n // Offset the center vertex position to get bevel SDF correct\n if (isBevel && !isRound) {\n xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);\n }\n }\n }\n\n ").concat(isEndpoints ? "float orientation = ".concat(meta.orientation ? meta.orientation.generate('') : 'mod(orientation,2.0)', ";") : '', ";\n\n // Since we can't know the orientation of end caps without being told. This comes either from\n // input via the orientation property or from a uniform, assuming caps are interleaved (start,\n // end, start, end, etc.) and rendered in two passes: first starts, then ends.\n ").concat(isEndpoints ? "if (orientation == CAP_END) lineCoord.xy = -lineCoord.xy;" : '', "\n\n // Point offset from main vertex position\n vec2 dP = mat2(xBasis, yBasis) * xy;\n\n // Dot with the tangent to account for dashes. Note that by putting this in *one place*, dashes\n // should always be correct without having to compute a unique correction for every point.\n float dx = dot(dP, tBC) * mirrorSign;\n\n // Interpolant: zero for using B, 1 for using C\n float useC = (mirror ? 1.0 : 0.0) + dx * (width / lBC);\n\n lineCoord.z = useC < 0.0 || useC > 1.0 ? 1.0 : 0.0;\n\n // The varying generation code handles clamping, if needed\n ").concat(_toConsumableArray(meta.varyings.values()).map(function (varying) { return varying.generate('useC', 'B', 'C'); }).join('\n'), "\n\n gl_Position = pB;\n gl_Position.xy += width * dP;\n gl_Position.xy /= resolution;\n gl_Position *= pw;\n}"), frag: frag, attributes: _objectSpread2(_objectSpread2({}, indexAttributes), spec.attrs), uniforms: { - vertexCount: function vertexCount(ctx, props) { + vertCnt2: function vertCnt2(ctx, props) { return computeCount(props); }, - capJoinRes: function capJoinRes(ctx, props) { - return [props.capResolution, props.joinResolution]; + capJoinRes2: function capJoinRes2(ctx, props) { + return [props.capRes2, props.joinRes2]; }, miterLimit: function miterLimit(ctx, props) { return props.miterLimit * props.miterLimit; @@ -245,7 +249,9 @@ orientation: regl.prop('orientation'), capScale: regl.prop('capScale') }, - primitive: 'triangle strip', + primitive: function primitive(ctx, props) { + return props.primitive || 'triangle strip'; + }, instances: isEndpoints ? function (ctx, props) { return props.splitCaps ? props.orientation === ORIENTATION$2.CAP_START ? Math.ceil(props.count / 2) : Math.floor(props.count / 2) : props.count; } : function (ctx, props) { @@ -253,7 +259,7 @@ }, count: function count(ctx, props) { var count = computeCount(props); - return 6 + 2 * (count[0] + count[1]); + return 6 + (count[0] + count[1]); } }); } @@ -820,24 +826,29 @@ var lineProps = _step.value; var joinType = sanitizeJoinType(lineProps.join); var capType = sanitizeCapType(lineProps.cap); - var capResolution = lineProps.capResolution === undefined ? 12 : lineProps.capResolution; + var capRes2 = lineProps.capResolution === undefined ? 12 : lineProps.capResolution; if (capType === 'square') { - capResolution = 3; + capRes2 = 3; } else if (capType === 'none') { - capResolution = 1; + capRes2 = 1; } - var joinResolution = 1; - if (joinType === 'round') joinResolution = lineProps.joinResolution === undefined ? 8 : lineProps.joinResolution; + var joinRes2 = 1; + if (joinType === 'round') joinRes2 = lineProps.joinResolution === undefined ? 8 : lineProps.joinResolution; // We only ever use these in doubled-up form + + capRes2 *= 2; + joinRes2 *= 2; var miterLimit = joinType === 'bevel' ? 1 : lineProps.miterLimit === undefined ? 4 : lineProps.miterLimit; var capScale = capType === 'square' ? SQUARE_CAP_SCALE : ROUND_CAP_SCALE; + var primitive = lineProps.primitive; var sharedProps = { - joinResolution: joinResolution, - capResolution: capResolution, + joinRes2: joinRes2, + capRes2: capRes2, capScale: capScale, capType: capType, - miterLimit: miterLimit + miterLimit: miterLimit, + primitive: primitive }; if (lineProps.endpointAttributes && lineProps.endpointCount) { diff --git a/dist/regl-gpu-lines.compat.min.js b/dist/regl-gpu-lines.compat.min.js index d77651d..87ec704 100644 --- a/dist/regl-gpu-lines.compat.min.js +++ b/dist/regl-gpu-lines.compat.min.js @@ -1 +1 @@ -!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(n="undefined"!=typeof globalThis?globalThis:n||self).reglLines=t()}(this,(function(){"use strict";function n(n,t){var e=Object.keys(n);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(n);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(n,t).enumerable}))),e.push.apply(e,r)}return e}function t(t){for(var r=1;rn.length)&&(t=n.length);for(var e=0,r=new Array(t);e=n.length?{done:!0}:{done:!1,value:n[r++]}},e:function(n){throw n},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable,non-array objects must have a [Symbol.iterator]()method.")}var a,s=!0,c=!1;return{s:function(){e=e.call(n)},n:function(){var n=e.next();return s=n.done,n},e:function(n){c=!0,a=n},f:function(){try{s||null==e.return||e.return()}finally{if(c)throw a}}}}var c={CAP_START:0,CAP_END:1,CAP_SHORT:2},f=c,l=function(n,e,r){var i=r.regl,a=r.meta,s=r.frag,c=r.segmentSpec,l=r.endpointSpec,u=r.indexAttributes,p=r.insertCaps,d=r.debug,v=e?l:c,y=["B","C","D"];e||y.unshift("A");function m(n){return p?e?[n.capResolution,Math.max(n.capResolution,n.joinResolution)]:[Math.max(n.capResolution,n.joinResolution),Math.max(n.capResolution,n.joinResolution)]:e?[n.capResolution,n.joinResolution]:[n.joinResolution,n.joinResolution]}return i({vert:"".concat(a.glsl,"\nconst float CAP_START=").concat(f.CAP_START,".0;const float CAP_END=").concat(f.CAP_END,".0;").concat(v.glsl,"\nattribute float index;").concat(d?"attribute float debugInstanceID;":"","\nuniform vec2 vertexCount,capJoinRes;uniform vec2 resolution,capScale;uniform float miterLimit;").concat(a.orientation||!e?"":"uniform float orientation;","\nvarying vec3 lineCoord;varying float dir;").concat(d?"varying vec2 triStripCoord;":"","\n").concat(d?"varying float instanceID;":"","\nbool invalid(vec4 p){return p.w==0.0;}const bool isRound=").concat(n?"true":"false",";const float pi=3.141592653589793;void main(){lineCoord=vec3(0);").concat(d?"instanceID=".concat(e?"-1.0":"debugInstanceID",";"):"","\n").concat(d?"triStripCoord=vec2(floor(index/2.0),mod(index,2.0));":"","\n").concat(y.map((function(n){return"vec4 p".concat(n,"=").concat(a.position.generate(n),";")})).join("\n"),"\nbool aInvalid=").concat(e?"false":"invalid(pA)",";bool bInvalid=invalid(pB);bool cInvalid=invalid(pC);bool dInvalid=invalid(pD);float mirrorIndex=2.0*vertexCount.x+3.0;float totalVertexCount=mirrorIndex+2.0+2.0*vertexCount.y;bool isMirrored=index>mirrorIndex;").concat(e?"if(dInvalid && isMirrored){gl_Position=pB;return;}":"","\nfloat pw=isMirrored?pC.w:pB.w;").concat(y.map((function(n){return"p".concat(n,"=vec4(vec3(p").concat(n,".xy*resolution,p").concat(n,".z)/p").concat(n,".w,1);")})).join("\n"),"\n").concat(e?"vec4 pA=pC;":"","\nif(bInvalid||cInvalid||max(abs(pB.z),abs(pC.z))>1.0){gl_Position=pB;return;}float mirrorSign=isMirrored?-1.0:1.0;if(isMirrored){vec4 vTmp=pC; pC=pB; pB=vTmp;vTmp=pD; pD=pA; pA=vTmp;bool bTmp=dInvalid; dInvalid=aInvalid; aInvalid=bTmp;}").concat(e?"bool isCap=!isMirrored;":"bool isCap=false;",";if(aInvalid){ ").concat(p?"pA=pC; isCap=true;":"pA=2.0*pB-pC;"," }if(dInvalid){ ").concat(p?"pD=pB;":"pD=2.0*pC-pB;"," }float width=isMirrored?").concat(a.width.generate("C"),":").concat(a.width.generate("B"),";vec2 tBC=pC.xy-pB.xy;float lBC=length(tBC);tBC/=lBC;vec2 nBC=vec2(-tBC.y,tBC.x);vec2 tAB=pB.xy-pA.xy;float lAB=length(tAB);if(lAB>0.0)tAB/=lAB;vec2 nAB=vec2(-tAB.y,tAB.x);vec2 tCD=pD.xy-pC.xy;float lCD=length(tCD);if(lCD>0.0)tCD/=lCD;vec2 nCD=vec2(-tCD.y,tCD.x);float cosB=clamp(dot(tAB,tBC),-1.0,1.0);const float tol=1e-4;float dirB=-dot(tBC,nAB);float dirC=dot(tBC,nCD);bool bCollinear=abs(dirB)1.0 && iSeg<=3.0){iSeg-=2.0;if(dirB*dirC>=0.0)iSeg+=iSeg==0.0?1.0:-1.0;}vec2 xBasis=tBC;vec2 yBasis=nBC*dirB;vec2 xy=vec2(0,1);lineCoord.y=dirB*mirrorSign;if(iSeg<0.0){float m2=dot(miter,miter);float lm=length(miter);float tBCm=dot(tBC,miter);yBasis=miter/lm;bool isBevel=1.0>miterLimit*m2;if(mod(i,2.0)==0.0){if(isRound||isCap){xBasis=dirB*vec2(yBasis.y,-yBasis.x);float cnt=(isCap?capJoinRes.x:capJoinRes.y)*2.0;float theta=-0.5*(acos(cosB)*(min(i,cnt)/cnt)-pi)*(isCap?2.0:1.0);xy=vec2(cos(theta),sin(theta));if(isCap){if(xy.y>0.001)xy*=capScale;lineCoord.xy=xy.yx*lineCoord.y;}} else {yBasis=bIsHairpin?vec2(0):miter;if(!isBevel)xy.y/=m2;}} else {lineCoord.y=0.0;xy=vec2(0);if(!isRound && isBevel && !isCap){xy.y=-1.0+sqrt((1.0+cosB)*0.5);}}} else if(iSeg>0.0){lineCoord.y=-lineCoord.y;float miterExt=0.0;if(cosB>-0.9999){float sinB=tAB.x*tBC.y-tAB.y*tBC.x;miterExt=sinB/(1.0+cosB);}float m=abs(miterExt);m=min(m,min(lBC,lAB)/width);xy=vec2(m,-1);}").concat(e?"float orientation=".concat(a.orientation?a.orientation.generate(""):"mod(orientation,2.0)",";"):"",";").concat(e?"if(orientation==CAP_END)lineCoord.xy=-lineCoord.xy;":"","\nvec2 dP=mat2(xBasis,yBasis)*xy;float dC=dot(dP,tBC)*mirrorSign;float useC=(isMirrored?1.0:0.0)+dC*(width/lBC);lineCoord.z=useC<0.0||useC>1.0?1.0:0.0;").concat(o(a.varyings.values()).map((function(n){return n.generate("useC","B","C")})).join("\n"),"\ngl_Position=pB;gl_Position.xy+=width*dP;gl_Position.xy/=resolution;gl_Position*=pw;}"),frag:s,attributes:t(t({},u),v.attrs),uniforms:{vertexCount:function(n,t){return m(t)},capJoinRes:function(n,t){return[t.capResolution,t.joinResolution]},miterLimit:function(n,t){return t.miterLimit*t.miterLimit},orientation:i.prop("orientation"),capScale:i.prop("capScale")},primitive:"triangle strip",instances:e?function(n,t){return t.splitCaps?t.orientation===f.CAP_START?Math.ceil(t.count/2):Math.floor(t.count/2):t.count}:function(n,t){return t.count-3},count:function(n,t){var e=m(t);return 6+2*(e[0]+e[1])}})};var u={NONE:0,REGULAR:1,EXTENDED:2,PER_INSTANCE:4},p=u,d=function(n){for(var e=[],r=n.split("\n"),o=0;o1&&void 0!==arguments[1]?arguments[1]:{},r=e.vert,i=void 0===r?null:r,a=e.frag,c=void 0===a?null:a,f=e.debug,l=void 0!==f&&f,u=e.insertCaps,p=void 0!==u&&u,d=t({},e),v=0,y=["vert","frag","debug","insertCaps"];vn.length)&&(t=n.length);for(var e=0,r=new Array(t);e=n.length?{done:!0}:{done:!1,value:n[r++]}},e:function(n){throw n},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable,non-array objects must have a [Symbol.iterator]()method.")}var a,s=!0,c=!1;return{s:function(){e=e.call(n)},n:function(){var n=e.next();return s=n.done,n},e:function(n){c=!0,a=n},f:function(){try{s||null==e.return||e.return()}finally{if(c)throw a}}}}var c={CAP_START:0,CAP_END:1,CAP_SHORT:2},f=c,l=function(n,e,r){var i=r.regl,a=r.meta,s=r.frag,c=r.segmentSpec,l=r.endpointSpec,u=r.indexAttributes,p=r.insertCaps,d=r.debug,v=e?l:c,h=["B","C","D"];e||h.unshift("A");var m=p?e?function(n){return[n.capRes2,Math.max(n.capRes2,n.joinRes2)]}:function(n){return[Math.max(n.capRes2,n.joinRes2),Math.max(n.capRes2,n.joinRes2)]}:e?function(n){return[n.capRes2,n.joinRes2]}:function(n){return[n.joinRes2,n.joinRes2]};return i({vert:"".concat(a.glsl,"\nconst float CAP_START=").concat(f.CAP_START,".0;const float CAP_END=").concat(f.CAP_END,".0;").concat(v.glsl,"\nattribute float index;").concat(d?"attribute float debugInstanceID;":"","\nuniform vec2 vertCnt2,capJoinRes2;uniform vec2 resolution,capScale;uniform float miterLimit;").concat(a.orientation||!e?"":"uniform float orientation;","\nvarying vec3 lineCoord;varying float dir;").concat(d?"varying vec2 triStripCoord;":"","\n").concat(d?"varying float instanceID;":"","\n").concat(d?"varying float vertexIndex;":"","\nbool invalid(vec4 p){return p.w==0.0;}void main(){const float pi=3.141592653589793;bool isRound=").concat(n?"true":"false",";").concat(d?"vertexIndex=index;":"","\nlineCoord=vec3(0);").concat(d?"instanceID=".concat(e?"-1.0":"debugInstanceID",";"):"","\n").concat(d?"triStripCoord=vec2(floor(index/2.0),mod(index,2.0));":"","\n").concat(h.map((function(n){return"vec4 p".concat(n,"=").concat(a.position.generate(n),";")})).join("\n"),"\ngl_Position=pB;bool aInvalid=").concat(e?"false":"invalid(pA)",";bool bInvalid=invalid(pB);bool cInvalid=invalid(pC);bool dInvalid=invalid(pD);vec2 v=vertCnt2+3.0;float N=dot(v,vec2(1));bool mirror=index>=v.x;").concat(e?"if(dInvalid && mirror)return;":"","\nfloat pw=mirror?pC.w:pB.w;").concat(h.map((function(n){return"p".concat(n,"=vec4(vec3(p").concat(n,".xy*resolution,p").concat(n,".z)/p").concat(n,".w,1);")})).join("\n"),"\n").concat(e?"vec4 pA=pC;":"","\nif(bInvalid||cInvalid||max(abs(pB.z),abs(pC.z))>1.0)return;if(mirror){vec4 vTmp=pC; pC=pB; pB=vTmp;vTmp=pD; pD=pA; pA=vTmp;bool bTmp=dInvalid; dInvalid=aInvalid; aInvalid=bTmp;}").concat(e?"bool isCap=!mirror;":"".concat(p?"":"const ","bool isCap=false"),";if(aInvalid){ ").concat(p?"pA=pC; isCap=true;":"pA=2.0*pB-pC;"," }if(dInvalid){ ").concat(p?"pD=pB;":"pD=2.0*pC-pB;"," }isRound=isRound||isCap;float width=mirror?").concat(a.width.generate("C"),":").concat(a.width.generate("B"),";vec2 tBC=pC.xy-pB.xy;float lBC=length(tBC);tBC/=lBC;vec2 nBC=vec2(-tBC.y,tBC.x);vec2 tAB=pB.xy-pA.xy;float lAB=length(tAB);if(lAB>0.0)tAB/=lAB;vec2 nAB=vec2(-tAB.y,tAB.x);vec2 tCD=pD.xy-pC.xy;float lCD=length(tCD);if(lCD>0.0)tCD/=lCD;vec2 nCD=vec2(-tCD.y,tCD.x);float cosB=clamp(dot(tAB,tBC),-1.0,1.0);const float tol=1e-4;float mirrorSign=mirror?-1.0:1.0;float dirB=-dot(tBC,nAB);float dirC=dot(tBC,nCD);bool bCollinear=abs(dirB)-0.9999?(tAB.x*tBC.y-tAB.y*tBC.x)/(1.0+cosB):0.0;xy=vec2(min(abs(m),min(lBC,lAB)/width),-1);lineCoord.y=-lineCoord.y;} else {float m2=dot(miter,miter);float lm=sqrt(m2);yBasis=miter/lm;xBasis=dirB*vec2(yBasis.y,-yBasis.x);bool isBevel=1.0>miterLimit*m2;if(mod(i,2.0)==0.0){if(isRound||i !=0.0){float theta=-0.5*(acos(cosB)*(clamp(i,0.0,res)/res)-pi)*(isCap?2.0:1.0);xy=vec2(cos(theta),sin(theta));if(isCap){if(xy.y>0.001)xy*=capScale;lineCoord.xy=xy.yx*lineCoord.y;}} else {yBasis=bIsHairpin?vec2(0):miter;xy.y=isBevel?1.0:1.0/m2;}} else {lineCoord.y=0.0;if(isBevel && !isRound){xy.y=-1.0+sqrt((1.0+cosB)*0.5);}}}").concat(e?"float orientation=".concat(a.orientation?a.orientation.generate(""):"mod(orientation,2.0)",";"):"",";").concat(e?"if(orientation==CAP_END)lineCoord.xy=-lineCoord.xy;":"","\nvec2 dP=mat2(xBasis,yBasis)*xy;float dx=dot(dP,tBC)*mirrorSign;float useC=(mirror?1.0:0.0)+dx*(width/lBC);lineCoord.z=useC<0.0||useC>1.0?1.0:0.0;").concat(o(a.varyings.values()).map((function(n){return n.generate("useC","B","C")})).join("\n"),"\ngl_Position=pB;gl_Position.xy+=width*dP;gl_Position.xy/=resolution;gl_Position*=pw;}"),frag:s,attributes:t(t({},u),v.attrs),uniforms:{vertCnt2:function(n,t){return m(t)},capJoinRes2:function(n,t){return[t.capRes2,t.joinRes2]},miterLimit:function(n,t){return t.miterLimit*t.miterLimit},orientation:i.prop("orientation"),capScale:i.prop("capScale")},primitive:function(n,t){return t.primitive||"triangle strip"},instances:e?function(n,t){return t.splitCaps?t.orientation===f.CAP_START?Math.ceil(t.count/2):Math.floor(t.count/2):t.count}:function(n,t){return t.count-3},count:function(n,t){var e=m(t);return 6+(e[0]+e[1])}})};var u={NONE:0,REGULAR:1,EXTENDED:2,PER_INSTANCE:4},p=u,d=function(n){for(var e=[],r=n.split("\n"),o=0;o1&&void 0!==arguments[1]?arguments[1]:{},r=e.vert,i=void 0===r?null:r,a=e.frag,c=void 0===a?null:a,f=e.debug,l=void 0!==f&&f,u=e.insertCaps,p=void 0!==u&&u,d=t({},e),v=0,h=["vert","frag","debug","insertCaps"];v [props.capRes2, Math.max(props.capRes2, props.joinRes2)] // Both could be either a cap or a join + : props => [Math.max(props.capRes2, props.joinRes2), Math.max(props.capRes2, props.joinRes2)] : isEndpoints // Draw a cap + ? props => [props.capRes2, props.joinRes2] // Draw two joins + : props => [props.joinRes2, props.joinRes2]; return regl({ vert: `${meta.glsl} const float CAP_START = ${ORIENTATION$2.CAP_START}.0; @@ -48,7 +44,7 @@ ${spec.glsl} attribute float index; ${debug ? 'attribute float debugInstanceID;' : ''} -uniform vec2 vertexCount, capJoinRes; +uniform vec2 vertCnt2, capJoinRes2; uniform vec2 resolution, capScale; uniform float miterLimit; ${meta.orientation || !isEndpoints ? '' : 'uniform float orientation;'} @@ -57,7 +53,9 @@ varying vec3 lineCoord; varying float dir; ${debug ? 'varying vec2 triStripCoord;' : ''} ${debug ? 'varying float instanceID;' : ''} +${debug ? 'varying float vertexIndex;' : ''} +// This turns out not to work very well // bool isnan(float val) { // return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true; // } @@ -66,10 +64,11 @@ bool invalid(vec4 p) { return p.w == 0.0; } -const bool isRound = ${isRound ? 'true' : 'false'}; -const float pi = 3.141592653589793; - void main() { + const float pi = 3.141592653589793; + + bool isRound = ${isRound ? 'true' : 'false'}; + ${debug ? 'vertexIndex = index;' : ''} lineCoord = vec3(0); ${debug ? `instanceID = ${isEndpoints ? '-1.0' : 'debugInstanceID'};` : ''} @@ -77,51 +76,58 @@ void main() { ${verts.map(vert => `vec4 p${vert} = ${meta.position.generate(vert)};`).join('\n')} + // A sensible default for early returns + gl_Position = pB; + bool aInvalid = ${isEndpoints ? 'false' : 'invalid(pA)'}; bool bInvalid = invalid(pB); bool cInvalid = invalid(pC); bool dInvalid = invalid(pD); - float mirrorIndex = 2.0 * vertexCount.x + 3.0; - float totalVertexCount = mirrorIndex + 2.0 + 2.0 * vertexCount.y; + // Vertex count for each part (first half of join, second (mirrored) half). Note that not all of + // these vertices may be used, for example if we have enough for a round cap but only draw a miter + // join. + vec2 v = vertCnt2 + 3.0; + + // Total vertex count + float N = dot(v, vec2(1)); // If we're past the first half-join and half of the segment, then we swap all vertices and start // over from the opposite end. - bool isMirrored = index > mirrorIndex; + bool mirror = index >= v.x; // When rendering dedicated endoints, this allows us to insert an end cap *alone* (without the attached // segment and join) - ${isEndpoints ? `if (dInvalid && isMirrored) { - gl_Position = pB; - return; - }` : ''} + ${isEndpoints ? `if (dInvalid && mirror) return;` : ''} // Convert to screen-pixel coordinates // Save w so we can perspective re-multiply at the end to get varyings depth-correct - float pw = isMirrored ? pC.w : pB.w; + float pw = mirror ? pC.w : pB.w; ${verts.map(v => `p${v} = vec4(vec3(p${v}.xy * resolution, p${v}.z) / p${v}.w, 1);`).join('\n')} // If it's a cap, mirror A back onto C to accomplish a round ${isEndpoints ? `vec4 pA = pC;` : ''} - if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) { - gl_Position = pB; - return; - } + // Reject if invalid or if outside viewing planes + if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) return; - float mirrorSign = isMirrored ? -1.0 : 1.0; - if (isMirrored) { + // Swap everything computed so far if computing mirrored half + if (mirror) { vec4 vTmp = pC; pC = pB; pB = vTmp; vTmp = pD; pD = pA; pA = vTmp; bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp; } - ${isEndpoints ? `bool isCap = !isMirrored;` : `bool isCap = false;`}; + ${isEndpoints ? `bool isCap = !mirror;` : `${insertCaps ? '' : 'const '}bool isCap = false`}; + // Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a + // degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging. if (aInvalid) { ${insertCaps ? 'pA = pC; isCap = true;' : 'pA = 2.0 * pB - pC;'} } if (dInvalid) { ${insertCaps ? 'pD = pB;' : 'pD = 2.0 * pC - pB;'} } + isRound = isRound || isCap; - float width = isMirrored ? ${meta.width.generate('C')} : ${meta.width.generate('B')}; + // TODO: swap inputs rather than computing both and discarding one + float width = mirror ? ${meta.width.generate('C')} : ${meta.width.generate('B')}; // Tangent and normal vectors vec2 tBC = pC.xy - pB.xy; @@ -139,101 +145,119 @@ void main() { if (lCD > 0.0) tCD /= lCD; vec2 nCD = vec2(-tCD.y, tCD.x); + // Clamp for safety, since we take the arccos float cosB = clamp(dot(tAB, tBC), -1.0, 1.0); - // This section is very fragile. When lines are collinear, signs flip randomly and break orientation + // This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation // of the middle segment. The fix appears straightforward, but this took a few hours to get right. const float tol = 1e-4; + float mirrorSign = mirror ? -1.0 : 1.0; float dirB = -dot(tBC, nAB); float dirC = dot(tBC, nCD); bool bCollinear = abs(dirB) < tol; bool cCollinear = abs(dirC) < tol; bool bIsHairpin = bCollinear && cosB < 0.0; - bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0; + // bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0; dirB = bCollinear ? -mirrorSign : sign(dirB); dirC = cCollinear ? -mirrorSign : sign(dirC); vec2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB; + // Compute our primary "join index", that is, the index starting at the very first point of the join. // The second half of the triangle strip instance is just the first, reversed, and with vertices swapped! - float i = index <= mirrorIndex ? index : totalVertexCount - index; + float i = mirror ? N - index : index; - // Chop off the join to get at the segment part index - float iSeg = i - 2.0 * (isMirrored ? vertexCount.y : vertexCount.x); + // Decide the resolution of whichever feature we're drawing. n is twice the number of points used since + // that's the only form in which we use this number. + float res = (isCap ? capJoinRes2.x : capJoinRes2.y); - // After the first half-join, repeat two vertices of the segment strip in order to get the orientation correct - // for the next join. These are wasted vertices, but they enable using a triangle strip. for two joins which - // might be oriented differently. - if (iSeg > 1.0 && iSeg <= 3.0) { - iSeg -= 2.0; - if (dirB * dirC >= 0.0) iSeg += iSeg == 0.0 ? 1.0 : -1.0; - } + // Shift the index to send unused vertices to an index below zero, which will then just get clamped to + // zero and result in repeated points, i.e. degenerate triangles. + i -= max(0.0, (mirror ? vertCnt2.y : vertCnt2.x) - res); + + // Use the direction to offset the index by one. This has the effect of flipping the winding number so + // that it's always consistent no matter which direction the join turns. + i += (dirB < 0.0 ? -1.0 : 0.0); + + // Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly + // in the middle, where the mirrored and unmirrored halves meet. + i -= mirror ? 1.0 : 0.0; + // Clamp to zero and repeat unused excess vertices. + i = max(0.0, i); + + // Start with a default basis pointing along the segment with normal vector outward vec2 xBasis = tBC; vec2 yBasis = nBC * dirB; - vec2 xy = vec2(0, 1); + + // Default point is 0 along the segment, 1 (width unit) normal to it + vec2 xy = vec2(0); lineCoord.y = dirB * mirrorSign; - if (iSeg < 0.0) { + if (i == res + 1.0) { + // pick off this one specific index to be the interior miter point + // If not div-by-zero, then sinB / (1 + cosB) + float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0; + xy = vec2(min(abs(m), min(lBC, lAB) / width), -1); + lineCoord.y = -lineCoord.y; + } else { // Draw half of a join float m2 = dot(miter, miter); - float lm = length(miter); - float tBCm = dot(tBC, miter); + float lm = sqrt(m2); yBasis = miter / lm; + xBasis = dirB * vec2(yBasis.y, -yBasis.x); bool isBevel = 1.0 > miterLimit * m2; if (mod(i, 2.0) == 0.0) { // Outer joint points - if (isRound || isCap) { + if (isRound || i != 0.0) { // Round joins - xBasis = dirB * vec2(yBasis.y, -yBasis.x); - float cnt = (isCap ? capJoinRes.x : capJoinRes.y) * 2.0; - float theta = -0.5 * (acos(cosB) * (min(i, cnt) / cnt) - pi) * (isCap ? 2.0 : 1.0); + float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0); xy = vec2(cos(theta), sin(theta)); if (isCap) { + // A special multiplier factor for turning 3-point rounds into square caps (but leave the + // y == 0.0 point unaffected) if (xy.y > 0.001) xy *= capScale; lineCoord.xy = xy.yx * lineCoord.y; } } else { // Miter joins yBasis = bIsHairpin ? vec2(0) : miter; - if (!isBevel) xy.y /= m2; + xy.y = isBevel ? 1.0 : 1.0 / m2; } } else { - // Repeat vertex B to create a triangle fan + // Center of the fan lineCoord.y = 0.0; - xy = vec2(0); // Offset the center vertex position to get bevel SDF correct - if (!isRound && isBevel && !isCap) { + if (isBevel && !isRound) { xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5); } } - //} else if (iSeg == 0.0) { // No op: vertex B + line B-C normal - } else if (iSeg > 0.0) { - // vertex B + inner miter - lineCoord.y = -lineCoord.y; - - float miterExt = 0.0; - if (cosB > -0.9999) { - float sinB = tAB.x * tBC.y - tAB.y * tBC.x; - miterExt = sinB / (1.0 + cosB); - } - float m = abs(miterExt); - m = min(m, min(lBC, lAB) / width); - xy = vec2(m, -1); } ${isEndpoints ? `float orientation = ${meta.orientation ? meta.orientation.generate('') : 'mod(orientation,2.0)'};` : ''}; + + // Since we can't know the orientation of end caps without being told. This comes either from + // input via the orientation property or from a uniform, assuming caps are interleaved (start, + // end, start, end, etc.) and rendered in two passes: first starts, then ends. ${isEndpoints ? `if (orientation == CAP_END) lineCoord.xy = -lineCoord.xy;` : ''} + // Point offset from main vertex position vec2 dP = mat2(xBasis, yBasis) * xy; - float dC = dot(dP, tBC) * mirrorSign; - float useC = (isMirrored ? 1.0 : 0.0) + dC * (width / lBC); + // Dot with the tangent to account for dashes. Note that by putting this in *one place*, dashes + // should always be correct without having to compute a unique correction for every point. + float dx = dot(dP, tBC) * mirrorSign; + + // Interpolant: zero for using B, 1 for using C + float useC = (mirror ? 1.0 : 0.0) + dx * (width / lBC); + lineCoord.z = useC < 0.0 || useC > 1.0 ? 1.0 : 0.0; + + // The varying generation code handles clamping, if needed ${[...meta.varyings.values()].map(varying => varying.generate('useC', 'B', 'C')).join('\n')} gl_Position = pB; @@ -246,17 +270,17 @@ void main() { ...spec.attrs }, uniforms: { - vertexCount: (ctx, props) => computeCount(props), - capJoinRes: (ctx, props) => [props.capResolution, props.joinResolution], + vertCnt2: (ctx, props) => computeCount(props), + capJoinRes2: (ctx, props) => [props.capRes2, props.joinRes2], miterLimit: (ctx, props) => props.miterLimit * props.miterLimit, orientation: regl.prop('orientation'), capScale: regl.prop('capScale') }, - primitive: 'triangle strip', + primitive: (ctx, props) => props.primitive || 'triangle strip', instances: isEndpoints ? (ctx, props) => props.splitCaps ? props.orientation === ORIENTATION$2.CAP_START ? Math.ceil(props.count / 2) : Math.floor(props.count / 2) : props.count : (ctx, props) => props.count - 3, count: (ctx, props) => { const count = computeCount(props); - return 6 + 2 * (count[0] + count[1]); + return 6 + (count[0] + count[1]); } }); } @@ -717,24 +741,29 @@ void main() { for (const lineProps of props) { const joinType = sanitizeJoinType(lineProps.join); const capType = sanitizeCapType(lineProps.cap); - let capResolution = lineProps.capResolution === undefined ? 12 : lineProps.capResolution; + let capRes2 = lineProps.capResolution === undefined ? 12 : lineProps.capResolution; if (capType === 'square') { - capResolution = 3; + capRes2 = 3; } else if (capType === 'none') { - capResolution = 1; + capRes2 = 1; } - let joinResolution = 1; - if (joinType === 'round') joinResolution = lineProps.joinResolution === undefined ? 8 : lineProps.joinResolution; + let joinRes2 = 1; + if (joinType === 'round') joinRes2 = lineProps.joinResolution === undefined ? 8 : lineProps.joinResolution; // We only ever use these in doubled-up form + + capRes2 *= 2; + joinRes2 *= 2; const miterLimit = joinType === 'bevel' ? 1 : lineProps.miterLimit === undefined ? 4 : lineProps.miterLimit; const capScale = capType === 'square' ? SQUARE_CAP_SCALE : ROUND_CAP_SCALE; + const primitive = lineProps.primitive; const sharedProps = { - joinResolution, - capResolution, + joinRes2, + capRes2, capScale, capType, - miterLimit + miterLimit, + primitive }; if (lineProps.endpointAttributes && lineProps.endpointCount) { diff --git a/dist/regl-gpu-lines.min.js b/dist/regl-gpu-lines.min.js index 55146dd..0b59199 100644 --- a/dist/regl-gpu-lines.min.js +++ b/dist/regl-gpu-lines.min.js @@ -1 +1 @@ -!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(n="undefined"!=typeof globalThis?globalThis:n||self).reglLines=e()}(this,(function(){"use strict";var n={CAP_START:0,CAP_END:1,CAP_SHORT:2};const e=n;var t=function(n,t,{regl:i,meta:o,frag:r,segmentSpec:s,endpointSpec:a,indexAttributes:l,insertCaps:f,debug:p}){const d=t?a:s,c=["B","C","D"];t||c.unshift("A");function u(n){return f?t?[n.capResolution,Math.max(n.capResolution,n.joinResolution)]:[Math.max(n.capResolution,n.joinResolution),Math.max(n.capResolution,n.joinResolution)]:t?[n.capResolution,n.joinResolution]:[n.joinResolution,n.joinResolution]}return i({vert:`${o.glsl}const float CAP_START=${e.CAP_START}.0;const float CAP_END=${e.CAP_END}.0;${d.glsl}attribute float index;${p?"attribute float debugInstanceID;":""}uniform vec2 vertexCount,capJoinRes;uniform vec2 resolution,capScale;uniform float miterLimit;${o.orientation||!t?"":"uniform float orientation;"}varying vec3 lineCoord;varying float dir;${p?"varying vec2 triStripCoord;":""}${p?"varying float instanceID;":""}bool invalid(vec4 p){return p.w==0.0;}const bool isRound=${n?"true":"false"};const float pi=3.141592653589793;void main(){lineCoord=vec3(0);${p?`instanceID=${t?"-1.0":"debugInstanceID"};`:""}${p?"triStripCoord=vec2(floor(index/2.0),mod(index,2.0));":""}${c.map((n=>`vec4 p${n}=${o.position.generate(n)};`)).join("\n")}bool aInvalid=${t?"false":"invalid(pA)"};bool bInvalid=invalid(pB);bool cInvalid=invalid(pC);bool dInvalid=invalid(pD);float mirrorIndex=2.0*vertexCount.x+3.0;float totalVertexCount=mirrorIndex+2.0+2.0*vertexCount.y;bool isMirrored=index>mirrorIndex;${t?"if(dInvalid && isMirrored){gl_Position=pB;return;}":""}float pw=isMirrored?pC.w:pB.w;${c.map((n=>`p${n}=vec4(vec3(p${n}.xy*resolution,p${n}.z)/p${n}.w,1);`)).join("\n")}${t?"vec4 pA=pC;":""}if(bInvalid||cInvalid||max(abs(pB.z),abs(pC.z))>1.0){gl_Position=pB;return;}float mirrorSign=isMirrored?-1.0:1.0;if(isMirrored){vec4 vTmp=pC; pC=pB; pB=vTmp;vTmp=pD; pD=pA; pA=vTmp;bool bTmp=dInvalid; dInvalid=aInvalid; aInvalid=bTmp;}${t?"bool isCap=!isMirrored;":"bool isCap=false;"};if(aInvalid){ ${f?"pA=pC; isCap=true;":"pA=2.0*pB-pC;"} }if(dInvalid){ ${f?"pD=pB;":"pD=2.0*pC-pB;"} }float width=isMirrored?${o.width.generate("C")}:${o.width.generate("B")};vec2 tBC=pC.xy-pB.xy;float lBC=length(tBC);tBC/=lBC;vec2 nBC=vec2(-tBC.y,tBC.x);vec2 tAB=pB.xy-pA.xy;float lAB=length(tAB);if(lAB>0.0)tAB/=lAB;vec2 nAB=vec2(-tAB.y,tAB.x);vec2 tCD=pD.xy-pC.xy;float lCD=length(tCD);if(lCD>0.0)tCD/=lCD;vec2 nCD=vec2(-tCD.y,tCD.x);float cosB=clamp(dot(tAB,tBC),-1.0,1.0);const float tol=1e-4;float dirB=-dot(tBC,nAB);float dirC=dot(tBC,nCD);bool bCollinear=abs(dirB)1.0 && iSeg<=3.0){iSeg-=2.0;if(dirB*dirC>=0.0)iSeg+=iSeg==0.0?1.0:-1.0;}vec2 xBasis=tBC;vec2 yBasis=nBC*dirB;vec2 xy=vec2(0,1);lineCoord.y=dirB*mirrorSign;if(iSeg<0.0){float m2=dot(miter,miter);float lm=length(miter);float tBCm=dot(tBC,miter);yBasis=miter/lm;bool isBevel=1.0>miterLimit*m2;if(mod(i,2.0)==0.0){if(isRound||isCap){xBasis=dirB*vec2(yBasis.y,-yBasis.x);float cnt=(isCap?capJoinRes.x:capJoinRes.y)*2.0;float theta=-0.5*(acos(cosB)*(min(i,cnt)/cnt)-pi)*(isCap?2.0:1.0);xy=vec2(cos(theta),sin(theta));if(isCap){if(xy.y>0.001)xy*=capScale;lineCoord.xy=xy.yx*lineCoord.y;}} else {yBasis=bIsHairpin?vec2(0):miter;if(!isBevel)xy.y/=m2;}} else {lineCoord.y=0.0;xy=vec2(0);if(!isRound && isBevel && !isCap){xy.y=-1.0+sqrt((1.0+cosB)*0.5);}}} else if(iSeg>0.0){lineCoord.y=-lineCoord.y;float miterExt=0.0;if(cosB>-0.9999){float sinB=tAB.x*tBC.y-tAB.y*tBC.x;miterExt=sinB/(1.0+cosB);}float m=abs(miterExt);m=min(m,min(lBC,lAB)/width);xy=vec2(m,-1);}${t?`float orientation=${o.orientation?o.orientation.generate(""):"mod(orientation,2.0)"};`:""};${t?"if(orientation==CAP_END)lineCoord.xy=-lineCoord.xy;":""}vec2 dP=mat2(xBasis,yBasis)*xy;float dC=dot(dP,tBC)*mirrorSign;float useC=(isMirrored?1.0:0.0)+dC*(width/lBC);lineCoord.z=useC<0.0||useC>1.0?1.0:0.0;${[...o.varyings.values()].map((n=>n.generate("useC","B","C"))).join("\n")}gl_Position=pB;gl_Position.xy+=width*dP;gl_Position.xy/=resolution;gl_Position*=pw;}`,frag:r,attributes:{...l,...d.attrs},uniforms:{vertexCount:(n,e)=>u(e),capJoinRes:(n,e)=>[e.capResolution,e.joinResolution],miterLimit:(n,e)=>e.miterLimit*e.miterLimit,orientation:i.prop("orientation"),capScale:i.prop("capScale")},primitive:"triangle strip",instances:t?(n,t)=>t.splitCaps?t.orientation===e.CAP_START?Math.ceil(t.count/2):Math.floor(t.count/2):t.count:(n,e)=>e.count-3,count:(n,e)=>{const t=u(e);return 6+2*(t[0]+t[1])}})};var i={NONE:0,REGULAR:1,EXTENDED:2,PER_INSTANCE:4};const o=i;var r=function(n){const e=[],t=n.split("\n");for(let n=0;nn.trim())).filter((n=>!!n)),r=(n,e)=>`${i}(${o.map((t=>(e||"")+t+n)).join(",")})`;return{type:"property",property:n,returnType:t,name:i,inputs:o,generate:r}}if(e=n.match(f)){const n="extrapolate"===e[1],t=e[2],i=e[3],o=e[4],r=e[5].split(",").map((n=>n.trim())).filter((n=>!!n)),s=(e,t,s)=>{const a=n?e:`clamp(${e},0.0,1.0)`;return`${i}=${o}(${r.map((n=>`mix(${n+t},${n+s},${a})`)).join(",")});`};return{type:"varying",returnType:t,name:i,getter:o,inputs:r,generate:s}}throw new Error(`Unrecognized lines pragma:"${n}"`)}function c(n){const e=new Map,t=new Map;for(const i of n)"attribute"===i.type?(e.set(i.name,i),i.vertexUsage=o.NONE,i.endpointUsage=o.NONE):"varying"===i.type&&t.set(i.name,i);let i,r,s;for(const t of n)if("property"===t.type){switch(t.property){case"width":if(i)throw new Error(`Unexpected duplicate pragma for property "${t.property}"`);i=t;break;case"position":if(r)throw new Error(`Unexpected duplicate pragma for property "${t.property}"`);r=t;break;case"orientation":if(s)throw new Error(`Unexpected duplicate pragma for property "${t.property}"`);s=t;break;default:throw new Error(`Invalid pragma property "${t.property}"`)}for(const n of t.inputs)if(!e.has(n))throw new Error(`Missing attribute ${n} of property ${t.property}`)}for(const t of n)if(t.inputs)for(const n of t.inputs){const i=e.get(n);"property"!==t.type&&"varying"!==t.type||("position"===t.property?(i.vertexUsage|=o.EXTENDED,i.endpointUsage|=o.EXTENDED):"orientation"===t.property?i.endpointUsage|=o.PER_INSTANCE:(i.endpointUsage|=o.REGULAR,i.vertexUsage|=o.REGULAR))}return{varyings:t,attrs:e,width:i,position:r,orientation:s}}const u=[];u[5120]=1,u[5122]=2,u[5124]=4,u[5121]=1,u[5123]=2,u[5125]=4,u[5126]=4;var v=function(n,e,t){const i={};if(!e)return i;for(let[o,r]of n.attrs){const n=e[o];if(!(t?r.endpointUsage:r.vertexUsage))continue;const s={buffer:null,dimension:r.dimension,offset:0,type:NaN,stride:NaN,divisor:1,bytesPerElement:NaN};if(!n)throw new Error(`Missing buffer for ${t?"endpoint":"vertex"} attribute '${o}'`);if("buffer"===n._reglType)s.buffer=n,s.type=s.buffer._buffer.dtype;else{if("buffer"!==n.buffer._reglType)throw new Error(`Invalid buffer for attribute '${o}'`);if(s.buffer=n.buffer,g(n,"dimension")&&n.dimension!==s.dimension)throw new Error(`Size of attribute(${n.dimension})does not match dimension specified in shader pragma(${r.dimension})`);g(n,"offset")&&(s.offset=n.offset),g(n,"type")?s.type=C[n.type]:s.type=s.buffer._buffer.dtype,g(n,"divisor")&&(s.divisor=n.divisor),g(n,"stride")&&(s.stride=n.stride)}s.bytesPerElement=m[s.type],Number.isNaN(s.stride)&&(s.stride=s.bytesPerElement*r.dimension),i[o]=s}return i};const m=u,C={int8:5120,int16:5122,int32:5124,uint8:5121,uint16:5123,uint32:5125,float:5126,float32:5126};function g(n,e){return Object.prototype.hasOwnProperty.call(n,e)}const y=[];y[1]="float",y[2]="vec2",y[3]="vec3",y[4]="vec4";var h=function(n,e,t){const i=t?["B","C","D"]:["A","B","C","D"],o=[],r={};return n.attrs.forEach(((n,s)=>{const a=t?n.endpointUsage:n.vertexUsage;if(!a)return;const l=[];function f(n,i){const o=s+i;if(l.push(o),t){const t=a&b.PER_INSTANCE?1:3;r[o]={buffer:e.prop(`buffers.${s}.buffer`),offset:(e,t)=>t.buffers[s].offset+t.buffers[s].stride*((t.orientation!==B.CAP_START&&t.splitCaps?3:0)+n),stride:(n,e)=>e.buffers[s].stride*t*(e.splitCaps?2:1),divisor:(n,e)=>e.buffers[s].divisor}}else r[o]={buffer:e.prop(`buffers.${s}.buffer`),offset:(e,t)=>t.buffers[s].offset+t.buffers[s].stride*n,stride:(n,e)=>e.buffers[s].stride,divisor:(n,e)=>e.buffers[s].divisor}}if(a&b.PER_INSTANCE&&f(0,""),a&b.REGULAR||a&b.EXTENDED)for(let n=0;n{o.push(`varying ${n.returnType} ${e};`)})),{glsl:o.join("\n"),attrs:r}};const b=i,x=y,B=n;const w=t,A=r,$=v,E=h,R=function(n,e,t){return function(i){if(!i)return t;if(-1===e.indexOf(i))throw new Error(`Invalid ${n} type. Valid options are:${e.join(",")}.`);return i}},D=n;var T=_;_.CAP_START=D.CAP_START,_.CAP_END=D.CAP_END;const S=new Set(["count","instances","attributes","elements"]),I=["round","bevel","miter"],j=["round","square","none"],N=[1,1],P=[2,2/Math.sqrt(3)];function _(n,e={}){const{vert:t=null,frag:i=null,debug:o=!1,insertCaps:r=!1}=e,s={...e};for(const n of["vert","frag","debug","insertCaps"])delete s[n];const a=Object.keys(s),l=0===a.length;if(a.forEach((n=>{if(S.has(n))throw new Error(`Invalid parameter '${n}'. Parameters ${[...S].map((n=>`'${n}'`)).join(",")} may not be forwarded to regl.`)})),!t)throw new Error("Missing vertex shader,`vert`");if(!i)throw new Error("Missing fragment shader,`frag`");const f=A(t),p=E(f,n,!1),d=E(f,n,!0),c=n({uniforms:{resolution:n=>[n.viewportWidth,n.viewportHeight]}}),u=l?(n,e)=>e():n(s),v={};o&&(v.debugInstanceID={buffer:n.buffer(new Uint16Array([...Array(16384).keys()])),divisor:1}),v.index={buffer:n.buffer(new Int8Array([...Array(184).keys()])),divisor:0};const m={regl:n,meta:f,segmentSpec:p,endpointSpec:d,frag:i,indexAttributes:v,debug:o,insertCaps:r},C=w(!1,!1,m),g=w(!0,!1,m),y=w(!1,!0,m),h=w(!0,!0,m),b=R("join",I,"miter"),x=R("cap",j,"square"),B=[],T=[],_=[],M=[];function U(n){u(n,(()=>{B.length&&g(B),_.length&&C(_),T.length&&h(T),M.length&&y(M),B.length=0,_.length=0,T.length=0,M.length=0}))}return function(n){if(!n)return;const e=Array.isArray(n);e||(n=[n]);const t=l&&!e;c((()=>{for(const e of n){const n=b(e.join),i=x(e.cap);let o=void 0===e.capResolution?12:e.capResolution;"square"===i?o=3:"none"===i&&(o=1);let r=1;"round"===n&&(r=void 0===e.joinResolution?8:e.joinResolution);const s="bevel"===n?1:void 0===e.miterLimit?4:e.miterLimit,a={joinResolution:r,capResolution:o,capScale:"square"===i?P:N,capType:i,miterLimit:s};if(e.endpointAttributes&&e.endpointCount){const t={buffers:$(f,e.endpointAttributes,!0),count:e.endpointCount,...a},i="round"===n?T:M;f.orientation?i.push({...t,splitCaps:!1}):i.push({...t,orientation:D.CAP_START,splitCaps:!0},{...t,orientation:D.CAP_END,splitCaps:!0})}if(e.vertexAttributes&&e.vertexCount){("round"===n?B:_).push({buffers:$(f,e.vertexAttributes,!1),count:e.vertexCount,...a})}t||U(e)}t&&U(n)}))}}return T})); \ No newline at end of file +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).reglLines=n()}(this,(function(){"use strict";var e={CAP_START:0,CAP_END:1,CAP_SHORT:2};const n=e;var t=function(e,t,{regl:i,meta:r,frag:o,segmentSpec:s,endpointSpec:a,indexAttributes:p,insertCaps:f,debug:l}){const d=t?a:s,c=["B","C","D"];t||c.unshift("A");const u=f?t?e=>[e.capRes2,Math.max(e.capRes2,e.joinRes2)]:e=>[Math.max(e.capRes2,e.joinRes2),Math.max(e.capRes2,e.joinRes2)]:t?e=>[e.capRes2,e.joinRes2]:e=>[e.joinRes2,e.joinRes2];return i({vert:`${r.glsl}const float CAP_START=${n.CAP_START}.0;const float CAP_END=${n.CAP_END}.0;${d.glsl}attribute float index;${l?"attribute float debugInstanceID;":""}uniform vec2 vertCnt2,capJoinRes2;uniform vec2 resolution,capScale;uniform float miterLimit;${r.orientation||!t?"":"uniform float orientation;"}varying vec3 lineCoord;varying float dir;${l?"varying vec2 triStripCoord;":""}${l?"varying float instanceID;":""}${l?"varying float vertexIndex;":""}bool invalid(vec4 p){return p.w==0.0;}void main(){const float pi=3.141592653589793;bool isRound=${e?"true":"false"};${l?"vertexIndex=index;":""}lineCoord=vec3(0);${l?`instanceID=${t?"-1.0":"debugInstanceID"};`:""}${l?"triStripCoord=vec2(floor(index/2.0),mod(index,2.0));":""}${c.map((e=>`vec4 p${e}=${r.position.generate(e)};`)).join("\n")}gl_Position=pB;bool aInvalid=${t?"false":"invalid(pA)"};bool bInvalid=invalid(pB);bool cInvalid=invalid(pC);bool dInvalid=invalid(pD);vec2 v=vertCnt2+3.0;float N=dot(v,vec2(1));bool mirror=index>=v.x;${t?"if(dInvalid && mirror)return;":""}float pw=mirror?pC.w:pB.w;${c.map((e=>`p${e}=vec4(vec3(p${e}.xy*resolution,p${e}.z)/p${e}.w,1);`)).join("\n")}${t?"vec4 pA=pC;":""}if(bInvalid||cInvalid||max(abs(pB.z),abs(pC.z))>1.0)return;if(mirror){vec4 vTmp=pC; pC=pB; pB=vTmp;vTmp=pD; pD=pA; pA=vTmp;bool bTmp=dInvalid; dInvalid=aInvalid; aInvalid=bTmp;}${t?"bool isCap=!mirror;":(f?"":"const ")+"bool isCap=false"};if(aInvalid){ ${f?"pA=pC; isCap=true;":"pA=2.0*pB-pC;"} }if(dInvalid){ ${f?"pD=pB;":"pD=2.0*pC-pB;"} }isRound=isRound||isCap;float width=mirror?${r.width.generate("C")}:${r.width.generate("B")};vec2 tBC=pC.xy-pB.xy;float lBC=length(tBC);tBC/=lBC;vec2 nBC=vec2(-tBC.y,tBC.x);vec2 tAB=pB.xy-pA.xy;float lAB=length(tAB);if(lAB>0.0)tAB/=lAB;vec2 nAB=vec2(-tAB.y,tAB.x);vec2 tCD=pD.xy-pC.xy;float lCD=length(tCD);if(lCD>0.0)tCD/=lCD;vec2 nCD=vec2(-tCD.y,tCD.x);float cosB=clamp(dot(tAB,tBC),-1.0,1.0);const float tol=1e-4;float mirrorSign=mirror?-1.0:1.0;float dirB=-dot(tBC,nAB);float dirC=dot(tBC,nCD);bool bCollinear=abs(dirB)-0.9999?(tAB.x*tBC.y-tAB.y*tBC.x)/(1.0+cosB):0.0;xy=vec2(min(abs(m),min(lBC,lAB)/width),-1);lineCoord.y=-lineCoord.y;} else {float m2=dot(miter,miter);float lm=sqrt(m2);yBasis=miter/lm;xBasis=dirB*vec2(yBasis.y,-yBasis.x);bool isBevel=1.0>miterLimit*m2;if(mod(i,2.0)==0.0){if(isRound||i !=0.0){float theta=-0.5*(acos(cosB)*(clamp(i,0.0,res)/res)-pi)*(isCap?2.0:1.0);xy=vec2(cos(theta),sin(theta));if(isCap){if(xy.y>0.001)xy*=capScale;lineCoord.xy=xy.yx*lineCoord.y;}} else {yBasis=bIsHairpin?vec2(0):miter;xy.y=isBevel?1.0:1.0/m2;}} else {lineCoord.y=0.0;if(isBevel && !isRound){xy.y=-1.0+sqrt((1.0+cosB)*0.5);}}}${t?`float orientation=${r.orientation?r.orientation.generate(""):"mod(orientation,2.0)"};`:""};${t?"if(orientation==CAP_END)lineCoord.xy=-lineCoord.xy;":""}vec2 dP=mat2(xBasis,yBasis)*xy;float dx=dot(dP,tBC)*mirrorSign;float useC=(mirror?1.0:0.0)+dx*(width/lBC);lineCoord.z=useC<0.0||useC>1.0?1.0:0.0;${[...r.varyings.values()].map((e=>e.generate("useC","B","C"))).join("\n")}gl_Position=pB;gl_Position.xy+=width*dP;gl_Position.xy/=resolution;gl_Position*=pw;}`,frag:o,attributes:{...p,...d.attrs},uniforms:{vertCnt2:(e,n)=>u(n),capJoinRes2:(e,n)=>[n.capRes2,n.joinRes2],miterLimit:(e,n)=>n.miterLimit*n.miterLimit,orientation:i.prop("orientation"),capScale:i.prop("capScale")},primitive:(e,n)=>n.primitive||"triangle strip",instances:t?(e,t)=>t.splitCaps?t.orientation===n.CAP_START?Math.ceil(t.count/2):Math.floor(t.count/2):t.count:(e,n)=>n.count-3,count:(e,n)=>{const t=u(n);return 6+(t[0]+t[1])}})};var i={NONE:0,REGULAR:1,EXTENDED:2,PER_INSTANCE:4};const r=i;var o=function(e){const n=[],t=e.split("\n");for(let e=0;ee.trim())).filter((e=>!!e)),o=(e,n)=>`${i}(${r.map((t=>(n||"")+t+e)).join(",")})`;return{type:"property",property:e,returnType:t,name:i,inputs:r,generate:o}}if(n=e.match(f)){const e="extrapolate"===n[1],t=n[2],i=n[3],r=n[4],o=n[5].split(",").map((e=>e.trim())).filter((e=>!!e)),s=(n,t,s)=>{const a=e?n:`clamp(${n},0.0,1.0)`;return`${i}=${r}(${o.map((e=>`mix(${e+t},${e+s},${a})`)).join(",")});`};return{type:"varying",returnType:t,name:i,getter:r,inputs:o,generate:s}}throw new Error(`Unrecognized lines pragma:"${e}"`)}function c(e){const n=new Map,t=new Map;for(const i of e)"attribute"===i.type?(n.set(i.name,i),i.vertexUsage=r.NONE,i.endpointUsage=r.NONE):"varying"===i.type&&t.set(i.name,i);let i,o,s;for(const t of e)if("property"===t.type){switch(t.property){case"width":if(i)throw new Error(`Unexpected duplicate pragma for property "${t.property}"`);i=t;break;case"position":if(o)throw new Error(`Unexpected duplicate pragma for property "${t.property}"`);o=t;break;case"orientation":if(s)throw new Error(`Unexpected duplicate pragma for property "${t.property}"`);s=t;break;default:throw new Error(`Invalid pragma property "${t.property}"`)}for(const e of t.inputs)if(!n.has(e))throw new Error(`Missing attribute ${e} of property ${t.property}`)}for(const t of e)if(t.inputs)for(const e of t.inputs){const i=n.get(e);"property"!==t.type&&"varying"!==t.type||("position"===t.property?(i.vertexUsage|=r.EXTENDED,i.endpointUsage|=r.EXTENDED):"orientation"===t.property?i.endpointUsage|=r.PER_INSTANCE:(i.endpointUsage|=r.REGULAR,i.vertexUsage|=r.REGULAR))}return{varyings:t,attrs:n,width:i,position:o,orientation:s}}const u=[];u[5120]=1,u[5122]=2,u[5124]=4,u[5121]=1,u[5123]=2,u[5125]=4,u[5126]=4;var m=function(e,n,t){const i={};if(!n)return i;for(let[r,o]of e.attrs){const e=n[r];if(!(t?o.endpointUsage:o.vertexUsage))continue;const s={buffer:null,dimension:o.dimension,offset:0,type:NaN,stride:NaN,divisor:1,bytesPerElement:NaN};if(!e)throw new Error(`Missing buffer for ${t?"endpoint":"vertex"} attribute '${r}'`);if("buffer"===e._reglType)s.buffer=e,s.type=s.buffer._buffer.dtype;else{if("buffer"!==e.buffer._reglType)throw new Error(`Invalid buffer for attribute '${r}'`);if(s.buffer=e.buffer,g(e,"dimension")&&e.dimension!==s.dimension)throw new Error(`Size of attribute(${e.dimension})does not match dimension specified in shader pragma(${o.dimension})`);g(e,"offset")&&(s.offset=e.offset),g(e,"type")?s.type=v[e.type]:s.type=s.buffer._buffer.dtype,g(e,"divisor")&&(s.divisor=e.divisor),g(e,"stride")&&(s.stride=e.stride)}s.bytesPerElement=h[s.type],Number.isNaN(s.stride)&&(s.stride=s.bytesPerElement*o.dimension),i[r]=s}return i};const h=u,v={int8:5120,int16:5122,int32:5124,uint8:5121,uint16:5123,uint32:5125,float:5126,float32:5126};function g(e,n){return Object.prototype.hasOwnProperty.call(e,n)}const y=[];y[1]="float",y[2]="vec2",y[3]="vec3",y[4]="vec4";var C=function(e,n,t){const i=t?["B","C","D"]:["A","B","C","D"],r=[],o={};return e.attrs.forEach(((e,s)=>{const a=t?e.endpointUsage:e.vertexUsage;if(!a)return;const p=[];function f(e,i){const r=s+i;if(p.push(r),t){const t=a&b.PER_INSTANCE?1:3;o[r]={buffer:n.prop(`buffers.${s}.buffer`),offset:(n,t)=>t.buffers[s].offset+t.buffers[s].stride*((t.orientation!==x.CAP_START&&t.splitCaps?3:0)+e),stride:(e,n)=>n.buffers[s].stride*t*(n.splitCaps?2:1),divisor:(e,n)=>n.buffers[s].divisor}}else o[r]={buffer:n.prop(`buffers.${s}.buffer`),offset:(n,t)=>t.buffers[s].offset+t.buffers[s].stride*e,stride:(e,n)=>n.buffers[s].stride,divisor:(e,n)=>n.buffers[s].divisor}}if(a&b.PER_INSTANCE&&f(0,""),a&b.REGULAR||a&b.EXTENDED)for(let e=0;e{r.push(`varying ${e.returnType} ${n};`)})),{glsl:r.join("\n"),attrs:o}};const b=i,w=y,x=e;const B=t,A=o,$=m,E=C,R=function(e,n,t){return function(i){if(!i)return t;if(-1===n.indexOf(i))throw new Error(`Invalid ${e} type. Valid options are:${n.join(",")}.`);return i}},D=e;var T=_;_.CAP_START=D.CAP_START,_.CAP_END=D.CAP_END;const j=new Set(["count","instances","attributes","elements"]),I=["round","bevel","miter"],N=["round","square","none"],S=[1,1],P=[2,2/Math.sqrt(3)];function _(e,n={}){const{vert:t=null,frag:i=null,debug:r=!1,insertCaps:o=!1}=n,s={...n};for(const e of["vert","frag","debug","insertCaps"])delete s[e];const a=Object.keys(s),p=0===a.length;if(a.forEach((e=>{if(j.has(e))throw new Error(`Invalid parameter '${e}'. Parameters ${[...j].map((e=>`'${e}'`)).join(",")} may not be forwarded to regl.`)})),!t)throw new Error("Missing vertex shader,`vert`");if(!i)throw new Error("Missing fragment shader,`frag`");const f=A(t),l=E(f,e,!1),d=E(f,e,!0),c=e({uniforms:{resolution:e=>[e.viewportWidth,e.viewportHeight]}}),u=p?(e,n)=>n():e(s),m={};r&&(m.debugInstanceID={buffer:e.buffer(new Uint16Array([...Array(16384).keys()])),divisor:1}),m.index={buffer:e.buffer(new Int8Array([...Array(184).keys()])),divisor:0};const h={regl:e,meta:f,segmentSpec:l,endpointSpec:d,frag:i,indexAttributes:m,debug:r,insertCaps:o},v=B(!1,!1,h),g=B(!0,!1,h),y=B(!1,!0,h),C=B(!0,!0,h),b=R("join",I,"miter"),w=R("cap",N,"square"),x=[],T=[],_=[],U=[];function k(e){u(e,(()=>{x.length&&g(x),_.length&&v(_),T.length&&C(T),U.length&&y(U),x.length=0,_.length=0,T.length=0,U.length=0}))}return function(e){if(!e)return;const n=Array.isArray(e);n||(e=[e]);const t=p&&!n;c((()=>{for(const n of e){const e=b(n.join),i=w(n.cap);let r=void 0===n.capResolution?12:n.capResolution;"square"===i?r=3:"none"===i&&(r=1);let o=1;"round"===e&&(o=void 0===n.joinResolution?8:n.joinResolution),r*=2,o*=2;const s="bevel"===e?1:void 0===n.miterLimit?4:n.miterLimit,a={joinRes2:o,capRes2:r,capScale:"square"===i?P:S,capType:i,miterLimit:s,primitive:n.primitive};if(n.endpointAttributes&&n.endpointCount){const t={buffers:$(f,n.endpointAttributes,!0),count:n.endpointCount,...a},i="round"===e?T:U;f.orientation?i.push({...t,splitCaps:!1}):i.push({...t,orientation:D.CAP_START,splitCaps:!0},{...t,orientation:D.CAP_END,splitCaps:!0})}if(n.vertexAttributes&&n.vertexCount){("round"===e?x:_).push({buffers:$(f,n.vertexAttributes,!1),count:n.vertexCount,...a})}t||k(n)}t&&k(e)}))}}return T})); \ No newline at end of file diff --git a/package.json b/package.json index 7cf8937..41f7114 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "regl-gpu-lines", - "version": "0.0.22", + "version": "0.0.23", "description": "Pure GPU instanced, screen-projected lines for regl", "main": "dist/regl-gpu-lines.min.js", "repository": {