우리는 단일 점, 삼각형, 그리고 상자를 만들어 봤습니다. 이제 구를 만들어보고 싶습니다.
구는 삼각형으로만 이루어져 있지는 않습니다. 그렇게 평평하지 않죠.
음, 근사치부터 시작해 봅시다. 표면이 삼각형으로 이루어진 무언가 말이죠. 정육면체는 어떨까요? 이미 정육면체는 만들어 봤으니, 재미가 없잖아요?
정규 정이십면체(regular icosahedron)를 사용해 봅시다.
main 함수의 시작 부분에서, 정육면체 관련 설정을 전부 제거합니다:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let eventloop = winit::event_loop::EventLoop::new();
let window = winit::window::Window::new(&eventloop)?;
let mut aetna = Aetna::init(window)?;
let mut ico = Model::cube();
ico.insert_visibly(InstanceData {
modelmatrix: na::Matrix4::new_scaling(0.5).into(),
colour: [0.5, 0.0, 0.0],
});
ico.update_vertexbuffer(&aetna.allocator)?;
ico.update_indexbuffer(&aetna.allocator)?;
ico.update_instancebuffer(&aetna.allocator)?;
aetna.models = vec![ico];
let mut camera = Camera::builder().build();
use winit::event::{Event, WindowEvent};
eventloop.run(move |event, _, controlflow| match event {
그리고 바로 작업에 착수하여 let mut ico = Model::cube();를 let mut ico = Model::icosahedron();로 교체합니다. model.rs 파일의 cube() 함수 옆에 다음 코드를 추가합니다:
impl Model<[f32; 3], InstanceData> {
pub fn icosahedron() -> Model<[f32;3],InstanceData>{
Model {
vertexdata: vec![],
indexdata: vec![],
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,
}
}
당연히 vertexdata와 indexdata는 아직 채워야 합니다.
어떻게 할까요? 한번 살펴봅시다:
(위키피디아에 감사합니다)
이 정이십면체의 직사각형들이 좌표 평면에 놓여 있다고 가정하면, 점들은 (예를 들어 짙은 녹색 사각형의 경우) [b,a,0], [-b,a,0], [b,-a,0], [-b,-a,0]에 위치하게 됩니다. 여기서 b>a>0인 어떤 값입니다. 다른 축에서도 마찬가지입니다: [a,0,b] (부호 변경 가능; 연한 녹색) 그리고 [0,b,a] (보라색, 역시 모든 부호 조합 가능). 아직 a와 b를 찾아야 합니다. 간단하게 a=1로 설정합시다 (나중에 모델의 크기를 조절할 수 있으니까요).
그렇다면 b는 얼마나 클까요?
이것은 정규 정이십면체이므로, 모든 변의 길이는 같습니다. 그리고 그 길이는 2입니다. 직사각형의 짧은 변을 보세요. 다른 변은 예를 들어 [b,1,0]과 [0,b,1] 사이에 있습니다. 그 길이는
이므로,
,
, 또는
, 즉 “황금비”입니다.
그래서:
let phi = (1.0 + 5.0_f32.sqrt()) / 2.0;
let darkgreen_front_top = [phi, -1.0, 0.0]; //0
let darkgreen_front_bottom = [phi, 1.0, 0.0]; //1
let darkgreen_back_top = [-phi, -1.0, 0.0]; //2
let darkgreen_back_bottom = [-phi, 1.0, 0.0]; //3
let lightgreen_front_right = [1.0, 0.0, -phi]; //4
let lightgreen_front_left = [-1.0, 0.0, -phi]; //5
let lightgreen_back_right = [1.0, 0.0, phi]; //6
let lightgreen_back_left = [-1.0, 0.0, phi]; //7
let purple_top_left = [0.0, -phi, -1.0]; //8
let purple_top_right = [0.0, -phi, 1.0]; //9
let purple_bottom_left = [0.0, phi, -1.0]; //10
let purple_bottom_right = [0.0, phi, 1.0]; //11
그리고
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,
],
이들 중 어느 것들이 연결되어 있을까요? 음, 우리에겐 그림이 있잖아요, 그렇죠? (각 정점이 몇 번 사용되었는지 세어보면서 빠진 부분을 확인하는 것이 도움이 될 수 있습니다. 각 정점은 5개의 삼각형에 포함되어야 합니다. 그리고 총 20개의 면이 있어야 합니다. 결국 정이십면체(icosahedron)니까요.)
indexdata: vec![
0,9,8,//
0,8,4,//
0,4,1,//
0,1,6,//
0,6,9,//
8,9,2,//
8,2,5,//
8,5,4,//
4,5,10,//
4,10,1,//
1,10,11,//
1,11,6,//
2,3,5,//
2,7,3,//
2,9,7,//
5,3,10,//
3,11,10,//
3,7,11,//
6,7,9,//
6,11,7//
],
자, 프로그램을 실행해 보면… 무언가가 보입니다. 정이십면체라고 명확하게 알아보기는 어렵네요.
와이어프레임 모드를 어떻게 사용하는지 기억나시나요?
renderpass_and_pipeline.rs 파일에 “폴리곤 모드(polygon mode)”에 관한 줄이 있습니다. 일단 .polygon_mode(vk::PolygonMode::LINE);으로 설정합시다.
아, 이제 훨씬 나아 보이네요.
이제 구에 더 가깝게 만들어 봅시다. 무엇을 할 수 있을까요?
더 작고 더 많은 삼각형이 필요합니다.
하지만 이걸 손으로 (또는 출력물에 점을 찍어 가며) 알아내는 건 정말 많은 노력이 필요해 보입니다. 이걸 자동화할 수 있을까요? 물론이죠.
각 삼각형을 생각해 볼 수 있습니다. 삼각형 하나가 주어졌을 때:
이것을 여러 개로 쪼갤 수 있습니다. 예를 들면:
하지만 그러면 두 개의 새로운 삼각형은 이전 삼각형과 모양이 달라지고, 이 과정을 너무 많이 반복하면 매우 얇은 삼각형이 많이 생길 수 있습니다.
또한: 어느 점이 두 개의 새로운 삼각형에 포함되고, 어느 점이 각각 하나의 삼각형에만 남을지 어떻게 선택해야 할까요?
이 질문들에 대한 답을 찾을 수도 있겠지만, 대신에 삼각형을 네 개로 쪼개 봅시다:
모델의 모든 삼각형에 이 작업을 수행하는 refine 함수를 만들어 봅시다. 이 함수는 정이십면체를 생성한 후, 버퍼를 업데이트하기 전에 호출할 것입니다:
let mut ico = Model::icosahedron();
ico.refine();
ico.insert_visibly(//etc.
이 함수는 Model의 새로운 메서드가 될 것입니다. 이렇게 시작해 봅시다:
pub fn refine(&mut self){
let mut new_indices=vec![];
for triangle in self.indexdata.chunks(3){
new_indices.extend_from_slice(triangle);
}
self.indexdata=new_indices;
}
이것은 아직 모델을 바꾸지 않습니다. 더 좋게 만들어 봅시다:
let a = triangle[0];
let b = triangle[1];
let c = triangle[2];
let mab =// ?
let mbc =// ?
let mca =// ?
new_indices.extend_from_slice(&[mca, a, mab, mab, b, mbc, mbc, c, mca, mab, mbc, mca]);
이제 중점 mab, mbc, mca는 어디서 얻을 수 있을까요?
점 A, B, C로부터 계산합니다. 참고: 현재 우리가 가진 것은 점 A, B, C의 인덱스이지, 점 자체가 아닙니다. 먼저 이들을 가져와서 vertex_a 등으로 부르겠습니다. 그런 다음 중점(vertex_ab 등)을 계산하고, vertexdata 벡터에 추가한 뒤, 최종적으로 그 인덱스를 얻습니다.
pub fn refine(&mut self) {
let mut new_indices = vec![];
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 vertex_ab = [
0.5 * (vertex_a[0] + vertex_b[0]),
0.5 * (vertex_a[1] + vertex_b[1]),
0.5 * (vertex_a[2] + vertex_b[2]),
];
let mab = self.vertexdata.len() as u32;
self.vertexdata.push(vertex_ab);
let vertex_bc = [
0.5 * (vertex_b[0] + vertex_c[0]),
0.5 * (vertex_b[1] + vertex_c[1]),
0.5 * (vertex_b[2] + vertex_c[2]),
];
let mbc = self.vertexdata.len() as u32;
self.vertexdata.push(vertex_bc);
let vertex_ca = [
0.5 * (vertex_c[0] + vertex_a[0]),
0.5 * (vertex_c[1] + vertex_a[1]),
0.5 * (vertex_c[2] + vertex_a[2]),
];
let mca = self.vertexdata.len() as u32;
self.vertexdata.push(vertex_ca);
new_indices.extend_from_slice(&[mca, a, mab, mab, b, mbc, mbc, c, mca, mab, mbc, mca]);
}
self.indexdata = new_indices;
}
알겠습니다. 하지만 각 변은 두 개의 삼각형에 속해야 합니다. 이는 모든 중점을 두 번씩 생성한다는 의미가 아닐까요? 그럴 겁니다.
dbg!(&self.indexdata.len());
dbg!(&self.vertexdata.len());
이 함수 끝에 위 코드를 추가하면 240과 72가 출력됩니다.
이것을 240과 42로 줄일 것입니다. (적어도 그 숫자가 맞습니다: 240/3은 80개의 삼각형, 즉 처음의 20개 면의 4배입니다; 42는 처음 시작했던 12개의 점에 각 변에 대한 점(정확히는 각 변의 중점) 30개를 더한 값입니다. 변의 수를 어떻게 아냐구요? 정이십면체는 구멍이 없는 멋진 볼록 다면체이므로, 오일러의 다면체 공식에 따라 정점(V), 변(E), 면(F)의 수는 V - E + F = 2를 만족합니다. 그리고 우리는 V=12, F=20이라는 것을 알고 있었습니다.)
다시 본론으로 돌아와서: 이 정확한 숫자를 어떻게 얻을 수 있을까요?
이미 새로운 점을 계산하는 데 사용한 점들을 저장해두고, 만약 결과가 존재한다면 저장된 결과를 사용합니다:
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 = [
0.5 * (vertex_a[0] + vertex_b[0]),
0.5 * (vertex_a[1] + vertex_b[1]),
0.5 * (vertex_a[2] + vertex_b[2]),
];
let mab = self.vertexdata.len() as u32;
self.vertexdata.push(vertex_ab);
midpoints.insert((a, b), mab);
midpoints.insert((b, a), mab);
mab
};
let mbc = if let Some(bc) = midpoints.get(&(b, c)) {
*bc
} else {
let vertex_bc = [
0.5 * (vertex_b[0] + vertex_c[0]),
0.5 * (vertex_b[1] + vertex_c[1]),
0.5 * (vertex_b[2] + vertex_c[2]),
];
let mbc = self.vertexdata.len() as u32;
midpoints.insert((b, c), mbc);
midpoints.insert((c, b), mbc);
self.vertexdata.push(vertex_bc);
mbc
};
let mca = if let Some(ca) = midpoints.get(&(c, a)) {
*ca
} else {
let vertex_ca = [
0.5 * (vertex_c[0] + vertex_a[0]),
0.5 * (vertex_c[1] + vertex_a[1]),
0.5 * (vertex_c[2] + vertex_a[2]),
];
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;
dbg!(&self.indexdata.len());
dbg!(&self.vertexdata.len());
}
더 낫네요. (그리고 함수 끝의 dbg! 줄들은 이제 지워도 됩니다.)
우리는 구를 만들고 싶었습니다:
pub fn sphere(refinements: u32)->Model<[f32;3],InstanceData>{
let mut model=Model::icosahedron();
for _ in 0..refinements{
model.refine();
}
model
}
그리고
let mut aetna = Aetna::init(window)?;
let mut sphere = Model::sphere(3);
sphere.insert_visibly(InstanceData {
modelmatrix: na::Matrix4::new_scaling(0.5).into(),
colour: [0.5, 0.0, 0.0],
});
sphere.update_vertexbuffer(&aetna.allocator)?;
sphere.update_indexbuffer(&aetna.allocator)?;
sphere.update_instancebuffer(&aetna.allocator)?;
aetna.models = vec![sphere];
let mut camera = Camera::builder().build();
실행 결과를 보면 여전히 정이십면체라는 것을 알 수 있습니다. 면들이 더 작은 삼각형으로 쪼개졌을 뿐이죠.
구는 모든 정점이 중심으로부터 같은 거리에 있는 것으로 정의됩니다. 이 내용을 함수에 포함시킬 수 있습니다:
pub fn sphere(refinements: u32) -> Model<[f32; 3], InstanceData> {
let mut model = Model::icosahedron();
for _ in 0..refinements {
model.refine();
}
for v in &mut model.vertexdata {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
*v = [v[0] / l, v[1] / l, v[2] / l];
}
model
}
훨씬 좋아졌습니다. 구 뒷면에서 오는 선들이 약간 거슬리네요. 이것들을 제거할 수 있습니다.
let rasterizer_info = vk::PipelineRasterizationStateCreateInfo::builder()
.line_width(1.0)
.front_face(vk::FrontFace::COUNTER_CLOCKWISE)
.cull_mode(vk::CullModeFlags::BACK)
.polygon_mode(vk::PolygonMode::LINE);
(renderpass_and_pipeline.rs의 Pipeline::init() 안에서; 이전에는 CullModeFlags::NONE이었습니다)
이 설정은 인덱스 버퍼의 순서에 따라 점들이 화면에 반시계 방향으로 나타나는 삼각형만 보인다는 것을 의미합니다. 화면의 결과 이미지를 보면 점들의 번호를 매길 때 운이 좋았던 것 같습니다. যদিও 앞면과 뒷면이 대칭적인 모델에서는 실수를 발견하기 어렵습니다. 또한: 이 설정은 “모든 게 올바른 것 같은데” 일부 모델 (또는 그 일부)이 보이지 않을 때 가장 유력한 용의자 중 하나입니다.
폴리곤 모드를 다시 vk::PolygonMode::FILL로 되돌립시다. 우리는 속이 꽉 찬 구에 관심이 있으니까요.
프로그램을 실행하면, 꽉 찬 ‘원’이 보입니다. 하지만 와이어프레임은 훨씬 더 입체적으로 보였는데요.
무언가 불완전합니다. 다음 장에서 이 문제를 살펴봐야겠습니다.
(다음 장으로 넘어가기 전에, 다시 경고가 너무 많이 쌓이기 전에 이 장을 위한 작은 마무리 메모:
warning: method is never used: `cube`
--> src/model.rs:237:5
이것에 #[allow(dead_code)] 어노테이션을 붙여줍시다.)