Bitbanging a Procedural Sphere: Precision to Perfection

Precision, Performance, and Perfection Procedurally drawing a sphere in WebGL is fascinating because, despite the underlying geometry remaining mathematically consistent, implementations diverge widely based on load constraints and polygon counts. This contrast is a paradox in itself—after all, a perfect sphere has no edges. Procedurally generating high-quality 3D geometry for the web demands navigating a fine balance between visual elegance, computational efficiency, and seamless user experience. While traditional methods often depend on heavy libraries or complex pre-processing steps, the innovative "bitbanging" approach presented here revolutionizes this paradigm with its elegant simplicity, exceptional smoothness, and visually superior outcomes. Why "Bitbanging"? "Bitbanging" typically refers to precisely manipulating hardware signals using literal bit-by-bit instructions for fine-grained, optimized control. Similarly, our procedural sphere generation meticulously handcrafts vertex positions and indices, providing direct, granular control over geometry creation—earning its metaphorical namesake as a masterfully optimized WebGL technique. For us, it means designing our functions around the 4 x 4 matrices function mat4Perspective(out, fovy, aspect, near, far) { const f = 1 / Math.tan(fovy / 2); out[0] = f/aspect; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = (far+near)/(near-far); out[11] = -1; out[12] = 0; out[13] = 0; out[14] = (2*far*near)/(near-far); out[15] = 0; } function mat4Identity(m) { m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0; m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; } function mat4Translate(out, x, y, z) { out[12] += out[0]*x + out[4]*y + out[8]*z; out[13] += out[1]*x + out[5]*y + out[9]*z; out[14] += out[2]*x + out[6]*y + out[10]*z; out[15] += out[3]*x + out[7]*y + out[11]*z; } The Sphere Revolution Contrary to conventional procedural spheres that rely on bulky mathematical computations and extensive dependencies, our approach streamlines the process through direct manipulation of vertices and indices. By harnessing optimized looping techniques, carefully executed trigonometric computations, and straightforward typed arrays, this method achieves remarkable efficiency and visual fidelity. Highlights of our approach include: Ultra-light Footprint: Pure JavaScript and WebGL with minimal dependencies, resulting in extraordinarily lightweight implementations. Visually Stunning Smoothness: Precisely calculated vertex placements based on carefully incremented latitude and longitude angles eliminate polygonal artifacts and seams, delivering seamless, aesthetically pleasing spheres even at minimal vertex counts. Direct Control of Geometry: Separate horizontal and vertical index buffers facilitate crisp line rendering, allowing detailed customization of sphere wireframes and ensuring impeccable visual clarity. Efficiency Redefined This method generates spheres by directly computing vertex positions through optimized trigonometric functions, seamlessly integrating these positions into WebGL buffers. This eliminates the overhead associated with traditional geometry libraries, significantly reducing memory usage and CPU demands. Unparalleled Precision Typical procedural spheres suffer from visible seams, uneven vertex distributions, and jagged artifacts. In contrast, our carefully optimized latitude and longitude increments produce remarkably uniform and visually pleasing vertex spacing, ensuring consistently smooth appearances from every angle. Clean, Powerful Code At the core of this implementation lies concise, elegant code. Minimalist vertex and fragment shaders offer straightforward pathways for customization or expansion. Geometry generation remains lean, clear, and maintainable—qualities rarely found in procedural geometry implementations. Practical Impact Beyond theoretical elegance, this optimized method holds tangible real-world value, particularly for resource-constrained environments such as real-time visualizations, mobile 3D apps, and VR experiences. Its superior efficiency translates into smoother frame rates, rapid load times, and dramatically enhanced user experiences. A New Standard in Procedural Generation With its meticulous precision, unmatched smoothness, and exceptional efficiency, the "bitbanging" procedural sphere method presented here doesn't merely improve upon existing approaches—it sets an entirely new benchmark for WebGL geometry implementations. This isn't just another procedural sphere; it's arguably the definitive procedural sphere implementatio

May 1, 2025 - 16:30
 0
Bitbanging a Procedural Sphere: Precision to Perfection

Precision, Performance, and Perfection

Procedurally drawing a sphere in WebGL is fascinating because, despite the underlying geometry remaining mathematically consistent, implementations diverge widely based on load constraints and polygon counts. This contrast is a paradox in itself—after all, a perfect sphere has no edges.

Procedurally generating high-quality 3D geometry for the web demands navigating a fine balance between visual elegance, computational efficiency, and seamless user experience. While traditional methods often depend on heavy libraries or complex pre-processing steps, the innovative "bitbanging" approach presented here revolutionizes this paradigm with its elegant simplicity, exceptional smoothness, and visually superior outcomes.

Why "Bitbanging"?

"Bitbanging" typically refers to precisely manipulating hardware signals using literal bit-by-bit instructions for fine-grained, optimized control. Similarly, our procedural sphere generation meticulously handcrafts vertex positions and indices, providing direct, granular control over geometry creation—earning its metaphorical namesake as a masterfully optimized WebGL technique.

For us, it means designing our functions around the 4 x 4 matrices

function mat4Perspective(out, fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy / 2);
    out[0]  = f/aspect; out[1]  = 0; out[2]  = 0;                           out[3]  = 0;
    out[4]  = 0;        out[5]  = f; out[6]  = 0;                           out[7]  = 0;
    out[8]  = 0;        out[9]  = 0; out[10] = (far+near)/(near-far);       out[11] = -1;
    out[12] = 0;        out[13] = 0; out[14] = (2*far*near)/(near-far);     out[15] = 0;
  }
  function mat4Identity(m) {
    m[0]  = 1; m[1]  = 0; m[2]  = 0;  m[3]  = 0;
    m[4]  = 0; m[5]  = 1; m[6]  = 0;  m[7]  = 0;
    m[8]  = 0; m[9]  = 0; m[10] = 1;  m[11] = 0;
    m[12] = 0; m[13] = 0; m[14] = 0;  m[15] = 1;
  }
  function mat4Translate(out, x, y, z) {
    out[12] += out[0]*x + out[4]*y + out[8]*z;
    out[13] += out[1]*x + out[5]*y + out[9]*z;
    out[14] += out[2]*x + out[6]*y + out[10]*z;
    out[15] += out[3]*x + out[7]*y + out[11]*z;
  }

The Sphere Revolution

Contrary to conventional procedural spheres that rely on bulky mathematical computations and extensive dependencies, our approach streamlines the process through direct manipulation of vertices and indices. By harnessing optimized looping techniques, carefully executed trigonometric computations, and straightforward typed arrays, this method achieves remarkable efficiency and visual fidelity.

Highlights of our approach include:

  • Ultra-light Footprint: Pure JavaScript and WebGL with minimal dependencies, resulting in extraordinarily lightweight implementations.
  • Visually Stunning Smoothness: Precisely calculated vertex placements based on carefully incremented latitude and longitude angles eliminate polygonal artifacts and seams, delivering seamless, aesthetically pleasing spheres even at minimal vertex counts.
  • Direct Control of Geometry: Separate horizontal and vertical index buffers facilitate crisp line rendering, allowing detailed customization of sphere wireframes and ensuring impeccable visual clarity.

Efficiency Redefined

This method generates spheres by directly computing vertex positions through optimized trigonometric functions, seamlessly integrating these positions into WebGL buffers. This eliminates the overhead associated with traditional geometry libraries, significantly reducing memory usage and CPU demands.

Unparalleled Precision

Typical procedural spheres suffer from visible seams, uneven vertex distributions, and jagged artifacts. In contrast, our carefully optimized latitude and longitude increments produce remarkably uniform and visually pleasing vertex spacing, ensuring consistently smooth appearances from every angle.

Clean, Powerful Code

At the core of this implementation lies concise, elegant code. Minimalist vertex and fragment shaders offer straightforward pathways for customization or expansion. Geometry generation remains lean, clear, and maintainable—qualities rarely found in procedural geometry implementations.

Practical Impact

Beyond theoretical elegance, this optimized method holds tangible real-world value, particularly for resource-constrained environments such as real-time visualizations, mobile 3D apps, and VR experiences. Its superior efficiency translates into smoother frame rates, rapid load times, and dramatically enhanced user experiences.

A New Standard in Procedural Generation

With its meticulous precision, unmatched smoothness, and exceptional efficiency, the "bitbanging" procedural sphere method presented here doesn't merely improve upon existing approaches—it sets an entirely new benchmark for WebGL geometry implementations.

This isn't just another procedural sphere; it's arguably the definitive procedural sphere implementation available on the web today.

Read the full write up at https://chahla.net/posts?pid=1743700263180


  const latBands  = 20;
  const lonBands  = 20;
  const radius    = 1.0;


