复杂的光照与更复杂的阴影实现过程——ShaderCP9

 

——20.8.28

 

这章的内容看了很久,也有很多复杂的内容。中途还有事情耽搁了一会。开学后就继续好好记录努力。

我们在游戏中能看到的让人觉得真实感的来源之一就是真实的光照以及光照所产生的阴影。下面的内容分为两个部分一个是光照的部分一个是阴影的生成部分。

说到光照在Unity中渲染路径就是决定光照是以何种方式应用到Shader中的。所以要通过指定Pass通道的渲染路径类型 “LightMode” 来进行Shader的光照计算。简单地说就是设定渲染路径后Unity便会提供对应的光照信息给用户进行光照计算。

目前有三种渲染路径 1)前向渲染 2)延迟渲染(有新的) 3)顶点照明渲染(被抛弃)

当GPU不支持所选择的渲染路径则会降级

设置渲染路径便是在Tags设定 “LightMode” 有以下支持的的标签 

如果没有指定渲染路径可能会出错,因为此时当作为顶点照明渲染路径,当中的一些光照变量可能就不会被赋值。

一、前向渲染路径

每进行一次完整的前向渲染路径1.我们需要渲染该对象的渲染图元2.计算两个缓冲区的信息 a.深度缓冲区(决定是否可见)b.颜色缓冲区(若可见,更新颜色缓冲区)对灯光范围内的物品每一个灯光处理一次执行一次Pass

三种处理光照的方式 1)逐顶点处理 2)逐像素处理 3)球谐函数(SH)

一种光源的处理方式由类型和渲染模式(是否重要的,RenderMode)渲染一个物体要对光源进行排序(对物体的影响程度)要遵循以下规则 1)场景中最亮的平行光总是按逐像素处理2)渲染模式被设置为Not Important按逐顶点或是SH 3)渲染模式被设置为Important按逐像素处理 4)如果按以上规则得到的逐像素光源数量小于Quality Setting(PixelLight Count)将会由更多的光源按照逐像素处理

 

 

 

 注意事项:1.BasePass支持的光照特性 2.BasePass渲染的平行光默认有灯光 AddtivePass渲染的光源默认情况下没有阴影(即使设置了ShadowType)可以通过 #pragma multi_compile_fwdadd_fullshadows 代替 #pragma multi_compile_fwdadd 为点光源和聚光灯开启阴影 3.环境光(环境光和平行光不同)和自发光在BasePass计算因为只计算一次在AdditivePass会导致多次叠加 4.AdditivePass还要开启混合不然只有一个光源的影响 5.一个BassPass可定义多次(比如双面渲染)但只运行一遍而AdditivePass会根据影响该物体的逐像素光源多次调用 

 

以上是一些内置的函数和变量

二、顶点照明渲染

顶点照明可在前向渲染中完成,一个顶点照明的Pass最多8个逐顶点光源

 

三、延迟渲染路径

G缓冲 存储了离摄像机最近的表面的其他信息 包含两个单元第一个Pass计算哪些片元可见,第二个Pass利用G缓冲区中的信息光照计算。效率取决于场景的复杂度,而取决于屏幕空间大小。

优点 1)场景中的光源数目多,用前向渲染会造成性能瓶颈 2)每个光源采用逐像素处理 缺点 1)不支持抗锯齿 2)不能处理半透明 3)对显卡由需求

分析完渲染路径之后我们简单过一下基础的灯光类型在unity中存在的灯光类型以及它所具有的特点以及属性 1)平行光(不存在衰减 面向全局)2)点光源(边缘的强度为0,中间的衰减值可以用函数表示)3)聚光灯(同上,衰减计算更加复杂)4)面光源(只有烘培有用) 灯光所具有的基本特性 1)位置 2)方向 3)颜色 4)强度 5)衰减(到某点的衰减与距离有关)

UnityShader这本书只有对前向渲染有具体的内容,这里留一个延迟渲染路径的小坑(?)

主要就是注意要添加一个库“AutoLight.cginc” 其中有对之前对应渲染路径的内置变量定义,否则就找不到 比如_LightMatrix0之类

1
2
3
4
5
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif

  这部分是在AdditivePass中的中区分平行光以及其他光源的光源方向。

1
2
3
4
5
6
7
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
//atten = 1.0;
#endif

  这里部分是最重要的这部分是在AdditivePass中对衰减的定义。在BasePass因为主要逐像素处理一个最重要的平行光 而平行光不存在衰减所以atten为1 而在AdditivePass需要区分平行光和其他光源 点光源采用对应函数进行衰减。在Unity中为了方便使用一张衰减纹理_LightTexture0来为光源计算衰减,首先要把对应距离灯光的点通过坐标运算从世界到光源空间。然后通过点乘也就是顶点距离的平方来进行纹理取样。来避免开方计算。上面的".rr"相当于是取x并定义float2(x,x)用于取样。UNITY_ATTEN_CHANNEL是衰减值所在的衰减通道。我们不妨看一下这张_LightTexture0是什么样的图,就能理解为什么要平方之后再取样。

 

 这张图就是按照平方值设计的。可以放到画图工具中发现它不是线性的。实际相当于一维坐标。因为只有x。

