Series 0 - Spike Ball

Oct 02, 2021
Growth & Decay | Explosions & Implosions | Sphere Morphology.

SpikeBall #4

Minted: /
Current price: ETH
Paused to purchases:
Permanently locked:
(This means it is impossible for anyone to change the number of tokens that can be minted or update the code stored).

Connect wallet to see up to date info about the series.

Purchase made. Congratulations!

Your art piece will be added to the site within the next day and a video file of it uploaded to IPFS that will be rendered in any NFT supported wallets.

In the meantime, there are 2 steps to visualize your newly minted artwork yourself (it is too computationally expensive to be rendered in the browser). This information will disappear upon refreshing this page.:

1. Download Processing version 3.5.4 (stable) or 4.0.0 (beta) on a computer: Mac Download, Windows Download. Linux Download General Processing download site.

2. Copy and paste the code for this series which is hosted on the IPFS (your unique random seed has already been input to the first line!):

  Placeholder code

50% of all proceeds from this Series and all future ones be donated to GiveDirectly, a GiveWell top rated charity.

About the Series

The Spike Balls series emerged out of a whimsical exploration of sphere meshes and Perlin noise. The first challenge was finding the best way to represent a sphere as a mesh object that could be manipulated to morph it in different ways. Then experimenting with Perlin noise and stochastically manipulating the surface produced aesthetically pleasing and surprising results.

Having the sphere become highly entropic before slowly returning to its low entropy, starting state, is analogous to states of wake and sleep, alteration and restoration that can be played indefinitely on repeat. Each repetition uses the same underlying parameters but follows a slightly different, unique trajectory.


(Click any piece to see its video & details)

Generative Properties

With nine different parameters set from a hash combining your wallet address, the token ID and attributes of the block it is minted on (the block difficulty and time), every spike ball is unique. In fact, as a conservative estimate, there are over five million possible variations. The features that vary for each ball include:

  • Growth probability - Each point that is selected has a probability for growing or decaying. This probability is biased in a particular, random direction. There is a 20% chance of the ball shrinking on average and four general categories: Fast Growth, Slow Growth, Slow Decay, Fast Decay.
  • Ball size - How large the radius of the ball is. There are roughly three categories: Small, Medium, Large.
  • Ball number of vertices - The ball can have a Low, Medium or High number of vertices on its surface that grow or decay. This changes the amount of complexity on the ball’s surface.
  • Ball vertices selected - Perlin noise (noise that is correlated) is used to choose which vertices to grow/decay. This randomly set parameter determines how correlated these vertices are. There are roughly three categories of selection: Sparse, Distributed, and Dense.
  • Ball colour - The ball’s surface colour. 256 possible red, green and blue values are randomly chosen. We can simplify the colour spectrum by categorizing these as the seven primary rainbow colours. (There are actually over 147 unique colours defined by CSS.)
  • Ball stroke colour - Same as above but the stroke colours connecting each vertex.
  • Background colour - The background of the scene.
  • Lighting colour - The colour of a light that adds contrast to the ball.
  • Ball Rotation - Two rotational axes and their speeds are randomly sampled. There are twenty five categories of rotation, ranging from Fast, Medium and Slow in the Negative and Positive Directions along both axes. Ignoring the negative and positive directions gives 3/*3=9 combinations.

Using this lower bound approximation there are at least 4*3*3*3*7*7*7*7*9=2,333,772 possible one of a kind Spike Balls.

Code Explanation:


A mesh in the shape of a sphere is created out of lots of small triangles. A subset of the triangle points are selected to morph. In morphing, each of these points flips a biased coin to see if it grows or decays and by how much. The bias of the coin is determined by a random number unique to each ball.

The subset of points selected to morph are chosen using Perlin (correlated) noise that leads to the uneven clumping of growth/decay on the surface. This process is repeated for a fixed number of iterations before freezing, returning to its starting state, and repeating again on an infinite loop. The videos displayed on this site each capture two of these loops.


An icosphere (or geodesic polyhedron) is created by first creating a icosahedron and resizing each point to exist on an L2-norm unit sphere. The icosahedron is converted into a sphere by choosing points that exist mid way between every current triangle and splitting them into new triangles. This is the only method that can be used to create a sphere that has a uniform distribution of triangles along its mesh. See this tutorial for great diagrams and descriptions of the process.

When the sphere is built, at each iteration we: (i) select 5% of all vertices using Perlin noise such that the points are somewhat correlated. (ii) Sample a growth/decay probability for each point and use this to multiply its vector position by a re-scaled amount of this probability.

In order to encourage a winner-takes-all approach where the spikes that grow the most first stay the largest, we make it harder for the smaller vertices to catch up by reducing the overall growth probability as a function of the largest vector size.

For high growth probabilities it is possible for the spikes to grow out of view. We keep track of the L2-norm of each vector and if a vector of max size tries to grow further it is prevented from doing so.

This whole process is repeated until the defined number of iterations when the result is frozen, before returning to its original state and restarting with the same parameters but slightly different results because the random number generator is not reset.


Code has been stored to IPFS with the CID: QmdRBCwPcsfCeX1SvXur8vSLTw3Ko77GsM86usvmZAL9tt

String rand_seed = "SEED"; String d = rand_seed.replaceAll("[^0-9]", "").replaceAll("^0+(?!$)", ""); int ri = Integer.parseInt(d.substring( d.length() -9)); float P = (1 + sqrt(5)) / 2; float[][] sv = { {-1,P, 0}, { 1,P, 0}, {-1, -P, 0}, { 1, -P, 0}, {0, -1, P}, {0,1, P}, {0, -1, -P}, {0,1, -P}, { P, 0, -1}, { P, 0,1}, {-P, 0, -1}, {-P, 0,1} }; int[][] sf = { {0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11}, {1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8}, {3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9}, {4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1} }; ArrayList<float[]> vs = new ArrayList<float[]>(); ArrayList<int[]> fs = new ArrayList<int[]>(); float fv = 0.05; float nc = 0.1; float rl = 0.01; int ms = 130; int fr = 400; int ft = fr; int fp = 120; int fc = 200; public float[] nv(float[] v) { float l2 = sqrt(sq(v[0])+sq(v[1])+sq(v[2])); float[] r = {v[0]*rad/l2,v[1]*rad/l2,v[2]*rad/l2}; return r; } HashMap<String, Integer> mp = new HashMap<String, Integer>(); public int mt(int p1, int p2) { int si = min(p1, p2); int gi = max(p1, p2); String key = Integer.toString(si)+"-"+Integer.toString(gi); if (mp.containsKey(key)) { return mp.get(key);} float[] v1 = vs.get(p1); float[] v2 = vs.get(p2); float[] md = { (v1[0]+v2[0])/2, (v1[1]+v2[1])/2, (v1[2]+v2[2])/2 }; vs.add( nv(md) ); int ix = vs.size() - 1; mp.put(key, ix); return ix; } public ArrayList<int[]> de(ArrayList<int[]> fs) { ArrayList<int[]> fsd = new ArrayList<int[]>(); for (int i = 0; i < fs.size(); i++) { int[] tri = fs.get(i); int v1 = mt(tri[0], tri[1]); int v2 = mt(tri[1], tri[2]); int v3 = mt(tri[2], tri[0]); fsd.add(new int[] {tri[0], v1, v3}); fsd.add(new int[]{tri[1], v2, v1}); fsd.add(new int[]{tri[2], v3, v2}); fsd.add(new int[]{v1, v2, v3}); } return fsd; } int[] cs = new int[12]; ArrayList<float[]> vd = new ArrayList<float[]>(); int lix = 0; int np; ArrayList<float[]> og_vs = new ArrayList<float[]>(); float rad; float rix; float ps; float[] ot; float sg; float lln; void setup(){ randomSeed(ri); noiseSeed(ri); size(1000, 1000, P3D); smooth(); frameRate(30); int sb = Math.round( random(3) )+3; lln = rad; rix = random(1000, 10000); ps = 10; ot = new float[] {random(-rl, rl), random(-rl, rl) }; if (random(1)<0.2){ sg = random(0.42, 0.50); rad = random(20)+ 75; } else { sg = random(0.54, 0.62); rad = random(20)+ 55; } for (int i = 0; i < sf.length; i++) { fs.add(sf[i]); } for (int i = 0; i < 12; i++) { cs[i] = Math.round(random(255)); } for (int i = 0; i <sv.length; i++) { vs.add( nv(sv[i]) ); } for (int i = 0; i < sb; i++) { fs = de(fs); } for (float[] v : vs){ og_vs.add(v); } np = Math.round(fv*vs.size())+1; background(cs[0],cs[1],cs[2]); } void draw() { translate(width/2,height/2,600); background(cs[0],cs[1],cs[2]); lights(); rotateY(ot[0]*frameCount); rotateZ(ot[1]*frameCount); directionalLight(cs[3], cs[4], cs[5], -1, 0, 0); fill(cs[6],cs[7],cs[8]); stroke(cs[9],cs[10],cs[11]); beginShape(TRIANGLES); for (int[] face_points : fs){ for (int p : face_points) { vertex(vs.get(p)[0],vs.get(p)[1],vs.get(p)[2]); } } endShape(); float sd = 1 -(0.000003*sq(lln)); float pw = sg * sd; if (frameCount < ft) { for (int i = 0; i < np; i++) { int r_ind = Math.round( noise(rix*random(0.1)*i*(ft-frameCount) )*(vs.size()-1) ); float ln = sqrt(sq(vs.get(r_ind)[0])+sq(vs.get(r_ind)[1])+sq(vs.get(r_ind)[2])); if (ln>lln) { lln = ln; lix = r_ind; } float nois = 1-( nc*(1-pw) ) + (nc*noise(og_vs.get(r_ind)[0]+random(ps), og_vs.get(r_ind)[1]+random(ps), og_vs.get(r_ind)[2]+random(ps) )); if (ln>ms && nois>1.0){ continue; } vs.set(r_ind, new float[] {vs.get(r_ind)[0]*nois, vs.get(r_ind)[1]*nois, vs.get(r_ind)[2]*nois} ); } } else if (frameCount>=ft && frameCount < (ft+fp)) { } else if ( frameCount >= (ft+fp) && frameCount <= (ft+fp+fc) ) { if (frameCount == ft+fp){ for (int i = 0; i < vs.size(); i++) { float[] v1 = vs.get(i); float[] v2 = og_vs.get(i); vd.add( new float[] {(v1[0]-v2[0])/fc, (v1[1]-v2[1])/fc, (v1[2]-v2[2])/fc} ); } } else { for (int i = 0; i < vs.size(); i++) { float[] v1 = vs.get(i); float[] v2 = vd.get(i); vs.set(i, new float[] { v1[0]-v2[0],v1[1]-v2[1],v1[2]-v2[2] } ); } } } else{ ft = fr+frameCount; vd = new ArrayList<float[]>(); lln = rad; lix = 0; } }