Cylinder into a spline

Hello! i`ve this algorithm to make a 3D cylinder, but i want to deform this and create splines or even apply noise, like a ray. I dont know anything about 3D geometry, maybe some of you can give me an aproach. Also, if its posible, i would like to do that distortion in a vertex shader. (is this recommended?, or is better to performance to do this calculations in cpu? )THANKS A LOT!

int rr = 50;
int h = 1000;
int vertices = 25;
beginShape(TRIANGLE_STRIP);

    for (int i = 0; i <= vertices; i++) {
      float angle = TWO_PI / vertices;
      float x = sin(i * angle);
      float z = cos(i * angle);
      float u = float(i) / vertices;
     
   vertex(x * rr, -h/2, z * rr);
     vertex(x * rr, +h/2, z * rr);
    }
endShape();

a simple vertex, i try to do some kind of displacement but i dont get any potable results.

#version 150

#define PROCESSING_COLOR_SHADER

#ifdef GL_ES
precision mediump float;
#endif

   in vec4 position;
   in vec3 color;
   out vec3 Color;
   uniform mat4 transform;
   in vec2 texCoord;
   out vec2 TexCoord;
   in vec4 normal;
   uniform float u_time;

   uniform mat3 normalMatrix;

 	void main() {

    vec4 pos = position;

    pos.y += sin(texCoord.y*10+u_time*normal.y)*200 +
              sin(texCoord.x*10+u_time*normal.y)*200;
    // pos.x *= abs(texCoord.x+normal.x)*1;

    TexCoord = texCoord;

    Color = color;

 		gl_Position = transform * pos;

 	}

Hi vjjv,

I can’t really speak to the GPU approach.

For the CPU approach, you may want to investigate libraries like Toxiclibs, which looks like it has a Spline3D class.

Working on custom code, a cylinder could be thought of as the extrusion of a circle along a straight line. A place to start might be revamping bezierPoint and bezierTangent (or curvePoint and curveTangent) with vector (like PVector) and 4x4 matrix (like PMatrix3D) objects. You can then test how these distribute geometric primitives. For a single curve, as an example,

static class Bezier {
  final static String toStrFmt = "(%.2f, %.2f, %.2f,\n"
    + " %.2f, %.2f, %.2f,\n"
    + " %.2f, %.2f, %.2f,\n"
    + " %.2f, %.2f, %.2f)\n";

  PVector[] pt = {new PVector(), new PVector(), 
    new PVector(), new PVector()};

  Bezier() {
  }

  Bezier(PVector a0, PVector c0, 
    PVector c1, PVector a1) {
    set(a0.x, a0.y, a0.z, 
      c0.x, c0.y, c0.z, 
      c1.x, c1.y, c1.z, 
      a1.x, a1.y, a1.z);
  }

  Bezier(float a0x, float a0y, 
    float c0x, float c0y, 
    float a1x, float a1y, 
    float c1x, float c1y) {
    set(a0x, a0y, 0, 
      c0x, c0y, 0, 
      c1x, c1y, 0, 
      a1x, a1y, 0);
  }

  Bezier(float a0x, float a0y, float a0z, 
    float c0x, float c0y, float c0z, 
    float a1x, float a1y, float a1z, 
    float c1x, float c1y, float c1z) {
    set(a0x, a0y, a0z, 
      c0x, c0y, c0z, 
      c1x, c1y, c1z, 
      a1x, a1y, a1z);
  }

  String toString() {
    return String.format(toStrFmt, 
      pt[0].x, pt[0].y, pt[0].z, 
      pt[1].x, pt[1].y, pt[1].z, 
      pt[2].x, pt[2].y, pt[2].z, 
      pt[3].x, pt[3].y, pt[3].z);
  }

  void set(PVector a0, PVector c0, 
    PVector a1, PVector c1) {
    pt[0].set(a0);
    pt[1].set(c0);
    pt[3].set(a1);
    pt[2].set(c1);
  }

  void set(float a0x, float a0y, float a0z, 
    float c0x, float c0y, float c0z, 
    float a1x, float a1y, float a1z, 
    float c1x, float c1y, float c1z) {
    pt[0].set(a0x, a0y, a0z);
    pt[1].set(c0x, c0y, c0z);
    pt[2].set(c1x, c1y, c1z);
    pt[3].set(a1x, a1y, a1z);
  }

  Bezier copy() {
    return new Bezier(pt[0], pt[1], pt[2], pt[3]);
  }

  Bezier draw(PGraphics3D pg) {
    pg.bezier(pt[0].x, pt[0].y, pt[0].z, 
      pt[1].x, pt[1].y, pt[1].z, 
      pt[2].x, pt[2].y, pt[2].z, 
      pt[3].x, pt[3].y, pt[3].z);
    return this;
  }

  PVector calcPoint(float t) {
    return calcPoint(t, new PVector());
  }

  PVector calcPoint(float t, PVector out) {
    if (t <= 0.0) {
      return out.set(pt[0]);
    } else if (t >= 1.0) {
      return out.set(pt[3]);
    }

    float u = 1.0 - t;
    float tcb = t * t;
    float ucb = u * u;
    float usq3t = ucb * 3.0 * t;
    float tsq3u = tcb * 3.0 * u;
    ucb *= u;
    tcb *= t;

    return out.set(
      pt[0].x * ucb +
      pt[1].x * usq3t +
      pt[2].x * tsq3u +
      pt[3].x * tcb, 

      pt[0].y * ucb +
      pt[1].y * usq3t +
      pt[2].y * tsq3u +
      pt[3].y * tcb, 

      pt[0].z * ucb +
      pt[1].z * usq3t +
      pt[2].z * tsq3u +
      pt[3].z * tcb);
  }

  PVector calcTangent(float t) {
    return calcTangent(t, new PVector());
  }

  PVector calcTangent(float t, PVector out) {
    if (t <= 0.0) {
      return PVector.sub(pt[1], pt[0], out);
    } else if (t >= 1.0) {
      return PVector.sub(pt[3], pt[2], out);
    }

    float u = 1.0 - t;
    float usq3 = u * u * 3.0;
    float tsq3 = t * t * 3.0;
    float ut6 = u * t * 6.0;

    return out.set(
      (pt[1].x - pt[0].x) * usq3 +
      (pt[2].x - pt[1].x) * ut6 +
      (pt[3].x - pt[2].x) * tsq3, 

      (pt[1].y - pt[0].y) * usq3 +
      (pt[2].y - pt[1].y) * ut6 +
      (pt[3].y - pt[2].y) * tsq3, 

      (pt[1].z - pt[0].z) * usq3 +
      (pt[2].z - pt[1].z) * ut6 +
      (pt[3].z - pt[2].z) * tsq3);
  }

  PMatrix3D calcPMatrix3D(float t, PVector up) {
    return calcPMatrix3D(t, up, new PMatrix3D());
  }

  PMatrix3D calcPMatrix3D(float t, PVector up, PMatrix3D out) {
    PVector point;
    PVector k;

    if (t <= 0) {
      point = pt[0];
      k = PVector.sub(pt[1], pt[0]);
    } else if (t >= 1) {
      point = pt[3];
      k = PVector.sub(pt[3], pt[2]);
    } else {
      point = calcPoint(t);
      k = calcTangent(t);
    }

    k.normalize();

    PVector i = new PVector();
    PVector.cross(up, k, i);
    i.normalize();

    PVector j = new PVector();
    PVector.cross(k, i, j);
    j.normalize();

    out.set(
      i.x, j.x, k.z, point.x, 
      i.y, j.y, k.y, point.y, 
      i.z, j.z, k.z, point.z, 
      0, 0, 0, 1);
    return out;
  }

  Bezier scale(float scalar) {
    for (int i = 0, sz = pt.length; i < sz; ++i) {
      pt[i].mult(scalar);
    }
    return this;
  }

  Bezier translate(PVector v) {
    for (int i = 0, sz = pt.length; i < sz; ++i) {
      pt[i].add(v);
    }
    return this;
  }

  static Bezier random() {
    return random(new Bezier());
  }

  static Bezier random(Bezier out) {
    for (int i = 0, sz = out.pt.length; i < sz; ++i) {
      PVector.random3D(out.pt[i]);
    }
    return out;
  }
}

094
In the main sketch:

Bezier bezier = new Bezier();
int detail = 10;
PVector up = new PVector(0.0, 1.0, 0.0);

void setup() {
  size(526, 256, P3D);
  Bezier.random(bezier);
  bezier.scale(min(
    width, height) * 0.5);
  bezier.translate(
    new PVector(width * 0.5, height * 0.5));
}

void draw() {
  background(0xffffffff);
  lights();

  pushStyle();
  strokeWeight(1.5);
  stroke(0xff000000);
  noFill();
  bezier.draw((PGraphics3D)g);
  popStyle();

  float detailToStep = 1.0 / float(detail - 1);
  pushStyle();
  noStroke();
  for (int i = 0; i < detail; ++i) {
    float step = i * detailToStep;
    pushMatrix();
    applyMatrix(bezier.calcPMatrix3D(step, up));
    fill(lerpColor(0xffff0000, 0xff0000ff, step, HSB));
    box(20);
    popMatrix();
  }
  popStyle();
}

void mouseReleased() {
  Bezier.random(bezier);
  bezier.scale(
    min(width, height) * 0.5);
  bezier.translate(
    new PVector(width * 0.5, height * 0.5));
}

From there, you can expand to create a spline with multiple sets of anchor and control points beyond the initial four. Measures could also be taken to store pre-calculated points along the spline. Then look into applying the transformation to vertices of a mesh.

Some helpful resources may be Pomax’s A Primer on Bézier Curves, Jasper Flick’s C# implementation for Unity and Joachim Holmer’s talk A coder’s guide to spline-based procedural geometry (also for Unity). Three.js also supports 3D curves, and its source code is available.

1 Like

thanks im gonna to keep researching
here is what i want, how generate that kind of geometry bending.
i think that it must exist and vertex shader aproach

here i find what i want, in he-mesh library. but now i `ve a problem when i trying to modify parameters:

import wblut.nurbs.*;
import wblut.hemesh.*;
import wblut.core.*;
import wblut.geom.*;
import wblut.processing.*;
import wblut.math.*;



WB_Render render;
WB_BSpline C;
WB_Point[] points;
HE_Mesh mesh;
HEC_SweepTube creator;

void setup() {
  size(1000,1000,P3D);
  smooth(8);
  // Several WB_Curve classes are in development. HEC_SweepTube provides
  // a way of generating meshes from them.

  //Generate a BSpline
  points=new WB_Point[11];
  for (int i=0;i<11;i++) {
    points[i]=new WB_Point(5*(i-5)*(i-5), -200+40*i, random(100));
  }
  C=new WB_BSpline(points, 4);

  creator=new HEC_SweepTube();
 
  creator.setCurve(C);//curve should be a WB_BSpline
  creator.setSteps(40);
  creator.setFacets(8);
  creator.setCap(true, true); // Cap start, cap end?
  mesh=new HE_Mesh(creator); 
  //HET_Diagnosis.validate(mesh);
  render=new WB_Render(this);
}

void draw() {
   creator.setRadius(mouseX);

  
  background(55);
  directionalLight(255, 255, 255, 1, 1, -1);
  directionalLight(127, 127, 127, -1, -1, 1);
  translate(width/2,height/2);
  rotateY(mouseX*1.0f/width*TWO_PI);
  rotateX(mouseY*1.0f/height*TWO_PI);
  stroke(0);
  render.drawEdges(mesh);
  noStroke();
  render.drawFaces(mesh);
}

radius dont change anything… any body know why?

at the end of this page founded some good data.
but i dont have the math skills to implemented.

http://paulbourke.net/geometry/circlesphere/index.html

Hi vjjv,

Cool, that’s a great link. Come to think of it, you may also find Inigo Quilez’s work interesting, particularly his creation of 3D primitives and a Bezier curve with shaders.

As far as Processing goes, if the bend of the tube is governed by a function - say your making a helix, caduceus or sine wave - the approach I described earlier can be generalized to

  1. Calculate points on the ‘spine’ of the curve. (If you write functions which take the same inputs and return the same result, you can swap these functions with each other, e.g., creating functional interfaces if you want to get fancy.)
  2. Calculate foward directions that an object will face at each point.
  3. Calculate the left and right directions, then store left, right and forward in a lookAt matrix.
  4. Calculate the 2D form (cross-section) to be extruded (like a ring, circle).
  5. Extrude that form along the curve by multiplying the points in the form by a lookAt matrix on the spine.
  6. Connect and represent the transformed points (between beginShape and endShape). Depending on whether you use quadrilaterals or triangles as polygons you may have to vary the code, due to the clockwise winding from vertex to vertex around the polygon.

Depending on the function, the segments will not necessarily be evenly distributed along the curve, and you may not wish them to be. Sharp elbows, for example, will break the illusion of self-continuity. Problems in appearance will also arise from extreme twisting and the curve intersecting itself.

Expressed in vanilla Processing, this could look like

preview

// Number of segments along length of curve.
int segments = 256;

// Number of points in ring per segment.
int rdDetail = 42;

// To convert progress through loops to a percent.
float segToStep = 1.0 / float(segments - 1);

// Extents;
PVector minExt = new PVector();
PVector maxExt = new PVector();

// Vertical min/max stops.
float ymin0;
float ymax0;
float ymin1;
float ymax1;

// Change in vertical extents along curve.
float yDiminish = 0.3;

// Radius of ring at each segment.
float radmin = 40.0;
float radmax = 4.0;

// Frequency of wave.
float freq = 4.0;
float minFreq = 1.0;
float maxFreq = 5.0;

// Points along center of curve.
PVector[] points = new PVector[segments];

// Original 2D form to be extruded.
PVector[] ring = new PVector[rdDetail];

// Reference up vector when calculating lookAt matrix.
PVector ref = new PVector(0.0, 1.0, 0.0);

// Temporary variables for lookAt function.
PVector right = new PVector();
PVector up = new PVector();
PVector forward = new PVector();

// Lookat matrices to orient each ring-segment.
PMatrix3D[] matrices = new PMatrix3D[segments];

// Temporary matrix to store previous segment
// when drawing quadrilaterals.
PMatrix3D prevSegment = new PMatrix3D();

// Four corners of the quadrilateral which
// panels a segment.
PVector[] vs = {
  new PVector(), 
  new PVector(), 
  new PVector(), 
  new PVector() };

// Camera.
float halfw;
float halfh;
boolean isOrtho = true;
float camDist;
float animTheta = 0.0;
float orbitSpeed = 1 / 30.0;
PVector camPos = new PVector();
PVector camLook = new PVector();
PVector camUp = new PVector(0.0, 1.0, 0.0);

// Light info.
float[] lightClr = { 255.0, 245.0, 215.0 };
PVector lightDir = new PVector(0.0, 1.0, 0.0);
float[] ambientClr = { 48.0, 48.0, 48.0 };

// Colors, styles.
color bkgColor = 0xffffffff;
color startColor = 0xffff003f;
color stopColor = 0xff3f00ff;
color strokeColor = 0xff000000;
float strokeWeight = 0.0;

void setup() {
  size(512, 256, P3D);
  smooth(8);
  strokeWeight(strokeWeight);
  stroke(strokeColor);

  // Set horizontal extents.
  halfw = width * 0.5;
  minExt.x = -halfw * 0.9;
  maxExt.x = -minExt.x;

  // Set vertical extents.
  // They will change from the start
  // to the end of the curve.
  halfh = height * 0.5;
  ymin0 = -halfh * 0.9;
  ymax0 = -ymin0;
  ymin1 = ymin0 * yDiminish;
  ymax1 = ymax0 * yDiminish;

  // Calculate camera default distance.
  camDist = height * 0.86602;
  camPos.set(halfw, halfh, camDist);

  // Initialize objects.
  for (int seg = 0; seg < segments; ++seg) {
    points[seg] = new PVector();
    matrices[seg] = new PMatrix3D();
  }

  // Calculate points around a circle.
  float toTheta = TWO_PI / float(rdDetail);
  float scalar = 1.0;
  for (int i = 0; i < rdDetail; ++i) {
    float theta = i * toTheta;

    // Optional: alter the scale to break up
    // the circular shape.
    scalar = i % 3 == 0 ? 0.667 : 1.0;

    // Create vector from polar coordinates.
    ring[i] = new PVector(
      cos(theta) * scalar, 
      sin(theta) * scalar, 
      0.0);
  }
}

void draw() {

  // For camera orbit and frequency.
  animTheta += orbitSpeed;
  //animTheta = map(mouseX, 0, width, -PI, PI);
  float cosat = cos(animTheta);
  float sinat = sin(animTheta);

  // Animate frequency of curve.
  freq = lerp(minFreq, maxFreq, 0.5 + sinat * 0.5);
  float freqTwoPi = TWO_PI * freq;

  // Animate camera position.
  camPos.set(cosat * camDist, 0.0, sinat * camDist);

  // Switch between orthographic and perspective
  // camera with key press.
  if (isOrtho) { 
    ortho(-halfw, halfw, -halfh, halfh, EPSILON, 2000.0);
  } else { 
    perspective();
  }
  camera(camPos.x, camPos.y, camPos.z, 
    camLook.x, camLook.y, camLook.z, 
    camUp.x, camUp.y, camUp.z);
  background(bkgColor);

  // Lighting.
  directionalLight(
    lightClr[0], lightClr[1], lightClr[2], 
    lightDir.x, lightDir.y, lightDir.z);
  ambientLight(
    ambientClr[0], ambientClr[1], ambientClr[2]);

  // Step along the curve.
  float segStep, theta, fac, scl;
  int target, origin, fill;
  boolean onLastSegment;
  beginShape(TRIANGLES);
  for (int seg = 0; seg < segments; ++seg) {

    // Convert from range (0, detail) to range (0, 1).
    segStep = seg * segToStep;

    // Convert from range (0, 1) to range (0, TWO_PI).
    theta = segStep * freqTwoPi;

    // Set vertical extents for segment on curve.
    minExt.y = lerp(ymin0, ymin1, segStep);
    maxExt.y = lerp(ymax0, ymax1, segStep);

    // Function which determines points on curve.
    fac = animTheta + theta;
    if (mousePressed) {
      cosWave(fac, minExt, maxExt, segStep, points[seg]);
    } else {
      helix(fac, minExt, maxExt, segStep, points[seg]);
    }

    // Wait until after first ring to begin drawing.
    if (seg > 1) {
      fill = lerpColor(startColor, stopColor, segStep, RGB);
      fill(fill);
      for (int ang0 = 0, ang1; ang0 < rdDetail; ++ang0) {

        // The next angle in the ring, which wraps
        // around to zero when it reaches the end.
        ang1 = (ang0 + 1) % rdDetail;

        // Calculate each corner of the segment.
        prevSegment.mult(ring[ang0], vs[0]);
        matrices[seg].mult(ring[ang0], vs[1]);
        matrices[seg].mult(ring[ang1], vs[2]);
        prevSegment.mult(ring[ang1], vs[3]);

        // Draw triangle 1.
        vertex(vs[0].x, vs[0].y, vs[0].z);
        vertex(vs[1].x, vs[1].y, vs[1].z);
        vertex(vs[3].x, vs[3].y, vs[3].z);

        // Draw triangle 2.
        vertex(vs[3].x, vs[3].y, vs[3].z);
        vertex(vs[2].x, vs[2].y, vs[2].z);
        vertex(vs[1].x, vs[1].y, vs[1].z);
      }
    }

    // Cache previous lookAt matrix.
    prevSegment.set(matrices[seg]);

    // Decide what to do with the last segment on the curve,
    // where there is no 'forward' point to look at.
    // Even though this is not a loop, last looks to first.
    onLastSegment = seg == segments - 1;
    target = onLastSegment ? 0 : seg + 1;

    // (Origin assignment is meaningless according to
    // the above, since it is 'seg' either way.)
    origin = onLastSegment ? seg : seg;

    // Create a lookAt matrix.
    lookAt(
      points[origin], points[target], 
      ref, right, up, forward, 
      matrices[seg]);

    // Increase radius of extruded circle along the curve.
    scl = lerp(radmin, radmax, segStep);
    matrices[seg].scale(scl);
  }
  endShape(CLOSE);

  // Check frame rate in sketch window title.
  surface.setTitle(String.format("%.2f", frameRate));
}

PVector cosWave(float in, PVector minExt, PVector maxExt, 
  float segStep, PVector out) {
  return out.set(
    lerp(minExt.x, maxExt.x, segStep), 
    map(cos(in), -1.0, 1.0, minExt.y, maxExt.y), 
    0.0);
}

PVector helix(float in, PVector minExt, PVector maxExt, 
  float segStep, PVector out) {
  float radius = lerp(minExt.y, maxExt.y, segStep);

  // lerp can be replaced by manual calculation.
  // float u = 1.0 - segStep;
  // float radius = u * minExt.y + segStep * maxExt.y;
  return out.set(
    radius * cos(in), 
    radius * sin(in), 
    lerp(minExt.x, maxExt.x, segStep));
  // u * minExt.x + segStep * maxExt.x);
}

static PMatrix3D lookAt(PVector origin, PVector target, 
  PVector ref, PVector i, PVector j, PVector k, PMatrix3D out) {

  // Calculate forward.
  PVector.sub(target, origin, k);

  // If forward is near 0, return early.
  float m = k.magSq();
  if (m < EPSILON) {
    return out;
  }

  // Otherwise, normalize forward.
  k.mult(1.0 / sqrt(m));

  // Calculate right (i) by crossing forward (k)
  // with reference or world up.
  PVector.cross(k, ref, i);
  i.normalize();

  // Calculate local up (j) by crossing forward (k)
  // with right.
  PVector.cross(k, i, j);
  j.normalize();

  // Set lookAt matrix, with axes on columns
  // and axis components (x, y, z) on rows.
  out.set(
    i.x, j.x, k.x, origin.x, 
    i.y, j.y, k.y, origin.y, 
    i.z, j.z, k.z, origin.z, 
    0.0, 0.0, 0.0, 1.0);
  return out;
}

void keyReleased() {
  isOrtho = !isOrtho;
}

Anyway, looks like you’re on your way with the HE_Mesh library, hope you find the results you’re looking for!

2 Likes

Thanks. no, i decide implement my own code instead of using a librarie. So you re really help me. Im not a geometry expert, so i need time to understand the all thing. One question to begin my study, in this code you write, witch parameter do i need ajust just to make a tube without anycurve? just from point A to point B in rect line? thank you a lot!

im getting some results learning the code from

this function generates a randomCurve:


void randomCurve(float sclr) {
  PVector.random3D(b.pt[0]).mult(sclr);
  PVector.random3D(b.pt[1]).mult(sclr);
  PVector.random3D(b.pt[2]).mult(sclr);
  PVector.random3D(b.pt[3]).mult(sclr);

}

who can i do to start from a rect / no curve ? try this but it doesnt work:


  b.pt[0] = new PVector(0.1, 0, 0).mult(sclr);
  b.pt[1] = new PVector(0, 0, 0).mult(sclr);
  b.pt[2] = new PVector(0, 0, 0).mult(sclr);
  b.pt[3] = new PVector(-0.1, 0, -0).mult(sclr);

here is what i get, no tube:


the idea is deform the tube in time. but starting with a rect line tube. is this possible? thanks, learning a lot here.

assume that b.pt[1] and b.pt[2] are the point of controll of the bezier, and [0] and [3] are the start and end, right?

Hi vjjv,

Glad it’s working out for you. Yes, your assumption that b.pt[1] and b.pt[2] are controls and b.pt[0] and b.pt[3] are anchors is correct. As the controls get closer to the line created by the anchors, the curve straightens out. You can test this out with the following

341

int count = 15;
float invCount = 1.0 / float(count - 1);

float ap0x, ap0y, 
  cp0x, cp0y, 
  cp1x, cp1y, 
  ap1x, ap1y;

void setup() {
  size(512, 256);
  float halfW = width * 0.5;
  float halfH = height * 0.5;
  ap0x = random(halfW);
  ap0y = random(halfH);
  cp0x = random(halfW);
  cp0y = random(halfH);
  ap1x = random(halfW, width);
  ap1y = random(halfH, height);
  cp1x = random(halfW, width);
  cp1y = random(halfH, height);
}

void draw() {
  cp0x = map(mouseX, 0, width, ap0x, ap0x + width);
  cp1x = map(mouseX, 0, width, ap1x, ap1x - width);
  cp0y = map(mouseY, 0, height, ap0y, ap0y + height);
  cp1y = map(mouseY, 0, height, ap1y, ap1y - height);

  background(255);
  strokeWeight(1);
  stroke(0);
  bezier(ap0x, ap0y, 
    cp0x, cp0y, 
    cp1x, cp1y, 
    ap1x, ap1y);
    
  stroke(0, 255, 127);
  line(ap0x, ap0y, cp0x, cp0y);
  line(ap1x, ap1y, cp1x, cp1y);

  strokeWeight(5);
  stroke(255, 0, 0);
  for (int i = 0; i < count; ++i) {
    float t = i * invCount;
    float px = bezierPoint(ap0x, cp0x, cp1x, ap1x, t);
    float py = bezierPoint(ap0y, cp0y, cp1y, ap1y, t);
    point(px, py);
  }

  stroke(0, 0, 255);
  point(cp0x, cp0y);
  point(cp1x, cp1y);
}

This means you can change the random function to

