https://threejs.org/examples/?q=ocean#webgl_shaders_ocean
思路
從鏡面上看到反射的物像是因爲物體表面的光線經鏡面反射到達了人眼(camera)。而根據初中物理,這可以想象爲鏡面的另一端也有一個相機,稱爲mirror camera,這兩個camera的連線垂直於反射面(平行於法向量)。要得到反射面上的倒影,就是要找到這個mirror camera的位置,然後從它的視角渲染一幅圖,將這個圖作爲texture貼到反射面上即可。
官方例子中還用到了法線貼圖增強真實感,下次再學。。。
關鍵代碼
https://github.com/mrdoob/three.js/blob/master/examples/js/objects/Water.js
shader中涉及倒影實現的不多,主要看這個函數onBeforeRender
,它實現了根據物理原理計算mirror camera的3個基本參數:世界位置, 上向量, 目標位置(lookAt)。
three.js裏面的camera是沒有target屬性的,.lookAt()
方法只能用於設置target,不能獲取。
下面簡單解釋:
(注意這個函數裏rotationMatrix
的值先是反射面的旋轉變換,後來是camera的旋轉變換)
scope.onBeforeRender = function ( renderer, scene, camera ) {
mirrorWorldPosition.setFromMatrixPosition( scope.matrixWorld );
// 反射面的世界位置
cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );
// camera的世界位置
rotationMatrix.extractRotation( scope.matrixWorld );
// scope就是反射面的mesh對象
normal.set( 0, 0, 1 );
normal.applyMatrix4( rotationMatrix );
這裏是設置一些初始值。normal
的計算我猜想是這樣的:首先,這個海面是一個PlaneGeometry
,初始存在xOy平面,法向量爲(0,0,1),然後手動地根據該海面的mesh的旋轉去旋轉它。
view.subVectors( mirrorWorldPosition, cameraWorldPosition );
view
是mirror camera的世界位置,這裏首先求了一個從camera指向反射面位置的向量。
// Avoid rendering when mirror is facing away
if ( view.dot( normal ) > 0 ) return;
如果此時的view
和反射面法向量點乘>0即夾角小於90°,說明當前camera看向的方向和法向量同向,也就是說camera是看不到海面的,就不用渲染了。
view.reflect( normal ).negate();
view.add( mirrorWorldPosition );
這個畫個圖能明白,最後計算出來的view就會指向mirror camera的位置了。
然後計算target。
rotationMatrix.extractRotation( camera.matrixWorld );
lookAtPosition.set( 0, 0, - 1 );
lookAtPosition.applyMatrix4( rotationMatrix );
lookAtPosition.add( cameraWorldPosition );
target.subVectors( mirrorWorldPosition, lookAtPosition );
target.reflect( normal ).negate();
target.add( mirrorWorldPosition );
lookAtPosition.set( 0, 0, - 1 );
這一句我不太確定是不是因爲camera的默認target是(0,0,-1)。 要加上cameraWorldPosition
的原因大概是three.js中的lookAt
得接受世界座標吧。這部分代碼還不是太懂。。。
mirrorCamera.position.copy( view );
mirrorCamera.up.set( 0, 1, 0 );
mirrorCamera.up.applyMatrix4( rotationMatrix );
mirrorCamera.up.reflect( normal );
mirrorCamera.lookAt( target );
這裏計算了上向量,跟計算反射面法向量原理差不多,這裏的rotationMatrix
是對應camera的rotation的。
mirrorCamera.far = camera.far; // Used in WebGLBackground
mirrorCamera.updateMatrixWorld();
mirrorCamera.projectionMatrix.copy( camera.projectionMatrix );
textureMatrix
在shader中用於計算texture的座標,目測是實現了把[-1,1]映射到[0,1]
// Update the texture matrix
textureMatrix.set(
0.5, 0.0, 0.0, 0.5,
0.0, 0.5, 0.0, 0.5,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0
);
textureMatrix.multiply( mirrorCamera.projectionMatrix );
textureMatrix.multiply( mirrorCamera.matrixWorldInverse );
下面一部分是對這個mirror texture進行裁剪的,在官方例子中如果不執行這一段,在球體靠近水面時, 倒影就像會浮出水面一樣。(還沒有仔細看這段。。
// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
mirrorPlane.setFromNormalAndCoplanarPoint( normal, mirrorWorldPosition );
mirrorPlane.applyMatrix4( mirrorCamera.matrixWorldInverse );
clipPlane.set( mirrorPlane.normal.x, mirrorPlane.normal.y, mirrorPlane.normal.z, mirrorPlane.constant );
var projectionMatrix = mirrorCamera.projectionMatrix;
q.x = ( Math.sign( clipPlane.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ];
q.y = ( Math.sign( clipPlane.y ) + projectionMatrix.elements[ 9 ] ) / projectionMatrix.elements[ 5 ];
q.z = - 1.0;
q.w = ( 1.0 + projectionMatrix.elements[ 10 ] ) / projectionMatrix.elements[ 14 ];
// Calculate the scaled plane vector
clipPlane.multiplyScalar( 2.0 / clipPlane.dot( q ) );
// Replacing the third row of the projection matrix
projectionMatrix.elements[ 2 ] = clipPlane.x;
projectionMatrix.elements[ 6 ] = clipPlane.y;
projectionMatrix.elements[ 10 ] = clipPlane.z + 1.0 - clipBias;
projectionMatrix.elements[ 14 ] = clipPlane.w;
eye.setFromMatrixPosition( camera.matrixWorld );
下面就是對render的一些設置,關鍵執行renderer.render( scene, mirrorCamera, renderTarget, true );
這一句即可。
var currentRenderTarget = renderer.getRenderTarget();
var currentVrEnabled = renderer.vr.enabled;
var currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
scope.visible = false;
renderer.vr.enabled = false; // Avoid camera modification and recursion
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
renderer.render( scene, mirrorCamera, renderTarget, true );
scope.visible = true;
renderer.vr.enabled = currentVrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
renderer.setRenderTarget( currentRenderTarget );
};
學習心得(tucao)
這兩天想把這個效果用到期末proj上,我確定我已經基本看懂了反射部分的代碼,於是把它抽了出來,但發現我做的效果是錯的,目測主要是texture的變換不正確:
百思不得其解,看了十幾遍關鍵部分的代碼,我覺得我已經和例子做的一模一樣了。還是不行,於是找了原作者之一的一個簡化版來看:
http://stemkoski.github.io/Three.js/FlatMirror-Water.html
差別並不大,不過這個版本感覺應該正確一些,第一個版本里面有些地方不知道爲什麼要調用negate()
。於是我把它下載到本地運行,打算看看到底是哪裏出錯了。
經過一輪鼓搗之後跑起來了,但居然運行出來的投影也是錯的:
黑人問號臉??
後來想到可能是three.js版本問題,把作者網站上的拷貝過來就。。好了。。:
猜想問題可能出在這兩行代碼:
this.mirrorWorldPosition.getPositionFromMatrix( this.matrixWorld );
this.cameraWorldPosition.getPositionFromMatrix( this.camera.matrixWorld );
getPositionFromMatrix
在新版本中調用會提示你使用另一個函數setFromMatrixPosition
。但提示說只是進行了renamed
,也不確定是不是它的鍋。
而且官方的ocean運行是沒有問題的。可能那幾個negate()就是修復這個問題的吧(雖然並不知道是什麼問題
所以還是模仿官網的例子吧(:з」∠)..
今天終於發現了錯誤,出在mirror camera的位置計算上。本來我寫的是:
var seedir = seaworldpos.clone().sub(camworldpos);
if (seedir.dot(seanorm) <= 0) {
mirworldpos = seedir.reflect(seanorm);
mirworldpos.add( seaworldpos );
正確的是:
var seedir = seaworldpos.clone().sub(camworldpos);
if (seedir.dot(seanorm) <= 0) {
mirworldpos = seedir.reflect(seanorm).negate(); // 要反過來
mirworldpos.add( seaworldpos );
正確效果: