真的是干。
整理/秋秋&依光流
10月24日下午,叠纸《恋与深空》团队在Unite Shanghai 2025上,带来了一系列关于游戏研发的干货分享。
说起《恋与深空》,很多人可能已经不陌生了。
其作为叠纸有史以来收入最高的游戏,上线一年半以来,在全球市场展现出惊人的爆发力——AppMagic预测,其累计营收已突破50亿元大关;前不久,它还作为唯一入选的中国游戏,获得了2025科隆游戏展「最佳移动游戏」奖项;在国内,游戏也曾6次冲上iOS畅销总榜榜首。
可以说,无论是商业成绩还是行业影响力,《恋与深空》都能称得上一款创造历史的游戏。

在本次Unite大会上,叠纸《恋与深空》的引擎负责人阮天龙、技术美术负责人秦平与资深物理算法工程师赵英杰,分别从影视级渲染管线搭建和物理效果开发等角度,分享了背后的技术实践与心得。
从葡萄君现场的感受来看,叠纸这轮分享比干货还「干」,并结合了大量实例。
第一场引擎负责人的演讲PPT几乎全是代码与示意图,葡萄君可以坦诚地说一句「完全没看懂」;后两场演讲虽用了大量动图演示,但其信息密度同样达到了万字级别——换句话说,今天可能是叠纸对开发者们最「掏心掏肺」的一次了。

01
从光影到脸红流汗
大家好,先自我介绍一下,我叫秦平,2017年加入叠纸,此前有幸参与了《神秘海域4》《怪物猎人:世界》等项目,成为了叠纸首位TA,参与开发了《闪耀暖暖》《恋与深空》,现任《恋与深空》项目的TA负责人。
因为之前做过一些主机游戏,所以对高品质的游戏效果有一定的追求。今天想借这个机会,跟大家聊一聊《恋与深空》项目在追求角色效果表现上面的一些问题和解决思路。
我先总结一下对我们角色表现影响比较大的几点:
第一是分镜,项目的每一个具体表演和每一段约会,都采用了影视级导演分镜思维,来指导创作。一个好的分镜思维,是优秀作品不可缺少的部分。
第二,我们采用了大量真人动捕来提升角色动作表现;
第三,有了分镜和动作后,我们工具组的同事花了大半年的时间,打造的一套专业的剧情表演工具;
第四,我们采用了AI驱动的校对口型、表情同步的技术方案;
第五,也就是我们这次分享的主题:定制化的光照渲染方案。

说到这里,可能大家都会遇到一些上述问题。比如有人会抱怨游戏场景和角色分裂感很严重,或者一个静帧打光完成后,看着效果还可以,但镜头和角色动起来,效果就差了很多等等。
我将具体的光照问题分成四部分:
第一是光照需求的冲突。比如一个夜晚的场景,需要用冷色调渲染夜晚的氛围,但角色面部,则需要一个暖光来保持自然肤色——如果我们只用冷色调强调氛围,那就容易让角色不自然或失去立体感;
第二是协作问题。比如一个室内的场景已经设计好了,但放入角色后,发现角色表演的位置透不进来光,就去跟场景同学沟通开窗或者增加光源,但补光又可能导致动作与光源不匹配……这些反复沟通可能会浪费很多成本;

最后是高品质的需求。我们希望游戏呈现的内容,每一帧都经得起大家反复观看,因此就需要灯光效果可以逐帧精修。
我们将上述问题拆解一下。
关于场景和角色,我们需要这两者的光照可以分开调整,尽量互不影响,同时支持逐帧调整光照参数和效果,以及支持模板的保存和编辑。

