渲染技术专题

# 渲染管线技术

# Shader 是什么

Shader 着色器,是一种较为短小的程序片段,告诉图形硬件如何计算和输出图像。简单的说 Shader 就是可编程图形管线的算法片段。主要分为 Vertex Shader (顶点着色器) 和 Fragment Shader (片元着色器)。

# Shader 编程语言

HLSL:基于微软公司 DirectX。

GLSL:基于 OpenGL。

CG:Nvidia公司发明的图形编程语言。

OPenGL(Open Graphics Library) 是跨编程、跨平台的编程接口,是一个与硬件无关的软件接口。支持 OpenGL 的软件具有很好的移植性。
CG(Computer Graphics) 是一种脚本语言,它的宿主就是 Dircet3D 和 OpenGL。CG 语言是运行在 Dircet3D 和 OpenGL 之上的。
1
2

# Shader 、材质、贴图之间的关系

Shader 实际上是一小段程序,它负责将输入的顶点数据以指定的方式和输入的贴图或者颜色等组合起来,然后输出。绘图单元可以依据这个输入将图像绘制到屏幕上。

贴图、颜色、Shader、以及对 Shader 参数的设置,将这些打包到一起就得到了材质 Material。

它们之间可以形象的比喻为:贴图、颜色是原料,Shader 是工厂,材质是商品。

# 什么是渲染管线

Render 渲染或者称为绘制,是指在电脑绘图中以软件将模型生成图形的过程。渲染是三维计算机图形学中最重要的研究课题之一,并且在实践领域它与其它技术密切相关。渲染用于描述计算视频编辑软件中的效果,以生成最终视频的输出过程。在图形流水线中,渲染是最后一项重要步骤,通过它得到模型与动画最终显示效果。自从二十世纪七十年代以来,随着计算机图形的不断复杂化,渲染也成为一项越来越重要的技术。

渲染的应用领域包括:计算机与视频游戏、模拟、电影或者电视特效以及可视化设计,每一种应用都是特性与技术的综合考虑。渲染涉及的学科理论包括光学、视觉感知、数学以及软件开发。

模型是用语言或者数据结构进行严格定义的三维物体或者虚拟场景的描述,它包括几何、纹理、视点、照明和阴影等信息。

图像是数学图像或者位图图像。

三维计算机图形渲染可分为预渲染 (Pre-rendering / Offline rendering) 和实时渲染 (Real-time rendering / Online rendering)。预渲染主要用在影视动画,实时渲染主要用在游戏仿真。渲染通常透过图形处理器GPU 来完成。

游戏引擎绘制整个游戏场景中物体的一个方式和流程,获取场景资源并将其渲染到屏幕上。一个渲染管线就是一个 pass。渲染的策略分为内置渲染管线(向前渲染管线与延时渲染管线)和可编程渲染管线(自己定制的渲染管线)。

渲染管线也叫渲染流水线,是显示芯片内部处理图形信号相互独立的并行处理单元。一个流水线是一系列可以并行和按照固定顺序进行的阶段。每一个阶段都从它的前一个阶段接收输入,然后把输出发给随后的阶段。就像一个在同一时间内,不同阶段不同的汽车一起制造的装配线,传统的图形硬件流水线以流水线的方式处理大量的顶点、几何图元和片段。

# GPU的优越性

GPU 具有大量的并行处理单元。

# 游戏引擎如何绘制物体

CPU 将要绘制的数据加载到内存中,然后再拷贝到显存,最后 CPU 给 GPU 下绘制命令。具体步骤:

游戏引擎将美术提供的 3d 模型加载进来。
读取文件,放到内存,再拷贝到显存。
游戏世界中,物体在场景中执行平移、旋转、缩放、矩阵变换等操作。
放置一个摄像机,定义了观察者的数据 (位置、方向),视角 (摄像机的属性,成像方式包括透视与正交)。
模型空间变换是局部坐标 - 世界坐标 - 视图坐标 - 裁剪 - 齐次坐标。
传递模型数据给 gpu,模型的点、线、面、纹理数据、光照的一些数据等,顶点数据包括模型坐标-纹理坐标-法线向量-切线向量等。
纹理数据如何生成:先弄出一个模型,想给模型上色。准备一张白纸,用白纸包住模型,每个顶点按下一个钉子,这样白纸就固定到模型上。给白纸上涂颜色就是所谓的纹理。每个钉子的洞就是纹理坐标。
所有的数据都准备好后就下一个 drawcall 指令。
1
2
3
4
5
6
7
8

