Skip to content

Commit

Permalink
v0.0.21
Browse files Browse the repository at this point in the history
  • Loading branch information
rreusser committed Nov 17, 2021
1 parent 1d4f532 commit 954476a
Show file tree
Hide file tree
Showing 38 changed files with 571 additions and 119 deletions.
2 changes: 1 addition & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Instantiate a drawing command using the specified shaders.
- `vert` (string): vertex shader, using pragma specification defined below
- `frag` (string): fragment shader
- `debug`: Debug mode, which exposes additional properties for viewing triangle mesh
- `insertCaps` (boolean, default: `false`) (*experimental*): Automatically insert a cap wherever a break is encountered, signaled by a position with `w = 0`. Only implemented for round joins. Allows drawing lines and caps with a single draw call, although caps may be lower resolution since they are constructed from the potentially-lower number of join vertices.
- `insertCaps` (boolean, default: `false`) Automatically insert a cap wherever a break is encountered, signaled by a position with `w = 0`. Allows drawing lines and caps with a single draw call. *Use this option with care though*. If using miter joins and round caps with a high cap resolution, then *every segment instance* will have enough points to draw two caps, even if they never actually result in valid triangles.

Additional configuration parameters are forwarded to a `regl` command which wraps drawing.

Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
## 0.0.21

### Features

- Clean up one of the worst parts of the shader code and get end cap insertion working with all combinations of joins and caps. :tada:

## 0.0.20

### Features

- Add optional `extrapolate` keyword, as in `#pragma lines: extrapolate varying float name` to distinguish between varyings which are extrapolated outside the bounds of their respective segment endpoint values, and varyings which are clamped to the range of the segment. This can be used to dash caps and joins or to ensure colors are not extrapolated.

## 0.0.19

### Features

- Add `insertCaps` option to be explicit about when caps are automatically inserted

### Bugfixes

- Switch to preferring `w = 0` instead of `NaN` since `NaN` detection is a bit unreliable in GLSL.

## 0.0.18
Expand Down
32 changes: 20 additions & 12 deletions dist/regl-gpu-lines.compat.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/regl-gpu-lines.compat.min.js

Large diffs are not rendered by default.

51 changes: 33 additions & 18 deletions dist/regl-gpu-lines.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@
const spec = isEndpoints ? endpointSpec : segmentSpec;
const 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];
}

return regl({
vert: `${meta.glsl}
const float CAP_START = ${ORIENTATION$2.CAP_START}.0;
Expand All @@ -39,7 +48,7 @@ ${spec.glsl}
attribute float index;
${debug ? 'attribute float debugInstanceID;' : ''}
uniform vec3 joinRes;
uniform vec2 vertexCount, capJoinRes;
uniform vec2 resolution, capScale;
uniform float miterLimit;
${meta.orientation || !isEndpoints ? '' : 'uniform float orientation;'}
Expand Down Expand Up @@ -69,14 +78,21 @@ void main() {
${verts.map(vert => `vec4 p${vert} = ${meta.position.generate(vert)};`).join('\n')}
// Check for invalid vertices
if (invalid(pB) || invalid(pC)${insertCaps ? '' : `${isEndpoints ? '' : '|| invalid(pA)'} || invalid(pD)`}) {
if (invalid(pB) || invalid(pC)) {
gl_Position = vec4(1,1,1,0);
return;
}
float mirrorIndex = 2.0 * vertexCount.x + 3.0;
float totalVertexCount = mirrorIndex + 2.0 + 2.0 * vertexCount.y;
// 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 > joinRes.x * 2.0 + 3.0;
bool isMirrored = index > mirrorIndex;
// When rendering dedicated endoints, this allows us to insert an end cap *alone* (without the attached
// segment and join)
${isEndpoints ? 'if (invalid(pD) && isMirrored) { gl_Position = vec4(0); return; }' : ''}
// Convert to screen-pixel coordinates
// Save w so we can perspective re-multiply at the end to get varyings depth-correct
Expand All @@ -86,8 +102,6 @@ void main() {
// If it's a cap, mirror A back onto C to accomplish a round
${isEndpoints ? `vec4 pA = pC;` : ''}
vec2 res = isMirrored ? joinRes.yx : joinRes.xy;
float mirrorSign = isMirrored ? -1.0 : 1.0;
if (isMirrored) {
vec4 tmp;
Expand All @@ -97,14 +111,14 @@ void main() {
${isEndpoints ? `bool isCap = !isMirrored;` : `bool isCap = false;`};
if (invalid(pA)) { pA = pC; isCap = true; }
if (invalid(pD)) { pD = pB; }
if (invalid(pA)) { ${insertCaps ? 'pA = pC; isCap = true;' : 'pA = 2.0 * pB - pC;'} }
if (invalid(pD)) { ${insertCaps ? 'pD = pB;' : 'pD = 2.0 * pC - pB;'} }
float width = isMirrored ? ${meta.width.generate('C')} : ${meta.width.generate('B')};
// Invalidate triangles too far in front of or behind the camera plane
if (max(abs(pB.z), abs(pC.z)) > 1.0) {
gl_Position = vec4(1,1,1,0);
gl_Position = vec4(0);
return;
}
Expand Down Expand Up @@ -141,10 +155,10 @@ void main() {
vec2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;
// The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!
float i = index < 2.0 * joinRes.x + 4.0 ? index : 2.0 * (res.x + res.y) + 5.0 - index;
float i = index <= mirrorIndex ? index : totalVertexCount - index;
// Chop off the join to get at the segment part index
float iSeg = i - 2.0 * res.x;
float iSeg = i - 2.0 * (isMirrored ? vertexCount.y : vertexCount.x);
// 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
Expand Down Expand Up @@ -173,12 +187,12 @@ void main() {
if (isRound || isCap) {
// Round joins
xBasis = dirB * vec2(yBasis.y, -yBasis.x);
float divisor = ${isEndpoints ? 'res.x' : 'min(res.x, isCap ? joinRes.z : res.x)'} * 2.0;
float theta = -0.5 * (acos(cosB) * (min(i, divisor) / divisor) - pi) * (isCap ? 2.0 : 1.0);
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.5) xy *= capScale;
if (xy.y > 0.001) xy *= capScale;
lineCoord.xy = xy.yx * lineCoord.y;
}
} else {
Expand Down Expand Up @@ -231,17 +245,18 @@ void main() {
...spec.attrs
},
uniforms: {
joinRes: (ctx, props) => [// First half-join is actually a cap if we're drawing endpoints
isEndpoints ? props.capResolution : props.joinResolution, // Second half-join is always a join
props.joinResolution, // The resolution of inserted caps
props.capType === 'square' ? props.capResolution : props.capType === 'none' ? 0 : props.joinResolution],
vertexCount: (ctx, props) => computeCount(props),
capJoinRes: (ctx, props) => [props.capResolution, props.joinResolution],
miterLimit: (ctx, props) => props.miterLimit * props.miterLimit,
orientation: regl.prop('orientation'),
capScale: regl.prop('capScale')
},
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: isRound ? (ctx, props) => 6 + 2 * (props.joinResolution + (isEndpoints ? props.capResolution : props.joinResolution)) : (ctx, props) => 6 + 2 * (1 + (isEndpoints ? props.capResolution : 1)) + 10
count: (ctx, props) => {
const count = computeCount(props);
return 6 + 2 * (count[0] + count[1]);
}
});
}

Expand Down
2 changes: 1 addition & 1 deletion dist/regl-gpu-lines.min.js

Large diffs are not rendered by default.

88 changes: 66 additions & 22 deletions docs/border.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
#pragma lines: width = getWidth(xy);
uniform float width;
uniform vec2 translate, scale;
// Implement the functions above
vec4 getPosition(vec2 xy) { return vec4(xy, 0, 1); }
vec4 getPosition(vec2 xy) { return vec4(xy * scale + translate, 0, 1); }
float getWidth(vec2 xy) { return width; }
float getX(vec2 xy) { return xy.x; }`,
frag: `
Expand All @@ -46,33 +47,33 @@
// Convert the line coord into an SDF
float sdf = length(lineCoord.xy) * width;
// Use the y-value of the line coord to distinguish between sides of the line
vec3 borderColor = lineCoord.y < 0.0 ? vec3(1, 0.5, 0.5) : vec3(0.5, 0.5, 1);
vec3 borderColor = 0.5 + 0.5 * vec3(lineCoord.xy, 0);
// Apply a border with 1px transition
gl_FragColor = vec4(
mix(vec3(0), borderColor, smoothstep(width - borderWidth - 0.5, width - borderWidth + 0.5, sdf)),
mix(vec3(0), borderColor, smoothstep(width - borderWidth - 1.0, width - borderWidth + 1.0, sdf)),
1);
}`,

// Additional regl command properties are valid
uniforms: {
width: (ctx, props) => ctx.pixelRatio * props.width,
borderWidth: (ctx, props) => ctx.pixelRatio * props.borderWidth,
translate: regl.prop('translate'),
scale: regl.prop('scale')
},
depth: {enable: false}
});

