一、光线

回顾上一篇,我们配置好了项目,接下来我们探究射线:

如果\overrightarrow{O}为原点,\overrightarrow{D}为方向,t为时间,\overrightarrow{P}为射线上一点,则有

\overrightarrow{P}=\overrightarrow{O}+t\overrightarrow{D},

其中t\in (0,+\infty ),\overrightarrow{D}是单位向量,

这样t就是射线传播的长度。

我们在RayTracing.compute中写这个结构体和它的构造函数:

struct Ray
{
    float3 O, D;
};
Ray newRay(float3 o, float3 d)
{
    Ray r;
    r.O = o;
    r.D = d;
    return r;
}

我们要让它创建相机光线,所以在RTRP.cs每个相机的循环中,asset.RT调度前写:

asset.RT.SetMatrix("CTW", cam.cameraToWorldMatrix);
asset.RT.SetMatrix("CIP", cam.projectionMatrix.inverse);

在RTRP.cs每个相机的循环前写:

asset.RT.SetVector("Screen", new Vector2(Screen.width, Screen.height));

在RayTracing.compute开头写:

float4x4 CTW, CIP;
float2 Screen;

在RayTracing.compute中间写:

Ray CamRay(float2 uv)
{
    float3 o = mul(CTW, float4(0.0, 0.0, 0.0, 1.0)).xyz;
    float3 d = mul(CIP, float4(uv * 2.0 - 1.0, 0.0, 1.0)).xyz;
    d = mul(CTW, float4(d, 0.0)).xyz;
    d = normalize(d);
    return newRay(o, d);
}

将RayTracing.compute的核函数Main内容改为:

Ray r = CamRay(id.xy / Screen);
tex[id.xy] = float4(r.D * 0.5 + 0.5, 0.0);

懵逼了吧?你先别懵逼,我们慢慢说起。

变量Screen是渲染分辨率,不用每个相机设置一遍,所以在相机循环前写;

函数CamRay将被用于生成相机光线(我们探究反向光线追踪,从相机找光源),

图片中大写O是世界坐标原点,小写o由第一行算出,第二行算出在相机坐标系下的\overrightarrow{D}(uv*2.0-1.0是将屏幕uv转换到CIP矩阵使用的uv),第三行将\overrightarrow{D}从相机坐标系变换的世界坐标系,第四行将\overrightarrow{D}归一化;

核函数中r.D*0.5+0.5是因为方向的每个分量属于[-1, 1]经过这个计算变换入[0, 1]。

最后你会看到:

二、追踪

打开RayTracing.compute写入:

struct Hit
{
    float3 P, N;
    float2 UV;
    float t;
};
Hit newHit()
{
    Hit h;
    h.P = 0.0;
    h.N = 0.0;
    h.UV = 0.0;
    h.t = 1.#INF;
    return h;
}
bool IntersectTriangle_MT97(Ray r, float3 vert0, float3 vert1, float3 vert2, inout float t, inout float u, inout float v)
{
    float3 edge1 = vert1 - vert0;
    float3 edge2 = vert2 - vert0;
    float3 pvec = cross(r.D, edge2);
    float det = dot(edge1, pvec);
    if (det < 1e-8)
        return false;
    float inv_det = 1.0f / det;
    float3 tvec = r.O - vert0;
    u = dot(tvec, pvec) * inv_det;
    if (u < 0.0 || u > 1.0f)
        return false;
    float3 qvec = cross(tvec, edge1);
    v = dot(r.D, qvec) * inv_det;
    if (v < 0.0 || u + v > 1.0f)
        return false;
    t = dot(edge2, qvec) * inv_det;
    return true;
}
void IntersectTriangle(Ray r, float3 v0, float3 v1, float3 v2, inout Hit h)
{
    float t, u, v;
    if (IntersectTriangle_MT97(r, v0, v1, v2, t, u, v))
    {
        if (t > 0.0 && t < h.t)
        {
            h.t = t;
            h.P = r.O + t * r.D;
            h.N = normalize(cross(v1 - v0, v2 - v0));
            h.UV = float2(u, v);
        }
    }
}
Hit Trace(Ray r)
{
    Hit h = newHit();
    IntersectTriangle(r, float3(0.0, 0.0, 0.0), float3(2.0, 2.0, 2.0), float3(5.0, 0.0, 0.0), h);
    return h;
}

然后将核函数改为:

Ray r = CamRay(id.xy / Screen);
Hit h = Trace(r);
tex[id.xy] = float4(h.N * 0.5 + 0.5, 0.0);

上面代码中,我们定义了结构体Hit以储存相交结果,P是交点坐标,N是交点法线,UV是交点几何UV,t是光线时间,或是说距离,为了方便,Hit的构造函数中t是无穷大,最后你会看到:

三、抗锯齿

如果放大图像,会发现有很明显的锯齿,为了减少锯齿,我们使用超采样抗锯齿。

我们在RTRPA.cs和RayTracing.compute内定义SSC(SuperSamplingCount,可以看出我为少写几个字使用缩写)

我们在每个像素内随即确定一个点来实现单次采样,因此我们要先写随机数生成器:

float seed, pix_x, pix_y, pix_z;
float rand()
{
    seed = frac(cos(dot(float3(pix_x, pix_y, pix_z), float3(0.954731, 0.57743, 0.6432)) + seed) * 43758.5453);
    return seed;
}
float2 randBox()
{
    return float2(rand(), rand());
}

上面四个浮点数是生成随机数的参数,seed由RTRP.cs传入随机非负浮点数,其中float3(0.954731, 0.57743, 0.6432)每个分量小于1否则随机值转颜色后有明显的条纹,影响效果,pix_x、pix_y、pix_z由核函数Main设置:

float3 col = 0.0;
pix_x = id.x;
pix_y = id.y;
for (int a = 0; a < SSC; a++)
{
    pix_z = a;
    Ray r = CamRay((id.xy + randBox()) / Screen);
    Hit h = Trace(r);
    col += h.N * 0.5 + 0.5;
}
tex[id.xy] = float4(col / SSC, 0.0);

每次采样得到的颜色加给col,最后col / SSC就是求了几次采样的平均值,最后你会看到:

更多推荐