# 摄像机如何渲染物体颜色

  • 渲染公式

    这个公式也可以看做是:人眼看到的颜色 = 自发光 + 物体本色 * 反射周围环境中所有光的叠加。
    自发光物体:太阳、电灯、PBR 材质的自发光等。
    物体本色:baseColor色/diffuseColor色/albedo色。
    反射光:直接光源过来的光,经过物体 A 反射又经过物体 B 反射过来的光。(PBR 材质的漫反射 + 高光反射)
    环境遮挡:物体放在不同的环境中也会有影响,比如物体 A 遮挡物体 B,或者物体自身的一部分会遮挡另一部分。(PBR 材质的 AO 贴图)
    自然界大部分的物体是不发光的,但是可以反射周围环境的光,看起来就有了光。自然界物体表面反射出不同颜色的光是因为可以吸收一些颜色,反射出另一些颜色。
    
    1
    2
    3
    4
    5
    6
  • 游戏开发时简化处理

    情况一:
    
    反射光:只考虑直接光源过来的光,其他的反射光我们不考虑。
    环境遮挡:我们不考虑其他物体对这个物体的环境遮挡,只考虑自身环境的遮挡。
    
    简化后计算公式:人眼看到的颜色 = 自发光 + 物体本色 * (反射周围环境光源的光 + 自己物体环境的遮挡) + 环境光。
    
    1
    2
    3
    4
    5
    6
    情况二:
    
    反射光进一步分为镜面反射和漫反射时。
    
    简化后计算公式:人眼看到的颜色 = 自发光 + 物体本色 * (镜面反射光源的光 + 漫反射光源的光 + 自己物体环境的遮挡) + 环境光。
    
    1
    2
    3
    4
    5
  • 各种光包含的内容有

    自发光:颜色 + 贴图。
    本来的颜色:baseColor/Diffuse/Albedo建模时的纹理贴图。
    自身环境遮挡:贴图。
    环境光:引擎场景里整体的光照。
    
    简化的摄像机看到的颜色:
    方案1 = baseColor
    方案2 = baseColor * 漫反射的光
    
    1
    2
    3
    4
    5
    6
    7
    8

# 光照计算与法线贴图

着色 = 自发光 + 物体本色 * (漫反射 + 镜面反射 + 环境遮挡) + 环境光;

解决反射的问题有基于经验模型和基于物理 PBR 模型两个方向。

  • 基于经验模型:

    漫反射 (兰伯特 + 半兰伯特)

    镜面反射 (Phong 高光,BlinPhong 高光)

  • 基于物理PBR:

    遵循物理定律(公式和能量守恒)

    模拟真实物理世界的漫反射和反射来计算

    法线:光照计算会影响渲染的效果和细节。而法线参与光照计算,所以是一个非常重要的数据。
    法线贴图:是一种细节增强技术。(渲染管线不使用插值数据而是用法线数据)
    增强细节的方式有建高模,体现细节。但是高模计算量变大了。法线贴图可以在不增加面数的情况下来增加模型的细节。法线贴图里包含了高模每个点的法线数据,这样这张贴图贴到低模上通过 UV 提取出高模法线数据就可以体现出高模的细节了。
    
    1
    2
    3
    自发光传说

    自发光(Emissive) 是一种物体表面发出光线的效果,通常用于表示某些物体本身就发光,而不是反射周围的光源。换句话说,自发光的物体看起来就像是光源一样,它能够自己产生光并照亮周围的环境,而不需要外部光源照射。

    一、自发光的原理:

    自发光的贴图(Emissive Texture): 自发光效果通常通过一个专门的纹理来实现,这个纹理被称为自发光贴图(Emissive Map)。这张贴图的作用是定义物体表面上哪些区域是发光的,哪些区域是非发光的。自发光的区域通常会表现为非常亮的颜色。

    例如,一个具有自发光贴图的物体可能会有一些区域显示为亮的红色,而其他区域则保持正常的外观。

    Shader 中的 Emissive 属性: 自发光通常是在材质的 Shader(着色器)中处理的。在现代的图形渲染引擎中(如 Unity 或 Unreal Engine),自发光是通过 Shader 中的 Emissive 属性来控制的。这个属性决定了物体的表面是否会发光,以及它的发光强度。

    例如,在 Unity 的 Standard Shader 中,你可以设置 Emission(自发光)属性,选择颜色和强度,使物体看起来像是一个发光体。

    光照模型中的自发光: 自发光是与场景中的光源不同的。它不依赖于场景中的其他光源,而是物体本身产生光。在 PBR(物理基础渲染)中,自发光被视为物体的一个独立属性,它不会影响周围的光照计算,但在渲染时,物体的表面会根据自发光贴图的强度来亮化。

    二、自发光的作用

    模拟发光物体:自发光通常用于表现实际发光的物体,如 LED 灯、火焰、屏幕、星星等。它可以用来给物体添加更加生动、真实的效果。

    增强视觉效果:自发光效果可以帮助物体从其他物体中脱颖而出,给物体增加视觉吸引力。例如,游戏中常用自发光来表示激活的按钮、发光的道路标志或具有高科技感的物品。

    动态光源:在某些情况下,自发光也可以用来模拟一个实际的光源。尽管它不影响周围的环境光照,但一些引擎(如 Unity)提供了选项,使得自发光物体不仅看起来发光,还可以给周围的物体提供间接光照(如通过光照探针或者通过启用光源)。

    三、在 Unity 中使用自发光:

    在 Unity 中,自发光通常通过材质的 Emission(发光)属性来实现。你可以这样做:

    创建自发光材质

    在 Unity 中,选择一个材质,找到材质属性面板中的 Emission 部分。

    点击 Emission 方框来启用自发光属性。

    你可以设置 Color 来定义自发光的颜色,选择一个亮色,比如白色、红色、蓝色等。

    如果有自发光的贴图,你可以将它拖动到 Emission Map 插槽中,来控制物体的自发光区域。

    如果你希望自发光更亮,可以调整 Intensity(强度)值。

    添加光源效果(可选)

    如果你希望自发光的物体不仅看起来发光,还能实际照亮周围的物体,可以通过在该物体上附加 Light 组件来实现。

    例如,你可以将一个点光源添加到物体上,使得它会影响场景中的其他物体。

    四、自发光的使用场景

    显示电子屏幕、按钮、指示灯:用于游戏或应用中的虚拟屏幕、按钮、指示灯等,这些物体通常会发光,给用户或玩家反馈。

    例如:虚拟手机屏幕、显示器、交通信号灯等。

    火焰、光源、魔法效果:用于表现火焰、魔法、能量球等物体,它们本身发光并照亮周围环境。

    例如:火把、魔法水晶、发光的植物等。

    夜晚或暗黑场景:自发光可以帮助在黑暗环境中增加一些亮度,例如,发光的星星、夜间的街灯等。

    装饰性效果:自发光也常用在装饰性物品上,比如具有未来感的科幻道具、发光的地板、虚拟现实设备等。

    五、总结

    自发光是一种让物体本身发出光的效果,不依赖于场景中的外部光源。它通过自发光贴图和材质的 Emission 属性来实现,使得物体可以在暗环境中发光,或者表现为一些实际的光源。自发光可以提升视觉效果,模拟发光物体,同时在某些情况下,结合光源使用时也可以影响其他物体的光照。

    光和颜色的故事

    贴图的颜色和光确实有密切关系,但它们不是同一个东西。简单来说:

    贴图颜色(Texture Color):是贴图本身的颜色信息。它通常由图像文件(如 PNG、JPG、TGA 等)存储并应用到物体表面,用来表现物体的细节、外观、纹理等。贴图的颜色不会直接受光照影响,而是根据材质和纹理本身的像素值来显示。

    光(Lighting):是场景中光源发出的光线对物体表面颜色的影响。光照模型通过模拟光线的传播、反射、折射等,来决定物体表面在不同光照条件下的颜色表现。光照通常是动态的,受到光源的类型(如点光源、方向光、聚光灯等)、强度、位置、角度、颜色等因素的影响。

    更详细的解释:

    • 贴图颜色:

      贴图是物体表面的一种纹理,包含了从图像中获取的颜色信息。当你在 Unity 中使用贴图时,这些颜色数据会根据材质被渲染到物体表面。贴图可能是:

      • Diffuse / Albedo(漫反射贴图): 这是最常见的贴图类型,包含了物体表面的基础颜色或图案。
      • Specular / Roughness(镜面反射贴图/粗糙度贴图): 用于控制物体表面反射的光泽度或粗糙度。
      • Normal Map(法线贴图): 用于模拟细节的凹凸感,而不是直接影响颜色。

      贴图中的颜色数据本身是固定的,并不随光照变化而变化。比如,如果你给一个物体应用了一个红色的贴图,它在没有光照的情况下就会是红色的,贴图的颜色是静态的。

    • 光源会影响物体的最终视觉效果,光照计算会考虑物体表面的反射特性、材质类型、光源位置和方向等因素。具体来说,光照有以下几种类型:

      • 环境光(Ambient Light):无方向的光,均匀照亮整个场景,使所有物体都有一定的光照。
      • 定向光(Directional Light):模拟太阳光,光线是平行的,影响整个场景。
      • 点光源(Point Light):从某个点发出的光,照亮周围的物体,类似灯泡的效果。
      • 聚光灯(Spot Light):从一个点发出,光线呈锥形,照射到指定区域。

      这些光源会根据物体的材质属性(如漫反射、镜面反射、金属度等)来影响物体的显示效果,使物体在不同光照下的表现看起来不同。例如:

      • 在有光照的情况下,物体的颜色可能变得更亮或更暗,或者颜色的阴影部分变得更深。
      • 如果场景中有强光照射,物体表面的颜色和贴图会根据光照的强度、角度等产生高光、阴影等 效果。
    • 贴图颜色与光的交互

      在现代的渲染引擎中,贴图的颜色与光照是结合在一起工作的。你可以把它看作是“基础颜色”和“光照效应”的组合:

      • 基础颜色(贴图颜色):通过纹理贴图来定义物体表面的外观。
      • 光照效果:光照会根据光源的种类、强度、物体的材质属性(如漫反射、镜面反射等)来影响物体表面最终显示的颜色。

      例如,如果一个物体有一个红色的贴图,并且正面有一个强光源照射,那么物体的表面可能会表现为红色的同时,受到光照的影响(可能会变得更亮,或者有高光效果)。

    • Shader 的作用:

      在实际开发中,Shader(着色器)会处理贴图的颜色和光照的交互。例如,PBR(物理基础渲染)模型会根据不同的光照条件计算物体表面的颜色,使得物体表现更加真实。PBR 渲染模型结合了贴图(如 Albedo、Normal、Metallic、Roughness)和光照模型,来计算每个像素的最终颜色。

    • 举个例子

      你有一个物体,使用了一个红色的漫反射贴图(Albedo)。如果没有光照,物体看起来是均匀的红色。

      如果场景中有一个强烈的点光源照射到物体上,物体表面会根据光照强度和材质特性(如反射、粗糙度等)显示出不同的亮度和阴影区域。

      如果你使用 PBR 渲染,物体表面的反射和高光效果会根据光照的角度、强度等变化,给人一种更自然的感觉。

    • 总结

      贴图的颜色 是物体表面纹理本身的颜色信息,通常由纹理图像文件提供。

      光照 是场景中的光源对物体表面颜色的影响,它通过材质和光照模型来决定物体在不同光照下的外观。

      两者的关系是,贴图提供了物体的基础颜色,而光照则决定了这些颜色如何表现(亮度、阴影、反射等)。

      所以,贴图颜色和光是相互独立的两个概念,但它们共同决定了物体在场景中的最终视觉效果。

# 逐顶点光照和逐像素光照

shader 里如何来体现光照?
颜色 = (baseColor/Diffuse/Albedo) * 光的颜色
光的颜色 * 漫反射强度 = 漫反射光的颜色
光的颜色 * 镜面反射的强度 = 镜面反射光的颜色
逐顶点光照:在顶点 shader 的时候,计算每个顶点的光照颜色。计算光照次数少,性能好,效果差。
逐像素光照:在片元 shader 的时候,baseColor * 插值的光照颜色。计算光照的次数多,性能差,效果好。

# 法线贴图原理与数据存储

  • 法线的作用是什么?

    光照计算时都会用到法线,它是光照计算中非常重要的数据。

    光照的反射,对效果影响也是非常重要的。

  • 片元着色器计算光照的时候法线如何来的?

    着色点(片元)在计算光照时,获取法线的方法是由渲染管线三角形顶点法线插值得到的。如果对于平滑的物体,可能影响不大,但是对于那些细节比较细腻的模型使用插值效果就不太好了。

    解决办法有:

    将模型做精致,增加面数,但是这会产生较大的性能消耗。
    采用法线贴图技术实现细节增强,建立高低模,制作法线贴图并将其赋给低模就能够实现细节增强了。低模上的片元UV坐标寻址到高模的法线数据,再经过光照计算,就实现了真实的细节。
    
    1
    2
  • Shader里如何获取法线数据?

    A-RGBA(0-255) 转换成 B-RGBA(0-1) 的范围,再转换成 C-RGBA(-1,1) 的范围。

    A/255--->B*2-1--->C

# 渲染队列与ZTest、ZWrite

  • 游戏引擎的渲染队列:

    一般情况下,绘制画面,先绘制最远的,然后一层一层,直到最近的物体。

    3D 为了优化,反过来的,先绘制距离摄像机较近的,再绘制较远处的物体。这样远处被遮挡的就不用绘制了。

    • 先绘制的物体,先将其片元颜色写入颜色缓冲区中。
    • 深度缓冲区记录这个片元的深度值。
    • ZTest 绘制后面物体的片元的时候,先到深度缓冲区里面比较,这个片元是不是比当前的深度缓冲区里最近的片元还要近,ZWrite 如果是,绘制这个片元,把颜色输出到颜色缓冲区里面,把深度写入到深度缓冲区中。如果不是,就不绘制不写入。

    如果要绘制透明物体,使用上面的规则就不适用了。这时需要用渲染队列来解决。A队列 1000; B队列 2000; C队列 3000; D队列 4000 ... ... 根据游戏场景物体的队列来排布。游戏引擎绘制时按照一个一个队列来绘制的。先绘制小序号队列中物体并将其放入颜色缓冲区,再绘制大序号队列中的物体并将其放入颜色缓冲区。

  • 深度测试与深度写入

    ZTest:深度测试,绘制片元的深度和当前片元的深度进行比较,通过深度测试才着色,通不过就丢弃。深度测试:ALWAYS:永远通过,这样片元的颜色就输入到颜色缓冲区。NEVER:永远通不过,这样偏远的颜色就不输入到颜色缓冲区。LESS:小于,距离摄像机近的,就通过测试。... ...

    ZWrite:深度写入,是否把片元的深度信息更新到颜色缓冲区。

    ZWrite off: 关闭深度写入 ,不会把片元的深度信息写入颜色缓冲区。

    ZWrite on: 开启深度写入 ,可以把片元的深度信息写入颜色缓冲区。

    通常情况下,片元更新了颜色缓冲区后也会更新深度信息。

    Blend:颜色与颜色缓冲区中的颜色进行混合。

    直接覆盖和透明度混合,常用的 Blend 混合模式有下面几种:

    正常混合:Blend SrcAlpha OneMinusSrcAlpha。

    Darken变暗:Blend Op Min。

    Screen滤色:Blend OneMinusDstColor One 或者 Blend One OneMinusSrcColor。

    Soft Additive柔和叠加:Blend OneMinusDstColor One。

    Lighten变亮:BlendOp Max。

    Multiply正片叠底、相乘: Blend DstColor Zero。

    2x Multiply 两倍叠加、相加:Blend DstColor SrcColor。

    线性叠加:Blend One One。

  • 总结

    游戏渲染的时候,从前往后绘制,这样可以减少片元着色次数。后面被前面遮挡的片元就不用参与着色计算了,这样就是做了优化。

    ZTest:深度测试,测试片元距离摄像机最近的一个深度。深度测试后可以确定哪些片元需要被绘制。

    ZWrite:深度写入,通过深度测试的片元是否写入深度缓存。深度测试通过的如果不开启深度写入也可以,只是图像会出现不是我们想要的效果。

    颜色混合:将目标颜色和源颜色进行混合。

# 光照衰减

  • 各种光源的衰减计算公式

    点光源:球体衰减。

    聚光灯:光源到目标点方向的衰减 + 聚光范围上的衰减。

    衰减计算方式:直接使用公式计算 | 空间换时间,将衰减系数存放到一张纹理贴图里,然后再从纹理中提取出来。

  • 计算光照衰减的原理与源码分析

# 阴影原理与计算

  • 引擎阴影计算的原理

    计算阴影区域。通过算法计算着色片元是否在阴影的范围内。 步骤:在光源处摆一个摄像机,看得见的部分就没有阴影,看不见的部分就有阴影。在 shader 里编写一个特殊的pass,只有计算阴影的时候才调用 pass。这个 pass保存了从这个光源方向来看的着色点的深度信息。首先需要将着色点的坐标转换到光源的坐标系下, 如果着色点的深度比可见部分的深度大的时候,就在阴影内。如果相等就表示这个着色点不在阴影范围内。

    阴影区域叠加颜色。

  • 阴影投射 Pass 编写

    shader 编写一个 pass,引擎会在光源的位置架设一个摄像机,然后,把深度信息更新给引擎,作为阴影的深度信息。

    如果这个物体阴影投射是关闭的,那么这个时候,我们在光源的位置架设摄像机,计算高度的时候,就不会考虑这个物体,自然也就不会调用物体的阴影投射的 pass。

  • 接受阴影 pass 编写

# Unity3d ShaderLab 技术

# 渲染管线策略

  • Forward 前向渲染

    内置渲染额外的灯光需要在 addpass 中计算,而 urp 渲染则将所有灯一次性打包后渲染。

    向前渲染管线是标准的默认内置渲染管线,渲染整个物体的时候我们分为两个部分的 pass 。basepass 最重要的逐像素光源光照计算 + 不重要逐顶点光源光照计算 + 环境光 + 阴影。additionalpass 我们有多个重要光源的时候,剩余的每个 "重要光源" 都要执行一次 additionalpass。

  • Deferred 延迟渲染

    需要支持 mrt(Multi Render Target) 技术,对硬件要求较高。

  • 渲染管线全流程详解

    顶点初始化:cpu 将顶点数据放入 gpu 显存里,得到一个个顶点数据。
    顶点着色 shader:形状变换,通过代码改变顶点的模型坐标;变换坐标 (模型坐标 - 世界坐标 - 摄像机坐标 - 齐次坐标);传递数据,把需要的数据放在固定的盒子里。哪些数据存在哪些盒子里是有默认规定的。这些数据是由编程者自己控制的。
    三角形遍历:渲染管线会做一次插值,将顶点着色器传来的数据做插值。
    片元着色 shader:光照计算 + 纹理颜色。
    
    1
    2
    3
    4

# UnityShaderLab

  • Shader 结构

    Shader “Name”
    {
      [Properties]
      SubShaders ------ 至少有一个,多个 SubShader 是为了保证被硬件最大程度支持。
      [FallBack] ------ 上面的指令都无法满足要求,就执行回滚到最简单的方案。
    }
    
    Properties 和 FallBack 可有可无。
    多个 Subshader 从前往后指令逐渐简单、简化,为的就是有更好的匹配方案。
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    属性数据列表:编辑器检视面板上暴露出来的属性。这是用户设置的将 cpu 传递给 gpu 的数据,这些数据在 shader 里是不可以修改的。

    pass:渲染管道流水线,每个 pass 完整的执行了一次绘制。

    CGPROGRAM: CG代码开始

    ENDCG: CG代码结束

    #预编译指令:

    #pragma vertex vert  告诉编译器顶点着色器代码在哪个地方。 
    #pragma fragment frag  告诉编译器片元着色器代码在哪个地方。
    #include "UnityCG.cginc" unity 封装的 shader api ,可以实现代码复用。
    
    1
    2
    3

    顶点 shader:数据从渲染管线传递给顶点 shader。需要传递什么数据用户就定义什么数据。当数据准备好了以后。就可以在 shader 里做以下事情:

    改变模型坐标(改变物体形状)
    坐标变化(从观察者的角度来绘制物体)模型坐标 * 世界变换矩阵 * 摄像机变换矩阵 * 投影变换矩阵 ,就得到一个平面投影坐标
    传递数据(返回数据给渲染管线,将在顶点计算出的数据插值后丢给片元着色器)
    
    1
    2
    3

    appdata:就是顶点需要的数据结构,内部定义了若干需要传递给顶点 Shader 的数据。Unity3d 里定义了一些语义描述来描述这些数据盒子,比如 POSITION 描述了模型坐标信息。TEXTURE0 存放的就是纹理坐标。NORMAL 存放的就是法线坐标。还有其他种类的盒子... ...一旦数据进入到顶点 Shader 后盒子里存放的数据就由编程者自己控制。比如 TEXTURE0 可以存放法线的世界坐标。

    片元 shader:数据从顶点 shader 传递给片元 shader。片元着色器的调用次数是远远的大于顶点 shader 调用的次数。

    v2f:就是片元内部接收到的数据结构。其中数据盒子里存放的内容完全是由编程者确定的。比如 TEXCOORD0 可以存放法线的世界坐标。

  • Unity 可以实现的 Shader

    Surface Shaders (Unity 默认的 Shader 方案)
    Vertex and Fragment Shaders (可编程的)
    Fixed Function Shaders (可编程出现之前的、简单的光照、保守的、简单的纹理采样被广泛支持,Unity 说这种 Shader 能够支持所有的硬件平台,是最简单的、运行速度最快的 Shader)
    
    1
    2
    3

    注意

    Surface Shader 可以为我们生成一些以前手动编写的代码,比起 Vertex Fragment Shader 要简单些,不用我们关心太多的细节,可以快速的创建一个 Shader。这种 Shader不需要写 pass 通道,它是对 Vertex Fragment Shader 的一种包装,Unity 引擎最终还是会将其转换成 Vertex Shader 和 Fragment Shader。

  • UnityShader 种类

    Unlit 不发光,只是一个纹理,不被任何光照影响。(经常用于UI)
    Vertex Lit 顶点光照。
    Diffuse 漫反射。
    Normal Mapped 法线贴图,比漫反射更开销大,增加了一个或更多纹理和几个着色器结构。(面片数量较少,使用法线贴图弥补)
    Specular 高光,这增加了特殊的高光计算。(金属等)
    NormalMapped Specular 高光法线贴图。这比高光开销更大。
    Parallax Normal Mapped 视差法线贴图,这增加了视差法线贴图计算。
    Parallax Normal Mapped Specular 视差高光法线贴图。这增加了视差法线贴图和镜面高光计算。(左右眼看到的东西不一样)
    
    1
    2
    3
    4
    5
    6
    7
    8

# 兰伯特漫反射光照模型

A:(xa,ya,za) B:(xb,yb,zb)

A * B = (xa * xb,ya * yb,za * zb) = |A||B| * cos(夹角),当取单位向量时,A * B = cos(夹角)。

如果出现负数的情况下,将其值取到 0。为了解决这个问题,引出了半兰伯特光照模型:dot(L, N) * 0.5 + 0.5,其取值为[0,1]。

计算公式总结:

兰伯特模型:max(0,dot(N,L)) * 光照颜色;max取值可以防止钝角时值为负。

半兰伯特模型:(dot(N,L) * 0.5 + 0.5 ) * 光照颜色;

L: 光照的方向(反射点-光源的方向) N: 反射点所在面的法线

下面是一个最基本的无光着色器:只有本色没有光照效果。

  具体实施步骤:
  1 #include "Lighting.cginc"
  2 配置光照模式:"LightMode" = "ForwardBase"
  3 光照计算的时候放在哪里?是逐顶点还是逐片元?
  4 光照计算,你需要哪些数据?参考计算公式可以得出:
    光的方向 (由 Unity3d 引擎传给 shader)
    光的强度 (由 Unity3d 引擎传给 shader) 
    法线 (将模型的法线转换成世界的法线)
1
2
3
4
5
6
7
8
Shader "Unlit/ylxUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 逐顶点光照的半兰伯特光照模型

在顶点着色里计算每个顶点的光照,(dot(L,N) * 0.5 + 0.5) * 光的颜色。

三角形的三个顶点插值后,计算出每个片元的颜色。

片元 shader 将传过来的逐顶点光照数据叠加到纹理颜色上。

