今天有羣友在交流羣裏問,如何根據gl_FragCoord在片元座標中計算當前片元的世界座標,因爲能從頂點着色器傳遞到片元着色器,大家都沒有在意這件事情,晚上正好無事也進行了一番探索,這是一個矩陣變換的逆過程。渲染管線中矩陣變換可參考我的這篇文章Webgl矩陣變換總結。
話不多說直接,上效果,估計有點蒙,解釋一下,初始是紅色立方體,然後過兩秒,開始進行比對,gl_FragCoord在片元着色器中計算世界的座標和頂點着色器傳遞到片元着色的世界座標,如果兩者在一定誤差範圍內,立方體就會變成黃色,下圖截取的是立方體的一部分。
上着色器代碼,vs和fs。
precision mediump float;
precision mediump int;
uniform mat4 modelViewMatrix; // optional
uniform mat4 projectionMatrix; // optional
attribute vec3 position;
varying vec3 vPosition;
void main() {
vPosition = (modelViewMatrix * vec4( position, 1.0 )).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
頂點着色器部分比較簡單,就是計算gl_Position和vPosition ,vPosition是相機空間的座標,不是世界空間的座標,偷個懶,變換到相機空間,也就能變換世界空間了,原理都一樣。
precision mediump float;
precision mediump int;
//投影矩陣的逆矩陣
uniform mat4 projectionMatrixInverse;
//視口變換矩陣的逆矩陣
uniform mat4 viewPortMatrixInverse;
//開始測試的標誌
uniform bool testFlag;
varying vec3 vPosition;
//在0.00001的誤差範圍內進行比較
bool compare(float valueA,float valueB){
if(valueA>(valueB-0.00001) && valueA<(valueB+0.00001)){
return true;
}else{
return false;
}
}
void main() {
//屏幕座標系變換到NDC標準設備空間
vec4 ndcPosition = viewPortMatrixInverse * vec4(gl_FragCoord.xyz,1.0);
//標準設備空間變換到剪裁空間
vec4 clipPosition = ndcPosition/gl_FragCoord.w;
//剪裁空間變換到相機空間
vec4 viewPosition = projectionMatrixInverse * clipPosition;
//vs傳遞過來相機空間的頂底座標和通過gl_FragCoord算出的相機空間進行比對
if( compare(viewPosition.x,vPosition.x)&&
compare(viewPosition.y,vPosition.y)&&
compare(viewPosition.z,vPosition.z)&& testFlag){
//黃色,計算正確顯示
gl_FragColor=vec4(1.0,1.0,0.0,1.0);
}else{
//紅色,默認顯示
gl_FragColor=vec4(1.0,0.0,0.0,1.0);
}
}
片元着色器中,主要對通過gl_FragCoord計算得出的相機空間座標和頂點着色器傳遞過來的相機空間座標進行比對,如果誤差在一定範圍內就認爲計算正確。加上誤差範圍主要是因爲浮點數誤差。
簡單說一下,如何通過gl_FragCoord計算相機空間的座標,gl_FragCoord是Webgl的內置變量,官方文檔中這麼形容。
簡單來說,x,y就是屏幕viewport中的座標,z就是變換到(0,1)的片元深度值,1/w就是透視除法時候,用到的w。那如何計算呢。計算流程是這樣的,屏幕座標系->ndc標準設備座標系(三個軸都在<-1,1>的範圍內)->剪裁座標系->相機座標系,每一步都乘對應的逆矩陣進行變換,透視除法的逆過程就是乘上w,也就是除gl_FragCoord.w,代碼中已經寫明註釋,可以自己想一想。
我是基於Three.js進行開發的,viewPortMatrixInverse可能並不是很好獲取,我給出代碼,後期我在說明推導過程,先給出關鍵的代碼。
function getViewPortMatrixInverse(viewPort, depthRange) {
var _viewPortMatrix = new THREE.Matrix4();
var _viewPortMatrixInverse = new THREE.Matrix4();
_viewPortMatrix.set(
viewPort.width / 2, 0, 0, viewPort.x + viewPort.width / 2,
0, viewPort.height / 2, 0, viewPort.y + viewPort.height / 2,
0, 0, (depthRange.far - depthRange.near) / 2, (depthRange.far + depthRange.near) / 2,
0, 0, 0, 1
);
_viewPortMatrixInverse.getInverse(_viewPortMatrix, true);
return _viewPortMatrixInverse;
}
然後再給出mesh的構建過程代碼。
var geometry = new THREE.BoxBufferGeometry();
viewPortMatrixInverse = getViewPortMatrixInverse(
{x: 0, y: 0, width: window.innerWidth, height: window.innerHeight},
{near: 0, far: 1}
);
camera.updateProjectionMatrix();
material = new THREE.RawShaderMaterial({
uniforms: {
projectionMatrixInverse: {value: camera.projectionMatrixInverse},
viewPortMatrixInverse: {value: viewPortMatrixInverse},
testFlag: {value: false}
},
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent,
side: THREE.DoubleSide,
transparent: true
});
var mesh = new THREE.Mesh(geometry, material);
好了,到這裏就結束了,該洗洗睡了,可能寫的漏洞百出,請各位大佬多指點,可以加我的技術交流羣指點,嘻嘻,都是做3D的同志,webgl,opengl,vulkan,unity,ue4等等。
忘了說testFlag,這個的作用就是爲了顯示計算結果的正確性,就是這樣。
setTimeout(function () {
material.uniforms["testFlag"].value = true;
}, 2000);
睡覺Zzzzzzzz。