着色器(Shader)应用与计算机图形学领域,指一组提供计算机图形资源在渲染时执行的指令。
随着手机应用以及移动端游戏这几年的发展,着色器设计凭借着自身的灵活性以及适应性,越来越多的被移动端开发者所接受。
本人在App Store上发布了一个原创免费开源无广告的关于着色器的教育型软件,以研究着色器在移动端的设计为目的。
可惜由于时间匆忙,并没有做中文的本地化。所以将写几篇博客,手把手的教一些希望学习着色器的新手移动开发者如何把着色器应用在自己的App上。
首先,跟我学着色这个软件是使用coco2d-x 3.0 + GLSL 编写而成的。如果有朋友想直接下载源码运行,请使用正确的coco2d-x的版本。
整个软件分为3个部分,1是主体的教育软件,中间带有一些正常应用的按钮和比较功能。2是着色器的程序。这些着色器的程序有部分是我原创的,有些是包含在coco2d-x 游戏引擎里的,还有部分是来自于国外开源shadertoy上。我自身也在shadertoy上发表了一些着色器的程序,上面的所有着色器程序都是免费开源的。第三就是读取着色器程序的一段程序。其实GLSL是可以支持在很多引擎上运行的,所以无论是Unity还是cocos2d-x或者一些其他主流的引擎,只要自己写段读取的代码,着色器程序都可以运行。
因为今天是入门篇,所以今天先介绍几个静态的着色器设计。
首先是一张效果图,左上的是原图,第二个是灰色效果,第三是彩色横条效果,第四是高亮,第五是边缘检测,第六是冰花效果,第七是卡通渲染,第八是石刻效果,第九是高斯模糊。
因为篇幅有限,接下来介绍每种效果的设计原理。具体想了解更多的朋友可以直接去下我的软件,或者直接开我底部的源码链接编译运行。里面每种效果都非常详细,总共有超过100张PPT。介绍了每种效果的历史,起源,物理学原理,设计理念,实现效果以及一些可以实际应用上的Tricky Solution.
1. 灰色效果:
灰色效果是非常简单的一种Shader效果,对于这个效果更多的是用来测试基本的Shader效果能否实现。
灰色效果的设计理念就是使原图片的RGB的值按比例减少。我们都知道一般灰色是128,128,128.我们通过把RGB的值降低,可以明显的使图片色调变暗,从而实现变灰效果。
具体操作就是把原来图片的alpha通道保留,RBG点成一个比例。
Gray.fsh
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D CC_Texture0;
void main()
{
vec4 v_orColor = v_fragmentColor *texture2D(CC_Texture0,v_texCoord);
float gray =dot(v_orColor.rgb,vec3(0.3,0.3,0.3));
gl_FragColor =vec4(gray,gray,v_orColor.a);
}
一般来说,一个Shader效果可以有一个vertex shader一个 fragment shader. 上面的Gray.fsh就是我们实现变灰的代码,而下面的Gray.vsh则决定了位置。由于变灰效果网上很多大牛都写过,所以不做过多讲解。有一点就是下面的关于变灰后图片的位置网上有个代码使用了CC_MVPMatrix导致图片位置不对。Gray.vsh
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
void main()
{
gl_Position = CC_PMatrix * a_position;
v_fragmentColor = a_color;
v_texCoord = a_texCoord;
}
这个vertex shader是我看到后来别人回复修改过后的,大家使用我的就没有问题了,后面我更多的会针对fragment shader进行讲解,毕竟这是实现效果的主要部分。彩色横条效果是cocos2d-iphone和cocos2d-x 3.0里面都自带的一个shader效果。这个效果是从McCollough效果上衍生而来的,对历史和理论感兴趣的可以下载下我的App具体学习。如果我没记错,McCollough好像是从人类视觉感知的一个现象衍生出来的一个效果。(原谅我McCollough这个词wikipedia上没有中文翻译)
//inspired and modified from http://www.cocos2d-iphone.org
#ifdef GL_ES
precision lowp float;
#endif
varying vec2 v_texCoord;
uniform sampler2D CC_Texture0;
vec4 colors[8];
void main(void)
{
colors[0] =vec4(1,0,1);
colors[1] =vec4(0,1,1);
colors[2] =vec4(0,1);
colors[3] =vec4(0,1);
colors[4] =vec4(1,1);
colors[5] =vec4(1,1);
colors[6] =vec4(1,1);
int y =int( mod(gl_FragCoord.y /3.0,7.0 ) );
gl_FragColor = colors[y] *texture2D(CC_Texture0,v_texCoord);
}
然后我们把原图的材质给乘到打上横条以后的色彩上,就形成了彩色横条效果。
3. 高亮效果
这个效果我暂时压后,因为这个效果里面涉及到了高斯模糊,所以在后面讲解会更加方便。
4. 边缘检测
边缘检测的Shader效果也是cocos2d-x里面本身就带有的一个Shader效果。
具体上的操作是使用了索贝尔算子,来计算出2D图形空间上梯度。在技术上,它是一种离散性差分算子,用来运算图像亮函数的梯度近似值。
当然,索贝尔算子仅仅是边缘检测一种方法之一,另外还有罗盘算子,Canny算子等等。
索贝尔算子包含了2组3*3的矩阵,分别为横向以及纵向,将它与图像做平面卷积,既可以亮度差分近似值。
假设A代表原始图形,
上面的理论参考了wikipedia上的资料:http://en.wikipedia.org/wiki/Sobel_operator
然后我们来看具体实现,Edge Detection.fsh.
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
vec2 resolution;
uniform sampler2D CC_Texture0;
float lookup(vec2 p,float dx,float dy)
{
vec2 uv = p.xy + vec2(dx,dy ) / resolution.xy;
vec4 c = texture2D(CC_Texture0,uv.xy);
return 0.2126*c.r +0.7152*c.g +0.0722*c.b;
}
void main(void)
{
// simple sobel edge detection
resolution=vec2(1024.0,768.0);
vec2 p = v_texCoord.xy;
// Gx matrix multiplication
float gx = 0.0;
gx += -1.0 * lookup(p, -1.0,-1.0);
gx += -2.0 * lookup(p,0.0);
gx += -1.0 * lookup(p,1.0);
gx += 1.0 * lookup(p, 1.0,-1.0);
gx += 2.0 * lookup(p,0.0);
gx += 1.0 * lookup(p,1.0);
// Gy matrix multiplication
float gy = 0.0;
gy += -1.0 * lookup(p,-1.0);
gy += -2.0 * lookup(p, 0.0,-1.0);
gy += -1.0 * lookup(p,-1.0);
gy += 1.0 * lookup(p,1.0);
gy += 2.0 * lookup(p,1.0);
gy += 1.0 * lookup(p,1.0);
float g = gx*gx + gy*gy;
gl_FragColor.xyz = vec3(1.-g);
gl_FragColor.w = 1.;
}
//This shader is referred to cocos2d-x official engine shader.
//Check http://www.cocos2d-x.org/ for detailed information.
仔细看下,gx,gy的值就是根据索贝尔算子矩阵所得,最后的每个像素的梯度近似值也与理论相符。这也是为什么这个着色效果可以做出边缘检测的原因。
5. 冰花效果
冰花效果是个很有意思的模糊效果,这个效果源于一个很有爱的国际友人在10年博客上发表的一篇博文。(http://coding-experiments.blogspot.ca/2010/06/frosted-glass.html)
后来我在学习模糊效果的时候,一个国外论坛上有人引用了这个简单有有效的效果。
简单的说,这个着色效果模仿了冰花玻璃的效果,原理是根据一个伪随机的向量平移了像素的位置。
看上去是一个很有意思的效果,不过这个效果的实现却是惊人的简单。
Frost Blur.fsh
// Shader is inspired & modified from: http://coding-experiments.blogspot.ca/2010/06/frosted-glass.html
#ifdef GL_ES
precision mediump float;
#endif
varying lowp vec2 v_texCoord;
uniform sampler2D u_texture;
float rand(vec2 co)
{
return fract(sin(dot(co.xy,vec2(100,100))) +
cos(dot(co.xy,vec2(50,50))) *5.0);
}
void main()
{
vec2 rnd = vec2(0.0);
rnd = vec2(rand(v_texCoord),rand(v_texCoord));
gl_FragColor = texture2D(u_texture,v_texCoord+rnd*0.02);
}
核心代码只有5行,其实就是平移了像素的位置,不过效果却不错。
6. 卡通渲染
卡通渲染是一种去真实感的渲染方法,旨在使电脑生成的图像呈现出手绘的效果。渲染过程中一般会把常规光源的取值被逐一计算并投射到一小片独立的明暗区域上,产生卡通式的单调色彩。然后会有一个勾边的过程,用于突出物体。
当然,有些更精细的实现会得出更好效果。比如Unity就发表过一个卡通渲染的教程,中间包括采光,散光等一系列的处理。(http://en.wikibooks.org/wiki/GLSL_Programming/Unity/Toon_Shading)
现在我们来看下cocos2d-x的实现。
celShading.fsh
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
vec2 resolution;
uniform sampler2D CC_Texture0;
#define FILTER_SIZE 2
#define COLOR_LEVELS 7.0
#define EDGE_FILTER_SIZE 2
#define EDGE_THRESHOLD 0.05
vec4 edgeFilter(in int px,in int py)
{
vec4 color = vec4(0.0);
for (int y = -EDGE_FILTER_SIZE; y <= EDGE_FILTER_SIZE; ++y)
{
for (int x = -EDGE_FILTER_SIZE; x <= EDGE_FILTER_SIZE; ++x)
{
color += texture2D(CC_Texture0,v_texCoord +vec2(px + x,py + y) / resolution.xy);
}
}
color /= float((2 * EDGE_FILTER_SIZE +1) * (2 * EDGE_FILTER_SIZE +1));
return color;
}
void main(void)
{
// Shade
resolution=vec2(1024.0,768.0);
vec4 color =vec4(0.0);
for (int y = -FILTER_SIZE; y <= FILTER_SIZE; ++y)
{
for (int x = -FILTER_SIZE; x <= FILTER_SIZE; ++x)
{
color +=texture2D(CC_Texture0,v_texCoord +vec2(x,y) / resolution.xy);
}
}
color /= float((2 * FILTER_SIZE +1) * (2 * FILTER_SIZE +1));
for (int c =0; c <3; ++c)
{
color[c] =floor(COLOR_LEVELS * color[c]) / COLOR_LEVELS;
}
// Highlight edges
vec4 sum =abs(edgeFilter(0,1) - edgeFilter(0,-1));
sum += abs(edgeFilter(1,0) - edgeFilter(-1,0));
sum /= 2.0;
if (length(sum) > EDGE_THRESHOLD)
{
color.rgb =vec3(0.0);
}
gl_FragColor = color;
}
这里实现总共分为两个部分,一个是阴影(shade),一个是勾边(highlight edge)。其实打阴影的过程,就是我们前面理论里的把常规光源投射到独立明暗区间的过程。这也是为什么卡通渲染会颜色有所加深,或者说,让有感觉一眼就不是真实图片。因为明暗间有平滑过渡的取值被明确的分开成了卡通式的单调色彩。
勾边的过程,也可使用上个效果中的索贝尔算子,也确实有不少应用和游戏中勾边过程使用了索贝尔算子。原中的边缘滤镜方法我并没有深究,有兴趣的朋友可以探索下。
我对这个效果的探索不是很深,其中主要原因就是这个效果渲染起来并不是很顺畅。甚至有些卡卡的,在iPad mini上都是这样,更不用说手机了。所以如果在实际应用中,肯定需要重新修改。不过阀值修改后,渲染速度会变快,不过卡通的质量也会有所下降。
7. 石刻效果
石刻效果,是我个人非常喜欢的一个效果。这个效果理论上就是把像素变成高光或者阴影,换句话说就是明显的区分开来。
这个效果最早在coco2d的开发中,就被开发者所发现了。代码简单,效果也很有特色。
我最早看到关于石刻效果的教程,是在这个链接.(http://www.raywenderlich.com/10862/how-to-create-cool-effects-with-custom-shaders-in-opengl-es-2-0-and-cocos2d-2-x)
这篇教程把时刻效果和反色放一起讲解了,还是很详细了。
不过想具体实现,用这个代码就已经足够了.
#ifdef GL_ES
precision mediump float;
#endif
// The shader is inspired and modified from cocos2d custom shader tutorial
varying vec2 v_texCoord;
uniform sampler2D u_texture;
void main()
{
// pixel size
vec2 onePixel = vec2(1.0 /480.0,1.0 / 320.0);
// get v_texCoordate
vec2 texCoord = v_texCoord;
// exactly step follow the idea
vec4 color;
color.rgb = vec3(0.5);
color -= texture2D(u_texture,texCoord - onePixel) *5.0;
color += texture2D(u_texture,texCoord + onePixel) *5.0;
// grayscale
color.rgb = vec3((color.r + color.g + color.b) /3.0);
gl_FragColor = vec4(color.rgb,1);
}
核心代码其实就3行,我们想要让图片的像素看起来很明显不同,那我们需要个基本色,color.rgb=vec3(0.5);
然后基于这个基本色,把像素之间的色差给打开。
最后在利用我们之前的灰色效果,把整个图形变灰。
这样就能达成石刻效果了。
后来我在网上搜索,也有些算法是通过梯度计算边缘,然后也能达到同样的效果。不过光听都感觉,这个过程会有些繁复。
8. 高斯模糊
最后个效果也就是高斯模糊,也叫高斯平滑。在计算机图像领域,是个很重的话题。通常用它来减少图像噪声以及降低细节层次。这种模糊技术生成的图像,其视觉效果就像是经过一个半透明屏幕在观察图像,这与镜头焦外成像效果散景以及普通照明阴影中的效果都明显不同。
从数学角度来看,图像的高斯模糊效果就是图像与正态分布做卷积。由于正态分布又叫高斯分布,所以这个效果也叫高斯模糊。由于高斯函数的傅里叶变换是另一个高斯函数,所以高斯模糊对于图像来说是个低通滤波器。
这里对于高斯模糊的实现,有一些可以增加渲染速度的小技巧。
高斯模糊是一种图像模糊滤波器,它用正态分布计算图像中每个像素的变换。N维空间正态分布方程为:
但是在实际过程中,我们可以先对水平方向先做一次一维的模糊,再对垂直方向做一次一维模糊,同样可以达到使用二维矩阵变换得到的效果。
这是因为高斯模式线性可分,引用下Game Rendering开发小组的例子(http://www.gamerendering.com/2008/10/11/gaussian-blur-filter-shader/):
其实如上打开数学公式也可以证明线性可分。
我们可以通过高斯函数,预先计算出模糊的权重。
这张是我App里面的一个教学slides,解释的还是很清楚的。
所以最后的实现可以变成:
Gaussian_Blur.fsh
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D CC_Texture0;
vec2 blurSize;
uniform vec4 substract;
void main() {
blurSize=vec2((1.0/1024.0)*3.0,(1.0/768.0)*3.0);
vec4 sum =vec4(0.0);
sum += texture2D(CC_Texture0,v_texCoord -4.0 * blurSize) *0.063327;
sum += texture2D(CC_Texture0,v_texCoord -3.0 * blurSize) *0.093095;
sum += texture2D(CC_Texture0,v_texCoord -2.0 * blurSize) *0.122589;
sum += texture2D(CC_Texture0,v_texCoord -1.0 * blurSize) *0.144599;
sum += texture2D(CC_Texture0,v_texCoord ) *0.152781;
sum += texture2D(CC_Texture0,v_texCoord +1.0 * blurSize) *0.144599;
sum += texture2D(CC_Texture0,v_texCoord +2.0 * blurSize) *0.122589;
sum += texture2D(CC_Texture0,v_texCoord +3.0 * blurSize) *0.093095;
sum += texture2D(CC_Texture0,v_texCoord +4.0 * blurSize) *0.063327;
gl_FragColor = sum * v_fragmentColor;
}
最后来张效果图:
那么具体实现就是如果我们把原来图形的直接应用高斯函数求出来的数值,那就是高斯模糊。如果我们在原值上加上高斯函数的值,那就是高亮+模糊。
具体也可参照我Github上开源的代码里的bloom.fsh.
然后是给源码的时候了
Github:https://github.com/LingTian/shaderlearner/tree/shaderlearner/newmygame
ShaderTutorial: https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=964622922&mt=8
由于静态的着色器较为基本,所以大多已讲解为主。动态的着色器会变得更加酷炫,也会有更多的自主创造成分在里面。
我自己设计的几个Shader应该也会在后续的博文中详细介绍,有兴趣的朋友也可以直接去看我的源码。
另外感谢本文中的图片均以教学为目的,部分例子来自cocos2d-x开源游戏引擎和shadertoy开源网站,部分引用请核对软件内的详细引用。
软件中我自己设计的着色器欢迎大家随便使用,其他一些不属于我的代码,包括来自开源网站,网络资料,及游戏引擎的例子,请大家标明其出处。
本文欢迎转载,注明游云凌天原创即可。