光线跟踪器没有产生预期的输出

问题描述:

我遇到了一个问题:我读了一个带有C++代码的article on scratchapixel光线跟踪。 C++没问题。我试图将它转换成Python,它工作(结果减慢17倍,分辨率降低4倍)。我试图将其转换为C#,但我的代码无法正常工作。只有我能看到的是一张空白的800x600图像。请参阅以前链接的C++代码文章。光线跟踪器没有产生预期的输出

这是我对它的解释为C#代码:

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); 
    } 
} 
} 

任何人看到什么是错的?

编辑 程序正在将追踪的颜色写入控制台屏幕。然后,我可以使用Windows批处理文件写入ppm文件。 我使用CSC.EXE “CSC.EXE raytracer.cs” 和运行程序与 “raytracer.exe> out.ppm”

+0

如果它在C++中工作,并且由于C++通常更快,也许你应该把它放在你从C#中调用/调用的DLL中?我不知道你转换它的原因,但这就是我要做的。 –

+0

那么...你检查了生成的ppm文件吗?它实际上包含什么? – Cubic

+1

@VisualVincent这不是关于性能,而是将它转换为练习 – Cubic

的基本问题你的C#代码有创建可执行文件使用int值,其中你想要一个浮点结果。就像在C++代码中一样,原来的int值在转换为分区之前转换为float,您也需要在C#代码中执行此操作。特别是,你invHeightinvWidth,并aspectratio计算都需要使用的,而不是整数数学浮点运算进行:

float invWidth = 1f/width, invHeight = 1f/height; 
    float fov = 30, aspectratio = (float)width/height; 

像素之间的同时,你的文本输出实际上是缺少空格。在你的代码版本,你可以通过每个像素值前插入空格,除了在一行中的第一个解决这个问题:

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); 
    } 

或者你可以,当然,只是一直写空间:

 Console.Write(pixel.x + " " + pixel.y + " " + pixel.z + " "); 

你也不得不在转换一个小错误,因为你没有在trace()方法的末尾添加sphere.emissionColor

 return surfaceColor + sphere.emissionColor; 

这三个变化将解决哟你的代码并产生你想要的结果。


现在,这就是说,恕我直言,这是值得考虑一些其他的变化。最值得注意的是将struct类型用于Vec3fCollision而不是class。与C++不同,其中structclass之间的唯一真正区别是成员的默认可访问性,在C#中,这两种类型的基本行为有很大不同。在像这样的程序中,使用struct而不是class作为这些频繁使用的值可以显着提高性能,这可以通过最小化堆分配数据量,特别是仅临时存在且需要由垃圾回收器收集的数据你的程序正在尝试做其他工作。

您可能还想考虑将数据类型从float更改为double。我用两种方式测试了代码;它在视觉输出上没有任何区别,但我看到渲染平均需要2.1秒的时间,double和平均2.8秒,float。速度提高25%可能是你想要的。 :)

至于struct VS class问题去,在我的测试,使用更快的double类型的算术,我看到速度使用struct代替class(使用class这些类型运行在36%的改善3.3秒,同时使用struct在2.1秒内运行)。

与此同时,值可以修改的struct类型可能会导致难以找到的错误。一个struct真的应该是不可变的,所以作为变化的一部分,我调整了它们的类型。对于Collision类型,这是相对简单的,但在Vec3f的情况下,您的代码有一些地方修改了这些值(通过调用normalize())。要使更改为不可更改的struct值有效,必须更改这些全部值,以便使用normalize()方法的返回值代替原始值。

我的其他修改包括:

  • 卸下Vec3f()构造。无论如何,这是不允许的struct类型,并且不需要它,因为默认的构造函数会做正确的事情。
  • 将冲突检查t0 < 0转换为Collision类型,以支持该类型的不变性。
  • 更改您的Sphere迭代循环回到使用整数索引,如在原始的C++中。语句foreach涉及为每个循环分配一个枚举器;通过直接索引数组,您可以避免这些不必要的分配,这意味着变量名也更有意义(ij通常为索引保留,所以它是奇怪的阅读代码,它们代表了其他内容)。
  • 我还将代码返回为更类似于其他地方的C++代码,例如eta的初始化和排列更类似于C++代码的代码。
  • 我将代码从使用List<Vec3f>更改为使用数组。这样更有效,并且避免了必须定期重新分配列表的后备存储。

最后,我对程序的输出进行了重大改变。我对等待控制台窗口打印所有输出不感兴趣,也没有兴趣试图追踪并安装可读取并显示基于文本的图像输出的程序。

因此,我改变了文本输出,以便它只写入内存字符串,并添加了代码,以便程序生成一个实际的PNG文件,我可以直接打开,派对节目。

一切都成定局,这是我的了:

ray-traced balls

这里是我的代码的最终版本:

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); 
    } 
} 

注意,对于上面的代码编译,你”您需要在您的项目中添加对PresentationCore,WindowsBase和System的引用。Xaml组件。您还需要检查项目设置中的“允许不安全的代码”选项。

+0

非常感谢!我现在看到我的错误。我在编程时认为浮动就像真正的数学(如1/2)。现在我明白为什么C++代码在任何地方都使用0.0表达式。再次感谢你。我会记住这些。 –