# How to implement cylindrical projection?

This question (and particularly, its answer) talked about how traditional, planar projection matches most monitors... except when it doesn't.

Assuming I'm a glutton for punishment, and really, really must, at whatever (performance) cost, have accurate projection for a curved monitor, how would I go about coding a cylindrical projection?

Ignore the boring affine transformation from world space to camera space. Assume my input coordinates are X,Y,Z, with the camera at the origin, looking at an arbitrary, fixed vector of your choice (either +x or +y, unless you're feeling evil). Assume that the screen represents a partial cylinder with H denoting the arc length and V denoting the angle between the top and bottom (i.e. the same vfov as in planar projection). How do I go from my input X,Y,Z to the projection onto the cylinder?

Assuming that the cylinder has radius r, the expected result should be a coordinate with -π < x < π, and where 0,1,1 represents a point at (world) z=r whose projection into the XY plane is r from the origin.

I am just looking for the math or pseudocode; answers do not need to be in a specific language (e.g. GLSL).

Possible duplicate of this question which didn't receive any answers 😢.

• "Unless you're feeling evil" would this be a bad time to mention that graphics software typically uses x and y for the plane of the screen, so the view axis is almost always ±z? I guess all graphics APIs were feeling evil... 😈 Commented Jan 5, 2022 at 22:25
• @DMGregory, well, strictly speaking, if you want to give me an answer for world +z = "forward", world +y = up, that'd be fine; it's just an isometry. (I usually use world +z = up, though, at least for something like an FPS.) Commented Jan 6, 2022 at 15:44

## 2 Answers

The comment on the linked question is correct. If you did ray casting/marching/tracing, you just need to shoot the rays in the desired configuration (i.e. a cylinder) - an the rest of the process would be same as usual for those techniques.

So, if you are familiar with those techniques that tidbit is enough to get you started. And why why do we want those techniques? Because under these projection straight lines may appear curved, which is not posible to accomplish on the vertex shader. So we resource to ray casting and similar techniques.

As an alternative, you could do a two pass process: the first pass projects the geometry with a regular perspective projection to a texture, and a second pass renders the texture with the desired distortion (a barrel distortion), which could be approximated by applying the texture to curved geometry. In fact, if you are interested in the old monitor look, I'd argue, this is the way to go.

If you were doing ray casting/marching/tracing. The direction of the ray would take the x (after normalizing) as an angle. So your ray setup looks something like this (I have tested this in ShaderToy):

float camDst = 1.0f / tan(FOV * 0.5 * PI / 180.0);
vec2 screenPos = (fragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
vec3 rayTarget = vec3(sin(screenPos.x) * camDst, screenPos.y, cos(screenPos.x) * camDst);
vec3 rayDir = normalize(rayTarget);
vec3 rayPos = vec3(0.0, 0.0, 0.0);


Notice that here rayTarget is a cylinder of camDst radius, positioned vertically, centered at the origin.

When it would have usually been like this:

float camDst = 1.0f / tan(FOV * 0.5 * PI / 180.0);
vec2 screenPos = (fragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
vec3 rayTarget = vec3(screenPos.x, screenPos.y, camDst);
vec3 rayDir = normalize(rayTarget);
vec3 rayPos = vec3(0.0, 0.0, 0.0);


Notice that here rayTarget is a plane, parallel to the XY plane, at camDst from the origin. Yes, the camera is looking toward positive Z. Thanks to this "EvIl" the x and y axis of screen space and camera space are oriented the same way.

Where:

• iResolution is an uniform with the pixel size of the viewport.
• fraagCoord is the position of the pixel in normalized device coordinates (they go from -1.0 to 1.0).
• FOV is an uniform or constant with the field of view in degrees.
• PI is a constant with value the of π.

Alright, so we have a point in XYZ. And following with the above "EvIl", I'll work the screen coordinates from the above code.

The first line we can keep:

float camDst = 1.0f / tan(FOV * 0.5 * PI / 180.0);


Then, assume the point was hit a ray (and the camera is at the origin), so we know the direction of the ray:

float camDst = 1.0f / tan(FOV * 0.5 * PI / 180.0);
vec3 rayDir = normalized(X, Y, Z);


From the code:

vec3 rayDir = normalize(rayTarget);


We have that:

rayDir = normalize(rayTarget)
rayDir = f * rayTarget


We don't know f.

Let us see the components separately:

rayDir = normalize(rayTarget)
f * rayTarget.x = rayDir.x
f * rayTarget.y = rayDir.y
f * rayTarget.z = rayDir.z


And replace with rayTarget:

vec3 rayTarget = vec3(sin(screenPos.x) * camDst, screenPos.y, cos(screenPos.x) * camDst);


Which gives us:

rayDir = normalize(rayTarget)
f * sin(screenPos.x) * camDst = rayDir.x
f * screenPos.y = rayDir.y
f * cos(screenPos.x) * camDst = rayDir.z


I'll call screenPos.x as angle for now:

rayDir = normalize(rayTarget)
angle = screenPos.x
f * sin(angle) * camDst = rayDir.x
f * screenPos.y = rayDir.y
f * cos(angle) * camDst = rayDir.z


Since vec2(sin(angle), cos(angle)) must have length of 1, we can get it like this:

vec2(sin(angle), cos(angle)) = normalize(vec2(rayDir.x, rayDir.z))


Thus, we can get screenPos.x like this:

screenPos.x = angle = atan2(rayDir.x, rayDir.z)


We can get f by either of these means now:

f = rayDir.x / (sin(angle) * camDst)
f = rayDir.z / (cos(angle) * camDst)


And with f, we have screenPos.y:

screenPos.y = rayDir.y / f


So far the code looks something like this:

float camDst = 1.0f / tan(FOV * 0.5 * PI / 180.0);
vec3 rayDir = normalized(X, Y, Z);
float angle = atan2(rayDir.x, rayDir.z);
float f = rayDir.z / (cos(angle) * camDst);
vec2 screenPos = vec2(angle, rayDir.y / f);


To get the fragment coordinates, we need to undo this:

vec2 screenPos = (fragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;


Like this:

float camDst = 1.0f / tan(FOV * 0.5 * PI / 180.0);
vec3 rayDir = normalized(X, Y, Z);
float angle = atan2(rayDir.x, rayDir.z);
float f = rayDir.z / (cos(angle) * camDst);
vec2 screenPos = vec2(angle, rayDir.y / f);
vec2 fargCoord = (screenPos * iResolution.y) + (0.5 * iResolution.xy);


And that fargCoord should be the position in pixels on the screen.

Some links I found while looking at related questions:

• Okay... this is assuredly not the answer I "wanted", but it's useful. I think the way this would have to work is to also use subdivision, so that no individual triangle is large enough (in screen space) for the "issues" to be obvious. (Each subdivided vertex would need to be transformed.) Commented Jan 6, 2022 at 15:47
• @Matthew sounds like you want to do this in a vertex shader. Which will never be perfect, and I'm advocating against in the answer. But, yes, you can approximate it with more subdivisions. Commented Jan 6, 2022 at 18:50

I'm assuming you want to match a Curved Monitor with Cylindrical Projection. (and since you're not asking about spherical projection you are going for a very wide monitor with limited height)

Have you considered using polar coordinates for your input? I think then the game engine would only need the minimal transformations equations to project the "view" to a "cylinder" Monitor.

This would produce a picture without the distorted edges of a rectilinear projection. (with exception of some limited distortion due to height distances) However, you would have to match the FOV (field of view) to the monitor size and curvature to have a proper picture (with limited distortion). So if the monitor is like a quarter of a circle, your matching FOV would be 90 degrees. As you go further from that there will be more distortion. To calculate what part of a circle a monitor is you need to know the radius (most curved monitors I see are like 1800 mm) and the size (length).