古き良きGLSLで簡単なパストレーサーを書く

RTXをサポートするNvidiaからの新しいカードに関する興奮をきっかけに、興味深い記事を探してHabrをスキャンしているときに、パスのトレースなどのトピックが実際にはここでカバーされていないことに驚きました。「これはうまくいきません」-私は、このトピックについて小さなことをして、他の人に役立つようにするといいと思い、決めました。ちなみに、ここでは自分のエンジンのAPIをテストする必要があったので、自分のシンプルなパストレーサーを起動することにしました。これから何が起こったのか、あなたはこの記事のプレビューからすでに推測していると思います。





少し理論

. , , .





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





観察者の位置からの光線追跡

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





物理的に正しいレンダリングによってレンダリングされたさまざまなマテリアル
, -

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





:





  • (reflectance) -





  • (roughness) -





  • (emittance) - ,





  • (transparency/opacity) -





, , , , , , .





GLSL

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





正しいレンダリングをテストするためのコーネルボックスオプションの1つ
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 ).





さまざまなタイプの表面の光線分布

. - , . - , , - , , :





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





粗さが異なる反射

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





入射角、反射、屈折
,

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





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





フレネル効果

, ? , , . - . - . , :





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





1つのフレームといくつかを積み重ねてレンダリングします
,

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





2つの並列ミラーからの無限のトンネル効果
透明な球を通して見たときの光の屈折
間接照明とソフトシャドウ

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





  • 私が現在取り組んでいる私の主なプロジェクト:MxEngineゲームエンジン





  • レイトレースに関する関連トピックに関する@haqreuのクールな記事https//habr.com/ru/post/436790





  • 物理的に正しいレンダリング、重要度のサンプリングなどについては、@ MrShoor からhttps ://habr.com/ru/post/326852/












All Articles