void randomCurve(float sclr) {
  PVector.random3D(b.pt[0]).mult(sclr);
  //PVector.random3D(b.pt[1]).mult(sclr);
  //PVector.random3D(b.pt[2]).mult(sclr);
  PVector.random3D(b.pt[3]).mult(sclr);
  curve.pt[1] = PVector.lerp(curve.pt[0], curve.pt[3], 0.25);
  curve.pt[2] = PVector.lerp(curve.pt[0], curve.pt[3], 0.75);
}

To get straighter curves.

Should you want to make a straight cylinder with the helix code above, linear interpolation (lerp) distributes points along a line - the same processing as making a straight Bezier curve.

size(512, 256);
float x0 = random(width/2);
float x1 = random(width/2, width);
float y0 = random(height/2);
float y1 = random(height/2, height);
stroke(0);
strokeWeight(1);
line(x0, y0, x1, y1);
stroke(255, 0, 0);
strokeWeight(5);
int count = 20;
float invCount = 1 / float(count - 1);
for(int i = 0; i < count; ++i) {
  float step = i * invCount;
  float xp = lerp(x0, x1, step);
  float yp = lerp(y0, y1, step);
  point(xp, yp);
}

You can apply this idea by creating a function like this

PVector cylinder(float in, PVector minExt, PVector maxExt, 
  float segStep, PVector out) {
  return out.set(
    lerp(minExt.x, maxExt.x, segStep), 
    0.0, 0.0);
}

for a horizontal cylinder. A vertical cylinder would set the y component of the out vector. This function can be called around line 184 in lue of the helix or cosWave functions.

    //if (mousePressed) {
    //  cosWave(fac, minExt, maxExt, segStep, points[seg]);
    //} else {
    //  helix(fac, minExt, maxExt, segStep, points[seg]);
    //}
    cylinder(fac, minExt, maxExt, segStep, points[seg]);

This for-loop creates the circular cross section

  for (int i = 0; i < rdDetail; ++i) {
    float theta = i * toTheta;

    // Optional: alter the scale to break up
    // the circular shape.
    // scalar = i % 3 == 0 ? 0.667 : 1.0;

    // Create vector from polar coordinates.
    ring[i] = new PVector(
      cos(theta) * scalar, 
      sin(theta) * scalar, 
      0.0);
  }

and you can comment out the scalar = i % 3 line to remove the ridges and make a smooth ring. The cylinder’s taper is a result of the following.

    // Increase radius of extruded circle along the curve.
    scl = lerp(radmin, radmax, segStep);
    matrices[seg].scale(scl);

Hope that helps!

2 Likes

Just a lil’ code shortening: :grinning:

void randomCurve(final float sclr) {
  for (final PVector vec : b.pt)  PVector.random3D(vec, this).mult(sclr);
}
2 Likes

Ok… this is working, but i still dont understand the all process. Actually im working with the Bezier and Transform class. So i´ve this function randomCurve to choice the curve, Ok. Now i know that point control more close to de anchor points more straigth is the curve. But where exactly can i choose the start and end point? becouse as i said before, if i wrote that:

 b.pt[0] = new PVector(-0.5, 0.1, 0).mult(sclr);
  //b.pt[1] = new PVector(-0.5, 0, 0).mult(sclr);
  //b.pt[2] = new PVector(-0.5, 0, 0).mult(sclr);
  b.pt[3] = new PVector(0.5, 0.1, 0).mult(sclr);

  b.pt[1] = PVector.lerp(b.pt[0], b.pt[3], 0.2);
  b.pt[2] = PVector.lerp(b.pt[0], b.pt[3], 0.8);

…the tube is not drawing.

Rewrote that for better performance using PVector::set() and a non-static overload version of PVector::lerp() method, so it completely avoids instantiating redundant PVector objects: :smile_cat:

  1. set() / Reference / Processing.org
  2. lerp() / Reference / Processing.org
void lerpCurve(final float sclr) {
  final PVector[] vecs = b.pt;

  final PVector pt0 = vecs[0].set(-.5, .1, 0).mult(sclr);
  final PVector pt3 = vecs[3].set(.5, .1, 0).mult(sclr);

  vecs[1].set(pt0).lerp(pt3, .2);
  vecs[2].set(pt0).lerp(pt3, .8);
}
1 Like

Thanks both.

Im sorry for all this questions… do my best.

on this code… why this happens? this deformation at begin of the shape?

import peasy.*;
import peasy.org.apache.commons.math.*;
import peasy.org.apache.commons.math.geometry.*;

// Number of segments along length of curve.

PeasyCam cam;

// Number of segments along length of curve.
int segments = 9;

// Number of points in ring per segment.
int rdDetail = 50;

// To convert progress through loops to a percent.
float segToStep = 1.0 / float(segments - 1);

// Extents;
PVector minExt = new PVector();
PVector maxExt = new PVector();

// Vertical min/max stops.
float ymin0;
float ymax0;
float ymin1;
float ymax1;

// Change in vertical extents along curve.
float yDiminish = 0;

// Radius of ring at each segment.
float radmin = 20.0;
float radmax = 20.0;

// Points along center of curve.
PVector[] points = new PVector[segments];

// Original 2D form to be extruded.
PVector[] ring = new PVector[rdDetail];

// Reference up vector when calculating lookAt matrix.
PVector ref = new PVector(0.0, 1.0, 0.0);

// Temporary variables for lookAt function.
PVector right = new PVector();
PVector up = new PVector();
PVector forward = new PVector();

// Lookat matrices to orient each ring-segment.
PMatrix3D[] matrices = new PMatrix3D[segments];

// Temporary matrix to store previous segment
// when drawing quadrilaterals.
PMatrix3D prevSegment = new PMatrix3D();

// Four corners of the quadrilateral which
// panels a segment.
PVector[] vs = {
  new PVector(), 
  new PVector(), 
  new PVector(), 
  new PVector() };

float halfw;
float halfh;

void setup() {
  size(1000, 1000, P3D);
  smooth(8);

  cam = new PeasyCam(this, 200);
  // Set horizontal extents.
  halfw = width * 0.5;
  minExt.x = -halfw ;
  maxExt.x = -minExt.x;

  // Set vertical extents.
  // They will change from the start
  // to the end of the curve.
  halfh = height * 0.9;
  ymin0 = -halfh * 0.9;
  ymax0 = -ymin0;
  ymin1 = ymin0 * yDiminish;
  ymax1 = ymax0 * yDiminish;


  // Initialize objects.
  for (int seg = 0; seg < segments; ++seg) {
    points[seg] = new PVector();
    matrices[seg] = new PMatrix3D();
  }

  // Calculate points around a circle.
  float toTheta = TWO_PI / float(rdDetail);
  float scalar = 1.0;
  for (int i = 0; i < rdDetail; ++i) {
    float theta = i * toTheta;
    ring[i] = new PVector(
      cos(theta), 
      sin(theta), 
      0.0);
  }
}

void draw() {



  background(255);


  // Step along the curve.
  float segStep, theta, fac, scl;
  int target, origin, fill;
  boolean onLastSegment;
  beginShape(TRIANGLE);
  for (int seg = 0; seg < segments; ++seg) {

    // Convert from range (0, detail) to range (0, 1).
    segStep = seg * segToStep;

    // Convert from range (0, 1) to range (0, TWO_PI).


    // Set vertical extents for segment on curve.
    //minExt.y = lerp(ymin0, ymin1, segStep);
    //maxExt.y = lerp(ymax0, ymax1, segStep);

    // Function which determines points on curve.

    if (mousePressed) {
      //cosWave(0.5, minExt, maxExt, segStep, points[seg]);
    } else {
      cylinder(0.5, minExt, maxExt, segStep, points[seg]);
    }

    // Wait until after first ring to begin drawing.
    if (seg > 1) {
      noFill();
      stroke(0);
      strokeWeight(0.5);
      for (int ang0 = 0, ang1; ang0 < rdDetail; ++ang0) {

        // The next angle in the ring, which wraps
        // around to zero when it reaches the end.
        ang1 = (ang0 + 1) % rdDetail;

        // Calculate each corner of the segment.
        prevSegment.mult(ring[ang0], vs[0]);
        matrices[seg].mult(ring[ang0], vs[1]);
        matrices[seg].mult(ring[ang1], vs[2]);
        prevSegment.mult(ring[ang1], vs[3]);

        // Draw triangle 1.
        vertex(vs[0].x, vs[0].y, vs[0].z);
        vertex(vs[1].x, vs[1].y, vs[1].z);
        vertex(vs[3].x, vs[3].y, vs[3].z);

        // Draw triangle 2.
        vertex(vs[3].x, vs[3].y, vs[3].z);
        vertex(vs[2].x, vs[2].y, vs[2].z);
        vertex(vs[1].x, vs[1].y, vs[1].z);
      }
    }

    // Cache previous lookAt matrix.
    prevSegment.set(matrices[seg]);

    // Decide what to do with the last segment on the curve,
    // where there is no 'forward' point to look at.
    // Even though this is not a loop, last looks to first.
    onLastSegment = seg == segments - 1;
    target = onLastSegment ? 0 : seg + 1;

    // (Origin assignment is meaningless according to
    // the above, since it is 'seg' either way.)
    origin = onLastSegment ? seg : seg;

    // Create a lookAt matrix.
    lookAt(
      points[origin], points[target], 
      ref, right, up, forward, 
      matrices[seg]);

    // Increase radius of extruded circle along the curve.
    scl = lerp(radmin, radmax, segStep);
    matrices[seg].scale(scl);
  }
  endShape(CLOSE);

  // Check frame rate in sketch window title.
  surface.setTitle(String.format("%.2f", frameRate));
}

