GNSP – the swivel rendering

NFT

This fourth article (in a series of 7 articles) reveals the technique used to render the Nano swivel.

Timeline:

The collection is browsable on https://greweb.me/gnsp

OpenSea: https://opensea.io/collection/gnsp


This is very similar to the "nano screen" in that we will use a Canvas 2D as a texture to the GLSL shader and it will be projected on the swivel surface.

The Canvas 2D generative swivel code

A canvas 2D of 1200 x 400 pixels is used to draw the swivel engraved texture. We use it to possibly display a text and draw some "plotted lines".

async function metal(word, swivelPlotted) {
  const w = 1200;
  const h = 400;
  const canvas = document.createElement("canvas");
  canvas.width = w;
  canvas.height = h;
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = "#000";
  ctx.fillRect(0, 0, w, h);
  ctx.fillStyle = "#fff";
  let font = "Arial";
  // ...plot things...
  // ...draw text...
  return canvas;
}

Here is what the texture of #472 looks like:

and here is the final result in the raymarched object:

"plotted" lines

The swivel can sometimes be "laser plotted" which is a reference to the "plotting" work I've been doing last year. This "plotting" is a simple stacking of lines which create some mountains effect.

In the following code, swivelPlotted is defined when plotting is active and is an array of random values.

if (swivelPlotted) {
  ctx.strokeStyle = "#fff";
  ctx.lineWidth = 3;
  const octaveCount = Math.floor(3 + 6 * swivelPlotted[4]);
  const perlin = generatePerlinNoise(w, h, {
    octaveCount,
    amplitude: swivelPlotted[3],
    persistence: 0.2,
  });
  let pad = [50, 20];
  let amp = 120 * mix(swivelPlotted[0], swivelPlotted[1], swivelPlotted[2]);
  let incr = Math.floor(3 + 50 * swivelPlotted[1]);
  if (incr < 15 || (octaveCount < 4 && amp > 40)) {
    ctx.fillStyle = "#000";
    font = "Arial Black";
  }
  let heights = Array(w).fill(h);
  for (let y = h - pad[1]; y > pad[1] + amp; y -= incr) {
    ctx.beginPath();
    let up = true;
    for (let x = pad[0]; x < w - pad[0]; x++) {
      let dy = amp * perlin[y * w + x];
      let yy = y - dy;
      let m = heights[x];
      if (yy > m) {
        up = true;
        continue;
      }
      heights[x] = yy;
      if (up) {
        ctx.moveTo(x, yy);
        up = false;
      } else {
        ctx.lineTo(x, yy);
      }
    }
    ctx.stroke();
  }
}

text

As shown at the beginning, we can also simply have a text on the swivel. Sometimes it is combined with the plotting effect.

if (word) {
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  const lines = word.split("\n");
  const sz = Math.floor(
    20 + 1200 / (3 + Math.max(...lines.map((l) => l.length)))
  );
  ctx.font = sz + "px " + font;
  lines.forEach((line, i) => // multi line
    ctx.fillText(
      line,
      w / 2,
      Math.round(h / 2 + 1.2 * sz * (i + 0.5 - lines.length / 2))
    )
  );
}

sticker text

We can also use emoji instead of text.

Raymarching shader integration

The swivel metal texture is rendered using a brownian noise. (like explained in https://thebookofshaders.com/13/)

Contextually to the raymarching, we have the local coordinate of the swivel (including its rotation), which allows us to do a texture lookup on the swivel text texture. All of this information (noise and texture) is encoded in the material value (a float). It is possible to encode it on one value like this because it's a grayscale value.

noiseMetal = fbm(vec2(40.0, 1000.) * p.xy);
vec2 coord = fract(vec2(1.0, -3.0) * p.xy + vec2(0.5));
vec4 mt = texture2D(metalText, coord);
float t = mix(0., grayscale(mt.rgb),mt.a * step(p.z, 0.) * step(p.x, -0.5) * step(abs(p.y), 0.16));
float swivelM = 2.2 + t;
s = opU(s, HIT(swivel, swivelM));

sticker emoji

For the sticker that appear on the swivel, we add to the distance a disc and we use a side effect vec3 sticker_color variable that is later used in the shade function. The position of the sticker is randomly placed.

${
  !opts.sticker
    ? ""
    : `
vec2 q = p.xy + vec2(${opts.stickerPosX.toFixed(2)}, ${opts.stickerPosY.toFixed(2)});
float sticker_size = 0.15;
float sticker_border = 0.01;
coord = fract(vec2(.5,-.5) * q / sticker_size - 0.5);
vec4 v = texture2D(stickerText, coord);
sticker_color = mix(vec3(1.), v.rgb, v.a);
float l = length(q.xy)-sticker_size;
s = opU(s, HIT(max(
  abs(p.z + 0.13)-0.005,
  l-sticker_border
), 4.2 - step(0.0, l)));
`
}
As a generative plotter artist, I use code to generate art (creative coding) and physically create it with pen plotters, which is itself a generative process – each physical plot is a unique variant. I love dualities, like digital vs analog physical, abstract vs figurative, orthogonal vs polar, photo vs noise,...