跳转至

Candycat Blog

「总结」历年图形渲染相关的演讲

前言

上海疫情在家快三个月了,完成了4月份立的两个flag,一个是要坚持锻炼(健身环终于一周目通关了,虽然体重基本没啥变化- -),一个要刷今年刚出的GDC。之前也一直在看但经常过段时间就有点想不起来在哪篇看到了哪个感兴趣的技术,想到索性可以把所有图形渲染相关的演讲搞到一个地方,方便检索,这就是这篇的目的了。

我的方法是,先过一遍GDC、SIGGRAPH这种会议,把跟图形渲染相关的演讲标题总结到这里,我会按照自己的理解分成几个类别,可以通过目录快速跳转,然后再重点挑自己感兴趣的过一遍。不一定完全是程序相关的,有些跟TA、PCG等我比较感兴趣的也会放进来,当然为了索引完整性还有一些水货会混进来 :)

目前更新到的会议有(主要是GDC 2022,其他不定期更新中):

  • GDC (2022, 2021)
  • SIGGRAPH (2021)
  • Digital Dragons (2021)

综合向

啥都会讲一点的那种。

[GDC 2022] One Frame in ‘Halo Infinite’

链接:PDFVideo

公司:343 Industries - Microsoft

[GDC 2022] An Overview of the ‘Diablo II: Resurrected’ Renderer

链接:PDFVideo

公司:Blizzard Entertainment

简介:介绍了给Diablo II开发的新渲染器,基于Forward+(便于快速开发shader),没有使用任何烘焙光照(有大量动态效果和过程化生成的场景),非常注重效果的Scalability(要能随时开关,保证可以在Switch这样的低端机上运行):

  • Overview of the renderer:简单介绍了整个管线包含的各个pass,比如grass visibility、light bin、global attenuation等
  • Deep dive into select techniques:重点挑选了几个技术点介绍,包括:
    • Specular Antialiasing:求法线的偏导得到着色表面的几何复杂度,将其加到粗糙度上减少aliasing
    • Scalable Lighting:场景光源很多走完整的forward lighting在低端机上开销太大,解决方法是维护玩家周围一定范围内的3D lightmap,把场景里的光源贡献渲染到这些voxel里。具体是使用CS生成两张32x8x32 PF16精度的纹理,一张负责累加相对于这个grid position的所有光源的方向,另一张存储光源的radiance,并使用一种近似方法来模拟高光
    • Tonemapping:基于Call of Duty的Tonemapping方法
      • tonemapping曲线几乎是线性的,主要目的就是为了把场景亮度round到10000 nits(HDR标准的peak brightness)
      • display mapping曲线是给显示器用的,和CoD一样,LDR显示器使用sRGB曲线,HDR使用BT.2390标准曲线
    • Color Grading:基于Call of Duty和Frostbite的工作
      • 以exr格式导出场景的HDR图像,其中不包含任何tonemapping和其他后处理效果
      • 再导出一张.cube格式的LUT,其中编码了LDR的tonemapping曲线
      • 导入Davinci,项目设置会使用上面导出的EXR和LUT以便让初始效果和引擎内看到的完全一致
      • 美术在Davinci里调色,然后导出格式同样为.cube的65x65x65 LUT作为这个场景的color grading
      • 使用PQ曲线去解决LUT的精度问题
    • Order Independent Transparency (OIT):因为游戏有大量效果是半透明的,同时玩家观察距离又很近,所以对透明排序要求很高
      • 参考了weighted blended order-independent transparency,共支持4种半透明模式:additive,additive with alpha,premultiplied和alpha blend
      • 靠把additive pass单独拆出来,带宽从128bpp优化到80bpp
      • 由于并不是真正的排序,weighted OIT的权重会极大影响最后的混合效果,需要和美术不断沟通调整
    • Hair Rendering:有另一个演讲专门讲Diablo II的头发渲染的,去瞄了眼全是公式和文字,连个图都没有不看了
  • A frame on the GPU:通过在Xbox Series X上截帧的结果,分析forward rendering和transparency rendering这两个pass的occupancy都比较低,原因是shader太复杂导致VGPR很高,给了优化VGPR的一些思路
    • 尽量缩短变量的生命周期
    • 尽可能让各个代码块之间相互独立不要有相互依赖
    • 可以尝试重新排序代码逻辑,可能会有奇效
    • QA环节有人提到这些优化可能会在不同平台有不同影响,尽量照顾性能比较弱的平台
  • A frame on the CPU:介绍渲染线程的CPU部分,介绍多线程job system,使用RenderGraph架构
  • Streaming Performance:简单介绍了streaming的策略
  • LODs:使用了Simplygon制作LOD,给出了PS4上不同LODs的指标和性能表现

[GDC 2022] New Graphics Features for ‘Forspoken’

链接:PDFVideo

公司:Luminous Productions Co., Ltd.

简介:作者是开发《最终幻想15》的日本公司,Forspoken是他们开发的一款所谓”AAA“游戏,使用自研引擎Luminous Engine开发,主要介绍引擎渲染的各种features,干货不多随便看看吧:

  • Model Rendering:包括GBuffer Layout(Normal使用了20 bits的Octahedral Normal Encoding),使用Translated World Space(就是UE的做法)去做坐标变换来提高矩阵乘法之后的Position精度
  • Shadows:使用Static Shadowmap提升CSM性能、Screen Space Shadow、Hybrid Ray-Traced Shadows/Ambient Occlusion
  • Lighting & Post Effects:
    • PRT:全局光照使用了PRT,使用ray tracing去bake,先生成cubemaps然后投影到SH上,通过在场景里摆放若干volumes去生成probes,在实时判断这些volumes与视椎体的可见性判断是否需要渲染
    • Specular Correction:由于specular probe的密度远小于irradiance probe,所以会使用Irradiance的采样强度去做Specular Correction,做法是拿irradiance的SH0和ibl的SH0做比值,拿结果去缩放ibl采样的亮度
    • Volumetric Cloud:解决体积云与半透明物体的混合问题,依靠记录若干固定density level的depths值,判断半透明物体位于哪两个levels之间再使用其对应的alpha值与半透明进行混合
    • Wide Gamut:支持广色域空间HDR
  • Optimizations:一些优化包括GBuffer Pass如何sort、使用Async Compute提高并行、使用VRS(见GDC 2022的AMD的演讲)减少诸如VFX这种heavy pixel pass的开销
  • Automatic LOD Generation:模型自动LOD生成

[GDC 2022] Breaking Down the World of Athia: The Technologies of Forspoken

链接:Video

公司:AMD, Luminous Productions

简介:主要是为了推销AMD的技术,介绍AMD帮助Forspoken做的一些features和优化,包括:

  • Single Pass Downsampler:一个pass输出所有的dowmsampled mips
  • CACAO:Ambient Occlusion,可以Async并行加速
  • RTAO:离摄像机一定距离以内的使用ray traced ao,以外用CACAO,使用ADM Shadow Denoiser
  • Variable Shading:VRS,通过上一帧每个tile的luminance variation来决定这帧的shading rate,需要AMD硬件支持
  • Hybrid Shadows:判断当前位置是否需要tracing,还是使用shadowmap就足够了
  • FidelityFX Super Resolution:类似DLSS
  • Microsoft DirectStorage:加快加载速度

[GDC 2022] A Guided Tour of Blackreef: Rendering Technologies in Deathloop

链接:Video

公司:AMD

[GDC 2022] Simulating Tropical Weather in ‘Far Cry 6’

链接:PDFVideo

公司:Ubisoft Toronto, Ubisoft Montreal

简介:介绍Far Cry 6里独特的热带天气系统是如何实现的:

  • Core Controls:介绍了Weather Manager的使用,游戏分为3种Region,每种region会以5天为单位循环天气,靠文本文件定义了一天内每个小时的天气preset
  • Material Wetness:介绍如何实现材质的Wetness效果,主要分为Static和Dynamic两种方式
    • Static用于静态物体,会渲染一张Wetness Shadowmap来避免在室内等区域错误地改变了场景湿度,同时每个材质控制Porosity属性来决定它受Wetness的影响程度,在Deferred Lighting时根据Porosity来修改albedo、smoothness、metallic和specular等材质属性
    • Dynamic用于角色、武器和载具等动态物体,需要额外的raycast来判断本身是否需要暴露在雨中,并依靠单独的Shader逻辑来应用Wetness,所以更加自由可控。介绍了对角色、武器、载具和植被的Dynamic Wetness的特殊处理,例如怎么渲染雨滴等效果
    • 如何处理Terrain的下雨效果
  • Rendering Features:介绍了大量跟天气系统相关的渲染features是如何实现的
    • Lighting:简单介绍使用的BRDF
    • Global Illumination:基于美术手摆的Probe,烘焙了11个时间点的光照+Sky Occlusion+Local Lights=13个frames的probes lighting数据
    • Atmospheric Scattering、Volumetric Cloud、Volumetric Fog:都是比较常规的方法,体积云是半分辨率渲染,用了Checkboard做upsampling。体积雾为了减弱shafts因为低分辨率导致的阶梯状瑕疵使用了Exponential Blurred Shadows
    • Reflection:有另一个专门的演讲介绍Far Cry 6的Raytracing方案计算反射,这里主要就介绍了作为fallback的Cubemap方案。美术手摆Probe,每个存一套GBuffer然后每帧计算Relighting,不考虑shadow,每帧只relight一个face
    • Rain:GPU粒子的方案
    • Lightning、Ocean、Tree Bending

[Digital Dragons 2021] Rendering Watch Dogs

链接:Video

公司:Ubisoft

Lighting

光照综合向相关。

[GDC 2022] Advanced Graphics Summit: ’Cyberpunk 2077’: Bringing Light to Night City

链接:Video

公司:CD Projekt RED

简介:介绍了2077在Lighting方面所做的工作,包括:

  • Visual idea:视觉设定
    • 介绍如何营造赛博朋克风格分光影,参考了攻壳机动队、星际穿越等电影作为参考
    • 制作Lookdev,编写了Nuke脚本生成Tonemapping使用的LUT,使得16位的Lighting结果经过LUT映射后的画面更加具有电影感,并且为SDR和HDR分别生成了两种LUT
  • Setting the Stage:
    • 24-hour Cycle:24小时的光照参数在物理光照参数的基础上进行了一定缩放,来得到更好的曝光和视觉效果,比如拉长了magic hour/blur hour时间、增强了月光和夜晚天空的环境光强度、大大降低了太阳光/天光的强度(大约100倍)等
    • Global Illumination:GI方案在参考了全境封锁的Probe + Surfel方案的基础上做了一些改善,包括使用Volume控制Probe密度、增加了防漏光数据等等,可以覆盖摄像机周围128~192米左右,超过这个距离就会fallback到Distance GI(使用topdown的RSM,竖直方向靠Reflection Probe的Visibility去弥补)
    • Reflection Probes:使用层级摆放的Reflection Probe,Global Probes -> Large City Probes -> Detailed City Probe -> Interior Probes,依次精度和优先级更高,最后SSR会作为最高级别的反射来源
  • Ray Tracing:项目后期NVIDIA团队帮忙支持Ray Tracing,使得GI可以使用更加统一方法实现
    • 使用了Hybrid Raytraced的方式来进一步提升GI效果解决复杂结构的全局光照问题,具体方式是用Raytracing的结果来替代GI系统中的Sky Light和Sun Light的第一次反弹贡献,其他部分保持不变
    • 使用Ray Tracing计算自发光物体光源、RTAO和Reflections
    • 增加了Raytraced Reference模式来使用光追完全替代全局光照的计算,不依赖任何传统的Subsystem,这一功能计划未来开放给玩家使用
  • Lights:
    • Capsule Light:面光的实时开销太大了,所以使用Capsule Light来模拟各种面光源,结合Volumetric Fog营造氛围
    • Dynamic GI with Portal Light:动态GI方案由于精度问题可能会导致室内光照比室外暗很多,并且由于性能问题GI并不会参与贡献Specular Response。为了给GI补光,增加了跟着TOD变化的Portal Lights,这些光源通常被放置在窗户和门口附近,美术也可以进一步调整光源参数来定制化效果
    • Character Lighting:角色光照单独打光,使用Raytraced Shadow得到更加正确柔和的阴影效果
  • Managing the Budget:解决性能问题
    • Large Angle/Radius Lights:把shadow radius和light radius分离开来调整,这样即便light radius很大,它的投影范围也不用那么大来节省性能,同时也能提高shadowmap利用率
    • High Light Density:使用Fog-only Lights来尽可能减少真正产生光照的光源数目;使用Light Channel Volume来指定光源可以影响到的空间区域,尽可能减少需要投影的光源;使用streaming distance和streaming range控制光源投影的淡入淡出距离;Dynamic Shadow Lights最多6个,Static Shadow Lights最多4个,后者的shadow不需要每帧更新
    • Deep Vistas:把光源bake到Distance Lights,只计算非常简化的diffuse only光照结果,会考虑光源的inner和outer angle以及模拟真实光源的闪烁,在渲染时会考虑Occlusion Query来判断是否需要渲染,在超远视距下和真正的光照结果做淡入淡出;使用Light Pollution Particles来减弱超远视距下的环境视觉噪声,和真正的Volumetric Fog效果做淡入淡出

