有時候我們會需要給一個物件邊框。無論是要讓物件在背景中突出、點選物件時有選中的感覺,或是在綠幕上有比較好裁切,物件邊框都能達到我們所需要的需求。
本篇介紹邊框,作為30天鐵人賽的最後一篇。
邏輯很簡單,複製本體,讓複製品比本體大一點點,就可以形成邊框了。
我們直接沿用上一篇的程式碼即可。
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)
但這個作法的缺點在於,邊框會隨著鏡頭放大而放大。如下圖:
而且,這樣的作法將使得錨點數目倍增。
同樣的方法,也可以透過Shader來達成,我繼續說明。
前一個方法相當好用,因為形狀簡單。但如果我們面對一個形狀複雜的物件,這個方法就不太合適了。
我們把前一篇同樣的邏輯,套用在比較複雜的物體的話:
- const geo = new THREE.SphereGeometry(5, 12, 12)
+ const geo = new THREE.TorusKnotGeometry( 10, 3, 100, 16 );
為什麼有些邊框比較細,有些邊框比較粗?如果我們設定wireframe是true,就可以知道其中的構造。
可以看到,由於物件是整理放大,所以並不是所有的面都向「外」突出。離中心越遙遠的地方,突出越是明顯,但離中心比較近的地方,突出非常不明顯。
而這也使得我們必須要用比較複雜的方法來處理。
如果我們加上這行程式碼,就可以解決這個問題。
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預覽的話就可以看到黑色的物件是怎麼放大的。
過去在「Day23: WebGL Shader——從認識GLSL開始釐清Shader」有提到,vertex shader很重要的任務,就是取得渲染的裁剪範圍,這使得fragment shader知道應該要修改哪些像素的顏色。
position
改成 position+normal*0.5
,能夠擴大裁剪範圍。這是因為每一個錨點,都往他們的法線向量位移了,而法線向量都是朝外的(除非有其他貼圖設定),所以就直接向外擴大了。
同樣的邏輯,面對面數比較少的物件,就會出現破綻。
+ 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);
要了解決這個問題,已經有神人開發出了OutlinePass
,pass像是一個濾鏡,只要套用,就能透過其本身的shader,去增加指定物件的邊框。
如果有興趣,可以看它程式碼是怎麼寫的。
我們先引入套件:
+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://codepen.io/umas-sunavan/pen/eYrXrmz
outlinePass教學
shader放大原理
outlinePass github