/images/avatar.png

Papalqi

UE4 RHIThread

´ RHIThread并不是所有平台都需要的。默认仅给PC平台开启。 RHI线程的话,做三件事情: DrawCommand to GPU Command, 把DrawCommand转化为渲染API的GPU Command GPU Command submit, 提交DrawCall。 Flip GPU,等待Swapchain发生。 我们的输入和最后的呈现其实会有6帧的延迟 主线程处理 渲染线程处理 RHI等提交给GPU GPU垂直同步 双重缓冲交换 为什么需要RHI线程 在某些平台上比如d3d11,提交渲染命令不支持并行化。为了解决这个问题,提交渲染命令是最初是放到渲染线程的,后来为了加快渲染线程的计算,把提交渲染命令单独抽到了一个线程,这个线程就是RHI线程。 我们可以看到,开启RHI线程后将会变的非常平滑

UE4材质系统源码剖析

材质(Material) 是可以应用到网格物体(Mesh)上的资源,用它可控制场景的可视外观。从较高的层面上来说,可能最简单的方法就是把材质视为应用到一个物体的"描画"。但这种说法也会产生一点点误导,因为材质实际上定义了组成该物体所用的表面类型(质感)。您可以定义它的颜色、它的光泽度及您是否能看穿该物体(半透明)等等。 在这里是默认大家对使用材质系统拥有基本的基本操作经验,所以本文并不会对一般的简单操作做任何的解释和说明,如果大家想对Unreal材质系统的基本操作感兴趣的话,参考官方文档: https://docs.unrealengine.com/zh-CN/Engine/Rendering/Materials/index.html 对与某些高端的材质TA向的技巧和经验总结以后会在之后的文档中进行总结和归纳。 因此,本文的重点主要是来分析材质系统的运行逻辑和手段,从自动的代码生成到生成变体的过程,最终到如何和几何信息绑定提交渲染的一整套的流程。 一.简介 由于后文即将阐述的编译功能或者变体功能更加的抽象和难以理解,所以我们需要一个比较直观的切入来审视Unreal的整个材质系统。 我希望呈现一种较为直接的介绍模式,这一切就在我们打开一个Materal.asset开始。其对应于代码中的UMaterial。 1.1 PBR和它的输出 当我们打开材质编辑器,最开始我们能最直接看见的就是我们的材质的输出。如下图的左 右图是迪斯尼的PBR参考,这是unreal 的PBR材质节点。 对应代码中,是对应的各种FXXXMaterialInput。材质输出属性在UE4中的基类叫FMaterialInput,它有一些子类用于表达具体的输出属性的具体数据类型,如FColorMaterialInput,FScalarMaterialInput,FVectorMaterialInput等。 UMaterial中对应的材质输出属性和材质编辑器上的输出节点属性几乎是一一对应的(注,显示出来的名字可能会和变量量不同,因为名字因材质类型,光照模型而显示不同的名字),完整定义如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { FColorMaterialInput BaseColor; FScalarMaterialInput Metallic; FScalarMaterialInput Specular; FScalarMaterialInput Roughness; FVectorMaterialInput Normal; FColorMaterialInput EmissiveColor; FScalarMaterialInput Opacity; FScalarMaterialInput OpacityMask; FVectorMaterialInput WorldPositionOffset; FVectorMaterialInput WorldDisplacement; FScalarMaterialInput TessellationMultiplier; FColorMaterialInput SubsurfaceColor; FScalarMaterialInput ClearCoat;FScalarMaterialInput ClearCoatRoughness; FScalarMaterialInput AmbientOcclusion; FScalarMaterialInput Refraction;FVector2MaterialInput CustomizedUVs[8]; FMaterialAttributesInput MaterialAttributes;FScalarMaterialInput PixelDepthOffset; FShadingModelMaterialInput ShadingModelFromMaterialExpression; } 这里比较重要的是有一个WITH_EDITORONLY_DATA的宏,其关闭和IOS竟然相关!那按道理来说如果平台不同的话确实在最后呈现上会有明显的不同。

UE4 Mesh Drawing

内存中的mesh是如何进行渲染的,主要涉及几个重要的数据结构 UPrimitiveComponent PrimitiveSceneProxy FMeshBatch FMeshDrawCommand 一.UPrimitiveComponent UPrimitiveComponent存在于Game Thread内的数据UPrimitiveComponent。 继承自USceneComponent,而USceneComponent的意思,主要是带有3D变化(位置、缩放、朝向)的ActorComponent。UPrimitiveComponent 更近一步,是带有图形(比如网格mesh或者particle system粒子)表现的UsceneComponent。我们所有的mesh数据结构,都会继承自UPrimitiveComponent。而这也是我们征程的开始。 UPrimitiveComponent包含的数据并不会直接在渲染线程使用,这里不使用原数据的主要原因也是显而易见的:主线程和渲染线程的同步问题。 在最开始的阶段world里会遍历所有Actor,Actor遍历所有的UActorComponent,然后执行函数 1 UActorComponent::ExecuteRegisterEvents(); 在这里,会调用然后通知World中的Scene进行AddPrimitive FPrimitiveSceneProxy和FPrimitiveSceneInfo 在FScene::AddPrimitive中,会根据UPrimitiveComponent,进行FPrimitiveSceneProxy和FPrimitiveSceneInfo的创建,创建完成后,调用 1 Scene->AddPrimitiveSceneInfo_RenderThread(RHICmdList, PrimitiveSceneInfo); 之后的调用顺序为: 从AddPrimitiveSceneInfo_RenderThread开始,就进入了渲染线程当中,这时我们已经脱离了UPrimitiveComponent,在渲染线程中仅能接触到的是FPrimitiveSceneInfo和FPrimitiveSceneProxy。 在这里,FScene中的成员变量会对FPrimitiveSceneProxy和FPrimitiveSceneInfo进行相当多的注册。 关于FPrimitiveSceneInfo和FPrimitiveSceneProxy,互为双向指针,二者的区别,我表示,打扰了 FPrimitiveSceneProxy is the rendering thread version of UPrimitiveComponent that is intended to be subclassed depending on the component type. It lives in the Engine module and has functions called during rendering passes. FPrimitiveSceneInfo is the primitive component state that is private to the renderer module.

UE4变体研究 源码剖析