[GDC 2022] Recalibrating Our Limits: Lighting on ‘Ratchet and Clank: Rift Apart’

链接:PDFVideo

公司:Insomniac Games

简介:制作蜘蛛侠、瑞奇与叮当系列的公司,比较轻松的演讲。印象最深刻的是大家都是远程工作的,达到这样的品质很不容易:

  • Pre-production:介绍新作打光的pipeline
    • Establish Base Exposure:先确定曝光值,这几乎会影响所有的东西,但曝光合适与否又不容易看出来,尤其在项目前期如果定了一个错误的曝光可能一时半会看不出来,到了后面才逐渐发现很麻烦,总体来说这些影响集中在自发光材质和特效、灯光强度、自动曝光范围
    • Establish Key Lights:确定主光源强度、设置直接光和间接光比值(大约3:1),其他光源的亮度可以据此来调整
    • Place Lights:如何用尽可能少但范围大的光源去有效打光
    • Adjust Lights:修改光照、重新烘焙、检查结果,重复polish直到得到满意的效果
    • Post and Atmosphere:调整后处理和雾效等
  • An Example:给了一个无效打光的场景示例,虽然场景里打了很多光但看上去就是不好看,给出了怎么改善的过程
  • Performance:简单介绍了一些优化策略,包括如何减少角色光照的开销(新开发的角色毛发效果增加了很多开销,解决方法是让fur shells不参与shadow pass,这样一来可以减少开销二来有助于得到假的毛发SSS效果,后续在deferred lighting的时候使用screen space shadow来给毛发增加阴影效果)、优化Raytracing效率、增加LOD等
  • Developing A Look:简单介绍了下怎么从原画到最终效果验收合格的一个场景示例
  • QA环节有人问如何保证SDR和HDR效果一致性,回答是美术都是在HDR显示器下工作的(壕),会经常跑游戏看效果

[GDC 2021] Advanced Graphics Summit: Lifting the Fog: Geometry & Lighting in ‘Demon’s Souls’

链接:PDF, Video

公司:Bluepoint Games

[SIGGRAPH 2021] Real-Time Samurai Cinema: Lighting, Atmosphere, and Tonemapping in Ghost of Tsushima

链接:PDF, Video

公司:Sucker Punch Productions

Global Illumination

全局光照相关。

[SIGGRAPH 2021] Large-Scale Global Illumination at Activision

链接:Video

公司:Activision

[SIGGRAPH 2021] Radiance Caching for Real-Time Global Illumination

链接:Video

公司:Epic Games

[SIGGRAPH 2021] Global Illumination Based on Surfels

链接:Video

公司:Electronic Art

Shadows

阴影相关。

[GDC 2021] Advanced Graphics Summit: Shadows of Cold War: A Scalable Approach to Shadowing

链接:Video

公司:Treyarch

Volumetrics

体渲染相关。

[GDC 2022] The Real-Time Volumetric Superstorms of ‘Horizon Forbidden West’

链接:PDFVideo

公司:Guerrilla Games

简介:作者之前是在Blue Sky动画公司做体积云模拟和渲染的,后来在2013年加入了Guerrilla Games,这次主要分享怎么扩展整个Cloud System去支持暴风雨效果的。前半部分主要回顾之前NUBIS系统是如何渲染体积云的,基本在之前的演讲里都介绍过了。下半部分介绍如何对系统做扩展去支持渲染暴风雨效果,可参考A站上给的效果https://www.artstation.com/delta307,演讲包括:

  • Modeling Density:对暴风雨的云密度进行建模来模拟云的形状,主要包括两种云:
    • Anvil Cloud:砧积云,即顶部的云团
    • Mesocyclone:中气旋,即暴风雨中心的旋状云团,也是龙卷风形成的地方
  • Modeling Light:模拟特有的光照表现,基本是靠各种probability field去trick各种变化效果,包括:
    • Attenuation和Ambient:模拟云的光照衰减和环境光效果
    • Red Glow:避免计算二次raymarching,使用一个球状光源的probability来模拟
  • Lightning Effects:模拟闪电
  • Environmental Effects:与环境表现结合
  • Controlling Chaos:整个世界分为5个可以产生暴风雨的区域,每个区域有自己特有的配置。每分钟会随机从其中挑选一个位置,然后在2分钟左右的时间加载暴风雨,持续一段时间后再在2分钟消散,持续上述循环直到玩家完成修复天气控制系统的任务
  • Scaling PS4 & PS5:给出了PS4和PS5上的一些渲染配置以及PS4的优化,全看天空的时候控制在4ms以内,其他时间控制在2ms左右
  • Extension:正在开发新的功能让玩家可以自由在云里穿梭(参见:https://www.artstation.com/artwork/ZeXyPZ)

Ray Tracing

光线追踪相关。

[GDC 2022] Performant Reflective Beauty: Hybrid Raytracing with Far Cry 6

链接:Video

公司:Ubisoft Toronto, AMD

简介:介绍Far Cry 6是热带天气风格的FPS游戏,经常下雨导致场景有大量反射,使用了SSR和硬件Raytracing结合的方案,过程如下:

  • Generate Rays:根据GGX BRDF得到反射lobe,把生成的rays存储到半分辨率的buffer里
  • SSLR:Screen Space Local Reflections,比Raytracing要快,但有一些瑕疵:
    • 在有限步长下结果不稳定:解决方法是使用Linear Trace With Refine,即不使用HiZ而是使用Linear Trace,最多64个large steps后跟8个refine steps,性能跟HiZ一样但更加稳定),为了防止large step会略过一些edges,使用random ray start offsets
    • SS Tile Classifications:优化策略,光滑表面需要的精度更高而粗糙表面精度不需要很高,所以可以根据roughness计算出来的cone angle包含的像素数选择depth mip
  • Ray Trace & Lighting:远距离用SS,近距离用HW RT,中间的区域靠计算confidence决定混合
  • Particles Trace:如何在HW RT中计算2D billboards,由于billboard始终面向摄像机所以默认下在ray trace的时候可能刚好旋转角度不可见,解决方法是使用3个相互交叉的片去模拟粒子,在trace的时候选择与射线垂直的那个平面计算简化后的光照

[GDC 2022] Real-Time Ray Tracing in ‘Hitman 3’

链接:Video

公司:IO Interactive

[GDC 2022] Bringing 4K Ray Traced Visuals to the World of Hitman 3

链接:Video

公司:IO Interactive, Intel

Terrain & Grass

地形和植被系统相关。

[GDC 2022] Adventures with Deferred Texturing in ‘Horizon Forbidden West’

链接:PDFVideo

公司:Guerrilla

[GDC 2022] Advanced Graphics Summit: Designing the Terrain System of ‘Flight Simulator’: Representing the Earth

链接:PDFVideo

公司:Asobo Studio

简介:法国游戏工作室,之前制作过《瘟疫传说》等游戏,这次介绍微软飞行模拟器是如何渲染地形相关的各种features的,前两个部分暂时不感兴趣先略过:

  • Terrain System Architecture:基于四叉树
  • Flexibility:保证系统足够灵活
  • Scalability:
    • Large Coordinates:CPU使用double 64位,GPU使用Inverted Depth Buffer和Anchor Space(原点是摄像机,竖直Y轴是当前位置法线方向)
    • Trees:要渲染上百万个树,使用了堡垒之夜的3D Imposter的方法
    • Shadows:和大表哥2使用了类似的阴影方案,CSM + Terrain Shadow (Topdown渲染地形到heightmap上,然后raymarch这张heightmap) + Small Shadows (Screen Space Raymarched Shadows)

[GDC 2021] Advanced Graphics Summit: Procedural Grass in ‘Ghost of Tsushima’

链接:PDF

公司:Sucker Punch Productions

[GDC 2021] Samurai Landscapes: Building and Rendering Tsushima Island on PS4

链接:Video

公司:Sucker Punch Productions

[GDC 2021] Boots on the Ground: The Terrain of Call of Duty

链接:PDF

公司:Teryarch

[SIGGRAPH 2021] Experimenting with Concurrent Binary Trees for Large Scale Terrain Rendering

链接:Video

公司:Unity Technologies

Wind Simulation

风场模拟相关。

[GDC 2021] Blowing from the West: Simulating Wind in ‘Ghost of Tsushima’

链接:PDFVideo 公司:Sucker Punch Productions

Particles

粒子相关。

[GDC 2021] Enhancement of Particle Simulation Using Screen Space Techniques in ‘The Last of Us: Part II’

链接:Video

公司:Naughty Dog

Open World

大世界相关。

[GDC 2021] Zen of Streaming: Building and Loading ‘Ghost of Tsushima’

链接:Video

公司:Sucker Punch Productions

GPU Driven

大量依赖GPU算力的都放这里了。

[SIGGRAPH 2021] A Deep Dive into Nanite Virtualized Geometry

链接:Video

公司:Epic Games

Post Process

后处理相关。

[GDC 2022] FidelityFX Super Resolution 2.0

链接:Video

公司:AMD

[SIGGRAPH 2021] Improved Spatial Upscaling through FidelityFX Super Resolution for Real-Time Game Engines

链接:Video

公司:AMD

Hair & Fur

毛发渲染相关。

[GDC 2022] Hair and Fur Rendering in ‘Diablo II: Rescurrected’

链接:Video

公司:Bizzard Entertainment

简介:感觉作者没好好准备,约等于一张演示图都没有,目前看不下去。

Procedural

过程化生成相关。

[GDC 2022] Never The Same Twice: Procedural World Handling in ‘Returnal’

链接:Video

公司:Housemarque

Visual Arts

TA管线或经验相关。

[GDC 2022] Visual Effects Summit: How to (Not) Create Textures for VFX

链接:Video

公司:Wild Sheep Studio

[GDC 2022] Technical Artist Summit: Lighting the Way: Efficient Dynamic Lighting & Shadows on Mobile

链接:Video

公司:NetEase Games

简介:介绍网易动作手游《Dark Bind》的渲染技术,非常水随便看看:

  • Point Light的阴影渲染使用了Dual Paraboloid Shadow Mapping,缺点是阴影会有所变形
  • Bloom使用Dual Filtering替代Gaussian Blur,从1.1ms可以优化到0.5ms

[GDC 2022] Technical Artist Summit: Finding Harmony in Anime Style and Physically Based Rendering

链接:PDFVideo

公司:NetEase Games

简介:介绍网易自研Messiah引擎制作的二次元游戏Mirage的卡通渲染中的关键技术,游戏风格是和PBR结合的NPR渲染,支持local lights等光照结果,基于Hybrid Forward + Deferred Lighting,大部分技术都见过了,有些有点意思的点:

  • Face Lighting:由于脸部的Flat Shading导致多光源叠加的时候能量不守恒脸部亮度过曝,所以对non shadow shadow做了wrap处理,对main light来说会计算statistical coverage precomputation
  • Skin Lighting:根据直接光的luminance去shift hue,提高暗部和阴影交界线的饱和度
  • Shadow:
    • Hair使用Shadow Proxy Mesh去绘制阴影、GI和AO等
    • 脸部为了防止接受到环境杂乱的阴影,用了Partial Shadow(没太理解)
  • Hair Lighting:各向异性高光不想在多光源下过于杂乱,所以没有使用Kajiya-Kay的half vector去计算,而是从view direction和main light direction插值得到的一个方向,把它计算得到的specular mask直接存到GBuffer里给所有local lights使用

[GDC 2022] Technical Artist Summit: Bringing the World to Your Shaders

链接:PDFVideo

公司:Epic Games

[GDC 2022] Building the World of ‘The Ascent’

链接:Video

公司:Neon Giant

简介:介绍了《The Ascent》的视觉效果是如何实现的。The Ascent是用UE4制作的一款RPG游戏,这次分享介绍了小团队(位于瑞典,目前团队11人)是如何非常快速有效地制作这样一款看起来细节很丰富的游戏:

  • Modeling Workflow:为了能够最快速地迭代场景制作,整个游戏只有低模没有使用任何高模,所有资产几乎只用了同一张Texture(包括了trimsheets和用于表现细节的纹理结构),大量使用decals来隐藏纹理的重复性
  • Shader Worldflow:最有亮点的部分,由于全场景只使用一张Texture,需要使用一种快速给物体上色制作细节的方法,无法忍受给每个asset单独制作很多个材质人力成本太高,绘制mask也不行。解决方法核心是靠类似UDIM的思想靠不同UV空间去控制颜色和各种属性,在此基础上叠加各个layer(dirt、emissive layers等)来得到科幻废土风格的效果。好处是纹理内存非常少(因为只有一张纹理),制作流程和规则简单明了强制美术风格的一致,技术非常可控,场景中的顶点复杂度、材质复杂度等都完全在预知范围内,非常有利于做优化
  • Assembing the World:介绍了场景制作时使用的自动化工具,主要是基于Houdini的PCG工具,包括管道、广告牌和破碎效果等,比较常规
  • Adding Life to the World:介绍如何让场景丰富生动,靠AI驱动NPC,以及模拟大量广告牌灯光(在Houdini里处理所有的广告片段把每一帧blur到一个pixel,导出这些颜色序列帧成CSV文件并导入到UE4,实时读取这些颜色去驱动灯光动画,伪造广告牌光照效果)等
  • Optimization:介绍一些灯光优化,主要是靠调整UE4的参数实现,比如调大lightmap分辨率(因为其他纹理内存占用非常少)来减少dc,调整volumetric lightmap密度减少内存,以及调小Reflection Probe的分辨率。游戏里大量使用手绘的Reflection Probe,即反射内容和场景不完全一致而是由美术决定,因为游戏视角比较固定不会像FPS游戏那样所以可行
  • Warm Up:最后反思了一些经验教训,制定规则虽然很烦人但可以强制大家关注big picture,同时由于本作太关注制作速度和数量,有些忽略了质量,希望在下一作做得更好

[GDC 2022] Creating the Many Faces of ‘Horizon Forbidden West’

链接:Video

公司:Guerilla Games

简介:介绍地平线2是如何在三年时间内制作了168个高质量Faces的:

  • Creating Our Heros:扫描和制作Hero的人脸模型,这里是挑选特定长相的模特,后面会再基于扫描数据做一些风格化处理
  • Populating the Trbes:这一步目的是既要保证角色的多样性,又要保证质量可以和Heroes相当。扫描各种真实的人脸模型,组成GenePool数据库,这里只考虑数据多样性(形状特征明显,各种不同年龄、种族),而不会想着某种特定的样子。GenePool包含60个模型数据,具有完整的Rigging数据,而美术在ZBrush制作游戏真正的角色模型缺少Rigging,此时就会使用内部工具GeneSplicer,它会识别美术制作的模型,识别出它的各个部分与数据库里匹配的数据,在GenePool的大量数据样本之间做Blend,生成具有完整Rigging信息的人脸模型
  • Look Development:在游戏引擎内部制作Lookdev环境,介绍了脸部绒毛(一开始靠插片但overdraw太高了所以改为插很多更高精度的polygons)、肤色(Albedo本身是灰色调的图,靠另一张查找表给皮肤上色)等角色制作方法
  • Living in the World:靠decals、face paint等方法进一步丰富脸部表现

[GDC 2021] The Art of Not Reinventing the Wheel in ‘Wild Rift’ Asset Pipeline

链接:PDF

公司:Riot Games

杂七杂八

不知道分类到哪里。

[GDC 2022] Shifts and Rifts: Dimensional Tech in ‘Ratchet and Clank: Rift Apart’

链接:Video

公司:Insomniac Games

简介:介绍如何在瑞奇与叮当中实现高效的传送门效果:

  • Portal Rendering:介绍了Portal的渲染实现(Render to Texture)以及遇到的各种问题和解决方法,包括:
    • Lighting:解决两个世界光强差异过大的问题,做一次反向Tonemapping或者直接关闭Tonemapping,如果光强仍然不一样会再使用一个自定义光强去调整Portal
    • Motion Blur:解决Portal显示的诸如Motion Blur和Depth of Field等后处理错误问题,方法是把Portal的Depth和Velocity Buffer等拷贝到Main Buffer里
    • Camera Popping:解决角色越过Portal时摄像机突变,方法是在玩家跨过Portal时不要立刻切换摄像机,而是继续追踪一个虚拟的玩家目标,直到整个摄像机也一起跨过Portal后再重设成真正的玩家目标
    • Portal Threshold:解决角色在跨过边界的时候会被错误裁剪的问题,方法是在Portal两侧复制两个相同的物体,两边的角色模型分别在Shader里使用了Clipping来避免绘制到Portal模型的另一侧
    • Inputs:解决进入Portal后角色移动与玩家输入不匹配的问题,原因是输入移动方向是基于入口处的观察空间的,尽管模型移动轨迹在此空间下的确是正确的,但因为玩家是从Portal处观察移动方向的所以看起来是错误的。方法就是对输入方向做一次转换,当模型跨过Portal后,使用输入方向和Portal的相对方向去控制玩家移动方向
    • Optimizations:优化Portal的渲染效率,包括使用Portal Model作为遮挡等
  • Minimizing Load Times:介绍如何解决传送门在两个世界传送时快速Loading,最后成功让Loading时间在PS5上控制在1秒以内

[GDC 2022] ‘Dead by Daylight’: Intergrating Shaders into the User Interface Pipeline

链接:Video

公司:Epic Games

[GDC 2021] Driving Innovation: A New Vehicle Pipeline for ‘The Last of Us: Part II’

链接:PDF

公司:Naughty Dog

[GDC 2021] Rope Simulation in ‘Uncharted 4’ and ‘The Last of Us: Part 2’

链接:Video

公司:Naughty Dog

「闲谈」UE5真好玩

前言

这两天生病休息,趁着有时间想要找点有意思的事情做。之前偶然看到Blender的Art Gallery,里面有很多艺术家开源的各种风格的Blender原工程文件,想着能不能在引擎里复现下看看能复现到什么程度,从里面我挑了自己比较喜欢的Blender 2.81的Splash Screen项目。

一开始本来想在Unity HDRP里做做看,无奈搞了一会实在是劝退,跟UE相比Unity缺少很多查看Lighting的快捷方式,遂弃,还是乖乖滚回UE的怀抱。

结果

那就废话不多说直接UE5搞起。一顿操作下来初见成效,不得不说UE5真强啊啧啧啧。我们先来看下分别用Blender的Cycles和UE5实时渲染的对比:

Blender Cycles渲染结果

Snipaste_2022-01-05_23-22-03
Snipaste_2022-01-05_23-22-03

UE5实时渲染结果

HighresScreenshot00007
HighresScreenshot00007

(没头发的萌妹子秒变打工人,头发真的很重要)尽管UE5缺少某些Blender的渲染features,比如光源的软阴影、面光源计算与离线有差异等,但也十分接近了。

宇宙的尽头果然是UE5。

「Graphics Study」RDR2渲染分析 — 阴影篇

前言

一直感叹RDR2的阴影渲染质量很高,数毛社有篇分析视频演示了RDR2的阴影表现,没玩过的小伙伴一定不要错过。RDR2的阴影有非常出色的Contact Shadow,即距离物体更近的阴影更加锐利,反之越远越模糊。这个模糊半径甚至和当前的天气状况有关。因此,这一篇我们就主要分析来RDR2是如何绘制平行光阴影的。

不同时间段阴影 不同时间段阴影
contact-shadow0
contact-shadow0
contact-shadow1
contact-shadow1

先回忆上一篇的内容,我们给出了GBuffer的Layout,其中跟阴影绘制相关的主要是GBufferC的A通道,它包含了逐材质计算的一些平行光阴影信息(例如在计算Parallax Mapping时计算的自阴影信息),这也是平行光阴影的起点。RDR2后续会继续计算屏幕空间阴影、CSM阴影等,将它们结合起来作为最终的平行光阴影更新到GBufferC的A通道:

GBufferC.a Before GBufferC.a After
csm
csm
shadow-final
shadow-final

可以看到,RDR2最后得到的平行光阴影非常柔和,它的半影范围很大,甚至可以媲美Ray Traced Shadow。RDR2为了得到这样的效果也做了很多事情。总体来说,RDR2绘制平行光阴影包括几个计算部分:

  • 处理Scene Stencil,标记出边界像素部分,以便在后面的Shadow Pass里对边界像素计算抗锯齿后的阴影(可选)
  • 绘制场景的Cascade Shadow Map(CSM)
  • 处理上一步的CSM,为CSM每一级计算一定半径范围的最小/最大深度值,以便后面计算软阴影
  • 计算平行光阴影
    • 绘制远距离阴影
    • 绘制近距离阴影

下面我们就来具体分析上述与阴影相关的各个Pass。

处理Scene Stencil

一开始在场景的GBuffer绘制完成后,初始Stencil Buffer大致如下:

stencil-input
stencil-input

在开始渲染CSM之前,RDR2会处理上述的Scene Stencil Buffer。总体来说,这些处理的目的是对GBuffer的各个属性进行边缘检测,将有差异的边缘部分在Stencil Buffer中标记出来(对应Stencil的第6个bit),之后会靠这些标记为屏幕边界像素的计算抗锯齿后的阴影。这个标记处理可以分为两个Screen Pass。

Screen Pass 0:标记Stencil的差异部分

第一个Screen Pass的输入就是Stencil Buffer本身。由于RDR2开启了8x MSAA来渲染GBuffer,因此在这个Pixel Shader里可以为每个Pixel手动采样8x MSAA的8个samples,分析它们的Stencil值的差异,据此来计算边缘检测。这个Pass的伪代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pixel_shader (every pixel with screen position (x, y))
{
    int2 Index = int2(x, y);
    int2 Offset = int2(0, 0);
    
    int StencilOr = 0;
    int StencilAnd = 0xFF;
    for (int i = 0; i  < 7; i++)
    {
        int Stencil = StencilTexture.Load(Index, Offset, i);
        StencilOr |= Stencil;
        StencilAnd &= Stencil;
    }

    if (StencilOr & 0x20)
        return 1;
    else if ((StencilOr ^ StencilAnd) & (-33))
        return 1;
    else
        return 0;
}

其实本质来说,上面Pass的结果就是判断8个MSAA samples的Stencil值是否完全一样,如果完全一致就输出黑色,否则就标记它为一个特殊像素。如果其中任意一个sample的Stencil值已经被标记成了0x20这个Bit(即已经被标记为特殊的边界像素了),就直接保留它。

这个Pass的计算结果会输出到一张格式为R8_UNORM的权重图中(为显示明显对下图进行了提亮):

stencil-mask
stencil-mask

注意到上图中大部分标红区域相当于Stencil的边缘检测结果,而大片的红色区域(似乎是某些特定的墙壁和灌木部分)就对应了Stencil & 0x20的部分。

Screen Pass 1:标记GBuffer的差异部分

第二个Screen Pass会对Stencil Buffer进行真正的标记。整个Pass会利用Stencil Test忽略那些Stencil已经被标记为0x20的部分,而只修改其余部分像素的Stencil。下图显示了这个Pass的Stencil Test结果(红色为Stencil & 0x20部分,绿色为!(Stencil & 0x20)部分):

stencil-0x20
stencil-0x20

上述绿色的屏幕像素部分的Stencil值,会在这个Pass中被继续修改。简单来说这个Pass的目的是进一步检测MSAA各个samples之间的GBuffer数据是否相同,如果不同则标记成一个边界像素。再结合上一个Pass标记出来的Stencil边界像素部分,把这些所有的边界像素部分统一标记为0x20。伪代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pixel_shader (every pixel with screen position (x, y))
{
    int2 Index = int2(x, y);
    int2 Offset = int2(0, 0);

	bool bStencilIsSame = DecodeStencilMask(StencilMask.Load(Index, Offset)) == 0;

	bool bGBufferIsDiff = false;
	int2 CompPairs[4] = { int2(7, 5), int2(5, 6), int2(6, 0) };	
	for (int i = 0; i < 4; i++)
	{
		int2 SamplePair = CompPairs[i];
		FGBufferData SampleData0 = DecodeGBufferData(Index, Offset, SamplePair.x);
		FGBufferData SampleData1 = DecodeGBufferData(Index, Offset, SamplePair.y);
		bGBufferIsDiff |= CheckGBufferDataIsDiff(SampleData0, SampleData1);
	}

	if (!bGBufferIsDiff && bStencilIsSame) discard;
}

上面的代码在比对GBuffer数据时共采样了3对samples,这3对samples的位置关系可以参考Microsoft的文档

sample-pattern
sample-pattern

判定使用的GBuffer数据包括Depth、GBufferB(Normal)、GBuferC的yz通道(猜测这两个通道编码了计算Specular使用的材质信息)。如果各个samples之间的Stencil值和GBuffer值被判定为相同(实际代码里会检测一定的误差判定范围),这个pixel就会被discard。只有那些有差异的像素会得以保留来更新Stencil的值。


经过两个Pass的处理后,标记前后的Stencil Buffer对比如下:

Stencil Before Stencil After
stencil-input
stencil-input
stencil-output
stencil-output

可以发现,现在所有的边界像素都在Stencil Buffer中被标记了出来。

CSM Shadow Depth

平行光的Shadowmap使用了常见的CSM策略。RDR2共使用了四级CSM,每一级分辨率为2048x2048,总共分辨率为2048x8192:

csm
csm

Wires & Particles

RDR2对于电线这种很细以及诸如烟等透明粒子效果的物体的阴影是单独另开Pass进行绘制的。除了绘制到上面的CSM中,还额外分配了一张格式为A8_UNORM、大小同样为2048x8192的纹理作为Color RT,这张RT记录了这些特殊物体的Mask信息,后面全屏计算CSM阴影的时候会用到它:

wires
wires

先来看电线的绘制。电线使用了D3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST作为Primitive Topology,再配合Tessellation Shader绘制出电线的形状:

wires
wires

绘制的电线的Pixel Shader很简单,它会采样一张32x32分辨率(包含4级mips)、格式为BC1_UNORM的纹理,把得到的颜色值输出到那张Mask RT上(混合模式为Alpha Blend),并把深度值渲染到CSM的Shadowmap中(开启了Depth Test):

wires
wires

除了电线,这个Pass还会处理半透明粒子的阴影。绘制粒子的Pixel Shader会采样粒子动画的序列帧Atlas,同样会把读取到的粒子透明度值输出到Mask RT上:

wires
wires

跟电线处理不太一样的是,粒子的PS还会采样4次CSM Shadowmap的值,并据此来修改输出到Shadowmap里的深度值。具体原理需要配合Vertex Shader再来分析下。

CSM Min/Max Depth

这部分计算的主要目的是为CSM的每一级Shadowmap分别计算不同半径范围内的最小/最大深度值,将结果保存到另一张RT里,以便在后续的Pass里计算软阴影。这部分计算可以再细分为以下两个部分。

1/4 CSM Shadow Depth

绘制完整个场景的CSM后,RDR2会根据它再生成两张四分之一分辨率(512x2048)、格式均为R16G16的RTs:

shadow_init
shadow_init

这两张RT分别包含了:

  • RT0:似乎是计算了四分之一分辨率下的VSM
  • RT1:为四分之一分辨率下的每个输出像素,计算其对应在全分辨率CSM下、每个4x4块中的最小深度值和最大深度值,分别存储到RG通道中

由于上面的RT0和场景的平行光阴影没有直接关系,这里我们就不再讨论。这个RT0的作用主要是作为一组Compute Shader的输入来计算得到一张3D Texture,似乎是给后续计算God Ray等效果使用的,之后有机会再讨论吧。

计算RT1部分的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
compute_shader (every pixel in RT1 with position (x, y))
{
	int2 OutputIndex = int2(x, y);
    float2 BufferUV = (OutputIndex + 0.5) / TextureSize;

	float MinDepth = FLT_MAX;
	float MaxDepth = FLT_MIN;
	for (int i = {-1, 1})
	{
		for (int j = {-1, 1})
		{
			float4 ShadowDepths = DepthTexture.Gather(BufferUV, int2(i, j));
			MinDepth = min(MinDepth, min(min(ShadowDepths.y, ShadowDepths.w), min(ShadowDepths.x, ShadowDepths.z)));
			MaxDepth = max(MaxDepth, max(max(ShadowDepths.y, ShadowDepths.w), max(ShadowDepths.x, ShadowDepths.z)));
		}
	}

	Output[OutputIndex] = float2(MinDepth, MaxDepth);
}

通过4次Gather计算,RT1的每个像素可以计算在全分辨率CSM下该点周围半径2个像素大小范围(共16个有效像素)内的最小深度值和最大深度值。

Min/Max Depth

RDR2使用了更多的Pass去计算更大半径范围的最小和最大深度值。这个部分包含了4个Compute Pass,每个Pass负责处理初始化Pass中输出的RT1(即四分之一分辨率下的最小/最大深度值)中的某一级Cascade,为其计算一定半径内阴影深度的最大和最小值,并将结果存储到另一张512x2048的RT里。这部分伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
compute_shader (every pixel in Cascade0/1/2/3 in RT1 with position (x, y))
{
	int2 OutputIndex = int2(x, y);

	float MinDepth = FLT_MAX;
	float MaxDepth = FLT_MIN;
	for (int i = -SearchRadius; i <= SearchRadius; i++)
	{
		int SubSearchRadius = floor(sqrt(SearchRadius * SearchRadius - i * i));
		for (int j = -SubSearchRadius; j <= SubSearchRadius; j++)
		{
			float2 MinMaxDepth = RT1.Load(int2(j, i)).xy;

			MinDepth = min(MinDepth, MinMaxDepth.x);
			MaxDepth = max(MaxDepth, MinMaxDepth.y);
		}
	}

	Output[OutputIndex] = float2(MinDepth, MaxDepth);
}

对于每一级Cascade来说,上面的SearchRadius是不同的:

  • Cascade 0:SearchRadius = 8(对应全分辨率CSM下的半径32个像素),采样了197次纹理
  • Cascade 1:SearchRadius = 4(对应全分辨率CSM下的半径16个像素),采样了49次纹理
  • Cascade 2:SearchRadius = 3(对应全分辨率CSM下的半径12个像素),采样了29次纹理
  • Cascade 3:SearchRadius = 0(对应全分辨率CSM下的半径2个像素),采样了1次纹理

可见,RDR2对Cascade 0计算的半径是非常可怕的(性能也很可怕),也难怪可以做出半影范围那么大的软阴影了。

经过这4个CS计算后,最终得到每一级Cascade一定半径范围内的最小深度值和最大深度值:

shadow-dilation-erosion
shadow-dilation-erosion

Apply Shadows

接下来就是把平行光阴影绘制到屏幕上并存储到GBufferC的A通道里。RDR2的平行光阴影共包括两个部分:

  • 不使用CSM的远距离阴影(Far Shadows Pass)
  • 使用CSM的近距离阴影(Near Shadows Pass)

这两种类型的阴影会通过设置不同的Depth Bounds来处理不同距离的阴影。其中,远距离阴影范围大约覆盖距离摄像机深度值>200米的区域,近距离阴影覆盖距离摄像机深度值<200米的区域。每种类型的阴影绘制会再细分到2个Screen Pass中(共2x2=4个Screen Pass),这2个Screen Pass会基于之前处理得到的Stencil Buffer、使用Stencil Test来处理屏幕空间的不同像素部分,第一个Screen Pass处理绝大部分常规像素,第二个Screen Pass处理之前被特殊标记的那些Stencil值或GBuffer值有差异的边界像素部分。这两个Screen Pass的Stencil Test通过结果如下所示:

Screen Pass 0 Screen Pass 1
screen-pass0-stencil-test
screen-pass0-stencil-test
screen-pass1-stencil-test
screen-pass1-stencil-test

这两个Screen Pass其实代码基本完全相同,只是Screen Pass 0在采样GBuffer(包括Depth&Stencil Buffer)时直接采样SampleIndex=0的位置,而Screen Pass 1的Pixel Shader会额外传入MSAA的Sample Index,使用这个Index再去采样GBuffer(包括Depth&Stencil Buffer)进行相关计算。原因在于,我们之前提到过Shadow Pass的Color RT其实是GBufferC,而RDR2中的GBuffer都会开启MSAA,因此Screen Pass 1可以利用这一特性在边界像素处手动计算各个MSAA Sample位置处的阴影结果,相当于在边界处手动计算了阴影的SSAA抗锯齿。但代价是原本只需要执行一次的Pixel Shader在Screen Pass 1里要执行8次,这也是为什么一开始RDR2要把这些边界像素单独标记出来。

这里利用了DirectX的SV_SampleIndex语义,具体可参见Microsoft的文档。MJP也写过相关文章讲解过可编程的MSAA特性,推荐阅读。

由于两个Screen Pass的代码几乎完全一样,区别只在于是否需要单独采样MSAA的Sample Index处理抗锯齿,因此我们下面只解释每种类型阴影计算的具体原理,不再赘述这两个Screen Pass的区别了。

实际上,每种阴影类型是否需要再细分到两个Screen Pass似乎是由摄像机位置和渲染质量决定的,在低配或者离地角度比较高的时候,RDR2就不会再拆分这两个Screen Pass,而是直接使用一个Screen Pass绘制所有远/近距离像素了,不再靠Stencil去单独处理边界像素的阴影了,这样一共只需要两个Pass去绘制全屏幕的阴影。

每种距离的阴影计算来源不同的,我们先来看远距离阴影的计算部分。

Far Shadows Pass

我们之前选取的这一帧截图由于远处大部分区域被房屋遮挡住了,看不太出来远距离阴影的变化,因此这里我们临时换成另一帧远距离阴影计算前后变换更明显的图像进行说明:

Far Shadows Before Far Shadows After
far-shadow-before
far-shadow-before
far-shadow-after
far-shadow-after

可以看到,远距离阴影主要有以下几个计算来源:

  • Cloud Shadows
  • Raytraced Screen Space Shadows
  • Baked Shadows
  • Raytraced Terrain Shadow

这些阴影的计算都不依赖CSM,而是使用其他的数据计算实现。

Cloud Shadows

云的阴影计算比较容易理解,主要还是依赖Shadowmap。RDR2为体积云渲染了另一张Shadowmap:

cloud-shadowmap
cloud-shadowmap

这张Shadowmap的绘制也是本帧通过CS完成的,之后有时间我再补充到这里。

通过在Pixel Shader里把当前的像素坐标转换到体积云的Shadowmap空间,再比较当前像素深度和Shadowmap中的已有深度,就可以计算得到云的阴影值。这部分伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pixel_shader (every pixel with screen position (x, y))
{
    int2 Index = int2(x, y);
    float Depth = DepthTexture.Load(Index);
    float3 ViewSpacePos = ConvertToViewSpacePosition(Depth);

    float Shadow = 1.0;

    // Compute cloud shadow
    if (IsWithinCloudShadowSpace(ViewSpacePos))
    {
        float2 CloudSpaceUV = ConvertToCloudSpace(ViewSpacePos);
        float CloudSpaceDepth = CloudShadowDepth.Sample(CloudSpaceUV);
        
        float CloudShadow = ComputeCloudShadow(CloudSpaceDepth, ViewSpacePos);
        float CloudShadowWeight = ComputeToCloudSpaceBorderWeight(ViewSpacePos);
        
        Shadow *= lerp(1.0, CloudShadow, CloudShadowWeight);
    }

    ...
}

体积云的阴影覆盖范围似乎是有限的,所以RDR2考虑了当前像素点距离覆盖边界的权重,当超过体积云阴影覆盖范围时就会退化到阴影值1。

Raytraced Screen Space Shadows

RDR2会在屏幕空间沿着光源方向计算一定数目的shadow trace(在截帧数据中NumTrace = 12),比较每个trace point的深度值和Scene Depth中的深度值计算屏幕空间的阴影。这部分伪代码如下:

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
pixel_shader (every pixel with screen position (x, y))
{
    int2 Index = int2(x, y);
    float Depth = DepthTexture.Load(Index);
    float3 ViewSpacePos = ConvertToViewSpacePosition(Depth);

    float Shadow = 1.0;
    
    ....

    // Compute screen space shadow
    int Stencil = StencilTexture.Load(Index);
    float3 Normal = DecodeWorldNormal(GBufferB.Load(Index));
    int NumTrace = GetScreenSpaceTraceCount(Depth, Stencil);

    float3 ScreenTraceStartPos = GetPixelScreenSpacePosition(Depth);
    float3 ScreenTraceEndPos = GetTraceStartPosition(Depth, Normal, LightDir);
    float3 ScreenTraceStep = (ScreenTraceEndPos - ScreenTraceStartPos) / NumTrace;

    float ScreenSpaceShadow = 1.0;
    float3 ScreenTracePos = ScreenTraceStartPos + Random * ScreenTraceStep;
    for (int i = 0; i < NumTrace; i++)
    {
        int2 TracePosIndex = floor(ScreenTracePos.xy * BufferSize);
        float TracePosDepth = DepthTexture.Load(TracePosIndex);
        ScreenSpaceShadow *= ComputeScreenSpaceShadow(TracePosDepth, ScreenTracePos.z);
        ScreenTracePos += ScreenTraceStep;
    }

    ApplyScreenSpaceShadowWeight(ScreenSpaceShadow, LightDir, Normal, Depth);
    Shadow *= ScreenSpaceShadow;

    ...
}

其实计算屏幕空间阴影的时候还是有很多细节处理的,比如RDR2考虑了Stencil值和是否是背光面来影响trace的距离、步数以及最终的阴影权重,这部分计算因为个人能力有限理解还不到位就不写出来误导人了。

Baked Shadows

这部分计算很有意思,妙啊妙啊。RDR2应该是提前烘焙了8个方向的平行光入射角度下整个地图(覆盖大约12.5km x 12.5km)中某些大型遮挡物的阴影投影结果,把它们存储到两张分辨率为512x512、格式为R16G16B16A16的纹理中,一共有8个方向的阴影信息,绑定到Pixel Shader的Input Texture 6&7上:

InputTexture6.r InputTexture6.g InputTexture6.b InputTexture6.a
far-input6-r
far-input6-r
far-input6-g
far-input6-g
far-input6-b
far-input6-b
far-input6-a
far-input6-a
InputTexture7.r InputTexture7.g InputTexture7.b InputTexture7.a
far-input7-r
far-input7-r
far-input7-g
far-input7-g
far-input7-b
far-input7-b
far-input7-a
far-input7-a

Pixel Shader里会根据当前的光源方向计算8个方向的权重对它们的采样结果进行混合,再计算得到的真正的阴影值。

但细想一下会发现,这8个方向只能表示XY平面(Z为垂直方向)上的阴影变化,但光照的仰角变化要怎么办呢?这就是妙的地方,实际上这8个方向存的并不是绝对阴影值,而是一个仰角弧度值。我猜测这8个方向的烘焙过程是这样的(纯属猜测概不负责欢迎讨论):给定光源在XY平面的入射方向(8张图对应了8个固定方向),逐渐改变光源的仰角,使其从最小角度逐渐变化到最大仰角角度,检查地图上每个位置此时是否处于阴影中,如果在多个角度下都处于阴影中,就记录下这些角度的最大值,最后把这个角度存储到贴图中。也就是说,这8个方向阴影图中存储的值实际上是角度(以弧度为单位)。在Pixel Shader里得到加权混合后的烘焙阴影角度后,再次根据当前光源的仰角与烘焙角度进行比较,只有当烘焙角度≥光源仰角时,才意味着该位置此刻处于阴影中。这部分计算的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pixel_shader (every pixel with screen position (x, y))
{
    int2 Index = int2(x, y);
    float Depth = DepthTexture.Load(Index);
    float3 ViewSpacePos = ConvertToViewSpacePosition(Depth);

    float Shadow = 1.0;

    ...

    // Compute baked shadow
    float3 SamplePosWithinMap = ComputePositonWithinGameMap(Depth);
    float4 BakedShadowAngles0 = InputTexture6.Sample(SamplePosWithinMap.xy);
    float4 BakedShadowAngles1 = InputTexture7.Sample(SamplePosWithinMap.xy);
    float BakedShadowAngle = max(dot(BakedShadowAngles0, BlendWeights0), dot(BakedShadowAngles1, BlendWeights1);

    float BakedShadow = saturate(abs(LightPitchAngle - BakedShadowAngle) / BlendAngle);
    BakedShadow = saturate((BakedShadowAngle > LightPitchAngle) ? (smoothstep(1.0, 0.0, BakedShadow) * 0.5) : (smoothstep(0.0, 1.0, BakedShadow) + 0.5));
    Shadow *= BakedShadow;
    
    ...
}

可以发现上面伪代码的最后并不是直接取二值对比结果,RDR2会传入一个过渡角度来做阴影的渐变:

baked-shadow-blend
baked-shadow-blend

感兴趣的话可以去看下在desmos上的一个实时演示,改改参数调调看就可以理解了。

这种方法当然只是一种近似,它的可行性建立在一个重要的假设上:当固定光源XY平面角度且仰角角度从小到大变化时,地图上每个观察点的阴影变化是单调的。这一假设在充分空旷环境下绝大部分时候是成立的,但对于有复杂遮挡物的环境来说,它明显有很多无法成立的情况,所以我猜测RDR2可能烘焙的是一些比较大结构的遮挡物的阴影投影状况。

Raytraced Terrain Shadow

这部分是我猜测绘制的是地形阴影,因为这部分计算主要依靠采样Pixel Shader的Input Texture 8(左图),它是一张分辨率为1024x1024、格式为R16_UNORM的纹理,看起来像是RDR2整个地图环境地形的归一化后的高度图:

heightmap
heightmap

刚好对应了游戏地图(来源Reddit)中的山区部分:

rdr2-map
rdr2-map

这部分计算比较好理解,就是沿着光源方向、按照固定步长去trace一定数目的高度图(在截帧中NumTrace = 8),比较每次trace point的高度值和Heightmap中记录的高度值,据此计算阴影。这部分计算伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pixel_shader (every pixel with screen position (x, y))
{
    int2 Index = int2(x, y);
    float Depth = DepthTexture.Load(Index);
    float3 ViewSpacePos = ConvertToViewSpacePosition(Depth);

    float Shadow = 1.0;

    ...

    // Compute shadow from height map
    for (int i = 0; i < NumTrace; i++)
    {
        float3 SamplePos = SamplePosWithinMap + HeightMapTraceStep * i;
        float SampleHeight = HeightMap.Sample(SamplePos.xy);
        float HeightDiff = SampleHeight - SamplePos.z;
        Shadow *= 1.0 - saturate((HeightDiff + i * HeightBias) * MaxHeight / BlendHeight)
    }

    return Shadow;
}

从截帧来看,用来归一化Heightmap使用的MaxHeight大约为700~800米,BlendHeight大约为10米~20米。

Near Shadows Pass

近距离阴影计算前后的对比如下:

Near Shadows Before Near Shadows After
near-shadow-before
near-shadow-before
near-shadow-after
near-shadow-after

近距离阴影同样依次有几个计算来源:

  • Cloud Shadows
  • CSM Shadows
  • Raytraced Screen Space Shadows
  • Baked Shadows
  • Raytraced Terrain Shadow

其中,其中四种来源的计算和Far Shadows几乎完全一样,在此不再赘述。

CSM Shadows

回忆在之前的各个Pass里,RDR2一共根据CSM Shadowmap生成了以下几张RT。接下来会使用CSM Shadowmap和这两张RT来计算平行光Cascade阴影:

  • CSM Min/Max Depth RT:格式为R16G16_FLOAT,分辨率为CSM Shadowmap的四分之一,其中R和G通道分别记录了32p(以第一级Cascade为单位,后面逐级递减)半径范围内CSM Shadow Depth的最小值和最大值
  • Wires & Particles Mask RT:格式为A8_UNORM,分辨率与CSM Shadowmap一致,其中A通道记录了电线和粒子等特殊物体的阴影混合度值

之前提到RDR2的软阴影范围很大,这主要是因为它采样Shadowmap的半径很大。传统计算软阴影的方法,例如PCSS,需要先靠一个blocker search步骤来预估blocker到receiver之间的距离,并据此来得到filter size,最后再使用该值去计算PCF。可以看到,这种方法的采样个数取决于blocker search num和pcf filter num两者的和,开销比较大。RDR2高明的一点是,只使用一次采样循环即可完成整个软阴影的计算。它的核心思想是:

  • 在Shadowmap中以当前位置点为中心、半径为48(以第一级Cascade为单位,后面逐级等量换算)的圆盘,将其划分成32个圆环
  • 每个圆环采样一次Shadowmap(按照Vogel Disk分布),记录采样得到的阴影值Shadow[32](值为0或1,靠一个32位的bitmask即可记录),以及这32个采样点的平均深度值AverageShadowDepth
  • 遍历32个采样点的Shadow[32],如果该采样点位于阴影中,就叠加其对应的圆环面积,累计后得到所有阴影点覆盖的面积
  • 根据当前平行光的方向和Light Source Angle,将AverageShadowDepth换算成对应的软阴影采样半径(类似PCSS)
  • 将上一步的软阴影采样半径平方得到面积单位,与所有阴影点覆盖的面积做比值,该值即为软阴影值

为了避免为每一个像素都进行上述完整的32次采样,RDR2依次使用以下优化来跳过上述完整的软阴影计算:

  • 采样半径为1范围内的4次Shadowmap,得到ApproxShadow
  • 采样CSM Min/Max Depth RT,判断MinDepth + MinBias < Depth ≤ MaxDepth + MaxBias,是则继续向下判断,否则返回ApproxShadow
  • 判断NdotL ≤ 0 && ApproxShadow < 0.0001,是则返回0,否则进行完整的软阴影计算

注意以上优化并不能精确定位半影区域的像素,只能跳过那些百分之百位于全影或自阴影中的像素,而那些完全位于非阴影区域的像素仍然需要进行完整的软阴影计算,造成性能浪费,更通用的方法可以考虑提前生成一张低分辨率的Penumbra Mask,只标记出半影区域的像素。

最后,采样Wires & Particles Mask RT来修改最终的Cascade Shadow:

1
CascadeShadow *= 1.0 - MaskRT.Sample(CascadeUV).a;

后记

这里只分析了平行光的阴影(注意上述分辨率和采样数等数据都是基于最佳画质下的截帧数据,中配和低配数据会有所不同),实际还有Local Lights的阴影没有分析,这篇博客提到了部分Local Lights的阴影技术,感兴趣的可以看看。

终于填完了一个坑!下一篇我们再见!

返回总篇

「Graphics Study」RDR2渲染分析 — 总篇

前言

从大表哥2发售以来,就一直想学习下里面的渲染技术。从体积云渲染到天气系统,从多光源的软阴影到车马的压痕轨迹,还有出色的抗锯齿,虽然别的游戏也有类似的功能,但它的实现落地就是做得更胜一筹,画面非常细腻。从截帧结果也能看出,R星花了很多功夫在那些“细枝末节”的效果处理上,这些部分在其他游戏可能就得过且过了,但也也是以性能为代价的,有些处理甚至也非常粗暴。

R星几乎很少在公共领域宣讲他们的渲染技术,印象中唯一一次就是SIGGRAPH 2019上他们分享了体积云和大气的渲染技术,大概强者就是这样孤傲?去年外网就有一篇博客分析过大表哥2的截帧分析,但博主写得有些简略,算是很好的科普级别的文章。

我这里是拿游戏性能测试时使用的渲染片段截帧分析的。不得不说大表哥2渲染一帧真的是干了太多事了,从RenderDoc看一帧的Event就有3w+。由于能力有限,我只能选几部分自己最感兴趣的尝试分析下。


我们要分析的这一帧渲染结果如下:

frame
frame

GBuffer Layout

常规的渲染Pass中,总共输出6张1920x1080大小的RT,并且都开了8x MSAA:

GBuffer 格式 RGB通道 A通道
GBufferA R8G8B8A8_SRGB
RGB:Base Color
A:Unknown
gbuffera_rgb
gbuffera_rgb
gbuffera_a
gbuffera_a
GBufferB R8G8B8A8_UNORM
RGB:Normal
A:特殊Shading Model的Custom Data
gbufferb_rgb
gbufferb_rgb
gbufferb_a
gbufferb_a
GBufferC R8G8B8A8_UNORM
RGB:Unknown
A:逐材质的Shadow信息
gbufferc_rgb
gbufferc_rgb
gbufferc_a
gbufferc_a
GBufferD R8G8B8A8_UNORM
RGB:Unknown
A:Unknown
gbufferd_rgb
gbufferd_rgb
gbufferd_a
gbufferd_a
GBufferE R16G16_FLOAT
gbuffere_rg
gbuffere_rg
 
Depth D32S8
depth
depth
stencil
stencil

GBuffer先摆到这里,有些用处还不太清楚,可以先参考文章最开始提到的那片博客,之后分析更多了会更新下上面的表格。

后续计划

ok,这篇文章只是开个头,因为RDR2中很多feature都干了很多事情,所以会把它们拆开到不同的文章中,内容暂定为:

那下一篇再见!

「博客更新」I'm Back!

时隔四年,我又回来了!知乎不争气啊,哎。

不知不觉马上要进入工作的第五年了,我要反思我变懒了,好习惯还是不能丢啊。这年头在国内找个能分享技术博客的地方越来越难了,太丑的看不上,广告太多的不想去,太小的容易倒闭不敢去,真是矫情。想了想还是靠自己丰衣足食!花了点时间更新了博客主题和内容,看看这次能坚持多久,嘿嘿。

不在这里的四年,发生了很多,刚经历了公司的十周年纪念日,很庆幸自己毕业选择了米哈游,感谢一路上遇到的贵人,自己好像一直是个很幸运的人,但除了年会抽奖的时候T T。想想2017年刚加入公司时只有两百多人,现在已经是个有三四千的大公司了,真是不可思议,现在还能记着当年在民润大楼的一间会议室里面试的自己,“你们公司有女程序员吗”,“有啊”,“真的啊,那她是做什么工作的”,“渲染开发”,“这么巧,那是谁啊”,“就是你啊”,啊哈哈哈哈太中二了,真是年轻啊。

“站在巨人的肩膀上,如果一个不够就站十个”,希望能更勇敢,更有担当,在这里的职业生涯能做出更棒的作品。就这样!

anniversary
anniversary

「Game Tricks」Smoke材质的二三事

前言

这篇文章是我第一次使用Prose来写博客,尝试一下。以前我都是在Cmd Markdown里写,然后编辑一下再发布的,感觉过程略蠢……


这几天看了些关于粒子特效材质的文章,主要是怎么得到比较流畅的动画效果,比如爆炸之类的。感觉有一些想法很有意思,这篇主要参考了Simon的Fallout 4 – The Mushroom Case一文。

帧动画

效果基本上是基于帧动画作为基础。Simon解释了Fallout 4里面在帧动画基础上所做的一些改进。与传统的把全部信息(颜色和透明度)存储在一张帧动画图片中不同,Fallout 4里面分离出来了颜色信息和一定程度上的透明度信息,然后在渲染的时候通过两张ramp texture来得到真正的值,以此来获得更高的自由度。

  • 颜色:帧动画图片中只存储光照的灰度信息,而真正的颜色可以靠一张ramp texture去决定。绝妙之处在于,这张ramp texture是二维的,横轴表示了光照强度的变化,而纵轴则是粒子的时间变化(lifetime)。这样的好处是,粒子在不同的lifetime里可以有不同的渐变颜色值,多张不同的ramp texture就可以得到一些差异较大的粒子效果,得以重用。

smoke_remap_color
smoke_remap_color

  • 透明度:Fallout 4里部分抽离了透明度信息。帧动画的A通道仍然保存了一个透明度值,但它并不是最后的透明度,我们同样用一张二维的ramp texture来重映射透明度值,使之受lifetime的影响。

smoke_remap_alpha
smoke_remap_alpha

实现起来还是很简单的,主要的代码就是:

1
2
float3 curRampCol = tex2D(_ColorRamp, float2(curCol.r, lifeTime)).rgb;
float curRampAlpha = tex2D(_AlphaRamp, float2(curCol.a, lifeTime)).a;

上面的lifeTime用来控制纵坐标方向上的采样。

Motion Vectors

后来搜资料的时候又发现了一种优化动画过渡的方法。一般帧动画的播放方法有两种,要么每时刻只播放一帧,这种一般播放速度很快并且帧与帧之间的差别不大,使人看不出跳跃;要么就是使用线性混合的方法混合相邻两帧。虽然后面这种的确可以起到一定平滑过渡的作用,但是如果相邻两帧图像差别较大,速度不够快的时候还是可以看到比较明显的穿帮。于是就有了motion vector的方法。

Klemen Lozar在他的一篇文章中比较详细地讲了如何在帧动画中应用motion vector。文章中指出,这种做法最开始是由Guerrilla Games在Killzone 2的cutscene中开始使用的技术。核心思想就是靠motion vector来偏移坐标。Motion vector其实和flow map等这些概念本质上是一样的,它表明了当前这个像素点将来的走向,据此可以达到平滑流动的效果。水渲染里面也经常使用flow map来模拟河流的流动。

在实现方法上,我们除了需要传统的帧动画图像外,还需要一张motion vector贴图。据了解,一般都是在用模拟软件比如Houdini在渲染得到帧动画的同时,渲染motion vector。如果你只有一张RGB通道的普通的帧动画,我搜到了一个小神器可以直接生成——FacedownFX的Slate Editor,效果还可以。下面是一个例子:

smoke_cloud
smoke_cloud
smoke_motion
smoke_motion

额因为我用的试用版,所以生成出来的图片都有水印。除了生成motion vector(还可以很方便地查看帧动画的效果),Slate Editor还可以快速分割图片,是个不错的小工具。

代码的话也比较简单,要说的话都在代码里了:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
///
/// 使用motion vector来混合前后两帧图像,实现平滑的帧动画效果
///
Shader "Smoke Frame" {
	Properties {
		_MainTex ("Image Sequence (Tiling)", 2D) = "white" {}
		_FlowMap ("Motion Vectors", 2D) = "white" {}
		_ColorRamp ("Color Ramp", 2D) = "white" {}
		_AlphaRamp ("Alpha Ramp", 2D) = "white" {}
		_Speed ("Speed", Range(1, 100)) = 30
		_LifeTime ("Life Time", Range(0, 1)) = 0
		_DistortionStrength ("Distortion Strength", Range(0, 0.005)) = 0
	}

	SubShader{
		Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }

		Pass {
			Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
			Cull Off

			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _FlowMap;
			sampler2D _ColorRamp;
			sampler2D _AlphaRamp;
			float _Speed;
			float _LifeTime;
			float _DistortionStrength;

			struct a2v {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
				float2 blendFactor : TEXCOORD1;
			};

			// 算第frame帧图像对应的uv坐标,subImages是帧图像的行列数目
			inline float2 SubUVCoordinates(float2 uv, float frame, float2 subImages) {
				float time = floor(frame);
				float row = floor(time / subImages.y);
				float column = time - row * subImages.x;
				uv = uv + half2(column, subImages.x - 1 -row);

				uv.x /= subImages.x;
				uv.y /= subImages.y;

				return uv;
			}

			v2f vert (a2v v) {
				v2f o;

				o.pos = UnityObjectToClipPos(v.vertex);

				float frame = _Time.y * _Speed;
               			// 前后两帧图像的uv坐标
				o.uv.xy = SubUVCoordinates(v.texcoord.xy, frame, _MainTex_ST.xy);
				o.uv.zw = SubUVCoordinates(v.texcoord.xy, frame + 1, _MainTex_ST.xy);
                		// 混合前后量帧时使用的混合系数
				o.blendFactor.x = frac(frame);
                		// 计算lifetime,第0帧对应lifetime值0,最后一帧对应lifetime值1
				o.blendFactor.y = frac(frame / (_MainTex_ST.x * _MainTex_ST.y));

				return o;
			}

			inline float2 SampleMotionVector(float2 uv) {
            			// 这里会乘以float2(1, -1)是因为motion vector中存储的方向和Unity里面纹理采样的方向有所不同
				return (tex2D(_FlowMap, uv).rg * 2.0 - 1.0) * float2(1, -1);
			}

			float4 frag (v2f i) : SV_Target {
				float2 curUV = i.uv.xy;
				float2 nextUV = i.uv.zw;
				float blendFactor = i.blendFactor.x;
				float lifeTime = i.blendFactor.y;

				float2 curMotion = SampleMotionVector(curUV);
				float2 nextMotion = SampleMotionVector(nextUV);

				// 使用当前帧/后一帧的motion vector偏移当前帧/后一帧的采样坐标
				curUV = curUV - curMotion * blendFactor * _DistortionStrength;
				nextUV = nextUV + nextMotion * (1 - blendFactor) * _DistortionStrength;

				float4 curCol = tex2D(_MainTex, curUV);
				float4 nextCol = tex2D(_MainTex, nextUV);

				// 使用前一节的方法映射得到真正的颜色值和透明值
				float3 curRampCol = tex2D(_ColorRamp, float2(curCol.r, lifeTime)).rgb;
				float curRampAlpha = tex2D(_AlphaRamp, float2(curCol.a, lifeTime)).a;
				float3 nextRampCol = tex2D(_ColorRamp, float2(nextCol.r, lifeTime)).rgb;
				float nextRampAlpha = tex2D(_AlphaRamp, float2(nextCol.a, lifeTime)).a;

				float4 col;
				col.rgb = lerp(curRampCol, nextRampCol, blendFactor);
				col.a = lerp(curRampAlpha, nextRampAlpha, blendFactor);

				return col;
			}

			ENDCG
		}
	}
}

过程式生成的Smoke效果

Simon的文章很有意思的一点是,他总是会举一反三列举很多相关的实现链接。Fallout 4 – The Mushroom Case这篇文章的Update 4里列举了一个方法,实现的效果很有趣:

Smoke09
Smoke09

然后我就尝试按照原文中的实现了一下。这个作者Zoltan是一名TA,之前就看过他写的一些很有意思的文章,但是每次看的时候没有辣么瘾就是因为他对于技术实现总是写的非常简略,不像其他博主起码会给出一些代码片,所以要重现他的效果要完全自己猜。然后我重现的效果是……

smoke_particle
smoke_particle

不谈效果好坏吧,我觉得实现的过程还是很有意思。他的做法也应用到了motion vector的思想,通过在cellular noise上应用motion vector控制噪声流动,来模拟一种很有意思的孔洞变大的效果。我先放上代码吧,关于噪声真的很有意思,有时间在下一篇里面再写吧。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
Shader "Smoke Particle" {
	Properties {
		_ParticleNoiseTex ("Noise Tex (RG: Derivatives B: Noise)", 2D) = "white" {}
		_RampTex ("Ramp Tex", 2D) = "white" {}
		_NoiseTex ("Noise Tex", 2D) = "white" {}
		_Speed ("Speed", Range(0, 5)) = 1
	}

	SubShader{
		Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }

		Pass {
			Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
			Cull Off

			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			sampler2D _ParticleNoiseTex;
			float4 _ParticleNoiseTex_ST;
			sampler2D _RampTex;
			sampler2D _NoiseTex;
			float _Speed;

			struct a2v {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv0 : TEXCOORD0;
				float2 uv1 : TEXCOORD1;
			};

			v2f vert (a2v v) {
				v2f o;

				o.pos = UnityObjectToClipPos(v.vertex);

				o.uv0.xy = TRANSFORM_TEX(v.texcoord, _ParticleNoiseTex);
				o.uv0.zw = TRANSFORM_TEX(v.texcoord, _ParticleNoiseTex).yx + float2(0.5, 0.5);

				o.uv1.xy = v.texcoord;

				return o;
			}
			
			inline float SampleNoise(float2 uv, float dist) {
				float2 derivatives = tex2D(_ParticleNoiseTex, uv).rg * 2.0 - 1.0;
				float noise = tex2D(_ParticleNoiseTex, uv + derivatives * float2(1, -1) * dist).b;
				return noise;
			}

			inline float RadialGradient(float2 pos, float radius, float blur) {
				return 1.0 - smoothstep(1.0 - blur * 2.0, 1.0, sqrt(dot(pos, pos) / max(0.0001, radius * radius)));
			}

			float4 frag (v2f i) : SV_Target {
				float2 dist = i.uv1.xy - 0.5;
                		// 来模拟从中间向外扩散的效果
				float time = dot(dist, dist) * (0.5 + tex2D(_ParticleNoiseTex, i.uv1.xy * 0.3).b * 0.5) * 4.0 - _Time.y * _Speed;

				// 模拟两层运动
				float loop1 = time;
				float loop2 = time + 0.5;
				float2 offsetUV0 = float2(floor(loop1) * 0.01, 0.3);
				float2 offsetUV1 = float2(floor(loop2) * 0.01, 0.6);
				float distort0 = frac(loop1);
				float distort1 = frac(loop2);

				float2 uv0 = i.uv0.xy + tex2D(_NoiseTex, offsetUV0).rg;
				float2 uv1 = i.uv0.zw + tex2D(_NoiseTex, offsetUV1).rg;
				float noise0 = SampleNoise(uv0, lerp(-0.05, 0.05, distort0));
				float noise1 = SampleNoise(uv1, lerp(-0.05, 0.05, distort1));

				float noise = lerp(noise0, noise1, lerp(0, 1, abs(distort0 - 0.5) * 2.0));
				noise = pow(noise, 1.2);

				// 计算smoke的颜色
				float fieryFade = lerp(0.5, 1.5, RadialGradient(dist, 0.2 + noise * 0.3, 0.5));
				float3 col = tex2D(_RampTex, float2(noise, 0.5)) * fieryFade;
				float alpha = RadialGradient(dist, 0.2 + noise * 0.3, 0.1);

				return float4(col, alpha);
			}

			ENDCG
		}
	}
}

「博客更新」博客评论系统更新记录

原因

由于众所周知的原因,多说评论系统要在儿童节关闭了。作为懒癌患者,我一直坚挺到最后一天才在网友的督促下更新了博客的评论系统。请为我鼓掌……

选择

今天改评论系统的时候,看了下网上的一些建议。我先看了下之前关注的一些博主是怎么换的,毕竟我懒得自己搞前端。惊讶地发现他们(我博客模板的制作者钱钱的博客雨松MOMO的博客)都换成了网易云跟帖,既然大家都这么用,随大流总没错。额,但是网易大大不让我抱大腿……目前,网易云跟帖只认根域名,所以我这种原生github.io的域名总是会报错……既然大大看不上我们这种小用户,用不了就只能拉到。另一方面,网易云跟帖对于留言的命名实在是很土,什么“有态度网友xxx”,我这种外貌协会会有些心理不适应。

然后就是Disqus了,当然由于众所周知的原因会经常打不开,如果有其他更好的还是不要优先选择它了。

最后,选择了livere,韩国人做的一个评论系统,符合我小而美的审美观,而且支持国内国外几乎所有流行的社交平台的登录,这点非常棒。但缺点也是有的,比如不支持第三方评论导入,所以我之前的多说评论似乎好像就这么要没了……还好我博客访问量还比较小,留言不多,现在只希望它不要落得跟多说一样的下场就好。

最后

恩,我好久没更新博客了,工作了的确时间不能那么自由了,有些东西也由于某些原因写不了了。希望今后有时间有能力可以分享更多的文章吧。啦啦啦啦啦啦~

「总结」Bake Shading to Texture踩坑记录

写在前面

最近需要完成这样一个需求,在此记录下踩的坑。这种需求还是比较常见的,比如皮肤渲染里会把diffusion profile存储在图像空间里,进行处理后再贴回到模型表面。

流程

首先大概讲一下流程吧。

  1. 把shading的结果根据模型的uv烘焙到一张固定大小的texture上(假定摄像机背景色为黑色),我们称之为baked texture
  2. 在实际渲染的时候再根据模型uv贴回去,理论上来说可以得到和直接渲染一样的渲染效果

下面这张就是把模型简单的漫反射结果根据uv烘焙到纹理上得到的baked texture,它的layout取决于uv展开时的layout。

baked-texture.png
baked-texture.png

接缝问题

然而,当把这张图贴回模型的时候,我们可以发现在明暗变化比较明显的地方(通常是烘焙贴图分辨率低于屏幕像素密度)或者在uv接缝处会有缝隙。

seam-texture0.png
seam-texture0.png

原因

上面那张图还是经过双重滤波后的样子。如果我们用point filter去采样这张baked texture来贴到模型上,就可以发现那些明显的异常像素点:

seam-texture1.png
seam-texture1.png

这些黑色的点是因为采样到了背景像素(在前面我们把摄像机的背景色设成了黑色)。可以分析原因如下。这些位置处的顶点会存在UV Split的情况,虽然顶点位置一样,但在物理上它们会对应多个具有不同uv值的顶点。

seam-texture2.png
seam-texture2.png

对于这样接缝边相邻的两个三角形,由于这条边的两个顶点在两个三角形内完全是不同的uv坐标,所以在光栅化的时候它们实际上光栅化到了baked texture上完全不相邻的两个位置。当我们重新要把这张baked texture贴上来的时候,采样到接缝处时很有可能就采样到旁边的背景像素。如果开始了纹理filter和mipmap,则会因为uv导数不连续,同样导致无法得到正确的结果。

实际上除了这些黑色像素,你还可以发现出现了棋盘状的一些效果。我们来尝试还原这些黑色像素和棋盘状效果的产生过程。我们把最后输出时候的像素标记为$P_s$,烘焙时候的像素称为$P_b$。那么过程如下:

  1. 在第一步烘焙的时候,在vertex shader阶段我们根据模型uv输出顶点的屏幕位置,然后经过处理后进入光栅化。由于接缝处uv不连续,因此实际会光栅化到baked texture的完全不相邻的位置上去。同时,由于baked texture的分辨率为512x512,因此光栅化时插值的uv精度(即更新baked texture的精度)也会是1/512。然后我们进行了正常的光照计算,把结果存到了这些光栅化像素$P_b$中。
  2. 在把baked texture贴回去的时候,经过屏幕空间光栅化后生成了$P_s$,这次$P_s$会按照模型uv坐标去查找baked texture。由于纹理分辨率问题,多个$P_s$会采样到同一个$P_b$,所以会出现图中棋盘状的结果,这是因为每一个格子内的$P_s$对应了同一个$P_b$。而黑色像素则是因为,这个精度位置的$P_b$压根在烘焙的时候压根就没有被更新到,所以还会保留着背景颜色。

既然如此,要想消灭这些背景像素,理所当然想要的一个方法就是提高baked texture的分辨率。提高分辨率这样的确可以减少黑色像素,但还是无法完全消灭。下面是baked texture为4096x4096,屏幕大小为800x800时候的情况:

seam-texture3.png
seam-texture3.png

这主要是因为烘焙时候的精度和uv展开的layout也有关系,这造成总会有一些屏幕空间的像素无法在烘焙的时候得到更新,导致仍然保留的背景色。一个解决方法是把baked texture向背景部分bleed几个像素,使得这些接缝处的像素可以向外扩展一些。这样的确可以在baked texture分辨率较高的情况下清除掉这些背景像素:

seam-texture4.png
seam-texture4.png

但是,在baked texture分辨率较低、且接缝处明暗变化较大时,还是会出现肉眼可见的接缝。下面是baked texture为512x512、屏幕分辨率为800x800,在bleed前后的对比图(bleed时取背景像素点为中心的3x3方格内所有非背景像素点的平均值):

seam-texture5.png
seam-texture5.png
seam-texture6.png
seam-texture6.png

可以从右图明显地看到不连续和残留的黑色背景像素点。背景残留还是因为精度差别较大导致bleed的像素个数不够,当然你可以通过增加bleed的宽度,但这样无疑会增加采样点,baked texture分辨率越低,需要增加的宽度就越多。而不连续一方面是因为此时棋盘格子(由于uv layout以及采样到了同一个$P_b$)比较明显,一方面是bleed的时候我们只把当前这一边的像素扩展出去,而没有真的去混合uv接缝处实际的另一边的像素值。

总结

理论上,要从根本上解决接缝处的问题,我们应该要知道接缝处真正的旁边像素是什么,即修改它的偏导,这样就可以真的去混合正确的相邻颜色得到平滑的结果,但这难做到。或者是真的去“膨胀边界”,这和建模软件里展UV道理相同,可以想象为什么普通的贴图没有接缝问题,这是因为建模软件在展开的时候可以选择在边界处多渲染一个宽度,这个宽度内的像素不是像我们上面那样简单的复制或取当前的平均值,而是找到真正相邻位置的信息渲染到这个边界处,从而在采样的时候就可以得到正确的混合结果,但这个渲染的过程在实时里负担比较重。

实际上,这个问题和烘焙lightmap时候出现的light/shadow bleeding问题很像。大部分解决方法就是向背景部分膨胀,也就是我们上面提到的bleed方法。或者是提高分辨率。

Nvidia在2007 Advanced Skin里也提到了一些方法:

  • Change clear color(鸡肋)
  • Object / no-object alpha map(应该就是类似我们这里的bleed)
  • Multiple UV sets

额,最后一个不知道是什么东西。

总之,到了这里,对于我的需求来说,我决定就此放弃这个方法了……

「闲谈」一本技术书籍是如何出版的……

会看这篇文章的人估计知道,额小妹不才之前写了一本书。已经过去了一段时间了,目前觉着趁着自己还有一些实践,打算对第一版本进行修缮写一下第二版(主要是看不过去一些书中的错误…),值此之际,写一下第一次出书的感悟。如果有朋友看到了这篇文章,同时也打算出书的话,希望这篇文章里面的一些事情和经历可以让你有所借鉴,少走一些弯路。

前因后果

我记得很清楚,当时是2015年的六月份,我的邮箱里出现了一封来自人民邮电出版社的编辑,打开之后着实吓了一跳,竟然有人要找我写书……之前的确也想过一些出版的时候,不过当时只是想着可以靠自己的能力翻译一本书,没错,那本书就是《Unity Shaders and Effects Cookbook》的第一版。很早之前,大概是大三的时候,我开始想要自己学习一下shader方面的东西,找到了这本书。这本书应该是当时国内外唯一一本讲Unity Shader的书,我也是因为在自己的博客里更新相关文章才为大家所知。因此,当时有电子工业出版社的人找到我想让我翻译,我当时很快就答应了,反正我在博客里已经翻译了很多了…………额不过后来小编告诉我他们没有拿到国外的版权(已经被机械工业出版社拿走了)啊哈哈哈哈…………这次不一样,因为是我重头开始自己写一本全新的书,我心里还是有点忐忑的。当然了,嘴上说不要身体却很诚实,我考虑了没多久还是答应了。我记得我当时满怀忐忑地和编辑说,我已经做好了被读者骂的准备了。编辑笑说,哪有那么严重啊。我会这么说,是因为我对国内技术书籍的出版现状表示比较委婉的无语,这就不用我多说了。而且我一个无名小辈,那么多大大都没著书……编辑会找到我,是因为他们之前的一个作者推荐了我。总之,我就这么接下了这活。

出书的过程

我个人出书的过程大概是这样的:

  1. 和编辑讨论出版流程、版税(重要),双方达成共识。
  2. 和编辑大概讨论后,我花了一周的时间定下了书的目录,以及一个样章。同时编辑给了我一个选题的表格,大概讲一下书的受众人群之类的。
  3. 编辑拿到这些材料后,会在出版社申请书号以及后续的一些流程,随后就给我寄了一份合同来签约。
  4. 之后就写书ing…………
  5. 完成后将书稿交给编辑,由出版社进行排版等工作,这期间基本不需要我参与。
  6. 排版后编辑发给我让我查看和修改,没问题后他们开始印刷,出版上市
  7. 然后卖完就可以收钱了

我的写书工具:马克飞象+Markdown,然后用Pandoc转换成word。画图是用的OmniGraffle,好评。

不过每一步都有一点坑啊(偶尔也有大坑)……版税这个东西,我个人感觉如果不是什么大咖且是第一次出书的话每个作者应该都一样,感觉应该是他们出版界的规矩了。我当时货比两家,版税给的都一样,最后感觉人邮出版社的这个编辑资历比较长,所以最后选择了人邮出版社。我记得当时问编辑,我写书的时候会不会有人帮我审稿,万一我书里写错了什么有专业人士审稿的话能更有保障一点。当时编辑肯定地告诉我说,恩我们会请一些这方面的专业人士来看,请你放心。然而,并没有啊!!!此乃第一小坑。关于彩版的问题,我当时也问过,编辑当时说,恩我们可以等你写完再讨论,会尽力出彩版。恩当然后来也没有出彩版。其实吧,如果是我这种第一次出书的情况,基本不会出彩版的,出版社毕竟要为销量操心,彩版多的那几十块钱很多时候还是很重要的。前几天还有个人明目张胆地跟我说他在看我的盗版书籍,说买不起……就五十块钱啊……

书的目录也是很重要的,我当时大概翻看了我电脑里面所有相关的技术书籍的目录,也借鉴了不少。现在看来这个目录还不错,我后面写书的时候几乎没有改动,都是按照这个顺序来写的,所以写得时候还比较顺利。我印象里写书的时候最痛苦的地方有三章,首先就是数学章了,那公式打得我呀……我当时是用markdown写的,但编辑说他们不会用markdown,所以我前期调研了下确保markdown可以转成word才决定全书都使用markdown来写。虽说markdown打公式还比较方便,可是那几百个矩阵啊,每天打得我感觉手指都要抽筋了……然后就是高级光照那一章了,此时Unity闭源的弊端不能再明显了,官方文档大家也懂的,我每次写一个原理都要查证很久,生怕写错了。可是,最后还是逃不过人家版本升级啊……最后就是基于物理的渲染了,这一章主要是有很多比较复杂的公式和论文,我现在回过头来看我对第一版的这一章不怎么满意,现在已经重写了。其实整体来看,整个写书的过程还是挺愉快了,每天都很充实。由于担心自己的知识储备,每天都要看很多书和网页,感觉那段时间自己也学到了很多知识。年轻真好呀~

好了,书写完了,真正的大坑马上就要来了。我记得我交稿的时候是十一月份,所以整本书的写作时间大概是4、5个月,因为当时在读书还比较闲,所以基本大部分时间都用来看书写书了。我当时满心欢喜地以为2016年年前就能看到成书,事实证明太天真了啊。我基本每周都问一遍编辑,书排版的怎么样了,什么时候可以出版。编辑一开始说可能12月底可以排好,一月出版;到了十二月又说1月可以排好;到了一月说,雾霾天工厂都停工了所以延期,说年后发售销量会更好。期间,编辑有发过来一些他们审稿的照片,表示说他们排版都是要打印出来一页一页地看,很费功夫,让我不要着急。

终于终于,在三月份的时候编辑告诉了我好消息,排版终于排好了!我现在回想起来当时大概是由于太高兴所以疏忽大意了。当时,距离我把成稿交给编辑已经又过了四个月了,由于之前没有出过书没有经验,我拿到排版好的书稿后打算过一遍看有没有问题。我打开看了看前两章,发现没有什么太大的问题,加之我之前由markdown转为word的时候平均每章都看过2遍以上,这次再看就缺乏了耐心,加上对出版社的工作人员过于信任,由此犯了大错。我自己没有完整地把出版社的稿子过一遍,加上实验室的同学对这本书挺有兴趣,我就发给了他,让他一边看一边帮我检查一下,我自己就懈怠了……啊啊啊,我对不起读者大人们……后来的问题可能有些人也知道,数学一章出了非常大的问题,有非常多的公式出了问题。那天,我拿到编辑寄给我的样书,本来心情是非常美丽的,要知道我等了已经半年多了。可是当我看到数学那一章的时候,脑袋一下就热了,WTF?!当我心里无数次骂娘之后,我意识到现在的问题是………………TMD出版社是怎么排版的?!!!!………………额这是实话,我当时发给编辑的时候这一章看得遍数是最多的(大概有三四遍吧),就怕转换的时候公式出了问题,怎么现在成了这样。我在假装冷静地和编辑诉说之后,编辑表示会帮我去询问到底怎么回事。当然了,我也没闲着,发了条朋友圈求朋友安慰……

wechat0
wechat0

好了诉苦诉完了,说到底还是自己做事不够认真才会有这样的问题。当时已经印刷了2000册,这意味着有2000个读者看到的是这样的书,啊啊啊不要跟人说这书是我写的……我跟编辑说,我打算把这一章公开到网上,以此来减轻我的罪过。编辑也就同意了。后来编辑说,因为排版人员不熟悉、公式太多等等才出了这样的问题,向我道歉,并且表示已经说过了排版人员。

实际上,在这之前也发过好多次修改意见给出版社,这主要是依靠实验室的那个朋友在看书的时候给我反映的一些问题。其实这时候勘误的时候我就发现了排版人员有些粗心,比如上一次发给他的勘误还是没有改正,或者又改错了等等,只是我没有料到数学一章会出如此大的差错。而我的那个朋友由于嫌数学章太长他基础又比较好,所以也没有细看。我每次都想,只要排版人员多看那么一眼,或者我当时再看一遍数学章,也不可能会出这么多问题,不过,可能当时他任务很重,有很多排版工作,也可能当时他心情不好,只想快点完工,都只是为了混口饭吃而已。我猜测,出版社的排版人员是完全重新打了一遍我的所有公式,我一直以为他会直接粘贴,所以才如此放心。后来勘误的时候我发现,甚至连表格里的内容似乎也是手打的,并不是粘贴,因为也有一些严重的错误……所以,排版人员也是出了苦力的。

另一件有些坎坷的事情就是封面和开头的彩插了。封面的原稿是我内人设计的,但是后来发现编辑发给我的成稿上的封面设计署名却只有出版社的一个人。你懂得……感觉国内出版社很多地方版权意识都不够强,我之前看人邮出版的一些书籍,封面都很丑(实话),后来发现一张特别酷炫的封面,是cocos2d的一本书。我专门发给编辑问这也是出版社人员设计的吗?“嗯是啊”。我想,哇塞,原来有厉害的啊。后来我内人一看,这不是某某某游戏的海报么……么……么……不过好在跟编辑反映过后加上了内人的名字。

另外就是开头的彩插了。当时,编辑让我挑一些书里的插图,放在目录那里好看。我立马翻了一遍插图,精心挑了好几张打包成word,配好文字发给了编辑。谁知道天有不测风云,你永远跟不上出版社排版人员的思路……

wechat1
wechat1

恩,我挑的图一张都没有,对,就是一、张、都、没、有。逗我么啊哈哈哈……后来当然在我的据理力争下改过来了,恩,后来图片居中这些我也参与了修改。

出现了这么些事情,我内人看起来比我还气,我一边安慰他一边安慰我自己,大家都不容易,改了就好,改了就好。

呐,一些开心的事情

当然最开心的还是书最后是出版了,也有不少读者给了它不错的评价,我真的特别开心。有个读者说,“能感觉到作者满满的让读者也能真正学会写shader的心意”。恩,写这本书的时候心里最大的愿望就是这个了。当然啦,也有批评,不过我不想写下来哇哈哈哈哈~

最后的话

好了,你也看出来了这本质是一篇抱怨文哈哈哈哈。不过说实话,整个过程虽然出了些问题,但出版社编辑一直都是有问题随时沟通、出错就改、凡事好商量的态度,还是值得点赞的。出书不易,写完稿真的只是开始的一步,当然这一步很重要。我拿到样书的时候是5月份,书正式上架是6月份,所以,我写书是从2015.6到2015.11共四个月,排版以及上市共用了七个多月。虽然坎坷,但是还是得感谢各种工作人员和朋友。最后,写一些建议吧,希望后面可以有所借鉴:

  • 一定一定一定不要过分相信和依赖出版社的工作人员。在我和编辑沟通的过程中,编辑一直都非常礼貌和客气,表现也很专业。只是遇到排版、设计封面等实实在在的工作上面,一定要自己认认真真地过一遍他们的工作。
  • 尽管编辑会不停地告诉你,你只需要安心写书,其他的什么都不用操心,但是你可千万不能信啊。理由同上。(我只是想再强调一遍……)
  • 拿到出版社排好的书稿后,一定要从头到尾、仔仔细细地看一遍。真的,他们有时会莫名其妙改掉你的稿子(不知道哪里来的知识和自信),有时会加上一些莫名其妙的东西,有时会引入新的错别字,有时会改掉你的专有名词大小写,有时会吞掉你的许多资源链接。总之,一定要看一遍。(当然我相信排版人员也应该帮我处理了很多错字……)
  • 如果出版社让你在开头写上由出版社管理的QQ答疑群,相信我,千万不要让他们写上去。(我会告诉你里面有大量成人广告又没人管么……)
  • 一般新书是不会出彩版的,所以写书的一开始最好就想好彩图的问题。我是放在了github上,但是很多读者没有看第一章所以压根不知道有彩图,常常抱怨图的标注问题。应该在重要彩图的地方时时刻刻提醒读者可以在网上找到彩图。源码也同样适用。
  • 勘误很重要,盯着出版社勘误更加重要。因为出问题出怕了,后面但凡是勘误和修改问题,我都会要求出版社把修改后的稿子发给我,我会亲自再过一遍。
  • 尽管我说了出版社很多坏话,但是沟通是最重要的,凡事好好说话。
  • 出书是一件开心的事情,尤其是收到读者好评的时候。
  • 最后,真正用心写书永远是最重要的事情。

「Project」MagicToon: A 2D-to-3D Creative Cartoon Modeling System with Mobile AR

MagicToon: A 2D-to-3D Creative Cartoon Modeling System with Mobile AR

Authors

Lele Feng, Xubo Yang, Shuangjiu Xiao
Digital ART Lab, Shanghai Jiao Tong University, China

Publication

IEEE Virtual Reality 2017, Los Angeles, California, March 18-22, 2017

Abstract

We present MagicToon, an interactive modeling system with mobile augmented reality (AR) that allows children to build 3D cartoon scenes creatively from their own 2D cartoon drawings on paper. Our system consists of two major components: an automatic 2D-to-3D cartoon model creator and an interactive model editor to construct more complicated AR scenes. The model creator can generate textured 3D cartoon models according to 2D drawings automatically and overlay them on the real world, bringing life to flat cartoon drawings. With our interactive model editor, the user can perform several optional operations on 3D models such as copying and animating in AR context through a touchscreen of a handheld device. The user can also author more complicated AR scenes by placing multiple registered drawings simultaneously. The results of our user study have shown that our system is easier to use compared with traditional sketch-based modeling systems and can give more play to children’s innovations compared with AR coloring books.

Supplemental Materials

Videos

BibTeX

@inproceedings{feng2017magictoon,
  title={MagicToon: A 2D-to-3D Creative Cartoon Modeling System with Mobile AR},
  author={Feng, Lele and Yang, Xubo and Xiao, Shuangjiu},
  booktitle={IEEE Virtual Reality},
  year={2017}
}
			

Attachments