Vulkan’s coordinate system

One of the key differences between OpenGL and Vulkan -and something that needs careful consideration when porting to Vulkan- is the coordinate system. Khronos’ Vulkan working group decided not to use GL’s commonly used coordinate conventions in favor of something more widely used and accurate and that’s the main reason behind this shift.

The first thing to note is that Vulkan requires the right hand NDC space compared to GL that requires the left hand. In other words the (-1, -1) NDC coordinate maps to the top left corner for Vulkan and to the bottom left corner for GL. In practice, if a Vulkan application ignored this convention the final output will end up flipped in the Y axis and inside out. Khronos group understood that this difference is not easy to workaround that’s why they tried to fix it as part of KHR_VK_maintainance1 extension. What KHR_VK_maintainance1 basically does is to allow a negative height in VkViewport structure. In practice this allows us to flip the viewport in the Y axis during the viewport transform.

How AnKi is flipping the viewport is a bit weird and I’m not sure if it’s the best way to go.

  • The first thing that needs to happen is to enable KHR_VK_maintainance1 extension when creating the device.
  • When drawing to offscreen framebuffers the VkPipelineRasterizationStateCreateInfo::frontFace is the opposite of what it supposed to be. The result is rendered to the offscreen framebuffers flipped.
  • The final piece of the puzzle is what happens when drawing into the swapchain images (a.k.a. default framebuffer). Unlike offscreen rendering the VkPipelineRasterizationStateCreateInfo::frontFace is not changed but the viewport’s height gets negated and that will cause a flip.

The logical question here is why do we need the second step? Why not flip the viewport at all times and for all types of render passes? Vulkan not only expects a right hand NDC space but it also requires gl_FragCoord’s origin to be at the top left corner as well. gl_FragCoord’s origin is configurable in GL but SPIR-V doesn’t allow us to move it to the bottom left corner. The SPIR-V specification has some wording in place for a bottom left origin but at the moment it’s an error to use it. The fact that offscreen render passes are logically flipped helps us to workaround this limitation.

This is the code that changes the frontFace (in AnKi the frontFace is not configurable and it’s always CCW):

VkPipelineRasterizationStateCreateInfo rastCi;
rastCi.frontFace = (!m_defaultFb) ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE;

And this is the code that flips the viewport:

const Bool flipvp = m_defaultFb;

const int minx = m_viewport[0];
const int miny = m_viewport[1];
const int maxx = m_viewport[2];
const int maxy = m_viewport[3];

unsigned fbWidth, fbHeight;
getBoundFramebufferAttachmentsSize(fbWidth, fbHeight);

VkViewport s;
s.x = minx;
s.y = (flipvp) ? (fbHeight - miny) : miny; // Move to the bottom;
s.width = maxx - minx;
s.height = (flipvp) ? -(maxy - miny) : (maxy - miny);
s.minDepth = 0.0;
s.maxDepth = 1.0;
vkCmdSetViewport(m_handle, 0, 1, &s);

The next difference between the two APIs is the range or Z in NDC space (a.k.a. Zd). In GL, by using a typical GL projection matrix Zd ends up in [-1, 1] after the perspective division just like X and Y. The viewport transform for the Z component is given by this equation:

((f-n)/2)*Zd + (n+f)/2

The f and n are the far and near limits and they are typically 0.0 and 1.0 respectively. So the equation above gives a new range in [0, 1] and that’s the depth value that will be used for depth testing and will be stored in the depth buffer. The fact that GL expects Zd in [-1, 1] gives some kind of consistency since all 3 components (X, Y and Z) end up in the same range.

One interesting property of floating point numbers (in the [0.0, 1.0] range) is that they have better precision closer to 0.0. This property has an interesting impact in the accuracy of the depth buffer. For GL’s range it practically gives OK accuracy close to the near plane (Zd==-1) the best quality somewhere close to the camera (Zd==0.0) and worse close to the far plane (Zd==1). Direct3D is using another convention where it expects Zd in [0.0, 1.0] for optimal depth accuracy. Vulkan went the same road.

In Vulkan, the viewport transform for the Z component is given by this equation instead:

(maxDepth-minDepth)*Zd + minDepth

maxDepth is typically 1.0 and minDepth 0.0. If Zd is mapped in [0, 1] then depth will be in [0, 1] as well.

It’s worth noting that GL 4.5 supports Vulkan’s convention through GL_ARB_clip_control extension. To switch to the new system you can call glClipControl(XXX, GL_ZERO_TO_ONE) to move from GL’s default GL_NEGATIVE_ONE_TO_ONE to Vulkan’s GL_ZERO_TO_ONE.

Having a common system for Zd for both GL and Vulkan is a bit tricky but there are two solutions. The first is to use GL’s convention for both APIs and append this at the end of every Vulkan vertex shader:

gl_Position.z = (gl_Position.z + gl_Position.w) / 2.0;

Without going into much detail the above code re-maps the Zd to what Vulkan expects.

The second solution is to use Vulkan’s convention and use GL_ARB_clip_control extension to move GL to what Vulkan prefers. This sounds like a better solution because it allows for a better depth buffer accuracy and no extra overhead in the vertex shader. Unfortunately it requires a big code refactoring to move from one coordinate system to another. It’s also worth noting that GL_ARB_clip_control is not part of OpenGL ES and that limits this solution to desktop GL only.

Initially, AnKi was using the vertex shader trick but eventually I decided to use the second solution. That required a few careful changes but overall everything worked just fine. For AnKi these changes were:

  • Alter all the projection matrices to map Zd to the new range. In AnKi there are a few functions that generate projection matrices so this change was easy enough.
  • Change the shader code that unprojects depth to view space Z. In AnKi there are quite a few places that do that but overall no issues. The shader code became a bit simpler as well.
  • Align the software rasterizer to the new system as well. No big issues there as well.

Overall the current way of handling GL and Vulkan’s coordinates in the same codebase works quite well for AnKi without any major issues at the moment. If you found yourself in a similar situation and you want to share your thoughts don’t hesitate to drop a comment bellow.

Posted in Uncategorized | Tagged , | Leave a comment

Clustered volumetric fog

I’ve written an article on how volumetric fog works in AnKi. You can read it at ARM’s developer portal. This is still in progress but for now the performance is acceptable.

Posted in Uncategorized | Tagged , | Leave a comment

Porting AnKi to Vulkan part 2

In my Porting AnKi to Vulkan post I went into detail describing how AnKi’s interfaces changed to accommodate the Vulkan backend and how this backend looked like. Eight months have passed since then and a few things changed mainly towards greater flexibility. This post describes what are the differences with the older interfaces, how is the performance currently and what new extensions AnKi is using now.

Continue reading

Posted in Uncategorized | Tagged , , | Leave a comment

Volumetric lights/fog test

Short video of a test for volumetric lights. It is using the same information as the cluster deferred shading path. The shader iterates every cluster and samples at a random position inside the cluster. The result is too noisy at the moment, especially with shadows.

Posted in Uncategorized | Tagged | Leave a comment

One decade working on AnKi. Uploaded the first snapshot I found in my archive

It’s almost 10 years of AnKi so I decided to to upload a very old snapshot of the engine I have lying in my hard drive. This early snapshot is a few years before I used any revision control. Browse the code here https://github.com/godlikepanos/anki-3d-engine-2007

Some interesting facts:

  • It started as a simple skeletal animation project and grow to what it is now.
  • Was featuring a blender 2.4 exporter for meshes, skeletons and skeletal animations.
  • It was running on OpenGL 1.1 with immediate mode.
  • The code is a weird mixture of (Microsoft-centric) C++ and C.
  • It was compiling only in ancient versions of VisualC++. Had to struggle to get it compile with GCC.
  • Different code style from what AnKi has now.
  • The first name of AnKi was Malice. Kept that name for a few months.
Posted in Uncategorized | Leave a comment

Deferred decals video demonstration

Short video of deferred decals in AnKi. For now it supports diffuse and roughness.

Posted in Uncategorized | Leave a comment

The journey of porting AnKi to Vulkan

Someone once said “make it work, make it fast, make it pretty” and I’m happy (and at the same time relieved) to say that the effort of porting AnKi to Vulkan just hit the first major milestone! It’s working! I think this is a good time to share a few thoughts on how it was achieved, the pains and generally the overall experience. Disclaimer: Whatever you read in the following lines reflects my own views and not those of my current employer.

So let’s start from the beginning. Continue reading

Posted in Uncategorized | Tagged , | 4 Comments

Solving the Linux multi-monitor problem with SDL 2.0

A few days back I decided to upgrade my Ubuntu 14.04 LTS to Ubuntu 16.04 LTS and the first thing I did, after the upgrade, was to re-build and test AnKi. Everything seemed to work fine except the fullscreen mode. In my dual-monitor setup and with 14.04 SDL was creating a fullscreen SDL_Window in my primary monitor and the second monitor was left alone. In 16.04 though the fulscreen window covered both the monitors and it was placed in a very odd position. This is the code I’m using to create the SDL_Window:

U32 flags = SDL_WINDOW_OPENGL;

if(init.m_fullscreenDesktopRez)
{
	flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}

m_impl->m_window = SDL_CreateWindow(&init.m_title[0],
	SDL_WINDOWPOS_UNDEFINED,
	SDL_WINDOWPOS_UNDEFINED,
	init.m_width,
	init.m_height,
	flags);

I spend quite a few hours trying to understand the issue and after some digging I found out that SDL was reporting only one display (SDL_GetNumVideoDisplays) with the resolution of my entire desktop. Obviously that was wrong. After trying different stuff I found out the culprit. Apparently SDL 2.0 is using libxinerama.so to query some information related to multi-monitor setups. Since my Ubuntu installation was brand new I didn’t have that library installed. That didn’t stop SDL from building but querying multiple monitors was not working properly.

Hopefully this sort post will help people that have similar problems. If you have multiple monitors and you are using SDL 2.0 install libxinerama-dev.

Posted in Uncategorized | Tagged , | Leave a comment