À 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
, , , . , " " , , , ( , ).
, : ? , , , - . , - , , . , , - . - , ( - , , ).
, - , . . : - : ( ), (, -) (, ). , .
:
(reflectance) -
(roughness) -
(emittance) - ,
(transparency/opacity) -
, , , , , , .
GLSL
( ?) , . c , , , cornell-box.
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'. :
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':
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/