В этом посте мы собираемся сделать кое-что более увлекательное, используя те же строительные блоки, что и в предыдущем посте. Мы собираемся добавить несколько сфер в нашу сцену и добавить отражения.

1. Добавление нескольких сфер

Давайте сначала добавим несколько сфер и начнем с определения макроса для необходимого количества сфер.

#define NUM_SPHERES 5

Мы уже определили структуру для нашей сферы в предыдущем посте. Давайте воспользуемся этим и определим 4 новые сферы в функции initialize(), чтобы добавить их в нашу сцену.

// Create spheres
spheres[0].center = vec3(-0.25, 1.5 * sin(uTime), -0.25);
spheres[0].radius = 0.3;
spheres[0].color = vec3(1.0, 0.0, 0.0);
spheres[1].center = vec3(0.5 * sin(uTime), 0.25, 0.75);
spheres[1].radius = 0.2;
spheres[1].color = vec3(0.0, 1.0, 0.0);
spheres[2].center = vec3(-0.75, 0.0, 0.5);
spheres[2].radius = 0.2;
spheres[2].color = vec3(0.0, 0.0, 1.0);
spheres[3].center = vec3(-0.25 , 0.4 * sin(uTime), 0.1* sin(uTime));
spheres[3].radius = 0.25;
spheres[3].color = vec3(0.0, 1.0, 1.0);
spheres[4].center = vec3(-1.5, 0.15 * sin(uTime), 0.15 * sin(uTime));
spheres[4].radius = 0.35;
spheres[4].color = vec3(1.0, 1.0, 0.0);

2. Рисование сфер

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

float getIntersection(Sphere sphere, Ray ray) {
    vec3 sphereCenter = sphere.center;
    vec3 colorOfSphere = sphere.color;
    float radius = sphere.radius;
    vec3 cameraSource = ray.origin;
    vec3 cameraDirection = ray.direction;
vec3 distanceFromCenter = (cameraSource - sphereCenter);
    float B = 2.0 * dot(cameraDirection, distanceFromCenter);
    float C = dot(distanceFromCenter, distanceFromCenter) - pow(radius, 2.0);
    float delta = pow(B, 2.0) - 4.0 * C;
    float t = 0.0;
    if (delta > 0.0) {
        float sqRoot = sqrt(delta);
        float t1 = (-B + sqRoot) / 2.0;
        float t2 = (-B - sqRoot) / 2.0;
        t = min(t1, t2);
    }
    if (delta == 0.0) {
        t = -B / 2.0;
    }
    return t;
}

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

for (int i=0; i < NUM_SPHERES; i++) {
    float t = getIntersection(spheres[i], ray);
    if (t > 0.0 && t < minT) {
        minT = t;
        sphereToShow = spheres[i];
    }
}

Обратите внимание, что начальное значение minT равно INFINITY. Поэтому мы определяем макрос для этого. Пусть БЕСКОНЕЧНОСТЬ будет относительно большим числом для нашей сцены. Я определяю это как 100000.0

#define INFINITY 100000.0

Теперь, когда у нас есть sphereToShow для нашей сцены, мы можем вернуть цвет этой сферы в `gl_FragColor`. Но нам также нужно вернуть ReflectRay для этой точки вместе с цветом. Итак, я определил новую структуру под названием RayTracerOutput.

struct RayTracerOutput {
    Ray reflectedRay;
    vec3 color;
};

3. Вычисление отраженного луча

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

R = 2 (-Wn) n + W

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

Происхождение = S + ε R,где S — точка поверхности, а ε — очень малое число

Направление = R

Теперь наш новый луч (то есть отраженный луч) будет:

Источник + t * Направление

Вот так все это выглядит в коде.

if (minT > 0.0 && minT != INFINITY) {
    vec3 surfacePoint = cameraSource + (minT * cameraDirection);
    vec3 surfaceNormal = normalize(surfacePoint - sphereCenter);
// Reflection
    vec3 reflection = 2.0 * dot(-ray.direction, surfaceNormal) * surfaceNormal + ray.direction;
    reflectionRay.origin = surfaceNormal + epsilon * reflection;
    reflectionRay.direction = reflection;
    color = colorOfSphere * (ambience + ((1.0 - ambience) * max(0.0, dot(surfaceNormal, lightSource))));
    rayTracer.color = color;
    rayTracer.reflectedRay = reflectionRay;
}

Я объединил все это в одну функцию под названием trace().

RayTracerOutput trace(Sphere spheres[NUM_SPHERES], Ray ray, Light light) {
    RayTracerOutput rayTracer;
    Ray reflectionRay;
    Sphere sphereToShow;
    float minT = INFINITY;
    vec3 cameraSource = ray.origin;
    vec3 cameraDirection = ray.direction;
    vec3 lightSource = light.position;
    float ambience = light.ambience;
    vec3 color = vec3(0.0, 0.0, 0.0);
for (int i=0; i < NUM_SPHERES; i++) {
        float t = getIntersection(spheres[i], ray);
        if (t > 0.0 && t < minT) {
            minT = t;
            sphereToShow = spheres[i];
        }
    }
vec3 sphereCenter = sphereToShow.center;
    vec3 colorOfSphere = sphereToShow.color;
if (minT > 0.0 && minT != INFINITY) {
        vec3 surfacePoint = cameraSource + (minT * cameraDirection);
        vec3 surfaceNormal = normalize(surfacePoint - sphereCenter);
// Reflection
        vec3 reflection = 2.0 * dot(-ray.direction, surfaceNormal) * surfaceNormal + ray.direction;
        reflectionRay.origin = surfaceNormal + epsilon * reflection;
        reflectionRay.direction = reflection;
        color = colorOfSphere * (ambience + ((1.0 - ambience) * max(0.0, dot(surfaceNormal, lightSource))));
        rayTracer.color = color;
        rayTracer.reflectedRay = reflectionRay;
    }
    return rayTracer;
}

4. Добавление отраженного цвета другим сферам.

Теперь все, что нам нужно сделать, это вызвать trace() с отраженным лучом и тем же набором сфер. Но на этот раз нас интересует цвет сферы, на которую падает отраженный луч. Мы добавляем этот цвет к исходному цвету сферы из первого вызова trace(), чтобы увидеть отражения. Вот как выглядит функция main().

void main() {
    initialize();
    RayTracer rayTracer = trace(spheres, rays[0], light[0]);
    // Second call to get reflections
    RayTracer reflection = trace(spheres, rayTracer.reflectedRay, light[0]);
    gl_FragColor = vec4(rayTracer.color + reflection.color, 1.0);
}

Теперь у нас есть работающий трассировщик лучей с несколькими сферами и отражениями!

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