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

但是最大的問題,就是當鏡頭轉向時,會出現破綻:
.gif)
為什麼會有這個問題呢?
CodePen

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軸)往下俯瞰,示意圖大概長這樣子:

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

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

.gif)
那解決的方法很簡單:
目前我們已經知道,我們只要提供dot()兩個向量(法線向量、位置向量),就可以兩向量的相似度,製作光暈。以目前的狀況來說,我們提供正z軸(0.,0.,1.)拿去跟法線向量計算,所以光暈只有從正z軸的方向看才有正確的光暈,從別的方向看就會破功。
那很簡單,我們就將鏡頭的位置,取代正z軸的位置,使得鏡頭不管往哪個方向看,都是正確的光暈。
那我們就試試看將鏡頭的數值,傳送到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:
傳入的方法如下:
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);
其內光暈也能正確顯示:

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計算結果永遠都是最小。
.gif)

https://codepen.io/umas-sunavan/pen/poVYJYx?editors=1010
我們有了內光暈,如果可以套用在地球上,就能夠相當完美。
有內光暈的地球.gif)
沒有內光暈的地球.gif)
要怎麼製作具有內光暈的地球呢?有兩個方法:
我們使用第一個作法。
我們可以直接把我們在第十一天的程式碼搬過來用:

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
}
添加之後,會先看到地球,因為地球比我們的內光暈球體還要大

使光暈的球體比地球大,就可以呈現光暈效果。
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()
	...
}
雖然說此舉可以添加光暈,但也使得陸地跟海洋看不到了,因為被光暈的球體包覆住。

為了解決這個問題,我們加上混合模式,並且設定光暈為半透明。
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
	}
.gif)

https://codepen.io/umas-sunavan/pen/PoeLPad?editors=1010
由於Shader的概念很難解釋,在這幾篇文章中,我重複的用多種說法解釋Shader,就是希望能夠完整的幫助大家釐清Shader的概念。
我們成功的建立了具有光暈的地球,下一篇將介紹如何透過shader,製作邊框。
GLSL 三种变量类型(uniform,attribute和varying)理解