3
\$\begingroup\$

In January Notch tweeted about WebGL 2:

WebGL 2 is happening. Bye, compatibility! Hello, 3d textures and pixel buffers!

oh god it's got free vertex indices in the shader. That is basically a geometry shader.

.. which is great considering I'm rendering like five triangles..

What are these "free vertex indices" and how do they relate to geometry shaders? I'm new to all this and my google-fu is failing me on this occasion.

\$\endgroup\$

3 Answers 3

2
\$\begingroup\$

I'm just guessing it's using gl_VertexID to generate vertices in the shader. Example:

function main() {
  const gl = document.querySelector("canvas").getContext("webgl2", { alpha: false });
  if (!gl) {
    alert("needs WebGL2");
    return;
  }
  const programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
  const vertexCount = 100000;

  function render(time) {
    twgl.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

    var uniforms = {
      time: time * 0.001,
      resolution: [gl.canvas.width, gl.canvas.height],
      vertexCount: vertexCount,
    };

    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, uniforms);
    gl.drawArrays(gl.TRIANGLES, 0, vertexCount);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
body {
  margin: 0;
}
canvas {
  width: 100vw;
  height: 100vh;
  display: block;
  background-color: black;
}
<canvas></canvas>
<script id="vs" type="notjs">
#version 300 es
uniform vec2 resolution;
uniform float time;
uniform float vertexCount;

out vec4 v_color;

#define PI radians(180.0)

vec3 hsv2rgb(vec3 c) {
  c = vec3(c.x, clamp(c.yz, 0.0, 1.0));
  vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

mat4 rotX(float angleInRadians) {
    float s = sin(angleInRadians);
    float c = cos(angleInRadians);
  	
    return mat4( 
      1, 0, 0, 0,
      0, c, s, 0,
      0, -s, c, 0,
      0, 0, 0, 1);  
}

mat4 rotY(float angleInRadians) {
    float s = sin(angleInRadians);
    float c = cos(angleInRadians);
  	
    return mat4( 
      c, 0,-s, 0,
      0, 1, 0, 0,
      s, 0, c, 0,
      0, 0, 0, 1);  
}

mat4 rotZ(float angleInRadians) {
    float s = sin(angleInRadians);
    float c = cos(angleInRadians);
  	
    return mat4( 
      c,-s, 0, 0, 
      s, c, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1); 
}

mat4 trans(vec3 trans) {
  return mat4(
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    trans, 1);
}

mat4 ident() {
  return mat4(
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1);
}

mat4 scale(vec3 s) {
  return mat4(
    s[0], 0, 0, 0,
    0, s[1], 0, 0,
    0, 0, s[2], 0,
    0, 0, 0, 1);
}

mat4 uniformScale(float s) {
  return mat4(
    s, 0, 0, 0,
    0, s, 0, 0,
    0, 0, s, 0,
    0, 0, 0, 1);
}

mat4 persp(float fov, float aspect, float zNear, float zFar) {
  float f = tan(PI * 0.5 - 0.5 * fov);
  float rangeInv = 1.0 / (zNear - zFar);

  return mat4(
    f / aspect, 0, 0, 0,
    0, f, 0, 0,
    0, 0, (zNear + zFar) * rangeInv, -1,
    0, 0, zNear * zFar * rangeInv * 2., 0);
}

mat4 trInv(mat4 m) {
  mat3 i = mat3(
    m[0][0], m[1][0], m[2][0], 
    m[0][1], m[1][1], m[2][1], 
    m[0][2], m[1][2], m[2][2]);
  vec3 t = -i * m[3].xyz;
    
  return mat4(
    i[0], t[0], 
    i[1], t[1],
    i[2], t[2],
    0, 0, 0, 1);
}

mat4 lookAt(vec3 eye, vec3 target, vec3 up) {
  vec3 zAxis = normalize(eye - target);
  vec3 xAxis = normalize(cross(up, zAxis));
  vec3 yAxis = cross(zAxis, xAxis);

  return mat4(
    xAxis, 0,
    yAxis, 0,
    zAxis, 0,
    eye, 1);
}

mat4 cameraLookAt(vec3 eye, vec3 target, vec3 up) {
  #if 1
  return inverse(lookAt(eye, target, up));
  #else
  vec3 zAxis = normalize(target - eye);
  vec3 xAxis = normalize(cross(up, zAxis));
  vec3 yAxis = cross(zAxis, xAxis);

  return mat4(
    xAxis, 0,
    yAxis, 0,
    zAxis, 0,
    -dot(xAxis, eye), -dot(yAxis, eye), -dot(zAxis, eye), 1);  
  #endif
  
}



// hash function from https://www.shadertoy.com/view/4djSRW
float hash(float p) {
	vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427));
    p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137));
	return fract(p2.x * p2.y * 95.4337);
}

// times 2 minus 1
float t2m1(float v) {
  return v * 2. - 1.;
}

// times .5 plus .5
float t5p5(float v) {
  return v * 0.5 + 0.5;
}

float inv(float v) {
  return 1. - v;
}

vec3 getQuadStripPoint(const float id) {
  float ux = floor(id / 6.) + mod(id, 2.);
  float vy = mod(floor(id / 2.) + floor(id / 3.), 2.);
  return vec3(ux, vy, 0);
}

void getCirclePoint(const float numEdgePointsPerCircle, const float id, const float inner, const float start, const float end, out vec3 pos) {
  float ux = floor(id / 6.) + mod(id, 2.);
  float vy = mod(floor(id / 2.) + floor(id / 3.), 2.); // change that 3. for cool fx
  float u = ux / numEdgePointsPerCircle;
  float v = mix(inner, 1., vy);
  float a = mix(start, end, u) * PI * 2. + PI * 0.0;
  float s = sin(a);
  float c = cos(a);
  float x = c * v;
  float y = s * v;
  float z = 0.;
  pos = vec3(x, y, z);  
}


#define CUBE_POINTS_PER_FACE 6.
#define FACES_PER_CUBE 6.
#define POINTS_PER_CUBE (CUBE_POINTS_PER_FACE * FACES_PER_CUBE)
void getCubePoint(const float id, out vec3 position, out vec3 normal) {
  float quadId = floor(mod(id, POINTS_PER_CUBE) / CUBE_POINTS_PER_FACE);
  float sideId = mod(quadId, 3.);
  float flip   = mix(1., -1., step(2.5, quadId));
  // 0 1 2  1 2 3
  float facePointId = mod(id, CUBE_POINTS_PER_FACE);
  float pointId = mod(facePointId - floor(facePointId / 3.0), 6.0);
  float a = pointId * PI * 2. / 4. + PI * 0.25;
  vec3 p = vec3(cos(a), 0.707106781, sin(a)) * flip;
  vec3 n = vec3(0, 1, 0) * flip;
  float lr = mod(sideId, 2.);
  float ud = step(2., sideId);
  mat4 mat = rotX(lr * PI * 0.5);
  mat *= rotZ(ud * PI * 0.5);
  position = (mat * vec4(p, 1)).xyz;
  normal = (mat * vec4(n, 0)).xyz;
}

float Hash( vec2 p) {
     vec3 p2 = vec3(p.xy,1.0);
    return fract(sin(dot(p2,vec3(37.1,61.7, 12.4)))*3758.5453123);
}

float noise(in vec2 p) {
    vec2 i = floor(p);
     vec2 f = fract(p);
     f *= f * (3.0-2.0*f);

    return mix(mix(Hash(i + vec2(0.,0.)), Hash(i + vec2(1.,0.)),f.x),
               mix(Hash(i + vec2(0.,1.)), Hash(i + vec2(1.,1.)),f.x),
               f.y);
}

float fbm(vec2 p) {
     float v = 0.0;
     v += noise(p*1.0)*.5;
//     v += noise(p*2.)*.25;
//     v += noise(p*4.)*.125;
     return v;
}

float crv(float v) {
  return fbm(vec2(v, v * 1.23));
  //float o = sin(v) + sin(v * 2.1) + sin(v * 4.2) + sin(v * 8.9); 
  //return o / 4.;
}

vec3 fgetCurvePoint(float t) {
//  return vec3(sin(-t), sin(t * 0.8), sin(t * 0.6));
//  return vec3( mod(t, 1.) * 0.01, 0, mod(t, 1.));
  return vec3(
    crv(t),
    crv(t + .3),
    crv(t + .6)
  ) * 2. - 1.;
}  

vec3 getCurvePoint(const float id) {
  return vec3(
    sin(id * 0.99),
    sin(id * 2.43),
    sin(id * 1.57));
}

const float expand = 80.0;


void sky(const float vertexId, const float vertexCount, float base, const mat4 cmat, out vec3 pos, out vec4 color) {
  float starId = floor(vertexId / 3.);
  float numStars = floor(vertexCount / 3.);
  float starV = starId / numStars;
  
  float h = hash(starId * 0.017);
  
  
  float pId = mod(vertexId, 3.);
  //float sz = h * 2.;
  float sz = clamp(500.0 / min(resolution.x, resolution.y), 2., 200.); 
  
  pos = normalize(vec3(
    t2m1(hash(starId * 0.123)),
    t2m1(hash(starId * 0.353)),
    t2m1(hash(starId * 0.627)))) * 500. + cmat[3].xyz;
  pos += cmat[0].xyz * sz * step(0.5, pId);
  pos += cmat[1].xyz * sz * step(1.5, pId);
  
  color = vec4(h, h, h, 1);
}

void cube(const float vertexId, const float vertexCount, const float base, const mat4 cmat, const mat4 vmat, out vec3 pos, out vec4 color) {
  vec3 cpos;
  vec3 cnormal;
  
  float pointId = vertexId;
  getCubePoint(pointId, cpos, cnormal);
  float cubeId = floor(pointId / 36.);
  float numCubes = floor(vertexCount / 36.);
  float down = floor(pow(numCubes, .333));
  float across = floor(floor(numCubes / down) / down);
  float deep = floor(numCubes / (down * across));
  
  float cx = mod(cubeId, across);
  float cy = mod(floor(cubeId / across) , down);
  float cz = floor(cubeId / (across * down));
  
  float cu = cx / (across - 1.);
  float cv = cy / (down - 1.);
  float cw = cz / (deep - 1.);
  
  float ca = cu * 2. - 1.;
  float cd = cv * 2. - 1.;
  float ce = cw * 2. - 1.;
  
  float tm = time * 0.1;
  mat4 mat = ident();
 
  const float dim = 640.0;
  vec3 t = vec3(
      hash(cubeId * 0.123),
      hash(cubeId * 0.719),
      hash(cubeId * 0.347)) * 2. - 1.;
  
  
  
  float s = pow(abs(sin(cubeId + time * 0.01)), 1000.) * .9;

  float pump = step(0.7, s);
  
  
  
  #if 0
    mat *= trans(vec3(ca, ce, cd) * 120.0);
    mat *= uniformScale(2.);
  #else
    mat *= trans(t * dim * .5);
    mat *= rotX(time * 1. + cubeId);
    mat *= rotZ(time * 1.1 + cubeId);
    mat *= uniformScale(mix(4., 20. + pump * 20., pow(s, 5.)));
   #endif
  
  pos = (mat * vec4(cpos, 1)).xyz;
  vec3 n = normalize((mat * vec4(cnormal, 0)).xyz);
  
  
  float hue = time * .03 + mix(1., 1.1, pump);//abs(ca * cd) * 2.;
  float sat = 1.;mix(1., 0., abs(ca));
  float val = 1.;mix(1., 0.5, abs(cd));
  vec3 tcolor = hsv2rgb(vec3(hue, sat, val));
  
  vec3 lightDir = normalize(vec3(0.3, 0.4, -1));
  vec3 lightPos = vec3(0);
  vec3 surfaceToLight = normalize(lightPos - pos);
  vec3 surfaceToView = normalize(cmat[3].xyz - pos);
  vec3 halfVector = normalize(surfaceToLight + surfaceToView);
 
  float light = dot(n, surfaceToLight);
  float specular = pow(clamp(dot(n, halfVector), 0., 1.), 20.);
  
  //color = vec4(tcolor * (dot(n, lightDir) * 0.5 + 0.5), 1);  
  color = vec4(tcolor * light + vec3(specular), 1); 
  
  color.a = mix(1., 10., pump);
  color.rgb *= color.a;
}

void main() {
  float vertexId = float(gl_VertexID);
  const float numCubePoints = 90000.0;
    
  //float base = 15.;  // good place to adjust
  float base = time * 0.125;

  const float coff = 0.14;
  
  vec3 b0 = getCurvePoint(base + coff * 0.);
  vec3 b1 = getCurvePoint(base + coff * 1.);
  vec3 b2 = getCurvePoint(base + coff * 2.);
  
  vec3 c0 = normalize(b1 - b0);
  vec3 c1 = normalize(b2 - b1);
  
  vec3 czaxis = normalize(c1 - c0);
  vec3 cxaxis = normalize(cross(c0, c1));
  vec3 cyaxis = normalize(cross(czaxis, cxaxis));
  
  mat4 pmat = persp(radians(60.0), resolution.x / resolution.y, .1, 1000.0);

  vec2 ms = vec2(0); //texture2D(touch, vec2(0, 0)).xy + vec2(0, 1);  
  
  vec3 eye    = b0 * expand + cyaxis * .001 + czaxis * 2.2;
  vec3 target = b1 * expand + cyaxis * .002 + czaxis + ms.x * cxaxis * 2. + ms.y * cyaxis * 40.;
  vec3 up     = cyaxis;
  
 // eye = vec3(1, 1, 1);
//  target = vec3(0);
//  up = vec3(0,1,0);
  
  mat4 cmat = lookAt(eye, target, up);
  mat4 vmat = rotZ(asin(up.y) * 1.) * inverse(cmat);
  
  vec3 pos;
  vec4 color;
  
  if (vertexId < numCubePoints) {
    cube(vertexId, vertexCount, base, cmat, vmat, pos, color);
  } else {
    sky(vertexId, vertexCount, base, cmat, pos, color);
  }
  
  gl_Position = pmat * vmat * vec4(pos, 1);
  v_color = color;
  
  float cz = gl_Position.z / gl_Position.w * .5 + .5;
  v_color.rgb = mix(v_color.rgb, vec3(0), mix(4., 0., cz)); 
  
}
  </script>
  <script id="fs" type="notjs">
#version 300 es
precision mediump float;

in vec4 v_color;
out vec4 outColor;

void main() {
  outColor = v_color;
}
  </script>
<script src="https://twgljs.org/dist/3.x/twgl.min.js"></script>

Here's a website based on that concept.

As a simple probably more useful example if you just want to draw a fullscreen quad you can do it without setting up buffers and attributes like this

function main() {
  const gl = document.querySelector("canvas").getContext("webgl2", { alpha: false });
  if (!gl) {
    alert("needs WebGL2");
    return;
  }
  const programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
  const vertexCount = 6;

  function render(time) {
    twgl.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

    var uniforms = {
      time: time * 0.001,
      resolution: [gl.canvas.width, gl.canvas.height],
    };

    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, uniforms);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
body {
  margin: 0;
}
canvas {
  width: 100vw;
  height: 100vh;
  display: block;
  background-color: black;
}
<canvas></canvas>
<script id="vs" type="notjs">
#version 300 es

void main() {
  int x = gl_VertexID % 2;
  int y = (gl_VertexID / 2 + gl_VertexID / 3) % 2;
  gl_Position = vec4(vec2(x, y) * 2. - 1., 0, 1);
}
  </script>
  <script id="fs" type="notjs">
#version 300 es
precision mediump float;

uniform vec2 resolution;
uniform float time;

out vec4 outColor;

void main() {
  // taken from glslsandbox.com
	vec2 position = ( gl_FragCoord.xy / resolution.xy );

	float color = 0.0;
	color += sin( position.x * cos( time / 15.0 ) * 80.0 ) + cos( position.y * cos( time / 15.0 ) * 10.0 );
	color += sin( position.y * sin( time / 10.0 ) * 40.0 ) + cos( position.x * sin( time / 25.0 ) * 40.0 );
	color += sin( position.x * sin( time / 5.0 ) * 10.0 ) + sin( position.y * sin( time / 35.0 ) * 80.0 );
	color *= sin( time / 10.0 ) * 0.5;

	outColor = vec4( vec3( color, color * 0.5, sin( color + time / 3.0 ) * 0.75 ), 1.0 );
}
  </script>
<script src="https://twgljs.org/dist/3.x/twgl.min.js"></script>

\$\endgroup\$
3
  • \$\begingroup\$ Oh wow, this is really cool. It'll take me a little while to go through and understand everything here, that shader's a bit beyond anything I've played with so far. Am I right in thinking that the central principle here is supplying a high vertexCount but no attributes, then doing all the maths to position the vertices in the vertex shader? So this means attributes are unusable, you can't really add detail to an existing model with this, but you can do full terrain generation and stuff? So everything about the generation needs to be determined by uniforms only? \$\endgroup\$
    – JoJnr
    May 9, 2018 at 20:03
  • \$\begingroup\$ Although I suppose a simple model could be loaded into uniforms. \$\endgroup\$
    – JoJnr
    May 9, 2018 at 20:13
  • \$\begingroup\$ @JoJnr see my answer below (only 4 years late!) – you can load large amounts of attribute data via textures, and the API is clearly designed with this in mind, even though you wouldn't know it from the way things are named and documented, because OpenGL \$\endgroup\$
    – bobtato
    Apr 20, 2022 at 22:59
2
\$\begingroup\$

This question was a useful starting point for me, so, to expand on previous answers:

gl_VertexID lets you generate vertices as pure functions of a monotonically increasing integer, which the examples on vertexshaderart.com illustrate very well. You can produce a wide range of abstract visuals this way without passing any data to the GPU at all (because drawArrays lets you read any number of values from an empty vertex buffer).

By itself, though, that technique is quite limited, because it also means you can't pass any data to the GPU in the usual way. If you use drawArrays to pass actual data, you have to supply exactly one set of attributes per vertex, as with WebGL 1; and if you use drawElements then gl_VertexID is fairly useless anyway, as then it just reads back the index you passed in – if you process the same input vertex 10 times, your shader can't tell whether it's on the first or the tenth instance.

The missing piece of the puzzle is that you can call drawArrays with no actual array, but pass attributes to your shader via textures, and WebGL 2 has a couple of features that make this much more practical. Firstly, the GLSL textureFetch function makes it easier to treat a texture as an array; and more importantly, WebGL 2 provides specific texture formats that are useful for this sort of thing. For instance, a texture in RGBA32F format is simply an array of single-precision vec4 values, which (despite the name) you are free to load as vertex coordinates, or quaternions, or 2D rectangles, or whatever you want. (In WebGL 1, RGBA texel components are 8-bit ints at best, and you can't even guarantee that)

Concrete example: If you want to draw thick lines with OpenGL, you have to convert each line segment into a rectangle (i.e. a pair of triangles). There are several pages describing how to do this using WebGL 1, but it involves doing much of the geometry calculation in JavaScript, then passing quite a lot of data to the GPU (at least 6 vertices for each 2-vertex line segment), so as well as being much harder than using CanvasRenderingContext2D, it might even be slower, too.

But with WebGL 2, you can dump the vertices of your lines straight into a texture, put all the calculations into a vertex shader, and run it 6 * (number of lines) times:

    #version 300 es
    out vec3 vertexColor;
    uniform float u_weight;    // line weight parameter
    uniform sampler2D u_lines; // "texture" containing vertices
    int DIM;                   // dimension of the (square) texture
    vec4 vertex(int i) {
        // load vertex i from the RGBA32F texture
        return texelFetch(u_lines, ivec2(i%DIM, i/DIM), 0);
    }
    void main() {
        DIM = textureSize(u_lines,0).x;
        // get line vertices, reversed for corners 1, 3 and 5:
        vec4 p1 = vertex(2*(gl_VertexID/6) + gl_VertexID%2);
        vec4 p2 = vertex(2*(gl_VertexID/6) + 1&~(gl_VertexID%2));
        // offset direction, reversed for corners 2, 3 and 4:
        float sign1 = float( ((1+gl_VertexID%3)&2) - 1 );
        // matrix to rotate by +/- pi/2:
        mat2 m = mat2( 0.0, -sign1, sign1, 0.0 );
        // displace by (weight/2) * rotated unit vector of line: 
        vec2 ofs = m * normalize(p2.xy - p1.xy) * (u_weight/2.0);
        p1 += vec4(ofs, 0.0, 0.0);
        gl_Position = p1;
        vertexColor = vec3(0.7, 0.3, 0.5);
    }

(the purpose of all that crunchy integer math is to generate the six different vertex positions from a single calculation. Code like this is unreadable without working it out on paper, but the common wisdom says it's best to write shaders without using if statements)

It is easy to see how you could adapt this for things like particle systems, displacement textures, or any other task that processes lots of fixed-size packets of data.

I'm sure there are performance implications to using textures vs. regular vertex attribute buffers, but it's a safe bet that moving tons of calculations from JS to the GPU makes it worthwhile.

\$\endgroup\$
1
  • \$\begingroup\$ Can you please convert the sample code into a working embedded example like the ones in gman's answers? That would be really helpful! \$\endgroup\$
    – trusktr
    Jan 17, 2023 at 2:32
0
\$\begingroup\$

Presumably this is a reference to the availability of gl_VertexID in WebGL 2.

In a vertex shader, this variable holds the index of the vertex. If you were interested in this information in WebGL 1, you'd have to provide it to the shader yourself. In WebGL 2 the presence of the built-in variable provides the information to you "for free."

I'm not sure preciously what the comparison is when calling this feature "basically a geometry shader." It's possible in some cases one could use the vertex index for the same kinds of operations you might use gl_PrimitiveIDIn (from a geometry shader) for, depending on the other context available to (or that can be assumed by) your shader programs.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks. I'd found a reference to gl_VertexID but dismissed it as it didn't seem relevant to geometry shaders. Given the context of "which is great considering I'm rendering like five triangles" I'd assumed this was about being able to generate additional vertices in the shader, but perhaps not. \$\endgroup\$
    – JoJnr
    May 8, 2018 at 17:38
  • \$\begingroup\$ It wouldn't surprise me if the original post was referring to something else, but the only way to be sure would to be reply to that on Twitter and ask. \$\endgroup\$
    – user1430
    May 8, 2018 at 17:45
  • \$\begingroup\$ Yeah, sadly a few people already replied and asked with no response. \$\endgroup\$
    – JoJnr
    May 8, 2018 at 18:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .