Escribir un Path Tracer simple en el buen GLSL antiguo

A raíz de la emoción en torno a las nuevas tarjetas de Nvidia con soporte RTX, mientras escaneaba Habr en busca de artículos interesantes, me sorprendió descubrir que un tema como el rastreo de rutas prácticamente no se trata aquí. "Esto no funcionará" - pensé y decidí que sería bueno hacer algo pequeño sobre este tema, y ​​que sería útil para otros. Aquí, por cierto, era necesario probar la API de su propio motor, así que decidí: empezaré mi propio trazador de rutas simple. Lo que resultó de esto, cree que ya lo ha adivinado en la vista previa de este artículo.





Un poco de teoría

. , , .





, , , . , " " , , , ( , ).





trazado de rayos desde la posición del observador

, : ? , , , - . , - , , . , , - . - , ( - , , ).





varios materiales renderizados por renderizado físicamente correcto
, -

, - , . . : - : ( ), (, -) (, ). , .





:





  • (reflectance) -





  • (roughness) -





  • (emittance) - ,





  • (transparency/opacity) -





, , , , , , .





GLSL

( ?) , . c , , , cornell-box.





una de las opciones de la caja de Cornell para probar el renderizado correcto
cornell box'a

GLSL . , , , : , , - vec2



, vec3



, mat3



..





, ! :





struct Material
{
    vec3 emmitance;
    vec3 reflectance;
    float roughness;
    float opacity;
};

struct Box
{
    Material material;
    vec3 halfSize;
    mat3 rotation;
    vec3 position;
};

struct Sphere
{
    Material material;
    vec3 position;
    float radius;
};
      
      



: , , , , :





bool IntersectRaySphere(vec3 origin, vec3 direction, Sphere sphere, out float fraction, out vec3 normal)
{
    vec3 L = origin - sphere.position;
    float a = dot(direction, direction);
    float b = 2.0 * dot(L, direction);
    float c = dot(L, L) - sphere.radius * sphere.radius;
    float D = b * b - 4 * a * c;

    if (D < 0.0) return false;

    float r1 = (-b - sqrt(D)) / (2.0 * a);
    float r2 = (-b + sqrt(D)) / (2.0 * a);

    if (r1 > 0.0)
        fraction = r1;
    else if (r2 > 0.0)
        fraction = r2;
    else
        return false;

    normal = normalize(direction * fraction + L);

    return true;
}

bool IntersectRayBox(vec3 origin, vec3 direction, Box box, out float fraction, out vec3 normal)
{
    vec3 rd = box.rotation * direction;
    vec3 ro = box.rotation * (origin - box.position);

    vec3 m = vec3(1.0) / rd;

    vec3 s = vec3((rd.x < 0.0) ? 1.0 : -1.0,
        (rd.y < 0.0) ? 1.0 : -1.0,
        (rd.z < 0.0) ? 1.0 : -1.0);
    vec3 t1 = m * (-ro + s * box.halfSize);
    vec3 t2 = m * (-ro - s * box.halfSize);

    float tN = max(max(t1.x, t1.y), t1.z);
    float tF = min(min(t2.x, t2.y), t2.z);

    if (tN > tF || tF < 0.0) return false;

    mat3 txi = transpose(box.rotation);

    if (t1.x > t1.y && t1.x > t1.z)
        normal = txi[0] * s.x;
    else if (t1.y > t1.z)
        normal = txi[1] * s.y;
    else
        normal = txi[2] * s.z;

    fraction = tN;

    return true;
}
      
      



- - . , , , .





, , GLSL . , - :





#define FAR_DISTANCE 1000000.0
#define SPHERE_COUNT 3
#define BOX_COUNT 8

Sphere spheres[SPHERE_COUNT];
Box boxes[BOX_COUNT];

bool CastRay(vec3 rayOrigin, vec3 rayDirection, out float fraction, out vec3 normal, out Material material)
{
    float minDistance = FAR_DISTANCE;

    for (int i = 0; i < SPHERE_COUNT; i++)
    {
        float D;
        vec3 N;
        if (IntersectRaySphere(rayOrigin, rayDirection, spheres[i], D, N) && D < minDistance)
        {
            minDistance = D;
            normal = N;
            material = spheres[i].material;
        }
    }

    for (int i = 0; i < BOX_COUNT; i++)
    {
        float D;
        vec3 N;
        if (IntersectRayBox(rayOrigin, rayDirection, boxes[i], D, N) && D < minDistance)
        {
            minDistance = D;
            normal = N;
            material = boxes[i].material;
        }
    }

    fraction = minDistance;
    return minDistance != FAR_DISTANCE;
}
      
      



. , . , , .





, , ( ). : L' = E + f*L, E - (emittance), f - (reflectance), L - , , L' - , . , , , , , , .





, :





//    
#define MAX_DEPTH 8

vec3 TracePath(vec3 rayOrigin, vec3 rayDirection)
{
    vec3 L = vec3(0.0); //   
    vec3 F = vec3(1.0); //  
    for (int i = 0; i < MAX_DEPTH; i++)
    {
        float fraction;
        vec3 normal;
        Material material;
        bool hit = CastRay(rayOrigin, rayDirection, fraction, normal, material);
        if (hit)
        {
            vec3 newRayOrigin = rayOrigin + fraction * rayDirection;
            vec3 newRayDirection = ...
            // ,   

            rayDirection = newRayDirection;
            rayOrigin = newRayOrigin;

            L += F * material.emmitance;
            F *= material.reflectance;
        }
        else
        {
            //     -    
            F = vec3(0.0);
        }
    }
    //    
    return L;
}
      
      



C++, L CastRay



. , GLSL , , . , , . - , emittance . , , . " ", , , .





: ? , path-tracer' - . , , ( , , ) , , , (. specular ), , , (. diffuse ), . , D = normalize(a * R + (1 - a) * T), a - / , R - , T - , . , a = 1 , a = 0, , . , 0 1, , , (. glossy ).





distribución de rayos para diferentes tipos de superficies

. - , . - , , - , , :





#define PI 3.1415926535

vec3 RandomHemispherePoint(vec2 rand)
{
    float cosTheta = sqrt(1.0 - rand.x);
    float sinTheta = sqrt(rand.x);
    float phi = 2.0 * PI * rand.y;
    return vec3(
        cos(phi) * sinTheta,
        sin(phi) * sinTheta,
        cosTheta
    );
}

vec3 NormalOrientedHemispherePoint(vec2 rand, vec3 n)
{
    vec3 v = RandomHemispherePoint(rand);
    return dot(v, n) < 0.0 ? -v : v;
}
      
      



: , . , : , , :





vec3 hemisphereDistributedDirection = NormalOrientedHemispherePoint(Random2D(), normal);

vec3 randomVec = normalize(2.0 * Random3D() - 1.0);

vec3 tangent = cross(randomVec, normal);
vec3 bitangent = cross(normal, tangent);
mat3 transform = mat3(tangent, bitangent, normal);

vec3 newRayDirection = transform * hemisphereDistributedDirection;
      
      



: Random?D



0 1. GLSL . , ( StackOverflow ):





float RandomNoise(vec2 co)
{
    return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
      
      



(gl_FragCoord), , - . , .





reflejos con diferente rugosidad

TracePath



:





vec3 TracePath(vec3 rayOrigin, vec3 rayDirection)
{
    vec3 L = vec3(0.0);
    vec3 F = vec3(1.0);
    for (int i = 0; i < MAX_DEPTH; i++)
    {
        float fraction;
        vec3 normal;
        Material material;
        bool hit = CastRay(rayOrigin, rayDirection, fraction, normal, material);
        if (hit)
        {
            vec3 newRayOrigin = rayOrigin + fraction * rayDirection;
            vec3 hemisphereDistributedDirection = NormalOrientedHemispherePoint(Random2D(), normal);

            randomVec = normalize(2.0 * Random3D() - 1.0);

            vec3 tangent = cross(randomVec, normal);
            vec3 bitangent = cross(normal, tangent);
            mat3 transform = mat3(tangent, bitangent, normal);
            
            vec3 newRayDirection = transform * hemisphereDistributedDirection;
                
            vec3 idealReflection = reflect(rayDirection, normal);
            newRayDirection = normalize(mix(newRayDirection, idealReflection, material.roughness));
            
            //       
            //  0.8   
            // ,         ,   
            newRayOrigin += normal * 0.8;

            rayDirection = newRayDirection;
            rayOrigin = newRayOrigin;

            L += F * material.emmitance;
            F *= material.reflectance;
        }
        else
        {
            F = vec3(0.0);
        }
    }

    return L;
}
      
      



, . , , , , ? , , , . , , , a, b (. ): b = arcsin(sin(a) * n1 / n2), n1 - , , a n2 - , . , , , , , .





Ángulo de incidencia, reflexión y refracción
,

: sin(a) 0 1 . n1 / n2 , 1. , sin(a) * n1 / n2 arcsin. ? , ?





, , ! , , , " ", , . , , , . , , , . .





efecto fresnel

, ? , , . - . - . , :





float FresnelSchlick(float nIn, float nOut, vec3 direction, vec3 normal)
{
    float R0 = ((nOut - nIn) * (nOut - nIn)) / ((nOut + nIn) * (nOut + nIn));
    float fresnel = R0 + (1.0 - R0) * pow((1.0 - abs(dot(direction, normal))), 5.0);
    return fresnel;
}
      
      



, : , , :





vec3 IdealRefract(vec3 direction, vec3 normal, float nIn, float nOut)
{
    // ,     
    //   -        
    bool fromOutside = dot(normal, direction) < 0.0;
    float ratio = fromOutside ? nOut / nIn : nIn / nOut;

    vec3 refraction, reflection;
    refraction = fromOutside ? refract(direction, normal, ratio) : -refract(-direction, normal, ratio);
    reflection = reflect(direction, normal);

    //      refract   0.0
    return refraction == vec3(0.0) ? reflection : refraction;
}
      
      



, , , . , , . -, :





bool IsRefracted(float rand, vec3 direction, vec3 normal, float opacity, float nIn, float nOut)
{
    float fresnel = FresnelSchlick(nIn, nOut, direction, normal);
    return opacity > rand && fresnel < rand;
}
      
      



: TracePath



, - :





#define N_IN 0.99
#define N_OUT 1.0

vec3 TracePath(vec3 rayOrigin, vec3 rayDirection)
{
    vec3 L = vec3(0.0);
    vec3 F = vec3(1.0);
    for (int i = 0; i < MAX_DEPTH; i++)
    {
        float fraction;
        vec3 normal;
        Material material;
        bool hit = CastRay(rayOrigin, rayDirection, fraction, normal, material);
        if (hit)
        {
            vec3 newRayOrigin = rayOrigin + fraction * rayDirection;
            vec3 hemisphereDistributedDirection = NormalOrientedHemispherePoint(Random2D(), normal);

            randomVec = normalize(2.0 * Random3D() - 1.0);
            vec3 tangent = cross(randomVec, normal);
            vec3 bitangent = cross(normal, tangent);
            mat3 transform = mat3(tangent, bitangent, normal);
            vec3 newRayDirection = transform * hemisphereDistributedDirection;
                
            // ,   .  ,      
            bool refracted = IsRefracted(Random1D(), rayDirection, normal, material.opacity, N_IN, N_OUT);
            if (refracted)
            {
                vec3 idealRefraction = IdealRefract(rayDirection, normal, N_IN, N_OUT);
                newRayDirection = normalize(mix(-newRayDirection, idealRefraction, material.roughness));
                newRayOrigin += normal * (dot(newRayDirection, normal) < 0.0 ? -0.8 : 0.8);
            }
            else
            {
                vec3 idealReflection = reflect(rayDirection, normal);
                newRayDirection = normalize(mix(newRayDirection, idealReflection, material.roughness));
                newRayOrigin += normal * 0.8;
            }

            rayDirection = newRayDirection;
            rayOrigin = newRayOrigin;

            L += F * material.emmitance;
            F *= material.reflectance;
        }
        else
        {
            F = vec3(0.0);
        }
    }
    return L;
}
      
      



N_IN



N_OUT



. -, , ( ). , , .





!

: , , . : : direction



- . up



- "" ( ), fov



- . - ( 0 1 x y) . - , .





vec3 GetRayDirection(vec2 texcoord, vec2 viewportSize, float fov, vec3 direction, vec3 up)
{
    vec2 texDiff = 0.5 * vec2(1.0 - 2.0 * texcoord.x, 2.0 * texcoord.y - 1.0);
    vec2 angleDiff = texDiff * vec2(viewportSize.x / viewportSize.y, 1.0) * tan(fov * 0.5);

    vec3 rayDirection = normalize(vec3(angleDiff, 1.0f));

    vec3 right = normalize(cross(up, direction));
    mat3 viewToWorld = mat3(
        right,
        up,
        direction
    );

    return viewToWorld * rayDirection;
}
      
      



, , , . 16 . ! : 4 16 , . , ( ), , float'. :





renderizar un fotograma y varios apilados juntos
,

main



( - TracePath



):





// ray_tracing_fragment.glsl

in vec2 TexCoord;
out vec4 OutColor;

uniform vec2 uViewportSize;
uniform float uFOV;
uniform vec3 uDirection;
uniform vec3 uUp;
uniform float uSamples;

void main()
{
    //    
    InitializeScene();

    vec3 direction = GetRayDirection(TexCoord, uViewportSize, uFOV, uDirection, uUp);

    vec3 totalColor = vec3(0.0);
    for (int i = 0; i < uSamples; i++)
    {
        vec3 sampleColor = TracePath(uPosition, direction);
        totalColor += sampleColor;
    }

    vec3 outputColor = totalColor / float(uSamples);
    OutColor = vec4(outputColor, 1.0);
}
      
      



!

, . , , RGB ( ) . - RGB32F ( , ). - .





, . , - ( tone-mapping', - , ):





// post_process_fragment.glsl

in vec2 TexCoord;
out vec4 OutColor;

uniform sampler2D uImage;
uniform int uImageSamples;

void main()
{
    vec3 color = texture(uImage, TexCoord).rgb;
    color /= float(uImageSamples);
    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0 / 2.2));
    OutColor = vec4(color, 1.0);
}
      
      



GLSL . - . API , , . API, :





virtual void OnUpdate() override
{
    //     ,    
    auto viewport = Rendering::GetViewport();
    auto output = viewport->GetRenderTexture();

    //     (,    ..)
    auto viewportSize = Rendering::GetViewportSize();
    auto cameraPosition = MxObject::GetByComponent(*viewport).Transform.GetPosition();
    auto cameraRotation = Vector2{ viewport->GetHorizontalAngle(), viewport->GetVerticalAngle() };
    auto cameraDirection = viewport->GetDirection();
    auto cameraUpVector = viewport->GetDirectionUp();
    auto cameraFOV = viewport->GetCamera<PerspectiveCamera>().GetFOV();

    // ,   .   ,     
    bool accumulateImage = oldCameraPosition == cameraPosition &&
                           oldCameraDirection == cameraDirection &&
                           oldFOV == cameraFOV;

    //         
    int raySamples = accumulateImage ? 16 : 4;

    //     ,   
    this->rayTracingShader->SetUniformInt("uSamples", raySamples);
    this->rayTracingShader->SetUniformVec2("uViewportSize", viewportSize);
    this->rayTracingShader->SetUniformVec3("uPosition", cameraPosition);
    this->rayTracingShader->SetUniformVec3("uDirection", cameraDirection);
    this->rayTracingShader->SetUniformVec3("uUp", cameraUpVector);
    this->rayTracingShader->SetUniformFloat("uFOV", Radians(cameraFOV));

    //       ,       
    //    ,     
    if (accumulateImage)
    {
        Rendering::GetController().GetRenderEngine().UseBlending(BlendFactor::ONE, BlendFactor::ONE);
        Rendering::GetController().RenderToTextureNoClear(this->accumulationTexture, this->rayTracingShader);
        accumulationFrames++;
    }
    else
    {
        Rendering::GetController().GetRenderEngine().UseBlending(BlendFactor::ONE, BlendFactor::ZERO);
        Rendering::GetController().RenderToTexture(this->accumulationTexture, this->rayTracingShader);
        accumulationFrames = 1;
    }

    //         - 
    this->accumulationTexture->Bind(0);
    this->postProcessShader->SetUniformInt("uImage", this->accumulationTexture->GetBoundId());
    this->postProcessShader->SetUniformInt("uImageSamples", this->accumulationFrames);
    Rendering::GetController().RenderToTexture(output, this->postProcessShader);

    //    
    this->oldCameraDirection = cameraDirection;
    this->oldCameraPosition = cameraPosition;
    this->oldFOV = cameraFOV;
}
      
      



, ! . path-tracing', , , . - . , path-tracer':





efecto túnel sin fin de dos espejos paralelos
refracción de la luz cuando se mira a través de una esfera transparente
iluminación indirecta y sombras suaves

  • Path-Tracer' GitHub: https://github.com/MomoDeve/PathTracer





  • Mi proyecto principal en el que estoy trabajando actualmente: motor de juego MxEngine





  • Genial artículo de @haqreu sobre un tema relacionado sobre el trazado de rayos : https://habr.com/ru/post/436790





  • Acerca de la representación físicamente correcta, el muestreo de importancia y mucho más de @MrShoor: https://habr.com/ru/post/326852/












All Articles