curveVertex not working correctly

Hi,

Today I’ve been trying to make a simpler version of this music player by Misaki Nakano.
Out of the different options available, one has caught my eyes:

  • the cubic (or Catmull-Rom spline) interpolation mode.

I believe the easiest way to achive that effect in Processing is to use the curveVertex() function which is an actual implementation of Catmull-Rom splines.
However, when simply replacing vertex() by curveVertex() in the script below (without forgeting to remove QUAD_STRIPS from beginShape()) my sketch goes from this:


… to this:

Do you know what I’m missing here ?

Also, I notice the animation in Misaki’s player is smooth and “bouncy” while my version is somewhat brisk and jerky. Do you have an idea of what I could improve to get a smoother animation ?

There are other smaller issues I have but I prefer to stop here for now.

Any suggestion would be helpful. Thank you.

add_library('minim')
add_library('peasycam')

cylindrical, isPlaying, isMute = False, False, False
pts, radius, latheRadius, segments = 20, 1, 300, 128 
vertices, vertices2 = [[PVector() for e in range(pts+1)] for e in range(2)]

def setup():
    global song, fftLin, fftLog, cam
    size(1200, 800, P3D)
    frameRate(1000)
    smooth(8)

    cam = PeasyCam(this, 600)
    cam.setMaximumDistance(600)
    cam.rotateX(-PI/2.4)
    perspective(60 * DEG_TO_RAD, width/float(height), 2, 6000)

    minim = Minim(this)
    song = minim.loadFile("tm1.mp3")
    
    fftLin, fftLog = [FFT(song.bufferSize(), song.sampleRate()) for e in range(2)]
    fftLin.linAverages(30), fftLog.logAverages(22, 3)
    
def draw():
    background(225, 250, 255)
    lights()
    ambientLight(255,153,39)
    directionalLight(255,43,192, 1, 0, 0)
    pointLight(255,215,60, width, height/2, 0)
    rotateZ(frameCount*PI/560)
    fill(255,82,139)
    noStroke()

    song.play() if isPlaying else song.pause()
    song.mute() if isMute else song.unmute()
    
    fftLin.forward(song.mix)
    fftLog.forward(song.mix)

    latheAngle = 0
    for s in range(segments):
        angle = 0
        beginShape(QUAD_STRIP)
        for i, v in enumerate(vertices2):
            division = 1 if s%2 == 0 else 6
            step = s*2 # select 1 freq every 2 freq (up to 256 out of 512)
            c_step = (s*2)+((s%2)*2) # select 1 freq every 4 freq (up to 256 freq out of 512)
            if cylindrical: sscale = map(fftLin.getBand(c_step)*((s+10)/10), 0, 35, .8, 35)
            else: sscale = map(fftLin.getBand(step)*((s+10)/10), 0, 30, .8, 24)
            sscale = constrain(sscale, 0, 50)
            
            vertices[i].x = latheRadius + sin(radians(angle)) * radius * sscale 
            vertices[i].z = cos(radians(angle)) * radius * sscale 
            angle+=360.0/pts
            vertex(v.x, v.y, v.z)

            v.x = cos(radians(latheAngle))  * vertices[i].x 
            v.y = sin(radians(latheAngle))  * vertices[i].x 
            v.z = vertices[i].z 
            vertex(v.x, v.y, v.z)
            
        latheAngle += (360.0+260)/(segments*6/division) if cylindrical else 360.0/segments
        endShape()
       
    cam.beginHUD()
    text("'p' = PLAY/PAUSE", 20, 30)
    text("'r' = REPLAY", 20, 50)
    text("'m' = MUTE", 20, 70)
    text("'c' = MODE", 20, 90)
    cam.endHUD()
    
def keyPressed():
    global isPlaying, cylindrical, isMute
    if key == 'p': isPlaying = not isPlaying
    if key == 'm': isMute = not isMute
    if key == 'c': cylindrical = not cylindrical
    if key == 'r': song.rewind() 
2 Likes

Hi @solub!

I see you’re trying to map the audio signals with the torus section radius, but the vertex ordering for generating QUAD_STRIP shapes is different from curveVertex, hence the unpredictable results.

It is possible to do it in your current way, but to save you some headaches, I suggest you try a different approach. You can create a circular rail, and place a sphere at each vertex, where the radii would correspond to the values you’re getting from the audio. I’ve written an illustration that you can try to incorporate in your sketch. It’s written in java, but I think you know enough java to make sense of it (If you needed a py verison I don’t mind rewriting it.)
output

PShape s;
float a;
int res = 200;
void setup() {
  size(400, 400, P3D);
  
  //Setting up rail parameters
  float angle = TWO_PI/res;
  float radius = 200;
  
  //Create circle rail
  s = createShape();
  s.beginShape();
  s.noFill();
  s.stroke(0, 0, 255);
  for (float i = 0; i < TWO_PI; i+=angle) {
    s.curveVertex(radius*cos(i), radius*sin(i), 0);
  }
  s.endShape(CLOSE);
  
}

void draw() {
  background(255);
  
  //Setup lights and coordinate system
  lights();
  ambientLight(255,153,39);
  translate(width/2, height/2, -200);
  rotateX(PI/3);
  pointLight(255,215,60, width, height/2, 0);
  directionalLight(255,43,192,-1.0*mouseX/width,1.0*mouseY/height,0);
  rotateZ(a);
  
  //Draw spheres at each vertex of circular rail
  noStroke();
  for(int i = 0; i < s.getVertexCount(); i++){
    float t = 1.0*i/s.getVertexCount();
    PVector vec = s.getVertex(i);
    pushMatrix();
    translate(vec.x,vec.y,vec.z);
    fill(#eb34ae);
    sphere(map(sin(t*50),-1,1,5,30));
    popMatrix();
  }
  
  //Circular rail
  //shape(s, 0, 0);
  
  //Animate
  a += 0.05;
}

You can set up the connection between your audio and fine tune the geometry attributes with this bit I hope. Let me know how this works for you.

2 Likes

I should also mention that I didn’t forget about your other thread with the dof shader. I think Andres Colubri (the awesome guy who wrote the PShader tutorial) is registered in this forum, so maybe you can try to tag him for help.

Some helpful examples of the different vertex orders for TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, et cetera are in the beginShape() reference entry:

1 Like

Hi @WakeMeAtThree, that’s so nice of you to share this workaround. I got to say, I never would have though of this technique (translating spheres) to achieve that blobby effect… very clever !

Following your answer I made a Python version, tweaked the code a little bit, changed some part and it works… However I must admit I would still prefer to stick with the curveVertex() option. And this for 2 reasons at least:

  • Displaying spheres along a circular shape significantly slows down the sketch. Because of the lags, it’s almost impossible to get an accurate representation of the frequencies motion

  • At some point in the sketch I’d like to display the QUAD_STRIP strokes, maybe adding effects to it. With sphere strokes I’m losing the rectangular and symmetrical aspect of the quad trips + I’m revealing that the toroid… is not really a toroid.

I understand I have to “flip” the order of the vertices in order to be correctly read by curveVertex(). For now vertices are distributed vertically, then projected horizontally along a circular axis. I believe it’s the other way around that should be working but I’m not sure how to do that…

Regarding the DoF effect, I’m lucky to have noahbuddy (from the old Processing forum) helping me as well. He replied me yesterday but couldn’t find the time to run his script yet. Be assured I’ll update the thread with his findings.
Anyway, I’m truely grateful to both of you for your help, it means a lot.

1 Like

@jeremydouglass Not sure that posting a link to the beginShape() reference (that, obviously, I’ve read dozens of time befores coming to this forum) is relevant here. An insight/suggestion would have been more helpful.

Hi @solub. What Jeremy mentioned was directly relevant, given that your glaring issue was the mix-up of vertex orders. Please maintain a more friendly attitude when you respond to people helping you. There are some interesting discussions happening here, and words like that only help undercut them.

Back to the mesh generation issue, you can think of a torus as a sweep of a circle around a circle rail. So it helps to think of it as a list of circles, with each circle radius varying depending on your audio input. The circles are also lists of PVectors that we can use for creating quad strips.

So in order to create a strip, we need to call two consecutive circles at a time, and generate a QUAD_STRIP between them, and repeat that for the entire list. Here’s my take on this, I tried to keep it as clear as possible syntax-wise. Try to connect it with your audio visualization workflow and let us know what happens. I’ll rewrite it for processing.py at a later time.

output

ArrayList<ArrayList<PVector>> torus;
int sectionRes = 40;
int railRes = 100;

float ang1 = TWO_PI/sectionRes;
float ang2 = TWO_PI/railRes;
float a;

void setup() {
  size(400, 400, P3D);
  background(255);
  torus = new ArrayList<ArrayList<PVector>>();
}

void draw() {
  background(255);
  lights();

  //Setup lights and coordinate system
  translate(width/2, height/2, -200);
  rotateX(PI/4);
  rotateZ(a);

  //Display Settings
  strokeWeight(1);
  stroke(255, 0, 128, 50);
  fill(255);

  //Store previous section's radius
  float radius1, radius2;
  radius2=0;
  for (float i = 0; i < TWO_PI+ang2; i+=ang2) {
    //Delay offset
    float param = 15.0*TWO_PI * i/(TWO_PI+ang2);

    //Set radius to the two sections
    radius1 = lerp(5, 35, fract(a+param));
    ArrayList<PVector> vertices1 = torusSection(i, radius2, 200);
    ArrayList<PVector> vertices2 = torusSection(i+ang2, radius1, 200);
    radius2 = radius1;

    //Draw strip
    beginShape(QUAD_STRIP);
    for (int j = 0; j < sectionRes+1; j++) {
      PVector vert1 = vertices1.get(j%sectionRes);
      PVector vert2 = vertices2.get(j%sectionRes);
      vertex(vert1.x, vert1.y, vert1.z);
      vertex(vert2.x, vert2.y, vert2.z);
    }
    endShape();
  }

  //Clear torus elements
  torus = new ArrayList<ArrayList<PVector>>();

  //Animate
  a += 0.01;
}

ArrayList<PVector> torusSection(float rot, float radius, float offset) {
  /* This function takes in radius of circle section, offset distance 
   from origin, and rotation from origin and returns a list of
   section vertices. Spherical coordinates are used for vectors.
   */

  float phi = 0;
  ArrayList<PVector> vertices = new ArrayList<PVector>();
  for (float i=0; i<=TWO_PI; i+=ang1) {
    PVector vert = new PVector();
    vert.x = offset+radius*sin(i)*cos(phi);
    vert.y = 0 + radius*sin(i)*sin(phi);
    vert.z = radius*cos(i);
    vert.rotate(rot);
    vertices.add(vert);
  }

  return vertices;
}

//Some sample shaping funcs

float func1(float a, float param) {
  return map(sin(a+param), -1, 1, 0, 1);
}

float fract(float x) {
  return x - floor(x);
}

float step(float x, float threshold) {
  // Step will return 0.0 unless the value is over 0.5,
  // in that case it will return 1.0
  if (x>threshold) {
    return 1.0;
  } else {
    return 0.0;
  }
}

Some parameters to play with: the resolution of circle section, and circle rail and the shaping functions (difference between smooth and linear functions). You can shape the values of the radii that you’re getting from the audio to make it more smooth, or more linear using shaping functions in lerp()

1 Like

Upon reading more from mnmxmx’s github repo, I realized that she left a link to a useful article that elaborates more on the interpolation she used.

Processing already has a linear interpolate method, lerp(), and you can expand your interpolation repertoire by translating the article’s interpolate methods:

def CosineInterpolate(y1, y2,  mu):
    mu2 = (1-cos(mu*PI))/2;
    return(y1*(1-mu2)+y2*mu2)

def CubicInterpolate(y0, y1,y2, y3, mu):
    mu2 = mu*mu;
    a0 = y3 - y2 - y0 + y1;
    a1 = y0 - y1 - a0;
    a2 = y2 - y0;
    a3 = y1;

    return(a0*mu*mu2+a1*mu2+a2*mu+a3)

You can forgot about the shaping functions inside the lerp method I used previously, and just focus on using lerp, cosine, and cubic interpolation. The ingredients are all on the table now.

EDIT: Python version

sectionRes = 40
railRes = 10

ang1 = TWO_PI/sectionRes
ang2 = TWO_PI/railRes
a = 0
torus = []

def setup():
    size(400,400,P3D)

def draw():
    background(255)
    lights()
    global a
    
    #Setup lights and coordinate system
    translate(width/2,height/2,-200)
    rotateX(PI/4)
    rotateZ(a)
    
    #Display settings
    strokeWeight(1)
    stroke(255,0,128,50)
    fill(255)

    #Store previous section's radius
    radius1,radius2 = 0,0
    for i in range(railRes+1):
        param = 1.0*i/(railRes)
        radius1 = CosineInterpolate(5,45,param)
        
        vertices1 = torusSection(i*ang2, radius2, 200)
        vertices1 = vertices1+[vertices1[0]]
        vertices2 = torusSection((i+1)*ang2, radius1, 200)
        vertices2 = vertices2+[vertices2[0]]
        
        beginShape(QUAD_STRIP);
        for vert1,vert2 in zip(vertices1,vertices2):
            vertex(vert1.x,vert1.y,vert1.z)
            vertex(vert2.x, vert2.y, vert2.z)
        endShape();
        

        radius2 = radius1

    
    torus = []
    a += 0.01

def torusSection(rot, radius, offset):
    """This function takes in radius of circle section, offset distance 
    from origin, and rotation from origin and returns a list of
    section vertices. Spherical coordinates are used for vectors."""
    phi = 0
    vertices = []
    
    for i in range(sectionRes):
        vert = PVector(offset+radius*sin(i)*cos(phi),
                       0 + radius*sin(i)*sin(phi),
                       radius*cos(i))
        vert.rotate(rot)
        vertices.append(vert)
    
    return vertices

def CosineInterpolate(y1, y2,  mu):
    mu2 = (1-cos(mu*PI))/2;
    return(y1*(1-mu2)+y2*mu2)

def CubicInterpolate(y0, y1,y2, y3, mu):
    mu2 = mu*mu;
    a0 = y3 - y2 - y0 + y1;
    a1 = y0 - y1 - a0;
    a2 = y2 - y0;
    a3 = y1;

    return(a0*mu*mu2+a1*mu2+a2*mu+a3)
1 Like

No offense intended – I personally have re-encountered surprises with vertex order several times, and revisiting that reference has helped me. Since it was mentioned, I linked it. If that material was not helpful for you then hopefully it might still help later forum users.

1 Like

Please maintain a more friendly attitude when you respond to people helping you.

I realise my comment could be seen as rude but I certainly didn’t want to show disrespect to @jeremydouglass, my apologies. I like to be straightforward and the fact that I’m far from being fluent in english may make me come off as a cold person sometimes. I honestly still believe a link to the beginShape() reference entry wasn’t necessary here, mainly because it seems obvious to me that one doesn’t ask for help regarding a specific Processing function without having checked its reference page before. I’ve been asking many question on this forum but most of the time I spend hours browsing the old forums and related issues before even thinking of opening a new thread.

What Jeremy mentioned was directly relevant, given that your glaring issue was the mix-up of vertex orders.

Not exactly. As I had mentionned in my previous comment I was aware that the problem had to do with the vertices order but didn’t know how to “invert” it:

I understand I have to “flip” the order of the vertices in order to be correctly read by curveVertex(). For now vertices are distributed vertically, then projected horizontally along a circular axis. I believe it’s the other way around that should be working but I’m not sure how to do that

A simple suggestion regarding that matter would have been helpful.

Now back to your example: I must admit I don’t really grasp the logic behind your code.

  • I’ve read the Paul Bourke article multiple times before asking for help on this forum and from what I understand y0, y1, y2 and y3 are supposed to be points coordinates. However in your script y0 and y1 are constant integers (5 and 45), why is that ?

“…the function requires 4 points in all labelled y0, y1, y2, and y3, in the code below. mu still behaves the same way for interpolating between the segment y1 to y2.”

  • Also, unlike what your answer is implying, Paul Breeuwsma’s cubic interpolation method (referred to as Catmull-Rom splines) is already implemented in Processing. Basically, your CubicInterpolate() function is like calling curvePoint() with y0, y1, y2, y3 as parameters. Thus:
def CubicInterpolate(y0, y1,y2, y3, mu):
    mu2 = mu*mu;
    a0 = y3 - y2 - y0 + y1;
    a1 = y0 - y1 - a0;
    a2 = y2 - y0;
    a3 = y1;

    return(a0*mu*mu2+a1*mu2+a2*mu+a3)

is somewhat similar to:

curvePoint(y0, y1, y2, y3, mu)

Hence my question on how to solve the problem I had using curveVertex() specifically.
The good thing is I’ve now managed to combine beginShape() with curveVertex(). I just had to:

  • store the vertices in a separate array list
  • invert the intial double for loop (to match curveVertex())
  • draw splines horizontally AND vertically (to mimic QUAD_STRIP)
    latheAngle = 0
    for s in range(segments+1):
        angle = 0
        beginShape(QUAD_STRIP)
        for i, v in enumerate(vertices2):
            division = 1 if s%2 == 0 else 6
            step = s*2 # select 1 freq every 2 freq (up to 256 out of 512)
            c_step = (s*2)+((s%2)*2) # select 1 freq every 4 freq (up to 256 freq out of 512)
            
            if cylindrical: sscale = map(fftLin.getBand(c_step)*((s+10)/10), 0, 35, .8, 35)
            else: sscale = map(fftLin.getBand(step)*((s+10)/10), 0, 30, .8, 24)
            sscale = constrain(sscale, 0, 50)
            
            vertices[i].x = latheRadius + sin(radians(angle)) * radius * sscale 
            vertices[i].z = cos(radians(angle)) * radius * sscale 
            angle+=360.0/pts
            vertex(v.x, v.y, v.z) if isCurved == False else None

            v.x = cos(radians(latheAngle))  * vertices[i].x 
            v.y = sin(radians(latheAngle))  * vertices[i].x 
            v.z = vertices[i].z 
            vertex(v.x, v.y, v.z) if isCurved == False else None

            if isCurved: cVerts[s][i] = PVector(v.x, v.y, v.z) # Store vertices in an array list
            
        latheAngle += (360.0+260)/(segments*6/division) if cylindrical else 360.0/segments
        endShape()

    if isCurved:
        noFill()
        stroke(238,90,110)
        for p in range(pts+1): # Inverting the initial for loop
            beginShape() # Drawing splines HORIZONTALLY
            curveVertex(cVerts[0][p].x, cVerts[0][p].y, cVerts[0][p].z)
            for s in range(segments):    
                curveVertex(cVerts[s][p].x, cVerts[s][p].y, cVerts[s][p].z)
            curveVertex(cVerts[-1][p].x, cVerts[-1][p].y, cVerts[-1][p].z)
            endShape(CLOSE)
        for s in range(segments):
            beginShape() # Drawing splines VERTICALLY
            curveVertex(cVerts[s][0].x, cVerts[s][0].y, cVerts[s][0].z)
            for p in range(pts+1):
                curveVertex(cVerts[s][p].x, cVerts[s][p].y, cVerts[s][p].z)
            endShape(CLOSE)

Horizontal splines

Horizontal + Vertical splines

Unfortunately, this workaround has 2 main drawbacks:

  • It is slow (at each iteration I’m computing the vertices with a double for loop + storing them in an array list + drawing the splines both hoziontally and vertically with 2 other double for loops.
  • Because I’m drawing splines hoziontally THEN vertically (with 2 separate loops), I can’t properly fill the toroid

all the space within the toroid is filled, not the toroid itself

3 Likes