放樣成形(loft)


如果可以的話,提供切面時讓每個切面的頂點數相同,就可以透過〈掃掠(sweep)〉來建構 3D 物件;然而有時候,或許你想使用不同頂點數的切面,例如,從四個頂點的面變成六個頂點的面,構成漸變的效果。

從 m 個頂點到 n 個頂點之間的變化該怎麼定義呢?不知道!事實就是如此,如果你沒有提供過渡期間頂點的變化過程,資訊就是不充足的,有時使用者的需求就是這麼模糊「就自動幫我漸變」的概念。

那麼就來猜吧!一個方式是,取兩個面頂點數的最小公倍數,然後將重新構造兩個面的頂點數,使它們的數量都是該最小公倍數,這樣就可以掃掠了,因此,先定義出最小公倍數的函式 lcm

function gcd(m, n) {
    return n == 0 ? m : gcd(n, m % n);
}

function lcm(m, n) {
    return m * n / gcd(m, n);
}

如果最小公倍數是 n,那麼對於某個切面,若原頂點數 section.length,那麼可以在原頂點與頂點前,以內插的方式計算出 n / section.length 個頂點:

function interPts(section, n) {
    const vts = section.map(p => createVector(p[0], p[1], p[2]))
    const pts = [];
    for(let i = 0; i < section.length; i++) {
        const p1 = vts[i];
        const p2 = vts[(i + 1) % vts.length];
        for(let j = 0; j < n; j++) {
            pts.push(p5.Vector.lerp(p1, p2, j / n));
        }
    } 
    return pts;
}

使用者可能提供多個切面,不過,先來解決兩個切面的問題:

function loft2(s1, s2) {
    const n = lcm(s1.length, s2.length);
    sweep([
        interPts(s1, n / s1.length).map(p => [p.x, p.y, p.z]),
        interPts(s2, n / s2.length).map(p => [p.x, p.y, p.z])
    ]);
}

然後,每兩個切面為單位,呼叫 loft2 就可以了:

function loft(sections) {
    for(let i = 0; i < sections.length - 1; i++) {
        loft2(sections[i], sections[i + 1]);
    }
}

來看看效果如何:

你也可以進一步將這個範例進行變化,例如,在兩個切面之間加入 slices,在兩個切面間,以內插的方式增加切面,這可以讓 3D 物件細緻一些,以下是個示範,有興趣就自行研究: