Трассировка лучей: что, как и почем

Рендеринг наших первых сфер

противоположномза камерой

За камерой
Между камерой и плоскостью проекции
Сцена

передзазабудем

O = <0, 0, 0>
for x in [-Cw/2, Cw/2] {
    for y in [-Ch/2, Ch/2] {
        D = CanvasToViewport(x, y)
        color = TraceRay(O, D, 1, inf)
        canvas.PutPixel(x, y, color)
    }
}
CanvasToViewport(x, y) {
    return (x*Vw/Cw, y*Vh/Ch, d)
}
TraceRay(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    if closest_sphere == NULL
        return BACKGROUND_COLOR
    return closest_sphere.color
}

какой-то

IntersectRaySphere(O, D, sphere) {
    C = sphere.center
    r = sphere.radius
    oc = O - C

    k1 = dot(D, D)
    k2 = 2*dot(OC, D)
    k3 = dot(OC, OC) - r*r

    discriminant = k2*k2 - 4*k1*k3
    if discriminant < 0:
        return inf, inf

    t1 = (-k2 + sqrt(discriminant)) / (2*k1)
    t2 = (-k2 - sqrt(discriminant)) / (2*k1)
    return t1, t2
}
viewport_size = 1 x 1
projection_plane_d = 1
sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
}
sphere {
    center = (2, 0, 4)
    radius = 1
    color = (0, 0, 255)  # Синий
}
sphere {
    center = (-2, 0, 4)
    radius = 1
    color = (0, 255, 0)  # Зелёный
}

Исходный код и рабочее демо >>

Рендеринг с тенями

ClosestIntersection(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    return closest_sphere, closest_t
}
TraceRay(O, D, t_min, t_max) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

    if closest_sphere == NULL
        return BACKGROUND_COLOR

    P = O + closest_t*D  # Compute intersection
    N = P - closest_sphere.center  # Compute sphere normal at intersection
    N = N / length(N)
    return closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)
}
ComputeLighting(P, N, V, s) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point {
                L = light.position - P
                t_max = 1
            } else {
                L = light.direction
                t_max = inf
            }

            # Проверка тени
            shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max)
            if shadow_sphere != NULL
                continue

            # Диффузность
            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))

            # Зеркальность
            if s != -1 {
                R = 2*N*dot(N, L) - L
                r_dot_v = dot(R, V)
                if r_dot_v > 0
                    i += light.intensity*pow(r_dot_v/(length(R)*length(V)), s)
            }
        }
    }
    return i
}

Исходный код и рабочее демо >>Теперь

Производительность

Трассировка лучей: что, как и почем

Даже не вдаваясь в цифры и графики становится видно, как эти завораживающие визуальные эффекты печальнейшим образом сказываются на производительности ПК. Частота смены кадров падает просто катастрофически.

К примеру, Metro Exodus, запущенная в разрешении 3440х1440 с «экстремальными» настройками графики, выдает на RTX 2080 Ti в среднем 41 кадр в секунду. Однако, стоит только включить трассировку лучей, как fps падает до среднего значения в 23 кадра. Разумеется, играть можно, но… вы понимаете. К счастью, эта игра поддерживает технологию DLSS, использующую интеллектуальный апскейл разрешения. С ней вы сможете выжать из игры больше: к примеру, во время теста Metro Exodus с «экстремальной» графикой и «ультра» рейтрейсингом показала в среднем 44 кадра в секунду.

Следовательно, в погоне за трассировкой лучей вам придется быть более консервативным в выборе разрешения, ведь для более или менее комфортной игры в 4K вам определенно не обойтись без RTX 2080 Ti. Для привычного же 1080р с рейтрейсингом вполне будет достаточно RTX 2060.

Суперсэмплинг

квадрата

Псевдокод трассировщика лучей

CanvasToViewport(x, y) {
    return (x*Vw/Cw, y*Vh/Ch, d)
}


ReflectRay(R, N) {
    return 2*N*dot(N, R) - R;
}


ComputeLighting(P, N, V, s) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point {
                L = light.position - P
                t_max = 1
            } else {
                L = light.direction
                t_max = inf
            }

            # Проверка теней
            shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max)
            if shadow_sphere != NULL
                continue

            # Диффузность
            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))

            # Блеск
            if s != -1 {
                R = ReflectRay(L, N)
                r_dot_v = dot(R, V)
                if r_dot_v > 0
                    i += light.intensity*pow(r_dot_v/(length(R)*length(V)), s)
            }
        }
    }
    return i
}


ClosestIntersection(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    return closest_sphere, closest_t
}


TraceRay(O, D, t_min, t_max, depth) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

    if closest_sphere == NULL
        return BACKGROUND_COLOR

    # Вычисление локального цвета
    P = O + closest_t*D  # Вычисление точки пересечения
    N = P - closest_sphere.center  # Вычисление нормали сферы в точке пересечения
    N = N / length(N)
    local_color = closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)

    # Если мы достигли предела рекурсии или объект не отражающий, то мы закончили
    r = closest_sphere.reflective
    if depth <= 0 or r <= 0:
        return local_color

    # Вычисление отражённого цвета
    R = ReflectRay(-D, N)
    reflected_color = TraceRay(P, R, 0.001, inf, depth - 1)

    return local_color*(1 - r) + reflected_color*r
}


for x in [-Cw/2, Cw/2] {
    for y in [-Ch/2, Ch/2] {
        D = camera.rotation * CanvasToViewport(x, y)
        color = TraceRay(camera.position, D, 1, inf)
        canvas.PutPixel(x, y, color)
    }
}
viewport_size = 1 x 1
projection_plane_d = 1

sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
    specular = 500  # Блестящий
    reflective = 0.2  # Немного отражающий
}
sphere {
    center = (-2, 1, 3)
    radius = 1
    color = (0, 0, 255)  # Синий
    specular = 500  # Блестящий
    reflective = 0.3  # Немного более отражающий
}
sphere {
    center = (2, 1, 3)
    radius = 1
    color = (0, 255, 0)  # Зелёный
    specular = 10  # Немного блестящий
    reflective = 0.4  # Ещё более отражающий
}
sphere {
    color = (255, 255, 0)  # Жёлтый
    center = (0, -5001, 0)
    radius = 5000
    specular = 1000  # Очень блестящий
    reflective = 0.5  # Наполовину отражающий
}


light {
    type = ambient
    intensity = 0.2
}
light {
    type = point
    intensity = 0.6
    position = (2, 1, 0)
}
light {
    type = directional
    intensity = 0.2
    direction = (1, 4, 4)
}

Рендеринг с диффузным отражением

light {
    type = ambient
    intensity = 0.2
}
light {
    type = point
    intensity = 0.6
    position = (2, 1, 0)
}
light {
    type = directional
    intensity = 0.2
    direction = (1, 4, 4)
}
ComputeLighting(P, N) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point
                L = light.position - P
            else
                L = light.direction

            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))
        }
    }
    return i
}
    return closest_sphere.color
    P = O + closest_t*D  # вычисление пересечения
    N = P - closest_sphere.center  # вычисление нормали сферы в точке пересечения
    N = N / length(N)
    return closest_sphere.color*ComputeLighting(P, N)
sphere {
    color = (255, 255, 0)  # Yellow
    center = (0, -5001, 0)
    radius = 5000
}

Исходный код и рабочее демо >>

Что такое трассировка лучей?

Это метод рендеринга изображения, позволяющий создавать потрясающе реалистичные световые эффекты. По сути, алгоритм отслеживает путь каждого луча от источника и симулирует способ, с которым свет взаимодействует с каждым виртуальным объектом на своем пути.

В последние годы мы наблюдали, как внутриигровое освещение становится все более и более правдоподобным, но преимущества новой технологии касаются больше не самого света, а того, каким образом он применяется в игровом мире.

Благодаря трассировке мы с вами сможем увидеть гораздо более реалистичные тени и отражения, а также продвинутые эффекты просвечивания и рассеивания света

Алгоритм принимает во внимание то, каким образом лучи падают на объект, просчитывает их взаимодействие и выдает изображение, подобное тому, что увидел бы человеческий глаз в схожих условиях в реальном мире. При этом влияние на результат оказывает множество деталей, включая даже видимые в сцене цвета

При наличии достаточной вычислительной мощности можно создавать невероятно реалистичные изображения, практически неотличимые от реальности. Проблема заключается лишь в том, что подобной производительностью на сегодняшний день могут похвастать лишь дорогие ПК, а никак не консоли нынешнего поколения.

При создании визуальных эффектов для кинофильмов и сериалов трассировка лучей используется в несравнимо большей степени, и причина у этого все та же: для такой работы студии могут позволить себе целые серверные фермы. И даже в таком случае применение технологии остается длительным и кропотливым процессом, не говоря уже о заоблачной стоимости подобного оборудования.

В видеоиграх же традиционно применяется растеризация (или растрирование, если угодно), как гораздо более быстрый способ рендеринга. Это процесс превращения трехмерной графики в двухмерные пиксели на вашем экране, для отображения реалистичного освещения использующий программы-шейдеры. К счастью, появляется все больше игр с поддержкой специального железа от Nvidia, способного управиться с применением трассировки в реальном времени. Да, растрирование по-прежнему преобладает в игрострое, но это будет продолжаться до тех пор, пока разработчики, следуя за распространением RTX видеокарт, не станут внедрять в свои проекты освещение на основе рейтрейсинга.

И, если прямо сейчас мы с вами не наблюдаем чего-то крышесносящего, фундамент для появления действительно потрясающей картинки уже заложен. С учетом того факта, что в консолях нового поколения уже заявлена поддержка трассировки, нас ждет революция в гейминге.

Рендеринг с диффузным отражением

light {
    type = ambient
    intensity = 0.2
}
light {
    type = point
    intensity = 0.6
    position = (2, 1, 0)
}
light {
    type = directional
    intensity = 0.2
    direction = (1, 4, 4)
}
ComputeLighting(P, N) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point
                L = light.position - P
            else
                L = light.direction

            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))
        }
    }
    return i
}
    return closest_sphere.color
    P = O + closest_t*D  # вычисление пересечения
    N = P - closest_sphere.center  # вычисление нормали сферы в точке пересечения
    N = N / length(N)
    return closest_sphere.color*ComputeLighting(P, N)
sphere {
    color = (255, 255, 0)  # Yellow
    center = (0, -5001, 0)
    radius = 5000
}

Исходный код и рабочее демо >>

Техника

Трассировка лучей: что, как и почем
Трассировка луча света, проходящего через среду с изменяющимся показателем преломления . Луч продвигается немного вперед, а затем направление пересчитывается.

Трассировка лучей работает, предполагая, что частица или волна могут быть смоделированы как большое количество очень узких лучей ( лучей ) и что существует некоторое расстояние, возможно, очень маленькое, на котором такой луч локально прямой. Трассировщик лучей продвигает луч на это расстояние, а затем использует локальную производную среды для вычисления нового направления луча. Из этого места отправляется новый луч, и процесс повторяется до тех пор, пока не будет сгенерирован полный путь. Если симуляция включает твердые объекты, луч может проверяться на пересечение с ними на каждом шаге, изменяя направление луча, если обнаружено столкновение. Другие свойства луча также могут быть изменены по мере продвижения моделирования, такие как интенсивность , длина волны или поляризация . Этот процесс повторяется с таким количеством лучей, которое необходимо для понимания поведения системы.

Рендеринг с «зеркальными» отражениями

sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
    specular = 500  # Блестящий
}
sphere {
    center = (-2, 1, 3)
    radius = 1
    color = (0, 0, 255)  # Синий
    specular = 500  # Блестящий
}
sphere {
    center = (2, 1, 3)
    radius = 1
    color = (0, 255, 0)  # Зелёный
    specular = 10  # Немного блестящий
}
sphere {
    color = (255, 255, 0)  # Жёлтый
    center = (0, -5001, 0)
    radius = 5000
    specular = 1000  # Очень блестящий
}
ComputeLighting(P, N, V, s) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point
                L = light.position - P
            else
                L = light.direction

            # Диффузность
            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))

            # Зеркальность
            if s != -1 {
                R = 2*N*dot(N, L) - L
                r_dot_v = dot(R, V)
                if r_dot_v > 0
                    i += light.intensity*pow(r_dot_v/(length(R)*length(V)), s)
            }
        }
    }
    return i
}
TraceRay(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    if closest_sphere == NULL
        return BACKGROUND_COLOR

    P = O + closest_t*D  # Вычисление пересечения
    N = P - closest_sphere.center  # Вычисление нормали сферы в точке пересечения
    N = N / length(N)
    return closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)
}

Исходный код и рабочее демо >>

Тени

должныесли между точкой и источником есть объект, то не нужно добавлять освещение, поступающее от этого источниказа

Рендеринг с «зеркальными» отражениями

sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
    specular = 500  # Блестящий
}
sphere {
    center = (-2, 1, 3)
    radius = 1
    color = (0, 0, 255)  # Синий
    specular = 500  # Блестящий
}
sphere {
    center = (2, 1, 3)
    radius = 1
    color = (0, 255, 0)  # Зелёный
    specular = 10  # Немного блестящий
}
sphere {
    color = (255, 255, 0)  # Жёлтый
    center = (0, -5001, 0)
    radius = 5000
    specular = 1000  # Очень блестящий
}
ComputeLighting(P, N, V, s) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point
                L = light.position - P
            else
                L = light.direction

            # Диффузность
            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))

            # Зеркальность
            if s != -1 {
                R = 2*N*dot(N, L) - L
                r_dot_v = dot(R, V)
                if r_dot_v > 0
                    i += light.intensity*pow(r_dot_v/(length(R)*length(V)), s)
            }
        }
    }
    return i
}
TraceRay(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    if closest_sphere == NULL
        return BACKGROUND_COLOR

    P = O + closest_t*D  # Вычисление пересечения
    N = P - closest_sphere.center  # Вычисление нормали сферы в точке пересечения
    N = N / length(N)
    return closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)
}

Исходный код и рабочее демо >>

Тени

должныесли между точкой и источником есть объект, то не нужно добавлять освещение, поступающее от этого источниказа

Суперсэмплинг

квадрата

Псевдокод трассировщика лучей

CanvasToViewport(x, y) {
    return (x*Vw/Cw, y*Vh/Ch, d)
}


ReflectRay(R, N) {
    return 2*N*dot(N, R) - R;
}


ComputeLighting(P, N, V, s) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point {
                L = light.position - P
                t_max = 1
            } else {
                L = light.direction
                t_max = inf
            }

            # Проверка теней
            shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max)
            if shadow_sphere != NULL
                continue

            # Диффузность
            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))

            # Блеск
            if s != -1 {
                R = ReflectRay(L, N)
                r_dot_v = dot(R, V)
                if r_dot_v > 0
                    i += light.intensity*pow(r_dot_v/(length(R)*length(V)), s)
            }
        }
    }
    return i
}


ClosestIntersection(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    return closest_sphere, closest_t
}


TraceRay(O, D, t_min, t_max, depth) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

    if closest_sphere == NULL
        return BACKGROUND_COLOR

    # Вычисление локального цвета
    P = O + closest_t*D  # Вычисление точки пересечения
    N = P - closest_sphere.center  # Вычисление нормали сферы в точке пересечения
    N = N / length(N)
    local_color = closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)

    # Если мы достигли предела рекурсии или объект не отражающий, то мы закончили
    r = closest_sphere.reflective
    if depth <= 0 or r <= 0:
        return local_color

    # Вычисление отражённого цвета
    R = ReflectRay(-D, N)
    reflected_color = TraceRay(P, R, 0.001, inf, depth - 1)

    return local_color*(1 - r) + reflected_color*r
}


for x in [-Cw/2, Cw/2] {
    for y in [-Ch/2, Ch/2] {
        D = camera.rotation * CanvasToViewport(x, y)
        color = TraceRay(camera.position, D, 1, inf)
        canvas.PutPixel(x, y, color)
    }
}
viewport_size = 1 x 1
projection_plane_d = 1

sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
    specular = 500  # Блестящий
    reflective = 0.2  # Немного отражающий
}
sphere {
    center = (-2, 1, 3)
    radius = 1
    color = (0, 0, 255)  # Синий
    specular = 500  # Блестящий
    reflective = 0.3  # Немного более отражающий
}
sphere {
    center = (2, 1, 3)
    radius = 1
    color = (0, 255, 0)  # Зелёный
    specular = 10  # Немного блестящий
    reflective = 0.4  # Ещё более отражающий
}
sphere {
    color = (255, 255, 0)  # Жёлтый
    center = (0, -5001, 0)
    radius = 5000
    specular = 1000  # Очень блестящий
    reflective = 0.5  # Наполовину отражающий
}


light {
    type = ambient
    intensity = 0.2
}
light {
    type = point
    intensity = 0.6
    position = (2, 1, 0)
}
light {
    type = directional
    intensity = 0.2
    direction = (1, 4, 4)
}

Ветрозащитный флис

Непродуваемые флисовые куртки и пуловеры могут изготавливаться из низкоплотного, среднеплотного и плотного флиса. Материал в этих случаях обычно трёхслойный – между внешним и внутренним слоем флиса прокладывается ветрозащитная мембрана. В результате ткань заметно теряет свои воздухопроницаемые свойства, благодаря чему такую одежду хорошо использовать для малоподвижного времяпрепровождения в холодную или ветреную погоду.

Трассировка лучей: что, как и почем

На фото: Одежда из ветрозащитного флиса.

Изучая ассортимент одежды из ветрозащитного флиса, вы увидите, что продавцы и производители используют для наименования этого материала несколько различных названий, в частности: Polartec Thermal Wind Pro, Windwall, R4 или Windstopper. Часто одежда из ветрозащитного флиса делается с жёсткой лицевой поверхностью для повышения устойчивости к истиранию и дополнительной защиты от ветра.

Описание и наилучшее применение:

  • имеет меньшую воздухопроницаемость, чем обычный флис;
  • лучше всего носить в качестве одежды верхнего слоя в холодную и ветреную погоду;
  • ветрозащита встречается у курток и пуловеров из низкоплотного, среднеплотного и плотного флиса.

Наиболее популярные модели:

  • куртка с капюшоном (худи) Arc’teryx Fortrex;
  • куртка Patagonia R4.

⇡#Shadow of the Tomb Raider

Из трех игр, которые в настоящий момент используют трассировку лучей в реальном времени, Shadow of the Tomb Raider располагается в промежутке между Battlefield V и Metro Exodus по требованиям к быстродействию GPU. Графический движок выполняет Ray Tracing для рендеринга теней с тремя уровнями детализации: от среднего до ультравысокого. Самая щадящая опция (Medium) на видеокартах под маркой GeForce RTX 20 забирает от 13 до 18 % частоты смены кадров — в зависимости от разрешения экрана. Впрочем, настройка Medium в Shadow of the Tomb Raider активирует трассировку лучей только для редких точечных источников света и в большинстве игровых сцен не производит заметного эффекта. Промежуточный вариант качества теней (High) снижает быстродействие этих устройств на 37–44 %, а высший (Ultra) — на 39–48 %.

Ускорители семейства GeForce GTX 10 при среднем качестве эффектов DXR отделались снижением быстродействия в 16–24 % (единственным исключением оказался GeForce GTX 1060, который потерял уже 31 % FPS при разрешении 1440p и 2160p). Установка высокого качества трассированных теней приводит к тому, что частота смены кадров на «Паскалях» падает на 41–57 % даже при разрешении экрана ниже 4К, а в режиме 2160p потери увеличиваются до 61–68 %. И наконец, ради трассировки на уровне Ultra владельцу видеокарты серии GeForce GTX 10 придется пожертвовать 54–67 % FPS при условно низких разрешениях экрана и 67-74 % в 4К.

Перед новыми устройствами NVIDIA в средней ценовой категории — GeForce GTX 1660 и GTX 1660 Ti — Shadow of the Tomb Raider открыл возможность доказать преимущества архитектуры Turing перед чипами Pascal без помощи специализированных RT-ядер. Действительно, эти ускорители справляются с тестами в условиях гибридного рендеринга более эффективно, чем близкие по производительности модели GeForce 10-й серии. Так, потери быстродействия на GeForce GTX 1660 Ti при трех уровнях качества эффектов DXR равняются 16–21, 50–55 и 51–61 % — в зависимости от разрешения экрана. У GeForce 1070 эти показатели достигают 19–21, 53–68 и 58–72 %.

Пусть эффекты трассировки лучей на минимальном уровне качества не так сильно повышают запросы игры к быстродействию железа, как в Metro Exodus или даже в Battlefield V, активация DXR мгновенно снимает с дистанции все видеокарты прошлого поколения. Исключением стал только GeForce GTX 1080 Ti: бывший флагман смог пробить отметку 60 FPS при разрешении 1080p. В любом случае, даже GeForce GTX 1080 Ti в любых тестах с трассировкой лучей проигрывает младшей модели среди полноценных «Тьюрингов» — GeForce RTX 2060.

Трассировка лучей: что, как и почем

Shadow of the Tomb Raider, макс. Качество
1920 × 1080 SMAA 4x
RT Off RT Medium RT High RT Ultra
NVIDIA GeForce RTX 2080 Ti FE (11 Гбайт) 100% -14% -38% -40%
NVIDIA GeForce RTX 2080 FE (8 Гбайт) 100% -14% -40% -42%
NVIDIA GeForce RTX 2070 FE (8 Гбайт) 100% -13% -38% -40%
NVIDIA GeForce RTX 2060 FE (6 Гбайт) 100% -14% -40% -43%
NVIDIA GeForce GTX 1660 Ti (6 Гбайт) 100% -16% -50% -51%
NVIDIA GeForce GTX 1660 (6 Гбайт) 100% -16% -45% -50%
NVIDIA GeForce GTX 1080 Ti (11 Гбайт) 100% -16% -49% -54%
NVIDIA GeForce GTX 1080 (8 Гбайт) 100% -18% -51% -56%
NVIDIA GeForce GTX 1070 Ti (8 Гбайт) 100% -18% -51% -55%
NVIDIA GeForce GTX 1070 (8 Гбайт) 100% -19% -53% -58%
NVIDIA GeForce GTX 1060 (6 Гбайт) 100% -19% -57% -63%

Трассировка лучей: что, как и почем

Shadow of the Tomb Raider, макс. Качество
2560 × 1440 SMAA 4x
RT Off RT Medium RT High RT Ultra
NVIDIA GeForce RTX 2080 Ti FE (11 Гбайт) 100% -14% -37% -39%
NVIDIA GeForce RTX 2080 FE (8 Гбайт) 100% -15% -39% -41%
NVIDIA GeForce RTX 2070 FE (8 Гбайт) 100% -15% -39% -41%
NVIDIA GeForce RTX 2060 FE (6 Гбайт) 100% -17% -39% -43%
NVIDIA GeForce GTX 1660 Ti (6 Гбайт) 100% -19% -48% -53%
NVIDIA GeForce GTX 1660 (6 Гбайт) 100% -18% -48% -53%
NVIDIA GeForce GTX 1080 Ti (11 Гбайт) 100% -18% -52% -57%
NVIDIA GeForce GTX 1080 (8 Гбайт) 100% -19% -54% -60%
NVIDIA GeForce GTX 1070 Ti (8 Гбайт) 100% -18% -54% -60%
NVIDIA GeForce GTX 1070 (8 Гбайт) 100% -19% -57% -63%
NVIDIA GeForce GTX 1060 (6 Гбайт) 100% -31% -41% -67%

Трассировка лучей: что, как и почем

Shadow of the Tomb Raider, макс. Качество
3840 × 2160 AA Off
RT Off RT Medium RT High RT Ultra
NVIDIA GeForce RTX 2080 Ti FE (11 Гбайт) 100% -14% -37% -40%
NVIDIA GeForce RTX 2080 FE (8 Гбайт) 100% -18% -40% -43%
NVIDIA GeForce RTX 2070 FE (8 Гбайт) 100% -18% -40% -43%
NVIDIA GeForce RTX 2060 FE (6 Гбайт) 100% -22% -44% -48%
NVIDIA GeForce GTX 1660 Ti (6 Гбайт) 100% -21% -55% -61%
NVIDIA GeForce GTX 1660 (6 Гбайт) 100% -37% -59% -65%
NVIDIA GeForce GTX 1080 Ti (11 Гбайт) 100% -21% -61% -67%
NVIDIA GeForce GTX 1080 (8 Гбайт) 100% -23% -64% -70%
NVIDIA GeForce GTX 1070 Ti (8 Гбайт) 100% -23% -63% -69%
NVIDIA GeForce GTX 1070 (8 Гбайт) 100% -24% -66% -72%
NVIDIA GeForce GTX 1060 (6 Гбайт) 100% -31% -68% -74%

Рендеринг с отражением

sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
    specular = 500  # Блестящий
    reflective = 0.2  # Немного отражающий
}
sphere {
    center = (-2, 1, 3)
    radius = 1
    color = (0, 0, 255)  # Синий
    specular = 500  # Блестящий
    reflective = 0.3  # Немного более отражающий
}
sphere {
    center = (2, 1, 3)
    radius = 1
    color = (0, 255, 0)  # Зелёный
    specular = 10  # Немного блестящий
    reflective = 0.4  # Ещё более отражающий
}
sphere {
    color = (255, 255, 0)  # Жёлтый
    center = (0, -5001, 0)
    radius = 5000
    specular = 1000  # Очень блестящий
    reflective = 0.5  # Наполовину отражающий
}
ReflectRay(R, N) {
    return 2*N*dot(N, R) - R;
}
        color = TraceRay(O, D, 1, inf, recursion_depth)
TraceRay(O, D, t_min, t_max, depth) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

    if closest_sphere == NULL
        return BACKGROUND_COLOR

    # Вычисление локального цвета
    P = O + closest_t*D  # Вычисление точки пересечения
    N = P - closest_sphere.center  # Вычисление нормали к сфере в точке пересечения
    N = N / length(N)
    local_color = closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)

    # Если мы достигли предела рекурсии или объект не отражающий, то мы закончили
    r = closest_sphere.reflective
    if depth <= 0 or r <= 0:
        return local_color

    # Вычисление отражённого цвета
    R = ReflectRay(-D, N)
    reflected_color = TraceRay(P, R, 0.001, inf, depth - 1)

    return local_color*(1 - r) + reflected_color*r
}

Исходный код и рабочее демо >>

Произвольная камера

единственноеположениянаправлениеПоложение

for x in [-Cw/2, Cw/2] {
    for y in [-Ch/2, Ch/2] {
        D = camera.rotation * CanvasToViewport(x, y)
        color = TraceRay(camera.position, D, 1, inf)
        canvas.PutPixel(x, y, color)
    }
}

Исходный код и рабочее демо >>

Техника

Трассировка лучей: что, как и почем
Трассировка луча света, проходящего через среду с изменяющимся показателем преломления . Луч продвигается немного вперед, а затем направление пересчитывается.

Трассировка лучей работает, предполагая, что частица или волна могут быть смоделированы как большое количество очень узких лучей ( лучей ) и что существует некоторое расстояние, возможно, очень маленькое, на котором такой луч локально прямой. Трассировщик лучей продвигает луч на это расстояние, а затем использует локальную производную среды для вычисления нового направления луча. Из этого места отправляется новый луч, и процесс повторяется до тех пор, пока не будет сгенерирован полный путь. Если симуляция включает твердые объекты, луч может проверяться на пересечение с ними на каждом шаге, изменяя направление луча, если обнаружено столкновение. Другие свойства луча также могут быть изменены по мере продвижения моделирования, такие как интенсивность , длина волны или поляризация . Этот процесс повторяется с таким количеством лучей, которое необходимо для понимания поведения системы.

Рендеринг с отражением

sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
    specular = 500  # Блестящий
    reflective = 0.2  # Немного отражающий
}
sphere {
    center = (-2, 1, 3)
    radius = 1
    color = (0, 0, 255)  # Синий
    specular = 500  # Блестящий
    reflective = 0.3  # Немного более отражающий
}
sphere {
    center = (2, 1, 3)
    radius = 1
    color = (0, 255, 0)  # Зелёный
    specular = 10  # Немного блестящий
    reflective = 0.4  # Ещё более отражающий
}
sphere {
    color = (255, 255, 0)  # Жёлтый
    center = (0, -5001, 0)
    radius = 5000
    specular = 1000  # Очень блестящий
    reflective = 0.5  # Наполовину отражающий
}
ReflectRay(R, N) {
    return 2*N*dot(N, R) - R;
}
        color = TraceRay(O, D, 1, inf, recursion_depth)
TraceRay(O, D, t_min, t_max, depth) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

    if closest_sphere == NULL
        return BACKGROUND_COLOR

    # Вычисление локального цвета
    P = O + closest_t*D  # Вычисление точки пересечения
    N = P - closest_sphere.center  # Вычисление нормали к сфере в точке пересечения
    N = N / length(N)
    local_color = closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)

    # Если мы достигли предела рекурсии или объект не отражающий, то мы закончили
    r = closest_sphere.reflective
    if depth <= 0 or r <= 0:
        return local_color

    # Вычисление отражённого цвета
    R = ReflectRay(-D, N)
    reflected_color = TraceRay(P, R, 0.001, inf, depth - 1)

    return local_color*(1 - r) + reflected_color*r
}

Исходный код и рабочее демо >>

Произвольная камера

единственноеположениянаправлениеПоложение

for x in [-Cw/2, Cw/2] {
    for y in [-Ch/2, Ch/2] {
        D = camera.rotation * CanvasToViewport(x, y)
        color = TraceRay(camera.position, D, 1, inf)
        canvas.PutPixel(x, y, color)
    }
}

Исходный код и рабочее демо >>

Сцена

сцену

Часть I: трассировка лучей

обязаныШвейцарский ландшафтГрубая аппроксимация ландшафта

Для каждого пикселя холста
    Закрасить его нужным цветом
Разместить глаз и рамку в нужных местах
Для каждого пикселя холста
    Определить квадрат сетки, соответствующий этому пикселю
    Определить цвет, видимый сквозь этот квадрат
    Закрасить пиксель этим цветом

Основы трассировки лучей

сейчасположением камерыокном просмотра (viewport)областью видимости (field of view)

Разместить глаз и рамку в нужных местах (1)
Для каждого пикселя холста
    Определить квадрат сетки, соответствующий этому пикселю (2)
    Определить цвет, видимый сквозь этот квадрат (3)
    Закрасить пиксель этим цветом (4)

Рендеринг с тенями

ClosestIntersection(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    return closest_sphere, closest_t
}
TraceRay(O, D, t_min, t_max) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

    if closest_sphere == NULL
        return BACKGROUND_COLOR

    P = O + closest_t*D  # Compute intersection
    N = P - closest_sphere.center  # Compute sphere normal at intersection
    N = N / length(N)
    return closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)
}
ComputeLighting(P, N, V, s) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point {
                L = light.position - P
                t_max = 1
            } else {
                L = light.direction
                t_max = inf
            }

            # Проверка тени
            shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max)
            if shadow_sphere != NULL
                continue

            # Диффузность
            n_dot_l = dot(N, L)
            if n_dot_l > 0
                i += light.intensity*n_dot_l/(length(N)*length(L))

            # Зеркальность
            if s != -1 {
                R = 2*N*dot(N, L) - L
                r_dot_v = dot(R, V)
                if r_dot_v > 0
                    i += light.intensity*pow(r_dot_v/(length(R)*length(V)), s)
            }
        }
    }
    return i
}

Исходный код и рабочее демо >>Теперь

Рендеринг наших первых сфер

противоположномза камерой

За камерой
Между камерой и плоскостью проекции
Сцена

передзазабудем

O = <0, 0, 0>
for x in [-Cw/2, Cw/2] {
    for y in [-Ch/2, Ch/2] {
        D = CanvasToViewport(x, y)
        color = TraceRay(O, D, 1, inf)
        canvas.PutPixel(x, y, color)
    }
}
CanvasToViewport(x, y) {
    return (x*Vw/Cw, y*Vh/Ch, d)
}
TraceRay(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in  and t1 < closest_t
            closest_t = t1
            closest_sphere = sphere
        if t2 in  and t2 < closest_t
            closest_t = t2
            closest_sphere = sphere
    }
    if closest_sphere == NULL
        return BACKGROUND_COLOR
    return closest_sphere.color
}

какой-то

IntersectRaySphere(O, D, sphere) {
    C = sphere.center
    r = sphere.radius
    oc = O - C

    k1 = dot(D, D)
    k2 = 2*dot(OC, D)
    k3 = dot(OC, OC) - r*r

    discriminant = k2*k2 - 4*k1*k3
    if discriminant < 0:
        return inf, inf

    t1 = (-k2 + sqrt(discriminant)) / (2*k1)
    t2 = (-k2 - sqrt(discriminant)) / (2*k1)
    return t1, t2
}
viewport_size = 1 x 1
projection_plane_d = 1
sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Красный
}
sphere {
    center = (2, 0, 4)
    radius = 1
    color = (0, 0, 255)  # Синий
}
sphere {
    center = (-2, 0, 4)
    radius = 1
    color = (0, 255, 0)  # Зелёный
}

Исходный код и рабочее демо >>

Зачем читать эту статью?

  1. Шейдеры. В первых видеопроцессорах алгоритмы были жёстко заданы в «железе», но в современных программист должен писать собственные шейдеры. Другими словами, вам всё равно нужно реализовывать большие фрагменты ПО рендеринга, только теперь оно выполняется в видеопроцессоре.
  2. Понимание. Вне зависимости от того, используете ли вы готовый конвейер или пишете свои шейдеры, понимание того, что происходит за кулисами позволит вам оптимальнее использовать готовый конвейер и писать шейдеры лучше.
  3. Интересность. Немногие области информатики могут похвастаться возможностью мгновенного получения видимых результатов, которые даёт нам компьютерная графика. Чувство гордости после запуска выполнения первого запроса SQL несравнимо с тем, что вы чувствуете в первый раз, когда удастся правильно оттрассировать отражения. Я преподавал компьютерную графику в университете в течение пяти лет. Меня часто удивляло, как мне удавалось семестр за семестром получать удовольствие: в конце концов мои усилия оправдывали себя радостью студентов от того, что они могли использовать свои первые рендеры в качестве обоев рабочего стола.