IT 이모저모

Unity의 GPU 레이 트레이싱 - 1

exien 2018. 5. 8. 16:38

다음 기사는 원래 여기에 게시되었습니다 : http://blog.three-eyed-games.com/2018/05/03/gpu-ray-tracing-in-unity-part-1/

광선 추적을위한 정말 흥미로운 시간입니다. AI 가속 denoising 과 같은 최신 기술 DirectX 12 및 Peter Shirley의 기본 지원을 발표 한 Microsoft 는 유료로  을 발표하면서 레이 트레이싱과 같이 보이게 만들었습니다. 결국 법정에서 받아 들일 수있는 기회가되었습니다. 혁명의 시작에 대해 이야기하기에는 아직 시기상 일 수도 있지만 주제에 관한 지식을 배우고 구축하는 것은 좋은 생각입니다.

이 기사에서는 Unity에서 계산 쉐이더를 사용하여 매우 간단한 레이 트레이서를 처음부터 작성하려고합니다. 우리가 사용할 언어는 스크립트의 경우 C #이고 셰이더의 경우 HLSL입니다. 따라하면 다음과 같은 렌더링이 끝납니다.

광선 추적 이론

기본 광선 추적 이론을 빠르게 검토하여 시작하겠습니다. 익숙하다면 언제든지 건너 뛸 수 있습니다.

실세계에서 사진이 어떻게 출현하는지 생각해 봅시다 - 매우 단순화되었지만, 렌더링의 목적으로는 이것이 잘되어야합니다. 모두 광자를 방출하는 광원으로 시작합니다. 광자는 표면에 닿을 때까지 일직선으로 날아간다.이 지점에서 반사되거나 굴절되어 표면으로 흡수 된 에너지를 뺀 거리를 계속 지나간다. 결국 일부 광자가 카메라의 이미지 센서에 부딪혀 결과 이미지가 생성됩니다. 광선 추적은 기본적으로 사진 리얼리 스틱 이미지를 만들기 위해 이러한 단계를 시뮬레이션합니다.

실제로, 광원에 의해 방출되는 광자의 극히 일부만이 카메라에 충돌합니다. 따라서 헬름홀츠의 상호성 원칙을 적용하면 계산이 일반적으로 뒤집 힙니다. 광원에서 광자를 쏘는 대신 카메라에서 장면으로 광선을 반사하거나 굴절시켜 결국 광원에 도달시킵니다.

레이 트레이서는 Turner Whitted 의 1980 년 논문을 기반으로합니다 하드 그림자와 완벽한 반사를 시뮬레이션 할 수 있습니다. 또한 굴절, 확산 된 전역 조명, 광택 반사 및 부드러운 그림자와 같은보다 고급 효과를위한 기초 역할을합니다.

기본 설정

새로운 Unity 프로젝트를 만들어 보겠습니다. C # 스크립트 RayTracingMaster.cs와 계산 쉐이더를 RayTracingShader.compute만듭니다. 몇 가지 기본 코드로 C # 스크립트를 채 웁니다.

using UnityEngine;

public class RayTracingMaster : MonoBehaviour
{
    public ComputeShader RayTracingShader;

    private RenderTexture _target;

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Render(destination);
    }

    private void Render(RenderTexture destination)
    {
        // Make sure we have a current render target
        InitRenderTexture();

        // Set the target and dispatch the compute shader
        RayTracingShader.SetTexture(0, "Result", _target);
        int threadGroupsX = Mathf.CeilToInt(Screen.width / 8.0f);
        int threadGroupsY = Mathf.CeilToInt(Screen.height / 8.0f);
        RayTracingShader.Dispatch(0, threadGroupsX, threadGroupsY, 1);

        // Blit the result texture to the screen
        Graphics.Blit(_target, destination);
    }

    private void InitRenderTexture()
    {
        if (_target == null || _target.width != Screen.width || _target.height != Screen.height)
        {
            // Release render texture if we already have one
            if (_target != null)
                _target.Release();

            // Get a render target for Ray Tracing
            _target = new RenderTexture(Screen.width, Screen.height, 0,
                RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
            _target.enableRandomWrite = true;
            _target.Create();
        }
    }
}

이   OnRenderImage 함수는 카메라가 렌더링을 완료 할 때마다 Unity에 의해 자동으로 호출됩니다. 렌더링하려면 먼저 적절한 차원의 렌더링 타겟을 만들고 계산 쉐이더에 알려줍니다. 0은 계산 쉐이더의 커널 함수의 인덱스입니다. 우리는 하나만 가지고 있습니다.

다음으로 셰이더를 전달 합니다. 이것은 셰이더 코드를 실행하는 많은 스레드 그룹에서 GPU가 바쁘다고 말하고 있음을 의미합니다. 각 스레드 그룹은 셰이더 자체에 설정된 여러 스레드로 구성됩니다. 쓰레드 그룹의 크기와 수는 최대 3 차원으로 지정 될 수 있으므로 어느 한 차원의 문제에 계산 쉐이더를 쉽게 적용 할 수 있습니다. 여기서는 렌더 타겟의 픽셀 당 하나의 스레드를 생성하려고합니다. Unity 계산 쉐이더 템플릿에 정의 된 기본 스레드 그룹 크기 [numthreads(8,8,1)]는 8이므로 8 × 8 픽셀 당 하나의 스레드 그룹을 생성합니다. 마지막으로, 우리는 화면에 결과를 써 넣습니다 Graphics.Blit.

시도 해보자. RayTracingMaster컴퍼넌트를 씬의 카메라에 추가합니다 (이것은 중요합니다 OnRenderImage), 계산 쉐이더를 지정하고 재생 모드로 들어갑니다. Unity의 계산 쉐이더 템플릿의 출력을 아름다운 삼각형 프랙탈 형태로보아야합니다.

카메라

이제는 화면에 물건을 표시 할 수있게되었으므로 카메라 광선을 생성 해 봅시다. Unity가 우리에게 완벽하게 작동하는 카메라를 제공하기 때문에 계산 된 매트릭스를 사용하여이를 수행합니다. 먼저 셰이더에 행렬을 설정합니다. 스크립트에 다음 행을 추가하십시오 RayTracingMaster.cs.

private Camera _camera;

private void Awake()
{
    _camera = GetComponent<Camera>();
}

private void SetShaderParameters()
{
    RayTracingShader.SetMatrix("_CameraToWorld", _camera.cameraToWorldMatrix);
    RayTracingShader.SetMatrix("_CameraInverseProjection", _camera.projectionMatrix.inverse);
}

전화 SetShaderParameters에서 OnRenderImage렌더링하기 전에.

셰이더에서는 행렬, Ray구조 및 구조를 정의합니다 C #으로, 함수 나 변수 선언을 표시 할 필요는 달리, HLSL에 유의하시기 바랍니다 전에 이 사용됩니다. 각 화면 픽셀의 중심에 대해 광선의 원점과 방향을 계산하고 후자를 색상으로 출력합니다. 다음은 전체 쉐이더입니다.

#pragma kernel CSMain

RWTexture2D<float4> Result;
float4x4 _CameraToWorld;
float4x4 _CameraInverseProjection;

struct Ray
{
    float3 origin;
    float3 direction;
};

Ray CreateRay(float3 origin, float3 direction)
{
    Ray ray;
    ray.origin = origin;
    ray.direction = direction;
    return ray;
}

Ray CreateCameraRay(float2 uv)
{
    // Transform the camera origin to world space
    float3 origin = mul(_CameraToWorld, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz;
    
    // Invert the perspective projection of the view-space position
    float3 direction = mul(_CameraInverseProjection, float4(uv, 0.0f, 1.0f)).xyz;
    // Transform the direction from camera to world space and normalize
    direction = mul(_CameraToWorld, float4(direction, 0.0f)).xyz;
    direction = normalize(direction);

    return CreateRay(origin, direction);
}

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // Get the dimensions of the RenderTexture
    uint width, height;
    Result.GetDimensions(width, height);

    // Transform pixel to [-1,1] range
    float2 uv = float2((id.xy + float2(0.5f, 0.5f)) / float2(width, height) * 2.0f - 1.0f);

    // Get a ray for the UVs
    Ray ray = CreateCameraRay(uv);

    // Write some colors
    Result[id.xy] = float4(ray.direction * 0.5f + 0.5f, 1.0f);
}

속성에서 카메라를 회전하십시오. '화려한 하늘'이 그에 따라 적절히 작동해야합니다.

이제 색상을 실제 스카이 박스로 바꾸자. 저는 HDRI Haven의 Cape Hill 을 예제로 사용하고 있습니다 만, 물론 당신이 좋아하는 것을 사용할 수 있습니다. 다운로드하여 Unity에 놓습니다. 가져 오기 설정에서 2048보다 높은 해상도를 다운로드 한 경우 최대 해상도를 높이십시오. 이제 public Texture SkyboxTexture스크립트 에 a 를 추가 하고 속성에 텍스처를 지정하고 SetShaderParameters함수에 다음 줄을 추가하여 쉐이더에 설정합니다 .

RayTracingShader.SetTexture(0, "_SkyboxTexture", SkyboxTexture);

셰이더에서 텍스처와 해당 샘플러, π 상수를 정의하고 잠시 후에 사용합니다.

Texture2D<float4> _SkyboxTexture;
SamplerState sampler_SkyboxTexture;
static const float PI = 3.14159265f;

방향을 색상으로 쓰는 대신 skybox를 샘플링합니다. 이를 위해 우리는 직교 좌표 벡터를 구 좌표로 변환하고 이를 텍스처 좌표로 매핑합니다. 이것의 마지막 비트를 다음과 같이 바꿉니다 CSMain.

// Sample the skybox and write it
float theta = acos(ray.direction.y) / -PI;
float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f;
Result[id.xy] = _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, float2(phi, theta), 0);

트레이싱

여태까지는 그런대로 잘됐다. 이제 우리는 광선의 실제 추적을 얻고 있습니다. 수학적으로 우리는 광선과 씬 기하학의 교차점을 계산하고, 히트 매개 변수 (위치, 법선 및 광선을 따라 거리)를 저장합니다. 우리 광선이 여러 객체에 닿으면 우리는 가장 가까운 것을 선택합니다. RayHit셰이더 에서 구조체 를 정의 해 봅시다 .

struct RayHit
{
    float3 position;
    float distance;
    float3 normal;
};

RayHit CreateRayHit()
{
    RayHit hit;
    hit.position = float3(0.0f, 0.0f, 0.0f);
    hit.distance = 1.#INF;
    hit.normal = float3(0.0f, 0.0f, 0.0f);
    return hit;
}

일반적으로 장면은 많은 삼각형으로 구성되어 있지만 간단하게 시작합니다 : 무한한 지면과 소수의 영역을 교차 시키십시오!

접지면

y = 0에서 무한 평면을 가진 선을 교차시키는 것은 매우 간단합니다. 긍정적 인 광선 방향으로 만 조회를 허용하고 잠재적 인 이전 조회보다 근접하지 않은 조회는 거부합니다.

기본적으로 HLSL의 매개 변수는 참조가 아닌 값으로 전달되기 때문에 복사 작업을 수행하고 변경 내용을 호출 함수에만 전파 할 수 있습니다. 원본 구조체를 수정할 수 있도록 한정자를 전달 RayHit bestHit합니다 inout셰이더 코드는 다음과 같습니다.

void IntersectGroundPlane(Ray ray, inout RayHit bestHit)
{
    // Calculate distance along the ray where the ground plane is intersected
    float t = -ray.origin.y / ray.direction.y;
    if (t > 0 && t < bestHit.distance)
    {
        bestHit.distance = t;
        bestHit.position = ray.origin + t * ray.direction;
        bestHit.normal = float3(0.0f, 1.0f, 0.0f);
    }
}

그것을 사용하기 위해, 프레임 워크 Trace 함수를 추가해 봅시다  (잠시 후에 확장 할 것입니다) :

RayHit Trace(Ray ray)
{
    RayHit bestHit = CreateRayHit();
    IntersectGroundPlane(ray, bestHit);
    return bestHit;
}

게다가 기본 쉐이딩 함수가 필요합니다. 다시 말하지만, 우리는 통과 Ray에 inout- 우리는 우리가 반사에 대해 이야기 할 때 나중에 그것을 수정합니다. 디버그 목적으로 지오메트리가 정상이면 if를 반환하고 그렇지 않으면 스카이 박스 샘플링 코드로 돌아갑니다.

float3 Shade(inout Ray ray, RayHit hit)
{
    if (hit.distance < 1.#INF)
    {
        // Return the normal
        return hit.normal * 0.5f + 0.5f;
    }
    else
    {
        // Sample the skybox and write it
        float theta = acos(ray.direction.y) / -PI;
        float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f;
        return _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, float2(phi, theta), 0).xyz;
    }
}

두 함수를 모두 사용합니다 CSMainSkybox 샘플링 코드를 아직 제거하지 않았 으면 제거하고 다음 행을 추가하여 광선을 추적하고 적중을 음영 처리하십시오.

// Trace and shade
RayHit hit = Trace(ray);
float3 result = Shade(ray, hit);
Result[id.xy] = float4(result, 1);

구체

비행기가 세계에서 가장 흥미로운 것은 아니므로, 구체를 즉시 추가합시다. 선 - 구 교차점에 대한 수학은 Wikipedia 에서 찾을 수 있습니다 이번에는 두 개의 광선 공격 후보자가있을 수 있습니다 : 진입 점 p1 - p2과 종점 p1 + p2진입 점을 먼저 확인하고 다른 진입 점이 유효하지 않은 경우에만 종료점을 사용합니다. 우리의 경우 구는 float4위치 (xyz)와 반경 (w) 으로 구성된 것으로 정의됩니다 코드는 다음과 같습니다.

void IntersectSphere(Ray ray, inout RayHit bestHit, float4 sphere)
{
    // Calculate distance along the ray where the sphere is intersected
    float3 d = ray.origin - sphere.xyz;
    float p1 = -dot(ray.direction, d);
    float p2sqr = p1 * p1 - dot(d, d) + sphere.w * sphere.w;
    if (p2sqr < 0)
        return;
    float p2 = sqrt(p2sqr);
    float t = p1 - p2 > 0 ? p1 - p2 : p1 + p2;
    if (t > 0 && t < bestHit.distance)
    {
        bestHit.distance = t;
        bestHit.position = ray.origin + t * ray.direction;
        bestHit.normal = normalize(bestHit.position - sphere.xyz);
    }
}

구를 추가하려면 다음과 같이이 함수를 호출하면됩니다 Trace.

// Add a floating unit sphere
IntersectSphere(ray, bestHit, float4(0, 3.0f, 0, 1.0f));

안티 앨리어싱

현재 접근법에는 한 가지 문제가 있습니다. 각 픽셀의 중심 만 테스트하므로 결과에서 불쾌한 앨리어싱 효과 (두려움을주는 '톱니 모양')를 볼 수 있습니다. 이를 피하기 위해 픽셀 당 하나의 광선이 아니라 여러 개의 광선을 추적합니다. 각 광선은 픽셀 영역 내에서 임의의 오프셋을 가져옵니다. 허용되는 프레임 속도를 유지하기 위해 점진적 샘플링을 수행합니다. 즉, 각 프레임의 픽셀 당 하나의 광선을 추적하고 카메라가 움직이지 않으면 시간 경과에 따라 결과를 평균화합니다. 카메라가 움직일 때마다 (또는 시야, 장면 기하학 또는 장면 조명과 같은 다른 매개 변수가 변경 될 때마다), 우리는 처음부터 다시 시작해야합니다.

몇 가지 결과를 더하는 데 사용할 간단한 이미지 효과 셰이더를 만들어 보겠습니다. 셰이더의 이름을 지정 AddShader하고 첫 번째 줄을 읽습니다 Shader "Hidden/AddShader"Cull Off ZWrite Off ZTest Always추가 후 Blend SrcAlpha OneMinusSrcAlpha알파 블렌딩을 활성화합니다. 그런 다음 기본 frag함수를 다음 행 으로 대체하십시오 .

float _Sample;

float4 frag (v2f i) : SV_Target
{
    return float4(tex2D(_MainTex, i.uv).rgb, 1.0f / (_Sample + 1.0f));
}

이 쉐이더는 불투명도가 1 인 첫 번째 샘플을 그릴 것이고, 다음 샘플은 1/2, 그 다음 1/3 등등을 사용하여 모든 샘플의 평균을 동일하게 계산할 것입니다.

스크립트에서 샘플을 세고 새로 만든 이미지 효과 셰이더를 사용해야합니다.

private uint _currentSample = 0;
private Material _addMaterial;

_currentSamples = 0렌더 대상이 다시 빌드되면 다시 설정  InitRenderTexture하고 Update카메라 변형을 감지 하는 함수를 추가해야합니다 .

private void Update()
{
    if (transform.hasChanged)
    {
        _currentSample = 0;
        transform.hasChanged = false;
    }
}

커스텀 쉐이더를 사용하려면, 머티리얼을 초기화하고, 현재 샘플에 대해 알려주고 Render함수 의 스크린에 블리 팅하기 위해 사용해야합니다 :

// Blit the result texture to the screen
if (_addMaterial == null)
    _addMaterial = new Material(Shader.Find("Hidden/AddShader"));
_addMaterial.SetFloat("_Sample", _currentSample);
Graphics.Blit(_target, destination, _addMaterial);
_currentSample++;

따라서 점진적 샘플링을 수행하고 있지만 픽셀 센터를 항상 사용하고 있습니다. 컴퓨팅 쉐이더에서, 정의 float2 _PixelOffset와 그에서 사용하는 CSMain대신에 하드의 float2(0.5f, 0.5f)오프셋 (offset). 스크립트에서 다음 줄에 추가하여 임의의 오프셋을 만듭니다 SetShaderParameters.

RayTracingShader.SetVector("_PixelOffset", new Vector2(Random.value, Random.value));

카메라를 움직이면 이미지에 여전히 앨리어싱이 표시되지만 두 프레임 동안 정지해도 빠르게 사라집니다. 다음은 우리가 해낸 결과를 비교 한 것입니다.

반사

레이 트레이서의 기본 작업이 완료되었으므로 다른 렌더링 기술과는 별도로 레이 트레이싱을 실제로 설정 한 멋진 작업을 시작할 수 있습니다. 완벽한 반성은 우리 목록의 첫 번째 항목입니다. 아이디어는 간단합니다 : 우리가 표면에 부딪 칠 때마다, 우리는 당신이 학교에서 기억할 반사 법칙 (입사각 = 반사각)에 따라 광선을 반사시키고, 에너지를 줄이며, 우리가 하늘을 날 때까지 반복합니다. 에너지가 고갈되거나 일정량의 최대 바운스 후에

셰이더에서 a float3 energy를 광선에 추가 하고 CreateRay함수 에서이를 초기화합니다  ray.energy = float3(1.0f, 1.0f, 1.0f)광선은 모든 색상 채널에서 전체 처리량으로 시작하며 각 반사에 따라 감소합니다.

이제 우리는 최대 8 개의 트레이스 (원래 광선과 7 번의 반사)를 실행하고 Shade함수 호출 의 결과를 더할 것이지만 광선의 에너지를 곱합니다. 예를 들어 한 번 반사되어 그 에너지의 3/4을 잃은 광선을 상상해보십시오. 이제는 하늘을 쳐다보고 하늘에 닿으므로 하늘의 에너지 1/4을 픽셀로 전송합니다. CSMain이전 Trace과 Shade통화를 대체하여 다음 과 같이 조정하십시오 .

// Trace and shade
float3 result = float3(0, 0, 0);
for (int i = 0; i < 8; i++)
{
    RayHit hit = Trace(ray);
    result += ray.energy * Shade(ray, hit);

    if (!any(ray.energy))
        break;
}

우리의 Shade기능은 이제 에너지를 업데이트하고 반사 광선을 생성하는 책임 inout이 있습니다. 그래서 여기서 중요한 부분이 있습니다. 에너지를 업데이트하기 위해 우리는 표면의 반사 색상으로 원소 단위의 곱셈을 수행합니다. 예를 들어, 금은 대략적인 반사율을 가지 float3(1.0f, 0.78f, 0.34f)므로 100 %의 적색광, 녹색의 78 %, 34 %의 청색광을 반사하여 반사광에 뚜렷한 황금색을 부여합니다. 당신이 아무데도 아무데도 에너지를 창조하지 않을 것이기 때문에, 그 값들 중 하나를 가지고 1을 넘지 않도록 조심하십시오. 또한 반사율은 종종 생각하는 것보다 낮습니다. Naty Hoffman이 물리학 및 음영 수학의 슬라이드 64를 참조하십시오 .

HLSL은 주어진 법선을 사용하여 광선을 반사하는 inbuilt 함수를 가지고 있습니다. 부동 소수점 부정확 (floating point 부정확)으로 인해 반사 된 광선이 반사 된 표면에 의해 차단되는 경우가 있습니다. 이 자기 폐색을 방지하기 위해 우리는 정상 방향을 따라 조금만 위치를 상쇄합니다. 새로운 Shade기능 은 다음과 같습니다 .

float3 Shade(inout Ray ray, RayHit hit)
{
    if (hit.distance < 1.#INF)
    {
        float3 specular = float3(0.6f, 0.6f, 0.6f);

        // Reflect the ray and multiply energy with specular reflection
        ray.origin = hit.position + hit.normal * 0.001f;
        ray.direction = reflect(ray.direction, hit.normal);
        ray.energy *= specular;

        // Return nothing
        return float3(0.0f, 0.0f, 0.0f);
    }
    else
    {
        // Erase the ray's energy - the sky doesn't reflect anything
        ray.energy = 0.0f;

        // Sample the skybox and write it
        float theta = acos(ray.direction.y) / -PI;
        float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f;
        return _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, float2(phi, theta), 0).xyz;
    }
}

skybox의 intensity를 1보다 큰 factor로 곱함으로써 intensity를 조금 증가 시키길 원할 것입니다. 지금 당신의 Trace기능으로 놀아 라 루프에 몇 가지 구체를 넣으면 다음과 같은 결과가 나옵니다.

방향성 등

그래서 우리는 거울 같은 반사를 추적 할 수 있습니다. 이것은 부드러운 금속 표면을 렌더링 할 수있게 해주지 만, 비금속의 경우에는 확산 반사가 더 필요합니다. 간단히 말해, 금속은 반사광으로 반사되는 광선을 반사하는 반면, 비금속은 빛이 표면으로 굴절하고 산란하며 알베도 색으로 착색 된 임의의 방향으로 남겨 둡니다. 일반적으로 가정되는 이상적인 램버트 표면의 경우 확률은 상기 방향과 표면 법선 사이의 각의 코사인에 비례한다. 주제에 대한 심층적 인 토론은 여기에서 찾을 수 있습니다 .

확산 조명을 시작하기 위해,이를 추가 할 수 있도록 public Light DirectionalLight우리에게 RayTracingMaster장면의 방향 등을 지정합니다. Update카메라의 변형에 대해 이미 수행 한 것처럼 함수 의 변형 된 변형을 감지 할 수도 있습니다 이제 SetShaderParameters함수에 다음 행을 추가하십시오 .

Vector3 l = DirectionalLight.transform.forward;
RayTracingShader.SetVector("_DirectionalLight", new Vector4(l.x, l.y, l.z, DirectionalLight.intensity));

셰이더로 돌아가서 정의하십시오 float4 _DirectionalLight이 Shade함수에서 반사 색을 반사 색 아래에 정의하십시오.

float3 albedo = float3(0.8f, 0.8f, 0.8f);

이전 검은 색 리턴을 간단한 확산 음영으로 교체하십시오.

// Return a diffuse-shaded color
return saturate(dot(hit.normal, _DirectionalLight.xyz) * -1) * _DirectionalLight.w * albedo;

내적은 다음과 같이 정의됩니다 dot(a, b) = length(a) * length(b) * cos(theta)우리의 벡터 (법선 방향과 빛 방향)는 모두 단위 길이이므로, 점 제품은 정확히 우리가 찾고있는 각도의 코사인입니다. 광선과 빛은 반대 방향을 향하고 있기 때문에 정면 조명의 경우 내적 값은 1 대신 -1을 반환합니다.이를 보상하기 위해 부호를 뒤집어야합니다. 마지막으로, 우리는 음의 에너지를 방지하기 위해이 값을 포화시킵니다 (즉, [0,1] 범위로 고정).

방향성 빛이 그림자를 드리 우기 위해 그림자 광선을 추적합니다. 그것은 문제의 표면 위치에서 시작합니다 (셀프 섀도우 잉을 피하기 위해 아주 작은 변위로 다시). 그리고 빛이 오는 방향의 점. 어떤 것이 무한대로가는 길을 막는다면, 우리는 확산 빛을 사용하지 않을 것입니다. diffuse return 문 위에 다음 행을 추가하십시오.

// Shadow test ray
bool shadow = false;
Ray shadowRay = CreateRay(hit.position + hit.normal * 0.001f, -1 * _DirectionalLight.xyz);
RayHit shadowHit = Trace(shadowRay);
if (shadowHit.distance != 1.#INF)
{
    return float3(0.0f, 0.0f, 0.0f);
}

이제 우리는 광택있는 플라스틱 구체를 단단한 그림자로 추적 할 수 있습니다! Specular의 경우 0.04를 설정하고 알베도의 경우 0.8을 설정하면 다음 이미지가 생성됩니다.

장면 및 자료

오늘의 크레센도처럼 좀 더 복잡하고 다채로운 장면을 만들어 봅시다! 셰이더의 모든 것을 하드 코딩하는 대신 C #으로 장면을 정의하여 유연성을 높입니다.

먼저 RayHit셰이더 에서 구조 를 확장합니다 Shade함수 에서 머티리얼 속성을 전역 적으로 정의하는 대신 객체별로 정의하고이를에 저장할 것입니다 RayHit추가 float3 albedo 및 float3 specular구조체, 그리고 그들을 초기화 float3(0.0f, 0.0f, 0.0f)에서 CreateRayHit또한 하드 코딩 된 Shade값 hit대신 이 값을 사용 하도록 함수를 조정하십시오 .

CPU와 GPU에서 구가 무엇인지에 대한 일반적인 이해를 돕기 Sphere위해 셰이더와 C # 스크립트에서 구조체를 정의 하십시오. 쉐이더 측면에서는 다음과 같이 보입니다.

struct Sphere
{
    float3 position;
    float radius;
    float3 albedo;
    float3 specular;
};

C # 스크립트에서이 구조를 미러링합니다.

셰이더에서 우리는 IntersectSphere함수를 대신에 커스텀 구조체와 함께 작동 시킬 필요가  float4있습니다. 이것은 간단합니다 :

void IntersectSphere(Ray ray, inout RayHit bestHit, Sphere sphere)
{
    // Calculate distance along the ray where the sphere is intersected
    float3 d = ray.origin - sphere.position;
    float p1 = -dot(ray.direction, d);
    float p2sqr = p1 * p1 - dot(d, d) + sphere.radius * sphere.radius;
    if (p2sqr < 0)
        return;
    float p2 = sqrt(p2sqr);
    float t = p1 - p2 > 0 ? p1 - p2 : p1 + p2;
    if (t > 0 && t < bestHit.distance)
    {
        bestHit.distance = t;
        bestHit.position = ray.origin + t * ray.direction;
        bestHit.normal = normalize(bestHit.position - sphere.position);
        bestHit.albedo = sphere.albedo;
        bestHit.specular = sphere.specular;
    }
}

또한 설정 bestHit.albedo과 bestHit.specular에서 IntersectGroundPlane함수의 재료를 조정.

다음으로 정의하십시오 StructuredBuffer<Sphere> _Spheres이것은 CPU가 장면을 구성하는 모든 구체를 저장할 장소입니다. Trace함수 에서 하드 코드 된 모든 영역을 제거 하고 다음 행을 추가하십시오.

// Trace spheres
uint numSpheres, stride;
_Spheres.GetDimensions(numSpheres, stride);
for (uint i = 0; i < numSpheres; i++)
    IntersectSphere(ray, bestHit, _Spheres[i]);

이제 우리는 그 장면을 약간의 삶으로 채울 것입니다. C #으로 돌아가서 구형 배치와 실제 계산 버퍼를 제어하기위한 몇 가지 공용 매개 변수를 추가해 보겠습니다.

public Vector2 SphereRadius = new Vector2(3.0f, 8.0f);
public uint SpheresMax = 100;
public float SpherePlacementRadius = 100.0f;
private ComputeBuffer _sphereBuffer;

장면을 설정 OnEnable하고 버퍼를 안으로 놓습니다 OnDisable이렇게하면 임의의 장면이 구성 요소를 활성화 할 때마다 생성됩니다. 이 SetUpScene함수는 구체를 특정 반경에 배치하려고 시도하고 이미 존재하는 구체와 교차하는 것을 거부합니다. 구체의 반은 금속 (검은 알베도, 색깔의 반사)이고 나머지 절반은 비금속 (색이있는 알베도, 4 % 반사)입니다.

private void OnEnable()
{
    _currentSample = 0;
    SetUpScene();
}

private void OnDisable()
{
    if (_sphereBuffer != null)
        _sphereBuffer.Release();
}

private void SetUpScene()
{
    List<Sphere> spheres = new List<Sphere>();

    // Add a number of random spheres
    for (int i = 0; i < SpheresMax; i++)
    {
        Sphere sphere = new Sphere();

        // Radius and radius
        sphere.radius = SphereRadius.x + Random.value * (SphereRadius.y - SphereRadius.x);
        Vector2 randomPos = Random.insideUnitCircle * SpherePlacementRadius;
        sphere.position = new Vector3(randomPos.x, sphere.radius, randomPos.y);

        // Reject spheres that are intersecting others
        foreach (Sphere other in spheres)
        {
            float minDist = sphere.radius + other.radius;
            if (Vector3.SqrMagnitude(sphere.position - other.position) < minDist * minDist)
                goto SkipSphere;
        }

        // Albedo and specular color
        Color color = Random.ColorHSV();
        bool metal = Random.value < 0.5f;
        sphere.albedo = metal ? Vector3.zero : new Vector3(color.r, color.g, color.b);
        sphere.specular = metal ? new Vector3(color.r, color.g, color.b) : Vector3.one * 0.04f;

        // Add the sphere to the list
        spheres.Add(sphere);

    SkipSphere:
        continue;
    }

    // Assign to compute buffer
    _sphereBuffer = new ComputeBuffer(spheres.Count, 40);
    _sphereBuffer.SetData(spheres);
}

매직 넘버 40 new ComputeBuffer(spheres.Count, 40)은 우리 버퍼의 보폭, 즉 메모리에있는 하나의 구의 바이트 크기입니다. 이를 계산하려면 Spherestruct 에서 float 수를 세고 float의 바이트 크기 (4 바이트)로 곱합니다. 마지막으로 SetShaderParameters함수 의 셰이더에 버퍼를 설정합니다 .

RayTracingShader.SetBuffer(0, "_Spheres", _sphereBuffer);

결과

축하해, 너는 그것을 만들었다! 이제는 GPU 기반의 Whitted ray tracer를 사용하여 거울처럼 반사, 단순한 확산 조명 및 단단한 그림자로 비행기와 많은 구체를 렌더링 할 수 있습니다. 전체 소스 코드는 Bitbucket 에서 찾을 수 있습니다 구체 배치 매개 변수로 놀고 아름다운 경치를 즐기십시오 :

무엇 향후 계획?

오늘날 우리는 무언가를 성취했습니다. 그러나 보편적 인 전역 조명, 광택 반사, 부드러운 그림자, 굴절이있는 불투명하지 않은 재료, 그리고 분명히 구체 대신 삼각형 메쉬를 사용하는 방법이 많이 있습니다. 다음 기사에서 우리는 언급 된 현상을 극복하기 위해 Whitted ray tracer를 경로 추적기로 확장 할 것입니다.

'IT 이모저모' 카테고리의 다른 글

Google Map Fragment 만들기  (0) 2018.05.09
.NET Core 2.1 Preview 2 (향상된 네트워크 기능)  (0) 2018.05.09
화웨이 아너10, 5월 15일 글로벌 론칭  (0) 2018.05.08
LG 'G7 씽큐'  (0) 2018.05.03
boost Version 1.67.0  (0) 2018.05.03