iT邦幫忙

2022 iThome 鐵人賽

DAY 30
0

有時候我們會需要給一個物件邊框。無論是要讓物件在背景中突出、點選物件時有選中的感覺,或是在綠幕上有比較好裁切,物件邊框都能達到我們所需要的需求。

本篇介紹邊框,作為30天鐵人賽的最後一篇。

本篇內容

  1. 簡單形狀邊框——物件放大
  2. 複雜形狀的邊框——用Normal調整裁剪範圍
  3. LowPoly形狀的邊框——用OutlinePass

完成品

https://storage.googleapis.com/umas_public_assets/michaelBay/day30/Untitled%20(94).gif

簡單形狀邊框——物件放大

邏輯很簡單,複製本體,讓複製品比本體大一點點,就可以形成邊框了。

準備程式碼

我們直接沿用上一篇的程式碼即可。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505WLL6SKeGdm.png

https://codepen.io/umas-sunavan/pen/vYjPRmZ?editors=1010

建立邊框物件

我們製作另一個球體,取代它的material,使其呈現白色,並且些微放大,即可完成。

	const mesh = addSphere()
	scene.add(mesh)
+	const outline = addSphere()
+	outline.material = new THREE.MeshBasicMaterial({color: 0xffffff, side: THREE.BackSide})
+	outline.geometry.scale(1.05,1.05,1.05)
+	scene.add(outline)

https://ithelp.ithome.com.tw/upload/images/20221015/20142505BOQ9L6C7Bl.png

但這個作法的缺點在於,邊框會隨著鏡頭放大而放大。如下圖:

https://ithelp.ithome.com.tw/upload/images/20221015/201425057ONToS1xUs.png

而且,這樣的作法將使得錨點數目倍增。

同樣的方法,也可以透過Shader來達成,我繼續說明。

複雜形狀的邊框——用Normal調整裁剪範圍

前一個方法相當好用,因為形狀簡單。但如果我們面對一個形狀複雜的物件,這個方法就不太合適了。

我們把前一篇同樣的邏輯,套用在比較複雜的物體的話:

- const geo = new THREE.SphereGeometry(5, 12, 12)
+ const geo = new THREE.TorusKnotGeometry( 10, 3, 100, 16 );

https://ithelp.ithome.com.tw/upload/images/20221015/201425056mVdcglgac.png

為什麼有些邊框比較細,有些邊框比較粗?如果我們設定wireframe是true,就可以知道其中的構造。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505giH8CTHSYn.png

可以看到,由於物件是整理放大,所以並不是所有的面都向「外」突出。離中心越遙遠的地方,突出越是明顯,但離中心比較近的地方,突出非常不明顯。

而這也使得我們必須要用比較複雜的方法來處理。

如果我們加上這行程式碼,就可以解決這個問題。

	const mesh = addSphere()
	scene.add(mesh)
	const outline = addSphere()
+	outline.material.onBeforeCompile = shader => {      
+	  const token = `#include <begin_vertex>`
+	  const customTransform = `vec3 transformed = position + normal*0.05;`
+	  shader.vertexShader = shader.vertexShader.replace(token,customTransform)
+	}
-	outline.material = new THREE.MeshBasicMaterial({color: 0xffffff, side: THREE.BackSide})
-	outline.geometry.scale(1.05,1.05,1.05)
	scene.add(outline)

WebGL會編譯GLSL,因為它不像javascript是腳本語言,而onBeforeCompile 將會在編譯之前執行。所以,即使我們已經在HTML準備好Shader程式碼,仍然可以在這裡進行修改。

我準備了token,只要shader有包含token的文字,就會換成update文字。如此一來就可以修改shader邏輯,同時又不影響本體物件的Shader。

在上面總共修改了一處vertex shader。

其中,#include <begin_vertex>就是在vertext shader當中的一段程式碼,從整個shader上下文大概可以知道 ,這行主要在描述每個錨點應該如何呈現。

而position就是每個錨點的位置,normal就是每個面的垂直向量。作用是將垂直向量向外延伸0.05,讓每個面依據自己的垂直normal向外延伸。

如果我們開啟wireframe預覽的話就可以看到黑色的物件是怎麼放大的。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505UhEoGuQTyo.png

過去在「Day23: WebGL Shader——從認識GLSL開始釐清Shader」有提到,vertex shader很重要的任務,就是取得渲染的裁剪範圍,這使得fragment shader知道應該要修改哪些像素的顏色。

position 改成 position+normal*0.5,能夠擴大裁剪範圍。這是因為每一個錨點,都往他們的法線向量位移了,而法線向量都是朝外的(除非有其他貼圖設定),所以就直接向外擴大了。

LowPoly形狀的邊框——用OutlinePass

同樣的邏輯,面對面數比較少的物件,就會出現破綻。

+	const addBook = async () => {
+			const path = 'https://storage.googleapis.com/umas_public_assets/michaelBay/day30/book06%20outline.gltf'
+			const gltf = await new GLTFLoader().loadAsync(path);
+			const mesh = gltf.scene
+			gltf.scene.traverse(object => {
+				if (object.isMesh) {
+					object.castShadow = true
+					object.receiveShadow = true
+				}
+			})
+			return mesh
+		}
	
+	const book = await addBook()
-	const mesh = await addSphere()
+	scene.add(book);
-	scene.add(mesh);

https://ithelp.ithome.com.tw/upload/images/20221015/20142505xJT6cckd77.png

要了解決這個問題,已經有神人開發出了OutlinePass,pass像是一個濾鏡,只要套用,就能透過其本身的shader,去增加指定物件的邊框。

如果有興趣,可以看它程式碼是怎麼寫的。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505feP0b6PCN8.png

我們先引入套件:

+import { OutlinePass } from 'https://unpkg.com/three@latest/examples/jsm/postprocessing/OutlinePass';
+import { EffectComposer } from 'https://unpkg.com/three@latest/examples/jsm/postprocessing/EffectComposer';
+import { RenderPass } from 'https://unpkg.com/three@latest/examples/jsm/postprocessing/RenderPass';

接著加上Composer:

+const initComposer = () => {
+    composer = new EffectComposer(renderer);
+    var renderPass = new RenderPass(scene, camera);
+   composer.addPass(renderPass);
+    outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
+		outlinePass.selectedObjects = [book];
+	}
+	initComposer()

最後,修改animate()

function animate() {
	requestAnimationFrame(animate);
- renderer.render(scene, camera);
+	if (composer) {
+		composer.render();
+		outlinePass.edgeStrength = 80
+		outlinePass.edgeThickness = 4
+		console.log(outlinePass.edgeStrength);
+	}

}
animate();

完成品

https://storage.googleapis.com/umas_public_assets/michaelBay/day30/Untitled%20(94).gif

CodePen

https://ithelp.ithome.com.tw/upload/images/20221015/20142505zHJha5Kdt9.png

https://codepen.io/umas-sunavan/pen/eYrXrmz

參考資料

outlinePass教學
shader放大原理
outlinePass github


上一篇
Day29: WebGL Shader—用Shader做全視角內光暈、星球材質
下一篇
Day31: 完賽心得
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言