I’ve been inspired by the fact that a very nice person commented on my previous post about getting the world-space coordinates of the near plane of the view frustum to revisit this topic, because it’s been bugging me that my previous technique required a matrix multiplication and that feels like it might be more expensive than strictly needed. As I discussed before, you might want to know where exactly in your world the screen is so that if it intersects with something, you can treat that part of the screen differently – for example if your camera is partially submerged in water, you might want to apply a fogging and distortion effect to those fragments below the surface, but not above it.
The first thing to understand is how your camera is oriented. For historical reasons, and because of the way that my world axes are set up (+x is east, +y is north, +z is down; the camera’s neutral direction is looking north down the y axis.), the camera orients itself in world space by rotating around the z axis to look left-right, and around the x axis to look up-down. Just to make things more confusing, because the world moves around the camera in OGL, remember that in your shaders, the camera’s coordinates are negative (i.e. your shaders think your camera is at (-cameraX, -cameraY, -cameraZ). You can cut through a lot of confusion by using a system like gluLookAt() to orient your camera, which confers a huge bonus in that it returns both the direction in which the camera is facing and also the camera’s direction for “up”, which will be very handy.
The first step is to work out where the camera is and which direction it’s looking. In my case, I keep track of the camera’s position as (cameraX, cameraY, cameraZ), and rotation around Z and X in radians (i.e. pi radians is 180 degrees). My camera matrix rotates the camera around the Z axis and then around its own X axis, and then translates to its location in world space. Using this system, the camera’s unit vector is worked out like this:
float sinPhi = sinf(camZRot); float cosPhi = cosf(camZRot); float sinTheta = sinf(camXRot); float cosTheta = cosf(camXRot); float xInFrontOfCamera = (1.0 * sinPhi * sinTheta); float yInFrontOfCamera = (1.0 * cosPhi * sinTheta); float zInFrontOfCamera = (1.0 * cosTheta);
Therefore, the central point of the camera frustum’s near plane in world space is:
float centreOfNearCameraPlaneWSX = -camXPos - xInFrontOfCamera * MAIN_FRAMEBUFFER_NEAR_PLANE; float centreOfNearCameraPlaneWSY = -camYPos - yInFrontOfCamera * MAIN_FRAMEBUFFER_NEAR_PLANE; float centreOfNearCameraPlaneWSZ = -camZPos - zInFrontOfCamera * MAIN_FRAMEBUFFER_NEAR_PLANE;
We need to figure out where the bottom-left hand corner of the near plane of the camera frustum is in world space, because that will map to (0, 0) in screen coordinates. To do that we need to know the dimensions of the near camera plane in world space units, and the vectors along which the camera plane’s x and y axes are aligned. Fortunately, these are exactly the same as “up” and “right” from the camera’s point of view. “Up” is conveniently always going to be found by rotating pi/2 radians anticlockwise around the x-axis, so the unit “up” vector is:
float sinThetaUp = sinf(camXRot - piOverTwo); float cosThetaUp = cosf(camXRot - piOverTwo); float cameraUpX = (1.0 * sinPhi * sinThetaUp); float cameraUpY = (1.0 * cosPhi * sinThetaUp); float cameraUpZ = (1.0 * cosThetaUp);
The “right” vector is now easy to get: it’s the cross-product of the camera facing vector and the camera “up” vector. Now we can work out where the bottom left-hand corner of the clipping plane is:
float aspectRatio = screenWidth/screenHeight; float nearClippingPlaneHeight = (2 * tanf(CAMERA_FOV / 2) * NEAR_PLANE_DISTANCE); float nearClippingPlaneWidth = nearClippingPlaneHeight * aspectRatio; float bottomLeftX = centreOfNearCameraPlaneWSX - (cameraUpX * halfNearClippingPlaneHeight) - (cameraRightX * halfNearClippingPlaneWidth); float bottomLeftY = centreOfNearCameraPlaneWSY - (cameraUpY * halfNearClippingPlaneHeight) - (cameraRightY * halfNearClippingPlaneWidth); float bottomLeftZ = centreOfNearCameraPlaneWSZ - (cameraUpZ * halfNearClippingPlaneHeight) - (cameraRightZ * halfNearClippingPlaneWidth);
We pass the world space position of the bottom left corner, screen dimensions, “up” vector and “right” vector to our shader as uniforms, and we can get the world space position of screen fragments in a single line, like so:
vec3 worldSpacePositionOfScreenFragment = vec3(bottomLeftCornerWS + cameraUpVector*nearPlaneDimensionsWS.y * (gl_FragCoord.y/screenHeightInPixels) + cameraRightVector * nearPlaneDimensionsWS.x * (gl_FragCoord.x/screenWidthInPixels);
The final result feels less precise than the previous technique, but might be a bit cheaper in terms of shader calculations.