Skip to content

Textures

Sample 05

Samples/Textures ยท builds on basic rendering.

This sample draws a textured quad. It covers loading an image, uploading it to the GPU, generating mip maps and sampling it in the shader.

Texture coordinates

The shader needs to know where on the image each vertex sits, which is what texture coordinates describe. The Vertex structure already has them, so only the input assembler gains another attribute:

.withAttribute(2, BufferFormat::XY32F, offsetof(Vertex, TextureCoordinate0), AttributeSemantic::TextureCoordinate, 0)

In the fragment shader, the texture and its sampler are two separate bindings:

ConstantBuffer<CameraData> camera : register(b0, space0);
Texture2D diffuseMap              : register(t1, space0);
SamplerState diffuse              : register(s2, space0);

FragmentData PSMain(VertexData input)
{
    FragmentData fragment;
    fragment.Depth = input.Position.z;
    fragment.Color = diffuseMap.Sample(diffuse, input.TextureCoordinate) * input.Color;
    return fragment;
}

The sampler describes how the image is read: how it is filtered between pixels and mip levels, and what happens outside its bounds.

Creating the texture

The image file is loaded with stb_image, and the texture is created with six mip levels:

auto imageData = ImageDataPtr(::stbi_load("assets/logo_quad.tga", &width, &height, &channels, STBI_rgb_alpha), ::stbi_image_free);

if (imageData == nullptr)
    throw std::runtime_error("Texture could not be loaded: \"assets/logo_quad.tga\".");

texture = device.factory().createTexture("Texture", Format::R8G8B8A8_UNORM, Size2d(width, height), ImageDimensions::DIM_2, 6, 1,
    MultiSamplingLevel::x1, ResourceUsage::AllowWrite | ResourceUsage::TransferDestination | ResourceUsage::TransferSource);

The usage flags say what the texture is used for: it is written by the mip map generation (AllowWrite), and it is both the destination of the upload and the source when the mip levels are generated from each other.

Barriers and layouts

Unlike buffers, images have a layout that describes how the GPU currently stores them. A layout is optimal for exactly one purpose, so the image has to be transitioned before it is used differently. A barrier performs that transition, and at the same time orders the commands around it:

UniquePtr<TBarrier> barrier = device.buildBarrier()
    .waitFor(PipelineStage::None).toContinueWith(PipelineStage::Transfer)
    .blockAccessTo(*texture, ResourceAccess::TransferWrite).transitionLayout(ImageLayout::CopyDestination).whenFinishedWith(ResourceAccess::None);

commandBuffer->barrier(*barrier);
commandBuffer->transfer(imageData.get(), texture->size(0), *texture);

Read as a sentence: wait for nothing, continue with transfer commands, and block their write access to the texture until it has been transitioned to the layout for copy destinations.

After the upload, the smaller mip levels are generated from the largest one. The engine provides a Blitter for this, which records the necessary dispatches and barriers:

auto blitter = Blitter<TBackend>::create(device);
blitter->generateMipMaps(*texture, *commandBuffer);

Finally, the texture is transitioned into the layout for shader resources, so the fragment shader can sample it:

barrier = device.buildBarrier()
    .waitFor(PipelineStage::All).toContinueWith(PipelineStage::Fragment)
    .blockAccessTo(*texture, ResourceAccess::ShaderRead).transitionLayout(ImageLayout::ShaderResource).whenFinishedWith(ResourceAccess::None);

commandBuffer->barrier(*barrier);
auto transferFence = commandBuffer->submit();

Mip map generation is a compute workload, so the command buffer is created on the graphics queue here, not on the transfer queue.

The sampler

The sampler is created separately from the texture, and the same sampler can be used with many textures:

sampler = device.factory().createSampler("Sampler", FilterMode::Linear, FilterMode::Linear,
    BorderMode::Repeat, BorderMode::Repeat, BorderMode::Repeat, MipMapMode::Linear,
    0.f, std::numeric_limits<Float>::max(), 0.f, 16.f);

Linear filtering interpolates between neighboring pixels and mip levels, Repeat tiles the image outside its bounds, and the last argument enables anisotropic filtering, which keeps steeply viewed surfaces sharp.

Texture and sampler share the descriptor set with the camera, since all three are written once and then stay unchanged:

auto staticBindings = staticBindingLayout.allocate({ { 0, *cameraBuffer }, { 1, *texture }, { 2, *sampler } });