Écrire un traceur de chemin simple dans un bon vieux GLSL

À la suite de l'enthousiasme suscité par les nouvelles cartes de Nvidia avec support RTX, tout en scannant Habr à la recherche d'articles intéressants, j'ai été surpris de constater qu'un sujet tel que le tracé de chemin n'est pratiquement pas couvert ici. "Cela ne fonctionnera pas" - J'ai pensé et j'ai décidé que ce serait bien de faire quelque chose de petit sur ce sujet, et que ce soit utile aux autres. Ici, au fait, l'API de son propre moteur devait être testée, alors j'ai décidé: je vais démarrer mon propre traceur de chemin simple. Qu'est-il arrivé, vous pensez que vous l'avez déjà deviné à partir de l'aperçu de cet article.





Un peu de théorie

. , , .





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





lancer de rayons depuis la position de l'observateur

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





divers matériaux rendus par un rendu physiquement correct
, -

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





:





  • (reflectance) -





  • (roughness) -





  • (emittance) - ,





  • (transparency/opacity) -





, , , , , , .





GLSL

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





l'une des options de cornell box pour tester le bon rendu
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 ).





distribution des rayons pour différents types de surfaces

. - , . - , , - , , :





#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), , - . , .





reflets avec différentes rugosités

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 - , . , , , , , .





Angle d'incidence, de réflexion et de réfraction
,

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





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





effet 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'. :





rendre un cadre et plusieurs empilés ensemble
,

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':





effet tunnel sans fin de deux miroirs parallèles
réfraction de la lumière en regardant à travers une sphère transparente
éclairage indirect et ombres douces

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





  • Mon projet principal sur lequel je travaille actuellement: le moteur de jeu MxEngine





  • Article sympa de @haqreu sur un sujet lié au traçage de rayons : https://habr.com/ru/post/436790





  • À propos du rendu physiquement correct, de l'échantillonnage d'importance et bien plus encore de @MrShoor: https://habr.com/ru/post/326852/












All Articles