/// <summary> /// Shoot a ray into the scene and determine the colour at the impact point. /// </summary> /// <param name="ray">The ray to shoot into the scene.</param> /// <returns>The colour of the impacted point in the scene.</returns> private Colour ShootRay(Ray ray) { Colour finalOutput = new Colour(0.0f, 0.0f, 0.0f); float coefficient = 1.0f; uint count = 0u; do { Colour output = new Colour(0.0f, 0.0f, 0.0f); IObject closestObject = null; float closestDistance = float.MaxValue; foreach (var o in Objects) { float t = o.IntersectDistance(ray); if (t > 0.001f && t < closestDistance) { closestObject = o; closestDistance = t; } } if (closestObject == null) { break; } // Calculate the point of intersection. Point intersection = ray.Start + closestDistance * ray.Direction; // Get normal of object surface at impact of ray. Vector normal = closestObject.GetNormalAtPoint(intersection); foreach (var l in Lights) { Vector toLightSource = (l.Location - intersection).Normalise(); float cosineLightAngle = normal * toLightSource; if (cosineLightAngle <= 0.0f) { // The light is below the surface. continue; } bool inShadow = false; foreach (var o in Objects) { if (o == closestObject) { continue; } if (o.IntersectDistance(new Ray(intersection, toLightSource)) > 0.0f) { inShadow = true; break; } } if (!inShadow) { var lambertianReflectance = (Colour)closestObject.Material.Colour * (Colour)l.Colour * cosineLightAngle; output += lambertianReflectance; var halfwayVector = (toLightSource - ray.Direction).Normalise(); var halfwayNormalAngle = halfwayVector * normal; var blinnTerm = (Colour)l.Colour * closestObject.Material.SpecularTerm * (float)Math.Pow(Math.Max(halfwayNormalAngle, 0.0f), closestObject.Material.SpecularPower); output += blinnTerm; } } finalOutput += output * coefficient; var newRayDirection = (ray.Direction - 2 * normal * (normal * ray.Direction)).Normalise(); ray = new Ray(intersection, newRayDirection); coefficient *= closestObject.Material.Reflectance; } while (++count < MAX_ITERATIONS && coefficient > 0.0f); return finalOutput; }
/// <summary> /// Renders the image. /// </summary> public void Draw() { float exposure = CalculateExposure(); float widthRatio = (float)Camera.Width / output.Width; float heightRatio = (float)Camera.Height / output.Height; ParallelOptions options = new ParallelOptions() { #if DEBUG //MaxDegreeOfParallelism = 1 #endif }; Parallel.For(0, output.Height, options, (y) => { for (var x = 0u; x < output.Width; ++x) { float sampleWidth = 1.0f / SubSampling; float weight = sampleWidth / SubSampling; Colour outputColour = new Colour(0.0f, 0.0f, 0.0f); uint countX = 0u; for (float sampleX = x; countX < SubSampling; sampleX += sampleWidth, ++countX) { uint countY = 0u; for (float sampleY = y; countY < SubSampling; sampleY += sampleWidth, ++countY) { Ray r = Camera.GetRay(sampleX * widthRatio, sampleY * heightRatio); Colour c = ShootRay(r); Colour exposedColour = new Colour(1.0f - (float)Math.Exp(c.Red * exposure), 1.0f - (float)Math.Exp(c.Green * exposure), 1.0f - (float)Math.Exp(c.Blue * exposure)); outputColour += exposedColour * weight; } } Colour sRgbColour = outputColour.ToSrgb(); output.Write((uint)x, (uint)y, sRgbColour); } }); }