model-by-coding 是做什么的?
从照片出发,完全通过代码构建物理物体的3D模型,然后将其制作成拆解影片和GIF。该管线唯一坚持的核心理念是:每一步都通过照片验证模型,因此影片展示的是被拍摄的真实物体,而非一个看似合理的替代品。
使用场景
- 需要在一段短视频中解释或展示某个硬表面产品(风扇、台灯、音箱、相机、工具、厨房电器)的内部结构;
- 物体实际存在,可以从多个角度拍摄照片;
- 输出物是一个用于README、帖子或作品集的循环拆解片段;
- 已有的照片集需要转化为可编辑、可重新渲染的模型——而不是扫描网格。
不适合的场景
- 需要照片级真实的资产:应使用摄影测量法或扫描仪;
- 物体是有机、柔软或可变形的(织物、植物、面部);
- 没有可用照片,或物体仅出现在视频中;
- 单张静态图片就足够——则无需HyperFrames步骤;
- 主题是文字/UI动画:该管线专注于几何体。
管线
| 步骤 | 产出 | 验收标准 |
|---|---|---|
| 1. 测量 | 一个标尺尺寸、一个比例表、一个DIM对象 | 比例在≥2张照片上交叉验证,每个假设标记为推断 |
| 2. 建模 | 代码构建的Three.js场景、一个IIFE包、查看器页面 | 组装状态无间隙;p = 1时每个零件分离 |
| 3. 验证 | 六张相同姿态的照片/渲染对比图 | 所有六张图上轮廓和特征比例在容差范围内 |
| 4. 拍摄 | 一个HyperFrames合成、check报告、交付MP4 | ok: true,无错误,时长与data-duration匹配 |
| 5. 剪辑 | 一个循环GIF | 起始和结束于静止姿态,小于约1 MB |
一个物体的合理时间预算为几小时到两天,大部分时间花在步骤2-3上。跳过步骤3会把一周的建模变成按时交付的错误模型。
第一步 — 从照片测量
收集六个姿态(正面、真实侧面、背面、背面3/4、俯视、仰视),选择一个大尺寸作为标尺,用径向扫描测量,然后将所有其他尺寸表示为其比例。通过角周期计数离散特征(肋条、叶片、辐条),而非目测。将所有数字保存在一个DIM对象中,并附注释标明其来源照片以及是测量值还是推断值;绝对真实世界尺寸是一个决策,而非测量值。
深度参考:references/01-modeling.md §0–2。
第二步 — 用代码建模
一个场景模块,提供纯状态API:create、setView、setExplode、setExplodeMap、setSpin、setHeadTilt、setLift、render、getAnchors、resize、dispose。世界约定:物体朝向+Z,基座在y=0,单位为米。将外壳构建为车削轮廓,格栅构建为扁平带状(非圆管),叶片构建为参数化曲面,并通过一个函数注册每个零件,该函数存储其组装位置、爆炸偏移和锚点偏移。零件命名一次;影片、标签、锚点和声音都基于这些名称。
深度参考:references/01-modeling.md §3–6。
第三步 — 与照片验证
使用esbuild构建包,在无头Chrome中为每个姿态渲染一帧,然后将每帧渲染与其照片进行并排对比,并裁剪到物体的边界框。将比例和零件位置读作物体自身边界框的分数;不要在有复杂背景上对边缘拟合圆——拟合会吸附到背景弧线上。六个姿态,每个迭代几次。
深度参考:references/03-verify.md。
第四步 — 渲染影片
按照HyperFrames约定构建合成:一个根合成元素,带有data-duration,一个暂停的GSAP时间线位于window.__timelines["main"],仅通过hf-seek(而非rAF/定时器)重绘Three.js,具有自身时长的媒体标记为class="clip"并带有data-start/data-duration,一个音频混音作为音频片段,文本尺寸28 px或更大,大型包作为assets/vendor/*.min.js供应。在动画化之前编写镜头列表;从一个导演脚本驱动相机、爆炸和标签,以便音频提示可以共享其常量。运行check,阅读报告,然后运行render --quality delivery。
深度参考:references/02-render.md。
第五步 — 剪辑GIF
选择一个3-4秒的稳定片段(通常是爆炸后的保持状态),在gif-master中剪辑(宽度480,12 fps,循环),或使用ffmpeg双通道调色板命令,并在发布前验证大小、帧数和循环。深度参考:references/04-gif.md,scripts/make_gif.md。
硬约束
以下是违反时会中断管线的规则;它们不是风格偏好。
- 约定优先。 物体朝向+Z,基座在y=0,单位为米,相机:
target + dist * (sin(yaw)cos(pitch), sin(pitch), cos(yaw)cos(pitch)),yaw/pitch以度为单位。每个脚本和影片共享此约定。 - 纯状态。 场景API必须将相同状态渲染到相同像素。无动画循环,无挂钟时间,无依赖于帧的代码中的
Math.random()。 - 帧 = f(t)。 相机、爆炸、旋转、标签、卡片和声音都只是影片时间的函数;除此之外别无其他。
- 合成约定。 根元素上应有
data-composition-id、data-start、data-duration、data-width、data-height;暂停的时间线注册为window.__timelines["<id>"];在hf-seek内使用e.detail.time进行重绘。 - 剪辑。 具有自身起始和时长的媒体(音频、视频、插入片段)携带
class="clip"、data-start、data-duration;音频额外携带data-track-index。由时间线动画化的DOM无需剪辑属性。 - 可读性。 在1080p下,任何信息性内容不得低于28 px(检测器的类型下限为28 px = 短边的2.6%)。
- 供应商包权重。 three.js和GSAP以
.min.js格式放在assets/vendor/下;使用本地字体;渲染期间无网络请求。 - 脚本通用。
scripts/中无物体名称、无绝对路径、无个人目录;每个脚本必须能从全新仓库运行。 - 验证命令。 写入文档、脚本或影片的每个命令和参数必须在信赖前针对固定版本执行一次。
参考路由
| 任务 | 阅读 |
|---|---|
| 测量、比例、车削轮廓、格栅/叶片参数、零件、爆炸、锚点 | references/01-modeling.md |
| 合成约定、导演/故事板、音频同步、HyperFrames命令和输出 | references/02-render.md |
| 截图循环、对比图、角度读取、圆形拟合失败 | references/03-verify.md |
| gif-master操作卡、致谢、ffmpeg GIF配方 | references/04-gif.md |
| 仓库、GitHub Release、社区列表PR、YouTube/X | references/05-publish.md |
| 症状→原因→修复目录(光照、阴影、肋条、类型、包、Chrome) | references/06-gotchas.md |
此技能附带的工具:
| 文件 | 作用 |
|---|---|
scripts/view.html | 单帧查看器;从查询参数获取姿态和包 |
scripts/verify_sheet.py | 照片与渲染并排对比图(--photo --render --out --bbox) |
scripts/hf_read_check.py | 汇总HyperFrames检查报告;--fail-on-error用于CI |
scripts/make_gif.md | 浏览器/ffmpeg GIF操作卡 |
最小可运行序列
命令从仓库根目录针对examples/fan/中的示例运行;对于新物体,替换src/scene.js、photos/和video/。工作文件放入%TEMP%,保持仓库清洁。保留参数——它们是已验证的。在PowerShell中运行。
$work = Join-Path $env:TEMP "mbc-verify"; New-Item -ItemType Directory -Force $work | Out-Null
# 1. 打包场景(新物体的模板;需要安装`three`)。
# 示例已提供其包:examples/fan/video/assets/vendor/fan.bundle.min.js,
# 因此对于示例可跳过此步骤。
npx --yes [email protected] src/scene.js --bundle --format=iife --global-name=Scene `
--minify --target=chrome120 --outfile=dist/scene.bundle.min.js
# 2. 截图示例的一个验证姿态(1920x1080,每次运行使用新的配置文件)
$chrome = "C:\Program Files\Google\Chrome\Application\chrome.exe"
$png = Join-Path $work "front.png"
$udd = Join-Path $env:TEMP ("view-" + [guid]::NewGuid().ToString("N"))
& $chrome --headless=new --use-angle=d3d11 --window-size=1920,1080 `
--hide-scrollbars --virtual-time-budget=12000 `
--screenshot=$png --user-data-dir=$udd `
"file:///$(($PWD.Path).Replace('\','/'))/scripts/view.html?bundle=../examples/fan/video/assets/vendor/fan.bundle.min.js&yaw=-10&pitch=12&dist=0.85&ty=0.185&fov=30&noshadow=1"
# 3. 将该渲染与相同姿态的前面照片进行对比
python scripts/verify_sheet.py --photo examples/fan/reference/fan.jpg --render $png `
--out (Join-Path $work "front-sheet.png") --bbox "130,245,1195,1455"
# 4. 检查影片是否符合HyperFrames约定,然后阅读报告
Set-Location examples/fan/video
# 需要 assets/audio/mix.wav(Release资源;见 examples/fan/README.md)
npx --yes [email protected] check --at 21.15,27.6,35.18
$report = Get-ChildItem .hyperframes/artifacts/checks/*.json |
Sort-Object LastWriteTime | Select-Object -Last 1
python ../../../scripts/hf_read_check.py $report.FullName --fail-on-error
npx --yes [email protected] snapshot --at 29.6
# 5. 渲染交付母版
npx --yes [email protected] render --quality delivery
Set-Location ../../..
# 6. 用ffmpeg剪辑GIF(浏览器方法见:scripts/make_gif.md)
$mp4 = (Get-ChildItem examples/fan/video/renders/*.mp4 | Sort-Object LastWriteTime | Select-Object -Last 1).FullName
ffmpeg -hide_banner -loglevel error -y `
-ss 29.6 -t 3.8 -i "$mp4" `
-vf "fps=12,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=sierra2_4a" `
-loop 0 (Join-Path $work "teardown.gif")
步骤3打印两个裁剪框;已验证的运行打印saved ...\front-sheet.png 1112x702 | photo (130, 245, 1195, 1455) | render (652, 264, 1252, 1080)。
预期输出:check写入video/.hyperframes/artifacts/checks/<uuid>.json;snapshot写入video/.hyperframes/artifacts/snapshots/<uuid>/frame-00-at-29.6s.png;render写入video/renders/video_<YYYY-MM-DD>_<HH-MM-SS>.mp4(43.5秒@1920×1080/30 fps约36 MB,带有硬件GPU时约2.5分钟捕获时间)。
要求
- Node.js(测试版本22.19.0)并带有
npx;通过npx使用esbuild 0.24.2。 - Python 3.11(测试版本3.11.1)并带有Pillow用于对比图;numpy可选。
- ffmpeg在PATH中(测试版本为2025-12-28的git构建)。
- Chrome用于无头截图(测试版本157.0.8103.40);以上路径是Windows默认路径。
- HyperFrames固定为0.9.36,通过
npx --yes [email protected]使用。 check和render驱动无头Chrome并带有--browser-gpu自动模式:有硬件GPU时使用硬件,否则使用软件(SwiftShader);preview在有GPU时更流畅。
完成标准
- 六张对比图可接受地读出,偏差已列出并解释;
hf_read_check.py --fail-on-error在最终检查报告上返回0;- 交付MP4存在,其时长等于合成的
data-duration; - GIF循环干净且足够小以便发布;
- 该技能中的每个命令已在全新仓库中运行一次。