摄像机

在光线生成着色器中,我们需要根据摄像机所处的坐标和朝向来发出射线。在光栅化中,我们会计算出观察矩阵和投影矩阵,这两个矩阵也可以用于推算出射线信息,就不再需要存储额外的相机信息。

首先计算出屏幕的像素坐标相关信息:

1
2
3
4
5
6
// raygen.hlsl

// Calculate pixel information
float2 pixelCenter = DispatchRaysIndex().xy + (float2) 0.5f;
float2 screenPos = pixelCenter / DispatchRaysDimensions().xy;
screenPos = screenPos * 2.0f - 1.0f;

根据求得的像素坐标,应用观察矩阵和投影矩阵的逆即可获得射线的原点和方向,这个过程实际上是在将视锥体逆推回世界空间中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// raygen.hlsl

#include "raycommon.hlsl"

struct PassConstants {
float4x4 viewMatrix;
float4x4 projMatrix;
float4x4 invViewMatrix;
float4x4 invProjMatrix;
float tMin;
float tMax;
...
};

RaytracingAccelerationStructure rs : register(t0);
RWTexture2D<float4> renderTarget : register(u1);

ConstantBuffer<PassConstants> passConstants : register(b0, space1);

[shader("raygeneration")]
void main() {
// Calculate pixel information
...

// Calculate ray information
float4 rayOrigin = mul(passConstants.invViewMatrix, float4(0.0f, 0.0f, 0.0f, 1.0f));
float4 rayTarget = mul(passConstants.invProjMatrix, float4(screenPos.x, screenPos.y, 1.0f, 1.0f));
float4 rayDirection = mul(passConstants.invViewMatrix, float4(normalize(rayTarget.xyz), 0.0f));

// Trace ray
RayDesc rayDesc = {
rayOrigin, passConstants.tMin, rayDirection, passConstants.tMax
};
Payload rayPayload;
TraceRay(rs, 0x0u, 0xffu, 0u, 1u, 0u, rayDesc, rayPayload);

renderTarget[DispatchRaysIndex().xy] = rayPayload.color;
}

我们的光线负载结构体定义在 raycommon.hlsl 中,所有光线追踪着色器都需要包含这个头文件:

1
2
3
4
5
// raycommon.hlsl

struct Payload {
float4 color;
};

顶点属性插值

加速结构对于我们来说是一个黑盒,它的唯一作用就是让 GPU 去自动执行相交检测,我们无法从加速结构中得到想要的数据,所以无论是顶点缓冲、索引缓冲还是物体常量,都需要将其当成由数组构成的 Uniform Buffer,通过描述符在着色器中引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct VertexConstants {
float3 localPos;
float3 normal;
float3 tangent;
float2 texCoord;
};

struct ObjectContants {
float4x4 worldMatrix;
uint vertexOffset;
uint indexOffset;
}

cbuffer VertexBuffer : register(b0, space2) {
VertexConstants vertexConstantsArray[];
};

cbuffer IndexBuffer : register(b1, space2) {
uint indices[];
};

cbuffer ObjectBuffer : register(b2, space2) {
ObjectContants objectConstantsArray[];
};

HLSL 提供了以下函数,以便查询到当前所命中几何体的实例索引和图元索引:

  1. InstanceIndex:当前实例在顶层加速结构中自动生成的索引。
  2. InstanceID:用户在创建顶层加速结构时,为所引用的底层加速结构提供的标识,对应于VkAccelerationStructureInstanceKHR中的instanceCustomIndex
  3. PrimitiveIndex:底层加速结构实例内部几何体中图元自动生成的索引。

我们首先通过InstanceIndexInstanceID访问当前物体常量数据:

1
ObjectConstants objectConstants = objectConstantsArray[InstanceID()];

接着根据实例对应的顶点和索引偏移以及PrimitiveIndex,访问当前图元的顶点与索引数据:

1
2
3
4
5
6
7
8
9
// Indices of the triangle
uint3 triangleIndices = uint3(indices[objectConstants.indexOffset + PrimitiveIndex() * 3],
indices[objectConstants.indexOffset + PrimitiveIndex() * 3 + 1],
indices[objectConstants.indexOffset + PrimitiveIndex() * 3 + 2]);

// Vertices of the triangle
VertexConstants v0 = vertexConstantsArray[objectConstants.vertexOffset + triangleIndices.x];
VertexConstants v1 = vertexConstantsArray[objectConstants.vertexOffset + triangleIndices.y];
VertexConstants v2 = vertexConstantsArray[objectConstants.vertexOffset + triangleIndices.z];

和我们在顶点着色器里所做的一样,我们需要将顶点的坐标、法线、切线从局部坐标系变换到世界坐标系中:

1
2
3
4
5
6
7
8
9
10
11
float3 worldPos0 = mul(objectConstants.worldMatrix, float4(v0.localPos, 1.0f)).xyz;
float3 worldPos1 = mul(objectConstants.worldMatrix, float4(v0.localPos, 1.0f)).xyz;
float3 worldPos2 = mul(objectConstants.worldMatrix, float4(v0.localPos, 1.0f)).xyz;

float3 normal0 = mul(objectConstants.worldMatrix, float4(v0.normal, 0.0f)).xyz;
float3 normal1 = mul(objectConstants.worldMatrix, float4(v0.normal, 0.0f)).xyz;
float3 normal2 = mul(objectConstants.worldMatrix, float4(v0.normal, 0.0f)).xyz;

float3 tangent0 = mul(objectConstants.worldMatrix, float4(v0.tangent, 0.0f)).xyz;
float3 tangent1 = mul(objectConstants.worldMatrix, float4(v0.tangent, 0.0f)).xyz;
float3 tangent2 = mul(objectConstants.worldMatrix, float4(v0.tangent, 0.0f)).xyz;

接下来我们要面对的问题便是是顶点属性插值,和光栅化管线不同,光线追踪管线很明显不会帮我们自动进行插值。对于三角形图元而言,我们可以在最近命中着色器中得到命中点的三角形重心坐标,使用重心坐标可以简单地完成插值操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// closesthit.hlsl

struct Attribute {
float2 bary;
};

[shader("closesthit")]
void main(inout Payload output, in Attribute attribute) {
...

// Interpolate
const float3 bary = float3(1.0f - attribute.bary.x - attribute.bary.y, attribute.bary.x, attribute.bary.y);

float3 worldPos = bary.x * worldPos0 + bary.y * worldPos1 + bary.z * worldPos2;
float3 normal = bary.x * normal0 + bary.y * normal1 + bary.z * normal2;
float3 tangent = bary.x * tangent0 + bary.y * tangent1 + bary.z * tangent2;
float2 texCoord = bary.x * v0.texCoord + bary.y * v1.texCoord + bary.z * v2.texCoord;

...
}

材质

一般我们会通过一系列材质常量和纹理贴图来定义一个材质,

散射

散射要求我们在光线与几何体相交后,发射出次生光线,

蒙特卡洛积分

降噪