iT邦幫忙

2022 iThome 鐵人賽

DAY 10
0
Modern Web

Three.js 學習日誌系列 第 10

Day9 - 「點點到位」 - 幾何結構Geometry(三)

  • 分享至 

  • xImage
  •  

Day9 - 「點點到位」 - 幾何結構Geometry(三)

這裡是「Three.js學習日誌」的第9篇,本篇的主旨是要介紹Geometry的概念,還有一些常用的Geometry子類的使用方法。這系列的文章假設讀者看得懂javascript,並且有Canvas 2D Context的相關知識。

在上一篇中我們提到了要怎麼去定義一個正四面體Geometry的面(faces)還有uv,而這篇我們則是要看看:怎麼樣從只有頂點座標的狀態,建立一個Geometry

以點構成空間

我們之前有展示過透過直接設定position attribute的方式來產生隨機三角形,而這次我要來給大家講講BufferGeometry.setFromPoints這個方法。

BufferGeometry.setFromPoints這個方法跟setAttribute的做法最主要的差別就只有setAttribute是必須傳入一整個包含Float32Array陣列內容的BufferAttribute,而BufferGeometry.setFromPoints則是可以直接傳入一個點座標陣列。

這邊用表格表示,釐清一下差異。

泛用性 傳入型別 直觀程度
setAttribute 較高 BufferAttribute 較低
setFromPoints 較低 Vector3[] 較高

BufferGeometry.setFromPoints在把座標傳入BufferGeometry之後,最終其實也是一樣會把值傳遞到position attribute,所以結果沒甚麼差異。

從零建立正立方體

這邊我們示範一下要怎麼用BufferGeometry.setFromPoints來排出一個正立方體。

這是一個中心位於(0,0,0)的立方體

img

我們先在程式裡面把所有的頂點列出來。

//頂點集
const pts = [
    new Vector3(-0.5, 0.5, -0.5),//A
    new Vector3(0.5, 0.5, -0.5),//B
    new Vector3(-0.5, 0.5, 0.5),//C
    new Vector3(0.5, 0.5, 0.5),//D
    new Vector3(-0.5, -0.5, -0.5),//E
    new Vector3(0.5, -0.5, -0.5),//F
    new Vector3(-0.5, -0.5,0.5),//G
    new Vector3(0.5, -0.5, 0.5),//H
  ];

接著,因為我們之前在webgl hello world也有講過,webgl沒有提供四邊形的Primitive(參考連結),所以每個面我們都需要用2個三角形去組合出來。

在這邊組合三角形成為一個平面就是一個重點的地方,因為三角形的頂點排列順序會決定:「這個三角形面是正面,還是反面」。

Three.js預設是不會渲染一個平面反面的,不過這個其實也可以在Material的設置中改為雙面渲染(DoubleSide)。

const mat = new MeshBasicMaterial({
  color:0xff0000,
  side:DoubleSide //要注意DoubleSide是一個常數,而不是字串,必須從three.js的module中引入
})

上述雙面渲染(DoubleSide)的例子只是先提供給大家參考,這邊我們還是維持只渲染正面(FrontSide)的操作。

所以我們必須要先理解,怎麼樣的三角形頂點排列規則,才能使該三角面Three.js定義為正面

這邊我們要講的規則就是所謂的Winding Rule(纏繞規則)。

img

延伸閱讀: OpenGL上關於winding rule的解釋

這邊我畫了一張簡單的圖(下圖)來讓讀者理解,假設現在Scene裡面有一個平面,他是由[ABC]和[BCD]這兩個三角形構成的,[ABC]的纏繞順序是A>B>C,而[BCD]的纏繞順序則是B>C>D,以Three.js的定義來講,被螢幕前的觀者定義為逆時鐘的纏繞順序會判定為正面,反之則是反面

所以ABC在這邊算反面,按照預設他是不會渲染出來的,除非觀者旋轉這個平面,從平面的後面看。

img

所以在這邊如果我們要做出一個會正確顯示所有的正立方體,並按照我們剛剛的頂點集規劃,我們必須要像這樣去排列。

所有當前看的到的面,都應該要是逆時鐘的纏繞順序,看不見的面則反之。

img

const points = [
    pts[0].clone(), //A
    pts[2].clone(), //C
    pts[1].clone(), //B
    //
    pts[1].clone(), //B
    pts[2].clone(), //C
    pts[3].clone(), //D
    //ABC+BCD這樣算一個面
    pts[0].clone(), //A
    pts[1].clone(), //B
    pts[4].clone(), //E
    //
    pts[1].clone(), //B
    pts[5].clone(), //F
    pts[4].clone(), //E
    //ABE+BFE
    pts[4].clone(), //E
    pts[5].clone(), //F
    pts[6].clone(), //G
    //
    pts[5].clone(), //F
    pts[7].clone(), //H
    pts[6].clone(), //G
    //EFG+FHG
    pts[2].clone(), //C
    pts[6].clone(), //G
    pts[3].clone(), //D
    //
    pts[3].clone(), //D
    pts[6].clone(), //G
    pts[7].clone(), //H
    //EFG+FGH
    pts[0].clone(), //A
    pts[6].clone(), //G
    pts[2].clone(), //C
    //
    pts[0].clone(), //A
    pts[4].clone(), //E
    pts[6].clone(), //G
    //ACG+AEG
    pts[1].clone(), //B
    pts[3].clone(), //D
    pts[7].clone(), //H
    //
    pts[1].clone(), //B
    pts[7].clone(), //H
    pts[5].clone() //F
    
  ];

  geo.setFromPoints(points);

img

codepen連結:點我

為正立方體做面分組

接著也許你會想像我們上一篇做的一樣,把這個正立方體六個面都填上不同的材質。

那你就會碰到上一篇也碰過的group問題,畢竟這個方塊是從0建立出來的,裡面的頂點沒有做任何的分組,而且uv也沒有給,同樣也沒有normal,所以等於是剩下的東西都要自己建立出來。

所以接著我們先來完成group的部分。

geo.addGroup(0,6,0)
geo.addGroup(6,6,1)
geo.addGroup(12,6,2)
geo.addGroup(18,6,3)
geo.addGroup(24,6,4)
geo.addGroup(30,6,5)

然後把多重材質套上去。

const mats = [
    new MeshBasicMaterial({ color: new Color("red") }),
    new MeshBasicMaterial({ color: new Color("yellow") }),
    new MeshBasicMaterial({ color: new Color("orange") }),
    new MeshBasicMaterial({ color: new Color("brown") }),
    new MeshBasicMaterial({ color: new Color("blue") }),
    new MeshBasicMaterial({ color: new Color("purple") })
  ];

  const mesh = new Mesh(geo, mats);

img

定義uv映射

再來是uv,其實也沒甚麼特別的,就是照著剛剛setFromPoints的順序去決定uv映射的狀況。

geo.setAttribute(
    "uv",
    new Float32BufferAttribute(
      [
        0,1, //A
        0,0, //C
        1,1, //B
        1,1, //B
        0,0, //C
        1,0, //D
        //
        1,1, //A
        0,1, //B
        1,0, //E
        0,1, //B
        0,0, //F
        1,0, //E
        //
        1,1, //E
        0,1, //F
        1,0, //G
        0,1, //F
        0,0, //H
        1,0, //G
        //
        0,1, //C
        0,0, //G
        1,1, //D
        1,1, //D
        0,0, //G
        1,0, //H
        //
        0,1, //A
        1,0, //G
        1,1, //C
        0,1, //A
        0,0, //E
        1,0, //G
        //
        1,1, //B
        0,1, //D
        0,0, //H
        1,1, //B
        0,0, //H
        1,0 //F
      ],
      2
    )
  );

 //把材質換成圖片材質,這樣才可以看出來uv有沒有錯誤狀況
  const tl = new TextureLoader();
  const mats = [
    new MeshBasicMaterial({
      map: tl.load("https://picsum.photos/seed/123/picsum/300/300")
    }),
    new MeshBasicMaterial({
      map: tl.load("https://picsum.photos/seed/456/300/300")
    }),
    new MeshBasicMaterial({
      map: tl.load("https://picsum.photos/seed/789/300/300")
    }),
    new MeshBasicMaterial({
      map: tl.load("https://picsum.photos/seed/012/300/300")
    }),
    new MeshBasicMaterial({
      map: tl.load("https://picsum.photos/seed/345/300/300")
    }),
    new MeshBasicMaterial({
      map: tl.load("https://picsum.photos/seed/678/300/300")
    })
  ];

