OptiX Quick Start

선비Sunbei·2023년 9월 1일
0

OptiX

목록 보기
15/25
rt 접두사를 사용하는 것으로 보아 OptiX 5.0 이전 버전의 내용으로 추측한다. (optix 접두사는 OptiX 5.0 이후부터 도입) 
따라서 optix shader를 어떻게 쓰는지만 개념을 잡으면 될 것 같다.
https://docs.nvidia.com/gameworks/content/gameworkslibrary/optix/optix_quickstart.htm

OptiX SDk는 아주 간단한 것부터 중간 정도의 복잡한 것까지 여러 가지 기본 ray tracing 효과를 구현하는 방법을 보여주는 source sample, tutorial을 제공한다. sample은 11단계로 구성되어 있으며, 각 단계마다 새로운 효과가 추가된다. 이 section에서는 각 단계에 대해 설명하고 shading과 intersection에 대한 program을 보여준다.

이 tutorial은 CUDA C programing machanism에 중점을 두며 Host API를 사용하여 object를 설정하는 방법은 설명하지 않는다. CUDA C 및 Host API부분에 대한 전체 소스 코드는 SDK에 포함되어 있다. 이 tutorial은 OptiX를 시작하기 위한 목적으로만 제공되며, visit program 및 acceleration struct와 같은 고급 기능은 다른 SDK sample에서 확인할 수 있다. OptiX를 사용한 고급 렌더링 기법 및 과학적 컴퓨팅에 대해서는 여기서 다루지 않는다.

1.1 Tutorial 0 - Normal Shader

OptiX에서 가장 일반적인 program 유형은 closest hit program이다. 이 program은 OptiX가 ray와 object 사이의 가장 가까운 intersection을 찾을 때마다 실행된다. 일반적으로 closest hit program의 목적은 intersection point의 color를 결정하는 것이다. 사용자는 여러 개의 closest hit programs을 생성하고 각각을 장면의 object에 binding하여 서로 다른 object가 서로 다른 모양을 가질 수 있도록 할 수 있다. 이 tutorial에서는 장면의 각 object에 하나의 간단한 closest hit program을 binding한다. 이 program은 normal shader로, object normal을 world space로 변환하고 각 component가 0과 1사이에 위치하도록 scale을 조정한다. 결과 (x,y,z) vector는 color로 직접해석되어 ray와 관련된 payload에 저장된다.

RT_PROGRAM void closest_hit_radiance0()
{
    prd_radiance.result = normalize(rtTransformNormal(
                                    RT_OBJECT_TO_WORLD,
                                    shading_normal))
                                    *0.5f+0.5f;
}

위의 program은 shading_normal이라는 변수를 참조한다. box floor에 대한 intersection program(여기에는 표시되지 않음, 해당 코드는 SDK 참조)은 교차점이 발견되면 모두 이 변수를 계산한다. 이 변수는 여러 program에서 공유되므로 다음과 같은 특별한 방식으로 선언해야 한다.

rtDeclareVariable(float3,
                  shading_normal,
                  attribute shading_normal, );

결과 색상은 prd_radience라는 다른 변수에 기록된다. 이것은 각 ray와 관련된 데이터를 전달하는 사용자 정의 구조체의 instance이다. 이 경우 결과라는 구조체의 일부에 float3 색상을 기록한다. 이 구조의 다른 두 요소에 대한 자세한 설명은 나중에 다시 설명한다.

struct PerRayData_radiance
{
    float3 result;
    float  importance;
    int depth;
};
rtDeclareVariable(PerRayData_radiance,
                  prd_radiance, rtPayload, );

변수 이름 shading_normal과 prd_radiance에는 특별한 의미가 없다. semantic name이라고 하는 rtDeclareVariable 매크로의 세 번째 인수는 이러한 변수를 시스템에서 올바른 위치에 바인딩하는데 사용된다. 여기서 rtPayload를 semantic name으로 사용하면 OptiX가 이 데이터 구조가 각 개별 ray와 연관되어야 함을 알 수 있다. ray payload의 결과 부분은 아래의 별도 program에서 ray tracer의 출력에 복사된다.

rtDeclareVariable(float3, bg_color, , );

RT_PROGRAM void miss()
{
    prd_radiance.result = bg_color;
}

bg_color 값은 host가 설정하며 ray tracer를 여러 번 호출할 때마다 수정할 수 있다. 이것은 Host와 OptiX program 간의 통신을 위한 가장 일반적인 machanism이다. OptiX는 이러한 변수에 대한 상속 모델도 제공하지만 자세한 내용은 이 tutorial에서 설명하지 않는다.

ray 자체를 생성하기 위한 pinhole 모델을 사용한다. ray-generation program은 ray를 생성하여 scene에 촬영하고 결과 color을 output buffer에 복사하는 작업을 담당한다. 이후 output buffer는 host에서 추가 분석을 위해 사용하거나 rendering을 위해 OpenGL에서 사용한다. OptiX는 임의의 수의 output buffer를 사용할 수 있으며 이러한 buffer는 임의의 유형을 가질 수 있다. 이 tutorial에서 단일 output buffer는 OpenGL texture로 효율적으로 전송하기 위해 설계된 2차원 RGBA8 image이다. 도움을 주는 함수 make_color(여기서는 표시x)는 부동 소수점 RGB 색상을 적절한 RGBA8 정수 값으로 변환하고 필요에 따라 scaling 및 clamping을 수행한다.

RT_PROGRAM void pinhole_camera()
{
    size_t2 screen = output_buffer.size();

    float2 d = make_float2(launch_index) /
               make_float2(screen) * 2.f - 1.f;
    float3 ray_origin = eye;
    float3 ray_direction = normalize(d.x*U + d.y*V + W);

    OptiX::Ray ray(ray_origin, ray_direction,
                   radiance_ray_type, scene_epsilon );
    PerRayData_radiance prd;
    prd.importance = 1.f;
    prd.depth = 0;

    rtTrace(top_object, ray, prd);

    output_buffer[launch_index] = make_color( prd.result );
}

이 프로그램에서 가장 중요한 부분은 rtTrace에 대한 호출이다. 이 함수에는 3가지 인수가 있다.

  1. scene을 나타내는 object hierarchy의 루트이다. 이 hierarchy는 rayTracer를 시작하기 전에 Host가 생성한 것이다.
  2. 위에서 벡터 수학을 사용하여 pinhole camera의 시야 frustum을 simulation하기 위해 계산된 광선이다.
  3. 각 ray에 연결된 데이터 구조를 보관하는 local variable에 대한 참조이다. (위에서 설명한) prd_radiance 변수는 rtPayload semantic으로 선언되었으므로 이 local variable는 다른 모든 OptiX program에서 prd_radiance에 binding된다.

ray가 object에 닿으면 closest hit program이 결과 멤버를 일반 색상으로 설정하고, object에 닿지 않으면 miss program이 결과 멤버를 배경색으로 설정한다. ray가 완전히 추적되면 camera program으로 제어권이 반환되고, 여기서 색상을 출력 버퍼에 저장한다.

마지막으로 한가지 참고할 사항 : OptiX는 traverse와 shading 모두 재귀를 지원하므로 상태를 유지하기 위해 local stack이 사용된다. stack이 충분히 크지 않으면 stack이 overflow될 수 있다. 이 경우 현재 ray-generation program에 대한 모든 처리가 중단되고 exception program이 실행된다. 이 튜토리얼에서는 output buffer를 특수 색상(host가 설정한 색상)으로 설정하여 사용자에게 이러한 상황이 발생했음을 알린다.

rtDeclareVariable(float3, bad_color, , );

RT_PROGRAM void exception()
{
    output_buffer[launch_index] = make_color( bad_color );
}

이러한 모든 program이 준비되면 tutorial program을 실행하여 위에 표시된 이미지를 생성할 수 있다. 이러한 각 program은 초당 수백만 번씩 OptiX에서 실행되어 interactive image를 생성한다. 이제 이러한 기본 사항을 기반으로 보다 사실적이고 복잡한 이미지를 렌더링 해보겠다.

1.2 Tutorial 1 - Diffuse Shading

다음 단계는 scene의 object에 간단한 shading을 추가하는 것이다. 이를 위해 closest hit program을 다시 작성하여 각 hit point에서 조명 계산을 수행하면 된다.

RT_PROGRAM void closest_hit_radiance1()
{
    float3 world_geo_normal = normalize(rtTransformNormal(
                                        RT_OBJECT_TO_WORLD,
                                        geometric_normal));
    float3 world_shade_normal = normalize(rtTransformNormal(
                                          RT_OBJECT_TO_WORLD,
                                          shading_normal));
    float3 ffnormal = faceforward(world_shade_normal,
                                  -ray.direction,
                                  world_geo_normal);
    float3 color = Ka * ambient_light_color;
 
    float3 hit_point = ray.origin + t_hit * ray.direction;

    for(int i = 0; i < lights.size(); ++i) {
        BasicLight light = lights[i];
        float3 L = normalize(light.pos - hit_point);
        float nDl = dot( ffnormal, L);

        if( nDl > 0 )
            color += Kd * nDl * light.color;
    }
    prd_radiance.result = color;
}

faceforward : vertex의 eye-space position vector가 geometric normal의 반대 방향을 가리키면 normal을 그대로 반환하고, 그렇지 않으면 normal의 음수를 반환하다.

수학적으로 l와 Ng의 dot product가 음수이면 N이 변경되지 않고, 그렇지 않으면 -N이 반환된다.


이 program은 세가지 기본 단계로 구성된다.

첫째, world space에서 정확한 normal을 계산한다. 이를 위해서는 shading normal과 geometric normal을 모두 가져와 worldspace로 변환한 다음 faceforward 함수를 사용하여 normal의 방향이 ray의 원점을 향하도록 해야 한다. 대부분의 rendering system은 vertex normal interpolation 또는 bump mapping face와 같이 음영 노멀과 기하학적 normal을 구분한다. 이 tutorial에는 이러한 효과가 포함되어 있지 않지만 shadinig은 복잡한 scene에서 이 program을 즉시 재사용할 수 있도록 이러한 효과를 사용한다.

둘째, 이 program은 표면의 ambient color를 계산한다. ambient light은 object에 떨어지는 모든 조명의 평균 밝기 근사치이다. 이 경우 host에서 이러한 매개 변수를 가져오는 데 두 개의 변수를 사용한다. 첫 번째 변수인 Ka는 object에 property이며 tutorial의 host code에 의해 material object 또는 geometric object에 바인딩된다. 다른 ambient_lgith_color는 OptiX Context에 binding된 전역 property이다. 이러한 변수를 host의 scene hierarchy structure에서 다른 지점에 연결하면, 렌더링 되는 object의 모든 하위 집합에 영향을 줄 수 있으므로 OptiX 상속 메커니즘은 이러한 변수를 지정하는 데 강력한 메커니즘이다. 변수 상속은 이 tutorial에서 자세히 설명하지 않으므로 자세한 내용은 OptiX programming guide를 참조해야 한다.

마지막으로 scene의 각 광원을 반복하고 해당 광원의 기여도를 계산한다. 이 예제에서 각 광원은 BasicLight라는 사용자 선언 구조로 설명되며, 광원 집합은 lights라는 1차원 입력 buffer에 저장된다. host 코드는 rayTracer를 시작하기 전에 조명 버퍼를 할당하고 채운다. 다음은 device의 조명 구조체이다.

struct BasicLight
{
    float3 pos;
    float3 color;
    int    casts_shadow;
    int    padding;     // make the structure more efficient
};
rtBuffer<BasicLight> lights;

lighting loop에서는 표면 법선 벡터와 광원 방향 사이의 cosine을 기반으로 하는 간단한 Lambertian shading model을 사용한다. 표면 색은 host에서 초기화된 다른 변수인 Kd에 지정된다. 이후 tutorial에서는 이 색상을 절차적으로 계산하여 더 복잡한 시각적 외관을 시뮬레이션 한다.

1.3 Tutorial 2 - Phong Highlight

기본 lambertian shaing model을 간단히 수정하는 한 가지 방법은 실제 플라스틱과 또는 금속 물체에서 볼 수 있는 밝은 하이라이트인 phong highlight를 추가하는 것이다. 여기서 halfway vector, 즉 L과 음의 광선 방햐의 중간에 위치한 벡터를 계산하는 Jim Blinn의 접근 방식을 사용한다. halfway vector H와 법선 벡터 N 사이의 각도의 cosine은 highlight의 선명도를 제어하는 user-specified power phing_exp를 사용한다. 이 예제에서는 xy를 계산하는 내장함수 pow(x,y)를 사용한다. 이 함수는 GPU의 특수 목적 하드웨어를 황룔하여 이 연산을 효율적으로 수행한다.

아래 코드는 이전 tutorial에서 diffuse shading을 약간 수정해야 하는 부분을 보여준다.

...
if( nDl > 0 ){
    float3 Lc = light.color;
    color += Kd * nDl * Lc;

    float3 H = normalize(L - ray.direction);
    float nDh = dot( ffnormal, H );
    if(nDh > 0)
        color += Ks * Lc * pow(nDh, phong_exp);

}
...

1.4 Tutorial 3 - Shadows

지금까지 OpenGL로 쉽게 만들 수 없는 이미지는 만들지 못했다. 하지만 ray tracing의 강력한 기능 중 하나는 그림자나 반사 등 복잡한 조명 효과를 아주 적은 노력으로 추가할 수 있다는 것이다. 그림자를 지원하도록 이전 tutorial을 수정하려면 몇 줄의 코드를 추가하여 다른 ray를 추적한다. 이 경우 새 ray은(shadow ray) 음영 지점의 표면에서 시작하여 광원을 가리키게 된다.

... 
if( nDl > 0.0f ){
    // cast shadow ray
    PerRayData_shadow shadow_prd;
    shadow_prd.attenuation = 1.0f;
    float Ldist = length(light.pos - hit_point);
    OptiX::Ray shadow_ray(hit_point, L, shadow_ray_type,
                          scene_epsilon, Ldist );
    rtTrace(top_shadower, shadow_ray, shadow_prd);
    float light_attenuation = shadow_prd.attenuation;

    if( light_attenuation > 0.0f ){
        float3 Lc = light.color * light_attenuation;
        color += Kd * nDl * Lc;

        float3 H = normalize(L - ray.direction);
        float nDh = dot( ffnormal, H );
        if(nDh > 0)
           color += Ks * Lc * pow(nDh, phong_exp);
    }
}
...

이 코드는 pinhole camera와 마찬가지로 새로운 ray를 생성한다. ray generation의 세 번째 파라미터인 shadow_ray_type이 pinhole camera의 해당 인자와 다르다는 점을 알아야 한다. 이러한 ray type과 다르다는 점을 주목해야 한다. 이러한 ray type은 host code에서 제공하는 정수 변수로, OptiX가 다양한 ray type을 개별적으로 처리할 수 있도록 한다. 또한 shadow ray는 0에서 1 사이의 감쇠 계수로 표시되는 occlusion 정보 이외의 데이터를 전달할 필요가 없기 때문에 shadow ray에는 camera ray와는 다른 종류의 ray payload인 PerRayData_shadow가 있다는 점을 유의해야 한다.

ray를 1.0의 감쇠를 갖도록 초기화하고 camera code에서와 같이 rtTrace를 호출한다. optix program은 효과적으로 재귀적이라는 점을 주목해야 한다. 이 rtTrace 호출은 caemara 함수의 rtTrace 호출 내부에서 이루어진다. 지금은 불투명한 object로 제한할 것이므로 object에 닿은 shadow ray는 완전히 차단된다.

shadow ray는 어떤 object에 닿는지는 중요하지 않으므로 closest hit가 필요하지 않는다. 따라서 closest hit program을 사용하는 대신 이러한 ray에 대해 any-hit program을 사용한다. 모든 hit program은 ray와 object의 교차점에서 OptiX에 의해 호출된다. ray를 따라 intersection point가 여러 개 있는 경우 모든 hit program을 호출하는 순서는 지정되지 않는다.

RT_PROGRAM void any_hit_shadow()
{
    // this material is opaque, so it fully attenuates all
    // shadow rays
    prd_shadow.attenuation = 0.0f;

    rtTerminateRay();
}

ray intersection point가 발견되면 ray payload의 감쇠를 0으로 설정하면 빛이 obejct에 도달하지 않음을 나타낸다. 또한 더 이상 교차점을 검색할 필요가 없으므로 가장 최근에 rtTrace를 호출한 함수(여기서는 위에 표시된 closest hit program)에 즉시 제어권을 반환하는 rtTerminateRay를 호출한다.

closest hit program은 shadow feeler가 광원으로 가는 도중에 막히지 않은 경우에만 광원의 기여도를 추가한다. 또한 광원의 기여도에 결과 감쇠를 곱하여 이 tutorial의 뒷부분에서 투명한 object의 color shadow를 지원할 수 있다. 그렇지 않으면 이 section에서는 이전과 동일한 phong model을 사용한다.

1.5 Tutorial 4 - Reflections

ray tracing system에 완벽한 거울 반사를 추가하는 것은 매우 간단하다. 이러한 유형의 효과는 ray tracing을 유연한 이미지 합성 방법으로 만드는 이유이다. 이 경우 바닥에 binding된 material만 집중하겠다. host code에서 geometry에 closest hit program을 binding하여 각 object의 material을 개별적으로 제어할 수 있다.

바닥의 material이 반사되도록 하기 위해 shading 중인 표면에서 시작하여 완벽한 거울 반사 방향으로 향하는 새로운 광선을 구성한다. 표준 ray tracing text에서 이 반사 방향을 도출하는 방법을 확인할 수 있지만, 여기서는 반사라는 내장 OptiX 함수를 사용하여 이러한 세부 사항을 숨긴다. 새로운 ray를 생성하고(다시 한 번 ray generation의 세 번째 parameter를 주의해야 한다.) 이를 추적한다. 결과 색상에 반사율 매개변수(host code에서 제공됨)을 곱한 다음 이전에 계산한 표면 색상에 추가된다.

RT_PROGRAM void floor_closest_hit_radiance4()
{
    // Calculate direct lighting using Phong shading, as
    // shown above
    ...
    float3 R = reflect( ray.direction, ffnormal );
    OptiX::Ray refl_ray( hit_point, R, radiance_ray_type,
                         scene_epsilon );
    rtTrace(top_object, refl_ray, refl_prd);
    color += reflectivity * refl_prd.result;

    prd_radiance.result = color;
}

shadow ray와 마찬가지로 이 함수는 재귀적이어서 reflect ray의 색상을 계산하면 다른 reflect ray, shadow ray 등을 전송할 수 있다. OptiX는 작은 함수 호출 스택을 사용하여 이러한 모든 결과를 계산한 후 제어권을 반환한다. 이로 인해 OptiX가 호출 스택을 overflow하여(광선이 무한히 튀어 오르는 거울의 홀이 예이다.) 위에 표시된 exception program이 호출될 수 있는 잠재적인 종료 문제가 부각된다.

이 문제를 해결하려면 특정 횟수의 bounce 이후에는 ray 전송을 중지하도록 반사 코드를 수정해야 한다. ray paload의 깊이 변수를 사용하여 재귀 깊이를 추적한다. 이 깊이가 사용자가 지정한 임계값을 초과하면 reflect ray를 보내지 않는다. tutorial 예제에서는 최대 100개의 bounce를 ㅅ전송하므로 거의 모든 scene에 적합하다.

RT_PROGRAM void floor_closest_hit_radiance4()
{
    // Calculate direct lighting using Phong shading, as
    // shown above
    ...
    if(prd_radiance.depth < max_depth) {
        PerRayData_radiance refl_prd;
        refl_prd.depth = prd_radiance.depth+1;
        float3 R = reflect( ray.direction, ffnormal );
        OptiX::Ray refl_ray( hit_point, R, 0, scene_epsilon );
        rtTrace(top_object, refl_ray, refl_prd);
        color += reflectivity * refl_prd.result;
    }
    prd_radiance.result = color;
}

한 가지 간단한 수정만으로도 많은 scene에서 성능을 크게 향상시킬 수 있다. 게다가 ray의 깊이를 추적하는 것외도 광선의 '중요도'를 추적한다. 중요도는 색의 energy가 최종 색에 얼마나 추가되는지를 추적한다. 이를 추적하기 위해 ray payload는 1.0으로 초기화한 다른 변수를 사용하고 매번 bounce할 때마다 relectivity를 곱한다. 그런 다음 반사 코드에 최종 조건을 추가하여 예상 기여도가 너무 어두울 때 반사를 보내지 않도록 한다. 또 다른 함수인 luminance는 이 중요도를 계산하기 위해 색상의 밝기 값을 계산하는 데 사용된다.

1.6 Tutorial 5 - Enviroment Mapping

이제 reflection을 활성화했으므로 scene에 enviroment map을 추가하여 scene을 훨씬 더 재미있게 만들 수 있다. 이 경우 세 번째 유형의 선언을 사용한다.

rtTextureSampler<float4, 2> envmap;

host에서 이 texture는 파일에서 읽은 이미지에 binding된다. 그런 다음 miss program을 수정하여 ray direction의 위도와 경도를 계산하고 host에서 생성한 enviroment map에서 색상을 조회한다.

RT_PROGRAM void envmap_miss()
{
    float theta = atan2f( ray.direction.x, ray.direction.z );
    float phi   = M_PIf * 0.5f -  acosf( ray.direction.y );
    float u     = (theta + M_PIf) * (0.5f * M_1_PIf);
    float v     = 0.5f * ( 1.0f + sin(phi) );
    prd_radiance.result = make_float3( tex2D(envmap, u, v) );
}

배경에 High-dynamic range picture를 사용하면 miss program을 수정할 필요가 없지만, 더 멋진 반사를 얻을 수 있다.

1.7 Tutorial 6 - Fresnel Reflectance

표면에 스치는 각도로 부딪히는 ray가 표면에 수직에 가까운 광선보다 더 많이 반사되는 Fresnel effect에 대한 근사치를 사용하면 반사를 더욱 풍부하게 만들 수 있다. 이 효과는 왁스 칠한 바닥, 자동차 페인트, 유리와 같은 많은 실제 소재에서 볼 수 있다.

표면 법선과 들어오는 ray direction 사이의 각도를 기반으로 이 근사치를 계산하기 위해 내장된 schlick 함수를 사용한다. 이 결과 중요도를 감쇠시키고 재귀 광선으로 계산된 반사를 변조하는 데 사용된다. tutorial을 실행하고 바닥을 스치는 각도에서 바라보면 이 효과를 확인할 수 있다.

RT_PROGRAM void floor_closest_hit_radiance5()
{
    ...
    float3 r = schlick(-dot(ffnormal, ray.direction),
                       reflectivity_n);
    float importance = prd_radiance.importance*luminance(r);

    if(importance > importance_cutoff &&
        prd_radiance.depth < max_depth) {
        PerRayData_radiance refl_prd;
        refl_prd.importance = importance;
        refl_prd.depth = prd_radiance.depth+1;
        float3 R = reflect( ray.direction, ffnormal );
        OptiX::Ray refl_ray( hit_point, R, radiance_ray_type,
                             scene_epsilon );
        rtTrace(top_object, refl_ray, refl_prd);
        color += r * refl_prd.result;
    }
    prd_radiance.result = color;
}

1.8 Tutorial 7 - Simple Procedural Texture

여기까지 보면 OptiX program이 임의의 계산을 수행할 수 있다는 것을 알 수 있을 것이다. object에 시각적 디테일을 추가하는 간단한 메커니즘 중 하나는 특정 함수를 기반으로 material attribute를 변조하는 것이다. 이 section에서는 모듈 식 연산을 사용하여 간단한 타일 패턴을 시뮬레이션 한다. 이 산술의 결과는 타일 색상과 균일 색상 중에서 선택하는 데 사용한다. 이 색상은 이전 예제에서 사용자가 지정한 단일 material reflectivity를 대신 사용된다.

RT_PROGRAM void floor_closest_hit_radiance()
{
    ...
    float3 hit_point = ray.origin + t_hit * ray.direction;
    float v0 = dot(tile_v0, hit_point);
    float v1 = dot(tile_v1, hit_point);
    v0 = v0 - floor(v0);
    v1 = v1 - floor(v1);
    float3 local_Kd;
    if( v0 > crack_width && v1 > crack_width ){
        local_Kd = Kd;
    } else {
        local_Kd = crack_color;
    }
    // Shade with calculated Kd
    ...
}

1.9 Tutorial 8 - Complex Procedural Texture

procedural texture는 복잡한 물리적 현상을 시뮬레이션 할 수 있다. 이 경우 Larry Gritz가 작성한 정교한 renderman shader를 옵틱스로 바꿨다. 이 shader는 상단히 복잡하므로 여기서는 수학을 설명하지 않겠다. 관심 있는 독자는 larry gritz의 전체 소스를 보려면 RenderMan repository(http://renderman.org/RMR/Shaders/LGShaders/LGRustyMetal.sl)를 참조하기 바란다.

RT_PROGRAM void box_closest_hit_radiance()
{
    float3 world_geo_normal   = normalize(rtTransformNormal(
                                          RT_OBJECT_TO_WORLD,
                                          geometric_normal));
    float3 world_shade_normal = normalize(rtTransformNormal(
                                          RT_OBJECT_TO_WORLD,
                                          shading_normal));
    float3 ffnormal = faceforward(world_shade_normal,
				  -ray.direction,
				  world_geo_normal );
    float3 hit_point = ray.origin + t_hit * ray.direction;

    // Sum several octaves of abs(snoise), i.e. turbulence.
    // Limit the number of octaves by the estimated change in
    // PP between adjacent shading samples.
    float3 PP = txtscale * hit_point;
    float a = 1.0f;
    float sum = 0.0f;
    for(int i = 0; i < MAXOCTAVES; i++ ){
        sum += a * fabs(snoise(PP));
        PP *= 2;
        a *= 0.5;
    }

    // Scale the rust appropriately, modulate it by another
    // noise computation, then sharpen it by squaring its 
    // value.
    float rustiness = step (1-rusty, clamp (sum,0.0f,1.0f));
    rustiness *= clamp (abs(snoise(PP)), 0.0f, .08f) / 0.08f;
    rustiness *= rustiness;

    // If we have any rust, calculate the color of the rust,
    // taking into account the perturbed normal and shading
    // like matte.
    float3 Nrust = ffnormal;
    if(rustiness > 0) {
        // If it's rusty, also add a high frequency bumpiness
        //to the normal
        Nrust = normalize(ffnormal + rustbump * snoise(PP));
        Nrust = faceforward (Nrust, -ray.direction,
                             world_geo_normal);
    }

    float3 color = mix(metalcolor * metalKa, rustcolor *
                       rustKa, rustiness) * ambient_light_color;

    for(int i = 0; i < lights.size(); ++i) {
        BasicLight light = lights[i];
        float3 L = normalize(light.pos - hit_point);
        float nmDl = dot(ffnormal, L);
        float nrDl = dot(Nrust, L);

        if( nmDl > 0.0f || nrDl > 0.0f ){
            // cast shadow ray
	    PerRayData_shadow shadow_prd;
	    shadow_prd.attenuation = 1.0f;
	    float Ldist = length(light.pos - hit_point);
	    OptiX::Ray shadow_ray(hit_point, L, 1,
				  scene_epsilon, Ldist);
	     rtTrace(top_shadower, shadow_ray, shadow_prd);
	    float light_attenuation = shadow_prd.attenuation;

	    if( light_attenuation > 0.0f ){
		float3 Lc = light.color * light_attenuation;
		nrDl = max(nrDl * rustiness, 0.0f);
		color += rustKd * rustcolor * nrDl * Lc;

		float r = nmDl * (1.0f-rustiness);
		if(nmDl > 0.0f){
		    float3 H = normalize(L - ray.direction);
		    float nmDh = dot( ffnormal, H );
		    if(nmDh > 0)
			color += r * metalKs * Lc *
				pow(nmDh, 1.f/metalroughness);
		}
	    }
	}
    }

    float3 r = schlick(-dot(ffnormal, ray.direction),
			reflectivity_n * (1-rustiness));
    float importance = prd_radiance.importance*luminance(r);

    // reflection ray
    if(importance > importance_cutoff &&
	rd_radiance.depth < max_depth) {
	PerRayData_radiance refl_prd;
	refl_prd.importance = importance;
	refl_prd.depth = prd_radiance.depth+1;
	float3 R = reflect( ray.direction, ffnormal );
	OptiX::Ray refl_ray( hit_point, R, 0, scene_epsilon );
	rtTrace(top_object, refl_ray, refl_prd);
	color += r * refl_prd.result;
    }

    prd_radiance.result = color;
}

1.10 Tutorial 9 - Procedural Geometry

optiX의 강력한 기능 중 하나는 새로운 기하학적 primitive를 추가하는 기능이다. 이 기능은 거의 모든 종류의 triangle vertex 형식을 수용하는 데 사용할 수 있어 모든 데이터 구조에 저장된 모델을 쉽게 직접 렌더링 할 수 있을 뿐만 아니라 light custom primitive를 추가하는 데에도 사용할 수 있다.

여기서는 이 매커니즘을 사용하여 "convex hull" primitive를 추가하였다. 이제 ray가 object와 교차하는지, 교차한다면 ray를 따라 어느 지점에서 첫 번째 교차점이 있는지 결정하는 intersection program을 작성하기만 하면 된다. 이 program은 rtTrace가 호출될 때마다 실행되며 AS에서 ray가 object 근처이 있는지 확인한다.

contex hull는 관심 영역을 경계로 하는 방향이 지정된 평면 집합으로 정의할 수 있다. ray는 ray가 들어오는 모든 평면 중 마지막 평면을 교차하면 object에 들어가고 ray가 나가는 모든 평면 중 첫 번째 평면을 교차하면 object에서 나온다. ray가 각 평면에 들어오는지 또는 나가는지를 결정하기 위해 평면 법선과 ray direction 사이의 dot product의 부호를 사용한다. loop는 단순히 마지막으로 들어온 평면과 처음 나온 평면을 추적한다. 또한 각 평면에 연결된 법선은 새로운 진입점 또는 출구점이 될 때 유지된다.

rtBuffer<float4> planes;
RT_PROGRAM void chull_intersect(int primIdx)
{
    int n = planes.size();
    float t0 = -FLT_MAX;
    float t1 = FLT_MAX;
    float3 t0_normal = make_float3(0);
    float3 t1_normal = make_float3(0);
    for(int i = 0; i < n && t0 < t1; ++i ) {
	float4 plane = planes[i];
	float3 n = make_float3(plane);
	float  d = plane.w;

	float denom = dot(n, ray.direction);
	float t = -(d + dot(n, ray.origin))/denom;
	if( denom < 0){
	    // enter
	    if(t > t0){
		t0 = t;
		t0_normal = n;
	    }
	} else {
	    //exit
	    if(t < t1){
		t1 = t;
		t1_normal = n;
	    }
	}
    }
    if(t0 > t1)
    return;
// code continued below

진입점(t0)이 출구점(t1)보다 크면 ray가 object를 놓친 것이므로 이 함수가 반환된다.

그렇지 않으면 OptiX runtime에 보고된다. 이 과정은 두 단계로 진행된다. 먼저, rtPotentialIntersection은 교차점이 ray에 대해 유효한 t 간격 내에 있는지 확인한다. true를 반환하면 intersection program은 object와 관련된 모든 속성(이 경우 음영 및 기하학적 노멀)을 계산한다. 마지막으로 rtReportIntersection은 속성이 완료되었음을 OptiX runtime에 보고한다. 이 함수의 parameter는 이 교차점과 연관된 material number를 지정한다. 이 parameter는 양면 쉐이딩, triangle mesh에서 인덱싱된 material등을 구현하는데 사용할 수 있다. 이 시점에서 OptiX는 이 object 및 material와 연관된 any-hit program이 있는 경우 실행된다.

// intersection program continued from above
    if(rtPotentialIntersection( t0 )){
	shading_normal = geometric_normal = t0_normal;
	rtReportIntersection(0);
    } else if(rtPotentialIntersection( t1 )){
	shading_normal = geometric_normal = t1_normal;
	rtReportIntersection(0);
    }
}

intersection program 외에도 이 primitive에 대한 axis-sort bound box를 계산하는 bounding program을 제공해야 한다. 이 프로그램은 단일 object만 생성하므로(예를 들어 triangle mesh가 아닌) primIdx 매개변수는 무시된다.

RT_PROGRAM void chull_bounds (int primIdx, float result[6])
{
    OptiX::Aabb* aabb = (OptiX::Aabb*)result;
    aabb->m_min = chull_bbmin;
    aabb->m_max = chull_bbmax;
}

primitive bound 계산은 이 예제에서 host code에서 수행되므로 bounding program은 host가 제공한 chull_bbmin 및 chull__bbmax 변수만 반환한다.

여기에 표시된 육각형은 8개의 평면(총 32개의 float)으로 구성되며, 이는 기하학적으로 20개의 삼각형(정점 12개float 3개+ index 20개 정수3 개 또는 약 3배의 데이터)와 동일하므로 저장 공간을 크게 줄일 수 있다. 또한 ray를 convex hull와 교차시키면 삼각형에 해당하는 계산보다 훨씬 적은 계산이 필요하다. 이러한 절감 효과가 전체 런타임으로 이어지는 방식은 AS의 품질이나 material shader의 계산 비용과 같은 여러 요인에 따라 달라진다. 적절한 상황에서 프로그래밍 가능한 object intersection point는 optix ray tracing system을 확장하는데 매우 강력한 매커니즘이 된다.

1.11 Tutorial 10 - shadowing Transparent Objects

OptiX any-hit program의 유연성을 보여주기 위해 glass obelisk의 그림자를 수정하여 이 부분적인 shadow를 드리우도록 하겠다 .이는 물리 기반 유리 시뮬레이션은 아니지만 매우 빠르게 계산할 수 있으며 장면에 상당한 사실감을 더한다. 가성 물질과 같은 더 복잡한 효과도 OptiX를 사용하여 시뮬레이션 할 수 있지만, 이는 tutorial의 범위를 벗어나며 일반적으로 많은 수의 광선을 추적해야 한다.

RT_PROGRAM void glass_any_hit_shadow()
{
    float3 world_normal = normalize(rtTransformNormal(
				    RT_OBJECT_TO_WORLD,
				    shading_normal));
    float nDi = fabs(dot(world_normal, ray.direction));

    prd_shadow.attenuation *= 1-fresnel_schlick(nDi, 5,
			      1-shadow_attenuation, 1);

    rtIgnoreIntersection();
}

이 program은 tutorial 3에 표시된 불투명 any-hit program과 유사하지만 두 가지 주요 차이점이 있다. 첫째, 사실적인 반사를 제공하기 위해 사용한 것과 동일한 Schlick Fresnel 근사법을 기반으로 광선의 분수 감쇠를 계산한다. 둘째, ray를 종료하는 대신 rtIgnoreIntersection을 사용하여 ray이 계속되도록 한다. 이 예제에서는 유리 블록의 그림자와 있는 모든 광선에 대해 glass_any_hit_shadow program이 object에 들어올 떄와 나갈 때 한 번씩 두 번 계산된다.

1.12 Tutorial 11 - Environment Map Camemra

마지막 tutorial에서는 첫 번째 예제에서 pinhole camera ray generation program을 수정하여 OptiX(및 일반적으로 ray tracing)의 유연성을 보여준다. 이 새로운 camera는 ray를 구형 분포로 촬영하여 다른 program에서 환경 맵으로 용하거나 tutorial 5단계에서 배경 이미지로 사용할 수 있는 이미지를 생성한다.

이 program은 pinhole camera의 동일한 U,V,W 기준과 eye point를 사용하므로 mouse control을 사용하여 장면을 계속 조작할 수 있다.

RT_PROGRAM void env_camera()
{
    size_t2 screen = output_buffer.size();

    float2 d = make_float2(launch_index) /
		make_float2(screen) *
		make_float2(2.0f * M_PIf , M_PIf) +
		make_float2(M_PIf, 0);
    float3 angle = make_float3( cos(d.x) * sin(d.y),
				-cos(d.y),
				sin(d.x) * sin(d.y));
    float3 ray_origin = eye;
    float3 ray_direction = normalize(angle.x*normalize(U) +
  				     angle.y*normalize(V) +
				     angle.z*normalize(W));

    OptiX::Ray ray(ray_origin, ray_direction,
		   radiance_ray_type, scene_epsilon);

    PerRayData_radiance prd;
    prd.importance = 1.f;
    prd.depth = 0;

    rtTrace(top_object, ray, prd);

    output_buffer[launch_index] = make_color( prd.result );
}

1.13 Next Steps

이 tutorial은 OptiX로 수행할 수 있는 작업의 시작에 불과하다. tutorial program을 사용하여 더 자세히 살펴볼 것을 권장한다. sphere primitive를 추가하거나 SDK와 함께 제공된 것을 사용한다. shadow payload를 수정하여 단일 float 대신 색상 값 감쇠를 사용하고 이를 사용하여 유리 그림자가 녹색으로 착색되도록 한다. 샘플링 메커니즘으로 ambient occlusion을 계산할 수 있다. 수정된 shading 및 camera program set는 무차별 경로 추적을 수행할 수 있다. ray를 삼각형과 교차시키는 프로그램을 작성하고 mesh object를 import할 수 있다.

이러한 개념과 더 많은 개념을 보여주는 예제가 OptiX SDK에 포함되어 있다. 또한 SDK는 random number streams, selector programs, texture maps, meshed objects 및 Acceleration structure를 사용하는 방법도 보여준다. 또한 OptiX 2.0은 raytracing buffer와 OpenGL 또는 DirectX buffer 간의 상호 운용성을 추가하여 hybrid rendering 기법 또는 raytracing image를 텍스처 맵으로 zero-copy 사용할 수 있도록 지원한다. 이 샘플은 고성능 ray tracing 기반 소프트웨어를 직접 제작하는 데 유용한 기술 source이다.

0개의 댓글