继续OpenGL的大作业系列—— 今天的内容是Terrain Engine,渲了一个海岛的小场景。Shading在作业基础要求上用各种Trick做了一丢丢提升,但整体感塑料感还是很强,之后有空的话把水面shading写一下~
HeightMap -> Model
基础part我们就快快略过! 高度图之前做地形已经很熟悉了,我们在顶点着色器中去处理高度图的采样,所以顶点的密度会直接影响地形的精度。 为了偷懒不用再读一张high res的plane mesh,这里直接手写了一个quad的顶点数据,以便水面、地形的复用。
float quadVertices[] = {
//position //texture coordinate
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
1.0f, 0.0f, 1.0f, 1.0f, 1.0f
};
我使用TER_WIDTH和TER_UNIT两个参数来控制模型的尺寸和精度,记得循环迭代时也将i、j和总数count告诉顶点着色器,以便计算采样heightmap的坐标。 这里要注意一下平移和缩放的顺序。
//IN RENDER LOOP
glBindVertexArray(quadVAO);
int count = TER_WIDTH/TER_UNIT;
terrainShader.setInt("count", count);
for(int i = 0; i< count; i++){
for(int j = 0; j< count; j++){
glm::vec3 offset = glm::vec3(i*TER_UNIT - TER_WIDTH/2.0, -0.87f, j*TER_UNIT - TER_WIDTH/2.0);
glm::mat4 m = glm::mat4(1.0f);
m = glm::translate(m, offset);
m = glm::scale(m, glm::vec3(TER_UNIT,TER_HEIGHT,TER_UNIT));
terrainShader.setMat4("model", m);
terrainShader.setVec2("index", glm::vec2(i,j));
glDrawArrays(GL_TRIANGLES, 0, 6);
}
}
顶点着色器中,我们计算出每个quad的坐标,采样hightmap后赋值给顶点位置的y值即可。这个材质坐标也要传递给片元着色器,以便采样colormap等。
void main()
{
textureCoord = vec2((index.x+aCoord.x)/count,(index.y+aCoord.y)/count);
float yValue = texture(maskMap, textureCoord).x * yScale;
vec3 localPos = aPos;
localPos.y += yValue;
Position = vec3(model * vec4(localPos, 1.0));
gl_Position = projection * view * model * vec4(localPos, 1.0);
}
80年代复古shading
和直接搬了LearnOpenGL的轮子,就也不赘述了。 如果使用作业提供的素材,就可以得到如同图形学刚刚出现时期的渲染画面——
素材是美丽shading的基础!
显然二女士不会止步于此(也为了之后能放进作品集...),WorldCreator启动,来套个island模板。 (wc的这个光追渲染真是美丽得太多了,导致后面在opengl里怎么写怎么觉得丑........) 除了heightmap和colormap,记得也导出Normal来便于计算光照。(虽然也可以在顶点着色器算,但是懒。)
从次时代回到原始时代,从PBR退化为Blinn-Phone。。 这里把roughnessmap当做specularmap用了一下(...),不推荐哈。 另外安装资料要求加了一下detailmap,虽然我们worldcreator导出的素材已经蛮高清的啦。
因为对shading没啥强制要求,就不想写pbr了,但还可以再用点trick强化一下渲染效果,比如烘个光照。 这里用的是blender的bake to texture,还是很方便的!用houdini应该也可以做。 (blender的渲染没咋用过,没找到在哪里调整平行光的软阴影,所以——嘿嘿,软边缘是我ps高斯模糊抹的..) 这里用到的几张灰度数据存进了同一张map,结构如图。
lowlow水面
作业要求只需要实现动态纹理,在此基础上加了一点水面下的可视效果+边缘泡沫,这些都是基于深度的效果,由于我们的深度图是现成的,实现起来就很简单啦。 理论上,折射和反射应该先渲染到帧缓存,然后再用噪声干扰后采样,但先偷懒下直接用透明度混合水水。水下可视程度的衰减是指数增长的,来简单pow一个。 有几个数据稍微说明一下:uv2用于采样高度图,6.f是地形尺寸,忘记给uniform了;beach是试出来的当前水面的高度,每张高度图因为归一化可能会不太一样。
//wave.fs
void main()
{
float speed = 0.1f;
float scale = 40.0f;
vec2 uv1 = textureCoord * scale;
uv1 += speed*frametime;
vec2 uv2;
uv2.x = clamp((Position.x)/6.f +0.5,0.0,1.0);
uv2.y = clamp((Position.z)/6.f +0.5,0.0,1.0);
float depth = texture(maskMap,uv2).r;
float beach = 0.47;
depth = clamp(beach - depth,0.0,1.0);
depth = depth*(1.0/beach);
float foamheight = 0.1;
float foam = smoothstep(foamheight,0.0,depth);
float alpha = 0.6 + 0.4 *smoothstep(0.0,0.5,pow(depth*2.0,2.0))+ foam*0.3;
alpha = clamp(alpha,0.0,1.0);
vec3 color = texture(wave,uv1).rgb;
vec3 shallowcolor = vec3(0.08,0.28,0.47);
vec3 deepcolor = vec3(0.06,0.25,0.47);
color = mix(mix(shallowcolor,deepcolor,smoothstep(0.0,0.7,depth)),color,0.5);
color = color+foam*0.7;
FragColor = vec4(color,alpha);
}
水面反射
按照之前的尽量偷懒不用帧缓冲的偷懒原则(?),我们完整的场景分为了五次绘制: 关于gl特性的一些配置如下图,比较关键的部分我标为了高亮~ 首先是关于模板缓冲,在水面绘制时写入模板,反射绘制时只在模板区域绘制。这里一定记得在glClear时,要将StencilMask改为0xFF,不然Clear是无法起效的~(之前魔方也遇到了这个问题!)
另外深度缓冲在绘制反射前清空了一次缓冲,以便倒影中的天空和地形不会产生冲突。
为了稍微提升一些些效率,我开启了面剔除,但注意绘制反射时我们给它上下翻转了一下(glm::scale(,glm::vec3(1.0,-1.0,1.0))),所以绘制的其实是反面;天空盒因为在盒子内部所以也是反面。 反射的山地绘制时,记得切除掉水下的部分,可以用discard命令(类似alphaclip?)。如果只是将水下部分alpha设置为0的话,还是会写入深度缓冲,就会影响后面背景的绘制。这里反射部分的由于不是画面中心,用了比较低的网格精度,带来的问题就是用高度图的值去clip时,由于顶点的精度不够,裁切出来不是一条等高线,犬牙呲互的.. 所以还针对边缘进行了一下透明度混合,久违地用回了smoothstep()~