GlContext

GlContext是bgfx里使用OpenGL的最底层的封装,它的具体实现有5种。
EGL,主要用于OpenGL ES & Android
WGL,主要用于OpenGL & Windows
GLX,主要用于OpenGL & Linux
EAGL,用于iOS
NSGL,用于OS X
这里选取EGL进行分析
eglOpen和eglClose用于加载libEGL库并获取对应的函数地址,bgfx通过BGFX_USE_GL_DYNAMIC_LIB宏支持动态加载或静态链接。https://www.khronos.org/registry/EGL/sdk/docs/man/html/indexflat.php
GlContext最主要的函数是create,create初始化EGL,创建EGL的Surface和Context:
eglGetDisplay获取一个EGL display connection
eglInitialize初始化这个EGL display connection并返回EGL版本号
eglChooseConfig配置需要的frame buffer属性,EGL返回满足条件的EGLConfig
eglCreateWindowSurface​根据EGLConfig创建一个window surface
eglCreateContext根据EGLConfig创建一个rendering context
eglMakeCurrent​绑定context到当前的渲染线程,同时绑定context到刚创建的draw/read surface
eglSwapInterval指定eglSwapBuffers最小的刷新周期为0,默认为1即开启垂直同步,0为只要渲染完成就刷新
最后通过import加载libGLESv2库并获取OpenGL ES的函数地址供以后调用,至此一个GIContext就创建好了,重要的数据都保存在成员变量里。
glcontext_egl.h/.cpp里除了GlContext外还有一个SwapChainGL结构,这个结构用于根据GlContext的m_display和m_config创建新的surface和context。

TRANSFORM_TEX

TRANSFORM_TEX是Unity Shader里提供的一个获取正确的UV坐标的宏定义函数,它定义在UnityCG.cginc里:

// Transforms 2D UV by scale/bias property
#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

Vertex Shader的输入参数里已经传入了顶点的UV坐标了,为什么仍需要TRANSFORM_TEX来做转换呢?

原因是当我们的Shader中有对Texture进行引用、采样时,Unity的材质编辑器默认支持了对Texture的Tiling(平铺)和Offset。

没有通过TRANSFORM_TEX进行转换的UV坐标Tiling和Offset的设置就不会生效。那么Unity Shader是如何通过TRANSFORM_TEX实现Tiling和Offset的呢?答案就在它的宏定义里。

tex是传入的UV坐标,它是float4类型,但UV只使用了其xy值。

name是UV将要Sampler的Texture名,通过宏的##连成了一个叫name_ST的变量名,这个变量就是用来存储Tiling和Offset的,其中xy是Tiling,zw是Offset。

因此Unity Shader中当你定义一个Texture Sampler的时候(如sampler2D _MainTex;)总要同时定义一个float4变量(如float4 _MainTex_ST;),这个变量看似没被使用,其实是它的使用被包含在TRANSFORM_TEX里了。

Script Execution Order Settings

Unity MonoBehaviour不同脚本中的Awake、OnEnable和Update方法的回调顺序默认情况下是由脚本的加载顺序决定的,而脚本的加载顺序是不确定的,因此一个脚本的Awake是否先于另一个脚本的Awake被调用也就无法确定。

Unity提供了一个Script Execution Order Settings来供开发者解决这个问题,调整不同脚本的回调顺序(限于Awake、OnEnable和Update)。 它的实现方式如下。

class MonoBehaviour通过m_Methods保存了一个MonoBehaviour中部分可回调方法的数组,这些方法通过一个枚举值索引。

这个枚举中看不到OnEnable、OnDisable等方法,这是因为该枚举用于引擎内部,暴露给C#层的方法名算是它的别名,可以用下图一一对应。

Awake的回调顺序控制主要是在AwakeFromLoadQueue里通过控制加载顺序来实现。编辑器下会即时调用SortBehaviourItemByExecutionOrderAndInstanceID来实时排序,而游戏中是Build过程将排序写入了场景文件。

Update由专门的BaseBehaviourManager来管理,BaseBehaviourManager中有一个m_Lists,正是它通过Script Execution Order将同一order的脚本Update串连起来、将不同order的脚本Update按顺序调用。

Unity中有4个BaseBehaviourManager,分别是BehaviourManager、FixedBehaviourManager、LateBehaviourManager和UpdateManager。前3个分别负责调用MonoBehaviour的Update、FixedUpdate和LateUpdate方法,而最后一个UpdateManager实际和BehaviourManager作用一样也是调用Update,但并没有看到有脚本的方法注册到它里面。

从PlayerLoop里的调用顺序来看依次是:GetFixedBehaviourManager ().Update (); GetBehaviourManager ().Update (); GetLateBehaviourManager ().Update (); GetUpdateManager ().Update ();

从屏幕刷新频率到Unity VSync

显示器有一个属性叫屏幕刷新频率,它是指每秒刷新屏幕的次数,单位为Hz,一般设置为60Hz。
1

什么是刷新屏幕呢?我们屏幕是由像素矩阵组成的,其(CRT)显示图像的原理是靠电子束从左到右、从上到下逐行激发屏幕内表面的荧光粉单元(像素)来实现的。电子束一次水平方向的扫描叫行扫描,一次完整的扫描就是刷新屏幕,形成的图像就是一帧。因此60Hz的刷新率也就是每秒60帧,人眼的视觉暂留需要满足每秒24帧及以上。

显示器扫描的过程中有两个重要的概念:HBlank(行消隐)和VBlank(场消隐)。HBlank是指当行扫描到最右端时需要快速返回到下一行的最左端的过程。而VBlank则是指扫描完一帧,准备开始扫描下一帧,扫描线从右下角返回到左上角的过程。这两个过程的时间间隔中,扫描线需要变得blank,以防止看到一条斜线显示在屏幕上。

游戏渲染中的VSync(垂直同步)技术就与刷新率和VBlank有关。先说说为什么需要VSync。一般情况下CPU执行游戏逻辑和GPU执行渲染的计算都会快于屏幕的刷新,也就是CPU提交渲染数据和命令到GPU、GPU填充渲染结果到显存的速度会快于屏幕刷新,因此当屏幕刷新进行的过程中很可能会出现显存内容被改变的情况,从而造成Tearing(画面撕裂)。
2

解决Tearing问题的方案就是VSync。前面提到屏幕刷新的两帧之间会有一个VBlank,这个间隔硬件就会产生一个VSync的信号,游戏可以在只有收到该信号后再去计算下一帧,这样就能避免Tearing。实际上,即使在不使用VSync且不会发生Tearing的情况下,由于屏幕刷新率的限制,CPU/GPU的帧率再快对于渲染结果来说并没有影响,反而是一种冗余计算的浪费。而关闭VSync的好处是可以更快的响应输入。

Unity在Quality Settings中有个VSync Count属性可以设置VSync,分别是Don’t Sync(关闭VSync)、Every VBlank(每个VBlank计算一帧)、Every Second VBlank(每两个VBlank计算一帧)。https://docs.unity3d.com/Manual/class-QualitySettings.html
3

VSync是需要图形API(DirectX、OpenGL)支持的,Unity对于关闭了VSync或者VSync的设置不被支持的情况采用了在CPU上同步的方案来维持目标帧率:
4

void TimeManager::Sync (float framerate)中传入目标帧率framerate,函数中通过Sleep和循环来控制每帧的耗时,从而控制帧率。

参考资料:
垂直同步
D3DPRESENT_INTERVAL
理解VSync
http://bbs.3dmgame.com/thread-3642221-1-1.html

Unity script bindings to a C++ class

Unity源码中C#到C++的绑定是通过一个描述C#和C++代码的txt文件(如UnityEngineGameObject.txt)来生成的。

这类文件放在Runtime\Export和Editor\Mono目录下,Unity源码会根据它们生成一个.cs和一个.cpp文件。.cs文件提供了C#层的接口,.cpp文件通过mono_add_internal_call实现了C#到C++的绑定。

Unity采用Embedding Mono的方式实现C#到C++的调用,Embedding Mono参考http://www.mono-project.com/docs/advanced/embedding/

其C#层的对象都继承于UnityEngine.Object,Object中有一个很重要的成员变量m_UnityRuntimeReferenceData:
m_UnityRuntimeReferenceData

ReferenceData结构:

ReferenceData

Unity通过这里的cachedPtr存储C++层与之关联的对象的内存地址。

当C#层通过Mono调用C++方法时传入对象的this引用,C++方法将获得一个MonoObject的对象指针(MonoObject*,Unity typedef MonoObject为ScriptingObject),这个对象指针指向的内存其实就是C#对象的内存,其内存布局如图:
3
4

如上图,Mono中每个C#对象的内存布局都以MonoObject开始,而Unity中每个C#对象的内存布局是紧接在MonoObject后会有一个ReferenceData。因此Unity C++层定义了一个UnityEngineObjectMemoryLayout与C#层ReferenceData相对应:
5

Runtime\Scripting\Scripting.h

MonoObject的定义:
6
https://github.com/mono/mono/blob/master/mono/metadata/object.h
http://docs.go-mono.com/index.aspx?link=xhtml%3Adeploy%2Fmono-api-object.html

Unity C++层通过将C#层的对象内存地址+8bytes来取得ReferenceData结构并解析为UnityEngineObjectMemoryLayout,同时将C++层与C#绑定的对象使用cachedPtr进行存取。
7

Rendering Paths

Unity支持多种不同的渲染路径,渲染路径主要影响光照和阴影。在Graphics Settings中可以设置项目的渲染路径,还可以为Camera设置单独的渲染路径。如果设置的渲染路径显卡不支持,Unity会自动使用更低保真度的渲染路径,如Deferred Shading不支持,Unity会用Forward Rendering代替。

Deferred Shading:延时光照,拥有最高保真度的光照和阴影,适合有很多实时光照的情况。

Forward Rendering:正向渲染,支持法线贴图、逐像素光照和阴影等。但在默认配置下,只有少数最亮的光才会逐像素渲染,其它的则是每个对象或对象的每个顶点逐一渲染。

Legacy Deferred:老版的延时光照,和Deferred Shading基本一样。不支持Unity5的基于物理的shader。

Legacy Vertex Lit:顶点光照,最低保真度并且不支持实时阴影,是Forward Rendering的子集。

【原】实时渲染中常用的几种Rendering Path

Vertex and fragment shader examples

Mac SOIL踩坑记

SOIL即Simple OpenGL Image Library,是一个跨平台的支持多个格式图片加载的库,主要作用是加载图片成为OpenGL的texture。该库最后的更新时间是2008年7月7日,因此现在Mac上使用又踩到了几个坑。

http://www.lonesock.net/soil.html下载源文件,在src和lib目录下分别已经存在了用于编译链接的SOIL.h头文件和libSOIL.a静态库文件,将它们加入工程编译链接时Xcode报错:

Undefined symbols for architecture x86_64

可见libSOIL.a并不是x86_64的库文件,需要重新编译生成。cd进入SOIL/projects/makefile目录,运行make命令编译报错:

error: unable to open output file ‘obj/image_helper.o’: ‘No such file or directory’

在makefile目录下新建obj目录重新make可以重新编译生成libSOIL.a库文件了,但此时库文件仍是32位的,修改makefile文件,加入编译选项-m64,重新编译生成即可。