Skip to content

Instantly share code, notes, and snippets.

@nsbalbi
Last active August 21, 2022 23:17
Show Gist options
  • Save nsbalbi/ab299269bb0dbfd9c1296dc3bdaad39b to your computer and use it in GitHub Desktop.
Save nsbalbi/ab299269bb0dbfd9c1296dc3bdaad39b to your computer and use it in GitHub Desktop.
Astral Knot
// Processing Code By Nicholas Sbalbi (@nsbalbi)
// Twitter post: https://twitter.com/nsbalbi/status/1413940646063317001
// Associated tutorial: https://nsbalbi.github.io/Blog%20Posts/blog_knot.html
// Github gist: https://gist.github.com/nsbalbi/ab299269bb0dbfd9c1296dc3bdaad39b
// 7-12-2021 Updated: 7-12-2022
// Motion blur via @beesandbombs
float t;
int[][] result;
int nFrames = 160;
// Motion blur parameters
int samplesPerFrame = 8; // number of drawings used to render each final frame with motion blur
float shutterAngle = 0.4; // kind of the time interval used for each frame in the motion blur
// Knot parameters
float scale = 20;
float r = 8;
// Camera parameters
float cameraX = 175; float cameraY = 0; float cameraZ = 0;
// Particle parameters
int numParticles = 1000;
Particle[] particles = new Particle[numParticles];
void setup() {
size(600,600,P3D);
result = new int[width*height][3];
// initialize particles
for (int i = 0; i < numParticles; i++) {
particles[i] = new Particle();
}
}
void draw() {
// Outer draw function (for adding screen effects)
// @beesandbombs Motion Blur Template
//////////////////////////////////////////////////////////////////////////////
for (int i=0; i<width*height; i++)
for (int a=0; a<3; a++)
result[i][a] = 0;
for (int sa=0; sa<samplesPerFrame; sa++) {
t = map(frameCount-1 + sa*shutterAngle/samplesPerFrame, 0, nFrames, 0, 1);
draw_();
loadPixels();
for (int i=0; i<pixels.length; i++) {
result[i][0] += pixels[i] >> 16 & 0xff;
result[i][1] += pixels[i] >> 8 & 0xff;
result[i][2] += pixels[i] & 0xff;
}
}
loadPixels();
for (int i=0; i<pixels.length; i++)
pixels[i] = 0xff << 24 |
int(result[i][0]*1.0/samplesPerFrame) << 16 |
int(result[i][1]*1.0/samplesPerFrame) << 8 |
int(result[i][2]*1.0/samplesPerFrame);
updatePixels();
// Template End
//////////////////////////////////////////////////////////////////////////////
saveFrame("Output/fr###.png");
println(frameCount,"/",nFrames);
if (frameCount==nFrames)
exit();
}
void draw_() {
// Main draw function
background(0);
camera(cameraX,cameraY,cameraZ,0,0,0,0,-1,0);
pushMatrix();
rotateY(2*PI*t); // rotate Y over time
rotateX(2*PI*t); // " X
// drawPath(); // would draw only knot path
drawSurface(); // draws knot surface
// draw particles
for (int i = 0; i < numParticles; i++) {
particles[i].drawPoint();
}
popMatrix();
}
PVector knotPath(float s) {
// Returns knot path parameterized by s, s loops every 2PI
// Trefoil Knot
float x = sin(s) + 2*sin(2*s);
float y = cos(s) - 2*cos(2*s);
float z = -sin(3*s);
return new PVector(scale*x,scale*y,scale*z);
}
PVector dKnotPath(float s) {
// Returns tangent to knot path parameterized by s, s loops every 2PI
// Trefoil Knot
float x = cos(s) + 4*cos(2*s);
float y = -sin(s) + 4*sin(2*s);
float z = -3*cos(3*s);
PVector out = new PVector(x,y,z);
return out.normalize();
}
PVector pipe(float s, float theta, float r) {
// Returns location on knot given parameter s along path and theta around pipe surface with radius r
// s and theta loop every 2PI
// randomly generate vector to cross the tangent with to generate a normal vector
// must avoid the crossing vector from being ~ parallel to the tangent vector
PVector vect = new PVector(1*s,2*s,30*s); // fudge with crossing vector to prevent zeroing out
PVector gamma = knotPath(s); // knot path
PVector tangent = dKnotPath(s); // knot tangent
PVector normal = tangent.cross(vect).normalize(); // normal vector to knot
PVector binormal = normal.cross(tangent).normalize(); // binormal vector to knot
// generate circle around the knot path at the given position s, using the normal and binormal vectors
return gamma.add(normal.mult(r*cos(theta))).add(binormal.mult(r*sin(theta)));
}
void drawSurface() {
// Draws parameterized surface, adapted from @etiennejcb
int n1 = 160; // s subdivisions
int n2 = 15; // theta subdivisions
stroke(150); // white lines
fill(0); // black fill
noStroke(); // comment out for wireframe
for (int i = 0; i < n1; i++) { // for each s subdivision
beginShape(TRIANGLE_STRIP); // start shape
for (int j = 0; j < n2+1; j++) { // for each theta subdivision
float s1 = map(i,0,n1,0.01,2*PI+0.01);
// have to avoid exact 0 due to vector operations
float s2 = map(i+1,0,n1,0.01,2*PI+0.01);
float theta = map(j,0,n2,0,2*PI);
PVector v1 = pipe(s1,theta,r); // grab coordinates
PVector v2 = pipe(s2,theta,r);
vertex(v1.x,v1.y,v1.z);
vertex(v2.x,v2.y,v2.z);
}
endShape(); // end shape
}
}
void drawPath() {
// Draws knot path
int n = 100; // number of segments
PVector[] vectors = new PVector[n+1]; // one extra point to complete the loop
float ds = 2*PI/n; // segment spacing
for (int i = 0; i <= n; i++) {
// generate points along path
vectors[i] = knotPath(ds*i);
}
strokeWeight(2);
stroke(255);
// draw path
for (int i = 0; i < n; i++) {
// for each point, draw a line between it and the next
line(vectors[i].x,vectors[i].y,vectors[i].z,vectors[i+1].x,vectors[i+1].y,vectors[i+1].z);
}
}
// Particle Class
class Particle {
float s = random(0,2*PI); // position along knot
float theta = random(0,2*PI); // angle along pipe/tube
float offset = random(0,1); // random initial offset
float weight = random(3,4); // base stroke weight
void drawPoint() {
// Draws a point at the particle's location (with some time-based movement)
PVector v = pipe(s + 2*PI*t, theta, r+0.1); // get point on knot, move along s with t
stroke(255);
strokeWeight(weight + 2*sin(2*PI*(t+offset))); // slight shimmer effect
point(v.x,v.y,v.z);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment