一、玻璃效果
首先来讲如何模拟玻璃效果。玻璃的渲染包括三部分,普通场景物体的渲染、反射和折射模拟、毛玻璃模拟。作为场景物体,那么类似其它场景物体Shader一样,可以使用PBR、BlingPhong或者Matcap,甚至三阶色卡通渲染都可以。玻璃比较特殊的地方是模拟对环境的反射和折射,以及模拟玻璃污渍效果。
对于场景物体的基础着色部分不再赘述,下面来介绍环境反射和折射、玻璃污渍模拟部分。
1.1 环境反射和折射
对于不要求实时反映环境变化的效果,那么采样静态贴图进行模拟,是一种性能和效果都更优的方式。从效果上来说,美术可以自由定制贴图,那么可以方便控制效果;从性能上来说,不要求实时blit出当前的colorbuffer,性能远超实时反射和折射。
1.1.1 静态Cubemap模拟
最常见的方式是使用Cubemap来模拟环境的反射和折射。
反射
- 计算当前着色像素的反射方向。
- 使用反射方向去从Cubemap中采样出反射颜色
half3 reflectVector = reflect(-inputData.viewDirectionWS, inputData.normalWS);
half3 reflectColor = SAMPLE_TEXTURECUBE_LOD(_EnvironmentCubeMap, sampler_EnvironmentCubeMap, reflectVector, _EnvironmentCubemapLod).rgb * _EnvironmentReflectionColor * _EnvironmentReflectionIntensity;
如上述代码,使用内置函数reflect即可计算视线到当前像素的反射方向,然后用该方向去采样Cubemap即可。具体相关数学原理,比较简单,不再赘述。
折射
- 计算当前着色像素的折射方向。
- 使用折射方向去从Cubemap中采样出折射颜色。
如何计算折射方向?
- -viewDirectionWS。最简单的方式是假定折射方向没有发生偏转,那么简单使用相机到该像素点的方向即可,即-inputData.viewDirectionWS。由于,这本来就是一种近似效果,因此简单使用视线方向得到的结果也能差强人意。
- refract。即使用折射定律来计算折射方向,直接调用refract函数即可,需要提供参数来调整折射率。
- Refraction Model。生活中真正的玻璃,光线是先折射进入玻璃,然后再折射出来到空气中,我们需要的是最终的方向,而不是到玻璃内的折射方向。要模拟真实的折射方向,可以使用简化的模拟来模拟,比如假设折射是通过一定厚度的球体或者立方体。相关内容和代码,在HDRP内已经使用,参考文档:Refraction in the High Definition Render Pipeline的Refraction Model部分。代码在com.unity.render-pipelines.core内,因此urp也可以使用。如果需要使用该折射模型,搜索RefractionModelBox或者RefractionModelSphere即可。
最终结果
使用fresnel定律,将反射和折射颜色叠加起来作为最终的环境颜色。这部分的关键在于正确计算出反射和折射的贡献比例,并不一定需要严格计算fresnel定律,只需要接近该定律的现象即可。
fresnel定律的基本意思是:视线方向与法线的角度越大,反射越明显。对于基本只剩下反射的区域,也可以叫做掠角。
因此,最终结果可以使用下述代码叠加起来。
float fresnel = pow(saturate(1 - dot(inputData.viewDirectionWS, inputData.normalWS)), 5.0);
half3 color = reflectColor * fresnel + refractColor * (1 - fresnel);
最终效果:
1.1.2 Matcap模拟
使用Matcap来模拟的话,思路与Cubemap类似。问题转换成如何从2D的Matcap贴图中计算反射颜色和折射颜色。
反射
反射其实可以理解为高光,那么可以参考Matcap如何实现高光的模拟部分:Matcap模拟高光。
折射
折射更像一个扭曲的过程,因为折射后方向发生了改变。那么,可以直接对uv进行扭曲,比如采样噪声图对uv进行叠加,再去采样一张折射matcap。
最终结果
与使用Cubemap类似,都需要使用计算fresnel定律计算折射和反射的混合比例。
1.1.3 实时反射和折射模拟
实时反射和折射,与前面两个算法的区别,是用反射和折射方向去采样当前的渲染结果,作为反射和折射的计算结果。
获得ColorBuffer
需要在管线内插入一个Pass,将ColorBuffer进行Blit到一个低分辨率的RT上,然后对该RT进行采样。对于URP渲染管线,我们只要设置请求OpaqueTexture后,既可以在Shader对_CameraOpaqueTexture进行采样。
反射
由于OpaqueTexture是屏幕空间纹理,那么需要在屏幕空间内计算反射方向,可以参考文章反射效果的实现总结的屏幕空间反射部分。
或者更直接参考GitHub上的开源项目:UnityURP-MobileScreenSpacePlanarReflection。在屏幕空间计算反射,算法部分比较复杂,不再赘述,请参考相关资料。
折射
由于反射要求方向精确,但是折射就没有这种要求,因此最简单的方式是计算出当前像素的屏幕空间位置后,然后对该位置进行扭曲,再采样OpaqueTexture即可获得折射结果。
当然如果要计算精确的折射方向,类似屏幕空间反射,都需要在屏幕空间内使用类似算法进行精确的方向计算, 然后再去采样屏幕空间RT。
最终结果
与使用Cubemap类似,都需要使用计算fresnel定律计算折射和反射的混合比例。
1.2 玻璃污渍模拟
该效果是对玻璃角落通常会出现污渍现象的模拟。通过观察,玻璃或者窗户一般是四个角落积累污渍。因此,可以计算与角落或者中心的距离,以这个距离归一化为默认的污渍强度。再结合一个污渍掩码贴图和污渍强度噪声贴图就可以让美术精细控制污渍了,当然不提供任何贴图也有默认的角落污渍。
具体功能,参考下图:
1.2.1 角落污渍
float uDis = min(abs(_FrostCenter.x - uv.x), abs(1 - _FrostCenter.x - uv.x));float vDis = min(abs(_FrostCenter.y - uv.y), abs(1 - _FrostCenter.y - uv.y));float dis = length(float2(uDis, vDis)) / 0.707;//斜边距离,然后归一化dis = _FrostReverse * (1 - dis) + (1 - _FrostReverse) * dis;//反转距离
如上述代码,_FrostCenter定义的是污渍的中心,这个通常是(0.5,0.5),即UV的中心。然后,计算当前uv到中心的归一化距离,用该距离作为污渍强度。
1.2.2 污渍强度Noise
half noiseDistance = SAMPLE_TEXTURE2D(_FrostNoiseMap, sampler_FrostNoiseMap, TRANSFORM_TEX(uv, _FrostNoiseMap)).r * _FrostNoiseIntensity;dis *= smoothstep(0, _FrostNoiseMax, noiseDistance);
从贴图内读取噪声强度,然后对强度进行smoothstep归一化,再乘以到原来的强度距离上。
1.2.3 污渍Mask
float4 frostMask = SAMPLE_TEXTURE2D(_FrostMaskMap, sampler_FrostMaskMap, TRANSFORM_TEX(uv, _FrostMaskMap));dis *= lerp(1, lerp(frostMask.x, 1 - frostMask.x, _FrostMaskReverse), _FrostBlendFactor);//blend with mask
从贴图内读取mask,然后将mask乘到原有的强度上。至于_FrostMaskReverse则是强度反转控制,_FrostBlendFactor是mask比例控制。
1.2.4 将距离转化为污渍颜色
float weight = smoothstep(0, _FrostDistance / 0.707, dis);color.rgb += weight * _FrostColor * _FrostIntensity;
_FrostDistance/0.707是归一化的最大距离。然后对dis进行smoothstep后就可以得到污渍强度。最后,将
污渍强度与污渍颜色、污渍整体强度相乘后叠加到最终颜色上即可。当然,也可以有其它的应用方式,比如用污渍weight来改变法线等。
最终效果如图:
二、窗户室内模拟
这是另外一种窗户模拟效果,跟玻璃效果差距较大,但是也可以作为通用Shader的一部分整合进来,因此放在一起讲述。详细的效果和算法可以参考文章:案例学习——Interior Mapping 室内映射(假室内效果)。
该算法的整体思想,是计算当前视线与室内的交点,然后从室内环境Cubemap获取交点的颜色作为最终颜色。计算交点有两种方式,一种是在模型空间计算,一种是在切线空间计算。在模型空间计算,依赖模型空间坐标系的具体范围,更通用的方式是在切线空间计算。
input.interiorViewDir.z *= 1 / (1 - _InteriorDepthScale) - 1;half3 revseseViewDir = SafeNormalize(-input.interiorViewDir);#if _INTERIOR_TANGENTfloat2 interiorUV = frac(TRANSFORM_TEX(uv, _InteriorCubemap) + 0.0001);// raytrace box from tangent view dirfloat3 pos = float3(interiorUV * 2.0 - 1.0, 1.0);#elsefloat3 pos = frac(input.positionOS * _InteriorCubemap_ST.xyx + _InteriorCubemap_ST.zwz + 0.0001);// raytrace box from object view dir// transform object space uvw( min max corner = (0,0,0) & (+1,+1,+1)) // to normalized box space(min max corner = (-1,-1,-1) & (+1,+1,+1))pos = pos * 2.0 - 1.0;#endiffloat3 id = 1.0 / revseseViewDir;float3 k = abs(id) - pos * id;float kMin = min(min(k.x, k.y), k.z);pos += kMin * revseseViewDir;refractColor += SAMPLE_TEXTURECUBE(_InteriorCubemap, sampler_InteriorCubemap, pos.xyz).rgb * _InteriorIntensity;
_InteriorDepthScale表示室内的深度,对室内进行远近拉伸,默认是0.5,表示没有拉伸。详细的推导算法请参考上述文章。
具体效果:
外表凹凸不平的是玻璃本身的颜色贴图和法线贴图效果,内部是室内模拟效果。
参考资料
Refraction in the High Definition Render Pipeline
Matcap模拟高光
反射效果的实现总结
UnityURP-MobileScreenSpacePlanarReflection
案例学习——Interior Mapping 室内映射(假室内效果)