iT邦幫忙

2022 iThome 鐵人賽

DAY 30
1
Modern Web

Three.js 學習日誌系列 第 30

Day29 - 跟著甜甜圈大師~ 使用three.js + blender 一起製造甜甜圈 (二)

  • 分享至 

  • xImage
  •  

Day29 - 跟著甜甜圈大師~ 使用three.js + blender 一起製造甜甜圈

這裡是「Three.js學習日誌」的第29篇,這篇主要在講解如何把Blender建立的模型,拿到three.js專案裡面使用。這系列的文章假設讀者看得懂javascript,並且有Canvas 2D Context的相關知識。

今天就是最後一戰了! 我們今天主要的目標就是利用Blender去實作一個甜甜圈的建模,然後把它放到three.jsscene裡面進行渲染。

1. 先來看看一些blender的基本快捷鍵

我們在上一回有提到,blender有非常多的預設快捷鍵,這邊我們先來講講比較常用的幾個:

自由旋轉視角(滑鼠中鍵)

就跟three.jsorbitControls差不多,只要按下中鍵就可以旋轉視角。

如果同時按住shift,則可以平移視角。

抓取(g)

之所以是g,是因為它代表的是Grab(抓取),我們可以在物體/編輯(點/線/面)模式對個別對象做抓取,
而被抓取起來的物件就可以自由移動。

另外:

  • 按下g之後馬上接著按下x/y/z,就可以鎖定在指定的軸向移動。
  • 按下g之後馬上接著按下x/y/z,在接著指定一個數字N,就可以朝該軸向移動N單位。
  • 按下g之後馬上接著按下x/y/z,然後接著在維持按住shift的狀況下在按一次x/y/z,就會可以限制在某個平面上移動(例如先按X再按下y,就會在xy平面上移動)

這邊要注意一下,blender跟three.js的坐標軸不太一樣,blender朝上的座標軸是Z,但three.js是y

旋轉(r)

旋轉的快捷鍵沒有甚麼意外的是r,它跟抓取(g)一樣,也可以在按下r之後,按下指定的軸向,這樣就可以限制在以該軸為軸心做旋轉。

縮放(s)

縮放的快捷鍵同樣也沒有甚麼意外的是s,跟抓取(g)一樣,也可以在按下s之後,按下指定的軸向,這樣就可以限制在以該軸為軸向做拉伸、縮短。

切換編輯/物體模式(Tab)

我們在上一回其實有講過這個快捷鍵,這邊就不多提。

顯示視點模式的轉盤(z)

我們在上一回其實有講過這個快捷鍵,這邊也是不多提。

刪除物體/點/線/面(x)

初次使用blender的新手通常會因為想要刪除物體,但又不知道怎麼刪除,覺得很苦惱,因為blender的刪除不是delete,而是x

這邊說的新手就是我。。。

只要先選取物體/點/線/面然後按下x,就會彈出小選單問你說是不是確定要刪除。

新增物體(shift+a)

img img

其實這個快捷鍵也可以直接點選上面的按鈕,點選之後就會出現新增物件的選單。

全選(a)

全選會把整個Scene裡面的物件都選起來,其中包括光源、甚至是攝影機

數字鍵盤

數字鍵盤(Numpad)的用途是用來檢視上下左右前後正交主攝影機,...etc.所看到的景象。

img img

如果你的電腦沒有Numpad,blender有提供一個設置可以打開,打開之後你就可以用鍵盤英文字母上方的數字模擬Numpad的行為(如上方右圖)。

2. 開始製作甜甜圈吧~

2-1 首先先從一個Torus開始。

img

按下(shift+a)然後從mesh這個類別裡面找到Torus

基本上所有的物體(Object),在被創建的時候,左下角都會出現這個視窗。

img

而且重點是,這個左下角的視窗,只要在當你一取消對該物體的Focus(Active),就會直接消失。

這時我們可以按下F9來讓他重新顯示(mac鍵盤的話要搭配fn(fn+f9))。

不過,如果你除了取消Focus(Active)之外,還對這個物體作了移動/旋轉/縮放,那就算是按下F9也沒辦法把它叫回來了。

就算是Ctrl+z也沒有用哦~

這個小視窗主要是讓我們決定這個物體的初始Config設置,就有點像three.js torusGeometry的建構式,它可以決定torus的內、外圈、截面半徑,也可以決定物體的segments, (也就是我們在three.js講過的網格細分數量)

