笔试的时候有这么一道题,讲模拟水面,用FlowMap。当时没有太懂,就随便回答的,正好这次图形学大作业有机会来实现一下。
FlowMap是用来解决水面流动方向单一问题的方法,SIGGRAPH2010上有篇文章讲的V社做Portal2时使用Flowmap的方法,见此。V社官网上也有一篇文章讲水面内容相似。他们做好场景以后用Houdini做了一个Flowmap,也是蛮厉害的。还有一篇博客,不过讲的不是很清楚,没说FlowMapOffset0和FlowMapOffset1怎么算。
注:以下代码是GLSL version 3.3
最朴素的scrolling diffuse map,就是根据时间改变uv坐标了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
... 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了,未说明的变量定义同上文
1 2 3 4 5 6 7 8 9 10 11 |
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的,这个法线贴图在时间久了以后就会跪掉,出现奇怪的法线。 解决方法是让时间循环,比如这样:
1 2 3 4 5 6 7 8 9 |
... 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的文章给了这两张图讲的还是比较清楚的,前面提到的另一篇博客没有提两个法线贴图周期的相位差,让我悟了好久。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
... 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就好了。
1 2 3 4 5 |
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就能判断某点在这个平面上还是下。代码这个样子:
1 2 3 4 5 6 7 8 9 10 11 |
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就可以
1 2 3 4 |
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绑定上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 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); |
然后在渲染循环里
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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分量了,所以比较粗略和假,然而效果凑合。
1 2 3 4 5 6 7 8 9 10 11 12 |
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); ... } |