乌啦呀哈呀哈乌啦!

欢迎光临,这里是喵pass的个人博客,希望有能帮到你的地方

0%

顶点着色器 vs 片元着色器

One main difference is that a vertex shader can manipulate the attributes of vertices. which are the corner points of your polygons.
The fragment shader on the other hand takes care of how the pixels between the vertices look. They are interpolated between the defined vertices following specific rules.

白话:顶点着色器负责定位三角形位置!片段着色器负责修改像素颜色!!

顶点着色器(Vertex Shader)

顶点着色器是图形渲染管线中的第一个可编程阶段。它的主要任务是处理从CPU发送到GPU的顶点数据。每个顶点都会通过顶点着色器进行一次,通常用于执行以下操作:

  • 变换:将顶点从模型空间转换到世界空间,然后进一步转换到视图空间和投影空间。这通常涉及到矩阵乘法运算,用于实现平移、旋转和缩放等变换。
  • 光照计算(可选):在某些情况下,顶点着色器也用于执行初步的光照计算,但这通常是在更简单的渲染场景中,或者作为更复杂的片元级光照计算的一个初步步骤。
  • 传递数据:顶点着色器可以计算并传递额外的数据到后续的渲染阶段,如片元着色器。这些数据可以是颜色、纹理坐标或其他自定义属性。

片元着色器

片元着色器是图形渲染管线中处理像素级渲染的阶段。它接收由顶点着色器插值得到的片元(即屏幕上的像素或像素的候选者),并生成最终的颜色和其他与像素相关的数据。以下是片元着色器的一些主要用途:

  • 纹理映射:从纹理中读取颜色信息,并应用到相应的像素上。这可以用于实现贴图、细节增强等效果。
  • 光照计算:执行更详细的光照计算,如计算每个像素上的光照强度和颜色。这可以包括漫反射、镜面反射、环境光等多种光照模型。
  • 颜色混合和特殊效果:实现各种颜色混合模式,以及应用如模糊、发光、深度测试等后处理效果。
  • 输出最终颜色:基于上述计算,确定每个像素的最终颜色,并将其发送到渲染管线的下一个阶段(通常是帧缓冲区)。

Credits:

https://zhuanlan.zhihu.com/p/718015588


Vertex Shader的输出在Clip Space,然后GPU自己做透视除法变到了NDC( 取值范围[-1, 1] )。


裁剪空间

裁剪空间变换的思路是,对平截头体进行缩放,使近裁剪面和远裁剪面变成正方形,使坐标的w分量表示裁剪范围,此时,只需要简单的比较x,y,z和w分量的大小即可裁剪图元。
完全位于这块空间内部的图元将会被保留,完全位于这块空间外部的图元将会被剔除,与这块空间边界相交的图元就会被裁剪。而这块空间就是由视椎体来决定的。

In clip space, clipping is not done against a unit cube. It is done against a cube with side-length w. Points are inside the visible area if each of their x,y,z coordinate is smaller than their w coordinate.

In the example you have, the point [6, 0, 6.29, 7] is visible because all three coordinates (x,y,z) are smaller than 7.

透视投影矩阵


此时我们就可以按如下不等式来判断一个变换后的顶点是否位于视椎体内

正交投影矩阵


判断一个变换后的顶点是否位于视椎体内使用的不等式和透视投影中的一样,这种通用性也是为什么要使用投影矩阵的原因之一。


NDC

齐次除法将Clip Space顶点的4个分量都除以w分量,就从Clip Space转换到了NDC了。
而NDC是一个长宽高取值范围为[-1, 1]的立方体,超过这个范围的顶点,会被GPU剪裁。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。

透视投影除法

正交投影除法


细心一点会发现,齐次坐标对于透视投影空的裁剪空间变化更大,而对正交投影的裁剪空间没有影响(正交投影的裁剪空间中顶点的w已经是1了)。

视口变换(Viewport Transformation)

At this moment, we’re still in 3D space.How do we get to 2D space?

We need to transform our vertex from 3D NDC to 2D screen coordinates.

When initializing the Canvas, we are responsible for configuring its size. This size is used to convert our NDC coordinates to screen coordinates.

No Dependency Injection

不使用依赖注入,必须在依赖方(Dependent Class)中主动创建或者获取被依赖方(Denpendency Class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ClassA
{
private readonly ClassB _classB;

public ClassA()
{
_classB = new ClassB(); //主动创建对象B
}

public void Process()
{
Console.WriteLine("Class A start process");
_classB.DoSomething();
Console.WriteLine("Class A finish process");
}
}

public class ClassB
{
public void DoSomething()
{
// ClassB performs some logic
}
}

Denpendency Injection and IoC

使用依赖注入,不需要在依赖方的代码里主动创建或者获取被依赖方,反而,只需要在构造器参数里声明需要对象B的引用。

ClassA.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ClassA
{
private readonly IInterfaceB _b;

public ClassA(IInterfaceB b)
{
_b = b;
}

public void Process()
{
Console.WriteLine("Class A start process");
_b.DoSomething();
Console.WriteLine("Class A finish process");
}
}

在使用依赖注入时,更多时候,对于依赖更提倡使用接口,这样解耦了接口和实现:ClassA不需要知道ClassB的内部,只需要知道IClassB有个叫DoSomething的方法可以调用
并且,业务代码中不需要主动实例化对象,即无需这样主动调用构造函数 new ClassA(new ClassB())

ClassB.cs
1
2
3
4
5
6
7
public class ClassB : IClassB
{
public void DoSomething()
{
Console.WriteLine("class B is doing something ...");
}
}
IClassB.cs
1
2
3
4
public interface IClassB
{
void DoSomething();
}
Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
internal class Program
{
public static void Main(string[] args)
{
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddSingleton<ClassA>();
services.AddSingleton<IInterfaceB, ClassB>();
}).Build();

ClassA a = host.Services.GetRequiredService<ClassA>();
a.Process();
}
}

原本是在ClassA内部决定使用怎样的ClassB实例,使用了依赖注入设计后,这种控制关系(决定关系)变为由外部控制了,这就是所谓的“控制反转”(Inversion of Control)

  • Host 以及它内部的 Services可以理解为 C# 提供的依赖注入系统
  • 通过 GetRequiredService 可以获得对应的实例并执行业务逻辑(此处为ClassA实例)

使用依赖注入系统实例化对象

如果我们想要达到不需要手动实例化ClassA的效果,可以新建一个类并实现IHostedService接口

DoSomethingHostedService.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DoSomethingHostedService : IHostedService
{
private readonly ClassA _a;

public DoSomethingHostedService(ClassA a)
{
_a = a;
}

public Task StartAsync(CancellationToken cancellationToken)
{
_a.Process();
return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
修改 Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal class Program
{
public static async Task Main(string[] args)
{
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddSingleton<ClassA>();
services.AddSingleton<IInterfaceB, ClassB>();

services.AddHostedService<DoSomethingHostedService>();
}).Build();

await host.RunAsync();
}
}
  • IHostedService 是一个特殊的接口,实现这个接口的类通过在Host中的services里注册后,可以在Host运行时自动实例化并执行
  • services.AddHostedService<>() 用于注册要自动执行的类
  • await host.RunAsync() 运行Host实例

Credits:

https://zhuanlan.zhihu.com/p/592698341


坐标空间有:世界空间、模型空间、摄像机空间、齐次裁剪空间、屏幕空间,以及法线映射会用到的切线空间(之前的纹理基础篇就讲到过)。

那为什么会有这么多个坐标空间呢?

一些概念只有在特定的坐标空间下才有意义,才更容易理解。这就是为什么在渲染中我们要使用这么多坐标空间。

——《Unity Shader 入门精要》

  • 坐标空间转换:在渲染管线中,把一个点或一个向量从一个坐标空间转换到另一个坐标空间,比如模型空间 -> 裁剪空间
  • 变换矩阵:实现坐标空间转换的过程,则需要用到变换矩阵
  • 顶点着色器:顶点着色器是图形渲染管线中对每个顶点进行坐标变换的程序,而MVP变换(模型Model - 视图View - 投影Projection)是顶点着色器中一种将顶点坐标从模型空间转换为裁剪空间的常用技术。

1. 模型空间(Model Space)

又称为对象空间(Object Space)或局部空间(Local Space),每个模型都有属于自己的模型空间

  • 以模型本身为参考系,会随着模型的旋转/移动而旋转/移动
  • 包含前、后、左、右等自然方向概念.

在模型空间中描述模型上的某一点位置时,坐标会被扩展到齐次坐标系下:(1,0,0) -> (1,0,0,1),为顶点变换中的平移变换做准备

顶点变换Step 1 - 模型变换(MVP中的M)

Model Transformation 把3D物体从模型空间变换到世界空间

在Unity中,我们直接给Cube拖出来就行

我们发现,Cube位置从(0, 2, 4, 1) -> (9, 4, 18.071),unity帮助我们把Cube的坐标完成了模型变换


2. 世界空间(World Space)

游戏场景中的最大空间,一般世界空间的原点会放在游戏空间的正中心,同时世界空间的位置就是绝对位置——这个绝对位置你可以理解成Unity里没有父节点(parent)的游戏对象的位置。

顶点变换Step 2 - 观察变换(MVP中的V)

View Transformation 把3D物体从世界空间变换到观察空间

此时的变换矩阵等于:把观察空间当做一个模型空间,将其变换到世界空间(用模型变换的方法),然后取此变换矩阵的逆即为 观察空间 <- 世界空间

在unity中,我们可以直接把Cube从世界空间拖到Main Camera下,此时Cube的Transform组件变为观察空间下的坐标信息

我们发现Cube位置变为(9, 8.839, 27.310),但此时Transform信息是左手坐标系下的,因此正确描述在观察空间中的Cube坐标应为(9, 8.839, -27.310)


3. 观察空间(View Space)

也叫做摄像机空间(Camera Space),摄像机在场景中不可见,但是一般会给它生成一个图标并在场景窗口可视化,例如:

  • In Unity’s camera/view space, the forward direction (the direction the camera is looking) is along the negative Z-axis. This means objects further away from the camera will have larger negative Z values in view space.
  • 注意区分观察空间屏幕空间,观察空间是3d,屏幕空间是2d,观察空间 -> 屏幕空间需要经过投影操作

顶点变换Step 3 - 投影变换(MVP中的P)

Projection Transformation 把3D物体从观察空间变换到裁剪空间

观察空间->裁剪空间的变换矩阵有更准确的称呼 —— 裁剪矩阵(Clipping Matrix),也被叫做投影矩阵(Projection Matrix)。
此时的坐标为顶点着色器的输出,即图元顶点在裁剪空间中的坐标。


4. 裁剪空间(Clip Space)

也被称为其次裁剪空间,渲染管线中几何阶段的裁剪步骤在这一环节完成,这一环节里我们无法操控(Non-programmable),完全由GPU去做

4.1 视锥体

从观察空间到屏幕空间的途中,需要经过裁剪空间,其目的在于剔除在视野范围外的图元,而由视锥体(Frustum)决定的裁剪空间为这一剔除过程提供了便利。
很显然,场景越大,裁剪的优越性更加突出,如果不进行裁剪就直接投影到2d屏幕空间,后续会产生非常多不必要的开销,例如渲染完全在电脑屏幕外的图元。

视锥体是观察空间中的一块区域,决定着摄像机的可见范围(即最终在屏幕上可见并渲染的物体),它由六个面组成,被称为裁剪平面(Clipping Planes)。

4.1.1 透视投影

  • FOV: 视角度数,同时FOV Axis决定这个视角是横向还是纵向
  • Clipping Planes: 设置近裁剪平面距离 和 远裁剪平面距离
  • Viewport Rect: This refers to the Camera.rect property, which defines the portion of the screen where a camera’s view is drawn. By adjusting these values, you can control where a camera renders on the screen and how much of the screen it occupies. This config is commonly used for split-screen effects.
  • Depth: This property controls the order in which multiple cameras in a scene render their output. A camera with a lower depth value renders before a camera with a higher depth value. This is crucial for achieving effects like picture-in-picture, UI overlays, or rendering specific layers with different cameras. If multiple cameras have the same depth value, their rendering order is determined by their order in the scene hierarchy.

4.1.2 正交投影

4.2 投影矩阵的目的

前面已经讨论过了裁剪的必要性 —— 进行渲染提出视野范围外的图元;这里需要讨论的是,为何不在视锥体裁剪,而是要先变换到裁剪空间再进行裁剪?

《Unity Shader 入门精要》中做了很清楚的解释:“直接在视锥体定义的空间进行裁剪,对于透视投影的视锥体想要判断一个顶点是否在这个空间内是十分麻烦的,我们需要一种更加通用、整洁方便的方式进行裁剪,因此就需要一个投影矩阵将顶点转换到一个裁剪空间(Clip space)中。”

从观察空间到裁剪空间的变换叫做投影变换。虽然叫做投影变换,但是投影变换并没有进行真正的投影。

4.2.1 为真正的投影做准备

真正的投影可以理解成空间的降维,4d -> 3d,3d -> 2d,真正的投影发生在屏幕映射过程中对顶点进行齐次除法后获得其二维坐标这一步,而投影矩阵只是进行了坐标空间转换,并没有实实在在地进行投影这一操作。
齐次(裁剪)空间实质上是一个四维空间,变换到齐次空间的顶点之间仍然是线性相关的,可以使用线性插值。(此时没有除以W变成3D坐标,是齐次坐标)

4.2.2 对x、y、z进行缩放

投影矩阵虽然叫做投影矩阵,但并没有真正进行投影,而是为投影做准备。经过投影矩阵的缩放后,我们可以直接使用w分量作为范围值,只有x,y,z分量都位于这个范围内的顶点才认为是在裁剪空间内。并且w分量在真正的投影时也会用到。


标准化设备坐标(Normalized Device Coordinate)

在齐次裁剪空间的基础上进行透视除法(Perspective division)或称齐次除法(Homogeneous division),得到的坐标叫做NDC空间坐标。

  • 裁剪空间是顶点乘以MVP矩阵之后所在的空间,Vertex Shader的输出就是在裁剪空间上(划重点)。
  • 接着由GPU自己做透视除法,将顶点转移到标准化设备坐标(NDC)。


5. 屏幕空间(Screen Space)

完成了裁剪工作,下一步就是进行真正的投影了,将视锥体投影到屏幕空间中,这一步会得到真正的像素位置,而不是虚拟的三维坐标。
这一环节可以理解为做了以下两步:

5.1 齐次除法

首先进行标准齐次除法(homogeneous division),也被称为透视除法(perspective division),其实就是x、y、z分别除以w,经过齐次除法后的裁剪空间会变成一个单位立方体,这个立方体空间里的坐标叫做归一化的设备坐标(也就是之前提到的NDC)。因此,也可以说齐次除法是做了空间裁剪坐标到NDC坐标的转换操作。

5.2 屏幕映射(渲染管线中几何阶段的一步)

这里就顺利的跟之前的渲染管线GPU负责的几何阶段部分联系在一起了。在获得了NDC立方体后,接下来就是根据变换后的x、y坐标映射输出窗口对应的像素坐标,本质就是个缩放的过程。

  • 虽然屏幕是2d空间,但z分量此时并没有被抛弃,会被储存起来(深度缓存或者其他的储存格式)

我们前面说到Vertex Shader的输出在Clip Space,接着GPU会做透视除法变到NDC。这之后GPU还有一步,应用视口变换(Viewport Transformation),转换到屏幕空间,输入给Fragment Shader:

(Vertex Shader) => Clip Space => (透视除法) => NDC => (视口变换) => Window Space => (Fragment Shader)


Credits:

MVP矩阵:https://blog.csdn.net/qq_41835314/article/details/126851074



CPU + GPU工作流程

  1. 剔除,把一些不想看到的,或者看不到的东西排除掉
  2. 确定物体的先后渲染顺序
  3. 将对应的模型数据、材质等打包发送给GPU
  4. 发送SetPassCall和Drawcall告诉GPU渲染管线渲染模型数据所需的shader
  5. 数据在GPU渲染管线中绘制,将3D物体渲染为2D图像
  6. 将渲染图像存放在帧缓冲区(FrameBuffer)中。可以理解为一个和屏幕大小等大的临时画布
  7. 后处理操作。通过CPU拿到帧缓冲区的图像,调用shader再进入GPU渲染管线,对帧缓冲区的图像进行二次的修改,比如调色、bloom等操作
  8. 显示在屏幕上

CPU应用程序端逻辑:经过剔除、排序等等,将模型数据打包发给GPU渲染管线
GPU渲染管线:拿到模型数据后,讲图像画出来,存放在对应的帧缓冲区中


GPU渲染管线

  1. CPU打包数据:vertex buffer/index buffer/frustum视锥/directional light平行光/texture+shader
    • SetPass Call:设定好渲染设置后,告诉GPU使用哪个shader,使用哪种混合模式、设置背面剔除等等,当使用不同的材质或者相同的材质下不同的Pass时需要设置切换多个渲染状态,就会增加SetPassCall,所以SetPassCall的次数也能反映性能
    • Draw Call(绘制调用):CPU打包数据发送给GPU,告诉GPU使用哪些模型数据进行渲染
  2. Vertex Shader: 使用顶点/视锥信息将模型空间变换到屏幕空间
  3. Triangle Processing 图元装配:把顶点信息按照缓存连接成三角形图元
  4. Rasterization:插值计算出三角形每个像素的深度/颜色
  5. Pixel Shader (i.e. Fragment Shader):根据灯光/纹理贴图采样
  6. Frame Buffer:post processing 抗锯齿 校色 景深(DOF) 动态模糊


图元 vs 片元

  • 图元

渲染图元(rendering primitives)为图形渲染开发接口中用来描述各种图形元素的图形数据,所对应的就是绘图界面上看得见的实体,它包括了渲染所需的几何信息,可以是顶点数据、线段、多边形等。


图元至少要包含一个顶点(Vertex);一个顶点定义了2D或3D坐标系中一个点,也同样定义了若干个可以影响如何把顶点渲染到屏幕上的属性,如:


  • 片元

在GPU流水线中的三角形遍历阶段,将会检查每个像素是否被一个三角网格所覆盖;如果被覆盖的话,就会生成一个片元(fragment);需要注意的是,一个片元并不是真正意义上的像素,而是包括了很多状态的集合,这些状态用于计算每个像素的最终颜色;这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其它从几何阶段输出的顶点信息,例如法线、纹理坐标等。


Credits

https://blog.csdn.net/lel18570471704/article/details/134708949
https://zhuanlan.zhihu.com/p/667522425
https://zhuanlan.zhihu.com/p/407046161
图元vs片元:https://blog.csdn.net/DoomGT/article/details/115806364


固定坐标系下,一个对象的变换等价于固定对象所处的坐标系变换。


经过线性变换(直线依旧是直线,保持网格线平等且等距分布,并且原点保持固定):


如图,新坐标系的基就是那个绿箭头和红箭头,在原来的ij坐标系下的坐标值是[1,-2]和[3,0]。经过如图的计算过程,坐标系的变化,导致原来的V向量变成了[5,2],实现了移动。

Transformation Matrix

常见的变换矩阵类型:

四元数的定义

四元数(Quaternion)是带有一个实部和三个虚部的一种扩展复数,有两种表达方式

  1. 标量形式

  2. 标量+向量形式

由于标量表达式中的 i,j,k 可以理解为相互正交的3个单位向量,于是四元数也可以表示为标量+向量的形式

四元数相关运算

存在qa,qb两个四元数

共轭(conjugate):

取逆(inverse):

取模(magnitude):

乘法:

四元数表示旋转

从二维平面复数的乘法中,我们可知:
表示某条射线的复数 和 表示旋转角度的复数,其乘积为该射线按旋转角度旋转后的射线所表示的复数,那么四元数可以理解为其在三维空间的拓展应用。
实际上,四元数是一种表示三维姿态的方法,其特点是紧凑、易于迭代、又不会出现奇异值。


Unity C#四元数代码实现旋转

从欧拉角转换为四元数
1
2
Vector3 eulerAngles = new Vector3(45f, 30f, 60f); // 欧拉角
Quaternion quaternion = Quaternion.Euler(eulerAngles); // 转换为四元数
从欧拉角转换为四元数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RotateByQuaternion : MonoBehaviour
{
public float rotateSpeed = 300.0f; // 旋转速度

void Update()
{
// 如果用户按下鼠标左键,则旋转对象
if (Input.GetMouseButton(0))
{
// 获取鼠标移动的x和y量
float mouseX = Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime;
float mouseY = Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime;

// 创建一个四元数,表示绕Vector3.up轴旋转mouseY度,绕Vector3.right轴旋转mouseX度
Quaternion rotation = Quaternion.AngleAxis(mouseY, Vector3.up) * Quaternion.AngleAxis(mouseX, Vector3.right);

// 应用这个旋转到游戏对象上
transform.rotation *= rotation;
}
}
}

Credits

利用四元数表示空间向量的旋转:https://blog.csdn.net/qq_42648534/article/details/124072859


Unity 2022.3 Camera Component


相机参数

Clear Flags(清除标记):

A camera in Unity holds info about the displayed object’s colors and depths.
In this case, depth info means the game world distance from the displayed object to the camera.

When a camera renders an image it can get rid of all or just some of the old image information and then displays the new image on top of what’s left.
Depending on what clear flags you select, the camera gets rid of different info.

  • The skybox clear flag means that when the camera renders a new frame, it clears everything from the old frame and it displays the new image on top of a skybox.
  • The solid color clear flag means that when the camera renders a new frame, it clears everything from the old frame and it displays the new image on top of a solid color.
  • The depth only clear flag means that when the camera renders a new frame, it clears only the depth information from the old frame and it keeps the color information on top of which displays the new frame. This means that the old frame can still show but it doesn’t have depth information, meaning the new objects to be displayed can’t be shown as intersected or obstructed by the old objects, because there’s no information about how far the old objects are(depth info was cleared). So the new objects will be on top of the old ones, mandatory.

Background(背景):

The color applied to the remaining screen after (1) all elements in view have been drawn and (2) there is no skybox.

Culling Mask(剔除遮罩):

Includes or omits layers of objects to be rendered by the Camera.

Projection(投射方式):

  • Perspective(透视): Camera will render objects with perspective intact.
    • Field of view: The Camera’s view angle, measured in degrees along the axis specified in the FOV Axis drop-down.
  • Orthographic(正交): Camera will render objects uniformly, with no sense of perspective. NOTE: Deferred rendering is not supported in Orthographic mode. Forward rendering is always used.
    • Size: The viewport(The user’s visible area of an app on their screen.) size of the Camera when set to Orthographic.

Clipping Planes(剪裁平面):

Distances from the camera to start and stop rendering.

  • Near(近点): The closest point relative to the camera that drawing will occur.
  • Far(远点): The furthest point relative to the camera that drawing will occur.

Normalized Viewport Rect(标准视图矩形):

Four values that indicate where on the screen this camera view will be drawn. Measured in Viewport Coordinates (values 0–1).

  • X: The beginning horizontal position that the camera view will be drawn.
  • Y: The beginning vertical position that the camera view will be drawn.
  • W: Width of the camera output on the screen.
  • H: Height of the camera output on the screen.
    It’s easy to create a two-player split screen effect using Normalized Viewport Rectangle. After you have created your two cameras, change both camera’s H values to be 0.5 then set player one’s Y value to 0.5, and player two’s Y value to 0. This will make player one’s camera display from halfway up the screen to the top, and player two’s camera start at the bottom and stop halfway up the screen.

Depth(相机深度):

The camera’s position in the draw order. Cameras with a larger value will be drawn on top of cameras with a smaller value.

Rendering Path(渲染路径):

Options for defining what rendering methods will be used by the camera.

  • Use Graphics Settings: This camera will use whichever Rendering Path is set in the Project Settings -> Player
  • Forward(快速渲染): 摄像机将所有游戏对象将按每种材质一个通道的方式来渲染。对于实时光影来说,Forward的消耗比Deferred更高,但是Forward更加适合用于半烘焙半实时的项目。Forward解决了一个Deferred没能解决的问题:Deferred不能让Mixed模式的Directional Light将动态阴影投射在一个经过烘焙了的静态物体上。
  • Deferred(延迟光照): 最大的特点是对于实时光影来说的性能消耗更低了,这个模式是最适合动态光影的。对于次时代PC或者主机游戏, 当然要选择这个。次时代游戏几乎不需要烘焙光照贴图了,全都使用实时阴影是很好的选择。通过阴影距离来控制性能消耗。而在Viking Village的场景中,由于整个场景全部使用了动态光源,Forward的Rendering方面的性能消耗要比Deferred高出一倍! 因此在完全使用动态光源的项目中千万不能使用Forward。
  • Vertex Lit(顶点光照): All objects rendered by this camera will be rendered as Vertex-Lit objects.

Rendering path is the technique that a render pipeline uses to render graphics. Choosing a different rendering path affects how lighting and shading are calculated. Some rendering paths are more suited to different platforms and hardware than others.

  • Forward vs Deferred Rendering

Forward rendering does all of the work for rendering geometry up front when it gets submitted to the rendering pipeline. You submit your geometry and materials to the pipeline, and at some point the fragments(pixels) that represent your geometry are calculated and invokes a fragment shader to figure out the final “color” of the fragment based on where it is located on the screen (render target). This is implemented as logic in a pixel shader that does the expensive lighting and special effects calculations on a per-pixel basis.

The inefficiency with this approach is, when pixels get overwritten by geometry submitted later in the pipeline that appear in front of it. You did all of that expensive work for nothing. Enter deferred rendering:

Deferred rendering should really be called deferred shading because the geometry is (or can be) submitted to the pipeline very much in the same way as forward rendering. The difference is that the result is not actually a final color value for the final image. The pipeline is configured in a way such that instead of actually going through with the calculations, all of the information is stored in a G-buffer to do the calculations later. That way, this information can be overwritten multiple times, without ever having calculated the final color value until the last fragment’s information is written. At the very end, the entire G-buffer is processed and all of the information it stored is used to calculate the final color value.

Last words:

Neither technique is really harder to learn. We’re just coming from a forward rendering past. Once you have a good grasp of deferred rendering (and perhaps have a solid computer graphics background), you realize it’s just another way to do things.

It’s hard to say which technique is better. As pixel/fragment shaders get more GPU processing expensive, deferred shading becomes more effective. If early Z testing (https://docs.unity3d.com/6000.0/Documentation/Manual/SL-ZTest.html) can be employed or other effective culling techniques are used, deferred shading becomes less important. G-buffers also take up a lot of graphics memory.

Target Texture(目标纹理):

相机渲染不再显示在屏幕上,而是映射到纹理上。一般用于制作导航图或者画中画等效果。

Reference to a Render Texture that will contain the output of the Camera view. Setting this reference will disable this Camera’s capability to render to the screen.
Render Texture is a special type of Texture that is created and updated at runtime. To use them, first create a new Render Texture and designate one of your Cameras to render into it. Then you can use the Render Texture in a Material just like a regular Texture.

Occlusion Culling(遮挡剔除):

Occlusion culling is a process which prevents Unity from performing rendering calculations for GameObjects that are completely hidden from view (occluded) by other GameObjects, for example if they are behind walls. (https://docs.unity.cn/Manual/OcclusionCulling.html)

  • Unity can bake static GameObjects (does not move at runtime) into the occlusion culling data as a Static Occluder and/or a Static Occludee.
  • Unity cannot bake dynamic GameObjects into the occlusion culling data. A dynamic GameObject can be an occludee at runtime, but it cannot be an occluder.

Allow HDR(渲染高动态色彩画面):

Enables High Dynamic Range rendering for this camera.

Allow MSAA(硬件抗锯齿):

Enables multi sample antialiasing for this camera.

Allow Dynamic Resolution(动态分辨率渲染):

Enables Dynamic Resolution rendering for this camera.

Target Display(目标显示器):

A camera has up to 8 target display settings. The camera can be controlled to render to one of up to 8 monitors. This is supported only on PC, Mac and Linux. In Game View the chosen display in the Camera Inspector will be shown.


Credits

Unity documentation: https://docs.unity.cn/Manual/class-Camera.html
Unity 摄像机参数介绍:https://blog.csdn.net/Scopperil/article/details/80440448


  • VAO(vertex-array object)顶点数组对象,用来管理VBO。
  • VBO(vertex buffer object)顶点缓冲对象,用来缓存用户传入的顶点数据。
  • EBO(element buffer object)索引缓冲对象,用来存放顶点索引数据。

A VAO is an array of VBOs


一、什么是OpenGL

OpenGL是一套方便于用户使用的规范,而其本身包含了调用不同厂商直接在GPU中写好的程序接口,那些接口完成所有的功能实现,如完成2D、3D矢量图形渲染等功能(跨语言,跨平台)。

OpenGL by itself is not an API, but merely a specification. The OpenGL specification specifies exactly what the result/output of each function should be and how it should perform. It is then up to the developers implementing this specification to come up with a solution of how this function should operate.
The people developing the actual OpenGL libraries are usually the graphics card manufacturers. Each graphics card that you buy supports specific versions of OpenGL which are the versions of OpenGL developed specifically for that card (series).

OpenGL vs OpenCL

  1. 总体来说,OpenGL 主要做图形渲染,OpenCL 主要用 GPU 做通用计算。图形渲染的主要特点是 渲染管线基本单元,如光栅化、深度测试、模板测试、混合等等的实现。
  2. OpenGL 可以用 Compute Shader 实现 OpenCL 同样的功能,但一般厂商对 Compute Shader 中低精度计算的支持(fp16 / int8 等)不如 OpenCL ,性能会差一些
  3. 基于 OpenCL 编程可以自己实现 OpenGL 中的渲染操作,但由于没有图形接口,实现渲染管线基本单元效率较低。
  4. OpenCL 和 OpenGL 最终实现都是往 GPU 发 Command Buffer ,不会互相影响,最多就是互相抢GPU计算资源。但如果有数据依赖关系,因为管线不同,两者是需要做额外同步的。

opengl里叫drawcall,opencl里叫enqueue,vulkan里叫commandbuffer,虽然叫法不一样,但目的都是把指令从CPU发到GPU上运算。

cuda其实就是把kernel代码和c代码混合到一起写而已,最终也要把数据发到GPU去算,属于编译器支持的隐式发送。就好比写c/c++调其他库api的时候,可以配置好库lib或者so位置,包个头文件就直接用,也可以自己动态加载对应库API一样。

OpenGL渲染过程

因为c++写的程序都是在cpu上运行的,但是OpenGL的接口是在GPU上运行的,而且OpenGL并不能凭空做程序中的数据或者是取代一些程序上的事,它是一种状态机,程序从cpu发数据到缓冲区并且告诉GPU你从哪一块缓冲区取数据画什么,然后提前设计好的着色器开始根据数据画图最后显示在显示器上。

画图渲染的顺序如下:

  1. 声明一个缓冲区
  2. 声明之后需要绑定,因为在GPU中的缓冲区都是有编号的,或者说是有管理的
  3. 现在要给一个缓冲区塞数据,每个接口函数都可以通过说明文档来查看参数的意义和使用
  4. 我们需要告诉着色器我们的数据是怎么样的, 或者说是怎么处理这些数据

glVertexAttribPointer函数

1
2
void glVertexAttribPointer(GLuint index, GLint size, GLenum type,
GLboolean normalized, GLsizei stride, const GLvoid * pointer);

人话就是:

index:我们从第几个顶点开始访问
size:一个顶点属性值里面有几个数值
type:每个值的数据类型
normalized:是否要转化为统一的值
stride:步幅 每个顶点属性值的大小,就是到下一个顶点的开始的字节偏移量。
pointer:在开始访问到顶点属性值的时候开始的指针位置(注意和Index的区别)
其实你就是把顶点属性值想象成结构体就行了,然后多个结构体一起存,和网络传输一样,我发送给了另一边需要解析网络包,是不是需要找我结构体开始的位置,然后一个结构体的大小,然后结构体对齐里面有什么,分别解析,还有步幅指针,我还可以跳过结构体,是一样的道理。

二、VBO

glVertex

最原始的设置顶点方法,在glBegin和glEnd之间使用。OpenGL3.0已经废弃此方法。每个glVertex与GPU进行一次通信,十分低效。

1
2
3
4
5
glBegin(GL_TRIANGLES);
glVertex(0, 0);
glVertex(1, 1);
glVertex(2, 2);
glEnd();

顶点数组 Vertex Array

顶点数组也是收集好所有的顶点,一次性发送给GPU。不过数据不是存储于GPU中的,绘制速度上没有显示列表快,优点是可以修改数据。

1
2
3
4
5
#define MEDIUM_STARS 40
M3DVector2f vMediumStars[MEDIUM_STARS];
//在这做点vMediumStars的设置//
glVertexPointer(2, GL_FLOAT, 0, vMediumStars);
glDrawArrays(GL_POINTS, 0, MEDIUM_STARS);

VBO 顶点缓冲对象

VBO,全称为Vertex Buffer Object,与FBO,PBO并称,但它实际上老不少。就某种意义来说,他就是VA(Vertex Array)的升级版。VBO出现的背景是人们发现VA和显示列表还有让人不满足的地方。一般,在OpenGL里,提高顶点绘制的办法:

(1)显示列表:把常规的glBegin()-glEnd()中的代码放到一个显示列表中(通常在初始化阶段完成),然后每遍渲染都调用这个显示列表。
(2)VA:使用顶点数组,把顶点以及顶点属性数据作为数组,渲染的时候直接用一个或几个函数调动这些数组里的数据进行绘制,形式上是减少函数调用的次数(告别glVertex),提高绘制效率。

但是,这两种方法都有缺点。VA是在客户端设置的,所以执行这类函数(glDrawArray或glDrawElement)后,客户端还得把得到的顶点数据向服务端传输一次(所谓的“二次处理”),这样一来就有了不必要的动作了,降低了效率——如果我们写的函数能直接把顶点数据发送给服务端就好了——这正是VBO的特性之一。显示列表的缺点在于它的古板,一旦设定就不容许修改,所以它只适合对一些“固定”的东西的绘制进行包装。(我们无办法直接在硬件层改顶点数据,因为这是脱离了流水线的事物)。
而VBO直接把顶点数据交到流水线的第一步,与显示列表的效率还是有差距,但它这样就得到了操作数据的弹性——渲染阶段,我们的VBO绘制函数持续把顶点数据交给流水线,在某一刻我们可以把该帧到达了流水线的顶点数据取回客户端修改(Vertex mapping),再提交回流水线(Vertex unmapping),或者用 glBufferData/glBufferSubData 重新全部或buffer提交修改了的顶点数据,这是VBO的另一个特性。

VBO结合了VA和显示列表这个说法不太妥当,应该说它结合了两者的一些特性,绘制效率在两者之间,且拥有良好的数据更改弹性。这种折衷造就了它一直为目前最高的地位。

当我们在顶点着色器中把顶点传送到GPU中的时候,不能每个顶点数据传送一次,因为太耗时而且造成资源浪费,所以就要用到缓冲对象,我们把大量的数据存储在GPU内存上,然后一次传输大量数据到显卡上,顶点缓冲对象就是帮助我们来管理GPU内存的。

顶点缓冲流程

首先我们需要使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据,一定要使用该函数来配置各个属性的数据,因为顶点数据不只包含位置,还可能会包含顶点颜色、顶点法线等等,那一个顶点数据是如何被OpenGL解析然后放入到顶点着色器的各个属性中,就需要通过该函数进行准确的配置。
每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVetexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的,因为同一个类型的缓冲区同时最多绑定一个目标缓冲。

三、VAO

VAO 顶点数组对象

VAO并不是必须的,VBO可以独立使用,VBO缓存了数据,而数据的使用 方式(glVertexAttribPointer 指定的数据宽度等信息)并没有缓存,VBO将顶点信息放到GPU中,GPU在渲染时去缓存中取数据,二者中间的桥梁是GL-Context。GL-Context整个程序一般只有一个,所以如果一个渲染流程里有两份不同的绘制代码,当切换VBO时(有多个VBO时,通过glBindBuffer切换 ),数据使用方式信息就丢失了。而GL-context就负责在他们之间进行切换。这也是为什么要在渲染过程中,在每份绘制代码之中会有glBindbuffer、glEnableVertexAttribArray、glVertexAttribPointer。VAO记录该次绘制所需要的所有VBO所需信息,把它保存到VBO特定位置,绘制的时候直接在这个位置取信息绘制。 

VAO的全名是Vertex Array Object,首先,它不是Buffer-Object,所以不用作存储数据;其次,它针对“顶点”而言,也就是说它跟“顶点的绘制”息息相关。我们每一次绘制的时候,都需要绑定缓冲对象以此来拿到顶点数据,都需要去配置顶点属性指针以便OpenGL知道如何来解析顶点数据,这是相当麻烦的,对一个多边形而言,它每次的配置都是相同的,如何来存储这个相同的配置呢。
VAO为我们解决了这个大麻烦,当配置顶点属性数据的时候,只需要将配置函数调用执行一次,随后再绘制该物体的时候就只需要绑定相应的VAO即可,这样,我们就可以通过绑定不同的VAO(提醒,与VBO一样,同一时刻只能绑定一个VAO),使得在不同的顶点数据和属性配置切换变得非常简单。VAO记录的是一次绘制中所需要的信息,这包括“数据在哪里glBindBuffer”、“数据的格式是怎么样的glVertexAttribPointer”、shader-attribute的location的启用glEnableVertexAttribArray。

1
2
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

VAO的使用非常简单,要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,配置完以后,VAO中就存储了我们想要的各种数据,之后解绑VAO供之后使用,再次使用需要我们再次绑定。

注意:glVertexAttribPointer()这个函数会自动从当前绑定的VBO里获取顶点数据,所以在第一节VBO里如果想要使用正确的顶点数据,每次都要绑定相应的VBO,但是现在,我们在绑定VBO之前绑定了VAO,那么glEnableVertexAttribPointer()所进行的配置就会保存在VAO中。我们可以通过不同的配置从一个VBO内拿到不同的数据放入不同的VAO中,这样VAO中就有了我们所需要的数据,它根据顶点配置到VBO中索取到数据,之后直接绑定相应的VAO即可,glDrawArrays()函数就是需要从VAO中拿到数据进行绘制。但是要明白的是,我们是通过VAO来间接绑定VBO的,实际上数据还是要存储在VBO中的,VAO内并没有存储顶点的数据,如果我们要绘制两个不同的三角形,我们可能需要定义两个VBO,每个VBO内有三个顶点数据。


Credits

OpenGL: https://learnopengl.com/Getting-started/OpenGL
VAO和VBO: https://blog.csdn.net/p942005405/article/details/103770259


Common Language Runtime

公共语言运行时

VM和LR其实比较类似,有人说LR的创建就是为了对标VM。简单来说,就是一个程序运行所需要的环境,包括各种资源、各种操作等等。不同语言、不同操作系统所需要的运行时环境都不一样。举个例子,Windows上的可执行程序都被包装成了.exe格式,而这种.exe格式文件提供了一个程序从加载到运行所需要的所有资源和环境。

而CLR提供了:

  1. 一个支持GC的虚拟机,该虚拟机有自己的一套指令集,即CIL(公共中间语言,COmmon Intermediate Language)。高级语言最终会转化成CIL,
  2. 一种丰富的元数据表示,用来描述数据类型、字段、方法等。通过这些统一的描述方法来生成对应的程序。
  3. 一种文件格式,一种专属的不于操作系统和硬件绑定的格式,即跨平台。
  4. 一套类库,提供了垃圾回收、异常、泛型等基本功能,提供了字符串、数组、列表、字典等数据结构,提供了文件、网络、交互等操作系统功能。
  5. 一系列规则,定制了在运行时如果查找引用其他文件、生命周期等一系列规则。


Common Language Specification

CLR最大的优势就在于跨语言跨平台支持。目前微软已经为多种语言开发旅了基于CLR的编译器,包括C++、C#、Visual Basic、F#、Iron Python、 Iron Ruby和IL。还有一些大学、公司和机构为一些语言也开发了基于CLR的编译器,包括da、APL、Caml、COBOL、Eiffel、Forth、Fortran、Haskell、Lexicon、LISP、LOGO、Lua、Mercury、ML、Mondrian、Oberon、Pascal、Perl、PHP、Prolog、RPG、Scheme、Smaltak、Tcl/Tk等。
CLR为不同的编程语言提供了统一的运行平台,对于开发者来说,他们无需考虑平台运行问题,无论使用什么语言开发,最终都会编译成IL,供CLR运行。对于CLR来说,它并不知道也无需知道IL是从什么语言编译过来的。

但是这么多种各式各样的语言最终都要编译成IL,肯定需要一种规范,CLS就是用来规范语言的。CLS全称Common Language Specification,即公共语言规范。也就是说所有被CLR支持的高级语言都需要最少支持CLS所规定的功能集。只要高级语言最少支持了CLS之后,其它附加功能/特性可自行实现。

Managed Code

C#中的托管代码是指由.NET运行时环境(CLR)管理和执行的代码。当我们使用C#编写的代码被编译后,它会被转换成中间语言(IL)代码,也称为托管代码。托管代码在运行时由CLR加载和执行,CLR负责内存管理、垃圾回收、安全性等任务,开发者无需过度关注资源的释放。其实可以从字面上理解,托管代码委托给CLR进行管理,开发者不管。
而至于非托管代码是指不受CLR管理的代码,通常是使用其他编程语言(如C++)编写的代码,比如操作系统代码、C#中的Socket、Stream等,这些代码无法通过CLR的GC完全释放占用的资源。一般来说,非托管的功能都被包装过了,比如当我们访问文件的时候,肯定不会直接使用操作系统的CreateFile函数,而是使用System.IO.File类。

托管代码具有以下特点:
自动内存管理:CLR负责分配和释放内存,开发人员无需手动管理内存。
垃圾回收:CLR会自动检测和回收不再使用的对象,减少内存泄漏的风险。
安全性:CLR提供了安全性机制,确保代码的执行不会对系统造成损害。
跨平台:托管代码可以在不同的操作系统上运行,只要有对应的CLR。

相对应的,非托管代码直接操作计算机的硬件和操作系统资源,需要手动管理内存和资源的分配和释放。非托管代码在性能方面可能更高效,但也更容易出现内存泄漏和安全问题。C#可以通过使用InteropServices命名空间中的功能与非托管代码进行交互,这样可以在C#中调用非托管代码的功能。

FCL

The Framework Class Library or FCL provides the system functionality in the .NET Framework as it has various classes, data types, interfaces, etc. to build different types of applications including desktop applications, web applications, mobile applications. The Framework Class Library is integrated with the Common Language Runtime (CLR) and is used by all the .NET languages such as C#, F#, Visual Basic .NET, etc.

BCL vs. FCL

  • The Base Class Library (BCL) is literally that, the base. It contains basic, fundamental types like System.String and System.DateTime.
  • The Framework Class Library (FCL) is the wider library that contains the totality: ASP.NET(web application framework 对标 Node.js), WinForms, the XML stack, ADO.NET and more. You could say that the FCL includes the BCL.

C# Compilers

C#源文件通过编译器(如CSC.exe)编译成中间语言(IL)和元数据,生成.exe或.dll文件。IL是一种伪代码,独立于任何CPU,可以在任何装有.Net Framework的机器上运行‌

Common Intermediate Language (CIL), formerly called Microsoft Intermediate Language (MSIL) or Intermediate Language (IL) is the intermediate language binary instruction set defined within the Common Language Infrastructure (CLI) specification. CIL instructions are executed by a CIL-compatible runtime environment such as the Common Language Runtime. Languages which target the CLI compile to CIL. CIL is object-oriented, stack-based bytecode. Runtimes typically just-in-time compile CIL instructions into native code.

  • Just In Time compiler
    即时编译。当程序运行时,IL通过CLR中的即时编译器(JIT)将CIL的byte code编译为目标平台的原生码(针对特定CPU的机械码)。JIT编译是在程序运行时进行的,确保了代码的可移植性和执行效率‌程序运行过程中。
  • Ahead Of Time compiler
    提前编译。程序运行之前,提前编译器(AOT)将C#源码直接编译为目标平台的原生码并且存储。这种方式通常用于生成本地应用程序,提高启动速度和性能‌。

将.exe或.dll文件中的CIL的byte code

Unity compiler

Unity编译C#脚本的过程通常是自动进行的,当你在Unity编辑器中构建项目时(比如导出为执行文件或者打包为Android/iOS应用),Unity会编译所有C#脚本。
如果你需要在Unity编辑器外部编译C#代码,你可以使用Mono的mcs编译器或者.NET Core SDK。

以下是使用mcs编译器的基本命令行示例:

mcs -out:YourGame.exe -recurse:*.cs

Mono

Mono, the open source development platform based on the .NET Framework, helps developers to build cross-platform applications. Mono’s .NET implementation is based on the ECMA standards for C# and the Common Language Infrastructure. Mono was originally reimplementation of the .NET for linux. Today is much more.

Unity Mono

The Mono scripting backend compiles code at runtime, with a technique called just-in-time compilation (JIT). Unity uses a fork of the open source Mono project.
Some platforms don’t support JIT compilation, so the Mono backend doesn’t work on every platform. Other platforms support JIT and Mono but not ahead-of-time compilation (AOT), and so can’t support the IL2CPP backend. When a platform can support both backends, Mono is the default.

IL2CPP

The IL2CPP backend converts MSIL (Microsoft Intermediate Language) code (for example, C# code in scripts) into C++ code, then uses the C++ code to create a native binary file (for example, .exe, .apk, or .xap) for your chosen platform. This type of compilation, in which Unity compiles code specifically for a target platform when it builds the native binary, is called ahead-of-time (AOT) compilation.

How IL2CPP works

  1. The Roslyn C# compiler compiles your application’s C# code and any required package code to .NET DLLs (managed assemblies).
  2. Unity applies managed bytecode stripping(代码裁剪). This step can significantly reduce the size of a built application.
  3. The IL2CPP backend converts all managed assemblies into standard C++ code.
  4. The C++ compiler compiles the generated C++ code and the runtime part of IL2CPP with a native platform compiler.
  5. Unity creates either an executable file or a DLL, depending on the platform you target.

Mono vs. IL2CPP on Unity

Unity中C#代码的处理过程

  1. 编写C#代码:
    开发者在Unity编辑器中编写C#脚本,这些脚本通常用于实现游戏逻辑、控制角色行为、处理用户输入等。

  2. 编译C#代码:
    当开发者在Unity中保存C#脚本时,Unity会自动触发编译过程。Unity使用Mono或IL2CPP作为其脚本引擎。
    Mono:在使用Mono时,Unity会将C#代码编译成CIL(Common Intermediate Language)。这个过程是在Unity编辑器中完成的,生成的CIL代码会被打包到Unity的可执行文件中。
    IL2CPP:如果选择使用IL2CPP,Unity会将C#代码首先编译为CIL,然后将CIL代码转换为C++代码,最后再编译为本地机器代码。IL2CPP的主要优点是可以提高性能和安全性。

  3. 生成的CIL代码:
    生成的CIL代码会被打包到Unity的可执行文件中,通常是一个DLL文件。这个DLL文件包含了所有的游戏逻辑和功能。

  4. 构建过程:
    在构建游戏时,Unity会将所有的资源(如纹理、模型、音频等)和编译后的CIL代码打包成一个可执行文件(如EXE、APK、IPA等),具体取决于目标平台。
    Unity的构建系统会处理所有的依赖关系,确保所有需要的资源和代码都包含在最终的构建中。

  5. 运行时执行:
    当用户运行构建的游戏时,Unity的运行时环境会加载可执行文件。
    如果使用Mono,运行时会在需要时将CIL代码即时编译为本地机器代码(JIT编译)。如果使用IL2CPP,CIL代码已经在构建时转换为本地机器代码,因此可以直接执行。
    运行时会管理内存、处理输入、渲染图形等,确保游戏的正常运行。

  6. 总结
    在Unity中,开发者编写的C#代码会经过编译过程,生成CIL代码,并在构建时打包到可执行文件中。Unity使用Mono或IL2CPP作为脚本引擎,分别通过JIT编译或AOT编译将CIL代码转换为本地机器代码。这个过程使得Unity能够在不同平台上运行相同的代码,同时也为开发者提供了灵活的开发环境。

Cross-platform

Mono运行时编译器支持将IL代码转为对应平台原生码
IL可以在任何支持CLI(Common Language Infrastructure,通用语言架构/环境)的平台上运行,IL的运行是依托于Mono运行时。

IOS Platform

IOS不支持动态生成的代码具有执行权限,而通常jit就是运行过程中动态编译代码为机器码并缓存/执行(Mono运行时将IL编译成机械码),所以:封存了内存的可执行权限 等于 变相的封锁了jit编译方式


Credits

CLR简介:https://blog.csdn.net/weixin_42186870/article/details/119621977/
Mono简介:https://www.mono-project.com/docs/about-mono/
IL2CPP简介:https://docs.unity3d.com/6000.0/Documentation/Manual/scripting-backends-il2cpp.html
IOS平台代码热更:https://blog.csdn.net/qq_33060405/article/details/144314440