mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-07-04 15:03:08 +02:00
feat: wip flake formatting
This commit is contained in:
parent
732ffb38f1
commit
8ab55ab252
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-python.black-formatter",
|
||||||
|
"ms-python.autopep8",
|
||||||
|
"ms-python.pylint"
|
||||||
|
]
|
||||||
|
}
|
||||||
283
code/main.py
283
code/main.py
@ -1,37 +1,83 @@
|
|||||||
|
""" Renders an image using raytracing """
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
w = 400
|
IMAGE_WIDTH = 400
|
||||||
h = 300
|
IMAGE_HEIGHT = 300
|
||||||
|
|
||||||
|
|
||||||
def normalize(x):
|
def normalize(x):
|
||||||
|
"""
|
||||||
|
Normalize a vector.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
x (numpy.ndarray): The input vector to be normalized.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy.ndarray: The normalized vector.
|
||||||
|
"""
|
||||||
x /= np.linalg.norm(x)
|
x /= np.linalg.norm(x)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def intersect_plane(O, D, P, N):
|
|
||||||
# Return the distance from O to the intersection of the ray (O, D) with the
|
def intersect_plane(ray_origin, ray_direction, plane_point, plane_normal):
|
||||||
# plane (P, N), or +inf if there is no intersection.
|
"""
|
||||||
# O and P are 3D points, D and N (normal) are normalized vectors.
|
Calculate the intersection of a ray with a plane.
|
||||||
denom = np.dot(D, N)
|
|
||||||
|
Parameters:
|
||||||
|
ray_origin (numpy.ndarray): A 3D point representing the origin of the ray.
|
||||||
|
ray_direction (numpy.ndarray): A normalized 3D vector representing the
|
||||||
|
direction of the ray.
|
||||||
|
plane_point (numpy.ndarray): A 3D point representing a point on the plane.
|
||||||
|
plane_normal (numpy.ndarray): A normalized 3D vector representing
|
||||||
|
the normal of the plane.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The distance from the origin ray_origin to the intersection
|
||||||
|
point with the plane.
|
||||||
|
Returns +inf if there is no intersection or if the intersection is
|
||||||
|
behind the origin.
|
||||||
|
"""
|
||||||
|
denom = np.dot(ray_direction, plane_normal)
|
||||||
if np.abs(denom) < 1e-6:
|
if np.abs(denom) < 1e-6:
|
||||||
return np.inf
|
return np.inf
|
||||||
d = np.dot(P - O, N) / denom
|
d = np.dot(plane_point - ray_origin, plane_normal) / denom
|
||||||
if d < 0:
|
if d < 0:
|
||||||
return np.inf
|
return np.inf
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def intersect_sphere(O, D, S, R):
|
|
||||||
# Return the distance from O to the intersection of the ray (O, D) with the
|
def intersect_sphere(ray_origin, ray_direction, sphere_center, sphere_radius):
|
||||||
# sphere (S, R), or +inf if there is no intersection.
|
"""
|
||||||
# O and S are 3D points, D (direction) is a normalized vector, R is a scalar.
|
Calculate the intersection of a ray with a sphere.
|
||||||
a = np.dot(D, D)
|
|
||||||
OS = O - S
|
Parameters:
|
||||||
b = 2 * np.dot(D, OS)
|
ray_origin (numpy.ndarray): A 3D point representing the origin of the ray.
|
||||||
c = np.dot(OS, OS) - R * R
|
ray_direction (numpy.ndarray): A normalized 3D vector representing the
|
||||||
|
direction of the ray.
|
||||||
|
sphere_center (numpy.ndarray): A 3D point representing
|
||||||
|
the center of the sphere.
|
||||||
|
sphere_radius (float): The radius of the sphere.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The distance from the origin ray_origin to the intersection
|
||||||
|
point with the sphere.
|
||||||
|
Returns +inf if there is no intersection or if the intersection is
|
||||||
|
behind the origin.
|
||||||
|
"""
|
||||||
|
a = np.dot(ray_direction, ray_direction)
|
||||||
|
origin_to_center = ray_origin - sphere_center
|
||||||
|
b = 2 * np.dot(ray_direction, origin_to_center)
|
||||||
|
radius_squared = sphere_radius * sphere_radius
|
||||||
|
c = np.dot(origin_to_center, origin_to_center) - radius_squared
|
||||||
disc = b * b - 4 * a * c
|
disc = b * b - 4 * a * c
|
||||||
if disc > 0:
|
if disc > 0:
|
||||||
distSqrt = np.sqrt(disc)
|
distance_squared = np.sqrt(disc)
|
||||||
q = (-b - distSqrt) / 2.0 if b < 0 else (-b + distSqrt) / 2.0
|
# q is used to find the roots of the quadratic equation
|
||||||
|
if b < 0:
|
||||||
|
q = (-b - distance_squared) / 2.0
|
||||||
|
else:
|
||||||
|
q = (-b + distance_squared) / 2.0
|
||||||
t0 = q / a
|
t0 = q / a
|
||||||
t1 = c / q
|
t1 = c / q
|
||||||
t0, t1 = min(t0, t1), max(t0, t1)
|
t0, t1 = min(t0, t1), max(t0, t1)
|
||||||
@ -39,69 +85,167 @@ def intersect_sphere(O, D, S, R):
|
|||||||
return t1 if t0 < 0 else t0
|
return t1 if t0 < 0 else t0
|
||||||
return np.inf
|
return np.inf
|
||||||
|
|
||||||
def intersect(O, D, obj):
|
|
||||||
if obj['type'] == 'plane':
|
|
||||||
return intersect_plane(O, D, obj['position'], obj['normal'])
|
|
||||||
elif obj['type'] == 'sphere':
|
|
||||||
return intersect_sphere(O, D, obj['position'], obj['radius'])
|
|
||||||
|
|
||||||
def get_normal(obj, M):
|
def intersect(ray_origin, ray_direction, object_):
|
||||||
# Find normal.
|
"""
|
||||||
if obj['type'] == 'sphere':
|
Calculate the intersection of a ray with an object.
|
||||||
N = normalize(M - obj['position'])
|
|
||||||
elif obj['type'] == 'plane':
|
Parameters:
|
||||||
N = obj['normal']
|
ray_origin (numpy.ndarray): A 3D point representing the origin of the ray.
|
||||||
return N
|
ray_direction (numpy.ndarray): A normalized 3D vector representing the
|
||||||
|
direction of the ray.
|
||||||
def get_color(obj, M):
|
obj (dict): A dictionary representing the object with keys
|
||||||
color = obj['color']
|
'type', 'position', 'normal' (for planes), and 'radius' (for spheres).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The distance from the origin ray_origin to the intersection
|
||||||
|
point with the object.
|
||||||
|
Returns +inf if there is no intersection or if the intersection is
|
||||||
|
behind the origin.
|
||||||
|
"""
|
||||||
|
if object_['type'] == 'plane':
|
||||||
|
return intersect_plane(ray_origin, ray_direction,
|
||||||
|
object_['position'], object_['normal'])
|
||||||
|
elif object_['type'] == 'sphere':
|
||||||
|
return intersect_sphere(ray_origin, ray_direction,
|
||||||
|
object_['position'], object_['radius'])
|
||||||
|
|
||||||
|
|
||||||
|
def get_normal(object_, intersection_point):
|
||||||
|
"""
|
||||||
|
Calculate the normal at the intersection point on the object.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj (dict): A dictionary representing the object with keys
|
||||||
|
'type' and 'position'.
|
||||||
|
intersection_point (numpy.ndarray): A 3D point representing the
|
||||||
|
intersection point on the object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy.ndarray: The normal vector at the intersection point.
|
||||||
|
"""
|
||||||
|
if object_['type'] == 'sphere':
|
||||||
|
normal = normalize(intersection_point - object_['position'])
|
||||||
|
elif object_['type'] == 'plane':
|
||||||
|
normal = object_['normal']
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown object type: {object_['type']}")
|
||||||
|
return normal
|
||||||
|
|
||||||
|
|
||||||
|
def get_color(object_, intersection_point):
|
||||||
|
"""
|
||||||
|
Get the color of the object at the intersection point.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
object_ (dict): A dictionary representing the object with a key 'color'.
|
||||||
|
intersection_point (numpy.ndarray): A 3D point representing the
|
||||||
|
intersection point on the object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy.ndarray: The color of the object at the intersection point.
|
||||||
|
"""
|
||||||
|
color = object_['color']
|
||||||
if not hasattr(color, '__len__'):
|
if not hasattr(color, '__len__'):
|
||||||
color = color(M)
|
color = color(intersection_point)
|
||||||
return color
|
return color
|
||||||
|
|
||||||
def trace_ray(rayO, rayD):
|
|
||||||
|
def trace_ray(ray_origin, ray_direction):
|
||||||
|
"""
|
||||||
|
Trace a ray and find the color at the intersection point.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ray_origin (numpy.ndarray): A 3D point representing the origin of the ray.
|
||||||
|
ray_direction (numpy.ndarray):
|
||||||
|
A normalized 3D vector representing the direction of the ray.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing the object,
|
||||||
|
intersection point, normal at the intersection,
|
||||||
|
and the color at the intersection point.
|
||||||
|
Returns None if there is no intersection.
|
||||||
|
"""
|
||||||
# Find first point of intersection with the scene.
|
# Find first point of intersection with the scene.
|
||||||
t = np.inf
|
t = np.inf
|
||||||
for i, obj in enumerate(scene):
|
obj_idx = -1
|
||||||
t_obj = intersect(rayO, rayD, obj)
|
for index, object_ in enumerate(scene):
|
||||||
|
t_obj = intersect(ray_origin, ray_direction, object_)
|
||||||
if t_obj < t:
|
if t_obj < t:
|
||||||
t, obj_idx = t_obj, i
|
t, obj_idx = t_obj, index
|
||||||
# Return None if the ray does not intersect any object.
|
# Return None if the ray does not intersect any object.
|
||||||
if t == np.inf:
|
if t == np.inf:
|
||||||
return
|
return
|
||||||
# Find the object.
|
# Find the object.
|
||||||
obj = scene[obj_idx]
|
object_ = scene[obj_idx]
|
||||||
# Find the point of intersection on the object.
|
# Find the point of intersection on the object.
|
||||||
M = rayO + rayD * t
|
intersection_point = ray_origin + ray_direction * t
|
||||||
# Find properties of the object.
|
# Find properties of the object.
|
||||||
N = get_normal(obj, M)
|
normal = get_normal(object_, intersection_point)
|
||||||
color = get_color(obj, M)
|
color = get_color(object_, intersection_point)
|
||||||
toL = normalize(L - M)
|
to_light = normalize(L - intersection_point)
|
||||||
toO = normalize(O - M)
|
to_origin = normalize(O - intersection_point)
|
||||||
# Shadow: find if the point is shadowed or not.
|
# Shadow: find if the point is shadowed or not.
|
||||||
l = [intersect(M + N * .0001, toL, obj_sh)
|
shadow_intersections = [intersect( intersection_point + normal * .0001,
|
||||||
for k, obj_sh in enumerate(scene) if k != obj_idx]
|
to_light,
|
||||||
if l and min(l) < np.inf:
|
obj_sh
|
||||||
|
)
|
||||||
|
for k, obj_sh in enumerate(scene) if k != obj_idx]
|
||||||
|
if shadow_intersections and min(shadow_intersections) < np.inf:
|
||||||
return
|
return
|
||||||
# Start computing the color.
|
# Start computing the color.
|
||||||
col_ray = ambient
|
color_ray = ambient
|
||||||
# Lambert shading (diffuse).
|
# Lambert shading (diffuse).
|
||||||
col_ray += obj.get('diffuse_c', diffuse_c) * max(np.dot(N, toL), 0) * color
|
diffuse_intensity = object_.get('diffuse_c', diffuse_c) * max(
|
||||||
|
np.dot(normal, to_light), 0)
|
||||||
|
color_ray += diffuse_intensity * color
|
||||||
|
|
||||||
# Blinn-Phong shading (specular).
|
# Blinn-Phong shading (specular).
|
||||||
col_ray += obj.get('specular_c', specular_c) * max(np.dot(N, normalize(toL + toO)), 0) ** specular_k * color_light
|
half_vector = normalize(to_light + to_origin)
|
||||||
return obj, M, N, col_ray
|
specular_intensity = object_.get('specular_c', specular_c) * max(
|
||||||
|
np.dot(normal, half_vector), 0) ** specular_k
|
||||||
|
color_ray += specular_intensity * color_light
|
||||||
|
return object_, intersection_point, normal, color_ray
|
||||||
|
|
||||||
|
|
||||||
def add_sphere(position, radius, color):
|
def add_sphere(position, radius, color):
|
||||||
return dict(type='sphere', position=np.array(position),
|
"""
|
||||||
radius=np.array(radius), color=np.array(color), reflection=.5)
|
Create a dictionary representing a sphere object.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
position (list or numpy.ndarray):
|
||||||
|
A 3D point representing the position of the sphere.
|
||||||
|
radius (float): The radius of the sphere.
|
||||||
|
color (list or numpy.ndarray): The color of the sphere.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary representing the sphere object.
|
||||||
|
"""
|
||||||
|
return dict(type='sphere', position=np.array(position),
|
||||||
|
radius=np.array(radius), color=np.array(color), reflection=.5)
|
||||||
|
|
||||||
|
|
||||||
def add_plane(position, normal):
|
def add_plane(position, normal):
|
||||||
return dict(type='plane', position=np.array(position),
|
"""
|
||||||
normal=np.array(normal),
|
Create a dictionary representing a plane object.
|
||||||
color=lambda M: (color_plane0
|
|
||||||
if (int(M[0] * 2) % 2) == (int(M[2] * 2) % 2) else color_plane1),
|
Parameters:
|
||||||
diffuse_c=.75, specular_c=.5, reflection=.25)
|
position (list or numpy.ndarray):
|
||||||
|
A 3D point representing a point on the plane.
|
||||||
|
normal (list or numpy.ndarray):
|
||||||
|
A normalized 3D vector representing the normal of the plane.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary representing the plane object.
|
||||||
|
"""
|
||||||
|
return dict(type='plane', position=np.array(position),
|
||||||
|
normal=np.array(normal),
|
||||||
|
color=lambda M: (color_plane0
|
||||||
|
if (int(M[0] * 2) % 2) == (int(M[2] * 2) % 2)
|
||||||
|
else color_plane1),
|
||||||
|
diffuse_c=.75, specular_c=.5, reflection=.25)
|
||||||
|
|
||||||
|
|
||||||
# List of objects.
|
# List of objects.
|
||||||
color_plane0 = 1. * np.ones(3)
|
color_plane0 = 1. * np.ones(3)
|
||||||
color_plane1 = 0. * np.ones(3)
|
color_plane1 = 0. * np.ones(3)
|
||||||
@ -109,7 +253,7 @@ scene = [add_sphere([.75, .1, 1.], .6, [1., 0., 0.]),
|
|||||||
add_sphere([-.75, .1, 2.25], .6, [0., 1., 0.]),
|
add_sphere([-.75, .1, 2.25], .6, [0., 1., 0.]),
|
||||||
add_sphere([-2.75, .1, 3.5], .6, [0., 0., 1.]),
|
add_sphere([-2.75, .1, 3.5], .6, [0., 0., 1.]),
|
||||||
add_plane([0., -.5, 0.], [0., 1., 0.]),
|
add_plane([0., -.5, 0.], [0., 1., 0.]),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Light position and color.
|
# Light position and color.
|
||||||
L = np.array([5., 5., -10.])
|
L = np.array([5., 5., -10.])
|
||||||
@ -125,17 +269,17 @@ depth_max = 5 # Maximum number of light reflections.
|
|||||||
col = np.zeros(3) # Current color.
|
col = np.zeros(3) # Current color.
|
||||||
O = np.array([0., 0.35, -1.]) # Camera.
|
O = np.array([0., 0.35, -1.]) # Camera.
|
||||||
Q = np.array([0., 0., 0.]) # Camera pointing to.
|
Q = np.array([0., 0., 0.]) # Camera pointing to.
|
||||||
img = np.zeros((h, w, 3))
|
img = np.zeros((IMAGE_HEIGHT, IMAGE_WIDTH, 3))
|
||||||
|
|
||||||
r = float(w) / h
|
r = float(IMAGE_WIDTH) / IMAGE_HEIGHT
|
||||||
# Screen coordinates: x0, y0, x1, y1.
|
# Screen coordinates: x0, y0, x1, y1.
|
||||||
S = (-1., -1. / r + .25, 1., 1. / r + .25)
|
S = (-1., -1. / r + .25, 1., 1. / r + .25)
|
||||||
|
|
||||||
# Loop through all pixels.
|
# Loop through all pixels.
|
||||||
for i, x in enumerate(np.linspace(S[0], S[2], w)):
|
for i, x in enumerate(np.linspace(S[0], S[2], IMAGE_WIDTH)):
|
||||||
if i % 10 == 0:
|
if i % 10 == 0:
|
||||||
print(i / float(w) * 100, "%")
|
print(i / float(IMAGE_WIDTH) * 100, "%")
|
||||||
for j, y in enumerate(np.linspace(S[1], S[3], h)):
|
for j, y in enumerate(np.linspace(S[1], S[3], IMAGE_HEIGHT)):
|
||||||
col[:] = 0
|
col[:] = 0
|
||||||
Q[:2] = (x, y)
|
Q[:2] = (x, y)
|
||||||
D = normalize(Q - O)
|
D = normalize(Q - O)
|
||||||
@ -149,10 +293,11 @@ for i, x in enumerate(np.linspace(S[0], S[2], w)):
|
|||||||
break
|
break
|
||||||
obj, M, N, col_ray = traced
|
obj, M, N, col_ray = traced
|
||||||
# Reflection: create a new ray.
|
# Reflection: create a new ray.
|
||||||
rayO, rayD = M + N * .0001, normalize(rayD - 2 * np.dot(rayD, N) * N)
|
rayO, rayD = M + \
|
||||||
|
N * .0001, normalize(rayD - 2 * np.dot(rayD, N) * N)
|
||||||
depth += 1
|
depth += 1
|
||||||
col += reflection * col_ray
|
col += reflection * col_ray
|
||||||
reflection *= obj.get('reflection', 1.)
|
reflection *= obj.get('reflection', 1.)
|
||||||
img[h - j - 1, i, :] = np.clip(col, 0, 1)
|
img[IMAGE_HEIGHT - j - 1, i, :] = np.clip(col, 0, 1)
|
||||||
|
|
||||||
plt.imsave('fig.png', img)
|
plt.imsave('fig.png', img)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user