網格數量很重要~所以下好物件離手之前都要再三確認過設置沒問題

這邊segments注意先不要下的太大,因為網格面數太多會導致模型的檔案容量過大。

img

建議可以看一下Blender guru的設置,在ep2 的 4:54

2-2 調整一下大小

這邊我們需要調整一下大小,讓Torus比較接近現實生活中甜甜圈的大小。

img

畢竟如果按照我們現在設置的大小,這會是個怪物等級的甜甜圈XD,放到three.js的Scene裡面會超級大~

這邊我們把它縮小到大約直徑為10公分左右(按s)。

img

這邊接著要注意,在blender裡面,如果有做過任何的形變,最後一定要記得「Apply」。

所謂的「Apply」就是把擴張/縮小的值實際的附加到Geometry頂點座標上。

按下Ctrl+AApply剛剛的形變(transform)

2-3 使用Smooth shade + subdivision modifier 來做一次整體優化

這邊我們要點選右鍵,並使用一個黑魔法!! 「Smooth shade」直接把Torus的陰影修成不會這樣一格一格的。

其實這是某種shader的應用方式,原理是透過對每個面的normal值做線性映射來計算曲面的陰影顏色,就有點像是我們之前說過的normal map(法線貼圖),是一種障眼法。

在這次的賽程我自己覺得時間太趕了,而且three.js東西真的太多,所以除了開場的webgl以外,沒有怎麼帶到shader的內容QQ,畢竟那算是更進階的know-how了。

img

接著,因為目前雖然Torus表面的陰影看起來已經優化,但我們若從Torus的邊緣看過去,仍然可以看到邊緣的亮橘色框線有一格一格的感覺。

所以我們這邊要來使用blendermodiefier(修改器)。

右邊側選單有一個扳手的小圖示~

假設我們把物體剛生成出來,並設定好參數(寬、高、網格細分等)這個階段稱作「先天」,那麼modiefier(修改器)則可以看成是一種「後天」對物體網格進行操作的手段。

這邊我們要使用的是Subdivision Surface這一種modiefier(修改器),它的用途是可以「後天」的為物體做進階的網格細分

這邊如果在修改器裡面登打數字時,會莫名的出現「按一下結果出現兩個字」的狀況的話,可以切換一下輸入語言,換成英文

img

這邊順帶一提,假如你在按下Tab這邊切換回去編輯模式,你會發現網格面數好像並沒有增加(但是面確實變平整了),那是因為modiefier(修改器)其實也類似形變(transform),它也必須要經過「Apply」才能夠附加到Geometry的頂點座標上面。

這邊如果要執行「Apply」的話,必須要先切換回去物體模式(Object mode),然後點modiefier右上角的小箭頭。

img

不過我們這邊先不做「Apply」,畢竟之後還有可能會再次調整這個細分的數值。

2-4 搭配比例調整(Propotional Editing)來調整Vertex

接下來我們要稍微的調整這個Torus網格的形狀,把它變得更像現實中的甜甜圈。

先切換到點編輯模式。

然後點亮這個按鈕。

img

這個按鈕的功能是比例調整(Propotional Editing),意思就是說當我們拖動其中一個頂點的時候,周遭的頂點也會根據與被拖動頂點距離的遠近,來產生線性的變化。

記得先按下g來拖動其中一個頂點。

這邊我們可以使用滑鼠滾輪來決定對周遭頂點影響的範圍大小(因為我們剛剛有調整過Scale,所以這邊可能得多滾幾下才可以看到調整影響範圍的提示圈圈)

img

這邊如果想要看更Detail的操作,可以看ep2的18:50

2-5 創造糖衣(Icing)

這邊我們要來開始製作甜甜圈上層的糖衣(Icing)。

原理是透過把Geometry的上半部複製出來一份,並且覆蓋在原本的Geometry上面

首先使用Numpad1,把視角轉到正視圖,接著開啟X-Ray模式,並且框選出上半部的Geometry

之所以開啟X-Ray模式是因為不開的話選不到背面的vertices

img

接下來按下(shift+d)來複製這些框選起來的網格,這邊要注意,複製 vertice並不會直接為我們創造一個新的物件。(blender反而會認為這些複製出來的頂點仍然屬於原本物件)

所以我們這邊在做完複製之後,還必須要按下p,並選擇Selection

img

這邊我們可以看到我們選擇的上半部頂點群被獨立出來成為了Torus.001

這邊如果想看細節操作,可以看ep3的3:49

2-6 實體化修改器

這邊因為我們剛複製出來的糖衣只是一層薄薄的Geometry,然而真實的甜甜圈糖衣看起來應該更要有一種厚度感,所以我們在這邊要給糖衣套上另外一種修改器Solidify(實體化)」,這是一種可以讓geometry的每個頂點,沿著法向量向外延伸,進而增加模型厚度的一種修改器。

img

這邊可以注意到,透過複製Torus本體而創造出來的糖衣同樣會繼承本體的Subdivision修改器。

這邊我們可調整Subdivision修改器和Solidify修改器的順序,讓糖衣變成「先增厚再細分」。

img

這邊的調整修改器所造成的差異主要會體現在糖衣邊緣的部分。

2-7 捏出糖衣滴落變形的部分

這邊我們先開啟磁性吸附(sanp),這個功能可以讓我們在拖動頂點的時候,對拖動的方式進行輔助。這邊除了點亮磁性吸附(sanp)的磁鐵按鈕以外,右邊的吸附模式要記得選擇面模式,並且開啟「project indivisual element」這個選項。

img

這麼一來我們就可以沿著鄰近物件(也就是Torus)的面去拖動糖衣上面的vertex

這邊的操作細節可以看ep4 的4:30

調整完這部分的細節之後先「Apply」糖衣的Subdivision修改器,然後再重新賦予另外一個Subdivision修改器

選定Solidify修改器後按下Ctrl+A也可以直接「Apply」

img

這邊我們也可以先選擇某幾個邊緣上的點,再搭配Extrude(按鍵E)這個功能,用來拉出來比較誇張的滴落形狀

這邊的細節操作可以看ep4的10:15

2-8 捏出甜甜圈中間的凹陷處

接著我們要來做出甜甜圈中間的凹陷處,首先選出Torus上面位於垂直方向偏中間的點,然後按下(Alt+滑鼠左鍵)選取一整圈的頂點,接著使用Scale(s)來把整圈頂點做縮放以營造出甜甜圈中間的凹陷。

img

如果按一下沒有選到水平中線,那就多按幾下。

這時因為Torus的中間被我們往內收縮了,所以糖衣跟Torus中間出現了一部分的空洞。

在這個情況下,我們可以利用Shrink wrap修改器。Shrink wrap修改器可以藉由選擇一個對象(Target),來讓被覆加上這個修改器的物體「貼合」這個對象。

img

附加上Shrink wrap修改器之後記得要把Shrink wrap修改器的順序提高到第一順位。

這邊的細節操作可以看ep4的15:36

2-9 進入雕刻模式開始實作細節

首先在開始這一步之前先把糖衣和Torus的所有modifier(修改器)都「Apply」掉。

然後再點擊上方的Sculpting,就會進入雕刻模式

img

雕刻模式其實就像是小畫家,他在左側會有多種的雕刻筆刷可以使用(這部分也可以搭配有感壓機能的繪圖板來操作,效果會比用滑鼠來的更好)

筆者比較常使用的工具有:

  • Draw
  • inflate
  • Blob
  • Smooth (尤其推Smooth,根本是救場神器QQ)

除此之外,這些筆刷都可以透過按下f/shift+f來調整筆刷大小/硬度。

這邊其實就是按照個人的感覺去選擇工具來使用就好,沒有什麼特別的步驟。

這邊的操作細節可以看ep5

2-10 開始處理渲染環節囉~

在開始渲染之前,第一步我們先來把攝影機調整到適當的位置。

當然在這邊我們可以直接手動調整攝影機的位置~ 但Blender guru這邊有提供一個比較簡單的方法,也就是「Camera to View」。

首先我們先按Ctrl+alt+Numpad0,這樣就可以直接把攝影機移動到你當前檢視景物的視角,接著再在右邊的選單裡面勾選「Camera to View」,這樣你就可以把操作視角移動的機能鎖定在當前的View裡面。

img

接下來,我們要來講講blender提供的兩種渲染引擎: 「eevee」和「cycles」。

首先這兩種引擎都像我們平常在three.js裡面做的事一樣,透過計算物體模型的面法向量、位置、景深、質地、顏色,...etc. 來運算出來在某個視角下物體所呈現的樣子。

但是它們的差異在於「處理光反射的邏輯」

eevee

其實我不確定「eevee」這個詞是不是來源於寶可夢的依布(eevee) XD。

eevee」是一種實時渲染(realtime-render)的渲染引擎

所謂的實時渲染(realtime-render)重視的是效率速度,這種引擎在渲染的過程中會犧牲掉一部分的細節,用來換取更高的效能。

eevee」適合的場景主要就是像網頁前端的模型,還有一些比較偏卡通風格渲染Style。

cycles

cycles」則是一種靜態光線追蹤(raytracing)的渲染引擎

所謂的「光線追蹤(raytracing)」,是一種會計算光源在物體之間反射的渲染演算方式。

近年來「光追」這個名詞應該有被越來越多人聽到。

我們可以想像,假設有一個點光源,他會向外去散發出無數條的光線,假設其中有一條光線先打到了一個紅色的物體A上,那麼那個紅色的物體A,按照自然規律,就應該會反射出一定量的紅色光到鄰近的物體B上,接著鄰近的物體B會又因為接收到射過來的紅光,再根據自己的顏色、質地去決定要反射出什麼顏色的反射光到下一個物體,或是觀察者的眼中。

而像上面這樣一連串的光線路徑投射過程的運算,就是光追的本質之一。

光線追蹤式的渲染模式追求的是細膩度與自然度,所以它拋棄了效率和速度,渲染起來相對的需要花時間。

cycles」渲染引擎通常適用在可以預先渲染的動畫,或是高仿真的照片。

img

上圖左是光追的運算結果,之所以龍的部分身體有較多的紅色像素堆積,是因為光線在龍的身體各部位反射(比方說頭反射的光線接著打到前胸,前胸又反射到某處)所計算出來的結果。

另外,「cycles」在渲染的時候通常會出現下圖的狀況,也就是圖像是一點一點地被算出來,跟我們習慣的three.js實時渲染不同~

cycles」的渲染速度會和顯卡/CPU的等級呈正相關,所以做3D動畫渲染一般會需要較好的顯卡。

img

接下來我們會先focus在「eevee」的使用,畢竟我們的目標是要把甜甜圈給丟到網頁前端做渲染。

科普講解差不多先到這邊~

接下來我們在場景裡面加入一個平面(Plane)。

img

這邊我在開場的時候不小心忘記先把Torus往上拉一點點XD,所以這裡甜甜圈會卡在平面上。

我們把Torus和糖衣一起往上拉。

可以搭配Numpad0使用,這樣比較好確認是不是剛好拖到平面上,當然拖曳形變做完要記的「Apply」!

假如發現拖不動的話,記得檢查一下是不是你的磁性吸附(snap)還開著沒關XD

接著我先把光源拉的離甜甜圈&平面近一點,且把光源強度下修到100W,這樣就可以看到陰影出現在平面上。

img

img

再來我調整了一下相機的焦距參數

img

接著我們可以看到因為現在陰影看起來破破的(Ragged),原因其實就跟我們之前在three.js裡面發生過的一樣。也就是「shadowmap」的解析度不夠。

這邊我們可以調整eeveeshadow相關設置。

img

再調整一下光源的Shadow屬性。

img

算是好多了。

然後我們可以按下F12來看一下渲染的結果。

img

有興趣的話也可以試試切換到cycles來看看渲染的狀況唷~

這邊的操作細節可以看ep6的10:25

2-11. 上材質囉~

img

首先我們先分別點選糖衣Torus平面,然後再點選右側邊欄的材質頁籤(Material Properties),來給這三個部分填上不同的材質

img img

通常材質的選色會比較吃建模師對顏色的敏感度,平常可以多練習這部分的直覺。

blender的材質有各式各樣的參數,其中當然也有我們熟悉的MetallicRoughness,這邊我們可以稍微調整一下糖衣MetallicRoughness,讓它變得比較能夠反射光源。

img

這邊我們再介紹一個有趣的屬性: Subsurface,這是一個可以改變材質透光率的屬性,將這個數值提高可以帶來類似果凍的質感,這邊我們大概給個0.01~0.02就有明顯的不同了。

img

接下來我們要往下一個階段邁進了~

在這個部分Blender guru會講比較多關於Cycles渲染的細節,大部分我是略過了,如果有興趣的話可以從ep6的15:51看起。

2-12. 在blender裡面生成紋理貼圖

大部分的甜甜圈,在中間的凹槽,顏色是比較白的。

img img

但在我們的甜甜圈上面卻沒有這一圈白色的痕跡,而且本體顏色看起來也有點太乾淨。所以我們這邊其實需要一種方法來實作比較寫實的甜甜圈

首先我們先隱藏糖衣平面(點選之後按下h),或是也可以點選該物件的眼睛圖標。

img

img

接下來我們打開Shading這個功能頁面。

img

視角的縮放可能會需要調整一下。

Shading功能頁面其實就是讓使用者可以用節點式的操作方法,來自定義一個shader材質的Feature。

其實這個功能就類似於three.jsShaderMaterial

但很可惜我們在這次的賽程沒有時間可以講到ShaderMaterial,所謂的ShaderMaterial是一種運用Shader腳本,直接生成材質貼圖的方法,它的用法非常廣泛,裡面牽涉到的Know-how也非常的複雜。

如果鐵人賽可以開一個組別是能夠自由決定能夠寫幾天的就好了QQ,30天For three.js真的略短。

延伸閱讀: three.js官方文件上ShaderMaterial的頁面

接著我們先來看看Shading功能頁面都有些甚麼東西。

img

  • 上半部就是即時顯示紋理生成在模型上的狀況
  • 下半部我們把它稱為node-editor,也就是節點式的紋理編輯器

我們接著會在這邊透過實作出由噪聲所生成的材質貼圖。

2-13. 使用node-editor來生成基於噪聲(Noise)的材質貼圖

首先這邊先介紹一下node-editor的使用方式。

node-editor中,我們可以看到很多的面板,這些面板也就是我們所謂的節點

img

  • 節點節點之間的連線,可以透過滑鼠左鍵去點擊=>拖拉=>連接

  • 按下Ctrl + 滑鼠右鍵拖曳,就可以切斷路徑上的節點連線。

  • 按下滑鼠中鍵,可以平移拖曳畫面

  • 按下shift+A,可以決定要加入什麼樣的節點

  • 這些連線和節點會決定最後輸出的紋理結果。

筆者個人認為節點式渲染的原理應該是有點類似函數的Input/Output,把函數的Output傳給某個函數作為Input的概念,不過目前還沒有驗證過這個推論是不是正確的。

img

在這邊筆者是採用上述的節點連結還有參數,即可生成如下的紋理。

img

這邊節點的運用建議可以多看看ep7影片中的介紹,不過筆者自己認為這種節點式的運算方法如果要達到可以自由運用,可能要經過多次練習,並且要熟悉每個節點的使用情境。

2-13. 繪製基底紋理

我們其實可以發現,在前面的node-editor中我們做到的,其實有點類似three.jsHeight mapcolorNormal map等多種屬性疊加起來的綜合結果。

而在three.js中,我們平常還可以加上map這一個屬性,也就是作為基底的紋理

這邊我們要展示的就是要如何使用blender來生成基底紋理

首先我們得要先把剛剛的Shading功能頁面中的color ramp這個節點刪除。並新增一個image texture的節點。

img

並點擊image texture上面的New按鈕,新增一張512*512大小,底色與原本Torus底色相近的的基底紋理,並取名為base map

接著紋理的預覽畫面應該會變成像這樣。

img

接著再切換到 Texture Paint這個功能頁面。

img

Texture Paint顧名思義就是紋理繪製,它除了可以繪製基底紋理以外,當然也是可以繪製例如Metallic mapHeight map等其他類型的紋理。

只要你能畫得出來的話。

這邊我們可以直接把白色的區塊塗在模型中間凹槽的地方。

如果發現突然不能直接在模型上塗色,切回去shading再切回來texture painting就可以了。

img

接著畫完要記得存檔。

點擊畫面左上角有寫著一個小"image"的地方,並選擇"save as",blender會直接以你剛剛在image-texture節點上命名的名字來為這個材質命名。

img

接著我們再回到shading功能面板,會發現紋理預覽上面已經出現我們剛剛繪製的基底紋理了~

img

這邊我們再稍微調整一下node editor的參數,並加入一個「mixRGB」的節點,然後把這個節點上提供的混和模式選項改為Overlay。

img

最後回到modeling功能頁面,解除糖衣平面的隱藏~就得到了下圖的結果

img

有沒有變得越來越像一回事了XD~?

這邊的詳細操作可以看 ep8

2-14. 使用Geometry node 給糖衣撒上糖粒(Sprinkles)

接著我們要為這糖衣的部分撒上糖粒。

要使用Geometry nodes這個功能。

