作者丨ferluht
编译丨崔皓
策划丨孙淑娟、梁策
一、简介
在本文中,我将尝试简要概述什么是生成艺术,怎样将它与 NFT(Non-Fungible Token,非同质化代币)联系起来,以及如何在区块链上制作生成品。蘑菇的设计将会用到 JavaScript 以及 Three.js,本文将借此向大家介绍如何在虚拟世界生成和发布艺术作品。
背景
出于兴趣,我常会写些奇奇怪怪的文章。新年期间,关于 NFT 的消息一度让我大为震惊,所以我也想试试在这种模式下玩出一些有创意的东西。我之前不觉得把 JPEG 上传到区块链上有什么,但是能够将艺术品上链的可能性却让人满怀期待。
简而言之,这背后的思想是制造一些代币生成器,每次你“铸造”代币的时候会给你一个独特的艺术品(实际上,是区块链中的调用方法,你既需要在其执行时花钱,也需要付给艺术家一定钱款)。很显然,你可能会有一种奇妙感受,觉得这笔交易生成了一些独特物品,并永远存储在区块链中。
由此有一些艺术平台利用了这个想法,其中最著名的是 artblocks.io。它建立在以太坊区块链的基础上,仍然使用工作量证明 (proof-of-work) 模式并且 Gas(手续费)非常高。所以,我决定尝试一个更自主、更便宜、更环保的平台 - fxhash.xyz。
什么是生成式 NFT 艺术品?
所有的生成 NFT 艺术品(NFT,全称为 Non-Fungible Token,指非同质化代币,包括 jpg 和视频剪辑形式,是用于表示数字资产的唯一加密货币令牌,可以买卖。)基本上都以网页方式呈现,并使用 vanilla JavaScript 或一些第三方库在画布上绘制。NFT 艺术品可以分为三类:抽象数学艺术品、有形程序艺术品和可变手绘艺术品。
如图 1 所示。
第一类,抽象数学艺术品,利用一些数学概念来生成抽象图像,可能有一些分形(fractals,一种复杂的几何形状)、吸引子(attractors,一个系统有朝某个稳态发展的趋势,这个稳态就叫做吸引子)、元胞自动机(cellular automata,是一种时间、空间、状态都离散,空间相互作用和时间因果关系为局部的网格动力学模型,具有模拟复杂系统时空演化过程的能力。)等。
第二类,有形程序艺术品,试图使用参数化的方式来描述一些具体的事物。
第三类,可变手绘艺术品,是对图像预先绘制的部分进行简单随机化处理。
图 1
此外,还有一些实验性和互动性的作品,甚至还有模块化合成器和游戏之类的艺术品,不过比较少见。本文将描述一个蘑菇艺术品的创作过程,并使用交易哈希对其进行随机化。再加上一些艺术视野、构图和风格化,我们最终创作出了这个“生成式 NFT 艺术品”。
二、画蘑菇
Otinium caseubbacula — 生成的蘑菇作品之一
介绍完理论知识,我们进入技术环节。本项目完全使用 Three.js 库,它有一个使用简单的 JavaScript 库,其 API 使用方式很容易在网上找到。
1、生成菌柄
首先要绘制出菌柄的样条线(我们称其为基本样条线,样条线的作用是辅助生成实体),针对该样条线进行参数化形成菌柄的轮廓。为了创建基本样条线,我使用了 Three.js 中的 CatmullRomCurve3 类。然后,通过沿基本样条线的移动,创造闭合形状来构建对应的几何图形,最后将这些图形连接起来,如图 2 所示。为此使用了 Three.js 中的 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();
菌柄生成的阶段:样条、顶点、面。图 2
2、菌柄噪声
为了让菌柄看上去更自然,其表面会随着高度而发生变化,我们定义了噪声函数,该函数对半径进行定义,通过改变基本样条曲线上点的角度和相对高度两个参数的方式生成不一样的半径信息。代码如下所示:
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;
菌柄噪声变化。图 3
如图 3 所示,通过半径的噪声函数,根据角度和高度生成不同的半径。
3、菌帽
菌帽的生成方式也可以通过在菌柄顶部加入旋转的样条曲线,然后再对曲线进行参数化来完成。这里可以将旋转产生的表面命名为基础表面,然后定义基础表面在基础样条上的位置,再加入围绕菌柄顶部旋转的函数。这种参数化的编程方式将方便后面加入噪声函数。代码如下:
adial 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();
帽生成阶段:样条、顶点、面。图 4
如图 4 所示,通过基础样条以及顶点生成基础平面,这个平面就是菌帽。
菌帽噪声
同样为了让艺术品看上去更加真实,菌帽也需要加入一些噪声。我将菌帽噪声分为三类:径向噪声、角度噪声和法线噪声。径向噪声会影响顶点在基本样条上的相对位置。角度噪声改变了围绕柄顶部基本样条旋转的角度。最后,法线噪声会改变顶点沿基面的位置。在坐标系中定义菌帽表面时,会对扭曲应用 2d Perlin 噪声,因此这里会使用 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;
从左到右的噪声分量:径向、角度、法线。图 5
如图 5 所示,从左到右分别给菌帽径内噪声、角度噪声和法线噪声。
蘑菇的其余部分:菌鳞、菌褶、菌环
菌褶和菌环的几何形状与菌帽的几何形状非常相似。可以在帽表面上的一些随机锚点周围生成嘈杂的顶点,然后基于它们创建 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);
鳞片、鳃、环和蘑菇的完整几何形状。图 6
如图 6 所示,绘制出菌鳞、菌褶、菌环和蘑菇的完整几何形状。
碰撞检查
由于在同一个场景中会生成多个蘑菇,此时蘑菇之间会产生交叉层叠的情况,所以需要检查它们之间的碰撞关系。这里使用一个代码片段来自检测每个网格点的光线投射,从而达到检查蘑菇碰撞的目的。
为了减少计算时间,我在生成这一蘑菇时还生成了其低模。然后使用这个低模来检查它与其他蘑菇的碰撞。
代码如下:
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...
}
用于更快碰撞检查的简化模型。图 7
如图 7 所示,检查蘑菇的碰撞效果。
渲染和风格化
最初,我想实现 2D 绘图的效果,虽然所有的生成都是用 3D 制作的。在风格化的背景下,我首先想到的是轮廓效果。由于我们不是着色方面的专家,所以只是从这个例子中获取了轮廓效果,并得到蘑菇轮廓的铅笔画风格。
图 8
如图 8 所示,显示蘑菇的铅笔画风格。
接着就是着色了,蘑菇的纹理应该有噪点并且有一些柔和的阴影。如果不想处理 UV 的话,简便的方式就是使用 BufferGeometry API 定义对象的顶点颜色。使用这种方法还可以将顶点的颜色进行参数化,也就是加入角度和位置作为参数从而改变颜色,因此噪声纹理的生成变得稍微容易一些了。
图 9
如图 9 所示,通过角度和位置给顶点添加颜色。
最后,使用 EffectComposer 添加了一些全局噪声和类似膜颗粒效果(Film Grain 是模拟照相膜的随机光学纹理)。代码如下:
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
如图 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
如图 11 所示,在 fxhash 上铸造的 15 个蘑菇。
准备发布
首先,要准备一个项目在 fxhash 上发布,只需将代码中的所有随机调用更改为 fxrand() 方法。主要思想是必须为每个哈希生成唯一的输出,需要对相同的哈希生成完全相同的输出。然后在沙箱中测试代币,最后在铸币过程开启时铸币。
于是我们的《蘑菇图集》就诞生了。你可以在此处查看并关注它的变化。虽然它不像之前的一些作品那样很快售罄,但我认为这是自己艺术生涯中最前沿也最具挑战性的作品。希望铸造这个代币的人也能在非同质化代币的世界中领略不同质的美!
译者介绍
崔皓,51CTO 社区编辑,资深架构师,拥有 18 年的软件开发和架构经验,10 年分布式架构经验。曾任惠普技术专家。乐于分享,撰写了很多热门技术文章,阅读量超过 60 万。《分布式架构原理与实践》作者。