img

codepen 連結: 點我

定義各頂點法向量

最後是normal

我們在上一回沒有實作到normal的部分,我們在這邊先介紹一下。

three.js中,因為attribute的值都是by頂點去儲存的,所以沒有辦法直接定義某個面的法向量,反而是必須要取該面所有頂點的法向量平均。

計算法向量平均這一部分Three.js會自己完成,我們只需要給定每一個頂點的法向量就好~

img

而我們因為在初期已經把正立方體的中心定在(0,0,0)了,而且我們要作的模型是一個正立方體。

所以這邊我們其實可以把(0,0,0)到每個頂點座標所形成的向量,先轉變成單位向量之後,把這些單位向量當作頂點法向量儲存到normal attribute中。

除此之外, 記得還要在Scene裡面補上一盞光源,並且把MeshBasicMaterial換成MeshStandardMaterial,這樣我們才能看到材質光源產生反應。

const nPoints = points.map((o) => {
    return o.clone().normalize();
  });

  const normalArr = [];

  nPoints.forEach((o) => {
    normalArr.push(o.x);
    normalArr.push(o.y);
    normalArr.push(o.z);
  });

  geo.setAttribute("normal", new Float32BufferAttribute(normalArr, 3));

  ...

  const pl = new PointLight(0xffffff, 1);
  pl.position.set(2, 2, 2);

  scene.add(mesh, pl);

img

codepen連結:點我

小結

今天我們提到了如何從只有點座標到建立完全的3D模型,大家從過程中應該就可以理解到像這樣徒手建立一個新的Geometry其實非常的花時間。

在正常狀況下,大多數的建模都是透過3D建模軟體直接操作,不會像這樣一個座標一個座標慢慢處理。

不過個人是覺得能有像這樣自己動手作的經驗還蠻不錯的XD,希望大家喜歡今天的介紹。

延伸閱讀

-https://zh.m.wikipedia.org/zh-tw/%E9%A0%82%E9%BB%9E%E6%B3%95%E5%90%91%E9%87%8F

-https://www.khronos.org/opengl/wiki/Face_Culling


上一篇
Day8 - 「面面俱到」 - 幾何結構Geometry(二)
下一篇
Day10 - 最後補充 - 幾何結構Geometry(四)
系列文
Three.js 學習日誌31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
良葛格
iT邦新手 2 級 ‧ 2022-09-26 09:00:09

就中心在原點的正多面體來說,你用頂點位置 normalize 作為頂點法向量,這麼做雖然可以,相對地,用這種頂點法向量的平均來求一個面的法向量,雖然是沒問題。

然而若不是中心在原點的任意多面體,這麼做就會有問題。

一般來說:

  • 三個點可以構成面,也就可以透過 cross product 求得面法向量(而不是透過頂點法向量平均來求得)。
  • BufferGeometry 有個 computeVertexNormals,它計算頂點法向量的方式,是將共用頂點的所有面法向量平均後求得。
Mizok iT邦新手 3 級 ‧ 2022-09-26 12:37:59 檢舉

感謝良大回應 :DD

這個我有想到, 所以我有標示出來『而且我們要作的模型是一個正立方體』

Mizok iT邦新手 3 級 ‧ 2022-09-26 12:39:22 檢舉

不過我確實沒特別去找要怎麼實作其他類型geometry 的法向量, 感謝補充/images/emoticon/emoticon82.gif

我要留言

立即登入留言