Raytracing Practice In Unity(Part One)

Summary

Posted by Fantasy Wang on April 8, 2018

In this week, I researched SmallPt. It’s a kind of Global Illumination by raytracing. So I use Zavis’s implementation as a reference and implement a Unity version of SmallPt. Here is my rendering result in one frame: RayTracing Result

Main Structure
  • On c# side, I manage all sphere objects in a ComputeBuffer:
    const int SphereCount = 9;
    ComputeBuffer sceneBuffer;
    Material material;
    float Maxistance
    {
      get { return (float)1e5; }
    }
    void Start()
    {
      material = GetComponent<MeshRenderer>().sharedMaterial;
      InitSceneBuffer();
    }
    void InitSceneBuffer()
    {
      if (sceneBuffer != null)
          sceneBuffer.Release();
      sceneBuffer = new ComputeBuffer(SphereCount, 4 + 12 + 12 + 12 + 4);
      Sphere[] spheres = new Sphere[SphereCount];
      spheres[0] = new Sphere(16.5f, new Vector3(27f, 16.5f, 57f), Vector3.zero, new Vector3(1,1,1), 1);
      spheres[1] = new Sphere(16.5f, new Vector3(73f, 16.5f, 78f), Vector3.zero, new Vector3(0.7f, 1f, 0.9f), 2);
      spheres[2] = new Sphere(600f , new Vector3(50f, 681.33f, 81.6f), new Vector3(12,12,12), Vector3.zero, 0);
    
      spheres[3] = new Sphere(Maxistance, new Vector3(-Maxistance+1, 40.8f, 81.6f), Vector3.zero, new Vector3(0.75f, 0.25f, 0.25f), 0);
      spheres[4] = new Sphere(Maxistance, new Vector3(-Maxistance + 99, 40.8f, 81.6f), Vector3.zero, new Vector3(0.25f, 0.25f, 0.75f), 0);
      spheres[5] = new Sphere(Maxistance, new Vector3(50, 40.8f, -Maxistance), Vector3.zero, new Vector3(0f, 0f, 0f), 0);
      spheres[6] = new Sphere(Maxistance, new Vector3(50, 40.8f, Maxistance + 170), Vector3.zero, new Vector3(0.75f, 0.75f, 0.75f), 0);
      spheres[7] = new Sphere(Maxistance, new Vector3(50, -Maxistance-9, 81.6f), Vector3.zero, new Vector3(0.75f, 0.75f, 0.75f), 0);
      spheres[8] = new Sphere(Maxistance, new Vector3(50, Maxistance + 81.6f, 81.6f), Vector3.zero, new Vector3(0.75f, 0.75f, 0.75f), 0);
    
      sceneBuffer.SetData(spheres);
      material.SetBuffer("spheres", sceneBuffer);
      material.SetInt("_ObjCount", SphereCount);
    }
    void OnDisable()
    {
      if (sceneBuffer != null)
          sceneBuffer.Release();
      sceneBuffer = null;
    }
    
  • On shader side, I just do raytracing like Zavie did in shadertoy.
    Shader "Custom/RayTracing" {
      Properties
      {
          _ObjCount("Object Count", int) = 1
      }
      SubShader  
      {    
          Tags {"Queue" = "Transparent" "RenderType" = "Transparent"}
    
          Blend SrcAlpha OneMinusSrcAlpha 
    
          Pass  
          {  
              CGPROGRAM  
              #pragma target 5.0
    
              #pragma vertex vert  
              #pragma fragment frag  
      
              #include "UnityCG.cginc"  
    
              #define SAMPLES 6
              #define MAXDEPTH 4
              #define ENABLE_NEXT_EVENT_PREDICTION
    
              #define PI 3.14159265359 
              #define DIFF 0
              #define SPEC 1
              #define REFR 2
    
              float seed;
              float rand() 
              { 
                  seed = seed + 1;
                  return frac(sin(seed)*43758.5453123); 
              }
    			
              struct Ray
              {
                  float3 o;
                  float3 d;
              };
    
              struct Sphere
              {
                  float r;
                  float3 p;
                  float3 e;   //emission
                  float3 c;   //color
                  int refl; //DIFF, SPEC, REFR
              };
    
              StructuredBuffer<Sphere> spheres;
              int _ObjCount;
    
              float intersect(Sphere s, Ray r) {
                  float3 op = s.p - r.o;
                  float t, epsilon = 1e-3, b = dot(op, r.d), det = b * b - dot(op, op) + s.r * s.r;
                  if (det < 0.) return 0.; else det = sqrt(det);
                  return (t = b - det) > epsilon ? t : ((t = b + det) > epsilon ? t : 0.);
              }
    
              int intersect(Ray r, out float t, out Sphere s, int avoid)
              {
                  int id = -1;
                  t = 1e5;
                  s = spheres[0];
    
                  for (int i = 0; i < _ObjCount; ++i) {
                      Sphere S = spheres[i];
                      float d = intersect(S, r);
                      if (i!=avoid && d!=0. && d<t) { t = d; id = i; s=S; }
                  }
                  return id;
              }
    
              float3 jitter(float3 d, float phi, float sina, float cosa) {
                  float3 w = normalize(d), u = normalize(cross(w.yzx, w)), v = cross(w, u);
                  return (u*cos(phi) + v*sin(phi)) * sina + w * cosa;
              }
    
              float3 radiance(Ray r) {
                  float3 acc = float3(0,0,0);
                  float3 mask = float3(1,1,1);
                  int id = -1;
                  for (int depth = 0; depth < MAXDEPTH; ++depth) {
                      float t;
                      Sphere obj;
                      if ((id = intersect(r, t, obj, id)) < 0) break;
                      float3 x = t * r.d + r.o;
                      float3 n = normalize(x - obj.p), nl = n * sign(-dot(n, r.d));
    
                      //float3 f = obj.c;
                      //float p = dot(f, float3(1.2126, 0.7152, 0.0722));
                      //if (depth > DEPTH_RUSSIAN || p == 0.) if (rand() < p) f /= p; else { acc += mask * obj.e * E; break; }
    
                      if (obj.refl == DIFF) {
                          float r2 = rand();
                          float3 d = jitter(nl, 2.*PI*rand(), sqrt(r2), sqrt(1. - r2));
                          float3 e = float3(0, 0, 0);
              #ifdef ENABLE_NEXT_EVENT_PREDICTION
                          //for (int i = 0; i < NUM_SPHERES; ++i)
                          {
                              // Sphere s = sphere(i);
                              // if (dot(s.e, float3(1.)) == 0.) continue;
    
                              // Normally we would loop over the light sources and
                              // cast rays toward them, but since there is only one
                              // light source, that is mostly occluded, here goes
                              // the ad hoc optimization:
                              Sphere s =  {20, float3(50, 81.6, 81.6), float3(12, 12, 12), float3(0,0,0), DIFF};
                              int i = 2;
    
                              float3 l0 = s.p - x;
                              float cos_a_max = sqrt(1. - clamp(s.r * s.r / dot(l0, l0), 0., 1.));
                              float cosa = lerp(cos_a_max, 1., rand());
                              float3 l = jitter(l0, 2.*PI*rand(), sqrt(1. - cosa*cosa), cosa);
    
                              Ray rTemp = {x, l};
                              if (intersect(rTemp, t, s, id) == i) {
                                  float omega = 2. * PI * (1. - cos_a_max);
                                  e += (s.e * clamp(dot(l, n),0.,1.) * omega) / PI;
                              }
                          }
              #endif
                          float E = 1.;//float(depth==0);
                          acc += mask * obj.e * E + mask * obj.c * e;
                          mask *= obj.c;
                          Ray rTemp = {x, d};
                          r = rTemp;
                      } else if (obj.refl == SPEC) {
                          acc += mask * obj.e;
                          mask *= obj.c;
                          Ray rTemp = {x, reflect(r.d, n)};
                          r = rTemp;
                      } else {
                          float a=dot(n,r.d), ddn=abs(a);
                          float nc=1., nt=1.5, nnt=lerp(nc/nt, nt/nc, float(a>0.));
                          float cos2t=1.-nnt*nnt*(1.-ddn*ddn);
                          Ray rTemp = {x, reflect(r.d, n)};
                          r = rTemp;
                          if (cos2t>0.) {
                              float3 tdir = normalize(r.d*nnt + sign(a)*n*(ddn*nnt+sqrt(cos2t)));
                              float R0=(nt-nc)*(nt-nc)/((nt+nc)*(nt+nc)),
                                  c = 1.-lerp(ddn,dot(tdir, n),float(a>0.));
                              float Re=R0+(1.-R0)*c*c*c*c*c,P=.25+.5*Re,RP=Re/P,TP=(1.-Re)/(1.-P);
                              if (rand()<P) { mask *= RP; }
                              else { mask *= obj.c*TP; Ray rTemp = {x, tdir}; r = rTemp; }
                          }
                      }
                  }
                  return acc;
              }
    
              struct appdata  
              {  
                  float4 vertex : POSITION;  
              };  
      
              struct v2f  
              {  
                  float4 vertex : SV_POSITION;  
                  float3 worldPos : TEXCOORD1;  
              };  
      
              v2f vert(appdata v)  
              {  
                  v2f o;  
                  o.vertex = UnityObjectToClipPos(v.vertex);  
                  o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                  return o;  
              }  
    			
              float4 frag(v2f input) : SV_Target  
              {   
                  seed = input.worldPos.x + input.worldPos.y + _Time.x;
                  float3 viewDirection = normalize(input.worldPos - _WorldSpaceCameraPos);
                  Ray ray = {input.worldPos,viewDirection};
                  float3 color = float3(0,0,0);
                  for (int i = 0; i < SAMPLES; ++i)
                  {
                      color += radiance(ray);
                  }
    
                  return float4(pow (saturate(color / SAMPLES), 1/2.2),1);
              }  
              ENDCG  
          }  
      }  
      FallBack "Diffuse"
    }
    
Next

I will talk about some detail key technique points in following parts of my post series.