用代码复现米罗风格:参数化生成“米罗水族馆”的完整指南

发布时间:2026/9/18 6:59:24
用代码复现米罗风格:参数化生成“米罗水族馆”的完整指南
做了个小工具把米罗画里的那些弯弯绕绕的线条、圆点、月牙和鱼的形态搅在一起让它自己长出一条又一条不可能存在的鱼。朋友看了生成结果的第一反应是问我在哪学的画画。我说没学这段代码写的。她愣了几秒然后把手机递过来让我把鱼变大她想截个图当壁纸。那一刻我知道这事成了。如果你也喜欢米罗那些充满童趣的符号语言又恰好会一点代码想做一个艺术风格极强的生成作品那这篇东西就是为你准备的。从视觉语言拆解、鱼形参数化、色彩系统到完整代码实现我尽量把每一步怎么想、为什么这么写都交代清楚。涉及的部分不需要你有很强的编程背景只要愿意动手看完全文就能复现出一套属于你自己的“米罗水族馆”。1. 米罗的符号世界天然就是一套编程语法先说个很多人都有的疑问米罗的画看起来这么“自由”“随意”像是随手画的儿童涂鸦怎么可能用代码去生成这不是自相矛盾吗其实恰恰相反米罗的绘画系统是我见过的最适合“参数化生成”的艺术语言之一。原因很简单米罗的视觉词汇高度符号化、高度类型化。圆点是圆点星是星月牙是月牙直线是直线他的画里没有复杂的透视结构、没有细腻的明暗交界线、没有体积感塑造。这些东西本来就是高度“矢量”的本质上就是几何元素和线条的组合只是组合的韵律和节奏非常个人化。这就是为什么很多人看米罗的画会有一种“我上我也行”的错觉但自己真画起来又总觉得哪里不对劲——因为难的不是元素而是组合的韵律。把这个观察转换到代码世界就是一个很清晰的映射定点圆和自由线 - 点和圆函数月牙和三角形 - 可旋转的路径图形粗细不一的黑线 - 可变strokeWeight的线条铺色块 - 半透明或不透明的填充形状换句话说米罗的每一张画都可以被看作一串非常优美、充满张力的绘图指令序列。我们做生成艺术时只需要构造一套“符号生成器”让程序自动生成这些指令序列再控制序列里的节奏感就能复现出很强的米罗气息。这也是我把这个项目取名为“MiroFish”的原因。MiRo是米罗的谐音引用Fish是鱼的外形骨架。整个项目的内核就八个字用鱼的形态承载米罗的视觉语法。艺术风格提供审美秩序鱼形提供结构框架生成算法提供无穷变化——这三者缺一不可。顺着这个思路我把“米罗风格”拆成了三个可以在代码里精确控制的核心属性属性视觉表现代码映射形状有机感、无棱角、像软糖一样贝塞尔曲线和圆角多边形符号圆点、眼点、月牙、小星、竖线独立绘制函数按概率出现色彩红黄蓝绿等高饱和原色分区铺陈预置色板 随机权重抽取每一只鱼本质上就是这三个属性的一个随机组合实例。代码跑一次就生成一条鱼跑一万次就是一万条鱼理论上没有穷尽。2. 鱼形怎么来贝塞尔曲线的生物感与控制点博弈鱼形是整个项目里最关键的一步也是最容易搞砸的地方。太写实了就和米罗风格冲突太抽象了又看不出是鱼。我调了两三个晚上才找到合适的平衡点这里直接把方法和思考过程分享出来。2.1 为什么不直接用椭圆或基础几何图形拼鱼身很多人做这种项目时第一步想到的肯定是鱼不就是一个椭圆加个三角形尾巴吗的确最基础的鱼形可以这么凑合但生成出来的东西往往缺乏生命力像一个简笔画图标不是一个有感觉的图像。因为真实生物的形态充满了不对称、起伏和微妙的变化。米罗本人画的鱼或生物也从来不按规则几何来他画的线条通常带有一种模仿有机生命体的柔软感是“生长的”而不是“描绘的”。因此我的思路很明确鱼身用曲线来定义而不是用几何运算。P5.js里画曲线最方便、也最好控制的做法是bezier()四个点两两组合起始点、终止点、两个控制点。给足控制点参数就可以画出非常丰富的起伏。2.2 鱼身轮廓的参数化起点我把鱼身抽象成一条基于中点线的两边轮廓。先假想鱼从头到尾有一条中心轴然后鱼身的上下两条边缘线分别基于中心轴生成// 核心思路沿鱼轴方向取多个点根据距离鱼头/鱼尾的位置 // 计算上下边缘到轴线的距离也就是鱼的“半厚函数” function fishOutline(axisPoints, thicknessProfile) { let upper []; let lower []; for (let i 0; i axisPoints.length; i) { let offset thicknessProfile[i]; // 取垂直于轴线方向的偏移 let perpendicular createVector(-axisPoints[i].y, axisPoints[i].x).normalize(); upper.push(p5.Vector.add(axisPoints[i], p5.Vector.mult(perpendicular, offset))); lower.push(p5.Vector.sub(axisPoints[i], p5.Vector.mult(perpendicular, offset))); } return { upper, lower }; }上半边缘和下半边缘各是一个点的序列然后我把这些点输入给beginShape()vertex()quadraticVertex()或bezierVertex()处理成平滑的轮廓线。关键在thicknessProfile怎么设计。我的经验是不要用固定值而是用一个类似钟形曲线的分布贴近鱼头的地方比较窄中间偏前的位置最宽接近尾柄的地方快速收窄这样鱼身的流线感立刻就出来了。2.3 控制点抖动让每条鱼都有自己的身体如果每条鱼的厚度曲线都一样那代码生成一百条鱼跟复制粘贴一百条没有任何区别。要增加生物感就必须让形状具备随机性——但这里的随机不是毫无约束的随机是“受控抖动”。我的做法是给厚度数组加上Perlin噪声let t noise(i * 0.12, seedOffset); let thickness baseThickness * (1 (t - 0.5) * 0.4);noise()会生成平滑连续的随机值不会像random()那样跳变所以画出来的鱼身轮廓是自然起伏的——有的鱼背略微隆起有的鱼肚子圆鼓鼓每条鱼看起来都像是同一种鱼的不同个体而不是同一张脸的复制品。控制点坐标的整体缩放也很重要。我的鱼身轴长在80到140像素之间浮动厚度比例在0.18到0.26之间浮动这两个值直接决定了鱼是修长型还是胖墩型。2.4 鱼尾和鱼鳍的抽象逻辑鱼尾和鱼鳍是风格化的重点。米罗画里的“三角形”常常不是几何三角形而是像折扇一样的曲线三角形。我在代码里用两条三次贝塞尔曲线拼出尾鳍先确定尾柄末端作为三角形顶点再分别画两片尾叶每片尾叶都用一个曲线路径勾边function fishTail(basePoint, directionAngle, size, wiggle) { // basePoint是尾柄末端directionAngle决定尾巴甩向哪一侧 // wiggle控制尾部曲线的不对称程度 push(); translate(basePoint.x, basePoint.y); rotate(directionAngle); // 左叶 noFill(); stroke(0, 0, 0); strokeWeight(2); beginShape(); vertex(0, 0); bezierVertex(size * 0.6, -size * 0.25, size * 1.1, -size * 0.6, size, -size * 0.9); bezierVertex(size * 0.7, -size * 0.5, size * 0.3, -size * 0.2, 0, 0); endShape(); pop(); }胸鳍和背鳍则简化成飘带状。飘带感的秘诀是线条的起点固定在鱼身上终点随着一个正弦波时间变量或随机相位移摆动。这样出来的静态作品像截取了一个运动瞬间画面有定格感而不是干巴巴的标本图。这里有我踩过的一个比较深的坑提醒一下鱼尾尺寸比例不要贪大。最初我把鱼尾设成身体长度的0.7倍视觉上非常夸张放出来朋友说像一只蝴蝶长了鱼头。后来反复试了几版最终定在0.3到0.45倍之间这个范围在“一眼是鱼”和“米罗式奇幻生物”之间最耐看。3. 米罗视觉语法的程序化实现点、眼、月牙与线形状框架搭好之后真正给鱼注入米罗灵魂的是那些点缀性的符号元素。米罗的画里大量使用圆点、眼睛形状、月亮弧线和类似“米”字星的结构这些元素单独看没有什么了不起组合起来却能有极强的辨识度。在这一步我做了五个可以在鱼身上随机分布的小元素所有元素都基于鱼身的局部坐标系生成这样鱼在移动、旋转、缩放时不会出现符号“飘”在鱼外面的问题。3.1 圆点和双色同心圆圆点是最简单也是最高频出现的元素。变化出在填充色和描边色上我允许三种模式实心小圆、白底黑边同心圆、以及红蓝双色半圆拼合。米罗作品里经常出现在纯色底上叠一个对比色小圆点的做法这个对比关系几乎是米罗个人风格的签名。代码实现上就是用ellipse()嵌套第一层大圆填充主色第二层小圆填充辅色或白色描边统一黑色、两像素宽。3.2 月牙形月牙是米罗符号系统里极有代表性的一个。实现方式不复杂先画一个扇形或圆弧再用另一个覆盖的圆“挖”掉一部分。在Canvas 2D和p5.js里更省事的方案是直接画两条贝塞尔曲线闭合形成一个新月形function crescent(cx, cy, radius, angleOffset, thickness) { push(); translate(cx, cy); rotate(angleOffset); noStroke(); fill(244, 196, 48); // 米罗黄 beginShape(); vertex(radius * cos(0), radius * sin(0)); bezierVertex(radius * 0.8, radius * 0.4 - thickness, radius * 0.2, radius * 0.5 - thickness, 0, 0); bezierVertex(radius * 0.2, radius * -0.1, radius * 0.6, radius * -0.3, radius * cos(0), radius * sin(0)); endShape(CLOSE); pop(); }这段代码的核心思路是起点在圆弧左上端中间经过内凹点再弧回到起点让封闭区域形成半月形状。参数thickness控制月牙胖瘦radius控制整体大小。我习惯在鱼身、鱼尾、甚至背景里都随机撒几颗月牙它会快速把画面拉进米罗的气场里。3.3 眼睛符号米罗世界里最重要的“表情包”米罗的眼睛符号非常标志性有时是十字上的圆点有时是椭圆里套圆点有时是侧脸的“一只眼睛”形态。这类符号用到鱼身上会产生奇效——因为鱼本身没有表情但加上米罗式的眼睛它就有了拟人化的情绪。代码里我实现了三种眼睛第一种是最简单的实心圆点第二圈用白色描边黑色主体。第二种是一个黑点在圆内偏上位置像人在翻白眼俏皮感很强。第三种是我最爱用的“线挂眼”一条短直线垂直落下末端顶点一个圆点米罗很多人物肖像里都有这个细节。这个眼睛一旦出现整条鱼的“性格”立刻跳出来艺术感也最强。3.4 十字小星与竖线组除了圆、月牙、眼睛米罗作品里还经常出现疏疏落落的小十字星点和三五根并列的竖短线。它们不是画面主角更像是一种呼吸感的铺陈让画面的留白部分不至于空洞。在我的鱼身上这些元素通常出现在鱼身的中后段或鳍的边缘。竖线组的长度、间距、根数都做随机化三根到六根不等。那个小十字星点上还会偶尔带一个短横线类似音符上的符杠画面里会多出一丝飘动的音乐感。function starCross(x, y, size) { stroke(20, 20, 20); strokeWeight(Math.max(1.5, size * 0.08)); line(x - size / 2, y, x size / 2, y); line(x, y - size / 2, x, y size / 2); }3.5 如何控制符号的密度不破坏鱼形这是生成艺术里最常见的问题元素一多主体形状就被淹没整个画面变得杂乱无章。我在实践里摸索出一条规则简单但有奇效——把鱼身分成前、中、后三段符号数量按比例配额鱼头前段从吻部到鳃盖最多两个符号通常放眼睛系鱼身中段最宽的区域最多三个符号通常放圆点或月牙鱼尾后段尾柄到尾鳍最多一个符号或空着每个符号还有最小间距约束距离太近时会重新采样。这个规则的背后逻辑是米罗的画虽然看起来满但每个元素之间都有呼吸和停顿。如果像一个圣诞树一样把所有东西都堆上去就完全丢失了他那种闲庭信步的自由感。4. 色彩策略不是随意上色是计算出来的童趣色彩是MiroFish项目里最容易被忽略、但影响最巨大的模块。同样的鱼形和符号系统用错颜色出来的效果像劣质儿童涂鸦APP用对颜色就是可以挂在客厅的装饰画。这里面的差距不在配色审美的高低而在你有没有把色彩当成一个系统性工程来管理。4.1 米罗色板的三层结构米罗的用色习惯并不复杂他偏爱高饱和度、清晰的原色色相。我统计了自己喜欢的米罗作品后把常用的颜色归纳成三层结构层级颜色角色常见色值底色层背景大色块灰白、淡黄、暖白主色层鱼身主体色正红、正黄、钴蓝、翠绿点缀层眼、点、鳍的对比色黑、白、橙、草绿、中蓝每只鱼生成时从主色层随机取一个颜色作为鱼身填色从点缀层取一到两个颜色做符号最后用黑描边让整个轮廓统一。4.2 色块分离的秩序感看米罗的油画时会注意到他常常在平涂的大色块中保留一点“手绘感”——颜料之间会有轻微的晕染和覆盖不是绝对干净的矢量边界。我虽然用生成代码但也希望视觉上保留这种质感。实现方式是给填充色增加一个两像素的羽化或者给大面积色块稍微增加一个同色系的暗部偏移。具体操作先用深一个色调填充鱼身整体轮廓再缩放到约90%后叠上主色这样鱼身的边缘就自然出现了一圈“手绘阴影”整体立刻显得有厚度而不扁平。这个技巧非常便宜但效果超出预期。4.3 限定色板带来的约束力与辨识度我在设计色板时特意下了约束主色层就五个颜色点缀层八个颜色绝对不动态生成任意色相。因为米罗的辨识度很大程度来自颜色的精确性屏幕上低饱和度的细微分色反而会削弱风格感。也就是说限制自己反而是获得自由的方式。只用五个主色每条鱼鱼身的色相是稳定的但通过明度变化、与其他符号颜色的搭配能产生丰富的组合。每一条鱼都不一样但放在一起必然是一家人。4.4 白底黑线米罗风格的隐形骨架色彩之外最重要的不是别的颜色而是黑和白。米罗的很多作品底色是画布本身的米白轮廓线是硬朗的黑色。这个黑白系统像骨架一样把彩色的形体和符号串联成一个整体。如果去掉黑线米罗的很多元素拼在一起会显得轻飘飘、像贴纸。一旦加上那根两像素黑线整个画面立刻有了分量和秩序。因此我的代码里有一个严格约定所有可见形状的描边默认是黑色描边宽度在2到3像素之间不随图像缩放而缩放。使用stroke(0)和strokeWeight(2)的优先级高于其他任何色值设定。很多初学者在这个问题上翻车总觉得黑线丑想改成灰色或棕色结果风格辨识度直线下降——这其实是误解了米罗绘画中黑线的组织作用。5. 从单只鱼到一幅完整的画生成逻辑的工程化实现前几部分的逻辑都是围绕“单只鱼怎么做得像米罗”但一个作品只有一条鱼显然不够。当鱼超过三条、元素超过几十个之后生成逻辑的工程化程度就直接决定最终画面是否干净、有序、有主次。这一节讲我从零构建整个生成流程时踩过的关键节点。5.1 生成一张MiroFish作品的完整流程我最终的代码是P5.js整个生成过程分六个步骤初始化画布与随机种子设定背景底色按概率生成水面波纹和前景装饰符号生成2到5只鱼的位置列表确保鱼与鱼之间不重叠对每条鱼依次执行选色板 - 画鱼身 - 画鱼尾鳍 - 画符号叠加一层极淡的“纸纹噪声”颗粒模拟画布肌理输出PNG定位鱼的位置时我用了一个简单的overlapCheck()函数给每只鱼一个几何包围盒两两之间做矩形相交测试相交就重新采样。最多重试四十次超出后减少一只鱼的数量。在实际使用中这个方案又快又稳定很少触发上限。5.2 随机种子的使用一次生成永续复现生成艺术需要很强的可复现性——你可能喜欢某张的输出结果但忘记保存或者同一设置想再生成十张兄弟版这时候随机种子就非常关键。在P5.js里一行randomSeed()配合noiseSeed()就能锁死所有随机数。let seed int(random(100000, 999999)); randomSeed(seed); noiseSeed(seed); print(当前种子: seed);我建议在画面上方把种子号也画成一行小字或者同时输出到控制台。这样你或用户看到喜欢的图时可以拿着种子号重新生成。做系列作品时种子号更像是“DNA编号”既是身份标识也是复现钥匙。5.3 纸纹噪声数字生成作品的最后一毫米质感纯数字生成的画面无论元素多精致总会显得太新、太整齐仿佛刚刚打印机打完。为了让它更接近手绘作品的质感我会在所有元素画完后叠加一层很淡的随机噪声颗粒loadPixels(); for (let i 0; i pixels.length; i 4) { let grain random(-18, 18); pixels[i] grain; // R pixels[i1] grain; // G pixels[i2] grain; // B } updatePixels();噪点强度控制在正负18以内肉眼不会觉得画面脏但会明显消除数字绘图的“光滑塑料感”。这个技巧对于生成艺术来说价值非常高几乎可以在任何项目中复用。5.4 完整的骨架代码示例由于完整实现太长这里贴一个精炼版的骨架代码涵盖鱼身轮廓和符号调用的主体逻辑。你可以在这个基础上扩展function drawFish(x, y, size, seedOffset) { push(); translate(x, y); rotate(random(-0.3, 0.3)); // 1. 鱼身轮廓 let bodyColorA randomColor(mainPalette); let bodyColorB darken(bodyColorA, 12); // 深色打底 fill(bodyColorB); noStroke(); fishBodyPath(size * 1.04, seedOffset); // 主色覆盖 fill(bodyColorA); fishBodyPath(size, seedOffset); // 2. 鱼尾 fill(secondColor); fishTail(size * random(0.3, 0.42), random(-0.2, 0.2), seedOffset); // 3. 符号 drawSymbolsOnFish(size); // 4. 黑色轮廓最后统一画保证所有曲线外层都有黑边 noFill(); stroke(0); strokeWeight(2); fishBodyPath(size, seedOffset); // 再次描边路径 pop(); }注意我特意把黑色描边放在填充之后单独画一层而不是在填充函数里直接开stroke()。这样做的原因是P5.js里描边和填充是同时渲染的如果填充和描边同时画最后叠加其他符号时会出现描边被后续元素盖住的问题。分层绘制可以让黑线始终保持在最上层画面轮廓更干净利落。6. 生成鱼群时的节奏控制稀疏与留白是米罗的呼吸感当你能稳定生成一只好看的鱼之后新的问题马上出现鱼多了画面却乱了。很多生成艺术项目做出来的单张元素都非常惊艳一旦让多个元素同屏画面就成了一锅粥。这背后是布局与节奏的失控。米罗的画很少有密集堆叠的时刻他的叙事是靠元素之间的“关系”完成的——一个圆点靠近一个月牙会形成对话三个竖线排在一起会有音乐的节拍感。如果离得太远各走各的画面就散了如果挤在一起又失去了张力。我把这个规律总结成两句话元素之间既要有联系又要有距离留白不是空是画面结构的缓冲区。在代码里实现这种微妙的平衡我参考了三大原则6.1 主从关系一个中心鱼的视觉重量整幅画里必须有一条视觉上最重的鱼它或者尺寸最大或者颜色最跳跃或者符号元素最丰富。其余鱼保持小一号或素色一些的姿态。没有主从关系的画面即使所有元素都精致看起来也只是一个目录页或素材拼贴。我的实现方式是先随机选一条鱼作为“主鱼”尺寸设为100到130%的基准值其余鱼强制限制在70到90%的基准值。6.2 疏密节奏非均匀分布鱼的摆放不要像九宫格一样均匀分布。我使用Perlin噪声场来决定候选位置让x和y都经过噪声映射后选择噪声值较高的区域作为“密集区”让更多鱼出现在那里而画布的其他区域保持大面积留白。这个做法的视觉结果就是鱼群像洋流一样聚散有致而不是棋盘上的棋子。6.3 符号与鱼的呼应有时候我会在靠近鱼身但脱离鱼体轮廓的空白区域额外生成几粒小圆点或小星花这一类“碎片”是我个人非常喜欢的手法。相当于鱼在游动时脱落的视觉鳞片既连接了鱼和背景的关系又增加了画面层次。实测下来在每幅画中让1到2粒碎片符号出现在鱼的附近空白处效果最自然。7. 调参心得那些让画面从“像”到“对”的参数细节这一节单独拿出来写是因为我觉得它价值最高。很多教程教你画出“对”的作品却很少说清楚怎么从“像某个风格”调整到“就是某个风格”。这里面的差距是几个关键参数的微调。7.1 边缘曲线的“松弛度”最初版本里我的鱼身曲线用是带夹角的多边形嵌套贝塞尔结果出来的鱼形特别“紧”像撑开的雨伞缺乏米罗绘画里那种软塌塌的松弛感。后来我把贝塞尔曲线控制点之间的夹角放宽、让控制臂加长鱼的轮廓才变得懒洋洋起来。最终确定的参数是曲线的控制点长度在鱼身宽度的0.4到0.6倍之间小于这个值鱼形紧绷大于这个值会扭成奇怪形状。7.2 符号大小与鱼身的比例符号的大小对视觉冲击力影响极大。在米罗的作品里部分圆点或星星并不比主体小多少甚至和主体一样大这也构成他画面的童趣感。因此在鱼身上我允许个别符号达到鱼身长度的12%甚至15%其余符号保持在5%到8%之间。大小差异拉开后画面一下子有了戏剧性。7.3 鱼身的颜色和相关“脏色”处理副色不能抢主色的戏但如果两个颜色属于同一色系且深浅太接近鱼身就会显得平平无奇。我的折中方案是副色从“主色的互补色或近似互补色”色板中抽取。比如鱼身偏红时符号选黄或绿鱼身偏蓝时符号选红或橙。颜色之间的对冲会制造出一种视觉上活跃的颤动感这与米罗作品中色彩的跳跃感非常统一。7.4 微调时最容易忽略的旋转旋转是很多生成艺术初学者的盲区。我的经验是鱼的整体倾斜角在-15到20度之间随机过大就有失衡感过小则无方向感。鱼尾的摆动角度控制在30到50度之间。当鱼与鱼之间的倾斜角差异在40度以上时画面会像一个真实的水族箱——鱼群往不同方向游去。如果角度太接近它们就像排队过马路的鸭子看着很刻意。8. 规模化生产一套“米罗水族馆”系列作品的实操方案当单张画能稳定生成之后我开始思考这个项目更大的可能性——能不能直接生成一系列作品形成一套视觉上统一、内容上丰富可以直接输出成装饰画或做小展览的作品集这个阶段的问题不再是“怎么画”而是“怎么批量生产又能保持每一张的独特质量”。8.1 建立“配方”文件用配置控制风格倾向我放弃了在代码里写死所有参数的做法改成用一份JSON配置驱动生成过程。配置里可以指定主色色板、辅助色色板、鱼数量的上下限、符号密度、背景底色等。这样每次生成一批作品时只要切换配置就能在“非常接近的系列感”和“风格跨度极大”之间自由滑动。const recipe { canvas: { width: 1600, height: 1200, bgBase: #f5efe2 }, palette: { main: [#e63b2e, #f5a623, #2b7de9, #9b3aa3, #2e9e5c], accent: [#f7e05e, #f28e2b, #4aa3df, #d0d3d4, #1f1f1f] }, fish: { countMin: 2, countMax: 5, primaryScale: 1.3, bodyThickness: [0.16, 0.24] }, symbols: { density: 0.55, minDistance: 16, includeCrescent: true } };我强烈建议你从一开始就用配置化的思路管理这些参数而不是把随机范围散落在代码各处。配置文件的另一个好处是当你生成了一百张作品以后可以随时回溯某一张图的确是用了哪一套配方生成的方便做展览归档或系列整理。8.2 成品筛选不是每张生成图都值得展示生成艺术总是伴随大量废稿。哪怕是调好参数的系统也有大约12%的输出会因为布局不协调、鱼群叠太多或者风格倾向不明确而不适合展示。我的做法是生成后人工快速浏览一遍从中挑选精品再把种子号和配方文件存档。这个筛选环节不可省略——你会逐渐发现自己的审美也在参与生成过程这是生成艺术创作者和纯画家的一个本质区别你写的是概率和秩序但选择依然由你做。8.3 系列命名的意义感作为一个从个人项目出发的创作者我给每个批次的作品加上系列名。同一配方的二十张作品属于一个系列系列之间有统一的色彩情绪或鱼形倾向。这个动作看起来很简单但对作品的完整度和传播力影响巨大。当你拿着一个“系列”而不是“一堆图”去展示时观者的态度会有明显不同。这里我不再展开太多商业层面的东西只分享一个原则创作工具的意义不在完美复刻某一位艺术家的风格而是让他的语言成为你表达的起点。米罗可以成为你切入生成艺术的那扇门但这道门后面有无限条路。鱼的题材、鸟的题材、植物的题材、抽象的构图……每一类都可以嫁接到这套生成逻辑上。9. 踩坑横评从废物参数到高质感输出之间我走过的弯路最后分享几个我在调试过程中走过的弯路。这些坑每个都耗费了我不少时间如果你正打算做类似项目绝对能用上。9.1 全随机参数并不等同于风格化刚开始时我把鱼的长宽比、颜色、符号数量、曲线控制点全部丢进随机函数里结果十张图里能出两张好的已经是幸运。问题出在“风格”其实是由大量约束构成的控制点角度固定、厚度比例固定范围、符号数量的配额限制、色相明度的取值范围。没有这些约束生成结果只是一堆随机图形的堆积。风格的本质不是放得开而是收得住。9.2 “看起来简单”的贝塞尔路径实际调试成本最高鱼身轮廓的贝塞尔曲线调试成本极高因为我需要同时保证曲线闭合、没有尖角、没有自交。自交问题在鱼尾附近尤其常见——控制点让曲线折返形成一个闭环。我的调试方案是在开发模式里开启显示控制点逐个观察曲线走向并加上一组后处理校验将曲线采样成精密的点检查相邻点之间的方向角如果转角超过规定值就认为存在异常并重新生成。这个方法比较笨拙但可靠。9.3 Perlin噪声的种子隔离这是一个非常容易忽略的细节。如果鱼身厚度的噪声和鱼身位置随机没有隔离最终输出往往会出现难以解释的联动效应比如鱼更大时身体也更肿或者颜色和位置异常相关。正确做法是把噪声和随机分成两条线所有空间变化使用noise()并传递独立的种子偏移所有离散选择使用random()。单独使用一套会造成混乱的隐形关联。9.4 描边顺序真的要分层前面说到黑线最后统一描这是我在反复实验中提炼出来的关键顺序。如果你把每段路径画的时候就直接加描边符号之间重叠的部分就乱作一团尤其是鱼身和鱼尾、鱼身和符号之间。分层格式化输出也让后续的动画化比如每只鱼身体律动、尾巴摆动变得更容易因为黑线轮廓和彩色填充可以分别更新。9.5 屏幕色彩到打印色彩的偏差把作品输出到纸上时屏幕上的高饱和红蓝绿会变灰不少。如果你打算出实体装饰画建议在生成时就预留一个“打印色板”配置——颜色饱和度稍高几个点或者输出后统一做一轮轻微的色彩增强。我就因为没注意这个细节第一批打印作品显得色彩寡淡后面重新调整才找回预期中的跳跃感。结束语让生成艺术替你说话对我来说MiroFish这个项目最迷人的地方不在于能生成多少张鱼而在于它验证了一件事艺术风格并非不可分割的整体而是可以被拆解、理解、程序化重组的一套语言系统。米罗的画看上去自由不羁但他强大的风格正是建立在极其个人化、极其稳定的语法之上。而这份稳定恰好是代码最擅长的事。如果在阅读过程中你脑海中已经浮现出“我这个选题是不是也能拆成几个可生成的元素”这种念头那这篇内容就值了。欢迎拿这个项目做基础去尝试你自己的风格迁移和符号生成实验。生成艺术是一扇门推开之后会发现风格本身也不过是另一种可以被编写的语言。