第1章: Shader必备概念

第1节: 渲染管线是什么

渲染管线概述

什么是渲染管线?

渲染管线(渲染流水线)

它是计算机图形学中用于将三维场景转换为最终屏幕所见图像的过程

它是由一系列的阶段和操作组成的,每个阶段都负责执行特定的任务

逐步处理输入的集合数据和纹理信息

最终生成可视化图像的过程

简单的概括:渲染管线(流水线)就是将数据分阶段的变为屏幕图像的过程

渲染管线中的数据指的是什么?

渲染管线(流水线)的概念是:

将数据分阶段的变为屏幕图像的过程

这里的数据指的是:

  • 顶点数据:模型的顶点坐标、法线向量、纹理坐标等等

  • 纹理数据:纹理贴图等

  • 光照数据:光照参数、光源信息等

等等 Unity场景上相关的数据

渲染管线中的分阶段指的是什么?

渲染管线(流水线)的概念是:

将数据分阶段的变为屏幕图像的过程

这里的分阶段指的是:

渲染管线分为3个阶段

应用阶段 ——> 几何阶段 ——> 光栅化阶段

在每一个阶段都会对数据进行处理

最终目的就是在屏幕上让我们看见最终的图

总结

渲染管线(流水线)就是将数据分阶段的变为屏幕图像的过程

其中

数据就是我们在游戏场景中放置的模型、光源、摄像机等等内容的数据

阶段就是渲染管线中的三个阶段

应用阶段——>几何阶段——>光栅化阶段

通过这三个阶段对数据的处理,最终我们就能够在屏幕上看见最终的图像

注意:不同的渲染管线和图形API可能会有不同的术语和具体实现细节

应用阶段

应用阶段主要做什么

渲染管线的应用阶段中大部分的内容都和渲染无关(比如:游戏逻辑处理、动画更新、物理模拟、场景管理等等)

当应用阶段完成后,后面的几何阶段以及光栅化阶段将开始处理和图形渲染相关的数据和操作。

那么应用阶段为什么会归纳到渲染管线中呢?

那是因为应用阶段为渲染管线的后续提供了最重要的内容——数据

应用阶段主导者是CPU,在这一阶段,我们将渲染需要用到的数据传递给GPU用于后续的两个阶段的处理

应用阶段为渲染准备了些什么?

  1. 把不可见的物体数据剔除

  2. 准备好模型相关数据(顶点、法线、切线、贴图、着色器等等)

  3. 将数据加载到显存中

  4. 设置渲染状态(设置网格需要使用那个着色器、材质、光源属性等等)

  5. 调用DrawCall(CPU通知GPU使用相关的数据和渲染状态进行渲染)

渲染管线(流水线)中的应用阶段

主要是CPU主导的阶段

它为渲染完成的最主要的工作就是提供后续的渲染数据

比如:

顶点、法线、切线、纹理坐标、变换矩阵、材质属性等等

在应用阶段中,我们主要就是按照Unity的规则进行游戏开发即可

我们需要注意的就是关于DrawCall的优化

DrawCall

关于DrawCall

一次DrawCall其实就是CPU命令GPU进行渲染的命令

主要的性能的瓶颈是CPU造成的

每次调用DrawCall之前,CPU需要想GPU发送很多内容,包括数据、状态、命令等等。

如果DrawCall过多,CPU就会把大量的时间花费在提交DrawCall上,造成CPU过载,让玩家感受到卡顿

如何减少DrawCal

使用批处理,可以有效的减少DrawCall,从而提升性能表现

1.合并网格(可以将静态物体合并网格)

2.共用材质(在不同网格之间共用一种材质)

3.合并图集(2D游戏和UI中,可以将多张图片合并为一张大图)

等等

总结

渲染管线(流水线)中的应用阶段

主要是CPU主导的阶段

它为渲染完成的最主要的工作就是提供后续的渲染数据

比如:

顶点、法线、切线、纹理坐标、变换矩阵、材质属性等等

在应用阶段中,我们主要就是按照Unity的规则进行游戏开发即可

DrawCall过多时,性能瓶颈是由CPU造成的,我们可以用批处理技术优化它

几何阶段

知识必备

图元

在渲染管线中,图元是指几何数据的基本单元

它是构成几何体的最小可绘制的单元

图元可以是点、线、三角形,在渲染管线的几何阶段,顶点数据会被组合为图元

这些图元将在后续的光栅化阶段转换为像素,最终呈现在屏幕上

几何阶段主要做什么

概括

渲染管线的几何阶段主要由GPU主导,因此我们无法拥有绝对的控制权,但是GPU为我们开放了部分控制权

几何阶段主要做的事情是根据应用阶段输入的数据信息进行顶点坐标转换以及裁剪不可见图元等工作

具体过程

  • 顶点着色器:

它处理来自应用阶段由CPU传递过来的顶点相关数据,输入进来的每一个顶点都会调用一次顶点着色器中的逻辑

顶点着色器需要完成的工作主要有:

  1. 坐标变换 —— 顶点变换、法线变换、纹理坐标变换等

  2. 顶点属性处理—— 对顶点的其他属性进行处理,比如顶点颜色、透明度、切线向量等,可以用于实现顶点动画、着色、光照等效果

  3. 顶点插值 —— 计算顶点属性的插值值

等等

对于我们来说顶点着色器是完全可编程的

  • 曲面细分着色器、几何着色器:

它们两对于对于我们来说是可选的着色器,并且他们需要硬件和驱动程序的支持才能使用

因此,我们在学习过程中不做详细讲解

  • 裁剪:

裁剪阶段会自动的将不在视野内和部分在视野内的图元(点、线、三角形)进行裁剪,我们可以进行一些配置,但是一般我们

不需要进行任何处理,渲染管线会自动帮助我们进行处理

  • 屏幕映射:

将输入的三维坐标系下的图元坐标转换到屏幕坐标系中

几何阶段为渲染准备了些什么

在渲染管线(流水线)的几何阶段,最主要做的工作就是

对顶点进行处理,并进行坐标转换,裁剪画面外的图元

最主要完成的就是将模型的顶点从其 本地坐标 转换到最终的 屏幕坐标 中

对于我们来说,我们只要在顶点着色器中进行一些操作就可以带来不同的表现效果的体现

比如:水波纹、布料等等

总结

渲染管线(流水线)中的几何阶段

主要是GPU主导的阶段

它为渲染完成的最主要的工作就是 顶点处理,坐标转换,裁剪画面外图元等等

在几何阶段中,我们主要通过自定义 顶点着色器 阶段

为我们带来一些不同的画面表现效果

光栅化阶段

知识必备

像素

像素是计算机图形学中的基本概念,它是组成图像的最小可控单位

具有位置和属性,用于表示图像中的颜色和其他信息

它是二位图像中的一个点,每个像素都占据屏幕上的一个固定位置

比如我们常见的显示器分辨率为:1920 x 1080

就表示宽度为1920个像素,高度为1080个像素

片元

在渲染管线中,片元是指在光栅化阶段生成的像素或像素片段

片元是渲染管线中进行像素级别操作和计算的基本单位

每个片元代表了屏幕上的一个像素,并且具有位置信息和与之相关的属性

比如:颜色、深度值、法线等等

光栅化阶段主要做什么

概括

渲染管线的光栅化阶段同样由GPU主导,同样我们无法拥有绝对的控制权,同样GPU为我们开放了部分控制权

光栅化阶段主要做的事情是根据几何阶段输入的信息计算每个图元覆盖哪些像素,以及为这些像素计算他们的颜色等等工作

具体过程

  • 三角形设置:

几何阶段输入到光栅化阶段的数据主要是三角形网格的顶点信息,我们得到的只是三角形网格每条边的两个端点信息

如果想要得到整个三角形网格对像素的覆盖情况,就必须计算每条边上的像素坐标,为了能计算三角形边界像素的坐标信息,

我们必须得到三角形边界的表示方式。

在三角形设置这个小阶段,GPU主要做的事情就是计算三角形网格的表示数据

  • 三角形遍历:

该阶段主要根据三角形设置中计算出的三角形网格数据

检查每个像素是否被一个三角形网格所覆盖

如果覆盖的话,就会生成一个片元(包含屏幕坐标、深度、法线等等信息)

这个阶段也被成为扫描变换

在三角形遍历这个小阶段

GPU主要做的事情就是根据三角形网格信息得到被它们覆盖的片元序列

  • 片元着色器(像素着色器):

它主要完成对 三角形遍历输入的片元序列中的 每个片元(像素)的着色计算和属性处理

片元着色器需要完成的工作主要有:

  1. 光照计算 —— 计算片元的光照效果

  2. 纹理映射 —— 根据片元在纹理中的位置,对纹理进行采样,将纹理颜色映射到片元上,实现表面贴图效果

  3. 材质属性处理 —— 根据材质的属性,比如颜色、透明度、反射率等,计算片元的最终颜色和透明度

  4. 阴影计算 —— 根据光源等信息,计算片元是否处于阴影中,影响其最终颜色

等等

对于我们来说片元着色器是完全可编程的

  • 逐片元操作(输出合并阶段):

它主要完成对 片元着色器 输出数据(最终颜色、法线、纹理坐标、深度等)进行各种处理和计算

逐片元操作主要完成的工作主要有:

  1. 决定每个片元的可见性,比如深度测试、模板测试

  2. 如果通过了所有测试,需要把片元的颜色值和已经存储在颜色缓冲区的颜色进行合并(混合)

等等

对于我们来说逐片元操作是可配置的

光栅化为渲染准备了些什么

在渲染管线(流水线)的光栅化阶段,最主要做的工作就是

对片元(像素)进行最终处理

最主要完成的就是确定片元(像素)最终是否渲染到屏幕上

并且确定其的最终渲染的颜色效果

对于我们来说,我们只要在片元着色器中进行一些处理就可以带来不同的表现效果的体现

比如:逼真的水面效果、火焰、黑白、模糊等等效果

总结

渲染管线(流水线)中的光栅化

主要是GPU主导的阶段

它为渲染完成的最主要的工作就是 确定片元最终是否被渲染,并且确定片元的最

终渲染颜色效果等

在光栅化阶段中,我们主要通过自定义 片元着色器 阶段

为我们带来一些不同的表现效果

第2节: Shader开发是什么

Shader开发是什么

Shader 是什么

Shader的中文意思是 着色器

是一种用于描述如何渲染图形和计算图形外观的程序

主要用于控制图形的颜色、光照、纹理和其他视觉效果

着色器通常由着色器语言编写,这些着色器语言提供了指令和语法

用于编写描述光照、纹理映射、阴影、反射等图形外观的代码

总的来说:Shader就是着色器,是用于编写图形表现效果的程序代码

而Shader开发本质就是对

渲染管线(流水线)中上一阶段传递过来的

数据 进行自定义处理后

再传递给下一阶段

通过自定义处理,让图形图像最终能够以我们想要的方式显示到屏幕上

Shader 和 渲染管线 的关系

渲染管线(流水线)的基本概念是将数据分阶段的变为屏幕图像的过程

而Shader开发就是针对其中某些阶段的自定义开发

从而决定图形图像最终呈现到屏幕上的表现效果

在Untiy中,我们学习的Shader开发主要针对渲染管线中的两个小阶段的

  • 几何阶段 —— 顶点着色器 小阶段

  • 光栅化阶段 —— 片元着色器 小阶段

Shader开发的本质

通过对渲染管线中的数据进行自定义处理来决定最终的渲染效果

简而言之

通过Shader代码来处理渲染数据

第3节: 其它相关必备概念

计算机图形程序接口

概述

计算机图形程序接口(Graphics API)对于游戏开发程序员来说是非常重要的知识,是学习图形学时必不可少的内容。

计算机图形程序接口是一套可编程的开放标准,不管做2D还是3D游戏都需要这部分的底层API支持。

它本质上是软件,并不是硬件,是前辈们提前为你写好的调用系统硬件(GPU)绘制图形的代码。我们甚至可以把它简单理解成是显卡厂商定义的一系列的底层的进行图形操作的加速API接口。

由于目前各种游戏引擎的出现,即使你没有系统的学习过图形学相关知识,也能够独立的通过游戏引擎开发游戏。但是你必须知道的是,游戏引擎的一部分本质就是对图形程序接口的封装,游戏引擎通过图形程序接口帮助我们完成了图像渲染相关的工作,我们只需要把工作重心放在游戏逻辑开发上了。

因此,计算机图形程序接口对于游戏开发来说,是非常重要的内容,即使你现在还没有时间,或者没有需求学习计算机图形学,你也必须对他们有一定的认识。

必了解的图形程序接口

  • OpenGL(Open Graphics Library)

中文翻译过来是开放图形库,它定义了一个跨平台、跨语言的编程接口规格的专业图形程序接口,可以用于3D、2D图形渲染,是一个功能强大、调用方便的底层图形库。由于它跨平台、跨语言、出现时间早,因此它的应用极其广泛!

  • OpenGL ES(OpenGL for Embedded Systems)

中文翻译过来是用于嵌入式系统的开放图形库,它是OpenGL的子级,主要针对手机、游戏主机等嵌入式设备而设计,免授权费、跨平台、功能完善。

GLES2.0、GLES3.0 指的就是OpenGL ES这套标准,他们也是Android和IOS手机上常用的图形处理标准。

Unity在移动平台进行图形渲染处理时,就包含了OpenGL ES方案

  • Vulkan

“下一代”开放的图形显示API,是与DX12能够匹敌的GPU API标准。它有一套最新的图形加速API接口,目标是提供更灵活和丰富的底层操作接口,以替代OpenGL 和 OpenGL ES接口,可以把Vulkan看做是OpenGL的升级版,目前新版本的Unity支持使用Vulkan方案。

  • Directx(Direct eXtension)

中文翻译过来是直接拓展,简称DX。它是由微软公司创建的多媒体编程接口。它不跨平台,只针对微软的相关产品,被广泛使用于Windows操作系统、xBox游戏主机的图形应用程序开发中。

其中的D3D算是DX一部分,是对标OpenGL的图形程序接口

  • WebGL(Web Graphics Library)

中文翻译过来是网页图形库,它是针对Web端(网页)的3D绘图协议,这个标准允许把JavaScript和OpenGL ES 2.0结合在一起,网页开发人员可以借助系统显卡在浏览器里流畅的展示3D场景和模型,可以在网页里进行3D图形开发。

  • Metal

中文翻译过来是金属,它是苹果公司为游戏开发者提供的图形技术,该技术能够为3D图像提高10倍渲染性能,但是它不支持跨平台,主要针对IOS、macOS苹果自家的操作系统,只有苹果手机、电脑能够使用。

对于我们的意义

了解这些图形程序接口的基本概念对我们有什么意义呢?

我们从他们的简单介绍中需要知道,他们主要支持的平台为

  • Windows电脑:DX、OpenGL、Vulkan

  • 苹果电脑:Metal、OpenGL、Vulkan

  • 安卓手机:OpenGL ES、Vulkan

  • 苹果手机:OpenGL ES、Vulkan、Metal

  • 网站网页:Web GL

如果不通过游戏引擎,我们想要在这些平台上开发游戏,那么就必须要针对不同平台学习对应的图形程序接口相关的知识。你会发现OpenGL的身影在各主流平台中都占有一席之地,由于它出现早、跨平台、跨语言,所以也是为什么在学习计算机图形学时,OpenGL是必学的内容。

我们现在使用游戏引擎开发游戏,在绝大多数情况下,不需要直接和图形程序接口打交道,但是我们在Unity当中发布应用程序时,经常会看到和他们相关的一些设置,你必须要了解了他们的基本概念,才能清楚我们在设置什么。

因此本篇文章的主要作用就是让你了解他们,并且可以为你指明学习计算机图形学时的一些学习方向。

你还需要知道的是,这些图形程序接口还有不同的版本,比如DX10、DX11、DX12,比如OpenGL ES 2.0、OpenGL ES 3.0。不同的设备、不同的操作系统他们支持的版本也是不同的。比如我们进行手机游戏开发时,我们可以去查询主流移动设备支持的图形程序接口的版本,来决定在发布时,对于图形程序接口版本的兼容选择。这样才能保证我们发布的应用程序能够支持更多的移动设备。

总结

对于Unity来说,它针对你发布的不同平台,会进行图形程序接口方案的切换

比如:

1. 发布Windows应用时,使用DX方案

2. 发布苹果电脑应用时,使用Metal方案

3. 发布移动平台应用时,使用OpenGL ES 或 Vulkan方案

4. 发布网页应用时,使用WebGL方案

我们不需要掌握对应图形程序接口方案的相关知识,也能利用Unity进行游戏开发,因为引擎已经把图形渲染的核心内容封装起来,不需要我们直接操作他们。

但是长远考虑,随着你的能力提升,计算机图形学还是游戏开发程序员的必学知识,他可以帮助你理解渲染相关的原理

Shader开发相关必备概念

渲染管线和图形接口程序的关系

图形接口程序(OpenGL、DX等)主要是用于控制和管理渲染管线流程的

通过图形接口程序提供的API,我们就可以配置和操作渲染管线中的某些阶段

设置输入数据、控制图形处理、应用各种渲染效果,最终实现图形渲染和呈现。

图形接口程序充当了开发者和图形硬件之间的中间层,将开发者的渲染命令和设

置转化为硬件能够理解和执行的指令

总体而言:

图形接口程序(OpenGL、DX等)提供了对

渲染管线(流水线)的控制和管理功能,它是开发者和硬件打交道的中间层

Shader和图形接口程序的关系

Shader(着色器)是一种小型程序,用于自定义渲染数据的处理,从而决定最

终的渲染效果。

图形接口程序(OpenGL、DX等)为Shader开发提供了各种API,Shader开发

需要针对不同的图形接口程序使用不同的Shader开发语言来调用相关API。

图形接口程序会将Shader程序和渲染管线的各个阶段连接起来,它会把我们的

数据和指令传递给硬件(GPU等),从而实现图形渲染的最终呈现。

总体而言:Shader属于图形接口程序(OpenGL、DX等)的一部分

不同图形接口程序对Shader开发的影响

使用的着色器语言不同

OpenGL:GLSL(OpenGL Shading Language)

DX: HLSL(High-Level Shading Language)

Metal: MSL(Metal Shading Language)

WebGL: GLSL ES(OpenGL ES Shading Language)

坐标系原点不同

OpenGL、WebGL、Metal: 原点位于屏幕左下角

DX:原点位于屏幕左上角(注意:最新的DX12可以改为左下角原点)

总结

1. 渲染管线(流水线)和图形接口程序的关系

图形接口程序(OpenGL、DX等)提供了对

渲染管线(流水线)的控制和管理功能,它是开发者和硬件打交道的中间层

2. Shader和图形接口程序的关系

Shader属于图形接口程序的一部分

3. 不同图形接口程序对Shader开发的影响

开发语言不同、坐标系原点不同

第2章: Shader必备基础

第1节: 数学基础——基础数学知识

本节与文章的内容Unity基础重合,请移步去学习:

Unity基础 - 张先生的小屋 (klned.com)

主要需要掌握的知识:

  • Mathf Api

  • 三角函数

  • 向量

第2节: 数学基础——平移缩放旋转变换

矩阵的基本概念

线性代数是什么

线性代数是数学的一个分支学科

线性代数是数学中研究线性方程和线性函数及其表达通过矩阵和向量空间的学科。

而计算机图形学里面,我们主要强调其矩阵和向量空间有关的内容。

我们可以把向量想象成有大小和方向的箭头,线性代数主要研究如何使用这些箭

头进行加减乘除运算,以及它们之间的变换规则。

简而言之:线性代数是一门研究向量和变换的数学学科

矩阵是什么

矩阵(Matrix)是线性代数中的一个核心概念和重要工具

通过矩阵,我们可以方便的进行向量的相关计算

也可以更好的理解和解决线性代数中的各种问题

简而言之:

矩阵是一种用来表示和处理数据的数学工具

它可以帮助我们有效的管理和计算大量的数据

矩阵的表示

根据矩阵的结构是由 m x n 个标量组成

写成:

Matrix =

M11 ⋯ 𝑀1𝑛

[ ⋮ ⋱ ⋮ ]

𝑀m1 ⋯ 𝑀m𝑛

那么在程序中,我们用于存储矩阵结构的容器类型有很多选择,最常见的的为:

1.数组(一维、二维都可以)

2.嵌套列表(两个List嵌套)

3.开发工具提供的类或结构体(Unity中的Matrix4x4、Matrix3x2结构体)

为什么要学习矩阵

我们刚才学习的概念知识,知道了矩阵是线性代数中的核心工具

它可以用来进行向量相关的计算。

我们在进行Shader开发时,进行的很多数学计算需要利用矩阵来完成

比如:

坐标系转换、投影计算、光照计算、纹理映射等等

简而言之:

学习矩阵的目的,就是为了能在Shader开发中利用其进行相关数学计算

矩阵乘法

矩阵和标量的乘法

矩阵(M)和标量(k)的乘法很简单

直接让矩阵(M)中的每一个标量和标量(k)相乘即可

矩阵和矩阵的乘法

1. 首先需要判断两个矩阵是否能够相乘

判断条件:左列右行要相等

2. 相乘得到的矩阵结构是定死的规则

结果结构:左行右列

3. 标量相乘的规则:左行乘右列再相加

矩阵和矩阵的乘法的注意事项

1.矩阵之间的乘法 不满足交换律

AB ≠ BA

2.矩阵之间的乘法 满足结合律

(AB)C = A(BC)

ABCDE = (AB)(CD)E = A((BC)D)E

总结

1.矩阵和标量的乘法

直接让矩阵(M)中的每一个标量和标量(k)相乘即可

2.矩阵和矩阵的乘法

判断条件: 左列右行要相等才能相乘

结果矩阵结构: 左行右列

标量相乘的规则: 左行乘右列再相加

不满足交换律,满足结合律

特殊矩阵——五个基本特殊矩阵

方块矩阵

方块矩阵简称方阵

它的特点是行列数相等

比如 3x3、4x4的矩阵都可以称为方阵

对角矩阵

对角矩阵是一种特殊的方阵

它是 只有主对角线有值,其余元素全为零的 方阵

1 0 0

0 2 0

0 0 3

注意:矩阵中的主对角线就是指 从左上角到右下角的对角线

单位矩阵

单位矩阵是一种特殊的对角矩阵

它是 主对角线上的元素均为1 的 对角矩阵

1 0 0

0 1 0

0 0 1

注意:任何矩阵(M)和单位矩阵(I)相乘的结果都是原来的矩阵(前提是满足相乘的规则)

MI = IM = M

数量矩阵

数量矩阵是一种特殊的对角矩阵

它是 主对角线上的元素为同一值 的 对角矩阵

9.5 0 0

0 9.5 0

0 0 9.5

转置矩阵

转置矩阵是将原始矩阵的行和列互换得到的新矩阵

假设矩阵为 M ,那 M 的转置矩阵一般写为 MT

转置矩阵对于我们来说有一些常用的性质

1. 矩阵转置的转置等于原矩阵

(MT)T = M

2. 矩阵串接的转置,等于反向串接各个矩阵的转置

(AB)T =BT𝐴T

特殊矩阵——逆矩阵

介绍

逆矩阵是所有特殊矩阵当中计算最复杂的矩阵

它的特点是 逆矩阵是必须是一个方阵,并且不是所有矩阵都有逆矩阵

假设 一个方阵 M , 它的逆矩阵用 M−1 表示

那么 MM−1 = M−1M = I(单位矩阵)

如果一个矩阵存在对应的逆矩阵,我们就说该矩阵是可逆的(或称非奇异的)

如果不存在,那么该矩阵为不可逆的(或称奇异的)

逆矩阵的计算

  1. 确定矩阵为方阵(即行列数相等)

  2. 计算矩阵的行列式(若行列式值为零,则该矩阵没有逆矩阵)

  3. 计算矩阵的代数余子式矩阵

  4. 计算标准伴随矩阵(转置代数余子式矩阵)

  5. 计算逆矩阵(标准伴随矩阵 / 行列式)

计算过程总体较为复杂,请自寻查找学习

逆矩阵的重要性质

1. 逆矩阵的逆矩阵是原矩阵本身

(M-1)-1 = M

2. 矩阵乘以自己的逆矩阵为单位矩阵

MM-1 = M-1M = I

3. .单位矩阵的逆矩阵是它本身

I-1= I

4. 转置矩阵的逆矩阵是逆矩阵的转置

(MT)-1= (M-1)T

5. 矩阵串接相乘后的逆矩阵 等于 反向串接各个矩阵的逆矩阵 相乘

(AB)-1 = B-1A-1

(ABCD)-1 = D-1C-1B-1A-1

6.逆矩阵可以计算矩阵变换的反向变换(M为矩阵,v为一个矢量)

M-1(Mv) = (M-1M)v = Iv = v

特殊矩阵——正交矩阵

介绍

正交矩阵是一种特殊的方阵,正交的意思是垂直

它的特点是:一个方阵和它的转置矩阵相乘为单位矩阵,那么它就是正交矩阵

MMT = MTM = 𝐼

通过正交矩阵的这一性质,再根据上节课学习的逆矩阵的一个重要性质

MM−1 = M−1M = 𝐼

我们可以推导出:如果一个矩阵是正交的,那么它的逆矩阵等于其转置矩阵

MT = M−1

如果一个矩阵是正交矩阵,那么它的转置矩阵也是正交矩阵

名字的由来

诞生于向量点积的基本概念和矩阵理论的深入研究,正交矩阵的名称源自于其行和列向量的点积表现出的正交性和单位长度特性。这种矩阵在数学中的作用不仅是体现向量间的正交关系,还在于其在描述线性变换,如旋转和反射时的独特能力,同时保持向量的长度和夹角不变。正交矩阵的命名强调了点积在其定义和应用中的核心作用,展示了数学将基础概念融入更高级结构以解决复杂问题的能力。

你可以理解为,这个矩阵诞生于向量,与向量的绑定较深,所以我们可以使用点乘来审视里面的计算性质。

结合向量点乘来看待正交矩阵

请注意,当你使用点乘来理解正交矩阵的时候,请把矩阵里的每行每列矩阵直接视为向量。

如何判断正交矩阵

根据正交矩阵的基本概念,我们可以总结出判断一个矩阵是否是正交矩阵的方式有:

1. 判断MMT = MTM = 𝐼 ,满足则为正交矩阵

2. 判断矩阵的每一行(列)是否是 单位向量

n 维向量 v = (v₁, v₂, ..., vₙ), 向量模长|v| = sqrt(v₁² + v₂² + ... + vₙ²) == 1

同时

判断矩阵的行(列)向量是否彼此正交(垂直)

v*u = 0

满足则为正交矩阵

正交矩阵的重要性质

如果矩阵 M 为正交矩阵

1. 𝑴𝑻 也为正交矩阵

2. 𝑴𝑻 = 𝑴−𝟏

3. 正交矩阵的行列式为1或-1

4. 正交矩阵的 各行都是单位向量且彼此正交(垂直)

5. 正交矩阵的 各列都是单位向量且彼此正交(垂直)

特殊矩阵——行矩阵,列矩阵

列矩阵和行矩阵的重要概念

在Unity Shader开发中我们经常会对向量进行矩阵运算

而在Unity的三维坐标系中

向量一般都是三维向量(x,y,z),由于要进行矩阵计算,我们就需要把向量用矩

阵表示。

通过我们之前对矩阵的学习,三维向量的矩阵表示,可以有两种:

  1. 列矩阵:只有一列的矩阵

  2. 行矩阵:只有一行的矩阵

但是在进行向量的矩阵运算时,把它作为列矩阵和行矩阵得到的运算结果是不同

我们都知道一个项目里同一个解决方法每次开发的算法不统一,对维护而言是致命的,为此我们在Unity里面使用的适合,得制定一个规则来方便使用。

列矩阵和行矩阵在Unity中的使用规则

在标准的线性代数中,矩阵和向量的乘法是按列进行的。

在Unity 的 Shader开发中,为了和标准的数学和线性代数概念保持一致,更加

便于理解和应用,它也遵循这种矩阵乘法的定义,也就是:

将向量表示为列矩阵进行计算

使用列矩阵的结果是,我们的阅读顺序是从右到左的,

假设向量为列矩阵v,A、B、C分别为3个变换矩阵

CBAv = C(B(Av)) (矩阵乘法满足结合律)

即先对 向量v使用A进行变换,再依次使用B、C进行变换

注意:如果想把向量作为行矩阵处理,也是可以的

为了让行矩阵计算结果和列矩阵相同,我们可以采用以下规则

列矩阵: CBAv = C(B(Av)) 等价于

行矩阵: v𝐴𝑇𝐵𝑇𝐶𝑇 = ((v𝐴𝑇)𝐵𝑇)𝐶𝑇

行矩阵必须乘以变换矩阵的转置矩阵才能保证计算结果和列矩阵结果一致

矩阵的几何意义

矩阵的几何意义

点和向量能在图像中画出来,那么矩阵可以吗?

答案是肯定的,矩阵的可视化结果就是:变换

在游戏开发中,如果你看到了一个矩阵,那么基本上你可以认为你看到的是一个

变换,这些变换一般包含:平移、旋转、缩放

比如:

我们想要将一个点、一个向量进行一种变换(平移、旋转、缩放)

那么我们可以利用矩阵来进行数学计算,从而达到变换的目的

变换的种类

矩阵的几何意思是变换,在数学中常用的几何变换有两种:

线性变换 和 仿射变换

他们的主要区别在于是否保持直线的平行性和原点位置

线性变换:保持直线的平行性和原点位置不变,比如缩放、旋转等操作

简而意之就是只对向量进行旋转、缩放等操作,而不影响方向和原点的位置

仿射变换:由一个线性变换和一个平移组成,比如缩放后、旋转后再平移

简而言之就是缩放后、旋转后平移,会改变原点的位置

对于我们的意义

我们可以利用我们之前学习的矩阵相关知识

对三维空间中的向量进行平移、旋转、缩放、坐标变换、投影 等等计算

这样我们就可以对Shader中的数据进行处理

让其最终在屏幕上的效果是按照我们的需求来呈现的

因此我们后面几节课将着重来学习如何利用矩阵进行

平移、旋转、缩放等等计算

齐次坐标

齐次坐标是什么

齐次坐标是一种在计算机图形学中常用的表示坐标的方式。

它是通过引入一个额外的维度来扩展传统的笛卡尔坐标系,

就是将一个原本是 n 维的向量或矩阵用 n + 1维来表示,

让我们可以更方便的进行几何变换和矩阵运算。

举例:

三维空间中有一个向量或点 ( x , y , z),它对应的齐次坐标就是给它加一维,变成

(x , y , z , w),其中 w 值的改变可以让它有具有不同的含义。

为什么要使用其次坐标进行矩阵运算

  • 主要原因1:

通过齐次坐标,我们可以明确的区分向量和点

刚才的例子讲解:

三维空间中的 ( x , y , z) ,它既可以表示点,也可以表示向量。

那么我们可以利用齐次坐标给它加一维,变成

(x , y , z , w),其中 w = 1 时 代表是一个点,w = 0 时 代表一个向量。

这样我们就可以明确它是点还是向量了

  • 主要原因2:

3x3矩阵是不能直接表示平移变换(具体为什么不能,我们之后讲解平移变换时就知道答案了)

它只能表示线性变换,也就是只能描述对象的旋转、缩放等线性变换,而不能描述对象的平移。

平移涉及到改变对象在空间中的位置,包括移动对象的原点。因此,我们需要引入一个额外的维度

来表示平移操作,所以我们使用齐次坐标来将3x3矩阵加一个维度变为4x4的矩阵

3x3矩阵一般称为线性矩阵,主要处理线性变换(主要进行旋转、缩放等线性变换)

4x4矩阵一般称为仿射矩阵,主要处理仿射变换(线性变换 + 平移变换)

平移矩阵

基础变换矩阵的构成规则

通过上节课齐次坐标的学习,我们知道:

4x4的矩阵为仿射矩阵

它不仅可以表示出线性变换(缩放、旋转等),还可以表示出平移变换。

因此,我们将要学习的平移、缩放、旋转相关的变换

将会使用4x4的矩阵来进行计算

请注意,严格来说,旋转、缩放这类线性变换用3X3即可实现,用4X4只是为了统一性。

基础变换矩阵的构成规则

平移矩阵的计算

平移矩阵是否是正交矩阵

可惜的是,肉眼可见,平移矩阵并不是正交矩阵

因此,我们无法直接通过转置获取它的逆矩阵来将已经变换的矩阵逆向变换回去。

不过令人庆幸的是,它的逆矩阵是固定而且简单的:

旋转矩阵

旋转矩阵的构成

旋转操作需要指定一个旋转轴(不一定是空间中的坐标轴)

我们这节课中的旋转矩阵是指绕着 空间中的x轴、y轴、z轴进行旋转时的变换矩阵

他们分别是

旋转矩阵的计算

需要注意的是,旋转矩阵主要是由基础变换矩阵的构成规则当中的 3x3 矩阵决定的

因此,平移部分的3x1矩阵都为0,并不影响计算,

所以点(w=1)和向量(w=0)与旋转矩阵进行计算都会发生改变。

几何意义就是 点 或 向量 围绕某一个轴进行旋转,得到一个新的 点 和 向量。

因此旋转矩阵的计算就是直接和表示向量或点的列矩阵进行乘法运算即可

得到的结果就是旋转后的结果

旋转矩阵是否是正交矩阵

旋转矩阵是正交矩阵(通过正交矩阵的判断方式可以得到该结论)

因此x、y、z 轴的旋转矩阵的逆矩阵是它们的转置矩阵

我们可以利用旋转矩阵的逆矩阵来进行还原旋转

假设P点绕某个轴的旋转矩阵R进行了旋转变换,得到了P’,如果我们想将P’还原为P,则只需要用R

的转置矩阵乘以P’即可得到结果P

缩放矩阵

缩放矩阵的构成

缩放矩阵主要就是对向量或点进行缩放操作,它可以用于改变向量或点再各个坐标轴上的尺度

使其在每个方向上变大或变小,在三维空间中,它主要由x、y、z轴的缩放因子构成。

缩放矩阵的构成为以下矩阵结构:(kx、ky、kz分别代表x、y、z轴的缩放因子)

缩放矩阵的计算

需要注意的是,缩放矩阵主要是由基础变换矩阵的构成规则当中的 3x3 矩阵决定的

因此,平移部分的3x1矩阵都为0,并不影响计算,

所以点(w=1)和向量(w=0)与缩放矩阵进行计算都会发生改变。

几何意义是 :

对点的缩放(一般是构成模型的顶点),相当于就是在缩放模型大小。

对向量的缩放,统一缩放时只会改变向量的大小(模长),不会改变向量的方向

非统一缩放时不仅会改变大小,可能还会改变向量的方向

因此缩放矩阵的计算就是直接和表示向量或点的列矩阵进行乘法运算即可

得到的结果就是缩放后的结果

缩放矩阵是否是正交矩阵

缩放矩阵通常不是正交矩阵

为什么说通常不是呢,因为对于缩放矩阵来说kx、ky、kz一般都不会是1,是1后就会是单位矩阵了

那只要不是1,就不能满足正交矩阵的条件(行、列向量为单位向量),因此大多数情况下,它是

不具备正交矩阵的特性的,所以缩放矩阵的逆矩阵我们需要通过计算得出。

不过好在,它的逆矩阵依然是有固定规律的。

复合运算

什么是复合运算

所谓的复合运算,其实就是我们在计算矩阵变换时,可以把平移、旋转、缩放等计算组合起来,

通过结合多矩阵的乘法,来形成一个复杂的变换过程。

比如:

我们可以将一个模型

先缩放到2倍大小,再绕y轴旋转60°,最后再向x轴平移5个单位。

这种复合变换过程,我们可以通过矩阵的串联来实现。

我们之前的知识点讲过,我们通过列矩阵的形式进行计算时,利用乘法结合律

可以从右往左去计算:先缩放、后旋转、再平移。

P= M平移M旋转M缩放P

简而意之:复合运算就是把各种矩阵变换组合起来,形成一个复杂的变换过程

计算顺序对结果的影响

在进行复合运算时,变换的结果依赖于变换的顺序

原因:

矩阵乘法不满足交换律

也就是说,不同的变换顺序得到的计算结果也可能是不一样的。

比如:

一个人先往前一步,再左转

一个人先左转,再往前一步

得到的结果是不一样的,在矩阵运算的乘法运算中也会出现这样的情况

主要原因,就是因为矩阵乘法不支持交换律

Unity中需要遵守的规则

1. 在进行平移、旋转、缩放的复合运算时

绝大多数情况下,我们约定的变换顺序为:先缩放、再旋转、后平移

2. 在进行x轴、y轴、z轴旋转的复合运算时

绝大多数情况下,我们约定的变换顺序为:z->x->y

之后我们在Unity中进行Shader开发时,遵从这两个规则即可。

第3节: 数学基础——坐标空间的变换

坐标空间的变换

坐标空间是什么

坐标空间是一个用于描述和定位物体位置的数学概念。

它一般由一个基础参照物(原点)和轴线(相互垂直的轴线)组成。

常见的坐标空间包括二维平面坐标空间和三维空间

在三维空间中,通常决定一个原点(0,0,0)并

使用三个坐标轴(通常是x轴、y轴和z轴)来描述点的位置。

例如,在三维坐标空间中,一个点可以通过其x、y和z坐标来确定其在空间中的位置。

为什么有很多不同的坐标空间

我们在Unity基础中讲解的坐标系相关知识中提到了:

世界坐标系

物体坐标系

屏幕坐标系

等等

在渲染管线中,顶点、法线等相关模型数据会经过以下的空间变换

模型空间 →世界空间 →观察空间 →裁剪空间 →屏幕空间

这些就是各种不同的坐标空间,之所以存在这么多不同的坐标空间,主要是因为

不同的问题需要不同的坐标系来描述和解决特定的空间问题,帮助我们更方便的完成需求

坐标空间的变换

在Shader开发中为了方便我们制作模型,使用模型,渲染模型,也存在很多不同的坐标空间

比如:模型空间、世界空间、观察空间、裁剪空间、屏幕空间

我们这里的坐标空间的变换主要是指

在渲染管线中的,我们需要将坐标数据,在这几种空间当中进行变换计算(利用矩阵相关知识)

举例:

在设计模型时,使用的是模型空间(所有的顶点、法线等等相关数据都是基于模型空间坐标系的)

当我们将模型导入到Unity后,最终能够被我们在屏幕上看到,这里面的就经历了我们看不到的坐

标空间变换

从 模型空间 →世界空间 →观察空间 →裁剪空间 →屏幕空间

因此在这一阶段,我们主要学习的就是如何进行坐标空间的变换

坐标空间的变换规则

坐标空间的组成

想要定义一个坐标空间

我们必须要具备以下两点:

  1. 坐标原点位置

  2. 3个坐标轴的方向(三维坐标系中)

有了这两点信息,我们就能够决定一个坐标空间了

坐标空间之间的关系

在Unity中,世界坐标空间相当于我们的基础坐标空间,

Unity中其他的大部分坐标空间,都是世界坐标空间的子坐标空间。

这些子坐标空间的原点和轴向的相关表示数据,都是基于世界坐标空间的。

因此,在Unity中坐标空间之间会形成一种层级结构,大部分坐标空间都是另一个坐标

空间的子空间。

所以,Unity中的坐标空间的变换实际上就是父空间和子空间之间对点或向量进行变换。

如何判断坐标系之间的父子关系

虽然我们是针对两个坐标系的变换,但是在变换公式的构建的时候,我们是需要确定一个唯一的负责度量的坐标系,它得确定我们的转换目标坐标系的三条轴,来构建变换公式的。

而在两个坐标系中,被我们用来度量方位的坐标系就是父坐标系

同时,子坐标系通常是我们用来构建变换公式的三条轴和点的所属对象。

所以:

如果是子坐标系变换为父坐标系,可以直接套用变换公式。

如果是父坐标系变换为子坐标系,需要对变换公式取逆。

下一小结会更详细的说明这方面的知识。

坐标空间的变换矩阵

我们现在假设一个父坐标空间为F,子坐标空间为S。

我们已知 S 坐标空间的原点位置和3个单位坐标轴(基于F坐标空间的数据表达)

对于坐标空间转换我们一般会有以下两种需求:

  1. 把子坐标空间 S下的点或向量 As转换到父坐标空间 F中为 Af

  2. 把父坐标空间 F下的点或向量 Bf转换到子坐标空间 S中为 Bs

如果用矩阵来表示的话:

  1. Af = Ms-f As —— Ms-f 代表从子坐标空间到父坐标空间的变换矩阵。

  2. Bs = Mf-sBf —— Mf-s 是 Ms-f 的逆矩阵(我们之前学习过逆矩阵可以进行逆向变换),代表父到子的变换矩阵。

除了直接套用上文的坐标空间变换固定公式之外,还可以直接通过移动目标坐标系到当前坐标系的思维,直接通过基础的变换公式实现坐标系变换,这也是上文公式的推导来源:

规律如下

  1. 子->父:先缩放,再旋转,最后平移。

  2. 父->子:先平移,再旋转,最后缩放。

坐标系变换的过程和结果视角

  1. 过程视角

    • 描述位移操作时,特别是涉及到父坐标系到子坐标系的时候,我们通常会看到某些文献,以变换前的坐标系为参考。比如世界坐标系到观察坐标系,有可能会被描述为:将世界坐标系移动到模型坐标系原点的说法,这是从初始坐标系即世界坐标系的视角出发的。

    • 这种描述方式可能导致理解上的混淆,因为它没有明确表示变换后的新参考框架。

  2. 结果视角

    • 实际上,这种位移类型的表达效果是在新的坐标系中表达的,这实质上代表了从原始坐标系到目标坐标系的切换。

    • 例如,将世界中心移动到观察空间原点的操作,实质上是将坐标系从世界坐标系切换到观察坐标系的视角。

我需要强调的是,上面的情况更多发生在父坐标到子坐标

父坐标系到子坐标系的变换:

当我们从父坐标系变换到子坐标系时,通常涉及的是一种“靠近”的操作。在这种情况下,可以想象为将观察点(或子坐标系)移动到父坐标的原点,以便实现从子坐标系的视角观察世界。这种变换可以通过平移和旋转来实现,使得子坐标系的原点和方向与特定的目标或对象对齐。因此,从过程视角描述这种变换时,使用“移动”或“平移”来描述是比较直观的。

子坐标系到父坐标系的变换:

相反,从子坐标系变换回父坐标系时,概念上更接近于一种“远离”的操作,尤其是在直观上理解时。这是因为这种变换实质上是在撤销之前应用的平移和旋转,以恢复到全局视角或父坐标系的视角。在这种情况下,描述变换为“平移”可能会导致一些混淆,因为实际上我们是在进行相反的操作,即从子坐标系的局部视角返回到一个更广阔的、全局的父坐标系视角。

但是有趣的是,几何想象上看起来更直观的父到子,使用矩阵变换固定公式的时候常常更加复杂,因为需要逆运算,这也很好理解,因为我们的公式基于子到父坐标,这本质上类似一种平移的“撤销操作”。

模型空间变换

模型空间的意义

模型空间(model space)也被成为

对象空间(object space)或局部空间(local space)

它一般指3D模型的局部坐标系,每个模型都有自己独立的坐标空间,

模型空间的主要意义是方便我们建模,模型的顶点等数据都是基于

模型空间表达的。

注意:

在Unity中当模型移动或旋转时,模型空间坐标系也会随着变换,

大部分情况下,模型空间的Z轴始终朝向模型的正前方。

模型空间中的注意事项

在模型空间中,我们一般会有上、下、左、右、前、后六种方向概念

Unity使用的是左手坐标系

因此模型空间的x、y、z轴,对应的是模型的右、上、前三个方向。

需要注意的是在不同的软件中,比如3DMax和Maya中

模型空间中的xyz不见得是上面这种关系,

因此在开发时,需要让美术同学导出模型时,修改相关设置,

让模型导出后能够满足Unity中的规范

模型空间变换指什么

图形学的模型空间变换指的主要是将模型空间中的点或向量通过

矩阵乘法计算,变换为相对于世界坐标空间下数据。

渲染管线是将数据分阶段的变为屏幕图像的过程

其中在几何阶段以及光栅化阶段中,我们需要将顶点等数据进

行相关的变换,让其最终的数据能够显示在屏幕上。

而模型空间变换就是其中一个重要的变换步骤

就是将模型空间下的点和向量数据转换到世界空间下进行表示

如何进行模型空间变换

模型空间变换的顺序是:物体坐标系->世界坐标系。是典型的子->父的坐标系变换。

所以,有两种方法

1.直接套用坐标系变换公式。

2.根据点所在的对象的面板,先缩放,再旋转,后平移,实现物体坐标系转移到世界坐标系原点的效果。

关于第二点,具体的说:

模型空间变换的变化规则就是:

模型空间下的点或向量相对于世界空间下的数据表达 =

平移矩阵 旋转矩阵 缩放矩阵 * 模型空间下的点或向量

其中平移、旋转、缩放矩阵中的具体变换值都是相对于世界空间下的数据

如下:

如果有多个父对象的话,对于每个父对象,它的坐标都是子对象的父坐标,直接一层层往上计算即可

观察空间变换

观察空间的意义

观察空间(view space)也被成为

摄像机空间(camera space)

观察空间可以认为是一个特殊的模型空间,这里的模型指的是场景中的

摄像机,摄像机可以认为是一个非常特殊的模型,它不可见,但是它可以

决定我们在屏幕上看到的内容,因此我们将摄像机的模型空间单独提出来

讨论和学习,并将它称为观察空间。

观察空间的主要意义是摄像机决定了渲染的视角和视野

观察空间中的注意事项

在模型空间中,我们讲过模型空间的x、y、z轴,对应的是模型的右、上、前三个方向。

是因为Unity中的模型空间遵循左手坐标系原则。

但是在Unity的观察空间中,观察空间是遵循右手坐标系原则的,因此它的坐标轴方向有所不同。

观察空间中的x、y、z轴的正方向 分别对应摄像机的 右、上、后方。

主要原因:

在计算机图形学的OpenGL中,为了统一处理场景中物体的渲染和投影

通常使用右手坐标系,为了方便之后的数据处理,因此在Unity当中

的观察空间,也遵循右手坐标系

观察空间变换指什么

本课程中的观察空间变换指的主要是将模型空间中的点或向量从世界空间中变换到观察空间中。

它是顶点变换的第二步,就是将 数据 从 世界空间—>观察空间 进行变换

观察空间变换也可以称为观察变换(view transform)

如何进行观察空间变换

从世界空间——>观察空间,是父->子的变换过程

所以,采取上节模型空间变换的逆运算即可。

依然是两种方法:

1.直接套用坐标系变换公式,随后取逆矩阵,相乘即可。

2.根据点所在的对象的面板,先平移,再旋转,后缩放,实现观察坐标系转移到世界坐标系原点的效果。(请注意这里的变换顺序是不同)

请注意点二,本质上,我们都可以理解为切换坐标轴就是为了将子坐标轴移动到父坐标轴的原点的过程,这样子坐标轴就会变成新的世界坐标轴,点的视角自然也被了切换过去。

齐次裁剪空间——导入

视锥体

在渲染管线中,顶点、法线等相关模型数据会经过以下的空间变换

模型空间 →世界空间 →观察空间 →裁剪空间 →屏幕空间

通过之前的学习,我们已经可以将相关数据在 模型空间 →世界空间 →观察空间 之间进行变换

我们知道观察空间也被称为摄像机空间,此时获取到的顶点等数据都是基于摄像机空间中的数

据表达,而摄像机中有一个非常重要的概念,就是视锥体。

摄像机的视锥体是在三维空间中表示摄像机可见区域的虚拟体积,它类似一个六面体的形状,

根据摄像机的属性和投影方式而定。

视锥体定义了摄像机在场景中能够看到的物体区域,超出这个区域的物体将在渲染时被裁减掉,

从而提高渲染性能。

视锥体主要包含几种重要部分:

远近裁剪平面

左、右、上、下裁剪平面

  • 透视投影中,视锥体类似一个金字塔形状,远裁剪面比近裁剪面大,所以产生透视效果(因为大部分点会被映射到裁剪空间里面,有一个压缩偏移坐标的过程。)

  • 正交投影中,视锥体类似于长方体的形状,远近裁剪平面大小一致,不会产生透视效果(因为所有点都不需要压缩偏移坐标。)

之前提到,我们希望根据视锥体围成的区域对顶点等数据进行裁剪,超出视锥体这个范围的坐标在渲染时会被裁减掉,只保留视锥体范围内的坐标。

但是,如果直接使用视锥体定义的空间来进行裁剪,那不同的视锥体就需要不同的处理过程,而且对于透视投影的视锥体来说,判断顶点是否在范围内相对较麻烦。

因此,我们希望用更通用统一、便捷的方式来进行裁剪工作,就需要将观察空间(摄像机空间)中的数据转换到齐次裁剪空间中

齐次裁剪空间

裁剪空间也被称为 齐次裁剪空间

齐次裁剪空间 (裁剪空间)是一个非常特殊的坐标空间

齐次裁剪空间 是一个三维空间,是在计算机图形学中用于在图形渲染过程中进行裁剪和投影的。

它的坐标范围为(-1,-1,-1)到(1,1,1),超出这个范围的坐标在渲染时会被裁减掉,只会保留范围

内的坐标。

齐次裁剪空间是通过将摄像机的视锥体投影到一个规范化的立方体而转换来的。

这个立方体就是齐次裁剪空间。

裁剪空间变换 —— 正交投影变换

正交投影变换目的

将摄像机视锥体的 正交投影 空间(观察坐标系下) 转换到 齐次坐标裁剪空间(裁剪空间坐标系下) 时的 变换矩阵

我们可以将其分成两步来完成

1.将视锥体中心位移到观察空间原点中心

2.将长方体视锥体的xyz坐标范围映射到(-1,1)长宽高为2的正方体中

正交投影重要参数

接下来我们需要获取具体的数值

Projection:该参数为Orthographic时,为正交摄像机

Size:视锥体竖直方向上高度的一半

Clipping Planes:裁剪平面

Near:近裁剪面离摄像机的距离

Far:远裁剪面离摄像机的距离

利用已知参数,获取到远近裁剪面的高度

已知:

Size:视锥体竖直方向上高度的一半

可得:

近裁剪面高 = 2 * Size

远裁剪面高 = 2 * Size

现在我们已经可以得到

近裁剪面高 = 远裁剪面高 = 2 * Size

我们还可以知道远近裁剪面的宽

可以通过摄像机参数得到Game窗口的宽高比

Camera.main.aspect

Aspect = 宽 : 高 = 宽 / 高

因此可以得到

近裁剪面宽 = 远裁剪面宽 = Aspect * 2 * Size

因此,通过上面的推导,我们获取到了远近裁剪面的宽高信息

远近裁剪面

高 = 2 * Size

宽 = Aspect 高 = Aspect 2 * Size

正交投影变换矩阵

一些疑惑

问题1

第一步推导的矩阵平移公式是将观察坐标系切换到裁剪坐标系的视角之下,但是第二步的公式全程推导都建立在观察坐标系的视角之下,因为立方体的坐标都是基于观察坐标系的

如果按这个顺序来说从几何意义上不是会导致变换矩阵的坐标系不统一的问题吗?

答:不会有问题,因为你仔细观察会发现第二步的推导本质上是一个比例公式,我们只需要拿到的只是比例关系,并不绑定坐标系

问题2

为什么所有推导都基于观察坐标系视角推导

答:是因为观察坐标系的点我们已经知道了,都是初始信息,用起来方便呗。

问题3

为什么正交投影的两步顺序是这样呢?

答:需要注意的是,你可以颠倒顺序,但是颠倒的话,你把切换左右手坐标系的步骤放在了第一步,可能会导致后续的平移操作变得更加复杂,因为你需要在一个新的、可能不太直观的观察坐标系中考虑平移向量。

既然你有现成的观察坐标,而且规范化不依赖坐标系还能顺便切换坐标系,那我们先平移不就省事多了?

裁剪空间变换 —— 透视投影变换

透视投影变换目的

将摄像机视锥体的 透视投影 转换到 齐次坐标系 时的 变换矩阵

我们可以将其分成三步来完成

1.将透视视锥体变成一个长方体

将该长方体进行正交投影变换的操作

2.将视锥体中心位移到观察空间原点中心

3.将长方体视锥体的xyz坐标范围映射到(-1,1)长宽高为2的正方体中

透视投影重要参数

Projection:该参数为Perspective时,为透视摄像机

FOV(Field of View):决定视锥开口角度

Clipping Planes:裁剪平面

Near:近裁剪面离摄像机的距离

Far:远裁剪面离摄像机的距离

利用已知参数,获取到远近裁剪面的高度

已知:

Near:近裁剪面离摄像机的距离

Far:远裁剪面离摄像机的距离

FOV(Field of View):决定视锥开口角度

可得:

近裁剪面高 = 2 *Near* tan(FOV/2)

远裁剪面高 = 2 *Far*tan(FOV/2)

我们还需要知道远近裁剪面的宽,以便之后进行变换矩阵的推导

可以通过摄像机参数得到Game窗口的宽高比

Aspect = 宽 : 高 = 宽 / 高

因此可以得到

近裁剪面宽 = Aspect 近裁剪面高 = Aspect * 2 *Near tan(FOV/2)

远裁剪面宽 = Aspect 远裁剪面高 = Aspect * 2 *Fartan(FOV/2)

至此,重要参数获取完毕

透视投影变换矩阵

过程较为复杂,建议直接查阅PPT:任务学习 - 泰课在线 -- 志存高远,稳如泰山 - 国内专业的在线学习平台|Unity3d培训|Unity教程|Unity教程 Unreal 虚幻 AR|移动开发|美术CG - Powered By EduSoho (taikr.com)

最终,我们会得到一个固定的矩阵变换公式,套用此公式就可以对一个观察坐标系的点进行裁剪空间变换

裁剪空间变换的意义

裁剪空间变换的意义

我们之所以要将观察空间中的顶点等信息变换到裁剪空间中

主要意义,其实我们一开始就提到过

是为了让我们可以更通用、便捷的来进行裁剪工作。

因为如果直接使用视锥体定义的空间来进行裁剪,那不同的视锥体就需要不同的处理过程,

比如正交摄像机的 Size、Near、Far等参数,透视摄像机中的FOV、Near、Far等参数

他们决定了视锥体的体积大小会各不相同,而且对于透视投影的视锥体来说,判断顶点是否在

其范围内相对较麻烦。

因此前辈们就制定了裁剪空间这一规则,让我们可以更容易的完成裁剪工作,

这也是该变换的意义所在

如何决定顶点是否被裁剪

屏幕空间变换

屏幕空间的意义

屏幕空间(Screen Space)是计算机图形学中的概念

指渲染结果在屏幕上显示的坐标空间

三维坐标经过一系列转换后会转换到最终的二维屏幕坐标空间中

使得图像可以在屏幕上进行展示。

屏幕空间的主要意义是屏幕空间中对应的位置信息是真正的像素位置,而不是虚拟的三维坐标。

有了相对屏幕空间的坐标位置,才能准确的控制屏幕上像素点的显示效果

屏幕空间中的注意事项

在Unity中

屏幕空间左下角为像素坐标(0,0)点

屏幕空间右上角为像素坐标(分辨率宽,分辨率高)

屏幕空间变换指什么

图形学中,主要是将模型空间中的点或向量从裁剪空间中变换到屏幕空间中

它是顶点变换的第四步,就是将 数据 从裁剪空间 →屏幕空间 进行变换

从图中我们可以看到,主要是将三维坐标(x,y,z)中的x,y分量映射到屏幕上,而z分量一般会被用于

深度缓冲,之后用于深度测试等(决定是否被遮挡等)

如何进行屏幕空间变换

屏幕空间变换,可以说是所有空间变换中最简单的一步,我们只需要将齐次裁剪空间(xyz范围在-1到1之间,长度为2的正方体)中的顶点映射到屏幕坐标即可。我们只需要找到他们的映射关系即可。

如何进行屏幕空间变换

首先我们进行类似上一节的操作,通过透视除法获取我们的点的准确坐标和深度。

接着我们来找到齐次裁剪空间和屏幕空间的映射关系

阶段总结

总结

1. 我们学习的Shader开发主要是针对渲染管线中的两个小阶段的

几何阶段——顶点着色器

光栅化阶段——片元着色器

通过在这两个小阶段中 对渲染管线中的数据进行自定义处理来决定最终的渲染效果

2. 学习Shader开发,主要要学习

数学相关知识、语法相关知识、着色器开发相关知识 等

我们目前已经完成了数学相关知识的学习

3. 通过三角函数、向量、线性代数相关知识点的学习

我们可以利用这些数学知识,来进行顶点、向量的矩阵变换

我们在这一部分的学习中,主要学习了如何将模型的顶点数据利用相关数学知识变换到屏幕坐标中

变换主要有 四个步骤:模型空间 →世界空间 →观察空间 →裁剪空间 →屏幕空间

注意:这几个空间中,除了观察空间

中坐标是基于右手坐标系

的,其他都是左手坐标系

顶点着色器的最基本任务就是把顶点坐标从模型空间转换到裁剪空间中

片元着色器中可以得到片元在屏幕空间的像素位置

这一阶段的知识点,以后会在Shader开发中频繁使用

以后我们学习的对应空间变换的API,内部的实现原理就是我们学习的这些知识点

注意

我们经常提到空间的变换,不仅仅是针对顶点的,模型数据中还有切线、法线相关数据

关于它们是如何变换的,基本原理还是进行矩阵运算,但是其中法线数据的变换规则会有些许不同

我们会在之后需要进行法线变换时,再详细进行讲解。

第4节: 语法基础——ShaderLab语法

材质和Shader

知识点一 Unity Shader 和 Shader的区别

Shader是一个更通用的概念,用于描述图形渲染程序中的着色器程序

而Unity Shader是特指在Unity中使用的着色器

你可以认为Unity Shader是对Shader的一种封装

它是对底层图形渲染技术的封装,它提供了一种叫做ShaderLab的语言

来让我们更加轻松的编写和管理着色器

我们在这一阶段主要对Unity Shader当中的ShaderLab进行学习

我们之后的学习在提到Shader这个词时,主要指的就是Unity Shader

知识点二 Unity中的材质和Shader

如果我们想要在Unity中体现出一个Shader的渲染效果

必须配合使用材质(Material)和 Shader(Unity Shader)才能达到目标

一般的使用流程是:

  1. 创建一个材质

  2. 创建一个Unity Sahder,把该Shader赋给上一步中创建的材质

  3. 将材质赋予给想要渲染的对象

  4. 在材质面板中调整Unity Shader的相关属性,以达到最终效果

也就是说,Unity中的Shader必须配合材质才能正常使用

我们接下来就类分别的创建它们,来感受下他们的作用

知识点三 创建材质

我们可以在Project窗口中右键创建材质

Create ——> Material

  1. 创建好材质后,可以选中它后,在Inspector窗口中Shader选项中选择对应的着色器进行关联使用

  2. Inspector窗口下方的内容就是选中的Shader提供的可编辑变化的相关变量,他们会直接影响渲染结果

  3. 关联好Shader后,材质需要赋值给GameObject对象上依附的Mesh Renderer等相关渲染器组件上进行使用

也就是说只要Unity Sahder当中提供对应的可以编辑属性,我们就可以直接在材质中进行编辑

而不需要去修改Shader代码来达到不同效果了

知识点四 创建Shader

我们可以在Project窗口中右键创建Shader

Create ——> Shader

1.Standard Surface Shader(标准曲面着色器)

包含标准光照模型的表面着色器模板

2.Unlit Shader

不包含光照的基本顶点/片元着色器

3.Image Effect Shader

用于实现屏幕后处理效果的基本模板

4.Compute Shader

利用GPU并行计算一些和常规渲染流水线无关的内容

5.Ray Tracing Shader

用于实现光线追踪效果的着色器

我们之后的学习重点主要是顶点/片元着色器

因此我们更多的会去学习 Unlit Shader 着色器的编写

我们可以先来认识下Shader文件在Inspector窗口上显示的内容

知识点五 配合使用

有了材质和Shader后,我们才能够将其配合在一起使用

只需要将材质设置为使用对应的着色器,并设置着色器提供的可编辑的相关属性

我们就可以利用他们来渲染出我们想要的各种效果了

总结

在Unity中,材质和Shader是密不可分的两兄弟

想要在Unity中使用Shader就必须要配合材质进行使用

ShaderLab的基本结构

知识点一 什么是ShaderLab

上节课我们学到

Unity Shader是对Shader的一种封装

它是对底层图形渲染技术的封装,它提供了一种叫做ShaderLab的语言

来让我们更加轻松的编写和管理着色器

ShaderLab其实就是Unity自定义的一种语法规则

是用于在Untiy中编写和管理着色器的专门的语言

它提供了一种结构化的方式来描述Unity着色器的各个部分

从而让我们可以更轻松的创建和管理着色器

总而言之:

无论我们编写哪种类型的Shader,或是选择哪种语言去编写Shader

在Unity中总会通过ShaderLab语言对其进行包装和组织

它是Unity自定义的一种语法规则

知识点二 ShaderLab的基本结构

ShaderLab主要由4个部分组成

  1. Shader的名字

  2. Shader的属性

  3. 1~n个子着色器

  4. 备用的Shader

//第一部分
Shader "着色器名字" 
{ 
      //第二部分
      Properties
      {
          //材质面板上可以看到的属性
      }

      //第三部分
      SubShader
      {
          //顶点-片段着色器 或 表面着色器 或 固定函数着色器
      }
      SubShader
      {
          //更加精简的版本
          //目的是适配旧设备
      }
      .....可以有n个SubShader代码块

      //第四部分
      Fallback "备用的Shader"
}

我们创建的所有Shader,都是基于ShaderLab的这些语法规则的

我们可以来观察下创建的一些默认Shader内容

Shader的名字

Shader的名字

1.直接修改Shader文件中 Shader后的名字即可

2.Shader的名字决定了在材质面板的选择路径

注意:

1.不要使用中文命名我们的Shader

2.Shader的文件名和在文件中的命名建议保持一致

ShaderLab的属性

知识点一 Shader的属性的作用

在Shader编写时我们经常会用到不同类型的变量或贴图等资源

为了增加Shader的可调节性,有些变量不会直接在Shader程序中写死

而是作为开放的属性显示在我们的材质面板上,供我们使用时调节

而这些开放的属性就是通过属性来定义的

Shader的属性具有两个特点

1.可以在材质面板被编辑

2.可以在后续当作输入变量提供给所有子着色器使用

知识点二 Shader的属性的基本语法

1.在Shader文件中

Shader属性是存在于Shader语句块中的Properties属性语句块

我们只需要在Properties语句块中按照语法规则声明属性即可

2.Unity Shader的属性主要分成三大类:数值、颜色和向量、纹理贴图

3.属性的基本语法

_Name("Display Name", type) = defaultValue[{options}]

_Name: 属性名字,规则是需要在前面加一个下划线,方便在之后获取

Display Name:材质面板上显示的名字

type:属性的类型

defaultValue:将Shader指定给材质的时候初始化的默认值

例子:

//第一部分
Shader "着色器名字" 
{ 
      //第二部分
      Properties
      {
          //材质面板上可以看到的属性
          _Name("Display Name", type) = defaultValue[{options}]
      }

      //第三、四部分
      .......
      .......
}

知识点三 数值类型属性

数值类型有三种:

1.整形

_Name("Display Name", Int) = number

2.浮点型

_Name("Display Name", Float) = number

3.范围浮点型

_Name("Display Name", Range(min,max)) = number

注意:Unity Shader中的数值类型属性基本都是浮点型(Float)数据

虽然提供了整数(Int),但是编译时最终都会转换为浮点型

因此我们更多的使用的还是Float类型

知识点四 颜色和向量类型属性

颜色和向量类型属性之所以归纳在一起

是因为

颜色是由RGBA四个分量代表

向量是由XYZW四个分量代表

他们都可以由一个四个数组成的类型表示

1.颜色

_Name("Display Name", Color) = (number1,number2,number3,number4)

注意:颜色值中的RGBA的取值范围是 0~1 (映射0~255)

2.向量

_Name("Display Name", Vector) = (number1,number2,number3,number4)

注意:向量值中的XYZW的取值范围没有限制

知识点五 纹理贴图类型属性

纹理贴图类型有四种

1.2D 纹理(最常用的纹理,漫反射贴图、法线贴图都属于2D纹理)

_Name("Display Name", 2D) = "defaulttexture"{}

2.2DArray 纹理(纹理数组,允许在纹理中存储多层图像数据,每层看做一个2D图像,一般使用脚本创建,较少使用,了解即可)

_Name("Display Name", 2DArray) = "defaulttexture"{}

3.Cube map texture纹理(立方体纹理,由前后左右上下6张有联系的2D贴图拼成的立方体,比如天空盒和反射探针)

_Name("Display Name", Cube) = "defaulttexture"{}

4.3D纹理(一般使用脚本创建,极少使用,了解即可)

_Name("Display Name", 3D) = "defaulttexture"{}

注意:

1.关于defaulttexture默认值取值

不写:默认贴图为空

white:默认白色贴图(RGBA:1,1,1,1)

black:默认黑色贴图(RGBA:0,0,0,1)

gray:默认灰色贴图(RGBA:0.5,0.5,0.5,1)

bump:默认凸贴图(RGBA:0.5,0.5,1,1),一般用于法线贴图默认贴图

red:默认红色贴图(RGBA:1,0,0,1)

2.关于默认值后面的 {} ,固定写法(老版本中括号内可以控制固定函数纹理坐标的生成,但是新版本中没有该功能了)

总结

Shader的属性主要就是三种类型

数值、颜色和向量、纹理贴图

我们只需要掌握属性声明的基础语法即可

它的主要是:

1.可以在材质面板被编辑

2.可以在后续当作输入变量提供给所有子着色器使用

Shader的子着色器—SubShader基本构成

知识点一 SubShader语句块的作用

每一个Shader中都会包含至少一个SubShader

当Unity想要显示一个物体的时候

就会在Shader文件中去检测这些SubShader语句块

然后选择第一个能够在当前显卡运行的SubShader进行执行

因此在一个Shader当中实现一些高级效果时

为了避免在在某些设备上无法执行

可能会存在多个SubShader语句块,用于适配这些低端设备

SubShader当中包含最终的渲染相关代码,决定了最终的渲染效果

知识点二 SubShader的基本构成

SubShader语句块中主要由3部分构成

1.渲染标签:通过标签来确定什么时候以及如何对物体进行渲染

2.渲染状态:通过状态来确定渲染时的剔除方式、深度测试方式、混合方式等等内容

3.渲染通道:具体实现着色器代码的地方(每个SubShader语句块中至少有一个渲染通道,可以有多个)

//第三部分
SubShader
{
  //1.渲染标签
  Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" .....}

  //2.渲染状态
  .....

  //3.渲染通道
  Pass
  {
      //第一个渲染通道
  }
  Pass
  {
      //第二个渲染通道
  }
  .............
}

注意:

在SubShader中每定义一个渲染通道Pass,就会让物体执行一次渲染

n个Pass,就会有n次渲染,在实现一些复杂渲染效果时需要使用多个Pass进行组合实现

但是我们要尽量减少它的数量,更多的Pass会增加性能消耗

总结

SubShader子着色器基本构成为:

--------Tags(渲染标签)

|--------States(渲染状态)

SubShader---|--------Pass(渲染通道1)

|--------Pass(渲染通道2)

|--------....(渲染通道n)

在Shader文件中可以存在多个SubShader语句块

当Unity想要显示一个物体的时候

会在Shader文件中去检测这些SubShader语句块

然后选择第一个能够在当前显卡运行的SubShader进行执行

因此在一个Shader当中实现一些高级效果时

为了避免在在某些设备上无法执行

可能会存在多个SubShader语句块,用于适配这些低端设备

Shader的子着色器—Tags渲染标签

知识点一 渲染标签的语法结构

Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" "标签名2" = "标签值2" .......}

渲染标签是通过键值对的形式进行声明的

并且没有数量限制,可以使用任意多个标签

知识点二 渲染队列Queue

主要作用:

确定物体的渲染顺序

Tags{ "Queue" = "标签值" }

常用Unity预先定义好的渲染队列标签值:

1.Background(背景)(队列号:1000)

最早被渲染的物体的队列,一般用来渲染天空盒或者背景

Tags{ "Queue" = "Background" }

2.Geometry(几何)(队列号:2000)

不透明的几何体通常使用该队列,当没有声明渲染队列时,Unity会默认使用这个队列

Tags{ "Queue" = "Geometry" }

3.AlphaTest(透明测试)(队列号:2450)

有透明通道的,需要进行Alpha测试的几何体会使用该队列

当所有Geometry队列实体绘制完后再绘制AlphaTest队列,效率更高

Tags{ "Queue" = "AlphaTest" }

4.Transparent(透明的)(队列号:3000)

该队列中几何体按照由远到近的顺序进行绘制,半透明物体的渲染队列,所有进行透明混合的几何体都应该使用该队列

比如:玻璃材质,粒子特效等

Tags{ "Queue" = "Transparent" }

5.Overlay(覆盖)(队列号:4000)

用是放在最后渲染的队列,于叠加渲染的效果,比如镜头光晕等

Tags{ "Queue" = "Overlay" }

6.自定义队列

基于Unity预先定义好的这些渲染队列标签来进行加减运算来定义自己的渲染队列

比如:

Tags{ "Queue" = "Geometry+1" } 代表的队列号就是 2001

Tags{ "Queue" = "Transparent-1" } 代表的队列号就是 2999

自定义队列在一些特殊情况下,特别有用

比如 一些水的渲染 想要在不透明物体之后,半透明物体之前进行渲染,就可以自定义

注意:自定义队列只能基于预先定义好的各类型进行计算,不能在Shader中直接赋值数字

如果实在想要直接赋值数字,可以在材质面板中进行设置

知识点三 渲染类型RenderType

主要作用:

对着色器进行分类,之后可以用于着色器替换功能

摄像机上有对应的API,可以指定这个渲染类型来替换成别的着色器

Tags{ "RenderType" = "标签值" }

常用Unity预先定义好的渲染类型标签值:

1.Opaque(不透明的)

用于普通Shader,比如:不透明、自发光、反射等

2.Transparent(透明的)

用于半透明Shader,比如:透明、粒子

3.TransparentCutout(透明切割)

用于透明测试Shader,比如:植物叶子

4.Background(背景)

用于天空盒Shader

5.Overlay(覆盖)

用于GUI纹理、Halo(光环)、Flare(光晕)

了解即可

6.TreeOpaque

用于地形系统中的树干

7.TreeTransparentCutout

用于地形系统中的树叶

8.TreeBillboard

用于地形系统中的Billboarded树

9.Grass

用于地形系统中的草

10.GrassBillboard

用于地形系统中的Billboarded草

知识点四 禁用批处理

主要作用:

当使用批处理时,模型会被变换到世界空间中,模型空间会被丢弃

这可能会导致某些使用模型空间顶点数据的Shader最终无法实现想要的结果

可以通过开启禁用批处理来解决该问题

总是禁用批处理

Tags{ "DisableBatching" = "True" }

不禁用批处理(默认值)

Tags{ "DisableBatching" = "False" }

了解即可

LOD效果激活时才会禁用批处理,主要用于地形系统上的树

Tags{ "DisableBatching" = "LODFading" }

知识点五 禁止阴影投影

主要作用:

控制该SubShader的物体是否会投射阴影

