Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Давайте сейчас рассмотрим написание на Vulkan с использованием вспомогательных классов более сложного приложения - у нас будет честный 3D, uniform-буфер и текстура.
Для начала нам понадобится класс Texture
, "заворачивающий" в себя текстуру.
Он будет включать в себя как класс Image
(инкапсуляция изображения на Vulkan), так
и image view.
Также для чтения из текстуры нам понадобится класс Sampler
.
Также как мы завернули буфер в класс Buffer
, будет удобно завернуть изображение Vulkan (VkImage)
в класс Image
.
Как и буфер это не копируемый (но moveable) объект, значит его можно хранить в стандартных
контейнерах.
У него есть метод clean
очищающий объект (и уничтожающий все связанные с ним ресурсы),
метод isOk возвращает статус изображения (можно ли его прямо сейчас использовать).
Также имеется ряд get-методов, возвращающих свойства изображения -
getWidth
, getHeight
, getDepth
, getMipmapLevels
, getArrayLayers
,
getHandle
(возвращающий соответствующий VkImage), getDevice
.
Метод getFormat
возвращает формат изображения, метод hasDepth возвращает содержит ли это
изображение значения глубины (т.е. изображение либо depth либо depth-stencil).
Также есть методы hasStencil
и isDepthStencil
.
Для доступа к памяти, соответствующей изображению, можно использовать методы map
и unmap
,
отвечающие за отображение памяти изображения в память CPU.
Также для доступа к памяти служит метод copyFromBuffer
.
void copyFromBuffer ( SingleTimeCommand& cmd, Buffer& buffer, uint32_t width, uint32_t height,
uint32_t depth = 1, uint32_t layers = 1, uint32_t mipLevel = 0 );
У класса Image
имеется два метода create
для создания изображения с заданными свойствами.
bool create ( Device& dev, uint32_t w, uint32_t h, uint32_t d, uint32_t numMipLevels, VkFormat fmt, VkImageTiling tl,
VkImageUsageFlags usage, int mapping, VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED );
bool create ( Device& dev, ImageParams& info, int mapping );
Второй вариант метода create
вместо явной передачи всех требуемых свойств изображения
используем объект ImageParams
, содержащий все эти свойства и позволяющих задавать их
в удобной форме.
image.create ( ImageCreateInfo ( width, height ).setFormat ( myFmt ).setUsage ( usage ) );
Метод transitionLayout
служит для перевода изображения из одного внутреннего представления - layout -
(например, оптимизированного для копирования со стороны CPU) в другое (например, оптимизированного
для чтения из шейдера).
void transitionLayout ( SingleTimeCommand& cmd, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout );
Texture
На самом деле нам обычно нужно не изображение (image), а текстура, из которой можно читать в шейдере.
Для этого нужно к изображению (image) добавить вид на изображение (image view), задающий способ интерпретации изображения.
Для этого мы создадим класс Texture
, содежащий внутри себя как объект класса Image
, так и объект типа VkImageView
.
Как и Image
, данный класс будет не копируемым, но moveable.
Метод clean
будет освобождать все выделенные ресурсы, а набор get-методов буфет возвращать свойства текстуры.
Для создания текстуры (без загрузки в нее данных) мы будем использовать метод create
.
Методы load
, loadCubemap
и loadRaw
отвечают за создание и загрузку данных (текселов) в изображение.
Метод generateMipmaps
будет строить все уровни mipmap-пирамиды по нулевому.
void generateMipmaps ( SingleTimeCommand& cmd, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels );
bool load ( Device& dev, const std::string& fileName, bool mipmaps = true, bool srgb = false );
bool loadCubemap ( Device& dev, const std::vector<const char *>& files, bool mipmaps = true, bool srgb = false );
bool loadRaw ( Device& dev, int w, int h, const void * ptr, VkFormat format, bool mipmaps = true );
Sampler
В отличии от OpenGL в Vulkan для чтения из текстуры обязательно наличие объекта-сэмплера (sampler).
Мы будем инкапсулировать этот объект в виде класса Sampler
.
Это также не копируемый но moveable объект, поддерживающий ряд методов set* для задания параметров
сэмплера. Для всех задаваемых параметров есть значения по умолчанию, после того,
как все параметры будут заданы, вызывается метод create
для создания VkSampler.
class Sampler
{
VkDevice device = VK_NULL_HANDLE;
VkSampler sampler = VK_NULL_HANDLE;
VkFilter minFilter = VK_FILTER_NEAREST;
VkFilter magFilter = VK_FILTER_NEAREST;
VkSamplerAddressMode addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
VkSamplerAddressMode addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
VkSamplerAddressMode addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
VkBool32 anisotropy = VK_FALSE;
float maxAnisotropy = 1.0f; // 16
VkBool32 unnormalized = VK_FALSE;
VkSamplerMipmapMode mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
float lodBias = 0;
float minLod = 0;
float maxLod = 0;
public:
Sampler () {}
Sampler ( Sampler&& s )
{
std::swap ( device, s.device );
std::swap ( sampler, s.sampler );
std::swap ( minFilter, s.minFilter );
std::swap ( magFilter, s.magFilter );
std::swap ( addressModeU, s.addressModeU );
std::swap ( addressModeV, s.addressModeV );
std::swap ( addressModeW, s.addressModeW );
std::swap ( anisotropy, s.anisotropy );
std::swap ( maxAnisotropy, s.maxAnisotropy );
std::swap ( unnormalized, s.unnormalized );
std::swap ( mipmapMode, s.mipmapMode );
std::swap ( lodBias, s.lodBias );
std::swap ( minLod, s.minLod );
std::swap ( maxLod, s.maxLod );
}
Sampler ( const Sampler& ) = delete;
~Sampler ()
{
clean ();
}
Sampler& operator = ( const Sampler& ) = delete;
bool isOk () const
{
return device != nullptr && sampler != VK_NULL_HANDLE;
}
VkSampler getHandle () const
{
return sampler;
}
void clean ()
{
if ( sampler != VK_NULL_HANDLE )
vkDestroySampler ( device, sampler, nullptr );
sampler = VK_NULL_HANDLE;
}
Sampler& setMinFilter ( VkFilter filter )
{
minFilter = filter;
return *this;
}
Sampler& setMagFilter ( VkFilter filter )
{
magFilter = filter;
return *this;
}
Sampler& setAddressMode ( VkSamplerAddressMode u, VkSamplerAddressMode v, VkSamplerAddressMode w )
{
addressModeU = u;
addressModeV = v;
addressModeW = w;
return *this;
}
Sampler& setAnisotropy ( bool enable, float maxAniso = 0 )
{
anisotropy = enable ? VK_TRUE : VK_FALSE;
maxAnisotropy = maxAniso;
return *this;
}
Sampler& setNormalized ( bool flag )
{
unnormalized = flag ? VK_FALSE : VK_TRUE;
return *this;
}
Sampler& setMipmapMode ( VkSamplerMipmapMode mode )
{
mipmapMode = mode;
return *this;
}
Sampler& setMipmapBias ( float bias )
{
lodBias = bias;
return *this;
}
Sampler& setMinLod ( float v )
{
minLod = v;
return *this;
}
Sampler& setMaxLod ( float v )
{
maxLod = v;
return *this;
}
void create ( Device& dev );
};
Прежде всего нам понадобится описания структур для содержимого uniform-буфера (struct Ubo
)
и для описания отдельной вершины выводимой геометрии (struct Vertex
).
Также нам понадобится инстанциировать функцию registerVertexAttrs
для Vertex
.
struct Ubo
{
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
struct Vertex
{
glm::vec3 pos;
glm::vec3 color;
glm::vec2 texCoord;
};
template <>
GraphicsPipeline& registerVertexAttrs<Vertex> ( GraphicsPipeline& pipeline )
{
return pipeline
.addVertexAttr ( 0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos) )
.addVertexAttr ( 0, 1, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, color) )
.addVertexAttr ( 0, 2, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, texCoord) );
}
Далее нам нужно задать сам массив вершин (vertices
), задающий выводимую геометрию,
и массив индексов (indices
), задающий как строить треугольники из вершин.
const std::vector<Vertex> vertices =
{
{{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
{{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
{{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
{{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}},
{{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
{{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
{{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
{{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
};
const std::vector<uint16_t> indices =
{
0, 1, 2, 2, 3, 0,
4, 5, 6, 6, 7, 4
};
Теперь давайте посмотрим на начало описания главного класса приложения - ExampleWindow
.
Как и в предыдущем примере он содержит поля commandBuffers
, descriptorSets
, pipeline
, renderPass
.
Однако теперь у нас будет два буфера - вершинный vertexBuffer
и индексный indexBuffer
.
Поскольку мы собираемся читать из текстуры в шейдере у нас будет объекты texture
(сама текстура
из которой мы будем читать) и sampler
(объект, задающий как именно будет происходить чтение из текстуры).
Также нам понадобится uniform-буфер. Но здесь есть один момент, заметно отличающийся от того, что было у нас в OpenGL. В аналогичном примере на OpenGL у нас был всего один uniform-буфер. Но здесь у нас возможно будет происходить рендеринг сразу нескольких кадров (но при этом эти кадры будут соответствовать разным моментам времени).
А раз они соответствуют разным моментам времени, то uniform-буфера для них будут отличаться и поэтому нам нужен набор из нескольких uniform-буферов, по одному на каждый кадр.
class ExampleWindow : public VulkanWindow
{
std::vector<CommandBuffer> commandBuffers;
std::vector<DescriptorSet> descriptorSets;
std::vector<Uniform<Ubo>> uniformBuffers;
GraphicsPipeline pipeline;
Renderpass renderPass;
Buffer vertexBuffer;
Buffer indexBuffer;
Texture texture;
Sampler sampler;
Ниже приводятся добавленные в класс функции createUniformBuffers
и freeUniformBuffers
.
ExampleWindow ( int w, int h, const std::string& t ) : VulkanWindow ( w, h, t )
{
vertexBuffer.create ( device, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, vertices, Buffer::hostWrite );
indexBuffer.create ( device, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, indices, Buffer::hostWrite );
sampler.setMinFilter ( VK_FILTER_LINEAR ).setMagFilter ( VK_FILTER_LINEAR ).create ( device );
texture.load ( device, "textures/texture.jpg", false, false );
createPipelines ();
}
void createUniformBuffers ()
{
uniformBuffers.resize ( swapChain.imageCount() );
for ( size_t i = 0; i < swapChain.imageCount (); i++ )
uniformBuffers [i].create ( device, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT );
}
void freeUniformBuffers ()
{
uniformBuffers.clear ();
}
При создании множеств дескрипторов на нужно добавить в каждый из них дескрипторы на uniform-буфер и объединенный дескриптор на текстуру с сэмплером.
void createDescriptorSets ()
{
descriptorSets.resize ( swapChain.imageCount () );
for ( uint32_t i = 0; i < swapChain.imageCount (); i++ )
{
descriptorSets [i]
.setLayout ( device, descAllocator, pipeline.getDescLayout () )
.addUniformBuffer ( 0, uniformBuffers [i], 0, sizeof ( Ubo ) )
.addImage ( 1, texture, sampler )
.create ();
}
}
При создании объекта-конвейера (pipeline
) нам необходимо указать что мы будем
использовать uniform-буфер (binding 0) и текстуру с сэмплером (binding 1).
virtual void createPipelines () override
{
createUniformBuffers ();
createDefaultRenderPass ( renderPass );
pipeline.setDevice ( device )
.setVertexShader ( "shaders/shader.4.vert.spv" )
.setFragmentShader ( "shaders/shader.4.frag.spv" )
.setSize ( swapChain.getExtent () )
.addVertexBinding ( sizeof ( Vertex ) )
.addVertexAttributes <Vertex> ()
.addDescLayout ( 0, DescSetLayout ()
.add ( 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT )
.add ( 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT ) )
.setCullMode ( VK_CULL_MODE_NONE )
.create ( renderPass );
// create before command buffers
swapChain.createFramebuffers ( renderPass, depthTexture.getImageView () );
createDescriptorSets ();
createCommandBuffers ( renderPass );
}
virtual void freePipelines () override
{
commandBuffers.clear ();
pipeline.clean ();
renderPass.clean ();
freeUniformBuffers ();
descriptorSets.clear ();
descAllocator.clean ();
}
Слегка изменится и функция createCommandBuffers
.
void createCommandBuffers ( Renderpass& renderPass )
{
auto framebuffers = swapChain.getFramebuffers ();
commandBuffers = device.allocCommandBuffers ( (uint32_t)framebuffers.size ());
for ( size_t i = 0; i < commandBuffers.size(); i++ )
{
commandBuffers [i].begin ().beginRenderPass ( RenderPassInfo ( renderPass ).framebuffer ( framebuffers [i] ).extent ( swapChain.getExtent () ).clearColor ().clearDepthStencil () )
.pipeline ( pipeline )
.bindVertexBuffers ( { {vertexBuffer, 0} } )
.bindIndexBuffer ( indexBuffer )
.addDescriptorSets ( { descriptorSets[i] } )
.drawIndexed ( static_cast<uint32_t>(indices.size()) )
.end ();
}
}
Функция submit
ничем не отличается от того, что было в предыдущем примере, за исключением того что в ней появился вызов updateUniformBuffer
.
Ее задача - для текущего момента времени обновить содержимое uniform-буфера.
virtual void submit ( uint32_t imageIndex ) override
{
updateUniformBuffer ( imageIndex );
defaultSubmit ( commandBuffers [imageIndex] );
}
void updateUniformBuffer ( uint32_t currentImage )
{
float time = (float)getTime ();
auto fwd = glm::vec3(0.0f, 0.0f, 1.0f);
uniformBuffers [currentImage]->model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), fwd);
uniformBuffers [currentImage]->view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), fwd);
uniformBuffers [currentImage]->proj = projectionMatrix ( 45, getAspect (), 0.1f, 10 );
}
Исходный код этого примера находится в github репозитории по адресу https://github.com/steps3d/vulkan-with-classes.git.