【Unity】Unity Shader的基础

Shader概念

Shader是什么

中文名“着色器”,顾名思义,它的作用可以先简单理解为给屏幕上的物体画上颜色。而什么东西负责给屏幕上画颜色?当然是GPU,所以我们写shader的目的就是告诉GPU往屏幕哪里画、怎么画。说到这其实大家应该很明白了,如果我们连GPU的工作原理都不知道,何谈指挥它?

所以Shader其实就是一段能够针对屏幕显示内容进行操作并被GPU执行的程序。主要是用在3d的模型中,2d在要实现某些特殊效果时,比如光照,模糊,锐化,圆角,UV动画等等。在3d中就是控制模型的每个顶点和像素的颜色渲染技术。

Shader主要负责将输入的Mesh(网格)以指定的方式和输入的贴图或颜色等组合作用,然后输出。绘图单元可以依据这个输出来将图像绘制到屏幕上。在游戏开发、电影制作和计算机图形学等领域,Shader编程被广泛应用,以实现各种复杂的视觉效果。

渲染管线是什么

渲染管线就是CPU与GPU合力处理图像数据的流程工序。

渲染管线(Rendering Pipeline),也称为渲染流水线或像素流水线,是计算机图形学中用于生成图像的流程和阶段。

Shader就是一个代码程序,而渲染管线是执行shader的一个流水线。所以两个的关系密切,但分类不相关。

Shader的工作原理

首先,要理解CPU与GPU的区别。

为什么着色器能更高效的完成复杂场景或逼真效果等这些工作呢?因为着色器通常是运行在GPU上的程序,而GPU硬件相比传统CPU硬件更适合处理数据量庞大的工作。

普通计算机就像这个管道一样运行,一个接一个地处理这些任务,但是图形计算例如现在常见分辨率 1980x1080屏幕每秒运行60帧,需要每秒处理128,304,000个像素,这个时候,并行处理就是最好的解决方案。比起用四个或八个强大的微处理器来处理这些信息,用一大堆小的微处理器来并行计算,能够更高效的完成这些工作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


Shader有哪些分类

其次,要明白Shader既然是基于GPU给图形效果的程序,那么不同的底层硬件不同的架构就有不同的图形渲染API。比如Windows的图形API就是DirectX,具体:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在mac和ios还有其他对应的Shader或中间语言。比如SPIR-V、MSL、OpenGL ES Shading Language(基于GLSL的变种)。


每一种语言有推出不同的渲染管理流程去适应不同的环境以及情况。

比如HLSL(High-Level Shading Language)就有很多种不同的渲染管线:

  • 图形渲染管线(基本的光栅化图形渲染管线)

    最传统的渲染管线,主要用于实时图形渲染。
    主要由顶点着色器(Vertex Shader)和像素着色器(Pixel Shader,也被称为片段着色器Fragment Shader)构成。
    顶点着色器处理顶点数据,如坐标变换和逐顶点光照。像素着色器处理每个像素的颜色和效果。

  • 计算管线(计算着色器渲染管线)

    在Compute Shader渲染管线则只有compute shader一个种类,利用GPU做较通用的数据处理。
    计算着色器是一种在GPU上执行通用计算的着色器类型。
    它不依赖于传统的图形渲染流程,而是可以独立执行并行计算任务。
    计算着色器常用于物理模拟、图像处理、数据并行计算等领域。

  • 光线追踪渲染管线(光追)

    光线追踪是一种先进的渲染技术,用于模拟光线在场景中的传播和交互。
    它可以实现更加真实的光照、阴影和反射效果。
    光线追踪渲染管线通常包含专门的光线追踪着色器,用于处理光线与场景中物体的交互。

  • 网格管线(网格渲染管线)

    网格管线是一种较新的渲染管线,它旨在优化适应现代GPU硬件架构下的光栅化渲染。
    网格着色器(Mesh Shader)可以生成或修改图元(如三角形)的集合,从而提供更灵活的渲染控制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


关于GLSL(OpenGL Shading Language)的渲染管线:

GLSL(OpenGL Shading Language)是为OpenGL设计的着色器语言,它允许开发者编写自定义的着色器程序,以实现复杂的图形效果和渲染算法。在OpenGL中,渲染管线通常被划分为几个主要阶段,每个阶段都可以由开发者通过GLSL编写着色器来进行自定义。以下是GLSL中常见的渲染管线阶段:

  1. 顶点着色器(Vertex Shader,必有):
    • 这是渲染管线的第一个可编程阶段。
    • 顶点着色器处理每个顶点的数据,如位置、法线、纹理坐标等。
    • 开发者可以在顶点着色器中实现顶点变换、逐顶点光照、顶点动画等效果。
  2. 曲面细分着色器(Tessellation Shader,可选):
    • 曲面细分着色器对图元(如三角形)进行细分,以增加几何细节。
    • 这个阶段通常用于生成更平滑的曲面和更精细的几何体。
    • 曲面细分着色器包括控制着色器(Tessellation Control Shader)和评估着色器(Tessellation Evaluation Shader)。
  3. 几何着色器(Geometry Shader,可选):
    • 几何着色器处理整个图元,可以创建或销毁图元。
    • 它允许开发者根据输入图元生成新的图元,或者修改现有图元的属性。
    • 几何着色器常用于实现粒子系统、阴影体积、毛发渲染等效果。
  4. 裁剪(Clipping):
    • 裁剪阶段用于剔除视锥体(Viewing Frustum)之外的图元部分。
    • 这个阶段确保只有可见的图元部分被传递到下一个阶段。
  5. 屏幕映射(Viewport Transformation/Homogeneous Division):
    • 屏幕映射阶段将裁剪后的图元从裁剪空间转换到屏幕空间。
    • 这个过程包括透视除法(Perspective Division)和视口变换(Viewport Transform),将图元映射到屏幕上。
  6. 光栅化(Rasterization,必有):
    • 光栅化阶段将图元(如三角形)转换为像素(或称为片元Fragment)。
    • 这个过程包括图元组装、三角形遍历和插值计算,生成每个像素的颜色、深度和其他属性。
  7. 片元着色器(Fragment Shader,必有):
    • 片元着色器是渲染管线的另一个可编程阶段。
    • 它处理每个片元的颜色、纹理、光照等信息,并生成最终的像素颜色。
    • 开发者可以在片元着色器中实现纹理采样、逐像素光照、透明效果等。
  8. 逐片元操作(Per-Fragment Operations):
    • 逐片元操作阶段包括深度测试、模板测试、透明度测试和混合等。
    • 这些测试用于确定哪些片元应该被写入帧缓冲区(Framebuffer),并更新像素颜色。

但注意在开发者的角度来看待的话,可以简单的看为:

  1. 顶点输入:将顶点数据(如位置、颜色、纹理坐标等)输入到管线中。这个阶段的数据通常来自CPU,并且是以数组的形式传递的。
  2. 顶点着色器:对输入的顶点数据进行处理,如变换、光照和投影等。这个阶段是可编程的,开发者可以使用GLSL编写自定义的顶点着色器。
  3. 图元装配:将顶点着色器输出的顶点组装成图元(如点、线、三角形等)。这个阶段通常是由GPU自动完成的,开发者无法直接编程修改。
  4. 几何着色器(可选):对图元进行进一步的处理,如细分、裁剪或生成新的图元等。虽然几何着色器是可编程的,但并非所有渲染管线都包含这个阶段,且其使用相对较少。
  5. 光栅化:将图元转换为屏幕上的像素片段(fragments)。这个阶段是将图元映射到屏幕像素的过程,通常是由GPU自动完成的,开发者无法直接编程修改。
  6. 片段着色器:对光栅化生成的像素片段进行处理,如计算颜色、纹理映射和透明度等。这个阶段是可编程的,开发者可以使用GLSL编写自定义的片段着色器。
  7. 像素操作(或称为“后期处理”):对片段着色器输出的像素进行最终的处理,如混合、测试和输出等。虽然这个阶段包含一些可编程的元素(如混合方程),但大部分操作是由GPU自动完成的,且其可编程性相对较低。

从开发者的理解看就是,只有顶点着色器和片段着色器是可编程的,几何着色器和像素操作也可以少量编程,但使用较少,通常是由GPU自动完成的,开发者无法直接编程修改。这些自动化流程确保了渲染管线的高效性和一致性。


其余Shader语言就不展开说了,但总结就是要明白Shader本身并不是一个细致的概念,而是一个对所有系统以及硬件架构的各种语言的总称,在具体框架具体环境的时候就要具体分类了。

Shader有哪些流程

在上方的Shader GLSL开发者角度就可以看到GPU处理Shader的具体流程,虽然不同的图形API可能会有所不同,但大致原理如此,可以以图来形容:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在不同的建模软件中,Shader有区别吗

根据上面的信息,就可以知道,Shader能不能通用要看对应的系统环境,当然一般的建模软件或游戏引擎都有自己的渲染引擎和着色器系统,所以Shader是不能通用的。比如Maya和3DMax的Shader是无法直接移植到Unity中使用的。

当然能不能直接把效果粘贴过去这要涉及到具体的材质系统和贴图系统来看,当然在底层来看Shader是有变化的。

因为Unity本身是提供了自身的Shader语言系统和编译工具,这和我们上面讲的底层基础Shader是有所区别的,这就是后面要开始讲的Unity Shader(ShaderLab)。

Unity Shader (ShaderLab)

Unity Shader是Unity做了什么样的封装

Unity Shader严格来说并不是传统意义上的Shader,而是Unity自身封装后的一种便于书写的Shader,又称为ShaderLab。

在实际开发中,开发者通常不需要直接编写GLSL、HLSL或Cg代码。Unity提供了一个更高层次的抽象层,即ShaderLab,它允许开发者使用一种类似于脚本的语言来定义着色器的行为。
在ShaderLab中,开发者可以定义着色器的属性、子着色器、渲染状态等,而无需深入了解底层的GLSL、HLSL或Cg代码。

Unity Shader是基于多种图形API的语言,这些图形API能够处理不同类型的Shader。Unity在构建应用时,需要将Shader编译为目标平台能够识别的类型。以下是一些常见的图形API以及它们对应的Shader类型:

  • DirectX:这是Windows平台上广泛使用的图形API,对应的Shader语言是HLSL(High Level Shader Language)。
  • OpenGL:这是跨平台的图形API,在Windows、macOS和Linux等操作系统上都能使用,对应的Shader语言是GLSL(OpenGL Shading Language)。
  • Vulkan:这是新一代跨平台的图形和计算API,也支持在Windows、macOS和Linux等操作系统上使用,但它使用的着色语言通常是SPIR-V(Standard Portable Intermediate Representation),这是一种中间表示形式,可以从HLSL、GLSL等语言编译而来。
  • Metal:这是苹果公司为macOS和iOS平台开发的图形API,对应的着色语言是Metal Shading Language。

Unity Shader需要有自己的一套逻辑框架(ShaderLab),以便针对特定图形API生成相应的Shader。在Unity中编写的Shader文件,当Unity对这部分资源导入时,会进行预编译并将数据缓存起来。在运行时,Unity会根据目标平台选择合适的图形API,并将Shader编译成该API能够识别的格式,然后加载到GPU中执行。

因此,可以说Unity Shader并不是完全基于某一种特定的图形API的语言,而是能够支持多种图形API,并根据目标平台自动选择合适的API进行编译和执行。这种跨平台的能力使得Unity成为了一个非常流行的游戏开发引擎。

以一个思维导图来看:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


现在看Unity具体的封装,首先根据上面内容明白了Unity Shader是一个基于底层系统Shader的一个高级封装,可以根据目标平台自动选择合适的API进行编译执行。
所以Unity Shader的封装体现在很多定制化的方面。

Unity Shader和Shader的区别和优缺点

在Unity里,Unity Shader实际上指的就是一个ShaderLab文件——硬盘上以.shader作为文件后缀的一种文件。

Unity Shader相比于Shader,有优点也有缺点。

优点有:

  • 在传统的Shader中,我们仅可以编写特定类型的Shader,例如顶点着色器、片元着色器等。而在Unity Shader中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码。

  • 在传统的Shader中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在Unity Shader中,我们通过一行特定的指令就可以完成这些设置。

  • 在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入输出,要小心地处理这些输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便地改变这些属性。而且对于模型自带的数据(如顶点位置、纹理坐标、法线等),UnityShader也提供了直接访问的方法,不需要开发者自行编码来传给着色器。

缺点有:

  • 由于UnityShader的高度封装性,我们可以编写的Shader类型和语法都被限制了。例如:Unity对曲面细分着色器、几何着色器等的支持性就差一些。

可以说,UnityShader提供了一种让开发者同时控制渲染流水线中多个阶段的方式,不仅仅是提供Shader代码。作为开发者而言,我们绝大部分时候只需要和UnityShader打交道,而不需要关心渲染引擎底层的实现细节。

Unity Shader的渲染管线和着色器类型

Unity Shader有哪些

Unity提供了多种着色器类型,已满足不同的渲染需求:

  • 顶点着色器(Vertex Shader)

    在每个顶点上执行的着色器。主要用于处理模型的形态,例如修改波浪表面上每一个顶点的值。

  • 片元着色器(Fragment Shader)

    在每个最终图像中可能出现的像素上执行的着色器。干预像素的着色,可以创建各种视觉效果。

  • 无光照着色器(Unlit Shader)

    不考虑光照影响的着色器。通常用于创建简单的颜色效果或作为其他效果的基础结构。适用于低端硬件,代码结构相对简单,易于理解和修改。
    虽然无光照着色器本身可能不是Unity特有的封装类型,但Unity提供了多种内置的无光照着色器模板。
    这些模板封装了基本的着色逻辑,使得开发者可以轻松创建不考虑光照影响的着色效果。

  • 表面着色器(Surface Shader)

    基于顶点/片元着色器(Vertex/Fragment Shader)进行了二次封装,从而降低了着色器的编写难度。表面着色器使用一种更高级别的语言来描述光照和材质属性,使得开发者可以用少量的代码来完成不同的光照计算以及跨平台逻辑。
    定义了物理性质的材质,计算每个像素的最终颜色,并模拟光照效果。
    大多数表面着色器是作为标准表面着色器(Standard Surface Shader)的扩展,提供了更多的自定义选项。
    封装了顶点着色器和片元着色器的功能,并提供了易于使用的光照模型,如Lambert、Blinn-Phong等。
    开发者只需编写简单的表面着色器代码,Unity会自动将其转换为相应的顶点着色器和片元着色器代码。

  • 图像特效着色器(Image Effect Shader)

    实现屏幕特效的着色器,如抗锯齿、环境光遮蔽、模糊、溢光等。
    主要用于后处理效果,通过修改渲染后的图像来创建各种视觉效果。
    这些特效着色器通常封装了复杂的图像处理算法,并提供了易于使用的参数接口,以便开发者在材质面板中调整效果。

  • 计算着色器(Compute Shader)

    在图形管线之外在GPU上执行计算的着色器。
    可以用于加速某些游戏处理过程,如物理模拟、图像处理等。
    编程语言为HLSL,需要深入理解GPU架构和并行算法。

  • 光线追踪着色器(Ray Tracing Shader)

    实验性的着色器类型,用于在GPU上进行光线追踪处理。
    可以生成逼真的光照效果,如全局光照、反射、折射等。
    仅在高清渲染管线中工作,并需要支持RTX的显卡和相应的Unity版本。

Unity的渲染管线

Unity以自由性来看提供了两种渲染管线:

  • 固定渲染管线

    注意,固定渲染管线和Built-in Shader是两个不同的概念。固定渲染管线指的是一种硬件级别的渲染方式,而Built-in Shader则指的是Unity游戏引擎提供的一种预定义好的着色器类型。

    固定渲染管线(Fixed Function Pipeline)中的Shader通常是不可编程的,它们依赖于硬件提供的固定功能进行渲染。也即是说用户只能在面板更换图片颜色数值等操作。

    以下是一些固定渲染管线中Shader的例子:

    • 固定光照模型Shader:
      • 在固定渲染管线中,光照模型通常是固定的,如Phong光照模型、Lambert光照模型等。这些光照模型内置在图形硬件中,开发者无法更改其实现方式。
      • Shader可能仅允许开发者设置一些参数,如漫反射颜色、镜面反射颜色、光泽度等,这些参数将影响光照模型的计算结果。
    • 纹理映射Shader:
      • 纹理映射是固定渲染管线中的一个常见功能,它允许开发者将纹理图像映射到3D模型的表面上。
      • 在固定渲染管线中,纹理映射的Shader通常是内置的,开发者只需指定纹理图像和映射方式即可。
    • 简单变换Shader:
      • 固定渲染管线中的Shader可能还包括一些简单的变换功能,如平移、旋转、缩放等。
      • 这些变换通常是通过矩阵运算实现的,而矩阵运算在固定渲染管线中是内置的。

    随着图形硬件的发展,现代GPU已经普遍支持可编程渲染管线(Programmable Rendering Pipeline),即允许开发者通过编写着色器代码来自定义渲染过程中的各个步骤。因此,在现代游戏引擎(如Unity)中,固定渲染管线已经被可编程渲染管线所取代。

    在Unity中,虽然仍然可以使用一些内置的Shader(这些Shader可能在内部实现上使用了固定渲染管线的某些功能,但它们在Unity的上下文中被视为可编程渲染管线的一部分,因为Unity提供了ShaderLab和HLSL/CG等高级着色器语言来编写自定义Shader)。但严格来说,Unity中已经没有传统意义上的固定渲染管线Shader了。

  • 可编程渲染管线

    对渲染管线中的顶点运算和像素运算分别进行编程处理,而无须像固定渲染管线那样套用一些固定函数,取代设置参数来控制管线。

    URP和HDRP都是可编程渲染管线(SRP)。SRP本身不是一个具体的渲染管线,而是一个概念框架。

    URP和HDRP都算是SRP的一个预构建的SRP模板,有对应的使用环境,实际可以完全单独使用SRP自定义渲染管线。

Unity以预构建的具体功能性来看提供了3种渲染管线:

  • Built-in Render Pipeline
    Unity的默认渲染管线,提供了基本的渲染功能,包括前向渲染和延迟渲染两种路径。然而,它的自定义选项相对有限,可能无法满足所有项目的需求。
  • URP 通用渲染管线(Universal Render Pipeline)
    URP是一种快速、可扩展的渲染管线,适用于各种平台,包括移动设备、PC和主机。它提供了前向渲染、延迟渲染和透明渲染等预设路径,并支持自定义SRP自由度。URP还内置了Shader Graph工具,可以快速创建自定义着色器。
  • HDRP 高清渲染管线 (High Definition Render Pipeline)
    HDRP是Unity为现代平台开发的高保真可编程渲染管线。它利用基于物理的光照技术、线性光照、HDR光照和可配置的混合平铺/聚类延迟/前向光照架构,提供了创建高图形标准游戏、技术演示和动画等应用所需的工具。HDRP主要针对高品质要求的平台,如PC、Xbox和PS等。

Unity Shader的可视化封装(Unity Shader Graph)

Unity Shader Graph是Unity官方推出的一种可视化的着色器制作工具,它允许开发者以图形化的方式创建和编辑着色器,而无需编写传统的着色器代码。以下是对Unity Shader Graph的详细介绍:

Unity Shader Graph 的基本概念
  • Shader Graph:一种图形化的着色器制作工具,使用流程图的形式表达顶点变换和片元着色的流程。
  • 节点(Node):Shader Graph中的基本元素,每个节点代表一个特定的图形操作或功能,如颜色混合、纹理采样、法线处理等。
  • 连接(Connection):节点之间的连接线,用于传递数据或信号。
Unity Shader Graph 主要特点
  • 可视化编程:Shader Graph提供了直观的图形化界面,开发者可以通过拖拽和连接节点来定义着色器的外观和行为。
  • 简化着色器制作:相较于传统的着色器编写方式,Shader Graph大大降低了着色器制作的门槛,使得非专业的图形程序员也能够创建出复杂的视觉效果。
  • 实时预览:Shader Graph支持实时预览修改的效果,开发者可以在编辑器中实时看到着色器的变化。
  • 兼容性:Shader Graph支持多种渲染管线,包括Unity的内置渲染管线、URP(Universal Render Pipeline)和HDRP(High Definition Render Pipeline)。
Unity Shader 功能介绍
  • 节点库:Shader Graph提供了丰富的节点库,包括颜色、纹理、数学、程序纹理、输入、实用工具、UV变换等类型的节点。
  • Blackboard:用于创建外部属性,相当于Shader中的Properties,可以创建Float、Vector、Color、Texture等类型变量。
  • Graph Inspector:包含节点设置和图设置两个选项卡,用于设置节点的命名、参数值域、默认值等信息,以及着色器数值计算精度、支持的渲染管线等。
  • Main Preview:用于预览着色器渲染效果,在该窗口右键可以选择预览的模型。
  • 顶点着色器(Vertex):顶点变换、法线变换、切线变换在这里进行。
  • 片元着色器(Fragment):光照计算、贴图在这里进行,可以选择Lit(PBR光照模型)或Unlit(无光照模型)的片元着色器。
Unity Shader Graph 使用流程
  1. 安装Shader Graph:在Unity编辑器中,通过Window -> Package Manager打开包管理器,下载并安装Shader Graph插件。
  2. 创建Shader Graph:在Assets窗口右键,依次选择Create -> Shader Graph,然后选择所需的渲染管线(如Builtin、URP等)和着色器类型(如Lit Shader Graph、Unlit Shader Graph等)。
  3. 编辑Shader Graph:在Shader Graph编辑窗口中,使用节点库中的节点构建着色器逻辑,通过连接节点来定义着色器的行为。
  4. 创建材质并应用Shader Graph:在Assets窗口右键创建新的材质,然后将创建的Shader Graph拖拽到材质中。
  5. 将材质应用到场景中的对象:将编辑好的材质拖拽到场景中的对象上,即可看到着色器的渲染效果。

要明白Unity Shader细分的分类

根据上面的内容并分析可以得到思维导图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在Unity中Shader如何使用

在Assets中Create创建一个Shader文件。
然后再创建Create一个材质Material,将Shader拖进Material的Shader属性中。
然后将Material拖拽进Mesh Render中的Materials中。

  • 在Unity中,多个组件都可以使用Material。具体来说,Mesh Renderer和Particle System是两种常见的使用Material的组件。Mesh Renderer用于渲染3D网格物体,而Particle System则用于渲染粒子效果,比如火焰、烟雾等。这些组件通过Material来定义其外观和渲染行为。

  • 关于一个Material是否只能使用一个Shader的问题,答案是肯定的。在Unity中,一个Material确实只能绑定一个Shader。Shader决定了Material如何响应光照、如何渲染颜色和其他视觉效果。因此,每个Material都有一个与之关联的Shader,该Shader定义了渲染过程中使用的算法和公式。

  • 当一个物品(通常指一个GameObject)有多个Material时,这通常意味着该物品由多个部分或组件组成,每个部分或组件都有自己的Material和Shader。在Unity中,一个GameObject可以有多个Mesh Renderer或其他使用Material的组件,每个组件都可以绑定一个不同的Material。这样,每个部分就可以有自己独特的外观和渲染效果。

  • 要实现多个Material的混合Shader效果,通常需要使用Shader中的混合模式(Blending Mode)或自定义Shader来实现。混合模式允许两个或多个纹理或颜色在渲染过程中进行混合,从而创造出复杂的视觉效果。例如,可以使用透明度混合(Alpha Blending)来混合两个纹理,使得一个纹理可以部分或完全覆盖另一个纹理。

Unity中Shader与Material是如何关系的

Shader与Material的关系

  • Shader是Material的一部分:
    Shader实际上是一段程序,它定义了如何将输入的网格(Mesh)、贴图(Texture)或颜色等按照一定的方式组合起来,然后输出渲染结果。在Unity中,每个Material都会关联一个Shader,该Shader决定了Material如何响应光照、如何渲染颜色和其他视觉效果。
  • Material使用Shader来定义渲染方式:
    当开发者创建一个Material时,需要为其选择一个Shader。这个Shader定义了Material的渲染逻辑,包括如何处理光照、阴影、反射等。因此,可以说Material是通过Shader来实现其渲染效果的。

Unity在Material组件上所做的工作

  • 提供Shader选择:
    Unity为开发者提供了多种内置的Shader,如Standard Shader、Unlit Shader等。开发者可以在创建Material时选择这些内置Shader,也可以编写自定义Shader来实现特定的渲染效果。
  • 管理Material属性:
    Material具有多种属性,如颜色、纹理、反射率等。Unity的Material组件允许开发者在Inspector窗口中编辑这些属性,以调整Material的外观和渲染效果。
  • 支持材质实例化:
    Unity允许开发者通过脚本来动态创建和修改Material。这意味着开发者可以在游戏运行时根据需要创建新的Material实例,并调整其属性以实现动态效果。

Material起的作用

游戏引擎有着跟着色器密切相关的数据名为材质,这种资源让着色器有更好的使用性。材质是一种数据结构引纪录了着色器和输入着色器的真实数据,这种结构的好处是让同个着色器能根据表现不同替换输入参数和输入资源,把着色器当成可重复利用的模板。

  • 定义物体外观:
    Material定义了3D对象的外观,包括其颜色、纹理、光照属性和反射等。通过调整Material的属性,开发者可以改变对象的视觉效果,使其更加逼真或符合游戏风格。
  • 提高渲染性能:
    在渲染大量对象时,使用相同的Material可以提高性能。因为Unity可以对相同Material的对象进行批处理(Batching),从而减少渲染调用次数和渲染时间。
  • 支持自定义渲染效果:
    通过编写自定义Shader,开发者可以实现各种复杂的渲染效果,如实时全局光照(GI)、屏幕空间反射(SSR)、体积光(Volumetric Lighting)等。这些自定义渲染效果可以极大地增强游戏的视觉效果和沉浸感。

总的来说就是Shader只是Material的具体渲染流程实现的部分,具体的实例化与属性赋值比如贴图、颜色等等要在Material上实现调整。

Unity Shader与UE Shader的区别

  • 在Unreal Engine中,Shader是一个高度封装的东西,留给用户有限的空间进行自定义。
  • 用户可以通过蓝图系统或加入自定义HLSL写的函数节点来创建着色器效果,但深度优化或自定义Shading需要花费大量时间注册自己的Shader并重新编译引擎。
  • 对于技术侧重的学习者,UE Shader的学习需要更强的引擎编程技能。

跨平台引擎Shader的编译流程

跨平台引擎Shader的编译流程是一个复杂的过程,它涉及将高级着色器语言(如HLSL)编写的着色器代码转换为可以在不同平台和硬件上运行的GPU机器码。以下是Unity和Unreal Engine这两个跨平台游戏引擎中Shader编译流程的一般概述:

Unity Shader编译流程

  1. Shader语言选择:Unity Shader使用的是ShaderLab语言和HLSL(High-Level Shading Language)。ShaderLab用于描述Shader的结构和属性,而HLSL用于编写Shader的具体逻辑和计算。
  2. Shader文件创建:在Unity编辑器中,开发者可以创建一个新的Shader文件来开始编写自定义Shader。选择适合的Shader类型,如表面着色器、顶点/片段着色器等。
  3. 编写Shader代码:在ShaderLab部分定义Shader的属性、子程序和渲染阶段,在HLSL部分编写具体的Shader逻辑和计算代码。
  4. 跨平台编译:Unity在编辑器层面实现了D3DCompiler.dll的跨平台(参考了Wine的PELoader),然后再通过DXBC翻译至多平台Shader语言。但需要注意,由于D3DCompiler.dll是X86的指令,所以它的编译方案不支持移动设备上的HLSL Compute Shader编译。
  5. 材质应用:编写完Shader后,将其应用到Unity的材质上,并设置相应的属性和参数。
  6. 优化与调试:使用Unity的调试工具和性能分析工具来优化和调试Shader代码,以提高性能和效果。

Unreal Engine Shader编译流程(以UE4为例)

  1. 材质与Shader关联:UE的材质是一个通用化的蓝图系统,而Shader高度依赖于具体平台和硬件。材质编译和Cook机制将二者联系起来。
  2. Cook流程:运行CookCommandlet,触发所有被引用资源的Cook流程,包括材质Cook。单个材质Cook启动后,根据目标平台的FeatureLevel和MaterialQualityLevel,触发一个或多个ShaderMap的编译。
  3. ShaderMap编译:将材质蓝图翻译成HLSL格式的着色器代码。这个着色器代码是上层的,需要进一步编译到目标平台的代码。
  4. Shader编译:遍历材质可以应用的Mesh类型,对每个类型支持的所有Shader变体,编译生成一系列Shader Code。此时的Shader为OpenGL、DX、Metal、Vulkan等平台专用格式。
  5. 序列化与反序列化:编译完成后,将ShaderMap和ShaderCode序列化到硬盘或材质资源中,以便后续加载和使用。
  6. Shader加载与渲染:在游戏运行时,加载所需的Shader代码(或中间码),并提交给GPU驱动编译成机器码。然后,根据渲染命令和Shader资源索引,将渲染命令提交到渲染线程进行渲染。

跨平台编译的核心思想

跨平台编译的核心思想是使用中间字节码。中间字节码的前端对应各种Shader语言,后端对应各种GPU的机器码。编译期的优化可以放在中间字节码来处理,这样优化的部分就可以共用了。这种方式大大提高了Shader跨平台编译的效率和灵活性。

Unity Shader基础框架

Unity中Shader整体的框架写法可以用如下的形式来概括:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Shader "name"  
{  
    [Properties]  
    SubShaders  
    [Fallback]  
}
  • Shader “name”:指定Shader的名字,这个名字会显示在Inspector检视面板中,且不需要和shader文件名同名。它应该是简单的描述性词语,可以通过在name后面加上“/”来在Inspector面板中创造出二级菜单(多个“/”可以创建多级菜单)。
  • [Properties]:属性模块,定义了一些属性来指定Shader有哪些输入。这些属性会出现在材质面板中,允许开发者在使用Shader时直接编辑这些属性。属性的类型包括数字与滑动条、颜色与向量、贴图等。
  • SubShaders:子着色器模块,可以包含一个或多个子着色器(SubShader)。在实际运行中,Unity会选择第一个可用的子着色器来使用。每个子着色器中包含一个或多个的Pass,用于定义渲染的具体过程和状态。
  • [Fallback]:备选着色器模块,用于处理所有SubShader都不能运行的情况。它指定了一个备用的Shader,当所有SubShader都无法使用时,Unity会使用这个备用的Shader。

Unity Shader的着色器结构

对上面框架进行详细解析:

着色器声明

着色器的声明位于文件的最开始,用于定义着色器的名称和类别。通常,着色器名称采用“Category/ShaderName”的格式来组织和分类。例如:

Shader "MyShader/CustomShader"  
{  
    // 着色器内容  
}

属性声明定义Properties

属性部分定义了可以在Unity Inspector面板中调整的参数。这些参数可以是纹理(2D)、颜色(Color)、数值(Float)、向量(Vector)等。属性定义使得用户能够在不修改着色器代码的情况下,直接通过Unity编辑器调整着色器的效果。例如:

这一部分就是Unity对Shader的封装。将参数提取出来,作为属性然后可以在Material的Inspector中进行可视化修改等。

Properties  
{  
    _MainTex("Base (RGB)", 2D) = "white" {}  
    _Color("Main Color", Color) = (1,1,1,1)  
    _Glossiness("Smoothness", Range(0,1)) = 0.5  
    _Metallic("Metallic", Range(0,1)) = 0.0  
}

这里的命名规则以及格式一定要注意。

    _驼峰命名("显示名称", 类型格式) = 类型初始化数据
    PropName("DisplayName", PropType) = DefaultValue[{Options}]
Shader属性举例
  • PropName : 属性名,即当前Shader的外部传入的数据变量名。通常以下划线_开头。在SubShader中使用。
  • DisplayName : Unity材质Material的Inspector窗口中显示的参数名称,可作为脚本传输时的Key来给属性赋值。
  • PropType : 属性类型,Unity中可以支持的属性类型包括以下这些(Color、2D、Rect、Cube、Range、Float、Vector等等):
  • DefaultValue : 属性默认初始值,这些值可以后面在代码中赋值。
  • {Options} :针对Rect、2D、Cube。在初始值的后面必须加上一个{},当有特定选项的适合就写在{}里面,选项有ObjectLiner、EyeLiner、SphereMap、CubeReflect、CubeNormal等等(这些都是OpenGL中TexGen的模式)
    • Color :颜色,有RGBA四个量来定义。初始值的格式为(1,1,1,1)。每个通道的范围时0-1。
    • 2D : 一个2阶的大小的贴图,也就是2的n次幂像素,就是一张正方形贴图,这张贴图将在被采样后按照模型顶点UV进行颜色赋值。初始值可以用代表颜色的字符串,如“white”,“black”,“yellow”
    • 3D :定义三位纹理,通常用于仿真诸如雾或者烟的体积特效,不过只能通过脚本创建,默认值一般为空。且需要版本OpenGL3.0以上支持。
    • Rect :一个非2阶的贴图,还是一个平面贴图。初始值与2D的格式一样。
    • Cube :立方体纹理,是有6张有关系的2d贴图的组合,主要用做反射效果(比如天空盒和动态反射),也会被按照对应坐标采样。初始值与2D的格式一样。
    • Range(min,max) :一个介于最大值与最小值之间的浮点数范围。初始值为一个浮点数。
    • Float :浮点数,初始值也为一个浮点数。
    • Vector : 任意一个四维数。初始值格式为(x,y,z,w)。可以用于表示颜色,位置,方向等。
    • Int :任意一个整数。
Shader高级属性特性
  • [HDR] :
    指示纹理或颜色属性使用高动态范围(HDR)值。对于颜色属性,Unity编辑器会使用HDR拾色器编辑,举例:
    [HDR] _EmisssionColor(“Emisssion Color”,Color) = (0,0,0)
  • [MainTexture] :
    设置主纹理,默认情况下,Unity将属性名为_MainTex的纹理视为主纹理。如果希望将其他纹理视为主纹理,可以使用此特性。举例:
    [MainTexture] _CustomMainTex(“Custom Main Texure”,2D) = “white”{}
  • [MainColor] :
    设置主色。默认情况下,Unity将具有属性名称_Color的颜色视为主色。如果希望将其他颜色视为主色,可以使用此特性。
    [MainColor] _CustomColor(“Custom Color”,Color) = (1,1,1,1)
  • [NoScaleOffset] :
    隐藏纹理的平铺和偏移字段。例如:
    [NoScaleOffset] _NoScaleOffsetTex(“No Scale Offset Tex”, 2D) = “white”{}
  • [Normal] :
    指示纹理属性需要法线贴图。如果分配了不兼容的纹理,Unity编辑器会显示警告。例如:
    [Normal] _NormalMap(“Normal Map”, 2D) = “bump”{}
  • [Toggle] :
    定义一个切换开关,用于启用或禁用Shader中的某些功能。例如:
    [Toggle] _Invert(“Invert color?”, Float) = 0
    或者重新指定一个Shader关键词:
    [Toggle(_TEX_ON)] _Toggle(“Enable Main Texture”, Float) = 0
  • [Enum] :
    定义一个枚举属性,用于在Inspector面板中提供一组预定义的选项。例如:
    [Enum(ON, 0, OFF, 1)] _Enum(“Test Enum”, Float) = 1
  • [KeywordEnum] :
    与[Enum]类似,但用于定义Shader关键词枚举。例如:
    [KeywordEnum(None, Add, Multiply)] _Overlay(“Overlay mode”, Float) = 0
  • [Header] :
    在Inspector面板中为属性组添加标题。例如:
    [Header(“MainTex”)] _Prop1(“Prop1”, Float) = 0
  • [Space] :
    在Inspector面板中为属性添加垂直空间。例如:
    [Space(50)]
    表示在属性前添加50行的垂直空间。
Shader属性注意事项
在定义属性时,要确保属性名、显示名、类型和默认值等信息的准确性和一致性。 根据需要选择合适的属性类型和特性,以满足Shader的功能需求。 在使用高级特性时,要注意Unity版本和平台兼容性问题。

子着色器SubShaders

子着色器是定义整个Shader渲染流程的主要程序逻辑片段。
但一个着色器可以有多个子着色器。用于处理不同的渲染路径,如高端设备和低端设备的不同渲染。

每个子着色器都有自己的渲染标签(Tags)、渲染阶段(Pass)、着色器代码(CG\HLSL)。

渲染标签 Tags

渲染标签一般用来指定该子着色器的渲染类型、渲染队列等等预定义的类型。

标签是键值对(Key/Value)的形式,键值都是字符串的类型。
在SubShader中,标签用于告诉渲染引擎如何以及何时渲染该子着色器。

常见的渲染标签
  • RenderType(渲染类型)
    对着色器进行分类,Unity可以运行时替换符合特定Rendertype的所有Shader。常见值包括:

    • Opaque(翻译不透明)用于所有不透明的着色器。
  • Queue (渲染队列)
    控制渲染顺序,指定该物体属于哪一个渲染队列。常见的值包括:

    • Background(值为1000),多用于天空盒,在所有的不透明对象和透明对象之前绘制。
    • Geometry(默认值为0)(翻译为几何学),用于大多数的不透明对象。
    • AlphaTest(值2450):用于alpha测试的不透明对象。
    • Transparent(值从2500开始,如Transparent+1、Transparent+2等)(翻译为透明),用于透明对象,并按照距离排序进行渲染。
  • DisableBatching (禁用批处理)
    指是否对该着色器禁用批处理。常见值包括:

    • True,始终对此subshader禁用批处理。
    • False,默认值,不禁用批处理。
    • LODFading,当LOD淡化处于激活状态的时候,禁用批处理,主要用于树木。
  • ForceNoShadowCasting(强制无阴影投射)
    控制该SubShader的物体是否会投射阴影。常见值为True,表示不投射阴影。

  • IgnoreProjector(忽略投影仪)
    设置该SubShader的物体是否受Projector影响。常见的值为True,表示不受Projector影响,常用于半透明物体。

  • CanUseSpriteAtlas(可以使用精灵图集)
    当该SubShader用于Sprite精灵图时,如果该标签设为False,则表示不用精灵图集。

  • PreviewType(预览类型)
    材质面板的预览类型,一般默认材质预览效果是球形,还可以改为Plane(平面)或者Skybox(天空盒)。

渲染标签格式
Tags  
{  
    "RenderType"="Opaque"  
    "Queue"="Geometry"      
}
渲染标签使用注意事项

标签必须位于SubShader部分中,不能在Pass中声明。
除了Unity可以识别的内置标签外,还可以使用自己的标签,并通过Material.GetTag函数来查询这些标签。
合理使用标签可以优化渲染性能,例如通过控制渲染顺序来减少过度绘制。

LOD

leave of detail
shader中的LOD是一种优化渲染性能的技术,主要表现就是根据控制的细节等级来控制subshader的使用。

LOD,即细节层次,根据摄像机和游戏对象的距离,自动切换使用不同的细节级别的模型或Shader,从而优化渲染性能。在Unity中,LOD技术可以应用于网格和Shader两个方面。

LOD网格和LOD Shader
  • 网格LOD
    当游戏对象使用LOD时,Unity会根据游戏对象与摄像机的距离来显示该游戏对象的相应LOD级别。每个LOD级别都存在于单独的游戏对象中,每个游戏对象都有一个显示该LOD级别的Mesh Renderer组件。

    要使用网格LOD,必须有一个包含LOD Group组件的游戏对象。LOD Group组件提供了相应控件来定义LOD在此游戏对象上的行为方式,并会引用Unity为每个LOD级别显示或隐藏的游戏对象。

    • LOD Group组件
      LOD Group组件是Unity中用于管理LOD的组件,当一个GameObject被设置为使用LOD时,需要在这个游戏对象上添加LOD Group组件。

      LOD Group组件允许你自定义多个LOD级别,每个级别对应不同的游戏对象,这些游戏对象具有不同的复杂度和细节,Unity会根据游戏对象与摄像机的距离自动选择并显示适当的LOD级别的对象。这些游戏对象也就是平时所说的低模、中模、高模。

      距离阈值:这是定义何时切换到该LOD级别的距离值。当游戏对象与摄像机的距离小于或等于阈值时,将显示该LOD级别的游戏对象。

      动态切换:Unity会根据游戏对象与摄像机的实时距离,动态地切换显示的LOD级别。当距离变化时,Unity会平滑地过渡到新的LOD级别,以最小化视觉上的突兀感。

      平滑过渡:为了确保视觉上的连续性,建议在不同LOD级别之间使用相似的形状和纹理,以实现平滑过渡。

      合理使用:虽然LOD可以优化性能,但过度使用或不当配置可能导致视觉质量的显著下降。因此,应谨慎选择LOD级别和对应的游戏对象。

  • Shader LOD 全局
    在Unity中,Shader的LOD(Level of Detail)确实与SubShader的LOD有关,但Shader本身也可以有一个总体的LOD概念。

    指的是Shader在渲染过程中可能使用的不同细节级别的能力。这通常是通过在Shader中定义多个SubShader来实现的,每个SubShader都具有不同的LOD值,并根据需要被选择执行。

    在渲染过程中,Unity会根据当前渲染的上下文(如摄像机与游戏对象的距离、全局LOD设置等)选择一个LOD值最合适的SubShader来执行。具体来说,Unity会查找第一个LOD值小于或等于Shader当前设置的最大LOD值(通过Shader.maximumLOD或全局设置Shader.globalMaximumLOD来指定)的SubShader,并执行它。

    如果Shader中的所有SubShader的LOD值都高于当前设置的最大LOD值,那么将执行Fallback指定的Shader或SubShader(如果存在的话)。

    Shader总体的LOD是一个更宽泛的概念,它涵盖了Shader可能具有的不同细节级别的能力。而SubShader的LOD则是这种能力在Shader中的具体实现方式。Shader总体的LOD通过设置SubShader的LOD值和Shader的最大LOD值来定义和控制。在实际渲染过程中,Unity会根据这些设置来选择最合适的SubShader来执行。

    通常情况下,Shader当前设置的最大LOD值(Level of Detail)有默认值。在Unity这一游戏开发引擎中,Shader.globalMaximumLOD是一个属性,用于全局设置Shader的最大细节等级。这个属性的值是一个整数,表示Shader的最大LOD值,其默认值通常是1000。

    开发者可以根据游戏的需求和性能考虑来调整这个值。通过调整最大LOD值,开发者可以在保证游戏画面质量的同时,优化游戏的渲染性能,提升玩家的游戏体验。

  • LOD Group和SubShader LOD

    • LOD Group的LOD
      LOD Group组件是Unity中用于实现细节层次(Level of Detail)优化的重要工具。通过为游戏对象添加LOD Group组件,开发者可以定义多个LOD级别,并为每个级别指定不同的游戏对象或模型资源。这些资源通常具有不同的复杂度和细节,以适应不同的渲染需求。LOD Group组件会根据摄像机与游戏对象的距离自动选择并显示适当的LOD级别,从而优化渲染性能。

    • Shader中SubShader的LOD
      在Unity的Shader中,每个SubShader都可以设置一个LOD值。这个LOD值用于在运行时由渲染系统选择适当的SubShader进行执行。具体来说,渲染系统会查找第一个LOD值小于或等于当前全局或Shader特定的LOD设置的SubShader,并执行它。如果所有SubShader的LOD值都大于当前设置,则执行Fallback Shader。

    • 两者之间的关系与关联
      全局与局部:LOD Group的LOD设置是全局的,它针对整个游戏对象或场景中的一组对象进行配置。而Shader中SubShader的LOD设置是局部的,它仅影响该Shader内部的SubShader选择。

      目的不同:LOD Group的LOD主要用于根据距离优化渲染性能,通过在不同距离下使用不同复杂度的模型资源来实现。而Shader中SubShader的LOD则用于在Shader内部根据LOD值选择不同的渲染路径或效果。

      相互影响:虽然两者在目的和应用层面有所不同,但它们共同作用于游戏的渲染性能优化。通过合理配置LOD Group和Shader中的LOD值,开发者可以实现更加精细和高效的渲染效果。

    • 实际应用中的注意事项
      合理配置LOD级别:在配置LOD Group时,应确保不同LOD级别的模型资源在视觉上具有连贯性,以避免出现明显的跳跃或失真现象。

      优化Shader性能:在编写Shader时,应合理设置SubShader的LOD值,并根据需要选择适当的渲染路径和效果。同时,应关注Shader的性能开销,避免过度复杂的计算或纹理采样等操作。

      测试与调优:在实际应用中,需要对LOD Group和Shader中的LOD设置进行充分的测试和优化。通过不断调整和测试,可以找到最佳的配置方案,以实现最佳的视觉效果和性能表现。

    综上所述,LOD Group的LOD和Shader中SubShader的LOD虽然存在一定的关系和关联,但它们在应用层面和目的上有所不同。通过合理配置和优化这两者的LOD设置,开发者可以实现更加高效和精细的游戏渲染效果。

LOD作用
  • 提高渲染性能:
    LOD技术通过为Shader提供不同的细节级别,使得渲染引擎可以根据需要选择最合适的Shader变体进行渲染。在距离较远或不需要高精度渲染的情况下,使用较低细节级别的Shader可以显著减少GPU的工作量,从而提高渲染性能。
  • 降低GPU负载:
    随着游戏或场景规模的增大,GPU的负载也会相应增加。LOD技术通过动态调整Shader的细节级别,可以在保持视觉效果的同时,有效降低GPU的负载,避免渲染瓶颈的出现。
  • 增强游戏适应性:
    LOD技术使得游戏能够在不同配置的硬件上运行得更加流畅。对于低配硬件,游戏可以选择较低细节级别的Shader来保证游戏的流畅性;而对于高配硬件,则可以选择更高细节级别的Shader来提升游戏的视觉效果。
LOD意义
  • 提升用户体验:
    通过LOD技术,游戏开发者可以在保证游戏流畅性的同时,提供更加丰富和细腻的视觉效果。这不仅可以提升玩家的游戏体验,还可以增强游戏的吸引力。
  • 优化资源利用:
    LOD技术通过动态调整Shader的细节级别,可以更加合理地利用渲染资源。在需要高精度渲染的情况下,使用较高细节级别的Shader;在不需要高精度渲染的情况下,则使用较低细节级别的Shader。这样可以避免资源的浪费,提高资源的利用效率。
  • 推动游戏技术创新:
    LOD技术是游戏渲染领域的一项重要技术创新。它不仅为游戏开发者提供了更加灵活和高效的渲染手段,还为游戏技术的发展和进步提供了新的思路和方法。
LOD使用方式

LOD value

  • LOD值的设置以及赋值
    每个SubShader都可以设置一个LOD值,这个值用于决定SubShader是否会被渲染,系统会根据Shader的LOD值去寻找第一个LOD小于等于Shader的LOD的SubShader来执行,其他Shader则不会执行。

    默认情况下,一个Shader的LOD层级是无限大的,也就是只要硬件的支持的Shader就可以工作,但通过设置Shader.maximumLOD或Shader.gloabIMaximumLOD,可以控制单独的Shader或全局的所有Shader的LOD层级。

    在摄像机较远的情况下,可以使用LOD值较小的的SubShader(即细节较少的Shader),而在接近摄像机的时候,则切换到LOD值较大的SubShader(即细节更多的Shader)。

    LOD的value都是一个整数,可以根据需要自由设置,但Unity已经提供了一组内置的Built-in Shader里有LOD数值参考,其中Standard Shader的LOD的值为300,而一些简单的着色器如Unlit/Texture的LOD值为100.

    如果需要高性能的话,也就是要流畅为主,那么优化性能以减少资源消耗,选择较小的LOD数值。较小的LOD数值意味计算量较小,性能较高,渲染所需的资源较少,通常适用摄像机较远的物体,相当于简化渲染过程。

    如果需要高质量的话,也就是更好的画面,选择较大的LOD数值,较大的LOD数值意味着计算量较大,可以提供更丰富的视觉效果。渲染所需的资源较多,通常适用摄像机较劲的物体,更容易观察细节,提高画面质量。

    按需赋值,根据游戏的具体需求,为每个SubShader选择合适的LOD数值,例如,对于需要精细的渲染的物体,可以使用较大的LOD数值;而对于较远的背景物体或不太重要的细节,可以使用较小的LOD数值。

    参考built-in Shader 的LOD值,基本在100-300这些中选择赋值。

    当然可以通过玩家的需求来动态调整LOD, 也就是说可以根据设备的性能和玩家的需求来动态的调整展示的LOD级别,比如说玩家调整画面质量,那么就会换一套符合的低LOD的SubShader渲染。

在Unity等游戏引擎中,LOD技术通常通过为Shader定义多个SubShader,并为每个SubShader设置不同的LOD值来实现。在游戏运行时,渲染引擎会根据需要选择最合适的SubShader进行渲染。此外,还可以通过C#代码来设置全局或特定Shader的LOD值,以进一步控制渲染的性能和效果。

综上所述,Shader中的LOD技术是一种非常重要的渲染优化技术。它不仅可以提高渲染性能和降低GPU负载,还可以增强游戏的适应性和用户体验。因此,在游戏开发和渲染优化中,LOD技术具有广泛的应用前景和重要的实际意义。

LOD的优化策略
  • 资源分级:为了实施LOD技术,美术人员需要在资源上制作多个细节级别的模型或Shader。这通常包括高精度模型(用于近景)、中精度模型(用于中景)和低精度模型(用于远景)。
  • 程序框架:程序人员需要搭建完善的LOD框架和辅助工具,以确保LOD技术能够在游戏中正确应用。这包括设置LOD Group组件、配置SubShader的LOD值等。
  • 设备适配:根据不同设备的性能差异,可以调整LOD的切换策略。例如,在低端设备上使用更简单的材质和Shader,以降低GPU压力。
LOD注意

在Shader中,必须将子着色器按LOD降序排列,Unity会选择所找到的第一个有效子着色器进行渲染。

注意全局设置的Shader的LOD,方法在赋值的第二段。

渲染阶段 Pass

渲染阶段定义了着色器的一次渲染操作。每个渲染阶段都可以有自己的渲染状态设置(如背面剔除、深度写入、深度测试、混合模式等)和着色器代码。

Pass块就是SubShader中的一个语义块,用于定义着色器代码的具体实现。每个Pass块都代表了控制一次被渲染的几何体对象的渲染方式过程,可以包含渲染状态设置、纹理设置等等然后实现复杂渲染效果,多重纹理、光照效果、阴影等。

一个SubShader可以有多个Pass块。这些Pass块在渲染过程中起着至关重要的作用,定义了渲染物体时所需要的不同渲染阶段和状态。

多个Pass块之间的关系
  • 渲染顺序:

    按照Pass块的前后顺序进行渲染,这意味着,第一个Pass块会首先被渲染,然后是第二个,以此类推。这种顺序性允许开发者对渲染过程进行精细的控制,以实现复杂的渲染效果。

  • 状态设置:

    每个Pass块都可以设置自己的渲染状态和参数,如光照模型、纹理、材质属性等等。这些设置仅仅对该Pass块有效,不会影响其他Pass块。因此,开发者可以在不同的Pass块中实现不同的渲染效果,如阴影、高光、反射。

  • 资源消耗:

    虽然多个Pass块可以提供更复杂的渲染效果,但它们也会增加渲染时间和资源消耗,因此,在编写Shader时,应尽量控制Pass块的数量,以权衡渲染效果和性能。

  • 代码复用:

    可以给Pass命名,可以在不同的Shader中引用相同的Pass块,实现代码复用,提高开发效率。

Pass块结构
  • Name :Pass块的名称,用于引用和标识,在Unity中,Pass块的名字通常使用大写字母。
  • RenderSetup :渲染状态设置,包括深度测试、背面剔除、混合模式等。这些设置决定了渲染过程中显卡的状态。
  • TextureSetup :纹理设置,用于定义Pass块中使用的纹理,这包括纹理的采样方式、组合方式等。
Pass  
{  
    //Nmae
    Name "BASE"
    //RenderSetup 渲染状态设置
    Cull Off  
    ZWrite On  
    ZTest LEqual  
    Blend SrcAlpha OneMinusSrcAlpha

    //TextureSetup 纹理设置
    SetTexture [_MainTex]
    {
        combine texture
    }
  
    CGPROGRAM  
    // 着色器代码  
    ENDCG  
}
Pass块类型与功能
  • Regular Pass(常规Pass)
    普通的渲染通道,用于执行标准的渲染操作,意思就是是最基本的渲染代码,有完整的渲染流程以及算法。

    Regular Pass是最常见的Pass类型,它用于执行标准的渲染操作。这包括设置渲染状态(如深度测试、背面剔除等)、纹理采样、光照计算等。Regular Pass是着色器中最基本的渲染单元,它负责将几何体对象渲染到屏幕上。

    每个Regular Pass都是独立的渲染过程,可以包含自己的渲染状态和纹理设置。

    Regular Pass通常用于实现基本的渲染效果,如漫反射、镜面反射等。在Unity中,Regular Pass通常通过编写具体的着色器代码来定义。

    分辨方法:在着色器代码中,Regular Pass通常没有特殊的标签或指令来标识它,而是通过编写具体的渲染代码来定义。可以通过查看Pass块中的渲染状态和纹理设置来判断它是否是Regular Pass。

  • UsePass(使用Pass)
    引用其他Shader中的Pass块,复用代码提高可重复性,引用的Pass块必须定义了名字,便引用。

    UsePass是一种特殊的Pass类型,通过引用其他Shader中的Pass块,你可以轻松地在不同的Shader之间共享渲染逻辑。

    UsePass不直接包含渲染逻辑代码,而是引用其他Shader中的Pass块,UsePass可以用于实现复杂的渲染效果,同时保持代码的简洁性和可维护性。

    分辨方法:在着色器代码中,UsePass通过UsePass “ShaderName/PassName” 指令来引用其他Shader中的Pass块。可以通过查找UsePass指令来判断一个Pass块是否是UsePass。

    举例:

        // MyEffect.fx  
        // 基础Pass定义  
        Pass MyBasePass  
        {  
            VertexShader = "cg_vertex_shader"  
            {  
                void main(in float4 inPos : POSITION, out float4 outPos : POSITION)  
                {  
                    outPos = inPos;  
                }  
            }  
        
            FragmentShader = "cg_fragment_shader"  
            {  
                void main(out float4 outColor : COLOR)  
                {  
                    outColor = float4(1.0, 0.0, 0.0, 1.0); // 红色  
                }  
            }  
        }  
        
        // 引用基础Pass的Pass  
        Pass MyDerivedPass  
        {  
            UsePass = "MyBasePass"  
        
            // 可以在这里添加额外的设置或覆盖某些属性  
            // 例如,我们可以修改片段着色器来输出不同的颜色  
            FragmentShader = "cg_fragment_shader_override"  
            {  
                void main(out float4 outColor : COLOR)  
                {  
                    outColor = float4(0.0, 1.0, 0.0, 1.0); // 绿色  
                }  
            }  
        }
    
  • GrabPass(抓取Pass)
    GrabPass是一种特殊的渲染通道,它用于捕获屏幕内容(实际就是当前屏幕渲染的帧缓存(framebuffer))并写入到一个纹理中。这可以用于实现屏幕空间效果,如果屏幕后处理、反射等。通过GrabPass,你可以将当前屏幕的内容捕获到一个纹理中,并在后续的Pass块中通过特定的变量名进行访问。

    GrabPass不直接参与集合体的渲染,没有渲染逻辑代码,他是捕获屏幕内容。捕获的屏幕内容可以作为一个纹理在后续的Pass块中使用。

    GrabPass通常用于实现复杂的屏幕空间效果。如屏幕空间反射、屏幕空间环境光遮蔽等。

    分辨方法:在着色器代码中,GrabPass通过GrabPass{}指令来定义。可以通过查找GrabPass指令来判断一个Pass块是否是GrabPass。需要注意的是GrabPass块可能会对性能产生一定的影响,因为需要额外的渲染操作来捕获屏幕内容。

    GrabPass 可能会增加渲染开销,因为它需要额外的渲染操作来捕获帧缓存。在移动平台上,GrabPass 可能不受支持或性能较差,因此在使用时需要谨慎。

    举例:

        Shader "Custom/GrabPassExample"  
        {  
            Properties  
            {  
                _MainTex ("Base (RGB)", 2D) = "white" {}  
            }  
            SubShader  
            {  
                Tags { "RenderType"="Opaque" }  
                LOD 200  
    
                // 第一个Pass:使用GrabPass捕获当前帧缓存  
                Pass  
                {  
                    Name "GrabPass"  
                    ZTest Always  
                    ZWrite Off  
                    ColorMask 0  
                        
                    // 捕获当前帧缓存到_GrabTexture纹理  
                    GrabPass {}  
                }  
    
                // 第二个Pass:使用捕获的帧缓存纹理进行渲染  
                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;  
                    sampler2D _GrabTexture; // 捕获的帧缓存纹理  
    
                    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 baseColor = tex2D(_MainTex, i.uv);  
    
                        // 读取捕获的帧缓存颜色(可以混合或处理)  
                        fixed4 grabColor = tex2D(_GrabTexture, i.uv);  
    
                        // 这里简单地将两者混合,可以根据需要调整混合方式  
                        fixed4 finalColor = lerp(baseColor, grabColor, 0.5);  
    
                        return finalColor;  
                    }  
                    ENDCG  
                }  
            }  
            FallBack "Diffuse"  
        }
    

    解释:

    1. Properties:定义了一个基础纹理_MainTex。
    2. SubShader:
      • 第一个Pass:使用GrabPass捕获当前帧缓存。注意:
        • ZTest Always 和 ZWrite Off 确保这个Pass不会影响深度测试或写入。
        • ColorMask 0 确保这个Pass不会写入任何颜色数据(只捕获帧缓存)。
      • 第二个Pass:使用捕获的帧缓存纹理_GrabTexture进行渲染。
        • 顶点着色器vert将顶点数据转换为裁剪空间坐标,并传递UV坐标。
        • 片段着色器frag读取基础纹理和捕获的帧缓存纹理,并将它们混合(这里简单地使用lerp函数混合两者)。
Pass块中的渲染状态设置

渲染状态的设置对渲染性能和效果有重要影响,应根据具体需求进行调整。
不同的渲染状态组合可以实现不同的渲染效果,如透明渲染、阴影渲染等。
在编写Shader时,应确保渲染状态的设置与着色器代码的实现相匹配,以避免渲染错误或性能问题。

  1. 多边形剔除(Cull)
    Cull Back:不渲染多边形的背面(默认)。
    Cull Front:不渲染多边形的正面。
    Cull Off:不剔除任何面,两面都渲染。
  2. 深度测试(ZTest)
    ZTest LEqual:仅当当前像素的深度值小于或等于深度缓冲区中的值时,才更新像素(默认)。
    其他选项包括:Less、Greater、GEqual、Equal、NotEqual、Always。
  3. 深度写入(ZWrite)
    ZWrite On:写入深度缓冲区(默认)。
    ZWrite Off:不写入深度缓冲区。
  4. 深度偏移(Offset)
    用于解决深度冲突问题,如自遮挡或渲染顺序问题。
    设置Z缓冲区深度偏移,通常用于半透明物体的渲染。
  5. 混合模式(Blend)
    控制像素如何与帧缓冲区中的现有像素混合。
    Blend SrcAlpha OneMinusSrcAlpha:使用源像素的Alpha值作为源混合因子,1减去源像素的Alpha值作为目标混合因子(默认)。
    其他混合模式包括:加性混合、乘性混合、屏幕混合等。
  6. 颜色遮罩(ColorMask)
    设置颜色通道写入遮罩。
    ColorMask RGB:仅写入RGB通道。
    ColorMask A:仅写入Alpha通道。
    ColorMask 0:关闭对所有颜色通道的渲染。
  7. 双面渲染(Double Sided):控制是否渲染物体的双面。默认情况下,只有一面(通常是正面)会被渲染。
  8. 模板测试(Stencil Test):用于基于模板缓冲区的值来决定是否更新像素。它通常用于更高级的渲染技术,如轮廓渲染、区域剔除等。
  9. 裁剪测试(Scissor Test):限制渲染区域,只有位于指定矩形区域内的像素才会被渲染。
  10. 透明度测试(Alpha Test):根据像素的Alpha值来决定是否渲染该像素。通常用于实现物体的透明部分被剔除的效果。
  11. 阴影投射(Shadow Casting):控制物体是否投射阴影。这对于实现逼真的光照效果至关重要。

除了这些之外,Unity允许开发者自定义渲染状态,以满足特定的渲染需求。这通常通过ShaderLab或HLSL/CG等高级着色器语言来实现。
这些渲染状态可以帮助开发者可以实现更复杂的渲染逻辑和效果,从而满足特定的游戏或应用需求。

Pass块中的纹理设置

在Pass块中,纹理设置通常通过SetTexture命令来完成。这个命令用于将纹理应用到渲染的物体上,并指定纹理的组合方式。SetTexture命令必须放在Pass块的末尾。

SetTexture的基本语法
SetTexture [TexturePropertyName] { TextureBlock }
  • [TexturePropertyName]:这是纹理的属性名,它必须在Shader的属性部分被定义。
  • { TextureBlock }:这是纹理块,用于定义如何应用纹理。在纹理块内部,可以使用combine、matrix(Unity 5.0之前支持)和constantColor等命令来设置纹理的组合方式。
纹理组合方式

在纹理块中,combine命令是最常用的,它允许开发者定义纹理与其他输入(如上一个纹理的结果、常量颜色、顶点颜色等)的组合方式。combine命令的语法如下:

combine src1 operator src2 [modifier]
  • src1和src2:这是两个输入源,它们可以是前一个SetTexture的结果(previous)、常量颜色(constant)、顶点颜色(primary)或指定的纹理(texture)。
  • operator:这是操作符,用于定义src1和src2的组合方式,如相加(+)、相乘(*)、相减(-)、插值(lerp)等。
  • [modifier]:这是可选的修饰符,用于调整组合结果的颜色亮度(如Double使颜色亮度加倍)或颜色反相(在src属性前加-)。

举例:

Shader "Custom/TextureExample"  
{  
    Properties  
    {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
    }  
    SubShader  
    {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
  
        Pass  
        {  
            // 设置渲染状态(省略)  
            // ...  
  
            // 设置纹理  
            SetTexture [_MainTex]  
            {  
                combine texture * primary, double  
            }  
        }  
    }  
    FallBack "Diffuse"  
}

解释:定义了一个名为_MainTex的纹理属性,并在Pass块中使用SetTexture命令将其应用到渲染的物体上。在纹理块中,我们使用combine命令将纹理与顶点颜色相乘,并将结果的颜色亮度加倍。

注意
  • 纹理设置对渲染效果有重要影响,应根据具体需求进行调整。
  • 在使用SetTexture命令时,应确保纹理属性名与在Properties部分定义的名称一致。
  • 纹理组合方式的选择取决于所需的渲染效果,如相加可以实现亮化效果,相乘可以实现暗化效果等。
渲染着色器代码 CGPROGRAM … ENDCG

着色器代码包含了顶点和片段着色器的实现等等,这些代码通常涉及最底端所以用HLSL或GLSL编写,
一般还定义并编写顶点着色器函数和片段着色器函数。

  • 顶点着色器处理顶点数据,如位置、法线和纹理坐标等等,并将它们处理成屏幕空间中的坐标。
  • 片段着色器处理每个像素的颜色和光照信息。
  • 纹理采样:在片段着色器中,通常会进行纹理采样来获取像素的颜色,这涉及到如何读取和应用纹理到渲染的物体上。
  • 光照模型:片段着色器中可能会包含光照计算,如漫反射(Diffuse)、镜面反射(Specular)、
    环境光(Ambient)等等,这些决定了物体表面的最终颜色。

解析一个着色器代码时,需要理解关注的方面:

  • 输入输出:

    理解顶点着色器和片段着色器的输入和输出变量,这些变量通常包含位置、颜色、法线、纹理和坐标。

  • 着色器逻辑:

    分析顶点着色器和片段着色器中的数学逻辑运算,这些运算可能涉及到矩阵变换、光照计算、纹理采样等等。

  • 渲染状态:

    注意Pass块中的渲染状态,他们会影响最终的渲染效果。

  • 纹理和材质属性:

    理解着色器代码中如何使用纹理和材质属性。这些属性中可能会包含漫反射纹理、法线贴图、光泽度等等。

  • 性能考虑:

    分析着色器代码对性能的影响。过多的计算或复杂的纹理采样可能会降低渲染速度。

注意

在Unity的Shader开发中,没有直接提供GLSLPROGRAM … ENDGLSL或METALPROGRAM … ENDMETAL这样的代码块。Unity的Shader系统主要基于HLSL(High-Level Shader Language)或与之兼容的CG(C for Graphics)语言,通过CGPROGRAM … ENDCG代码块来包含着色器代码。

然而,Unity的着色器编译器非常灵活,它可以根据目标平台自动将HLSL代码转换为适合该平台的着色器代码。例如,当目标平台是iOS或macOS时,Unity会使用Metal作为图形API,并自动将HLSL代码转换为Metal着色器代码。同样地,当目标平台是WebGL时,Unity会将HLSL代码转换为GLSL(OpenGL Shading Language)代码。

这些转换过程是在Unity的引擎内部处理的,你不需要(也不能)直接在Unity的Shader代码中编写GLSL或Metal代码。相反,你应该继续使用HLSL来编写你的着色器代码,并信任Unity的引擎来为你处理与目标平台相关的细节。

着色器代码演示
CGPROGRAM  
#pragma vertex vert  
#pragma fragment frag  
#include "UnityCG.cginc"  
  
// 顶点着色器  
v2f vert(appdata v)  
{  
    v2f o;  
    o.pos = UnityObjectToClipPos(v.vertex);  
    o.uv = v.uv;  
    return o;  
}  
  
// 片段着色器  
fixed4 frag(v2f i) : SV_Target  
{  
    fixed4 texColor = tex2D(_MainTex, i.uv);  
    return texColor * _Color;  
}  
ENDCG
着色器代码解释
  • #pragma 是一个预编译指令,意思就是“编译指示”,就是c语言中的宏定义。

    • #pragma vertex vert 这条指令告诉着色器编译器,顶点着色器的函数名是vert,顶点着色器是图形管线中的一个阶段,负责处理每个顶点的数据,如位置、颜色、纹理坐标等。

    • #pragma fragment frag 这条指令指定了片段着色器的函数名叫frag。片段着色器也是图形渲染管线中的一个阶段,负责处理每个像素或片段的颜色或其他属性。

  • #include “UnityCG.cginc”

    • 这条指令就是说包含了Unity提供的一个公共代码文件UnityCG.cginc。这个文件包含了许多有用的变量、宏和函数。它们可以帮助你简化着色器的编写。
      例如,UnityCG.cginc中可能包含了用于光照计算的函数、用于处理纹理采样的宏等。通过包含这个文件,你可以在你的着色器代码中直接使用这些预定义的变量、宏和函数,而无需自己重新编写它们。

