Draw a sphere using WebGL without Three.js

I need to draw a sky-sphere with camera controls. It should take 10kb max, but three.js weights 600kb.

I've initialized the WebGL context. It is similar to 2d canvas context, but a bit tricky. There were browser wars, so we have to check which one is supported webgl, experimental-webgl, webkit-3d, moz-webgl. If context is not supported, then getting a context cause throws an error.

var getCTX = function(canvas, cfg) {
  var ctx = null;

  ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"]
    .find(function( type ){
      try {
        ctx = canvas.getContext(type, cfg);
      } catch(e) {}
      return ctx;
    });

  return ctx;
};

Triangle

All 3D objects are made out of polygons. Polygon is just a triangle that have 3 points and a glsl script that describe how to fill point:

var vertices = new Float32Array([
  0, 0.6,
  0.7, -0.6,
  -0.7, -0.6
]);

// Vertex shader
var VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
varying highp vec4 v_Color;

void main() {
  gl_Position = a_Position;
  v_Color = a_Color;
}`;

// Fragment shader
var FSHADER_SOURCE = `
varying highp vec4 v_Color;
void main() {
  gl_FragColor = v_Color;
}`;

When we want to draw a triangle — we need to pass its points and Vertex\Fragment shaders to WebGL pipeline. I use my own wrapper that simplifies this operations.

var triangle,
  shader = scene.shader(VSHADER_SOURCE, FSHADER_SOURCE);

scene.add(
  triangle = new Form3D.Object.Mesh( {
    shader,
    triangles: [
      [ 0, 30, 0 ], { a_Color: [ 1.0, 0.0, 0.3 ] },
      [ -30, -30, 0 ], { a_Color: [ 0.3, 1.0, 0.2 ] },
      [ 30, -30, 0 ], { a_Color: [ 0.0, 0.3, 1.0 ] }
    ]
  }
) );

scene.clear();
scene.draw();

Square

Square is made just from two triangles.

Cube

Cube is a more complex object. We can build it from 6 squares.

So we would create squares from two triangles, and put them on each cube face.

Faces:

var faces = [
  [0,1,2,3],
  [5,4,7,6],

  [4,0,3,7],
  [1,5,6,2],

  [5,1,0,4],
  [2,6,7,3]
];

Also we need to define each point position in the 3D space:

var dots = [
  [-1,-1,1],
  [1,-1,1],
  [1,-1,-1],
  [-1,-1,-1],

  [-1,1,1],
  [1,1,1],
  [1,1,-1],
  [-1,1,-1]
];

Now it is time to build triangles for each quad (four points surface). For this purpose we would add addQuad function to the Mesh prototype:

Form3D.Object.Mesh.prototype.addQuad = function(a, b, c, d){
  this.addVertex(a, b, d);
  this.addVertex(d, b, c);
  return this;
};

And finally lets map each face to corresponding point coordinates:

faces.forEach(face => this.addQuad.apply(this, face.map(d => dots[d])));
var cube = new Form3D.Object.Cube( {
  shader: scene.shader(VSHADER, FSHADER),
  size: 45,
  eachVertex: {
    a_Color: function( p,x ){
      return [
        [1.0, 0.0, 0.5],
        [1.0, 0.5, 0.0],
        [0.0, 1.0, 0.5],
        [0.5, 1.0, 0.0],
        [0.5, 0.0, 1.0],
        [0.0, 0.5, 1.0],
        [0.0, 0.5, 1.0],
      ][(p+x) / 6 | 0]
    }
  }
} );

Sphere

Sphere is a much more complex object. Let's decompose steps of generating its geometry:

  1. Define top and one bottom point.
  2. Create N rings with M points in each of them
  3. Connect rings points together with triangles

Top and bottom

This is the simplest part. Sphere would be defined by its center location and radius, so we would just subtract radius from the y coordinate of the center and get the top point, for bottom — we would do the opposite operation and add the radius.

var background = 0x001133; var pointColor = 0x00ffff; rect(0,0,w,h, background); var X = 64; var Y = 64; var R = 60; circle( X, Y, R, 0x007777 ); var center = new Point(X, Y); var top = new Point(X, Y-R); var bottom = new Point(X, Y+R); circle( top.x, top.y, 2, pointColor ); circle( bottom.x, bottom.y, 2, pointColor ); circle( center.x, center.y, 2, pointColor );

N rings

We need to sparse rings not evenly, but in the sin manner

var background = 0x001133; var pointColor = 0x00ffff; rect(0,0,w,h, background); var X = 64, Y = 64, R = 60; var M = 7; circle( X, Y, R, 0x007777 ); var center = new Point(X, Y); var top = new Point(X, Y-R); var lastRing; for(var i = 0; i < M; i++) { var ringY = cos( i * PI / (M-1) ) * R; var ringDX = sqrt( R * R - ringY * ringY ); var leftPoint = new Point(X - ringDX, ringY + Y); var rightPoint = new Point(X + ringDX, ringY + Y); line( leftPoint, rightPoint, 0x0ff034 ); if(lastRing) { line( lastRing[ 0 ], leftPoint, 0xcfa034 ); line( lastRing[ 1 ], rightPoint, 0xcfa034 ); } lastRing = [leftPoint, rightPoint] }

Let me show how the evenly split would look like this:

Top and bottom cones are awful.

M points in each ring

To place M points on a circle — just iterate the angle from zero to 360.

Drawing visible part of the sphere with N points on M slices

var background = 0x001133; var pointColor = 0x00ffff; rect(0,0,w,h, background); var X = 128, Y = 128, R = 120; var M = 7, N = 8; circle( X, Y, R, 0x007777 ); var center = new Point(X, Y); var top = new Point(X, Y-R); var lastRing; for(var i = 0; i < M; i++) { var ringY = cos( i * PI / (M-1) ) * R; var ringDX = sqrt( R * R - ringY * ringY ); var leftPoint = new Point(X - ringDX, ringY + Y); var rightPoint = new Point(X + ringDX, ringY + Y); var ring = []; for(var j = 0; j <= N/2; j++){ var pointAngle = j/N*PI*2; var point = new Point( X + cos(pointAngle)*ringDX, Y+ringY + sin(pointAngle)*ringDX/3 ); if(ring.length > 0) line( point, ring[ring.length - 1], 0x0ff034 ); ring.push(point); } if(lastRing) { line( lastRing[ 0 ], leftPoint, 0xcfa034 ); line( lastRing[ 1 ], rightPoint, 0xcfa034 ); } lastRing = [leftPoint, rightPoint] }

For covering top and bottom rings we would connect corresponding points with ring points with trianles like: [topPoint, ringPoint, previousRingPoint]

For the next layer we would create triangles by connecting points: [lastRing[i], lastRing[i-1], currentRing[i-1]], and [lastRing[i], currentRing[i-1], currentRing[i]]:

var background = 0x001133; var pointColor = 0x00ffff; rect(0,0,w,h, background); var X = 128, Y = 128, R = 120; var M = 7, N = 8; circle( X, Y, R, 0x007777 ); var center = new Point(X, Y); var top = new Point(X, Y-R); var lastRing; for(var i = 1; i < 4; i++) { var ringY = -cos( i * PI / (M-1) ) * R; var ringDX = sqrt( R * R - ringY * ringY ); var leftPoint = new Point(X - ringDX, ringY + Y); var rightPoint = new Point(X + ringDX, ringY + Y); var ring = []; for(var j = 0; j <= N/2; j++){ var pointAngle = j/N*PI*2; var point = new Point( X + cos(pointAngle)*ringDX |0, Y+ringY + sin(pointAngle)*ringDX/3 |0 ); ring.push(point); if(ring.length > 0 && lastRing && j > 0){ triangle( lastRing[j], lastRing[j-1], ring[j-1], i*j*0x341721 ); triangle( lastRing[j], ring[j - 1], ring[j], (i+j)*0x641721 ); line( lastRing[j], lastRing[j-1], 0x0ff034 ); line( lastRing[j-1], ring[j-1] , 0x0ff034 ); line( lastRing[j], ring[j-1], 0x0ff034 ); line( ring[j-1], ring[j], 0x0ff034 ); line( lastRing[j], ring[j], 0x0ff034 ); } } lastRing = ring }

With all above calculations we can finally build the 3D sphere! M = 30 N = 30

%0A%3Cdiv%20class%3D%22flex%22%3E%0A%3Ccanvas%20id%3D%22fourth%22%20width%3D%22320%22%20height%3D%22320%22%3E%3C/canvas%3E%0A%3Cscript%20src%3D%22/3d/eight.fns.js%22%3E%3C/script%3E%0A%3Cscript%20src%3D%22/3d/fourth.js%22%3E%3C/script%3E%0A%3C/div%3E%0A%0A%0A%3Cdiv%20class%3D%22flex%22%3E%0A%3Ccanvas%20id%3D%22fifth%22%20width%3D%22320%22%20height%3D%22320%22%3E%3C/canvas%3E%0A%3Cscript%20src%3D%22/3d/fifth.js%22%3E%3C/script%3E%0A%3C/div%3E%0A%0A//%20DRAWING%20radial%20texture%20from%206%20images%0A%3Cdiv%20class%3D%22flex%22%3E%0A%3Ccanvas%20id%3D%22sixs%22%20width%3D%22640%22%20height%3D%22320%22%3E%3C/canvas%3E%0A%3Cscript%20src%3D%22/3d/sixs.js%22%3E%3C/script%3E%0A%3C/div%3E%0A%0A//%20DRAWING%20heatmap%20in%20the%20pool%0A%3Cdiv%20class%3D%22flex%22%3E%0A%20%20%3Ccanvas%20id%3D%22eight%22%20width%3D%22640%22%20height%3D%22320%22%3E%3C/canvas%3E%0A%20%20%3Cscript%20src%3D%22/3d/dat.js%22%3E%3C/script%3E%0A%20%20%3Cscript%20src%3D%22/3d/eight.fns.js%22%3E%3C/script%3E%0A%20%20%3Cscript%20src%3D%22/3d/eight.js%22%3E%3C/script%3E%0A%3C/div%3E%0A%0A//%20DRAWING%20just%20the%20pool%0A%0A%3Cdiv%20class%3D%22flex%22%3E%0A%20%20%3Ccanvas%20id%3D%22eight%22%20width%3D%22640%22%20height%3D%22320%22%3E%3C/canvas%3E%0A%20%20%3Cscript%20src%3D%22/3d/dat.js%22%3E%3C/script%3E%0A%20%20%3Cscript%20src%3D%22/3d/eight.fns.js%22%3E%3C/script%3E%0A%0A%20%20%3Cscript%20src%3D%22/3d/nine.js%22%3E%3C/script%3E%0A%0A%3C/div%3E%0A%0A