Last active
October 28, 2025 14:14
-
-
Save wschutzer/f026a6ffbd859e75e707ed952e0c2c7f to your computer and use it in GitHub Desktop.
Ribbon wrapping an infinite cylinder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* Wrapping ribbon | |
| * --------------- | |
| * Shows a ribbon gradually wrapping up an infinite cylinder. | |
| * This code uses shaders to aproximate the motion blur effect by averaging the pixels along a sequence | |
| * of frames. | |
| * | |
| * Copyright (C) 2025 Waldeck Schutzer (@infinitymathart) | |
| * | |
| * This program is free software: you can redistribute it and/or modify | |
| * it under the terms of the GNU General Public License as published by | |
| * the Free Software Foundation, either version 3 of the License, or | |
| * (at your option) any later version. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU General Public License | |
| * along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| */ | |
| final boolean recording = false; | |
| final boolean testing = false; | |
| final boolean gif = true; | |
| final int duration = 2; // Seconds | |
| final int fps = gif ? 30 : 60; // Frames Per Second | |
| final float num_frames = fps*duration; | |
| // Motion blur: | |
| final int samples_per_frame = 7; // How many frames to average? | |
| final float shutter_angle = 0.5; // How distant the frames are in time? | |
| final int frameWidth = gif ? 600 : (recording ? 2160 : 800); | |
| final float fac = 1.0*frameWidth/800; // Reference scaling factor | |
| final float aspect = gif ? 1.0 : 1350.0/1080; // 1920.0/1080; | |
| final int frameHeight = int(aspect * frameWidth); | |
| float t = 0; // Normalized animation time in the interval 0 - 1. | |
| PShader motionBlur; | |
| PGraphics gtex0; // Each frame is drawn here | |
| // Vertex shader code | |
| String[] motionBlurVertShader = { | |
| "#version 100", | |
| "#define PROCESSING_TEXTURE_SHADER", | |
| "uniform mat4 transformMatrix;", | |
| "uniform mat4 texMatrix;", | |
| "attribute vec4 position;", | |
| "attribute vec4 color;", | |
| "attribute vec2 texCoord;", | |
| "varying vec4 vertColor;", | |
| "varying vec4 vertTexCoord;", | |
| "void main() {", | |
| "gl_Position = transformMatrix * position;", | |
| "vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);", | |
| "vertColor = color;", | |
| "}"}; | |
| String[] motionBlurFragShader = { | |
| "#version 100", | |
| "#ifdef GL_ES", | |
| "precision mediump float;", | |
| "precision mediump int;", | |
| "#endif", | |
| "#define PROCESSING_TEXTURE_SHADER", | |
| "uniform sampler2D texture; // Source and result frame, works as an accumulator", | |
| "uniform sampler2D tex0; // Currently drawn frame to be added", | |
| "uniform float alpha;", | |
| "uniform float beta;", | |
| "varying vec4 vertColor; // Seems to be white", | |
| "varying vec4 vertTexCoord;", | |
| "void main()", | |
| "{", | |
| " vec4 curColor = texture2D(tex0, vertTexCoord.xy); // Color of current pixel ", | |
| " vec4 accColor = texture2D(texture, vertTexCoord.xy); // Color in accumulator", | |
| " gl_FragColor = beta*(alpha*accColor + curColor); // Blending", | |
| "}"}; | |
| class thing | |
| { | |
| PVector p1, p2; | |
| PVector normal; | |
| PVector tangent; | |
| float r; | |
| float disp; | |
| thing(PVector p1_, PVector p2_, PVector nv, PVector tv, float r_) | |
| { | |
| p1 = p1_.copy(); | |
| p2 = p2_.copy(); | |
| normal = nv.copy().normalize(); | |
| tangent = tv.copy().normalize(); | |
| r = r_; | |
| } | |
| void draw(float t) | |
| { | |
| push(); | |
| if (p1.z < 0) | |
| { | |
| float d = p1.z/fac; | |
| stroke(255*exp(-d*d/(163840*2*fac))); | |
| } | |
| else | |
| stroke(255); | |
| noFill(); //fill(0); | |
| beginShape(); | |
| PVector right = tangent.cross(normal).normalize().mult(r); | |
| vertex(p1.x, p1.y, p1.z); | |
| vertex(p2.x, p2.y, p2.z); | |
| PVector p = PVector.add(p2, right); | |
| vertex(p.x, p.y, p.z); | |
| p = PVector.add(p1, right); | |
| vertex(p.x, p.y, p.z); | |
| endShape(CLOSE); | |
| stroke(color(255,0,0)); | |
| /* draw the coordinate vectors | |
| float ell = 20*fac; | |
| line(pos.x, pos.y, pos.z, pos.x + normal.x*ell, pos.y+normal.y*ell,pos.z+normal.z*ell); | |
| stroke(color(0,255,0)); | |
| line(pos.x, pos.y, pos.z, pos.x + right.x*ell, pos.y+right.y*ell,pos.z+right.z*ell); | |
| stroke(color(0,0,255)); | |
| line(pos.x, pos.y, pos.z, pos.x + tangent.x*ell, pos.y+tangent.y*ell,pos.z+tangent.z*ell); | |
| */ | |
| pop(); | |
| } | |
| } | |
| void settings() | |
| { | |
| size(frameWidth, frameHeight, P3D); // Use P2D renderer for shader support | |
| } | |
| int num_segs = 12; | |
| int num_rings = 100; | |
| float R = 100*fac; | |
| float h = R*cos(PI/num_segs); | |
| float stretch = 2; | |
| float ell = stretch*2*R*sin(PI/num_segs); | |
| thing rings[][]; | |
| float g(float t) | |
| { | |
| // this is: | |
| // (1) a fixed base: -1000*fac | |
| // (2) a backwards constant movement: t*ell/TWO_PI | |
| // (3) a nonlinear movement to create the wrapping effect: pow(t*ell/(TWO_PI*100*fac),3.5) | |
| return -1000*fac + t*ell/TWO_PI + pow(t*ell/(TWO_PI*100*fac)/stretch,3.5); | |
| } | |
| float h(float t) | |
| { | |
| return g(t+TWO_PI); | |
| } | |
| PVector gamma1(float theta) | |
| { | |
| return new PVector(R*cos(theta),R*sin(theta),g(theta)*t+(1-t)*h(theta)); | |
| } | |
| void setup() | |
| { | |
| gtex0 = createGraphics(width, height, P3D); | |
| motionBlur = new PShader(this, motionBlurVertShader, motionBlurFragShader); | |
| //ortho(); | |
| rings = new thing[num_rings][num_segs]; | |
| } | |
| void draw() | |
| { | |
| background(0); | |
| for(int i=0; i<samples_per_frame; i++) | |
| { | |
| t = map(((recording || testing) ? 1.0*(frameCount-1): 1.0*mouseX/width*num_frames) + i*shutter_angle/samples_per_frame, 0, num_frames, 0, 1)%1; | |
| draw_(t); // Draw on gtex1 | |
| motionBlur.set("tex0", g.get()); | |
| motionBlur.set("alpha", (float)(i)); | |
| motionBlur.set("beta", (float)(1.0/(i+1))); | |
| gtex0.filter(motionBlur); // Averages the frames with a moving average | |
| } | |
| image(gtex0,0,0); | |
| if (recording) | |
| { | |
| saveFrame("/tmp/r/frame_####.png"); | |
| println(frameCount,"/",num_frames); | |
| if (frameCount >= num_frames) | |
| exit(); | |
| } | |
| } | |
| void draw_(float t) | |
| { | |
| background(0); // Clear the canvas | |
| noStroke(); | |
| // Computes the the ribbon | |
| float theta = 0; | |
| for(int i=0; i<num_rings; i++) | |
| { | |
| for(int k=0; k<num_segs; k++) | |
| { | |
| float theta1 = theta + PI/num_segs; | |
| PVector p1 = gamma1(theta); | |
| PVector p2 = gamma1(theta1); | |
| PVector nv = PVector.add(p1, p2); // Normal vector | |
| nv.z = 0; | |
| PVector tv = new PVector(nv.y, -nv.x, 0); // Tangent vector | |
| rings[i][k] = new thing(p1, p2, nv, tv, ell); | |
| theta = theta1; | |
| } | |
| } | |
| push(); | |
| strokeWeight(1.5*fac); | |
| translate(width/2,height/2,0); | |
| for (thing[] ring : rings) { // iterate each ring (row) | |
| for (thing th : ring) { // iterate each thing within that ring | |
| th.draw(t); | |
| } | |
| } | |
| pop(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment