手绘3D建模 是做什么的?
你使用几个简单的实体为对象建立3D模型,然后工具包以手绘风格绘制它。每个面从左上角光源获得三个平涂的马克笔色调。相邻且色调相同的面会融合成一个色块。外轮廓用粗墨水线,折痕线用较细的、抖动渐尖的线条,画在奶油色纸上。场景根据一个循环变量 t 逐帧绘制。输出是一个循环动画SVG:帧通过CSS切换,无需脚本,因此可在浏览器和<img>标签中播放。文本使用内置手写字体,支持完整越南语。
工具包:assets/kit.js · 渲染器:assets/render.mjs · 所有函数和选项:references/kit.md(在编写场景前阅读) · 工作示例:examples/(杯子、茶壶、烤面包机、风车:阅读最相似的那个)。
首次设置
PNG/GIF导出需要 @resvg/resvg-js。如果 render.mjs 提示缺少此依赖,请在该技能的文件夹中运行 npm install。SVG本身无需任何外部依赖。
开始绘制前
- 它能被画出来吗? 如果主题是抽象的(如“希望”、“效率”),不要画模糊的东西。建议一两个能展示它的具体场景,然后绘制用户选择的一个。
- 图表、UI、按钮、交互式内容: 不使用本技能。
- 什么会动? 每张图片都动画化物体本身:相机从不移动。选取属于该物体的一到两个运动。水壶的盖子跳动并冒气。烤面包弹起。风车的风车叶片转动。风扇旋转。猫尾巴摆动。台灯点头。植物摇摆。如果用户指定了一个运动,就使用那个运动。如果没有东西可以合理移动,就使用蒸汽、轻微上下摆动或摇曳,以及线条本身的抖动效果。
工作流程
- 拆解部件。 列出物体的真实部件,以及每个部件如何由实体构建。在场景文件顶部的注释中将其写成一个表格:部件 · 调用函数 · 尺寸/位置 · 颜色。根据真实部件(边缘、盖子、旋钮、手柄、脚、槽、铰链)建模,而不是画一个带名字的色块。查询你不确定的比例。
- 建模到
scene.js中(世界坐标系:x向右,y向后,z向上,地面 z = 0;单位近似毫米;自动适配画面):- 可以在陶轮上旋转的圆形物体 →
k.lathe(容器使用wall:参数)、k.cylinder、k.sphere - 方形物体 →
k.box(使用round:参数实现柔软圆角)、异形轮廓 →k.extrude - 手柄、壶嘴、管道、腿、线缆、茎 →
k.tube,起始于主体内部(会在表面处被截断) - 容器里的内容物 →
{ in: container };平面上的标记 →k.panel/k.line加上{ on: solid } - 当物体的“侧视图”应面向观者时,使用
k.group({ rotate: { z: 45 } }, …)旋转整个物体(例如茶壶的壶嘴在左,手柄在右)
- 可以在陶轮上旋转的圆形物体 →
- 制作动画 使用
t∈ [0, 1):运动部件放在k.group({ about, rotate, at }, …)中,并由k.wave()、k.pulse()或360 * t驱动。每个运动必须在t = 1时回到起始位置。 - 渲染并检查:
使用阅读工具打开node <skill>/assets/render.mjs scene.js -o out/scene.svg --png out/scene.png --strip --thumbout/scene.png(静态图)和out/scene-frames.png(循环中的6帧),并观察它们。添加--grid以读取图纸坐标。检查out/scene-128.png:它必须在128像素大小时仍然能被识别出是什么物体。 - 修正并重新渲染。 检查:
- 一眼看去是否能识别出物体?首先要看轮廓。
- 没有顺序错误的绘制: 部件穿透主体、远处的部件遮住近处的、内容物被遮挡或穿出。使用
references/kit.md中“实体排序规则”下的方法修正:分离实体、in:、on:、只建模开口外部部分,以及最后手段的layer。 - 部件接触其支撑物。没有漂浮的部件(除非它应该漂浮,如跳动的盖子)。
- 运动在帧序列中一目了然,并且循环没有跳变。
- 标签位于边缘,并且尽可能少地跨越图形。
重复步骤4-5直到正确(通常需要2-4轮)。最终渲染不带
--grid。
在看过静态图和帧序列之前,绝不交付最终结果。 交付SVG路径(动画图片)、PNG,并展示PNG。当用户需要GIF,或者SVG需要放在不支持SVG动画的地方(如聊天应用、某些电子邮件客户端)时,添加 --gif out/scene.gif。
风格规则
- 统一风格: 纸上的墨线+平涂马克笔色调。无渐变、发光、模糊、透明度效果、SVG
<text>或原始SVG。仅使用此工具包。 - 颜色: 使用调色板名称(参见
references/kit.md),每张图片3-5种颜色加墨线。调色板名称效果最佳;任何十六进制颜色都会被柔化。每个填充自动提供亮、中、暗三个色调。不要用额外的实体伪造阴影。 - 相机: 默认为等距视角。仅在必要时更改
sheet.camera。对于高耸物体(塔、瓶子、灯、人物),使用较低俯仰角(20-25)和25-35偏航角可获得更友好的“产品展示”效果。 - 简洁而厚实: 几个干净的实体比许多琐碎的小块可读性更好。小部件(旋钮、脚、按钮)是使其看起来真实的关键:一定要添加它们。
- 放置于地面: 阴影是自动的。站在板、桌子或碟子上的物体将阴影投射到支撑物上。在小物体下放置一块板、托盘、土丘或桌子有助于其定位到空间中。
- 文字: 可选的
k.title(...)位于左上角,以及指向3D点的简短小写边缘注释(k.label)。使用用户的母语编写。图形上不要有文字框或对话气泡。 - 画布: 默认为1000×1000。对于宽场景,使用
export const sheet = { width: 1200, height: 900 }。加上--transparent可移除纸张背景。 - 动画规格: 默认16帧,12 fps(一个1.3秒的循环);典型的SVG大小在0.5-1 MB之间。对于慢动作,使用
sheet.fps = 8。保持frames≤ 24。
场景模板
// 三脚架上的茶壶;盖子跳动。
// 部件 调用函数 尺寸/位置 颜色
// 三脚架 cylinder r 120, h 10 wood
// 壶体 lathe { smooth } r 55→94→50, h 118, 在三脚架上 teal
// 壶嘴 tube [14, 7] 从壶体内部开始,向左延伸 teal
// 手柄 tube 8 两端都在壶体内部,右侧 teal
// 盖子+旋钮 lathe + sphere 在以其后边缘为铰链的组内 teal, ochre
export const sheet = { seed: 3 };
export function draw(k, t = 0) {
k.cylinder([0, 0, 0], 120, 10, { fill: 'wood' });
k.group({ rotate: { z: 45 } }, () => { // 壶嘴朝向屏幕左侧,手柄朝向屏幕右侧
k.lathe([0, 0, 10], [[55, 0], [78, 10], [94, 45], [90, 85], [68, 112], [50, 118]], { fill: 'teal', smooth: true });
k.tube([[-78, 0, 50], [-112, 0, 66], [-128, 0, 100], [-146, 0, 118]], [14, 7], { fill: 'teal' });
k.tube([[70, 0, 112], [112, 0, 108], [122, 0, 72], [84, 0, 40]], 8, { fill: 'teal' });
const hop = Math.max(0, k.wave(0, 2 * t)); // 0..1,每循环跳动两次
k.group({ about: [44, 0, 128], rotate: { y: -18 * hop }, at: [0, 0, 16 * hop] }, () => {
k.lathe([0, 0, 127], [[52, 0], [50, 5], [30, 13], [8, 18], [0, 19]], { fill: 'teal', smooth: true });
k.sphere([0, 0, 152], 10, { fill: 'ochre' });
});
k.steam([-150, 0, 125], { h: 90, n: 2, spread: 18 });
});
k.label('跳动的盖子', { to: [0, 0, 140], side: 'right' });
}