Avatar 引擎中的水面渲染,是用一种比较高效并且真实感强的方法实现的。水面的波纹模拟没有采用顶点波动方式,而是使用法向贴图来模拟。其效果可参见之前的几篇文章的图片。
首先需要实现的是水面倒影,水面倒影的渲染需要把摄像机置于水面的另一面对称位置,然后渲染整个场景至纹理(render to texture)。一般需要使用平面裁剪将水面以下的物体裁剪掉,避免错误的倒影,但在本方法中使用了一种特殊的方法,通过修改投影矩阵将视截体的裁剪近平面与水面重合,从而将水面下的物体裁剪掉,修改后的投影矩阵也叫近斜平面裁剪投影矩阵。计算方法如下
CMatrix4& CMatrix4::ObliqueNearPlaneClipping(const CMatrix4& proj, const CPlane& clip) {
float x = ((clip.m_fNormal[0] > 0? 1: (clip.m_fNormal[0] < 0? -1: 0)) + proj.m_fValue[8]) / proj.m_fValue[0];
float y = ((clip.m_fNormal[1] > 0? 1: (clip.m_fNormal[1] < 0? -1: 0)) + proj.m_fValue[9]) / proj.m_fValue[5];
float z = -1.0f;
float w = (1.0f + proj.m_fValue[10]) / proj.m_fValue[14];
float scale = 2.0f / (clip.m_fNormal[0] * x + clip.m_fNormal[1] * y + clip.m_fNormal[2] * z + clip.m_fDistance * w);
// 修改投影矩阵的第三行
m_fValue[0] = proj.m_fValue[0];
m_fValue[1] = proj.m_fValue[1];
m_fValue[2] = clip.m_fNormal[0] * scale;
m_fValue[3] = proj.m_fValue[3];
m_fValue[4] = proj.m_fValue[4];
m_fValue[5] = proj.m_fValue[5];
m_fValue[6] = clip.m_fNormal[1] * scale;
m_fValue[7] = proj.m_fValue[7];
m_fValue[8] = proj.m_fValue[8];
m_fValue[9] = proj.m_fValue[9];
m_fValue[10] = clip.m_fNormal[2] * scale + 1.0f;
m_fValue[11] = proj.m_fValue[11];
m_fValue[12] = proj.m_fValue[12];
m_fValue[13] = proj.m_fValue[13];
m_fValue[14] = clip.m_fDistance * scale;
m_fValue[15] = proj.m_fValue[15];
return *this;
}
CMatrix4 内部为列优先排列,同OpenGL
水面的几何结构是一个平面,四个顶点两个三角形,至于如何产生波浪,秘密全在 shader 中
attribute vec4 aPosition;
attribute vec4 aNormal;
attribute vec2 aTexCoord;
attribute vec4 aColor;
uniform mat4 uProjMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uModelMatrix;
uniform float uWaveLength;
uniform vec2 uWaveMovement;
uniform vec4 uLightPos;
varying vec2 vWavesTexCoord;
varying vec3 vTexCoord;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vLightPos;
void main()
{
mat4 modelView = uViewMatrix * uModelMatrix;
vec4 position = modelView * aPosition;
vec4 normal = normalize(modelView * aNormal);
vec4 finalPos = uProjMatrix * position;
vWavesTexCoord = (aPosition.xy / uWaveLength) + uWaveMovement;
vTexCoord.x = 0.5 * (finalPos.w + finalPos.x);
vTexCoord.y = 0.5 * (finalPos.w + finalPos.y);
vTexCoord.z = finalPos.w;
vPosition = position.xyz;
vNormal = normal.xyz;
vLightPos = uViewMatrix * uLightPos;
gl_Position = finalPos;
}
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
uniform sampler2D uTexture[2];
uniform vec4 uLightColor;
uniform float uWaveHeight;
uniform vec4 uWaterColor;
varying vec2 vWavesTexCoord;
varying vec3 vTexCoord;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vLightPos;
void main()
{
vec4 normalColor = texture2D(uTexture[0], vWavesTexCoord.xy);
vec2 waveMovement = uWaveHeight * (normalColor.xy - 0.5);
vec2 projTexCoord = clamp((vTexCoord.xy / vTexCoord.z) + waveMovement, 0.0, 1.0);
vec4 reflectiveColor = texture2D(uTexture[1], vec2(projTexCoord.x, -projTexCoord.y));
vec3 vPos = normalize(vPosition);
// 光照计算
float specular = 0.0;
vec3 lightVec = vLightPos.xyz;
if (vLightPos.w != 0.0)
{
lightVec = normalize(lightVec - vPosition);
}
vec3 r = reflect(lightVec, vNormal);
specular = pow(max(0.0, dot(r, vPos)), 64.0) * 0.5;
vec4 color = uWaterColor * reflectiveColor + vec4(uLightColor.rgb, 1.0) * specular;
// 反射系数
float fresnel = 1.0 - abs(dot(vNormal, -vPos));
color.a = uWaterColor.a * fresnel + specular;
gl_FragColor = color;
}
在 fragment shader 中计算 fresnel 是为了实现远处的水面反射光强而透射光弱,近处的水面看上去透光强而反射光弱,这也就是菲涅尔效应。