1.影响光照的码知因素有哪些? - 知乎
2.UE4ToUnitySSGI 效果展示
3.Unity移动端写实角色渲染-皮肤(URP)
4.Python 实现 Unity 资源检测
5.Unity 编辑器扩展和序列化踩坑记(一)
6.Unity Shadow Map(一 理论)
影响光照的因素有哪些? - 知乎
Unity的渲染路径决定了光照如何在Unity Shader中应用,需在每个Pass中指定渲染路径。码知前向渲染路径涉及计算颜色缓冲区和深度缓冲区,码知深度缓冲用于判断片元可见性,码知若可见则更新颜色缓冲区。码知Unity中有三种前向渲染处理光照方式:逐顶点处理、码知tomact源码剖析逐像素处理、码知球谐函数处理,码知基于光源类型和重要度排序光源进行渲染。码知
在前向渲染中,码知Unity会根据场景光源设置及影响程度对光源进行排序,码知一定数目的码知光源逐像素处理,最多4个逐顶点处理,码知其余按SH方式处理。码知渲染路径设置指引Unity理解Pass在前向渲染中的码知位置,底层渲染引擎进行计算并填充内置变量。
Unity提供内置光照变量和函数,根据渲染路径传递给Shader。每个Pass执行一次Base Pass,支持平行光的阴影计算,Additional Pass则用于额外光照结果的混合。
延迟渲染路径解决了大量实时光源性能瓶颈问题。它使用额外的G缓冲区存储表面信息,第一个Pass不进行光照计算,仅标记片元可见性,第二个Pass利用G缓冲区进行光照计算。延迟渲染路径中每个光源可逐像素处理,但不支持抗锯齿和透明物体渲染,对显卡有要求。
Unity支持四种光源类型:平行光、点光源、聚光灯和面光源。点光源和聚光灯需考虑位置、方向、颜色、强度及衰减。平行光没有位置限制,衰减不随距离改变;点光源和聚光灯有位置限制,衰减随距离减小。
在前向渲染中,访问光源属性使用Unity提供的方法。Base Pass和Additional Pass调用顺序由重要度决定,点光源首先渲染,衰减计算使用纹理查找表实现,纹理大小和预处理影响精度,不直观且限制了自定义衰减计算。
Unity使用_LightTexture0纹理进行衰减计算,通过顶点在光源空间中的位置和深度纹理进行采样。使用数学公式计算衰减时,纹理查找表可以提升性能,主力进货指标源码但限制了自定义公式。
在实时渲染中,Unity采用Shadow Map技术实现阴影,首先通过LightMode为ShadowCaster的Pass为光源计算阴影映射纹理。传统方法在Pass中处理顶点位置变换和深度信息,而Unity使用屏幕空间阴影映射技术,通过额外Pass和深度纹理直接在屏幕空间生成阴影图。
一个物体接收其他物体的阴影需要在Shader中对阴影映射纹理采样,而投射阴影则需要将物体加入到光源的阴影映射纹理计算中。透明物体的阴影需注意透明度测试和混合,可能需要调整Fallback。
标准Unity Shader提供了处理光照和阴影的工具,根据渲染路径和光源类型进行优化,实现高质量的光照效果和阴影渲染。
UE4ToUnitySSGI 效果展示
刚刚结束苏州GTC的行程,我立刻投入到学习UE4效果的旅程中。今天,我要分享的内容其实之前已经在知乎的“想法”栏目中发布过了。
我在阅读UE4 4.版本的更新文档时,发现UE4在4.版本就已经实现了SSGI,而在4.版本中进行了优化。于是我查阅了相关代码,并在Unity中实现了这一效果。实际效果比我预想的要好,因为还有一些优化尚未完成,所以我今天先展示一下Unity中的实际效果。
实际上,UE4中的SSR和SSGI都采用了相同的RayMarch算法。在下一篇文章中,我将花费时间深入分析UE4的RayMarch,包括实现原理、与普通Raymarch的不同之处以及其优化点。
接下来,让我们看看效果吧。
在这次gtc中,西山居的董大侠现场展示了SSGI的效果。
我临时想起要拍照,于是抓拍了一张照片,但董大侠翻到了下一页,只剩下了这张模糊的照片。
NV官网发布的实际视频:
在Unity这边,效果与上面展示的非常相似。以下是仅考虑光源效果的结果,没有考虑环境IBL,因此当光线找不到物体时,整个房间会变成黑色。
平行光:
点光源
聚光灯
当然,SSGI与其他屏幕空间算法一样,也存在一些缺陷。知识答题系统源码当物体不在屏幕中时,这些缺陷就会暴露出来。因此,最理想的状态应该是将其与其他GI算法结合使用。
最后,我放上了两张美图。为了展示GI效果,我故意将间接光调得更强,标题图效果显著。
Unity移动端写实角色渲染-皮肤(URP)
记录个人在移动端角色渲染过程中,对URP技术的学习与优化。
贴图准备阶段,除了角色身上的几张贴图,还需一张SSS LUT贴图,用于模拟次表面散射效果。在移动端性能有限的情况下,合并灰度图至一张贴图,并从RGBA四个通道采样,记得对应通道与贴图的关联。
角色渲染主要分为四个部分:主光源BRDF、额外光BRDF(BilinPhone模型)、环境光漫反射、环境光镜面反射,以及次表面散射。
移动端性能限制,次表面散射仅处理主光源部分计算。预积分次表面散射通过查找SSS查找表实现,横纵坐标由NdotL(兰伯特公式:Dot(Normal,Light))计算得出,纵轴为曲率。兰伯特公式计算的NdotL细节过于明显,为了保留明暗部分的低频信息,采用模糊法线贴图进行计算,结果令人满意。
曲率部分,考虑梨子橙大佬的文章,曲率也可以烘焙成一张贴图,便于控制曲率范围。查找SSS LUT,横纵坐标为NdotL,纵轴为曲率,结果需进一步优化。
高光部分,使用DirectBRDFSpecular函数,解析来自于知乎文章。该函数计算出的高光效果优于Kelemen高光,参数包括基础色、金属度、高光、粗糙度、源码笔记创建用户alpha,以及BRDFData结构体。通过调整参数,达到满意的高光效果。
间接光部分,利用球谐光照与IBL(环境光遮蔽)实现。考虑到移动端性能,间接光烘焙为一张光照贴图,减少计算量。烘焙贴图过程在Unity中完成,可控制间接光的强弱。
额外光使用BilinPhone光照模型,简化计算。Shader支持SRP Batcher,减少渲染批次,优化性能。
性能优化包括:减少贴图采样次数、灰度图合并、减少计算量、间接光烘焙、Shader支持SRP Batcher、调整联级阴影级别与关闭Bloom后处理。这些策略在移动端实现了角色渲染的高效与逼真效果。
Python 实现 Unity 资源检测
为解决在项目中判断 ScriptableObject 文件引用的材质或贴图是否丢失的问题,采用 Python 解析 Unity 资源检测。Unity 资源导入流程中,源资源格式通常需转换为平台兼容格式,依赖于资源导入设置,包括 GUID。ScriptableObject 是 Unity 自定义序列化对象,用于存储序列化数据,引用的源资源通过 GUID 保存在 YAML 文件中。Unity 将资源导入设置存储在名为 LMDB 的数据库文件中,用于跟踪源资源的最终格式、修改时间与哈希值。
为解析 ScriptableObject,使用 Python 的 yaml 库,直接读取 YAML 文件。利用 lmdb 库的 lmdb.open 接口,将 Library 下的 SourceAssetDB 数据库文件复制为 data.mdb 和 lock.mdb,方便读取。在检查 GUID 存在性时,注意 Python 3 中 LMDB 存储字符串为 bytes 类型,需进行转码处理,并可能需要对 GUID 进行字节序转换。
采用 Python 实现 Unity 资源检测的优点包括:
1. 节省调用 Unity 命令行的开销,避免 Unity 启动时间过长导致的检查脚本执行时间过长。
2. 灵活解析 YAML 文件,直接访问资源信息,怎么下载源码教程无需执行 Unity API。
3. 通过 LMDB 数据库高效查询资源状态,提升性能。
潜在问题包括:
1. 需要确保正确处理 LMDB 的 bytes 类型存储与字节序问题,以准确解析 GUID。
2. 确保源文件安全,避免直接使用原始数据库文件,减少潜在风险。
3. 对于复杂资源结构,可能需要更细致的解析逻辑以准确识别丢失资源。
参考资料:
1. Unity 官方文档:docs.unity3d.com/cn/
2.知乎问答:zhuanlan.zhihu.com/p/...
3.数据处理教程:python.land/data-proces...
4. lmdb 文档:lmdb.readthedocs.io/en/...
5.知乎问答:zhuanlan.zhihu.com/p/...
Unity 编辑器扩展和序列化踩坑记(一)
本篇文章未经作者本人授权,禁止任何形式的转载,谢谢!如果在第三方阅读发现公式等格式有问题,请到个人博客地址或知乎地址阅读。
最近两三周在写 Unity 的编辑器扩展,主要是为项目开发地图编辑器,过程中遇到了一些问题,因此决定记录下来,形成系列文章。这是第一篇,但真心希望不要有第二篇。
目前我使用的 Unity 版本是 .1.3.f1。
在编写编辑器扩展时,如果需要将数据保存到文件,序列化数据是必须的。我尝试过两种方案:Unity 自身的序列化系统配合 ScriptableObject,以及 C# 的序列化 API。两者各有优缺点。
C# 序列化虽然方便快捷,但在 Unity 中使用可能并不理想。如果使用 C# 序列化,class 或 struct 必须加上 Serializable 属性。然而,大多数你想序列化的 C# 内置 class 或 struct 都没有这个属性,例如 VectorX 系列,还有 ScriptableObject。在需要使用 C# 序列化时,我不得不自己实现一个可序列化版本,例如 Vector2 和 Vector3。
C# 序列化在 Unity 中的另一个问题是,它不如内置序列化系统方便。虽然 Unity 的序列化几乎每时每刻都在进行,但有相应接口,你必须确保在必须序列化的时机进行序列化,以免数据被清空。
Unity 内置序列化系统以及一些配套组件(例如 ScriptableObject 等)在 Unity 中能保证序列化和反序列化数据的正确性,并且几乎不需要关心序列化的时机,只需要指明哪些数据需要序列化即可。一般来说,在会被 Unity 进行序列化的内置类中,public 字段和被 SerializeField 属性修饰的字段会被序列化,例如 MonoBehaviour 中的字段,ScriptableObject 也是,其他自定义类我没验证过,但是 private 的我都加了 SerializeField 属性没有问题,public 的也可以正确序列化。
但是并非所有类型都可以序列化!在常用的需要序列化的类中,Dictionary、二维数组以及其他有嵌套关系的容器(目前看来是这样,不能保证所有这种容器一定不能被序列化)不能被序列化。幸运的是,都有相应的解决办法。
虽然 Dictionary 不能被 Unity 序列化,但 List 是可以的。我们可以把 Dictionary 的 keys 和 values 保存在两个 List 里,这样就可以序列化和反序列化字典了。请注意,我并不是说要用两个 List 完全代替 Dictionary 这个数据结构,这样就失去了 Dictionary 的数据结构特性了。我们只需要在 Unity 进行序列化和反序列化的时候把 Dictionary 搬到两个 List 里,再从两个 List 中复原 Dictionary 就行了。Unity 进行序列化和反序列化的时机是可以知道的,关键在于一个 interface。
这个 interface 有两个方法:OnBeforeSerialize 在将要序列化的时候执行,OnAfterDeserialize 在反序列化完成后执行。因此,我们只需要在 OnBeforeSerialize 中把 keys 和 values 保存到两个 List 里,在 OnAfterDeserialize 中根据两个 List 中的数据重建一个 Dictionary 就行了。需要注意的是,这个 interface 目前不支持 struct。
如果是类似这样的数据,里面的 X[] 是不能被序列化的。但是如果你将 X[] 写在一个类里,外部容器保存这个 XWrapper,就可以正确序列化数组中的数据了。因此,如果你在使用嵌套容器时出现问题,可以考虑这种方法。
在编写编辑器代码时,会有一些情况导致内存中的数据被清空。我判断内存数据被清空重置的方式是观察静态构造函数在控制台的输出。目前我遇到的情况有:退出运行不会进行内存重置,但很重要,因为退出也相当于加载了场景,会执行 Awake 等相关函数,如果你有一些初始化函数在这时候执行,你需要注意它是否会影响到你。
ScriptableObject 类看似很简单,但第一次使用却花费了我很长时间来与其搏斗。这个类主要用来进行数据交互,它不需要挂在 GameObject 上,可以作为单独的数据存储来使用。在编写编辑器扩展时使用它,有不少地方需要注意。
ScriptableObject 可以保存为 Asset,而且实际使用时发现,如果你不保存成 Asset,相关的引用就会在某些时候变为空。例如使用 ScriptableObject.CreateInstance 创建一个 ScriptableObject 后,把它保存在容器里,然后函数结束。当你下次再用,就会发现这个引用变为 null 了。如果 ScriptableObject.CreateInstance 之后把这个 ScriptableObject 保存为 Asset,则没有问题。
如果使用诸如 AssetDatabase.LoadAsset<> 之类的函数把 ScriptableObject Asset 加载进来,然后保存在一个 MonoBehaviour 的变量里,当你删除这个 Asset 时,这个变量也为空了。也就是说,如果是直接加载这个 ScriptableObject Asset,当 Asset 被删除,加载进来的对象也没了(我原本以为挂在物体上的数据还会保留)。这个不注意可能会有一些致命的问题,例如你有个 ScriptableObject Asset,里面可能还有很多子 ScriptableObject Asset,策划想要用你的编辑器继续做上次没完成的工作,于是把上次保存好的 ScriptableObject Asset 加载进来进行操作,操作完毕后进行保存,可能会直接选择上次的路径,代码里可能会使用 AssetDatabase.CreateAsset 等方法,这实际上会覆盖原先的资源,也就是说之前的资源被删掉了。这时问题就来了,资源被删除后,Scene 中的数据也没了,这时保存代码执行,自然什么也保存不到。
解决方案是加载后使用 Instantiate<> 去复制出一个 ScriptableObject,之后都操作这个复制出来的,或者干脆用 ScriptableObject.CreateInstance 创建一个新的(要注意上面提到的问题,最好创建完后立即使用或者挂在物体上)。
保存数据时,尽可能不要用枚举作为 key,因为一旦枚举因为某些原因数值发生变化,数据可能取不出来。可以考虑将其转换为字符串。参考了 C# 的源码,枚举在进行 GetHashCode 时,好像是用它的数值进行操作的。
这次暂时写这么多,再次希望不要有第二篇。以上如果有朋友知道更准确的原因,可以留言。感谢大家阅读。
Unity Shadow Map(一 理论)
本文主要阐述Unity阴影贴图的理论部分。对于实践部分的深入剖析,请参阅《Unity Shadow Map(二 实践) - 知乎 (zhihu.com)》。同时,关于阴影贴图的参考资料推荐《图形学中级学习推荐 - 知乎 (zhihu.com)》及《图形学好书——虎书 - 知乎 (zhihu.com)》。 1. ShadowMap原理
光源深度图
光源被遮挡,形成阴影区域。从光源角度“看”,记录那些距离光源最近的位置。通过将摄像机置于光源处,通过正交或透视矩阵将场景投影至z-buffer,记录距离光源最近的z值,形成光源深度图,即阴影贴图。比较与光源的距离
在渲染过程中,将物体坐标从观察或世界坐标系转换至光源坐标系,比较z值与阴影贴图中的值。若大于等于(对于reverse-z,则为小于等于),则为阴影区域。示意图
左侧示意图展示阴影贴图记录了光源视角下的最近距离场景物体点。右侧示意图中,点a与光源深度和阴影贴图记录一致或更小,在光照范围内;点b深度大于阴影贴图记录,处于阴影中。 1.2 ShadowMap 的问题自阴影 Shadow acne
问题1:有限精度和分辨率
深度数值有限精度,且阴影贴图分辨率有限,无法精确表示所有深度值。问题2:采样点不一致
光源深度采样点与摄像机采样点不同,且精度问题导致采样点深度值仅代表采样区域中心点。消除方法
常量偏移
物体表面沿光源方向施加常量偏移,使摄像机采样点深度小于阴影贴图中心点深度,消除自阴影。斜率比例偏移 + 常量偏移
根据表面与光源方向的倾斜程度调整偏移量,充分保证自阴影消除,避免过度偏移引发漏光和漂浮问题。法线偏移偏移
沿法线方向偏移,基于表面法线和光线的sinθ值,使表面相对光线倾斜时偏移增多。偏移方式的副作用
漏光(Light leaks)
模型表面在投影过程中发生收缩,导致模型间隙,漏光现象。漂浮(Peter Panning)
投影物体与阴影面分离,产生漂浮效果。其他消除方式
背光面投影
将背光面作为投影面,可消除自阴影,但易导致漏光,且适用于密封且有一定厚度的模型。中间面投影
追踪离光源最近的两个面,取中间深度值作为投影面,增加性能损耗。 1.3 Soft Shadow软阴影
介于完全阴影和完全照亮之间的半影,改善阴影贴图分辨率不一致导致的马赛克问题,使阴影更真实。Percentage Closer Filter (PCF)
原因
光源大小影响阴影范围,部分光照导致半影,距离越远,半影越小。思路
以渲染点为中心,对阴影贴图采样点(u, v)进行偏移采样,比较所有采样点的实际深度,计算阴影或照亮的百分比,决定半影明暗程度。采样区域与注意点
采样区域为渲染点对应的阴影贴图纹理采样点(u, v)周围,模拟渲染点的半影情况。
所有采样点与渲染点实际深度比较,假设渲染点周围对于光源方向为等距离平面或弧面。
均值滤波
简单的PCF滤波,使用box filter对阴影进行卷积操作,求取9个像素函数值的平均值。
三角滤波(Tent Filter)
更平滑的滤波方法,函数图像为等腰三角形,覆盖4个像素,用于减少边缘阶跃现象。
Unity中应用
硬件采样时,以2x2单位进行采样,进行双线性插值,3x3滤波中,进行4次硬件采样,覆盖个像素。
硬件采样点可能不在中心,利用已知的横向和纵向权重比,计算P点的采样坐标。
加权求和,将个像素的权重加权求和,得到P点的软阴影值。
基于Unity Compute Shader视锥剔除(一)
在深入学习Unity中的簇光处理和基于Computer Shader的视锥剔除技术时,本文(一)将简要介绍Computer Shader及其基础应用。若有误,期待读者指正。 主要参考了知乎上的文章《UnityCompute Shader的基础介绍与使用》,虽然部分内容是直接引用,但这并不影响理解。让我们直接进入主题:Computer Shader是利用GPU进行计算,将结果直接传给着色器,以提高效率并减轻CPU负担。1. Computer Shader的使用
首先,创建Computer Shader文件。默认文件包含#pragma kernel CSMain这样的主函数声明,用于GPU执行,以及RWTexture2D Result的声明,表示可读写纹理类型。接下来,我们理解核心部分的编写和线程管理。2. 编写和理解Computer Shader
在Shader中,#pragma kernel后面紧跟函数名,例如CSMain,它相当于GPU执行的入口点。注意,注释应与命令分开写。函数体中,你需要编写自定义的计算机着色器代码。 SV_DispatchThreadID参数涉及线程组和线程的管理。对于NVIDIA和AMD显卡,线程组数量应为或的倍数,以充分利用硬件并行性。在脚本中,通过Dispatch()函数分配线程组数量。3. 脚本代码示例
脚本中,通过设置randomWrite和纹理映射,以及Dispatch()来指定线程组分配。举例中,对6x6像素进行分块处理,每块由2x2线程处理,从而实现像素级别的操作。4. 实现视锥剔除
利用Computer Shader的灵活性,可以进一步实现视锥剔除功能,通过调整groupid、groupthreadid或dispatchthreadid,根据需要控制每个线程组的处理行为。unity lua该å¦åªä¸ä¸ª ç¥ä¹
é¦å ä½ å¾èªå¦å 个项ç®ï¼æ代表æ§ç并ä¸å¾æ·»å ä¸äºèªå·±çæ³æ³å¨éé¢ï¼ä½ åå»å ¬å¸ä»ä»¬å¾ç¥éèªå¦è½åæä¹æ ·ï¼èèä½ åºç¡ç¥è¯ç¶åè®²è§£ä½ ç项ç®ï¼å·¥ä½ç°å¨é½ä¸æ¯å«äººæ¥æ¾ä½ äºï¼èæ¯ä½ å¾å»äºé¾