不建模也能有体积:手工三角化一座浮岛
岛上的岩石不是从建模软件导出的,是五层不规则环带在运行时拼出来的。顶点色加平面着色,就足够让一块几何体看起来像被凿开的石头。
浮岛的岩体没有用任何建模软件,也没有 .glb 文件。它由五层不规则环带在运行时拼出来,整段代码大约四十行。
先把岛想成一叠环
从上往下看,岛是一个圆;从侧面看,它是一叠越来越小、越来越低的环。所以先定五层:
const rings = [
[radius * 0.94, 0.45],
[radius, -0.05],
[radius * 0.88, -depth * 0.28],
[radius * 0.64, -depth * 0.66],
[radius * 0.25, -depth],
];
每一层再切成 19 段,每段的角度是固定的,半径和高度各加一点随机抖动。抖动的幅度很小(半径 ±6%,高度 ±0.13),但足够让轮廓不再是一个规整的圆 —— 规整就是「一眼看出是程序生成」的原因。
手工三角化,只有两层循环
相邻两层、相邻两段之间,就是一个四边形;四边形拆成两个三角形。没有索引缓冲,顶点直接往数组里推:
for (let j = 0; j < rings.length - 1; j += 1) {
for (let i = 0; i < segments; i += 1) {
const a = ringPoints[j][i];
const b = ringPoints[j][(i + 1) % segments];
const c = ringPoints[j + 1][i];
const d = ringPoints[j + 1][(i + 1) % segments];
triangle(a, b, c, shades[Math.floor(random(0, shades.length))]);
triangle(b, d, c, shades[Math.floor(random(0, shades.length))]);
}
}
两个三角形各自随机取一档明暗。这一步是整段代码里最划算的:顶点色加平面着色让每一面的法线独立,颜色又随机,看上去就像岩石被凿开后的断面,而不是一个光滑的曲面。
别忘了底盖
最后一层环不能就这么敞着 —— 从下往上看会是一个空洞。所以再补一圈三角形,全部收进一个中心点:
triangle(last[i], last[(i + 1) % segments], vec(0, -depth * 1.12, 0), '#344b56');
那个中心点比最后一层还要再低一点,于是岛底是一个尖的锥体,和「浮空岛」的直觉一致。
草皮只是两层圆柱
岩体之上不需要再做地形。两层扁圆柱就够了:下面一层是草绿,半径略小于岩体顶面;上面一层浅一点、更薄,形成台地边缘。边缘那圈颜色差比任何细节都更能说明「这里是可以站人的」。
灌木、苔石、花头则完全交给实例化,它们只负责打破圆柱的规整感。
碎岛复用同一个函数
主岛之外的三个漂浮碎岛,用的是同一个 makeRock(),只是半径从 4.65 变成 0.48 到 0.77。因为它们共享同一套随机抖动,看起来像同一种地质 —— 这不是巧合,是复用带来的免费一致性。
随机数要固定种子
所有 random() 都来自一个种子为 413 的线性同余发生器。好处有两个:构建出来的岛每次一样,用户可以分享一张「我看到的岛」;调试时也不会因为刷新而换一座山。
收尾
程序化几何的难点不在数学,而在取舍:哪些细节值得用顶点表达(岩层、明暗),哪些用实例化(植被),哪些干脆用两层圆柱糊过去(草皮)。想清楚这三档,四十行就够一座岛。

评论
…