实际上这么分其实是不够的因为其他灯光的聚光灯的衰减是不一样的。这里暂时留一下(?)

  主要就是在之前的光照模型上区分了BasePass以及AdditivePass,需要指定渲染路径以及添加对应的#pragma。最后最主要的是增加灯光的衰减。我们可以看一下实际效果。通过改变点光源的atten。足以看出衰减的真实感。衰减也可以用公式不用取样。一样后面继续研究(?)老留坑专家了。

 

 接下来便是最令人头大的阴影了。但又是最不可获取的部分。书中的理解部分会居多。

我们先看一个概念ShadowMap。就是把摄像机放到光源重合的位置然后场景中的阴影就是摄像机看不见的位置。 (本质上是为了得到光源空间的深度图)

在前向渲染中,平行光开启阴影就会计算它的阴影映射纹理(shadowmap)。实际上他是一张深度图(mark一下后面的仔细研究一下深度的应用?)记录了从光源位置出发能看到离他位置最近的表面位置也就是深度信息。那具体是怎么判断距离光源最近的表面呢?有以下两种方法。1)就是用上面的这个ShadowMap方法,把摄像机放到光源位置然后通过前向渲染的BasePass/AdditivePass更新深度信息。但是使用这种正常渲染方式其中涉及狠毒复杂的光照模型计算所以很浪费 2)使用额外的Pass “LightMode”=“ShadowCaster” 渲染目标是阴影映射纹理不是帧缓存。首先一样是把摄像机放到光源位置然后调用该Pass(此事获得的深度信息是光源空间,因为和光源重合)。介绍了上述两个获得表面的方法,在unity中首先会a.找该Pass b.然后找FallBake(因为FallBack定义的默认Shader会有对阴影定义的ShadowCaster通道) c.如果都没有就没有阴影。

然后到了最后一步就是获得了对应的深度怎么把它应用到阴影上。阴影映射技术就是我们需要看的 主要是由两种方法 1.传统方法,把正常的顶点坐标转换到光源空间,然后用xy分量对阴影映射纹理取样。如果深度小于改顶点的深度值也就是z分量则在阴影里。 2.屏幕空间的阴影投射技术。用阴影映射纹理以及摄像机(相机摆放位置即Game视图)的深度纹理通过这两张图得到屏幕空间的阴影图。摄像机记录的表面深度深度>其转换到阴影纹理的深度即为可见,但在该灯源的阴影中。然后对阴影图采样。

所以总结一下 两个过程 1.我们想要让物体接受其他物体的阴影我们就要对阴影图采样然后于光照结果相乘。2.我们想要一个物体向其他物体投射阴影则需要把该物体加入阴影映射纹理的计算中。我们下面进入实践过程。

 

 

 

我们可以看到这张图发现有一点奇怪,物品命名有阴影但是这个平面却没有。主要原因是平面在unity中只有一面渲染另一面不渲染。所以解决方法便是改变平面的MeshRenderer中的CastShadow为TwoSides才能得到正确的阴影。

 

 

我们看一下这张图发现这个方块没有接受到平面的阴影但自己却有阴影而且在Unity设置是正确的即有开启CastShadow 实际上之所以方块有阴影是因为采用了FallBack中的“ShadowCaster”的通道但是我们可以从上图得知平面想要投射阴影需要加入阴影投射纹理。实际上是输出片元的时候没有采样阴影映射纹理。我们可以通过FrameBugger观察它的深度图。

 

 上面两张图是一样的阴影图。但是在渲染方块的时候却不一样。

 

 

1
2
3
4
5
SHADOW_COORDS(2)//在v2f中的定义
TRANSFER_SHADOW(o);//在vert中
//在frag中
fixed shadow = SHADOW_ATTENUATION(i);
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);

  首先SHADOW_COORDS(2)用于对阴影采样纹理的坐标里面的索引值是2也就对应的是TEXCOORD2.在片元函数中的就是进行阴影映射技术把信息存储到_ShadowCrood。SHADOW_ATTENUATION对纹理进行采样。要注意结构体的命名可能会导致定义的宏不可用。

 

现在我们就有平面的投影了。

 

1
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

  这个函数是可以直接统一管理衰减以及阴影。不需要定义atten可以直接用。

  这样BasePass和AdditivePass就可以统一。

最后就是上一节的一个小坑也是填上了。就是半透明物体的阴影关系。

  首先是透明测试的投影。效果图如下。图一是表示能够正常的接受阴影。图二图三分别是采用不同的FallBack的效果后者相对于前者是比较正确的。最后一张图是是否打开CastShadow中的TwoSides左边是打开的可以看出正方形的内部投影是和左边相比体现了处理。

 

 最后是透明度混合的阴影。同样的增加FallBack

  采用FallBack "VertexLit"会强制投影同时接受阴影。"Transparent/VertexLit"两者都不会。主要是半透明物体由于关闭了深度写入也会影响对应阴影的生成。

 

这就是最后了。感谢您读到这里。Cheers!

 PS:这章总算是看完了。还留了很多坑。慢慢补把。

posted @   xkyl  阅读(429)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示