| 名称 | machine |
| 描述 | 将一句话(“涡扇喷气发动机”“机械手表”“LVAD 心室辅助血泵”)转化为照片级、基于物理的交互式 3D 机械仿真,输出为单个 HTML 文件,并生成 20 到 30 秒的短片用于发布。真实比例以米为单位,真实材质(PBR 金属、涂料、聚合物、玻璃),带软阴影的工作室灯光,由小型求解器驱动的运动部件并计算真实数值(推力、扭矩、转速、流量、压力、效率),以及爆炸、干净截面封盖的剖切、X 光、慢放、标注和破坏等模式。只要有人要求构建、模拟、可视化、解释或“展示 X 如何工作”,对象是任何机器、发动机、机构、设备、仪器或硬件,或想要交互式 3D 模型或剖视图,或想要用于发布的机器视频,即使他们从未说出“仿真”或“three.js”,都应使用此技能。 |
机械技能
你构建机器的方式,就像产品摄影工作室拍摄机器、工程师讲解机器一样:真实物体、真实尺寸、真实材质、干净的摄影棚布景,并运行真实方程。一句话输入,输出一个 HTML 文件,一条命令生成短片。
输出始终为:
<name>.html:一个自包含页面(来自 jsDelivr 的 three.js 0.170,工具包已内联)。它可以从磁盘、任意静态托管或 iframe 中打开。- 一段短片:
node scripts/record.mjs <name>.html out/<name>.mp4 --music freepd:<Category>/<Name>。
先打开 examples/。每个示例都由其页面顶部的一句话生成。examples/turbofan.html 是参考:在写任何内容之前,从头到尾阅读它。
标准
开始之前,想象观众在三秒后看到的画面。它必须已经看起来像真实机器的摄影棚产品照:正确的轮廓和比例,金属看起来像金属,纸面地板上投下柔和阴影,没有悬浮物,没有穿模,没有占位符。然后它动起来,屏幕上的每个数字都是计算出来的。
只要以下任一情况为真,结果就不算完成:
- 机械师会预期的零件缺失(燃烧室里没有燃油喷嘴,手表里没有擒纵机构,发动机里没有气门)
- 有东西悬浮、相交,或在连接处有明显的接缝或间隙
- 材质本应是金属却看起来像塑料,或者所有东西都是同一种灰色
- 屏幕上的数字是手打的,而不是由求解器计算出来的
- 剖切显示的是空心壳体,而不是实心截面
- 短片的第一帧不漂亮
工作流程
1. 研究真实机器(5 分钟,不写代码)
在机器脚本顶部的注释块中写下(根据你的知识,如果有网络也可查网):
- 真实尺寸,以米为单位,列出定义轮廓的 5 到 10 个零件(涡扇:风扇直径 2.85 m,长度 4.9 m,短舱最大直径 3.44 m)。
- 零件清单,按装配顺序排列,每个零件带材质。目标为 15 到 40 个命名零件;重复零件(叶片、齿、螺栓、线圈)逐个计数。
- 使它工作的物理原理,写成 3 到 6 个你实际会计算的方程,带真实常数和工作点(涡扇:布雷顿循环,起飞时 N1 100%,OPR 42,T4 1,750 K,约 330 kN)。
- 观众最关心的一个数字:主角数值(推力、扭矩、每小时节拍、流量、功率)。它配有一个巨大的衬线读数和一个细条。
- 观众想驱动的输入:一个滑块(油门、风速、转速、泵速、研磨粒度)。
- 破坏:工程师担心的失效,以及求解器说会发生什么(风扇叶片飞脱与不平衡载荷,超过红线后的气门浮起,泵失速,超压)。这必须是计算出的后果,而不是动画。
2. 从模板构建页面
将 kit/template.html 复制为 <name>.html。它包含 import map(固定 three@0.170.0)、字体、一个用于放置工具包的标记块,以及你要填写的四个部分:
const { THREE, Lab, MAT, GEO, heatGlow, clamp, lerp, approach, fmt } = window.LabKit
const lab = new Lab({ prompt, no, title, sub, size, view, cut }) // 1. 实验室
// 2. 零件 lab.mesh(GEO..., MAT..., { name, pos, explode, xray, count })
// 3. 求解器 lab.hero(...); lab.slider(...); lab.onTick((dt, lab) => { ... lab.setHero(...) })
// 4. 破坏、解释、短片 lab.breakIt(...); lab.how(...); lab.autoFilm(...); lab.start()
复制后立即运行 node <skill dir>/scripts/inline.mjs <name>.html:它会把工具包粘贴到标记块中,使页面无论位于何处都是单个自包含文件(模板中的 src="../kit/lab-kit.js" 只在此仓库内可解析)。然后在第二个 script 块中编写你的机器。
3. 几何:真实尺寸的真实实体
在机器自身坐标系中以米为单位工作,Y 向上,除非设置 ground,否则地板位于 y = 0。将 size 设为最长真实尺寸;工具包会缩放显示,使每台机器在布景上占据相同大小,比例尺显示真实尺寸。
- 圆形零件(轴、盘、壳体、喷嘴、锥体、整流锥、活塞、表圈):使用 CLOSED 轮廓的
GEO.lathe([[r, y], ...])。壁:GEO.shell(profile, thickness),管:GEO.tube(rOut, rIn, h),倒角盘:GEO.disc(r, h)。棱角保持锐利,曲线自动平滑着色。 - 叶片和导叶:
GEO.rotor({ count, axis, r0, r1, chord, thick, camber, twist, pitch, sweep })构建真实的带弯度、扭转的翼型(NACA 厚度)作为闭合实体,并环绕排列。弦长、厚度和弯度可使用[root, tip]数组。 - 齿轮:
GEO.gear({ teeth, module, width, bore, helix, spokes, rim })。两个齿轮按中心距(z1 + z2) * module / 2啮合,并以齿数反比转动,错开半个齿。 - 管道、软管、线缆:
GEO.pipe(points, r)(闭合、带端盖)。沿结构从端口到端口布置,绝不能穿过零件。 - 紧固件:每个法兰上使用
GEO.bolts(n, R, size, h)。弹簧:GEO.spring(R, wire, turns, len)。盒子:GEO.rbox(w, h, d, r)(始终圆角,绝不尖锐)。带孔板:GEO.extrude(shape, depth)。 - 质量和惯量来自几何:
GEO.volume(geo)给出体积和质心,乘以真实密度。
保持干净的规则:
- 每个零件都是闭合实体(lathe 轮廓闭合,壳体有厚度,管道带端盖)。这正是剖切能绘制实心截面的原因。开放表面在剖切时会显示为空心。
- 没有明显相交,没有悬浮。 每个零件都与其支撑物接触(法兰坐在面上,轴在轴承中运行,支柱到达地板)。在特写中检查连接处。
- 把细节放在视线所及处:法兰上的螺栓圈、倒角、唇边、铭牌、燃油管、线束。大平面要有接缝、分模线或纹理饰面。
- 给机器一个底座:支架、托架、基座、工作台或安装座,脚落在地板上。机器在现实中很少悬浮。
- 预算:最多约 70 万三角形和 200 个网格。用
GEO.merge/GEO.ring合并重复的静态零件;保持运动组分离。
4. 材质:绝不用单一灰色
使用 MAT.* 预设;它们带有实测反射率,使钢、钛、铝、黄铜和铜并排看起来不同。每个预设都接受覆盖项(color、roughness、clearcoat、任意 MeshPhysicalMaterial 字段)和 finish(微粗糙度纹理:车削零件的 turned 环纹、brushed 拉丝、cast 铸造斑驳、smudge 污迹、knurl 滚花)。
| 用途 | 预设 |
|---|---|
| 轴、盘、机加工零件 | MAT.steel、MAT.stainless、MAT.chrome |
| 壳体、铸件 | MAT.aluminium、MAT.castIron、MAT.steelDark |
| 叶片、航空航天 | MAT.titanium、MAT.inconel(+ heatGlow) |
| 接头、布线 | MAT.brass、MAT.copper、MAT.gold |
| 喷漆机身 | MAT.paint({ color })、MAT.satin、MAT.anodised({ color }) |
| 聚合物 | MAT.plastic、MAT.rubber、MAT.carbon |
| 窗户、晶体 | MAT.glass、MAT.sapphire、MAT.ruby、MAT.enamel |
| 光、火焰、热气体 | MAT.glow、heatGlow(material, kelvin) |
像工业设计师一样选择调色板:以金属为主,加一两种涂料颜色,最多一个强调色。热零件根据求解器温度发光(material.userData.heat.value = T)。
5. 求解器:要么真实数字,要么没有
写一个小型纯函数,从输入和状态计算你显示的每个数字,使用真实单位和真实常数。用 approach(一阶滞后:加速、热质量)或 rk4(弹簧、擒纵、摆)积分动力学。然后由它驱动运动:求解器给出的 rpm 转动轴,流量移动粒子,温度加热金属。
- 一个主角:
lab.hero({ label, unit, max }),然后每 tick 调用lab.setHero(value, { state, sub, bad })。state是两到三个词(加速中、起飞、阻塞流)。sub是一行,包含另外 2 或 3 个数字。 - 一个滑块:
lab.slider({ id, label, min, max, value, unit, ends })。只有在机器确实需要时才用第二个。 - 标签挂在静态零件上,位于剖切平面上的点:
lab.label(obj, 'HP turbine', { at: [x, y, z], value: () => '12,979 rpm' })。 lab.how(words, equations)填充“如何计算”:用平实语言描述模型,以及代码中写出的方程。说明哪些是简化的。- 如果运动太快看不清(2,700 rpm 的风扇),显示慢放并在
how中说明(“旋转以 1:60 显示”)。绝不放慢数字。 - 用第 1 步写下的公开数据对工作点做合理性检查。如果偏差超过约 20%,修正模型,而不是修正数字。
6. 流动、热与效果
lab.flow({ paths, count, length, color, parent }):沿你根据真实流道构建的路径移动的流线(空气穿过管道、血液穿过泵、水穿过磨盘)。用求解器设置.speed,用lab.state.cut设置.opacity,使它们能在剖切中显示。按计算量(温度、压力)着色。lab.plume({ radius, length, dir, color: 'rocket' | 'jet', shocks }):带马赫环的尾焰;用求解器设置.power。lab.sparks.emit(worldPos, n, speed)和lab.shed(mesh, v, w)用于破坏:火花在重力下下落并反弹;飞脱零件成为刚体并落到地板上。
7. 模式(工具包负责,你做设置)
- 爆炸:每个
lab.add/lab.mesh接受父级坐标系中的explode: [dx, dy, dz](米)。让零件沿其装配轴移动,外壳最远,核心保持不动。任何东西在移出过程中都不应相互穿插。 - 剖切:在 Lab 上设置
cut: { a, b, center, reach }。a和b是被移除区域的方向:a = [0,0,1], b = [0,1,0]移除朝向观察者和上方的四分之一(最适合有轴线的物体);b = a移除一半。每个闭合实体都会自动绘制截面面,并带淡色剖面线。 - X 光:外壳上的
xray: 'shell'会将其变为带细边缘的玻璃,使核心在运行时可见。 - 慢动作、标签、暂停免费提供(按键 S、L、Space)。交互部分包括鼠标环绕、滑块和按钮。
- 破坏它:
lab.breakIt({ label, run(lab), reset(lab) })。run改变求解器状态(叶片释放、气门卡住、密封爆裂),求解器计算后果。在主角数值中显示(bad: true),并用火花、碎片或震动表现。
8. 像评论家一样观察它(必需)
渲染并阅读你制作的每一张图像。使用脚本:
node scripts/shot.mjs <name>.html out/review.png # 第一眼
node scripts/shot.mjs <name>.html out/cut.png --js "lab.toggle('cut', true); lab.toggle('labels', true)"
node scripts/shot.mjs <name>.html out/near.png --js "lab.setView({ az: 70, el: 8, dist: 0.8, target: [x, y, z] })"
node scripts/shot.mjs <name>.html out/film --film 1,4,8,12,17,22,26 # 短片静帧
至少进行四轮审查。每轮中:整体视图、带标签的剖切、爆炸视图、两个连接处特写,以及短片静帧。与你记忆中该机器的真实产品摄影比较:比例、零件数量、材质、光线如何落在其上。列出问题,修正,再次渲染。只有当列表为空时才停止。shot 脚本会打印 fps、零件数和三角形数;保持交互帧率高于 30。
9. 短片
lab.autoFilm({ runBeat, runSub, breakBeat, breakSub, sliderTo }) 根据你的机器编写标准的 28 秒故事,以编号节拍和编辑字幕呈现:
- 一句话就是整个简报(提示条自行输入)。
- Claude 构建机器(零件按装配顺序飞入,计数器攀升:“84 / 3,597 个零件”)。
- 它运行(光标拖动滑块;主角数字由求解器攀升)。
- 剖开它(截面封盖、带实时数值的标签)。
- 拆开它(爆炸)。
- 破坏它(光标点击破坏按钮;求解器决定什么失效)。
它会循环:最后一个相机关键帧返回第一个。对于自定义短片,使用 lab.film('name', { len, assemble, shots, events }):shots 是相机关键帧 { t, az, el, dist, target, fov },events 是 { t, beat: ['07', 'Bold sentence.', 'Grey sentence.'], set: { cut: 1 }, slider: { id, to, dur }, cursor: { to: 'slider:id:0.8' | 'button:break' | [x, y], click: true }, do(lab) {} }。
录制(需要 Node 18+、带 Chromium 的 Playwright、ffmpeg):
node scripts/record.mjs <name>.html out/<name>.mp4 --music freepd:Classical/Easy_Lemon
node scripts/record.mjs <name>.html out/<name>-portrait.mp4 --w 1080 --h 1350
页面在短片模式下运行(?film=1):其时钟只在录制器步进时移动,因此每一帧都完整,且每次拍摄的短片完全相同。录制器会防范无头 GPU 卡住时可能返回的洋红色帧,并扫描完成的短片;如果有任何粉色帧,它会以非零状态退出。FreePD 的音乐是 CC0;仍请注明曲目。
屏幕上的文字
页面是编辑式的,不是仪表盘:一个提示条,一个带斜体副标题的衬线标题,一个配斜体单位的巨大数字和一条细条,一行辅助数字,一个滑块。像好的图注一样写作:简短、具体、不夸张,使用真实单位并带空格(338 kN、1,750 K、4.2 L/min),千位分隔。
节拍字幕是一句粗体句子和一句灰色句子。
交付时说什么
给出 HTML 文件的路径、短片命令、求解器达到的工作点与公开数据的对比,以及你简化了什么。提供下一台机器。