private Vector3 CalcColor(Ray ray, int pathTracingLimit, int reflectionLimit) { var rgb = Vector3.Zero; var hitPoint = FindClosestHitPoint(ray); if (hitPoint != null) { var sphere = hitPoint.Sphere; var rayVecNorm = Vector3.Normalize(hitPoint.Ray.DirectionVec); var rayVec = (hitPoint.Ray.Lambda * rayVecNorm) + (-rayVecNorm * 0.001f) /* nudging..? */; var hitPointVec = hitPoint.Ray.StartVec + rayVec; var nVecNorm = Vector3.Normalize(hitPointVec - hitPoint.Sphere.Center); var material = sphere.CalcColor(hitPointVec); if (!PathTracing) { if (sphere.Brightness > 0) { rgb += material * sphere.Brightness; } if (Reflection) { // Reflection if (!sphere.IsWall && /* ignore walls */ reflectionLimit > 0) { var reflectionMaterial = Conversions.FromColor(Colors.White); var refVecNorm = Vector3.Normalize(rayVecNorm - 2 * Vector3.Dot(nVecNorm, rayVecNorm) * nVecNorm); var refVec2 = refVecNorm + (Vector3.One - refVecNorm) * (float)Math.Pow((1 - Vector3.Dot(nVecNorm, refVecNorm)), 5); var reflectionRay = new Ray(hitPointVec, refVec2); var reflectionColor = CalcColor(reflectionRay, 0, --reflectionLimit); var reflection = Vector3.Multiply(reflectionColor, reflectionMaterial) * 0.25f; rgb += reflection; } } for (int i = 0; i < _lightSources.Length; i++) { var lightSource = _lightSources[i]; var lVec = lightSource.Center - hitPointVec; var lVecNorm = Vector3.Normalize(lVec); var light_cos = Vector3.Dot(nVecNorm, lVecNorm); if (light_cos >= 0) { var light = Conversions.FromColor(lightSource.Color); if (Shadows) { if (SoftShadows) { light_cos *= GetShadowedRatio(hitPointVec, lightSource); } else { // Shadows if (HasObjectInFrontOfLightSource(hitPointVec, lightSource.Center)) { light_cos *= 0.01f; } } } if (DiffuseLambert) { // Diffuse "Lambert" var diffuse = Vector3.Multiply(light, material) * light_cos; rgb += diffuse; } if (SpecularPhong) { // Ignore walls if (!sphere.IsWall) { // Specular "Phong" var sVec = (lVec - ((Vector3.Dot(lVec, nVecNorm)) * nVecNorm)); var rVec = lVec - (2 * sVec); var specular = light * (float)Math.Pow((Vector3.Dot(Vector3.Normalize(rVec), rayVecNorm)), SpecularPhongFactor); rgb += specular; } } } } } else { if (pathTracingLimit > 0) { var numberOfRandomRays = 0; bool isBounced = pathTracingLimit < PathTracingMaxBounces; if (!isBounced) { numberOfRandomRays = PathTracingRays; } else { var russianRoulette = (float)_random.NextDouble(); if (russianRoulette > 0.2f) { numberOfRandomRays = 1; } } if (numberOfRandomRays > 0) { // Source: https://www.scratchapixel.com/lessons/3d-basic-rendering/global-illumination-path-tracing/global-illumination-path-tracing-practical-implementation Vector3 indirectDiffuse = Vector3.Zero; Vector3 ntVec = Vector3.Zero; if (Math.Abs(nVecNorm.X) > Math.Abs(nVecNorm.Y)) { ntVec = (new Vector3(nVecNorm.Z, 0, -nVecNorm.X) / (float)Math.Sqrt(nVecNorm.X * nVecNorm.X + nVecNorm.Z * nVecNorm.Z)); } else { ntVec = (new Vector3(0, -nVecNorm.Z, nVecNorm.Y) / (float)Math.Sqrt(nVecNorm.Y * nVecNorm.Y + nVecNorm.Z * nVecNorm.Z)); } var nbVec = Vector3.Cross(nVecNorm, ntVec); for (int i = 0; i < numberOfRandomRays; i++) { var cosTheta = (float)_random.NextDouble(); var phi = (float)(_random.NextDouble() * 2 * Math.PI); var sinTheta = (float)Math.Sqrt(1 - cosTheta * cosTheta); float x = sinTheta * (float)Math.Cos(phi); float z = sinTheta * (float)Math.Sin(phi); var sampleLocalVec = new Vector3(x, cosTheta, z); var sampleWorldVec = new Vector3() { X = sampleLocalVec.X * nbVec.X + sampleLocalVec.Y * nVecNorm.X + sampleLocalVec.Z * ntVec.X, Y = sampleLocalVec.X * nbVec.Y + sampleLocalVec.Y * nVecNorm.Y + sampleLocalVec.Z * ntVec.Y, Z = sampleLocalVec.X * nbVec.Z + sampleLocalVec.Y * nVecNorm.Z + sampleLocalVec.Z * ntVec.Z }; var randomRay = new Ray(hitPointVec, sampleWorldVec); indirectDiffuse += CalcColor(randomRay, pathTracingLimit - 1, 0) * cosTheta; } indirectDiffuse /= numberOfRandomRays * _pdf; rgb += Vector3.Multiply(indirectDiffuse, material) * sphere.Reflectiveness; } else { rgb += material * sphere.Brightness; } } else { rgb += material * sphere.Brightness; } } } return(rgb); }
public ImageSource GetImage() { //** clear buffers Array.Clear(_rgbArray, 0, _rgbArray.Length); for (int x = 0; x < _screenWidth; x++) { for (int y = 0; y < _screenHeight; y++) { _zBufferArray[x, y] = float.PositiveInfinity; } } if (_useDeferredRenderer) { //** first pass (Z-Prepass) for (int i = 0; i < _triangles.Length; i++) { var triangle = _triangles[i]; if (!triangle.IsBackfacing) { for (int x = (int)Math.Min(0, Math.Max(triangle.MinScreenX, 0)); x < (int)Math.Min(Math.Max(triangle.MaxScreenX, 0), _screenWidth); x++) { for (int y = (int)Math.Min(0, Math.Max(triangle.MinScreenY, 0)); y < (int)Math.Min(Math.Max(triangle.MaxScreenY, 0), _screenHeight); y++) { var z = triangle.CalcZ(x, y); if (!float.IsInfinity(z) && !float.IsNaN(z) && z < _zBufferArray[x, y]) { _zBufferArray[x, y] = z; } } } } } //** second pass for (int i = 0; i < _triangles.Length; i++) { var triangle = _triangles[i]; if (!triangle.IsBackfacing) { for (int x = (int)Math.Min(0, Math.Max(triangle.MinScreenX, 0)); x < (int)Math.Min(Math.Max(triangle.MaxScreenX, 0), _screenWidth); x++) { for (int y = (int)Math.Min(0, Math.Max(triangle.MinScreenY, 0)); y < (int)Math.Min(Math.Max(triangle.MaxScreenY, 0), _screenHeight); y++) { float z = _zBufferArray[x, y]; if (_visualizeZBuffer) { var zcolor = (int)((z - ZBUFFER_VISUALIZE_MIN) / (ZBUFFER_VISUALIZE_MAX - ZBUFFER_VISUALIZE_MIN) * 255) * new Vector3(1f / 255, 1f / 255, 1f / 255); _rgbArray[x, y] = zcolor; } else { var rgb = Vector3.Zero; if (triangle.CalcColorDeferred(x, y, _lightSources, z, out rgb)) { _rgbArray[x, y] = rgb; } } } } } } } else { for (int i = 0; i < _triangles.Length; i++) { var triangle = _triangles[i]; if (!triangle.IsBackfacing) { for (int x = (int)Math.Min(0, Math.Max(triangle.MinScreenX, 0)); x < (int)Math.Min(Math.Max(triangle.MaxScreenX, 0), _screenWidth); x++) { for (int y = (int)Math.Min(0, Math.Max(triangle.MinScreenY, 0)); y < (int)Math.Min(Math.Max(triangle.MaxScreenY, 0), _screenHeight); y++) { float z; Vector3 rgb; if (triangle.CalcColor(x, y, _lightSources, out z, out rgb) && !float.IsInfinity(z) && !float.IsNaN(z)) { if (z < _zBufferArray[x, y]) { _zBufferArray[x, y] = z; if (_visualizeZBuffer) { var zcolor = (int)((z - ZBUFFER_VISUALIZE_MIN) / (ZBUFFER_VISUALIZE_MAX - ZBUFFER_VISUALIZE_MIN) * 255) * new Vector3(1f / 255, 1f / 255, 1f / 255); _rgbArray[x, y] = zcolor; } else { _rgbArray[x, y] = rgb; } } } } } } } } //** buffer to image for (int x = 0; x < _screenWidth; x++) { for (int y = 0; y < _screenHeight; y++) { var rgb = _rgbArray[x, y]; var c = Conversions.FromRGB(rgb, _gammaCorrect); _bitmap.Set(x, y, c); } } return(_bitmap.GetImageSource()); }
public string GetImageFileName(int width, int height, double dpiX, double dpiY, CancellationToken cancellationToken, string settingsSummary, string exportDirectory) { var sw = Stopwatch.StartNew(); OutputLogEveryXPixel = (width * height / 100); if (AccelerationStructure) { _accelerationStructure = BVHNode.BuildTopDown(_spheres, _logger); } var bitmap = new BitmapImage(width, height, dpiX, dpiY); var divideX = width / (float)2; var alignX = 1 - divideX; var divideY = height / (float)2; var alignY = 1 - divideY; int workDone = 0; int totalWork = width * height; if (Parallelize) { var options = new ParallelOptions() { CancellationToken = cancellationToken, MaxDegreeOfParallelism = Environment.ProcessorCount }; Parallel.For(0, width, options, i => { for (int j = 0; j < height; j++) { if (cancellationToken.IsCancellationRequested) { break; } if (AntiAliasing) { Vector3 result = Vector3.Zero; for (int k = 0; k < AntiAliasingSampleSize; k++) { var dx = (float)_random.NextGaussian(0d, 0.5d); var dy = (float)_random.NextGaussian(0d, 0.5d); var x = (i + alignX + dx) / divideX; var y = ((height - j) + alignY + dy) / divideY; var rgb = GetColor(x, y); result += rgb; } var avg_rgb = result * (1f / AntiAliasingSampleSize); var c = Conversions.FromRGB(avg_rgb, GammaCorrect); bitmap.Set(i, j, c); } else { var x = (i + alignX) / divideX; var y = ((height - j) + alignY) / divideY; var rgb = GetColor(x, y); var c = Conversions.FromRGB(rgb, GammaCorrect); bitmap.Set(i, j, c); } var value = Interlocked.Increment(ref workDone); if (value % OutputLogEveryXPixel == 0) { var progress = (float)value / totalWork; WriteOutput($"{(progress * 100):F3}% progress. Running {sw.Elapsed}. Remaining {TimeSpan.FromMilliseconds(sw.Elapsed.TotalMilliseconds / progress * (1f - progress))}."); } } }); } else { for (int i = 0; i < width; i++) { if (cancellationToken.IsCancellationRequested) { break; } for (int j = 0; j < height; j++) { if (cancellationToken.IsCancellationRequested) { break; } if (AntiAliasing) { Vector3 result = Vector3.Zero; for (int k = 0; k < AntiAliasingSampleSize; k++) { var x = (i + alignX + (float)_random.NextGaussian(0d, 0.5d)) / divideX; var y = ((height - j) + alignY + (float)_random.NextGaussian(0d, 0.5d)) / divideY; var rgb = GetColor(x, y); result += rgb; } var avg_rgb = result * (1f / AntiAliasingSampleSize); var c = Conversions.FromRGB(avg_rgb, GammaCorrect); bitmap.Set(i, j, c); } else { // Question: Why is it mirrored? var x = (i + alignX) / divideX; var y = ((height - j) + alignY) / divideY; var rgb = GetColor(x, y); var c = Conversions.FromRGB(rgb, GammaCorrect); bitmap.Set(i, j, c); } ++workDone; if (workDone % OutputLogEveryXPixel == 0) { var progress = (float)workDone / totalWork; WriteOutput($"{(progress * 100):F3}% progress. Running {sw.Elapsed}. Remaining {TimeSpan.FromMilliseconds(sw.Elapsed.TotalMilliseconds / progress * (1f - progress))}."); } } } } if (cancellationToken.IsCancellationRequested) { WriteOutput("Operation canceled by user."); return(null); } var imageSource = bitmap.GetImageSource(); sw.Stop(); return(SaveImage(imageSource, sw.Elapsed, settingsSummary, exportDirectory)); }