Shader 编程入门初探-CPU Vs GPU
什么是 Shader ?
Shader 的中文含义是着色器。它其实是一段计算机程序,在执行渲染任务时,可以用特定的指令来计算图像的颜色或明暗,从而绘制出物体。
而 Shader 编程主要使用三种语言,GLSL,HLSL 和 CG。
GLSL 基于 Opengl 接口,全称“ OpenGL Shading Language ”。有很好的移植性,可在 Mac,Windows,Linux 甚至是移动平台上使用。
HLSL 基于 Direct3d 接口,全称“ High Level Shading Language ”,HLSL 大多用于游戏领域,并且只能在 Windows 平台使用。
CG 则是 Nvidia 公司开发的,含义是(C For Graphics),其中 Unity 中所用 的 Shader 使用的就是 CG 语言。
三者都十分类似于 C 语言,有各自不同的应用场景。但无论使用何种语言,它们最终都是跑在 GPU 上的。因为我们需要先了解 GPU 的工作方式。
GPU VS CPU
CPU 全称 Central Processing Unit,即中央处理器。 GPU 全称 Graphics Processing Unit ,即图形处理器。
CPU 通常长的比较朴素,小小的,四四方方。
而 GPU 通常长这样
也有长这样的高性能 GPU
CPU 和 GPU 都属于硬件,有不同的应用场景。在解决某些任务时,使用 CPU 处理会效率会更高,而另一些任务,则使用 GPU 会更好。它们在效率上没有绝对的优劣,很多时候都需要协同工作。
归根到底,CPU 和 GPU 其实都在做一件事,那就是计算。CPU 更像一个 “通才”,什么都可以做,有极强的通用性。而 GPU 更像一个 “专才”,擅长做类型统一,大规模的数据运算。比如图形类的数值计算。
下面介绍一个有趣精彩的例子,Adam Savage 和 Jamie Hyneman 做了一个视频试图阐述 CPU 与 GPU 在处理任务时的异同。
( 视频演示 )https://v.qq.com/txp/iframe/player.html?vid=o0396nmjqp4&width=500&height=375&auto=0
视频出处(
CPU
CPU 有点像上图的机械臂喷枪,强大且灵活,尽管速度快,但一次只能绘制一个像素。
GPU
而 GPU 就像一个由多个无法移动的喷枪组合成的巨型大炮。有点笨拙,但只需一次发射,就能同时绘制多个像素。
从这个形象的例子中,可以看到 GPU 这种并行处理的方式,在图形处理上有着天然的优势。所以我们常常会听到这样的说法,要玩某些大型的3D游戏,需要配置强悍的显卡,这是因为 3D 游戏,有巨量的数据需要去计算,诸如模型的顶点信息,光影贴图等等。这正是 GPU 所擅长的。
GPU 其实很像一个工厂,里面有许多工人在同时工作。但这些工人彼此之间是无法交流的,计算得到的数据无法相互传递。GPU 做的事情,是把一个大任务,拆分成多个相同小任务,然后交给一大批工人去处理,用简单的话说,就是用人海战术去解决问题。
正是由于 GPU 的这种特性,很大程度上影响了 shader 语言的编程风格。使得刚接触的初学者会十分不适,有种重新学习编程的错觉。
学习 GLSL - 开启 GPU 的强悍性能
既然 GPU 有这么神奇的特性,为了不要浪费它的性能。我们最好还是学习点 shader 语言。
由于 Processing,OpenFrameworks 都是基于 OpenGL 的封装。所以之后的章节里,我们会使用 GLSL 语言来写 shader。
下面直接从具体的实例开始,感受 shader 的威力。
在 Processing 中使用 CPU 绘图
之前,我们在 Processing 的 draw 函数中调用绘图函数,如 ellipse,rect 等等,它通常只使用到了 CPU 的性能。像下面一段程序,试图用特定宽度的矩形格子,把画布填满。同时左右移动鼠标,可以改变 B(蓝) 通道上的色值。
void setup() {
size(600,600);
noStroke();
}
void draw() {
float rectW = 40; // 格子的宽度
for (int i = 0; i < width; i+=rectW) {
for (int j = 0; j < height; j+=rectW) {
float r = i/(float)width * 255;
float g = j/(float)height * 255;
float b = mouseX/(float)width * 255;
fill(r, g, b);
rect(i, j, rectW, rectW);
}
}
println(frameRate);
}
其中变量 rectW 就代表每个格子的宽度,数值为 40 时,就会绘制 15 X 15 个矩形,合共 225 个。绘制这个数量的矩形,对 CPU 来说是小菜一碟。所以移动鼠标时,色彩的变化是非常实时的,帧率也会稳定在 60 fps 左右。
但当我们试着修改 rectW 的大小,比如写成 1。(刚好矩形宽等于像素宽)。这时程序在一帧当中就需要绘制 600 X 600 个矩形,合共 360000 个。同时你会发现,程序明显变慢了。当然,如果你电脑的 CPU 本身的性能就很强悍,变化不明显的话可以试着把 600 X 600 的画布分辨率继续调大,就能明显看到卡顿感。
视电脑的配置不同,帧率会有所区别。在本次测试中,通过 println 可以看到,帧率维持在 5 fps左右
在 Processing 中使用 GPU 绘图
接下来进入正题,开始使用 GPU 的实现相同的任务,方便对比两者间异同。
首先创建一个 pde 工程文件,输入以下代码
PShader myShader;
void setup() {
size(600,600,P2D);
myShader = loadShader("myShader.frag");
noStroke();
}
void draw() {
noStroke();
myShader.set("u_resolution",float(width),float(height));
myShader.set("u_mouseX",float(mouseX));
shader(myShader);
rect(0,0,width,height);
println(frameRate);
}
接着保存 pde,同时在这个 pde 的同个目录下,创建一个名为 myShader.frag 的文件(可以先新建一个 txt 文件,再把后缀名修改为 frag)。
创建后,再复制以下代码到 myShader.frag 文件中
uniform float u_mouseX;
uniform vec2 u_resolution;
void main( void ){
float r = gl_FragCoord.x / u_resolution.x;
float g = gl_FragCoord.y / u_resolution.y;
float b = u_mouseX/u_resolution.x;
gl_FragColor = vec4(r,g,b,1.0);
}
这段代码,就是用 GLSL 语言编写的 shader。这个阶段我们无需理解具体指令的含义。直接点运行,并左右移动鼠标
会发现程序执行得非常流畅,色彩过渡也同样平滑自然
我们可以把分辨率继续改大,会发现帧率丝毫没有变慢,始终维持在 60 fps,这便是 GPU 的威力。
在 Openframeworks 中使用 CPU 绘图
下面提供一个在 OF 中使用 Shader 的范例,首先是常规的绘图方式。
void ofApp::draw(){
float rectW = 1;
for(int i = 0;i < ofGetWidth();i+=rectW){
for(int j = 0;j < ofGetHeight();j+=rectW){
float r = i/(float)ofGetWidth() * 255;
float g = j/(float)ofGetHeight() * 255;
float b = mouseX/(float)ofGetWidth() * 255;
ofSetColor(r, g, b);
ofDrawRectangle(i,j,rectW,rectW);
}
}
}
在 Openframeworks 中使用 GPU 绘图
使用 Shader 的版本,主程序代码:
— ofApp.h 内
ofShader shader;
— ofApp.cpp 内
void ofApp::update(){
shader.load("shader");
ofSetWindowTitle(ofToString(ofGetFrameRate()));
}
void ofApp::draw(){
shader.begin();
shader.setUniform1f("u_mouseX", mouseX);
shader.setUniform2f("u_resolution", ofGetWidth(),ofGetHeight());
ofDrawRectangle(0,0,ofGetWidth(),ofGetHeight());
shader.end();
}
在创建工程文件后,需要在 data 文件夹中创建文件 shader.frag,再将以下代码复制保存
— Fragment Shader 内
uniform float u_mouseX;
uniform vec2 u_resolution;
void main( void ){
float r = gl_FragCoord.x / u_resolution.x;
float g = gl_FragCoord.y / u_resolution.y;
float b = u_mouseX/u_resolution.x;
gl_FragColor = vec4(r,g,b,1.0);
}
保存后点运行,可以看到同样的效果
Processing 与 Openframeworks 使用 Shader 的异同对比
Shader 部分的代码是完全一致的。他们的区别只在于加载 shader 的语法和传入数据的部分略有不同。
END
关于 GLSL,这节的介绍就到这里。学习 shader 的过程很接近于刚接触编程语言的感觉。像画圆形,画方形。一些原来只要使用简单绘图函数就能实现的效果,在 shader 里需要采取截然不同的思路。过往你所积累的图形编程经验是无法直接迁移的。
之后的章节,不会再细致地阐释每个指令的用法。但会尝试以范例库的形式,整合自己对一些基础概念的思考。从实践中理解 shader,把玩 shader。
相关资源与网站
shader教程:
shader创作平台:
shader创作平台: