
先说结论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任意整数全局随机种子控制最终结果想要图案整体变化时优先改它octaves4 到 7fBm 叠加的噪声层数太少图案单调太多会显得脏lacunarity1.8 到 2.5相邻层频率倍率默认 2调大会让细节更碎gain0.3 到 0.6每层振幅衰减越大细节越强但也容易噪scale / frequency0.5 到 4基础噪声缩放调小等于“拉远镜头”结构变大warp amplitude0 到 20域扭曲强度从 0 慢慢加体会“水彩感”warp frequency1 到 5扭曲的疏密调大后扭曲更细碎palette2 到 6 种颜色颜色映射表颜色顺序会影响画面重心blend modemultiply / 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 为 158octaves 取 5gain 取 0.4域扭曲幅度调小到 2 左右保证画面有大块面但不过分扭曲。颜色表用偏棕黄的旧纸色系从浅米色一路过渡到深褐色。生成完成后不要直接结束再加一层极其细微的颗粒噪点混合模式选 multiply。这层颗粒不需要密集强度控制在 0.1 左右否则看起来会像扫描仪上的灰尘。加完颗粒后纸的粗糙感马上出来比单用噪声平滑多了。最后如果你想让它更像古籍纸张可以在边缘叠一个椭圆暗角遮罩让四周轻微变暗。这个成品当时被我直接拿来做了读书类应用的背景图反馈是“看起来很安静、不刺眼”。4.2 水彩晕染背景水彩风格的核心在域扭曲。fBm 只负责给出色块区域真正让水彩“化开”的是 warp。我实测时把 warp amplitude 从 2 调到了 10 再到 30能明显看到色块边界越来越不规则像水在纸上洇开。颜色表上我选了两种同色系的深浅色加一种留白比如浅灰蓝、深蓝、接近白的底色这样会有大量留白很像水彩画的呼吸感。这个配方也要控制 octaves4 到 5 就够了再高反而让晕染区域碎掉。混合模式我用的是 normal 半透明叠加靠多层颜色叠加出层次。输出后用做详情页头图背景很合适不需要额外修图视觉上已经带上了手绘质感。4.3 低多边形风格色块低多边形风格其实可以完全避开 Delaunay 三角剖分直接用噪声量化来做。把 fBm 输出值乘以一个 level 数然后取整比如乘 5 或 6就得到分层的色块平面。每个层级对应一种颜色整体看起来像纸雕拼贴边缘虽然不规则但块面感强烈。具体参数上octaves 用 6gain 设置为 0.35这样大块面和小碎块同时存在。颜色表用从浅到深的一组相近色降低明暗反差后更有装饰感。如果你想要更“硬朗”的边缘可以让 domain warp 保持 0完全不加扭曲如果希望边缘稍微柔和一点warp 开到 2 到 3 就够了。这个风格做展会背景、活动海报的肌理都很出效果。4.4 三个配方的参数速查与适用边界把三套配方常用参数整理到一起方便直接对比参数复古纸张水彩晕染低多边形seed158427octaves546gain0.40.450.35warp amplitude2200palette4 色米棕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 这类半程序化工作流做批量素材的核心价值。把这个习惯养好你会少走很多弯路。