ashen-aetna-ko

잿빛의 에트나(Ashen Aetna)

— 재 덮인 화산 위에서 서툴게 비틀거리며

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

법선(Normals)과 밝기

우리가 만든 구는 그다지 구처럼 보이지 않았습니다. 평평하고, 2D 같았죠. 3D가 아니라요. 그 이유는 무엇일까요? 3차원임을 암시하는 모든 시각적 단서가 빠져있었기 때문입니다.

가장 중요한 단서 중 하나는 빛(과 그림자)의 효과로 주어집니다. 표면의 밝기 차이는 표면이 광원을 향하고 있는지, 아니면 반대 방향을 향하고 있는지를 나타냅니다.

표면의 방향을 고려하려면, 먼저 그 방향을 알아야 합니다.

가장 간단한 첫 단계로, 버텍스 셰이더에서 방향을 계산해 봅시다. 구의 경우 이는 간단합니다. 왜냐하면 표면에서 바깥을 향하는 방향은 원점에서 바깥을 향하는 방향, 즉 위치 벡터와 같기 때문입니다. 우리는 이를 정규화하고(방향만 중요하므로 길이는 상관없습니다) 프래그먼트 셰이더로 전달합니다.

#version 450

layout (location=0) in vec3 position;
layout (location=1) in mat4 model_matrix;
layout (location=5) in vec3 colour;

layout (set=0, binding=0) uniform UniformBufferObject {
	mat4 view_matrix;
	mat4 projection_matrix;
} ubo;

layout (location=0) out vec4 colourdata_for_the_fragmentshader;
layout (location=1) out vec3 normal;

void main() {
    gl_Position = ubo.projection_matrix*ubo.view_matrix*model_matrix*vec4(position,1.0);
    colourdata_for_the_fragmentshader=vec4(colour,1.0);
    normal=normalize(position);
}

법선(normal)을 얻는 이 방법은 원점에 중심을 둔 구에서만 작동합니다 (위치와 법선의 관계는 다른 모양에서는 다릅니다).

프래그먼트 셰이더에서는 이 새로운 변수를 받고, 빛의 방향도 정의합니다 (빛을 향한 방향으로; 아주 멀리 있는 광원인 “방향성 광원(directional lights)”의 경우, 씬의 모든 지점에서 이 방향이 동일할 수 있습니다). 이 또한 정규화된 벡터로 정의합니다:

#version 450

layout (location=0) out vec4 theColour;

layout (location=0) in vec4 data_from_the_vertexshader;
layout (location=1) in vec3 normal;

void main(){
	vec3 direction_to_light=normalize(vec3(-1,-1,0));
	theColour= data_from_the_vertexshader;
}

마지막으로, 법선과 빛의 방향에 따라 theColour 값을 변경해야 합니다.

법선과 빛의 방향이 일치하면 밝아야 하고, 법선이 빛으로부터 멀어지면 빛이 없어야 합니다. 법선과 빛 방향의 내적(dot product)으로 색상을 조절할 수 있습니다:

	theColour= dot(normal,direction_to_light)*data_from_the_vertexshader;

아, 잠깐, 내적 값은 음수가 될 수도 있습니다. 이렇게 하는 게 더 낫겠네요:

	theColour= max(dot(normal,direction_to_light),0)*data_from_the_vertexshader;

이전보다 확실히 더 3차원적으로 보입니다.

물론, 이제 모델의 한 부분이 완전히 사라졌습니다 (빛이 없으니 놀랍지는 않지만, 아마도 원하던 결과는 아닐 겁니다). 그 부분에도 색상의 일부를 남겨둘 수 있습니다:

	theColour= 0.5*(1+max(dot(normal,direction_to_light),0))*data_from_the_vertexshader;

이것은 엄청난 꼼수이며 나중에 더 나은 방법을 사용할 것입니다.

지금으로서는, 법선을 모델의 일부로 만드는 것이 좋겠습니다. Model<V,I>의 파라미터 V[f32; 3]를 별도의 타입 VertexData로 바꿔봅시다.

#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct VertexData {
    pub position: [f32; 3],
    pub normal: [f32; 3],
}

특히 구를 정의하고 모델을 세분화할 때 사용했던 두 개의 헬퍼 함수입니다:

impl VertexData {
    fn midpoint(a: &VertexData, b: &VertexData) -> VertexData {
        VertexData {
            position: [
                0.5 * (a.position[0] + b.position[0]),
                0.5 * (a.position[1] + b.position[1]),
                0.5 * (a.position[2] + b.position[2]),
            ],
            normal: normalize([
                0.5 * (a.normal[0] + b.normal[0]),
                0.5 * (a.normal[1] + b.normal[1]),
                0.5 * (a.normal[2] + b.normal[2]),
            ]),
        }
    }
}
fn normalize(v: [f32; 3]) -> [f32; 3] {
    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
    [v[0] / l, v[1] / l, v[2] / l]
}

그리고 정이십면체와 구의 생성을 수정한 코드입니다 (정육면체는 일단 완전히 무시하겠습니다. ->Model<[f32;3],InstanceData>로 남아있습니다):

impl Model<VertexData, InstanceData> {
    pub fn icosahedron() -> Model<VertexData, InstanceData> {
        let phi = (1.0 + 5.0_f32.sqrt()) / 2.0;
        let darkgreen_front_top = VertexData {
            position: [phi, -1.0, 0.0],
            normal: normalize([phi, -1.0, 0.0]),
        }; //0
        let darkgreen_front_bottom = VertexData {
            position: [phi, 1.0, 0.0],
            normal: normalize([phi, 1.0, 0.0]),
        }; //1
        // ... (이하 생략) ...
        let purple_bottom_right = VertexData {
            position: [0.0, phi, 1.0],
            normal: normalize([0.0, phi, 1.0]),
        }; //11
        Model {
            vertexdata: vec![
                darkgreen_front_top,
                darkgreen_front_bottom,
                darkgreen_back_top,
                darkgreen_back_bottom,
                lightgreen_front_right,
                lightgreen_front_left,
                lightgreen_back_right,
                lightgreen_back_left,
                purple_top_left,
                purple_top_right,
                purple_bottom_left,
                purple_bottom_right,
            ],
            indexdata: vec![
                0, 9, 8, //
                // ... (이하 생략) ...
                6, 11, 7, //
            ],
            // ... (이하 생략) ...
        }
    }
    pub fn sphere(refinements: u32) -> Model<VertexData, InstanceData> {
        let mut model = Model::icosahedron();
        for _ in 0..refinements {
            model.refine();
        }
        for v in &mut model.vertexdata {
            v.position = normalize(v.position);
        }
        model
    }
    pub fn refine(&mut self) {
        let mut new_indices = vec![];
        let mut midpoints = std::collections::HashMap::<(u32, u32), u32>::new();
        for triangle in self.indexdata.chunks(3) {
            let a = triangle[0];
            let b = triangle[1];
            let c = triangle[2];
            let vertex_a = self.vertexdata[a as usize];
            let vertex_b = self.vertexdata[b as usize];
            let vertex_c = self.vertexdata[c as usize];
            let mab = if let Some(ab) = midpoints.get(&(a, b)) {
                *ab
            } else {
                let vertex_ab = VertexData::midpoint(&vertex_a, &vertex_b);
                let mab = self.vertexdata.len() as u32;
                self.vertexdata.push(vertex_ab);
                midpoints.insert((a, b), mab);
                midpoints.insert((b, a), mab);
                mab
            };
            // ... (mbc, mca도 유사하게 처리) ...
            let mca = if let Some(ca) = midpoints.get(&(c, a)) {
                *ca
            } else {
                let vertex_ca = VertexData::midpoint(&vertex_c, &vertex_a);
                let mca = self.vertexdata.len() as u32;
                midpoints.insert((c, a), mca);
                midpoints.insert((a, c), mca);
                self.vertexdata.push(vertex_ca);
                mca
            };
            new_indices.extend_from_slice(&[mca, a, mab, mab, b, mbc, mbc, c, mca, mab, mbc, mca]);
        }
        self.indexdata = new_indices;
    }
}

그런 다음 Aetna의 정의에서도 Model<VertexData,InstanceData>를 사용해야 하며, 버텍스 셰이더로 보내는 데이터에 법선을 포함시켜야 합니다. 이는 “location” 속성을 변경해야 함을 의미합니다:

#version 450

layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in mat4 model_matrix;
layout (location=6) in vec3 colour;

layout (set=0, binding=0) uniform UniformBufferObject {
	mat4 view_matrix;
	mat4 projection_matrix;
} ubo;

layout (location=0) out vec4 colourdata_for_the_fragmentshader;
layout (location=1) out vec3 out_normal;

void main() {
    gl_Position = ubo.projection_matrix*ubo.view_matrix*model_matrix*vec4(position,1.0);
    colourdata_for_the_fragmentshader=vec4(colour,1.0);
    out_normal=normal;
}

셰이더 변수 변경과 함께 Pipeline::init()도 변경해야 합니다:

        let vertex_attrib_descs = [
            vk::VertexInputAttributeDescription {
                binding: 0,
                location: 0,
                offset: 0,
                format: vk::Format::R32G32B32_SFLOAT,
            },
            vk::VertexInputAttributeDescription { // <--- 법선을 위한 새 설정
                binding: 0,
                location: 1,
                offset: 12,
                format: vk::Format::R32G32B32_SFLOAT,
            },
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 2, // <--- 중복을 피하기 위해 조정되었습니다. (이후도 동일)
                offset: 0,
                format: vk::Format::R32G32B32A32_SFLOAT,
            },
            // ... (이하 생략) ...
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 6,
                offset: 64,
                format: vk::Format::R32G32B32_SFLOAT,
            },
        ];
        let vertex_binding_descs = [
            vk::VertexInputBindingDescription {
                binding: 0,
                stride: 24, // <--- 여기서의 변경을 잊지 마세요.
                input_rate: vk::VertexInputRate::VERTEX,
            },
            vk::VertexInputBindingDescription {
                binding: 1,
                stride: 76,
                input_rate: vk::VertexInputRate::INSTANCE,
            },
        ];

이것만으로도 이미 작동합니다.

이제 이 줄에 대해 생각해 봅시다:

    out_normal=normal;

여기서는 버텍스 셰이더로 전달된 법선을 그냥 프래그먼트 셰이더로 넘기고 있습니다. 하지만 위치 벡터에는 다른 작업을 했습니다: 먼저 위치를 몇몇 행렬(모델, 뷰, 프로젝션 행렬)로 변환했죠.

아마도, 법선도 변환해야 할 것입니다. 수정 없이 그대로 두면, 이것은 모델에 상대적인 법선, 즉 모델 공간(model space)의 법선입니다. 프래그먼트 셰이더에서 정의한 빛의 방향은… 음, 적어도 각 모델에 상대적이지는 않습니다. 왜냐하면 모델과 독립적으로 고정되어 있기 때문입니다. 이는 월드 공간(world space) 또는 뷰 공간(view space)에 있을 것입니다 (후자는 “카메라를 어떻게 움직이든 항상 화면의 왼쪽 위에서 오는 빛”, 전자는 “왼쪽 위에서 오지만, 우리가 앞으로 움직여 구를 통과한 다음 뒤돌아보면 오른쪽 위에서 오는 빛”을 의미합니다).

월드 공간이라고 가정해 봅시다. 그렇다면 여전히 모델 행렬과 관련된 어떤 변환을 적용해야 합니다. 하지만 정확히 어떤 행렬일까요?

법선은 직교 관계에 의해 정의되며, 직교성은 스칼라 곱과 밀접한 관련이 있습니다. 따라서 스칼라 곱이 행렬과 어떻게 상호 작용하는지에 대한 다음 관찰에서 시작하겠습니다:

, 왜냐하면

이제, 모델 표면에 두 점 가 있고, 이들이 너무 가까워서 연결선 또한 표면 위에 놓여 있다고 가정합시다. 그러면 법선 에 직교합니다: .

어떤 행렬 M으로 모델을 변환하면, 로 변환되고, 따라서 로 변환됩니다. 그렇다면 n은 어떤 행렬(가령, A)로 변환해야 할까요?

우리가 원하는 것은: 입니다 (변환 전후의 각도가 일치해야 합니다 (이 경우 둘 다 직각)). 행렬과 스칼라 곱에 대한 우리의 관찰에 따르면: . 따라서: 만약 의 역행렬이라면, 변환된 법선과 점들 사이의 각도는 변환 전의 각도와 동일합니다.

그러므로 우리는 법선을 , 즉 M의 역행렬의 전치 행렬(transpose of the inverse)로 변환해야 합니다 (또는 전치 행렬의 역행렬, 결과는 같습니다). — 참고로 는 새로운 표기법의 도입일 뿐, “-T”는 그 자체로는 의미가 없습니다.

다시 한번 강조하겠습니다: 법선은 점과 동일한 행렬이 아니라, 역행렬의 전치 행렬로 변환되어야 합니다.

(순수 회전 변환에서는 이 구분이 무의미합니다. 이미 보았듯이, 회전 행렬의 경우 역행렬과 전치 행렬이 동일하므로, 입니다. 하지만 이것은 특수한 경우일 뿐입니다.)

우리 셰이더에 적용하면 다음과 같습니다:

    out_normal=vec3(transpose(inverse(model_matrix))*vec4(normal,0.0));

일반적으로, 우리는 행렬의 역행렬을 계산하고 싶지 않습니다. 특히 매우 자주 호출되는 셰이더 안에서는 더욱 그렇습니다. 역행렬을 InstanceData의 일부로 만들어서 모델 행렬 자체와 함께 제출하는 것이 더 나은 방법일 것입니다.

그렇게 해봅시다:

#[repr(C)]
pub struct InstanceData {
    pub modelmatrix: [[f32; 4]; 4],
    pub inverse_modelmatrix: [[f32; 4]; 4],
    pub colour: [f32; 3],
}
impl InstanceData {
    pub fn from_matrix_and_colour(modelmatrix: na::Matrix4<f32>, colour: [f32; 3]) -> InstanceData {
        InstanceData {
            modelmatrix: modelmatrix.into(),
            inverse_modelmatrix: modelmatrix.try_inverse().unwrap().into(),
            colour,
        }
    }
}

그리고

    sphere.insert_visibly(InstanceData::from_matrix_and_colour(
        na::Matrix4::new_scaling(0.5),
        [0.5, 0.0, 0.0],
    ));

를 다음과 같은 셰이더 코드와 함께 사용합니다.

#version 450

layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in mat4 model_matrix;
layout (location=6) in mat4 inverse_model_matrix;
layout (location=10) in vec3 colour;

layout (set=0, binding=0) uniform UniformBufferObject {
	mat4 view_matrix;
	mat4 projection_matrix;
} ubo;

layout (location=0) out vec4 colourdata_for_the_fragmentshader;
layout (location=1) out vec3 out_normal;

void main() {
    gl_Position = ubo.projection_matrix*ubo.view_matrix*model_matrix*vec4(position,1.0);
    colourdata_for_the_fragmentshader=vec4(colour,1.0);
    out_normal = transpose(mat3(inverse_model_matrix))*normal;
}

그리고

        let vertex_attrib_descs = [
            vk::VertexInputAttributeDescription {
                binding: 0,
                location: 0,
                offset: 0,
                format: vk::Format::R32G32B32_SFLOAT,
            },
            vk::VertexInputAttributeDescription {
                binding: 0,
                location: 1,
                offset: 12,
                format: vk::Format::R32G32B32_SFLOAT,
            },
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 2,
                offset: 0,
                format: vk::Format::R32G32B32A32_SFLOAT,
            },
            // ... (중간 4x4 행렬의 각 행) ...
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 6,
                offset: 64,
                format: vk::Format::R32G32B32A32_SFLOAT,
            },
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 7,
                offset: 80,
                format: vk::Format::R32G32B32A32_SFLOAT,
            },
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 8,
                offset: 96,
                format: vk::Format::R32G32B32A32_SFLOAT,
            },
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 9,
                offset: 112,
                format: vk::Format::R32G32B32A32_SFLOAT,
            },
            vk::VertexInputAttributeDescription {
                binding: 1,
                location: 10,
                offset: 128,
                format: vk::Format::R32G32B32_SFLOAT,
            },
        ];
        let vertex_binding_descs = [
            vk::VertexInputBindingDescription {
                binding: 0,
                stride: 24,
                input_rate: vk::VertexInputRate::VERTEX,
            },
            vk::VertexInputBindingDescription {
                binding: 1,
                stride: 140,
                input_rate: vk::VertexInputRate::INSTANCE,
            },
        ];

(이 코드들을 어디에 넣어야 할지는 아시겠죠.)

좋습니다. 요약해 보겠습니다: 이제 구가 3차원처럼 보입니다. 우리는 법선을 변환하는 방법을 알게 되었고, 표면의 법선과 빛의 방향에 따라 픽셀의 색상에 영향을 주는 매우 간단한 “셰이딩 모델”을 갖게 되었습니다.

이 모델은 너무 기본적이지만, 이전보다는 훨씬, 훨씬 낫습니다. 지금으로서는 이 정도로 충분합니다.

(나중에 돌아와 셰이딩을 개선하고, 빛의 방향이 프래그먼트 셰이더에 하드코딩된 값이라는 사실에 대해 무언가 조치를 취해야 할 것입니다.)

계속