def test_sub(self): a = Vec3(0.2, 0.3, 0.4) b = Vec3(0.1, 0.1, 0.1) c = a - b self.assertAlmostEqual(c.x, 0.1, 5) self.assertAlmostEqual(c.y, 0.2, 5) self.assertAlmostEqual(c.z, 0.3, 5)
def test_add(self): a = Vec3(0.1, 0.1, 0.2) b = Vec3(0.2, 0.3, 0.5) c = a + b self.assertAlmostEqual(c.x, 0.3, 5) self.assertAlmostEqual(c.y, 0.4, 5) self.assertAlmostEqual(c.z, 0.7, 5)
def test_multVector(self): a = Vec3(0.2, 0.3, 0.4) b = Vec3(2, 2, 2) c = a * b self.assertAlmostEqual(c.x, 0.4, 5) self.assertAlmostEqual(c.y, 0.6, 5) self.assertAlmostEqual(c.z, 0.8, 5)
def scatter(self, rIn, rec): # returns (attenuation, scattered) or None reflected = Vec3.reflect(Vec3.unitVector(rIn.direction()), rec.normal) scattered = Ray(rec.p, reflected + Vec3.randomInUnitSphere()*self.fuzz) attenuation = self.albedo if Vec3.dot(scattered.direction(), rec.normal) > 0: return (attenuation, scattered) else: return None
def random_in_unit_sphere(): while True: p = Vec3(float(random.random()), float(random.random()), float(random.random())).Scale(2.0).Sub(Vec3(1.0, 1.0, 1.0)) if p.dot(p) >= 1.0: pass else: return p
def __init__(self): ''' @param {t: hit time, p: hit position, normal: hit point normal, mat_ptr: material} @return: None ''' self.t = 0 self.p = Vec3(0.0, 0.0, 0.0) self.normal = Vec3(0.0, 0.0, 0.0) self.mat_ptr = None
def render_fast(self): r = float(self.camera.width) / self.camera.height S = (-1., 1. / r + .25, 1., -1. / r + .25) x = jnp.tile(jnp.linspace(S[0], S[2], self.camera.width), self.camera.height) y = jnp.repeat(jnp.linspace(S[1], S[3], self.camera.height), self.camera.width) origin = Vec3(self.camera.origin[0], self.camera.origin[1], self.camera.origin[2]) image = self.ray_trace(origin, (Vec3(x, y, 0) - origin).norm()) return image
def dot(self, other): ''' get product with self and other, self is the left operand. ''' return Mat3( Vec3(self.x.dot(other.rx), self.x.dot(other.ry), self.x.dot(other.rz)), Vec3(self.y.dot(other.rx), self.y.dot(other.ry), self.y.dot(other.rz)), Vec3(self.z.dot(other.rx), self.z.dot(other.ry), self.z.dot(other.rz)))
def render_pixel_antialias(u, v, camera, light, asset, file, antialias_level=0): """ :param u: :param v: :param camera: :param light: :param asset: :param file: :param antialias_level: level of antialiasing n shoot 2^2n rays per pixel and average the result :return: """ a_n = 2**antialias_level ray_offset = [2 / a_n * i - (1 - 1 / a_n) for i in range(a_n)] # ray_offset = [-0.5, 0.5] # ray_offset = [-0.75, -0.25, 0.25, 0.75] c_list = [] for du in ray_offset: for dv in ray_offset: c = Vec3(0, 0, 0) ray = camera.shoot_ray(u + du, v + dv) intersect, i = asset.intersect(ray) if intersect: n = asset.normal(i) l = light.pos - i r = normalize(2 * n - l) l_intensity = light.strength(i) * light.color i_ambient = k_ambient * ambient i_diffuse = k_diffuse * max(dot(n, normalize(l)), 0) * l_intensity i_specular = k_specular * pow( max(dot(r, normalize(-1 * i)), 0), n_specular) * l_intensity c = mul(asset.color, i_ambient + i_diffuse + i_specular) c_list.append(c) c = Vec3(0, 0, 0) for ci in c_list: c += ci c = c / len(ray_offset)**2 file.write("{} {} {}\n".format(floatto8bit(c.x), floatto8bit(c.y), floatto8bit(c.z)))
def __init__(self, origin, lookat, vup, vfov, aspectRatio, aperture, focusDist): self.origin = origin self.lensRadius = aperture / 2 self.theta = math.radians(vfov) self.halfHeight = math.tan(self.theta / 2) self.halfWidth = aspectRatio * self.halfHeight self.w = Vec3.unitVector(lookat - origin) self.u = Vec3.unitVector(Vec3.cross(self.w, vup)) self.v = Vec3.cross(self.u, self.w) self.lowerLeftCorner = origin - self.u * self.halfWidth * focusDist - self.v * self.halfHeight * focusDist + self.w * focusDist self.horizontal = self.u * self.halfWidth * focusDist * 2 self.vertical = self.v * self.halfHeight * focusDist * 2
def __init__(self, camera: Camera, scene: Scene, light_position: Vec3 = Vec3(5., 5., -10.)): self.camera = camera self.scene = scene self.light_position = light_position
def scatter(self, ray, rec, wrapper): ''' @return: bool ''' self.wrapper = wrapper outward_normal = None reflected = self.reflect(ray.direction(), rec.normal) ni_over_nt = None self.wrapper.attenuation = Vec3(1.0, 1.0, 1.0) refracted = None if (ray.direction().dot(rec.normal) > 0): # 从密度小的介质到密度大的介质 outward_normal = rec.normal.Scale(-1.0) ni_over_nt = self.ri cosine = ray.direction().dot( rec.normal) / (ray.direction().length() * rec.normal.length()) else: # 从密度大的介质到密度小的介质 outward_normal = rec.normal ni_over_nt = 1.0 / self.ri cosine = -ray.direction().dot(rec.normal) / ( ray.direction().length() * rec.normal.length()) if self.refract(ray.direction(), outward_normal, ni_over_nt, self.wrapper): reflect_prob = self.schlick(cosine, self.ri) else: reflect_prob = 1.0 if random.random() < reflect_prob: self.wrapper.scattered = Ray(rec.p, reflected) else: self.wrapper.scattered = Ray(rec.p, self.wrapper.refracted) return True
def __init__(self, t, p, mat, r, outwardNormal): self.t = t self.p = p self.mat = mat self.frontFace = Vec3.dot(r.direction(), outwardNormal) < 0 self.normal = outwardNormal if self.frontFace else outwardNormal*-1
def test_normalize(self): a = Vec3(0.1, 0.2, 0.3) b = a.normalize() # result from here # https://calculator.academy/normalize-vector-calculator/#f1p1|f2p0 self.assertAlmostEqual(b.x, 0.2672, 3) self.assertAlmostEqual(b.y, 0.5345, 3) self.assertAlmostEqual(b.z, 0.8017, 3)
def rayColor(r, world, depth): # if we've exceeded the ray bounce limit, no more light is gathered. if depth <= 0: return Vec3(0,0,0) rec = world.hit(r, 0.001, float('inf')) if rec != None: result = rec.mat.scatter(r, rec) if result != None: attenuation = result[0] scattered = result[1] return rayColor(scattered, world, depth-1)*attenuation return Vec3(0,0,0) # background unitDirection = Vec3.unitVector(r.direction()) t = (unitDirection.y() + 1.0)*0.5 return Vec3(1.0, 1.0, 1.0)*(1.0-t) + Vec3(0.5, 0.7, 1.0)*t
def __init__(self, look_from, look_at, vup, vfov, aspect): self.lower_left = Vec3(-2.0, -2.0, -2.0) self.horizontal = Vec3(4.0, 0.0, 0.0) # width self.vertical = Vec3(0.0, 4.0, 0.0) # height self.origin = Vec3(0.0, 0.0, 0.0) # camara position # Camera cordinate self.u, self.v, self.w = None, None, None self.theta = float(vfov * math.pi / 180) # fov theta self.half_height = float(math.tan(self.theta / 2)) self.half_width = aspect * self.half_height self.origin = look_from self.w = look_from.Sub(look_at).normalize() self.u = vup.cross(self.w).normalize() self.v = self.w.cross(self.u).normalize() self.lower_left = self.origin.Sub(self.u.Scale(self.half_width)).Sub( self.v.Scale(self.half_height)).Sub(self.w) self.horizontal = self.u.Scale(2 * self.half_width) self.vertical = self.v.Scale(2 * self.half_height)
def light(self, origin, direction, intersection, light_position, eye_position, scene_objects, bounce=0, far=1.0e15): ''' Basic light model using a only diffuse lighting ''' rayhit = origin + direction * intersection normal = ((rayhit - self.center) * (1. / self.radius)) direction_to_light = (light_position - rayhit).norm() direction_to_eye = (eye_position - rayhit).norm() nudged = rayhit + normal * 0.001 # To avoid shadow acne # Create shadow mask light_distances = [ o.intersect(nudged, direction_to_light, far=far) for o in scene_objects ] light_nearest = reduce(jnp.minimum, light_distances) light_mask = light_distances[scene_objects.index( self)] == light_nearest # Ambient light color = Vec3(0.05, 0.05, 0.05) # Lambert shading (diffuse) light_hit = jnp.maximum(normal.dot(direction_to_light), 0) color += self.diffusecolor(rayhit) * light_hit * light_mask # Phong light phong = normal.dot((direction_to_light + direction_to_eye).norm()) color += Vec3(1., 1., 1.) * jnp.power(jnp.clip(phong, 0, 1), 50) * light_mask return color
def scatter(self, rIn, rec): # returns (attenuation, scattered ray) attenuation = Vec3(1.0, 1.0, 1.0) # Color # Check if the enter the material etaiOverEtat = (1.0 / self.refIdx) if rec.frontFace else self.refIdx unitDirection = Vec3.unitVector(rIn.direction()) cosTheta = min(Vec3.dot(unitDirection * -1, rec.normal), 1.0) sinTheta = math.sqrt(1.0 - cosTheta * cosTheta) # TIR if etaiOverEtat * sinTheta > 1.0: reflected = Vec3.reflect(unitDirection, rec.normal) scattered = Ray(rec.p, reflected) return (attenuation, scattered) # Reflection / Refraction ratio reflectProb = Dielectric.__schlick(cosTheta, etaiOverEtat) # Reflection if random.random() < reflectProb: reflected = Vec3.reflect(unitDirection, rec.normal) scattered = Ray(rec.p, reflected) return (attenuation, scattered) # Refraction refracted = Vec3.refract(unitDirection, rec.normal, etaiOverEtat) scattered = Ray(rec.p, refracted) return (attenuation, scattered)
def ray_trace(self, origin, normalized_direction): far = 1.0e15 # A large number, which we can never hit distances = [ o.intersect(origin, normalized_direction, far) for o in self.scene.objects ] nearest = reduce(jnp.minimum, distances) color = Vec3(0, 0, 0) for (o, d) in zip(self.scene.objects, distances): color += o.light( origin, normalized_direction, d, self.light_position, origin, self.scene.objects) * (nearest != far) * (d == nearest) return color
def color(r, world, depth): ''' @param {ray, hitable_list, recursion_depth} @return: ''' rec = HitRecord() if world.hit(r, 0, float('inf'), rec): # ray reflect target wrapper = Wrapper() # target = rec.p.Add(rec.normal).Add(random_in_unit_sphere()) # paint according to normal value # decay 50% every reflect if depth < 50 and rec.mat_ptr.scatter(r, rec, wrapper): return wrapper.attenuation.Mul( color(wrapper.scattered, world, depth + 1)) #return color(Ray(rec.p, target.Sub(rec.p)), world, depth+1).Scale(0.5) else: return Vec3(0.0, 0.0, 0.0) else: unit_dir = r.direction().normalize() t = 0.5 * (unit_dir.y() + 1.0) return Vec3(1.0, 1.0, 1.0).Scale(1.0 - t).Add(Vec3(0.5, 0.7, 1.0).Scale(t))
def create_scene(): obj_list = [] obj_list.append( Sphere(Vec3(0.0, 0.0, -1.0), 0.5, Lambertian(Vec3(0.1, 0.2, 0.5)))) obj_list.append(Sphere(Vec3(-1.0, 0.0, -1.0), 0.5, Deilectric(1.5))) obj_list.append( Sphere(Vec3(1.0, 0.0, -1.0), 0.5, Metal(Vec3(0.8, 0.6, 0.2), 0.2))) # Ground obj_list.append( Sphere(Vec3(0.0, -100.5, -1.0), 100.0, Lambertian(Vec3(0.5, 0.5, 0.5)))) world = Hitable_list(obj_list) return world
def writePPM(): width = 400 height = 200 ns = 100 # smaple nums lower_left = Vec3(-2.0, -2.0, -2.0) horizontal = Vec3(4.0, 0.0, 0.0) vertical = Vec3(0.0, 4.0, 0.0) origin = Vec3(0.0, 0.0, 0.0) with open('result.ppm', 'w') as f: f.write("P3\n" + str(width) + " " + str(height) + "\n255\n") index = 0 world = create_scene() camera = Camera(Vec3(-2.0, 2.0, 1.0), Vec3(0.0, 0.0, -1.0), Vec3(0.0, 1.0, 0.0), 40.0, float(width) / float(height)) with tqdm(total=height) as pbar: for j in range(height - 1, -1, -1): for i in range(0, width): col = Vec3(0, 0, 0) for s in range(0, ns): u = float(i + random.random()) / float( width) # antialiasing v = float(j + random.random()) / float(height) ray = camera.GetRay(u, v) col = col.Add(color(ray, world, 0)) col = col.Scale(1.0 / float(ns)) # average color # gamma col = Vec3(float(math.sqrt(col.x())), float(math.sqrt(col.y())), float(math.sqrt(col.z()))) index += 1 ir = int(255.59 * col.x()) ig = int(255.59 * col.y()) ib = int(255.59 * col.z()) f.write(str(ir) + " " + str(ig) + " " + str(ib) + "\n") pbar.update(1)
def render_pixel(u, v, camera, light, asset, file): c = Vec3(0, 0, 0) ray = camera.shoot_ray(u, v) intersect, i = asset.intersect(ray) if intersect: n = asset.normal(i) l = light.pos - i r = normalize(2 * n - l) l_intensity = light.strength(i) * light.color i_ambient = k_ambient * ambient i_diffuse = k_diffuse * max(dot(n, normalize(l)), 0) * l_intensity i_specular = k_specular * pow(max(dot(r, normalize(-1 * i)), 0), n_specular) * l_intensity c = mul(asset.color, i_ambient + i_diffuse + i_specular) file.write("{} {} {}\n".format(floatto8bit(c.x), floatto8bit(c.y), floatto8bit(c.z)))
def hit(self, r, tMin, tMax): a = r.direction().lengthSquared() oc = r.origin() - self.center halfB = Vec3.dot(oc, r.direction()) c = oc.lengthSquared() - self.radius * self.radius discriminant = halfB * halfB - a * c if discriminant > 0: root = sqrt(discriminant) t = (-halfB - root) / a if t < tMax and t > tMin: p = r.pointAtParameter(t) outwardNormal = (p - self.center) / self.radius return HitRecord(t, p, self.mat, r, outwardNormal) t = (-halfB + root) / a if t < tMax and t > tMin: p = r.pointAtParameter(t) outwardNormal = (p - self.center) / self.radius return HitRecord(t, p, self.mat, r, outwardNormal) return None
def main(): res = 100 # rendered image resolution camera = Camera((res, res)) sphere = Sphere(pos=Vec3(0, 2, 0), radius=0.5, color=Vec3(1, 0.2, 0.2)) light = Light(pos=Vec3(1, 0.5, 1), color=Vec3(1, 1, 0.5), strength=1, radius=1) triangle = Triangle([Vec3(0, 0, 1), Vec3(1, 0, 1), Vec3(0, 1, 1)]) asset = sphere #asset = triangle out_file = open("out.ppm", "w") out_file.write("P3\n{} {} {}\n".format(camera.res_width, camera.res_height, 255)) for u in range(camera.res_width): for v in range(camera.res_height): render_pixel_antialias(u, v, camera, light, asset, out_file, antialias_level)
if __name__ == '__main__': cam = Camera(width=400, origin=jnp.array([0, 0.05, -1.])) num_spheres = 50 scene_objects = [] min_x = -7.5 min_y = .5 min_z = 2.5 size_deviation = 0.5 max_deviation_x = 15. max_deviation_y = 10. max_deviation_z = 20. import random colorful_factor = 0.35 for i in range(1, num_spheres + 1): color = Vec3(random.random(), random.random(), random.random()) color_to_add = random.randint(0, 2) if color_to_add == 0: color.x += colorful_factor color.y -= colorful_factor color.z -= colorful_factor elif color_to_add == 1: color.x -= colorful_factor color.y += colorful_factor color.x -= colorful_factor else: color.x -= colorful_factor color.y -= colorful_factor color.x += colorful_factor x_coord = random.random() * max_deviation_x + min_x
def trans(self, v): ''' in-place transform the vector. ''' return Vec3(self.x.dot(v), self.y.dot(v), self.z.dot(v))
def randomSphereScene(): world = HitableList() # Bottom "plane" world.add(Sphere(Vec3(0,-1000,0), 1000, Lambertian(Vec3(0.5, 0.5, 0.5)))) # three large spheres world.add(Sphere(Vec3(0, 1, 0), 1.0, Dielectric(1.5))) world.add(Sphere(Vec3(-4, 1, 0), 1.0, Lambertian(Vec3(0.4, 0.2, 0.1)))) world.add(Sphere(Vec3(4, 1, 0), 1.0, Metal(Vec3(0.7, 0.6, 0.5), 0.0))) # numerous small spheres i = 1 for a in range(-11, 11): for b in range(-11, 11): chooseMat = random.random() center = Vec3(a + 0.9*random.random(), 0.2, b + 0.9*random.uniform(0,1)) if (center - Vec3(4, 0.2, 0)).length() > 0.9: if chooseMat < 0.8: # diffuse albedo = Vec3.random() * Vec3.random() world.add(Sphere(center, 0.2, Lambertian(albedo))) elif chooseMat < 0.95: # metal albedo = random.uniform(.5, 1) fuzz = random.uniform(0, .5) world.add(Sphere(center, 0.2, Metal(albedo, fuzz))) else: # glass world.add(Sphere(center, 0.2, Dielectric(1.5))) return world
def scatter(self, rIn, rec): # returns (attenuation, scattered) scatterDirection = rec.normal + Vec3.randomInUnitSphere() scattered = Ray(rec.p, scatterDirection) attenuation = self.albedo return (attenuation, scattered)
# return the translated [0,255] value of each color component. return [int(256 * clamp(r, 0.0, 0.999)), int(256 * clamp(g, 0.0, 0.999)), int(256 * clamp(b, 0.0, 0.999))] if __name__ == "__main__": aspectRatio = 16.0 / 9.0 imageWidth = 500 imageHeight = int(imageWidth / aspectRatio) samplesPerPixel = 1 maxDepth = 10 world = randomSphereScene() lookFrom = Vec3(13,2,3) lookAt = Vec3(0,0,0) vUp = Vec3(0,1,0) distToFocus = 10.0 aperture = 0.1 cam = Camera(lookFrom, lookAt, vUp, 20, aspectRatio, aperture, distToFocus) stopwatch = Stopwatch() image = imageHeight*imageWidth*[[0,0,0]] index = 0 for j in range(imageHeight): showProgress(stopwatch, j, imageHeight) for i in range(imageWidth): pixelColor = Vec3(0,0,0) for s in range(samplesPerPixel):