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 );
正确效果: