[OpenGL ES 08]Per-Pixel Light及卡通效果
[OpenGL ES 08]Per-Pixel Light及卡通效果
罗朝辉 (http://www.cnblogs.com/kesalin/)
本文遵循“署名-非商业用途-保持一致”创作公用协议
这是《OpenGL ES 教程》的第九篇,前八篇请参考如下链接:
[OpenGL ES 01]iOS上OpenGL ES之初体验
[OpenGL ES 02]OpenGL ES渲染管线与着色器
[OpenGL ES 03]3D变换:模型,视图,投影与Viewport
[OpenGL ES 04]3D变换实践篇:平移,旋转,缩放
[OpenGL ES 05]相对空间变换及颜色
[OpenGL ES 06]使用VBO:顶点缓存
[OpenGL ES 07-1]光照原理
[OpenGL ES 07-2]Per-Vertex Light及深度缓存
前言
本文是基于前文《光照原理》以及《Per-Vertex Light及深度缓存》两篇文章的,如果你还不熟悉光照相关的基础知识,请先阅读那两篇文章。在今天的这篇文章中,我们来研究 Per-Pixel 光照效果以及卡通效果。Per-Pixel 光照效果就是在片元着色阶段针对每个像素进行光照计算,而卡通效果是将散射光因子“分级”从而不再是连续(打个比方说,考试成绩上百分制是连续的,而分级制:好/良好/及格/不及格就不是连续的),这样就能获得漫反射跳跃的卡通效果。Per-Pixel Light 示例源码在这里,运行效果如下:
一,创建工程
Per-Vertex light 与 Per-Pixel 的光照计算基本上相同,只是进行的时机不同,Per-Vertex Light 在顶点着色阶段针对每个顶点进行光照计算,而 Per-Pixel 是在片元着色阶段针对每个像素进行光照计算。因此,本文将在前文《Per-Vertex Light及深度缓存》源码的基础上继续进行。
二,Per-Pixel Light
1,修改顶点着色
这次,顶点着色脚本非常简单,因为光照计算工作都将转移到片元着色脚本中进行。为了方便与前文中的脚本进行对比,在这里,保留前文中的脚本,新建 PerPixelVertex.glsl 以及 PerPixelFragment.glsl 脚本。
PerPixelVertex.glsl 脚本内容如下:
uniform mat4 projection; uniform mat4 modelView; uniform mat3 normalMatrix; attribute vec4 vPosition; attribute vec3 vNormal; attribute vec3 vDiffuseMaterial; varying vec3 vEyeSpaceNormal; varying vec3 vDiffuse; void main(void) { gl_Position = projection * modelView * vPosition; vEyeSpaceNormal = normalMatrix * vNormal; vDiffuse = vDiffuseMaterial; }
从上面的代码中可以看到,顶点着色器只是简单地转换 local space 中的法线到 view space,然后将相关 varying 传递给片元着色器。
PerPixelFragment.glsl 脚本内容如下:
varying mediump vec3 vEyeSpaceNormal; varying mediump vec3 vDiffuse; uniform highp vec3 vLightPosition; uniform highp vec3 vAmbientMaterial; uniform highp vec3 vSpecularMaterial; uniform highp float shininess; void main() { highp vec3 N = normalize(vEyeSpaceNormal); highp vec3 L = normalize(vLightPosition); highp vec3 E = vec3(0, 0, 1); highp vec3 H = normalize(L + E); highp float df = max(0.0, dot(N, L)); highp float sf = max(0.0, dot(N, H)); sf = pow(sf, shininess); mediump vec3 color = vAmbientMaterial + df * vDiffuse + sf * vSpecularMaterial; gl_FragColor = vec4(color, 1); }
从上面的代码可以看到,原先在顶点着色器中进行的光照计算被转移到片元着色器中了。这里没有什么特别的,光照计算过程还是前面两篇文章介绍的那些内容,因此在这里就不再累述了。
为了方便在不同着色脚本之间进行切换,我定义了一个 LightMode 枚举:
enum LightMode { PerVertex, PerPixel, PerPixelToon, }; const LightMode CurrentLightMode = PerPixel;
并在 setProgram 中根据当前的光照计算模式来载入对应的脚本:
- (void)setupProgram { // Load shaders // NSString * vertexShaderPath = nil; NSString * fragmentShaderPath = nil; if (CurrentLightMode == PerVertex) { vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader" ofType:@"glsl"]; fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader" ofType:@"glsl"]; } else if (CurrentLightMode == PerPixelToon) { vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"PerPixelVertex" ofType:@"glsl"]; fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"PerPixelToonFragment" ofType:@"glsl"]; } else { // default per-pixel light vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"PerPixelVertex" ofType:@"glsl"]; fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"PerPixelFragment" ofType:@"glsl"]; } //...... }
编译运行,效果如下图。细心的童鞋可以比较 Per-Vertex 与 Per-Pixel 两种光照的效果。Per-Vertex 光照计算是在顶点着色阶段进行,然后在光栅化阶段进行线性插值;而 Per-Pixel 光照计算是在片元着色阶段针对每一个像素进行,因此后者要比前者更加细致逼真,效果更好一些,当然计算量自然也要大。
三,卡通效果
前面说过,卡通效果是将散射光因子“分级”从而不再是连续的,打个比方说,考试成绩上百分制是连续的,而分级制:好/良好/及格/不及格就不是连续的,这样就能获得漫反射跳跃的卡通效果。
新建 PerPixelToonFragment.glsl 脚本,其内容如下:
varying mediump vec3 vEyeSpaceNormal; varying mediump vec3 vDiffuse; uniform highp vec3 vLightPosition; uniform highp vec3 vAmbientMaterial; uniform highp vec3 vSpecularMaterial; uniform highp float shininess; void main() { highp vec3 N = normalize(vEyeSpaceNormal); highp vec3 L = normalize(vLightPosition); highp vec3 E = vec3(0, 0, 1); highp vec3 H = normalize(L + E); highp float df = max(0.0, dot(N, L)); highp float sf = max(0.0, dot(N, H)); sf = pow(sf, shininess); if (df < 0.1) df = 0.0; else if (df < 0.2) df = 0.2; else if (df < 0.4) df = 0.4; else if (df < 0.6) df = 0.6; else if (df < 0.8) df = 0.8; else df = 1.0; mediump vec3 color = vAmbientMaterial + df * vDiffuse + sf * vSpecularMaterial; gl_FragColor = vec4(color, 1); }
注意看粗体部分,这就是新增的部分。这部分代码将漫反射因子调整为五个级别:0.0,0.2,0.6,0.8,1.0,因此漫反射就有层次效果了。如下图所示:
四,总结
Per-Vertex 与 Per-Pixel 两种光照的异同:两者都是基于相同的光照原理来进行光照计算的,Per-Vertex 光照计算是在顶点着色阶段进行,然后在光栅化阶段进行线性插值;而 Per-Pixel 光照计算是在片元着色阶段针对每一个像素进行。因此后者要比前者效果更好,看上去更加细致逼真,当然计算量自然也要多一些。
卡通效果是将漫反射因子分级,从而形成不连续的跳跃的漫反射效果。在本文中,是在片元着色阶段进行卡通效果处理的,它也可以在顶点着色阶段进行。
在这个系列的介绍中,只提及了一些简单的光照效果,还有很多更加逼真的光照算法或技巧没有涉及,比如菲涅尔效果或使用光照贴图。
菲涅尔效果:根据观察者的观察表面来调整反射率来实现的。比如你从水面,油漆表面或者丝绸的正上方看,反射光泽的柔和效果基本没有,如果侧着或平着看的话,反射光泽的柔和效果就很明显。
光照贴图:使用预先处理好的明暗纹理来模拟光照,这样可以减少实时的光照计算,但这样的技巧只适用于静态场景。