Введение в трассировку лучей | Кодементор

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

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

Если вы хотите пропустить чтение, вот играбельная демонстрация, основанная на этом блоге на SpiderEngine.io

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

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

Вот простая реализация этой идеи (в Typescript)

function rayCast(ray: Ray) {
    let toIntersection = -1;
    let closestIntersection = null;
    for (let obj of objects) {
        let intersection = obj.intersectsWithRay(ray);
        if (intersection) {
            let distance = Vector3.distance(ray.origin, intersection);
            if (toIntersection < 0 || distance < toIntersection) {
                closestIntersection = intersection;
                toIntersection = distance;
            }
        }
    }
    return closestIntersection;
};

let frameBuffer = new FrameBuffer(width, height, rgba);
for (let i = 0; i < height; ++i) {
    for (let j = 0; j < width; ++j) {
        let ray = new Ray().setFromPerspectiveView(
            fovRadians, 
            inverseView, 
            j, 
            i, 
            width, 
            height
        );        
        
        if (rayCast(ray)) {
            frameBuffer.setPixel(j, i, Color.red);    
        } else {
            // environment/background color
            frameBuffer.setPixel(j, i, Color.black);    
        }
    }
}

Затенение

Затенение — это процесс определения цвета каждого пикселя результирующего изображения. В этой статье мы будем использовать Диффузное затенение модель для моделирования того, как свет поглощается и отражается.

Для каждого пикселя мы собираем информацию, необходимую для затенения. А именно перекресток **точка (P) **между лучом, проецируемым из этого пикселя, и окружающей средой, точка свойства поверхности (нормальное и светлое направление) в этом месте, и свойства света. Цвет рассчитывается как:

Цвет = d \* Li \* Lc \* cos(θ)

  • d: диффузный цвет в точке пересечения
  • Ли: интенсивность света
  • Лк: светлый цвет
  • θ: угол между нормалью и направлением на свет (Ld)

Вот результирующее изображение вместе с примером кода:

function rayTrace (ray: Ray, colorOut: Color) {    
    let intersection = rayCast(ray);
    if (!intersection) {
        return;
    }

    // Diffuse shading
    for (let light of lights) {        
        let toLight = new Vector3().copy(light.transform.position)
            .substract(intersection.position)
            .normalize();

        let cosTheta = toLight.dot(intersection.normal);
        cosTheta = Math.max(cosTheta, 0); // Fully dark if facing away from light
        let currentColor = new Color().copy(intersection.diffuseColor)            
            .multiplyColor(light.color)
            .multiply(light.intensity)
            .multiply(cosTheta);
        colorOut.add(currentColor);
    }
}

// .. initialize frame buffer
for (let i = 0; i < height; ++i) {
    for (let j = 0; j < width; ++j) {
        // .. initialize ray
        finalColor.set(0, 0, 0); // environment/background color
        rayTrace(ray, finalColor);
        frameBuffer.setPixel(j, i, finalColor);    
    }
}

Размышления

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

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

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

function rayTrace (ray: Ray, colorOut: Color, currentBounce: number) {    
    let intersection = rayCast(ray);
    if (!intersection) {
        return;
    }
    
    // .. Diffuse shading

    // Handle reflections
    if (currentBounce < maxBounces) {
        let reflectance = intersection.object.getComponent("Visual").material.reflectance;
        if (reflectance > 0) {
            let secondaryRay = new Ray(
                // Nudge the reflection ray origin a bit along the normal to avoid self reflection artifacts
                new Vector3().copy(intersection.normal).multiply(.001).add(intersection.position), // Origin
                new Vector3().copy(ray.direction).reflect(intersection.normal) // Direction
            );
            let reflectedColor = new Color();
            rayTrace(secondaryRay, reflectedColor, currentBounce + 1);
            reflectedColor.multiply(reflectance);
            colorOut.add(reflectedColor);
        }        
    }
}

Тени

Для поддержки теней мы должны определить, достижима ли точка пересечения в каждом пикселе для света. Если нет доступа к свету, его необходимо затемнить. Мы вводим понятие теневые лучи. Для каждой точки пересечения мы отбрасываем луч к каждому источнику света. Если свет недоступен, мы удаляем его влияние из уравнения затенения, обнуляя его интенсивность.

Вот новая реализация с учетом теней:

function rayTrace (ray: Ray, colorOut: Color, currentBounce: number) {    
    let intersection = rayCast(ray);
    if (!intersection) {
        return;
    }    
    
    for (let light of lights) {        
        let toLight = new Vector3().copy(light.transform.position)
            .substract(intersection.position)
            .normalize();

        let shadowRay = new Ray(
            // Nudge the shadow ray origin a bit along the normal to avoid moire pattern
            new Vector3().copy(intersection.normal).multiply(.001).add(intersection.position), // Origin
            toLight // Direction
        );
        
        let shadowTest = rayCast(shadowRay);
        let lightIntensity = 1;
        if (shadowTest) {
            // Hit an object, check if it's obstructing light
            let toOccluder = Vector3.distance(shadowTest.position, intersection.position);
            let toLight = Vector3.distance(light.transform.position, intersection.position);
            if (toOccluder < toLight) {
                // Current light is not visible from intersection point                
                lightIntensity = 0;
            }
        }
        
        // .. Diffuse shading
        colorOut.add(currentColor.multiply(lightIntensity ));
    }

    // .. Handle reflections
}

Гладкие тени

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

Мы придаем источникам света ненулевой объем и определяем количество точек отсчета на их площади, которые будут использоваться для отбрасывания дополнительных теневых лучей. Реализация точно такая же, как и для резких теней, но поскольку теперь мы отбрасываем несколько теневых лучей, мы должны отслеживать количество перекрытых лучей. Затем мы настраиваем переменную lightIntensity на обратное отношение перекрытых лучей к общему количеству лучей:

lightIntensity=1 – \frac{occludedShadowRays}{totalShadowRays}

Вот результат с плавными тенями:

Оптимизации

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

Ознакомьтесь с игровой демо-версией, основанной на этом блоге, на SpiderEngine.io.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *