掃掠(sweep)


如果在三維空間中有多個面,代表將 3D 物件用刀橫切時得到的平面,例如:

掃掠(sweep)

將這些切面的頂點依序連接起來,就可以重組成 3D 物件:

掃掠(sweep)

為什麼需要這個功能呢?因為經常地,會想在三維空間中放上一些平面輪廓,然後自動生成 3D 物件,這個動作稱為放樣成形(loft)。

在討論放樣成形之前,先來看看其中一個特例,也就是若平面輪廓的頂點數相同的放樣成形,這個特例稱為掃掠(sweep)。

首先,必須定義頂點順序,對於向外的面,若我們看向它,面的頂點順序這邊採逆時針,例如,定義一個在 XY 平面上的圓面,看往 z 負方向時,頂點要是逆時針的話,可以如下:

function circlePoints(r, fn = 96) {
    const aStep = TWO_PI / fn;
    const points = [];
    for(let a = TWO_PI; a > 0; a -= aStep) {
        let {x, y} = p5.Vector.fromAngle(a, r);
        points.push([x, y, 0]);
    }
    return points;
}

如果有很多面代表切面,用一個清單來表示:

const s1 = circlePoints(100, 5);
const sections = [
    s1.map(p => [p[0] * 0.9, p[1] * 0.9, 60]),
    s1.map(p => [p[0] * 0.9, p[1] * 0.9, 30]),
    s1,
    s1.map(p => [p[0] * 0.9, p[1] * 0.9, -30]),
    s1.map(p => [p[0] * 0.8, p[1] * 0.8, -60])
];

第一個面可以直接繪製:

function fstFace(s) {
    beginShape();
    for(let i = s.length - 1; i >= 0; i--) {
        vertex(s[i][0], s[i][1], s[i][2]);
    }
    endShape(CLOSE);
}

最後一個面就要注意了,因為我們要看往 z 正方面,才會是它往外的面,因此頂點順序要反過來:

function lstFace(s) {
    beginShape();
    for(let i = 0; i < s.length; i++) {
        vertex(s[i][0], s[i][1], s[i][2]);
    }
    endShape(CLOSE);
}

至於掃掠時構成的面,可以每兩個面作為單位來處理,因為每個切面頂點數量相同,只要對應好索引就可以了,記得朝外的面要是逆時針:

function twoSections(s1, s2) {
    for(let i = 0; i < s1.length; i++) {
        const ni = (i + 1) % s1.length;
		tri([s1[i], s2[ni], s2[i]]);
		tri([s1[i], s2[ni], s1[ni]]);
    }
}

// 繪製三角面
function tri(points) {
    beginShape();
    points.forEach(p => {
        vertex(p[0], p[1], p[2]);
    });
    endShape(CLOSE);
}

這麼一來,就可以實作出 sweep

function sweep(sections) {
    fstFace(sections[0]);

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

    lstFace(sections[sections.length - 1]);
}

底下是個簡單的掃掠範例,每個切面的大小不同:

掃掠是建構放樣成形的基礎,上面的範例,是在 z 方向與大小做切面的變化,實際上,你還可以位移、旋轉切面等,你也可以給定一個 2D 輪廓以及路徑,實現各種擠出(extrude)功能,這之後會看到一些實現。