지금까지 우리가 GPU로 보낸 모든 데이터는 어떤 의미에서는 전체 모델에 대해 동일했습니다(또는, 적어도 정점당 최대 하나의 값만 있었습니다). 하지만 만약 우리가 삼각형 하나의 위에서 어떤 값(예를 들어, 색상)을 바꾸고 싶다면 어떨까요? 정점에서의 값들 사이를 선형 보간하는 것보다 더 복잡한 방식으로 말입니다. 만약 .jpg 파일 같은 그림을 화면에 그리고 싶다면 어떨까요? 모든 픽셀마다 정점을 만드는 것은 매우 비실용적일 것입니다. 그리고 그 그림을 화면이 아니라 어떤 모델의 표면에 입히고 싶다면요?
이 “문제”를 다루는 방법이 있습니다: 바로 텍스처입니다. 이미지를 불러와 GPU에 올리고, 렌더링할 때 “이 이미지의 특정 지점에서의 (색상) 값은 무엇인가?”라고 묻는 것입니다.
저는 우리가 지금까지 렌더링해왔던 장면에서 한 걸음 물러나 더 간단한 설정으로 시작하고 싶습니다. 다음의 간단한 정점 데이터와 모델에서 시작해 봅시다 (이것은 이 챕터 동안 계속 수정될 것입니다):
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct TexturedVertexData {
pub position: [f32; 3],
}
#[repr(C)]
pub struct TexturedInstanceData {
pub modelmatrix: [[f32; 4]; 4],
pub inverse_modelmatrix: [[f32; 4]; 4],
}
impl TexturedInstanceData {
pub fn from_matrix(modelmatrix: na::Matrix4<f32>) -> TexturedInstanceData {
TexturedInstanceData {
modelmatrix: modelmatrix.into(),
inverse_modelmatrix: modelmatrix.try_inverse().unwrap().into(),
}
}
}
impl Model<TexturedVertexData, TexturedInstanceData> {
pub fn quad() -> Self {
let lb = TexturedVertexData {
position: [-1.0, 1.0, 0.0],
}; //lb: 왼쪽-아래(left-bottom)
let lt = TexturedVertexData {
position: [-1.0, -1.0, 0.0],
};
let rb = TexturedVertexData {
position: [1.0, 1.0, 0.0],
};
let rt = TexturedVertexData {
position: [1.0, -1.0, 0.0],
};
Model {
vertexdata: vec![lb, lt, rb, rt],
indexdata: vec![0, 2, 1, 1, 2, 3],
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,
}
}
}
이는 Aetna가 pub models: Vec<Model<TexturedVertexData, TexturedInstanceData>>를 포함해야 한다는 것을 의미합니다.
셰이더(shader_textured.vert와 shader_textured.frag) 또한 꽤 간단합니다. 정점 셰이더에서는 위치 데이터와 카메라를 움직이는 기능을 유지하지만, 프래그먼트 셰이더에는 고정된 색상 값만 전달합니다. 프래그먼트 셰이더는 모든 조명 등을 처리했던 이전 셰이더보다 훨씬 적은 일을 합니다.
#version 450
layout (location=0) in vec3 position;
layout (location=1) in mat4 model_matrix;
layout (location=5) in mat4 inverse_model_matrix;
layout (set=0, binding=0) uniform UniformBufferObject {
mat4 view_matrix;
mat4 projection_matrix;
} ubo;
layout (location=0) out vec3 colourdata_for_the_fragmentshader;
void main() {
vec4 worldpos = model_matrix*vec4(position,1.0);
gl_Position = ubo.projection_matrix*ubo.view_matrix*worldpos;
colourdata_for_the_fragmentshader=vec3(1.0,1.0,0.5);
}
#version 450
layout (location=0) out vec4 theColour;
layout (location=0) in vec3 colour_in;
void main(){
theColour=vec4(colour_in,1.0);
}
물론, 이 셰이더는 “이 하나의 고정된 색상만 사용”하는 단계에 머물러 있지는 않을 것입니다. 하지만 지금은 아직 무대를 설정하는 중입니다. 그리고 제가 어느 부분을 변경해야 하는지 기억을 더듬고 있습니다. 이 프로젝트를 마지막으로 만진 지 꽤 시간이 흘렀거든요.
이러한 변경을 위한 또 다른 중요한 곳은 Pipeline::init() 함수입니다. 이 함수를 복사해서 다음과 같이 Pipeline::init_textured() 함수로 수정합니다:
pub fn init_textured(
logical_device: &ash::Device,
swapchain: &SwapchainDongXi,
renderpass: &vk::RenderPass,
) -> Result<Pipeline, vk::Result> {
let vertexshader_createinfo = vk::ShaderModuleCreateInfo::builder().code(
vk_shader_macros::include_glsl!("./shaders/shader_textured.vert", kind: vert),
);
let vertexshader_module =
unsafe { logical_device.create_shader_module(&vertexshader_createinfo, None)? };
let fragmentshader_createinfo = vk::ShaderModuleCreateInfo::builder().code(
vk_shader_macros::include_glsl!("./shaders/shader_textured.frag"),
);
let fragmentshader_module =
unsafe { logical_device.create_shader_module(&fragmentshader_createinfo, None)? };
let mainfunctionname = std::ffi::CString::new("main").unwrap();
let vertexshader_stage = vk::PipelineShaderStageCreateInfo::builder()
.stage(vk::ShaderStageFlags::VERTEX)
.module(vertexshader_module)
.name(&mainfunctionname);
let fragmentshader_stage = vk::PipelineShaderStageCreateInfo::builder()
.stage(vk::ShaderStageFlags::FRAGMENT)
.module(fragmentshader_module)
.name(&mainfunctionname);
let shader_stages = vec![vertexshader_stage.build(), fragmentshader_stage.build()];
let vertex_attrib_descs = [
vk::VertexInputAttributeDescription {
binding: 0,
location: 0,
offset: 0,
format: vk::Format::R32G32B32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 1,
offset: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 2,
offset: 16,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 3,
offset: 32,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 4,
offset: 48,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 5,
offset: 64,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 6,
offset: 80,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 7,
offset: 96,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 8,
offset: 112,
format: vk::Format::R32G32B32A32_SFLOAT,
}
];
let vertex_binding_descs = [
vk::VertexInputBindingDescription {
binding: 0,
stride: 12,
input_rate: vk::VertexInputRate::VERTEX,
},
vk::VertexInputBindingDescription {
binding: 1,
stride: 128,
input_rate: vk::VertexInputRate::INSTANCE,
},
];
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder()
.vertex_attribute_descriptions(&vertex_attrib_descs)
.vertex_binding_descriptions(&vertex_binding_descs);
let input_assembly_info = vk::PipelineInputAssemblyStateCreateInfo::builder()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST);
let viewports = [vk::Viewport {
x: 0.,
y: 0.,
width: swapchain.extent.width as f32,
height: swapchain.extent.height as f32,
min_depth: 0.,
max_depth: 1.,
}];
let scissors = [vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: swapchain.extent,
}];
let viewport_info = vk::PipelineViewportStateCreateInfo::builder()
.viewports(&viewports)
.scissors(&scissors);
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::FILL);
let multisampler_info = vk::PipelineMultisampleStateCreateInfo::builder()
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
let depth_stencil_info = vk::PipelineDepthStencilStateCreateInfo::builder()
.depth_test_enable(true)
.depth_write_enable(true)
.depth_compare_op(vk::CompareOp::LESS_OR_EQUAL);
let colourblend_attachments = [vk::PipelineColorBlendAttachmentState::builder()
.blend_enable(true)
.src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
.dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.color_blend_op(vk::BlendOp::ADD)
.src_alpha_blend_factor(vk::BlendFactor::SRC_ALPHA)
.dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.alpha_blend_op(vk::BlendOp::ADD)
.color_write_mask(
vk::ColorComponentFlags::R
| vk::ColorComponentFlags::G
| vk::ColorComponentFlags::B
| vk::ColorComponentFlags::A,
)
.build()];
let colourblend_info =
vk::PipelineColorBlendStateCreateInfo::builder().attachments(&colourblend_attachments);
let descriptorset_layout_binding_descs0 = [vk::DescriptorSetLayoutBinding::builder()
.binding(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::VERTEX)
.build()];
let descriptorset_layout_info0 = vk::DescriptorSetLayoutCreateInfo::builder()
.bindings(&descriptorset_layout_binding_descs0);
let descriptorsetlayout0 = unsafe {
logical_device.create_descriptor_set_layout(&descriptorset_layout_info0, None)
}?;
let desclayouts = vec![descriptorsetlayout0];
let pipelinelayout_info = vk::PipelineLayoutCreateInfo::builder().set_layouts(&desclayouts);
let pipelinelayout =
unsafe { logical_device.create_pipeline_layout(&pipelinelayout_info, None) }?;
let pipeline_info = vk::GraphicsPipelineCreateInfo::builder()
.stages(&shader_stages)
.vertex_input_state(&vertex_input_info)
.input_assembly_state(&input_assembly_info)
.viewport_state(&viewport_info)
.rasterization_state(&rasterizer_info)
.multisample_state(&multisampler_info)
.depth_stencil_state(&depth_stencil_info)
.color_blend_state(&colourblend_info)
.layout(pipelinelayout)
.render_pass(*renderpass)
.subpass(0);
let graphicspipeline = unsafe {
logical_device
.create_graphics_pipelines(
vk::PipelineCache::null(),
&[pipeline_info.build()],
None,
)
.expect("A problem with the pipeline creation")
}[0];
unsafe {
logical_device.destroy_shader_module(fragmentshader_module, None);
logical_device.destroy_shader_module(vertexshader_module, None);
}
Ok(Pipeline {
pipeline: graphicspipeline,
layout: pipelinelayout,
descriptor_set_layouts: desclayouts,
})
}
변경 사항은 위에서 본 정점 속성(Vertex...Description들)에 직접적으로 대응하거나, 조명 제거로 인해 이전 버전에 비해 descriptorset에 대한 일부 줄이 제거된 것에서 비롯됩니다.
당연히 Aetna::init()에서도 이전 함수 대신 이 함수를 호출합니다.
update_commandbuffer에서는 조명에 관한 줄을 제거합니다:
unsafe {
self.device.cmd_begin_render_pass(
commandbuffer,
&renderpass_begininfo,
vk::SubpassContents::INLINE,
);
self.device.cmd_bind_pipeline(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
self.pipeline.pipeline,
);
self.device.cmd_bind_descriptor_sets(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
self.pipeline.layout,
0,
&[
self.descriptor_sets_camera[index],
// self.descriptor_sets_light[index],
],
&[],
);
for m in &self.models {
m.draw(&self.device, commandbuffer);
}
self.device.cmd_end_render_pass(commandbuffer);
self.device.end_command_buffer(commandbuffer)?;
}
이제 실행될까요?
아직입니다. 우리는 여전히 어딘가에서 조명을 참조하고 있습니다. Aetna::init()에서, 최종 구조체 초기화 시 descriptor_sets_light: vec![],를 사용해야 하고, 조명과 그 디스크립터 셋 레이아웃에 의존하는 부분을 제거(또는 아마도 주석 처리하는 것이 더 좋을 것입니다)해야 합니다:
/* let desc_layouts_light =
vec![pipeline.descriptor_set_layouts[1]; swapchain.amount_of_images as usize];
let descriptor_set_allocate_info_light = vk::DescriptorSetAllocateInfo::builder()
.descriptor_pool(descriptor_pool)
.set_layouts(&desc_layouts_light);
let descriptor_sets_light = unsafe {
logical_device.allocate_descriptor_sets(&descriptor_set_allocate_info_light)
}?;
for descset in &descriptor_sets_light {
let buffer_infos = [vk::DescriptorBufferInfo {
buffer: lightbuffer.buffer,
offset: 0,
range: 8,
}];
let desc_sets_write = [vk::WriteDescriptorSet::builder()
.dst_set(*descset)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.buffer_info(&buffer_infos)
.build()];
unsafe { logical_device.update_descriptor_sets(&desc_sets_write, &[]) };
}*/
이제 실행됩니다. 창백한 노란색 사각형이 보입니다. 이제 작업을 시작할 수 있습니다.
우리의 목표는 표면의 노란색을 어떤 이미지로 바꾸는 것입니다. “텍스처”로 말이죠. 먼저 이미지가 필요합니다. gfx/image.png라는 .png 파일을 만들어 봅시다. 내용은 중요하지 않습니다. 그냥 창백한 노란색만 아니면 됩니다. 그래야 우리가 마지막에 성공했는지 알 수 있으니까요.
다음 단계: 이미지를 우리 프로그램으로 가져오기. main.rs에 mod texture;를 쓰고 texture.rs라는 새 파일을 만듭시다:
struct Texture {
image: image::RgbaImage,
}
impl Texture {
fn from_file<P: AsRef<std::path::Path>>(path: P) -> Self {
Texture {
image: image::open(path)
.map(|img| img.to_rgba())
.expect("unable to open image"),
}
}
}
다음 단계: 이 이미지를 GPU로 보내기. 이를 위해 Vulkan 이미지가 필요하며, 해당 메모리(GPU에 예약되고 이미지에 바인딩됨)도 필요합니다. 스크린샷 챕터에서처럼, 우리는 Vulkan Memory Allocator에 의존합니다.
struct Texture {
image: image::RgbaImage,
vk_image: vk::Image,
allocation: vk_mem::Allocation,
allocation_info: vk_mem::AllocationInfo,
}
impl Texture {
fn from_file<P: AsRef<std::path::Path>>(path: P, allocator: &vk_mem::Allocator) -> Self {
let image = image::open(path)
.map(|img| img.to_rgba())
.expect("unable to open image");
let img_create_info = vk::ImageCreateInfo::builder();
let alloc_create_info = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::GpuOnly,
..Default::default()
};
let (vk_image, allocation, allocation_info) = allocator
.create_image(&img_create_info, &alloc_create_info)
.expect("creating vkImage for texture");
Texture {
image,
vk_image,
allocation,
allocation_info,
}
}
}
이 img_create_info는 임시방편에 가까웠습니다. 이미지 크기부터 시작해서 몇 가지 데이터를 채워봅시다:
let (width, height) = image.dimensions();
let img_create_info = vk::ImageCreateInfo::builder()
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width,
height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.format(vk::Format::R8G8B8A8_SRGB)
.samples(vk::SampleCountFlags::TYPE_1)
.usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED);
중요한 부분은 크기(너비, 높이, 그리고 실제로는 없는 3차원, 그래서 깊이 1)와 사용 플래그입니다: 우리는 이 이미지로 데이터를 전송하고 셰이더에서 샘플링(아래 참조)에 사용하고 싶습니다.
만약 이미지를 단순한 데이터 배열 이상으로 사용하고 싶다면, 즉 접근하고 싶다면, 추가로 vk::ImageView가 필요합니다 (스왑체인 챕터에서 VkImage를 처음 만났을 때와 마찬가지로).
let view_create_info = vk::ImageViewCreateInfo::builder()
.image(vk_image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::R8G8B8A8_SRGB)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
level_count: 1,
layer_count: 1,
..Default::default()
});
let imageview = unsafe { device.create_image_view(&view_create_info, None) }
.expect("image view creation");
(이것은 방금 만든 이미지를 위한 뷰이고, 2D이며 항상 같은 포맷이고, 우리는 색상에 관심이 있으며 레이어는 하나뿐입니다.) 이 생성에는 논리 장치(logical device)에 대한 접근이 필요하며(이것을 함수의 또 다른 인자로 전달할 것입니다), imageview를 Texture의 또 다른 필드로 추가합니다.
이것만으로는 - 아직! - 충분하지 않습니다. 우리는 이 이미지(뷰)에서 색상을 읽는 데 관심이 있을 것입니다. 예를 들면: “이봐, GPU! 이 이미지뷰의 정확히 중앙에 있는 색상이 뭐야? 그 색상을 다음 위치에 똑같이 그려줘.” 우리가 전달하는 것은 본질적으로 색상 값의 그리드입니다. 이제 “정확히 중앙”이 이 그리드 위에 있지 않다면(우리 이미지의 픽셀(픽셀을 작은 사각형이 아닌 점으로 생각한다면)이 아니라면), 우리의 불쌍한 GPU는 어떻게 응답해야 할까요? 가장 가까운 값을 사용해야 할까요? 아니면 모든 이웃하는 값들을 가져와 보간해야 할까요?
글쎄요, 그것을 결정하는 것은 어떻게든 우리의 몫입니다. 그리고 이 결정을 기록하는 구조가 바로 샘플러(Sampler)입니다. (Texture 구조체에 새로운 필드가 보입니다…)
let sampler_info = vk::SamplerCreateInfo::builder()
.mag_filter(vk::Filter::LINEAR)
.min_filter(vk::Filter::LINEAR);
let sampler =
unsafe { device.create_sampler(&sampler_info, None) }.expect("sampler creation");
여기서 우리는 값이 선형으로 보간되어야 한다고 말하고 있습니다. (다른 옵션은 NEAREST였을 것입니다.)
이제 구조체들을 만들었지만, 아직 이미지의 내용을 GPU로 보내지는 않았습니다.
이 데이터로 버퍼를 준비해 봅시다:
let data = image.clone().into_raw();
let mut buffer = Buffer::new(
&aetna.allocator,
data.len() as u64,
vk::BufferUsageFlags::TRANSFER_SRC,
vk_mem::MemoryUsage::CpuToGpu,
)?;
buffer.fill(&aetna.allocator, &data);
다음으로, 실제로 몇 가지 명령을 보낼 수 있도록 커맨드 버퍼가 있어야 합니다.
let commandbuf_allocate_info = vk::CommandBufferAllocateInfo::builder()
.command_pool(aetna.pools.commandpool_graphics)
.command_buffer_count(1);
let copycmdbuffer = unsafe {
aetna
.device
.allocate_command_buffers(&commandbuf_allocate_info)
}
.unwrap()[0];
let cmdbegininfo = vk::CommandBufferBeginInfo::builder()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
unsafe {
aetna
.device
.begin_command_buffer(copycmdbuffer, &cmdbegininfo)
}?;
// 여기에 명령 삽입.
unsafe { aetna.device.end_command_buffer(copycmdbuffer) }?;
let submit_infos = [vk::SubmitInfo::builder()
.command_buffers(&[copycmdbuffer])
.build()];
let fence = unsafe {
aetna
.device
.create_fence(&vk::FenceCreateInfo::default(), None)
}?;
unsafe {
aetna
.device
.queue_submit(aetna.queues.graphics_queue, &submit_infos, fence)
}?;
unsafe { aetna.device.wait_for_fences(&[fence], true, std::u64::MAX) }?;
unsafe { aetna.device.destroy_fence(fence, None) };
aetna.allocator.destroy_buffer(buffer.buffer, &buffer.allocation)?;
unsafe {
aetna
.device
.free_command_buffers(aetna.pools.commandpool_graphics, &[copycmdbuffer])
};
// 여기에 명령 삽입에 관해서: 우리가 원하는 것은 버퍼에서 이미지로 데이터를 복사하는 것입니다. 사용할 명령어는 cmd_copy_buffer_to_image입니다. 합리적으로 들립니다. 또한 어떤 영역(버퍼와 이미지의)에 영향을 미치고 싶은지 설명해야 합니다:
let image_subresource = vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
};
let region = vk::BufferImageCopy {
buffer_offset: 0,
buffer_row_length: 0,
buffer_image_height: 0,
image_offset: vk::Offset3D { x: 0, y: 0, z: 0 },
image_extent: vk::Extent3D {
width,
height,
depth: 1,
},
image_subresource,
..Default::default()
};
unsafe {
aetna.device.cmd_copy_buffer_to_image(
copycmdbuffer,
buffer.buffer,
vk_image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[region],
);
}
이것은 다음을 발생시킵니다:
Submitted command buffer expects VkImage 0x290000000029[] (subresource: aspectMask 0x1 array layer 0, mip level 0) to be in layout VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL--instead, current layout is VK_IMAGE_LAYOUT_UNDEFINED.
(제출된 커맨드 버퍼는 VkImage ...가 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 레이아웃에 있을 것으로 예상하지만, 현재 레이아웃은 VK_IMAGE_LAYOUT_UNDEFINED입니다.)
맞습니다. 우리가 제출한 복사 명령어는 vk::ImageLayout::TRANSFER_DST_OPTIMAL을 포함합니다. (이것이 유일한 가능한 값은 아니지만, LAYOUT_UNDEFINED는 여기서 허용되는 선택이 아닙니다.) 따라서, 우리는 이미지를 준비해야 합니다. 이를 위해 메모리 배리어(MemoryBarrier)를 설정합니다 (커맨드 버퍼의 시작 부분에):
let barrier = vk::ImageMemoryBarrier::builder()
.image(vk_image)
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copycmdbuffer,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
이것으로 오류 메시지가 사라집니다. 하지만 이미지로 데이터를 전송하는 것이 최종 목적이 아니므로, 이미지 레이아웃을 한 번 더 (복사 명령 후에) vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL로 변경합시다:
let barrier = vk::ImageMemoryBarrier::builder()
.image(vk_image)
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.dst_access_mask(vk::AccessFlags::SHADER_READ)
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copycmdbuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
이제 이미지는 GPU에 있어야 합니다. 하지만 아직 사용하고 있지는 않습니다.
이것은 모든 정점이 공유하는 일정량의 데이터입니다(음, 단지 이미지를 보여주기 위해서라면, “모든”은 “네 개”일 수 있습니다 - 하지만 확실히 한 정점에서 다음 정점으로 변하지는 않습니다). 그래서 이것은 우리가 이전에 가졌던 카메라 데이터나 조명 버퍼와 약간의 유사점이 있습니다. 따라서 우리는 파이프라인 설정(함수 init_textured) 중에 디스크립터 셋 바인딩을 준비해야 합니다.
let descriptorset_layout_binding_descs1 = [vk::DescriptorSetLayoutBinding::builder()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build()];
let descriptorset_layout_info1 = vk::DescriptorSetLayoutCreateInfo::builder()
.bindings(&descriptorset_layout_binding_descs1);
let descriptorsetlayout1 = unsafe {
logical_device.create_descriptor_set_layout(&descriptorset_layout_info1, None)
}?;
let desclayouts = vec![descriptorsetlayout0,descriptorsetlayout1];
(마지막 줄은 수정되었고, 나머지는 모두 새로 추가된 것입니다.) 우리는 프래그먼트 셰이더에서 이미지를 사용할 것이고, 타입은 결합된 이미지와 샘플러(combined image and sampler)입니다.
aetna를 설정할 때, 디스크립터 풀의 크기를 지정하는 곳이 있었습니다. 이제 여기에 COMBINED_IMAGE_SAMPLER 타입의 디스크립터를 위한 공간이 필요하므로 다음을 삽입합니다:
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: swapchain.amount_of_images,
},
또한 Aetna에 새로운 필드 descriptor_sets_texture를 부여하고 다음과 같이 채웁니다:
let desc_layouts_texture =
vec![pipeline.descriptor_set_layouts[1]; swapchain.amount_of_images as usize];
let descriptor_set_allocate_info_texture = vk::DescriptorSetAllocateInfo::builder()
.descriptor_pool(descriptor_pool)
.set_layouts(&desc_layouts_texture);
let descriptor_sets_texture = unsafe {
logical_device.allocate_descriptor_sets(&descriptor_set_allocate_info_texture)
}?;
또한 커맨드 버퍼를 업데이트할 때 이 디스크립터 셋들을 바인딩해야 합니다 (aetna.update_commandbuffer에서):
self.device.cmd_bind_descriptor_sets(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
self.pipeline.layout,
0,
&[
self.descriptor_sets_camera[index],
self.descriptor_sets_texture[index],
],
&[],
);
남은 일은 디스크립터 셋과 (텍스처 로딩에서 온) 이미지 및 샘플러를 함께 연결하는 것입니다.
우리가 메인 함수의 시작 부분 가까이에서 텍스처를 생성한다고 가정해 봅시다:
let texture = Texture::from_file("../gfx/image.png", &aetna)?;
그런 다음 다음을 삽입합니다:
let imageinfo = vk::DescriptorImageInfo {
image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
image_view: texture.imageview,
sampler: texture.sampler,
..Default::default()
};
let descriptorwrite_image = vk::WriteDescriptorSet {
dst_set: aetna.descriptor_sets_texture[aetna.swapchain.current_image],
dst_binding: 0,
dst_array_element: 0,
descriptor_type: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: 1,
p_image_info: [imageinfo].as_ptr(),
..Default::default()
};
unsafe {
aetna
.device
.update_descriptor_sets(&[descriptorwrite_image], &[]);
}
커맨드 버퍼를 업데이트하기 바로 전에 말입니다.
이제 이미지는 GPU로 가는 길을 찾아 프래그먼트 셰이더 단계에서 접근 가능해야 합니다.
프래그먼트 셰이더로 들어가서 샘플러를 도입할 시간입니다:
layout(set=1,binding=0) uniform sampler2D texturesampler;
어떻게 접근할까요? 이렇게요:
theColour=texture(texturesampler,vec2(0.5,0.5));
vec2는 좌표를 제공합니다. 여기서는 이미지의 중간점을 사용하고 있습니다. (그리고 프로그램을 실행하면, 사각형은 더 이상 노란색이 아닙니다.)
물론, 이 줄로는 전체 사각형이 같은 색상을 가집니다. 만약 이미지를 닮게 만들고 싶다면, 텍스처 좌표를 변경하여 0.5만 사용하는 대신 0에서 1까지의 전체 범위를 사용해야 합니다.
가장 좋은 방법은 각 정점에 텍스처 좌표를 정점 데이터로 주고, 그것들이 (보간되어) 프래그먼트 셰이더로 전달되게 한 다음, 그것들을 사용하여 텍스처의 올바른 위치에서 읽는 것입니다.
x, y, z가 공간 좌표를 위해 예약되어 있으므로, 텍스처 좌표의 이름으로는 u와 v를 사용하는 것이 일반적입니다.
그러면 우리 프래그먼트 셰이더는 다음과 같이 됩니다:
#version 450
layout (location=0) out vec4 theColour;
layout (location=0) in vec2 uv;
layout(set=1,binding=0) uniform sampler2D texturesampler;
void main(){
theColour=texture(texturesampler,uv);
}
(이전에 여기서 전달되었던 색상 데이터는 제거했습니다. 색상은 오직 텍스처에서만 비롯되어야 합니다.)
#version 450
layout (location=0) in vec3 position;
layout (location=1) in vec2 texcoord;
layout (location=2) in mat4 model_matrix;
layout (location=6) in mat4 inverse_model_matrix;
layout (set=0, binding=0) uniform UniformBufferObject {
mat4 view_matrix;
mat4 projection_matrix;
} ubo;
layout (location=0) out vec2 uv;
void main() {
vec4 worldpos = model_matrix*vec4(position,1.0);
gl_Position = ubo.projection_matrix*ubo.view_matrix*worldpos;
uv = texcoord;
}
이것은 정점 셰이더로, 이제 정점의 속성 중 하나로 텍스처 좌표를 받습니다. location 값의 변경에 주목하세요.
이는 VertexAttributeDescription에 작은 변경이 필요하다는 것을 의미합니다:
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::R32G32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 2,
offset: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 3,
offset: 16,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 4,
offset: 32,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 5,
offset: 48,
format: vk::Format::R32G32B32A32_SFLOAT,
},
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,
},
];
let vertex_binding_descs = [
vk::VertexInputBindingDescription {
binding: 0,
stride: 20,
input_rate: vk::VertexInputRate::VERTEX,
},
vk::VertexInputBindingDescription {
binding: 1,
stride: 128,
input_rate: vk::VertexInputRate::INSTANCE,
},
];
그리고 텍스처 좌표를 VertexData 구조체의 일부로 추가해야 합니다:
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct TexturedVertexData {
pub position: [f32; 3],
pub texcoord: [f32;2],
}
또한, quad 생성 시 이 값들을 할당합니다:
pub fn quad() -> Self {
let lb = TexturedVertexData {
position: [-1.0, 1.0, 0.0],
texcoord: [0.0, 1.0],
}; //lb: 왼쪽-아래
let lt = TexturedVertexData {
position: [-1.0, -1.0, 0.0],
texcoord: [0.0, 0.0],
};
let rb = TexturedVertexData {
position: [1.0, 1.0, 0.0],
texcoord: [1.0, 1.0],
};
let rt = TexturedVertexData {
position: [1.0, -1.0, 0.0],
texcoord: [1.0, 0.0],
};
그리고 이제, 우리 이미지가 화면에 나타나는 것을 볼 수 있습니다.