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 Having a common system for Zd for both GL and Vulkan is a bit tricky but there are two solutions. 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. It’s worth noting that GL 4.5 supports Vulkan’s convention through GL_ARB_clip_control extension. If Zd is mapped in then depth will be in as well. MaxDepth is typically 1.0 and minDepth 0.0. In Vulkan, the viewport transform for the Z component is given by this equation instead: (maxDepth-minDepth)*Zd + minDepth Direct3D is using another convention where it expects Zd in for optimal depth accuracy. 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). This property has an interesting impact in the accuracy of the depth buffer. One interesting property of floating point numbers (in the range) is that they have better precision closer to 0.0. The fact that GL expects Zd in gives some kind of consistency since all 3 components (X, Y and Z) end up in the same range. So the equation above gives a new range in and that’s the depth value that will be used for depth testing and will be stored in the depth buffer. The f and n are the far and near limits and they are typically 0.0 and 1.0 respectively. The viewport transform for the Z component is given by this equation: ((f-n)/2)*Zd + (n+f)/2 In GL, by using a typical GL projection matrix Zd ends up in after the perspective division just like X and Y.
The next difference between the two APIs is the range or Z in NDC space (a.k.a. S.height = (flipvp) ? -(maxy - miny) : (maxy - miny) S.y = (flipvp) ? (fbHeight - miny) : miny // Move to the bottom GetBoundFramebufferAttachmentsSize(fbWidth, fbHeight)
ontFace = (!m_defaultFb) ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE Īnd this is the code that flips the viewport: const Bool flipvp = m_defaultFb This is the code that changes the frontFace (in AnKi the frontFace is not configurable and it’s always CCW): VkPipelineRasterizationStateCreateInfo rastCi The fact that offscreen render passes are logically flipped helps us to workaround this limitation. 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. gl_FragCoord’s origin is configurable in GL but SPIR-V doesn’t allow us to move it to the bottom left corner. 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. Unlike offscreen rendering the VkPipelineRasterizationStateCreateInfo::frontFace is not changed but the viewport’s height gets negated and that will cause a flip.
In other words the (-1, -1) NDC coordinate maps to the top left corner for Vulkan and to the bottom left corner for GL. The first thing to note is that Vulkan requires the right hand NDC space compared to GL that requires the left hand. 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. One of the key differences between OpenGL and Vulkan -and something that needs careful consideration when porting to Vulkan- is the coordinate system.