paint-brush
How to Draw Generative NFT Mushrooms with Three.js 🍄by@ferluht
4,490 reads
4,490 reads

How to Draw Generative NFT Mushrooms with Three.js 🍄

by ferluhtFebruary 16th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this article, I’ll try to give a brief but complete overview of what generative art is, how it is connected to NFTs, and how one can start making generative things on a blockchain. I’ll try to answer all these questions based on my personal experience of making and releasing a NFT collection of generative mushrooms written in javascript.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Draw Generative NFT Mushrooms with Three.js 🍄
ferluht HackerNoon profile picture

Part 1: Intro

In this article, I’ll try to give a brief but complete overview of what generative art is, how it is connected to NFTs, and how one can start making generative things on a blockchain. I’ll try to answer all these questions based on my personal experience of making and releasing an NFT collection of generative mushrooms written in javascript.

Background

I love to code unusual things just for fun. During the New Year holidays, I was spammed so hard by news about NFTs that I finally decided to try to make something creative in this paradigm. I was never excited by the idea of uploading JPEGs onto a blockchain, but the possibility of onchain generative art grabbed my attention.


Briefly, the idea behind it is to make some token generator that gives you a unique art object each time you “mint” it (in actuality, call a method in the blockchain which spends some of your money on its execution and also gives some money to the artist). Definitely, there is some magic in the feeling that your transaction generates a unique object which will be stored into the blockchain forever, isn’t it?


There are some art platforms that exploit this idea, the most famous of them is artblocks.io. But as it is has a lot of bureaucracy to enter and also it is built on the Ethereum blockchain, which still uses proof-of-work and has a very high gas price, I decided to try myself on a more democratic, cheap, and eco-friendly platform - fxhash.xyz

What is a generative NFT artwork?

All the generative NFTs are basically webpages that draw something on the canvas using either vanilla javascript or some third-party libraries. Taking a stab at classification, from my perspective I’d broadly divide all generative NFTs into 3 categories: abstract math artworks, concrete procedural artworks, and variative hand-drawn artworks.


The first class, abstract math, utilizes some mathematical concepts to generate an abstract image: there may be some fractals, attractors, cellular automatons, etc. Procedural arts are trying to describe some concrete things using parametrizations. And the third class, variative hand-drawn, is usually simple randomization of some pre-drawn parts of the image.


Also, there are some experimental and interactive works, even modular synthesizers and games, but these are much rarer.


Left to right examples of math, procedural and variative artworks. Credits: ciphrd, zancan, littlesilver


So what we will do during this article is to describe a procedural model of a mushroom and randomize it using the transaction hash. Combined with an artistic vision, composition, and stylization this gives us what’s called a generative NFT artwork.



Part 2: Drawing a mushroom 🍄


Otinium caseubbacula — one of the generative mushroom specimens


Ok, let’s end up with all that philosophy and move on to the technical part. This project was made entirely using Three.js library, which has a reasonably simple and well-documented API.

Stipe

Basically, a stipe can be parametrized as a closed contour extrusion along some spline (let’s call it base spline). To create the base spline I used CatmullRomCurve3 class from Three js. Then, I created the geometry vertex-by-vertex by moving another closed shape along the base spline and finally connected those vertices with faces. I used BufferGeometry for that purpose.


stipe_vSegments = 30; // vertical resolution
stipe_rSegments = 20; // angular resolution
stipe_points = []; // vertices
stipe_indices = []; // face indices

stipe_shape = new THREE.CatmullRomCurve3( ... , closed=false );

function stipe_radius(a, t) { ... }

for (var t = 0; t < 1; t += 1 / stipe_vSegments) {
  // stipe profile curve
  var curve = new THREE.CatmullRomCurve3( [
    new THREE.Vector3( 0, 0, stipe_radius(0, t)),
    new THREE.Vector3( stipe_radius(Math.PI / 2, t), 0, 0 ),
    new THREE.Vector3( 0, 0, -stipe_radius(Math.PI, t)),
    new THREE.Vector3( -stipe_radius(Math.PI * 1.5, t), 0, 0 ),
  ], closed=true, curveType='catmullrom', tension=0.75);

  var profile_points = curve.getPoints( stipe_rSegments );

  for (var i = 0; i < profile_points.length; i++) {
  	stipe_points.push(profile_points[i].x, profile_points[i].y, profile_points[i].z);
  }
}

// <- here you need to compute indices of faces
// and then create a BufferGeometry
var stipe = new THREE.BufferGeometry();
stipe.setAttribute('position', new THREE.BufferAttribute(new Float32Array(stipe_points), 3));
stipe.setIndex(stipe_indices);
stipe.computeVertexNormals();


Stages of a stipe generation: spline, vertices, faces

Stipe noise

To be more natural, the stipe surface may somehow vary along with its height. I defined stipe radius as a function of the angle and relative height of the point on the base spline. Then, a slight amount of noise is added to the radius value depending on these parameters.


base_radius = 1; // mean radius
noise_c = 2; // higher this - higher the deformations

// stipe radius as a function of angle and relative position
function stipe_radius(a, t) {
	return base_radius + (1 - t)*(1 + Math.random())*noise_c;
}


Stipe noise variations

Cap

Cap can also be parameterized as a spline (let’s also call it a base spline) rotating around the top of the stipe. Let’s name the surface spawned by this rotation a base surface. Then base surface will be defined as a function of the position of a point on the base spline and the rotation around the stipe top. This parametrization will allow us to gracefully apply some noises to the surface later.


cap_rSegments = 30; // radial resolution
cap_cSegments = 20; // angular resolution

cap_points = [];
cap_indices = [];

// cap surface as a function of polar coordinates
function cap_surface(a0, t0) {
  // 1. compute (a,t) from (a0,t0), e.g apply noise
  // 2. compute spline value in t
  // 3. rotate it by angle a around stipe end
  // 4. apply some other noises/transformations
  ...
  return surface_point;
}

// spawn surface vertices with resolution
// cap_rSegments * cap_cSegments
for (var i = 1; i <= cap_rSegments; i++) {
  var t0 = i / cap_rSegments;
  for (var j = 0; j < cap_cSegments; j++) {
    var a0 = Math.PI * 2 / cap_cSegments * j;
    var surface_point = cap_surface(a0, t0);
    cap_points.push(surface_point.x, surface_point.y, surface_point.z);
  }
}

// <- here you need to compute indices of faces
// and then create a BufferGeometry
var cap = new THREE.BufferGeometry();
cap.setAttribute('position', new THREE.BufferAttribute(new Float32Array(cap_points), 3));
cap.setIndex(cap_indices);
cap.computeVertexNormals();


Stages of cap generation: spline, vertices, faces

Cap noise

To be more realistic, the cap also needs some noise. I divided cap noise into 3 components: radial, angular and normal noises. Radial noise affects the relative position of the vertex on the base spline. Angular noise changes the angle of base spline rotation around the top of the stipe.


And finally, normal noise changes the position of the vertex along the base surface normally at that point. While defining the cap surface in a polar coordinate system it’s useful to apply 2d Perlin noise for these distortions. I used noisejs library for that.


function radnoise(a, t) {
  return -Math.abs(NOISE.perlin2(t * Math.cos(a), t * Math.sin(a)) * 0.5);
}

function angnoise(a, t) {
  return NOISE.perlin2(t * Math.cos(a), t * Math.sin(a)) * 0.2;
}

function normnoise(a, t) {
  return NOISE.perlin2(t * Math.cos(a), t * Math.sin(a)) * t;
}

function cap_surface(a0, t0) {
  // t0 -> t by adding radial noise
  var t = t0 * (1 + radnoise(a, t0));

  // compute normal vector in t
  var shape_point = cap_shape.getPointAt(t);
  var tangent = cap_shape.getTangentAt(t);
  var norm = new THREE.Vector3(0,0,0);
  const z1 = new THREE.Vector3(0,0,1);
  norm.crossVectors(z1, tangent);

  // a0 -> a by adding angular noise
  var a = angnoise(a0, t);
  var surface_point = new THREE.Vector3(
    Math.cos(a) * shape_point.x,
    shape_point.y,
    Math.sin(a) * shape_point.x
  );

  // normal noise coefficient
  var surfnoise_val = normnoise(a, t);

  // finally surface point
  surface_point.x += norm.x * Math.cos(a) * surfnoise_val;
  surface_point.y += norm.y * surfnoise_val;
  surface_point.z += norm.x * Math.sin(a) * surfnoise_val;

  return surface_point;
}


Noise components from left to right: radial, angular, normal

The rest of the shroom: scales, gills, ring

The geometries of the gills and ring are very similar to the geometry of the cap. An easy way to create scales is to spawn noisy vertices around some random anchor points on the cap surface and then create ConvexGeometry based on them.


bufgeoms = [];
scales_num = 20;
n_vertices = 10;
scale_radius = 2;

for (var i = 0; i < scales_num; i++) {
  var scale_points = [];

  // choose a random center of the scale on the cap
  var a = Math.random() * Math.PI * 2;
  var t = Math.random();
  var scale_center = cap_surface(a, t);

  // spawn a random point cloud around the scale_center
  for (var j = 0; j < n_vertices; j++) {
    scale_points.push(new THREE.Vector3(
      scale_center.x + (1 - Math.random() * 2) * scale_radius, 
      scale_center.y + (1 - Math.random() * 2) * scale_radius,
      scale_center.z + (1 - Math.random() * 2) * scale_radius
	);
  }

  // create convex geometry using these points
  var scale_geometry = new THREE.ConvexGeometry( scale_points );
  bufgeoms.push(scale_geometry);
}

// join all these geometries into one BufferGeometry
var scales = THREE.BufferGeometryUtils.mergeBufferGeometries(bufgeoms);


Scales, gills, ring and the full geometry of a mushroom

Collisions check

To prevent unreal intersections when spawning multiple mushrooms in the scene one needs to check collisions between them. Here I found a code snippet that checks collisions using raycasting from each mesh point.


To reduce computation time I generate a low-poly twin of the mushroom along with the mushroom itself. This low-poly model then is used to check collisions with other shrooms.


for (var vertexIndex = 0; vertexIndex < Player.geometry.attributes.position.array.length; vertexIndex++)
{       
    var localVertex = new THREE.Vector3().fromBufferAttribute(Player.geometry.attributes.position, vertexIndex).clone();
    var globalVertex = localVertex.applyMatrix4(Player.matrix);
    var directionVector = globalVertex.sub( Player.position );

    var ray = new THREE.Raycaster( Player.position, directionVector.clone().normalize() );
    var collisionResults = ray.intersectObjects( collidableMeshList );
    if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() ) 
    {
        // a collision occurred... do something...
    }
}


Simplified models for faster collision check

Rendering and stylization

Initially, I wanted to achieve an effect of 2d-drawing despite all the generation being made in 3d. The first thing that comes to mind in the context of stylization is the outline effect. I’m not a pro in shaders so I just took the outline effect from this example. Using it I got a nice pencil style of the shroom contour.


Three js outline effect


The next thing on the way back to 2d is proper colorization. The texture should be a bit noisy and have some soft shadows. There is a lazy hack for those who, like me, don't want to deal with UV-maps. Instead of generating a real texture and wrapping it using UV one can define vertex colors of an object using BufferGeometry API. More than that, using this approach the color of a vertex can be also parameterized as a function of angle and position, so the generation of a noisy procedural texture becomes slightly easier.


Adding some vertex color


Finally, I added some global noise and film-like grain using EffectComposer.


var renderer = new THREE.WebGLRenderer({antialias: true});
outline = new THREE.OutlineEffect( renderer , {thickness: 0.01, alpha: 1, defaultColor: [0.1, 0.1, 0.1]});
var composer = new THREE.EffectComposer(outline);

// <- create scene and camera

var renderPass = new THREE.RenderPass( scene, camera );
composer.addPass( renderPass );

var filmPass = new THREE.FilmPass(
  0.20,   // noise intensity
  0.025,  // scanline intensity
  648,    // scanline count
  false,  // grayscale
);

composer.addPass(filmPass);
composer.render();


Almost ready, colored and noisy shrooms

Name generation

For name generation, I used a simple Markov chain which was trained on 1k mushroom names from here. To preprocess and tokenize those names I used the python library YouTokenToMe. With it, I split all names into 200 unique tokens and wrote their transition probabilities to a javascript dictionary. The JS side of the code only reads those probabilities and stacks tokens until it generates a couple of words.


Here are some samples of mushroom names generated using this approach:


Stricosphaete cinus

Fusarium sium confsisomyc

Etiformansum poonic

Hellatatum bataticola

Armillanata gossypina mortic

Chosporium anniiffact

Fla po sporthrina



Part 3: Finalizing

The first 15 shrooms minted on fxhash

Preparing for the drop

To prepare a project for a release on fxhash one simply needs to change all random calls in the code to the fxrand() method as described here. The main idea is that your code must generate unique outputs for each hash but exactly the same output for the same hash. Then test the token in the sandbox and finally mint it when the minting will be opened. That’s it!


This brings us to the Mushroom Atlas (what I named this collection). You can check it out and see its variations here. Although it was not sold out like some of my previous works, I think that this is the most advanced and challenging thing that I’ve made in generative art yet. Hope that those who minted this token also enjoyed their fungi in the non-fungible world!

Links