---
date: Sunday, August 10^th^, 2025
title: Blowing up
---

*Blowing up* is a concept from algebraic geometry. Blowing up turns a
neighbourhood in a Euclidean space into a more complex manifold (called
the 'blow up' of the original neighbourhood). The transformation
preserves the Euclidean geometry in most parts of the neighbourhood, but
expands a particular subspace to have extra dimensions. This can help to
*resolve* singularities, with applications in singular learning theory.

The simplest non-trivial example is blowing up a point in a
2-dimensional neighbourhood (say, a disk). Unfortunately, even this
example is pretty hard to visualise---at least for someone like me who
doesn't think natively in terms of projective geometry and quotients.

Most attempts to depict this blow up focus on showing a single part of
the resulting 2-dimensional manifold at a time. However, it's actually
possible to represent the entirety of the manifold in three dimensions.
In fact, it has the topology of the well-known Möbius strip.

Below is a *Three.js* visualisation of the operation, and a brief
discussion of some of its properties.

## Visualisation

We start with a two-dimensional disk. We want to blow up the central
point into a one-dimensional space representing all of the different
lines along which we could approach that point. We want to somehow do
this without disturbing the topology of the rest of the disk.

How is this possible? Drag the slider to see:

`<input type="range"
  id="blowUpSlider"
  min="0"
  max="100"
  step="1"
  value="0"
  style="width: 100%;"
>`{=html}

Click and drag on the manifold to rotate it:

::: {#blowUp style="width: 100%; height: 56ch;"}
:::

Observe how the resulting manifold looks like a Möbius strip. The
central point is replaced by a circle (black line). A Möbius strip is
the perfect manifold to preserve the topology of the rest of the disk,
because it still allows us to walk 'around' the 'centre' by completing
two revolutions around the strip. The coloured boundary in the
visualisation represents one such circuit.

## Exercises

I think this visualisation could be improved in several ways.

1.  The main focus of this visualisation is the start point (the disk)
    and the end point (the blow up). The transition between them is
    imperfect, with some creasing of the manifold and some parts of the
    space passing through others. I don't think it's possible to
    continuously deform the original neighbourhood into the blown up
    manifold without these kinds of artefacts, because the two surfaces
    have different numbers of holes. However, it may be possible to
    interpolate between the two manifolds in a way that is more visually
    appealing.

2.  It would be cool if there were a version of this construction that
    showed the original neighbourhood and the blow up side by side
    (spinning, say), and made it so that if you highlight a point (or
    line) on one of the surfaces, it shows the corresponding point (or
    line) on the other. For points, this would more clearly show that
    the white sections are homeomorphic. For lines, this would more
    clearly show the motivation for the blow up, which is to expand the
    central point into a full projective space (here a circle) to allow
    tracking different lines of approach as different points.

3.  The blow up is a step that is used during the resolution of
    singularities. If there were a curve in the neighbourhood with a
    singularity at the point, then blowing up the point would normally
    lead to a curve with a less severe singularity in the blown up
    space. It would be cool to extend this visualisation to contain an
    example curve with a singularity, before and after the blow up.

4.  This blow up can be defined in several steps. First, take the
    product of the neighbourhood of the plane (a disc) and
    one-dimensional projective space (a circle). This product can be
    visualised as a solid donut. Next, in each copy of the disc along
    projective space (each slice of the donut), discard everything
    except the subset of points along a line through the origin with the
    same direction as represented by that part of projective space.
    (This is how I finally realised that the resulting space was the
    above Möbius strip.) It would be cool to create a visualisation that
    shows this construction, step by step.

```{=html}
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
```
```{=html}
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
```
```{=html}
<script>
  // constants
  let blowUpProportion = 0.0;
  const radius = 75;
  const lineCount = 512;
  
  // options
  const blowUpSlider = document.getElementById('blowUpSlider');
  blowUpProportion = parseFloat(blowUpSlider.value) / 100.0;
  blowUpSlider.addEventListener('input', function() {
    blowUpProportion = parseFloat(this.value) / 100.0;
    update();
  });

  // create renderer and fit to DOM
  const container = document.getElementById("blowUp");
  const CANVAS_WIDTH = container.clientWidth;
  const CANVAS_HEIGHT = container.clientHeight;
  const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true,
  });
  renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);
  renderer.setClearColor(0xeeeeee, 0);
  container.appendChild(renderer.domElement);

  // scene configuration
  const scene = new THREE.Scene();
  // lighting
  scene.add(new THREE.AmbientLight(0x404040));
  scene.add(new THREE.HemisphereLight(0xffffbb, 0x404040, 1));
  // skybox
  const loader = new THREE.CubeTextureLoader();
  const texture = loader.load([
    '/blowing-up/skybox-rt.png',
    '/blowing-up/skybox-lf.png',
    '/blowing-up/skybox-up.png',
    '/blowing-up/skybox-dn.png',
    '/blowing-up/skybox-fd.png',
    '/blowing-up/skybox-bk.png',
  ]);
  scene.background = texture;
  // camera
  const aspect = CANVAS_WIDTH / CANVAS_HEIGHT;
  const camera = new THREE.PerspectiveCamera(45, aspect, 1, 2000);
  camera.position.x = 0;
  camera.position.y = 200;
  camera.position.z = -200;
  camera.lookAt(scene.position);
  // camera controls
  const controls = new THREE.OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.075;
  controls.enableZoom = false;
  controls.screenSpacePanning = true;
  controls.autoRotate = false;
  controls.autoRotateSpeed = 0.2;
  controls.update();

  // create the objects
  const geometryLine = new THREE.BoxGeometry(100,2,2);
  const materialLine = new THREE.MeshPhongMaterial({color: 0xffffff});
  const geometryPoint = new THREE.BoxGeometry(3,3,3);
  const materialPoint = new THREE.MeshPhongMaterial({color: 0x000000});
  const lines = [];
  let i = 0;
  for (i; i < lineCount; i++) {
    // line
    const line = new THREE.Mesh(geometryLine, materialLine);
    scene.add(line);
    // center point
    const point = new THREE.Mesh(geometryPoint, materialPoint);
    line.add(point);
    // endpoints
    let j = 0;
    for (j; j < 2; j++) {
      const color = new THREE.Color().setHSL(
          i / lineCount * 0.5 + j * 0.5,
          1.0,
          0.5,
      );
      const materialEndpoint = new THREE.MeshPhongMaterial({color: color});
      const endpoint = new THREE.Mesh(geometryPoint, materialEndpoint);
      endpoint.position.x = 50 - j * 100;
      line.add(endpoint);
    }
    // store for updating later
    lines.push(line);
  }

  // when blowup proportion changes, recalculate position
  const update = () => {
    const b = blowUpProportion;
    let i = 0;
    for (i; i < lineCount; i++) {
      // position in original neighbourhood
      const px0 = 0.0;
      const py0 = 0.0;
      const pz0 = 0.0;
      const rx0 = 0.0;
      const ry0 = Math.PI/2 - Math.PI * i / lineCount;
      const rz0 = 0.0;
      
      // position in blown up manifold
      const px1 = radius * Math.cos(2 * Math.PI * i / lineCount);
      const py1 = 0.0;
      const pz1 = radius * Math.sin(2 * Math.PI * i / lineCount);
      const rx1 = 0.0;
      const ry1 = Math.PI / 2 - Math.PI * i / lineCount;
      const rz1 = Math.PI / 2 - Math.PI * i / lineCount;
      
      // move to interpolated position
      lines[i].position.set(
        px0 * (1-b) + px1 * b,
        py0 * (1-b) + py1 * b,
        pz0 * (1-b) + pz1 * b,
      );
      lines[i].rotation.set(
        rx0 * (1-b) + rx1 * b,
        ry0 * (1-b) + ry1 * b,
        rz0 * (1-b) + rz1 * b,
      );
    }
  };
  update();

  const animate = () => {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
  };
  animate();
</script>
```
