首页 > 代码库 > 《Unity Shader 与 计算机图形学》第二章

《Unity Shader 与 计算机图形学》第二章

提示:本篇将会非常长~

本系列文章分为

硬件

编程入门

工程实践


上一篇

主要介绍了GPU的特征工作原理 以及渲染的底层流程 其实对于新架构而言还有所不同
Shader描述了如何渲染物体的信息,包括:


Texture Setup、纹理设置
Material Property、材质设置
Render State、渲染状态
Blend Setup、混合设置
Pixel Shader、像素着色
Vertex Shader、定点着色
Render Target Setup 渲染目标设置


Shader并不直接和几何体相关联,因为对于同一个几何体,有可能会用不同的Shader来渲染。
为了简化描述我们来定义一下:
vs: vertex shader, 顶点处理/多边形处理的
ps: pixel shader, 处理像素单位的阴影和处理相关纹理
以上两种shader都是可编程的
在各自的着色器单元(Shader Unit)上,有着把各种各样的Shader程序来实现为3D图形的处理的结构。而且,作为用语,如果直接说[Programmable Shader],有指双方(VS和PS)的情况,或者是指这个整体的概念。
并且,由于这个Shader程序是软件,开发者可以自己制作独创的Shader程序,就可以在GPU上实现新的图形功能。
但是在之前的着色器架构中无法面对任务的调节 造成闲置资源
技术分享

而在现有的统一着色器架构下 资源配置得到了有效保障
技术分享


所以在DX11之后通用shader架构得到了广泛使用 比较理想的过程是:
技术分享


当然

以上看不懂也无所谓 因为这个是软件之前的一点小铺垫。

正式篇前导一,纹理?材质?贴图?

整个 CG 领域中这三个概念都是差不多的,在一般的实践中,大致上的层级关系是:

材质 Material包含贴图 Map,贴图包含纹理 Texture。


而我们要说的时更广泛应用的概念

纹理

是最基本的数据输入单位,游戏领域基本上都用的是位图。常见格式有PNG,TGA,BMP,TIFF此外还有程序化生成的纹理 Procedural Texture。
在内存中通常表示为二维像素数组。

贴图

英语 Map 其实包含了另一层含义就是“映射”。其功能就是把纹理通过 UV 坐标映射到3D 物体表面。贴图包含了除了纹理以外其他很多信息,比方说 UV 坐标、贴图输入输出控制等等。
一张图便能说明其之间的关系
技术分享

材质

本质就是一个数据集,主要功能就是给渲染器提供数据和光照算法。贴图就是其中数据的一部分,根据用途不同,贴图也会被分成不同的类型,比方说 Diffuse Map,Specular Map,Normal Map 和 Gloss Map 等等。另外一个重要部分就是光照模型 Shader ,用以实现不同的渲染效果。
贴图种类繁多:我做个不完全总结

 Diffuse Map -

漫反射贴图/也被称作反照率贴图albedo map 存储了物体相应部分漫反射颜色

 Normal Map -

法线贴图 本质上存储的是被RGB值编码的法向量 表现凹凸,比如一些凹凸不平的表面,光影在表面产生实时变化 常用来低多边形表现高多边形细节 比如在高多边形下生成normal map在匹配给低多边形模型 是一种常见的降低性能要求的做法

技术分享

对于讲解normal map来说这篇文章很值得借鉴
技术分享
N’= N + D =[0,0,c]+ D =[a,b,c]
a = -dF/du * 1/k
b = -dF/dv * 1/k
c = 1

常规来说就是光向量和法向量的点积来确定明暗以表现凹凸质感,也就是把法线存在纹理中

我们又会发现另外一个奇怪的地方

法线贴图的颜色为什么都这么怪?

事实上,真正的法线贴图并不是记录贴图上每个点的法线的绝对角度,而是记录的是相对于平面的一个差值。这样的话,随着平面的3D变换都能够实现即时的法线运算了。而借RGB数据存储的法线信息会被法线方向扰动,于是在RGB(x,y,z)上的平均分布空间上 x,z 值较大或者y得值比较大 因此表现出来合成的结果上普遍是紫色(xz)代表面 而绿色(y)反应深浅

 Specular Map -

高光贴图 表现质感 高光区域大小可真实反映材质区别

 Gloss Map -

光泽贴图,每个纹理元素上描述光泽程度

而讲解贴图和纹理的工作机理中这篇博客讲的很不错

总体上说 他们都是要被shader加工的原材料

正式篇前导二,光照模型?

我们先来探讨一下几个问题

1. 当一个物体能被我们看见需要具备什么因素 ?

2. 当一个物体是不同材质的时候,视觉上的区别在哪里?

带着这两个问题 我们来说一下 什么是光照模型
它总结了在什么情况下 一个物体能够被看见 以及以一个近似的概念描述了真实情况下光照的种类

基础-Phone式光照模型(Phone reflection model

 真实世界中的光照效果抽象为三种独立的光照效果的叠加

1.环境光(Ambient)

此为模拟环境中的整体光照水平,是间接反射光的粗略估计,间接反射的光使阴影部分不会变成全黑 关于环境光还有个事实,1某个可以独立分析的局部场合的环境光强和能够进入这个地方的光的强度有关。
其计算公式为:技术分享

2.漫反射光(Diffuse)

模拟直接光源在表面均匀的向各个方向反射,能够逼近真实光源照射到哑光表面的反射。比如在阳光下,由于路面粗糙的性质,我们发现从任意一个角度观察路面,亮度都是差不多。
其计算公式为技术分享

3.镜面反射光(Specular)

模拟在光滑表面会看到的光亮高光。会出现在光源的直接反射方向。镜子、金属等表面光亮的物体会有镜面反射光。镜面反射光同时与物体表面朝向、光线方向、视点位置有关。如图
技术分享
I是入射光,N是表面法线,R是反射光线,V是从物体上的目标观察点指向视点的向量,a是V和R的夹角。

我们可以判断出一个规律,夹角a越小,即视线与反射方向的偏离越小,则目标点的光强越大
其计算公式为:技术分享

Ks为物体对于反射光线的衰减系数
Shininess为高光指数

高光指数反映了物体表面的光泽程度。
Shininess越大,反射光越集中,当偏离反射方向时,光线衰减的越厉害,只有当视线方向与反射光线方向非常接近时才能看到镜面反射的高光现象,此时,镜面反射光将会在反射方向附近形成亮且小的光斑;
Shininess 越小,表示物体越粗糙,反射光分散,观察到的光斑区域小,强度弱。

以上所在的公式都是便于理解的形式 实际要复杂得多

 最后可得:技术分享

其组成类似于
技术分享

至于更多的光照模型结合了更多的物理光学等信息 在模拟单种材质例如塑料 合金 石膏陶瓷等等上要好于Phone式模型的效果 但是基本上属于Phone式模型的扩充


有了以上基础概念 我们就可以更好的理解shader的编写了

正式篇第一节 扫盲

前一章提到了 可编程shader分为vertex (顶点)/fragement(片段) shader
其可由多种语言去编写 比如CGSL/HLSL/CG
我们先来比较一下 三种主流语言的优缺点

GLSL:

基于OpenGL的OpenGL Shading Language

CG:

由NVIDIA公司开发。Cg极力保留C语言的大部分语义

HLSL :

基于DirectX的High Level Shading Language,


Unity官方手册上讲Shader程序嵌入的小片段是用Cg/HLSL编写的,从“CGPROGRAM”开始,到“CGEND”结束。

我们看一段由unity初始化的shader本体:


    Shader "Custom/s1" //级联菜单/shader名称
    {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}//主纹理
        _Glossiness ("Smoothness", Range(0,1)) = 0.5//光泽度
        _Metallic ("Metallic", Range(0,1)) = 0.0//金属
    }
    //属性  可在inspector中修改赋值  相当于供加工的材料
    SubShader {
        Tags { "RenderType"="Opaque" }//渲染标签
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        //基于物理的光照模型 打开所有光照类型的阴影
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        //使用3.0模型得到更好的视觉效果
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            //反照率来自于纹理着色的颜色
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            //金属性和平滑度来自于滑动变量
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
    }

所以,

Unity官方主要是用Cg/HLSL编写Shader程序片段。Unity官方手册也说明对于Cg/HLSL程序进行扩展也可以使用GLSL,不过Unity官方建议使用原生的GLSL进行编写和测试。
如果不使用原生GLSL,你就需要知道你的平台必须是Mac OS X、OpenGL ES 2.0以上的移动设备或者是Linux。在一般情况下Unity会把Cg/HLSL交叉编译成优化过的GLSL。因此我们有多种选择,我们既可以考虑使用Cg/HLSL,也可以使用GLSL。
不过由于Cg/HLSL更好的跨平台性,更倾向于使用Cg/HLSL编写Shader程序。

第二节-固定管线

我们采用更为简单易理解的方式去编写 那就是固定管线 在surface shader中pass通道被忽略 但是我们可以以一种更好的方式去理解

一个helloword性质的shader如下

    Shader "Custom/s2" {
      SubShader{
         pass {
          color(1,1,1,1)//白色
              }
           }
         }

把它和材质相结合之后 渲染出的球如下
技术分享

但是我们看到他只是画出了一个球而已 没有任何三维特征
我们再回到之前说的光照的阶段 如果一个三维物体想被看见 简单的来说要具备哪些必要的元素呢?
那就是

在固定管线中 其实有着简单的光照指令 这时候我们可以试着添加properties以供我们可以更好的添加需要加工的素材
像这样:技术分享
这样之后我们就可以在inspector里实时调节这个颜色

    Shader "test/s2" {
    properties
    {
        _color("main color",color)=(1,1,1,1)
    }
    SubShader{
    pass {
        material//命令块
        {
          diffuse[_color]//反射光

        }
        lighting on// 打开光照的命令
        //color(1,1,1,1)
    }
    }
    }

这下我们打开了光照 瞬间看起来像是那么回事了


技术分享

但是相比较而言似乎远远没有场景中其他物体特征那么明显
说到了光照自然不得不提光照模型:
技术分享
那么我们把环境 反射 高光加入shader的固定管线之后再加入高光强度参数描述


    Shader "test/s2" {
    properties
    {
        _color("main color",color)=(1,1,1,1)//RGBA
        _ambient("ambient",color)=(0.3,0.3,0.3,0.3)
        _specular("specular",color)=(1,1,1,1)
        _shininess("specular",range(0,8))=4
    }
    SubShader{
    pass {
        material
        {
          diffuse[_color]//反射光
          ambient[_ambient]//环境光
          specular[_specular]//高光
          shininess[_shininess]//高光强度
        }
        lighting on// or off//打开光照
        separatespecular on//镜面高光开启
    }
    }
    }

技术分享

到这里他已经比场景里其他东西真实度高得多了

现在

下面我们开始加入 材质
我们用两张风格迥异的素材作为混合元素
技术分享技术分享

为了加入不同材质我们要加入一些其他亦可赛艇的东西
但这之前我们需要了解一点先导也就是alpha测试
当渲染器在工作的时候必须要确定相对于摄像机的视角 物体的深度序列 来确定物体之间的遮挡 ,半透明物体的透光等 所以在
我们再来看看这张渲染步骤图:
技术分享

