游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2309|回复: 0

Shader从入门到跑路:朴实无华的图形学基础

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2020-3-9 13:58:23 | 显示全部楼层 |阅读模式
1.jpg


前言:

Unity Shader的学习的学习路径是非常陡峭的,笔者在学习的时候走了不少歪路,在这里权当分享一下学习过的内容,也是给自己做一个记录了

准备:

1.        基本的Unity使用经验

2.        天不怕地不怕的心态

枯燥但必须得搞清楚的图形学内容

因为笔者的目的主要是想介绍shader,因此书本上的图形学内容这里就不深入讲了。在传统的图形处理中,我们一般需要两种程序,一个叫Vertex Shader,另一个叫Fragment Shader,他们是一对好伙伴,往后的日子也会时常与他们打交道。

Vertex Shader负责获取处理的网格(Mesh)资讯然后进行处理,比方说我们想渲染一个正方形,那么首先就得先获取这个正方形的位置,纹理等信息,然后做一些矩阵转换传递给Fragment Shader。(关于矩阵转换的事情之后再讲,大概知道有这么回事就好)。

2.jpg

网格中的像素经过处理之后被存储在Fragments中被传给Fragment Shader。当Vertex Shader处理顶点的时候,它会将平均每三个顶点组成一个Fragment,最终整个被处理过的图像被称作Fragments。这个过程用图形学的知识来讲解颇为复杂,有兴趣的读者可以找冯女神的书看看。Fragment Shader其实就是进行核心计算的部分。

3.jpg

自问自答

既然Vertex Shader已经将网格的数据做好了空间转换,那这些信息不是已经可以用了吗?为什么还需要一个Fragment Shader呢不是多此一举吗?哎,别急,听我慢慢解释。这是因为在图像处理当中,我们并不一定需要用到所有的像素数据,因此有些数据需要被修改,而有些则直接弃之不用。比如在我们在实作模糊效果时,其中一种思路便是通过把一个像素周边的像素合并成一个相同颜色的像素实现的,这些内容过于复杂,无法交由CPU来计算,只能由GPU代劳,因此也可以将Vertex Shader看作和CPU打交道的程序,而Fragment则是负责GPU的内容。

4.jpg

于是,在Fragment Shader中,有些像素被保留,而另一些则可能被丢掉,接下来Fragment Shader讲处理好的内容传给颜色缓冲区(Color Buffer),结束了它的工作。后面的内容超出了本文的内容,就不延伸讲了。

我们已经半只脚迈进了图形学,下一步就迈远一些,直接写shader了。对于长期和各种编程语言打交道的我来说,从来都是先学点皮毛,然后立马开写,就像学英语一样,会那么几个单词就得冲去外国人堆里尬聊了。这种方法容错率最低,但也毫无疑问是最快的学习方法,只有最刻骨铭心的错误才能让人长记性。说的不好不能成为你不说的原因,那么废话少说,来看看这段你人生当中最无聊的shader。

