iT邦幫忙

2022 iThome 鐵人賽

DAY 29
0
Software Development

30天成為鍵盤麥可貝:前端視覺特效開發實戰系列 第 29

Day29: WebGL Shader—用Shader做全視角內光暈、星球材質

  • 分享至 

  • xImage
  •  

本篇內容

  1. 一、全視角的內光暈
  2. 二、內光暈在地球的應用

成品

earth.gif

一、全視角的內光暈

前一篇我們完成了具有內光暈的球體。

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

但是最大的問題,就是當鏡頭轉向時,會出現破綻:

Untitled

為什麼會有這個問題呢?

全視角的內光暈:我們先準備上一篇的程式碼

CodePen

https://ithelp.ithome.com.tw/upload/images/20221014/201425057c40T8eoEL.png

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

回顧一下上一篇,我們新增了以下Fragment Shader程式碼:

// index.html
<p id="fragmentShader">
+    varying vec3 vertexNormal;
    void main(void){
+      float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.));
+      vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
+      gl_FragColor=vec4(atmosphere,0.) + vec4(0.,0.,0.2,1.);
-      gl_FragColor=vec4(0.,0.,0.2,1.);
    }
</p>
<p id="vertexShader">
    varying vec3 vertexNormal;
    void main(void){
      vertexNormal = normal;
      gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
    }
</p>

全視角的內光暈:為什麼鏡頭移動到後面就會有破綻?

前一篇提到,指向正Z軸的法線向量,其數值最小。這是因為,我們透過函式dot(),計算了每一個像素其法線向量跟正Z軸向量的內積。兩者越接近,intensity越小。

float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.0));

如果我們從場景上方(正Y軸)往下俯瞰,示意圖大概長這樣子:

https://ithelp.ithome.com.tw/upload/images/20221014/20142505cX2VGIJAOG.png

下圖中,紅色是vertexNormal大致的方向,灰色是vec3(0.,0.,1.0) 方向,而內積計算兩向量相近程度。intensity最終計算結果可得到:

https://ithelp.ithome.com.tw/upload/images/20221014/20142505mTASyi0QIP.png

因為鏡頭剛好位在正Z軸,所以剛好可以看到中心數值最小,周圍數值最大。但如果鏡頭離開正Z軸,就可以看到另外一面並不是如此。下圖可以見到鏡頭不再於Z軸。

https://ithelp.ithome.com.tw/upload/images/20221014/20142505tO3VnQyW2W.png

Untitled

那解決的方法很簡單:

目前我們已經知道,我們只要提供dot()兩個向量(法線向量、位置向量),就可以兩向量的相似度,製作光暈。以目前的狀況來說,我們提供正z軸(0.,0.,1.)拿去跟法線向量計算,所以光暈只有從正z軸的方向看才有正確的光暈,從別的方向看就會破功。

那很簡單,我們就將鏡頭的位置,取代正z軸的位置,使得鏡頭不管往哪個方向看,都是正確的光暈。

那我們就試試看將鏡頭的數值,傳送到fragment shader中。

全視角的內光暈:將鏡頭位置傳送到Fragment Shader

在從javascript傳送鏡頭位置到Fragment Shader之前,先說明當前的程式碼。目前在three.js,我們建立了一個ShaderMaterial,它提供我們撰寫兩個shader。

const mat = new THREE.ShaderMaterial({
	fragmentShader: fragment,
	vertexShader: vertex
})

為了傳送數值,我們必須將數值放到參數裡面。最快的方法,就是加入uniform。如果你不知道uniform是什麼的話,我們有在「Day23: WebGL Shader——快認識速Shader並實作」使用過。如果你還是沒有印象為什麼要用uniform的話,我先在下面簡單解釋一下,再回來繼續討論。

全視角的內光暈:簡單解釋傳入參數的種類

我們可以從javascript 傳入參數到 vertex shader以及fragment shader,但傳入的參數有種類之分。傳入的種類我這邊介紹兩種最常用的:一種可以傳入buffer資料(可用來描述一連串位置、UV、Normal),稱作attribute;另一種可以傳入單一變數稱為uniform。

shader需要得知類型(不是型別喔)才可以運作。而three.js為了確保開發者傳入正確的類型,所以有了這樣的物件架構:

new THREE.ShaderMaterial({
	類別: { // 如uniform
		物件名稱: { // 如myPosition
			value: xxxx // 如0.5
		}
	}
})

透過上面的物件,three.js將可以幫我們把參數傳送到下面程式碼的myPosition中:

    uniform vec3 myPosition;
    void main(void){
        ...
    }

這就是簡單的解釋。我們繼續討論如何從javascript 傳入數值到vertex shader以及fragment shader:

全視角的內光暈:從javascript傳入參數

傳入的方法如下:

const control = new OrbitControls(camera, renderer.domElement);
+ const myPosition = control.object.position.clone().normalize()

const mat = new THREE.ShaderMaterial({
	fragmentShader: fragment,
	vertexShader: vertex,
+	uniforms: {
+		cameraPosition: {
+			value: myPosition
+		}
+	}
})

由上面可見,除了vertex shader以及fragment shader以外,我還抓取了鏡頭位置myPosition

  • 我用clone()避免影響原本的數值,再用normalize()使myPosition的長度變成只有一單位的單位向量。

如此一來,即使鏡頭起初位置隨便設定,都可以正確的呈現內光暈

// 鏡頭改成位在Y軸
+ camera.position.set(0, 30, 0);
- camera.position.set(14, 30, 20);

其內光暈也能正確顯示:

https://ithelp.ithome.com.tw/upload/images/20221014/20142505DcRd8bNtB5.png

全視角的內光暈:鏡頭移動時,動態傳入參數

OrbitControl可以傾聽點擊事件,加上點擊事件之後,就可以在每幀滑鼠移動時,傳入滑鼠位置到shader。

const addSphere = () => {
		...
		const myPosition = control.object.position.clone().normalize()
		const mat = new THREE.ShaderMaterial({
			...
		})
		...
	}

	const mesh = addSphere()

+	let cameraPosition = control.object.position.clone().normalize()
+	const onCameraChange = (event) => {
+		const control = event.target
+		cameraPosition = control.object.position.clone().normalize()
+		mesh.material.uniforms.orbitcontrolPosition.value = cameraPosition
+	}
+	control.addEventListener('change', onCameraChange)

如此一來,當鏡頭移動時,面向鏡頭的球面,其shader計算結果永遠都是最小。

成品

Untitled

CodePen

https://ithelp.ithome.com.tw/upload/images/20221014/20142505iip6xVUL3r.png

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

二、內光暈在地球的應用

我們有了內光暈,如果可以套用在地球上,就能夠相當完美。

  • 有內光暈的地球
    earth.gif

  • 沒有內光暈的地球
    Untitled

要怎麼製作具有內光暈的地球呢?有兩個方法:

  1. 用混合模式的作法,將光暈疊在地球上
  2. 將材質圖傳入shader,在shader上製作材質

我們使用第一個作法。

內光暈在地球的應用:準備程式碼

我們可以直接把我們在第十一天的程式碼搬過來用:

CodePen

https://ithelp.ithome.com.tw/upload/images/20221014/20142505Ms8cHob6cL.png

https://codepen.io/umas-sunavan/pen/yLjwNrQ?editors=0010

內光暈在地球的應用:大概介紹所準備的程式碼

基本上,就是將地球、雲以及其材質圖添加在

// 加入天球
const skydomeMaterial = ...
const skydomeGeometry = ...
const skydome = ...
scene.add(skydome);

// 新增環境光
const addAmbientLight = () => {
	...
}

// 新增點光
const addPointLight = () => {
	...
}

// 新增平行光
const addDirectionalLight = () => {
	...
}

addPointLight()
addAmbientLight()
addDirectionalLight()

// 加入地球
const earthGeometry = ...
const earthMaterial = ..
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);

// 加入雲
const cloudGeometry = ...
const cloudMaterial = ...
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
scene.add(cloud);

function animate() {
	...
// 旋轉物件
	earth.rotation.y +=0.005
	cloud.rotation.y +=0.004
	skydome.rotation.y += 0.001

}

添加之後,會先看到地球,因為地球比我們的內光暈球體還要大

https://ithelp.ithome.com.tw/upload/images/20221014/20142505FZjgvkA0sD.png

內光暈在地球的應用:放大光暈

使光暈的球體比地球大,就可以呈現光暈效果。

const addSphere = () => {
	const vertex = document.getElementById('vertexShader').innerHTML
	const fragment = document.getElementById('fragmentShader').innerHTML
+	const geo = new THREE.SphereGeometry(5.39, 60, 60)
-	const geo = new THREE.SphereGeometry(5, 60, 60)
	const myPosition = control.object.position.clone().normalize()
	...
}

雖然說此舉可以添加光暈,但也使得陸地跟海洋看不到了,因為被光暈的球體包覆住。

https://ithelp.ithome.com.tw/upload/images/20221014/201425056rpuZfnO7Q.png

內光暈在地球的應用:混合模式

為了解決這個問題,我們加上混合模式,並且設定光暈為半透明。

const addSphere = () => {
		const vertex = document.getElementById('vertexShader').innerHTML
		const fragment = document.getElementById('fragmentShader').innerHTML
		const geo = new THREE.SphereGeometry(5.39, 60, 60)
		const myPosition = control.object.position.clone().normalize()
		const mat = new THREE.ShaderMaterial({
+			transparent: true,
+			blending: THREE.AdditiveBlending,
			vertexShader: vertex,
			uniforms: {
				orbitcontrolPosition: {
					value: myPosition
				}
			}
		})
		const mesh = new THREE.Mesh(geo, mat)
		scene.add(mesh)
		return mesh
	}

內光暈在地球的應用:成品

Untitled

CodePen

https://ithelp.ithome.com.tw/upload/images/20221014/20142505NMbgUFZGgf.png

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

小結

由於Shader的概念很難解釋,在這幾篇文章中,我重複的用多種說法解釋Shader,就是希望能夠完整的幫助大家釐清Shader的概念。

我們成功的建立了具有光暈的地球,下一篇將介紹如何透過shader,製作邊框。

參考資料

GLSL 三种变量类型(uniform,attribute和varying)理解


上一篇
Day28: WebGL Shader—透過Shader製作光暈:光暈原理與多種變化形式
下一篇
Day30: WebGL Shader—製作物件的邊框
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言