# 冯高光与布林冯高光经验模型

  • Phong 镜面反射模型:反射向量与视向量夹角的余弦 ^ 光泽度

    反射向量:R = reflect(L,N) 这里的 L 是从光源射到物体顶点的

    V = 摄像机位置 - 反射点位置

    光泽度:n

    计算公式:强度 = pow(max(0,dot(R,V)),n)

  • Blin-Phong 光照模型:物体表面法线向量与半角向量的夹角的余弦 ^ 光泽度

    半角向量 H = 视向量 + 光照方向向量的夹角的一半的方向

    计算公式:强度 = pow(max(0,dot(H,N)),n)

    Shader "Unlit/ylxPhongShader"
    {
        Properties
        {
            _MainTex ("Texture", 2D) = "white" {}
            _Gloss("Gloss",Range(0.1,5))=1
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
            LOD 100
    
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
    
                #include "UnityCG.cginc"
                #include "Lighting.cginc"
    
                float _Gloss;
    
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                    float3 normal:NORMAL;
                };
    
                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                    float3 wNormal:TEXCOORD1;
                    float3 worldPos:TEXCOORD2;//存放顶点的世界坐标
                };
    
                sampler2D _MainTex;
                float4 _MainTex_ST;
    
                v2f vert (appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    o.wNormal = UnityObjectToWorldNormal(v.normal);
                    o.worldPos = mul(unity_ObjectToWorld,v.vertex);
                    return o;
                }
    
                fixed4 frag (v2f i) : SV_Target
                {
                    float3 L = normalize(-_WorldSpaceLightPos0);
                    float3 N = normalize(i.wNormal);
                    float3 R = reflect(N,L);
                    float V = normalize(_WorldSpaceCameraPos-i.worldPos);
    
                    float halfLam = dot(-L,N)*0.5+0.5;
                    float3 lamColor = halfLam*_LightColor0.rgb;
                    
                    //phong高光
                    float specular = pow(max(0,dot(R,V)),_Gloss);
                    float3 phongColor = specular *_LightColor0.rgb;
            
                    //布林phong高光
                    float specular = pow(max(0,dot(N,H)),_Gloss);
                    float3 blinphongColor = specular *_LightColor0.rgb;
                    
                  fixed4 allLight = fixed4(phongColor.rgb,1)*0.3 + fixed4(lamColor.rgb,1)*0.7;
        
                    fixed4 col = tex2D(_MainTex, i.uv);
                    return col*allLight;
                }
                ENDCG
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78

# 游戏引擎如何传递数据给 shader

Unity Shader 中用户可以自定义的一些装数据的盒子有:【POSITION NORMAL TANGENT TEXCOORD0 TEXCOORD1 TEXCOORD2 TEXCOORD3 ...TEXCOORDn ...COLOR】 n取值范围 :shader mode 2/3 时 n=8 ;shader mode 4/5/6 时 n=16。

系统指定用途的盒子:【SV_POSITION SV_Target】

渲染管线传递数据的时候,开始时候哪些数据存放在哪些盒子是有默认规定的。

顶点 Shader 通过传入的参数,将变量绑定到盒子上,访问变量即可访问到盒子数据。

经过顶点 Shader 后,除了 SV 开头的盒子,可以由开发者自己来管理,来确定盒子里存放哪些数据。

片元对应的数据是由顶点传递过来的。片段需要哪些数据,顶点就需要准备哪些数据。片元着色在拿到顶点着色传过来的数据前需要进行一次插值计算。顶点 shader 指定的是哪个盒子,那么渲染管线插值后,仍然会把数据存放在对应的盒子。

# 立方体纹理采样与自制天空盒

什么是立方体纹理:6 个面都会对应有纹理贴图。前后、左右、上下。一般是作为天空盒。

立方体纹理是如何采样的:中心点出发一个 3D 向量,与盒子碰撞处就是像素的颜色。

自制天空盒:首先,创建一个比较大的盒子(球体),包围住整个场景。其次,编写 shader,控制渲染盒子,将立方体纹理渲染到盒子上。再次,调整渲染队列的顺序,让它能够最先绘制。

# 如何处理多光源光照计算

自动选择 auto,根据系统自己来定。

重要光源 important,逐像素光照,每个片元都叠加这个光照计算。(性能差效果好)

不重要光源 not important,逐顶点光照或者是 SH(球谐光照解析)。(性能好效果差)

Unity3d 里面最多允许几个重要的光源呢?全局设置-渲染质量-向前渲染管线。

forwardbasepass:最重要的逐像素平行光源 + 不重要的光 + lightingmap + 环境光。

向前渲染:base 引擎会给你传递这些数据,光照计算的方法包含物理模型和经验模型。

# 向前渲染管线 shader 获取重要光源和非重要光源数据

ForwardBase:用于向前渲染,该 Pass 会计算最重要的平行光,环境光,逐顶点/SH光源和 LightingMap。
#pragma multi_compile_fwdbase
ForwardAdd:用于向前渲染,该 Pass会计算额外的逐像素光源。有几个光源就有几个 Pass。
#pragma multi_compile_fwdadd
_LightColor0:最重要逐像素光源的颜色。  
_WorldSpaceLightPos0:最重要的逐像素光源的位置。
unity_4LightPosX0
unity_4LightPosY0
unity_4LightPosZ0:前4个非重要的点光源在世界空间中的位置。
unity_4LightAten0:前4个非重要的点光源的衰减因子。
unity_LightColor:前4个非重要点光源的颜色。
1
2
3
4
5
6
7
8
9
10
11

# 渲染管线设置与质量设置

Camera-Rendering Path-(Use Graphics Settings /Forward /Deferred /Legacy Vertex Lit /Legacy Deferred)
Edit-Project Settings-Graphics-(Rendering Path)
Edit-Project Settings-Quality-(Pixel Light Count)
1
2
3

# PBR次世代美术工作流

  • 物体光照计算的组成部分

    颜色 = 物体自发光 + 颜色*(镜面反射 + 漫反射 + 环境遮挡) + 环境光

  • 物体主要光照计算的实现原理

    物体自发光:贴图纹理+颜色

    镜面反射:计算(pbr+经验模型)

    漫反射:计算

    自己环境遮挡:贴图纹理+数值

    环境光:全局变量,引擎里设置

  • PBR两种美术工作流

    基于能量守恒定律来模拟光的反射。

    算法是成熟的。

    调反射的方式不一样,PBR分成两种模式两个方向 。

    金属度 + 光滑度工作流

    Metalness 颜色贴图 (albedo/diffuse/basecolor) 模拟金属的镜面反射程序。简单的金属度模拟可以使用一个数值,复杂的就需要一张金属度贴图来表现。比如战士的铠甲,铠甲和皮肤是在一起的,其金属度当然是不同的。这样就不能使用单一数值来表示。

    Smooths 光滑度模拟漫反射的程度。

    Metalness+Smooths:优点各种贴图是分开的,创作较容易。纹理占用内存少,纹理都是单通道的灰度图。缺点边缘容易产生伪像。

    反射度+光泽度工作流:更接近真实的情况

    Specular 反射度:数值

    Smooths 光滑度:数值

    Specular+Smooths:优点边缘伪像不明显。控制比较灵活。缺点是控制可能导致不遵循能量守恒定律破坏 PBR 原则。RGB 贴图多,占用的内存多。

  • 细节增强技术

    法线贴图:高低模

    高度贴图:高低模

  • PBR 美术需要提供的数据

    自发光:颜色+贴图

    物体本色:diffuse/basecolor/albedo 颜色+贴图

    反射:金属度(数值+贴图)

    光滑度:(数值+贴图)

    环境遮挡:数值+贴图

    环境光:数值

    发线贴图:(数值+贴图)

    高度贴图:(数值+贴图)

# Matcap 技术(material capture)

概述:Matcap 一度是比较流行的技术,涉及到一个叫做薄膜干涉的现象(彩色的条纹)。在 pbr 技术出来之前,matcap 是比较流行的技术,后来逐渐被pbr所取代。在移动手机时代,使用 matcap 可以使用简单的计算就表现出还算不错的质感。

实现原理:获得模型的法线数据,将模型的法线数据转换到相机空间,将物体上的法线跟随着摄像机一起变化,拿到法线数据后把它当成UV坐标应用到纹理采样当中采样贴图信息。

缺点:效果根据视角做出变化,并且效果是固定的。相机边缘会出现采样缺陷。

# CG语言入门

  • 基本数据类型

    float
    half
    fixed
    bool 
    int
    sampler *
    profile
    
    1
    2
    3
    4
    5
    6
    7
  • 预编译指令 target

    #pragma target 2.0 (default) roughly shader model2.0  支持D3D9
    target 3.0 D3D9;
    target 4.0 D3D10;
    target 5.0 D3D11;(目前最高级别2018-04-02)
    
    1
    2
    3
    4
  • 流程控制

    各种数据类型
    向量和 swizzle
    
    ifelsewhile
    dowhilefor
    
    自定义函数和 #include
    值拷贝传参
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 函数类型

    数学函数:大量的数学函数库。
    几何函数:主要做一些具体的反射、折射运算。
    纹理函数:通过纹理来采样颜色。
    导数函数:
    
    1
    2
    3
    4
  • 输入输出语义

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    //POSITION表示objPos这个in float2类型变量的意义
    
    //POSITION表示pos这个,out float4类型变量的意义 
    //这个变量不需要在其他片段中使用,程序最终会自动处理这个变量
    void vert(in float2 objPos:POSITION,out float4 pos : POSITION,out float4 col : COLOR)
    {
      pos = float4(objPos,0,1);
      //col = float4(0, 0, 1, 1);
      col = pos;
    }
    //frag 这个片段程序的最终目的是要计算颜色的
    //实际中COLOR和COLOR0是等价的
    //half (16位)是 float(32位)精度的一半
    void frag(inout float4 col:COLOR)
    {
      col = float4(0,1,0,1);
    }
    ENDCG
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

# 3D数学基础

  • 坐标系

    模型坐标系
    世界坐标系
    摄像机坐标系
    屏幕投影坐标系
    
    1
    2
    3
    4
  • 向量

    向量
    向量的运算
    几何意义
    
    1
    2
    3

    Unity Shader 里有三类向量分别是:2D、3D、4D。

  • 矩阵

    矩阵的维度和记法
    矩阵的转置
    矩阵和标量的乘法
    矩阵和矩阵的乘法
    
    1
    2
    3
    4
  • 矩阵和矩阵变换

    2D旋转矩阵
    3D旋转矩阵
    C#顶点变换DEMO
    
    1
    2
    3
上次更新: 2025/02/15, 13:42:25
最近更新
01
Git问题集合
01-29
02
安装 Nginx 服务器
01-25
03
安装 Docker 容器
01-25
更多文章>
×
×