Geometry nodes 顧名思義也是一種節點編輯器,但是他和我們剛剛使用的shading功能面板不同,是可以直接生成Geometry的

這邊首先先打開Geometry nodes面板,然後選擇糖衣,接著按一下下圖中紅圈的NEW

img

所謂的Geometry nodes,其實基本上算是一種modifier(修改器),我們透過這種modifier可以給原有的Geometry生成新的內容,而且這些內容可以透過組合不同的節點來決定。

通常Geometry nodes會被應用在需要產生具有規律的群體物件上(ex:大樓群、課桌椅),又或者是透過隨機seed來產生隨機分布的Geometry(有點像粒子系統的概念)。

這邊我們透過剛剛的操作,已經為糖衣的部分掛上了一個Geometry nodes修改器

img

接下來的部份其實強烈建議要看過Blender guru本人的操作,在ep9

接著我們給節點編輯器加上

  • Join geometry

  • Distribute Points on Face

  • instance on points

這三個節點

img

然後建立一個Cylinder(圓柱狀)的物體(記得寬高要先設定好),然後把這個Cylinder,從物件列表拖曳進去節點編輯器

img
img
img

我們接著把拖進來的Cylinder節點,和其餘三個節點像下圖一樣去做連結。

img

接著就會發現在糖衣的上面出現了兩根一樣的Cylinder

img

其實做到這邊應該差不多可以看懂接下來大概要幹甚麼了。

接下來我們點選剛剛的Cylinder物件(不是節點哦~),然後輸入「rx90」讓他以X軸旋轉90度,並且把Distribute Points on Facedensity屬性提高。

形變完要記得「Apply」不然Geometry nodes不會有效果。

img

再來我們把Distribute Points on FaceRotation屬性和instance on pointsRotation屬性對連。

img

有點那個感覺出來了~

這邊我們先調整一下原本Cylinder物件的長度,讓他沿著Y軸縮短成為原本的一半。

當然要記得「Apply」

接著先加入下面兩個節點:

  • rotate euler
  • random value

然後如下串聯

img

這邊的用意在於把糖衣物件的面旋轉向量做隨機化,藉此讓每顆糖粒產生不同的旋轉角度。

img

2-15. 使用權重繪製模式來決定Geometry nodes的分布狀況

接下來我們按下 ctrl+tab,然後會出現權重繪製的畫面。

這邊的操作其實就跟剛剛繪製基底紋理的時候很類似,只是權重繪製的用途在於決定糖衣上面糖粒的分布狀況。

img

我們在繪製權重的時候,其實在被繪製的對象上都會把這個權重的mapping資料,寫入這個物體的data property中,

我們可以在這邊找到該Property。

img

這邊我們把它重新命名為sprinkle density

接下來我們把group input底下的空白屬性,和Distribute Points on Facedensity連結在一起,這時候會發現group input上面出現了density這個屬性。

我們這時候就也可以在修改器的面板看到修改器上面出現了density這個屬性。

這時候我們點擊density字樣右邊的十字按鈕,就可以發現我們可以把density指向剛剛建立的權重資料(sprinkle density)。

img

在把density指向剛剛建立的權重資料之後,我們必須要在group inputDistribute Points on Face的density連線之間加上一個Math 修改器,然後把我們剛剛原本下在Distribute Points on Facedensity值改成下在Math上,並且把Math的混和屬性改成Multipy

img

這樣糖粒就會跟著權重走了。

img

這邊我們其實已經可以試著給cylinder上個材質,然後看看會長怎樣了。

img

上完材質的cylinder會繼承顏色給畫面中所有的糖粒

接下來我們稍微修飾一下cylinder的形狀。

img

這邊我用我們前面介紹過的XRAY模式把圓柱的其中一個圓面頂點都框起來,接著按下Ctrl+B -- 也就是blender bevel(導角)的快捷鍵,接著拖拉就可以把一個面變成具有導角的面。

img

我們做到這邊為止,建模的部分先告一段落了。

其實Blender guru 的ep10 有提到要如何產生隨機形狀的糖粒,不過筆者自己覺得我們只是要展示這個甜甜圈放到three.js的scene裡面,可以不用做到這麼複雜,如果有興趣的人可以自己看看ep10

2-16. 輸出檔案

在輸出之前,我們會需要先把剛剛我們做的geometry nodes的修改器「Apply」。(很重要~~~~)