PVector cosWave(float in, PVector minExt, PVector maxExt, 
  float segStep, PVector out) {
  return out.set(
    lerp(minExt.x, maxExt.x, segStep), 
    map(map(noise(in/10), 0, 1, -1, 1), -1.0, 1.0, minExt.y, maxExt.y)*5, 
    0.0);
}

PVector helix(float in, PVector minExt, PVector maxExt, 
  float segStep, PVector out) {
  float radius = lerp(minExt.y, maxExt.y, segStep);

  // lerp can be replaced by manual calculation.
  // float u = 1.0 - segStep;
  // float radius = u * minExt.y + segStep * maxExt.y;
  return out.set(
    radius * cos(in), 
    radius * sin(in), 
    0);
  // u * minExt.x + segStep * maxExt.x);
}

static PMatrix3D lookAt(PVector origin, PVector target, 
  PVector ref, PVector i, PVector j, PVector k, PMatrix3D out) {

  // Calculate forward.
  PVector.sub(target, origin, k);

  // If forward is near 0, return early.
  float m = k.magSq();
  if (m < EPSILON) {
    return out;
  }

  // Otherwise, normalize forward.
  k.mult(1.0 / sqrt(m));

  // Calculate right (i) by crossing forward (k)
  // with reference or world up.
  PVector.cross(k, ref, i);
  i.normalize();

  // Calculate local up (j) by crossing forward (k)
  // with right.
  PVector.cross(k, i, j);
  j.normalize();

  // Set lookAt matrix, with axes on columns
  // and axis components (x, y, z) on rows.
  out.set(
    i.x, j.x, k.x, origin.x, 
    i.y, j.y, k.y, origin.y, 
    i.z, j.z, k.z, origin.z, 
    0.0, 0.0, 0.0, 1.0);
  return out;
}

PVector cylinder(float in, PVector minExt, PVector maxExt, 
  float segStep, PVector out) {
  return out.set(
    lerp(minExt.x, maxExt.x, segStep), 
    0, 
    0);
}

Hi vjjv,

I wrote the cylinder function to take the same information that the older functions did. But you can rewrite it any way you want. After revising it to take the start and end point, the function could look like

static PVector cylinder(PVector a, PVector b, float step, PVector out) {
  return out.set(a).lerp(b, step);
}

(Following some of GoToLoop’s advice about lerp.)

The twist at the end of the shape comes from a bad assumption I made in the original code. That assumption is about where the last point on your cylinder should look when there’s no more ‘forward’ direction information.

    // Decide what to do with the last segment on the curve,
    // where there is no 'forward' point to look at.
    // Even though this is not a loop, last looks to first.
    onLastSegment = seg == segments - 1;
    target = onLastSegment ? 0 : seg + 1;

    // (Origin assignment is meaningless according to
    // the above, since it is 'seg' either way.)
    origin = onLastSegment ? seg : seg;

The first revision, of the cylinder function, can help us solve the second issue with the twisting.

PVector tubeStart = new PVector();
PVector tubeEnd = new PVector();
PVector tubeDirection = PVector.sub(tubeEnd, tubeStart, 
  new PVector()).normalize();

The direction of the tube is its end point minus its start point. As of now, only the direction of the tube, not its length is important, so I normalized tubeDirection so it’s only 1 unit long.

So instead of the assumption above, instead I tried

    onLastSegment = seg == segments - 1;
    if (onLastSegment) {
      target.set(points[seg]).add(tubeDirection);
    } else {
      target.set(points[seg + 1]);
    }
    origin = points[seg];

Overall, this amounts to:

076

import peasy.*;
import peasy.org.apache.commons.math.*;
import peasy.org.apache.commons.math.geometry.*;

// NEW NEW NEW
PVector tubeStart = new PVector();
PVector tubeEnd = new PVector();
PVector tubeDirection = PVector.sub(tubeEnd, tubeStart, 
  new PVector()).normalize();
float tubeRange = 100;

PeasyCam cam;

// Number of segments along length of curve.
int segments = 9;

// Number of points in ring per segment.
int rdDetail = 50;

// To convert progress through loops to a percent.
float segToStep = 1.0 / float(segments - 1);

// Change in vertical extents along curve.
float yDiminish = 0;

// Radius of ring at each segment.
float radmin = 20.0;
float radmax = 20.0;

// Points along center of curve.
PVector[] points = new PVector[segments];

// Original 2D form to be extruded.
PVector[] ring = new PVector[rdDetail];

// Reference up vector when calculating lookAt matrix.
PVector ref = new PVector(0.0, 1.0, 0.0);

// Temporary variables for lookAt function.
PVector right = new PVector();
PVector up = new PVector();
PVector forward = new PVector();

// Lookat matrices to orient each ring-segment.
PMatrix3D[] matrices = new PMatrix3D[segments];

// Temporary matrix to store previous segment
// when drawing quadrilaterals.
PMatrix3D prevSegment = new PMatrix3D();

// Four corners of the quadrilateral which
// panels a segment.
PVector[] vs = {
  new PVector(), 
  new PVector(), 
  new PVector(), 
  new PVector() };

float halfw;
float halfh;

void setup() {
  size(500, 500, P3D);
  smooth(8);

  cam = new PeasyCam(this, 200);

  // NEW NEW NEW
  PVector.random3D(tubeStart).mult(tubeRange);
  PVector.random3D(tubeEnd).mult(tubeRange);

  // Initialize objects.
  for (int seg = 0; seg < segments; ++seg) {
    points[seg] = new PVector();
    matrices[seg] = new PMatrix3D();
  }

  // Calculate points around a circle.
  float toTheta = TWO_PI / float(rdDetail);
  for (int i = 0; i < rdDetail; ++i) {
    float theta = i * toTheta;
    ring[i] = new PVector(cos(theta), sin(theta), 0.0);
  }
}

void draw() {
  // Changed so reference markers are easier to see.
  background(0.0);
  noFill();
  stroke(255.0);
  strokeWeight(0.5);

  // NEW NEW NEW
  drawRefMarks(10, width, 5.0);
  drawOrigin(10.0);

  // Step along the curve.
  float segStep;
  float radius;
  PVector origin, target = new PVector();
  boolean onLastSegment;
  beginShape(TRIANGLE);
  for (int seg = 0; seg < segments; ++seg) {

    // Convert from range (0, detail) to range (0, 1).
    segStep = seg * segToStep;

    cylinder(tubeStart, tubeEnd, segStep, points[seg]);

    // Wait until after first ring to begin drawing.
    if (seg > 1) {
      for (int ang0 = 0, ang1; ang0 < rdDetail; ++ang0) {

        // The next angle in the ring, which wraps
        // around to zero when it reaches the end.
        ang1 = (ang0 + 1) % rdDetail;

        // Calculate each corner of the segment.
        prevSegment.mult(ring[ang0], vs[0]);
        matrices[seg].mult(ring[ang0], vs[1]);
        matrices[seg].mult(ring[ang1], vs[2]);
        prevSegment.mult(ring[ang1], vs[3]);

        // Draw triangle 1.
        vertex(vs[0].x, vs[0].y, vs[0].z);
        vertex(vs[1].x, vs[1].y, vs[1].z);
        vertex(vs[3].x, vs[3].y, vs[3].z);

        // Draw triangle 2.
        vertex(vs[3].x, vs[3].y, vs[3].z);
        vertex(vs[2].x, vs[2].y, vs[2].z);
        vertex(vs[1].x, vs[1].y, vs[1].z);
      }
    }

    // Cache previous lookAt matrix.
    prevSegment.set(matrices[seg]);

    // THIS IS THE PROBLEM HERE.
    // It is incorrect to assume that a cylinder is a loop,
    // and that the end cap of a cylinder will look to
    // the front.
    //onLastSegment = seg == segments - 1;
    //target = onLastSegment ? 0 : seg + 1;
    //origin = onLastSegment ? seg : seg;

    onLastSegment = seg == segments - 1;
    if (onLastSegment) {
      target.set(points[seg]).add(tubeDirection);
    } else {
      target.set(points[seg + 1]);
    }
    origin = points[seg];

    // Create a lookAt matrix.    
    lookAt(
      origin, target, 
      ref, right, up, forward, 
      matrices[seg]);

    // Increase radius of extruded circle along the curve.
    radius = lerp(radmin, radmax, segStep);
    matrices[seg].scale(radius);
  }
  endShape(CLOSE);

  // Check frame rate in sketch window title.
  surface.setTitle(String.format("%.2f", frameRate));
}

// NEW NEW NEW
static PVector cylinder(PVector a, PVector b, float step, PVector out) {
  return out.set(a).lerp(b, step);
}

void drawOrigin(float len) {
  pushStyle();
  strokeWeight(1.5);
  stroke(255.0, 0.0, 0.0);
  line(0.0, 0.0, 0.0, len, 0.0, 0.0);
  stroke(0.0, 255.0, 0.0);
  line(0.0, 0.0, 0.0, 0.0, len, 0.0);
  stroke(0.0, 0.0, 255.0);
  line(0.0, 0.0, 0.0, 0.0, 0.0, len);
  popStyle();
}

void drawRefMarks(int count, float refSpan, float markerSize) {
  pushStyle();
  strokeWeight(1.0);
  float toPercent = 1.0 / float(count - 1);
  for (int i = 0; i < count; ++i) {
    float iPrc = i * toPercent;
    float mz = lerp(-refSpan, refSpan, iPrc);

    for (int j = 0; j < count; ++j) {
      float jPrc = j * toPercent;
      float my = lerp(-refSpan, refSpan, jPrc);

      for (int k = 0; k < count; ++k) {
        float kPrc = k * toPercent;
        float mx = lerp(-refSpan, refSpan, kPrc);

        float avgPrc = (iPrc + jPrc + kPrc) * 0.33333333;
        color strokeColor = lerpColor(
          0xafff0000, 0xaf0000ff, 
          avgPrc, HSB);
        stroke(strokeColor);
        line(mx - markerSize, my, mz, 
          mx + markerSize, my, mz);
        line(mx, my - markerSize, mz, 
          mx, my + markerSize, mz);
        line(mx, my, mz - markerSize, 
          mx, my, mz + markerSize);
      }
    }
  }
  popStyle();
}

static PMatrix3D lookAt(PVector origin, PVector target, 
  PVector ref, PVector i, PVector j, PVector k, PMatrix3D out) {

  // Calculate forward.
  PVector.sub(target, origin, k);

  // If forward is near 0, return early.
  float m = k.magSq();
  if (m < EPSILON) {
    return out;
  }

  // Otherwise, normalize forward.
  k.mult(1.0 / sqrt(m));

  // Calculate right (i) by crossing forward (k)
  // with reference or world up.
  PVector.cross(k, ref, i);
  i.normalize();

  // Calculate local up (j) by crossing forward (k)
  // with right.
  PVector.cross(k, i, j);
  j.normalize();

  // Set lookAt matrix, with axes on columns
  // and axis components (x, y, z) on rows.
  out.set(
    i.x, j.x, k.x, origin.x, 
    i.y, j.y, k.y, origin.y, 
    i.z, j.z, k.z, origin.z, 
    0.0, 0.0, 0.0, 1.0);
  return out;
}

(I flipped the background and stroke colors to make some reference markers easier for me to see; they can be changed back.)

Hope that moves the ball forward.

[EDIT: Looks like there’s an extra problem with my original code in that the mesh drawn doesn’t match the length of the line and the segments doesn’t match that set at the top.]

2 Likes

Oh thanks ! this is a great sandbox to play and learn. As you see, i needed to understand how to construct a rect tube first, and idenfity in where part the start point and end point are defined. I see the error, it seems that the start point is displace according the tubeRange (more tube range, more displacement)…

if i comment

(tubeStart).mult(tubeRange);
 //(tubeEnd).mult(tubeRange);

the start point is correct, but of course the tube is not extrude.

for the other hand, if i want to set by myself the start point and end point, i still have the problem when i want to draw a rect curve, like:

PVector tubeStart = new PVector(0,0,0);
PVector tubeEnd = new PVector(1, 0, 0);
float tubeRange = 2000;

nothing is drawing, but if i added:

PVector tubeStart = new PVector(0,0,0);
PVector tubeEnd = new PVector(1, 0, 0.1);

the tube is draw, but whit that small displace to z exes.

this is : OK, i want to draw my tube from 0x to 1x (multypling by tubeRange = 0, 2000), this is rigth?

thanks again! PD: i m reading you articles, there are great!

Hi vjjv,

I’ll take a look at the mesh drawing error when I get a chance… I feel like I’ve made run into this – an off-by-one or fencepost error – before. In the meantime, there’s a Github link at the way bottom of the article you linked earlier – with the 3D bezier tube – that doesn’t seem to have this problem, and is interactive to boot.

It may help to have a gander at some of PVector’s functionality (reference, source code), read Dan Shiffman’s Nature of Code and/or watch his videos. Vectors can be confusing, because sometimes they hold info about points (x, y, z) in a space governed by the Cartesian coordinate system… and sometimes they hold info about directions of a certain length (or forces of a certain magnitude). I posted some snippets below to demo what I mean.

I don’t do much in WebGL shaders, but when I do, I find I’m often working with vectors in the range [0, 1], [-1, 1] or vectors that have a length of 1. So maybe that’s part of what’s confusing, as well.

If you generate a bunch of random vectors as directions, you get a sphere… you’re using a spherical coordinate system.
001

int count = 2000;
float strokeWeight = 5.0;
float scale = 0.0;
color[] clrs = new color[count];
PVector[] samples = new PVector[count];
PVector v;

void setup() {
  size(512, 256, P3D);
  scale = min(width, height) * 0.5;
  PVector center = new PVector(
    width * 0.5, 
    height * 0.5, 
    0.0);
  for (int i = 0; i < count; ++i) {
    clrs[i] = lerpColor(0xffff0000, 0xff0000ff, 
      random(0.0, 1.0), HSB);
    samples[i] = PVector.random3D(new PVector());
    samples[i].mult(scale);
    samples[i].add(center);
  }
}

void draw() {
  background(255.0);
  strokeWeight(strokeWeight);
  for (int i = 0; i < count; ++i) {
    v = samples[i];
    stroke(clrs[i]);
    point(v.x, v.y, v.z);
  }
}

When you generate a bunch of random points, you get a cube. You’re working in the Cartesian coordinate system.
001

int count = 2000;
float strokeWeight = 5.0;
float scale = 0.0;
color[] clrs = new color[count];
PVector[] samples = new PVector[count];
PVector v;
PVector center = new PVector();

void setup() {
  size(512, 256, P3D);
  float halfW = width * 0.5;
  float halfH = height * 0.5;
  float dist = 2.0 * height * 0.86602;
  center.set(
    halfW, halfH, 0.0);

  // Z-dimension arbitrarily set.
  PVector extents = new PVector(
    halfW, halfH, (halfH + halfW) * 0.5);

  for (int i = 0; i < count; ++i) {
    clrs[i] = lerpColor(0xffff0000, 0xff0000ff, 
      random(0.0, 1.0), HSB);
    samples[i] = new PVector(
      random(-extents.x, extents.x), 
      random(-extents.y, extents.y), 
      random(-extents.z, extents.z));
    samples[i].add(center);
  }

  ortho(-extents.x * 2.0, extents.x * 2.0, 
    -extents.y * 2.0, extents.y * 2.0);
  camera(dist, -dist, dist, 
    center.x, center.y, center.z, 
    0.0, 1.0, 0.0);
}

void draw() {
  background(255.0);
  strokeWeight(strokeWeight);
  for (int i = 0; i < count; ++i) {
    v = samples[i];
    stroke(clrs[i]);
    point(v.x, v.y, v.z);
  }
}

You can do this with a spherical coordinate system, too, like your code at the start of this thread.
001

int count = 2000;
float strokeWeight = 5.0;
float scale = 0.0;
color[] clrs = new color[count];
PVector[] samples = new PVector[count];
PVector v;
PVector center = new PVector();

void setup() {
  size(512, 256, P3D);
  noStroke();
  float halfW = width * 0.5;
  float halfH = height * 0.5;
  center.set(
    width * 0.5, height * 0.5, 0.0);

  // Z-dimension arbitrarily set.
  float rad = min(halfW, halfH);
  PVector extents = new PVector(
    rad, (halfH + halfW) * 0.5, rad);

  for (int i = 0; i < count; ++i) {
    clrs[i] = lerpColor(0xffff0000, 0xff0000ff, 
      random(0.0, 1.0), HSB);
    float t = random(-PI, PI);
    samples[i] = new PVector(
      cos(t) * extents.x, 
      random(-extents.y, extents.y), 
      sin(t) * extents.z);
    samples[i].add(center);
  }

  float dist = 2.0 * height * 0.86602;
  ortho(-width, width, 
    -height, height);
  camera(dist, -dist, dist, 
    center.x, center.y, center.z, 
    0.0, 1.0, 0.0);
}

void draw() {
  background(255.0);
  //directionalLight(255, 255, 255, 
  //  0.0, 0.8, -0.6);
  strokeWeight(strokeWeight);
  for (int i = 0; i < count; ++i) {
    v = samples[i];

    // Very slow frame rate, but will make
    // volume clearer with directionalLight.
    //pushMatrix();
    //translate(v.x, v.y, v.z);
    //fill(clrs[i]);
    //sphere(strokeWeight * 2);
    //popMatrix();

    stroke(clrs[i]);
    point(v.x, v.y, v.z);
  }
}

Which brings me to a suggestion. You can use divide and conquer as a general strategy to reach your final design. You can prototype the function that changes the tube separately from the mesh’s geometry. I think you mentioned Perlin noise earlier, so I used that some. (See the function foo at the bottom.)

int count = 2000;
float strokeWeight = 5.0;
float scale = 0.0;
color[] clrs = new color[count];
PVector[] samples = new PVector[count];
PVector v = new PVector();
PVector center = new PVector();

void setup() {
  size(512, 256, P3D);
  noStroke();
  float halfW = width * 0.5;
  float halfH = height * 0.5;
  center.set(width * 0.5, height * 0.5);
  float rad = min(halfW, halfH);
  PVector extents = new PVector(
    rad, (halfH + halfW) * 0.5, rad);

  for (int i = 0; i < count; ++i) {
    clrs[i] = lerpColor(0xffff0000, 0xff0000ff, 
      random(0.0, 1.0), HSB);
    float t = random(-PI, PI);
    samples[i] = new PVector(
      cos(t) * extents.x, 
      random(-extents.y, extents.y), 
      sin(t) * extents.z);
    samples[i].add(center);
  }

  float dist = 2.0 * height * 0.86602;
  ortho(-width, width, 
    -height, height);
  camera(dist, -dist, dist, 
    center.x, center.y, center.z, 
    0.0, 1.0, 0.0);
}

void draw() {
  background(255.0);
  strokeWeight(strokeWeight);
  for (int i = 0; i < count; ++i) {
    foo(this, i / float(count - 1), samples[i], v);
    stroke(clrs[i]);
    point(v.x, v.y, v.z);
  }
}

// So long as foo either returns and/or modifies a
// PVector, you can include whatever parameters in
// the function signature that work for you.
static PVector foo(PApplet pa, float fac, PVector in, PVector out) {
  
  // These variables could also be supplied to the function
  // from the outside.
  float anim = pa.frameCount * 0.05;
  float ampl = 75.0;
  float halfAmpl = ampl * 0.5;

  // Perlin noise outputs a float value in the range [0, 1].
  // You can shift that to the range [-1, 1] by multiplying
  // by 2 and subtracting 1. Multiply that formula by an amplifier,
  // called noiseOut scale and you get ampl * noise - halfAmpl;.
  return out.set(
    in.x + ampl * pa.noise(in.y, fac, anim) - halfAmpl,
    in.y + ampl * sin(anim), 
    in.z + ampl * pa.noise(in.x, fac, anim) - halfAmpl);
}

Hope that helps.

2 Likes

comment this:

if (seg > 1) {

and tube begin where correspond.
maybe is not a definive solution…

1 Like

Hi vjjv,

Yeah, making that change is a good idea, lol! After doing that, I count 7 tube sections and 8 rings when the value of the int segments is 9.

The mash-up of the Bezier code with the earlier code is below. My hack is to make the points which describe the spine of the tube 1 longer than the points which draw the tube. Functions which set (update) the tube’s vertices are split from functions which draw them. This leads to more for-loops than would be good for performance. While testing out the code, though, it’s hopefully easier to read, understand and change.

preview

import peasy.*;
import peasy.org.apache.commons.math.*;
import peasy.org.apache.commons.math.geometry.*;

// Fence posting - 9 linear detail = 8 segments
int linearDetail = 9;
int radialDetail = 50;

// Add more numbers to this array for tube radius.
// Sharp changes in tube radius result from this
// array being eased linearly. See smoothStep, easeIn
// functions for better options.
float[] taper = { 100, 25 };

// Turn on/off with arrow keys.
boolean showFill = false;
boolean lights = false;
boolean showStroke = true;

// Changed in setup when width, height are known.
PVector startPoint = new PVector();
PVector endPoint = new PVector();

color spineColor = 0xffff7f00;
color tubeColor = 0xff007fff;
color tubeStroke = 0xffffffff;

PeasyCam cam;
float lnDtlf = 1.0 / float(linearDetail - 1);
float rdDtlf = 1.0 / float(radialDetail);
PVector[] spine = new PVector[linearDetail + 1];
PMatrix3D[] matrices = new PMatrix3D[linearDetail];
PVector[][] vs = new PVector[linearDetail][radialDetail];

// For lookAt function.
PVector refUp = new PVector(0.0, 1.0, 0.0);
PVector right = new PVector();
PVector up = new PVector();
PVector forward = new PVector();

void setup() {
  size(512, 512, P3D);
  pixelDensity(displayDensity());
  smooth(8);
  cam = new PeasyCam(this, height);

  // Starting and ending points of cylinder spine.
  startPoint.set(-width * .35, -height * .35, height * 0.5);
  endPoint.set(width * .35, height * .35, -height * 0.5);

  for (int i = 0; i < linearDetail; ++i) {
    matrices[i] = new PMatrix3D();
    spine[i] = new PVector();
    for (int j = 0; j < radialDetail; ++j) {
      vs[i][j] = new PVector();
    }
  }
  spine[linearDetail] = new PVector();

  setSpine(spine, startPoint, endPoint, 0.5);
  //setMatrices(spine, matrices, refUp, right, up, forward);
  setVertices(matrices, vs);

  // Make sure that 100% of the line has been traversed.
  // spine[n - 2] should equal end point, while spine[n - 1]
  // should exceed end point due to unclamped lerp.
  if (spine.length < 15) {
    println(startPoint, endPoint);
    printArray(spine);
  }
}

void draw() {
  background(0.0);
  if (lights) {
    directionalLight(255.0, 255.0, 255.0, 
      0.0, 0.8, -0.6);
    ambientLight(32.0, 32.0, 32.0);
  } else {
    noLights();
  }

  setSpine(spine, startPoint, endPoint, frameCount * 0.01);
  //setMatrices(spine, matrices, refUp, right, up, forward);
  setVertices(matrices, vs);

  drawRefMarks(12, width, 10.0);
  drawOrigin(25.0);
  drawSpine(spine, 3.0, spineColor);
  drawTube(vs, tubeStroke, tubeColor);

  surface.setTitle(String.format("%.1f", frameRate));
}

void keyReleased() {
  if (keyCode == UP) {
    showFill = !showFill;
  }

  if (keyCode == DOWN) {
    showStroke = !showStroke;
  }

  if (keyCode == RIGHT) {
    lights = !lights;
  }
}

static PVector cylinder(PVector a, PVector b, float step, float anim, 
  PVector out) {
  // Anim variable unused so far.
  out.set(a).lerp(b, step);
  return out;
}

static PVector helix(PVector a, PVector b, float step, float anim, 
  PVector out) {
  float radius = lerp(a.y, b.y, step);
  float t = (step * anim);
  out.set(a).lerp(b, step);
  out.x += radius * cos(t);
  out.y += radius * sin(t);
  out.z += lerp(a.x, b.x, step) * 2.0;
  return out;
}

PVector[] setSpine(PVector[] spine, PVector startPoint, 
  PVector endPoint, float anim) {
  for (int i = 0; i < linearDetail + 1; ++i) {
    float step = i * lnDtlf;
    cylinder(startPoint, endPoint, step, anim, spine[i]);
    //helix(startPoint, endPoint, step, anim, spine[i]);
  }
  return spine;
}

PVector setCrossSection(float theta, float radius, PVector out) {
  return out.set(cos(theta) * radius, sin(theta) * radius);
}

// Combined with setVertices.
//PMatrix3D[] setMatrices(PVector[] spine, PMatrix3D[] ms, PVector refUp, 
//  PVector right, PVector up, PVector forward) {
//  for (int i = 0; i < linearDetail; ++i) {
//    lookAt(spine[i], spine[i + 1], 
//      refUp, right, up, forward, ms[i]);
//  }
//  return ms;
//}

PVector[][] setVertices(PMatrix3D[] ms, PVector[][] vs) {
  PVector v = new PVector();
  PMatrix3D m;
  for (int i = 0, j; i < linearDetail; ++i) {
    float istp = 1.0 - i * lnDtlf;

    // Radius of cylinder set here.
    float sclr = lerp(taper, istp);

    // Brought in from setMatrices.
    m = lookAt(spine[i], spine[i + 1], 
      refUp, right, up, forward, ms[i]);
    for (j = 0; j < radialDetail + 1; ++j) {
      float jstp;
      if (j < radialDetail) {
        jstp = j * rdDtlf;
        float t = TWO_PI * jstp;
        setCrossSection(t, sclr, v);
        m.mult(v, vs[i][j]);
      }
      jstp = j * rdDtlf;
    }
  }
  return vs;
}

void drawOrigin(float len) {
  pushStyle();
  strokeWeight(1.5);
  stroke(255.0, 0.0, 0.0);
  line(0.0, 0.0, 0.0, len, 0.0, 0.0);
  stroke(0.0, 255.0, 0.0);
  line(0.0, 0.0, 0.0, 0.0, len, 0.0);
  stroke(0.0, 0.0, 255.0);
  line(0.0, 0.0, 0.0, 0.0, 0.0, len);
  popStyle();
}

void drawRefMarks(int count, float refSpan, float markerSize) {
  pushStyle();
  strokeWeight(1.0);
  float toPercent = 1.0 / float(count - 1);
  for (int i = 0; i < count; ++i) {
    float iPrc = i * toPercent;
    float mz = lerp(-refSpan, refSpan, iPrc);
    for (int j = 0; j < count; ++j) {
      float jPrc = j * toPercent;
      float my = lerp(-refSpan, refSpan, jPrc);
      for (int k = 0; k < count; ++k) {
        float kPrc = k * toPercent;
        float mx = lerp(-refSpan, refSpan, kPrc);
        float avgPrc = (iPrc + jPrc + kPrc) * 0.33333333;
        color strokeColor = lerpColor(
          0xafff0000, 0xaf0000ff, 
          avgPrc, HSB);
        stroke(strokeColor);
        line(mx - markerSize, my, mz, 
          mx + markerSize, my, mz);
        line(mx, my - markerSize, mz, 
          mx, my + markerSize, mz);
        line(mx, my, mz - markerSize, 
          mx, my, mz + markerSize);
      }
    }
  }
  popStyle();
}

static PMatrix3D lookAt(PVector origin, PVector target, 
  PVector ref, PVector i, PVector j, PVector k, PMatrix3D out) {
  PVector.sub(target, origin, k);
  float m = k.magSq();
  if (m < EPSILON) {
    return out;
  }
  k.mult(1.0 / sqrt(m));
  PVector.cross(k, ref, i);
  i.normalize();
  PVector.cross(k, i, j);
  j.normalize();
  out.set(
    i.x, j.x, k.x, origin.x, 
    i.y, j.y, k.y, origin.y, 
    i.z, j.z, k.z, origin.z, 
    0.0, 0.0, 0.0, 1.0);
  return out;
}

// Last point on spine is not drawn.
void drawSpine(PVector[] spine, float strokeWeight, color stroke) {
  pushStyle();
  noFill();
  strokeWeight(strokeWeight);
  stroke(stroke);
  beginShape(LINE_STRIP);
  for (int i = 0; i < linearDetail; ++i) {
    vertex(spine[i].x, spine[i].y, spine[i].z);
  }
  endShape();
  popStyle();
}

void drawCaps(PVector[] vs, color stroke, color fill) {
  pushStyle();

  if (showStroke) {
    strokeWeight(1.0);
    stroke(stroke);
  } else {
    noStroke();
  }

  if (showFill) {
    fill(fill);
  } else {
    noFill();
  }

  beginShape(TRIANGLE_FAN);
  for (int j = 0; j < radialDetail; ++j) {
    vertex(vs[j].x, vs[j].y, vs[j].z);
  }
  endShape(CLOSE);
  popStyle();
}

void drawTube(PVector[][] vs, color stroke, color fill) {

  // Draw end caps.
  drawCaps(vs[0], stroke, fill);
  drawCaps(vs[linearDetail - 1], stroke, fill);

  pushStyle();

  if (showStroke) {
    strokeWeight(1.0);
    stroke(stroke);
  } else {
    noStroke();
  }

  if (showFill) {
    fill(fill);
  } else {
    noFill();
  }

  for (int i = 1, j; i < linearDetail; ++i) {
    int l = i - 1;
    for (j = 0; j < radialDetail; ++j) {
      int m = j + 1;
      int n = m % radialDetail;

      //beginShape(QUADS);
      //vertex(vs[l][j].x, vs[l][j].y, vs[l][j].z);
      //vertex(vs[i][j].x, vs[i][j].y, vs[i][j].z);
      //vertex(vs[i][n].x, vs[i][n].y, vs[i][n].z);
      //vertex(vs[l][n].x, vs[l][n].y, vs[l][n].z);
      //endShape(CLOSE);

      beginShape(TRIANGLES);
      vertex(vs[l][j].x, vs[l][j].y, vs[l][j].z);
      vertex(vs[i][j].x, vs[i][j].y, vs[i][j].z);
      vertex(vs[i][n].x, vs[i][n].y, vs[i][n].z);

      vertex(vs[l][j].x, vs[l][j].y, vs[l][j].z);
      vertex(vs[i][n].x, vs[i][n].y, vs[i][n].z);
      vertex(vs[l][n].x, vs[l][n].y, vs[l][n].z);
      endShape(CLOSE);
    }
  }
  popStyle();
}

// Taper float array is fed to this function to control the
// tube's radius.
static float lerp(float[] array, float t) {
  int sz = array.length;
  if (sz == 1 || t <= 0.0) {
    return array[0];
  } else if (t >= 1.0) {
    return array[sz - 1];
  }
  float sclt = t * float(sz - 1);
  int i = int(sclt);
  return lerp(array[i], array[i + 1], sclt - i);
}

Best,

1 Like

hi again! i`ve a last question… @behreajj

how do you exactly use the UV coordinates in this shape?

float lnDtlf = 1.0 / float(linearDetail - 1);
float rdDtlf = 1.0 / float(radialDetail);

void drawTube(PVector[][] vs, color stroke, color fill) {

  // Draw end caps.
  drawCaps(vs[0], stroke, fill);
  drawCaps(vs[linearDetail - 1], stroke, fill);

  pushStyle();

  if (showStroke) {
    strokeWeight(1.0);
    stroke(stroke);
  } else {
    noStroke();
  }

  if (showFill) {
    fill(fill);
  } else {
    noFill();
  }

  for (int i = 1, j; i < linearDetail; ++i) {
    int l = i - 1;
    for (j = 0; j < radialDetail; ++j) {
      int m = j + 1;
      int n = m % radialDetail;

      //beginShape(QUADS);
      //vertex(vs[l][j].x, vs[l][j].y, vs[l][j].z);
      //vertex(vs[i][j].x, vs[i][j].y, vs[i][j].z);
      //vertex(vs[i][n].x, vs[i][n].y, vs[i][n].z);
      //vertex(vs[l][n].x, vs[l][n].y, vs[l][n].z);
      //endShape(CLOSE);

      beginShape(TRIANGLES);
      vertex(vs[l][j].x, vs[l][j].y, vs[l][j].z);
      vertex(vs[i][j].x, vs[i][j].y, vs[i][j].z);
      vertex(vs[i][n].x, vs[i][n].y, vs[i][n].z);

      vertex(vs[l][j].x, vs[l][j].y, vs[l][j].z);
      vertex(vs[i][n].x, vs[i][n].y, vs[i][n].z);
      vertex(vs[l][n].x, vs[l][n].y, vs[l][n].z);
      endShape(CLOSE);
    }
  }
  popStyle();
}

thanks!