我一直致力于使用矢量、光线、材质、灯光、球体、场景和渲染引擎的类在 Python 中实现基本的光线追踪渲染器。但是,我得到的渲染图像与我的预期不符,我无法确定问题所在。
from math import sqrt,pi
from PIL import Image as img2ppm
class Vector:
def __init__(self, x=0.0, y=0.0, z=0.0):
self.x = x
self.y = y
self.z = z
def __str__(self):
return "({}, {}, {})".format(self.x, self.y, self.z)
def dot_product(self, other):
return self.x * other.x + self.y * other.y + self.z * other.z
def magnitude(self):
return sqrt(self.dot_product(self))
def normalize(self):
return self / self.magnitude()
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(self, other):
assert not isinstance(other, Vector)
return Vector(self.x * other, self.y * other, self.z * other)
def __rmul__(self, other):
return self.__mul__(other)
def __truediv__(self, other):
assert not isinstance(other, Vector)
return Vector(self.x / other, self.y / other, self.z / other)
def from_hex(hexcolor):
return Vector(int(hexcolor[1:3], 16) / 255.0, int(hexcolor[3:5], 16) / 255.0, int(hexcolor[5:7], 16) / 255.0)
class Image:
def __init__(self, width, height):
self.width = width
self.height = height
self.pixels = [[None for _ in range(width)] for _ in range(height)]
def set_pixel(self, x, y, col):
self.pixels[y][x] = col
def write_ppm(self, img_fileobj):
Image.write_ppm_header(img_fileobj, height=self.height, width=self.width)
self.write_ppm_raw(img_fileobj)
def write_ppm_header(img_fileobj, height=None, width=None):
img_fileobj.write("P3 {} {}\n255\n".format(width, height))
def write_ppm_raw(self, img_fileobj):
def to_byte(c):
return round(max(min(c * 255, 255), 0))
for row in self.pixels:
for color in row:
img_fileobj.write("{} {} {} ".format(to_byte(color.x), to_byte(color.y), to_byte(color.z)))
img_fileobj.write("\n")
class Ray:
def __init__(self, origin, direction):
self.origin = origin
self.direction = direction.normalize()
class RenderEngine:
MAX_DEPTH = 5
MIN_DISPLACE = 0.0001
def render(self, scene, img_fileobj):
width = scene.width
height = scene.height
x0 = -1.0
x1 = +1.0
xstep = (x1 - x0) / (width - 1)
aspect_ratio = float(width) / height
y0 = -1.0 / aspect_ratio
y1 = +1.0 / aspect_ratio
ystep = (y1 - y0) / (height - 1)
camera = scene.camera
pixels = Image(width, height)
for j in range(height):
y = y0 + j * ystep
for i in range(width):
x = x0 + i * xstep
ray = Ray(camera, Vector(x, y) - camera)
pixels.set_pixel(i, j, self.ray_trace(ray, scene))
pixels.write_ppm(img_fileobj)
def ray_trace(self, ray, scene, depth=0):
color = Vector(0, 0, 0)
dist_hit, obj_hit = self.find_nearest(ray, scene)
if obj_hit is None:
return color
hit_pos = ray.origin + ray.direction * dist_hit
hit_normal = obj_hit.normal(hit_pos)
color += self.color_at(obj_hit, hit_pos, hit_normal, scene)
if depth < self.MAX_DEPTH:
new_ray_pos = hit_pos + hit_normal * self.MIN_DISPLACE
new_ray_dir = (ray.direction - 2 * ray.direction.dot_product(hit_normal) * hit_normal)
new_ray = Ray(new_ray_pos, new_ray_dir)
color += (self.ray_trace(new_ray, scene, depth + 1) * obj_hit.material.reflection)
return color
def find_nearest(self, ray, scene):
dist_min = None
obj_hit = None
for obj in scene.objects:
dist = obj.intersects(ray)
if dist is not None and (obj_hit is None or dist < dist_min):
dist_min = dist
obj_hit = obj
return (dist_min, obj_hit)
def color_at(self, obj_hit, hit_pos, normal, scene):
material = obj_hit.material
obj_color = material.color_at(hit_pos)
to_cam = scene.camera - hit_pos
specular_k = 50
color = material.ambient * from_hex("#FFFFFF")
for light in scene.lights:
to_light = Ray(hit_pos, light.position - hit_pos)
color += (obj_color* material.diffuse* max(normal.dot_product(to_light.direction), 0))
half_vector = (to_light.direction + to_cam).normalize()
color += (light.color* material.specular* max(normal.dot_product(half_vector), 0) ** specular_k)
return color
class Light:
def __init__(self, position, color=from_hex("#FFFFFF")):
self.position = position
self.color = color
class Material:
def __init__(
self,color=from_hex("#FFFFFF"),ambient=0.05,diffuse=1.0,specular=1.0,reflection=0.5,):
self.color = color
self.ambient = ambient
self.diffuse = diffuse
self.specular = specular
self.reflection = reflection
def color_at(self, position):
return self.color
class Sphere:
def __init__(self, center, radius, material):
self.center = center
self.radius = radius
self.material = material
def intersects(self, ray):
sphere_to_ray = ray.origin - self.center
# a = 1
b = 2 * ray.direction.dot_product(sphere_to_ray)
c = sphere_to_ray.dot_product(sphere_to_ray) - self.radius * self.radius
discriminant = b * b - 4 * c
if discriminant >= 0:
dist = (-b - sqrt(discriminant)) / 2
if dist > 0:
return dist
return None
def normal(self, surface_point):
return (surface_point - self.center).normalize()
class ChequeredMaterial:
def __init__(
self,color1=from_hex("#FFFFFF"),color2=from_hex("#000000"),ambient=0.05,diffuse=1.0,specular=1.0,reflection=0.5,):
self.color1 = color1
self.color2 = color2
self.ambient = ambient
self.diffuse = diffuse
self.specular = specular
self.reflection = reflection
def color_at(self, position):
if int((position.x + 5.0) * 3.0) % 2 == int(position.z * 3.0) % 2:
return self.color1
else:
return self.color2
class Scene:
def __init__(self, camera, objects, lights, width, height):
self.camera = camera
self.objects = objects
self.lights = lights
self.width = width
self.height = height
CAMERA = Vector(0, -0.35, -1)
OBJECTS = [Sphere(Vector(0, 10000.5, 1),10000.0,ChequeredMaterial(color1=from_hex("#420500"),color2=from_hex("#e6b87d"),ambient=0.2,reflection=0.2,),),Sphere(Vector(0.75, -0.1, 1), 0.6, Material(from_hex("#0000FF"))),Sphere(Vector(-0.75, -0.1, 2.25), 0.6, Material(from_hex("#803980"))),]
LIGHTS = [Light(Vector(1.5, -0.5, -10), from_hex("#FFFFFF")),Light(Vector(-0.5, -10.5, 0), from_hex("#E6E6E6")),]
engine = RenderEngine()
with open(r'D:\puray-master\image.ppm', "w") as img_fileobj:
engine.render(Scene(CAMERA,objects=OBJECTS,lights=LIGHTS,width=300,height=300), img_fileobj)
img2ppm.open(r'D:\puray-master\image.ppm').show()