Рубрики
Без рубрики

Введение в Ray-Tracing

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

Автор оригинала: Amine Rehioui.

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

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

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

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

Намного лучше подход – это имитировать легкие пути от зрителя к источникам света. Это называется Обратная трассировка. Производительность – мудро, это выигрыш, потому что только объекты в поле зрения обрабатываются. Визуально говоря, результат может быть таким же, поскольку пропагация света является симметричным процессом, а уравнения работают одинаково в обратном направлении.

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

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: светлый цвет
  • θ: угол между нормальным и направлением к свету (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);    
    }
}

Отражения

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

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

Объект отражает свет в зависимости от его материальных свойств. В этой статье мы определяем Refruction фактор на материалах. Реализация-мудрый, лучшая практика – сделать Ray-Tracer A рекурсивный Процесс, так что отраженные лучи обрабатываются точно так же, как первичные лучи. Вот рекурсивный Рэй-Трасер и соответствующий результат:

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
}

Гладкие тени

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

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

Вот результат с гладкими тенями:

Оптимизация

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

Оформить заказ Playbable Demo на основе этого блога на spiderengine.io.