Three.js生成式NFT艺术品
在本文中,我将尝试简要而完整地概述什么是生成艺术,它如何与 NFT 连接,以及如何开始在区块链上制作生成的东西。我将尝试根据我制作和发布用 javascript 编写的 NFT 生成蘑菇集合的个人经验来回答所有这些问题。
1、NFT概述
我喜欢编写不寻常的东西只是为了好玩。在新年假期期间,我被关于 NFT 的消息所淹没,以至于我最终决定尝试在这个范式中创造一些有创意的东西。我从来没有对将 JPEG 上传到区块链的想法感到兴奋,但链上生成艺术的可能性引起了我的注意。
简而言之,它背后的想法是制作一些通证生成器,每次你“铸造”它时都会给你一个独特的艺术品。实际上,调用区块链中的一个方法,它会花费你的一些钱来执行它,同时也会给你一些钱给艺术家。毫无疑问,你的交易会产生一个独特的对象,该对象将永远存储在区块链中,这确实是一种神奇的感觉,不是吗?
有一些艺术平台利用了这个想法,其中最著名的是artblocks.io. 但由于它有很多官僚作风,而且它建立在以太坊区块链上,它仍然使用工作量证明并且gas价格非常高,我决定尝试一个更民主、更便宜、环保平台——fxhash.xyz
什么是生成式 NFT 艺术品?
所有的生成 NFT 基本上都是网页,它们使用 vanilla javascript 或一些第三方库在画布上绘制一些东西。尝试进行分类,从我的角度来看,我会将所有生成 NFT 大致分为 3 类:抽象数学艺术品、具体程序艺术品和可变手绘艺术品。
第一类,抽象数学,利用一些数学概念来生成抽象图像:可能有一些分形、吸引子、元胞自动机等。程序艺术试图使用参数化来描述一些具体的事物。第三类,可变手绘,通常是对图像的一些预先绘制的部分进行简单随机化。
此外,还有一些实验性和互动性的作品,甚至模块化合成器和游戏, 但这些比较少见。
所以我们在本文中要做的是描述一个蘑菇的过程模型,并使用事务哈希对其进行随机化。结合艺术视野、构图和风格化,这为我们提供了所谓的生成式 NFT 艺术品。
2、画蘑菇🍄
好的,让我们结束所有这些理念,然后进入技术部分。该项目完全使用three.js库,它有一个合理的简单且有据可查的 API.
3、菌柄的生成
基本上,可以将菌柄参数化为沿某个样条线(我们称其为基本样条线)的闭合轮廓挤压。创建我使用的基本样条线来自threejs 的CatmullRomCurve3类。然后,我通过沿基本样条线移动另一个封闭形状来逐个顶点地创建几何图形,最后将这些顶点与面连接起来。为此我用了BufferGeometry。
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();
4、菌柄噪声的添加
为了更自然,菌柄表面可能会随着它的高度而变化。我将菌柄半径定义为基本样条曲线上点的角度和相对高度的函数。然后,根据这些参数将少量噪声添加到半径值。
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;
}
5、盖帽的生成
盖帽 也可以参数化为围绕菌柄顶部旋转的样条曲线,我们也称其为基本样条曲线。让我们将此旋转产生的表面命名为基础表面。然后将基面定义为基样条上点的位置和围绕菌柄顶部的旋转的函数。这种参数化将允许我们稍后优雅地应用一些噪声到表面。
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();
6、盖帽噪音的添加
为了更真实,帽子还需要一些噪音。我将帽噪声分为 3 个分量:径向噪声、角度噪声和法线噪声。径向噪声会影响顶点在基本样条上的相对位置。角噪声改变了围绕柄顶部的基本样条旋转的角度。
最后,法线噪声会在该点正常地改变顶点沿基面的位置。在极坐标系中定义帽表面时,对其应用 2d 柏林噪声 产生扭曲很有用。我用了noisejs 库。
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;
}
7、蘑菇其余部分的生成
鳃和环的几何形状与帽的几何形状非常相似。创建比例的一种简单方法是在帽表面上的一些随机锚点周围生成嘈杂的顶点,然后基于他们创建ConvexGeometry。
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);
8、碰撞检查
为了防止在场景中生成多个蘑菇时出现不真实的交叉点,需要检查它们之间的碰撞。在这里我找到了一个代码片段使用来自每个网格点的光线投射检查碰撞。
为了减少计算时间,我生成了蘑菇的低多边形孪生以及蘑菇本身。然后使用这个低多边形模型来检查与其他蘑菇的碰撞。
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...
}
}
9、渲染和风格化
最初,我想实现 2d 绘图的效果,尽管所有的生成都是用 3d 制作的。在风格化的背景下,首先想到的是轮廓效果。我不是着色器的专业人士,所以我只是利用了此示例的轮廓效果. 使用它,我得到了蘑菇轮廓的漂亮铅笔样式:
下一件事是适当的着色。纹理应该有点嘈杂并且有一些柔和的阴影。对于像我这样不想处理 UV 贴图的人来说,有一个懒惰的技巧。可以使用BufferGeometryAPI 定义对象的顶点颜色,并使用 UV 包裹它。不仅如此,使用这种方法还可以将顶点的颜色参数化为角度和位置的函数,因此噪声程序纹理的生成变得稍微容易一些。
最后,我使用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();
10、名称生成
对于名称生成,我使用了一个简单的马尔可夫链,它利用这里 的数据进行了 1k 个蘑菇名称的训练. 为了预处理和标记这些名称,我使用了 python 库YouTokenToMe. 有了它,我将所有名称拆分为 200 个唯一标记,并将它们的转换概率写入 javascript 字典。代码的 JS 端只读取这些概率并堆叠标记,直到它生成几个单词。
以下是使用这种方法生成的一些蘑菇名称示例:
Stricosphaete cinus
Fusarium sium confsisomyc
Etiformansum poonic
Hellatatum bataticola
Armillanata gossypina mortic
Chosporium anniiffact
Fla po sporthrina
11、结束语
要准备一个项目以在 fxhash 上发布,只需将代码中的所有随机调用更改为 fxrand()
,方法参见这里描述. 主要思想是你的代码必须为每个哈希生成唯一的输出,但对于相同的哈希生成完全相同的输出。然后在沙箱中测试通证,最后铸币。这样就可以了!
这将我们带到了蘑菇地图集(我的这个集合的命名)。你可以在这里检查一下,看看它的变化. 虽然它不像我之前的一些作品那样售罄,但我认为这是我在生成艺术中所做的最先进和最具挑战性的事情。希望铸造这个NFT的人也能在不可替代的世界里享受他们的真菌!
原文链接:How to Draw Generative NFT Mushrooms with Three.js
BimAnt翻译整理,转载请标明出处