2

I am trying to render a 2D triangle using user touches. So, I will let a user touch three points on the screen and those points will be used as vertices of a triangle.

Saumya
  • 108
  • 1
  • 5

3 Answers3

6

You're already aware that you need to return clip-space coordinates (technically not normalized device coordinates) from your vertex shader. The question is how and where to go from UIKit coordinates to Metal's clip-space coordinates.

Let's start by defining these different spaces. Note that below, I actually am using NDC coordinates for the sake of simplicity, since in this particular case, we aren't introducing perspective by returning vertex positions with w != 1. (Here I'm referring to the w coordinate of the clip-space position; in the following discussion, w always refers to the view width).

UIKit to Metal NDC transform

We pass the vertices into our vertex shader in whatever space is convenient (this is often called model space). Since we're working in 2D, we don't need the usual series of transformations to world space, then eye space. Essentially, the coordinates of the UIKit view are our model space, world space, and eye space all in one.

We need some kind of orthographic projection matrix to move from this space into clip space. If we strip out the unnecessary parts related to the z axis and assume that our view bounds' origin is (0, 0), we come up with the following transformation:

UIKit to NDC transformation as a matrix

We could pass this matrix into our vertex shader, or we could do the transformation prior to sending the vertices to the GPU. Considering how little data is involved, it really doesn't matter at this point. In fact, using a matrix at all is a little wasteful, since we can just transform each coordinate with a couple of multiplies and an add. Here's how that might look in a Metal vertex function:

float2 inverseViewSize(1.0f / width, 1.0f / height); // passed in a buffer
float clipX = (2.0f * in.position.x * inverseViewSize.x) - 1.0f;
float clipY = (2.0f * -in.position.y * inverseViewSize.y) + 1.0f;
float4 clipPosition(clipX, clipY, 0.0f, 1.0f);

Just to verify that we get the correct results from this transformation, let's plug in the upper-left and lower-right points of our view to ensure they wind up at the extremities of clip space (by linearity, if these points transform correctly, so will all others):

Some sample points transformed

These points appear correct, so we're done. If you're concerned about the apparent distortion introduced by this transformation, note that it is exactly canceled by the viewport transformation that happens prior to rasterization.

warrenm
  • 28,472
  • 4
  • 71
  • 102
0

Here is a function that will convert UIKit view-based coordinates to Metal's clip space coordinates (based on warrenm`s answer). It can be added directly to a shader file & called from the vertex shader function.

float2 convert_to_metal_coordinates(float2 point, float2 viewSize) {
    
    float2 inverseViewSize = 1 / viewSize;
    float clipX = (2.0f * point.x * inverseViewSize.x) - 1.0f;
    float clipY = (2.0f * -point.y * inverseViewSize.y) + 1.0f;
    
    return float2(clipX, clipY);
}

You'll want to pass the viewSize (UIKit's bounds) to Metal somehow, say via a buffer parameter on the vertex function.

0

Translated Thompsonmachine's code to swift, using SIMD values which is what I need to pass to shaders.

func convertToMetalCoordinates(point: CGPoint, viewSize: CGSize) -> simd_float2 {
    let inverseViewSize = CGSize(width: 1.0 / viewSize.width, height: 1.0 / viewSize.height)
    let clipX = Float((2.0 * point.x * inverseViewSize.width) - 1.0)
    let clipY = Float((2.0 * -point.y * inverseViewSize.height) + 1.0)
    return simd_float2(clipX, clipY)
}
possen
  • 6,778
  • 2
  • 34
  • 43