Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active April 27, 2023 18:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save monfera/85aa9627de1ae521d3ac5b26c9cd1c49 to your computer and use it in GitHub Desktop.
Save monfera/85aa9627de1ae521d3ac5b26c9cd1c49 to your computer and use it in GitHub Desktop.
Shapes and WebGL tweening
license: mit

Code from codepen.io/monfera/pen/GjOBkJ

Update: now it uses the WebGL extension OES_standard_derivatives for antialiasing, if avaliable. There's also Z ordering and alpha blending, to support antialiasing with the alpha smoothstepping method. There's a slight trick, fwidth is applied on dist rather than r so as to avoid artifacts where the formula yields near-zero radii.

It shows:

  1. A simple use of the outstanding regl library - the below techniques can alternatively work with direct WebGL too
  2. Efficient tweening of possibly large amounts of data by loading two geometries (here, squeezed into one vec4 position attribute) to the GPU, and calculating the tweening on the GPU (contrast this to refreshing geometry in a rAF loop
  3. The use of smoothstep and smootherstep as a quick&dirty alternative to a configurable tweening function; enough for 90% of purposes
  4. Rendering marker shapes with the fragment shader - each point is one element in the vertex buffer, no need for representing shapes via a mesh
  5. Use of the so-called "superformula" generalization for shape definition - neat, but in practice it's both limiting and slow (too much trig in the frag shader)
  6. Similar to the geometry tweening, there's also shape tweening (via uniform tweening)
  7. Use of scijs/ndarray for a numpy-like interface, pretending that native typed JS arrays are multidimensional arrays (syntactic sugar in this case)

It's not meant to be efficient as it's more of a test for various features. It can be made much faster/nicer in various ways.

Links:

Uploaded with blockbuilder.org

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://rawcdn.githack.com/monfera/ndarray-bundle/cbbafcb5/ndarray.min.js"></script>
<script src="https://rawcdn.githack.com/regl-project/regl/v1.3.13/dist/regl.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<script>
// http://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12
// except yellow
const palette = [
[166,206,227],
[ 31,120,180],
[178,223,138],
[ 51,160, 44],
[251,154,153],
[227, 26, 28],
[253,191,111],
[255,127, 0],
[202,178,214],
[106, 61,154],
[177, 89, 40]
]
const regl = window.createREGL({extensions: ['OES_standard_derivatives']})
const lineCount = 11
const pointCount = 5
const lineWidth = 2
// padding is interpreted as percentage of the screen width or height
const xPadding = 0.1
const yPadding = 0.25
const xScale = x => 2 * (1 - 2 * xPadding) * (x - 0.5)
const yScale = y => 2 * (1 - 2 * yPadding) * (y - 0.5)
// nd :: typedArrayClass -> array -> ndarray
const nd = (Array, dimensions) => ndarray(
new Array(dimensions.reduce((p, n) => p * n, 1)),
dimensions
)
const colorLineAesthetic = ({lineWidth}) => ({
blend: {
enable: true,
func: {
srcRGB: 'src alpha',
srcAlpha: 1,
dstRGB: 'one minus src alpha',
dstAlpha: 1
},
equation: {
rgb: 'add',
alpha: 'add'
},
color: [0, 0, 0, 0]
},
depth: {
enable: true,
mask: true,
func: 'less',
range: [0, 1]
},
vert: `
precision mediump float;
attribute vec3 positionFrom;
attribute vec3 positionTo;
attribute vec4 color;
uniform float tween;
varying vec4 c;
float smootherstep(float edge0, float edge1, float x) {
x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
}
void main() {
vec3 tweened = mix(positionFrom, positionTo, smootherstep(0.0, 1.0, tween));
gl_Position = vec4(tweened, 1.0);
c = color;
gl_PointSize = 60.0;
}`,
frag: `
#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives : enable
#endif
precision mediump float;
uniform mat3 S1;
uniform mat3 S2;
uniform float tween;
varying vec4 c;
float superFormula(mat3 S, float fi) {
float a = S[0][0];
float b = S[0][1];
float m1 = S[0][2];
float m2 = S[1][0];
float n1 = S[1][1];
float n2 = S[1][2];
float n3 = S[2][0];
float s = S[2][1];
float r = pow( pow(abs(cos(m1 * fi / 4.0) / a), n2)
+ pow(abs(sin(m2 * fi / 4.0) / b), n3),
-1.0 / n1
);
return s * r;
}
void main() {
float alpha = 1.0, delta = 0.0;
vec2 pxy = 2.0 * gl_PointCoord.xy - 1.0;
float dist = length(pxy);
float fi = atan(pxy.y, pxy.x);
float r1 = superFormula(S1, fi);
float r2 = superFormula(S2, fi);
float r = mix(r1, r2, smoothstep(0.0, 1.0, tween));
float R = dist - r + 1.0;
#ifdef GL_OES_standard_derivatives
delta = fwidth(dist);
if(R > 1.0 + delta ) discard; // avoid further calc, blending if possible
alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, R);
#else
if(R > 1.0) discard;
#endif
gl_FragColor = vec4(c.xyz, c.a * alpha);
}`,
primitive: 'points',
lineWidth
})
/* For performance reasons, the model format is identical with the format
* that regl expects attribute arrays to be in.
*/
const model = (({lineCount, pointCount, xScale, yScale}) => {
const positionLength = 3
const colorLength = 4
const positionFrom = nd(Float32Array, [lineCount, pointCount, positionLength])
const positionTo = nd(Float32Array, [lineCount, pointCount, positionLength])
const color = nd(Float32Array, [lineCount, pointCount, colorLength])
let i, j, x1, x2, y1, y2, z
for(i = 0; i < lineCount; i++) {
for(j = 0; j < pointCount; j++) {
x1 = Math.random()
x2 = Math.random()
y1 = Math.random()
y2 = Math.random()
z = 1 - (j + i * pointCount) / (lineCount * pointCount)
positionFrom.set(i, j, 0, xScale(x1))
positionFrom.set(i, j, 1, yScale(y1))
positionFrom.set(i, j, 2, z)
positionTo.set(i, j, 0, xScale(x2))
positionTo.set(i, j, 1, yScale(y2))
positionTo.set(i, j, 2, z)
color.set(i, j, 0, palette[i][0] / 255)
color.set(i, j, 1, palette[i][1] / 255)
color.set(i, j, 2, palette[i][2] / 255)
color.set(i, j, 3, 0.8)
}
}
return {
lineCount,
pointCount,
attributes: {
positionFrom: positionFrom.data,
positionTo: positionTo.data,
color: color.data
},
staticUniforms: {
// cross
S1 : [
/* a= */ 1,
/* b= */ 0.6875,
/* m1= */ 8,
/* m2= */ 8,
/* n1= */ 1.3,
/* n2= */ 0.01,
/* n3= */ 3.313,
/* s= */ 0.99, // leave room for antialiasing band
/* */ 0,
],
// hexagon
S2 : [
/* a= */ 1,
/* b= */ 1,
/* m1= */ 6,
/* m2= */ 6,
/* n1= */ 100,
/* n2= */ 40,
/* n3= */ 40,
/* s= */ 0.8, // make hexagon smaller for similar visual weight
/* */ 0,
]
}
}
})({lineCount, pointCount, xScale, yScale})
const lineViewModelMaker = model => {
const elements = []
let i, j
let index = 0
const {lineCount, pointCount, attributes, staticUniforms} = model
const uniforms = Object.assign(
{},
staticUniforms,
{
tween: ({time}) => Math.cos(time) + 1 / 2
})
for(i = 0; i < lineCount; i++) {
for(j = 0; j < pointCount; j++) {
elements.push(index)
index++
}
}
return {attributes, uniforms, elements }
}
const linesDrawerMaker = model => {
const aesthetic = colorLineAesthetic
const layer = Object.assign(
{},
lineViewModelMaker(model),
aesthetic({lineWidth: lineWidth})
)
return regl(layer)
}
const linesDrawer = linesDrawerMaker(model)
const render = () => {
regl.frame(() => {
regl.clear({
color: [1, 1, 1, 1]
})
linesDrawer()
})
}
render()
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment