Coding shadows
Here's a small project I wrote in a few hours. It's a Python3 program that calculates and displays shadows based on information read from a JSON file.
Firstly, the user is presented with a list of files ending in .json in the current directory. They must enter a valid filename to proceed.
The JSON file is parsed. It contains the resolution of the image, the location of the light in the scene and the location and radius of the circles (which cast shadows) in the image. Each light and shape in the scene instantiate a new object from an appropriate class, with the afforementioned data stored in the object's attributes. These objects are appended to an array.
The program then iterates through each light in the lights array and each pixel in the x and y axis (from 0 to the total width and height of the image). It determines if the path from the light source to the pixel in question is obstructed by a shape.
To accomplish this, the program determines the equation of the straight line from the light source to the pixel ie y=mx+c. It then iterates through each x value between both locations and calculates the corresponding y value based on the equation of the line. If the calculated (x,y) value is closer in distance to the center of any shape than the shape's radius, then there is an intersection and the line would broken by a shape -- the pixel is in shadow.
The distance from the pixel to the light source is then calculated and an inverse proportionality formula used to find the intensity of the shadowed pixel.
Here's some example scenes:
Although it is functional, there are a number of issues with this algorithm.
Firstly, going through every pixel in the image one-by-one is horribly inefficient. For a 200x200 image with one light and two shapes, it took as long as 6 seconds to calculate and render the final result. To improve this, I could employ heuristic rules to dramatically reduce the number of pixels that would need to be looked at (using information about surrounding pixels to decide whether it is necessary to determine if a particular pixel is in shadow).
Secondly, it struggles if a shape is at an angle to the light source that is close to 0 degrees. This is likely due to the fact that the line equation between the two pixels produces y values that require a more precise grid.
Overall, this project was successful but has taught me a great deal about how complicated and challenging it would be to write real-time graphics engines that run at 60Hz in 3D instead of 0.16Hz in 2D!
Source code:
from PIL import Image, ImageDraw
import os, json
class shape():
def __init__(self):
self.type = None
self.center = []
self.radius = None
class light():
def __init__(self):
self.location = []
def startup():
availableFiles = os.listdir()
print("Available files:")
for file in availableFiles:
if ".json" in file:
print(file)
while fileName == "":
fileName = input("\nEnter filename: ")
if ".json" not in fileName:
fileName += ".json"
if fileName not in availableFiles:
print("Invalid filename")
fileName = ""
with open(fileName, "r") as file:
data = json.load(file)
shapes = []
lights = []
for item in data["shapes"]:
tempObject = shape()
tempObject.type = data["shapes"][item]["type"]
tempObject.center = data["shapes"][item]["center"]
tempObject.radius = data["shapes"][item]["radius"]
shapes.append(tempObject)
for item in data["lights"]:
tempObject = light()
tempObject.location = data["lights"][item]["location"]
lights.append(tempObject)
resolution = data["metadata"]["resolution"]
return resolution, shapes, lights
def intersection(point, shapes):
for shape in shapes:
if shape.type == "circle":
if pythag(shape.center, point) < shape.radius:
return True
def tracing(l1, l2, shapes, resolution):
if l1 == l2:
return False
elif l2[0] - l1[0] == 0:
for y in range(l1[1], l2[1]):
if intersection((l1[0], y), shapes):
return False
elif l2[1] - l1[1] == 0:
for x in range(l1[0], l2[0]):
if intersection((x, l1[1]), shapes):
return False
else:
dydx = (l2[1] - l1[1])/(l2[0] - l1[0])
yintercept = l1[1] - (dydx * l1[0])
if l2[0] < l1[0]:
t0, t1 = l2[0], l1[0]
else:
t0, t1 = l1[0], l2[0]
for x in range(t0, t1):
y = (dydx * x) + yintercept
if y < 0 or y > resolution[1]:
pass
elif intersection((x, y), shapes):
return False
return True
def pythag(l1, l2):
return ((l2[0] - l1[0])**2 + (l2[1] - l1[1])**2)**0.5
def main():
resolution, shapes, lights = startup()
im = Image.new("RGB", resolution, "white")
draw = ImageDraw.Draw(im)
for source in lights:
for x in range(0, resolution[0]):
for y in range(0, resolution[1]):
if not tracing(source.location, (x, y), shapes, resolution):
intensity = 10000 * (1/pythag(source.location, (x, y)))
if intensity > 100:
intensity = 100
else:
intensity = 100 - intensity
draw.point((x, y), fill = "hsl(0, 0%, {0}%)".format(intensity))
centerX, centerY, radius = source.location[0], source.location[1], 5
draw.ellipse((centerX - radius, centerY - radius, centerX + radius, centerY + radius), fill = "blue")
for item in shapes:
centerX, centerY, radius = item.center[0], item.center[1], item.radius
draw.ellipse((centerX - radius, centerY - radius, centerX + radius, centerY + radius), fill = "red")
im.show()
main()
Firstly, the user is presented with a list of files ending in .json in the current directory. They must enter a valid filename to proceed.
The JSON file is parsed. It contains the resolution of the image, the location of the light in the scene and the location and radius of the circles (which cast shadows) in the image. Each light and shape in the scene instantiate a new object from an appropriate class, with the afforementioned data stored in the object's attributes. These objects are appended to an array.
The program then iterates through each light in the lights array and each pixel in the x and y axis (from 0 to the total width and height of the image). It determines if the path from the light source to the pixel in question is obstructed by a shape.
To accomplish this, the program determines the equation of the straight line from the light source to the pixel ie y=mx+c. It then iterates through each x value between both locations and calculates the corresponding y value based on the equation of the line. If the calculated (x,y) value is closer in distance to the center of any shape than the shape's radius, then there is an intersection and the line would broken by a shape -- the pixel is in shadow.
The distance from the pixel to the light source is then calculated and an inverse proportionality formula used to find the intensity of the shadowed pixel.
Here's some example scenes:
![]() |
| Two circles |
![]() |
| Light changes position |
![]() |
| Smaller circle behind larger circle |
![]() |
| Large circle behind smaller circle |
Although it is functional, there are a number of issues with this algorithm.
Firstly, going through every pixel in the image one-by-one is horribly inefficient. For a 200x200 image with one light and two shapes, it took as long as 6 seconds to calculate and render the final result. To improve this, I could employ heuristic rules to dramatically reduce the number of pixels that would need to be looked at (using information about surrounding pixels to decide whether it is necessary to determine if a particular pixel is in shadow).
Secondly, it struggles if a shape is at an angle to the light source that is close to 0 degrees. This is likely due to the fact that the line equation between the two pixels produces y values that require a more precise grid.
![]() | |
| Unusual effects |
Overall, this project was successful but has taught me a great deal about how complicated and challenging it would be to write real-time graphics engines that run at 60Hz in 3D instead of 0.16Hz in 2D!
Source code:
from PIL import Image, ImageDraw
import os, json
class shape():
def __init__(self):
self.type = None
self.center = []
self.radius = None
class light():
def __init__(self):
self.location = []
def startup():
availableFiles = os.listdir()
print("Available files:")
for file in availableFiles:
if ".json" in file:
print(file)
while fileName == "":
fileName = input("\nEnter filename: ")
if ".json" not in fileName:
fileName += ".json"
if fileName not in availableFiles:
print("Invalid filename")
fileName = ""
with open(fileName, "r") as file:
data = json.load(file)
shapes = []
lights = []
for item in data["shapes"]:
tempObject = shape()
tempObject.type = data["shapes"][item]["type"]
tempObject.center = data["shapes"][item]["center"]
tempObject.radius = data["shapes"][item]["radius"]
shapes.append(tempObject)
for item in data["lights"]:
tempObject = light()
tempObject.location = data["lights"][item]["location"]
lights.append(tempObject)
resolution = data["metadata"]["resolution"]
return resolution, shapes, lights
def intersection(point, shapes):
for shape in shapes:
if shape.type == "circle":
if pythag(shape.center, point) < shape.radius:
return True
def tracing(l1, l2, shapes, resolution):
if l1 == l2:
return False
elif l2[0] - l1[0] == 0:
for y in range(l1[1], l2[1]):
if intersection((l1[0], y), shapes):
return False
elif l2[1] - l1[1] == 0:
for x in range(l1[0], l2[0]):
if intersection((x, l1[1]), shapes):
return False
else:
dydx = (l2[1] - l1[1])/(l2[0] - l1[0])
yintercept = l1[1] - (dydx * l1[0])
if l2[0] < l1[0]:
t0, t1 = l2[0], l1[0]
else:
t0, t1 = l1[0], l2[0]
for x in range(t0, t1):
y = (dydx * x) + yintercept
if y < 0 or y > resolution[1]:
pass
elif intersection((x, y), shapes):
return False
return True
def pythag(l1, l2):
return ((l2[0] - l1[0])**2 + (l2[1] - l1[1])**2)**0.5
def main():
resolution, shapes, lights = startup()
im = Image.new("RGB", resolution, "white")
draw = ImageDraw.Draw(im)
for source in lights:
for x in range(0, resolution[0]):
for y in range(0, resolution[1]):
if not tracing(source.location, (x, y), shapes, resolution):
intensity = 10000 * (1/pythag(source.location, (x, y)))
if intensity > 100:
intensity = 100
else:
intensity = 100 - intensity
draw.point((x, y), fill = "hsl(0, 0%, {0}%)".format(intensity))
centerX, centerY, radius = source.location[0], source.location[1], 5
draw.ellipse((centerX - radius, centerY - radius, centerX + radius, centerY + radius), fill = "blue")
for item in shapes:
centerX, centerY, radius = item.center[0], item.center[1], item.radius
draw.ellipse((centerX - radius, centerY - radius, centerX + radius, centerY + radius), fill = "red")
im.show()
main()





Comments
Post a Comment