资源说明
首先说明一点,这个demo的最终效果和选用的反射/折射贴图关系比较大,因为这个水的颜色是直接从环境采样得到的。这里我偷懒还是用了这个广为流传的天空盒(也就是从learning opengl 立方体贴图这里偷来的),其实并不是很合适,因为这个立方体贴图底面是水,实际水的底面应该是沙泥之类的,也就相当于我们透过水面看到水底的沙石、水里的鱼。
然后水的法线贴图还是从unreal的官方材质包偷来的,也就是T_Water_N,可以直接在Unreal中右键资源导出为TGA,除此之外还使用了一张柏林噪声贴图,也是材质包自带的,即T_Perlin_Noise_M。
开发环境:Qt + OpenGL
预备知识
该篇属于高级篇,需要对以下知识有所了解:
1.基本的光照计算
2.法线贴图(包括理解uv坐标,切线空间等概念)
3.反射、折射以及菲涅尔效应
概念引入
在游戏中,我们使用了许多动画表现方式来表现动态的画面,包括普通位移旋转动画、绑定动画、精灵动画、骨骼动画、顶点动画、morph动画等(关于一系列动画,在我填完渲染基础的坑后,会专门谈一谈)。除了二维的精灵动画外,其它三维的动画大多数都需要改变模型顶点所在的位置,来模拟物体在世界空间的动态。
还有一种值得一提的动画,也就是纹理uv动画,和改变模型顶点不一样,纹理动画改变的是每次采样的纹理坐标值,让纹理坐标值随着时间变化,也就是对uv做一个时间相关的扰动,从而让物体产生动态效果。该动画从原理上而言并没有什么难度,最重要的是要对uv动画有一个基本认识,毕竟,如果一个人对uv动画没有概念的话,他是很难想到要用这么一个东西来做自己想要的效果的。
在本例中,我们使用uv动画来制作我们的水面效果。该水面仅仅对法线贴图做了uv动画,并没有使用波形方程来改变顶点,可以应用于游戏中的一些简单水面,比如水桶、水池上方直接贴这么一个面片即可,像比较大型的湖泊、海洋这种对游戏整体画面效果影响较大的水体,还是做的稍微精细一点比较好。
关于uv,我们有几个常用的操作:
1.uv缩放:缩放纹理大小
2.uv偏移(panner) : 移动纹理位置
3.uv旋转 : 旋转纹理位置
4.uv扭曲
一般而言,展uv这项工作是由美术在软件中完成,并随着模型一起导入,不需要我们特别修改。在我们要做一些特殊效果的时候,才去做这些uv的操作。
实现细节
这里使用了菲涅尔效应 (折射 + 反射)从环境贴图获取颜色,本身没有为水体定义颜色,此处也可以自己加入别的颜色。本篇中主要对法线进行计算。
为了做出动画,首先我们需要一个时间。为了做出连续的效果,此处的时间是累积的系统时间。以下是我随便找的几个接口取的时间,实际项目中用大概率不靠谱,但基本是这么个意思:
QDateTime time = QDateTime::currentDateTime();
qint64 second = QDateTime::currentMSecsSinceEpoch();
static qint64 startTime = second;
float times;
second = second - startTime;
times = (float)second / 2000;
之后,把times传入shader即可。
program.setUniformValue("totalTime", times);
我们使用到的法线贴图是这么一张图:
接下来,我们这样计算水面的法线(请注意,以下关于法线的运算都是在切线空间中完成,运算完成后,我们再把法线转换到世界空间,因为反射、折射的运算我们是在世界空间中完成的):
(1) 以下绝大部分纹理坐标采样的计算,都是依照 uv = uv * 缩放值+ 偏移值 * 时间 这一公式进行的。
(2) 最终法线是由较大波浪的法线和较小波纹的法线混合得到的。
(3) 较大波浪的法线采样时,使用较小的缩放值 + 一定的纹理偏移值进行采样,这样能够得到较大的波动。此外,除了用当前时间采样一个基本纹理外,还可以再加上一个纹理扰动,纹理扰动也就是使当前时间沿着水面移动方向偏移,并取其正弦值,得到一个新的时间,用这一时间进行采样得到扰动纹理。
(4) 较小波纹的法线采样时,使用较大的缩放值+ 一定的纹理偏移值进行采样,这样能够得到较小的波动。之后,使用一个随机数(从噪声纹理中采样)与标准法线(即(0,0,1),蓝色的法线)进行线性混合得到最终结果,随机性增强可以使效果更细腻。
(5) 特别注意的是,我们在对两个法线进行混合的时候,不能简单的线性混合,而是要做一个矫正混合。(关于这一法线混合的计算原理,我目前还不是特别理解)
代码实现
Vertex Shader
#version 330 core
uniform mat4 ModelMatrix;
uniform mat4 IT_ModelMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ProjectMatrix;
attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec3 a_tangent;
attribute vec2 a_texcoord;
varying vec2 v_texcoord;
varying vec3 v_normal;
varying vec3 v_tangent;
varying vec3 worldPos;
void main()
{
gl_Position = ModelMatrix * a_position;
worldPos = vec3(gl_Position);
gl_Position = ViewMatrix * gl_Position;
gl_Position = ProjectMatrix * gl_Position;
v_texcoord = a_texcoord;
v_normal = mat3(IT_ModelMatrix) * a_normal;
v_tangent = mat3(ModelMatrix) * a_tangent;
}
Fragment Shader
#version 330 core
uniform sampler2D T_Water_N;
uniform sampler2D T_Perlin_Noise_M;
uniform samplerCube cubeMap;
uniform vec3 cameraPos;
uniform vec3 LightLocation;
uniform float totalTime;
varying vec3 v_normal;
varying vec2 v_texcoord;
varying vec3 v_tangent;
varying vec3 worldPos;
vec2 CalTexcoord(vec2 uv, vec2 scale, vec2 panner)
{
return uv * scale + panner;
}
vec3 UnpackNormal(vec3 normal)
{
vec3 N = normalize(v_normal);
vec3 T = normalize(v_tangent - N * v_tangent * N);
vec3 B = cross(N, T);
mat3 TBN = mat3(T,B,N);
return normalize(TBN * normal);
}
vec3 BlendAngleCorrectedNormals(vec3 baseNormal, vec3 additionalNormal)
{
baseNormal.b += 1;
additionalNormal *= vec3(-1, -1, 1);
vec3 normal = dot(baseNormal, additionalNormal) * baseNormal - baseNormal.b * additionalNormal;
return normalize(normal);
}
vec3 GetBaseNormal()
{
vec2 texcoord = CalTexcoord(v_texcoord,vec2(0.05, 0.08),totalTime * vec2(-0.03, -0.02));
vec3 normal = vec3(texture2D(T_Water_N, texcoord));
normal = normalize(2 * normal - 1);
return normal;
}
vec3 GetAdditionNormal()
{
float time = sin(worldPos.x / 150 + 0.4 * totalTime);
vec2 texcoord = CalTexcoord(v_texcoord,vec2(0.18, 0.15),time * vec2(-0.06,-0.04));
vec3 normal = vec3(texture2D(T_Water_N, texcoord));
normal = normalize(2 * normal - 1);
return normal;
}
float GetNoiseAlpha()
{
vec2 texcoord = 0.05 * v_texcoord;
vec4 noiseTex = texture2D(T_Perlin_Noise_M, texcoord);
float alpha = noiseTex.r;
return alpha * 0.3;
}
vec3 GetSmallWaveNormal(float noise)
{
vec2 texcoord = CalTexcoord(v_texcoord,vec2(0.75, 0.75),totalTime * vec2(-0.07, -0.07));
vec3 normal = vec3(texture2D(T_Water_N, texcoord));
normal = (1 - noise) * normal + noise * vec3(0, 0, 1);
normal = normalize(2 * normal - 1);
return normal;
}
void main()
{
vec3 ViewDir = normalize(cameraPos - worldPos);
vec3 lightDir = normalize(LightLocation - worldPos);
float noise = GetNoiseAlpha();
vec3 baseNormal = GetBaseNormal();
vec3 addNormal = GetAdditionNormal();
vec3 largeWaveNormal = BlendAngleCorrectedNormals(baseNormal, addNormal);
vec3 smallWaveNormal = GetSmallWaveNormal(noise);
vec3 normal = BlendAngleCorrectedNormals(largeWaveNormal, smallWaveNormal);
normal = UnpackNormal(normal);
float diffuse = 0.7 * clamp(dot(normal, lightDir), 0, 1);
float ambient = 0.1;
vec3 reflectDir = normalize(reflect(-lightDir,normal));
float specular = pow(clamp(dot(reflectDir,ViewDir),0,1),2.0);
vec3 R = reflect(-ViewDir,normal);
vec4 reflectedColor = textureCube(cubeMap, R);
vec3 T = refract(-ViewDir, normal, 0.9);
vec4 refractedColor = textureCube(cubeMap, T);
float fresnel = 0.4 + 0.6 * pow(1.0 - dot(ViewDir, normal), 6.0);
gl_FragColor = (mix(refractedColor, reflectedColor, fresnel) );
gl_FragColor = gl_FragColor *( specular + diffuse + ambient);
}