浙江铃声推荐联盟

Qualcomm张涛:移动平台创建高级3D特效

CSDN2018-12-05 14:16:26

在5月18日的Qualcomm骁龙游戏和图形开发者研讨会上,Qualcomm资深工程师张涛就移动平台上如何创建高级3D特效进行了分享。从卡通渲染、皮肤、衣服、毛皮、景深到随深度衰减的雾、上帝之光以及复古色等,深度讲授绘制各种材质的方法。


卡通渲染


卡通渲染是一类使图像具有手绘图风格的技术,换句话说,这种技术可以将渲染图像“卡通化”。与传统的真实感渲染技术不同,卡通渲染的光照计算结果被有意离散化,它们被映射到离散的色调中,并且通过突出模型边缘来产生“勾边”的效果。




实现


卡通渲染类似手绘图,它的风格看起来非常简洁,但算法实现却比较复杂,需要经过多步渲染才能得到最终结果。具体步骤如下:


  • 第一步:得到平坦化、条带状的图像。这是通过把光照计算的结果当成纹理坐标,用于索引一张颜色坡度纹理得到的。光照计算可以采用任意的光照模型。

  • 第二步:渲染法向图。场景模型的法向被渲染到一个临时的离屏render target上,这个法向图只是用来做边缘检测,并不会对最终结果产生直接影响。

  • 第三步:对法向图进行边缘检测,并将其结果与第一步的图像混合得到最终结果。


皮肤


皮肤渲染是实时计算机图形学的难题之一,一方面,这是因为皮肤光照的复杂性,另一方面,观察者对皮肤太熟悉了,一旦结果看起来“不像”,他们就很难接受。关于皮肤的光照模型有几个,比如Hanrahan和Krueger在1993年的Siggraph上首次将皮肤进行分层模拟计算光照反射。他们的方法看起来非常逼真,但是每个光照计算需要100个指令,这对于实时的要求来说太昂贵。




张涛在实例中展示了一个经过优化过的皮肤渲染算法,它可以运行在移动设备上。这个技术通过在阴影区域的漫反射中加入暖色调来逼近次表面散射。并在漫反射计算中加入Minnaert光照项,能得皮肤看起来柔嫩,最后,通过加入镜面高光使皮肤看起来富有光泽。


实现


该实现的光照方程如下:


Final color = Ambient color+ Minnaertdiffuse color+ Subsurface scattering color+ Specular highlights


除了传统的辐射反射,Minnaert光照模型还记录了从皮肤反射的光辐射度,因此它被用来模拟皮肤的漫反射。它比别的光照模型看起来更柔软,而且它还可以关闭自阴影,从而让艺术家可以控制皮肤的“粗糙度”。


[cpp] view plaincopy

  1. // General Minnaertformula:

  2. // Result = Color * (cos(NL)^k * cos(VN)^(1-k))

  3. floatA = saturate( pow( max( 0.02, NL ), g_MinnaertExponent) );

  4. floatB = saturate( pow( VN, 1.0-g_MinnaertExponent) );

  5. gl_FragColor.rgb+= Color.rgb* ( A * B );


衣服


衣服无处不在,通常人体90%的部分被衣服覆盖,因此想要创建逼真的人物模型的话,一个好的衣服着色器是必不可少的。需要注意的是,即便是最普通衣服上的一块布料也有着大量的凸起、褶皱和变形扭曲。衣服可以看起来是柔软的,也可以看起来像闪亮的丝绒。




马塞尔Minnaert在1941年提出一种各向异性光照模型,这个模型至今在现代计算机图形学的衣服渲染中仍被广泛使用。Minnaert渲染使用双向反射分布函数生成该各向异性光照函数。与其它的光照模型不同,该模型通过考虑视角来计算衣服模型的粗糙度,因此在光线和观察方向汇聚的时候漫反射颜色不会过饱和,另一方面,从接近模型平行的角度看去的话模型会显得较黑,看起来有丝绒的感觉。


实现


实例演示中的着色器使用了一个经过修改的Minnaert公式,从而可以得到出高光区域和阴影区域光滑过渡的效果。


// General Minnaertformula:
// Result = Color * (cos(NL)^k * cos(VN)^(1-k))
// float A = saturate( pow( NL, g_Minnaert) );
// float B = saturate( pow( VN, 1.0 -g_Minnaert) );
// float Diffuse = ( A * B );
// Enhanced Minnaert:
floatDiffuse = NL * pow( max( NL * VN, 0.1), 1.0-g_Minnaert);


在两个公式中,如果g_Minnaert为1.0,这个方程就是标准的Lambertian模型。随着Minnaert指数的变化,边缘的颜色会随之变深。




这个经过修改的公式更好的模拟了像衣服一样的模型表面,因为它磨平了模型的硬边。在一般的Minnaert方程中,硬边在Minnaert参数较小的情况下会出现在NL == 0的地方。这对某些视角或者在有诸如次表面散射等其它光照项存在的情况下很有用,它强化了这种自然过渡的感觉。


毛皮


渲染个人的头发非常耗时,因此毛皮着色器只能使用逼近的方法。做到这一点的一种方法是在越来越大的同心卷中多次渲染一个表面,并且同时渐缩图像的细节(创建毛发越靠近端部越稀疏的感觉),这种技术被称为“脱壳”或“壳纹理”。


实现


这个示例程序的着色器使用shell技术来近似模拟皮毛,头发长度可以实时调节。


算法如下:


  1. 打开alpha混合,渲染物体N次;

  2. 在每一次渲染物体的时候,通过顶点着色器将模型变大一点;

  3. 同时在每一次渲染的时候将头发的颜色调深一点。





头发密度通过漫反射纹理的alpha通道控制,斑点图通常最为有效,它有助于将“胖”像素(大于一个纹素)在皮毛上逐步变小。如果密度图的像素太小,毛发看起来像圆柱体而不是锥体,这样在毛发末梢就会显得不够柔软。




该细化算法减掉从密度图中取出的值,而不是乘以它们进行缩放,这看起来像是在毛发末梢“雕刻出”小细节。毛皮的绒毛感正是通过这种变薄的过程获得。


gl_FragColor= vec4( Color.rgb* NL, saturate( Color.w-g_FurThinning);


景深


景深出现在图像中可以清晰显示场景的部分,这是因为透镜只能聚焦在某一个距离。在透镜每一侧的焦距处,随着距离增加清晰度逐步降低,这是与针孔镜头不同的。针孔镜头只让一条光线通过,因此图像是非常清晰的,在很长的一段时间里,计算机生成的图像看起来就是这种感觉,现在可以通过模拟诸如景深这样的透镜效果来达到更逼真的感觉。


实现


张涛表示,有几个不同的方法来实时达到景深效果。Qualcomm使用的技术是通过深度缓冲来区分清晰的图像和经过模糊的图像,并将它们混合达到最终效果。这种技术具有非常高效的优点,这是因为模糊运算的开销是固定的,而且还可以通过使用四分之一屏幕的尺寸的render target进行加速。




具体步骤如下:


  1. 将整个场景渲染到一个离屏render target;

  2. 选取一个小的render target,对第一步的渲染结果进行模糊处理;

  3. 利用深度缓冲,将模糊图像和清晰图像融合得到最终结果。


随深度衰减的雾


雾是当云接近地面,能见度下降时发生的自然现象。能见度的衰减程度正比于观察者到物体的距离,这种衰减效应并不是只在有雾的情况下才有。即使在晴朗的天气下,远处的物体也会呈现衰减,一个典型的例子是地平线上的山脉,虽然空气非常干净,但它其中仍然含有颗粒物,而光只有在真空中才不会衰减。




实现


与景深类似,这种特效依赖于从深度缓冲区中重建深度值。开发者并不需要通过变换矩阵的逆矩阵来得到变换之前的完整坐标,只需要它的深度值。通过深度缓存的值来计算世界坐标系里深度值的公式如下:


World Depth = (-Zn * Q) / (DepthVal-Q)

Q = Zf/ (Zf-Zn)


其中,DepthVal是深度缓冲区的值,Zn是近平面距离,Zf是远平面距离。


上帝之光


“上帝之光”是光线经过空气中的颗粒物散射而成的。它们在日出和日落,山川和云层部分遮挡太阳光的时候最常见,也可以通过人工灯光在朦胧混沌的环境中生成。散射光的计算非常复杂,但它也可以通过把光线按某种模式进行拉伸来近似模拟。这些模式可以通过深度缓冲区和一些关键信息产生,光线可以通过按照远离光源的方向对相应模式进行模糊处理来创建。


实现


这是一种基于屏幕空间创建“上帝之光”的技术,它只需要访问深度缓冲区和一些临时render target进行处理。


首先,将场景渲染到一个离屏render target,这个render target的深度缓冲区在下一阶段中被当做sampler(GLSL中的纹理采样对象)来生成遮挡物体形状。接下来缩小得到的图像并进行模糊处理,然后把像素按照光源进行radical blur(径向模糊)处理。


复古色


复古色是一种色彩空间,它用来给图像一种陈旧或古老的感觉。为了达到好的复古色效果,Qualcomm使用了1953年用来编码NTSC视频的YIQ色彩空间,这需要对RGB和YIQ颜色空间进行相互转化。

实现


可以通过下面的GLSL代码将RGB变换成Y:


vec3IntensityConverter= vec3(0.299, 0.587, 0.114);
floatY = dot(IntensityConverter, Color.xyz);


通过下面的代码变换回RGB:


[cpp] view plaincopy

  1. vec4sepiaConvert= vec4(0.191, -0.054, -0.221, 0.0);

  2. gl_FragColor= Y + sepiaConvert;



CSDN移动将持续为您优选移动开发的精华内容,共同探讨移动开发的技术热点话题,涵盖移动应用、开发工具、移动游戏及引擎、智能硬件、物联网等方方面面,如果您有想分享的技术、观点,可通过电子邮件(tangxy#csdn.net,请把#改成@)投稿。


本文为CSDN原创,点击“阅读原文”可查看全文并参与讨论。


如果您喜欢这篇文章,请点击右上角“…”将本文分享给你的朋友。