不投射阴影

Tags{ "ForceNoShadowCasting" = "True" }

投射阴影(默认值)

Tags{ "ForceNoShadowCasting" = "False" }

知识点六 忽略投影机

主要作用:

物体是否受到Projector(投影机)的投射

Projector是Unity中的一个功能(以后讲解)

忽略Projector(一般半透明Shader需要开启该标签)

Tags{ "IgnoreProjector" = "True" }

不忽略Projector(默认值)

Tags{ "IgnoreProjector" = "False" }

知识点七 其他标签

1.是否用于精灵

想要将该SubShader用于Sprite时,将该标签设置为False

Tags{ "CanUseSpriteAtlas" = "False" }

2.预览类型

材质在预览窗口默认为球形,如果想要改变为平面或天空盒

只需要改变预览标签即可

平面

Tags{ "PreviewType" = "Panel" }

天空盒

Tags{ "PreviewType" = "SkyBox" }

知识点八 渲染标签的注意实现

以上这些标签只能在SubShader语句块中声明

之后讲解的Pass渲染通道语句块中也可以声明渲染标签

但是今天讲解的内容都不能在Pass中声明

Pass中有自己专门的标签类型,我们会在之后讲解

总结

渲染标签其实都是以键值对的形式出现的配置

键和值都是字符串类型

这些渲染标签是SubShader和渲染引擎之间的沟通桥梁

我们可以利用渲染标签来告诉Unity我们希望如何以及何时渲染这个对象

这些内容,大家可以将其记入自己的笔记当中

之后在使用他们时,翻看笔记即可

Shader的子着色器—States渲染状态

知识点一 渲染状态的语法结构

渲染状态 状态类型

渲染状态是通过 渲染状态关键词+空格+状态类型 决定的

如果存在多个渲染状态

可以通过空行隔开

知识点二 剔除方式

主要作用:

设置多边形的剔除方式,有背面剔除、正面剔除、不剔除

所谓的剔除,就是不渲染,背面剔除就是背面不渲染,正面剔除就是正面不渲染,不剔除就是都渲染

Cull Back 背面剔除

Cull Front 正面剔除

Cull Off 不剔除

不设置的话,默认为背面剔除

一般情况下,我们需要两面渲染时,会设置为不剔除

知识点三 深度缓冲

主要作用:

是否写入深度缓冲

深度缓冲(Depth Buffer):

深度缓冲是一个与屏幕像素对应的缓冲区,用于存储每个像素的深度值(距离相机的距离)。

在渲染场景之前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。

最后留在深度缓冲中的信息会被渲染

ZWrite On 写入深度缓冲

ZWrite Off 不写入深度缓冲

不设置的话,默认为写入

一般情况下,我们在做透明等特殊效果时,会设置为不写入

知识点四 深度测试

主要作用:

设置深度测试的对比方式

深度测试的主要目的是确保在渲染时,像素按照正确的深度(距离相机的距离)顺序进行绘制,

从而创建正确的遮挡关系和透视效果

在渲染场景之前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。

在渲染过程中,对于每个像素,深度测试会将当前像素的深度值与深度缓冲中对应位置的值进行比较。

一般情况下

1.如果当前像素的深度值小于深度缓冲中的值,说明当前像素在其他物体之前,它会被绘制,并更新深度缓冲。

2.如果当前像素的深度值大于等于深度缓冲中的值,说明当前像素在其他物体之后,它会被丢弃,不会被绘制,并保持深度缓冲不变。

ZTest Less 小于当前深度缓冲中的值,就通过测试,写入到深度缓冲中

ZTest Greater 大于当前深度缓冲中的值,就通过测试,写入到深度缓冲中

ZTest LEqual 小于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中

ZTest GEqual 大于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中

ZTest Equal 等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中

ZTest NotEqual 不等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中

ZTest Always 始终通过深度测试写入深度缓冲中

不设置的话,默认为LEqual 小于等于

一般情况下,我们只有在实现一些特殊效果时才会区修改深度测试方式,比如透明物体渲染会修改为Less,描边效果会修改为Greater等

知识点五 混合方式

主要作用:

设置渲染图像的混合方式(多种颜色叠加混合,比如透明、半透明效果和遮挡的物体进行颜色混合)

Blend One One 线性减淡

Blend SrcAlpha OneMinusSrcAlpha 正常透明混合

Blend OneMinusDstColor One 滤色

Blend DstColor Zero 正片叠底

Blend DstColor SrcColor X光片效果

Blend One OneMinusSrcAlpha 透明度混合

等等

不设置的话,默认不会进行混合

一般情况下,我们需要多种颜色叠加渲染时,就需要设置混合方式,具体情况具体处理

知识点六 其他渲染状态

1.LOD 控制LOD级别,在不同距离下使用不同的渲染方式处理

2.ColorMask 设置颜色通道的写入蒙版,默认蒙版为RGBA

等等

我们目前主要掌握剔除方式、深度缓冲、深度测试、混合方式即可

知识点七 渲染状态的注意事项

以上这些状态不仅可以在SubShader语句块中声明

之后讲解的Pass渲染通道语句块中也可以声明这些渲染状态

如果在SubShader语句块中使用会影响之后的所有渲染通道Pass

如果在Pass语句块中使用只会影响当前Pass渲染通道,不会影响其他的Pass

总结

渲染状态对于我们来说很重要

它可以影响最终我们看到的渲染效果

其中

剔除方式决定了 模型正面背面是否能够被渲染

深度缓冲和深度测试 决定了景深关系的确定以及透明效果的正确表达等

混合方式 决定了透明半透明颜色的正确表现,以及一些特殊颜色效果的表现

这些内容,大家可以将其记入自己的笔记当中

之后在使用他们时,翻看笔记进行复习

Shader的子着色器—Pass渲染通道

知识点一 渲染通道的语法结构

Pass{

1.Name 名称

2.渲染标签

3.渲染状态

4.其他着色器代码

}

知识点二 Pass的名字

主要作用:

我们对Pass命名的主要目的

可以利用UsePass命令在其他Shader当中复用该Pass的代码,

只需要在其他Shader当中使用

UsePass "Shader路径/Pass名"

注意:

Unity内部会把Pass名称转换为大写字母

因此在使用UsePass命令时必须使用大写形式的名字

Pass{

Name MyPass

}

在其他Shader中复用该Pass代码时

UsePass "TeachShader/Lesson4/MYPASS"

知识点三 Pass中的渲染标签

Pass中的渲染标签语法和SubShader中相同

Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" "标签名2" = "标签值2" .......}

但是我们之前讲解过的SubShader语句块中的渲染标签不能够在Pass中使用

Pass当中有自己专门的渲染标签

1.Tags{ "LightMode" = "标签值" }

主要作用:

指定了该Pass应该在哪个阶段执行

可以将着色器代码分配给适当的渲染阶段,以实现所需的效果

1-1.Always

始终渲染;不应用光照

1-2.ForwardBase

在前向渲染中使用;应用环境光、主方向光、顶点/SH 光源和光照贴图

1-3.ForwardAdd

在前向渲染中使用;应用附加的每像素光源(每个光源有一个通道)

1-4.Deferred

在延迟渲染中使用;渲染 G 缓冲区

1-5.ShadowCaster

将对象深度渲染到阴影贴图或深度纹理中

1-6.MotionVectors

用于计算每对象运动矢量

1-7.PrepassBase

在旧版延迟光照中使用;渲染法线和镜面反射指数

1-8.PrepassFinal

在旧版延迟光照中使用;通过组合纹理、光照和反光来渲染最终颜色

1-9.Vertex

当对象不进行光照贴图时在旧版顶点光照渲染中使用;应用所有顶点光源

1-10.VertexLMRGBM

当对象不进行光照贴图时在旧版顶点光照渲染中使用;在光照贴图为 RGBM 编码的平台上(PC 和游戏主机)

1-11.VertexLM

当对象不进行光照贴图时在旧版顶点光照渲染中使用;在光照贴图为双 LDR 编码的平台上(移动平台)

关于向前渲染、延迟渲染、旧版光照等概念了解

https://docs.unity.cn/cn/2019.4/Manual/RenderingPaths.html

2.Tags{ "RequireOptions" = "标签值" }

主要作用:

用于指定当满足某些条件时才渲染该Pass

目前Unity仅支持

Tags{ "RequireOptions" = "SoftVegetation" }

仅当Quality窗口中开启了SoftVegetation时才渲染此通道

3.Tags{ "PassFlags" = "标签值" }

主要作用:

一个渲染通道Pass可指示一些标志来更改渲染管线向Pass传递数据的方式

目前Unity仅支持

Tags{ "PassFlags" = "OnlyDirectional" }

在 ForwardBase 向前渲染的通道类型中使用时,此标志的作用是仅允许主方向光和环境光/光照探针数据传递到着色器

这意味着非重要光源的数据将不会传递到顶点光源或球谐函数着色器变量

知识点四 Pass中的渲染状态

我们上节课在SubShader语句块中学习的渲染状态同样适用于Pass

比如

剔除方式决定了 模型正面背面是否能够被渲染

深度缓冲和深度测试 决定了景深关系的确定以及透明效果的正确表达等

混合方式 决定了透明半透明颜色的正确表现,以及一些特殊颜色效果的表现

这些渲染状态都可以在单个Pass中进行设置

需要注意的是

如果在SubShader语句块中使用会影响之后的所有渲染通道Pass

如果在Pass语句块中使用只会影响当前Pass渲染通道,不会影响其他的Pass

不仅如此,Pass中还可以使用固定管线着色器的命令

知识点五 其他着色器代码

其他代码部分就是实现着色器的核心代码部分

我们可能会用到CG或HLSL等着色器语言来进行逻辑书写

我们之后会详细讲解

知识点六 GrabPass命令

我们可以利用GrabPass命令把即将绘制对象时的屏幕内容抓取到纹理中

在后续通道中即可使用此纹理,从而执行基于图像的高级效果。

举例:

将绘制该对象之前的屏幕抓取到 _BackgroundTexture 中

GrabPass

{

"_BackgroundTexture"

}

注意:

该命令一般写在某个Pass前,在之后的Pass代码中可以利用_BackgroundTexture变量进行处理

总结

Pass渲染通道语句块中

主要包含了

  1. 名字:可以帮助我们复用Pass代码

  2. 渲染标签:不能使用SubShader中的渲染标签,有自己独有的渲染标签

  3. 渲染状态:SubShader当中的渲染状态同样可以在Pass中使用,影响的区域不同

  4. 着色器语言逻辑:我们之后会详细学习的部分

四个部分

Shader的备用着色器

知识点一 备用Shader的作用

我们知道ShaderLab当中允许有多个SubShader子着色器

当执行渲染时,会从上到下使用第一个能够正常执行的SubShader子着色器来渲染对象

那有没有可能所有SubShade子着色器都不能够在显卡上执行呢?

答案是肯定的

有可能出现在某一个设备上,我们自定义的所有SubShader都无法正常执行(设备显卡无法支持一些api等情况)

那么这时备用Shader就可以起到很大作用了,它至少可以让对象能够正常渲染出来

因此

备用Shader主要作用就是当Shader文件中的所有SubShader子着色器都无法正常运行时

起到一个保险作用,让物体能够使用一个最低级的Shader去渲染出来(效果略差,但至少能够显示)

知识点二 备用Shader的语法

Fallback "Shader名"

或者

Fallback Off

在Fallback关键词后面空格并通过一个字符串来告诉Unity

“最低级的Unity Shader”是谁

也可以直接关闭Fallback功能,但是如果关闭那就意味着“放弃治疗”

总结

备用Shader的语法非常简单

Fallback "Shader名"

或者

Fallback Off

它的主要作用就是提供“救命稻草”

我们一般会使用较为低级的效果作为备用Shader

确保对象在低端设备上也能够正常渲染

Shader的编写形式——表面着色器

知识点一 Shader的形式是什么?

通过之前的课程,我们已经对Shader文件的文件结构有一定的认识

并且学习了ShaderLab语法相关的知识

通过学习我们知道 在Unity Shader当中我们可以通过ShaderLab语法去设置很多内容

比如属性、渲染状态、渲染标签等等

但是其最主要的作用是需要指定各种着色器所需的代码

而这些着色器代码即可以放在SubShader子着色器语句块中,也可以放在其中的Pass渲染通道语句块中

不同的Shader形式放置着色器代码的位置也有所不同

我们一般会使用以下3种形式来编写Unity Shader

  1. 表面着色器(可控性较低)

  2. 顶点/片元着色器(重点学习)

  3. 固定函数着色器(基本已弃用,了解即可)

知识点二 表面着色器

表面着色器(Surface Shader)是Unity自己创造的一种着色器代码类型

它的本质是对顶点/片元着色器的一层封装

它需要的代码量很少,很多工作都帮助我们去完成了

但是缺点是渲染的消耗较大,可控性较低

它的优点在于,它帮助我们处理了很多光照细节,我们可以直接使用而无需自己计算实现光照细节

我们可以在创建Shader时,选择创建Standard Surface Shader

通过观察该Shader文件的内部结构,你会发现

着色器相关代码被放在SubShader语句块中(并非Pass)的 CGPROGRAM 和 ENDCG 之间

表面着色器的特点就是

  1. 直接在SubShader语句块中书写着色器逻辑

  2. 我们不需要关心也不需要使用多个Pass,每个Pass如何渲染,Unity会在内部帮助我们去处理

  3. 可以使用CG或HLSL两种Shader语言去编写Shader逻辑

  4. 代码量较少,可控性较低,性能消耗较高

  5. 适用于处理需要和各种光源打交道的着色器(主机、PC平台时更适用,移动平台需要考虑性能消耗)

Shader的编写形式——顶点片元着色器

知识点

我们可以在创建Shader时,选择创建Unlit Shader来快速创建顶点/片元着色器模板

通过观察,我们发现

顶点/片元着色器的着色器代码是编写在Pass语句块中

我们需要自己定义每个Pass需要使用的Shader代码

虽然比起表面着色器来说我们需要编写的代码较多

但是好处是灵活性更高,可控性更强,可以控制更多的渲染细节

决定对性能影响的高低

它的特点是

1.需要在Pass渲染通道中编写着色器逻辑

2.可以使用CG或HLSL两种Shader语言去编写Shader逻辑

3.代码量较多,灵活性较强,性能消耗更可控,可以实现更多渲染细节

4.适用于光照处理较少,自定义渲染效果较多时(移动平台首选)

Shader的编写形式——固定函数着色器

知识点

表面着色器 和 顶点/片元着色器 这两种Unity Shader形式都使用了可编程管线

而对于一些老设备(DX7.0、OpenGL1.5或OpenGL ES 1.1),它们不支持可编程管线着色器

这时就需要使用固定函数着色器来进行渲染

这些着色器只能实现一些非常简单的效果

它的特点是:

  1. 需要在Pass渲染通道中编写着色器逻辑

  2. 需要使用ShaderLab语法中的渲染设置命令来编写,而非CG和HLSL着色器语言

但是由于这些旧设备目前市面上几乎已经没有了

所以固定函数着色器我们几乎不会再使用

只做了解即可

即使我们现在在Unity中使用固定函数着色器来编写Shader,在内部也会被编译为顶点/片元着色器

因此真正意义的固定函数着色器已经不存在了

总结

对于Unity Shader的三种编写形式

  1. 表面着色器

  2. 顶点/片元着色器

  3. 固定函数着色器

我们应该如何选择呢?

  1. 固定函数着色器基本已弃用,不是我们学习的重点

  2. 对于PC、主机性能较高的设备,为了追求更好的效果,使用表面着色器可能更加的方便

  3. 对于移动设备,为了权衡效果和性能,优先使用顶点/片元着色器

  4. 在有很多自定义的渲染效果时,优先使用顶点/片元着色器,因为更灵活可控

而表面着色器的本质是对顶点/片元着色器的封装

所以我们在学习过程中,着重学习顶点/片元着色器开发的相关知识

选修

任务学习 - 泰课在线 -- 志存高远,稳如泰山 - 国内专业的在线学习平台|Unity3d培训|Unity教程|Unity教程 Unreal 虚幻 AR|移动开发|美术CG - Powered By EduSoho (taikr.com)

第5节: 语法基础——CG语法

CG语句写在哪里

知识点一 CG语句写在哪里?

对于顶点\片元着色器来说

CG语句需要写在Pass渲染通道语句块中

我们需要在Pass语句块中

加入指令:

CGPROGRAM

//在这两个指令之间 就是我们书写CG代码的地方

ENDCG

知识点二 重要的编译指令 —— 指定着色器函数

在真正书写CG代码之前

我们需要先使用 #pragma 声明编译指令

我们目前只学习 指定顶点\片元着色器函数的 编译指令

其他相关编译指令我们以后再学习

定义实现 顶点/片元着色器 代码的函数名称

#pragma vertex name(实现顶点着色器的函数名)

#pragma fragment name(实现片元着色器的函数名)

这两个编译指令的作用是将顶点\片元着色器实现定位到两个函数中

之后我们只需要在这两个函数中书写Shader逻辑即可

CG基础数据类型

基础数据类型知识点一 基础数据类型

uint 32为无符号整形

int 32位整形

float 32位浮点数 符号:f

half 16位浮点数 符号:h

fixed 12位浮点数

bool 布尔类型

string 字符串

sampler 纹理对象句柄

sampler: 通用的纹理采样器,可以用于处理各种不同维度和类型的纹理

sampler1D: 用于一维纹理,通常用于对一维纹理进行采样,例如从左到右的渐变色

sampler2D: 用于二维纹理,最常见的纹理类型之一。它用于处理二维图像纹理,例如贴图

sampler3D: 用于三维纹理,通常用于体积纹理,例如体积渲染

samplerCUBE: 用于立方体纹理,通常用于处理环境映射等需要立方体贴图的情况

samplerRECT: 用于处理矩形纹理,通常用于一些非标准的纹理映射需求

他们都是用于处理纹理(Texture)数据的数据类型

他们的主要区别是纹理的维度和类型


知识点二 基础复合数据类型

数组:和C#中类似

一维

int a[4] = {1,2,3,4}

二维

int b[2][3] = {{1,2,3},{4,5,6}}

长度

CG语言无法直接获取数组长度,

所以建议在开头声明一个变量专门记录长度。

结构体

和C#基本一样

没有访问修饰符

结构体声明结束加分号

一般在函数外声明

总结

CG中的基础数据类型和C#中基本一致

重要区别是

  1. 多了几种浮点数类型 half和fixed

  2. 多了纹理类型 sampler

  3. 数组的声明有些许区别

CG特殊数据类型

知识点一 向量

向量类型属于CG语言的内置数据类型

内置的向量类型是基于基础数据类型声明的

向量的最大维度不超过4维

数据类型可以是任意数值类型

基本构成

数据类型2 = 数据类型2(n1,n2)

数据类型3 = 数据类型3(n1,n2,n3)

数据类型4 = 数据类型4(n1,n2,n3,n4)

知识点二 矩阵

矩阵类型属于CG语言的内置数据类型

矩阵的最大行列不大于4,不小于1

数据类型可以是任意数值类型

基本构成

数据类型'n'x'm' = {n1m1,n1m2,n1m3.....}

数据类型2x2

数据类型3x3

数据类型4x4

知识点三 bool类型特殊使用

bool类型同样可以用于如同向量一样声明

它可以用于存储一些逻辑判断结果

比如

float3 a = float3(0.5, 0.0, 1.0);

float3 b = float3(0.6, -0.1, 0.9);

bool3 c = a < b;

运算后向量c的结果为bool3(true, false, false)

总结

  1. 向量最大维度不超过4维

  2. 矩阵最大行列不大于4,不小于1,在赋值时一定注意行列的关系

  3. bool向量可以用来存储向量之间比较的结果

注意:CG中向量,矩阵和数组是完全不同的,向量和矩阵是内置的数据类型,而数组则是一种数据结构,不是内置数据类型

Swizzle操作符

知识点一 什么是Swizzle操作符

上节课我们学习了向量

但是我们并没有讲解如何获取向量中某元素的相关知识点

而这节课将要学习的Swizzle操作符就可以用于获取向量中元素

Swizzle操作符通常以点号(.)的形式使用,后面跟着所需的分量顺序

对于四维向量来说

我们可以通过

向量.xyzw

向量.rgba

两种分量的写法来表示向量中的四个值

其中 xyzw和rgba分别代表四维向量中的四个元素

在此的意义就是向量一般可以用来表示坐标和颜色

知识点二 如何使用Swizzle操作符

1.利用它来提取分量

fixed4 f4 = fixed4(1,2,3,4);
fixed f = f4.w;//xyzw
f = f4.a;//rgba

2.利用它来重新排列分量

fixed4 f4 = fixed4(1,2,3,4);
f4 = f4.yzxw;
f4 = f4.abgr;

3.利用它来创建新的向量

fixed3 f3 = f4.xyz;
fixed2 f2 = f3.xz;

知识点三 向量和矩阵的更多用法

1.利用向量声明矩阵

//在声明矩阵时,我们可以配合向量来进行声明
fixed4x4 f4x4 = {fixed4(1,2,3,4),
    fixed4(1,2,3,4),
    fixed4(1,2,3,4),
    fixed4(1,2,3,4)};

2.获取矩阵中的元素

//矩阵中元素的获取和二维数组一样
fixed  f = f4x4[0][0];

3.利用向量获取矩阵中的某一行

//我们可以用向量作为容器存储矩阵中的某一行
fixed4 f4_2 = f4x4[3];

4.高维转低维

fixed4x4 f4x4 = {fixed4(1,2,3,4),
    fixed4(1,2,3,4),
    fixed4(1,2,3,4),
    fixed4(1,2,3,4)};
fixed3x3 f3x3 = f4x4; //向量和矩阵都可以用低维存高维,会自动舍去多余元素,如这里只会保留3X3

总结

  1. 向量获取元素可以利用Swizzle操作符获取

  2. 矩阵获取元素和二维数组获取方式一样

  3. Swizzle操作符可以让我们对向量进行方便的操作

  4. 向量和矩阵在声明和获取时可以配合使用

  5. 高维向量或矩阵可以用低维容器装载,可以利用这个特点将4维变3维,4x4变3x3

运算符相关

知识点一 比较运算符

CG语言中比较运算符包括

大于 >

小于 <

大于等于 >=

小于等于 <=

等于 ==

不等于 !=

CG中的比较运算符的使用和C#中一样

运算结果为bool值

知识点二 条件运算符

CG语言中条件运算符(三目(三元)运算符)

condition ? value_if_true : value_if_false

condition 是一个条件表达式

如果为真将返回 value_if_true

否则返回 value_if_false

CG中的条件运算符的使用和C#中一样

知识点三 逻辑运算符

CG语言中逻辑运算符包括

逻辑或运算符 ||

逻辑与运算符 &&

逻辑非运算符 !

CG中的逻辑运算符的使用和C#中一样

唯一需要注意的是:CG中不存在C#中的"短路"操作

知识点四 数学运算符

CG语言中数学运算符包括

加法 +

减法 -

乘法 *

除法 /

取余 %

自增减 ++ --

CG中的数学运算符的使用和C#中一样

唯一需要注意的是:CG中取余符号只能向整数取余

总结

CG语法中比较、条件、逻辑、数学等运算符的使用和C#中一致

需要注意的是:

1.逻辑运算符在CG中不存在C#中的"短路"操作

2.数学运算符在CG中取余符号只能向整数取余

流程控制语句

知识点一 条件分支语句

  1. if语句

  2. switch语句

条件分支语句的使用和C#中一模一样

知识点二 循环语句

for循环

  1. while循环

  2. do while循环

循环语句的使用和C#中一模一样

总结

CG语法中的流程控制语句和C#中的使用一模一样

需要注意的是

在使用它们时要更多的考虑性能消耗

  1. 尽量少的使用循环语句,如果一定要用要减少次数和复杂度

  2. 要利用GPU并行性这一特点来替代循环

  3. 尽量避免复杂的条件分支

函数

注意

CG语法中的函数声明和使用几乎和C#中一模一样

知识点一 无返回值的函数

基本结构

void name(in 参数类型 参数名, out 参数类型 参数名)

{

函数体

}

void:以void开头,表示没有返回值

name:函数的名称

in:表示是输入参数,表示由函数外部传递给函数内部,内部不会修改该参数,只会使用该参数进行计算,允许有多个

out:表示是输出参数,表示由函数内部传递给函数的调用者,在函数内部必须对该参数值进行初始化或修改,允许有多个

注意:

in和out都可以省略,省略后就没有了in和out相关的限制

虽然可以省略,但是建议大家在编写Shader时不要省略in和out

因为他们可以明确参数的传递方式,提高代码的可读性和可维护性

可以让我们更容易的理解函数是如何与参数交互的,减少潜在的误解可能

知识点二 有返回值的函数

基本结构

type name(in 参数类型 参数名)

{

函数体

return 返回值;

}

type:返回值类型

return:返回指定类型的数据

注意:

虽然可以在有返回值的函数中使用out参数

但是这并不是常见做法,除非是一些自定义逻辑函数

对于顶点/片元着色器函数只会使用单返回值的方式进行处理

总结

CG当中的函数声明和函数使用和C#中基本一致

区别主要是CG中的in和out关键字

1.in表示输入参数,用于外部传给内部,内部只会用,不会改,可以有多个

2.out表示输出参数,用于内部传给外部,内部必须初始化或修改,可以有多个

对于有返回值的函数,要不采用返回值形式,要不采用out

顶点/片元着色器基本结构

注意

两个着色器的职责不同,缺一不可

就基本功能来说:

顶点着色器主要负责顶点坐标转换的处理,片元着色器主要负责顶点颜色的处理。

顶点着色器结构

  #pragma vertex myVert//指定顶点着色器的函数名

//顶点着色器 回调函数 
//POSITION 和 SV_POSITION是CG语言的语义
//POSITION:把模型的顶点坐标填充到输入的参数v当中
//SV_POSITION:顶点着色器输出的内容是裁剪空间中的顶点坐标
//如果没有这些语义来限定输入和输出参数的话,那么渲染器就完全不知道用户输入输出的是什么,就会得到错误的效果
float4 myVert(float4 v:POSITION):SV_POSITION
{
    //mul是CG语言提供的矩阵和向量的乘法运算函数(就是一个内置的函数)
    //UNITY_MATRIX_MVP 代表一个变换矩阵 是Unity内置的模型、观察、投影矩阵的集合
    //UnityObjectToClipPos它的作用和之前的矩阵乘法是一样的,主要目的就是在进行坐标变换 只不过新版本将其封装起来了 使用更加方便
    //mul(UNITY_MATRIX_MVP,v);
    return UnityObjectToClipPos(v);
}

片元着色器结构

  #pragma fragment myFrag

//片元着色器 回调函数
//SV_Target:告诉渲染器,把用户输出颜色存储到一个渲染目标中,这里将输出到默认的帧缓存中
fixed4 myFrag():SV_Target
{
    return fixed4(0,1,0,1);
}

语义

注意

ShaderLab的框架是高度封装的,有大量需要开发者遵循的语法约定

在上一节,我们使用了fixed4 myFrag():SV_Target,这是为返回值标注了语义。

使用语义来标记着色器输入和输出变量的用途,这是为了确保图形管线能够正确处理这些数据。Unity在编译和执行着色器时,会根据这些语义自动进行参数注入或处理返回值

知识点一 顶点着色器获取更多数据信息

当我们在顶点着色器当中想要获取更多模型相关信息时

可以使用结构体对数据进行封装

通过对结构体中成员变量加语义的方式来定义想要获取的信息

知识点二 片元着色器获取更多数据信息

当我们在片元着色器当中想要获取更多信息时

采用的方式还是封装结构体的方式

注意:

片元着色器中获取的数据基本上都是由顶点着色器传递过来的

所以我们封装的结构体还需要作为顶点着色器的返回值类型

总结

如果顶点/片元着色器想要传递更多参数

我们需要通过结构体进行封装,用语义修饰结构体成员变量来达到目的

注意:

只有顶点/片元着色器的回调函数相关参数和返回值才需要通过语义修饰

一般的自定义函数是不需要语义的

因为我们自己调用自己的自定义函数是可以明确知道每个参数的作用的

ShaderLab属性类型和CG变量类型的匹配关系

知识回顾 ShaderLab属性相关

Unity Shader的属性主要分成三大类:数值、颜色和向量、纹理贴图

属性的基本语法

_Name("Display Name", type) = defaultValue[{options}]

_Name: 属性名字,规则是需要在前面加一个下划线,方便在之后获取

Display Name:材质面板上显示的名字

type:属性的类型

defaultValue:将Shader指定给材质的时候初始化的默认值

数值类型有三种:

1.整形

_Name("Display Name", Int) = number

2.浮点型

_Name("Display Name", Float) = number

3.范围浮点型

_Name("Display Name", Range(min,max)) = number

4.颜色

_Name("Display Name", Color) = (number1,number2,number3,number4)

注意:颜色值中的RGBA的取值范围是 0~1 (映射0~255)

5.向量

_Name("Display Name", Vector) = (number1,number2,number3,number4)

注意:向量值中的XYZW的取值范围没有限制

6.2D 纹理(最常用的纹理,漫反射贴图、法线贴图都属于2D纹理)

_Name("Display Name", 2D) = "defaulttexture"{}

7.2DArray 纹理(纹理数组,允许在纹理中存储多层图像数据,每层看做一个2D图像,一般使用脚本创建,较少使用,了解即可)

_Name("Display Name", 2DArray) = "defaulttexture"{}

8.Cube map texture纹理(立方体纹理,由前后左右上下6张有联系的2D贴图拼成的立方体,比如天空盒和反射探针)

_Name("Display Name", Cube) = "defaulttexture"{}

9.3D纹理(一般使用脚本创建,极少使用,了解即可)

_Name("Display Name", 3D) = "defaulttexture"{}

知识点一 CG中变量类型的对应ShaderLab的属性类型

只要类型符合就可以装载。

ShaderLab属性类型 CG变量类型

Color,Vector float4,half4,fixed4

Range,Float,Int float,half,fixed

2D sampler2D

Cube samplerCube

3D sampler3D

2DArray sampler2DArray

知识点二 如何在CG语句块中使用ShaderLab中声明的属性

直接在CG语句块中

声明和属性中对应类型的同名变量即可

总结

ShaderLab中声明的属性都是需要在Shader(着色器)逻辑中使用的

我们需要在CG中声明和属性对应类型的同名变量

这样就可以在之后的Shader(着色器)逻辑中去利用它实现对应的逻辑了

我们需要掌握的就是ShaderLab属性类型和CG变量类型的对应关系

CG内置函数

知识点一 CG内置函数是什么?

Unity Shader中的CG语言

提供了各种用于图形编程的函数

这些函数是CG为我们封装好的逻辑

我们可以使用它们来编写Unity Shader

知识点二 有哪些内置函数?

1.数学函数

三角函数相关

sincos(float x, out s, out c) 该函数同时计算x的sin值和cos值通过s和c进行返回(比分别运算快很多)

sin(x) 正弦函数

cos(x) 余弦函数

tan(x) 正切函数

sinh(x) 双曲正弦函数

cosh(x) 双曲余弦函数

tanh(x) 双曲正切函数

asin(x) 反正弦函数,输入参数范围[-1,1],返回[-π/2,π/2]区间的角度值

acos(x) 反余弦函数,输入参数范围[-1,1],返回[0,π]区间的角度值

atan(x) 反正切函数,输入参数范围[-1,1],返回[-π/2,π/2]区间的角度值

atan2(y,x) 计算y/x的反正切值。和atan功能一样,只是输入参数不同。atan(x)=atan2(x,1)

向量、矩阵相关

cross(A,B) 叉乘(注意:传入向量必须是三维向量)

dot(A,B) 点乘(注意:传入向量必须是三维向量)

mul(M,N) 计算两个矩阵相乘

mul(M,v) 计算矩阵和向量相乘

mul(v,M) 计算向量和矩阵相乘

transpose(M) M为矩阵,计算M的转置矩阵

determinant(m) 计算矩阵的行列式因子

数值相关

abs(x) 返回输入参数的绝对值

ceil(x) 对输入参数向上取整

floor(x) 对输入参数向下取整

clamp(x,a,b) 如果x小于a,则返回a;x大于b,则返回b;否则,返回x("夹紧"函数)

radians(x) 角度转弧度

degrees(x) 弧度转角度

max(a,b) 返回最大值

min(a,b) 返回最小值

sqrt(x) 求x的平方根,x必须大于0

pow(x,y) x的y次方的值

round(x) 对x四舍五入

rsqrt(x) x的反平方根,x必须大于0

lerp(a,b,f) 差值函数,计算(1-f)*a + b*f 或者 a + f*(b-a)的值

exp(x) 计算e的x次方的值,e=2.71828182845904523536

exp2(x) 计算2的x次方的值

fmod(x,y) 返回x/y的余数,y不为0

frac(x) 返回标量或每个矢量分量的小数部分

frexp(x,out exp) 将浮点数x分解为尾数和直属,即 x = m * 2的exp次方,返回m,将指数存储exp中

isfinite(x) 判断标量或者向量中的每个数据是否是有限数,如果是返回true,否则返回false

isinf(x) 判断标量或者向量中的每个数据是否是无限,如果是返回true,否则返回false

isnan(x) 判断标量或者向量中的每个数据是否是非数据,如果是返回true,否则返回false

ldexp(x,n) 计算x * 2的n次方 的值

log(x) 计算ln(x)的值,x必须大于0

log2(x) 计算log2(x次方)的值,x必须大于0

log10(x) 计算log2(x次方)的值,x必须大于0

saturate(x) 如果x小于0,返回0;如果x大于1,返回1;否则,返回x

sign(x) 如果x大于0,返回1;如果x小于0,返回-1;否则,返回0

smoothstep(min,max,x) 值x位于min、max区间内,如果x=min,返回0;如果x=max,返回1;如果在两者之间,返回

-2* ((x-min)/(max - min))的三次方 + 3* ((x - min)/(max - min))的2次方

step(a,x) 如果x<a,返回0;否则,返回1

all(x) 输入参数均不为0,则返回true;否则返回False。相当于逻辑与&&

any(x) 输入参数只要有其中一个不为0,则返回true。相当于逻辑或||

其他

lit(NdotL,NdotH,m) N表示法向量;L表示入射光向量;H表示半角向量;m表示高光系数

noise(x) 噪声函数,返回值始终是0~1之间;对于相同的输入,始终返回相同值,不是真正意义的随机噪声

2.几何函数

length(v) 返回一个向量的模长

normalize(v) 归一化向量

distance(p1,p2) 计算两点之间的距离

reflect(I,N) 计算反射光方向向量,I为入射光,N为顶点法向量,。I是指向顶点的,I和N必须被归一化,必须是3维向量

refract(I,N,eta) 计算折射向量,I为入射光,N为顶点法向量,eta为折射系数。I是指向顶点的,I和N必须被归一化,必须是3维向量

3.纹理函数

注意:这些纹理采样函数返回值为 fixed4 类型的颜色值

二维纹理

tex2D(sampler2D tex, float2 s) 二维纹理查询

tex2D(sampler2D tex, float2 s, float2 dsdx, float2 dsdy) 使用导数值查询二维纹理

tex2D(sampler2D tex, float3 sz) 二维纹理查询,并进行深度值比较

tex2D(sampler2D tex, float3 sz, float2 dsdx, float2 dsdy) 使用导数值查询二维纹理,并进行深度值比较

tex2Dproj(sampler2D tex, float3 sq) 二维投影纹理查询

tex2Dproj(sampler2D tex, float4 szq) 二维投影纹理查询,并进行深度值比较

立方体纹理

texCUBE(samplerCUBE tex, float3 s) 查询立方体纹理

texCUBE(samplerCUBE tex, float3 s, float3 dsdx, float3 dsdy) 结合导数值查询立方体纹理

texCUBEDproj(samplerCUBE tex, float4 sq) 查询立方体投影纹理,并进行深度值比较

其他纹理

tex1D(sampler1D tex, float s) 一维纹理查询

tex1D(sampler1D tex, float s, float dsdx, float dsdy) 使用导数值查询一维纹理

tex1D(sampler1D tex, float2 sz) 一维纹理查询,并进行深度值比较

tex1D(sampler1D tex, float2 sz, float dsdx, float dsdy) 使用导数值查询一维纹理,并进行深度值比较

tex1Dproj(sampler1D tex, float2 sq) 一维投影纹理查询

tex1Dproj(sampler1D tex, float3 szq) 一维投影纹理查询,并进行深度值比较

texRECT(samplerRECT tex, float2 s) 矩形纹理查询

texRECT(samplerRECT tex, float2 s, float2 dsdx, float2 dsdy) 使用导数值查询矩形纹理

texRECT(samplerRECT tex, float3 sz) 矩形纹理查询,并进行深度值比较

texRECT(samplerRECT tex, float3 sz, float2 dsdx, float2 dsdy) 使用导数值查询矩形纹理,并进行深度值比较

texRECTproj(samplerRECT tex, float3 sq) 矩形投影纹理查询

texRECTproj(samplerRECT tex, float3 szq) 矩形投影纹理查询,并进行深度值比较

tex3D(sampler3D tex, float3 s) 查询三维纹理

tex3D(sampler3D tex, float3 s, float3 dsdx, float3 dsdy) 结合导数值查询三维纹理

tex3DDproj(sampler3D tex, float4 sq) 查询三维投影纹理,并进行深度值比较

总结

这节课的主要目的

是让大家对CG语言的内置函数有一个大致了解

大家需要把常用函数记录到自己的笔记当中,方便之后大家查阅使用

也可以通过以下链接来查看更多的相关函数

https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-intrinsic-functions

这是HLSL对应的内置函数,CG和它类似(注意:不是所有函数都在Unity中被支持)

CG内置文件

知识点一 CG内置文件的位置和作用

我们可以在Unity的安装目录中找到CG内置文件

在Editor—>Data—>CGIncludes中

后缀为cginc的文件为CG语言内置文件

后缀为glslinc的文件为GLSL语言内置文件

他们是预定义的Shader文件

里面包含了一些已经写好的Shader相关逻辑

作用和CG内置函数一样,可以提升我们的Shader开发效率

可以直接使用其中的方法等内容来进行逻辑开发

Unity中常用的内置文件有

1.UnityCG.cginc 包含最常用的帮助函数、宏和结构体等

2.Lighting.cginc 包含各种内置光照模型。如果编写的是Surface Shader(标准表面着色器),会自动包含进来

3.UnityShaderVariables.cginc 编译UnityShader时,会自动包含进来。包含许多内置的全局变量

4.HLSLSupport.cginc 编译UnityShader时,会自动包含进来。声明了很多用于跨平台编译的宏和定义

知识点二 如何使用CG内置文件

在CG语句块中进行引用

通过编译指令

#include "内置文件名.cginc"

的形式进行引用

我们便可以在CG语言中使用其中的内容

注意:一些常用的函数、宏、变量,可以不用引用,Unity会在编译时自动识别

但是为了避免报错,建议都引用

"宏"通常指的是一种预处理指令或代码片段,用于在代码中进行文本替换。

宏允许程序员定义一个标识符(通常以大写字母表示)来代表一个代码片段,

然后在编译时将这个标识符替换为相应的代码,这种替换过程称为宏展开。

说人话:就是为一些代码片段,取一个别名,方便使用。在真正编译时在把这个别名翻译成对应的代码。

知识点三 常用内容

方法(UnigyCG.cginc中)

1.float3 WorldSpaceViewDir(float4 v)

输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向

2.float3 ObjSpaceViewDir(float4 v)

输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向

3.float3 WorldSpaceLightDir(float4 v)

仅用于向前渲染中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。(返回值没有被归一化)

4.flaot3 ObjSpaceLightDir(float4 v)

仅用于向前渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。(返回值没有被归一化)

5.float3 UnityObjectToWorldNormal(float3 norm)

把法线方向从模型空间转换到世界空间中

6.float3 UnityObjectToWorldDir(in float3 dir)

把方向矢量从模型空间转换到世界空间中

7.float3 UnityWorldToObjectDir(float3 dir)

把方向矢量从世界空间转换到模型空间中

等等

结构体(UnigyCG.cginc中)

1.appdata_base

用于顶点着色器输入

顶点位置、顶点法线、第一组纹理坐标

2.appdata_tan

用于顶点着色器输入

顶点位置、顶点法线、顶点切线、第一组纹理坐标

3.appdata_full

用于顶点着色器输入

顶点位置、顶点法线、顶点切线、四组(或更多)纹理坐标

4.appdata_img

用于顶点着色器输入

顶点位置、第一组纹理坐标

5.v2f_img

用于顶点着色器输出

裁剪空间中的位置,纹理坐标

等等

变换矩阵宏(UnityShaderVariables.cginc中)

坐标空间变换顺序

模型空间 -> 世界空间 -> 观察空间 -> 裁剪空间 -> 屏幕空间

1.UNITY_MATRIX_MVP

当前的模型*观察*投影矩阵,用于将顶点/方向向量从模型空间变换到裁剪空间中

2.UNITY_MATRIX_MV

当前的模型*观察矩阵,用于将顶点/方向向量从模型空间变换到观察空间中

3.UNITY_MATRIX_V

当前的观察矩阵,用于将顶点/方向向量从世界空间变换到观察空间中

4.UNITY_MATRIX_P

当前的投影矩阵,用于将顶点/方向向量从观察空间变换到裁剪空间中

5.UNITY_MATRIX_VP

当前的观察*投影矩阵,用于将顶点/方向向量从世界空间变换到裁剪空间中

6.UNITY_MATRIX_T_MV

UNITY_MATRIX_MV的转置矩阵

7.UNITY_MATRIX_IT_MV

UNITY_MATRIX_MV的逆转置矩阵,用于将法线从模型空间变换到观察空间,也可用于得到UNITY_MATRIX_MV的逆矩阵

8._Object2World

当前的模型矩阵,用于将顶点/方向矢量从模型空间变换到世界空间

9._World2Object

_Object2World的逆矩阵,用于将顶点/方向矢量从世界空间变换到模型空间

等等

变量

1._Time (不用引用,直接使用即可)

自关卡加载以来的时间(t/20、t、t*2、t*3)用于对着色器内的事物进行动画处理

2._LightColor0 (向前渲染时,UnityLightingCommon.cginc;延迟渲染,UnityDeferredLibrary.cginc)

光的颜色

等等

总结

CG中的内置文件和内置函数一样

是用于帮助我们进行Shader开发的

利用其中的函数、宏、全局变量等内容

可以提升我们Shader的开发效率

我们需要做的是,将这些常用内容记到笔记中

之后进行查阅

如果想要了解更多的内置内容可以参阅Unity官网的资料

内置文件相关:https://docs.unity3d.com/Manual/SL-BuiltinIncludes.html

函数相关:https://docs.unity3d.com/Manual/SL-BuiltinFunctions.html

宏相关:https://docs.unity3d.com/Manual/SL-BuiltinMacros.html

变量相关:https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html

下一节

由于文章太长,这里切割到第二章