diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs index d984705c6..e1e52288b 100644 --- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs @@ -204,6 +204,12 @@ namespace ICSharpCode.Decompiler.Tests RunCS(options: options); } + [Test] + public void LINQRaytracer([ValueSource("defaultOptions")] CompilerOptions options) + { + RunCS(options: options); + } + void RunCS([CallerMemberName] string testName = null, CompilerOptions options = CompilerOptions.UseDebug) { string testFileName = testName + ".cs"; diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index f076a66e1..9227073bd 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -51,6 +51,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/LINQRaytracer.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/LINQRaytracer.cs new file mode 100644 index 000000000..3a2808a6c --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/LINQRaytracer.cs @@ -0,0 +1,390 @@ +// This test case is taken from https://blogs.msdn.microsoft.com/lukeh/2007/10/01/taking-linq-to-objects-to-extremes-a-fully-linqified-raytracer/ + +using System.Linq; +using System; +using System.Collections.Generic; + +namespace RayTracer +{ + public class RayTracer + { + static void Main() + { + const int width = 600; + const int height = 600; + + RayTracer rayTracer = new RayTracer(width, height, (int x, int y, Color color) => { + Console.Write("{0},{1}:{2};", x, y, color); + }); + rayTracer.Render(rayTracer.DefaultScene); + } + + private int screenWidth; + private int screenHeight; + private const int MaxDepth = 5; + + public Action setPixel; + + public RayTracer(int screenWidth, int screenHeight, Action setPixel) + { + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + this.setPixel = setPixel; + } + + private class Wrap + { + public readonly Func, T> It; + public Wrap(Func, T> it) { It = it; } + } + + public static Func Y(Func, Func> f) + { + Func>, Func> g = wx => f(wx.It(wx)); + return g(new Wrap>(wx => f(y => wx.It(wx)(y)))); + } + + class TraceRayArgs + { + public readonly Ray Ray; + public readonly Scene Scene; + public readonly int Depth; + + public TraceRayArgs(Ray ray, Scene scene, int depth) { Ray = ray; Scene = scene; Depth = depth; } + } + + internal void Render(Scene scene) + { + var pixelsQuery = + from y in Enumerable.Range(0, screenHeight) + let recenterY = -(y - (screenHeight / 2.0)) / (2.0 * screenHeight) + select from x in Enumerable.Range(0, screenWidth) + let recenterX = (x - (screenWidth / 2.0)) / (2.0 * screenWidth) + let point = + Vector.Norm(Vector.Plus(scene.Camera.Forward, + Vector.Plus(Vector.Times(recenterX, scene.Camera.Right), + Vector.Times(recenterY, scene.Camera.Up)))) + let ray = new Ray() { Start = scene.Camera.Pos, Dir = point } + let computeTraceRay = (Func, Func>) + (f => traceRayArgs => + (from isect in + from thing in traceRayArgs.Scene.Things + select thing.Intersect(traceRayArgs.Ray) + where isect != null + orderby isect.Dist + let d = isect.Ray.Dir + let pos = Vector.Plus(Vector.Times(isect.Dist, isect.Ray.Dir), isect.Ray.Start) + let normal = isect.Thing.Normal(pos) + let reflectDir = Vector.Minus(d, Vector.Times(2 * Vector.Dot(normal, d), normal)) + let naturalColors = + from light in traceRayArgs.Scene.Lights + let ldis = Vector.Minus(light.Pos, pos) + let livec = Vector.Norm(ldis) + let testRay = new Ray() { Start = pos, Dir = livec } + let testIsects = from inter in + from thing in traceRayArgs.Scene.Things + select thing.Intersect(testRay) + where inter != null + orderby inter.Dist + select inter + let testIsect = testIsects.FirstOrDefault() + let neatIsect = testIsect == null ? 0 : testIsect.Dist + let isInShadow = !((neatIsect > Vector.Mag(ldis)) || (neatIsect == 0)) + where !isInShadow + let illum = Vector.Dot(livec, normal) + let lcolor = illum > 0 ? Color.Times(illum, light.Color) : Color.Make(0, 0, 0) + let specular = Vector.Dot(livec, Vector.Norm(reflectDir)) + let scolor = specular > 0 + ? Color.Times(Math.Pow(specular, isect.Thing.Surface.Roughness), + light.Color) + : Color.Make(0, 0, 0) + select Color.Plus(Color.Times(isect.Thing.Surface.Diffuse(pos), lcolor), + Color.Times(isect.Thing.Surface.Specular(pos), scolor)) + let reflectPos = Vector.Plus(pos, Vector.Times(.001, reflectDir)) + let reflectColor = traceRayArgs.Depth >= MaxDepth + ? Color.Make(.5, .5, .5) + : Color.Times(isect.Thing.Surface.Reflect(reflectPos), + f(new TraceRayArgs(new Ray() { + Start = reflectPos, + Dir = reflectDir + }, + traceRayArgs.Scene, + traceRayArgs.Depth + 1))) + select naturalColors.Aggregate(reflectColor, + (color, natColor) => Color.Plus(color, natColor)) + ).DefaultIfEmpty(Color.Background).First()) + let traceRay = Y(computeTraceRay) + select new { X = x, Y = y, Color = traceRay(new TraceRayArgs(ray, scene, 0)) }; + + foreach (var row in pixelsQuery) + foreach (var pixel in row) + setPixel(pixel.X, pixel.Y, pixel.Color); + } + + internal readonly Scene DefaultScene = + new Scene() { + Things = new SceneObject[] { + new Plane() { + Norm = Vector.Make(0,1,0), + Offset = 0, + Surface = Surfaces.CheckerBoard + }, + new Sphere() { + Center = Vector.Make(0,1,0), + Radius = 1, + Surface = Surfaces.Shiny + }, + new Sphere() { + Center = Vector.Make(-1,.5,1.5), + Radius = .5, + Surface = Surfaces.Shiny + }}, + Lights = new Light[] { + new Light() { + Pos = Vector.Make(-2,2.5,0), + Color = Color.Make(.49,.07,.07) + }, + new Light() { + Pos = Vector.Make(1.5,2.5,1.5), + Color = Color.Make(.07,.07,.49) + }, + new Light() { + Pos = Vector.Make(1.5,2.5,-1.5), + Color = Color.Make(.07,.49,.071) + }, + new Light() { + Pos = Vector.Make(0,3.5,0), + Color = Color.Make(.21,.21,.35) + }}, + Camera = Camera.Create(Vector.Make(3, 2, 4), Vector.Make(-1, .5, 0)) + }; + } + + static class Surfaces + { + // Only works with X-Z plane. + public static readonly Surface CheckerBoard = + new Surface() { + Diffuse = pos => ((Math.Floor(pos.Z) + Math.Floor(pos.X)) % 2 != 0) + ? Color.Make(1, 1, 1) + : Color.Make(0, 0, 0), + Specular = pos => Color.Make(1, 1, 1), + Reflect = pos => ((Math.Floor(pos.Z) + Math.Floor(pos.X)) % 2 != 0) + ? .1 + : .7, + Roughness = 150 + }; + + + public static readonly Surface Shiny = + new Surface() { + Diffuse = pos => Color.Make(1, 1, 1), + Specular = pos => Color.Make(.5, .5, .5), + Reflect = pos => .6, + Roughness = 50 + }; + } + + class Vector + { + public readonly double X; + public readonly double Y; + public readonly double Z; + + public Vector(double x, double y, double z) { X = x; Y = y; Z = z; } + + public static Vector Make(double x, double y, double z) { return new Vector(x, y, z); } + public static Vector Times(double n, Vector v) + { + return new Vector(v.X * n, v.Y * n, v.Z * n); + } + public static Vector Minus(Vector v1, Vector v2) + { + return new Vector(v1.X - v2.X, v1.Y - v2.Y, v1.Z - v2.Z); + } + public static Vector Plus(Vector v1, Vector v2) + { + return new Vector(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z); + } + public static double Dot(Vector v1, Vector v2) + { + return (v1.X * v2.X) + (v1.Y * v2.Y) + (v1.Z * v2.Z); + } + public static double Mag(Vector v) { return Math.Sqrt(Dot(v, v)); } + public static Vector Norm(Vector v) + { + double mag = Mag(v); + double div = mag == 0 ? double.PositiveInfinity : 1 / mag; + return Times(div, v); + } + public static Vector Cross(Vector v1, Vector v2) + { + return new Vector(((v1.Y * v2.Z) - (v1.Z * v2.Y)), + ((v1.Z * v2.X) - (v1.X * v2.Z)), + ((v1.X * v2.Y) - (v1.Y * v2.X))); + } + public static bool Equals(Vector v1, Vector v2) + { + return (v1.X == v2.X) && (v1.Y == v2.Y) && (v1.Z == v2.Z); + } + } + + public class Color + { + public double R; + public double G; + public double B; + + public Color(double r, double g, double b) { R = r; G = g; B = b; } + + public static Color Make(double r, double g, double b) { return new Color(r, g, b); } + + public static Color Times(double n, Color v) + { + return new Color(n * v.R, n * v.G, n * v.B); + } + public static Color Times(Color v1, Color v2) + { + return new Color(v1.R * v2.R, v1.G * v2.G, v1.B * v2.B); + } + + public static Color Plus(Color v1, Color v2) + { + return new Color(v1.R + v2.R, v1.G + v2.G, v1.B + v2.B); + } + public static Color Minus(Color v1, Color v2) + { + return new Color(v1.R - v2.R, v1.G - v2.G, v1.B - v2.B); + } + + public static readonly Color Background = Make(0, 0, 0); + public static readonly Color DefaultColor = Make(0, 0, 0); + + private double Legalize(double d) + { + return d > 1 ? 1 : d; + } + + public override string ToString() + { + return string.Format("[{0},{1},{2}]", R, G, B); + } + } + + class Ray + { + public Vector Start; + public Vector Dir; + } + + class ISect + { + public SceneObject Thing; + public Ray Ray; + public double Dist; + } + + class Surface + { + public Func Diffuse; + public Func Specular; + public Func Reflect; + public double Roughness; + } + + class Camera + { + public Vector Pos; + public Vector Forward; + public Vector Up; + public Vector Right; + + public static Camera Create(Vector pos, Vector lookAt) + { + Vector forward = Vector.Norm(Vector.Minus(lookAt, pos)); + Vector down = new Vector(0, -1, 0); + Vector right = Vector.Times(1.5, Vector.Norm(Vector.Cross(forward, down))); + Vector up = Vector.Times(1.5, Vector.Norm(Vector.Cross(forward, right))); + + return new Camera() { Pos = pos, Forward = forward, Up = up, Right = right }; + } + } + + class Light + { + public Vector Pos; + public Color Color; + } + + abstract class SceneObject + { + public Surface Surface; + public abstract ISect Intersect(Ray ray); + public abstract Vector Normal(Vector pos); + } + + class Sphere : SceneObject + { + public Vector Center; + public double Radius; + + public override ISect Intersect(Ray ray) + { + Vector eo = Vector.Minus(Center, ray.Start); + double v = Vector.Dot(eo, ray.Dir); + double dist; + if (v < 0) { + dist = 0; + } else { + double disc = Math.Pow(Radius, 2) - (Vector.Dot(eo, eo) - Math.Pow(v, 2)); + dist = disc < 0 ? 0 : v - Math.Sqrt(disc); + } + if (dist == 0) return null; + return new ISect() { + Thing = this, + Ray = ray, + Dist = dist + }; + } + + public override Vector Normal(Vector pos) + { + return Vector.Norm(Vector.Minus(pos, Center)); + } + } + + class Plane : SceneObject + { + public Vector Norm; + public double Offset; + + public override ISect Intersect(Ray ray) + { + double denom = Vector.Dot(Norm, ray.Dir); + if (denom > 0) return null; + return new ISect() { + Thing = this, + Ray = ray, + Dist = (Vector.Dot(Norm, ray.Start) + Offset) / (-denom) + }; + } + + public override Vector Normal(Vector pos) + { + return Norm; + } + } + + class Scene + { + public SceneObject[] Things; + public Light[] Lights; + public Camera Camera; + + public IEnumerable Intersect(Ray r) + { + return from thing in Things + select thing.Intersect(r); + } + } +} \ No newline at end of file