// Construct an array of xy pairs
const n = 21;
const n = 11;
const xy = [...Array(n).keys()]
.map(i => (i / (n - 1) * 2.0 - 1.0) * 0.8)
.map(t => [t, 0.5 * Math.sin(8.0 * t)]);
.map(t => [t, 0.5 * Math.sin(54.0 * t)]);

// Set up the data to be drawn. Note that we preallocate buffers and don't create
// them on every draw call.
const lineData = {
join: 'round',
cap: 'round',
vertexCount: xy.length,
vertexAttributes: {
xy: regl.buffer(xy)
Expand All @@ -82,14 +83,35 @@
xy: regl.buffer([xy.slice(0, 3), xy.slice(-3).reverse()])
},

width: 25,
borderWidth: 10
width: 35,
borderWidth: 10,
miterLimit: 3.0,
scale: [1, 1]
};

window.addEventListener('mousemove', e => {
lineData.scale = [
e.offsetX / window.innerWidth * 2 - 1,
-e.offsetY / window.innerHeight * 2 + 1
];
draw();
});


function draw () {
regl.poll();
regl.clear({color: [0.2, 0.2, 0.2, 1]});
drawLines(lineData);
drawLines([{
...lineData,
translate: [0, -0.4],
cap: 'round',
join: 'round'
}, {
...lineData,
translate: [0, 0.4],
cap: 'round',
join: 'miter'
}]);
}

draw();
Expand Down Expand Up @@ -133,9 +155,10 @@
#pragma lines: width = getWidth(xy);

uniform float width;
uniform vec2 translate, scale;

// Implement the functions above
vec4 getPosition(vec2 xy) { return vec4(xy, 0, 1); }
vec4 getPosition(vec2 xy) { return vec4(xy * scale + translate, 0, 1); }
float getWidth(vec2 xy) { return width; }
float getX(vec2 xy) { return xy.x; }`,
frag: `
Expand All @@ -146,33 +169,33 @@
// Convert the line coord into an SDF
float sdf = length(lineCoord.xy) * width;

// Use the y-value of the line coord to distinguish between sides of the line
vec3 borderColor = lineCoord.y < 0.0 ? vec3(1, 0.5, 0.5) : vec3(0.5, 0.5, 1);
vec3 borderColor = 0.5 + 0.5 * vec3(lineCoord.xy, 0);

// Apply a border with 1px transition
gl_FragColor = vec4(
mix(vec3(0), borderColor, smoothstep(width - borderWidth - 0.5, width - borderWidth + 0.5, sdf)),
mix(vec3(0), borderColor, smoothstep(width - borderWidth - 1.0, width - borderWidth + 1.0, sdf)),
1);
}`,

// Additional regl command properties are valid
uniforms: {
width: (ctx, props) => ctx.pixelRatio * props.width,
borderWidth: (ctx, props) => ctx.pixelRatio * props.borderWidth,
translate: regl.prop('translate'),
scale: regl.prop('scale')
},
depth: {enable: false}
});

// Construct an array of xy pairs
const n = 21;
const n = 11;
const xy = [...Array(n).keys()]
.map(i => (i / (n - 1) * 2.0 - 1.0) * 0.8)
.map(t => [t, 0.5 * Math.sin(8.0 * t)]);
.map(t => [t, 0.5 * Math.sin(54.0 * t)]);

// Set up the data to be drawn. Note that we preallocate buffers and don't create
// them on every draw call.
const lineData = {
join: 'round',
cap: 'round',
vertexCount: xy.length,
vertexAttributes: {
xy: regl.buffer(xy)
Expand All @@ -182,14 +205,35 @@
xy: regl.buffer([xy.slice(0, 3), xy.slice(-3).reverse()])
},

width: 25,
borderWidth: 10
width: 35,
borderWidth: 10,
miterLimit: 3.0,
scale: [1, 1]
};

window.addEventListener('mousemove', e => {
lineData.scale = [
e.offsetX / window.innerWidth * 2 - 1,
-e.offsetY / window.innerHeight * 2 + 1
];
draw();
});


function draw () {
regl.poll();
regl.clear({color: [0.2, 0.2, 0.2, 1]});
drawLines(lineData);
drawLines([{
...lineData,
translate: [0, -0.4],
cap: 'round',
join: 'round'
}, {
...lineData,
translate: [0, 0.4],
cap: 'round',
join: 'miter'
}]);
}

draw();
Expand Down
10 changes: 6 additions & 4 deletions docs/debug.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
const state = wrapGUI(State({
lineConfig: State.Section({
capResolution: State.Slider(8, {min: 1, max: 30, step: 1}),
joinResolution: State.Slider(8, {min: 1, max: 30, step: 1}),
joinResolution: State.Slider(6, {min: 1, max: 30, step: 1}),
cap: State.Select('round', {options: ['round', 'square', 'none']}),
join: State.Select('round', {options: ['round', 'miter', 'bevel']}),
miterLimit: State.Slider(4, {min: 1, max: 8, step: 0.01}),
Expand All @@ -41,7 +41,7 @@
flip: State.Slider(1, {min: -1, max: 1, step: 0.001}),
}, {expanded: true}),
line: State.Section({
width: State.Slider(40, {min: 1, max: 100, step: 0.1}),
width: State.Slider(70, {min: 1, max: 100, step: 0.1}),
opacity: State.Slider(0.5, {min: 0, max: 1, step: 0.01}),
}, {label: 'line', expanded: false}),
border: State.Section({
Expand Down Expand Up @@ -361,6 +361,7 @@
lineColor: [0, 0, 0, state.line.opacity],
borderColor: [0, 0, 0, state.border.opacity],
dashColor: [0, 0, 0, state.dash.opacity],
//vertexCount: 0
});
}

Expand Down Expand Up @@ -405,7 +406,7 @@
const state = wrapGUI(State({
lineConfig: State.Section({
capResolution: State.Slider(8, {min: 1, max: 30, step: 1}),
joinResolution: State.Slider(8, {min: 1, max: 30, step: 1}),
joinResolution: State.Slider(6, {min: 1, max: 30, step: 1}),
cap: State.Select('round', {options: ['round', 'square', 'none']}),
join: State.Select('round', {options: ['round', 'miter', 'bevel']}),
miterLimit: State.Slider(4, {min: 1, max: 8, step: 0.01}),
Expand All @@ -415,7 +416,7 @@
flip: State.Slider(1, {min: -1, max: 1, step: 0.001}),
}, {expanded: true}),
line: State.Section({
width: State.Slider(40, {min: 1, max: 100, step: 0.1}),
width: State.Slider(70, {min: 1, max: 100, step: 0.1}),
opacity: State.Slider(0.5, {min: 0, max: 1, step: 0.01}),
}, {label: 'line', expanded: false}),
border: State.Section({
Expand Down Expand Up @@ -735,6 +736,7 @@
lineColor: [0, 0, 0, state.line.opacity],
borderColor: [0, 0, 0, state.border.opacity],
dashColor: [0, 0, 0, state.dash.opacity],
//vertexCount: 0
});
}

Expand Down
Loading

0 comments on commit 954476a

Please sign in to comment.