在编辑器打开时,会从资源包中LoadPackage,我们这里只关心的是材质问题。每一个Obj,都会调用自己的Obj->ConditionalPostLoad() 这里已经是在加载的后端了,在这里面会调用子类的PostLoad() 1 2 3 4 5 6 7 8 9 10 for (int32 i = 0; i < ObjLoaded.Num(); i++) { UObject* Obj = ObjLoaded[i]; check(Obj); #if WITH_EDITOR //一顿操作 #endif //又一顿操作 Obj->ConditionalPostLoad(); } 我们只看 UMaterial::PostLoad,如果调用到这里,也就是说我们加载的是一个材质。这是一个非常长的函数。就不在这里展开了。 这里会调用他父类的PostLoad,而UMaterial的父类就是UMaterialInterface,而他们的区别,和主要的成员分析见Unreal Material类的关系 在UMaterialInterface::PostLoad()中又会调用UMaterial::CacheResourceShadersForRendering Cache resource shaders for rendering. If a matching shader map is not found in memory or the DDC, a new one will be compiled.(关于什么是DDC,听过很多次) The results will be applied to this FMaterial in the renderer when they are finished compiling.

UE4 多线程架构

在UE4中使用多线程的方式非常丰富,在Engine初始化时我们就能看出一些端倪。除了最原始的使用FRunnable和FRunnableThread的方式,我们几乎还可以使用两种方式进行线程操作。 在我们引擎初始化时,我们就可以看到两种线程的创建方式。 一. 标准多线程实现FRunnable FRunnable 是指标准多线程,适合长期连续的操作。我们只需要进行简单的继承,就可以实现一个标准的多线程实例。 1.1 FRunable的细节 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class FSimpleRunnable :public FRunnable { public: FSimpleRunnable(); ~FSimpleRunnable(); private: // 必须实现的几个 virtual bool Init() override; virtual uint32 Run() override; virtual void Stop() override; virtual void Exit() override; }; Runnable对象初始化调用 Init(),并通过返回值确定是否成功。初始化失败,则该线程停止执行,并返回一个错误码;成功,则会执行 Run()。 如果Run()中的方法不是永远循环的,就可以直接退出。如果Run永远循环,线程无法退出。就算游戏主线程停止了,这个线程还在继续运行。 当Run执行完毕后,则会调用 Exit() 执行清理操作。 我们继承FRunnable实现这些接口,但是其FRunnable本身并没有线程功能,其线程本身是FRunnableThread。 1 2 void WaitForCompletion(); // 阻塞调用例程,直到线程执行完毕 bool Kill(bool bShouldWait); // 强制杀掉线程 如果调用 FRunnableThread的Kill(bool bShouldWait=false) 函数,会先执行 runnable 对象的 stop(),然后根据 bShouldWait 参数决定是否等待线程执行完毕。如果不等待,则强制杀死线程,可能会造成内存泄漏。 调用 FRunnableThread的WaitForCompletion() 函数,将阻塞调用线程直到线程执行完毕。 1.

UE4灯光的移动性

UE4的光源的移动性上主要分为:Static(静态) 、 Stationary(固定) 和 Mobile(移动),不同的设置在光照效果上有着显著的区别,以及性能上也各有差异(现在应该是基本不谈这个了,没什么卵用)。 光源:静态, 固定, 动态 物体:静态, 固定, 动态 9种对应关系,并且还有特例 Static光源(静态): 在运行时不会以任何方式改变或移动 光的位置 — 不能变 光的颜色 — 不能变 光的强度 — 不能变 光的其他属性 — 也不能变 UE4的静态光照是在光照构建(build)中进行预计算的部分,会对预计算的光照结果进行存储,例如光照贴图(LightMap,将在之后讲解)、阴影贴图这样的形式,可以在运行时支付较低的效率而获得较好的光照结果。它在运行时完全无法更改或移动的光源。这些光源仅在光照贴图中计算,一旦处理完,对性能没有进一步影响。可移动对象不能与静态光源集成,因此静态光源的用途是有限的。 它可以在World Setting里进行进行关闭。静态光照不能让移动(动态)的对象产生阴影。但是,如果照明的对象也是静态的,就能够产生面积(接触)阴影。这是通过调整源半径(Source Radius)属性实现的。但是,应当注意的是,获得柔和阴影的表面很可能必须设置相应的光照贴图分辨率以便阴影呈现较好的效果。静态光源的主要应用对象是移动平台上的低性能设备(笑了)。 静态光源对静态物体 直接光照: lightmap 间接光照: lightmap 直接阴影: lightmap 静态光源对固定物体和动态物体 直接光照:ILC,VLM 间接光照: ILC,VLM 直接阴影:无 Static光源的阴影 Static光源的阴影是能用与Static物体,存储在lightmass中,这意味着它们对动态对象不会产生 直接影响(静态光照由于烘焙到了间接光照缓存中,所以会有一些影响)。在编辑一个固定光源或者静态光源时,光照信息会变成未构建状态,预览阴影能够为您提供一个在光照构建后阴影的大致样子。 Static光源的特性 采用全局光照时,所有具有静态移动性的光源在默认情况下都是区域光源。点光源和聚光灯光源使用的形状是一个球体,其半径是由全局光照设置(Lightmass Settings)下的光源半径(Light Source Radius)设置的。定向光源使用一个圆盘,位于场景的边缘。光源的大小是控制阴影柔度的两个因素之一,因为较大的光源会产生较柔和的阴影。另一个因素是从接收位置到阴影投射物的距离。随着距离的增加,阴影变得柔和,就像在现实生活中一样 2.2 Stationary光源(固定): 光的位置 — 不能变 光的颜色 — 能变 光的强度 — 能变 光的其他属性 — 能变 固定光源(StationaryLights) 是保持固定位置不变的光源,但你可以改变光源的亮度和颜色等。这是与静态光源的主要不同之处,静态光源在gameplay期间不会改变。但是,如果在运行时更改亮度,请注意它仅影响直接光照。间接(反射)光照不会改变,因为它是在光照系统(Lightmass)中预先计算的。所有间接光照和来自固定光源的阴影都存储在光照贴图中。直接阴影存储在阴影贴图中。这些光源使用距离场阴影,这意味着,即使有光照对象上的光照贴图分辨率相当低,它们的阴影也将保持清晰。 Stationary Light在同一个被覆盖区域中只能有4个,当超过这个数目时,范围最小的那个StationaryLight会被转化为动态光源。 固定光源对静态物体 直接光照:直接采用延迟着色渲染 间接光照: lightmap 直接阴影:阴影贴图 固定光源对固定物体和动态物体 直接光照:直接采用延迟着色渲染 间接光照: ILC,VLM 直接阴影:很复杂 Stationary光源的阴影 Lightmass在重新构建光照过程中为Static对象上的Stationary光源生成距离场阴影贴图(DistanceFieldShadowmap)。特点是能使用非常低的分辨率但是可以生成非常锐利的边缘。和光照贴图类似,距离场阴影贴图要求所有StaticMesh具有唯一的展开的UV。必须构建光照才能显示距离场阴影,否则在预览时将会使用动态阴影。最多只能有4个重叠的Stationary光源具有静态阴影。它可以和动态的物体有很好的交互,并且如果和动态的阴影结合也不会产生混合。 距离场阴影的原理是使用到最近阴影过渡的距离代替标准的阴影因数存储(0代表阴影,1代表没有阴影)。然后为了在运行时重新构建阴影,只要简单地缩放或偏移到想得到的半影尺寸的距离即可。这样最大的好处是如果分辨率降低,阴影过渡可以随着距离场更温和地下降。当直接地存储阴影因数时,降低分辨率将会导致斑驳和锯齿阴影。当在使用距离场阴影时,降低分辨率,阴影的过渡仍然是同样的尖锐度,但是在阴影过渡的角落或高频率拐角处会变得更加弯曲。

UE4 主线程和渲染线程的同步

当我们完全了解UE4的多线程是怎么进行时,我们就需要看一下UE4的主线程和渲染线程到底如何进行的同步的。 UE4多线程架构 GameThread、RenderThread、RHI Thread和GPU之间的渲染器同步是一个非常复杂的主题。简而言之,虚幻引擎4通常配置为"后一帧(single frame behind)“渲染器。这意味着当RenderThread处理第N帧时GameThread处理第N + 1帧,除非RenderThread的运行速度比GameThread快。 添加RHI线程使同步过程更为复杂化,因为当RHI线程处理第N帧时,RenderThread能够通过完成第N+1帧的可视性计算而移动到RHI线程之前。最终结果是,当GameThread处理第N+1帧时,RenderThread可以处理第N帧或第N+1帧的命令,RHI线程也可以平移第N帧或第N+1帧的命令,具体取决于执行时间。 在帧的末尾,我们将执行主线程和渲染线程的同步。通过静态的FFrameEndSync来进行线程间的同步。 1 2 3 4 5 6 7 8 //同步主线程和渲染线程 { SCOPE_CYCLE_COUNTER(STAT_FrameSyncTime); static FFrameEndSync FrameEndSync; static auto CVarAllowOneFrameThreadLag =IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.OneFrameThreadLag")); FrameEndSync.Sync(CVarAllowOneFrameThreadLag -> GetValueOnGameThread() != 0); } 我们来看一下FFrameEndSync的数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 class FFrameEndSync { /** Pair of fences. */ FRenderCommandFence Fence[2]; /** Current index into events array. */ int32 EventIndex; public: /** * Syncs the game thread with the render thread.

使用Shader生成Noise

之前的白噪声确实不太行,因为我们现实生活中并不是那样的完全嗡嗡。 我们接下来就是要生成这样的纹理。 一维noize 接下来看不同方式生成的一维noize 1 2 3 4 5 float i = floor(x); // 整数(i 代表 integer) float f = fract(x); // 小数(f 代表 fraction) y = rand(i); //rand() 在之前的章节提过 y = mix(rand(i), rand(i + 1.0), f); y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f)); y = mix(rand(i), rand(i + 1.0), f);使用的是线性插值 y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f));顶点的变化如何变得顺滑了起来 二维Noise 在 2D 中,除了在一条线的两点(fract(x) 和 fract(x)+1.0)中插值,我们将在一个平面上的方形的四角(fract(st), fract(st)+vec2(1.,0.), fract(st)+vec2(0.,1.) 和 fract(st)+vec2(1.,1.))中插值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 uniform vec2 u_resolution; uniform vec2 u_mouse; uniform float u_time; // 2D Random float random (in vec2 st) { return fract(sin(dot(st.

利用shader生成随机数

本片文章主要讲述如何利用shader生成随机数。 一维随机 y = fract(sin(x)*1.0); 上面的例子中,我们提取了sin函数其波形的分数部分.我们可以用这种效果通过把正弦函数打散成小片段来得到一些伪随机数如何实现呢?通过在sin(x)的值上乘以大些的数。 y = fract(sin(x)*10.240); y = fract(sin(x)*100.0); y = fract(sin(x)*1000.0); y = fract(sin(x)*10000.0); 当我们不断的进行增大时,其实我们增大的是sin函数的幅度,但是由于fract的限制,这其实是增加fract的定义域,它会形成-1——1之间的更加密集的点。 细看,你可以看到 sin() 在 -1.5707 和 1.5707 上有较大波动——那就是sin取得最大值和最小值的地方。并且我们的中部更加的集中。 我们当然可以使用一些其他的变化 1 2 3 4 y = rand(x); y = rand(x)*rand(x); y = sqrt(rand(x)); y = pow(rand(x),5.); 这些都是伪随机,也就是说给定特定的数,生成的是特定的随机。 二维随机 我们要进行二维随机,其实就是能够把两个轴分开来看待。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #ifdef GL_ES precision mediump float; #endif uniform vec2 u_resolution; uniform vec2 u_mouse; uniform float u_time; float random (vec2 st) { return fract(sin(dot(st.

基础shader函数 与对应曲线

Fract 得到这个值的小数部分 1 2 3 4 5 float fract(float x) vec2 fract(vec2 x) vec3 fract(vec3 x) vec4 fract(vec4 x) ![fract(x).png][1] y = fract(x); Mod 得到两个值的模,其实就是余数 1 2 3 4 5 6 7 8 float mod(float x, float y) vec2 mod(vec2 x, vec2 y) vec3 mod(vec3 x, vec3 y) vec4 mod(vec4 x, vec4 y) vec2 mod(vec2 x, float y) vec3 mod(vec3 x, float y) vec4 mod(vec4 x, float y) y = mod(x,1.