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
}
}
}