ask:

Create a reactive 3D mesh.

Use p5.js and WebGL. Use the version of vertex() that includes the z parameter. Use both position and color attributes. Make your mesh react to user input. This can be using the mouse, keyboard, microphone, or any other input you can think of.

thought:

every enclosed body is a mesh, with vertices inside. shape is affected by the topology.

an enclosed body can be thought of as a group of similar colours.

output:


i tried programming something very complex, and it got too complicated.

//untitled; arjun; jan, 2026.
 
/*
ask: 
make a reactive mesh, with vertices that use the z-parameter. use position & colour. 
*/
 
/*
thought: 
every enclosed body is a mesh, with vertices inside. shape is affected by the topology. 
 
an enclosed body can be thought of as a group of similar colours. 
*/
 
let cam;
 
let vertices = [];
 
let col_similarity_threshold = 50;
 
function setup() {
  cam = createCapture(VIDEO, canv_to_asp);
  cam.hide();
}
 
//helper to convert canvas to aspect ratio. runs once.
function canv_to_asp() {
  let asp_ratio = cam.height / cam.width;
 
  let wh = windowWidth * asp_ratio;
 
  createCanvas(windowWidth, wh, WEBGL);
}
 
function draw() {
  background(0);
  push();
  //first flip
  scale(-1, 1);
  translate(-width, 0);
 
  translate(width / 2, -height / 2); //bring back to the center.
 
  assign_vertices();
 
  image(cam, 0, 0, width, height);
  pop();
}
 
let groups = [];
 
function assign_vertices() {
  cam.loadPixels();
  //now we get a cam.pixels array with rgba values for each pixel.
 
  /*
  algorithm: 
  - we go through every single x, y position on the screen. 
  - for each pixel, look at its neighbours (8).
  - if the colour of the neighbour is similar (under a certain threshold), accumulate them into an object. 
  - if the neighbour has been accumulated, don't look at it again. 
  */
 
  for (let x = 0; x < cam.width; x++) {
    for (let y = 0; y < cam.height; y++) {
      //we go through every single pixel.
      let n = get_pixel_index(x, y);
 
      //find neighbours in the pixel array:
      let neighbours = get_neighbours(x, y);
 
      //the above returns index values in cam.pixels array with neighbours.
 
      // for each neighbour, see if the rgba is under the threshold. if so, accumulate them into a group.
      for (let i = 0; i < neighbours.length; i++) {
        let ni = neighbours[i];
 
        // red
        if (cam.pixels[ni] < cam.pixels[n] - col_similarity_threshold) continue;
        if (cam.pixels[ni] > cam.pixels[n] + col_similarity_threshold) continue;
 
        // green
        if (cam.pixels[ni + 1] < cam.pixels[n + 1] - col_similarity_threshold) continue;
        if (cam.pixels[ni + 1] > cam.pixels[n + 1] + col_similarity_threshold) continue;
 
        // blue
        if (cam.pixels[ni + 2] < cam.pixels[n + 2] - col_similarity_threshold) continue;
        if (cam.pixels[ni + 2] > cam.pixels[n + 2] + col_similarity_threshold) continue;
 
        // neighbour is similar
        groups.push(ni);
      }
    }
  }
}
 
//helper to convert x,y coordinates to pixels index.
function get_pixel_index(x, y) {
  return (y * cam.width + x) * 4;
}
 
function get_neighbours(x, y) {
  let neighbours = [];
  const possible_neighbours = [
    [x - 1, y - 1], // top-left
    [x, y - 1], // top
    [x + 1, y - 1], // top-right
 
    [x - 1, y], // left
    [x + 1, y], // right
 
    [x - 1, y + 1], // bottom-left
    [x, y + 1], // bottom
    [x + 1, y + 1], // bottom-right
  ];
 
  for (const [dx, dy] of possible_neighbours) {
    if (dx >= 0 && dx < cam.width && dy >= 0 && dy < cam.height) {
      neighbours.push(get_pixel_index(dx, dy));
    }
  }
 
  return neighbours;
}

i decided to break it down into smaller chunks.

when i did that, i realized that the computer was not able to convert all of the pixels and then render them as vertices.


i tried a bunch of approaches that failed. the algorithm was too complex for me to work out.

