Reputation: 107
I have a problem: I've read an article on scratchapixel with C++ code for raytracing. C++ is ok. I tried to convert it into Python, it worked (17x slower result and 4x reduced resolution). I tried to convert it to C#, but my code is not working. Only thing I can see is a blank white 800x600 image. Please see the previously linked article for the C++ code.
This is my interpretation of it as C# code:
using System;
using System.Collections.Generic;
namespace raytracer
{
class Program
{
const int MAX_RAY_DEPTH = 8;
const float FAR = 100000000;
public static void Main(string[] args)
{
Sphere[] spheres = new Sphere[7];
spheres[0] = new Sphere(new Vec3f( 0.0f, -10004, -20), 10000, new Vec3f(0.20f, 0.20f, 0.20f), 0, 0.0f);
spheres[1] = new Sphere(new Vec3f( 0.0f, 0, -20), 4, new Vec3f(1.00f, 0.32f, 0.36f), 1, 0.5f);
spheres[2] = new Sphere(new Vec3f( 5.0f, -1, -15), 2, new Vec3f(0.90f, 0.76f, 0.46f), 1, 0.0f);
spheres[3] = new Sphere(new Vec3f( 5.0f, 0, -25), 3, new Vec3f(0.65f, 0.77f, 0.97f), 1, 0.0f);
spheres[4] = new Sphere(new Vec3f(-5.5f, 0, -15), 3, new Vec3f(0.90f, 0.90f, 0.90f), 1, 0.0f);
spheres[5] = new Sphere(new Vec3f( 2f, 2, -30), 4, new Vec3f(0.53f, 0.38f, 0.91f), 1, 0.7f);
spheres[6] = new Sphere(new Vec3f( 0, 20, -25), 3, new Vec3f(0.00f, 0.00f, 0.00f), 0, 0.0f, new Vec3f(3));
Render(spheres);
}
public class Collision
{
public float t0, t1;
public bool collide;
public Collision(bool col, float tt0 = 0, float tt1 = 0)
{
t0 = tt0;
t1 = tt1;
collide = col;
}
}
public class Vec3f
{
public float x, y, z;
public Vec3f(){ x = y = z = 0; }
public Vec3f(float v){ x = y = z = v; }
public Vec3f(float xx, float yy, float zz){ x = xx; y = yy; z = zz; }
public Vec3f normalize()
{
float nor2 = length2();
if (nor2 > 0)
{
float invNor = 1 / (float)Math.Sqrt(nor2);
x *= invNor; y *= invNor; z *= invNor;
}
return this;
}
public static Vec3f operator *(Vec3f l, Vec3f r)
{
return new Vec3f(l.x * r.x, l.y * r.y, l.z * r.z);
}
public static Vec3f operator *(Vec3f l, float r)
{
return new Vec3f(l.x * r, l.y * r, l.z * r);
}
public float dot(Vec3f v)
{
return x * v.x + y * v.y + z * v.z;
}
public static Vec3f operator -(Vec3f l, Vec3f r)
{
return new Vec3f(l.x - r.x, l.y - r.y, l.z - r.z);
}
public static Vec3f operator +(Vec3f l, Vec3f r)
{
return new Vec3f(l.x + r.x, l.y + r.y, l.z + r.z);
}
public static Vec3f operator -(Vec3f v)
{
return new Vec3f(-v.x, -v.y, -v.z);
}
public float length2()
{
return x * x + y * y + z * z;
}
public float length()
{
return (float)Math.Sqrt(length2());
}
}
public class Sphere
{
public Vec3f center, surfaceColor, emissionColor;
public float radius, radius2;
public float transparency, reflection;
public Sphere(Vec3f c, float r, Vec3f sc, float refl = 0, float transp = 0, Vec3f ec = null)
{
center = c; radius = r; radius2 = r * r;
surfaceColor = sc; emissionColor = (ec == null) ? new Vec3f(0) : ec;
transparency = transp; reflection = refl;
}
public Collision intersect(Vec3f rayorig, Vec3f raydir)
{
Vec3f l = center - rayorig;
float tca = l.dot(raydir);
if (tca < 0){ return new Collision(false); }
float d2 = l.dot(l) - tca * tca;
if (d2 > radius2){ return new Collision(false); }
Collision coll = new Collision(true);
float thc = (float)Math.Sqrt(radius2 - d2);
coll.t0 = tca - thc;
coll.t1 = tca + thc;
return coll;
}
}
public static float mix(float a, float b, float mix)
{
return b * mix + a * (1 - mix);
}
public static Vec3f trace(Vec3f rayorig, Vec3f raydir, Sphere[] spheres, int depth)
{
float tnear = FAR;
Sphere sphere = null;
foreach(Sphere i in spheres)
{
float t0 = FAR, t1 = FAR;
Collision coll = i.intersect(rayorig, raydir);
if (coll.collide)
{
if (coll.t0 < 0) { coll.t0 = coll.t1; }
if (coll.t0 < tnear) { tnear = coll.t0; sphere = i; }
}
}
if (sphere == null){ return new Vec3f(2); }
Vec3f surfaceColor = new Vec3f(0);
Vec3f phit = rayorig + raydir * tnear;
Vec3f nhit = phit - sphere.center;
nhit.normalize();
float bias = 1e-4f;
bool inside = false;
if (raydir.dot(nhit) > 0){ nhit = -nhit; inside = true; }
if ((sphere.transparency > 0 || sphere.reflection > 0) && depth < MAX_RAY_DEPTH)
{
float facingratio = -raydir.dot(nhit);
float fresneleffect = mix((float)Math.Pow(1 - facingratio, 3), 1, 0.1f);
Vec3f refldir = raydir - nhit * 2 * raydir.dot(nhit);
refldir.normalize();
Vec3f reflection = trace(phit + nhit * bias, refldir, spheres, depth + 1);
Vec3f refraction = new Vec3f(0);
if (sphere.transparency > 0)
{
float ior = 1.1f; float eta = 0;
if (inside){ eta = ior; } else { eta = 1 / ior; }
float cosi = -nhit.dot(raydir);
float k = 1 - eta * eta * (1 - cosi * cosi);
Vec3f refrdir = raydir * eta + nhit * (eta * cosi - (float)Math.Sqrt(k));
refrdir.normalize();
refraction = trace(phit - nhit * bias, refrdir, spheres, depth + 1);
}
surfaceColor =
(
reflection * fresneleffect + refraction *
(1 - fresneleffect) * sphere.transparency) * sphere.surfaceColor;
}
else
{
foreach(Sphere i in spheres)
{
if (i.emissionColor.x > 0)
{
Vec3f transmission = new Vec3f(1);
Vec3f lightDirection = i.center - phit;
lightDirection.normalize();
foreach(Sphere j in spheres)
{
if (i != j)
{
Collision jcoll = j.intersect(phit + nhit * bias, lightDirection);
if (jcoll.collide)
{
transmission = new Vec3f(0);
break;
}
}
}
surfaceColor += sphere.surfaceColor * transmission * Math.Max(0, nhit.dot(lightDirection)) * i.emissionColor;
}
}
}
return surfaceColor;
}
public static void Render(Sphere[] spheres)
{
int width = 800, height = 600;
List<Vec3f> image = new List<Vec3f>();
float invWidth = 1 / width, invHeight = 1 / height;
float fov = 30, aspectratio = width / height;
float angle = (float)Math.Tan(Math.PI * 0.5 * fov / 180);
for (int y = 0; y < height; y++)
{
for(int x = 0; x < width; x++)
{
float xx = (2 * ((x + 0.5f) * invWidth) - 1) * angle * aspectratio;
float yy = (1 - 2 * ((y + 0.5f) * invHeight)) * angle;
Vec3f raydir = new Vec3f(xx, yy, -1);
raydir.normalize();
image.Add(trace(new Vec3f(0), raydir, spheres, 0));
}
}
Console.Write("P3 800 600 255\r\n");
int line = 150;
for(int i = 0; i < width * height; ++i)
{
if(line <= 0) {line = 150; Console.Write("\r\n");}
line--;
Vec3f pixel = GetColor(image[i]);
Console.Write(pixel.x + " " + pixel.y + " " + pixel.z);
}
}
public static Vec3f GetColor(Vec3f col)
{
return new Vec3f(Math.Min(1, col.x)* 255, Math.Min(1, col.y)* 255, Math.Min(1, col.z)* 255);
}
}
}
Anyone see what is wrong?
EDIT Program is writing traced colours to console screen. Then I am able to use windows batch files to write into a ppm file. I am creating executable using csc.exe "csc.exe raytracer.cs" And run program with "raytracer.exe > out.ppm"
Upvotes: 2
Views: 366
Reputation: 70652
The basic problem your C# code has is using int
values where you want a floating point result. Just as in the C++ code, the original int
values are converted to float
before using them in division, you need to do that in your C# code as well. In particular, your invHeight
, invWidth
, and aspectratio
calculations all need to be performed using floating point math instead of integer math:
float invWidth = 1f / width, invHeight = 1f / height;
float fov = 30, aspectratio = (float)width / height;
Also, your text output is actually missing spaces between the pixels. In your version of the code, you can fix this by inserting a space before each pixel value, except for the first in a line:
for(int i = 0; i < width * height; ++i)
{
if(line <= 0) {line = 150; Console.Write("\r\n");}
else if (line < 150) Console.Write(" ");
line--;
Vec3f pixel = GetColor(image[i]);
Console.Write(pixel.x + " " + pixel.y + " " + pixel.z);
}
Or you can, of course, just always write the space:
Console.Write(pixel.x + " " + pixel.y + " " + pixel.z + " ");
You also had a minor error in the conversion, in that you failed to add sphere.emissionColor
at the end of the trace()
method:
return surfaceColor + sphere.emissionColor;
Those three changes will fix your code and produce the results you want.
Now, that said, IMHO it is worth considering some other changes. The most notable would be to use struct
types for Vec3f
and Collision
instead of class
. Unlike in C++, where the only real difference between struct
and class
is the default accessibility for members, in C# these two kinds of types are very different in their basic behavior. In a program like this, using struct
instead of class
for frequently-used values like these can improve performance significantly, by minimizing the amount of heap-allocated data, especially data which exists only temporarily and will need to be collected by the garbage collector while your program is trying to do other work.
You might also want to consider changing the data type from float
to double
. I tested the code both ways; it makes no difference in the visual output, but I was seeing the render take 2.1 seconds on average with double
and 2.8 seconds on average with float
. The 25% improvement in speed is probably something you'd want to have. :)
As far as the struct
vs class
question goes, in my tests, using the faster double
type for the arithmetic, I saw a 36% improvement in speed using struct
instead of class
(using class
for these types runs in 3.3 seconds, while using struct
runs in 2.1 seconds).
At the same time, struct
types where the values can be modified can lead to hard-to-find bugs. A struct
really ought to be immutable, so as part of the change, I adjusted the types so that they are. This was relatively simple for the Collision
type, but in the case of Vec3f
, your code has a number of places where these values were modified (by calling normalize()
) in place. To make the change to immutable struct
values work, these all had to change so that the return value of the normalize()
method was used in place of the original value.
Other changes I made include:
Vec3f()
constructor. This isn't allowed for struct
types anyway, and there's no need for it because the default constructor will do the right thing.t0 < 0
into the Collision
type, to support the immutability of that type.Sphere
iteration loops back to using integer indexes, as in the original C++. The foreach
statement involves allocating an enumerator for each loop; by indexing the array directly, you can avoid these unnecessary allocations, and it means that the variable names make more sense too (i
and j
are conventionally reserved for indexes, so it's odd reading code where they represent something else).eta
and lining up the code more similar to the C++ code.List<Vec3f>
to using an array instead. This is more efficient and avoids having to reallocate the backing storage for the list periodically.Finally, I made a significant change in the output of the program. I wasn't interested in waiting for the console window to print all the output, nor was I interested in trying to track down and install a program that would read and display the text-based image output.
So instead, I changed the text output so that it only wrote to an in-memory string, and I added code so that the program would generate an actual PNG file that I could open directly, without going through some third-party program.
All said and done, this is what I got:
Here is my final version of the code:
class Program
{
const int MAX_RAY_DEPTH = 8;
const float FAR = 100000000;
public static void Main(string[] args)
{
Sphere[] spheres = new Sphere[7];
spheres[0] = new Sphere(new Vec3f( 0.0f, -10004, -20), 10000, new Vec3f(0.20f, 0.20f, 0.20f), 0, 0.0f);
spheres[1] = new Sphere(new Vec3f( 0.0f, 0, -20), 4, new Vec3f(1.00f, 0.32f, 0.36f), 1, 0.5f);
spheres[2] = new Sphere(new Vec3f( 5.0f, -1, -15), 2, new Vec3f(0.90f, 0.76f, 0.46f), 1, 0.0f);
spheres[3] = new Sphere(new Vec3f( 5.0f, 0, -25), 3, new Vec3f(0.65f, 0.77f, 0.97f), 1, 0.0f);
spheres[4] = new Sphere(new Vec3f(-5.5f, 0, -15), 3, new Vec3f(0.90f, 0.90f, 0.90f), 1, 0.0f);
spheres[5] = new Sphere(new Vec3f( 2f, 2, -30), 4, new Vec3f(0.53f, 0.38f, 0.91f), 1, 0.7f);
spheres[6] = new Sphere(new Vec3f( 0, 20, -30), 3, new Vec3f(0.00f, 0.00f, 0.00f), 0, 0.0f, new Vec3f(3));
Render(spheres);
}
public struct Collision
{
public readonly float t0, t1;
public readonly bool collide;
public Collision(bool col, float tt0, float tt1)
{
t0 = tt0 < 0 ? tt1 : tt0;
t1 = tt1;
collide = col;
}
}
public struct Vec3f
{
public readonly float x, y, z;
public Vec3f(float v) { x = y = z = v; }
public Vec3f(float xx, float yy, float zz) { x = xx; y = yy; z = zz; }
public Vec3f normalize()
{
float nor2 = length2();
if (nor2 > 0)
{
float invNor = 1 / (float)Math.Sqrt(nor2);
return new Vec3f(x * invNor, y * invNor, z * invNor);
}
return this;
}
public static Vec3f operator *(Vec3f l, Vec3f r)
{
return new Vec3f(l.x * r.x, l.y * r.y, l.z * r.z);
}
public static Vec3f operator *(Vec3f l, float r)
{
return new Vec3f(l.x * r, l.y * r, l.z * r);
}
public float dot(Vec3f v)
{
return x * v.x + y * v.y + z * v.z;
}
public static Vec3f operator -(Vec3f l, Vec3f r)
{
return new Vec3f(l.x - r.x, l.y - r.y, l.z - r.z);
}
public static Vec3f operator +(Vec3f l, Vec3f r)
{
return new Vec3f(l.x + r.x, l.y + r.y, l.z + r.z);
}
public static Vec3f operator -(Vec3f v)
{
return new Vec3f(-v.x, -v.y, -v.z);
}
public float length2()
{
return x * x + y * y + z * z;
}
public float length()
{
return (float)Math.Sqrt(length2());
}
}
public class Sphere
{
public readonly Vec3f center, surfaceColor, emissionColor;
public readonly float radius, radius2;
public readonly float transparency, reflection;
public Sphere(Vec3f c, float r, Vec3f sc, float refl = 0, float transp = 0, Vec3f? ec = null)
{
center = c; radius = r; radius2 = r * r;
surfaceColor = sc; emissionColor = (ec == null) ? new Vec3f() : ec.Value;
transparency = transp; reflection = refl;
}
public Collision intersect(Vec3f rayorig, Vec3f raydir)
{
Vec3f l = center - rayorig;
float tca = l.dot(raydir);
if (tca < 0) { return new Collision(); }
float d2 = l.dot(l) - tca * tca;
if (d2 > radius2) { return new Collision(); }
float thc = (float)Math.Sqrt(radius2 - d2);
return new Collision(true, tca - thc, tca + thc);
}
}
public static float mix(float a, float b, float mix)
{
return b * mix + a * (1 - mix);
}
public static Vec3f trace(Vec3f rayorig, Vec3f raydir, Sphere[] spheres, int depth)
{
float tnear = FAR;
Sphere sphere = null;
for (int i = 0; i < spheres.Length; i++)
{
Collision coll = spheres[i].intersect(rayorig, raydir);
if (coll.collide && coll.t0 < tnear)
{
tnear = coll.t0;
sphere = spheres[i];
}
}
if (sphere == null) { return new Vec3f(2); }
Vec3f surfaceColor = new Vec3f();
Vec3f phit = rayorig + raydir * tnear;
Vec3f nhit = (phit - sphere.center).normalize();
float bias = 1e-4f;
bool inside = false;
if (raydir.dot(nhit) > 0) { nhit = -nhit; inside = true; }
if ((sphere.transparency > 0 || sphere.reflection > 0) && depth < MAX_RAY_DEPTH)
{
float facingratio = -raydir.dot(nhit);
float fresneleffect = mix((float)Math.Pow(1 - facingratio, 3), 1, 0.1f);
Vec3f refldir = (raydir - nhit * 2 * raydir.dot(nhit)).normalize();
Vec3f reflection = trace(phit + nhit * bias, refldir, spheres, depth + 1);
Vec3f refraction = new Vec3f();
if (sphere.transparency > 0)
{
float ior = 1.1f; float eta = inside ? ior : 1 / ior;
float cosi = -nhit.dot(raydir);
float k = 1 - eta * eta * (1 - cosi * cosi);
Vec3f refrdir = (raydir * eta + nhit * (eta * cosi - (float)Math.Sqrt(k))).normalize();
refraction = trace(phit - nhit * bias, refrdir, spheres, depth + 1);
}
surfaceColor = (
reflection * fresneleffect +
refraction * (1 - fresneleffect) * sphere.transparency) * sphere.surfaceColor;
}
else
{
for (int i = 0; i < spheres.Length; i++)
{
if (spheres[i].emissionColor.x > 0)
{
Vec3f transmission = new Vec3f(1);
Vec3f lightDirection = (spheres[i].center - phit).normalize();
for (int j = 0; j < spheres.Length; j++)
{
if (i != j)
{
Collision jcoll = spheres[j].intersect(phit + nhit * bias, lightDirection);
if (jcoll.collide)
{
transmission = new Vec3f();
break;
}
}
}
surfaceColor += sphere.surfaceColor * transmission *
Math.Max(0, nhit.dot(lightDirection)) * spheres[i].emissionColor;
}
}
}
return surfaceColor + sphere.emissionColor;
}
public static void Render(Sphere[] spheres)
{
int width = 800, height = 600;
Vec3f[] image = new Vec3f[width * height];
int pixelIndex = 0;
float invWidth = 1f / width, invHeight = 1f / height;
float fov = 30, aspectratio = (float)width / height;
float angle = (float)Math.Tan(Math.PI * 0.5 * fov / 180);
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++, pixelIndex++)
{
float xx = (2 * ((x + 0.5f) * invWidth) - 1) * angle * aspectratio;
float yy = (1 - 2 * ((y + 0.5f) * invHeight)) * angle;
Vec3f raydir = new Vec3f(xx, yy, -1).normalize();
image[pixelIndex] = trace(new Vec3f(), raydir, spheres, 0);
}
}
StringWriter writer = new StringWriter();
WriteableBitmap bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Rgb24, null);
bitmap.Lock();
unsafe
{
byte* buffer = (byte*)bitmap.BackBuffer;
{
writer.Write("P3 800 600 255\r\n");
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; ++x)
{
if (x > 0) { writer.Write(" "); }
Vec3f pixel = GetColor(image[y * width + x]);
writer.Write(pixel.x + " " + pixel.y + " " + pixel.z);
int bufferOffset = y * bitmap.BackBufferStride + x * 3;
buffer[bufferOffset] = (byte)pixel.x;
buffer[bufferOffset + 1] = (byte)pixel.y;
buffer[bufferOffset + 2] = (byte)pixel.z;
}
writer.WriteLine();
}
}
}
bitmap.Unlock();
var encoder = new PngBitmapEncoder();
using (Stream stream = File.OpenWrite("temp.png"))
{
encoder.Frames.Add(BitmapFrame.Create(bitmap));
encoder.Save(stream);
}
string result = writer.ToString();
}
public static Vec3f GetColor(Vec3f col)
{
return new Vec3f(Math.Min(1, col.x) * 255, Math.Min(1, col.y) * 255, Math.Min(1, col.z) * 255);
}
}
Note that for the above to compile, you'll need to add references in your project to the PresentationCore, WindowsBase, and System.Xaml assemblies. You'll also need to check the "Allow unsafe code" option in the project settings.
Upvotes: 3