Search code examples
javascriptwebglsigma.js

How to create nodes with a border with the webgl renderer in sigmajs


I have a rather large graph so it is necessary to use webgl instead of canvas. I tried to change the webgl node renderer by trying to trick it to draw two circles with the outer one being a little bit bigger, thus creating an border. Unfortunately this didn't work. In the data array the extra code is completely ignored. If someone has an idea it would be appreciated! Below is the code that renders the nodes for the webgl renderer.

    sigma.webgl.nodes.def = {
POINTS: 3,
ATTRIBUTES: 5,
addNode: function(node, data, i, prefix, settings) {
  var color = sigma.utils.floatColor(
    node.color || settings('defaultNodeColor')
  );

  data[i++] = node[prefix + 'x'];
  data[i++] = node[prefix + 'y'];
  data[i++] = node[prefix + 'size'];
  data[i++] = 7864320;
  data[i++] = 0;

  data[i++] = node[prefix + 'x'];
  data[i++] = node[prefix + 'y'];
  data[i++] = node[prefix + 'size'];
  data[i++] = 7864320;
  data[i++] = 2 * Math.PI / 3;

  data[i++] = node[prefix + 'x'];
  data[i++] = node[prefix + 'y'];
  data[i++] = node[prefix + 'size'];
  data[i++] = 7864320;
  data[i++] = 4 * Math.PI / 3;

  /*  This below was my idea to create another node which is slightly bigger 
  and white. The parameters for that are not the issue. The issue is that the 
  log seems to skip this after 12 indexes of the array data for every node. I 
  wasn't able to find how they define this. */
data[i++] = node[prefix + 'x'];
  data[i++] = node[prefix + 'y'];
  data[i++] = node[prefix + 'size'];
  data[i++] = color;
  data[i++] = 0;

  data[i++] = node[prefix + 'x'];
  data[i++] = node[prefix + 'y'];
  data[i++] = node[prefix + 'size'];
  data[i++] = color;
  data[i++] = 2 * Math.PI / 3;

  data[i++] = node[prefix + 'x'];
  data[i++] = node[prefix + 'y'];
  data[i++] = node[prefix + 'size'];
  data[i++] = color;
  data[i++] = 4 * Math.PI / 3;
 */
  //The log is in the picture below
 console.log(data);
},
render: function(gl, program, data, params) {
  var buffer;

  // Define attributes:


   // I guess they define the location and the attributes here.
  var positionLocation =
        gl.getAttribLocation(program, 'a_position'),
      sizeLocation =
        gl.getAttribLocation(program, 'a_size'),
      colorLocation =
        gl.getAttribLocation(program, 'a_color'),
      angleLocation =
        gl.getAttribLocation(program, 'a_angle'),
      resolutionLocation =
        gl.getUniformLocation(program, 'u_resolution'),
      matrixLocation =
        gl.getUniformLocation(program, 'u_matrix'),
      ratioLocation =
        gl.getUniformLocation(program, 'u_ratio'),
      scaleLocation =
        gl.getUniformLocation(program, 'u_scale');

  buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);

 // I don't know what happens here

  gl.enableVertexAttribArray(positionLocation);
  gl.enableVertexAttribArray(sizeLocation);
  gl.enableVertexAttribArray(colorLocation);
  gl.enableVertexAttribArray(angleLocation);

  gl.vertexAttribPointer(
    positionLocation,
    2,
    gl.FLOAT,
    false,
    this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
    0
  );
  gl.vertexAttribPointer(
    sizeLocation,
    1,
    gl.FLOAT,
    false,
    this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
    8
  );
  gl.vertexAttribPointer(
    colorLocation,
    1,
    gl.FLOAT,
    false,
    this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
    12
  );
  gl.vertexAttribPointer(
    angleLocation,
    1,
    gl.FLOAT,
    false,
    this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
    16
  );

  gl.drawArrays(
    gl.TRIANGLES,
    params.start || 0,
    params.count || (data.length / this.ATTRIBUTES)
  );
},

From Without Border

From Without Border

To with Border(I did this with the canvas renderer, where it was really easy)

To with Border(I did this with the canvas renderer, where it was really easy)

This is the log. You can see that only the first 3 blocks are looped(only the ones with the color value 7864320

This is the log. You can see that only the first 3 blocks are looped(only the ones with the color value 7864320)

If any of you know another method to achieve the border I would love to know.


Solution

  • A simple way to plot circles with WebGL is to use gl.POINTS instead of gl.TRIANGLES. With this trick, one vertex is used for one circle, whatever big the radius is. Moreover, you can have a border of the size you want.

    In the vertex shader, you can use gl_PointSize to set the diameter (in pixels) of the circle to draw for your vertex.

    attribute vec2 attCoords;
    attribute float attRadius;
    attribute float attBorder;
    attribute vec3 attColor;
    
    varying float varR1;
    varying float varR2;
    varying float varR3;
    varying float varR4;
    varying vec4 varColor;
    
    const float fading = 0.5;
    
    void main() {
      float r4 = 1.0;
      float r3 = 1.0 - fading / attRadius;
      float r2 = 1.0 - attBorder / attRadius;
      float r1 = r2 - fading / attRadius;
    
      varR4 = r4 * r4 * 0.25;
      varR3 = r3 * r3 * 0.25;
      varR2 = r2 * r2 * 0.25;
      varR1 = r1 * r1 * 0.25;
    
      varColor = vec4( attColor.rgb, 1.0 );
    
      gl_PointSize = 2.0 * attRadius;
      gl_Position = vec4( attCoords.xy, 0, 1 );
    }
    

    In the fragment shader, you can know which of the POINT pixel you are processing. You get the coords of this pixel in gl_PointCoord. (0,0) is th top-left pixel and (1,1) is the bottom-right pixel. Moreover, you can use the keyword discard which is equivalent to return but telling WebGL tht the current fragment must not be drawn.

    precision mediump float;
    varying float varR1;
    varying float varR2;
    varying float varR3;
    varying float varR4;
    varying vec4 varColor;
    
    const vec4 WHITE = vec4(1, 1, 1, 1);
    const vec4 TRANSPARENT = vec4(1, 1, 1, 0);
    
    void main() {
      float x = gl_PointCoord.x - 0.5;
      float y = gl_PointCoord.y - 0.5;
      float radius = x * x + y * y;
    
      if( radius > 1.0 ) discard;
    
      if( radius < varR1 )
        gl_FragColor = varColor;
      else if( radius < varR2 )
        gl_FragColor = mix(varColor, WHITE, (radius - varR1) / (varR2 - varR1));
      else if( radius < varR3 )
        gl_FragColor = WHITE;
      else
        gl_FragColor = mix(WHITE, TRANSPARENT, (radius - varR3) / (varR4 - varR3));
    }
    

    Basically, if the pixel is at more than attRadius from the center, you discard the pixel. If it is inside attRadius - attBorder you use the color. And in between, you use white.

    Finally, we added a subtlety consisting in bluring the limits between color and white, and white and transparent. This gives us anti-aliasing by adding a little bluriness.

    Here is a full working example: https://jsfiddle.net/7rh2eog1/2/