RTXをサポートするNvidiaからの新しいカードに関する興奮をきっかけに、興味深い記事を探してHabrをスキャンしているときに、パスのトレースなどのトピックが実際にはここでカバーされていないことに驚きました。「これはうまくいきません」-私は、このトピックについて小さなことをして、他の人に役立つようにするといいと思い、決めました。ちなみに、ここでは自分のエンジンのAPIをテストする必要があったので、自分のシンプルなパストレーサーを起動することにしました。これから何が起こったのか、あなたはこの記事のプレビューからすでに推測していると思います。
少し理論
, , , . , " " , , , ( , ).
, : ? , , , - . , - , , . , , - . - , ( - , , ).
, - , . . : - : ( ), (, -) (, ). , .
:
(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
私が現在取り組んでいる私の主なプロジェクト:MxEngineゲームエンジン
レイトレースに関する関連トピックに関する@haqreuのクールな記事:https://habr.com/ru/post/436790
物理的に正しいレンダリング、重要度のサンプリングなどについては、@ MrShoor から:https ://habr.com/ru/post/326852/