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:
Generative explorations in 2019 - #2
Generative explorations in 2019 - #3
Generative explorations in 2019 - #4
Generative explorations in 2019 - #5
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:
- 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.
- 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:
- Creating a system where each generated image is not only visually successful but also distinct in style and form.
- 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 colors
lighting system colors
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
Instead of placing ellipses along the diameter of an imaginary circle, let’s arrange directional lights around a 3D sphere.
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:
-
f
is shorter to type than frameCount, which can be convenient, especially in longer projects. -
I can reset
f
to 0 whenever I want. With the nativeframeCount()
, 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 ownf
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 whenf
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, ifnbFrames = 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)
- 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}
};
Let's dance!
In this creation, I imagine each 3D primitive as a dancer. With oscillations, we can personnalize objects and choreograph different movements:
-
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*/ -
By adding time into the angle calculation of the oscillation,
we create motion.
Code
x = sin(i + t*TAU) * 100;
y = cos(i + t*TAU) * 100;
z = sin(i + t*TAU) * 100;
-
By multiplying this time by an integer, we introduce variations
in the arrangement (see Lissajous Curves).
Dance this Lissajous curves - #1
Dance this Lissajous curves - #2
Dance this Lissajous curves - #3
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 -
By changing the 3D primitive and adding internal rotation to each
dancer (like whirling dervishes), we further enhance the choreography.
Dance this Lissajous curves - #4
Dance this Lissajous curves - #5
Dance this Lissajous curves - #6
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:
- Remove the background
- Adding a random global rotation on each axis
- Adding a Perlin Noise
Adding Noise #1
Adding Noise #2
Adding Noise #3
Adding Noise #4
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
);
}
}
Adding Noise #1
Adding Noise #2
Adding Noise #3
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
- Use of a monochrome palette + one random color
- A little orbiting satellite
- Random rotation of light System (X or Y or Z)
- Addition of a non-linear progression in the lateral movement
- Horizontal, vertical or random angle for the translations
- Internal rotation based on self position but with different axies
- A different number of rotation of the global scene
- Rotation of the global scene (what drove me crazy)
- Adding steps noise to the length of the ellipses
- ...
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
Explore all technical tests
RENDERING
Color
color(random(360), random(50,100), random(75,100))
Saturation:50, brightness:50
Saturation:50, brightness:75
Saturation:50, brightness:100
Saturation:100, brightness:50s
Render Type
See about render type parameters
random(["Mat","Glossy","Soft","PizzaPunk"])
Mat
Glossy
Soft
Glass
Metal
PizzaPunk
Distance of the pointlights
posLight: random(-width,width)
let lightPosx = cos(angle - t * TAU) * posLight;
let lightPosy = sin(angle - t * TAU) * posLight;
-width
0
width
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;
X axis
Y axis
Z axis
SCENE
Number of objects
nb: int(random(3,12)),
2 objects
7 objects
12 objects
20 objects
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])
1 rotation
3 rotations
6 rotations
180° around X
180° around Y
360° around Z
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);
Radius 0
Radius 50%
Radius 66%%
Radius 100%
Radius 200%
Power factor
pw: random(0.55,1.5);
nt = pow(t, pw);
See note about t (frameCount in a cycle)
Power 0.2
Power 1 (linear)
Power 3
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);
...
Noise 0
Noise 0.25
Noise 0.5
Noise 0.75
Radius Osc 0.1
Radius Osc 0.25
Radius Osc 0.5
Radius Osc 1
Osc x:1 y:1 z:3
Osc x:1 y:2 z:3
Osc x:1 y:5 z:2
Osc x:7 y:5 z:6
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(...)
}
Cycle 0.005
Cycle 0.01
Cycle 0.03
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;
}
None
Atan X
Atan Y
Atan Z
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
);
Steps false
Steps true X
Weight 0.01
Weight 0.02
Weight 0.0225
Weight 0.025
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)
Seed: 953811
Seed: 938851
Seed: 902991
Seed: 718483
Seed: 94129
Seed: 53045
Seed: 71549
Seed: 89243
Seed: 127113
Seed: 170019
Seed: 177519
Seed: 344913
Seed: 345128
Seed: 387939
Seed: 400888
Seed: 535264
Seed: 595827
Seed: 604575
Seed: 682018
Seed: 735412
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!