查看原文
其他

Shader 编程入门初探-CPU Vs GPU

2017-04-24 Wenzy InsLab

Shader 是图形编程中不可绕开的话题。要想在程序中获得强大的图形表现力,就不得不去接触了解。

什么是 Shader ?

Shader 的中文含义是着色器。它其实是一段计算机程序,在执行渲染任务时,可以用特定的指令来计算图像的颜色或明暗,从而绘制出物体。

而 Shader 编程主要使用三种语言,GLSL,HLSLCG

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创作平台:


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存