着色器代码函数示例解释
  1. 顶点着色器函数vert(示例)

    struct appdata_t  
    {  
        float4 vertex : POSITION;  
        float3 normal : NORMAL;  
        float4 texcoord : TEXCOORD0;  
    };  
    
    struct v2f  
    {  
        float4 pos : SV_POSITION;  
        float3 normal : NORMAL;  
        float2 uv : TEXCOORD0;  
    };  
    
    v2f vert(appdata_t v)  
    {  
        v2f o;  
        o.pos = UnityObjectToClipPos(v.vertex); // 将顶点从模型空间转换到裁剪空间  
        o.normal = UnityObjectToWorldNormal(v.normal); // 将法线从模型空间转换到世界空间  
        o.uv = v.texcoord.xy; // 传递纹理坐标  
        return o;  
    }
    

    在这个示例中,appdata_t是一个结构体,它定义了从顶点着色器输入的数据。v2f是另一个结构体,它定义了顶点着色器输出到片段着色器的数据。vert函数接受一个appdata_t类型的输入参数,并返回一个v2f类型的输出参数。

  2. 片段着色器函数frag(示例)

    sampler2D _MainTex;  
    float4 _MainTex_ST;  
    
    fixed4 frag(v2f i) : SV_Target  
    {  
        // 使用纹理采样器_MainTex和纹理坐标i.uv来获取纹理颜色  
        fixed4 col = tex2D(_MainTex, TRANSFORM_TEX(i.uv, _MainTex));  
        return col;  
    }
    

    在这个示例中,_MainTex是一个纹理采样器,_MainTex_ST是与之关联的纹理变换矩阵(尽管在这个简单的例子中我们并没有直接使用它)。frag函数接受一个v2f类型的输入参数,并返回一个fixed4类型的颜色值。这个函数使用tex2D函数来从纹理中采样颜色,并将结果作为片段的颜色输出。

备用着色器 FallBack

备用着色器是前面所有的子着色器都不支持当前设备时使用的备用着色器,确保有一个基本的渲染效果。

Fallback "Diffuse"   ///回退至 ‘漫反射’ Shader

Fallback "Diffuse"是Unity着色器代码中的一个指令,用于指定当当前着色器因为某些原因(如硬件不支持或编译错误)无法运行时,Unity应该回退到哪个着色器。这里的“Diffuse”通常指的是一个基本的漫反射着色器,它会给物体一个基本的颜色渲染效果。

在Unity Shader中,Fallback "Diffuse"指的是当Unity无法找到或运行Shader中定义的所有SubShader时,将回退到(Fallback到)一个名为"Diffuse"的Shader或其子着色器(Subshader)进行渲染。这里的"Diffuse"并不是一个可以手动配置的特定值或参数,而是一个预定义的Shader名称或类型,它通常指的是实现基础漫反射光照效果的Shader。

这个Diffuse Shader是一个Unity的Built-in Shader。
关于"Diffuse" Shader的渲染效果,它主要实现的是物体表面的基础漫反射光照。漫反射是光线照射到物体表面后,在各个方向上均匀反射的现象。在渲染中,这通常意味着物体会根据光源的位置和强度,以及物体表面的材质属性(如颜色、粗糙度等),来呈现出不同的明暗和色彩变化。

具体来说,“Diffuse” Shader的渲染效果可能包括以下几个方面:

  • 基础色彩:物体表面会呈现出其基础色彩,这个色彩通常由Shader的属性或材质设置中的漫反射颜色(Diffuse Color)决定。
  • 光照变化:随着光源位置和强度的变化,物体表面的明暗和色彩也会发生变化。这种变化是基于漫反射光照模型计算的,它考虑了光源方向、物体表面法线和漫反射颜色等因素。
  • 阴影效果:当光源投射阴影时,“Diffuse” Shader会参与阴影的计算和渲染。这意味着物体会在光源的照射下产生阴影,同时也会接收来自其他物体的阴影。

需要注意的是,虽然"Diffuse" Shader提供了一种基础且常见的渲染效果,但在实际应用中,开发者通常会根据具体需求对Shader进行定制和扩展。例如,添加高光反射、透明效果、细节纹理等,以创造出更加丰富和逼真的渲染效果。

此外,Unity的Shader系统还提供了丰富的标签(Tags)、状态设置和Pass定义等功能,允许开发者对渲染过程进行更精细的控制和优化。这些功能可以帮助开发者实现更复杂和高效的渲染效果,从而满足不同类型游戏和应用的需求。

自定义编辑器 CustomEditor(可选)

自定义编辑器允许开发者为着色器创建一个自定义的材质编辑器界面,以便在Unity Inspector中以自定义的方式编辑材质属性。这部分是可选的,但对于提高用户体验和简化材质设置过程非常有用。

在Unity中,自定义编辑器(Custom Editor)用于扩展Unity Editor的功能,使你可以自定义Inspector面板中的显示和行为。这对于创建复杂的Shader或Material属性非常有用。下面是一个简单的示例,展示如何为自定义Shader创建一个自定义编辑器。

首先,你需要有一个自定义Shader。为了简洁,这里只展示一个简单的Shader代码示例:

// MyCustomShader.shader  
Shader "Custom/MyCustomShader"  
{  
    Properties  
    {  
        _MainTex ("Texture", 2D) = "white" {}  
        _Color ("Color", Color) = (1,1,1,1)  
        _Glow ("Glow", Range(0, 1)) = 0.5  
    }  
    SubShader  
    {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
  
        Pass  
        {  
            CGPROGRAM  
            #pragma vertex vert  
            #pragma fragment frag  
  
            #include "UnityCG.cginc"  
  
            struct appdata_t  
            {  
                float4 vertex : POSITION;  
                float2 uv : TEXCOORD0;  
            };  
  
            struct v2f  
            {  
                float2 uv : TEXCOORD0;  
                float4 vertex : SV_POSITION;  
            };  
  
            sampler2D _MainTex;  
            float4 _MainTex_ST;  
            float4 _Color;  
            float _Glow;  
  
            v2f vert (appdata_t 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) * _Color;  
                col.rgb += _Glow;  
                return col;  
            }  
            ENDCG  
        }  
    }  
}

接下来,为这个Shader创建一个自定义编辑器脚本。这个脚本需要放在Editor文件夹中,并且需要继承自ShaderGUI类。

// MyCustomShaderEditor.cs  
using UnityEngine;  
using UnityEditor;  
  
[CustomEditor(typeof(Material))]  
[CanEditMultipleObjects]  
public class MyCustomShaderEditor : ShaderGUI  
{  
    private MaterialEditor materialEditor;  
  
    public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)  
    {  
        this.materialEditor = materialEditor;  
  
        // Fetch properties  
        MaterialProperty mainTexProp = FindProperty("_MainTex", props);  
        MaterialProperty colorProp = FindProperty("_Color", props);  
        MaterialProperty glowProp = FindProperty("_Glow", props);  
  
        // Using default labelWidth  
        EditorGUI.BeginChangeCheck();  
        {  
            // Shader properties  
            materialEditor.ShaderProperty(mainTexProp, "Texture");  
            materialEditor.ShaderProperty(colorProp, "Color");  
            materialEditor.ShaderProperty(glowProp, "Glow");  
        }  
        if (EditorGUI.EndChangeCheck())  
        {  
            foreach (Material mat in materials)  
            {  
                // Perform any additional updates if needed  
            }  
        }  
  
        // Additional custom GUI elements can be added here  
    }  
}

在这个自定义编辑器脚本中:

  • [CustomEditor(typeof(Material))]:这个属性指定了这个编辑器脚本是用于编辑Material的。
  • [CanEditMultipleObjects]:这个属性允许编辑器同时编辑多个对象。
  • OnGUI方法:这是自定义编辑器的主要方法,它会在Inspector面板中显示和编辑Shader属性。
  • FindProperty方法:用于获取Shader中的属性。
  • materialEditor.ShaderProperty方法:用于在Inspector面板中显示和编辑属性。

最后,将MyCustomShaderEditor.cs文件放在项目的Editor文件夹中。当你在Unity Editor中选中一个使用这个Shader的Material时,你会看到自定义的Inspector面板。

这样,你就成功创建了一个简单的自定义编辑器来编辑你的自定义Shader属性。你可以根据需要进一步扩展和自定义这个编辑器。

UnityShader变体

UnityShader变体的概述

定义

Shader变体是指通过不同的编译条件(#pragma指令、UNITY_INSTANCING_BUFFER等)生成的多个Shader版本。每个变体都对应着一组特定的属性和状态组合。

Shader变体使得着色器能够根据不同的渲染需求(如不同的光照模型、材质属性等)生成不同的渲染效果,从而提高了着色器的灵活性和适应性。

在运行时,Unity会根据当前的渲染需求和平台特性选择合适的Shader变体进行渲染。这个过程是通过Unity的渲染引擎和着色器管理系统实现的。

疑问
  1. Shader变体是否只有Unity引擎存在?
    Shader变体并非Unity引擎独有的特性。在图形渲染领域,着色器(Shader)的变体概念是为了适应不同的渲染需求和硬件平台而产生的。
    虽然Unity引擎对Shader变体的管理和优化提供了强大的支持,但其他图形引擎(如Unreal Engine、CryEngine等)也可能有类似的机制来生成和管理Shader变体。
    因此,Shader变体并非Unity引擎所独有。

  2. Shader变体是生成的吗?
    由开发者通过特定的编译指令(如Unity中的#pragma multi_compile和#pragma shader_feature)来定义的。这些指令告诉引擎在编译着色器时需要生成哪些变体。开发者需要根据项目的需求和目标平台的特性来合理地定义这些变体,以确保渲染效果和性能的优化。
    Shader变体的生成和管理是由引擎自动完成的,直接修改可能会导致渲染效果的不一致或性能问题。因此,在大多数情况下,开发者应该通过修改着色器代码和编译指令来影响Shader变体的生成和优化。

  3. Shader变体的内部细节?
    Shader变体内部细节也由着色器语言(如HLSL、GLSL等)编写的着色器代码在不同编译条件下的不同实现。在Unity中,Shader变体和Shader本体一样通常使用HLSL(High-Level Shading Language)或CG/HLSL(NVIDIA的C for Graphics,结合了C语言和HLSL的特性)来编写。这些语言提供了对图形硬件的直接控制,允许开发者实现复杂的渲染效果和算法。
    开发者可以查看Shader变体的内部细节。在Unity中,可以通过查看着色器代码和编译生成的变体文件来了解变体的具体实现。此外,Unity还提供了一些工具(如Shader Variant Collection)来帮助开发者管理和优化Shader变体。通过这些工具,开发者可以查看哪些变体被生成了、哪些变体被使用了以及它们的性能表现等信息。
    需要注意的是,虽然可以查看Shader变体的内部细节,但直接修改这些变体可能并不总是可行的或推荐的。直接修改可能会导致渲染效果的不一致或性能问题。因此,在大多数情况下,开发者应该通过修改着色器本体代码和编译指令来影响Shader变体的生成和优化。

综上所述,Shader变体是图形渲染领域中的一个重要概念,用于适应不同的渲染需求和硬件平台。虽然Unity引擎对Shader变体的管理和优化提供了强大的支持,但Shader变体并非Unity引擎所独有,也并非完全自动生成。开发者可以通过查看着色器代码和编译生成的变体文件来了解Shader变体的内部细节,并通过修改Shader本体着色器代码和编译指令来影响Shader变体的生成和优化。

变体的生成方式

Shader变体是由Shader本体通过不同的编译条件生成的。在Unity中,开发者可以使用#pragma multi_compile#pragma shader_feature等指令来定义变体。这些指令告诉Unity在编译着色器时生成哪些变体。

  • #pragma multi_compile
    根据提供的多个关键字组合生成不同的Shader变体。每个关键字都可以被视为一个编译选项,引擎会生成所有可能的组合变体。

    无差别构建Shader,即根据定义的变体数量,排列组合出全部的Shader可能。这种方法的缺点是可能会生成大量的变体,增加内存开销。

    所以如果定义的关键字较多,生成的变体组合数量会是指数级增长。组合爆炸多。而且无论是否动态启用,变体都会被打入包中。这可能导致打包速度变慢,并生成许多未使用的Shader变体。

  • #pragma shader_feature
    更智能,只打包材质球用到的变体组合,没有用到的变体组合不会打包。

    这种方法的优点是内存开销较小,但缺点是如果动态启用(代码中调用EnableKeyword)变体的话,由于没直接引用,可能导致想要的变体没被打进包中,找不到对应的Shader。

    此时需要使用其他方法(如ShaderVariantCollection)来确保变体被正确包含。

  • #pragma skip_variants
    用于排除某些特定的变体组合,防止它们被生成。

    当你知道某些变体组合是不需要的,或者它们会导致性能问题时,可以使用此指令来排除它们。

下面是Unity内置的预编译指令

  • #pragma multi_compile_fwdbase
    为ForwardBase的pass生成不同的光照变体。

  • #pragma multi_compile_fwdadd_fullshadows
    为ForwardAdd的pass生成包含自阴影的变体。

Shader变体的注意事项

Unity Shader变体的管理和优化是提升游戏性能的重要环节。不合理的Shader变体可能导致性能下降、内存占用增加以及构建时间延长等问题。因此,开发者需要深入理解Shader的工作原理和编译机制,通过合理的设计和技巧(如避免过多的条件判断、合并相似的变体等)来减少不必要的变体生成。

  • 全局宏限制:
    Unity中限制了全局宏的个数(通常为265个),因此在使用#pragma multi_compile或#pragma shader_feature等指令时需要注意宏的数量不要超过限制。可以使用本地宏(如shader_feature_local和multi_compile_local)来替代一部分全局宏。

  • 避免不必要的变体:
    开发者应尽量避免在Shader中使用过多的条件判断(如if-else语句),因为每个不同的条件分支都可能产生新的变体。可以通过合并相似的变体、使用参数控制权重或强度等方法来减少不必要的变体生成。

  • 变体数量控制:
    过多的Shader变体可能导致性能下降和内存占用增加。因此,开发者需要谨慎地定义和使用变体。

  • 变体生效方式:
    Shader变体可以通过两种方式生效:

    • 在材质中勾选相应的关键字
    • 通过代码调用Material.EnableKeyword或Shader.EnableKeyword来启用。
  • 变体收集与预加载:
    ShaderVariantCollection是Unity提供的一个工具,用于收集和管理Shader变体。它可以帮助开发者确保在打包时包含所有必要的变体,并在运行时预加载这些变体以减少卡顿。使用ShaderVariantCollection时,开发者需要指定要收集的变体,并通过ShaderVariantCollection.WarmUp()方法进行预加载。通过预加载来避免渲染时的卡顿。

Shader变体代码示例及解释

Shader变体(Shader Variants)是指根据不同的渲染路径、光照模型或材质属性等条件生成的Shader的不同版本。
以两个简单的Unity Shader变体代码示例及解释来说明这个概念。

  1. 第一个案例以if(属性名PropertiesName)来示例变体的一种情况。

    Shader "Custom/SimpleVariantShader"  
    {  
        Properties  
        {  
            _MainTex ("Texture", 2D) = "white" {}  
            _Color ("Color", Color) = (1,1,1,1)  
            _UseSpecular ("Use Specular", Bool) = false  
        }  
        SubShader  
        {  
            Tags { "RenderType"="Opaque" }  
            LOD 200  
    
            Pass  
            {  
                CGPROGRAM  
                #pragma vertex vert  
                #pragma fragment frag  
    
                #include "UnityCG.cginc"  
    
                struct appdata_t  
                {  
                    float4 vertex : POSITION;  
                    float2 uv : TEXCOORD0;  
                };  
    
                struct v2f  
                {  
                    float2 uv : TEXCOORD0;  
                    float4 vertex : SV_POSITION;  
                };  
    
                sampler2D _MainTex;  
                float4 _MainTex_ST;  
                float4 _Color;  
                bool _UseSpecular;  
    
                v2f vert (appdata_t v)  
                {  
                    v2f o;  
                    o.vertex = UnityObjectToClipPos(v.vertex);  
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);  
                    return o;  
                }  
    
                half4 frag (v2f i) : SV_Target  
                {  
                    half4 col = tex2D(_MainTex, i.uv) * _Color;  
    
                    // 根据_UseSpecular的值选择是否添加镜面高光  
                    if (_UseSpecular)  
                    {  
                        // 这里可以添加镜面高光计算代码,作为示例简化,直接修改颜色  
                        col.rgb += 0.5f; // 假设的镜面高光效果  
                    }  
    
                    return col;  
                }  
                ENDCG  
            }  
        }  
        FallBack "Diffuse"  
    }
    

    Shader代码解释

    注意看片段着色器函数中的if代码块,这里通过判断属性_UseSpecular的值来决定是否添加镜面高光。

    这个属性值_UseSpecular是全局的属性,在材质器的Inspector上可以选择勾选为true或false,根据_UseSpecular的值不同,Shader的行为会有所不同,从而生成两个不同的Shader变体。

  2. 第二个案例展示了Shader用代码写变体的第二种情况,预编译,以#pragma为例。

    Shader "Custom/MultiCompileShader"  
    {  
        Properties  
        {  
            _MainTex ("Texture", 2D) = "white" {}  
            _Color ("Color", Color) = (1,1,1,1)  
            _LIGHTING_MODEL ("Lighting Model", Float) = 0 // 0: Lambertian, 1: Phong  
        }  
        SubShader  
        {  
            Tags { "RenderType"="Opaque" }  
            LOD 200  
    
            Pass  
            {  
                CGPROGRAM  
                #pragma vertex vert  
                #pragma fragment frag  
    
                // 使用multi_compile指令生成变体  
                #pragma multi_compile LIGHTING_MODEL_LAMBERTIAN LIGHTING_MODEL_PHONG  
    
                #include "UnityCG.cginc"  
    
                struct appdata_t  
                {  
                    float4 vertex : POSITION;  
                    float3 normal : NORMAL;  
                    float2 uv : TEXCOORD0;  
                };  
    
                struct v2f  
                {  
                    float2 uv : TEXCOORD0;  
                    float3 worldNormal : TEXCOORD1;  
                    float4 vertex : SV_POSITION;  
                };  
    
                sampler2D _MainTex;  
                float4 _MainTex_ST;  
                float4 _Color;  
    
                v2f vert (appdata_t v)  
                {  
                    v2f o;  
                    o.vertex = UnityObjectToClipPos(v.vertex);  
                    o.worldNormal = UnityObjectToWorldNormal(v.normal);  
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);  
                    return o;  
                }  
    
                half4 frag (v2f i) : SV_Target  
                {  
                    half4 col = tex2D(_MainTex, i.uv) * _Color;  
    
                    // 根据LIGHTING_MODEL的值选择不同的光照模型  
                    #if defined(LIGHTING_MODEL_LAMBERTIAN)  
                        // Lambertian光照模型计算  
                        float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);  
                        float diff = max(0, dot(i.worldNormal, lightDir));  
                        col.rgb *= diff;  
                    #elif defined(LIGHTING_MODEL_PHONG)  
                        // Phong光照模型计算(简化版,未包含高光部分)  
                        float3 viewDir = normalize(_WorldSpaceCameraPos - i.vertex.xyz);  
                        float3 halfDir = normalize(lightDir + viewDir);  
                        float diff = max(0, dot(i.worldNormal, lightDir));  
                        // 这里可以添加Phong模型的高光计算,但为了简化示例,我们只修改漫反射部分  
                        col.rgb *= diff * 1.5f; // 假设的增强效果  
                    #endif  
    
                    return col;  
                }  
                ENDCG  
            }  
        }  
        FallBack "Diffuse"  
    }
    

    Shader代码解释

    注意看属性里有一个属性_LIGHTING_MODEL,这个属性是决定光照模型的选择,虽然作为Float类型定义,但实际上是作为开关。

    #pragma multi_compile LIGHTING_MODEL_LAMBERTIAN LIGHTING_MODEL_PHONG指示Unity为这两个不同的光照模型生成Shader变体。这意味着Unity会编译两个版本的片段着色器:一个使用Lambertian光照模型,另一个使用Phong光照模型。

    注意在片段着色器中,使用了预处理指令#if defined#elif defined来检查哪个光照模型被编译。根据编译的光照模型,执行相应的光照计算。

    • Lambertian光照模型:计算漫反射光照,使用点积来确定光线与法线之间的夹角。
    • Phong光照模型(简化版):同样计算漫反射光照,但这里为了示例简化了高光部分,只增强了漫反射效果。

    当你在Unity的材质编辑器中更改_LIGHTING_MODEL的值(尽管在这个例子中我们并没有直接暴露这个属性给编辑器,通常会通过代码(如Material.EnableKeyword和Material.DisableKeyword)或Shader Variant Collection来管理这些变体),Unity会根据当前设置的属性组合生成一个新的Shader变体。在这个例子中,会有两个变体:一个使用Lambertian光照模型,另一个使用Phong光照模型。

    在这个例子中,_LIGHTING_MODEL更像是一个内部机制,用于通过#pragma multi_compile指令生成变体,而不是一个用户可配置的属性。

    注意#pargma multi_compile指令

    当Unity处理一个包含 #pragma multi_compile 指令的Shader时,它会识别出所有可能的宏定义组合,并为每种组合生成一个Shader变体。这些变体在Shader首次被加载到项目中时(例如,当你将一个新的Shader分配给某个材质时),或者在Unity编辑器中显式触发Shader重新编译时(例如,通过更改材质设置或Shader代码),会被自动生成。

    然而,有几个关键点需要注意:

    • 变体生成是自动的:一旦Shader代码包含 #pragma multi_compile,Unity就会在需要时自动生成相应的变体,无需额外的代码调用。
    • 变体选择是动态的:在运行时,Unity会根据当前场景中的光照条件、材质设置以及可能的其他因素(如Shader中的其他预处理指令),选择最合适的Shader变体来渲染对象。
    • 性能考虑:虽然 #pragma multi_compile 提供了很大的灵活性,但过多的变体可能会导致Shader编译时间增加,并且在运行时占用更多的内存。因此,应该谨慎使用,并确保只包含真正需要的变体。
    • 手动管理变体:在某些情况下,你可能希望更精细地控制哪些变体被生成。这可以通过使用Shader Variant Collection来实现,它允许你显式指定哪些变体应该被包含在构建中。
    • 代码调用:虽然 #pragma multi_compile 本身不需要额外的代码调用来生成变体,但你可以通过代码(如 Material.EnableKeyword 和 Material.DisableKeyword)来动态地启用或禁用特定的Shader关键字,从而影响Unity选择哪个变体进行渲染。

    综上所述,#pragma multi_compile 指令本身不会立即生成变体,但它会告诉Unity在需要时生成哪些变体。这些变体是在Shader被实际使用或构建时自动生成的。

Shader变体补充链接

ue的shader和unity的shader学习路线有区别吗? - Wilson的回答 - 知乎
https://www.zhihu.com/question/506482067/answer/3051682735

Unity Shader Graph

请见第二篇md,《Unity Shader Graph》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值