/images/avatar.png

Papalqi

2D 距离场

前提概要 在 https://papalqi.cn/mathsdf/ 中我们讨论了纹理距离场,他们都没有带符号"signed"。如果不重新映射范围,它们就不是负值。这当然也是可行的,他可以做到类似以下内容这样: 这很牛,但是我们如何使用数学方法生成距离场而不使用纹理呢。这不光节省贴图资源,有的时候一些特殊的效果也只能靠这种方法实现。math SDFs 可以做出非常惊人的效果。如果依靠单纯的纹理来进行距离场的构建,那资源的精度和数量将非常多,不可接受。 例如,你想为某个图标设置一个漂亮的圆形背景。经典的方法可能是在 Photoshop 这样的图像编辑器中画一个圆圈,将其保存为纹理,导入到引擎中,然后将其放在图标下面。但是,更为有用的种方法是使用 MATH 来生成圆。 在这里先推荐一个纯数学的UE4开源工程,基本的图形实例都可以在这里找到。 https://github.com/papalqi/UE4-Math-SDF 在引擎中创建 SDF 可以灵活地更改大小/渐变/动画等等。让我们从这个圆的例子开始: 现在要将这个等式翻译成 Unreal 或 Unity的节点。用这个简单的方程,我只需要计算 UV 的长度,然后减去我们圆圈的半径r。 把这个方程式从公式中分解出来,节点P就是TexCoord[0],然后取长度,减去调用的参数Circle Size 圆的大小。这样我就成了一个圆。 但是有些地方不对劲,圆实际上是在左上角,这并不是在材质的中心。这是因为UV坐标系在一个0-1范围内。我会画一个非常粗略的图表来说明这一点: 这张图上的 x 轴代表红色通道,y 轴代表绿色通道。基于这个图,SDF 它的中心在[0,0] ,它的半径在0.5(我们的圆大小)。 现在为了好玩,我们可以看看 Unity ,你可以看到 Unity 的 y 坐标翻转了,因为它总是在左下角是[0,0]点 : 所以 Unity 绘制的 SDF 有点不同,它实际上是在左下角显示,而不是像 Unreal 那样在左上角显示。因为如果你看一下 UV 坐标,它们的 y 轴从底部开始为0,而不是Unreal从坐标顶部开始为0的。 不管引擎是什么,我都需要把坐标系放在材质的中间。为了做到这一点,我需要实际缩放和平移坐标,并把它移动过来。下面是一张通过 Photoshop 变换在着色器中做的动图: 如果我想要的范围从0-1到-1-1,我只需要减去0.5,乘以2。如果我只是乘以2,因为偏移缩放,它只会变成0-2的范围。 所以我们首先需要减去0.5,因为这将改变范围在负的-0.5-0.5,然后当你乘以2,你得到-1到1的范围。 因为范围是 -1-1之间。所以现在SDF是有真正的负值,这将为我们当前是内部距离还是外部距离。 在引擎中我们可以表示为: 虚幻也有一个constant bias node 常数偏置节点。它可以在一个节点上完成减法/加法和乘法,所以我大多数时候都是这么做的: 很好,现在我给自己弄了一个数学方法的 SDF 圆… 我能用它做什么?像Texture SDF,最基础的我可以使它羽化。那么我们在引擎中如何进行这个操作呢。 我们可以通过添加一个smoothstep node 平滑节点,然后把颜色反转过来one minus node.

4.27 虚幻引擎最新的内容分发策略 Content Distribution

一. 打包过程总览 Package overview 我们首先看一下Unreal整体的打包流程。 Build:最开始的关节是编译UAT的脚本,Compile AutomationScripts。之后我们要编译源代码,形成我们的可执行文件。 Cook:我们分为两个阶段, Cook 的第一个阶段Content Cook,这个阶段主要是把资产内容转化为目标平台所支持的资产格式。 Stage:为了distribution/execution,进行排序并且组织的文件。我们将多个asset资产进行Bunding创建一个或多个“.pak”文件。并且我们将进行Pak的压缩,之后我们将进行可执行文件的签名。 Package:更改为public distribution 或者 testable form。 Archive:存储归档 Deploy: 上传到设备、文件服务器等 对于分发,虚幻引擎可以将内容分割成 .pak 文件并将其与主要可执行文件分开交付给用户。引擎支持DCL和Patch。因此我们有三种基础方案: Release 可执行文件 +Prerequisite+最小内容 Ex.APK和APK内的OBB Patch:进行和 Release的更改比较 + added assets DLC (DLC build):release+新的版本 一直打Patch 的方案有很大的问题,我们需要适合频繁更改/添加资源的网络游戏的方法! 创建Chunk,通过ChunkID分隔资源,使其成为多个Chunk,利用UnrealPak程序 创建HTTP Chunk安装数据:将Chunk分成适合作为HTTP通信部署的大小(默认为1MB),利用构建修补程序工具,将结果(分叉的Chunk+Manifest)上传到CDN。仅自动下载客户端所需的部分,然后重新生成Pak。 Chunk创建+HTTP Chunk安装数据的限制: 本地重新生成Pak:Pak越大,再生成本成倍增加。 网络流量管理难:影响因素多,如何拆分和导出数据很难掌握。 Pak版本控制难点:需要单独管理当前版本的Pak列表。 在过去是专门为非常不稳定的移动网络用户设计 最新的内容分发解决方案: Chunk Downloader Google PAD 二.Chunk Downloader[4.26] 下载多个区块的功能“适合发布大量小文件的游戏”可以直接管理每个Patch版本的PAK列表,这是非常直观的。主要功能是:继承和PAK版本控制。

2D 距离场的使用一:什么是距离场

本系列将对2D距离场的使用做一个系统性的梳理,并且会使用纯数学来构造更为复杂的图形和图案。 首先我们要搞清楚什么是SDF ?SDF 代表有符号距离场,但我现在将忽略“有符号”,让我们来谈谈什么是距离场。二维距离场是一个物体与同一时刻中另一个物体的边缘或边缘之间的距离的直观表示。我们在这里使用Circle来进行演示: 如上图所示,左边是一个圆形,右面是这个圆形的距离场。这个距离场中,表示纯白色的区域(也就是数值为1的部分)就在对象上。纯黑色部分,就是距离物体最远的点。中间的灰色部分,数值在0-1之间。这是一种可视化0到1之间距离的方法。 把距离场想象成一堆不太透明的圆是很有用的: 因为你可以在你的着色器中选择一个范围区间,选择多少的距离领域的范围你想要渲染,所以我可以选择渲染最大范围1和最小范围0.5. 你可以看到,虽然我可以使一个圆圈大于我的原始纹理图像,它不是固有的有用的东西与完美的圆形形状。这是因为我的纹理在不同的压缩过程中被取样,很可能一个完美的圆看起来不会很好。 因此,这种使用贴图的距离场技术通常用于图标,而不是纯粹的圆形,当你想要一个“辉光”周围的东西,不是一个共同的形状。 当然我们可以使用基于Math 的SDF, 这是可以实现完美距离场的东西。当然这个东西我们将在下一篇文章进行讨论。 我们还是回到辉光的问题中。 首先在PS中制作这张贴图。 然后我们在材质当中进行使用: 你可以看到,如果你保持最大/最小距离在很近的距离内,你会得到一个清晰的图像,如果你保持最大/最小距离之间的距离更长,你会得到一个模糊的图像。 因此,一个纹理让我得到一个辉光和图标;它甚至可以做阴影和笔画… 但它确实需要一些逻辑和设置

UE4 Anisotropy

自虚幻引擎 4.26 起,各向异性已正式添加到材质中,使表达各向异性反射更加容易。 在本文中,我将介绍 Anisotropy,尤其是 Tangent 输入。 Anisotropy使用方法和输入 从 4.26 开始,默认情况下已将各向异性和切线添加到材质输入中。可以通过在此处输入值或纹理来使用它。 Anisotropy(各向异性)输入 在这里,您可以输入 [-1 到 1] 范围内的标量值。 当Anisotropy为 0 时,没有因各向同性反射而发生的变化。 当Anisotropy为正值时将沿切线(切线方向)突出显示。 当Anisotropy为负值时,遵循 Binormal 方向。(Tangent 和 Binormal 将在后面介绍。) 下图是 Tangent 朝向 UV 的 U 方向时的示例。可以看到正值和负值对应的方向是不一样的。 Tangent(切线)输入 首先,这个输入的Tangent(切线)指的是从曲面顶点的法向量沿曲面成90度角的切线向量。副法线Binormal是垂直于 Tanegnt 的向量。 您还可以从 UE4 的 Staic Mesh 工具栏中检查 Normal、Tangent 和 Binormal。绿色表示法线方向,红色表示切线方向,蓝色表示副法线方向。 如果材质的切线节点没有输入,那么我们将使用曲面顶点的切线(法线)。因此,如果每个顶点的Tangent都朝向是我们预期的方向,那么即使Tangent输入中没有输入也可以直接使用。 Tangent基本上是根据顶点坐标、法线、UV值计算的,但是在导入mesh的时候,需要选择使用“Normal Import Method”。 但是,在大多数情况下,Tangent 并不指向(难以指向)预期方向。因此,您可以通过将 Tangent(和 Binormal)的方向输入到 Tangent Input 来控制反射方向。 Tangent Input 的数值和纹理 那么我应该在 Tangent Input 中输入什么值?在这里,我们将介绍以下三种方法。 使用各向异性贴图 使用角度贴图 UE4内计算 各向异性贴图 这是一种使用纹理的方法,它存储了 R:Tangent、G:Binormal、B:Normal 的方向,如下图所示。

如何进行贴图的实时压缩

让我们暂时忽略具体的压缩算法实现,只考虑一个最基本的问题,如何让GPU生成一张压缩了的贴图。既然我们需要一张压缩了的贴图,那么首先,就应该以指定的压缩格式创建出一幅贴图,这没什么问题。然而遗憾的是,无论是pixel shader还是compute shader,通常都无法将数据写入压缩格式的贴图。 一种变通的方法是,第一步先把数据写入shader直接支持写入操作的格式的贴图中,如R32G32B32A32_UINT的贴图中,然后第二步进行数据类型的转换。 针对第一步,目前主流的GPU,都支持在shader中操作和写入整数类型的数据,所以在shader中按压缩格式编码贴图没有任何问题。 针对第二步,目前主流的API都提供了转换贴图格式的接口或方法,分别是: Windows:DX10.1及以上,使用CopyResource用一次拷贝转换格式 Android:ES3.2及以上(Vulkan),使用CopyImageSubData用一次拷贝转换格式 IOS:支持Metal就行,从MTLBuffer中创建MTLTexture作为RT,然后将MTLBuffer中的内容用copy复制到压缩格式的MTLTexture中 对于D3D12、Vulkan、Metal这些新一代API,应该可以用资源aliasing避免拷贝,还没有尝试 以上这些功能类似与C++中的reinterpret_cast,可以在保留数据的内容的前提下,按另一种格式来解释数据的内容,间接实现了在shader中写入压缩格式的贴图。 压缩的类型 贴图的内存的排布可以有多种形式,可以按照压缩的和非压缩的两大类。 我们当前举的是非压缩中的一种内存布局,按照这种内存布局把每个像素对应的数据填充到GPU纹理绑定的内存上。 GPU应用的数据是按照一定内存布局排布的非压缩或者压缩数据。那么美术同学制作的图片文件资源是不是就按照相应的格式存储的呢?一般我们美术同学会把贴图存储为PNG或者TGA格式。这是两种比较常用的压缩格式,和上文所说的GPU纹理所应用的压缩格式不同,应用的是流压缩方法。 GPU应用的压缩方式是块压缩模式。流压缩模式和块压缩模式不同的是:解压图片需要整个图片文件的完整压缩数据,块压缩不需要整个图片的压缩数据,只需要相关的分块即可。如果引擎用PNG,TGA格式的图片文件进行渲染,需要用CPU把图片文件软解为非压缩位图格式存储于内存中,然后把该位图内存绑定到GPU的纹理上。 因此PNG,TGA对于内存节省是没有益处的,反而因为CPU软解增加了计算开销,他们存在的意义是节省硬盘存储空间或者网络传输数据量。PNG,TGA不是针对GPU和渲染效率来设计的压缩格式,他们有其历史价值,在网络传输中大大减少了数据量。我们美术同学制作的资源是PNG, TGA,往往是因为很多美术相关软件例如PhotoShop更好的支持这两种格式的输出。 游戏业界中公认的压缩比和压缩质量很好的一种压缩类型是ASTC。ASTC是块压缩方式,它可以直接读入内存中不用CPU软解,直接绑定到GPU纹理上(大多数iOS和Android手机都支持ASTC压缩纹理)。因此ASTC在加载效率和内存空间占用都是比较优秀的。

