block by monfera 85aa9627de1ae521d3ac5b26c9cd1c49

Shapes and WebGL tweening

Full Screen

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

index.html

<!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>

// //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>