电子说
当前的阴影技术
在过去十年中,实时渲染阴影的流行方法是使用阴影贴图。这是场景从光源视角再次渲染到离屏深度缓冲区(称为阴影贴图)的地方,然后在着色过程中对阴影贴图进行采样,以使用深度比较计算可见性。虽然这种方法已经成功地应用于许多应用中,但它也有一些缺点。
最常见的问题是阴影锯齿——这是在阴影贴图的分辨率过低的地方,导致出现块状阴影。虽然可以通过使用更高分辨率的阴影贴图来解决,但会增加内存占用和带宽利用率,可能会对性能产生负面影响,尤其是在移动设备上。即使使用更高分辨率的阴影贴图,某些微观细节也很难保留,这要后续的屏幕空间阴影通道来细化。但是,在使用光线追踪时,可以在屏幕上为每个像素分配一条光线,这将产生像素完美的硬阴影。
光线追踪管线
光线生成
当在命令缓冲区调用 vkCmdTraceRaysKHR ,将为当前绑定的光线追踪流水线调用用户定义的光线生成着色器。追踪光线的命令允许开发人员为调度的线程设置各种参数。我们的演示是完全光线追踪的,这意味着最好为屏幕上的每个像素分配一个光线生成着色器线程。
每次调用光线生成着色器都必须指定将主光线发射到场景中所需的变量。光线需要有原点(视点)和行进方向。可以通过将逆视图矩阵应用于(0,0,0,1)来计算原点。要计算方向,需要当前像素的屏幕空间位置。可以使用 gl_LaunchIDEXT 从光线生成着色器查询分派坐标。使用此内置扩展,屏幕空间坐标和光线方向可以按如下方式计算:
const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
const vec2 inUV = pixelCenter / vec2(gl_LaunchSizeEXT.xy);
vec2 screenspace = inUV * 2.0 - 1.0;
vec4 target = mInvProjectionMatrix * vec4(screenspace.xy, 1, 1);
vec4 direction = mInvViewMatrix * vec4(normalize(target.xyz), 0);
从这里,我们可以使用 traceRayEXT 函数将主光线发射到场景中。然后,它将遍历加速结构,在该结构中,它将命中或错过场景中的几何体,并调用相应着色器组。执行的着色器组将取决于其命中内容,并将像素颜色存储在有效负载结构中。“未命中”着色器只是将光线的颜色设置为硬编码的清除颜色。
命中组着色器
一旦光线与场景中的对象发生碰撞,就会执行命中着色器。顶点缓冲区、索引缓冲区和材质等模型数据都附着到“命中组”着色器。光线追踪扩展允许我们获取命中对象的实例ID。在本演示中,每个模型都是唯一的,因此实例ID直接对应于模型ID。模型ID可用于查找上述缓冲区。
// Since each object is unique in this scene, instance ID is enough to identify which buffers to look up
uint objID = gl_InstanceID;
// indices of the triangle we hit
ivec3 ind = ivec3(indices[nonuniformEXT(objID)].i[3 * gl_PrimitiveID + 0], //
indices[nonuniformEXT(objID)].i[3 * gl_PrimitiveID + 1], //
indices[nonuniformEXT(objID)].i[3 * gl_PrimitiveID + 2]); //
// Vertices of the hit triangle
Vertex v0 = vertices[nonuniformEXT(objID)].v[ind.x];
Vertex v1 = vertices[nonuniformEXT(objID)].v[ind.y];
Vertex v2 = vertices[nonuniformEXT(objID)].v[ind.z];
gl_PrimitiveID 可以用来告诉我们使用哪些索引来查找命中的顶点,然后使用重心插值系数在它们之间进行插值,该插值系数来自命中着色器中声明为 hitAttributeEXT 类型的全局变量。然后,我们使用世界矩阵将插值顶点值转换到世界空间,并旋转法线值。
// Get the interpolation coefficients
const vec3 barycentrics = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y);
// Interpolate the position and normal vector for this ray
vec4 modelNormal = vec4(v0.nrm * barycentrics.x + v1.nrm * barycentrics.y + v2.nrm * barycentrics.z, 1.0);
vec4 modelPos = vec4(v0.pos * barycentrics.x + v1.pos * barycentrics.y + v2.pos * barycentrics.z, 1.0);
// Transform the position and normal vectors from model space to world space
mat4 worldTransform = transforms[nonuniformEXT(objID)];
vec3 worldPos = (worldTransform * modelPos).xyz;
// Don't translate the normal vector, only rotate and scale
mat3 worldRotate = mat3(worldTransform[0].xyz, worldTransform[1].xyz, worldTransform[2].xyz);
vec3 worldNormal = worldRotate * modelNormal.xyz;
使用击中点的法线和世界位置,我们可以计算光线相对于场景中静态光源的Phong照明分量。然后,我们使用一个新的命中和未命中组从该点向光源发射另一条光线。我们可以将次光线的最大长度设置为击中点和光源之间的距离。如果光线在这个距离内与加速度结构中的任何物体碰撞,我们可以得出结论,在碰撞点和光源之间有一个物体,因此该点处于阴影中。如果次光线未击中距离集内的任何对象,则会执行“未命中”着色器,我们可以假定该点不在阴影中。
在这个图中,我们可以看到两个例子。光线从视口(1)发射,第一条光线击中点3,第二条光线发射,在到达光源的途中不会击中任何几何体。视点的第二条光线在点4处照射,但在到达光源的过程中,在点5处再次发生碰撞。因此,我们可以得出结论,点4在阴影中,但点3不在阴影中。
优化
虽然光线追踪阴影提供了比传统方法更高的逼真度,但它们仍然不完全完美。因此,就实时计算而言,光线追踪算法需要更多的硬件,这是一个明显的缺点。除此之外,还可以采取一些不同的优化措施来改进本文概述的技术。
阴影检查
我们可以减少第一组用来检查硬阴影的二次光线的数量;这是通过首先检查已计算的Phong光照分量来完成的。如果由于曲面背向光源,光照分量已为0,则检查硬阴影没有意义,因为该点已处于黑暗中。
这将光线预算从屏幕上像素数的大约1.8倍减少到大约1.5倍。这显然取决于场景和其中的对象,因为它随未命中场景的主光线的比例以及通过或未通过阴影检查的主光线的比例而变化。
混合渲染一般来说,光线追踪核心将比传统的光栅化流水线慢(至少目前是这样)。有几个可能的原因,但主要的原因是光线追踪硬件仍然相对较新,因此与光栅化相比,GPU仍然没有为其投入更多的空间。这意味着可以计算一个标准的G缓冲区,并使用位置附件来定位从哪个位置发射阴影检查光线。G缓冲区在《Vulkan中的环境遮挡》中有介绍,所以如果你还没有看到,一定要看一看。简而言之,G缓冲区可以替代主光线,从而产生更好的任务重叠和更小的光线预算。
结束
虽然完全光线追踪的硬阴影在写这篇博文的时候可能不是最佳解决方案,但它们仍然提供了传统流水线难以模拟的细节和准确性。一如既往,我们强烈建议大家看看PowerVR SDK及其代码示例,以了解我们如何实现这些技术和实现这些算法的确切机制。我们也总是通过支持门户或论坛发送电子邮件。
如果您有兴趣了解更多关于各种图形技术的信息,请查看我们的文档网站,或者在SDK Github中探索我们的其他代码示例。
原文标题:Vulkan完全光线追踪硬阴影
文章出处:【微信公众号:Imagination Tech】欢迎添加关注!文章转载请注明出处。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !