정육면체는 몇 개의 정점(vertex)을 가질까요?
8개요?
vertexdata에 있는 항목의 수를 세어보세요:
fn cube() -> Model<[f32; 3], InstanceData> {
let lbf = [-1.0, 1.0, 0.0]; //lbf: 왼쪽-아래-앞 (left-bottom-front)
let lbb = [-1.0, 1.0, 1.0];
let ltf = [-1.0, -1.0, 0.0];
let ltb = [-1.0, -1.0, 1.0];
let rbf = [1.0, 1.0, 0.0];
let rbb = [1.0, 1.0, 1.0];
let rtf = [1.0, -1.0, 0.0];
let rtb = [1.0, -1.0, 1.0];
Model {
vertexdata: vec![
lbf, lbb, rbb, lbf, rbb, rbf, //아래쪽 면
ltf, rtb, ltb, ltf, rtf, rtb, //위쪽 면
lbf, rtf, ltf, lbf, rbf, rtf, //앞쪽 면
lbb, ltb, rtb, lbb, rtb, rbb, //뒤쪽 면
lbf, ltf, lbb, lbb, ltf, ltb, //왼쪽 면
rbf, rbb, rtf, rbb, rtb, rtf, //오른쪽 면
],
handle_to_index: std::collections::HashMap::new(),
handles: Vec::new(),
instances: Vec::new(),
first_invisible: 0,
next_handle: 0,
vertexbuffer: None,
instancebuffer: None,
}
}
36개가 있습니다. 반면에, 이 36개는 사실 8개의 서로 다른 점일 뿐입니다.
현재 우리는 36*3개의 float을 정점 버퍼에 넣고 있습니다.
8*3개의 float만으로도 가능할까요? 당연히 아닙니다: vertexdata는 어떤 점들이 삼각형을 이루는지에 대한 정보도 포함하고 있기 때문입니다.
하지만 우리는 이 매우 다른 두 종류의 정보를 함께 보관할 필요는 없습니다. 위치 정보를 먼저 제출하고, 연결성 정보는 “첫 번째, 세 번째, 여덟 번째 정점이 삼각형을 이룬다”와 같은 인덱스의 목록으로 따로 제출할 수 있습니다. 이렇게 하면 83개의 f32에 361개의 u32를 더한 것이 됩니다. 이전의 36*3=108개 변수 대신 총 60개의 변수만 필요한 셈입니다.
이것이 엄청나게 큰 이득이 아닐 수도 있지만, 우리는 겨우 8개의 정점을 가진 상자에 대해 이야기하고 있다는 점을 기억하세요. 더 큰 모델의 경우, 이 방법은 정말로 효과가 있을 것입니다.
벌칸은 이 “인덱스 드로잉”을 지원하며, 우리는 단지 .cmd_draw() 대신 .cmd_draw_indexed()를 사용하기만 하면 됩니다. 하지만 너무 앞서가지는 맙시다. 다른 부분에서도 몇 가지 변경이 필요하기 때문입니다.
예를 들어, Model 구조체를 재작업해야 합니다. 위 정육면체 예제에서 우리는 다음을 원합니다.
vertexdata:vec![lbf,lbb,ltf,ltb,rbf,rbb,rtf,rtb],
이것 외에는 아무것도 없습니다. 그리고 어떤 정점들이 삼각형을 이루는지 알려주는 인덱스들을 저장할 공간이 필요합니다.
(제가 이미 ‘인덱스’라는 용어를 인스턴스 벡터의 요소 인덱스를 가리키는 데 사용한 것이 약간 유감스럽긴 하지만, 약간 다른 유형의… 인덱스에 대해 같은 이름을 쓰는 것에 익숙해져야 할 것입니다.)
예를 들면,
indexdata: vec![
0, 1, 5, 0, 5, 4, //아래쪽 면
2, 7, 3, 2, 6, 7, //위쪽 면
0, 6, 2, 0, 4, 6, //앞쪽 면
1, 3, 7, 1, 7, 5, //뒤쪽 면
0, 2, 1, 1, 2, 3, //왼쪽 면
4, 5, 6, 5, 7, 6, //오른쪽 면
]
(“lbf”를 “0”으로 바꾸는 등) 그리고
indexbuffer: None
와 같이 말이죠. 전체 코드는 다음과 같습니다 (z 좌표에 약간의 수정 추가).
impl Model<[f32; 3], InstanceData> {
fn cube() -> Model<[f32; 3], InstanceData> {
let lbf = [-1.0, 1.0, -1.0]; //lbf: 왼쪽-아래-앞
let lbb = [-1.0, 1.0, 1.0];
let ltf = [-1.0, -1.0, -1.0];
let ltb = [-1.0, -1.0, 1.0];
let rbf = [1.0, 1.0, -1.0];
let rbb = [1.0, 1.0, 1.0];
let rtf = [1.0, -1.0, -1.0];
let rtb = [1.0, -1.0, 1.0];
Model {
vertexdata: vec![lbf, lbb, ltf, ltb, rbf, rbb, rtf, rtb],
indexdata: vec![
0, 1, 5, 0, 5, 4, //아래쪽 면
2, 7, 3, 2, 6, 7, //위쪽 면
0, 6, 2, 0, 4, 6, //앞쪽 면
1, 3, 7, 1, 7, 5, //뒤쪽 면
0, 2, 1, 1, 2, 3, //왼쪽 면
4, 5, 6, 5, 7, 6, //오른쪽 면
],
handle_to_index: std::collections::HashMap::new(),
handles: Vec::new(),
instances: Vec::new(),
first_invisible: 0,
next_handle: 0,
vertexbuffer: None,
indexbuffer: None,
instancebuffer: None,
}
}
}
물론, 변경된 Model 구조체와 함께 말이죠.
struct Model<V, I> {
vertexdata: Vec<V>,
indexdata: Vec<u32>,
handle_to_index: std::collections::HashMap<usize, usize>,
handles: Vec<usize>,
instances: Vec<I>,
first_invisible: usize,
next_handle: usize,
vertexbuffer: Option<Buffer>,
indexbuffer: Option<Buffer>,
instancebuffer: Option<Buffer>,
}
Model::update_vertexbuffer를 수정한 버전은 다음과 같습니다.
fn update_indexbuffer(
&mut self,
allocator: &vk_mem::Allocator,
) -> Result<(), vk_mem::error::Error> {
if let Some(buffer) = &mut self.indexbuffer {
buffer.fill(allocator, &self.indexdata)?;
Ok(())
} else {
let bytes = (self.indexdata.len() * std::mem::size_of::<u32>()) as u64;
let mut buffer = Buffer::new(
&allocator,
bytes,
vk::BufferUsageFlags::INDEX_BUFFER,
vk_mem::MemoryUsage::CpuToGpu,
)?;
buffer.fill(allocator, &self.indexdata)?;
self.indexbuffer = Some(buffer);
Ok(())
}
}
어떤 부분에서는 “vertex”가 “index”로 바뀌었고, size_of는 이제 V 대신 u32를 참조합니다. 사실 그냥 4로 바꿔도 됩니다.
한 가지 중요한 변경 사항은 BufferUsageFlags 또한 (VERTEX_BUFFER에서) INDEX_BUFFER로 바뀌었다는 것입니다.
또한 update_vertexbuffer 바로 다음에 이 함수를 호출하도록 합니다.
cube.update_indexbuffer(&aetna.allocator);
draw 함수에서는 먼저 인덱스 버퍼를 바인딩합니다.
logical_device.cmd_bind_index_buffer(
commandbuffer,
indexbuffer.buffer,
0,
vk::IndexType::UINT32,
);
버퍼, 오프셋(이 버퍼의 첫 항목부터 사용하고 싶지 않은 경우), 그리고 인덱스의 유형을 지정합니다. 우리는 u32를 저장했으므로 vk::IndexType::UINT32가 맞습니다. 하지만 u16(우리 정육면체에는 충분할 것입니다)이나 심지어 u8도 사용할 수 있지만, 후자는 다른 확장이 필요합니다.
그리기 명령은 draw_indexed로 변경됩니다.
logical_device.cmd_draw_indexed(
commandbuffer,
self.indexdata.len() as u32,
self.first_invisible as u32,
0,
0,
0,
);
이제 (인스턴스당) 그려야 할 정점의 수는 vertexdata의 항목 수가 아니라 indexdata의 항목 수로 주어집니다. 인스턴스의 수는 여전히 필요합니다. 그 다음에는 인덱스 버퍼에서 접근할 첫 번째 인덱스, 정점 오프셋(예를 들어, 인덱스 버퍼의 모든 1이라는 항목이 정점 버퍼의 첫 번째 정점, 즉 위치 0에 있는 정점을 참조하게 하려면 -1을 사용), 그리고 그릴 첫 번째 인스턴스의 인스턴스 ID가 옵니다.
전체 draw 함수는 다음과 같습니다 (아직 설명하지 않은 부분은 if let Some(indexbuffer) = &self.indexbuffer 줄 뿐입니다).
fn draw(&self, logical_device: &ash::Device, commandbuffer: vk::CommandBuffer) {
if let Some(vertexbuffer) = &self.vertexbuffer {
if let Some(indexbuffer) = &self.indexbuffer {
if let Some(instancebuffer) = &self.instancebuffer {
if self.first_invisible > 0 {
unsafe {
logical_device.cmd_bind_vertex_buffers(
commandbuffer,
0,
&[vertexbuffer.buffer],
&[0],
);
logical_device.cmd_bind_vertex_buffers(
commandbuffer,
1,
&[instancebuffer.buffer],
&[0],
);
logical_device.cmd_bind_index_buffer(
commandbuffer,
indexbuffer.buffer,
0,
vk::IndexType::UINT32,
);
logical_device.cmd_draw_indexed(
commandbuffer,
self.indexdata.len() as u32,
self.first_invisible as u32,
0,
0,
0,
);
}
}
}
}
}
}
마지막으로 Aetna::drop을 위한 정리 작업입니다.
if let Some(ib) = &m.indexbuffer {
self.allocator
.destroy_buffer(ib.buffer, &ib.allocation)
.expect("problem with buffer destruction");
}
프로그램을 실행하면 이전과 모든 것이 똑같이 보입니다. 원래 그래야 하고요.