[OpenGL] 纹理高级篇 - 纹理uv动画 水面篇

资源说明

        首先说明一点,这个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);
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值