正文

  1. Shader "Custom/ShaderLearning"
  2. {
  3.         SubShader
  4.         {
  5.                 Tags
  6.                 {
  7.                         "PreviewType" = "Plane"
  8.                 }
  9.                 Pass
  10.                 {
  11.                         CGPROGRAM
  12.                         #pragma vertex vert
  13.                         #pragma fragment frag

  14.                         #include "UnityCG.cginc"

  15.                         struct appdata
  16.                         {
  17.                                 float4 vertex : POSITION;
  18.                         };

  19.                         struct v2f
  20.                         {
  21.                                 float4 vertex : SV_POSITION;
  22.                         };

  23.                         v2f vert(appdata v)
  24.                         {
  25.                                 v2f o;
  26.                                 o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
  27.                                 return o;
  28.                         }

  29.                         float4 frag(v2f i) : SV_Target
  30.                         {
  31.                                 float4 color = float4(1,1,1,1);
  32.                                 return color;
  33.                         }
  34.                         ENDCG
  35.                 }
  36.         }
复制代码

将写好的shader赋值给material,然后新建一个3D平台将material拖入就可以看到效果(如果这个步骤都不会赶紧谷歌大法)。这个shader本质上只是输出白色像素而已,它并没有很酷,但为了写出酷炫的shader,这个阶段是必不可少的。

5.jpg

2.1 ShaderLearning文件的渲染结果

这里有很多代码,但我们只需要关注涉及到vertex/fragment shader输入输出的部分就好了。

首先是结构体appdata,它定义了从每个网格的顶点需要获取什么信息,在这里是所有网格顶点的空间信息(POSITION)。

  1. struct appdata
  2. {
  3.         float4 vertex : POSITION;
  4. };
  5. 结构体v2f则定义了需要传递什么数据给fragment shader。

  6. struct v2f
  7. {
  8.         float4 vertex : SV_POSITION;
  9. };
复制代码

接下来,我们有一个vert函数,以定义的appdata作为参数然后返回一个v2f类型的参数传递给frag函数,最后frag函数返回一个float4类型的颜色。(float4可以看成四个浮点数组成的四维数组)

  1. // Vertex Shader
  2. v2f vert(appdata v)
  3. {
  4.         v2f o;
  5.         ...
  6.         return o;
  7. }

  8. // Fragment Shader
  9. float4 frag(v2f i) : SV_Target
  10. {
  11.         return float4(1,1,1,1);
  12. }
复制代码

值得一提的是在appdata中定义的数据,是通过语义指挥unity来获取的,因此当我们定义好之后,在函数中就可以直接使用了。这些都是unity本身做好的封装,让我们可以专心思考shader的计算过程。

刚才是走马观花,现在我们先拿过来一个放大镜,仔细瞧瞧两个函数都做了些什么

6.jpg
  1. v2f vert(appdata v)
  2. {
  3.         v2f o;
  4.         o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
  5.         return o;
  6. }
复制代码

首先是作为shader里面打头阵的vertex shader,由于我们现在的shader是整个宇宙最简单的shader,因此它从网格上能得到的信息就只有顶点的位置而已。然后我们初始化了一个v2f叫o。接着用一个矩阵转换的方法设置o里面的顶点变量,目前你应该知道的就是这里使用了一个叫做矩阵乘法的运算,它将网格上的局部顶点从一个与网格相关的点,转换成一个屏幕上的点。

  1. float4 frag(v2f i) : SV_Target
  2. {
  3.         float4 color = float4(1,1,1,1);
  4.         return color;
  5. }
复制代码

而fragment shader的功能是将像素转化成屏幕上的颜色,我们不需要管这个像素在什么位置,因为在vertex shader里面已经处理过了,因此在这一段我们返回实际上就是一个有具体的位置的屏幕颜色。返回颜色是白色,它由四个内容组成,分别是红色,绿色,蓝色和透明通道。你可以试着将这些数字做一些更改,比如返回一个float4(1,0.6, 0,1)则可以获得橙色的颜色

7.jpg
2.2 输出橙色的渲染结果

那么大概就是这样了,或许这一部分对很多人来说已经是劝退的点,但其实Unity已经为我们做了很多去复杂化的工序了,如果想要深入了解这些背后的原理建议读者学一学openGL或者directX,你马上会明白Unity真的做的非常良心。

当然,只是输出纯色实在是太无聊了,我们可以在此基础上做一个更改,比如先在两个结构体里面加入一些东西

  1. struct appdata
  2. {
  3.         float4 vertex : POSITION;
  4.         float2 uv : TEXCOORD0;
  5. };

  6. struct v2f
  7. {
  8.         float4 vertex : SV_POSITION;
  9.         float2 uv : TEXCOORD1;
  10. };
复制代码


注意,你能让unity帮助你从网格那里拿到的数据是受到unity提供的语义所限制的,可以去这里看看:

https://docs.unity3d.com/Manual/SL-VertexProgramInputs.htmldocs.unity3d.com

在vertex shader做的事情并没有什么魔法,只需要将拿到的uv直接通过v2f传给frag函数就好了

  1. v2f vert(appdata v)
  2. {
  3.         v2f o;
  4.         o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
  5.         o.uv = v.uv
  6.         return o;
  7. }
复制代码

接下来把uv的值填入颜色的返回值中,得到下图

  1. float4 frag(v2f i) : SV_Target
  2. {
  3.         float4 color = float4(i.uv.r, i.uv.g, 0, 1);
  4.         return color;
  5. }
复制代码

8.jpg
2.3 以uv值作为颜色值的渲染结果

自问自答

之前不是说每个三角形只会执行三次来获取顶点数据吗?为什么2.3的所有点都有渲染到颜色呢?

9.jpg

2.4 理论上来说一个三角只有三个顶点被计算到了,那么2.3中那种渐变效果是如何得出的呢?

在上一篇文章我们说过,vertex shader将网格进行处理,然后将数据以fragment为单位输出到fragment shader中,其实我们可以将fragment看作一个三角形的图形(从图形学的角度看这么想不太对,但对知识进行三维建模还是有必要的)。而得出这一结果是因为shader对两个相邻顶点之间的值进行的了线性插值,因此虽然vertex shader只计算了四个顶点(上面三个,下面一个),但最终fragment shader还是会得到所有的值信息。

课后练习

试试修改返回的颜色值,并得到下面这个效果

10.jpg

作者:俊銘
专栏地址:https://zhuanlan.zhihu.com/p/85594617

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-12-22 19:58

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表