artcraft 这名字拆开就是 art 加 craft,艺术与手艺的结合。不过先说清楚,我这里讲的不是木工房,也不是手账拼贴,而是一套用代码批量生成视觉作品的程序化艺术工具链。核心想法很朴素:把一次性的"灵感"拆成可复现、可调节、可批量产出的"参数",让艺术创作本身也变成一种工程实践。
这个项目适合谁?想给博客文章配独一无二头图的独立博主,想给游戏快速产出风格统一素材的独立开发者,或者单纯觉得"拿 random 函数画画"很好玩的程序员,artcraft 这套思路都能给你一个立刻能上手的落地方案。不需要复杂的三维渲染管线,也不依赖付费商业软件,一台普通电脑加一个 Python 环境就够跑。
我自己写这个项目最大的感受是:艺术创作里九成的工作不是"画",而是"定规则"。规则定了,剩下的事交给参数和随机种子跑就行。
1. 项目定位与整体设计思路:为什么把艺术变成代码
1.1 传统创作方式的三个痛点
先讲我遇到的真实问题。之前做一个小型独立游戏项目,美术风格定的是"低多边形抽象几何风",听起来简单,实际要产出几十张背景素材、上百个装饰元素。如果走传统流程,要么手绘,要么用绘图软件一张张做,三个问题立刻暴露。
第一,不可复现。手绘一张图,想改一版配色就得重画,哪怕只是把主色相从偏红改成偏蓝,也要重新涂抹大半天。软件绘图虽然能存档,但"随机生成"这个动作基本做不了——你没法让软件自动生成一百个互不重复又风格统一的元素。
第二,不可参数化。一张素材的尺寸、配色、线条密度、形状复杂度全是固定的,换个应用场景就得重做。代码生成可以把这些全部变成变量:要 1920 宽就出 1920,要 64 像素图标就出 64 像素图标,参数一改,整套素材自动适配。
第三,不可批量。手绘一张图可能是享受,画一百张同样风格的图就是灾难。游戏素材、壁纸包、PPT 模板、社交配图,这些需求从来不是一张两张,而是几十上百张。按照传统流程,这部分时间成本根本压不下来。
artcraft 就是冲着解决这三个问题去的。它把创作拆成一套"规则系统":你写一套生成规则,喂不同的随机种子和参数组合,系统批量产出风格一致、内容各异的作品。规则本身可以反复打磨,打磨一次,受益无数次。
1.2 为什么选代码生成而不是绘图软件或三维渲染
有人会问:现在的绘图软件不是有很多滤镜、画笔、自动生成功能吗,为什么非要写代码?
我的回答是:滤镜和画笔解决的是"单张图片的美化",解决不了"一批图片的生成"。你要的是几十上百张互相有关联、风格统一、内容又不重复的画,手动作图和滤镜拼接在效率上完全跟不上。代码生成从第一天起就是程序化、结构化的,批量只是顺带的事情。
另一条路是三维渲染,用常见三维软件建模、打光、出图。优点是真实感强,缺点也很明显:学习成本高、渲染慢,而且对一个 2D 平面视觉项目来说纯属杀鸡用牛刀。为了一张平面纹理去折腾材质节点,性价比太低了。artcraft 选择的 2D 程序化生成有大量成熟算法可以借鉴,不需要昂贵硬件,笔记本就能跑。
当然代码生成也有边界。它不擅长表现"灵光一闪"的具象内容,比如画一个具体人物、一座具体建筑,想通过规则堆出来基本不现实。artcraft 更适合抽象几何、自然纹理、像素风格、生成式配图这类范畴。找准定位之后,代码生成的效率优势是碾压级的。
1.3 技术选型:Python、NumPy 与图像处理库的取舍
我最终选的是 Python 加 NumPy 加 Pillow 图像库,偶尔用标准库里的 json 做配置管理。选型理由很直接:Python 生态里的图像处理工具足够成熟,NumPy 处理像素数组的性能可以应付日常 2D 生成任务,Pillow 负责把数组写成 PNG、JPEG,接口稳定。
相比前端方案,Python 更适合批量离线渲染。比如用 p5.js 做实时预览很酷,但你不可能开个浏览器让脚本跑几百张图。Python 这边直接把脚本丢到服务器上,不依赖图形界面,后台慢慢跑就行。相比 Java 系的 Processing 工具,Python 环境更轻量,写起来也更顺手,尤其适合快速迭代生成规则。
如果以后想做交互版本,可以保留 Python 的生成核心,前端再套一层实时预览界面,两边共用同一套生成逻辑。这个架构的扩展性不错,核心算法不依赖界面,换外层壳很轻松。
2. 核心算法拆解:从随机到美感的四块基石
2.1 种子体系:一切可复现的起点
先说一个每次带新人做生成艺术都会遇到的坑:直接用 Python 的全局 random 函数。随机是够随机了,但完全不可控,同一个程序跑两次结果不一样,看到一版好看的效果想复现,根本找不到入口。
artcraft 从一开始就建立了完整的种子体系,两条规则贯穿始终:
- 一个主种子 master_seed 作为整个作品的唯一标识。
- 每个生成器、每个图层、每个元素都从主种子派生自己的子种子。
这样做的好处是:调整某一个图层的参数不会影响其他图层的随机状态,改一个颜色配置也不会让整张图"变脸"。子种子派生建议写成固定公式,我用的纯函数长这样:
import hashlib def derive_seed(master_seed: int, scope: str, index: int = 0) -> int: data = f"{master_seed}:{scope}:{index}".encode() return int(hashlib.sha256(data).hexdigest(), 16) % (2**32)这里关键点在于不用 Python 内置的 hash 函数。字符串 hash 在不同进程、不同启动环境下可能有随机化处理,结果并不稳定,而这个派生函数是纯数学计算,任何环境都返回同样结果。这一行代码是整条管线可复现的基石,我把它单独放一个 core/seed.py 模块,所有生成器共用。
2.2 噪声函数:让像素连成形态
直接给每个像素分配一个随机值,出来的效果类似电视机雪花屏,又碎又刺眼。艺术创作需要的是空间上连续、有过渡、局部又有变化的自然噪声。
最基础的是值噪声,思路很直白:先在画布上放一张稀疏网格,随机生成每个网格点的值,然后对某个像素所在网格的四个角点做平滑插值,得到该像素的值。这样相邻像素的数值是缓慢变化的,视觉上就像起伏的地形或云层。我在 artcraft 里维护了一个简化实现:
import random def value_noise_2d(x, y, seed, cell=16): xi, yi = int(x // cell), int(y // cell) xf, yf = (x % cell) / cell, (y % cell) / cell def random_at(gx, gy): rng = random.Random(seed + gx * 73856093 + gy * 19349663) return rng.random() v00 = random_at(xi, yi) v10 = random_at(xi + 1, yi) v01 = random_at(xi, yi + 1) v11 = random_at(xi + 1, yi + 1) # 平滑插值,消除线性插值的棱角感 sx = xf * xf * (3 - 2 * xf) sy = yf * yf * (3 - 2 * yf) top = v00 + sx * (v10 - v00) bottom = v01 + sx * (v11 - v01) return top + sy * (bottom - top)再进阶可以用 Perlin 或者 Simplex 噪声,方向性更好,纹理更细腻。但对 artcraft 的多数场景,值噪声加多倍频叠加已经够用。所谓多倍频叠加,就是把不同频率、不同振幅的噪声加在一起,大尺度结构上套小尺度细节,模拟出云层、山脊那种"大体轮廓加局部肌理"的自然感觉。
核心原则还是那句话:随机是零散的,噪声是连续的,连续才会产生形态感,形态感才是程序化生成和乱码像素的本质分界。
2.3 色彩管理:在 HSV 空间里搭调色板
新手最容易忽略色彩表达。在 RGB 空间调一组和谐的颜色,直觉上不太好使,因为红绿蓝三个通道任何一个微调,都会同时改变明度和色相,调着调着颜色就脏了。
artcraft 的调色逻辑全部放在 HSV 空间:色相 H 决定色彩基调,饱和度 S 决定鲜艳程度,明度 V 决定深浅。这样一次只动一个维度,另外两个保持不变,配色思路瞬间清晰。
常用几套配色方案在 HSV 空间里实现非常直观:
- 同类色:主色相 H0 附近左右偏 15 度取色。
- 互补色:主色相 H0 搭配 H0+180 度的色。
- 三角配色:主色相 H0、H0+120、H0+240 三色搭配。
代码里我写一个调色板生成函数,输入主色相、配色模式、颜色数量,输出一组 RGBA 元组,最后由 HSV 转回 RGB 写入像素:
import colorsys def build_palette(base_hue, mode, count): hues = [] if mode == "analogous": hues = [base_hue + i * 0.03 for i in range(count)] elif mode == "complementary": hues = [base_hue + (i % 2) * 0.5 + (i // 2) * 0.03 for i in range(count)] elif mode == "triadic": hues = [base_hue + (i % 3) * 0.3333 for i in range(count)] palette = [] for h in hues: r, g, b = colorsys.hsv_to_rgb(h % 1.0, 0.72, 0.92) palette.append((int(r * 255), int(g * 255), int(b * 255))) return palette这样一套配色方案就是一个函数,改一个 base_hue 参数,整套画面气质完全转变,批量生成换风格非常方便。
还有一个细节容易被忽略:Pillow 写 PNG 默认按 sRGB 处理,高对比显示器上预览和实际打印输出颜色会有偏差。素材交接时最好手动指定色彩空间,至少明确记录导出配置,避免下游环节拿到的颜色和屏幕上差一大截。
2.4 双通道渲染:像素图和矢量图一起出
artcraft 的渲染分两条路,像素通道和矢量通道。
像素通道适合纹理、噪点画、像素风。做法是先算出一张 NumPy 数组,数组每个元素存 RGB 或 RGBA 值,最后用 Pillow 的 fromarray 一次性转成图片。注意一定是先批量构建数组,再一次性转图,不要一个像素一个像素在 Pillow 的 ImageDraw 上画,性能相差几十倍。
矢量通道适合几何图形、线条画、色块构成。做法是用 Pillow 的 ImageDraw 接口画直线、圆弧、多边形,也可以直接输出 SVG 文本。SVG 的好处是无限放大不模糊,适合 logo、图标、印刷物料。我在 artcraft 里一般同时输出 PNG 和 SVG 两版,PNG 用来快速预览,SVG 留给后续二次编辑。两条通道共用同一套参数和种子体系,只是渲染后端不同,切换起来毫无违和感。
3. 实操过程:从零搭起 artcraft 生成管线
3.1 环境准备与目录结构
动手先搭环境,几行命令的事:
mkdir artcraft cd artcraft python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install numpy pillow目录结构不需要花哨,清晰最重要:
artcraft/ ├── configs/ # JSON 参数配置 ├── generators/ # 各类生成器 │ ├── __init__.py │ ├── fractal_tree.py │ ├── dot_grid.py │ ├── ring_cluster.py │ └── terrain_band.py ├── core/ │ ├── noise.py # 噪声函数 │ ├── color.py # 调色板 │ └── seed.py # 种子派生 ├── output/ # 生成产物 └── main.py # 入口脚本这个结构的好处是生成器之间完全独立,想加一种新风格,只要在 generators 目录下新增一个文件,按统一接口实现一个 generate 函数,然后注册到入口脚本即可,老模块不用动。
3.2 五个基础生成器:从瓦片到分形逐一落地
我挑五个有代表性的生成器讲,每个对应一类常见视觉风格,从简单到复杂,刚好覆盖 artcraft 的典型用例。
第一个是像素风瓦片生成器。把画布切成网格,每个格子根据随机值决定是否填充、填充哪个颜色。几十行代码就能做出复古游戏瓷砖地面的效果,代码也短:
import random import numpy as np def generate_pixel_tile(size, cell, palette, seed): rng = random.Random(seed) canvas = np.zeros((size, size, 3), dtype=np.uint8) for y in range(0, size, cell): for x in range(0, size, cell): if rng.random() > 0.25: color = rng.choice(palette) canvas[y:y + cell, x:x + cell] = color return canvas调整 cell 大小可以控制像素颗粒感,调整 0.25 这个阈值可以控制填充密度,两个参数足够玩出很多变化。
第二个是分形树生成器。用递归画树干分叉,分叉角度和长度比是关键参数。深色背景配浅色枝干,出来的图很有植物绘本的气质。递归深度建议控制在 8 到 12 层,太多会导致线条过密,画面糊成一团。
第三个是点线网格生成器。在画布上均匀布点,每个点向最近的几个邻居连线,按随机值决定某条线画不画。效果像星座图或者电路板,科技风封面很常用。
第四个是噪声地形带生成器。用前面写的噪声函数生成二维高度场,按高度分层着色,可以做出等高线地图和云雾纹理。层数越多,色带越细,画面越细腻,也需要关注性能。
第五个是圆环聚合生成器。画布上随机撒圆心,围绕圆心画多层圆环,半径和透明度做正弦变化。这个生成器适合抽象几何海报背景,参数调对了很有现代感。
3.3 配置化的参数控制面板:JSON 加命令行覆盖
光有生成器还不够,你得能方便地调节参数。artcraft 里我用 JSON 配置文件管理一切,一份典型配置长这样:
{ "generator": "terrain_band", "size": 1024, "master_seed": 20240517, "palette_mode": "complementary", "base_hue": 0.55, "octaves": 5, "frequency": 8.0, "output_dir": "output/demo" }这样做的最大收益在于:每一次实验的完整参数集都保存在一个文件里。想复现任何一张图,只要拿到配置文件和里面的主种子,就能一模一样地重新生成。协作时也不用反复问"这张图当时参数是怎么调的",直接把配置文件甩过去就好。
main.py 的入口逻辑我做成接收一个配置文件路径,额外支持命令行覆盖键值对。比如想快速测试不同 hue 值,执行类似python main.py configs/demo.json --override base_hue=0.72就能覆盖配置,既保留了配置文件的完整性,又方便临时实验。
3.4 多进程批量产出与文件命名规范
批量生成是 artcraft 的杀手锏。一次跑一百张图,代码层面用 Python 的ProcessPoolExecutor做多进程并行。生成艺术是纯 CPU 计算,多进程能把多核性能榨干,每个进程从同一个种子列表里取一个种子,独立生成互不干扰。
文件命名规范我踩过坑之后固定成这套格式:{style}_{seed:08d}_{size}px.png。举例来说,terrain_band_20240517_1024px.png 就是一张种子为 20240517 的地形带生成器产物。文件名本身就是元数据,不用打开图片就能知道风格、种子、尺寸。
每个输出目录还会生成一份 manifest.json,记录这一批所有配置和种子。这个文件很小但价值极大,后期想挑几张满意的图重新渲染高清版或者做风格迁移,直接根据 manifest 里的种子回放即可,不需要任何额外的状态记录。
4. 真实跑批中的踩坑记录与排查技巧
4.1 性能瓶颈:双层循环慢到怀疑人生
第一次生成 4096 像素大图时,代码跑了几分钟没出图,我把时间消耗定位在逐像素填充上。问题很典型——我在 Pillow 的 draw 对象上一点一点画像素,这种调用开销极大。
解决方案有两个思路。思路一:改用 NumPy 数组批量计算,最后一次性 fromarray 转成图片,这也是我前面反复强调的点。思路二:把大图拆成小图块分块处理,每个块独立计算再拼接,适合超大画布时降低单次内存占用。两个思路可以结合,分块加块内向量化,性能提升是数量级的。
多进程的内存开销也要注意。每个子进程创建时会复制父进程内存,数组越大翻倍越明显。我的做法是先在 512 或 1024 分辨率下确认构图,最后再上高分辨率渲染,避免反复在超大画布上试错。
4.2 可复现性坑:明明设了种子还是对不上
第二个坑特别隐蔽:第三方库的全局状态会破坏可复现性。我排查过一次"每次跑结果不一样"的问题,最终发现是生成器内部用了 NumPy 或者 Python 的全局随机模块,任何外部调用顺序变化都会改变后续随机序列。
正确做法是每个生成器内部创建独立的随机实例,加上一节 seed_scope 参数,所有随机调用全走这个实例。这样生成器和外部环境天然隔离,调用顺序完全不影响结果。另一个隐蔽点是集合遍历顺序,set 在多个进程中遍历顺序不稳定,绘制顺序一变,结果就变。解决办法是全程用列表,或者对集合排序后再遍历。
4.3 视觉质量问题:椒盐噪点、摩尔纹与色彩断层
第三个坑是产品层面最直观的:生成的纹理有肉眼可见的椒盐噪点,大量孤立的亮色或暗色像素破坏画面质感。根因是直接用了均匀随机值填充,相邻像素没有相关性。换成噪声函数并叠加两到三倍频后,高频细节平滑过渡,观感立刻柔和。
摩尔纹问题更容易出现在几何网格类生成器里。当线条频率和像素网格频率接近时,画面出现一圈一圈的干涉条纹。处理办法很简单:给线条位置加一个极小的随机偏移,打破周期性,条纹就消失了。
色彩断层出现在大面积渐变背景里,根源是 8 位通道只有 256 个亮度级别,平滑渐变必然出现可见色带。可以写入前对每个像素亮度加一个正负 1 的随机扰动做抖动,断带就能基本消失。这个技巧成本极低,收益明显。
4.4 输出文件问题:色差、体积与预览效率
最后一个坑来自输出环节。PNG 无损但体积大,JPEG 体积小但有损且不支持透明通道。我的经验是最小覆盖方案:同一张图同时输出 PNG 高清版用于后续处理,再输出 JPEG 压缩版用来快速预览。两版文件名一致,预览满意后再用高清版,省去手动对应的工作量。
色彩问题值得再提一次。打印和网页的色彩标准不一样,如果作品要制作实体物料,务必在导出前把图像转到打印色彩空间并做软打样预览,不然屏幕上鲜亮的橙色,印出来会变成砖红色。预览效率也要考虑,一百张全尺寸 PNG 在文件管理器里翻看非常卡,我会额外生成一张 4x4 的缩略图接触表,一屏看完所有作品,再挑喜欢的看大图。接触表用 Pillow 的 thumbnail 加 paste 实现,代码不超过二十行,强烈推荐。
4.5 问题排查速查表
把常见问题整理成一张表,方便后续开发直接查:
| 现象 | 根因 | 解决方向 |
|---|---|---|
| 单张图生成极慢 | 逐像素调用绘制接口 | 用 NumPy 批量构建数组,最终一次性转图 |
| 多次运行结果不一致 | 依赖全局随机状态或集合乱序 | 局部 Random 实例 + 确定性派生种子 + 有序遍历 |
| 画面出现孤立噪点 | 均匀随机值缺乏连续性 | 改噪声函数并叠加多倍频 |
| 图案出现干涉条纹 | 图形频率和像素网格共振 | 给位置加微小随机偏移打破周期 |
| 渐变色带 | 8 位通道亮度层级不够 | 加正负 1 的抖动扰动 |
| 屏幕预览与打印色差大 | 色彩空间不匹配 | 导出前转换打印色彩空间并软打样 |
| 大量 PNG 预览卡顿 | 图片体积过大过多 | 生成缩略图接触表 |
5. 一点个人体会
跑完 artcraft 这段项目,我最大的收获反而不是那一堆生成图,而是创作习惯的改变。以前画画靠状态,心情好笔下就顺;现在靠参数,把当时的审美判断固化成一个数值范围,交给程序去探索。以前一张图就是终点,画完就结束了;现在一张图只是无数种可能里的一个快照,种子一变,风格就从日出变成日落。
如果你也想做一个类似的生成艺术项目,我的建议是别一上来就追求复杂算法。先把随机种子体系、噪声函数、色彩管理这三件事做扎实,再慢慢往里面加生成器。这三件事像地基,地基稳了,上面长什么都好看。等管线跑顺了,你会发现自己对"创作"这两个字的理解,会比从前宽得多。