then i did some gpt stuff which got it working, but i didn’t understand what the hell was going on. i tried asking it to explain me, but i didn’t get it.

so i decided to write it from scratch.

learnt about using abs for comparison.

// we use abs to get rid of writing if > and if <. abs maps it to a number line.
        if (abs(neighbour_hue - studied_hue) < similarity_threshold) {

made this:

//untitled; arjun; month, 2026.
 
/*
ask: 
make a reactive mesh, with vertices that use the z-parameter. use position & colour. 
*/
 
/*
thought: 
 
*/
 
let cam;
 
let pixelation = 3;
 
let similarity_threshold = 10;
 
function setup() {
  cam = createCapture(VIDEO, { flipped: true }, make_canvas);
  pixelDensity(1);
  cam.hide();
}
 
//make canvas the same size as camera for easier calculations.
function make_canvas() {
  createCanvas(cam.width, cam.height, WEBGL);
}
 
function draw() {
    background(0);
 
  //   push();
  //   translate(-width / 2, -height / 2);
  //   //webgl renders canvas from the center of the screen; so.
  //   image(cam, 0, 0, width, height);
  //   pop();
 
  if (frameCount % 1 == 0) {
    make_groups();
  }
 
  draw_groups();
}
 
//this function will read all colour values, cluster similar colours together into a group.
 
// we keep two variables:
let groups = []; //stores data in the format: groups[id][indices], where id increments sequentially, and indices is a list of all cam.pixels indices that belong to the group.
 
let pixel_to_group; //this stores data in this format: studied_index: groupid. we use this to check if the pixel being studied has been grouped or not.
 
function make_groups() {
  cam.loadPixels();
  //^ returns a cam.pixels array with rgba information.
 
  // reset every frame
  groups = [];
  pixel_to_group = {};
 
  // go through every single pixel and compare its colour value with its neighbour.
  for (let x = 0; x < cam.width; x += pixelation) {
    for (let y = 0; y < cam.height; y += pixelation) {
      // this is the studied pixel. we get its values first.
      let studied_index = get_pixel_index(x, y);
 
      let studied_rgba = color(cam.pixels[studied_index], cam.pixels[studied_index + 1], cam.pixels[studied_index + 2], cam.pixels[studied_index + 3]);
 
      let studied_hue = Math.floor(hue(studied_rgba));
 
      // now we have the hue of the pixel being looked at. we now want to compare it to its neighbours.
      let neighbours = get_neighbours(x, y);
      // ^ returns an array of indices for cam.pixels.
 
      // this will store the group id if we find a matching neighbour.
      let found_group = null;
 
      // now we compare the studied hue to neighbour_hue.
      for (let i = 0; i < neighbours.length; i++) {
        let neighbour_index = neighbours[i];
 
        // we see if the neighbour has been grouped before. if not, we skip through this neighbour business, so that the current pixel can get its own group.
        if (pixel_to_group[neighbour_index] === undefined) {
          continue;
        }
 
        // a group near the neighbour being studied exists, which means we can group it to that group.
 
        //get its colour. we compare hue.
        let neighbour_rgba = color(cam.pixels[neighbour_index], cam.pixels[neighbour_index + 1], cam.pixels[neighbour_index + 2], cam.pixels[neighbour_index + 3]);
        let neighbour_hue = Math.floor(hue(neighbour_rgba));
 
        // we use abs to get rid of writing if > and if <. abs maps it to a number line.
        if (abs(neighbour_hue - studied_hue) < similarity_threshold) {
          // this is a valid colour to be grouped.
          found_group = pixel_to_group[neighbour_index];
          break;
        }
      }
 
      // if no neighbour matched, create a new group
      if (found_group === null) {
        let new_group_id = groups.length;
        groups.push([studied_index]);
        pixel_to_group[studied_index] = new_group_id;
      }
      // otherwise, add this pixel to an existing group, if found.
      else {
        groups[found_group].push(studied_index);
        pixel_to_group[studied_index] = found_group;
      }
    }
  }
}
 
//this one tries to take some dynamic decisions to make either a triangle or triangle strip topology.
// function draw_groups() {
//   push();
 
//   // since webgl draws from the center of the screen:
//   translate(-width / 2, -height / 2);
 
//   colorMode(RGB, 255);
//   noFill();
 
//   // we want to draw a mesh for every single group.
//   for (let n = 0; n < groups.length; n++) {
//     if (groups[n].length < 3) continue; //skip these groups, because they're too small to be a mesh.
 
//     // now we decide the topology.
 
//     //if it is divisible by 3, we form a triangle-strip.
//     if (groups[n].length % 3 === 0) {
//       beginShape(TRIANGLES);
 
//       for (let i = 0; i < groups[n].length; i++) {
//         let index = groups[n][i];
//         let { x, y } = index_to_xy(index);
 
//         // z keeps shifting.
 
//         let r = cam.pixels[index];
//         let g = cam.pixels[index + 1];
//         let b = cam.pixels[index + 2];
 
//         let osci = sin(frameCount * 0.05);
 
//         const z_depth = width;
 
//         let t = frameCount * 0.0005;
//         let z = map(noise(i * 0.1, t), 0, 1, 0, z_depth);
 
//         // stroke(r, g, b);
//         // strokeWeight(pixelation);
//         // point(x, y, 1);
 
//         noStroke();
//         fill (r,g,b);
//         vertex(x, y, z);
//       }
 
//       endShape();
//     } else {
//       beginShape(TRIANGLE_STRIP);
 
//       for (let i = 0; i < groups[n].length; i++) {
//         let index = groups[n][i];
//         let { x, y } = index_to_xy(index);
 
//         let r = cam.pixels[index];
//         let g = cam.pixels[index + 1];
//         let b = cam.pixels[index + 2];
 
//         const z_depth = width;
 
//         let t = frameCount * 0.0005;
//         let z = map(noise(i * 0.01, t), 0, 1, 0, z_depth);
 
//         noStroke();
//         fill(r, g, b);
//         vertex(x, y, z);
//       }
 
//       endShape();
//     }
//   }
 
//   pop();
// }
 
function draw_groups() {
  push();
  // since webgl draws from the center of the screen:
  translate(-width / 2, -height / 2);
  colorMode(RGB, 255);
  noFill();
  // we want to draw a mesh for every single group.
  for (let n = 0; n < groups.length; n++) {
    if (groups[n].length < 4) continue; //skip these groups, because they're too small to be a mesh.
    beginShape(QUAD_STRIP);
    for (let i = 0; i < groups[n].length; i++) {
      let index = groups[n][i];
      let { x, y } = index_to_xy(index);
      // z keeps shifting.
      let r = cam.pixels[index];
      let g = cam.pixels[index + 1];
      let b = cam.pixels[index + 2];
      let osci = sin(frameCount * 0.05);
      const z_depth = width;
      let t = frameCount * 0.0005;
      let z = map(noise(i * 0.1, t), 0, 1, 0, z_depth);
      // stroke(r, g, b);
      // strokeWeight(pixelation);
      // point(x, y, 1);
      noStroke();
      fill(r, g, b);
      vertex(x, y, z);
    }
    endShape();
  }
}
 
/* helpers: */
 
//helper written by an llm to fetch neighbours for a given pixel, taking in mind the pixelation value.
function get_neighbours(x, y) {
  let neighbours = [];
 
  // Check all 8 surrounding positions (or fewer at edges)
  for (let dx = -pixelation; dx <= pixelation; dx += pixelation) {
    for (let dy = -pixelation; dy <= pixelation; dy += pixelation) {
      // Skip the center pixel itself
      if (dx === 0 && dy === 0) continue;
 
      let nx = x + dx;
      let ny = y + dy;
 
      // Check bounds
      if (nx >= 0 && nx < cam.width && ny >= 0 && ny < cam.height) {
        let index = get_pixel_index(nx, ny);
        neighbours.push(index); // just return the index of the neighbours.
        //     {
        //   x: nx,
        //   y: ny,
        //   index: index,
        //   color: {
        //     r: cam.pixels[index],
        //     g: cam.pixels[index + 1],
        //     b: cam.pixels[index + 2],
        //     a: cam.pixels[index + 3],
        //   },
        // });
      }
    }
  }
 
  return neighbours;
}
 
//helper to convert x,y coordinates to pixels index.
function get_pixel_index(x, y) {
  return (y * cam.width + x) * 4;
}
 
//helper to convert index to coordinates.
function index_to_xy(index) {
  let p = index / 4;
  let x = p % cam.width;
  let y = Math.floor(p / cam.width);
  return { x, y };
}
 
function mousePressed() {
  noLoop();
}
 

my algorithm works decently, because it chunks vertices together.

achieved a cool gouache sort of sketch.

//untitled; arjun; month, 2026.
 
/*
ask: 
make a reactive mesh, with vertices that use the z-parameter. use position & colour. 
*/
 
/*
thought: 
 
*/
 
let cam;
 
let pixelation = 5;
 
let similarity_threshold = 1;
 
function setup() {
  cam = createCapture(VIDEO, { flipped: true }, make_canvas);
  pixelDensity(1);
  cam.hide();
}
 
//make canvas the same size as camera for easier calculations.
function make_canvas() {
  createCanvas(cam.width, cam.height, WEBGL);
}
 
function draw() {
    // background (255); 
 
  //for testing video feed. 
    push();
    translate(-width / 2, -height / 2);
    //webgl renders canvas from the center of the screen; so.
 
    // tint (100, 50); 
    // image(cam, 0, 0, width, height);
    pop();
 
  if (frameCount % 1 == 0) {
    make_groups();
  }
  tint(255, 255); 
  draw_groups();
}
 
//this function will read all colour values, cluster similar colours together into a group.
 
// we keep two variables:
let groups = []; //stores data in the format: groups[id][indices], where id increments sequentially, and indices is a list of all cam.pixels indices that belong to the group.
 
let pixel_to_group; //this stores data in this format: studied_index: groupid. we use this to check if the pixel being studied has been grouped or not.
 
function make_groups() {
  cam.loadPixels();
  //^ returns a cam.pixels array with rgba information.
 
  // reset every frame
  groups = [];
  pixel_to_group = {};
 
  // go through every single pixel and compare its colour value with its neighbour.
  for (let x = 0; x < cam.width; x += pixelation) {
    for (let y = 0; y < cam.height; y += pixelation) {
      // this is the studied pixel. we get its values first.
      let studied_index = get_pixel_index(x, y);
 
      let studied_rgba = color(cam.pixels[studied_index], cam.pixels[studied_index + 1], cam.pixels[studied_index + 2], cam.pixels[studied_index + 3]);
 
      let studied_hue = Math.floor(hue(studied_rgba));
 
      // now we have the hue of the pixel being looked at. we now want to compare it to its neighbours.
      let neighbours = get_neighbours(x, y);
      // ^ returns an array of indices for cam.pixels.
 
      // this will store the group id if we find a matching neighbour.
      let found_group = null;
 
      // now we compare the studied hue to neighbour_hue.
      for (let i = 0; i < neighbours.length; i++) {
        let neighbour_index = neighbours[i];
 
        // we see if the neighbour has been grouped before. if not, we skip through this neighbour business, so that the current pixel can get its own group.
        if (pixel_to_group[neighbour_index] === undefined) {
          continue;
        }
 
        // a group near the neighbour being studied exists, which means we can group it to that group.
 
        //get its colour. we compare hue.
        let neighbour_rgba = color(cam.pixels[neighbour_index], cam.pixels[neighbour_index + 1], cam.pixels[neighbour_index + 2], cam.pixels[neighbour_index + 3]);
        let neighbour_hue = Math.floor(hue(neighbour_rgba));
 
        // we use abs to get rid of writing if > and if <. abs maps it to a number line.
        if (abs(neighbour_hue - studied_hue) < similarity_threshold) {
          // this is a valid colour to be grouped.
          found_group = pixel_to_group[neighbour_index];
          break;
        }
      }
 
      // if no neighbour matched, create a new group
      if (found_group === null) {
        let new_group_id = groups.length;
        groups.push([studied_index]);
        pixel_to_group[studied_index] = new_group_id;
      }
      // otherwise, add this pixel to an existing group, if found.
      else {
        groups[found_group].push(studied_index);
        pixel_to_group[studied_index] = found_group;
      }
    }
  }
}
 
//this one tries to take some dynamic decisions to make either a triangle or triangle strip topology.
function draw_groups() {
  push();
 
  // since webgl draws from the center of the screen:
  translate(-width / 2, -height / 2);
 
  //coloring stuff; remains global across meshes:
  colorMode(RGB, 255);
  noStroke(); 
 
  // we want to draw a mesh for every single group.
  for (let n = 0; n < groups.length; n++) {
    if (groups[n].length < 3) continue; //skip these groups, because they're too small to be a mesh.
 
    // z transformations:
    let max_group_length = Math.max(...groups.map((g) => g.length));
    const max_z_depth = 50;
    const min_z_depth = -50;
 
    // now we decide the topology.
 
    //if it is divisible by 3, we form a triangle-strip.
    if (groups[n].length % 3 === 0) {
      push();
      let z = map(groups[n].length, 3, max_group_length, min_z_depth, max_z_depth);
      translate(0, 0, z);
      beginShape(TRIANGLES);
 
      for (let i = 0; i < groups[n].length; i++) {
        let index = groups[n][i];
        let { x, y } = index_to_xy(index);
 
        let r = cam.pixels[index];
        let g = cam.pixels[index + 1];
        let b = cam.pixels[index + 2];
 
        stroke (r,g,b,255); 
        strokeWeight (20); 
        fill(r, g, b, 50);
 
        vertex(x, y, 0);
      }
 
      endShape();
 
      pop();
    } else {
      push();
      let z = map(groups[n].length, 0, max_group_length, min_z_depth, max_z_depth);
      translate(0, 0, z);
      beginShape(TRIANGLE_STRIP);
 
      for (let i = 0; i < groups[n].length; i++) {
        let index = groups[n][i];
        let { x, y } = index_to_xy(index);
 
        let r = cam.pixels[index];
        let g = cam.pixels[index + 1];
        let b = cam.pixels[index + 2];
 
        let z = map(n, 0, max_group_length, min_z_depth, max_z_depth);
 
        stroke(r, g, b, 255);
        strokeWeight(20); 
        fill(r, g, b, 50);
 
        vertex(x, y, 0);
      }
 
      endShape();
      pop();
    }
  }
 
  pop();
}
 
// // this one just draws triangle-strip meshes.
// function draw_groups() {
//   push();
//   // since webgl draws from the center of the screen:
//   translate(-width / 2, -height / 2);
//   colorMode(RGB, 255);
//   noFill();
//   // we want to draw a mesh for every single group.
//   for (let n = 0; n < groups.length; n++) {
//     if (groups[n].length < 4) continue; //skip these groups, because they're too small to be a mesh.
//     beginShape(TRIANGLE_STRIP);
//     for (let i = 0; i < groups[n].length; i++) {
//       let index = groups[n][i];
//       let { x, y } = index_to_xy(index);
//       let r = cam.pixels[index];
//       let g = cam.pixels[index + 1];
//       let b = cam.pixels[index + 2];
 
//       let z = 0;
 
//       // stroke(r, g, b);
//       // strokeWeight(pixelation);
//       // point(x, y, 1);
//       noStroke();
//       fill(r, g, b);
//       vertex(x, y, z);
//     }
//     endShape();
//   }
// }
 
/* helpers: */
 
//helper written by an llm to fetch neighbours for a given pixel, taking in mind the pixelation value.
function get_neighbours(x, y) {
  let neighbours = [];
 
  // Check all 8 surrounding positions (or fewer at edges)
  for (let dx = -pixelation; dx <= pixelation; dx += pixelation) {
    for (let dy = -pixelation; dy <= pixelation; dy += pixelation) {
      // Skip the center pixel itself
      if (dx === 0 && dy === 0) continue;
 
      let nx = x + dx;
      let ny = y + dy;
 
      // Check bounds
      if (nx >= 0 && nx < cam.width && ny >= 0 && ny < cam.height) {
        let index = get_pixel_index(nx, ny);
        neighbours.push(index); // just return the index of the neighbours.
      }
    }
  }
 
  return neighbours;
}
 
//helper to convert x,y coordinates to pixels index.
function get_pixel_index(x, y) {
  return (y * cam.width + x) * 4;
}
 
//helper to convert index to coordinates.
function index_to_xy(index) {
  let p = index / 4;
  let x = p % cam.width;
  let y = Math.floor(p / cam.width);
  return { x, y };
}
 
//for debugging:
// function mousePressed() {
//   noLoop();
// }