3 Matrix mapped as cylinder

Hi all,

I looked at some of the other posts about mapping a cylinder but they didn’t help and I can’t quite figure this out. I have 3 8x32 matrices that I want to stack on top of each other and wrap to make a cylinder that is essentially 24 tall and 32 around.

Each matrix is wired like this:
image

2 Likes

Hi @nickbeaulieu -

Looks this is a zig-zag wired matrix where you’re using it rotated 90-degrees, wrapped into a cylinder, and you need three of them stacked.

Start by configuring the number of pixels in the Settings tab to (3 x 8 x 32) = 768 pixels.

Now, in the Mapper tab, load the Multiple Panel Matrix example:

First, we change the parameters to show us a single 8x32 panel rotated on its side:

Noting these sections:

  zigzag = false

  // ...

  //create a set of coordinates for a matrix panel
  //sized (w, h), rotated by an angle, and offset by (sx, sy)
  function panel(w, h, sx, sy, angle) {

  // ....

  map = map.concat(panel(8, 8, 0, 0, 0))
  map = map.concat(panel(8, 8, 8, 0, 0))
  // etc...

I changed these key parts first:

  zigzag = true

  //...

  map = map.concat(panel(8, 32, 0, 0, 90))

  return map

Ok, this looks good because now we see a single panel in the preview window, rotated like in your diagram.

Now let’s stack three of them vertically:

  map = map.concat(panel(8, 32, 0, 0, 90))
  map = map.concat(panel(8, 32, 0, 8, 90))
  map = map.concat(panel(8, 32, 0, 16, 90))

Good deal. Now for the hard part, which is that we want to make it three-dimensional. Right now, the Z coordinate is 0 across all pixels, because it’s flat. To make it into a cylinder, we need to compress the X-axis by cosine, and the Z-axis by sine. We should do this every 8 pixels, completing a full circle after every 8 * 32 = 256 pixels. I’ll do this with a simple for loop to make it easier to follow along.

First, I want to get an iterator going that adds a Z-axis value of zero, keeping it flat:

  // ...
  map = map.concat(panel(8, 32, 0, 16, 90))
  
  var map3D = [];
  
  for (i = 0; i < map.length; i++) {
    var x = map[i][0], y = map[i][1]
    
    map3D.push([x, y, 0])
  }
  
  return map3D

So far so good, looks right:

Now we need a bit of math that will give us that angle around the cylinder for each pixel. I know it’s 8 * 32 = 256 pixels each time around, so I could start with something basic that converts each of the 256 pixels into a fraction of a complete rotation, 0…1. Math.sin/cos take radians, which is 0…2 Pi instead of 0…1.

  for (i = 0; i < map.length; i++) {
    var x = map[i][0], y = map[i][1]
    
    var angle = i / 256 * 2 * Math.PI
    
    map3D.push([Math.cos(angle), y, Math.sin(angle)])
  }

Ah - I see, because Sin and Cos return values from -1…1, but the Y-axis height is still in units of pixels (it’s 24 pixels tall), I’ll cheat and just use the “fill” map mode instead of the “contain” mode for now. This will stretch it out in all dimensions to fill a 1x1x1 cube.

Great. Getting closer. The next thing to fix is to make the angle only advance every 8 pixels. There’s 32 columns of 8 pixels, so what math do I need to get each column’s angle in 0…2 Pi, in 8 pixel chunks?

    var columnNumber = Math.floor(i / 8)
    var angle = columnNumber / 32 * 2 * Math.PI

Let’s check it by grabbing it and rotating it in 3D.

Looks pretty good!

Here’s the final, complete map code:

Click to expand final cylinder map generator
function (pixelCount) {
  //set zigzag to true if every other LED row travels in reverse
  //if they are all straight across, set it to false
  zigzag = true
  
  //rotate a point (x, y), along a center (cx, cy), by an angle in degrees
  function rotate(cx, cy, x, y, angle) {
    var radians = (Math.PI / 180) * angle,
        cos = Math.cos(radians),
        sin = Math.sin(radians),
        nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
        ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
    return [nx, ny];
  }

  //create a set of coordinates for a matrix panel
  //sized (w, h), rotated by an angle, and offset by (sx, sy)
  function panel(w, h, sx, sy, angle) {
    var x, x2, y, p, map = []
    for (y = 0; y < h; y++) {
      for (x = 0; x < w; x++) {
        //for zigzag, flip direction every other row
        if (zigzag && y % 2 == 1)
          x2 = w - 1 - x
        else
          x2 = x
        p = rotate((w-1)/2, (h-1)/2, x2, y, angle);
        p[0] += sx
        p[1] += sy
        map.push(p)
      }
    }
    return map;
  }

  //assemble one or more panels
  var map = [];

  // Three 8x32 panels, rotated on their side, then stacked up in Y axis
  map = map.concat(panel(8, 32, 0, 0, 90))
  map = map.concat(panel(8, 32, 0, 8, 90))
  map = map.concat(panel(8, 32, 0, 16, 90))
  
  // Cobvert to a 3D map, and wrap into a cylinder
  var map3D = [];
  
  for (i = 0; i < map.length; i++) {
    var x = map[i][0], y = map[i][1]
    
    // Each column of 8 pixels should have the same angle in the cylinder
    var columnNumber = Math.floor(i / 8)

    // There're 32 columns in a full rotation. Radians = turns * 2Pi
    var angle = columnNumber / 32 * 2 * Math.PI
    
    map3D.push([Math.cos(angle), y, Math.sin(angle)])
  }
  
  return map3D
}

From here, you might need to make a few tweaks when you get it wired up. I’m going to leave those to you as an exercise, and I think you’ll be able to figure them out. If you struggle for more than an hour, post your issue here and let’s see if the community can help out. Some things you might encounter:

  1. The panels need to be stacked top-down instead of bottom-up.
  2. The zig-zag from left-to-right actually starts down-up instead of up-down as in your diagram.
  3. You don’t want to use map fill mode, you want to get the diameter of the cylinder accurate with respect to its height. Hint: a 32-LED circumference is a 5.1 LED radius, so multiply the Math.cos and Math.sin each by 5.1.
5 Likes

Oh wow, that’s a really great breakdown. Thank you, Jeff!

1 Like

Sweet bit of code here.
Just wondering if there is a simple change I can make to have it ‘wrap’ around the z-axis?

NVM I figured it out.

3 Likes

Thanks so much for this, Jeff.

I’m having an issue with most of the 2D patterns. anything that uses Render2D is giving an error that says “No valid render function found!”

Is that expected?

@nickbeaulieu, just be really certain that you’ve pressed the somewhat stealthy “Save” button at the bottom of the Mapper tab after entering your map. You may have to scroll down a little to see it.

If the map was saved successfully, you should be able to see your layout in the preview window on the right hand side of any Edit tab, whether the pattern is 1D, 2D or 3D.

yup, The map is saved

It would be worth trying that again, in case something happened. There’ve been a rash of forum posts about this and it makes me wonder if there’s a reliability bug, or just that many more people adding maps this time of year.

Try making a change to the map, even a whitespace change will do it, and clicking save while double-checking that you have a connected status.

I resaved it but still get the same issue.

@nickbeaulieu ,
Sorry, I’m being a bit slow and missed that you have a 3D map now! If you have a 3D map, a pattern with only render2D won’t work. It can be converted through, and if you want to wrap it around the outside (like you would a label on a can) then you can convert the 3D coordinates back down to 2D by using the angle of the cylinder surface circle as one of the coordinates, calculated by using atan2.

Something like this should do it. Since the map Jeff provides goes along Y instead of Z, this should do it. If you had a cylinder that ran along Z, then you could swap y and z in this code:

//Wrap the 2D patter around a cylinder
export function render3D(index, x, y, z) { 
  var unitAngle = (atan2(z, x) + PI) / PI2
  render2D(index, unitAngle, y)
}
1 Like

Thanks! :slight_smile: I thought 2D patterns worked, for some reason. It’s all good though :slight_smile:

This is what I’ve built: Shambhala Totem - 2023 - YouTube

1 Like