0
\$\begingroup\$

I'm not sure distortion is the right way to describe this. I've uploaded a video to show you guys. Apologies if it's long, but I wanted to show the code as well as posting it here. https://youtu.be/NfcGHjXZJpo Look at the cube carefully when I look down on it from the side.

Basically, when I rotate up or down when I'm close to a polygon, it just seems like it's stretching up or down. It doesn't feel natural to me. I'm not sure what it is or what's causing it. I thought it had to do with either world to camera or camera to perspective transforms, but they all seem right. Aspect ratios are fine too.

My setup:

  • I have a low res and high res framebuffers. Both have the same aspect ratio. I render the game to the low res buffer, and render font for example to the high res. When it's time to blit, I "stretch-blit" the low-res buffer to the high-res, and "bit-blit" the high res buffer directly to the screen. (I was stretch-blitting it for testing in the video, but it's a bit-blit)

  • I don't use any matrices. All my transformations are functions. For sake of simplicity and ease of 3D education, will add matrices later after I'm comfortable with things without them.

  • Camera uses quaternions for it's rotation. The effect still happens if I used a simpler euler angles camera.

Here's what I think is some relevant codes (note that fix or fixed is just a typedef for float I was using fixed points initially but switched to floating points for debugging purposes):

Camera:

struct camera
{
    vec Position;
    fix FOV, Tan;
    fix ScaleX, ScaleY;
    fix NearZ, FarZ;
    quat Rotation;

    INLINE vec Forward()
    {
        vec Result;

        Result = (Rotation*vec_Forward).Normalized();

        return(Result);
    }

    INLINE vec Right()
    {
        vec Result;

        vec F  = Forward();
        Result = vec_Up.Cross(F).Normalized();

        return(Result);
    }

    INLINE vec Up()
    {
        vec Result;

        var F  = Forward();
        vec R  = Right();
        Result = F.Cross(R).Normalized();

        return(Result);
    }
};

void CameraInit(camera *Camera, vec WorldP, fixed Aspect, fixed FOV)
{
    Camera->Position = WorldP;
    Camera->FOV = FOV;

    // tan fov/2
    Camera->Tan = Math_Tan(Camera->FOV / 2);

    // distance to the projection plane (which is of size 2*ar x 2).
    Camera->ScaleY = 1 / Camera->Tan;

    // we divide x by ar because we want to normalize things on the x too
    // otherwise we'd still be left in the range [-ar, +ar]
    Camera->ScaleX = Camera->ScaleY / Aspect;

    // near/far clipping planes
    Camera->NearZ = (fixed)0.01f;
    Camera->FarZ = (fixed)100.0f;
}

Input:

fix Horizontal = 0;
fix Vertical = 0;

if (Keys['W'] == Key_Held)
{
    Camera->Position += Camera->Forward() * CamMovSpeed * DeltaTime;
}
if (Keys['S'] == Key_Held)
{
    Camera->Position -= Camera->Forward() * CamMovSpeed * DeltaTime;
}
if (Keys['A'] == Key_Held)
{
    Camera->Position -= Camera->Right() * CamMovSpeed * DeltaTime;
}
if (Keys['D'] == Key_Held)
{
    Camera->Position += Camera->Right() * CamMovSpeed * DeltaTime;
}
if (Keys['E'] == Key_Held)
{
    Camera->Position += vec_Up * CamMovSpeed * DeltaTime;
}
if (Keys['F'] == Key_Held)
{
    Camera->Position -= vec_Up * CamMovSpeed * DeltaTime;
}
if (Keys['J'] == Key_Held)
{
    Horizontal += -CamRotSpeed * DeltaTime;
}
if (Keys['L'] == Key_Held)
{
    Horizontal += CamRotSpeed * DeltaTime;
}
if (Keys['I'] == Key_Held)
{
    Vertical += CamRotSpeed * DeltaTime;
}
if (Keys['K'] == Key_Held)
{
    Vertical += -CamRotSpeed * DeltaTime;
}

// mouse
int dx = Mouse->x - Mouse->LastX; Mouse->LastX = Mouse->x;
int dy = Mouse->LastY - Mouse->y; Mouse->LastY = Mouse->y;
if (Mouse->MMB == Key_Held) // pan
{
    Camera->Position -= (Camera->Up()*dy + Camera->Right()*dx) * SpeedMul/2 * DeltaTime;
}
if (Mouse->RMB == Key_Held) // orbit
{
    Horizontal += dx * SpeedMul*10 * DeltaTime;
    Vertical += dy * SpeedMul*10 * DeltaTime;
}
if (Mouse->Wheel) // zoom
{
    Camera->Position += Camera->Forward() * Mouse->Wheel * SpeedMul*10 * DeltaTime;
}

if (Horizontal != 0 || Vertical != 0)
{
    quat QVertical(vec_Right, -Vertical);
    quat QHorizontal(vec_Up, Horizontal);
    Camera->Rotation = QHorizontal * Camera->Rotation * QVertical;
    Camera->Rotation.Normalize();
}

Transforms:

INLINE vec ModelToWorld(vec ModelP, vec WorldP, quat Rotation, fix Scale)
{
    vec Result;

    Result = ModelP * Scale;
    Result = Rotation * Result;
    Result += WorldP;

    return(Result);
}

INLINE vec WorldToCamera(vec WorldP, camera *Camera)
{
    vec Result;

    vec Diff = WorldP - Camera->Position;
    Result = Camera->Rotation.Inverse() * Diff;

    // similar effect: project to camera basis (same as multiplying with a rotation matrix)
    //var R = Camera->Right();
    //var U = Camera->Up();
    //var F = Camera->Forward();
    //Result.x = Diff.Dot(R);
    //Result.y = Diff.Dot(U);
    //Result.z = Diff.Dot(F);

    return(Result);
}

INLINE vec CameraToPerspective(vec CameraP, camera *Camera)
{
    vec Result;

    Result = CameraP;
    fix ZInv = 1/Result.z;
    Result.x = (Result.x * Camera->ScaleX) * ZInv;
    Result.y = (Result.y * Camera->ScaleY) * ZInv;

    return(Result);
}

Quaternion (taken from MFGD)

struct quat
{
    fix w;
    vec v;

    INLINE quat()
    {
        w = 1;
        v = vec_Zero;
    }

    // Building a quaternion from an axis-angle rotation.
    // http://youtu.be/SCbpxiCN0U0
    INLINE quat(vec Axis, fix Angle)
    {
        fix HalfAngle = Angle/2;
        w = Math_Cos(HalfAngle);
        v = Axis * Math_Sin(HalfAngle);
    }

    // http://youtu.be/A6A0rpV9ElA
    INLINE quat Inverse()
    {
        quat Result;
        Result.w = -w;
        Result.v = v;
        return(Result);
    }

    // Multiplying two quaternions together combines the rotations.
    // http://youtu.be/CRiR2eY5R_s
    quat operator*(quat B)
    {
        quat C;

        C.w = w*B.w - v.Dot(B.v);
        C.v = v*B.w + B.v*w + v.Cross(B.v);

        //C.v.x = v.x*B.w + B.v.x*w + v.y*B.v.z - v.z*B.v.y;
        //C.v.y = v.y*B.w + B.v.y*w - v.x*B.v.z + v.z*B.v.x;
        //C.v.z = v.z*B.w + B.v.z*w + v.x*B.v.y - v.y*B.v.x;
        //C.w = w*B.w - v.x*B.v.x - v.y*B.v.y - v.z*B.v.z;

        return(C);
    }

    INLINE quat operator*=(quat Q)
    {
        return *this = *this * Q;
    }

    // Rotate a vector with this quaternion.
    // http://youtu.be/Ne3RNhEVSIE
    // The basic equation is qpq* (the * means inverse) but we use a simplified version of that equation.
    vec operator*(vec Vector)
    {
        vec Result;

        quat Q;
        Q.w = 0;
        Q.v = Vector;

        // Could do it this way:
        //const Quaternion& q = (*this);
        //return (q * p * q.Inverted()).v;

        // But let's optimize it a bit instead.
        Result = this->v.Cross(Vector);
        Result = Vector + Result*(2*w) + this->v.Cross(Result)*2;

        return(Result);
    }

    INLINE fix Length()
    {
        return sqrt(v.x*v.x + v.y*v.y + v.z*v.z + w*w);
    }

    INLINE quat Normalized()
    {
        quat Result;
        // TODO: handle L=0
        fix L = Length();
        Result.v = this->v/L;
        Result.w = this->w/L;
        return(Result);
    }

    INLINE void Normalize()
    {
        *this = this->Normalized();
    }
};

Any ideas what's causing that stretch? or am I just seeing things.

[EDIT] Rasterizer code (Note I have a flag to switch between z-buffering and z-sorting. Should have two different rasterize functions instead of the if-statement):

void Rasterize(vec v0, vec v1, vec v2, vec uv0, vec uv1, vec uv2, texture *Texture,
               framebuffer *LowBuffer, depthbuffer *DepthBuffer)
{
    assert(Texture);

    // set raster points
    BeginSample("Points&Box&Clip");
    raster_point p0 = { v0.x, v0.y };
    raster_point p1 = { v1.x, v1.y };
    raster_point p2 = { v2.x, v2.y };

    // get triangle bounding box
    i32 MinX = Math_Min3(p0.x, p1.x, p2.x);
    i32 MaxX = Math_Max3(p0.x, p1.x, p2.x);
    i32 MinY = Math_Min3(p0.y, p1.y, p2.y);
    i32 MaxY = Math_Max3(p0.y, p1.y, p2.y);

    // clip against screen
    MinX = Math_Max(MinX, 0);
    MinY = Math_Max(MinY, 0);
    MaxX = Math_Min(MaxX, LowBuffer->Width - 1);
    MaxY = Math_Min(MaxY, LowBuffer->Height - 1);
    EndSample();

    // calculate barycentric increments
    BeginSample("Bary Setup");
    i32 IncrementX_01 = (p0.y - p1.y), IncrementY_01 = (p1.x - p0.x);
    i32 IncrementX_12 = (p1.y - p2.y), IncrementY_12 = (p2.x - p1.x);
    i32 IncrementX_20 = (p2.y - p0.y), IncrementY_20 = (p0.x - p2.x);
    EndSample();

    // calculate initial edge values and areas
    BeginSample("Initial Edge");
    raster_point Point = { MinX, MinY };
    i32 E01_Row = EdgeFunction(p0, p1, Point);
    i32 E12_Row = EdgeFunction(p1, p2, Point);
    i32 E20_Row = EdgeFunction(p2, p0, Point);
    i32 Area = EdgeFunction(p0, p1, p2); //technically area x 2
    fix OneOverArea = 1.0f / Area;
    EndSample();

    // fill pixels
    BeginSample("Blending & Fill");
    for (i32 y = MinY; y <= MaxY; y++)
    {
        // cache row edge function values
        // so we can increment them in the
        // inner loop. we should NOT be
        // incrementing the original row 
        // values in there!
        i32 E01 = E01_Row;
        i32 E12 = E12_Row;
        i32 E20 = E20_Row;

        for (i32 x = MinX; x <= MaxX; x++)
        {
            if ((E01 | E12 | E20) >= 0)
            {
                fixed W0 = E12*OneOverArea;
                fixed W1 = E20*OneOverArea;
                fixed W2 = E01*OneOverArea;

                if ((W0 < 0 || W0 > 1) ||
                    (W0 < 0 || W0 > 1) ||
                    (W0 < 0 || W0 > 1))
                    continue;

                fixed InterpolatedZ = v0.z*W0 + v1.z*W1 + v2.z*W2;

                #if defined(PERSPECTIVE_CORRECT)
                fixed Z = 1.0f / InterpolatedZ;
                #else
                fixed Z = InterpolatedZ;
                #endif

                i32 Offset;

                if (UseZBuffer)
                {
                    Offset = (y*DepthBuffer->Width + x);
                    fixed DepthValue = DepthBuffer->Memory[Offset];
                    if (Z < DepthValue)
                    {
                        DepthBuffer->Memory[Offset] = Z;

                        Offset = (y*LowBuffer->Width + x)*LowBuffer->BytesPerPixel;
                        i32 *Pixel = (i32 *)(LowBuffer->Memory + Offset);

                        vec UV = uv0*W0 + uv1*W1 + uv2*W2;

                        #if defined(PERSPECTIVE_CORRECT)
                        UV *= Z;
                        #endif

                        UV.x = Math_Clamp(UV.x, (fix)0, (fix)1);
                        UV.y = Math_Clamp(UV.y, (fix)0, (fix)1);

                        i32 tx = (i32)((UV.u * (Texture->Width - 1)) + 0.5f);
                        i32 ty = (i32)((UV.v * (Texture->Height - 1)) + 0.5f);
                        byte *Color = (byte *)Texture->Memory + (Texture->Width*ty + tx)*Texture->BytesPerTexel;
                        *Pixel = Color[3]<<24 | Color[0]<<16 | Color[1]<<8 | Color[2];
                    }
                }
                else
                {
                    Offset = (y*LowBuffer->Width + x)*LowBuffer->BytesPerPixel;
                    i32 *Pixel = (i32 *)(LowBuffer->Memory + Offset);

                    vec UV = uv0*W0 + uv1*W1 + uv2*W2;

                    #if defined(PERSPECTIVE_CORRECT)
                    UV *= Z;
                    #endif

                    UV.x = Math_Clamp(UV.x, (fix)0, (fix)1);
                    UV.y = Math_Clamp(UV.y, (fix)0, (fix)1);

                    // Windows: ABGR
                    // Texture: RGBA
                    i32 tx = (i32)((UV.u * (Texture->Width - 1)) + 0.5f);
                    i32 ty = (i32)((UV.v * (Texture->Height - 1)) + 0.5f);
                    byte *Color = (byte *)Texture->Memory + (Texture->Width*ty + tx)*Texture->BytesPerTexel;
                    *Pixel = Color[3]<<24 | Color[0]<<16 | Color[1]<<8 | Color[2];
                }
            }

            // increment one to the right
            E01 += IncrementX_01;
            E12 += IncrementX_12;
            E20 += IncrementX_20;
        }

        // increment one row down
        E01_Row += IncrementY_01;
        E12_Row += IncrementY_12;
        E20_Row += IncrementY_20;
    }

    EndSample();
}

Here's how I set values for perspective correct stuff:

// perspective & screen transforms
    {
        ForI(RenderList->PolyCount)
        {
            var It = &RenderList->PolyStorage[i];
            if ((It->Flags & Polygon_Culled) == 0)
            {
                // camera to perspective
                {
                    It->Vertices[0] = CameraToPerspective(It->Vertices[0], &Camera);
                    It->Vertices[1] = CameraToPerspective(It->Vertices[1], &Camera);
                    It->Vertices[2] = CameraToPerspective(It->Vertices[2], &Camera);
                }

                // perspective to screen
                // X: [-1, 1] -> [0, Width]
                // Y: [1, -1] -> [0, Height]
                {
                    It->Vertices[0] = PerspectiveToScreen(It->Vertices[0], HalfWidth, HalfHeight);
                    It->Vertices[1] = PerspectiveToScreen(It->Vertices[1], HalfWidth, HalfHeight);
                    It->Vertices[2] = PerspectiveToScreen(It->Vertices[2], HalfWidth, HalfHeight);
                }

                #if defined(PERSPECTIVE_CORRECT)
                It->Vertices[0].z = 1.0f / It->Vertices[0].z;
                It->Vertices[1].z = 1.0f / It->Vertices[1].z;
                It->Vertices[2].z = 1.0f / It->Vertices[2].z;
                It->UVs[0] = It->UVs[0] * It->Vertices[0].z;
                It->UVs[1] = It->UVs[1] * It->Vertices[1].z;
                It->UVs[2] = It->UVs[2] * It->Vertices[2].z;
                #endif
            }
        }
    }
\$\endgroup\$
3
  • \$\begingroup\$ From the demo in your video, I would have guessed that you aren't doing perspective-correct texturing (or have a bug in it). I saw that your rasterizer supports a PERSPECTIVE_CORRECT flag, is that turned on? If not, there's your problem! If so (or if turning it on doesn't look right), I think your final UV calculation should be UV *= InterpolatedZ instead of UV *= Z. The texture coordinates you're interpolating should be UV/depth, so you need to multiply by depth (or divide by 1/depth) to get the UV out. \$\endgroup\$
    – Victor T.
    Commented Mar 4, 2017 at 17:08
  • \$\begingroup\$ @VictorT. I edited my question, posting the rasterize function code. And yes the flag is turned on. AFAIK you pass 1/Z, U/Z, V/Z to your rasterizer, interpolate 1/Z, U/Z, and V/Z and then multiply everything by Z to get the right values. \$\endgroup\$
    – vexe
    Commented Mar 4, 2017 at 17:36
  • \$\begingroup\$ I see, you've already inverted z before passing the vertices to the rasterizer. Never mind me, then! \$\endgroup\$
    – Victor T.
    Commented Mar 5, 2017 at 17:00

1 Answer 1

0
\$\begingroup\$

That's normal. It's the effect of perspective projection, the renderer tries to shove a spherical area into a flat area.

Try this: Take a balloon, draw a rectangle (and something in it if you want to have a better visual cue) on it and pop it. Then try cutting out the rectangle and flattening it. You probably won't succeed or if you do, you get a very distorted scene.

If the player'd see everything around himself/herself, he'd have a spherical viewport, but because you limit the view to a specific angle, the player's viewport is only a part of it. Here you get the same problem that cartographers are trying to solve for centuries now.

You can make the effect less apparent by reducing the field of view. In my experiences, a field of view of around 60° vertically makes the best area-distortion ratio, but it depends on the game.

\$\endgroup\$
14
  • \$\begingroup\$ Hmm interesting. So you're saying it's not a bug. By using vertical FOV, does that mean I have to change my viewplane dimensions from being 2ar x 2 to 2 x 2ar? Cause from my understanding I'm currently using a horizontal FOV because I scale x by aspect ratio and not y, is that what makes a horizontal vs vertical FOV? \$\endgroup\$
    – vexe
    Commented Mar 4, 2017 at 22:45
  • \$\begingroup\$ @vexe you can get the horizontal fov by multiplying the vertical fiv with the aspect ratio. If you have the horizontal fov, then you can get the vertixal fov by dividing it with aspect ratio, Whichever you have, try tweaking it around. \$\endgroup\$
    – Bálint
    Commented Mar 4, 2017 at 22:48
  • \$\begingroup\$ I'm just trying to understand which FOV I have right now, is it vertical or horizontal. What I do know is that my viewplane size is 2ar x 2 which I assume that makes it horizontal? \$\endgroup\$
    – vexe
    Commented Mar 4, 2017 at 22:53
  • \$\begingroup\$ @vexe By looking at your code, it's vertical, not like it matters in this case. \$\endgroup\$
    – Bálint
    Commented Mar 4, 2017 at 22:55
  • \$\begingroup\$ How can you tell it's vertical? And btw, changing it to 60 it's much better... \$\endgroup\$
    – vexe
    Commented Mar 4, 2017 at 22:56

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .