先说结论:artcraft 这个名字看着像 art 和 craft 的简单拼凑,真正用起来才发现它其实在讲一件事——如何把算法的意外和手工的可控叠在同一张画布上。我刚开始接触这个项目时以为它就是个挺常见的纹理生成器,研究了两轮才意识到,它是从随机噪声、图层合成、颜色映射到无缝导出一条链路都打通了的生成式创作工作流。平时做游戏原型、UI 背景、海报底纹、视频包装,或者只想给粗糙的创意编程作品收个尾,都可以在里面找到一套能直接复刻的参数和代码。这个分享不完全面向程序员,只要知道什么是 Canvas、知道一张位图由像素组成,就能跟得上。
整个内容按我实际折腾下来的顺序来讲:先聊它为什么这么设计,再拆核心实现,然后给三个能直接抄的配方,最后把踩过的坑一条条摆出来。
1. 项目到底想解决什么问题
1.1 artcraft 名字背后的设计哲学
我会把 artcraft 拆成两层理解。art 那一半,对应的是噪声、随机种子、大胆的参数尝试,它负责制造“意料之外”;craft 那一半,对应的是图层顺序、混合模式、颜色映射、缝隙处理,它负责把意外收拾成可以交付的作品。
很多人做生成艺术非常放飞,跑出来好看就开心,不好看就换个种子反复跑。这种思路当玩具没问题,一旦要交付给项目做素材就坏了,因为不可复现、不可批量、不可换风格。反过来,也有不少人追求“全自动一键生成”,把参数全锁死,结果生成一百张和一张没什么区别,又失去了手工打磨的味道。artcraft 恰好站在中间:算法负责造出结构,人负责调出质感。它想解决的并不是“怎么生成一张图”,而是“怎么在可控的情况下批量生成一堆风格统一、细节又有差异的图”。
这就像做饭。自动炒菜机一键出菜,省事但味道永远一个模子;完全手炒又累又依赖状态。artcraft 更像一个半自动灶台:洗切配是程序化完成的,火候和调味还是得人定。升不升最后的这层烟火气,就看你愿不愿意花几分钟拧参数。
1.2 它和现成工具差在哪里
市面上的纹理网站和生成器很多,点几下按钮就出一张底纹,确实快。但这类工具最大的毛病是风格雷同,你下载的东西别人也在用,而且几乎不给可复现的种子。做游戏原型时我最怕这种事:昨天生成的一张纸纹理很喜欢,今天再打开网站发现参数被重置,怎么都调不回那一版,只能从头再来。
纯 AI 生成工具的随机性更强,视觉上也常有惊喜。但当你需要“六张同风格但不同摆设”的棋盘背景时,纯 AI 生成就得反复抽卡,抽到第六张大概率风格已经飘了。手绘当然最稳,质量也最高,但成本摆在那里,画一套材质要半天,用来做素材占位太奢侈。artcraft 的做法是确定性优先:一个固定 seed 配合一组参数,结果就锁死在某个可复现的状态里。要让风格统一,就锁定除 seed 以外的所有参数;要让细节变化,就只改 seed。这种粒度控制是现成工具很少能给你的。
我后来接一个临时演示项目时,需要给几个按钮做同系列噪点底纹,就是用这个思路批量出了八张,每张只改 seed,其余完全一样,叠加到界面上后风格高度统一,完全看不出是临时生成的。
1.3 适合谁读、能用在哪些场景
这个方向最适合三类人。第一类是独立游戏开发者,尤其是做原型阶段的人,可以用它生成卡牌底纹、地图背景、材质占位,能省掉大量找素材的时间。第二类是 UI 设计师和运营设计师,做 banner 底纹、详情页质感背景、海报噪点的时候,参数化生成比在素材网站大海捞针靠谱得多。第三类是对创意编程感兴趣的初学者,通过这个项目能很直观地理解噪声、分形叠加、颜色映射这些概念,而且代码量不大,心理门槛低。
还有一类人可能没想到,就是插画师和数字艺术家。他们通常不做底层算法,但可以把 artcraft 当“灵感草稿机”:先生成一张纹理,再导入自己的绘画软件里二次加工,效率会高很多。这个项目的输出不需要是最终成品,它更常扮演的是素材链路的起点。
2. 核心架构与关键设计思路
2.1 整体分层:配置、生成、合成、导出
artcraft 的代码结构不复杂,但分层挺清楚,我拆成四层理解。
配置层是所有参数的统一出口,通常是一个 JSON 对象,里面包含尺寸、seed、octaves、颜色表、混合模式这些字段。把参数收敛在一处,好处是批量生成时非常方便:外层循环改 seed,其他字段不动,一次跑出一批风格一致的图。我见过很多生成项目把参数散落在各种函数里,最后想批量出图就得改代码,那是拿大刀绣花,完全没必要。
生成层负责把坐标变成数据,内部是各种噪声函数、fBm 分形叠加和域扭曲。这一层是纯粹的数学计算,不关心颜色,也不关心最终像素,只输出一个归一化到 0 到 1 之间的数值场。合成层再把生成层输出的数值映射成颜色,再叠加多个图层。复古纸张的颗粒感其实就是一个基础纹理叠一层细噪点,混合模式改成 multiply,效果就出来了。这个层是 artcraft 最有“craft”味道的地方,颜色表怎么排、图层怎么叠,直接决定画面气质。导出层管输出:无缝拼贴、分辨率缩放、浏览器里导出 PNG,或者批量写文件。
分层的最大好处是可替换性强。我后来想加一种“纸张纤维”质感,根本不需要动生成逻辑,只要在合成层新增一个纤维生成器,把它叠到现有图层下面就行。所谓架构,说白了就是给未来要加的新功能留好位置,不用推翻重来。
2.2 为什么选噪声合成而不是纯随机
有人会问,生成纹理为什么不能直接对每个像素取随机值?答案很简单,纯随机是白噪声,没有任何空间相关性,看起来像电视机雪花,既没有起伏也没有结构,做不了有质感的材质。
这里要用的是“受控的随机”——噪声。值噪声的思路是在网格顶点放随机数,中间位置做平滑插值,得到一个连续变化的随机场。它看起来像云,像烟雾,已经有了“区域性”。梯度噪声进一步让网格点的变化方向固定,画面更柔和,适合做地形、水波这类自然起伏。fBm 是分形布朗运动的缩写,核心操作就是把多层噪声叠在一起:第一层看大形,第二层加中等细节,第三层加微纹理,层数越多,画面越接近真实材质那种“自相似”的复杂感。
域扭曲是点睛之笔,也是 artcraft 能做出手绘感的关键。它把坐标本身再一次送入噪声函数,让原本笔直的结构发生弯曲。举个例子,画水彩晕染的时候,如果直接用 fBm 输出做颜色映射,边界会太顺滑,反而像塑料;加了域扭曲之后,颜色边界的走向变成连续、蜿蜒的,就出现了手工晕染的不规则感。
传统程序化生成在今天依然有价值,是因为它完全离线、完全确定、完全可控。深度学习模型风格化确实强大,但很多场景需要批量产素材、固定 seed、离线运行,这时候老派的噪声合成反而是最靠谱的方案。这不是厚古薄今,而是工程场景里“够用、稳定、可复现”这三件事往往比“效果惊奇”更重要。
2.3 参数体系:一张表看懂所有旋钮
artcraft 的核心参数不算多,但每个都有清晰作用。整理成表会更直观:
| 参数 | 常见范围 | 作用 | 调参注意点 |
|---|---|---|---|
| seed | 任意整数 | 全局随机种子,控制最终结果 | 想要图案整体变化时优先改它 |
| octaves | 4 到 7 | fBm 叠加的噪声层数 | 太少图案单调,太多会显得脏 |
| lacunarity | 1.8 到 2.5 | 相邻层频率倍率 | 默认 2,调大会让细节更碎 |
| gain | 0.3 到 0.6 | 每层振幅衰减 | 越大细节越强,但也容易噪 |
| scale / frequency | 0.5 到 4 | 基础噪声缩放 | 调小等于“拉远镜头”,结构变大 |
| warp amplitude | 0 到 20 | 域扭曲强度 | 从 0 慢慢加,体会“水彩感” |
| warp frequency | 1 到 5 | 扭曲的疏密 | 调大后扭曲更细碎 |
| palette | 2 到 6 种颜色 | 颜色映射表 | 颜色顺序会影响画面重心 |
| blend mode | multiply / overlay 等 | 图层叠加方式 | 叠加暗部细节常用 multiply |
我调参的固定路线一般是这样:先关掉所有花哨处理,只看一层基础噪声,确认大结构的方向对不对;然后逐级加 octaves,观察细节从哪里开始丰富起来;最后才动 warp 和 palette。直接一上来把七个参数全乱拧,结果基本是废图,而且你根本不知道是哪个参数把画面毁了。
3. 实操过程与核心环节实现
3.1 零依赖的环境搭建
很多人看到“生成纹理”就以为要装一堆库,其实不用。artcraft 的核心逻辑用原生 JavaScript 加 Canvas 2D 就能跑起来,新建一个空 HTML 文件,把代码贴进去,双击就能在浏览器里看到结果。这么做的好处是不依赖网络、不依赖安装环境、方便复制到任何机器上。Canvas 2D API 本身只做像素写入,但对于 1024 乘以 1024 的常规纹理生成,性能完全够用。
我建议先从浏览器端入手,因为调试方便:改一下参数,刷新页面就能对比效果。等确定配方之后,再考虑把同一套逻辑搬到 Node 环境做批量导出,这是很顺滑的演进路线。
3.2 核心生成器实现
下面这段是我整理出来的最小可运行版本,重点展示随机种子、值噪声、fBm 合成和像素写入四件事:
function mulberry32(seed) { let a = seed >>> 0; return function () { a += 0x6D2B79F5; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function valueNoise(seed, x, y) { const xi = Math.floor(x); const yi = Math.floor(y); const fx = x - xi; const fy = y - yi; const h = (a, b) => { let n = seed + a * 374761393 + b * 668265263; n = (n ^ (n >> 13)) * 1274126177; return ((n ^ (n >> 16)) >>> 0) / 4294967296; }; const a = h(xi, yi); const b = h(xi + 1, yi); const c = h(xi, yi + 1); const d = h(xi + 1, yi + 1); const ux = fx * fx * (3 - 2 * fx); const uy = fy * fy * (3 - 2 * fy); return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy; } function fbm(seed, x, y, octaves, lacunarity, gain) { let sum = 0; let amp = 1; let freq = 1; let max = 0; for (let i = 0; i < octaves; i++) { sum += amp * valueNoise(seed + i * 101, x * freq, y * freq); max += amp; amp *= gain; freq *= lacunarity; } return sum / max; } function generate(canvas, config) { const ctx = canvas.getContext('2d'); const img = ctx.createImageData(canvas.width, canvas.height); const w = canvas.width; const h = canvas.height; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const nx = x / w; const ny = y / h; const v = fbm(config.seed, nx, ny, config.octaves, config.lacunarity, config.gain); const value = Math.max(0, Math.min(255, v * 255 * config.contrast + config.brightness)); const idx = (y * w + x) * 4; img.data[idx] = value; img.data[idx + 1] = value; img.data[idx + 2] = value; img.data[idx + 3] = 255; } } ctx.putImageData(img, 0, 0); }这个版本里,mulberry32 是确定性伪随机数生成器,只要传入同一个 seed,它生成的随机序列就完全一样。这一点很关键,因为如果用 Math.random(),每次刷新页面的结果都会变,根本无法复现参数。valueNoise 用的是网格顶点的哈希随机数加平滑插值,虽然简单,但已经足够产生连续纹理。fbm 的核心是循环叠加:每一层噪声的频率乘以 lacunarity,振幅乘以 gain,再把所有层累加后归一化,最后得到的是一个数值稳定在 0 和 1 附近的标量场。
这个代码可以直接拿来跑,不过它还只能输出灰度图。后面加颜色映射后,才算真正让纹理活起来。
3.3 让纹理“活”起来的调参路线
第一版跑出来通常是一张灰蒙蒙的雾状图,很多人会觉得“这算哪门子艺术”。别急,这是正常的,因为你还没给画面加对比度和结构。我用一套固定流程来做“活化”处理。
第一步,把输出范围拉开。上面代码里的 contrast 参数我一般从 1 开始调,往 1.5 到 2 的方向走;brightness 则根据画面明暗调整,目标是让暗部不会全部糊成黑色。第二步,加 octaves。把 octaves 从 1 调到 4,画面上会出现明显的次级纹理;再到 6,细节开始丰富;但如果飙到 8 以上,画面会变得细碎、发脏,所以 5 到 6 通常就是甜区。第三步,加域扭曲。这是从“计算机纹理”变成“手绘纹理”的分水岭。给坐标先做一层扭曲,再送入 fBm,边界的走向就不再是数学的直线,而是一笔一笔的弯折。
最后的颜色映射最影响“作品感”。灰度图再漂亮也像素材底片,一旦把输出值映射到一组精心挑选的颜色,比如旧纸的黄褐、水彩的灰蓝,整张图立即变成可交付的设计素材。我在项目里是这样做的:先把噪声值归一化,再按它在渐变色带里的位置取色,必要时加一点抖动来避免色带断裂。具体抖动方法后面坑位里细讲。
3.4 无缝拼贴和最终导出
很多场景下需要纹理能无缝平铺,比如卡牌底纹、网页背景、包装纸。无缝处理在 artcraft 里有两条常见路线。
第一是使用周期噪声,让噪声在整个生成空间里天然循环。实现思路是把整数顶点按取模方式索引,这样网格左右两边能自然衔接。优点是数学上干净,适合做精细纹理;缺点是实现起来比普通噪声复杂。第二是中心裁切法。这个方法很取巧:生成一张宽高都是目标两倍的纹理,然后从正中央裁出目标尺寸。由于裁切区域四周都被完整纹理包围,平铺后接缝处的图案是连续的,几乎看不出边界。缺点是内存开销会变大,实际测试里生成 2048 像素的大图时内存峰值会高一截,但对常规需求完全可接受。
我现在常用的是中心裁切法,主要是省事,不需要额外写周期采样逻辑。导出直接用 Canvas 的 toBlob 或 toDataURL 就可以。批量导出则更简单,在外层循环里改 seed,把每个画布单独保存,顺便用 JSON 记录对应的参数文件,方便以后复查效果。
4. 三个可直接复用的实战配方
4.1 复古纸张底纹
做这个底纹的时候,我第一步是把基础噪声调得很软,对比度不要太强。目标不是做出明确的图案,而是让大面积区域有一种自然的纤维起伏。设置 seed 为 158,octaves 取 5,gain 取 0.4,域扭曲幅度调小到 2 左右,保证画面有大块面但不过分扭曲。颜色表用偏棕黄的旧纸色系,从浅米色一路过渡到深褐色。
生成完成后不要直接结束,再加一层极其细微的颗粒噪点,混合模式选 multiply。这层颗粒不需要密集,强度控制在 0.1 左右,否则看起来会像扫描仪上的灰尘。加完颗粒后,纸的粗糙感马上出来,比单用噪声平滑多了。最后如果你想让它更像古籍纸张,可以在边缘叠一个椭圆暗角遮罩,让四周轻微变暗。这个成品当时被我直接拿来做了读书类应用的背景图,反馈是“看起来很安静、不刺眼”。
4.2 水彩晕染背景
水彩风格的核心在域扭曲。fBm 只负责给出色块区域,真正让水彩“化开”的是 warp。我实测时把 warp amplitude 从 2 调到了 10 再到 30,能明显看到色块边界越来越不规则,像水在纸上洇开。颜色表上,我选了两种同色系的深浅色加一种留白,比如浅灰蓝、深蓝、接近白的底色,这样会有大量留白,很像水彩画的呼吸感。
这个配方也要控制 octaves,4 到 5 就够了,再高反而让晕染区域碎掉。混合模式我用的是 normal 半透明叠加,靠多层颜色叠加出层次。输出后用做详情页头图背景很合适,不需要额外修图,视觉上已经带上了手绘质感。
4.3 低多边形风格色块
低多边形风格其实可以完全避开 Delaunay 三角剖分,直接用噪声量化来做。把 fBm 输出值乘以一个 level 数然后取整,比如乘 5 或 6,就得到分层的色块平面。每个层级对应一种颜色,整体看起来像纸雕拼贴,边缘虽然不规则,但块面感强烈。
具体参数上,octaves 用 6,gain 设置为 0.35,这样大块面和小碎块同时存在。颜色表用从浅到深的一组相近色,降低明暗反差后更有装饰感。如果你想要更“硬朗”的边缘,可以让 domain warp 保持 0,完全不加扭曲;如果希望边缘稍微柔和一点,warp 开到 2 到 3 就够了。这个风格做展会背景、活动海报的肌理都很出效果。
4.4 三个配方的参数速查与适用边界
把三套配方常用参数整理到一起,方便直接对比:
| 参数 | 复古纸张 | 水彩晕染 | 低多边形 |
|---|---|---|---|
| seed | 158 | 42 | 7 |
| octaves | 5 | 4 | 6 |
| gain | 0.4 | 0.45 | 0.35 |
| warp amplitude | 2 | 20 | 0 |
| palette | 4 色米棕 | 3 色蓝白 | 5 色相邻系 |
| 关键后期 | 颗粒 + 暗角 | 半透明叠加 | 色块量化 |
三套方案没有绝对优劣,只有场景匹配。复古纸张适合需要安静质感的界面和印刷品;水彩晕染适合作为大面积的视觉背景,因为留白多、不抢内容;低多边形风格适合活动物料、演示稿开头,辨识度很高。如果你想拿来生成游戏中的地面纹理,低多边形风格可以直接铺;如果是做详情页基底,水彩晕染更合适。理解每个配方的参数差异,本质上就是理解画面气质的差异。
5. 常见问题与排查技巧实录
5.1 为什么总出现接缝和格纹
这是我早期最常碰到的问题,表现有两种:一种是整张图有明显的网格感,像把画面划分成了很多小块;另一种是导出后平铺时在边缘出现一条突兀的接缝。
格子感的根因通常是噪声层数太少或者插值太弱。值噪声如果只用线性插值,网格边界很容易暴露。解法是把插值换成 smoothstep,也就是代码里那种fx * fx * (3 - 2 * fx)的形式,再用到 quintic 插值会更好。另一个原因是域扭曲后,坐标很容易碰回整数网格边界,此时噪声值会产生跳变。遇到这种情况,我建议先把 warp 设置为 0,看问题是否消失,再逐步增加,定位临界值。无缝平铺接缝则多半是导产后边缘像素和起始像素对不上,中心裁切法一般能解决,但要注意裁切区域选取。
我一般排查看起来不明显时,会把画面放大到 4 倍看像素,或者只输出一层噪声单独检查。定位问题源再修复,比盲目调参数高效得多。
5.2 运行慢得离谱,怎么优化
生成 1024 乘 1024 的图,每个像素要算 4 到 6 层噪声,大约就是几百万次噪声调用,浏览器里 JavaScript 逐像素计算确实会卡。我第一次跑的时候直接卡了十几秒,一度以为浏览器崩了。别怕,这就是计算量的正常代价。
优化的第一刀先砍分辨率。很多纹理其实是放大后使用的,生成 512 乘 512,用 Canvas 的 drawImage 放大到目标尺寸,视觉损失通常可以接受,速度却能提升好几倍。第二刀是减少 octaves,从 6 降到 5,肉眼几乎分不出区别,但计算量直接少了六分之一。第三刀才是技术活,把生成逻辑放到 Web Worker 或 OffscreenCanvas 里跑,避免阻塞主线程,至少用户不会看到页面白屏。终极方案是把像素写入改成 WebGL,用片段着色器在 GPU 上做并行计算,每个像素独立运行,速度能到毫秒级。这套优化路径是从“能用”到“好用”的必经之路,慢慢来。
5.3 明明存了 seed,结果还是对不上
这个问题最隐蔽,也最容易让新手放弃。表面上看代码里写了 seed,但生成结果还是每次不同,基本可以断定有随机源没被 seed 控制住。最常见的一种是用了 Math.random() 做后续的颜色抖动或偏移;另一种是噪声函数里对 seed 做了加法运算后,传入了新的随机种子,但种子没固定下来;还有一种是不同浏览器对浮点数处理有细微差别,导致同一 seed 在两个环境下结果不完全一致。
解决办法分两层。代码层面,把项目里所有随机数入口都收敛到同一个随机函数上,最好是从 mulberry32 拿到的那个闭包函数。工程层面,在生成完成的同一时间把完整配置序列化成一个 JSON 文件存下来。以后想复现效果,直接读取 JSON 当参数重建就行,不要再依赖一个孤零零的 seed。我后来养成的习惯是:每跑一批素材就导出一份参数 JSON,文件名和图片前缀保持一致。做过几次之后,你就会被这种管理方式救过命。
5.4 颜色过渡出现色带,怎么消除
现象是画面中本该平滑的渐变区域出现了一圈一圈的条带,像等高线地图。原因很简单:8 位通道一共只有 256 个亮度等级,当渐变跨越的像素很多时,相邻等级之间的跳变就会被肉眼察觉。尤其在水彩晕染这种需要大面积平滑渐变的场景里,色带会特别明显。
最有效的方案是加抖动。经典的 Floyd-Steinberg 抖动会把当前像素的量化误差扩散到相邻像素,让条带转化成细颗粒,视觉上就变成了“质感”。实现起来不复杂,核心就是把量化前后的差值按比例加到未处理的邻域像素上。Bayer 抖动更轻量,适合实时场景。我在 artcraft 里的默认做法是对颜色映射前的噪声值做一层非常微弱的随机扰动,幅度大概在半个亮度等级以内。这样既打散了量化条带,又不会破坏色块结构,是性价比最高的一条路。
最后再说点个人经验
这个项目给我最大的启发,不是生成了多少张好看的纹理,而是重新理解了“可控的随机”这件事的价值。真正干活的工具未必是最炫的,但一定要能在明天、下个月、换一台电脑后,还能还原出今天的效果。artcraft 这种“固定参数,只动 seed”的思路,让我在后续很多生成类需求里都受益。
最后分享一个小技巧,也是我用得最顺手的一个:当你需要一批风格统一又不想完全雷同的素材时,不要一次换一堆参数,只改 seed,其他全部锁定。出来的图既保留同一套设计的语言,又有了各自独立的变化,这才是 artcraft 这类半程序化工作流做批量素材的核心价值。把这个习惯养好,你会少走很多弯路。