2
\$\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\$
1
\$\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\$
  • \$\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 '18 at 20:03
  • \$\begingroup\$ Although I suppose a simple model could be loaded into uniforms. \$\endgroup\$ – JoJnr May 9 '18 at 20:13
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\$
  • \$\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 '18 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\$ – Josh May 8 '18 at 17:45
  • \$\begingroup\$ Yeah, sadly a few people already replied and asked with no response. \$\endgroup\$ – JoJnr May 8 '18 at 18:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.