BACK

A Choreographic Coding

Project

Sculpted Flow The process behind

  • Date of release: Soon
  • Format: Generative
  • Plateforme: -
  • Code: P5.js WEBGL

In this piece, light becomes the storyteller.

A 3D form drifts slowly through time, tracing its path across unseen dimensions. Each fleeting moment is captured, layering into a delicate structure. As vibrant lights orbit the shape, the scene itself dances unpredictably, creating a sculpture bathed in ever-shifting hues.



What Drives the Creation

This project has roots dating back to my explorations in 2019, using p5.js with the webGL canvas. By omitting the background() function in the draw() loop, every stroke and movement on the screen was preserved, creating a kind of visual trail. Combining this technique with rotations, translations, and a dynamic lighting system, I was able to produce some intriguing images—though they were few and far between.

Around this time, I also began teaching, which led me to refocus on the core principles of generative design to make them accessible for students without coding backgrounds. Today, I’m revisiting this project to refine the work—or rather, it has found its way back to me. There’s an indescribable pull, a need to create in this space once more.

Some generative designs made between 2019-2021:

I revisited this idea with the goal of ensuring that every image generated would be a visual success in this new generative art piece.

Challenges in Generative Design

In generative design, images are dynamically generated through code, which often results in highly chaotic and unpredictable visuals. This poses several challenges:

  1. Curating the output: Each dynamically generated image can vary significantly in quality, making it necessary to curate the results to select only the successful visuals.
  2. Lack of centralization: In previous projects, each visual came from a different code base, without any unified framework or consistency across the images.

In this project, my aim is to address these challenges by:

  1. Creating a system where each generated image is not only visually successful but also distinct in style and form.
  2. Incorporating a wide range of variations, enabling the code to produce a diverse set of outputs that are each aesthetically compelling.

2019 Ideas, Now Refined

The concept revolves around:

  • using a lighting setup with multiple colors defined within a specific palette.
  • Applied these colors to the environment in a rotational manner

As demonstrated in the Z-axis rotation animation below, each light source follows its own distinct path, contributing to the overall effect and enriching the visual depth through dynamic shifts in color as the object rotates. The interplay between the lights and the object creates a unique generative result.

The color transitions are dictated by both the palette and the continuous rotation of the lighting system.

...my old lighting system
lighting system rotation XYZ

Diagrams need no further explanation.

Notice that a maximum of 5 point lights can be active at once with p5.js.

Coding the light system with polar coordinates

The first thing is to arrange multiple lights along the diameter of an imaginary circle. The technique for determining the x and y position of each light is well-established and thoroughly explained by Daniel Shiffman in his reference work, "The Nature of Code" (Chapter 3: Oscillation ↬ Polar vs. Cartesian Coordinates).

Here's the basic code
code p5.js editor

Instead of placing ellipses along the diameter of an imaginary circle, let’s arrange directional lights around a 3D sphere.

code p5.js editor
Rotate lights around the X axis Rotate lights around the X axis
Rotate lights around the Y axis Rotate lights around the Y axis
Rotate lights around the Z axis Rotate lights around the Z axis
Notes about code
About native variable frameCount

In the previous code, you might have noticed that I'm not using the native frameCount() function from p5.js. Instead, I start by creating my own variable f = 0 and then I increment f by 1 on each frame.

There are two main reasons why I do this:

  1. f is shorter to type than frameCount, which can be convenient, especially in longer projects.
  2. I can reset f to 0 whenever I want. With the native frameCount(), it's a built-in function that always increases from 0 and you can't reset it. But sometimes, I want the counter to restart, and using my own f variable gives me that flexibility.
About t = (f / nbFrames) % 1

the variable t is used to control how many complete rotations the lights make over a specific period of time, which is determined by the number of keyframes (nbFrames).

  • This formula ensures that t is always a value between 0 and 1. It essentially measures how far along the animation is, relative to the total number of frames.
  • As f (my custom frame counter) increases with each frame, t smoothly moves from 0 to 1, then resets back to 0 when f completes a full cycle. This gives a looping effect.
  • This setup ensures that one full rotation of the lights occurs over a span of nbFrames frames. So, if nbFrames = 90, it means the lights will complete a full rotation every 90 frames, creating a smooth looping animation

Unlike a directional light system, as used in the example above, final pieces utilizes a pointLight system.

It's enhances the contrast in terms of brightness and adds a more dramatic effect to the visual output. The pointLight setup allows for more localized lighting, creating highlights and shadows that change dynamically as the object rotates. This results in a richer and more intense rendering, emphasizing the depth and complexity of the shapes.

3D Objects Rendering (Colors Shaped by Lights)

Now that the lighting environment is set, let's dive into how we sculpt the overall shape using 3D primitives like spheres or cubes as below. The same technique is used for the placement of the box around the middle (with polar coordinates again)

Rotational light on immmobile objects Rotational light on self rotate objects
    Parameters:
  • shininess(1)
  • ambientLight(150)
  • ambientMaterial(100)
  • specularColor(150)
  • specularMaterial(150)
Notes abouts parameters

    Faced with the vast possibilities of visual variations in 3D lighting and color, I decided to create specific parameter schemes for my 3D renders. This approach allows me to work with a curated set of parameters, giving each render a distinct style. I now have presets for matte, glossy, metallic, soft, and glass finishes—along with a custom option for my personal settings. This framework keeps the creative process organized while still offering flexibility and control over each piece.

    const renderLights = {
    Mat : {shininess:1, ambientLight:10, ambientMaterial:120, specularColor:50, specularMaterial:30,metalness: 50},
    Glossy : {shininess:50, ambientLight:80, ambientMaterial:100, specularColor:200, specularMaterial:180,metalness: 50},
    Metallic : {shininess:300, ambientLight:60, ambientMaterial:70, specularColor:255,specularMaterial:250,metalness: 80},
    Soft : {shininess:10, ambientLight:150, ambientMaterial:140, specularColor:80, specularMaterial:20,metalness: 30},
    Glass : {shininess:120, ambientLight:50, ambientMaterial:50, specularColor:255, specularMaterial:230,metalness: 70},
    Pizzapunk : {shininess:5, ambientLight:0, ambientMaterial:10, specularColor:255, specularMaterial:200,metalness: 100}
    };

This arrangement forms the choreographic foundation for sculpting a harmonious shape.

Let's dance!

In this creation, I imagine each 3D primitive as a dancer. With oscillations, we can personnalize objects and choreograph different movements:

  1. Each object is assigned a color from the palette.
    Code

    c = int(cpt) % palette.length;
    fill(palette[c]);

    /*The modulo operator % is used here to ensure that c (the index for accessing colors in the palette array of- 5 max lights) stays within the bounds of the array length (6th color doesn't exist), no matter how large cpt (the number of balls) gets*/

  2. By adding time into the angle calculation of the oscillation, we create motion. Put on your red shoes and dance the blues
    Code

    x = sin(i + t*TAU) * 100;
    y = cos(i + t*TAU) * 100;
    z = sin(i + t*TAU) * 100;

  3. By multiplying this time by an integer, we introduce variations in the arrangement (see Lissajous Curves).
    Code

    //in setUp()
    nbx = int(random(1,6)); nby = int(random(1,6)); nbz = int(random(1,6))
    //in draw()
    x = sin(i * nbx + t*TAU) * 100;
    y = cos(i * nby + t*TAU) * 100;
    z = sin(i * nbz + t*TAU) * 100;
    // t*TAU is for recording the loop, we can use f*0.01 for a non recorded-looping version

  4. By changing the 3D primitive and adding internal rotation to each dancer (like whirling dervishes), we further enhance the choreography.
    Code

    rotateZ(atan2(x, y, z) + t * TAU);
    ellipsoid(50, 10, 10);


Preserving Every Move

Drawing with WebGL and p5.js

In this project, I explore the idea of drawing without erasing, leveraging p5.js and WebGL to create visuals that continuously build upon each other. By omitting the background() function in the draw() loop, each stroke and movement leaves a trace on the canvas, creating a layered and evolving composition.

Some adjustements:
  1. Remove the background
  2. Adding a random global rotation on each axis
  3. Adding a Perlin Noise

At this stage, we achieve a more organic choreography with Perlin noise, while maintaining geometric continuity through oscillating curves.


Looking for harmony

Let's make it more organic

Shifting the composition and adding an organic rythm:

Keeping in mind that I’m choreographing a ballet of 3D objects, I add:

  • Movement to all my dancers, initially in a straight line from top to bottom.
  • Adjust the oscillation speeds and Perlin noise to sync them with the timeline (nbFrames).
  • Creating rhythm in the construction, based on Perlin noise.
Code // More times
// A cycle of 900 keyframes
let nbFrames = 900;
let f = 0;
...
// in draw()
f++;
t = (f / nbFrames) % 1;
...
// Movement of the shape from top (start at 0) to bottom (end at 900)
translate(0, map(f, 0, nbFrames, -height * 0.25, height * 0.45));
...
for (let i = 0, cpt = 0; i < TAU; i += TAU / nb, cpt++) {
...
// 1.5 cycles of 2D noise with polar diplacement of values for a looping noise
// vNoiz depend on wanted results (between 0.001 to 1)
xoff = map(sin(i + t * TAU * 1.5), -1, 1, 0, vnoise);
yoff = map(cos(i + t * TAU * 1.5), -1, 1, 0, vnoise);
wT = width * 0.5;
// With wT, the noise gradually loses its effectiveness
n = noise(xoff, yoff) * (wT - t * wT);
...
// The number of oscillations is in angle, we control the number of oscillations in a cycle
// NBX and NBY are random values for the number of rotations during a cycle
x = cos(i * nbX + t * TAU) * n;
y = sin(i * nbY + t * TAU) * n;
// We add another noise for each object to interrupt the path and create rhythm.
// The size of the objects decreases over time
if (noise(i * 100 + t * TAU) % 0.1 > 0.005) {
  ellipsoid(
    width * 0.0125 - t * width * 0.0125,
    width * 0.075 - t * width * 0.075,
    width * 0.0125 - t * width * 0.0125
  );
}
}

Now we’re talking, this is looking pretty good!


RealTime Generative

The construction of the form in real time, guided by oscillations and subtly distorted by Perlin noise, unfolds in real time, creating a profound sense of satisfaction as the process comes to life before my eyes. It’s likely this mesmerizing quality—the feeling of watching something evolve organically—that drew me back to this work.


Refinements

Things added
  1. Use of a monochrome palette + one random color
  2. A little orbiting satellite
  3. Random rotation of light System (X or Y or Z)
  4. Addition of a non-linear progression in the lateral movement
  5. Horizontal, vertical or random angle for the translations
  6. Internal rotation based on self position but with different axies
  7. A different number of rotation of the global scene
  8. Rotation of the global scene (what drove me crazy)
  9. Adding steps noise to the length of the ellipses
  10. ...

I can’t detail every process—there was a tremendous amount of trial and error, countless iterations that worked to varying degrees, and many steps that led me back to square one. Attempting to explain it all would be daunting. My obsession with creating these generative forms pushed me into moments verging on a certain madness, as the pursuit of these intricate shapes took on a life of its own.

After countless tweaks, long nights of adjustments and testing, I managed to find the optimal settings.


Exploring Parameter Variations

With so many parameter options at my fingertips, it became essential to make decisions that would narrow the variations to results I truly enjoy.
Testing each possibility was fascinating, but it was also easy to get lost in endless combinations. By refining the choices, I could focus on a set of outcomes that align with my vision, bringing consistency and intentionality to the final work while still allowing room for discovery within those parameters.

Average parameters

For testing purposes, I start with mid-range quantitative parameters based on the final version’s settings.
By selecting the average between minimum and maximum values, I create a balanced foundation for further adjustments. For instance, if the minimum radius is set to 300 and the maximum to 700, I’ll set the test radius to 500. This approach helps me gauge the effect of each parameter in a stable environment, making it easier to fine-tune variations without straying too far from my intended vision.

Seed : 596862 ; 

************
RENDERING
Format : 1600,1600 ; // Canvas format in width and height
Palette : 
#BBB,#888,#EEE,rgba(79,222,55,1),#444 ;  // Color palette used in the artwork
Type Rendu : Mat ; // Render type chosen for lighting and material properties
Position Z Lights : 800 ; // Distance of the pointlights
Rotation Lights Axe  : X ; // Axis along which the lights are rotated (X, Y, or Z)
************
SCENE
nb = 7 ; // Number of objects in the main structure
nbTours = 1 ; // Number of full rotations for the entire scene
Dir : 1 ; // Direction for the rotation (1 for clockwise, -1 for counterclockwise)
lineaire = 0 ; // Linear or nonlinear mode for angle calculations
Pw : 1 ; // Power factor applied to smooth or sharpen the oscillations
Start : 3.6285249669951467 ; Fin : 0.4869323134053536 ; 
// Starting and ending angle of rotation in radians
Radius : 533.3333333333334 ; // Radius of the main layout structure
rX = 0.25 ; rY = 0.35 ; rZ = 0.5 ; // Rotation component along the axis
************
3D PRIMITIVES
nbX = 1 ; nbY = 1nbZ = 1 ; // Number of divisions or oscillations along the axis
radiusOsc = 0.25 ; // Radius used for oscillation in the layout
vNoiz : 0.25 ; // Noise intensity applied to the movement
modulCycle : 0.005 ; // Cycle modulation affecting the periodicity of the oscillations
Rotation Atan  : none ; // Mode for rotation based on the atan function
Length Steps  : 0 ; // Length variation steps for individual elements
wObj : 0.0225 ; // Weight of objects in the structure (affects size)
Arbitrary seed: 596862
Arbitrary seed for test
Explore all technical tests
RENDERING
Color
color(random(360), random(50,100), random(75,100))
Render Type

See about render type parameters

random(["Mat","Glossy","Soft","PizzaPunk"])
Distance of the pointlights
posLight: random(-width,width)
  let lightPosx = cos(angle - t * TAU) * posLight;
  let lightPosy = sin(angle - t * TAU) * posLight;
Axe of Rotation Lights
case "X": secdCvs.pointLight(color(palette[i]), 0, lightPosx, lightPosy); break;
  case "Y": secdCvs.pointLight(color(palette[i]), lightPosx, 0, lightPosy); break;
  case "Z": secdCvs.pointLight(color(palette[i]), lightPosx, lightPosy, 0); break;

See Lighting system Diagrams

SCENE
Number of objects
nb: int(random(3,12)),
Number of full rotations + Rotation component along the axis
nbTours: int(random(1,4));
  rX: random(.5), rY: random(.5), rZ: random(1)
  rotate(nt * TAU * nbTours * dir, [rX, rY , rZ])
Radius of the main layout structure
rad: random([0, width / 2, width / 3, width / 4 ]);
  ptStart = createVector(sin(angleStart) * rad, cos(angleStart) * rad);
  ptEnd = createVector(sin(angleEnd) * rad, cos(angleEnd) * rad);
  translate(map(nt, 0, 1, ptStart.x, ptEnd.x),map(nt, 0, 1, ptStart.y, ptEnd.y),0);
Power factor
pw: random(0.55,1.5);
  nt = pow(t, pw);

See note about t (frameCount in a cycle)

3D PRIMITIVES
Noise intensity applied to the movement + Radius used for oscillation + Number of oscillations along the axis
vNoiz: random(0.5);
  nbX: int(random(10)),nbY: int(random(10)),nbZ: int(random(10));
  for (let i = 0; i ≤ TAU; i += TAU / nb) ...
  xoff = map(sin(i + nt * TAU), -1, 1, 0, vNoiz);
  yoff = map(cos(i + nt * TAU), -1, 1, 0, vNoiz);
  radiusOsc: random(0.1,0.5);
  wT = width * radiusOsc;
  n = width * 0.1 + noise(xoff, yoff) * (wT - t * wT);
  ...

For osc, see variations in dancing

Cycle modulation affecting the periodicity of the oscillations
modulCycle: random(0.01);
  v = noise(i * 100 + nt * TAU * 2)
  if (v % 0.07 ≤ modulCycle) { 
    ellipsoid(...)
  }
Mode for rotation based on the atan function
modeRotateAtan: random(["none","x","y","z","xyz"]),
  switch (modeRotateAtan) {
  case "x":
    rotate(i + atan2(0, y, z))
    break;
  case "y":
    rotate(i + atan2(x, 0, z))
    break;
  case "z":
    rotate(i + atan2(x, y, 0))
    break;
    case "xyz":
  rotate(i + atan2(x, y, z))
    break;
    default: break;
  }
Length variation steps for individual elements + Weight of objects in the structure (affects size)
lengthSteps: random([0,0,1]);
  nLenght = lengthSteps ? int(noise(i * 100 + nt * TAU) * 3) / 3 : noise(i * 100);
  length = nLenght * 0.2;
  nWeight = 0.01 + noise(i * 1000)
  weight = nWeight * wObj;
  v = noise(i * 100 + nt * TAU * 2)
  if (v % 0.07 > modulCycle) { //0.005 à 0.035
      secdCvs.ellipsoid(
          1 + minFormat * length - t * minFormat * length * 0.5,
          1 + minFormat * weight - t * minFormat * weight,
          1 + minFormat * weight / 2 - t * minFormat * weight / 2,
      );
    }
Updates of last version
//Minor modfications;
  secdCvs.ellipsoid(
    1 + minFormat * length - t * minFormat * length * 0.5,
    1 + minFormat * weight - t * minFormat * weight * 0.25,
    2 + minFormat * weight * 0.5 - t * minFormat * weight * 0.25
  );

New Refinements Based on Test Insights

Through extensive testing, I was able to redefine my aesthetic preferences for this project, leading to several key adjustments:

    • Absolute Reference Size: I opted for an absolute reference size instead of a relative one to create a consistent visual scale.
    • Dominant Colors: A subjective selection of a few dominant colors now anchors the palette, establishing a cohesive look.
    • Light Spot Rotation: Rotation of light spots has been simplified to occur solely around the Y-axis of the canvas, enhancing spatial consistency.
    • Scene Rotation: The scene rotates more extensively around the X and Z axes, with limited rotation on the Y-axis, creating a more grounded perspective.
    • Internal Object Rotation: The internal specific rotation of objects around Y (rotate(i + atan2(x, 0, z))) has been removed to maintain visual clarity and reduce complexity.
    • Logarithmic Function for Oscillations: I introduced a logarithmic function into the parametric oscillations of the 3D objects, adding smoothness to their movement.
    • Removal of Length Steps: Abrupt changes in object size were removed as they felt too chaotic.
    • Uniformity in Object Size: The objects are now set to two minimal sizes, chosen randomly, to balance diversity and harmony.

These decisions collectively help to create a cleaner, more unified aesthetic and a controlled, organic rhythm in the visual composition.


Results (Work In Progress)



Final Reflections

By combining randomness, precise control over movement, and an evolving palette of colors and lights, I aim to create an experience that feels both organic and dynamic. In many ways, it’s a journey to express something deeply felt, something that only the design of the form itself can capture—an intangible sensation that emerges as the shapes and patterns come to life, revealing what words alone cannot convey.

Partnering

I am seeking a visionary partner in the Web3 space—an NFT gallerist or platform—to introduce Sculpted Flow to a global audience through exclusive NFT drops or events. If this vision resonates with you, feel free to send me a message on Twitter or Instagram, and I’ll share a private link to explore the artwork live in action.

Thanks for sticking around till the end!

⇱ Back to Projects

  1. What Drives the Creation
  2. 2019 Ideas, Now Refined
  3. Let's dance
  4. Remove background
  5. Looking for harmony
  6. Real Time
  7. Refinements
  8. Exploring Parameter Variations
  9. After Test Refinements
  10. Results (work in progress)
  11. Multiscreen tests
  12. Final Reflections
  13. Partnering