但是!! geometry nodes如果直接拿去「Apply」會整個消失掉看不見(有興趣的人可以試試看,反正可以復原),這邊我們必須要在geometry nodes上面再加上最後的一個節點「Realize Instance」

img

這樣一來geometry nodes產生的geometry在「Apply」之後就會繼續存在了。

接著我們需要把檔案輸出成GLTF檔。

首先我們點選File > Export> gltf2.0

然後如下設置:

img

原則上就是有需要的就輸出,沒有需要的取消勾選。(像是Animation這個部分,因為我們沒有做,所以這邊就全部取消)

檔案的格式則是選成把資源和gltf本體分離的格式。

img

輸出完接著就可以放進three.js 專案裡面,並使用gltf loader來讀取了。

3. 在three.js裡面讀取gltf檔案

three.js中,就像上面說的一樣,要讀取gltf檔案就必須靠gltfLoader

gltfLoader在使用的時候基本上與textureLoader很類似,簡單來說就是先進行實例化之後,使用.load方法來進行讀取。

const gltfLoader = new GLTFLoader();
const pm = new Promise((res) => {
  gltfLoader.load('../static/models/donuts2/donut2.gltf', (gltf) => {
    res(gltf);
  });
})

async function main(){
  const gltf = await pm;
}

讀取的方式基本是一樣的。

3-1 gltf讀取進來之後會長什麼樣子?

gltf的格式其實根本就是為了three.js而設,我們在把gltf讀取進來之後,利用console.log檢視他,會長得像這樣。

img

讀取進來就會直接是個JS物件~ 是不是很方便~

在這個讀進來的gltf物件底下,一定會存在scene這個屬性,而這個scene其實就是我們剛剛在blender建模時的scene~

我們再往scene的裡面翻

img

可以看到scene底下還有個children,這個children的兩個子項其實就分別是糖衣Torus

我猜應該看到這邊大多數人都明白,接下來要怎麼把它放進場景了吧XD?

3-2 把剛剛做的甜甜圈放入three.js的scene裡面

要把gltf放進場景,最快的方法其實就是直接把gltf.scene丟進去three.jsscene裡面。

scene.add(gltf.scene)

通常除非我們是需要gltf內部的特定內容(比方說假設我只需要糖衣的部分),不然通常都是直接把整個scene載入。

3-3 來把剛剛做好的模型實際做成前端畫面吧!

這邊我們還是一樣選用我們的「three.js boilerplate」來操作。

3-3-1. 首先把gltf檔案,放到./static資料夾

前面的初始化和安裝流程我們就先跳過了

這邊之所以要把gltf檔案放在./static資料夾,是因為我們不希望webpack會跑去解析這個檔案。

img

3-3-2. 在./src/ts/resource/底下建立一個./model.ts

建立完./model.ts之後,我們在裡面寫入要載入的資料。

interface Source {
    name: string,
    type: string,
    paths?: string[],
    path?: string
}

export const modelSources: Source[] = [
    {
        name: 'donutsModel',
        type: 'gltfModel',
        path: 'static/donuts/donuts.gltf'
    }
]

3-3-3. 修改./src/ts/resource/index.ts

這邊我主要是補上了用來取得gltfpromise包裹函數

const getGltf = (source: any) => {
	const prm: Promise<SourceObj> = new Promise((res, rej) => {
		gltfLoader.load(
			source.paths,
			(model) => {
				res({
					name: source.name,
					content: model
				});
			},
			null,
			rej
		);
	});
	return prm;
}

並且在getResources這個函數裡面,補上取得./src/ts/resource/model.ts內部資源的部分:

const sources = [...textureSources, ...modelSources];

for (let source of sources) {
		switch (source.type) {
			case 'cubeTexture': promiseArr.push(getCubeTexture(source));
				break;
			case 'texture': promiseArr.push(getTexture(source));
				break;
        // 加上了這個case
			case 'gltfModel': promiseArr.push(getGltf(source));
				break;
		}
	}

3-3-5. 新增./src/ts/mesh/donuts.ts

這邊我們新增甜甜圈專用的ts檔案。

import { Clock, Object3D } from "three";
import { Base } from "../class/base";

export class Donuts {
    scene: Object3D;
    constructor(private base: Base) {
        this.setModel()
    }

    setModel() {
        this.scene = this.base.resources.donutsModel.scene as Object3D;
        this.scene.scale.set(10, 10, 10)
        this.base.scene.add(this.scene);
        this.scene.scale.set(50, 50, 50)
        this.scene.position.set(2, 0, 0)
        this.scene.rotation.x = Math.PI * 0.3
        this.scene.rotation.z = Math.PI * 0.15
    }

    update(clock: Clock) {
        this.scene.rotation.y = clock.getElapsedTime()
    }
}

3-3-5. 調整./src/ts/class/playground.ts

這邊就沒啥特別的,就是實例化Donuts,並且發動Donutsupdate方法。

import { Env } from "./env";
import { Base } from "./base";
import { Clock } from "three";
import { Donuts } from '../mesh/index'

export class Playground {
    env: Env;
    donuts: Donuts;
    private ready = false;
    constructor(private base: Base) {
        this.init();
    }
    init() {
        this.base.getResources().then(() => {
            this.env = new Env(this.base);
            this.donuts = new Donuts(this.base);
            this.ready = true;
        })
    }

    update(clock: Clock) {
        if (this.ready) {
            this.env.update(clock);
            this.donuts.update(clock);
        }

    }
}

到這邊其實畫面已經有東西出來了~

img

接著就是調整環境光照,renderer輸出色彩參數,然後補上標題,背景色~

3-3-6 調整環境中的光源

其實3D建模的打光是一個學問。

但筆者小弟我感覺自己應該還不夠格談論這門學問QQ

不過依我自己的習慣,我自己通常會放置一盞以上的點光源,加上環境光

目標是希望可以凸顯物體的左右兩側,並給整體帶來一定的光照量。

這邊我們打上一盞環境光、一盞點光源,和一盞平行光

import { Base } from './base';
import { AmbientLight, Clock, DirectionalLight, PointLight } from 'three';

export class Env {
    ambientLight: AmbientLight;
    pointLight: PointLight;
    directionalLight: DirectionalLight;

    constructor(private base: Base) {
        this.setLights();
    }
    
    setLights() {
        this.setAmbientLight();
        this.setPointLight();
        this.setDirectionalLight();
    }
  //點光源
    setPointLight() {
        this.pointLight = new PointLight(0x2b41a1, 3.5);
        this.pointLight.position.set(5, -2, -1);
        this.base.scene.add(this.pointLight)
    }
// 平行光
    setDirectionalLight() {
        this.directionalLight = new DirectionalLight(0xfae598, 4);
        this.directionalLight.position.set(-3, 2, 2);
        this.base.scene.add(this.directionalLight);
    }
//環境光
    setAmbientLight() {
        this.ambientLight = new AmbientLight(0xfae598, 0.6);
        this.base.scene.add(this.ambientLight)
    }


    update(clock: Clock) {

    }

}

3-3-6 最後調整一下CSS/HTML....然後!!!

燈燈燈燈~

img

最後講評一下,這邊前端渲染的結果跟在blender裡面呈現的不同,通常會是下面幾個原因:

  • 「光源差異」: 最基本的狀況就是光照的強度/顏色不同。
  • 「blender的有些屬性前端其實不支援」:比方說我們前面用過,可以改變材質透光率的subSurface,這個gltf似乎是不支援的。
  • shading產生的紋理好像不見了: 這其實是因為blender沒有辦法把shading產生的紋理做打包,這個其實有方法可以解決,就是改成用cycles去做Baking,把shading產生的紋理烘焙成靜態貼圖。

不過shading這部分筆者小弟我老實說其實是因為時間不夠所以沒有處理QQ。

最後這邊附上:

Repo地址: https://github.com/mizok/donuts

Github Page: https://mizok.github.io/donuts/

4. 完結後記

這次參賽的感想一句話總結就是~

「想做的事果然還是太多了,30天根本不夠用」

如果鐵人賽有40天就好了。。。

不過儘管如此,我還是很努力地爬完了自己預計的全程

雖然說覺得自己至少也算是有運動家精神,But...

想到這邊總覺得心情很複雜 = =,但我說不出來為什麼,哈哈。


上一篇
Day28 - 跟著甜甜圈大師~ 使用three.js + blender 一起製造甜甜圈
下一篇
Day30 - 看似是第30天,實際則是第31天的完賽心得
系列文
Three.js 學習日誌31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
max19960419
iT邦新手 5 級 ‧ 2022-11-17 16:45:41

這篇有這麼美的甜甜圈竟然沒有人留言

Mizok iT邦新手 3 級 ‧ 2022-11-18 10:39:44 檢舉

哈哈哈可能是我廢話太多了QQ

我要留言

立即登入留言