Flowmap in OpenGL | OpenGL中Flowmap水面

笔试的时候有这么一道题,讲模拟水面,用FlowMap。当时没有太懂,就随便回答的,正好这次图形学大作业有机会来实现一下。

FlowMap是用来解决水面流动方向单一问题的方法,SIGGRAPH2010上有篇文章讲的V社做Portal2时使用Flowmap的方法,见此。V社官网上也有一篇文章讲水面内容相似。他们做好场景以后用Houdini做了一个Flowmap,也是蛮厉害的。还有一篇博客,不过讲的不是很清楚,没说FlowMapOffset0和FlowMapOffset1怎么算。

注:以下代码是GLSL version 3.3

最朴素的scrolling diffuse map,就是根据时间改变uv坐标了。

...
in vec2 uv;

out vec4 color;

uniform vec4 tiling;
uniform vec2 direction;
uniform float _time;
uniform float transparency;

uniform sampler2D texture_diffuse;

void main(){
    ...
    vec4 texcolor = texture(texture_diffuse, uv*tiling.xy + tiling.zw + direction * _time);
    color = vec4(texcolor.xyz, transparency);
}

...

当然这有些愚蠢,更真实一点的是scrolling normal map

...
in vec2 uv;
in vec3 WorldPos;

out vec4 color;

uniform vec3 ViewPos;
uniform vec4 tiling;
uniform vec2 direction;
uniform float _time;
uniform float transparency;

uniform sampler2D texture_normal;
uniform samplerCube skybox;

void main(){
    ...
    vec4 texnormal = texture(texture_normal, uv*tiling.xy + tiling.zw + direction * _time);

    //unpack normal map
    vec3 normal;
    normal.xz = texnormal.xy * 2.0f - 1.0f;
    normal.y = sqrt(1 - normal.x * normal.x - normal.z * normal.z);

    //sample skybox
    vec3 viewDir = normalize(WorldPos - ViewPos);
    vec3 refDir = reflect(viewDir, normal);
    vec4 skyCol = texture(skybox, refDir);

    color = vec4(skyCol.xyz, transparency);
}

...

这样就有一个bug,水流只能沿着特定的uv方向流动,没法改变流动方向。
解决的小trick叫FlowMap,就是一张贴图,用两个通道(比如RG,或者黑白)定义某处水流方向。

…略去大部分code了,未说明的变量定义同上文

uniform vec4 flowTiling;//flowmap的平铺尺寸
uniform sampler2D texture_flowmap;//flowmap

void main(){
    //unpack and sample flowmap
    vec2 flowSpeed = (texture(texture_flowmap, uv * flowTiling.xy + flowTiling.zw).rg * 2.0f - 1.0f;
    vec4 texnormal = texture(texture_normal, uv*tiling.xy + tiling.zw + flowSpeed * _time);
    ...
}
...

但这样是有bug的,这个法线贴图在时间久了以后就会跪掉,出现奇怪的法线。 解决方法是让时间循环,比如这样:

...
void main(){
    ...
    float phase0 = _time - floor(_time);
    vec4 texnormal = texture(texture_normal, uv*tiling.xy + tiling.zw + flowSpeed * phase0);
    ...
}
...

这样法线贴图就不会出问题了,但又有了bug,水流的周期性太强,也就是一个周期后会跳跃变回周期开始的样子,水面会震颤一下。
FlowMap这里的trick是用两个normal map,都按着同样方向移动。因为两个normal map也都是有周期性问题的,所以我们混合一下它们就好了。
第二个normal map的周期比第一个晚半个周期,这样错动半个周期混合。

那个SIGGRAPH的文章给了这两张图讲的还是比较清楚的,前面提到的另一篇博客没有提两个法线贴图周期的相位差,让我悟了好久。

vlachos-siggraph10-waterflow
vlachos-siggraph10-waterflow2

...
void main(){
    ...
    //using a noise map to add randomness for blending
    float cycleOffset = texture(texture_noise, uuv * flowTiling.xy + flowTiling.zw).r;

    float prePhase = cycleOffset * 0.5f + _time;
    float phase0 = prePhase - floor(prePhase);
    //phase1 is half circle later than phase0
    float phase1 = prePhase + 0.5f - floor(prePhase + 0.5f);
    //phase0 range from 0 - 1, lerpfactor has a zigzag shape between 0 - 1 
    float lerpfactor = (abs(.5f - phase0) / .5f);

    vec4 normalT0 = texture(texture_normal0, uv * tiling.xy + tiling.zw + flowspeed * phase0);
    vec4 normalT1 = texture(texture_normal1, uv * tiling.xy + tiling.zw + flowspeed * phase1);
    vec4 normalMix = mix(normalT0, normalT1, lerpfactor);
    vec3 normalT;
    normalT.xz = normalMix.xy * 2.0f - 1.0f;
    normalT.y = sqrt(1 - normalT.x * normalT.x - normalT.z * normalT.z);
    ...
}
...

当然做到这里水面流动的效果就有了,还需要做一下反射。skybox的反射好办,直接在shader里,用reflect算出光线方向采样一下skybox就好了。

vec3 viewDir = normalize(WorldPos - ViewPos);
vec3 WorldNormal = normalize(Normal);
vec3 refDir = reflect(viewDir, normalT);
vec4 skyCol = texture(skybox, refDir);

对物体的反射稍微麻烦一点。有一套Youtube教程讲这个思路,简单说就是用RenderToTexture把反射和折射的部分渲染出来,然后放到shader里扰动制作一个假的反射效果。有关RTT有教程。另外还有一个讲水面的博客比较精髓,比如如何做clip plane,如何做反射。得到RenderTexture以后我直接参考了GPU Gem的Generic Refraction Simulation这一章写Shader。

首先带着截平面渲染,原理是用Ax + By + Cz + D = 0定义一个平面,然后直接 (x,y,z) · (A, B, C) + D > 0就能判断某点在这个平面上还是下。代码这个样子:

uniform vec4 clip_plane;
...
void main()
{ 
    float clipPos = dot (WorldPos, clip_plane.xyz) + clip_plane.w;
    if (clipPos < 0.0) {
        discard;
    }
...
}

渲之前把clip_plane的参数传进来就行了,传(0,0,0,0)就是没有截平面。

之后渲染倒影也好办,直接换一个ModelMatrix,Y方向Scale -1就可以

glm::mat4 model = glm::mat4();
model = glm::scale(model, glm::vec3(1, -1, 1));
glUniformMatrix4fv(glGetUniformLocation(terrainShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));

然后渲染纹理,定义一个Framebuffer和两个texture绑定上

// Framebuffers
    GLuint reflectionBuffer;
    glGenFramebuffers(1, &reflectionBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, reflectionBuffer);
    // Create a color attachment texture

    GLuint reflectionTexture;
    glGenTextures(1, &reflectionTexture);
    glBindTexture(GL_TEXTURE_2D, reflectionTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, screenWidth, screenHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, reflectionTexture, 0);

    GLuint refractionTexture;
    glGenTextures(1, &refractionTexture);
    glBindTexture(GL_TEXTURE_2D, refractionTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, screenWidth, screenHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, refractionTexture, 0);

然后在渲染循环里

glBindFramebuffer(GL_FRAMEBUFFER, reflectionBuffer);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
//draw reflection

//render reflection texture
glDrawBuffer(GL_COLOR_ATTACHMENT1);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
//draw reflection
glBindFramebuffer(GL_FRAMEBUFFER, 0);

我这没有用stentil buffer,而是直接渲染了RGBA四个通道。。。。A通道就是stentil了,不知道性能有没有影响。

这俩texture传到水的shader里,采样要注意一下,要声明一下gl_FragCoord来计算屏幕空间坐标。然后要算一个texelSize来计算贴图坐标。然后这里rendertexture的扰动就直接用上面flowmap的法线xz分量了,所以比较粗略和假,然而效果凑合。

in vec4 gl_FragCoord;
void main(){
    ...
    vec3 vRefrBump = normalT.xyz * vec3(0.075, 1.0, 0.075);
    vec3 vReflBump = normalT.xyz * vec3(0.02, 1.0, 0.02);

    vec2 texelSize = 1.0 / vec2(textureSize(texture_refl, 0));
    vec4 refrColor = texture(texture_refr, gl_FragCoord.xy * texelSize + vRefrBump.xz);
    vec4 reflColor = texture(texture_refl, gl_FragCoord.xy * texelSize + vReflBump.xz);
    ...
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注