形状工作台 · 给引擎加一个新形状
想加一个新形状,
该动哪几行?
形状系统有 5 个注册点,其中 4 个是机械的。 真正不能猜的只有一件事:你的形状有没有凹口 —— 因为它直接决定必须用哪种 silhouette,而选错了凸包会把你的凹口静默吃掉。
STEP 0
先过决策树:三个问题定 silhouette
别先写代码。先回答下面三个问题 —— 它会告诉你 silhouette.kind 填什么。
solid —— 它的轮廓算法是凸包(pi()),
在数学上不可能产生凹形,你的凹口会被直接填平。继续看 Q2。
solid 最省事,也是最常见的一类。
{ kind:"solid", samples: Jt(mt("名")), balls:[], isLoop:false }
extrusion:把轮廓沿 ±z 挤出,边缘用 bevel 收圆。{ kind:"extrusion", rings: Nv(samples, halfDepth, bevel) }
loft:轮廓在赤道,向上向下各收成一个点。{ kind:"loft", rings: zv(samples, depth) }
balls,轮廓由射线和圆求交算出。{ kind:"solid", samples:[], balls: jl(名,"x"), isLoop:false }
WORKBENCH
工作台:拖顶点 → 拿到代码
拖动白点改形状,拖动形状本体不动。点一个顶点可以单独调它自己的圆角 ——
引擎的 ac() 支持逐顶点圆角数组,这是画齿轮 / 花瓣类形状的关键技巧。
RECIPE
五步配方
写路径生成器,注册进 Mv
只吃 2~3 个数字。用 4 个图元拼:eh(正多边形)、ac(圆角多边形)、
ar(极坐标函数)、th(圆并集)。
坐标以设计画布中心 x = 114.2705 为原点,y 轴向下为正。
moon: () => ac([[x-80,x+60], [x+40,x+90], …], 18), // 规则多边形一行搞定: moon: () => eh(114, 6, 20, Math.PI/6),
把它变成点列:mt() + Jt()
路径字符串先被 is() 采样成点,再由 Jt() 归一化成 3D 点(打平 z=0)。
这一步不用你写,但要知道点是采样出来的,所以路径本身决定最终精度。
mt("moon") // = is(Kr("moon").path, 4) → 每 4 单位采一个点
Jt(mt("moon")) // → [{x:(px-x)/x, y:-(py-x)/x, z:0}, …]
写描述符,注册进 Iv —— 这里选 silhouette
按 STEP 0 的决策树填。同时必须定义 seat,否则眼睛会飘在形状外面。
// 凸 → 最省事
moon: () => qn(1.17, "moon") // seat 自动 = wrap
// 凹 + 有厚度
moon: () => oh(1.17, Jt(mt("moon")), .36, .22)
// seat = { kind:"front", faceZ:.36 }
// 凹 + 鼓起来
moon: () => zl(1.17, Jt(mt("moon")), .62)
// seat = { kind:"loft", surfaceZ: Dv(rings) }
// 球链
moon: () => ({ size:1.17,
silhouette:{ kind:"solid", samples:[], balls: jl("moon","x"), isLoop:false },
seat:{ kind:"wrap", wrapR:…, wrapY0:…, wrapY1:…, radiusAt:… } })
(可选)注册 3D 实体 iS[name]
只有开 isProof 走软件光照时才用得到 —— 正常 SVG 渲染完全不需要。
但不填的话那套 3D 着色器就渲不出你的形状。
moon: { kind: "revolve" }, // 旋转体
// 可选:revolve / loft / extrusion / roundedSlab / pill / inscribedBalls / crystal
注册数据 + UI
三处:shapes.names、shapes.eyeMetrics,
以及 UI 的形状清单与文案。
// grok-recipes.json
"names": [ …, "moon" ],
"eyeMetrics": { "moon": { "width":.21, "height":.46, "gap":.47, "shiftY":.12 } }
// bundle 里的 UI 清单(Hw)与别名(wN)
Hw = [ …, "moon" ];
function wN(e) { switch (e) { /* blob→sphere, flower→starFlower, … */ } }
CHECKLIST
注册点清单
| 位置 | 必填 | 做什么 | 不填会怎样 |
|---|---|---|---|
| Mv[name] | 必填 | 路径生成器,返回 SVG path 字符串 | 直接报错 |
| Iv[name] | 必填 | 描述符 {size, silhouette, seat} | 直接报错 |
| Iv[name].seat | 必填 | 眼睛落座方式(front / wrap / wrapX / loft) | 眼睛飘在体外或不动 |
| shapes.names | 必填 | 白名单 —— bodyAlias() 不在名单里就回退成 sphere |
静默变成圆球 |
| shapes.eyeMetrics | 建议 | 眼睛的 width / height / gap / shiftY,逐形状微调 | 用默认值,眼睛比例不对 |
| iS[name] | 可选 | 3D 实体 kind,只给 isProof 光照用 |
开 proof 时渲不出来 |
| Hw / wN / 文案 | UI 需要 | 选形状面板的清单、UI 名到引擎名的别名、三语文案 | 用户选不到 / 名字对不上 |
shapes.names 漏了。
bodyAlias() 是这样的:shapeNames.includes(t) ? t : "sphere" ——
名字不在名单里不报错,静默回退成球。你会以为几何写错了,其实是名单没加。
GOTCHAS
几个会浪费你半天的坑
- y 轴方向。
Mv里 y 向下为正(x+t是「中心下方 t」), 但Jt()会翻转成数学坐标y: -(py-x)/x。写生成器时按屏幕方向想就对了。 - 不要自己转形状。静止姿态的旋转由引擎用真 3D 旋转矩阵
(
xh(quat)→Yv)加在轮廓上。你只要 author 正姿态。 ac()的圆角是二次贝塞尔,切线长 = 半径。 半径给太大(超过邻边一半)会自交。正多边形圆角建议 ≤ 边长 × 0.4。th()的圆坐标是绝对的(以画布中心x为参照写成x±d),不是相对某个圆心的。- 凹形状 +
solid= 凹口消失。不报错,只是形状不对。 工作台里的凸包开关就是让你提前看到这个结果。 eyeMetrics从相近形状抄。25 个形状的数值都落在很窄的带里: width 0.16~0.27、height 0.35~0.59、gap 0.36~0.64、shiftY 0~0.18。 主要调shiftY(眼睛的垂直位置),别的先抄邻居。
ar 极坐标公式(超椭圆 El 一个公式就覆盖方、圆角方、
宝石、蛋),凹形状全用 th(圆的并集)—— 两条公式足以覆盖绝大多数有机形状。
本页几何实现逐行对应原 bundle 的
Mv/Iv/Zv/ac/eh/th/Nv/zv/pi,
见 js/lib-shapes.js;生成的代码里坐标是设计画布坐标,可直接粘进 Mv 表。
原理细节见 形状系统解剖。
Grok 与 x.ai 商标归其所有者。本项目仅用于研究与学习。