Raytracing Diffuse Function not Working as Expected

I am trying to write a raytracing renderer using compute shaders in Metal. My implementation is based off of Peter Shirley's Raytracing in One Weekend: https://raytracing.github.io/books/RayTracingInOneWeekend.html.

When I remove the code to generate randomness in the rays, this is what I get: Notice how the second layer of black extends all the way around the bottom sphere.

When I run my own code, I get an issue where that layer of black doesn't go all the way around:

This is all my code:

class ray{
public:
float3 origin;
float3 direction;
ray(const float3 orig, const float3 dir){
origin = orig;
direction = dir;
}
ray(){
origin = float3(0,0,0);
direction = float3(0,0,0);
}
float3 rayAtPoint(float t){
return origin + t*direction;
}
};
class camera {
public:
camera() {
float aspectRatio = 16.0 / 9.0;
float cameraHeight = 2;
float cameraWidth = aspectRatio * cameraHeight;
float focalLength = 1.0;

origin = float3(0, 0, 0);
horizontal = float3(cameraWidth, 0.0, 0.0);
vertical = float3(0.0, cameraHeight, 0.0);
lower_left_corner = origin - horizontal/2 - vertical/2 - float3(0, 0, focalLength);
}

ray get_ray(float u, float v){
return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
}

private:
float3 origin;
float3 lower_left_corner;
float3 horizontal;
float3 vertical;
};

struct hit_record {
float3 p;
float3 normal;
float t;
bool front_face;

void set_face_normal(ray r, float3 outward_normal) {
front_face = dot(r.direction, outward_normal) < 0;
normal = front_face ? outward_normal :-outward_normal;
}
};
struct hit_return{
bool hit;
hit_record rec;
};

class sphere{
public:
sphere() {
exists = false;
}
sphere(float3 cen, float r){
exists = true;
center = cen;

};

hit_return hit(ray r, float t_min, float t_max);

public:
bool exists = false;
float3 center;
};
float rand(int x, int y, int z)
{
int seed = x + y * 57 + z * 241;
seed= (seed<< 13) ^ seed;
return (( 1.0 - ( (seed * (seed * seed * 15731 + 789221) + 1376312589) & 2147483647) / 1073741824.0f) + 1.0f) / 2.0f;
}
int simpleRand(int x, int y, int z){
return ((x + y + z) % 499);
}
float random_double(float min, float max, int x, int y, int z) {
// Returns a random real in [min,max).
return min + (max-min)*rand(x, y, z);
}
float3 random_float3(int x, int y, int z){
return float3(rand(x,y,z), rand(y,x,z), rand(z,x,y));
}
float3 random_float3(float min, float max, int x, int y, int z){
return float3(random_double(min, max, x, y, z), random_double(min, max, y, z, x), random_double(min, max, x, z, y));
}
float3 random_in_unit_sphere(int x, int y, int z){
while (true) {
auto p = random_float3(-1,1, x, y, z);
if (length_squared(p) >= 1) continue;
return p;
}
}
float hash(float3 p) {
return fract(sin(dot(p, float3(12.9898, 78.233, 45.5432))) * 43758.5453);
}

float3 random_point_in_unit_sphere(float2 seed)
{
float3 p;
do {
p = 2.0 * float3(fract(sin(float(seed.x) * 143758.5453 + float(seed.y) * 182673.7123)) - 0.5,
fract(sin(float(seed.x) * 832438.1234 + float(seed.y) * 934657.2374)) - 0.5,
fract(sin(float(seed.x) * 321632.9187 + float(seed.y) * 642547.3216)) - 0.5);
} while (dot(p, p) >= 1.0);
return normalize(p);
}
float3 randomPointInUnitSphere(float x, float y, float z) {
float3 point;
float r = rand(x, y, z); // generate random value between 0 and 1
float theta = rand(y, z, x) * 2.0 * M_PI; // generate random angle between 0 and 2pi
float phi = acos(2.0 * rand(z, x, y) - 1.0); // generate random angle between 0 and pi
point.x = r * sin(phi) * cos(theta);
point.y = r * sin(phi) * sin(theta);
point.z = r * cos(phi);
return point;
}
hit_return sphere::hit(ray r, float t_min, float t_max){
hit_record rec;
hit_return ret;

float3 oc = r.origin - center;
auto a = length_squared(r.direction);
auto half_b = dot(oc, r.direction);

auto discriminant = half_b*half_b - a*c;
if (discriminant < 0){
ret.hit = false;
return ret;
}
auto sqrtd = sqrt(discriminant);

// Find the nearest root that lies in the acceptable range.
auto root = (-half_b - sqrtd) / a;
if (root < t_min || t_max < root) {
root = (-half_b + sqrtd) / a;
if (root < t_min || t_max < root)
ret.hit = false;
return ret;

}

rec.t = root;
rec.p = r.rayAtPoint(rec.t);
float3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
ret.rec = rec;
ret.hit = true;
return ret;
}

class hittable_list {
public:
hittable_list(){
maxObjects = 2;
objectsUpTo = 0;
for(int i = 0; i < 100; i++){
exits[i] = false;
}
}

objects[objectsUpTo] = object;
exits[objectsUpTo] = true;
objectsUpTo++;
}

hit_return hit(ray r, float t_min, float t_max);

public:
int    maxObjects = 2;
int    objectsUpTo = 0;
bool   exits[100];
sphere objects[100];

};
hit_return hittable_list::hit(ray r, float t_min, float t_max) {
hit_record rec;
hit_record temp_rec;
hit_return temp_ret;
bool hit_anything = false;
auto closest_so_far = t_max;
//For some reason if I just set it to maxObjects gpu time goes up to ~25ms (~35fps)

for (int i = 0; i < 2; i++) {
if ((temp_ret = objects[i].hit(r, t_min, closest_so_far)).hit) {
temp_rec = temp_ret.rec;
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
}

hit_return returnn;
returnn.hit = hit_anything;
returnn.rec = rec;
return returnn;
}

half hit_sphere(float3 center, float radius, ray r) {
float3 oc = r.origin - center;
auto a = pow(length(r.direction),2);
auto half_b = dot(oc, r.direction);
auto discriminant = half_b*half_b - a*c;
if (discriminant < 0) {
return -1.0;

} else {
return (-half_b - sqrt(discriminant) ) / a;
}
}
float3 unit_vector(float3 v){
return v / length(v);
}
float3 ray_color(ray r, hittable_list world, int depth, int x, int y, int z){
hit_return ret;
hit_record rec;
float3 finalColor = float3(0,0,0);
for(int i = 0; i < 2; i++){

if ((ret = world.hit(r, 0, 1000)).hit) {
rec = ret.rec;
finalColor +=  (0.5 * (rec.normal + float3(1,1,1))) / 2;
}

half3 unit_dir = half3(unit_vector(r.direction));
auto t = 0.5*(unit_dir.y + 1.0);
finalColor += (((1-t)*float3(1.0,1.0,1.0) + t*float3(0.5, 0.7, 1.0)))/2;
}
return finalColor;
}
float3 ray_color_iterative(ray r, thread hittable_list* world, int depth, int x, int y, int z, constant float3 *points){
ray cur_ray = r;
float cur_attenuation = 1.0;
for (int i = 0; i < 5; i++){
hit_return ret;
hit_record rec;
if((ret = world->hit(cur_ray, 0.1, 1000)).hit){
rec = ret.rec;
float3 target = rec.p + rec.normal;
cur_attenuation *= 0.5;
cur_ray = ray(rec.p, target - rec.p);
}else{
float3 unit_direction = unit_vector(cur_ray.direction);
float t = 0.5 * (unit_direction.y + 1);
float3 c = (1.0-t)*float3(1.0, 1.0, 1.0) + t*float3(0.5, 0.7, 1.0);
return cur_attenuation * c;
}
}
return float3(0,0,0);
}

texture2d<half, access::write> texture [[texture(1)]],
texture2d<half, access::write> texture2 [[texture(2)]],
constant float3 *points [[buffer(1)]],
constant float2 &randomSeed [[buffer(2)]]){

//Image
half aspectRatio = 16.0/9.0;
uint16_t imageWidth  = 2560;
uint16_t imageHeight = imageWidth / aspectRatio;
uint16_t samplesPerPixel = 4;

camera cam;
float3 color(0,0,0);

//Scene
hittable_list scene;
sphere s(float3(0,0,-1), 0.5);
sphere sTwo(float3(0,-100.5,-1), 100);
float2 newRandomSeed = randomSeed;
for(int i = 0; i < samplesPerPixel; i++){
newRandomSeed.x *= i;
newRandomSeed.y /= i;
newRandomSeed.x += pixel.x;
newRandomSeed.y += pixel.y;
float u = (half(pixel.x) + rand(i, pixel.y, pixel.x)) / (imageWidth - 1);
float v = (half(pixel.y) + rand(pixel.x, pixel.y, i)) / (imageHeight - 1);
ray r = cam.get_ray(u, v);
color += ray_color_iterative(r, &scene, 1,  pixel.x + randomSeed.x * 2, pixel.y - randomSeed.y, i + randomSeed.y, points);
//
//
//
}
uint2 newPixel = uint2(pixel.x, ((pixel.y-720)*-1)+720);
color /= samplesPerPixel;
texture.write(half4(float4(color, 1)), newPixel);
}


Using the metal debugger, and print statements in C, I can see that the target variable is exactly the same in both my reference code and my metal code, given the same pixel, if that helps at all.

• I think you are generating random numbers incorrectly. Although you seem to have some random and hash functions defined, in the kernel function you just multiply the seed value coming from (the CPU, prolly) with scalars/floats and use that. That won't generate proper random numbers. As a test you can use those random numbers to generate a black/white image where if the random number is less than 0.5 it's black and vice versa. The result should be random noise not a pattern. May 6 at 3:33
• Here is a nice way to do rng on the GPU. reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11 May 6 at 3:34
• I solved the problem, it was with the 0 and 1000 min and max hit distance. Now, I do need a faster way to generate random numbers, however, the rand function works the best somehow 🤷‍♂️, and is the only function that provides me with good random numbers from 0 to 1.
– oli2
May 7 at 22:02