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

小编 2026-06-21 阅读:1513 评论:0
资源说明         首先说明一点,这个demo的最终效果和选用的反射/折射贴图关系比较大,因为这个水的颜色是直接从环境采样得到的。这里我偷懒还是用了这个广为流传的天空盒(也就是从learning...

\"\"

资源说明

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

 

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表