这几天整理文件,翻到了 ArtCraft 早期的几张草图。当时起名没想太多,就是 Art + Craft,艺术加手艺。结果做着做着,这个名字越来越贴切——它不是纯绘画工具,也不是单纯的矢量编辑器,而是一套面向独立创作者的手工质感数字生成流程。你可以用代码生成木纹、纸张、织物、几何纹样,导出成印刷或激光切割能用的 SVG,也可以直接出图。这篇文章把我做这个项目的整个思路、技术选型、核心实现和踩过的坑都整理出来,希望对做生成艺术、独立出版、游戏素材或手工设计的朋友有点用。
1. 项目定位与设计思路拆解
1.1 手工质感的数字化困境
做插画和独立出版的朋友应该都有感受:需要一张有手工温度的纹理时,选择其实非常有限。
买素材库的纹理贴图,一套几百上千个文件,看着很多,真正能用的没几个。关键是素材库的纹理是“扫描数字化”的,它把真实纸张、布料的质感拍下来,但拍下来的东西是死的——颜色固定、颗粒位置固定,你不能让一张水彩纸纹理去适配不同的颜色方案,也不能随意改变纤维的疏密方向。想调整?只能在 Photoshop 里叠加混合模式,效果勉强能用,但文件巨大,放大容易糊。
另一种思路是纯手工做,扫描或翻拍。这个方法质感最真实,但做一套可用的材质库要耗费大量时间,而且每次的材料状态不一样,重复性差。我试过连续拍同一批手工纸,光线角度稍有变化,色温和阴影就完全不同,后期校色比做纹理本身还累。
ArtCraft 的出发点,就是想绕开这两条路。用程序直接生成纹理和图案,让“手工质感”变成一套可调节的参数:颗粒大小、纤维方向、墨迹边缘、经纬密度,全都由数字控制。你要粗糙的宣纸?调高噪点幅度,降低频率。你要细腻的棉布?减小纤维长度,增加层数。同一个函数,拧几个旋钮,就能得到一批风格统一但细节各异的材质。
当时有位做独立游戏的朋友看到我的原型,补了一句很关键的话:游戏原型阶段最缺的就是这种“能快速换皮”的贴图方案。他拿同一个木纹生成器,调了三套参数,分别用在村庄栅栏、旧木箱和码头栈桥上,视觉上一下子就立住了。这就是程序化生成的核心价值——它不追求和真实材质一模一样,追求的是“够用、可调、风格统一”。
1.2 技术选型背后的逻辑
ArtCraft 最初想用现成的节点式工具做原型,但试了两天就放弃了。
节点编辑器解决的是“单个效果”的问题,但解决不了“批量生产”的问题。我需要的是一个可以被脚本调用的工作流:输入一批参数,输出一批图。节点操作要么手工拖拽,要么写复杂的表达式,最后还是要绕回代码。与其绕远路,不如直接用代码。
技术栈很朴素:
- Python 3.10 作为主语言
- Pillow 负责像素级图像合成
- NumPy 处理矩阵运算和噪声生成
- svgwrite 负责矢量导出
没有用 OpenCV,没有用 PyTorch。原因有两个。第一,这个项目的核心不是图像识别或者深度学习,是传统的程序化纹理合成,Pillow + NumPy 完全够用。第二,依赖越少,别人拿过去越好复现。我不想为了一个效果让使用者搭半天的环境。
选 Python 还有一个私心:生态里跟生成艺术相关的库非常多,比如后期如果想加入更复杂的材质模型,可以直接接上来。而且 Python 写这类算法逻辑非常接近伪代码,读起来好懂,方便自己几个月后再回来看,也方便别人二次开发。
矢量导出选了 SVG 而不是 PDF,是因为 SVG 是纯文本格式,每一笔路径都清清楚楚,方便做参数化修改。PDF 更像一个封闭容器,切割机和印刷厂确实认 PDF,但你在代码里很难精准控制每一个矢量元素的属性。SVG 则可以直接拼接字符串生成,也可以挂到激光切割软件里直接用。
1.3 目标场景与工作流程
做下来之后,ArtCraft 的使用场景逐渐清晰起来,大致有三类:
第一类是游戏美术的前期素材生成。独立游戏团队在原型阶段不需要精修贴图,需要的是大量语义明确的纹理图,用代码批量产出非常划算。
第二类是独立出版和文创设计。做封面、书签、包装纸、贺卡,需要一些几何纹样和精致底纹,手画太慢,素材网容易撞车,程序生成刚好是中间路线。
第三类是手工类创作的前置准备。比如刺绣图案、编织图案、拼布设计,需要精确的矢量图做排版或切割,ArtCraft 把 SVG 导出做好之后,这类需求可以直接作为设计工具链路的一环。
整个工作流的骨架也很简单:
- 通过参数描述需求(颜色、尺寸、密度、随机种子)
- 脚本生成图像数据或矢量路径
- 输出 PNG 缩略图给设计师预览
- 确认效果后导出 SVG 交付下游工具
这个流程听起来直接,但真要把每一步做扎实,细节点非常多。
2. 核心技术细节与关键模块解析
2.1 程序化纹理的底层逻辑
程序化纹理生成算法,核心就是三件事:噪声、叠加、滤波。
先讲噪声。很多人对“噪声”这个词有误解,觉得噪声就是信号不好的雪花点。实际上在程序化纹理里,噪声是纹理的“底色”。一张手工纸,放大看有纤维交错,有细小凹坑,分布看起来是随机的,但这种随机不是纯粹的杂乱,它有空间相关性——靠近的纤维点之间是有连续性的。
这就要用到 Perlin 噪声或者 Simplex 噪声。Perlin 噪声和纯随机不一样的地方在于:它在空间上是连续的,相邻位置的噪声值变化不会特别突兀,就像真正的材料表面一样。解释这个过程最简单的方式是用坐标网格:把平面分成小块,每个格子顶点给一个随机梯度向量,然后插值平滑出每一点的噪声值。
Python 里可以用 noise 库实现,也可以手写。我试过手写 Perlin 噪声,代码大约 60 行,核心就是梯度点乘和缓动插值。如果不想手写,也可以直接在 NumPy 上用叠加正弦波的方式模拟一个低频变化层——把不同频率、不同方向的正弦波叠在一起,得到的云纹效果也能模拟出很自然的纹理流动。
关键操作是:把噪声的频率和幅度拆成独立参数。频率决定纹理的“颗粒大小”,幅度决定纹理的“对比强度”。你把频率调高,就得到细腻的砂砾感;调低,就得到大块的云絮感。我在 ArtCraft 中把这两个参数暴露为basic_frequency和basic_amplitude,所有纹理生成器共用同一套接口。
第二层是叠加。真实的材料质感从来不是一层,而是多层信号的复合。手工纸有基底纤维层、有表面涂布层、有压纹凹凸层。程序化纹理要模拟这种丰富度,就得用多层噪声叠加。最简单的做法是不同频率的噪声按不同透明度混合,低频做主体,高频做细节,再叠加一个轻微的颜色变化层。这个思路和图像处理里的“多尺度分解”是一个逻辑。
第三层是滤波,也就是处理。原始噪声是连续值,范围从 0 到 1 或者从 -1 到 1,但真实纹理不是这种平滑渐变。需要做阈值化、钳制、曲线映射等操作。比如做纸张纹理时,会把低于某个阈值的噪声归零,模拟纸张上没涂到胶料的部分;做木纹时,会做垂直方向的拉伸,再把亮度映射成颜色深浅。
前阵子做拼贴风格的某位设计师朋友聊过:市面上很多生成纹理一眼假,就是因为他只加了一层噪声,没有叠加,也没有做阈值处理。他说用 ArtCraft 生成的基底,再叠上自己的手绘笔触,最后出来的效果像真的做了肌理。这句话让我确信这套“噪声+叠加+滤波”的结构是对的。
2.2 随机种子为什么这么重要
程序化纹理有一个很有价值的特点:可复现。同一套参数,固定随机种子,跑出来应该是一模一样的图。
但随机种子这个事,用不好就会踩坑。Python 内置的random模块和 NumPy 的随机模块是两套独立的随机流,如果你混合使用,又不统一设置种子,那同一个脚本每次跑出来的结果就都对不上。更隐蔽的是,如果你在代码里用了多线程或多进程,每个子进程会继承父进程的随机状态,但继承之后各自的随机序列就不一样了,导致并行处理同一批纹理时,结果是可复现的,但顺序换了,给下游加工造成混乱。
我的解决方案是:给每张生成图设一个独立的种子,这个种子作为参数传入生成函数。这样即使并行跑多个任务,每个任务的边界也是清晰的。整个项目里只有一个地方设置全局种子,就是 CLI 入口处的--seed参数。
这里可以类比相机底片:种子决定了你按下快门的瞬间捕获到的全部信息。同一卷底片在不同的暗房里冲出来,色调会有差异,但底片上的信息是一样的。种子就是你的底片。
2.3 图案生成中的数学基础
图案生成和纹理生成不一样。纹理是“连续变化”,图案是“重复结构”。ArtCraft 里做了两类图案算法,都建立在简单的数学变换上。
一类是几何镶嵌纹样,灵感来自经典的伊斯兰几何图案。做法是:先定义一个基础单元(比如正方形或者六边形),在这个单元里画入交叉线或圆弧,然后通过平移、旋转、镜像把基础单元铺满整个平面。数学上的关键是群论里的对称操作。实现的时候并不需要复杂的群论知识,用坐标变换就可以完成:
- 平移:对坐标加一个偏移向量
- 旋转:对坐标乘以旋转矩阵
- 镜像:对某个轴取坐标负值
只要保证基础单元拼接时边缘是连续的,铺满整个画面后视觉上就是完整的几何纹样。
第二类是编织纹样。编织的数学本质是一个二进制矩阵,经纬线交错。矩阵里每个格子的值是 0 或 1,0 代表经线在上,1 代表纬线在上。不同的矩阵排列,就形成平纹、斜纹、缎纹等不同的编织结构。SVG 里表达这种图案很简单:每个交叉点用一个多边形块表示,根据 0/1 决定用经线颜色还是纬线颜色画。
这里有一个关键参数:组织循环数。平纹是 2 个格子的循环,斜纹是 3 个、4 个或者更多。循环数决定了纹理的“颗粒大小”和纹路方向感。做织物纹理时,控制这个参数比直接调整颜色更有用。
2.4 参数体系:用少数旋钮控制多数变化
一个设计良好的程序化纹理系统,参数数量必须克制。
早版本的时候,我把所有可能的量都做成参数,比如木纹的年轮强度、弧线扭曲、颜色暗部偏移、亮部偏移、对比度、噪声种子、噪声频率、噪声幅度、生长方向……加起来十几个。参数多了之后,调制一个概念图要花很长时间,因为彼此之间有耦合:调了年轮扭曲,暗部偏移的效果就变了,于是再去调它,接着对比度又不合适,陷入调参循环。
后来我把参数分为三个层级:
- 全局参数:画布大小、背景色、随机种子
- 结构参数:决定纹理形态,比如木纹的疏密、纸纹的颗粒、编织的组织循环
- 表现参数:决定颜色映射、光照效果,比如色调、明暗对比、墨迹扩散量
结构参数很少,一般三到五个;表现参数控制在两到三个。全局参数单独一组。
这样做带来了一个巨大的好处:参数组合数量减少了,但是有效组合比例提高了很多。随手拧一个参数,出来的效果都算“能看”,而不是像老版本那样拧半天拧出一张奇怪的东西。
3. 实操过程与核心功能实现
3.1 环境搭建与项目骨架
这部分内容基于我实际跑通的一套流程,按步骤来基本不会出错。
建议用 Python 3.10 以上的版本,主要是因为类型注解的写法更顺手,以及 Pillow 新版对它支持得最好。
安装依赖一行命令:
pip install pillow numpy svgwritePillow 负责图像输出,NumPy 处理矩阵运算,svgwrite 生成 SVG 文件。这三个库没有复杂的依赖链,装起来很快。
项目骨架我习惯这样组织:
artcraft/ ├── artcraft/ │ ├── __init__.py │ ├── noise.py # 噪声生成与叠加 │ ├── textures.py # 纹理生成器 │ ├── patterns.py # 图案生成器 │ └── export.py # 导出 PNG/SVG ├── presets/ # 参数预设文件 ├── output/ # 生成结果 └── main.py # 入口脚本把噪声、纹理、图案、导出拆成四个模块,各自独立。main.py 只负责解析参数和调用模块。这样设计是我调试时慢慢养成的习惯:如果所有代码堆在一个文件里,改一个函数就担心影响别的地方,拆开之后思路清晰得多。
3.2 手工纸纹理的完整实现
拿手工纸纹理当第一个示例,因为它的参数最少,效果直觉最强,非常适合理解整套流程。
手工纸纹理的代码逻辑可以拆成五步:
- 生成低频噪声基底,模拟纸张纤维分布
- 叠加高频噪声,模拟细小颗粒
- 增加轻微的水平或垂直方向性,模拟纸张抄造时的纤维取向
- 把噪声值映射到纸张颜色区间(米白、浅黄、灰白)
- 最后做一次柔化,减少像素感
看代码更直观:
import numpy as np from PIL import Image, ImageFilter def make_paper(width=1200, height=1600, base_color=(245, 240, 230), grain_size=6.0, fiber_strength=0.3, seed=42): rng = np.random.default_rng(seed) # 低频基底:决定纸张整体的明暗起伏 y, x = np.mgrid[0:height, 0:width].astype(np.float32) noise_low = 0 for i in range(3): freq = (2 ** i) / grain_size phase = rng.uniform(0, np.pi) noise_low += np.sin(x * freq + phase) * np.sin(y * freq + phase * 0.7) noise_low = noise_low / 3.0 noise_low = (noise_low - noise_low.min()) / (noise_low.max() - noise_low.min()) # 高频细节:模拟纤维颗粒 noise_high = rng.random((height // 4, width // 4)) noise_high = np.kron(noise_high, np.ones((4, 4))) # 放大到原尺寸 noise_high = noise_high.astype(np.float32) noise_high = (noise_high - noise_high.mean()) / noise_high.std() # 方向性纤维:沿水平方向轻微拉伸 fiber = np.sin(y * 0.02 + noise_high * fiber_strength) * 0.15 # 合并并映射到颜色空间 combined = noise_low + noise_high * 0.12 + fiber combined = np.clip(combined, 0, 1) paper = np.zeros((height, width, 3), dtype=np.uint8) for c in range(3): # 给每个通道一点细微差异,模拟手工染色不均匀 channel_offset = rng.uniform(-6, 6) paper[:, :, c] = np.clip(base_color[c] - combined * 45 + channel_offset, 0, 255) img = Image.fromarray(paper, "RGB") return img.filter(ImageFilter.GaussianBlur(0.8)) if __name__ == "__main__": img = make_paper(seed=7) img.save("paper_demo.png")注意代码里的几个关键点。
rng = np.random.default_rng(seed)这行非常重要,它把随机源收敛到 NumPy 的 Generator 对象上。用np.random.seed(seed)是老写法,会在多线程环境下污染全局状态,尽量别用。
高频噪声用random()生成之后再用kron放大,这个是实用技巧。直接在原始分辨率上生成随机数,会造成每个像素独立明亮度,颗粒感很强但像电视雪花。先生成 1/4 分辨率再放大,相当于做了一个最简单的平滑,颗粒感更像真实纸张。
方向性纤维这一层是手工纸和普通噪声纸的区别所在。手工纸在抄造过程中,纤维会顺着水流方向排列,所以纸张表面摸起来有轻微的取向性。代码里用np.sin(y * 0.02 + noise_high * fiber_strength)来模拟这种方向感——纵向的纤维条,辅以细小的高斯扰动。
这一版跑出来的效果,已经有手工纸的“质”了,但颜色上可能偏干净。如果你想要做旧、染黄、斑驳的效果,可以把noise_low的幅度调大,或者把基底颜色改成暖黄。参数调出来的结果差异非常大。
3.3 编织纹样与 SVG 导出的完整实现
如果说纸张纹理对应的是“手工质感”,那编织纹样对应的就是“工艺结构”。编织纹样的数学表达非常规整,适合导出为矢量图,直接送给激光切割或者丝网印刷。
一个简单的斜纹编织生成器,核心是组织矩阵:
import numpy as np import svgwrite def twill_matrix(cycle=4, width=20): """生成一个斜纹组织矩阵,cycle 是组织循环数""" m = np.zeros((cycle, cycle), dtype=int) for r in range(cycle): for c in range(cycle): m[r, c] = 1 if (r + c) % cycle < cycle // 2 else 0 tile = np.tile(m, (width // cycle + 1, width // cycle + 1)) return tile[:width, :width] def export_weave_svg(filename="weave.svg", size_cm=10, threads_per_cm=8, cycle=4, color_a="#8B6F55", color_b="#D9C7B4"): matrix = twill_matrix(cycle, threads_per_cm * size_cm) n = matrix.shape[0] cell = size_cm * 10 / n # 单位换算为 mm dwg = svgwrite.Drawing(filename, profile="tiny", size=(f"{size_cm}cm", f"{size_cm}cm")) for i in range(n): for j in range(n): color = color_a if matrix[i, j] == 1 else color_b x = j * cell y = i * cell dwg.add(dwg.rect(insert=(x, y), size=(cell, cell), fill=color)) dwg.save() if __name__ == "__main__": export_weave_svg()这个实现里最核心的地方是twill_matrix函数。斜纹组织的特点是:每条纬线穿过经线时,浮长线依次错位一格,形成连续的斜向纹路。(r + c) % cycle < cycle // 2这个判断条件,就是让矩阵在横纵两个方向上均匀错位,正是斜纹视觉效果的最小完整单元。
cycle参数的取值很重要。cycle 为 2 时是平纹,经纬线一上一下紧密交织,纹路细而均匀;cycle 为 4 时是经典的斜纹,牛仔布就是这种组织;cycle 越大,纹路越疏松,视觉上的纹理颗粒越大,但是结构强度也相对降低。做织物设计的话,这个参数直接决定了织物的“手感观感”。
SVG 导出的几个参数需要注意:
- 尺寸单位用
cm,这是 svgwrite 的一个特性,可以在size参数里直接传字符串。激光切割软件和矢量软件识别 SVG 里的 cm 单位时,会正确换算成物理尺寸。 threads_per_cm是经纬密度,决定单位宽度内有多少根纱线。刺绣或者丝网印刷时,这个参数对应到材料和工艺的限制。- 颜色建议用十六进制字符串,兼容性最好,不会出现 RGB 元组写出时格式错乱的问题。
导出后的 SVG 可以直接导入 Adobe Illustrator、Inkscape、激光切割软件,都没问题。我在测试时用某开源矢量编辑器打开导出的文件,缩放到 100% 查看,边界是整齐的,没有出现缺失的格子。
3.4 从设计稿到落地的参数对照
为了让参数体系在真实项目中立得住,我把 ArtCraft 的几个预设整理成了一个小型对照表。每个预设都对应一套实际的使用场景,可以作为调参时的参考起点。
| 预设名称 | 核心参数组合 | 适合场景 | 注意事项 |
|---|---|---|---|
| 手工纸 A | grain_size=6, fiber_strength=0.3 | 书籍封面底纹、包装纸 | 颜色偏暖,适合跟文字搭配 |
| 手工纸 B | grain_size=3, fiber_strength=0.1 | 数码插画叠加肌理 | 颗粒较细,导出 PNG 时分辨率至少 300dpi |
| 粗斜纹 4/1 | cycle=4, threads_per_cm=8 | 纺织品图案、拼布设计 | 导出 SVG 后注意单位是 cm,别当成像素用 |
| 细平纹 1/1 | cycle=2, threads_per_cm=14 | 刺绣基底、印刷纹样 | 循环小,格子数量多,SVG 文件体积会偏大 |
| 几何密铺 | 蜂窝/三角铺贴,边长为参数 | 文创封套、壁纸图案 | 边缘连续性依赖基础单元的对称设计 |
参数预设文件我一般存成 JSON,这样可以在不同机器之间直接复制,不用改代码。比如手工纸 A 的预设是这样:
{ "preset": "paper_a", "type": "texture", "width": 1200, "height": 1600, "params": { "base_color": [245, 240, 230], "grain_size": 6.0, "fiber_strength": 0.3 }, "seed": 7 }把预设独立于代码的好处是,非程序员也能参与调参。直接把 JSON 发给设计师,让他们改颜色值,改完再跑一遍脚本,整个过程不需要打开代码编辑器。
4. 常见问题与排查技巧实录
做这类生成工具,真正花时间的不是写算法,而是排查各种结构性问题。下面这些问题都是我实际遇到的,有的还卡过好几天。
4.1 纹理“数字感”太重怎么办
这是玩程序化纹理最常碰到的问题。生成的纹理一眼看去很规整,像塑料表面,缺手工感。
原因通常有两种。第一种是噪声层次太少。只用一层中频噪声,画面会很“顺滑”,真实材料因为纤维分布不均匀,应该有一些局部突变。解决方法是叠加多层噪声,至少三层:低频做形变,中频做主体,高频做细节。每一层的权重不要太大,高频噪声一点点就够,加多了就变成雪花噪点。
第二种原因是没有做颜色通道偏差。很多人做纹理时,三个通道用了完全一样的数值,那么任何颜色都只会是灰调的变化,很像用灰色画笔在平涂。真实的手工染色,三个通道的颜料吸收程度会有细微差异,所以在映射颜色时,每个通道加一个随机偏移量,范围可以很小,正负五左右,效果就已经很明显。代码示例里的channel_offset干的就是这个事。
还有一个非常实用的技巧:最后加一个微小的全局模糊。GaussianBlur(0.5)到1.0,可以有效消除像素边缘的硬切。真实材料表面没有绝对的锐利边缘,所有边界都有过渡。这个小步骤经常被忽略,但是对观感的影响非常明显。
4.2 SVG 导出后尺寸对不上
有一次导出编织纹样给某位做丝网印刷的朋友,打开文件后发现整个图案只有不到一个指甲盖大小。排查下来是单位问题。
svgwrite 在size参数里传"10cm"时,会正确写出带单位的 SVG 属性,但如果尺寸是通过内部宽度高度变量计算的,有时候会写成纯数字。纯数字在 SVG 规范里默认单位是 px,而 1 英寸等于 96px,所以 10cm 如果被当成 10px,显示出来当然就是蚂蚁大小。
解决办法有两个:
- 在
Drawing构造时显式传入带单位的尺寸字符串 - 或者在根节点
<svg>里显式设置viewBox和width、height属性,确保不同软件都能正确解析
我后来养成的习惯是:所有 SVG 导出函数统一封装一层,在内部强制把厘米换算成毫米,并用f"{value}mm"的格式写字符串。这样不管哪个软件打开,物理尺寸都不会出错。
4.3 相同种子跑出不同结果
这个问题最容易在并行渲染时出现。我有一段时间为了提高生成速度,用多进程跑一批纹理,用的还是全局随机状态,结果导致同一个种子在不同进程里跑出完全不同的结果。做设计的人最怕这个,因为一旦选定了某个好看的效果,要再微调其他参数时,纹理的随机分布却变了,没法稳定复现。
解决方法是彻底放弃全局随机状态,改用局部种子。也就是在所有生成函数里,种子都作为参数显式传递,并且内部用Generator对象隔离随机源。这样不管并行跑多少个任务,每个任务的随机独立性都有保证,相同种子必定复现相同效果。
另外,如果你的生成器用了多层噪声,而每层噪声都要消耗随机数,那么每一层消耗的顺序一定不能乱。我在代码里会把所有随机数的消耗按固定流程排序,避免以后加了新噪声层导致种子语义变化。
4.4 生成结果偏暗或偏亮
程序化纹理生成里,颜色一致性是另一个大坑。原因是当你输出 PNG 时,很多生成器直接用了噪声值作为亮度,然后映射到一个固定区间。如果噪声分布不均匀,整体亮度就偏亮或偏暗。
解决方法是做一次归一化。把合并后的矩阵减去最小值,除以最大值和最小值的差,把数据压到 0 到 1 区间。代码里的(noise_low - noise_low.min()) / (noise_low.max() - noise_low.min())就是这个作用。
归一化之后一般就正常了。但如果想进一步微调曝光,可以再加一个 gamma 映射:out = value ** gamma。gamma 大于 1 时图像变暗,小于 1 时图像变亮。伽马控制在 0.8 到 1.2 之间,不会出现色彩失真。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 纹理看起来像塑料 | 噪声层数不够、缺少颜色通道偏移 | 叠加三层噪声,每通道加随机偏移 |
| SVG 尺寸显示错误 | 单位用了 px 或纯数字 | 显式用 cm/mm 单位字符串 |
| 相同种子结果不一致 | 全局随机状态被污染 | 使用局部Generator并传 seed 参数 |
| 整体偏暗或偏亮 | 噪声值未归一化 | 计算 min/max 并归一化到 0-1 |
| 输出文件太大 | SVG 里单个格子的矩形数量过多 | 提高格子的最小单元尺寸或降低经纬密度 |
| 边缘有锯齿 | 所有纹理像素都锐利 | 最后加一层 0.5-1.0 像素的高斯模糊 |
5. 一点扩展建议
ArtCraft 到现在并不是一个庞大的系统,但它证明了一个朴素且有效的道理:程序化生成的“手工感”,本质上来自对材料结构的理解。纸有纤维,布有经纬,木有年轮,这些结构呈现在数学上的规律,比我们想象的更简单,但产生的视觉信息量却可以非常丰富。
如果你也想做一个类似的生成工具,我建议从你熟悉的一种材料开始,把它拆成数学描述,用噪声加结构参数去模拟,不要一开始就想着做一套覆盖所有材质的平台。覆盖越广,单个材质的效果就越浅。
用这个思路,扩展方向其实是无限的。把 SVG 输出接到激光切割,就能做纸雕模板;把编织矩阵接到绣花机格式,就能直接变成刺绣参数;把纹理参数接上交互界面,就能做成一个让非程序员也能玩的参数化设计玩具。
我在实际使用中还有一个体会:程序生成纹理的最高目标不是以假乱真,而是提供一种新的创作语言。手工质感的魅力在于独特性,而参数化生成提供的正是“按需生产独特性”的能力。同一个种子,同一套参数,换来的是百分之百可复现场景下的那一份稳定,这个稳定本身,也是一种工艺。