Perspective projection and depth bounds (Video)

Video demonstrating the problem:


(Glitches appear when vertices approach the z-plane and projection occurs from both front and back despite depth bounds being set for normalized device coordinates)

My Vulkan application exhibits some perculiar behaviour. I suspect this is due to the z-axis clipping not happening, causing vertices on the z-plane to be projected to vertices with infinately large x and y values. I’ve verified that my projection maps the frustrum to a canonical view volume with corners (-1, -1, 0) and (1, 1, 1) and I’ve set the following parameters:

VkViewport viewport;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
..

VkPipelineDepthStencilStateCreateInfo depth_stencil_info;
depth_stencil_info.depthBoundsTestEnable = VK_TRUE;
depth_stencil_info.minDepthBounds = 0.0f;
depth_stencil_info.maxDepthBounds = 1.0f;
..

Any idea what I’m missing here?

Probably a sign-flip in perspective division, with w becoming negative?

How do you create the projection matrix?

The specs mention (in section 23.2. Primitive Clipping) a state called depthClampEnable in VkPipelineRasterizationStateCreateInfo.

depthClampEnable was VK_TRUE, setting it to VK_FALSE resolved the hull inverting when passing through it. Z-plane glitches remain unchanged. I use the DirectX perspective projection matrix from Real Time Rendering 3rd Ed. p. 96:

Projection generation:

typedef struct matrix4 {
	float e11, e21, e31, e41, e12, e22, e32, e42, e13, e23, e33, e43, e14, e24,
		e34, e44;
} matrix4;

void transform_perspective_projection(const float left, const float right,
	const float bottom, const float top, const float near, const float far,
	matrix4 *mout) {

	matrix4 m;

	m.e11 = (2.0f * near) / (right - left);
	m.e21 = 0.0f;
	m.e31 = 0.0f;
	m.e41 = 0.0f;

	m.e12 = 0.0f;
	m.e22 = (2.0f * near) / (top - bottom);
	m.e32 = 0.0f;
	m.e42 = 0.0f;

	m.e13 = - (right + left) / (right - left);
	m.e23 = - (top + bottom) / (top - bottom);
	m.e33 = far / (far - near);
	m.e43 = 1.0f;

	m.e14 = 0.0f;
	m.e24 = 0.0f;
	m.e34 = - (far * near) / (far - near); 
	m.e44 = 0.0f; 

	*mout = m;
}

get_projection_transform(matrix4 *projection) {	
	float aspect_ratio = 16.0f / 9.0f;

	float l = -0.1f;
	float r = 0.1f;
	float b = 0.1f / aspect_ratio;
	float t = -0.1f / aspect_ratio;
	float n = -0.1f;
	float f = -1000.0f;

	transform_perspective_projection(l, r, b, t, n, f, projection);
}

Vertex shader:

..
void main() {
	out_color = push_constants.color;
	out_position = position;

	gl_Position = 
		push_constants.projection_transform *
		push_constants.view_transform *
		push_constants.model_transform *
		vec4(position, 1.0);
		
	gl_Position /= gl_Position.w;
}

From what I know from other 3D APIs, the final perspective divide is internally done by fixed function rasterizer. Not sure if this has changed with vulkan, but it’s probably worth a try uncommenting that explicit divide…

I removed the following line in my vertex shader and no primitives were rendered:

gl_Position /= gl_Position.w;

That suggests that fixed function homogenization is either inactive or nonpresent. Even if it wasn’t, it would cause gl_Position to be divided by 1.0 and not change the result, unless something is seriously wrong. I’m gonna try to print out some post transform vertex coordiates from the shader to see if something looks out of place.

True, w should become 1 (and this should effectively deactivate perspective correction of texcoords and the like).

But I’d be suprised if that internal divide would be gone in vulkan. AFAIR it’s internally even a per Fragment operation. And DX9, webGL and GL to some version all defintely did that after vertex shading.

Sure, it’s strange that your rendering is gone after commenting that line. Sorry, I don’t see what it is, but it’ll still be some sign-bug, I guess.

Your intuition is correct, version 1.1 and 1.2 of the Vulkan spec confirms that that it’s indeed fixed-function. You’re definately onto something here, removing the explicit perspective division in the shader should not cause the primitives to change.

I’ve uploaded my code to GitHub:

If you continue reading in the section about interpolation (for example, 24.6.1. Basic Line Segment Rasterization), you’ll see that the w component is used there (even though it isn’t part of device-normalized coords).

For example, if you have a line from A to B in 4D coords as from vertex shader. Let them map to somewhere on screen, with 2D pixel coords. If you then (for another frame) decide to scale the 4D coords of A by scalar s>0, A’s mapping to 2D coords doesn’t change. The same Pixels on screen are effected. But there is something that changes: by varying s, the screenspace-center of the line can by adjusted to be near to A or near to B (in terms of texcoords, for example).

But true, this doesn’t have to do with your prob anymore. However, it’s pretty normal to have sign-bugs when initially trying out a 3D API. It’s nothing logical, just stupid detail to care about.

I solved it! I removed the perspective division in the vertex shader, changed the near and far planes from -0.1f and -1000f to 0.1f and 1000f and changed frontFace in VkPipelineRasterizationStateCreateInfo from VK_FRONT_FACE_COUNTER_CLOCKWISE to VK_FRONT_FACE_CLOCKWISE. Reintroducing the division causes the glitchs to return, so that’s certainly what’s causing them. I haven’t begun to understand all the intricacies of this, but I’ll figure it out. Thanks Jan, you’ve been a huge help! I’ve been stuck on this forever.