#! /usr/bin/python
# a rewrite of my pygame port of
# youtube.com/watch?v=N8elxpSu9pw
# which is by Bisqwit
# -theinternetftw
import pygame, math, random, sys
def vMul(a,b): return [ a[0]*b[0], a[1]*b[1], a[2]*b[2] ]
def vMulD(a,b): return [ a[0]*b, a[1]*b, a[2]*b ]
def vAdd(a,b): return [ a[0]+b[0], a[1]+b[1], a[2]+b[2] ]
def vAddD(a,b): return [ a[0]+b, a[1]+b, a[2]+b ]
def vSub(a,b): return [ a[0]-b[0], a[1]-b[1], a[2]-b[2] ]
def vSubD(a,b): return [ a[0]-b, a[1]-b, a[2]-b ]
def vNeg(a): return [-a[0], -a[1], -a[2] ]
def vPow(a,b): return [ a[0]**b, a[1]**b, a[2]**b ]
def vDot(a,b): return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
def vSqr(a): return vDot(a,a)
def vLen(a): return math.sqrt(vSqr(a))
def vNorm(a): return vMulD(a, 1.0/vLen(a))
def vMirrorAround(a, axis):
n = vNorm(axis)
v = vDot(a,n)
return vSub(vMulD(n,v+v), a)
# for color vectors (rgb)
def vLuma(a): return a[0]*0.299 + a[1]*0.587 + a[2]*0.114
def vClamp(a):
for i in range(3):
if a[i] < 0.0: a[i] = 0.0
elif a[i] > 1.0: a[i] = 1.0
return a
def vClampWithDesaturation(a):
# if the color represented by this triplet
# is too bright or too dim, decrease the saturation
# as much as required, while keeping the luma unmodified
l = vLuma(a)
if l > 1.0: return [1.0, 1.0, 1.0]
elif l < 0.0: return [0.0, 0.0, 0.0]
# if any component is over the bounds,
# calculate how much the saturation must
# be reduced to achieve an in-bounds value.
# Since the luma was verified to be in 0..1,
# a maximum reduction of saturation to 0% will
# always produce an in-bounds value, but usually
# such a drastic reduction is not necessary.
# Because we're only doing relative modifications,
# we don't need the original saturation level of the
# pixel.
sat = 1.0
for c in a:
if c > 1.0: sat = min(sat, (l-1.0) / (l-c))
elif c < 0.0: sat = min(sat, l / (l-c))
if sat != 1.0:
a = vAddD(vMulD(vSubD(a,l), sat),l)
a = vClamp(a)
return a
def vGetRotMatrix(ang):
cx,cy,cz = math.cos(ang[0]),math.cos(ang[1]),math.cos(ang[2])
sx,sy,sz = math.sin(ang[0]),math.sin(ang[1]),math.sin(ang[2])
sxsz,cxsz = sx*sz,cx*sz
cxcz,sxcz = cx*cz,sx*cz
return [ [cy*cz, cy*sz, -sy ],
[sxcz*sy - cxsz, sxsz*sy + cxcz, sx*cy],
[cxcz*sy + sxsz, cxsz*sy - sxcz, cx*cy] ]
def mTransform(m, vec):
return [ vDot(m[0], vec), vDot(m[1], vec), vDot(m[2], vec) ]
class Plane:
def __init__(self, norm, off):
self.normal = norm
self.offset = off
# declare six planes, each looks
# towards the origin and is 30 units away
planes = [ Plane( [0.0,0.0,-1.0], -30),
Plane( [0.0,1.0,0.0], -30),
Plane( [0.0,-1.0,0.0], -30),
Plane( [1.0,0.0,0.0], -30),
Plane( [0.0,0.0,1.0], -30),
Plane( [-1.0,0.0,0.0], -30) ]
class Sphere:
def __init__(self, c, r):
self.center = c
self.radius = r
# declare a few spheres
spheres = [ Sphere( [0.0,0.0,0.0], 7.0),
Sphere( [19.4, -19.4, 0], 2.1),
Sphere( [-19.4, 19.4, 0], 2.1),
Sphere( [13.1, 5.1, 0.0], 1.1),
Sphere( [-5.1, -13.1, 0], 1.1),
Sphere( [-30.0,-30.0,15.0], 11.0),
Sphere( [15.0, -30.0,30.0], 6.0),
Sphere( [30.0, 15.0, -30.0],6.0) ]
class LightSource:
def __init__(self, w, c):
self.where = w
self.color = c
#declare lightsources, each w/ a loc and a rgb color
lights = [ LightSource( [-28.0,-14.0, 4.0], [ .4,.51, .9] ),
LightSource( [-29.0,-29.0,-29.0], [.95, .1, .1] ),
LightSource( [ 14.0, 29.0,-14.0], [ .8, .8, .8] ),
LightSource( [ 29.0, 29.0, 29.0], [1.0,1.0,1.0] ),
LightSource( [ 28.0, 0.0, 29.0], [ .5, .6, .1] ) ]
numPlanes = len(planes)
numSpheres = len(spheres)
numLights = len(lights)
MAXTRACE = 6
def rayFindObstacle(eye, dir, hitDist):
#try to intersect ray w/ each object, see which gives the closest hit
hitType = -1
hitIndex = -1
hitLoc = [0.0,0.0,0.0]
hitNormal = [0.0,0.0,0.0]
for i in range(numSpheres):
v = vSub(eye, spheres[i].center)
r = spheres[i].radius
dv = vDot(dir,v)
d2 = vSqr(dir)
sq = dv*dv - d2*(vSqr(v) - r*r)
#does the ray coincide w/ the sphere?
if (sq < 1e-6):
continue
#if so, where?
sqt = math.sqrt(sq)
dist = min(-dv-sqt, -dv+sqt) / d2
if dist < 1e-6 or dist >= hitDist:
continue
hitType = 1
hitIndex = i
hitDist = dist
hitLoc = vAdd(eye, vMulD(dir,hitDist))
hitNormal = vMulD(vSub(hitLoc,spheres[i].center), 1/r)
for i in range(numPlanes):
dv = -vDot(planes[i].normal,dir)
if dv > -1e-6:
continue
d2 = vDot(planes[i].normal,eye)
dist = (d2 + planes[i].offset) / dv
if dist < 1e-6 or dist >= hitDist:
continue
hitType = 0
hitIndex = i
hitDist = dist
hitLoc = vAdd(eye, vMulD(dir,hitDist))
hitNormal = vNeg(planes[i].normal)
return hitType, hitIndex, hitDist, hitLoc, hitNormal
numArealightVectors = 20
arealightVectors = []
def initArealightVectors():
for i in range(numArealightVectors):
temp = []
for i in range(3):
temp.append(2.0*(random.random() - 0.5))
arealightVectors.append( [temp[0],temp[1],temp[2]] )
def rayTrace(eye, dir, k):
hitDist = 1e6
hitType, hitIndex, hitDist, hitLoc, hitNormal = rayFindObstacle(eye,dir,hitDist)
if hitType != -1:
# Found an obstacle. Next, find out how it is illuminated.
# Shoot a ray to each lightsource, and determine if there
# is an obstacle behind it. This is called "diffuse light".
# To smooth out the infinitely sharp shadows caused by
# infinitely small point-lightsources, assume the lightsource
# is actually a cloud of small lightsources around its center.
diffuseLight = [0.0,0.0,0.0]
specularLight = [0.0,0.0,0.0]
pigment = [1.0, .98, .94] #default pigment
for i in range(numLights):
for j in range(numArealightVectors):
v = vSub(vAdd(lights[i].where,arealightVectors[j]),hitLoc)
lightDist = vLen(v)
v = vNorm(v)
diffuseEffect = vDot(hitNormal,v) / (numArealightVectors*1.0)
attenuation = (1 + ((lightDist/34.0)**2.0))
diffuseEffect /= attenuation
if diffuseEffect > 1e-3:
shadowDist = lightDist - 1e-4
t,hi,hd,hl,hn = rayFindObstacle(vAdd(hitLoc,vMulD(v,1e-4)),v,shadowDist)
if t == -1: #no obstacle occluding the light
diffuseLight = vAdd(diffuseLight,vMulD(lights[i].color,diffuseEffect))
if k > 1:
# add specular light/reflection, unless recursion depth is maxed
v = vNeg(dir)
v = vMirrorAround(v, hitNormal)
specularLight = rayTrace(vAdd(hitLoc, vMulD(v,1e-4)),v,k-1)
if hitType == 0: #plane
diffuseLight = vMulD(diffuseLight,0.9)
specularLight = vMulD(specularLight,0.5)
# color the different walls differently
idx = hitIndex % 3
if idx == 0: pigment = [0.9,0.7,0.6]
elif idx == 1: pigment = [0.6,0.7,0.7]
elif idx == 2: pigment = [0.5,0.8,0.3]
elif hitType == 1: #sphere
diffuseLight = vMulD(diffuseLight,1.0)
specularLight = vMulD(specularLight, 0.34)
return vMul(vAdd(diffuseLight,specularLight),pigment)
#didn't hit anything, return black
return [0.0,0.0,0.0]
def main():
pygame.display.init()
screenw, screenh = 640, 480
w, h = int(sys.argv[1]),int(sys.argv[2])
screen = pygame.display.set_mode((screenw, screenh), pygame.DOUBLEBUF)
frame = pygame.Surface((w,h))
initArealightVectors()
camangle = [ 0.0, 0.0, 0.0]
camangledelta = [-.005,-.011,-.017]
camlook = [ 0.0, 0.0, 0.0]
camlookdelta = [-.001, .005, .004]
zoom = 46.0
zoomdelta = 0.99
contrast = 32
contrast_offset = -0.17
frames = []
#original numFrames = 9300
numFrames = int(sys.argv[3])
MAXTRACE = int(sys.argv[4])
for frameNo in range(numFrames):
# Put camera between central sphere and walls
campos = [0.0,0.0,16.0]
camrotatematrix = vGetRotMatrix(camangle)
campos = mTransform(camrotatematrix, campos)
camlookmatrix = vGetRotMatrix(camlook)
# Determine contrast ratio for this frame's pixels
thisframe_min = 100
thisframe_max = -100
pixels = pygame.PixelArray(frame)
for y in range(h):
for x in range(w):
camray = [ x/float(w) - 0.5,
y/float(h) - 0.5,
zoom ]
camray[0] *= 4.0/3 # Aspect Ratio Correction
camray = vNorm(camray)
camray = mTransform(camlookmatrix, camray)
campix = rayTrace(campos, camray, MAXTRACE)
campix = vMulD(campix, 0.5) #Adjust brightness
# update frame luma info for automatic contrast adjuster
lum = vLuma(campix)
if lum < thisframe_min: thisframe_min = lum
if lum > thisframe_max: thisframe_max = lum
# exagerate the colors to bring contrast better forth
campix = vMulD(vAddD(campix,contrast_offset),contrast)
# Clamp, and compensate for display gamma (for dithering)
# ...But don't actually dither. Because that shit is bananas.
# Maybe later, I can look up the EGA palette, etc.
campix = vClampWithDesaturation(campix)
# Draw pixel (use pygame)
r = int(campix[0] * 255)
g = int(campix[1] * 255)
b = int(campix[2] * 255)
pixels[x][y] = pygame.Color(r,g,b)
#del pixels?
pygame.transform.scale(frame, (screenw,screenh), screen)
pygame.display.flip()
for e in pygame.event.get():
if e.type == pygame.QUIT:
pygame.quit()
sys.exit()
frames.append(frame.copy())
print 'frame ',frameNo
sys.stdout.flush()
# Tweak coordinates/camera params for next frame
much = 1.0
# In the beginning, do some camera action (play with zoom)
if zoom <= 1.1:
zoom = 1.1
else:
if zoom > 40:
if zoomdelta > 0.95:
zoomdelta -= 0.001
elif zoom < 3:
if zoomdelta < 0.99:
zoomdelta += 0.001
zoom *= zoomdelta
much = 1.1 / ((zoom/1.1)**3.0)
# Update rotation angle
camlook = vAdd(camlook, vMulD(camlookdelta,much))
camangle = vAdd(camangle, vMulD(camangledelta,much))
# dynamically adjust the contrast based on the contents of the
# last frame
middle = (thisframe_min + thisframe_max) * 0.5
span = (thisframe_max - thisframe_min)
thisframe_min = middle - span*0.60 # Avoid dark tones
thisframe_max = middle + span*0.37 # Emphasize bright tones
new_contrast_offset = -thisframe_min
new_contrast = 1 / (thisframe_max - thisframe_min)
# Avoid abrupt changes, though
l = 0.85
if frameNo == 0: l = 0.7
contrast_offset = (contrast_offset*l + new_contrast_offset*(1.0-l))
contrast = (contrast *l + new_contrast *(1.0-l))
while True:
for f in frames:
pygame.transform.scale(f, (screenw,screenh), screen)
pygame.display.flip()
pygame.time.wait(33)
for e in pygame.event.get():
if e.type == pygame.QUIT:
pygame.quit()
sys.exit()
if __name__ == '__main__':
main()