共计 3924 个字符,预计需要花费 10 分钟才能阅读完成。
背景阐明
无论是这样的:
还是这样的:
其实都是由背景、角色、组件拼接而成的图片。
假如用以下素材去生成 NFT 盲盒:
- 背景:10 种不同的色彩
- 角色:10 种不同的肤色
-
配饰:
- 10 种帽子
- 10 种衣服
- 10 种手持物品
这么简略的 50 个素材,就曾经能够拼接成 10*10*10*10*10 = 10 万种不同的组合。
保姆级教程
以虚构我的项目案例手把手入门。示例我的项目代码下载:https://download.csdn.net/download/jslygwx/77829790
假如我的项目状况:
- 共计合成 10 万张图片
- 有 8 种类型素材(背景、角色、肤色、物品、配饰之类的,共计 8 种)
- 每种类型再提供 10 张素材图片
- 罕见水平设定(总计概率 100%)
1 / 100,
2 / 100,
3 / 100,
4 / 100,
10 / 100,
10 / 100,
16 / 100,
17 / 100,
18 / 100,
19 / 100
就轻易设置这么样个概率作为演示。(后续进阶教程再说特定罕见水平设定和个性化素材配置的相干内容)
生成组合
从简略例子着手,比如说 8 种部件类型,就按 0~7 的程序别离建设文件夹,示例:
- 0 背景图
- 1 角色
- 2 角色肤色
- 3 角色衣服
- 4 角色帽子
- 。。。。。。
// 设置总数 10 万
const TOTAL = 1e5;
// 类型定义,别离为 0 1 2 3 4 5 6 7
const TYPE = new Array(8).fill(0).map((_, i) => i);
// 部件定义,别离为 0 1 2 3 ... 9
// 如果超过 10,能够用字母 a b c d e 持续排序
const COMP = new Array(10).fill(0).map((_, i) => i);
// 对应 0 ~ 9 号部件生成的概率
const PR = [
1 / 100,
2 / 100,
3 / 100,
4 / 100,
10 / 100,
10 / 100,
16 / 100,
17 / 100,
18 / 100,
19 / 100
];
const START_TIME = new Date();
const RESULTS = new Set<string>();
// 打乱并生成
const END_TIME = new Date();
console.log(`Spent: ${END_TIME.getTime() - START_TIME.getTime()}ms`);
console.log(JSON.stringify([...RESULTS], null, 2));
以上算法(见我的项目示例代码),生成 500 万条数据应该也是在 1s 以内。
执行后果:
Spent: 34ms
[
"99696789",
"64766443",
"44467446",
//。。。10 万条数据
]
为什么应用随机种子。因为这样能保障所有的组合,都正好合乎概率,而且保障了程序运行的性能。
如果感觉生成的后果不够乱序,能够在上述标记排序的地位批改排序规定,或者多执行几次 sort()
打乱。
生成图片(繁多,一张)
其中以 99696789
为例阐明:
- 背景:9 ——第 10 种一般素材
- 角色:9 ——第 10 种一般素材
- 肤色:6 ——第 7 种一般素材
- ……
将各个素材从底向上拼接成图片,一个 NFT 即为生成。
生成图片:
node generate.js 99001122
生成单张图片,大概在 0.5~1.5 秒左右。
批量生成 NFT 图片
这里咱们能够用到多线程和音讯队列。
本地先启动 Redis(能够清空 db0)。跑一遍该脚本,把 10 万条工作插入到队列:
import * as fs from 'fs';
import * as path from 'path';
import {WhiteQ} from 'whiteq';
const wq = new WhiteQ();
const file = fs.readFileSync(path.join(__dirname, '../100k.log'), 'utf-8');
const tasks = file.split('\n');
wq.addJobs(
'tasks',
tasks.map((t) => ({name: t, data: t}))
)
.then(() => {process.exit(0);
})
.catch(console.error);
而后用 PM2 执行该脚本去多线程执行生成工作:
const {WhiteQ} = require('whiteq');
const {execSync} = require('child_process');
const path = require('path');
const wq = new WhiteQ();
wq.worker('tasks', async (job) => {console.log(job.name);
const result = execSync(`node ${path.join(__dirname, 'generate.js')} ${job.name}`, {encoding: 'utf-8'});
if (result.includes('Time spent')) {return true;}
console.log(result);
return false;
});
实测 10 万张图片生成的话,大概 5 个小时左右;一万张图片大概不到半个小时。
图片压缩
能够把原有 Redis 记录清空,并从新插入工作队列。
应用 pngquant 压缩,能将本来 3MB 左右的 png 图片压缩到 100~300KB。
批量重命名
压缩过的图片都带有 -fs8.png
的文件后缀,如果想要去掉,则能够应用批量重命名的形式:
const {WhiteQ} = require('whiteq');
const {execSync} = require('child_process');
const path = require('path');
const wq = new WhiteQ();
wq.worker('tasks', async (job) => {console.log(job.name);
execSync(`rm -rf ${path.join(__dirname, `../output/${job.name}.png`)}`);
execSync(`mv ${path.join(__dirname, `../output/${job.name}-fs8.png`)} ${path.join(__dirname, `../output/${job.name}.png`)}`
);
return true;
});
进阶教程
你能够对于部件的类型,每种部件元素的数量,以及各个元素掉落概率都有定制化的要求,那么则能够参考这个生成脚本来生成组合:
const TOTAL = 1e5;
const START_TIME = new Date();
const PR = {
'1background': [
8.8 / 100,
9.0 / 100,
8.7 / 100,
9.1 / 100,
9.0 / 100,
8.9 / 100,
9.2 / 100,
8.8 / 100,
8.5 / 100,
10 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100
],
'2skin': [
9.1 / 100,
9.2 / 100,
9.3 / 100,
9.0 / 100,
9.1 / 100,
8.4 / 100,
9.7 / 100,
8.5 / 100,
9.9 / 100,
8.8 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100
],
'3clothes': [
9.1 / 100,
9.2 / 100,
9.3 / 100,
9.0 / 100,
9.1 / 100,
8.4 / 100,
9.7 / 100,
8.5 / 100,
9.9 / 100,
8.8 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100
],
'4necklace': [
9.1 / 100,
10.5 / 100,
10.1 / 100,
10.6 / 100,
9.1 / 100,
9.0 / 100,
10.7 / 100,
9.2 / 100,
9.9 / 100,
10.8 / 100,
1 / 100
],
'5bag': [
9.6 / 100,
10.5 / 100,
10.1 / 100,
10.6 / 100,
9.1 / 100,
9.5 / 100,
10.7 / 100,
9.2 / 100,
9.9 / 100,
10.8 / 100
],
'6head': [
9.1 / 100,
9.3 / 100,
10.1 / 100,
9.4 / 100,
9.1 / 100,
9.0 / 100,
9.4 / 100,
9.2 / 100,
9.0 / 100,
9.4 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100
],
'7glasses': [
9.1 / 100,
9.2 / 100,
9.3 / 100,
9.2 / 100,
9.1 / 100,
8.8 / 100,
9.7 / 100,
8.9 / 100,
8.9 / 100,
8.8 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100
],
'8accessories': [
8.1 / 100,
9.2 / 100,
8.3 / 100,
9.0 / 100,
9.1 / 100,
8.4 / 100,
9.7 / 100,
8.5 / 100,
8.9 / 100,
8.8 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100,
1 / 100
]
};
const END_TIME = new Date();
console.log(`Spent: ${END_TIME.getTime() - START_TIME.getTime()}ms`);
console.log([...RESULTS].join('\n'));
其实比拟看一下代码的差别就晓得了。很容易了解。
生成胜利后,公布即可。
须要我的项目领导请 pm 分割。