3D perspective projection

Hello. I have created function for projection my 3D points at the 2D but it is doesn't work. When I increase ZOffset it shrinks as expected but it's also changes own position at the screen. What is wrong with that ? Thanks in advance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
v3
Project(f32 AspectRatio,f32 FocalLength, v3 P)
{
    v3 TransformedP;
    
    TransformedP.x = FocalLength*P.x / P.z;
    TransformedP.y = FocalLength*P.y*AspectRatio / P.z;
    
    return TransformedP;
}


void
GameUpdateAndRender(game_backbuffer *Backbuffer, controller *Input)
{
    
    v3 FrontFaceV0 = v3f(400.0f, 200.0f, 2.0f);
    v3 FrontFaceV1 = v3f(400.0f, 250.0f, 2.0f);
    v3 FrontFaceV2 = v3f(550.0f, 250.0f, 2.0f);
    
    v3 FrontFaceV00 = v3f(400.0f, 200.0f, 2.0f);
    v3 FrontFaceV01 = v3f(550.0f, 200.0f, 2.0f);
    v3 FrontFaceV02 = v3f(550.0f, 250.0f, 2.0f);
    
    v3 Vertices[6] = 
    {
        FrontFaceV0, FrontFaceV1, FrontFaceV2,
        FrontFaceV00, FrontFaceV01, FrontFaceV02
    };
    
    persist f32 Angle = 0.001f;
    
    
    ClearBackbuffer(Backbuffer);
    
    persist f32 ZOffset = 1.0f;
    if(Input->Controller[0].ButtonUp.IsDown)
    {
        ZOffset+=0.01f;
    }
    if(Input->Controller[0].ButtonDown.IsDown)
    {
        ZOffset-=0.01f;
    }
    
    
    f32 AspectRatio = (f32)WINDOW_WIDTH / (f32)WINDOW_HEIGHT;
    f32 FocalLength = 1.0f;
    
    for(u32 VerticesIndex = 0;
        VerticesIndex < CountOf(Vertices);
        VerticesIndex++)
    {
        Vertices[VerticesIndex].z = ZOffset;
        Vertices[VerticesIndex] = Project(AspectRatio, FocalLength, Vertices[VerticesIndex]);
    }
    
    DrawTriangle(Backbuffer, Vertices[0], Vertices[1], Vertices[2], 0x00FF00);
    DrawTriangle(Backbuffer, Vertices[3], Vertices[4], Vertices[5], 0x00FF00);
    
    Angle += 0.001f;
}

Edited by Roman on Reason: Initial post
Might need to center your coordinate system
It looks like you are using unsigned screen coordinates as 3D locations. When dividing by depth, your coordinates will then be drawn to the upper left corner. Create your models with coordinates centered around zero and then add the screen's center point as a perspective center after the projection.

The next step would be clipping
If you want to move closer to things, you might also want some triangle clipping using a view frustum to prevent crashing on NaN to integer conversions or getting smeared out geometry over the screen. The hard part about clipping triangles is to not create pixel sized holes along the new seam from precision errors between faces. Some graphics cards tesselate large triangles into quads before only drawing the visible ones (which can be observed when giving contradictory arguments as viewports in Vulkan). My software renderer places the clipping planes far outside of the image bounds so that less triangles have to be both visible and clipped.

Edited by Dawoodoz on
Thanks for such a detailed post. I will try center my coordinate system.
Hello. I have question about doing clipping and pipeline. I have read what clipping happens within cuboid shape and then in that shape I can use something like Sutherland–Hodgman algorithm to clip triangles. So, cuboid space defined in -1 to 1 coordinates right? Its mean I should map from image coordinates to NDC space after projection and only then to do clipping in range -1 to 1 right? Also I don't understand why is pespective divide transforms from clip space into NDC space by only dividing each component by w (typically w = 1). Are my TODO's in code correct? Thanks for any help. Sorry for bad english btw :c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
   for(u32 VerticesIndex = 0;
        VerticesIndex < CountOf(Vertices);
        VerticesIndex++)
    {
        Vertices[VerticesIndex].z = ZOffset;
        
        // NOTE(shvayko): Projection
        Vertices[VerticesIndex] = Project(AspectRatio, FocalLength, Vertices[VerticesIndex]);
        Vertices[VerticesIndex] = Vertices[VerticesIndex] + CENTER_OF_PROJECTION;
        
        // TODO(shvayko): Clipping
        
        // TODO(shvayko): Perspective divide(Divide by W = 1)
        
       // TODO(shvayko): Map the NDC coordinates into the window
    }
    
   // Rasterization stage

I've never done clipping manually, but in OpenGL (and probably DirectX and other apis) after the perspective projection you're in clip space.

In clip space, points that are "valid" (are in the view frustum) have their coordinates between -w and w (the 4th component of the transformed point), not -1 to 1. So points with coordinates not in the -w to w range need to be clipped (and produce new triangle if needed...).

After you are done clipping, the perspective projection is applied to produce value in normalized device coordinates (NDC) which means that x, y and z are divided by w to produce values in the -1 to 1 range.

I don't know if that answers you question.

Here is a another thread with conversations about perspective projection.

ExTray2020
Also I don't understand why is pespective divide transforms from clip space into NDC space by only dividing each component by w (typically w = 1)


I may be mistaken, but I believe that w shouldn't be 1 most of the time, unless you're doing orthographic projection.