ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SVG+Python:从零打造可复用的简笔画角色资产

SVG+Python:从零打造可复用的简笔画角色资产 如果你留意过独立小游戏、微信表情包、课程配图这些场景会发现开发者和独立创作者最缺的往往不是画功而是一套“能复用”的角色资产。手绘角色自由但改一个表情就要重画一遍AI 生成角色很快但下一张图角色长什么样就完全失控。今天这篇文章用“动漫简笔画任性的蓝豆包”作为真实例子把简笔画角色的设计、SVG 绘制、Python 批量生成到表情动画完整走一遍。这篇文章的核心判断很简单简笔画不是“画得简单”而是“形状足够统一”。只要把角色拆成可复用的圆形、椭圆、线条再把每个表情定义成模板哪怕没有美术基础也能用代码产出一套稳定、可改、可批量使用的角色图。读完你不仅能得到一只“任性的蓝豆包”还能掌握一种把任何 Q 版角色“参数化”的通用方法。1. 为什么简笔画角色值得用代码做很多人第一次想到“用代码画图”时第一反应是没必要手绘几分钟的事为什么要写程序这种想法在“画一张”的时候成立但在“画一套”“画一百张”“持续修改”的时候就不成立了。1.1 手绘、AI 生成和代码绘图到底差在哪手绘的核心成本是重复劳动。角色侧身、眨眼、生气、委屈、撇嘴这些表情在美术上并不难难的是每次修改保持风格一致。一个脸色稍微偏了一点、眼睛大小差了几个像素整套表情包立刻显得不统一。AI 生成解决的是“从无到有”的快但它很难解决“从有到稳定”。你让 AI 生成一只蓝色团子角色每张图都能看出来是蓝色团子但你很难保证它左眼和右眼的相对位置、腮红大小、嘴型弧度完全一致。做头像可以接受做需要逐帧动画、多尺寸切片、版本迭代的角色资产就非常吃力。代码绘图则走了另一条路它把角色变成“数据”。身体是一个坐标和半径表情是几组参数颜色是一个字典里的值。改任何一个参数所有输出同时更新不同表情只是同一套模板的不同参数组合。这种方法牺牲了手绘的随意性和 AI 的创造性换来了工程上最看重的一致性、可追溯性和批量生产能力。1.2 这篇文章到底要解决什么问题这篇文章不会教你如何成为插画师也不会讨论高深的图形学算法而是聚焦一件事如何让一个简笔画角色从“概念”变成“可复用的数字资产”。“任性的蓝豆包”就是贯穿全文的案例。我会先给这个角色做设定拆解然后用 SVG 把它画出来再用 Python 生成 PNG最后做几个表情帧并批量输出。整个过程不需要专业绘图软件不需要数位板也不需要联网调用任何大模型一台装了 Python 的电脑就够。如果你正在做以下任一件事这篇文章会特别适合你做独立小游戏需要一套风格统一的临时美术素材做聊天表情包或头像生成器需要批量产出不同表情做课程 PPT、视频封面、技术博客配图需要一个长期复用的虚拟形象想学习 SVG 或 Python 绘图但不想从枯燥的语法入门。2. 任性的蓝豆包角色设定与简笔画拆解在写任何代码之前先要做角色定义。很多教程直接给你一段 SVG 让你复制粘贴结果你复制完发现改一个表情就乱了因为你不知道每个数字代表什么。2.1 角色设定蓝豆包是谁“任性的蓝豆包”可以理解为一个 Q 版团子角色身体是一只圆润的蓝色豆包没有脖子和四肢主要靠表情表达情绪。它的性格关键词是“嘴硬”“不服气”“傲娇”所以表情设计的重点不是可爱而是“一眼看上去就知道它在闹别扭”。角色设定不需要复杂但必须明确几个基础信息身体形态圆形团子体底部有一对小脚视觉重点眼睛、眉毛、嘴型核心情绪任性、撇嘴、斜眼、微微昂头颜色基调蓝色身体、粉色腮红、深色五官。这些信息决定了下文所有代码的结构。你会发现角色能不能成立80% 在设定阶段就已经决定了绘制阶段只是把设定翻译成坐标和路径。2.2 简笔画的形状语言简笔画最容易犯的错误是“想画什么就画什么”。今天眼睛画成圆形明天眼睛画成椭圆形后天加了个高光看起来每个细节都差不多但整体就是不统一。解决办法是引入“形状语言”概念。所谓形状语言就是整个角色只使用有限几种基础几何元素。对于蓝豆包我限定为三种圆形身体、腮红、高光、眼睛的主体椭圆眼睛、脚线条眉毛、嘴。为什么限定成三种因为形状越少角色越容易被大脑快速识别。Q 版角色之所以可爱很大程度是因为它像“几何玩具”而不是像“迷你真人”。这也是为什么很多卡通角色几十年来设计语言基本不变形状一致性本身就是品牌感。2.3 配色方案先定颜色再画图颜色是角色风格的半条命。不要在画图过程中随手挑色而是先定好一套主题色后续所有代码都从这套颜色里取。下表是“任性的蓝豆包”推荐配色用途十六进制颜色色值说明身体主色#6BB5F5柔和的天空蓝避免饱和度太高五官主色#2D2D3F深蓝灰色比纯黑更柔和腮红#FF8FAB粉色透明度约 0.8高光#FFFFFF白色透明度约 0.6背景透明方便后续接入不同场景把颜色集中管理看起来是很小的习惯但它能避免一个非常常见的问题身体变成“色差蓝”嘴巴变成“深浅不一的灰”最后整套表情包像来自不同角色。3. 工具选型与环境准备“任性的蓝豆包”可以用很多工具实现。这里我选择 SVG 加 Python Pillow原因是它们各自解决了不同阶段的问题。3.1 为什么选 SVG Python PillowSVG 是矢量格式最大的优点是文本化。你可以直接在文本编辑器里写、可以直接放进 Git 做版本管理、可以在浏览器里预览、可以精确控制每个坐标和颜色。对程序员来说SVG 不是一种“图片格式”更像一种“描述图形的 DSL”。Python Pillow 解决的是栅格化输出问题。SVG 虽然精致但表情包平台、游戏引擎、聊天软件往往需要 PNG甚至要求特定尺寸。Pillow 可以按程序逻辑生成图片也可以把复杂的绘制过程封装成函数批量输出不同表情。两者的分工很清晰SVG 用于设计和展示Pillow 用于批量产出和程序化控制。实际项目里你也可以只选一种但两个都掌握以后你会发现角色绘制的灵活性提升了一个量级。3.2 环境准备本文代码不依赖特殊系统Windows、macOS、Linux 都可以。建议准备Python 3.9 以上版本Pillow 库一个现代浏览器用于预览 SVG一个能写代码的文本编辑器。版本不需要过度纠结。Pillow 的 API 近几个版本变化不大下面代码在 Pillow 9 和 Pillow 10 上都能运行。安装命令如下python -m venv .venv # Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate pip install --upgrade pip pip install pillow安装完成后可以通过下面的命令确认 Pillow 是否可用python -c from PIL import Image; print(Image.__version__)如果你看到版本号说明环境已经就绪。整个项目不依赖网络所有文件都在本地生成。4. SVG 绘制核心步骤SVG 是文本所以它很容易被“读”。但它的坐标系统需要先理解默认坐标系原点在画布左上角x 轴向右y 轴向下。这意味着你写坐标的时候数字越大越靠右下。很多人第一次把圆画到画布外面往往就是忽略了这一点。4.1 先搭画布与定位网格我建议把画布固定为 400x400中心点放在 (200, 200)。所有元素都围绕中心布局这样角色不会跑偏。先不要急着画细节把身体的大圆放上去确认位置和比例。一个适合 Q 版角色的比例大概是画布400x400身体圆圆心 (200, 210)半径 110身体底部大约 y320给下方留出小脚的位置。这段代码可以放到 VS Code 等编辑器中用 Live Preview 或直接在浏览器打开预览。你会发现角色绘制过程像在做“拼图”。svg width400 height400 viewBox0 0 400 400 xmlnshttp://www.w3.org/2000/svg circle cx200 cy210 r110 fill#6BB5F5 / /svg4.2 从身体到五官身体圆确定后往里加五官。绘制顺序很重要顺序决定了层叠关系先画底层再画上层。对于蓝豆包顺序是身体大圆身体上的高光腮红眼睛眉毛嘴型底部小脚。一个常见的误区是把脚放在身体后面画。如果你先画身体再画脚由于脚的位置在身体下方边缘视觉上会被截断。解决办法是把小脚放在身体大圆之前或者让脚的圆心略低于身体底部露出下半部分。4.3 用表情传递“任性”“任性”这个表情是整套案例的核心。它不只是一个嘴巴上翘或下撇而是眉毛、眼睛、嘴型三个部位共同作用的结果。具体来说左眉毛上扬右眉毛略微低垂形成“略带不屑”的感觉眼睛不完全对称右边眼睛比左边眼睛略扁嘴巴画成“撇嘴”形态左边高、右边低。这种不对称非常重要。如果你画一个完全对称的脸它表达的是“可爱”或“呆萌”而不是“任性”。这也是很多简笔画看起来“有情绪”和“没情绪”的差别所在。5. 完整 SVG 实现代码把前面的设计思路落到代码里就是下面这一段。建议先复制到本地保存为blue_doubao.svg再用浏览器打开预览。svg width400 height400 viewBox0 0 400 400 xmlnshttp://www.w3.org/2000/svg !-- 底部小脚 -- ellipse cx160 cy325 rx26 ry14 fill#6BB5F5 / ellipse cx240 cy325 rx26 ry14 fill#6BB5F5 / !-- 身体 -- circle cx200 cy210 r112 fill#6BB5F5 / !-- 高光 -- circle cx150 cy150 r32 fill#FFFFFF opacity0.6 / !-- 腮红 -- circle cx128 cy238 r20 fill#FF8FAB opacity0.8 / circle cx272 cy238 r20 fill#FF8FAB opacity0.8 / !-- 左眼 -- ellipse cx158 cy200 rx13 ry18 fill#2D2D3F / !-- 右眼 -- ellipse cx242 cy204 rx13 ry15 fill#2D2D3F / !-- 眼睛高光 -- circle cx153 cy192 r4 fill#FFFFFF / circle cx237 cy197 r4 fill#FFFFFF / !-- 眉毛左眉上扬、右眉低垂 -- path dM132 170 Q158 152 182 166 stroke#2D2D3F stroke-width8 stroke-linecapround fillnone / path dM218 176 Q242 178 268 166 stroke#2D2D3F stroke-width8 stroke-linecapround fillnone / !-- 嘴左侧偏高右侧偏低像在撇嘴 -- path dM176 248 Q202 264 236 238 stroke#2D2D3F stroke-width7 stroke-linecapround fillnone / /svg这段代码有几个可以调整的旋钮rx和ry控制眼睛的扁圆程度右眼ry比左眼小让表情更有“斜眼”感两段path的Q控制眉毛和嘴的弯曲程度数值越大越弯腮红的透明度通过opacity控制避免颜色盖过眼睛。打开浏览器预览后你会看到一只蓝色团子表情确实像是在“闹别扭”。到这里SVG 阶段就完成了。你已经有了一张可缩放、可版本管理、可手工调整的角色图。6. 用 Python 生成 PNG 角色图SVG 适合设计但在实际项目中游戏引擎、聊天表情、图片压缩、批量处理都更依赖 PNG。接下来我们改用 Python Pillow 重新绘制这只“任性的蓝豆包”。6.1 绘制函数结构为了让后续能批量生成表情Python 代码不能是“一次性画图 ”而要封装成函数。这里我建议最简设计一个draw_body()负责身体与小脚一个draw_face()负责眼睛、眉毛、嘴一个主函数负责组装画布并保存。这样当你想加“开心”“生气”“委屈”表情时只需要改draw_face()的参数身体不需要动。6.2 完整示例与运行把下面代码保存为draw_doubao.py运行后会在当前目录生成blue_doubao.png。# 文件路径draw_doubao.py from PIL import Image, ImageDraw W, H 400, 400 # 主题色 SKIN (107, 181, 245, 255) FACE (45, 45, 63, 255) BLUSH (255, 143, 171, 210) HIGHLIGHT (255, 255, 255, 160) WHITE (255, 255, 255, 255) def draw_body(draw: ImageDraw.ImageDraw) - None: # 小脚 draw.ellipse((134, 311, 186, 339), fillSKIN) draw.ellipse((214, 311, 266, 339), fillSKIN) # 身体 draw.ellipse((88, 98, 312, 322), fillSKIN) # 高光 draw.ellipse((130, 132, 172, 174), fillHIGHLIGHT) def draw_face(draw: ImageDraw.ImageDraw) - None: # 腮红 draw.ellipse((108, 218, 148, 258), fillBLUSH) draw.ellipse((252, 218, 292, 258), fillBLUSH) # 左眼 draw.ellipse((145, 182, 171, 218), fillFACE) draw.ellipse((150, 186, 155, 194), fillWHITE) # 右眼略微压扁配合“任性”表情 draw.ellipse((229, 188, 255, 218), fillFACE) draw.ellipse((234, 192, 239, 199), fillWHITE) # 左眉上扬 draw.line([(128, 172), (158, 158), (182, 170)], fillFACE, width8, jointcurve) # 右眉低垂 draw.line([(218, 178), (242, 180), (272, 170)], fillFACE, width8, jointcurve) # 撇嘴嘴型左高右低 draw.line([(172, 246), (200, 262), (236, 240)], fillFACE, width7, jointcurve) def main() - None: img Image.new(RGBA, (W, H), (0, 0, 0, 0)) draw ImageDraw.Draw(img) draw_body(draw) draw_face(draw) img.save(blue_doubao.png) print(生成成功blue_doubao.png) if __name__ __main__: main()在终端运行python draw_doubao.py如果代码正确你会看到输出“生成成功blue_doubao.png”。打开图片应该能看到一只和 SVG 版本风格一致的蓝色团子。6.3 验证输出为了确认图片尺寸和模式正常可以在 Python 中执行from PIL import Image img Image.open(blue_doubao.png) print(img.size, img.mode)预期输出(400, 400) RGBARGBA表示图片包含透明通道后续放到任何背景上都不会出现黑色方块。如果输出的是RGB说明创建图片时没有用RGBA模式需要回头检查代码。7. 让角色动起来表情动画与批量生产静态图只是第一步。在实际项目里你多半需要蓝豆包“动一下”比如眨眼、撇嘴、摇头。这里我给两种实现路径。7.1 用 SMIL 做眨眼动画SVG 内置 SMIL 动画能力不需要引入任何 JavaScript。所谓 SMIL就是通过animate标签改变 SVG 元素的属性。我们可以让右眼在 3 秒周期内完成一次眨眼。保存下面的内容为blue_doubao_blink.svg用浏览器打开svg width400 height400 viewBox0 0 400 400 xmlnshttp://www.w3.org/2000/svg !-- 省略身体和腮红只展示右眼动画 -- ellipse cx242 cy200 rx13 ry15 fill#2D2D3F animate attributeNamery values15;2;15 dur3s repeatCountindefinite / /ellipse /svg这个动画的关键是values15;2;15圆的高度先从 15 缩到 2再回到 15。一眼看上去就像是眼睛快速闭合又睁开。repeatCountindefinite表示循环播放。如果你希望蓝豆包“撇嘴”动作也更明显可以给嘴型path同时做stroke-dasharray动画或透明度闪烁。但注意并不是所有 SVG 属性都能平滑动画d属性在不同浏览器中的兼容性差异较大建议优先用ry、opacity、transform这类属性做动画。7.2 用 Python 批量输出表情包现在回到 Python。假设你已经有了一个可复用的draw_face()函数接下来可以做成批量表情生成器。最简单的做法是定义一组“表情配置”每个配置包含眼睛大小、嘴型方向、眉毛倾斜度等参数然后循环绘制并保存到不同文件。下面是一个最小批量示例用同样的身体、不同的嘴型与眉毛生成 4 张表情图# 文件路径batch_doubao.py from PIL import Image, ImageDraw W, H 400, 400 SKIN (107, 181, 245, 255) FACE (45, 45, 63, 255) BLUSH (255, 143, 171, 210) HIGHLIGHT (255, 255, 255, 160) WHITE (255, 255, 255, 255) def make_expression(name: str, brow_offset: int, mouth_points: list) - None: img Image.new(RGBA, (W, H), (0, 0, 0, 0)) draw ImageDraw.Draw(img) # 身体 draw.ellipse((134, 311, 186, 339), fillSKIN) draw.ellipse((214, 311, 266, 339), fillSKIN) draw.ellipse((88, 98, 312, 322), fillSKIN) draw.ellipse((130, 132, 172, 174), fillHIGHLIGHT) # 腮红 draw.ellipse((108, 218, 148, 258), fillBLUSH) draw.ellipse((252, 218, 292, 258), fillBLUSH) # 左眼 draw.ellipse((145, 182, 171, 218), fillFACE) draw.ellipse((150, 186, 155, 194), fillWHITE) # 右眼 draw.ellipse((229, 188, 255, 218), fillFACE) draw.ellipse((234, 192, 239, 199), fillWHITE) # 左眉 draw.line([(128, 172), (158, 158), (182, 170)], fillFACE, width8, jointcurve) # 右眉通过 brow_offset 调整角度 draw.line([(218, 178 brow_offset), (242, 180 brow_offset), (272, 170 brow_offset)], fillFACE, width8, jointcurve) # 嘴型 draw.line(mouth_points, fillFACE, width7, jointcurve) img.save(fblue_doubao_{name}.png) print(f生成blue_doubao_{name}.png) if __name__ __main__: make_expression(default, 0, [(172, 246), (200, 262), (236, 240)]) make_expression(pout_low, 8, [(172, 252), (200, 270), (236, 246)]) make_expression(upset, 12, [(170, 258), (200, 268), (238, 244)]) make_expression(happy, -6, [(168, 250), (200, 230), (234, 248)])运行python batch_doubao.py执行结束后目录下会多出 4 张 PNG。你会发现角色身体完全一致只有表情参数不同。这正是“代码绘图”相对手绘和 AI 生成最突出的优势一致性可控、批量成本极低。8. 常见问题与排查思路无论 SVG 还是 Pillow新手都会遇到一些固定问题。下面这张表基本覆盖了最常见的“蓝豆包翻车现场”。问题现象可能原因排查方式解决方案PNG 背景变成黑色创建图片时用了RGB而不是RGBA用print(img.mode)查看模式将Image.new的 mode 改为RGBA图片顶部或底部被裁切元素坐标超出了画布范围打开画布网格线逐个确认坐标调整圆心、半径或画布大小嘴型方向反了或者位置不对Pillow 的 y 轴向下容易把“上扬”画成“下撇”对照坐标值检查mouth_points调低第一点的 y调高最后一点的 ySVG 打开正常但导入 PPT 或微信显示异常对方不支持部分动画或外部字体先用浏览器导出 PNG再使用导出一份 400x400 的 PNG 作为替代角色看起来不像在“任性”脸部过于对称眼眉角度太温和观察左右眼ry、眉毛纵坐标差让右眼稍扁、右眉稍低嘴型左高右低保存的 PNG 边缘有明显锯齿直接用 400x400 绘制抗锯齿不足放大图片观察边缘先绘制 4 倍大小再使用resize()缩小这里最容易被忽略的是第二项“元素超出画布”。SVG 画布默认不会裁剪元素以外的内容所以有时候你看到的图片很完整但导入别的工具后反而被裁掉。建议在绘制完成后用浏览器截图或 Python 检查实际尺寸不要把 SVG 默认行为当作最终结果。9. 最佳实践把角色做成可复用资产一只“任性的蓝豆包”画完之后如果只是导出一张图那它还算不上资产。所谓角色资产指它可以被下一个项目继续使用、扩展、维护。要做到这一点必须从一开始就带着工程化思维。9.1 角色模板化把角色拆成“身体层 表情层”是很有用的习惯。身体层包括大圆、小脚、高光平时不需要改动表情层包括眼睛、眉毛、嘴是后续迭代的重点。在这个基础上你还可以把颜色集中到一个设置区。比如在 Python 脚本开头统一维护SKIN、FACE、BLUSH等变量在 SVG 中给关键颜色加注释。这样当你想把蓝豆包改成“粉豆包”“绿豆包”时只需要改少量颜色值不需要重画。9.2 工程化建议把我个人在实际写这类“小型角色生成器”时总结的几条经验列在这里命名规范要统一。比如blue_doubao_default.png、blue_doubao_angry.png不要出现1.png、最终版.png这种文件名。画布尺寸固定。尽量固定 400x400后续需要放大缩小再程序化处理而不是为每个尺寸重新画一遍。优先保存 SVG 源文件。SVG 是文本可以放进 Git每次修改都能看到 diff这对团队协作非常重要。不要忽视透明通道。除非有明确背景需求否则一律输出RGBA方便后续叠加到不同场景。批量生成时保持函数纯净。不要让某一个生成函数依赖全局状态否则表情一多很容易出现“这个表情把上一个表情覆盖了”的奇怪现象。这些规则看起来琐碎但它们决定了你能不能在两天后继续修改这只蓝豆包而不是重新画一遍。10. 后续可以深入的方向如果你跟着文章把“任意的蓝豆包”跑通了下一步可以往几个方向继续扩展。第一增加角色姿态。现在蓝豆包只有正面头像你可以尝试给它加上侧身、背面、跳跃、倒地等姿态。每增加一种姿态其实都是在“形状语言”基础上增加一组几何组合。第二做帧动画。用 Pillow 批量生成若干关键帧再用ImageSequence或直接输出 GIF可以做出更生动的小动画。这个过程可以复用本文的批量表情思路。第三接入游戏引擎或前端。SVG 可以直接用于 Web 前端PNG 则可以导入 Unity、Godot、Cocos 等引擎。很多情况下临时美术素材不需要专业人员用代码生成的角色足以支撑原型验证。第四把角色参数化并做成工具。当你发现“眼睛大小”“嘴型弧度”“眉毛高度”这些参数经常要调时可以做一个简单的配置文件甚至用 FastAPI 包成一个图片生成接口。这也是“角色资产化”最彻底的一种形态。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进