通过以下叠加结合的过程当我们把alpha调低后就可以看到半透明的物体了

    Shader "test/s2" {
    properties
    {
        _color("main color",color)=(1,1,1,1)  //color RGBA
        _ambient("ambient",color)=(0.3,0.3,0.3,0.3)
        _specular("specular",color)=(1,1,1,1)
        _shininess("specular",range(0,8))=4  //range(x,y)
        _Emission("Emission",color)=(1,1,1,1)
        _Maintex("MainTex",2d)=""             //texture 2d
        _SecondTex("SecondTex",2d)=""
    }
    SubShader{
    Blend srcalpha OneminusSrcAlpha
    //混合1-scralpha前面的图中有提到
    pass {
         //______________________________________________________
        material
        {
          diffuse[_color]//反射光
          ambient[_ambient]//环境光
          specular[_specular]//高光
          shininess[_shininess]//高光强度
        }
        //________________________________________________________
        lighting on// or off
        separatespecular on//镜面高光

        settexture[_Maintex]{
            //主纹理
            combine texture * primary double//结合,且将之前的x2 or quad x4
            //primary 代表顶点光照(material命令快)过后的颜色
        }
        settexture[_SecondTex]{//第二纹理
            combine texture * previous double//x2
            //previous 代表所有之前的颜色
        }
    }
    }
    }

技术分享

 我们看到  

者甚至出现了一个奇怪的好处就是可以营造

镂空效果

Tiling表示UV坐标的缩放倍数,Offset表示UV坐标的起始位置。如下图可见
技术分享
技术分享
当我们贴上了三角面 则可以看到模型的与之对应的过程
技术分享


一个模型能够正确被显示,贴图和模型的uv坐标必须能够对应

而我们利用其起始坐标和缩放系数的修改就可以营造出运动和镂空等多种效果。

第三节surface shader

我们再回到由unity初始化的surface shader中
当然我们学习surface最好的途径就是unity 手册的网站:
surface shader document

    Shader "Custom/s3" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        //渲染标签:渲染类型=不透明
        LOD 200

        CGPROGRAM//CG语言开始阶段

        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0//最高支持target5.0直接应用DX11
        //编译指令
        //#pragma surfaceFuntion Lightmodel [Optionalparams]
        //#pragma 函数名称 光照模式 [选项]
        //standard光照模型在unityPBSlighting.cginc中
        //默认fullforwardshadows 阴影

        struct Input {
            float2 uv_MainTex;
        };
        //纹理坐标必须以uv开头
        sampler2D _MainTex;
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        //在CG语言中需对properties的变量再次声明一一对应 
        //但是其变量类型并不是完全一致



        void surf (Input IN, inout SurfaceOutputStandard o) {
            // 来自纹理作色的反照率
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // 一个能被滑块定制的决定平滑度和金属性的变量其实就是range那个能被滑条操纵的变量
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        //处理段
        ENDCG//CG语言结束阶段
    }
    FallBack "Diffuse"
    }

我们看到了几乎是完全两种语言一样 因为他就是两种语言 unity推崇的surface shader中应用了CG语言的一部分因此 在其中特定区域可以写CG语言 同时 surface shader本身就是对两种可编程管线也就是vertex shader和fragment shader的包装 也因此 其中可探究的部分 真可谓写一本书都不够

下面

我们就从上到下的顺序开始对其进行解析一个标准的surface sahder有哪些不同的地方

1.渲染标签

    Tags { "RenderType"="Opaque" }
    //渲染标签:渲染类型=不透明

???????????

2.编译指令字段

        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0
        //编译指令
        //#pragma surfaceFuntion Lightmodel [Optionalparams]
        //#pragma 函数名称 光照模式 [选项]
        //standard光照模型在unityPBSlighting.cginc中
        //默认fullforwardshadows 阴影

由注释可以看到 函数名称后的光照模式和选项部分 其实是我们完全没有见过的东西

Tips:PBS

(基于物理的光照运算,从unity5开始引入,技术源于迪士尼。在unity4中不可用)


2.1光照模式

unity内部的东西实际上我们在客户端的安装目录中就可以找到这段指令的来源
Unity\Editor\Data\CGIncludes
这里包含了所有渲染需要用的引用
当我们找到了unityPBSlighting.cgnic这个文件就可以找到其来源

    inline half4 LightingStandard (SurfaceOutputStandard s, half3 viewDir, UnityGI gi)
    {
    s.Normal = normalize(s.Normal);

    half oneMinusReflectivity;
    half3 specColor;
    s.Albedo = DiffuseAndSpecularFromMetallic (s.Albedo, s.Metallic, /*out*/ specColor, /*out*/ oneMinusReflectivity);

    // shader relies on pre-multiply alpha-blend (_SrcBlend = One, _DstBlend = OneMinusSrcAlpha)
    // this is necessary to handle transparency in physically correct way - only diffuse component gets affected by alpha
    half outputAlpha;
    s.Albedo = PreMultiplyAlpha (s.Albedo, s.Alpha, oneMinusReflectivity, /*out*/ outputAlpha);

    half4 c = UNITY_BRDF_PBS (s.Albedo, specColor, oneMinusReflectivity, s.Smoothness, s.Normal, viewDir, gi.light, gi.indirect);
    c.rgb += UNITY_BRDF_GI (s.Albedo, specColor, oneMinusReflectivity, s.Smoothness, s.Normal, viewDir, s.Occlusion, gi);
    c.a = outputAlpha;
    return c;
    }

尤其要注意的是 定义这类光照模式的时候要以lighting为开头否则编译会出错
这是unity5之后使用的新型光照模式
从这个文件中我们还可以找到其他的的模式类型也就是我们熟悉的BlinnPhong


inline fixed4 LightingBlinnPhong (SurfaceOutput s, half3 viewDir, UnityGI gi)
{
    fixed4 c;
    c = UnityBlinnPhongLight (s, viewDir, gi.light);

    #if defined(DIRLIGHTMAP_SEPARATE)
        #ifdef LIGHTMAP_ON
            c += UnityBlinnPhongLight (s, viewDir, gi.light2);
        #endif
        #ifdef DYNAMICLIGHTMAP_ON
            c += UnityBlinnPhongLight (s, viewDir, gi.light3);
        #endif
    #endif

    #ifdef UNITY_LIGHT_FUNCTION_APPLY_INDIRECT
        c.rgb += s.Albedo * gi.indirect.diffuse;
    #endif

    return c;
}

2.2选项部分
Optional parameters在unity手册中写的比较清楚 类型过于多也不适合去展开说明我们就挑选出现了的阴影选项去讲解一下

Shadows and Tessellation - .附加指令以控制如何处理阴影和曲面细分。

addshadow - 通常使用自定义顶点修改,使得阴影投射也获得任何过程顶点动画。

fullforwardshadows - 支持正向渲染路径中的所有光影类型。默认情况下,着色器仅支持来自正向渲染中的一个定向光源的阴影(以保存内部着色器变量计数)。如果需要在正向渲染中点或点亮阴影,请使用此指令。

tessellate:TessFunction - 使用dx11计算曲面细分因子

3.定义部分

这部分倒是没什么说的 CG语言入门不是本篇文章的目的,所以各种变量类型以及作用都不做介绍
到了这里本文其实是更重视流程的阐述 所以在这个思想上我们往下看
不能直接处理定义在properties 中的元素
所以要在处理之前做定义以映射相关元素


        struct Input {
            float2 uv_MainTex;
        };
        //纹理坐标必须以uv开头
        sampler2D _MainTex;
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        //在CG语言中需对properties的变量再次声明一一对应 
        //但是其变量类型并不是完全一致

4.函数部分

所定义的函数部分要从更高的层面去讲解了
必须要回到我们的引用中去

    void surf (Input IN, inout SurfaceOutputStandard o) {
            // 来自纹理作色的反照率
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // 一个能被滑块定制的决定平滑度和金属性的变量其实就是range那个能被滑条操纵的变量
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }

参数列表,Cg 中还提供了三个关键字,in、out、inout,用于表示函数的输入参数的传递方式,称为输入/输出关键字,这组关键字可以和语义词合用表达硬件上不同的存储位置,即同一个语义词,使用in 关键字修辞和out 关键词修辞,表示的图形硬件上不同的寄存器。
我们在之前的pbslighting文件中找到参数列表中的输入输出参数类型是什么
输入部分(初始化中定义过了):

        struct Input {
            float2 uv_MainTex;
        };

输出部分:

    struct SurfaceOutputStandard
    {
    fixed3 Albedo;      // base (diffuse or specular) color
    fixed3 Normal;      // tangent space normal, if written
    half3 Emission;
    half Metallic;      // 0=non-metal, 1=metal
    // Smoothness is the user facing name, it should be perceptual smoothness but user should not have to deal with it.
    // Everywhere in the code you meet smoothness it is perceptual smoothness
    half Smoothness;    // 0=rough, 1=smooth
    half Occlusion;     // occlusion (default 1)
    fixed Alpha;        // alpha for transparencies
    };

整体来看 我们也能熟悉这个整体的套路
函数作为输入和输出的载体,而输入和输出都有着固定的模式,根据输入的IN计算OUT进入缓存。

当我们完成了surface shader的编写可以通过查看shader编译结果的方式了解整个处理流程
短短的一段surface shader被编译成8000多行的真正的执行shader其中还有d3d的支持部分
我们截取一小段来看一下例如:

    -- Vertex shader for "d3d11":
    // Stats: 22 math
    Uses vertex data channel "Vertex"
    Uses vertex data channel "Color"
    Uses vertex data channel "TexCoord"

    Constant Buffer "$Globals" (128 bytes) on slot 0 {
    Vector4 _MainTex_ST at 96
     }
    Constant Buffer "UnityLighting" (720 bytes) on slot 1 {
      Vector4 unity_SHBr at 656
      Vector4 unity_SHBg at 672
      Vector4 unity_SHBb at 688
      Vector4 unity_SHC at 704
    }
   Constant Buffer "UnityPerDraw" (352 bytes) on slot 2 {
     Matrix4x4 glstate_matrix_mvp at 0
     Matrix4x4 unity_ObjectToWorld at 192
     Matrix4x4 unity_WorldToObject at 256
    }

   -- Fragment shader for "d3d11":
    // Stats: 30 math, 4 textures, 2 branches
    Set 2D Texture "_MainTex" to slot 0
    Set 3D Texture "unity_ProbeVolumeSH" to slot 1

    Constant Buffer "$Globals" (128 bytes) on slot 0 {
     Float _Glossiness at 64
     Float _Metallic at 68
     Vector4 _Color at 80
    } 
    Constant Buffer "UnityLighting" (720 bytes) on slot 1 {
      Vector4 unity_SHAr at 608
      Vector4 unity_SHAg at 624
      Vector4 unity_SHAb at 640
    }
    Constant Buffer "UnityProbeVolume" (112 bytes) on slot 2 {
     Matrix4x4 unity_ProbeVolumeWorldToObject at 16
     Vector4 unity_ProbeVolumeParams at 0
   Vector3 unity_ProbeVolumeSizeInv at 80
     Vector3 unity_ProbeVolumeMin at 96
    }

这两个片段很好的描述了整个着色过程体现在代码阶段的样貌

当然

这只是凤毛菱角 函数中对于信息的计算和各种算法以及处理流程才是最重要的。而CG的入门应该在
NVIDIA_CG_page这里去入门。我们主要说流程以及不同的地方。

下一篇文章我们会讲解unity shader编写中怎样实现一些好的效果

Good bye Next Month~~

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    《Unity Shader 与 计算机图形学》第二章