前一篇我們完成了具有內光暈的球體。
但是最大的問題,就是當鏡頭轉向時,會出現破綻:
為什麼會有這個問題呢?
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軸。
那解決的方法很簡單:
目前我們已經知道,我們只要提供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計算結果永遠都是最小。
https://codepen.io/umas-sunavan/pen/poVYJYx?editors=1010
我們有了內光暈,如果可以套用在地球上,就能夠相當完美。
有內光暈的地球
沒有內光暈的地球
要怎麼製作具有內光暈的地球呢?有兩個方法:
我們使用第一個作法。
我們可以直接把我們在第十一天的程式碼搬過來用:
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
}
https://codepen.io/umas-sunavan/pen/PoeLPad?editors=1010
由於Shader的概念很難解釋,在這幾篇文章中,我重複的用多種說法解釋Shader,就是希望能夠完整的幫助大家釐清Shader的概念。
我們成功的建立了具有光暈的地球,下一篇將介紹如何透過shader,製作邊框。
GLSL 三种变量类型(uniform,attribute和varying)理解