关于 3d-toy-character
3D Toy Character 是用于制作软圆润玩具角色短视频的视觉风格技能,面向需要以角色演绎创意的制作者。它提供从分镜、动作到 Three.js 等制作路线的指导。
主要功能
- 01玩具质感角色造型规范
- 02分镜与文字可读性规划
- 03因果式角色动作设计
- 04Three.js、SVG 或 Blender 路线
- 05镜头、布景与光照指导
适用场景
- 01制作竖屏玩具角色短片
- 02让品牌角色演绎单一创意
- 03规划角色动作与视频节拍
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ phantomtonyt/ motion-design-skills --skill 3d-toy-character安装后在 Claude Code 中输入/3d-toy-character确认已加载。
详细介绍
3D 玩具角色
制作短视频,让一个柔和圆润、玩具般的 3D 角色演绎一个想法。整体应有触感、友好且略带俏皮感,就像在摄影棚灯光下的高端乙烯基玩具。这是一项视觉风格技能,而非通用 3D 建模技能。
选项
| 选项 | 默认值 | 备注 |
|---|---|---|
| format | 竖屏 1080×1920 | 或横屏 1920×1080、方形 1080×1080 |
| duration | 15 s | 8–30 s 效果最佳 |
| tone | playful | calm(缓和的缓动、更长的停留)、playful(默认)、hyper(更明显的挤压、更快的节奏、更多镜头移动) |
| character | 圆形玩具角色 | 或用户的品牌角色(匹配其轮廓和颜色) |
简述(规划前回答)
- 角色想要什么,或者核心信息是什么?
- 第一秒的钩子是什么(姿势加标题)?
- 出了什么差错或有什么意外,最终回报是什么?
- 视频以哪句台词收尾?
然后编写节拍列表(时间、画面内容、角色动作、文字、声音),各段时长相加应等于目标时长。
阅读时间表(与节拍列表一起编写)
为每个节拍写明观众必须理解的一件事(“机器人不会飞”“缓存未命中”),以及这项信息保持清晰可读的时长。一次只传达一项信息。文字时长底线:标签至少停留 0.8 s;句子完整显示后,每个词约需 0.3 s。如果文字无法在给定时间内读完,就删减文字,而不是加快速度。
动作规范
- 动作应有因果关系:角色会对画面中的事物作出反应,每个动作都有起因和结果。
- 角色不能完全静止:等待时要呼吸、眨眼、改变重心。这些待机动作本身就是表演,而非填充内容,因此要轻微且不规则。
- 避免动作同步:手臂不要像镜像一样移动,眼睛、触角和身体也不要在同一帧开始动作。各部分之间错开 2–4 帧。
- 过冲和挤压应体现在角色身上(落地、弹出、惊讶)。镜头和文字则应使用平滑的缓出。
- 每个场景只保留一个主要移动方向,并在切换场景时让角色或道具延续移动,不要交叉淡化。
- 在最终回报出现前,加入 0.3–0.75 s 的定格姿势,让效果充分呈现。
风格规则(始终遵守)
这些规则定义了此风格;即使更改颜色或故事,也要保留。
- 头部占总高度的 55–70%,因为大头会显得可爱且像玩具。
- 所有形体都应圆润,不带尖锐边缘,让轮廓呈现为几团柔和的形体。
- 材质为光滑乙烯基:粗糙度 0.35–0.5、轻微清漆层、金属度为 0。金属质感或光泽感会让画面像产品渲染图。
- 角色下方始终要有柔和的接触阴影,这能让角色看起来是 3D 的。
- 使用大型柔和的主光、较弱的补光和一条纤细的轮廓光。不要使用硬阴影。
- 每个动作都要经历预备、移动、过冲、稳定几个阶段,这样才有重量感。
默认值(可自由更改)
颜色、角色设计、背景、镜头角度、时长和文字。用户提供品牌配色或角色时,以用户提供的为准。优先顺序:用户的品牌或规格,其次是明确请求,最后是此文件中的默认值。
设置(一次性)
node -v # needs 22+
ffmpeg -version # needs any recent build
npx hyperframes init my-video --non-interactive --resolution portrait && cd my-video # landscape for 16:9
在现有的空文件夹中使用 npx hyperframes init . --non-interactive --resolution portrait。然后用下面的模板替换生成的 index.html。Three.js 和字体会在合成内容中从 CDN 加载(渲染时可使用 Google Fonts <link>);无需安装其他内容。Blender 仅用于离线流程。
构建前先确定
在编写代码前,先确定以下四项,并用一行告诉用户。
- 画幅比例: Reels、Shorts 和 TikTok 用 9:16(1080×1920);YouTube、网页和幻灯片用 16:9(1920×1080)。默认使用 9:16。
- 方案: 参见表格。默认使用 Three.js。
- 角色: 剪影、主色,配饰最多一种(天线、耳朵、围巾)。如果用户有品牌角色,请匹配其剪影和颜色。
- 节拍: 15 秒设置 3–6 个节拍。每个节拍只安排一个动作。
| 需求 | SVG + GSAP | Three.js(默认) | Blender |
|---|---|---|---|
| 简单的吉祥物反应 | 可以 | 可以 | 可以 |
| 真实的转身或纵深旋转 | 不可以 | 可以 | 可以 |
| 柔和光照和接触阴影 | 可模拟 | 可以 | 可以 |
| 光泽或清漆材质 | 可模拟 | 可以 | 可以 |
| 快速迭代、随处渲染 | 可以 | 可以 | 较慢 |
| 电影级效果、复杂几何体 | 不可以 | 部分支持 | 可以 |
选择能实现用户所需外观的最简单方案。如果角色从不转身,使用带渐变的 SVG 即可。如果角色会转身,或光照效果是重点,则使用 Three.js。
视觉语言
比例
- 头部:占总高度的 55–70%。头部可以就是整个身体。
- 眼睛:大而简洁,位置偏低,约在脸部高度的 55–60% 处。每只眼睛加一个大的白色高光和一个小的白色高光。
- 四肢:短、粗、圆润;手做成连指手套状;不显示关节。
- 避免尖锐边缘。所有边缘都做倒角或圆滑处理,让剪影呈现为几个圆润的形体。
- 面部部件应贴在头部表面,而不是嵌入其中。对于半径为 r、深度缩放系数为 sz 的球形头部,将眼睛、嘴巴和面罩放在 z ≈ r·sz 再加上部件自身深度的位置。放大查看快照,确认没有部件被埋入头部。
材质
- 使用光滑的乙烯基或塑料材质,粗糙度约为 0.35–0.5,并添加轻微清漆效果(约 0.3)。除非用户提出要求,否则不加纹理。
- 切勿使用金属材质。金属度保持为 0。
光照
- 从左上方打入一盏大型柔光主光;从另一侧补一盏较弱的补光(约为主光的 30%);从后方打入一盏细窄的轮廓光(约为主光的 60%)。
- 在角色下方添加柔和的接触阴影。这一细节能让角色呈现出 3D 效果。
- 避免硬边点光源阴影、戏剧化的低调光照、强烈泛光和恐怖风格的彩色灯光。
默认调色板(仅为默认值,并非固定风格):
- 背景:浅色、略偏冷的柔和色调(例如
#e8f0f7),或柔和的双色渐变。 - 角色:一种饱和但柔和的主色,并搭配更浅的腹部或脸部色块。
- 点缀色:一种用于道具和 UI 的对比色系柔和色调。
- 阴影:向背景色偏移,绝不使用纯黑。
动画语言
动作应柔和、略微夸张,并符合物理规律。每个动作都遵循预备 → 主动作 → 超调 → 稳定的过程。
| 动作 | 时长 | 说明 |
|---|---|---|
| 微小反应(眨眼、瞥视) | 0.2–0.5 s | 每隔 2–4 s 眨一次眼,间隔不规则 |
| 手势(挥手、拍手、指点) | 0.5–1.2 s | 手臂先动,身体晚 2–3 帧跟上;对于大头角色,将抬起的手臂向前倾斜(rotation.x ≈ 0.8),让手从头部前方经过;挥手时让手臂避开道具 |
| 跳跃 | 0.5–0.8 s | 挤压 0.08 s,然后上升、下落、落地时挤压,最后稳定 |
| 入场 | 0.8–1.5 s | 缩放弹入(0 → 1.1 → 0.95 → 1),以挤压动作结束;只有在可见的地面或画面边缘先遮住角色时,“从下方升起”的效果才明显 |
| 歪头(好奇) | 0.3–0.4 s | 倾斜 6–10°,保持,然后复位 |
| 待机 | 持续 | 按 2–3 s 的周期呼吸式缩放 ±2% |
- 保持体积感:身体纵向拉伸 10% 时,宽度缩小约 5%。
- 接触阴影要随之变化:角色上升时阴影缩小、变淡,落地时恢复完整。
- 不要让所有元素都弹跳。只在入场和落地时使用超调,并让待机动作保持平静。
构图
- 9:16: 角色位于中间三分之一处,面部位于 y 500 到 1100 之间。文字和面部应位于 x 60–960、y 270–1250 范围内。底部约 670 px 会被平台 UI 遮挡,因此字幕应放在其上方。
- 16:9: 角色位于一侧三分之一处,文字或 UI 占据另外三分之二;或者让角色居中,文字放在下方。保留 5% 的边距。
- 摄像机:略高于视线水平,视场角为 30–40°,不要使用极端广角。
- 取景:视场角为 32° 时,可见高度约为摄像机距离的 0.57 倍。对于一个约 3.3 个单位高的角色,摄像机 z ≈ 17–19 时,角色在 9:16 画面中约高 700 px。道具也在画面中时,将摄像机目标横向偏移约 0.45 个单位,并让角色的脚保持在 y ≈ 1300 以上。
文字
- 使用粗体圆角无衬线字体(例如来自 Google Fonts 的 Nunito 800),9:16 画面中的标题字号为 64–90 px;多行字幕的行高至少为 1.15。
- 文字对比度至少要达到 3:1(
hyperframes check会强制检查)。如果品牌色在品牌背景上的对比度不达标,就将其用于形状、填充和角色,并在文字中使用更深的同色调,或近黑色。 - 在时间线外使用
gsap.set(...)设置文字的隐藏初始状态,然后在时间线上将其动画显示。(在时间 0 调用tl.set会触发 lint 警告。)
打造吸睛效果
空背景上的角色看起来像测试渲染,而不是视频。每个输出都必须包含以下元素:
- 布景,而非空无一物。 添加天空渐变(置于透明画布后方的 CSS),再加上一个使用相同乙烯基材质的简单布景元素(屋顶边缘、书桌、浮岛),以及几个用于体现比例的道具。
- 三个景深层: 远景(云朵、山丘、雾气)、中景(布景)、近景(道具或从摄像机前掠过的粒子)。摄像机移动时,每层以各自的速度移动。
- 移动的摄像机。 在开场吸引点推进,跟随动作,并在高潮处拉远。使用平滑关键帧设置动画;角色离开布景时,让摄像机继续跟随:
const smooth = (x) => x * x * (3 - 2 * x), mix = (a, b, x) => a + (b - a) * x;
const keys = (t, ks) => { if (t <= ks[0][0]) return ks[0][1];
for (let i = 1; i < ks.length; i++) if (t <= ks[i][0]) return mix(ks[i - 1][1], ks[i][1], smooth(Math.min(1, (t - ks[i - 1][0]) / (ks[i][0] - ks[i - 1][0]))));
return ks[ks.length - 1][1]; };
// camera.position.y = keys(t, [[0, 1.8], [1.6, 2.2], [10, 3.6], [11.4, 9], [13.4, 1.8]]); same for x, z and the look-at target
- 一个发光点缀。 在故事的关键时刻(“顿悟”时刻)加入一个带点缀色的发光灯泡、屏幕或道具,以及一盏小型点光源。
- 每个节拍都要有次要运动: 飘动的云朵、撞击时扬起的尘埃、闪光或星星,以及动作之后晃动的天线和耳朵。
- 精心设计标题出场时刻。 将粗体圆角文字放在带柔和投影的白色圆角卡片上(也能解决天空背景杂乱时的对比度问题),并以超调效果弹入。角色移动经过该区域之前,将卡片移出画面。
- 以循环结尾: 最后的姿势和构图要与开场一致。
- 质量门槛: 如果任何快照看起来像占位内容(纯色背景、角色静止、没有任何运动),就说明尚未完成。
构建:Three.js 路线(默认)
使用一个 HTML 组合。HyperFrames 通过 hf-seek 事件告知 Three.js 要绘制的时间,因此每一帧都根据时间计算,不会自行运行时钟。
<!doctype html><html><head><meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<style>html,body{margin:0;width:1080px;height:1920px;overflow:hidden;background:#e8f0f7}
#root{width:100%;height:100%;position:relative}canvas{position:absolute;inset:0;width:100%;height:100%}</style></head>
<body><div id="root" data-composition-id="main" data-start="0" data-duration="15" data-width="1080" data-height="1920">
<canvas id="three-layer"></canvas>
<div id="caption" style="position:absolute;left:60px;right:120px;top:300px;text-align:center;font:800 76px/1.1 system-ui,sans-serif;color:#1f2a30">Hello, I'm a toy!</div></div>
<script>
// HyperFrames needs one registered paused timeline; put text/UI tweens on it, 3D is driven by hf-seek below.
const tl = gsap.timeline({ paused: true });
tl.fromTo("#caption", { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.4, ease: "back.out(2)" }, 1.2);
tl.to("#caption", { opacity: 0, duration: 0.3 }, 12); tl.set({}, {}, 15);
window.__timelines["main"] = tl;
</script>
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
const W = 1080, H = 1920; // 16:9: 1920, 1080 and move the camera back less
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("three-layer"), antialias: true, alpha: true });
renderer.setSize(W, H, false); renderer.setPixelRatio(1);
renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(32, W / H, 0.1, 100); camera.position.set(0, 2.4, 16); camera.lookAt(0, 1.3, 0);
const vinyl = (hex, rough = 0.42) => new THREE.MeshPhysicalMaterial({ color: hex, roughness: rough, clearcoat: 0.3, clearcoatRoughness: 0.4 });
const ball = (r, mat, x, y, z, sx = 1, sy = 1, sz = 1) => { const m = new THREE.Mesh(new THREE.SphereGeometry(r, 48, 32), mat); m.position.set(x, y, z); m.scale.set(sx, sy, sz); m.castShadow = true; return m; };
const C = { body: 0x35b5a7, belly: 0xc9f2ea, cheek: 0xffb4a2, eye: 0x1f2a30 }; // defaults; replace with brand colours
const hero = new THREE.Group(); scene.add(hero);
const bodyG = new THREE.Group(); hero.add(bodyG);
bodyG.add(ball(1.25, vinyl(C.body), 0, 1.2, 0, 1, 0.92, 0.95)); // head = body
bodyG.add(ball(0.55, vinyl(C.belly, 0.5), 0, 0.62, 0.86, 1, 0.55, 0.35)); // belly patch
const eyes = [-0.4, 0.4].map((x) => { // big, low eyes with two highlights
const g = new THREE.Group(); g.position.set(x, 1.12, 1.08);
g.add(ball(0.2, vinyl(C.eye, 0.2), 0, 0, 0, 1, 1.15, 0.55));
g.add(ball(0.06, new THREE.MeshBasicMaterial({ color: 0xffffff }), -0.07, 0.09, 0.1));
g.add(ball(0.03, new THREE.MeshBasicMaterial({ color: 0xffffff }), 0.07, -0.08, 0.1));
bodyG.add(g); return g;
});
[-0.78, 0.78].forEach((x) => bodyG.add(ball(0.17, vinyl(C.cheek, 0.6), x, 0.9, 0.93, 1, 0.55, 0.4))); // cheeks
const mouth = new THREE.Mesh(new THREE.TorusGeometry(0.1, 0.03, 12, 24, Math.PI), vinyl(C.eye));
mouth.position.set(0, 0.92, 1.17); mouth.rotation.z = Math.PI; bodyG.add(mouth);
const arm = (side) => { const p = new THREE.Group(); p.position.set(side * 1.15, 0.95, 0.15); // pivot at the shoulder
const m = new THREE.Mesh(new THREE.CapsuleGeometry(0.2, 0.32, 8, 16), vinyl(C.body)); m.position.y = -0.3; m.castShadow = true;
p.add(m); p.rotation.z = side * 0.45; bodyG.add(p); return p; };
const armL = arm(-1), armR = arm(1);
[-0.45, 0.45].forEach((x) => hero.add(ball(0.3, vinyl(C.body), x, 0.12, 0.25, 1, 0.55, 1.2))); // stubby feet
const floor = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), new THREE.ShadowMaterial({ color: 0x5a6b80, opacity: 0.14 })); // tinted, never black
floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);
// contact shadow that reacts to height (the key-light shadow alone doesn't shrink when airborne)
const blobTex = (() => { const c = document.createElement("canvas"); c.width = c.height = 128; const g = c.getContext("2d");
const r = g.createRadialGradient(64, 64, 0, 64, 64, 64); r.addColorStop(0, "rgba(60,72,90,0.55)"); r.addColorStop(1, "rgba(60,72,90,0)");
g.fillStyle = r; g.fillRect(0, 0, 128, 128); return new THREE.CanvasTexture(c); })();
const blob = new THREE.Mesh(new THREE.PlaneGeometry(3.2, 1.4), new THREE.MeshBasicMaterial({ map: blobTex, transparent: true, depthWrite: false }));
blob.rotation.x = -Math.PI / 2; blob.position.y = 0.01; scene.add(blob);
const key = new THREE.DirectionalLight(0xffffff, 2.3); key.position.set(-3, 9, 5); key.castShadow = true; // large soft key, upper left
key.shadow.mapSize.set(2048, 2048); Object.assign(key.shadow.camera, { left: -6, right: 6, top: 6, bottom: -6 }); scene.add(key);
const fill = new THREE.DirectionalLight(0xdfe9ff, 0.7); fill.position.set(5, 2, 4); scene.add(fill);
const rim = new THREE.DirectionalLight(0xffffff, 1.4); rim.position.set(0, 4, -6); scene.add(rim);
scene.add(new THREE.HemisphereLight(0xffffff, 0xcfd8e3, 0.6));
// deterministic helpers: every value comes from t (no Math.random, no Date.now)
const clamp = (x) => Math.min(1, Math.max(0, x));
const seg = (t, a, b) => clamp((t - a) / (b - a));
const backOut = (x, s = 1.7) => 1 + (s + 1) * (x - 1) ** 3 + s * (x - 1) ** 2;
const pulse = (t, a, b) => Math.sin(Math.PI * seg(t, a, b));
function renderAt(t) {
const enter = backOut(seg(t, 0.2, 0.9), 2.2); // scale pop with overshoot
const hop = pulse(t, 4.0, 4.6), land = pulse(t, 4.6, 4.8), antic = pulse(t, 3.85, 4.0);
const y = 1.1 * hop;
hero.position.y = y;
const squash = 0.1 * (land + antic + pulse(t, 0.85, 1.05));
hero.scale.set(enter * (1 + 0.8 * squash - 0.05 * hop), enter * (1 - squash + 0.08 * hop), enter); // keep volume
blob.scale.setScalar(enter / (1 + 0.55 * y)); blob.material.opacity = 0.35 + 0.65 / (1 + 0.55 * y); // shadow reacts
bodyG.rotation.z = 0.12 * pulse(t, 2.0, 2.8); // curious tilt
const blink = (a) => Math.max(0, seg(t, a, a + 0.07) - seg(t, a + 0.07, a + 0.14));
const b = Math.max(blink(3.1), blink(6.0), blink(11.4)); // irregular blinks
eyes.forEach((e) => (e.scale.y = 1 - 0.9 * b));
const raised = seg(t, 8.0, 8.2) - seg(t, 9.6, 9.8);
armR.rotation.z = 0.45 + 1.6 * raised + 0.35 * Math.sin(Math.PI * 4 * seg(t, 8.2, 9.6)); // wave
armR.rotation.x = 0.8 * raised; // hand passes in front of the head
armL.rotation.z = -0.45 - 0.1 * hop; // follow-through
bodyG.scale.y = 1 + 0.02 * Math.sin(t * 2.6); // breathing
renderer.render(scene, camera);
}
window.addEventListener("hf-seek", (e) => renderAt(e.detail.time)); // HyperFrames sends the frame time on every seek
renderAt(window.__hfThreeTime || 0); // and stores the current time here for the first draw
</script></body></html>
将文字和 UI 作为 HTML 或 SVG 叠加在画布上,并在已注册的 tl 上为其添加动画(即使所有运动都在 Three.js 中实现,HyperFrames 仍要求提供该时间线)。
此路线的规则:
- 根元素上必须设置
data-duration。Three.js 场景本身没有时长。 - 绝不能使用
requestAnimationFrame、setAnimationLoop、Date.now或Math.random来制作动画。每个值都必须根据t计算。 - 在首次渲染前加载所有模型或纹理。带动画片段的 GLTF 角色应使用
mixer.setTime(t)定位到指定时间。
构建:其他方案
- SVG + GSAP(无景深): 将角色绘制为带径向渐变填充的 SVG 椭圆(左上较亮,右下较暗),在头部添加白色椭圆形高光,用模糊椭圆作为接触阴影,并使用与上文相同的时序和 GSAP 缓动(入场使用
back.out(1.7),跳跃上升使用power2.out,下降使用power2.in)。在window.__timelines["main"]上注册一个暂停的时间轴。 - Blender(离线高品质): 使用细分曲面和倒角建模,采用粗糙度为 0.4、清漆层为 0.3 的 Principled BSDF,设置三点区域灯光和阴影捕捉地面,并按目标纵横比渲染 PNG 序列或 MP4。将其作为
<video>片段导入 HyperFrames,以添加文字、字幕和声音。
向用户开放的参数
aspect · duration · characterColor · accentColor · background · accessory · beats(时间 + 动作列表)· cameraAngle · captionText。将它们作为常量放在文件顶部,这样修改时只需改一行。
避免
- 写实的人体结构、小脑袋、细四肢、细致的鼻子或手指。
- 金属、铬或玻璃质感。它们看起来像产品渲染,而不是玩具。
- 纯黑阴影或轮廓。
- 持续弹跳、线性缓动,或所有部位步调一致地运动。
- 在脸上叠加文字,或在 9:16 画面底部 670 px 范围内放置重要内容。
- 在屏幕文字或标题中提及工作室或系列名称。改为描述视觉风格。
故障模式
| 观感 | 原因 | 修复方法 |
|---|---|---|
| 扁平矢量插画 | 没有接触阴影、主光太弱、材质扁平 | 添加柔和的地面阴影,增强主光,添加清漆层和轮廓光 |
| 写实的 3D 人物 | 头太小、面部细节过多、四肢太长 | 头部占身高的 60% 以上,面部简化为眼睛和嘴巴,缩短四肢 |
| 塑料产品渲染 | 金属度大于 0、边缘锐利、光泽过强 | 金属度设为 0,粗糙度约为 0.4,将所有部分做成圆润造型 |
| 动作像机器人 | 线性插值、没有预备动作、时序相同 | 添加预备动作、缓动曲线和过冲,并将手臂错开 2–3 帧 |
| 失重感 | 落地时没有挤压变形、阴影没有响应 | 接触时挤压变形,腾空时缩小并淡化阴影,添加稳定动作 |
| 色彩浑浊 | 背景太暗、阴影发灰 | 使用柔和的背景色,让阴影带有背景色调,并选用一种饱和的主色 |
| 渲染抖动 | 动画读取墙上时钟时间 | 仅在 renderAt 中根据 t 计算 |
最终渲染前进行验证
npx hyperframes check # lint, layout, contrast, timing
npx hyperframes snapshot --at <one time per beat at the peak of each action, plus one mid-transition>
npx hyperframes render -o renders/<name>.mp4
ffprobe -v error -show_entries stream=width,height,r_frame_rate:format=duration -of csv=p=0 renders/<name>.mp4
查看每一张快照以及 snapshots/contact-sheet.jpg(包含所有帧的单张图片),并确认:
- 每个节拍中的轮廓都清晰可辨,面部都可见
- 存在接触阴影,并会随跳跃产生变化
- 光照一致(左上方有主光,边缘可见轮廓光)
- 每个动作都有预备和收尾;没有漂浮感
- 文字位于所选纵横比对应的安全区域内
- 输出尺寸符合纵横比(1080×1920 或 1920×1080),时长正确
最终渲染前进行自我评估
对快照总览图按以下项目打分,范围为 1–10:吸引力(第一秒)、手机屏幕宽度下的可读性、动作质量、构图、准确性,以及“静音测试”(观众能否在没有声音的情况下用一句话概括内容?)。只修复得分为 7 分或以下的项目,然后重新生成快照并重复评估,直到所有项目都达到 8 分或以上。如果可以,请让另一个只读评估流程负责打分,以免自己给自己的作品打分。
| 不要 | 应该 |
|---|---|
| 在两个内容繁杂的布局之间交叉淡化 | 先移除旧内容,再引入新内容,或让一个物体贯穿整个过程 |
| 同时移动多个元素 | 错开动作,每个时刻只突出一个重点 |
| 用漂浮或摇摆填补停顿 | 保持静止,或让角色做出反应 |
| 为了表现“活力”添加粒子、彩虹渐变或 3D 翻转 | 加入更有力的事件:揭示、撞击或镜头移动 |
| 为了赶上时长而急着放入文字 | 删减文字 |
交付内容
- 海报帧:选取最稳妥的一帧,并将其烘焙为第 0 帧,这样缩略图就会显示它:
ffmpeg -ss <t> -i renders/<name>.mp4 -frames:v 1 poster.jpg,然后运行ffmpeg -i renders/<name>.mp4 -i poster.jpg -filter_complex "[0:v][1:v]overlay=enable='eq(n,0)'" -c:a copy renders/<name>-final.mp4。 - 一段 1–2 句、可直接发布的说明文字,点明创意,而非技法。
- 告诉用户每个节拍发生了什么,以及修改时应调整哪些参数。
参考这些作品
- @scheemunai,用 Three.js 制作的 90 年代卡通风格短片,采用挤压与拉伸的表演效果:https://x.com/scheemunai/status/2102788223835463902
- @JaydenDavisNC,在 Blender 中制作的绑定角色短片,采用影棚布光:https://x.com/JaydenDavisNC/status/2107086714820870617
- @angrypenguinPNG,根据一个提示词制作的黏土动画风格桌面场景(Blender,技能已开源):https://x.com/angrypenguinPNG/status/2103205636662341661
- @Stefan_3D_AI,带有跳跃和撕咬循环的生物绑定(预备动作与超调):https://x.com/Stefan_3D_AI/status/2102641562824135022
- 改编(而非照搬)自 HyperFrames 动态设计原则和无露脸解说技能、JohnHeibel/ClaudeAnimationBase 的动画指南、howseen-ai/claude-motion-design 的评审循环,以及 latent-spaces/brag 中的创意。