struct PushConstant {
uint32_t materialIndex;
uint32_t reflective;
};
目标:我们将从片段沿镜面反射方向投射一条光线,以查看它击中了什么,模拟反射材质(如镜子或光滑表面)。
反射的实现方式与阴影光线类似,但我们将从着色点沿镜面反射方向投射光线,并采样击中表面的颜色。
首先,我们将使用推送常量将反射材质标志传递给片段着色器。这将使我们能够确定当前材质是否具有反射性。
假设今天下了雨,桌子覆盖了一层水,因此它会反射周围环境。
我们需要更新 PushConstant 结构体,使其包含一个 reflective 标志,在渲染器中:
struct PushConstant {
uint32_t materialIndex;
uint32_t reflective;
};
以及在着色器中:
struct PushConstant {
uint materialIndex;
uint reflective;
};
[push_constant]
PushConstant pc;
并在发出绘制调用之前更新我们赋给它的值:
PushConstant pushConstant = {
.materialIndex = sub.materialID < 0 ? 0u : static_cast<uint32_t>(sub.materialID),
.reflective = sub.reflective
};
commandBuffers[frameIndex].pushConstants<PushConstant>(pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, pushConstant);
commandBuffers[frameIndex].drawIndexed(sub.indexCount, 1, sub.indexOffset, 0, 0);
然后,我们将在片段着色器中,在应用阴影效果之前,检索此标志,并调用一个辅助函数,该函数将基于反射 Ray Query 就地修改片段颜色:
float3 P = vertIn.worldPos;
float3 N = vertIn.fragNormal;
if (pc.reflective > 0) {
apply_reflection(P, N, baseColor);
}
bool inShadow = in_shadow(P);
apply_reflection() 函数的实现将与 in_shadow() 函数类似。Proceed() 循环不再是可选的,因为我们不仅需要检查是否有交点,还需要最近命中三角形的完整颜色来应用反射效果。
注意它需要法线方向(N)。这是因为反射是表面法线和视线方向 V 的函数。反射方向 R 可以使用内置的 reflect() 函数轻松计算:
void apply_reflection(float3 P, float3 N, inout float4 baseColor) {
// 构建反射光线
float3 V = normalize(ubo.cameraPos - P);
float3 R = reflect(-V, N);
然后我们定义光线描述,类似于我们对阴影所做的:
RayDesc reflectionRayDesc;
reflectionRayDesc.Origin = P;
reflectionRayDesc.Direction = R;
reflectionRayDesc.TMin = EPSILON;
reflectionRayDesc.TMax = 1e4;
并初始化 RayQuery 对象。然而,在这种情况下,我们不能使用 RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH 标志,因为我们需要检索最近三角形的完整颜色,而不仅仅是任何三角形:
// 初始化用于反射的 RayQuery
RayQuery<RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES> rq;
let rayFlags = RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES;
现在我们可以发射反射光线:
rq.TraceRayInline(accelerationStructure, rayFlags, 0xFF, reflectionRayDesc);
Proceed() 循环完全相同:
while (rq.Proceed())
{
uint instanceID = rq.CandidateRayInstanceCustomIndex();
uint primIndex = rq.CandidatePrimitiveIndex();
float2 uv = intersection_uv(instanceID, primIndex, rq.CandidateTriangleBarycentrics());
uint materialID = instanceLUTBuffer[NonUniformResourceIndex(instanceID)].materialID;
float4 intersection_color = textures[NonUniformResourceIndex(materialID)].SampleLevel(textureSampler, uv, 0);
if (intersection_color.a < 0.5) {
// 如果三角形是透明的,我们继续追踪
// 以找到下一个不透明三角形。
} else {
// 如果我们击中了不透明三角形,则停止追踪。
rq.CommitNonOpaqueTriangleHit();
}
}
bool hit = (rq.CommittedStatus() == COMMITTED_TRIANGLE_HIT);
我们唯一需要的额外逻辑是检索击中三角形的颜色并将其应用到片段的基色上。请注意,逻辑与循环中几乎相同,但这次我们使用函数的 Committed 版本,而不是 Candidate:
if (hit)
{
uint instanceID = rq.CommittedRayInstanceCustomIndex();
uint primIndex = rq.CommittedPrimitiveIndex();
float2 uv = intersection_uv(instanceID, primIndex, rq.CommittedTriangleBarycentrics());
uint materialID = instanceLUTBuffer[NonUniformResourceIndex(instanceID)].materialID;
float4 intersectionColor = textures[NonUniformResourceIndex(materialID)].SampleLevel(textureSampler, uv, 0);
baseColor.rgb = lerp(baseColor.rgb, intersectionColor.rgb, 0.7);
}
提示:作为练习,你可以扩展此函数,在光线未击中场景中任何几何体且没有提交三角形命中时采样天空盒。
使用以下设置重新构建并运行:
#define LAB_TASK_LEVEL 11
完成所有这些设置后,你现在应该能在桌子上看到一些闪亮的反射:
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。