我们知道光照是由直接光和间接光组成的,直接光是平行光、射灯和点光。一般情况下,我们只会有一盏平行光,我们习惯称之为主光。
我们的基本思路是:使用主光源正常照亮整个场景,但在处理角色时,我们只保留主光的方向信息,转而使用角色的PPV(Post Process Volume,后期处理体积)来覆盖主光原本的颜色和强度。
具体实现上,我们额外传递了一个专属于角色的主光颜色,确保角色获取到的是最符合其视觉需求的「角色色谱」。此外,还为角色单独添加了一盏不产生投影的平行光,专门用于勾勒角色的轮廓。
同时,我们还预留了两盏额外的灯光供角色使用,它们可以是任意的点光源或射灯组合。这些是正常的光源,能照亮其范围内的所有角色和场景物体。由于我们的系统规定在每个2米的格子里最多只能存在四盏这样的额外光,因此我们对其进行了划分:固定分配两盏给角色,另外两盏则留给场景使用。

至于环境光的高光部分,我们仍然使用了场景通用的反射指针来获取信息。但在一些特殊的角色材质上,我们支持了一些参数的覆盖输入。
接着,我们将所有影响角色光照的参数统一存储在一个对象(Scriptableobject)中,由灯光师调整完毕后,保存为一个可复用的模板。大家可以看到下方右侧这张图,这就是我们保存下来的一个资源包,我们称之为「角色工作模板」。

最后,我们通过一个管理器(Manager),采用类似「栈」的结构来统一管理这些灯光模板。这种栈式的管理方式,与我们的实际使用需求密切相关。通常情况下,除了加载一个新的灯光方案之外,最频繁的操作就是需要快速切换回上一个使用的灯光效果。栈结构天然支持这种「返回上一步」的逻辑,因此能非常高效地满足这一特性。
到这里,这套角色光照方案就基本完成了。它成功实现了我们最初拆分的核心需求:角色光照与场景光照能够分开调整,并且支持实时、灵活地切换。
下面是角色在不同光照方案之间切换的实时表现。

大家接下来看到的,就是在编辑器里进行动态配帧的实录。我们可以像处理角色动画一样,为光照变化「K帧」,精细调整每一刻的光影效果。可以看到,其中大量的参数都被设置了关键帧——不仅是后处理效果,包括灯光、阴影的细微调整,也全都录入了动画曲线。

而正如大家所知,光与影从来密不可分。介绍完「光」,接下来就重点谈谈我们的「阴影」。
我的同事之前提到了我们的阴影方案:三级CSM配合特写阴影。我将着重介绍后者——特写阴影。
它的原理很直观:我们可以在角色身上选定一根骨骼作为球心,并指定一个半径,从而构建一个包围球。我们的子系统中可以设置这个包围球的半径,并通过「父节点」参数来指定那根作为球心的骨骼。比如我们以选定的骨骼为球心,以0.85米为半径构建球体。


以上演示主要是为了说明,我们的特写阴影系统提供了丰富的参数可供独立调整。比如,我可以推动「近裁切平面」参数,将房屋的墙壁排除在阴影计算之外,从而确保角色面部被准确照亮——我们的目标是通过灵活调整这些参数,为不同场景找到最优的配置,最终呈现出最好的视觉效果。
说完了整体的光影,我们接下来聚焦于角色本身的渲染细节,特别是皮肤质感与面部细节表现。
在皮肤渲染方面,我们提供了高低两套配置方案:高配方案采用基于屏幕空间的3S技术(Subsurface cattering)。由于3S本身需要模糊处理,我们适当降低了分辨率进行计算,这反而在保证效果的同时兼顾了性能;低配方案则使用了一套专门的工具,来实时拟合与生成近似的皮肤质感——这部分技术今天就不详细展开了。
接下来,我们重点介绍一些更细微的表现。
例如,我们实现了动态的脸红和流汗效果,它们能极大地增强角色的生动感和情绪表达。此外,我们还完善了大量面部细节。比如解决了张嘴、闭嘴时口腔内部的环境光遮蔽问题;实现了眼球高光的动态变化,以及睁眼、闭眼时双眼皮的自然褶皱效果,但由于时间有限,我们先聊聊角色脸红和流汗效果。
下面是我录制的一张卡牌的动画效果,为大家展示角色的脸红表现。当女主与男主进行互动时,动态改变面部肤色能极大地增强画面的生动感和真实感。


而当脸红到一定程度,比如在运动后满脸通红时,如果只有脸红而没有流汗,真实感依然会打折扣。所以,我们还配套开发了流汗效果。流汗效果主要由两部分构成:
一是粒子表现:我们使用粒子系统来模拟汗珠飞溅的效果,用来描述一些甩汗的效果;
二是材质变化:这部分又细分为两点:首先是通过降低皮肤粗糙度,来模拟出汗后皮肤湿润的反光感;其次是生成了凝聚的汗珠。

具体来说,首先,我们将角色面部的UV空间划分为均匀的网格;然后,将每个网格的唯一ID作为输入,确保每个格子都会得到一个固定且唯一的随机数。
这样一来,我们就能根据这个处理后的随机值,来决定在哪个格子的范围内生成汗珠的初始凝聚点。这不仅保证了汗珠位置的可控性,也避免了不自然的均匀分布。
大家可以看到下面的这段简短的代码,就是这一逻辑的具体实现。右边视频所展示的、带有随机分布感的逼真流汗效果,正是基于这套机制实现的。


感谢大家的倾听,以上就是关于角色面部细节渲染的分享。
02
风吹衣角、发丝微颤……
这些细节全靠它
大家好,下面我为大家带来《恋与深空》物理效果开发的分享,首先做一下自我介绍,我叫赵英杰,2020年加入叠纸,参与过《闪耀暖暖》《恋与深空》项目,目前在《恋与深空》内主要担任资深物理算法工程师,主要负责物理和动画的相关内容开发。
这次分享主要分为四个部分:布料模拟的实现、实时表演的控制、基于Unity DOTS的开发,以及最后的检测模块。
首先是布料模拟的实现,这在《恋与深空》中其实占了相当大的部分,比如具体表演、战斗、互动、玩法当中,都大量使用了布料。
我们自主开发了一套基于骨骼的布料模拟系统,团队内部我们都叫它Stray Cloth。采用的模拟方法,是XPBD结合SubStep的方式。相比PBD,XPBD的优点是摆脱了迭代次数和时间步长的依赖,结合Substep可以显著提升收敛效果。


事实上,运动插值虽然性能开销不是很高,但由于碰撞类型比较多,比如有各种碰撞体,分场静态粒子等等,所以实践起来还是非常麻烦的。
这里有一个疑问,为什么我们使用骨骼,而不使用代理网格、使用顶点的方式,去模拟布料?
因为《恋与深空》对于布料模拟表现的需求其实比较复杂, 很多时候需要动画、解算的共同介入,骨骼方案可以很好地在这两者之间过度,以及平衡。
然后受限于移动端性能,骨骼方案结合我们的配置,可以给美术很大自由调节的空间。在骨骼的基础上,我们实际上构建了类似于顶点模拟的约束方式。下面右边的图,就是我们新资产的效果,可以达到和Mesh模拟相对近似的效果。

而且它的参数调整非常不直观,因为它有Local和Global两个参数,不利于美术调整,以及在不同场景下的效果匹配,特别是在大形变的情况下,效果表现就非常奇怪。


效果上更贴近于《恋与深空》3D写实美术风格;而且在弯曲参数上也只有一个参数,给美术调整起来更加直观;并且三个轴的参数是分离开的,在模拟一些特殊场合,比如带有裙撑的裙子,就可以通过各项分离的弯曲参数来模拟出近似裙撑的效果。
这个方法其实在正常情况下更多用于头发、绳子的模拟,所以头发也和衣服一样,使用同一套约束方案,这样整个工程量就会简化不少。如下图右侧是我们《恋与深空》最新日卡的表现效果,总的来说,基于Cosserat Rod的骨骼约束是可以满足项目表现需求的。


但是对于一些骨骼交界处,比如有多个骨骼的存在,或者说存在一定的骨骼拉伸和收缩的较为复杂的位置,比如说手肘,肩部,腰部,表现上容易出现布料和角色的分离。
这个时候,我们提供第二种吸附的方式,和Maya里面的吸附方式类似,我们将静态粒子吸附到角色模型的某个三角形上,通过并行的Bake Mesh来获取每帧顶点的更新位置,使用离线计算的重心坐标来更新粒子的Transform。
对于三角形存在的退化问题这种特殊情况,我们使用三角形顶点的Spin蒙皮骨骼的变化进行加权平均,来分析静态粒子的Transform来解决三角形退化时的这些特殊情况。


在碰撞解决流程上,我们并不在检测到潜在重叠后立即生成Contact Generation。而是先将这些检测到的重叠信息缓存起来,留到物理更新的Substep中去解决穿透问题。采用Substep的主要优点是:在大多数情况下,仅依靠离散碰撞检测,就能有效避免因物体快速移动而导致的穿透问题,从而无需引入计算代价更高的连续碰撞检测方案。

但是Mesh Collider作为不规则的凹凸,甚至有些时候它都不是封闭的,只是一个面,想要表达精准的碰撞效果,相对于参数化的几何体来说就比较困难,特别是在移动端设备下。
然后我们采用的方式,就是用Spatial Hashing来作为三角形的一个粗略查找方式,结合缓存的邻近三角形结果在迭代开始时,生成一次粒子三角形的碰撞对,在后续的迭代中判断粒子是否在三角形的范围内,如果超出范围我们会通过连接关系,进行限制步幅的查找来获取适合的三角形,可以缓存它的结果作为下一次的使用。
下面右边的图,就是项链和角色身体的表现,可以看到效果还是比较稳定的。


所以我们直接使用16x16的CubeMap,来一起计算各个方向上的三角形,这样子我们碰撞计算的时候,就可以快速查找到临近的三角形,相当于替换掉上一个Mesh Collider当中Spatial Hashing的粗略查找结果。


在实际实践中,我们使用上一次Spatial Hashing的粒子位置,和当前的粒子位置来进行碰撞,这样就可以简单的解耦的数据,避免它的依赖。然后下图右边是我们层间碰撞的例子。


因为我们的资产结构的关系,所以说它必然为一个Uniform网格,因此我们可以通过这个网格的交点,来比较简单的推测出其他粒子的推出三角形,最后对穿透的粒子三角形施加弹簧约束来解决他们的穿透。
在实际实践中,由于Substep的关系,穿透的概率相对来说是不大的,因此我们会采用分帧分块执行,来减轻它的系统压力,下面右图是一个三层的穿透分离的测试,可以看到各种布料可以从穿透当中恢复过来。

《恋与深空》中的大部分剧情表现,都是依托于Cutscene来实现的,实现各种各样的效果与控制的调节,这里要感谢工具同学开发和做一套非常强大的Cutscene工具。在他们的基础上,我们开发了多种功能轨道来具体的调控物理效果。
物理相关的功能轨道,其实我们做了非常非常多的准备,下面只是一部分比较常用的功能轨道,后面我会比较详细的介绍它们的具体功能。


然后是SmoothBlendPose,在表现当中一个非常常见的问题就是动作瞬间切换带来的物理抖动,这个无论是在剧情表演中还是在换装当中都非常容易出现。
我们开发了一个较为通用的办法,通过记录初始物理姿态,在切换的时候就是用初始姿态与当前姿态进行姿态插值计算,这样就可以大幅度缓解这个抖动。当然这可能会带来一些时间开销,一般来说会在几毫秒到几十毫秒左右,大多数情况下,都是可以接受的。
我们还会额外提供一些参数,比如插值次、插值步幅大小,然后美术可以根据实际的需要去调整。下面图片右侧是拍照换装切动作时,会有一个大幅度的动作切换,可以看到它的表现还是相对稳定的。


这种情况下,我们提供了一个比较直接的方案,就是一键,由美术直接保存某个时间的物理状态,在播放的时候,直接将物理状态用到布料上,这样就可以完美避免切镜带来的布料抖动问题。


我们提供编辑参数的轨道,通过这个轨道来实时的修改参数,绝大部分的参数都可以被我们这个轨道来覆盖到,可以非常方便美术来针对一小段的时间帧进行参数修改。
这个参数修改其实还可以用来做一些特殊的效果。比如说下图当中,美术会利用这个参数轨道来对约束参数做一些编辑,这样可以实现一个实时的类似于布料断开状态的效果。


在实际的制作流程当中,动画在DCC里面和最终进引擎的表现差异非常大,还包括一些引擎的支持率的修改系统之后,动画可能会和其他地方有穿透,所以我们会在动画融合的基础上再叠加上物理碰撞的效果来避免一些穿插。


通过角色部件类型,还有布料的层分组,来细节控制所要影响的对象范围,并且碰撞体和风场轨道的绝大部分参数都可以添加动画帧来控制,包括碰撞体的形态大小,风场的方向、范围、强度、湍流等,方便美术细节把控物理效果跟风吹变化。下图就是碰撞体和风场的表现例子。


当然,在针对性的在这个项目使用当中,我们也做了一些优化来进一步的提升性能。第一个就是Cache Job。模拟当中Job教务数量和依赖关系是相对比较确定的,Job data并不频繁变化,针对的一般为相同数量和依赖关系的Job组进行多次循环。

实际实现上相对来说比较简单,因为它是一个专用的结构,只考虑一些固定的使用场景,我们额外添加了一个Atomic Queue来存进Cache Job,使用Fetch-and-Add Array来具体存它需要的Job data,下图右边是我们执行Cache Job的一个简单的流程示意图。

下面是具体的使用案例,对市面上流行机都有比较好的性能提升(骁龙855开始、苹果a11开始),特别是做展开用指令集能得到更高的性能提升。




当然,如果只是直接的转制,也可以得到更快的性能,但之所以要像前面这么做,是因为我们一般在项目中,是通过这4个float转制,来将点乘变成矢量乘,所以这里写成这样子。
但在实际的使用当中,你的Mathematics的代码实际是类别进你具体的一个上下文当中的,它最终生成的Simd Code可能会相当于你自己的代码测试,差别会非常大。
所以说在具体优化的时候,你还要根据代码的上下文进行一个具体的调整,可以根据Brust Inspector分析测试还有一些优化手册来具体修改。

因为《恋与深空》中有相当多不同种类的玩法,玩法及种类也相对独立,有一些模块,比如说战斗,需要有特殊的一个Trigger触发和退出机制,并且他们希望在底层有更多的支持,所以执行流程上,也希望有更多灵活的控制。
然后我们在细分探索上,也有一些自己的想法,就是在只需要碰撞测试的情况下,我们利用DOTS能够提升它的性能。


并且对于查询结果,在底层去保证的线程安全,将上层可以无负担调用,结合DOTS进行轻量化的结合实践以及一些结合实际需求的优化。在性能测试当中,我们最高获得了15%的性能提升。
下面是游戏战斗的画面,整个角色的移动包括一些技能运动,都是用我们自己的这套碰撞检测模块来实现的。

然后我们使用基于SAH的Dynamic BVH作为加入结构。在插入、删除,以及超出范围的时候,对当前操作节点的临近几个层级进行旋转、平衡,在修改效率以及数的质量之间做了一个平衡。
因为碰撞检测的功能相对来说比较概括,对于精度没有那么高的要求,所以我们适当的也牺牲了一些精度,简化了一些碰撞检测算法,来提升它的性能。

最后,我们会通过History计数来标记Collider版本,来解决整个战斗当中它对一些角色或者说碰撞体的复用,可能导致的潜在问题。我的分享结束了,谢谢大家。

点击「阅读原文」可了解详情
推荐阅读

游戏行业书籍推荐:
点击下方名片,关注公众号
(星标可第一时间收到推送和完整封面)