贴图压缩格式

压缩格式 传统上完全不压缩的像素RGB8,一个像素需要24位。块压缩是一种有损纹理压缩技术,用于减小纹理大小并减少内存占用,从而提高性能。 块压缩纹理可以比每种颜色 32 位的纹理小。 一.BC压缩格式 BC1 BC1是将一个块(4*4)内16个像素384进行压缩到64位.压缩比例可以达到6倍。 我们将选中块中所有颜色的极值,分别记为BaseColor0和BaseColor1。共占32位。 BaseColor的格式因为从24位转为了16位的颜色会有精度问题,位数为RGB565格式。 根据两个BaseColor生成另外两个BaseColor颜色。这两个中间颜色是根据比例勾兑的 BaseColor2 = 2/3 *BaseColor0 + 1/3 * BaseColor1 BaseColor3 = 2/3 *BaseColor1 + 1/3 * BaseColor0 另外32位每个像素有两位的索引,用来进行BaseColor的组合。 如果Color0>Color1代表完全不透明的,反之有一位的透明信息。 如果判断有一位的透明信息,则去掉BaseColor3,BaseColor2= 1/2* BaseColor0 + 1/2* BaseColor1,BaseColor变为RGB555,多出一位作为透明度的记录。 BC2 BC2 格式将具有相同位数和数据布局的颜色存储为 BC1 格式;但是,BC2 需要额外 64 位内存来存储 alpha 数据,如下图所示 BC2的alpha使用的是4位,因此表达的alpha 的精度并不精确 BC3 BC3用来压缩alpha值为8位的贴图,可以用来存储高度连贯的alpha数据。颜色值的存储是一致的,Alpha 的存储使用类似于颜色的插值方案来处理。 我们记录两个极值alpha0和alpha1。共16位,剩下的48位分给索引值,每个像素三位。如果 alpha0 大于 alpha1,则 BC3 内插 6个 alpha 值; 否则,它将内插4个alpha值。 当 BC3 仅内插 4 个 alpha 值时,它会设置两个额外的 alpha 值(0 表示完全透明,255 表示完全不透明)

毛发渲染总结

毛发的渲染技法有很多。我们主要讨论模型和着色的方案。 一.毛发模型方案 1.1 Shell Based Fur 这类毛发可以参考GFur Pro插件。 https://www.unrealengine.com/marketplace/zh-CN/product/gfur-pro 这类毛发技术使用的是多层渲染。如下图所示,只要这些面片距离足够的近,我们就有一种是单根头发的错觉。 这类技术的问题是如果我们需要长的毛发,那么我们就需要非常多的截面,性能受到的影响就越大。 当然我们并不是一个面片一个面片进行绘制,密集毛发是通过在单个多边形上绘制多个毛发横截面来实现的。 如果我们以一个球体为例,我们想用毛皮覆盖它,我们需要复制球体并稍微膨胀它的表面,以便我们得到第一层,我们在其上绘制毛皮的第一个横截面。然后我们继续添加更多层,每一层都略高于另一层,直到我们到达毛皮的预期尖端。这些层也被称为Shell,因此得名这项技术。 我们可以在顶点着色器中移动shell的顶点。 1.2 面片(card)与Mesh 传统而言,虽然存在个体差异,但是人体发量大体在10w左右。使用真实意义上的strands去渲染对性能影响巨大。因此划分面片或者Mesh 的操作将是一种妥协式的解决方案。 Epic放出的老的数字人的demo和Paragon都是使用的这种方案,只不过在面片的精度上Paragon要稍微低一点。 《最后生还者》即采用了这种方式来模拟头发。 根据画面风格的不同,我们将选取Mesh或者Card的方式进行。针对偏向卡通风格可能更适合Mesh,而写实风格则使用面片。 1.3 HairWorks&TressFX 英伟达的HairWorks,最早的Hairwork用于2013年的《使命召唤:幽灵》上,用于模拟剧中那种狗狗“莱利”的毛发。虽然从效果上看一般,和《古墓丽影》中的柔丝顺滑相比视觉上并不吸引眼球。后来又用在了又叫好又叫座的惊世大作《巫师3:狂猎》上。HairWorks 基于曲面细分。对HairWorks的官方支持在虚幻引擎4.16版就停止了。现在已经开源。 AMD的TressFX,开源。2013年新版《古墓丽影》第一个引入了这种技术。 1.4 Strand sig19,frostbite提出的基于strand的方式以及进一步处理single/multiple scattering。虚幻在4.24提供的Groom作为解决方案。4.26放出的MeerkatDemo里面,也使用了这个技术。 1.5其他 二.着色模型 2.1 kajiya-kay(1989) kajiya是基于经验的着色模型,也就是并不是完全的着色物理正确。不过对于大多数硬件性能较低的平台也算够用。 在Kajiya-Kay模型中,头发纤维被抽象化为一个不透明的圆柱体,不能够透射和产生内部反射。因此,Kajiya-Kay模型不能表现一些肉眼观察到的头发效果,同时也是能量不守恒的。在kajiya中,光照主要分为diffuse和Specular。 $$ Color=Diffuse+Specular $$ 其中Diffuse使用Lambert,而Specular使用Phong。如下图所示黄色的粗圆柱表示一根头发,T是其切线,V是视线,L是光源方向, H是L和V之间夹角的一半。 我们首先看一下diffuse 的计算。 $$ Diffuse=K_d sin(T,L) $$ 这里可以看出,其跟Lambert并不是完全一致。 这是因为我们可以把这种微观的细丝看成直径非常小长度很长的圆柱,那么我们宏观上看到的这类表面上某一点的光照实际上是一圈圆柱上的点的光照的总和,这一圈点有无数个方向不同的法线,因此需要对这一圈点的入射光和BRDF的乘积进行一个半圆形的积分。 因此,我们在某一个点的漫反射是这个点的的法线半球上的积分,但是对于头发来说,我们计算的是这个点的切面半圆上的积分。 可以很明显看出来高光变成了环形,且有发丝的感觉,纵向上处于同一圈内的点可以当做是一根头发,计算出每根头发的高光效果,纵向环绕一圈后自然就成为了圆环。 $$ Specular=K_d sin(T,H)^{specularity} $$ 同样的,我们的在Phong模型中,对于一个点,我们使用的是镜面反射方向。但是在Kajiya中,光照击中头发之后,我们求的依旧是切面的所有反射方向。反射方向是沿着切线而不是法线以镜面反射角度射出的,这样所有的反射光线均位于以切线为轴线,角度为θ(光照与切线之间的夹角)的圆锥体上。我们数学推导后的积分计算就是上述式子。 Kajiya-Kay模型存在的主要问题是,它不是基于物理的,并且它将头发建模成不透明的圆柱体,因此不能模拟光线可能穿过头发或者在头发中传播的情况,这就导致了其不能模拟出背光以及二次高光等效果。 2.2 Marschner(siggraph03) Marschner模型是除Kajiya-Kay模型外另一个应用广泛的头发着色模型。UE4 在Groom之前的中的头发渲染(Paragon)就采用了Marschner模型。Marschner模型则是基于对头发纤维的物理分析,归纳推导出来的模型。 头发是十分透明的(黑色除外),因此Marschner模型和Kajiya-Kay模型不同的一点就是,Marschner模型将头发纤维抽象为一个透明的椭圆柱体。其最主要的三个光路如下: R :光线到达头发纤维角质层直接被反射。 TRT:光线透射角质层进入皮层,角质层内层折射,又透射到空气中。可以知道,出射点距离入射点已经产生了偏移。 TT:光线透射进入皮层,又从中直接透射出去 我们当然还可以继续进行更多的内部反弹,但这些通常是相当微弱,因为大部分能量都被吸收掉了。 因此,我们的公式可以写为: $$ S=S_R+S_{TT}+S_{TRT} $$