ashen-aetna-ko

Ashen Aetna

— 잿빛 화산 위에서 서툴게 비틀거리기

(3D 그래픽스, Rust, Vulkan, ash에 대한/속의/관한/와 함께하는 튜토리얼)

첫 번째 삼각형

PipelineInputAssembly와 PrimitiveTopology

이제 우리는 여섯 개의 개별적인 점을 그리고 있습니다. 하지만 단일하고 고립된 점보다는 솔리드(solid) 객체를 그리는 것이 더 일반적입니다. 우리가 볼 수 있는 솔리드 객체는 바로 그 표면입니다.

모든 표면은 평평합니다. 네, 이 말은 모든 함수는 선형이라는 말과 거의 비슷한 수준의 진실입니다. 평평한 표면이나 선형 맵은 다루기 비교적 쉽기 때문에 편리한 허구이지만, 그럼에도 불구하고 허구입니다. 그리고 더 중요하게는, “무한히 가까이에서 볼 수 있다면” 대략적으로 사실입니다.

점 하나만으로는 평평한 표면, 즉 평면을 묘사하기에 충분하지 않으며, 두 개의 점도 마찬가지입니다. 반면에 세 개의 점은…

그리고 우리는 모든 (평평한) 표면을 삼각형으로 나눌 수 있습니다. 따라서 본질적으로 모든 지오메트리(geometry)는 삼각형의 집합으로 표현됩니다.

우리는 프로그램에게 삼각형을 그리고 싶다고, 즉 세 개의 버텍스(정점)를 하나의 삼각형으로 해석하라고 알려주어야 합니다. 다시 말해, 우리는 입력이 기하학적 객체로 어셈블(조립)되는 방식을 바꾸고 싶습니다. Pipeline::init() 함수에서 우리는 현재 “입력 어셈블리 상태”를 POINT_LIST로 설정하고 있습니다. 이것을 삼각형(TRIANGLE_LIST)으로 바꿔봅시다.

        let input_assembly_info = vk::PipelineInputAssemblyStateCreateInfo::builder()
            .topology(vk::PrimitiveTopology::TRIANGLE_LIST);

그리고… 이게 전부입니다. (지난 장의 마지막 부분에서) 코드에 더 이상의 변경은 필요하지 않습니다. 우리는 멋진 녹색 삼각형 하나를 얻게 됩니다.

점을 더 추가해 봅시다. 이전 장을 통해 우리는 이 작업을 어떻게 하는지 알고 있습니다. 버퍼에 데이터를 더 붓고 드로우 콜(draw call)에서 버텍스의 수를 조정하기만 하면 됩니다.

버퍼:

        let buffer1 = Buffer::new(
            &allocator,
            96,
            vk::BufferUsageFlags::VERTEX_BUFFER,
            vk_mem::MemoryUsage::CpuToGpu,
        )?;
        buffer1.fill(
            &allocator,
            &[
                0.5f32, 0.0f32, 0.0f32, 1.0f32, 0.0f32, 0.2f32, 0.0f32, 1.0f32, -0.5f32, 0.0f32,
                0.0f32, 1.0f32, -0.9f32, -0.9f32, 0.0f32, 1.0f32, 0.3f32, -0.8f32, 0.0f32, 1.0f32,
                0.0f32, -0.6f32, 0.0f32, 1.0f32,
            ],
        )?;
        let buffer2 = Buffer::new(
            &allocator,
            120,
            vk::BufferUsageFlags::VERTEX_BUFFER,
            vk_mem::MemoryUsage::CpuToGpu,
        )?;
        buffer2.fill(
            &allocator,
            &[
                15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32, 15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32,
                15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32, 1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32,
                1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32, 1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32,
            ],
        )?;

드로우 콜:

            logical_device.cmd_draw(commandbuffer, 6, 1, 0, 0);

이제 두 개의 삼각형이 보입니다.

그런데, 만약 6개가 아닌 5개의 버텍스만 보낸다면 어떻게 될까요? 한 번 시도해 봅시다 (버퍼까지 수정할 필요는 없습니다):

            logical_device.cmd_draw(commandbuffer, 5, 1, 0, 0);

다시 삼각형 하나만 보입니다. 말이 되죠. 첫 세 개의 점이 첫 번째 삼각형을 형성합니다. 나머지 두 개의 점은… 글쎄요, 삼각형을 이루지 못하므로 무시됩니다. 숫자를 다시 6으로 바꾸고 계속 진행합시다.

점 목록이나 삼각형 목록 외에도 다른 종류의 “토폴로지”가 있습니다. 예를 들어, (각각 두 개의 버텍스를 사용하는) 선 목록(list of lines)을 사용할 수 있습니다. 선을 생각해보면, 만약 선을 사용하고 P1, P2, P3, P4, P5, P6 점들을 보낸다면, P1과 P2 사이에 선 하나, P3과 P4 사이에 또 다른 선 하나, P5와 P6 사이에 선 하나를 얻게 될 것입니다. (실제로, 입력 토폴로지에 관한 줄을 .topology(vk::PrimitiveTopology::LINE_LIST);로 바꿔서 시도해볼 수 있습니다.) P1에서 P2로, P2에서 P3로, 이런 식으로 끊김 없이 연결하고 싶을 수도 있다는 것을 쉽게 상상할 수 있습니다. 이것 또한 가능합니다: vk::PrimitiveTopology::LINE_STRIP 입니다. 비슷하게, 이전의 몇몇 점들을 재사용하는 삼각형을 위한 변형도 있습니다. 그림과 함께 개요를 보려면 Vulkan 사양: 프리미티브 토폴로지의 하위 섹션을 참조할 수 있습니다. (이 중 일부는 지금 우리에게 의미가 없는데, 우리가 지오메트리 셰이더를 사용하고 있지 않기 때문입니다.)

이 설정들을 가지고 충분히 실험해 보셨다면, 코드를 다시 TRIANGLE_LIST로 되돌려 놓으세요.

PolygonFillMode와 DeviceFeatures

그럼에도 불구하고, 가끔은 “와이어프레임 모드”로 전환하여 삼각형의 외곽선만 표시하는 것이 유용할 수 있습니다. 이전 문단에서처럼 TRIANGLE_LISTLINE_LIST로 교체하는 것은 이에 적합하지 않을 것입니다. 삼각형 하나가 LINE_LIST로는 선 1.5개에 해당하거나, LINE_STRIP으로는 각 삼각형의 세 번째 버텍스와 다음 삼각형의 첫 번째 버텍스 사이에 연결이 생길 것입니다. 이런 변경을 위해 (아마도 빠른 디버깅 확인을 위해) 버텍스 목록을 재배열하는 것은 실용적이지 않습니다. 그렇다면, 삼각형 목록을 유지하면서 무언가를 할 수 있을까요?

네, 그렇지 않았다면 제가 아마 여기서 이 문제를 꺼내지 않았을 겁니다.

또한 파이프라인 초기화 시, 우리 옛 코드에서는 래스터라이저 단계에 대한 다음 설정을 찾을 수 있습니다:

        let rasterizer_info = vk::PipelineRasterizationStateCreateInfo::builder()
            .line_width(1.0)
            .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
            .cull_mode(vk::CullModeFlags::NONE)
            .polygon_mode(vk::PolygonMode::FILL);

여기 마지막 줄에서 PolygonMode::FILLPolygonMode::LINE (또는 PolygonMode::POINT까지도)으로 바꿀 수 있습니다. 그러면 삼각형이 선이나 점으로만 그려집니다. 그것들은 여전히 삼각형입니다. (6개의 점 대신 5개나 2개만 그려보면 이를 확인할 수 있습니다. 불완전한 삼각형의 한두 점에 대한 데이터가 제공되었더라도 전체 삼각형이 그려지지 않는 것을 볼 수 있습니다.)

제가 “그러면 삼각형이 …으로 그려집니다”라고 말했지만 — 실제로는 다른 일이 동시에 일어나고 있습니다. 유효성 검사 레이어(validation layers)가 불평을 합니다:

[Debug][error][validation] "vkCreateGraphicsPipelines parameter, VkPolygonMode pCreateInfos->pRasterizationState->polygonMode cannot 
be VK_POLYGON_MODE_POINT or VK_POLYGON_MODE_LINE if VkPhysicalDeviceFeatures->fillModeNonSolid is false."

적어도 제 컴퓨터에서는 여전히 작동합니다. 하지만 우리는 보고된 이 오류(사실은 모든 오류)를 처리해야 합니다.

이 오류의 배경은 무엇일까요?

모든 GPU가 가지고 있지 않아서 당연하게 여겨서는 안 되는 몇 가지 특별한 기능들이 있습니다. 여기서 “특별한”이라는 말에 너무 큰 의미를 두지는 맙시다. 그중 몇몇은 꽤 평범해 보입니다. 하지만 명시적으로 활성화해야 합니다. (이것이 Vulkan의 핵심 설계 원칙 중 하나라고 생각합니다: 가능한 한 모든 것을 명시적으로 만들어라.)

먼저 이 기능들에 대해 알아봅시다. 물리 디바이스를 나열하고 선택하는 함수를 먼저 수정하여 다음과 같이 호출할 수 있도록 하겠습니다.

        let (physical_device, physical_device_properties, physical_device_features) =
            init_physical_device_and_properties(&instance)?;

이들을 얻는 것은 instance의 해당 메서드를 호출하는 것을 의미합니다:

fn init_physical_device_and_properties(
    instance: &ash::Instance,
) -> Result<
    (
        vk::PhysicalDevice,
        vk::PhysicalDeviceProperties,
        vk::PhysicalDeviceFeatures,
    ),
    vk::Result,
> {
    let phys_devs = unsafe { instance.enumerate_physical_devices()? };
    let mut chosen = None;
    for p in phys_devs {
        let properties = unsafe { instance.get_physical_device_properties(p) };
        let features = unsafe { instance.get_physical_device_features(p) };
        if properties.device_type == vk::PhysicalDeviceType::DISCRETE_GPU {
            chosen = Some((p, properties, features));
        }
    }
    Ok(chosen.unwrap())
}

(이것이 단지 튜토리얼 코드가 아니라 실제 제품용 코드라면) 어떤 디바이스를 사용할지 선택할 때 이 기능들을 고려하는 것이 매우 합리적일 것입니다. 저는 적어도 이 결과를 우리의 큰 Aetna 구조체에 저장할 것입니다. 하지만 어디에서도 이에 반응하지는 않을 겁니다 (예를 들어 파이프라인 생성을 준비할 때 선만 그리는 것이 가능한지 테스트하는 것처럼요 — 이것 역시 실제 애플리케이션에서는 해야 할 일일 것입니다).

우리는 여전히 그 기능을 요청해야 합니다. 이것은 논리 디바이스 생성 시, 즉 fn init_device_and_queues에서 일어납니다.

여기서 이것은 DeviceCreateInfo의 일부입니다:

    let device_create_info = vk::DeviceCreateInfo::builder()
        .queue_create_infos(&queue_infos)
        .enabled_extension_names(&device_extension_name_pointers)
        .enabled_layer_names(&layer_name_pointers)
        .enabled_features(&features);

여기서 사용할 기능은 다음과 같이 선택했습니다:

    let features = vk::PhysicalDeviceFeatures::builder().fill_mode_non_solid(true);

그리고 나면 다시 오류 메시지 없이 삼각형을 선이나 점으로만 볼 수 있습니다.

참고로, 만약 우리 GPU가 가지지 않은 기능을 요청하면 프로그램은 다음과 같은 메시지를 내며 충돌합니다:

Error: ERROR_FEATURE_NOT_PRESENT

자, 다시 PolygonMode::FILL로 돌아갑시다.

버텍스 셰이더에서 프래그먼트 셰이더로 가는 길에 무슨 일이 일어날까요?

현재, 우리는 크기와 색상 버텍스 버퍼를 다음 값들로 채우고 있습니다:

        buffer2.fill(
            &allocator,
            &[
                15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32, 15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32,
                15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32, 1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32,
                1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32, 1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32,
            ],
        )?;

짧게 말해, 첫 번째 삼각형은 녹색, 두 번째는 노란색입니다.

그리고 우리는 각 점의 색상을 전달하고 있지만, 삼각형 전체가 그 색으로 채워집니다. (이것이 당연한 결과입니다: 우리는 결국 PolygonMode::FILL을 선택했으니까요.)

하지만 만약 한 삼각형의 모든 버텍스가 같은 색이 아니라면 어떤 일이 일어날까요 (어떤 색이 선택될까요)?

알아봅시다. 여기서 저는 맨 마지막 점을 노란색 대신 파란색으로 설정했습니다.

        buffer2.fill(
            &allocator,
            &[
                15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32, 15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32,
                15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32, 1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32,
                1.0f32, 0.8f32, 0.7f32, 0.0f32, 1.0f32, 1.0f32, 0.0f32, 0.0f32, 1.0f32, 1.0f32,
            ],
        )?;

어떻게 보이나요?

색상은 첫 번째 (그리고 두 번째) 버텍스의 노란색에서 세 번째 버텍스의 파란색으로 비교적 부드럽게 변합니다.

우리는 버텍스 셰이더에서 프래그먼트 셰이더로 전달하는 색상 값, 즉 그 값이 보간(interpolated)된다는 것을 알게 됩니다. 그리고 삼각형 중간 어딘가에 있는 점/픽셀/프래그먼트들은 한 점의 색상 값을 받는 것이 아니라, 그 위치에 따라 모든 점들의 값들이 조합된 값을 받게 됩니다.

생각해보면, 이건 위치 변수에도 똑같이 일어나는 일 아닌가요?

그리고 보통은, 이것이 바로 우리가 원하는 것입니다.

이것이 원치 않는 효과인 경우, 이 동작을 끌 수 있습니다:

#version 450

layout (location=0) out vec4 theColour;

layout (location=0) flat in vec4 data_from_the_vertexshader;

void main(){
	theColour= data_from_the_vertexshader;
}

이전과의 차이점을 발견하셨나요? 작습니다: flat은 전체 삼각형이 한 버텍스의 값을 사용하게 만드는 키워드입니다. 어떤 버텍스일까요? 첫 번째 버텍스입니다. (“프로보킹 버텍스(provoking vertex)”라고 하며, 프리미티브 토폴로지에 관한 페이지의 다이어그램에 표시되어 있습니다.)

지금은 다시 flat을 제거합시다.

이제 삼각형 이상의 것을 봐야 할 것 같습니다. 먼저 상자는 어떨까요?

계속