// given a ray, trace it into the scene and return the colour of the surface it hits // (handles reflections recursively) static Color Trace(Ray ray, int traceDepth) { // See if the ray intersected an object CheckIntersection(ref ray); if (ray.closestHitDistance >= Ray.WORLD_MAX || ray.closestHitObject == null) // No intersection { return(BG_COLOR); } // Got a hit - set initial colour to ambient light float r = 0.15f * ray.closestHitObject.color.R; float g = 0.15f * ray.closestHitObject.color.G; float b = 0.15f * ray.closestHitObject.color.B; // Set up stuff we'll need for shading calcs Vector3f surfaceNormal = ray.closestHitObject.GetSurfaceNormalAtPoint(ray.hitPoint); Vector3f viewerDir = -ray.direction; // Direction back to the viewer (simply negative of ray dir) // Loop through the lights, adding contribution of each foreach (Light light in lights) { Vector3f lightDir = new Vector3f(); float lightDistance; // Find light direction and distance lightDir = light.position - ray.hitPoint; // Get direction to light lightDistance = lightDir.Magnitude(); //lightDir = lightDir / lightDistance; // Light exponential falloff lightDir.Normalise(); // Shadow check: check if this light's visible from the point // NB: Step out slightly from the hitpoint first Ray shadowRay = new Ray(ray.hitPoint + (lightDir * TINY), lightDir); shadowRay.closestHitDistance = lightDistance; // IMPORTANT: We only want it to trace as far as the light! CheckIntersection(ref shadowRay); if (shadowRay.closestHitObject != null) // We hit something -- ignore this light entirely { continue; } float cosLightAngleWithNormal = surfaceNormal.Dot(lightDir); if (MATERIAL_DIFFUSE_COEFFICIENT > TINY) { // Calculate light's diffuse component - note that this is view independant // Dot product of surface normal and light direction gives cos of angle between them so will be in // range -1 to 1. We use that as a scaling factor; common technique, called "cosine shading". if (cosLightAngleWithNormal <= 0) { continue; } // Add this light's diffuse contribution to our running totals r += MATERIAL_DIFFUSE_COEFFICIENT * cosLightAngleWithNormal * ray.closestHitObject.color.R; g += MATERIAL_DIFFUSE_COEFFICIENT * cosLightAngleWithNormal * ray.closestHitObject.color.G; b += MATERIAL_DIFFUSE_COEFFICIENT * cosLightAngleWithNormal * ray.closestHitObject.color.B; } if (MATERIAL_SPECULAR_COEFFICIENT > TINY) { // Specular component - dot product of light's reflection vector and viewer direction // Direction to the viewer is simply negative of the ray direction Vector3f lightReflectionDir = surfaceNormal * (cosLightAngleWithNormal * 2) - lightDir; float specularFactor = viewerDir.Dot(lightReflectionDir); if (specularFactor > 0) { // To get smaller, sharper highlights we raise it to a power and multiply it specularFactor = MATERIAL_SPECULAR_COEFFICIENT * (float)Math.Pow(specularFactor, MATERIAL_SPECULAR_POWER); // Add the specular contribution to our running totals r += specularFactor * ray.closestHitObject.color.R; g += specularFactor * ray.closestHitObject.color.G; b += specularFactor * ray.closestHitObject.color.B; } } } // Now do reflection, unless we're too deep if (traceDepth < MAX_DEPTH && MATERIAL_REFLECTION_COEFFICIENT > TINY) { // Set up the reflected ray - notice we move the origin out a tiny bit again Vector3f reflectedDir = ray.direction.ReflectIn(surfaceNormal); Ray reflectionRay = new Ray(ray.hitPoint + reflectedDir * TINY, reflectedDir); // And trace! Color reflectionCol = Trace(reflectionRay, traceDepth + 1); // Add reflection results to running totals, scaling by reflect coeff. r += MATERIAL_REFLECTION_COEFFICIENT * reflectionCol.R; g += MATERIAL_REFLECTION_COEFFICIENT * reflectionCol.G; b += MATERIAL_REFLECTION_COEFFICIENT * reflectionCol.B; } // Clamp RGBs if (r > 255) { r = 255; } if (g > 255) { g = 255; } if (b > 255) { b = 255; } return(Color.FromArgb(255, (int)r, (int)g, (int)b)); }
// given a ray, trace it into the scene and return the colour of the surface it hits // (handles bounces recursively) static Color Trace(Ray ray, int traceDepth) { // See if the ray intersected an object (only if it hasn't already got one - we don't need to // recalculate the first intersection for each sample on the same pixel!) if (ray.closestHitObject == null) { CheckIntersection(ref ray); } if (ray.closestHitDistance >= Ray.WORLD_MAX || ray.closestHitObject == null) // No intersection { return(Color.Black); } // Got a hit - was it an emitter? If so just return the emitter's colour if (ray.closestHitObject.isEmitter) { return(ray.closestHitObject.color); } if (traceDepth >= MAX_DEPTH) { return(Color.Black); } // Get surface normal at intersection Vector3f surfaceNormal = ray.closestHitObject.GetSurfaceNormalAtPoint(ray.hitPoint); // Pick a point on a hemisphere placed on the intersection point (of which // the surface normal is the north pole) if (surfaceNormal.Dot(ray.direction) >= 0) { surfaceNormal = surfaceNormal * -1.0f; } float r1 = (float)(random.NextDouble() * Math.PI * 2.0f); float r2 = (float)random.NextDouble(); float r2s = (float)Math.Sqrt(r2); Vector3f u = new Vector3f(1.0f, 0, 0); if (Math.Abs(surfaceNormal.x) > 0.1f) { u.x = 0; u.y = 1.0f; } u = Vector3f.CrossProduct(u, surfaceNormal); u.Normalise(); Vector3f v = Vector3f.CrossProduct(u, surfaceNormal); // Now set up a direction from the hitpoint to that chosen point Vector3f reflectionDirection = (u * (float)Math.Cos(r1) * r2s + v * (float)Math.Sin(r1) * r2s + surfaceNormal * (float)Math.Sqrt(1 - r2)); reflectionDirection.Normalise(); // And follow that path (note that we're not spawning a new ray -- just following the one we were // originally passed for MAX_DEPTH jumps) Ray reflectionRay = new Ray(ray.hitPoint, reflectionDirection); Color reflectionCol = Trace(reflectionRay, traceDepth + 1); // Now factor the colour we got from the reflection // into this object's own colour; ie, illuminate // the current object with the results of that reflection float r = ray.closestHitObject.color.R * reflectionCol.R; float g = ray.closestHitObject.color.G * reflectionCol.G; float b = ray.closestHitObject.color.B * reflectionCol.B; r /= 255.0f; g /= 255.0f; b /= 255.0f; return(Color.FromArgb(255, (int)r, (int)g, (int)b)); }