I'm positive. I mean, assuming your hitPoint function does what I think it does.
hitInfo.surfaceNormal = normalize(hitInfo.hitPoint - c);
Are you sure? That's what my book says (Fundamentals of Computer Graphics, 3rd edition)
Actually, it's the same. I normalized them both and the result is the same.
OK, now I am normalizing the vectors (I had forgotten to do so), and things are better. Much better.
But there's a small problem:

I don't understand why this happens.
This is the new shade function:
Vector3 shade(HitInfo hitInfo) const
{
Vector3 l = {0, 1, 0}; // let's say that the light is at this position
Vector3 lightVector = l - hitInfo.hitPoint; // vector from the hit point to the light
Vector3 I = {1.0f, 1.0f, 1.0f}; // color of the light
lightVector.normalize();
hitInfo.surfaceNormal.normalize();
// TODO: fix
if( hitInfo.surfaceNormal.dot(lightVector) <= 0 )
{
return I * color * 0;
}
else
{
return I * color * hitInfo.surfaceNormal.dot(lightVector);
}
}
By the way, this is the loop in which the rays are casted:
for(int x = 0; x < SCREEN_WIDTH; ++x)
{
for(int y = 0; y < SCREEN_HEIGHT; ++y)
{
//x = SCREEN_WIDTH / 2;
//y = SCREEN_HEIGHT / 2;
Vector3 p = {(x - SCREEN_WIDTH * 0.5f) / (SCREEN_WIDTH * 0.5f), (y - SCREEN_HEIGHT * 0.5f) / (SCREEN_HEIGHT * 0.5f), 0};
Ray r = {cameraPos, p - cameraPos};
HitInfo hitInfo;
Surface closestObject; // default initialized to null
float t = float.max;
foreach(obj; scene.objects)
{
if( obj.hit(r, 0.1f, 1000, hitInfo) && hitInfo.t < t ) // hit?
{
t = hitInfo.t;
closestObject = obj;
}
}
if( closestObject !is null )
{
Vector3 color = closestObject.shade(hitInfo);
writePixel(screen, x, SCREEN_HEIGHT - 1 - y, cast(ubyte)(color.x * 255), cast(ubyte)(color.y * 255), cast(ubyte)(color.z * 255));
}
//x = y = 1000;
}
}
Oh, I forgot. Why do the spheres look like ovals?