(function() {


  // -- Get a WebGL 1 context --
  const canvas = document.getElementById("glcanvas");
  const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  if (!gl) {
    alert("Unable to initialize WebGL.");
    return;
  }

  // -- Vertex shader (positions only) --
  const vsSource = `
    attribute vec4 aPosition;
    uniform mat4 uProjectionMatrix;
    uniform mat4 uModelViewMatrix;

    void main(void) {
      gl_Position = uProjectionMatrix * uModelViewMatrix * aPosition;
    }
  `;

  // -- Fragment shader (simple solid color) --
  const fsSource = `
    precision mediump float;
    uniform vec4 uColor;
    void main(void) {
      gl_FragColor = uColor;
    }
  `;

  // -- Compile & link shaders --
  function createShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error("Error compiling shader:", gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
      return null;
    }
    return shader;
  }

  const vertexShader   = createShader(gl, gl.VERTEX_SHADER,   vsSource);
  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);

  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    console.error("Shader program failed to link:", gl.getProgramInfoLog(shaderProgram));
    return;
  }

  // -- Get locations --
  const aPositionLoc      = gl.getAttribLocation(shaderProgram, 'aPosition');
  const uProjectionMatrix = gl.getUniformLocation(shaderProgram, 'uProjectionMatrix');
  const uModelViewMatrix  = gl.getUniformLocation(shaderProgram, 'uModelViewMatrix');
  const uColorLoc         = gl.getUniformLocation(shaderProgram, 'uColor');

  // -- Create sphere geometry (split indices into horizontal vs vertical) --
  function createWireSphereData(latBands, lonBands, radius) {
    const positions = [];
    const horizontalIndices = [];
    const verticalIndices   = [];

    // Generate positions (vertex array)
    for (let lat = 0; lat <= latBands; lat++) {
      const theta = (lat * Math.PI) / latBands; 
      const sinTheta = Math.sin(theta);
      const cosTheta = Math.cos(theta);

      for (let lon = 0; lon <= lonBands; lon++) {
        const phi = (lon * 2.0 * Math.PI) / lonBands;
        const sinPhi = Math.sin(phi);
        const cosPhi = Math.cos(phi);

        const x = radius * cosPhi * sinTheta;
        const y = radius * cosTheta;
        const z = radius * sinPhi * sinTheta;
        positions.push(x, y, z);
      }
    }

    // The number of vertices in each "row" (one latitude)
    const vertsPerRow = lonBands + 1;

    // Build horizontal line indices (connect each row)
    for (let lat = 0; lat <= latBands; lat++) {
      const rowStart = lat * vertsPerRow;
      for (let lon = 0; lon < lonBands; lon++) {
        horizontalIndices.push(rowStart + lon, rowStart + lon + 1);
      }
    }

    // Build vertical line indices (connect columns across latitudes)
    for (let lon = 0; lon <= lonBands; lon++) {
      for (let lat = 0; lat < latBands; lat++) {
        const first  = lat * vertsPerRow + lon;
        const second = first + vertsPerRow;
        verticalIndices.push(first, second);
      }
    }

    return {
      positions: new Float32Array(positions),
      horizontal: new Uint16Array(horizontalIndices),
      vertical:   new Uint16Array(verticalIndices),
    };
  }


  const sphereData = createWireSphereData(latBands, lonBands, radius);

  // -- Create a single position buffer --
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, sphereData.positions, gl.STATIC_DRAW);

  // -- Create two index buffers: one for horizontal lines, one for vertical --
  const horizontalIndexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, horizontalIndexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sphereData.horizontal, gl.STATIC_DRAW);

  const verticalIndexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, verticalIndexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sphereData.vertical, gl.STATIC_DRAW);

  // -- Simple matrix utilities --
  function mat4Perspective(out, fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy / 2);
    out[0]  = f/aspect; out[1]  = 0; out[2]  = 0;                           out[3]  = 0;
    out[4]  = 0;        out[5]  = f; out[6]  = 0;                           out[7]  = 0;
    out[8]  = 0;        out[9]  = 0; out[10] = (far+near)/(near-far);       out[11] = -1;
    out[12] = 0;        out[13] = 0; out[14] = (2*far*near)/(near-far);     out[15] = 0;
  }
  function mat4Identity(m) {
    m[0]  = 1; m[1]  = 0; m[2]  = 0;  m[3]  = 0;
    m[4]  = 0; m[5]  = 1; m[6]  = 0;  m[7]  = 0;
    m[8]  = 0; m[9]  = 0; m[10] = 1;  m[11] = 0;
    m[12] = 0; m[13] = 0; m[14] = 0;  m[15] = 1;
  }
  function mat4Translate(out, x, y, z) {
    out[12] += out[0]*x + out[4]*y + out[8]*z;
    out[13] += out[1]*x + out[5]*y + out[9]*z;
    out[14] += out[2]*x + out[6]*y + out[10]*z;
    out[15] += out[3]*x + out[7]*y + out[11]*z;
  }

  // -- Setup projection & modelView matrix --
  const projectionMatrix = new Float32Array(16);
  mat4Perspective(projectionMatrix, 45 * Math.PI / 180, canvas.width / canvas.height, 0.1, 100);

  const modelViewMatrix = new Float32Array(16);
  mat4Identity(modelViewMatrix);
  mat4Translate(modelViewMatrix, 0, 0, -4); // move sphere away from camera

  // -- Render function --
  function drawScene() {
    gl.clearColor(0.0, 0.0, 0.0, 0.0)
    gl.clearDepth(1);
    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.useProgram(shaderProgram);

    // Bind position buffer & set up 'aPosition'
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.enableVertexAttribArray(aPositionLoc);
    gl.vertexAttribPointer(aPositionLoc, 3, gl.FLOAT, false, 0, 0);

    // Set matrices
    gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix);
    gl.uniformMatrix4fv(uModelViewMatrix,  false, modelViewMatrix);

    // -- 1) Draw horizontal lines in RED --
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, horizontalIndexBuffer);
    gl.uniform4f(uColorLoc, 1.0, 0.0, 0.0, 1.0);
    gl.drawElements(gl.LINES, sphereData.horizontal.length, gl.UNSIGNED_SHORT, 0);

    // -- 2) Draw vertical lines in BLUE --
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, verticalIndexBuffer);
    gl.uniform4f(uColorLoc, 0.0, 0.0, 1.0, 1.0);
    gl.drawElements(gl.LINES, sphereData.vertical.length, gl.UNSIGNED_SHORT, 0);
  }

  drawScene();
})();