1 #!/usr/bin/env python 2 3 """ 4 An adaptation of pygmy.py ("a rubbish raytracer") employing pprocess 5 functionality in order to take advantage of multiprocessing environments. 6 7 -------- 8 9 Copyright (C) 2005 Dave Griffiths 10 Copyright (C) 2006, 2007 Paul Boddie <paul@boddie.org.uk> 11 12 This program is free software; you can redistribute it and/or 13 modify it under the terms of the GNU General Public License 14 as published by the Free Software Foundation; either version 2 15 of the License, or (at your option) any later version. 16 17 This program is distributed in the hope that it will be useful, 18 but WITHOUT ANY WARRANTY; without even the implied warranty of 19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 GNU General Public License for more details. 21 22 You should have received a copy of the GNU General Public License 23 along with this program; if not, write to the Free Software 24 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 25 """ 26 27 import Image, ImageDraw, random, copy 28 from math import * 29 import pprocess 30 import sys 31 32 def sq(a): 33 return a*a 34 35 class vec: 36 def __init__(self, x, y, z): 37 self.x=float(x) 38 self.y=float(y) 39 self.z=float(z) 40 41 def __add__(self,other): 42 return vec(self.x+other.x,self.y+other.y,self.z+other.z) 43 44 def __sub__(self,other): 45 return vec(self.x-other.x,self.y-other.y,self.z-other.z) 46 47 def __mul__(self,amount): 48 return vec(self.x*amount,self.y*amount,self.z*amount) 49 50 def __div__(self,amount): 51 return vec(self.x/amount,self.y/amount,self.z/amount) 52 53 def __neg__(self): 54 return vec(-self.x,-self.y,-self.z) 55 56 def dot(self,other): 57 return (self.x*other.x)+(self.y*other.y)+(self.z*other.z) 58 59 def cross(self,other): 60 return vec(self.y*other.z - self.z*other.y, 61 self.z*other.x - self.x*other.z, 62 self.x*other.y - self.y*other.x) 63 64 def dist(self,other): 65 return sqrt((other.x-self.x)*(other.x-self.x)+ 66 (other.y-self.y)*(other.y-self.y)+ 67 (other.z-self.z)*(other.z-self.z)) 68 69 def sq(self): 70 return sq(self.x)+sq(self.y)+sq(self.z) 71 72 def mag(self): 73 return self.dist(vec(0,0,0)) 74 75 def norm(self): 76 mag=self.mag() 77 if mag!=0: 78 self.x=self.x/mag 79 self.y=self.y/mag 80 self.z=self.z/mag 81 82 def reflect(self,normal): 83 vdn=self.dot(normal)*2 84 return self-normal*vdn 85 86 class line: 87 def __init__(self, start, end): 88 self.start=start 89 self.end=end 90 91 def vec(self): 92 return self.end-self.start 93 94 def closestpoint(self, point): 95 l=self.end-self.start 96 l2=point-self.start 97 t=l.dot(l2) 98 if t<=0: return self.start 99 if t>l.mag(): return self.end 100 return self.start+l*t 101 102 class renderobject: 103 def __init__(self, shader): 104 self.shader=shader 105 106 def intersect(self,l): 107 return "none",vec(0,0,0),vec(0,0,0) # type, position, normal 108 109 class plane(renderobject): 110 def __init__(self,plane,dist,shader): 111 renderobject.__init__(self,shader) 112 self.plane=plane 113 self.dist=dist 114 115 def intersect(self,l): 116 vd=self.plane.dot(l.vec()) 117 if vd==0: return "none",vec(0,0,0),vec(0,0,0) 118 v0 = -(self.plane.dot(l.start)+self.dist) 119 t = v0/vd 120 if t<0 or t>1: return "none",vec(0,0,0),vec(0,0,0) 121 return "one",l.start+(l.vec()*t),self.plane 122 123 124 class sphere(renderobject): 125 def __init__(self, pos, radius, shader): 126 renderobject.__init__(self,shader) 127 self.pos=pos 128 self.radius=radius 129 130 def disttoline(self,l): 131 return self.pos.dist(l.closestpoint(self.pos)) 132 133 def intersect(self,l): 134 lvec=l.vec() 135 a = sq(lvec.x)+sq(lvec.y)+sq(lvec.z) 136 137 b = 2*(lvec.x*(l.start.x-self.pos.x)+ \ 138 lvec.y*(l.start.y-self.pos.y)+ \ 139 lvec.z*(l.start.z-self.pos.z)) 140 141 c = self.pos.sq()+l.start.sq() - \ 142 2*(self.pos.x*l.start.x+self.pos.y*l.start.y+self.pos.z*l.start.z)-sq(self.radius) 143 144 i = b*b-4*a*c 145 146 intersectiontype="none" 147 pos=vec(0,0,0) 148 norm=vec(0,0,0) 149 t=0 150 151 if i>0 : 152 if i==0: 153 intersectiontype="one" 154 t = -b/(2*a); 155 else: 156 intersectiontype="two" 157 t = (-b - sqrt( b*b - 4*a*c )) / (2*a) 158 # just bother with one for the moment 159 # t2= (-b + sqrt( b*b - 4*a*c )) / (2*a) 160 161 if t>0 and t<1: 162 pos = l.start+lvec*t 163 norm=pos-self.pos 164 norm.norm() 165 else: 166 intersectiontype="none" 167 168 return intersectiontype,pos,norm 169 170 def intersects(self,l): 171 return self.disttoline(l)<self.radius 172 173 class light: 174 def __init__(self): 175 pass 176 177 def checkshadow(self, obj, objects,l): 178 # shadowing built into the lights (is this right?) 179 for ob in objects: 180 if ob is not obj: 181 intersects,pos,norm = ob.intersect(l) 182 if intersects is not "none": 183 return 1 184 return 0 185 186 def light(self, obj, objects, pos, normal): 187 pass 188 189 class parallellight(light): 190 def __init__(self, direction, col): 191 direction.norm() 192 self.direction=direction 193 self.col=col 194 195 def inshadow(self, obj, objects, pos): 196 # create a longish line towards the light 197 l = line(pos,pos+self.direction*1000) 198 return self.checkshadow(obj,objects,l) 199 200 def light(self, shaderinfo): 201 if self.inshadow(shaderinfo["thisobj"],shaderinfo["objects"],shaderinfo["position"]): return vec(0,0,0) 202 return self.col*self.direction.dot(shaderinfo["normal"]) 203 204 class pointlight(light): 205 def __init__(self, position, col): 206 self.position=position 207 self.col=col 208 209 def inshadow(self, obj, objects, pos): 210 l = line(pos,self.position) 211 return self.checkshadow(obj,objects,l) 212 213 def light(self, shaderinfo): 214 if self.inshadow(shaderinfo["thisobj"],shaderinfo["objects"],shaderinfo["position"]): return vec(0,0,0) 215 direction = shaderinfo["position"]-self.position; 216 direction.norm() 217 direction=-direction 218 return self.col*direction.dot(shaderinfo["normal"]) 219 220 class shader: 221 def __init__(self): 222 pass 223 224 # a load of helper functions for shaders, need much improvement 225 226 def getreflected(self,shaderinfo): 227 depth=shaderinfo["depth"] 228 col=vec(0,0,0) 229 if depth>0: 230 lray=copy.copy(shaderinfo["ray"]) 231 ray=lray.vec() 232 normal=copy.copy(shaderinfo["normal"]) 233 ray=ray.reflect(normal) 234 reflected=line(shaderinfo["position"],shaderinfo["position"]+ray) 235 obj=shaderinfo["thisobj"] 236 objects=shaderinfo["objects"] 237 newshaderinfo = copy.copy(shaderinfo) 238 newshaderinfo["ray"]=reflected 239 newshaderinfo["depth"]=depth-1 240 # todo - depth test 241 for ob in objects: 242 if ob is not obj: 243 intersects,position,normal = ob.intersect(reflected) 244 if intersects is not "none": 245 newshaderinfo["thisobj"]=ob 246 newshaderinfo["position"]=position 247 newshaderinfo["normal"]=normal 248 col=col+ob.shader.shade(newshaderinfo) 249 return col 250 251 def isoccluded(self,ray,shaderinfo): 252 dist=ray.mag() 253 test=line(shaderinfo["position"],shaderinfo["position"]+ray) 254 obj=shaderinfo["thisobj"] 255 objects=shaderinfo["objects"] 256 # todo - depth test 257 for ob in objects: 258 if ob is not obj: 259 intersects,position,normal = ob.intersect(test) 260 if intersects is not "none": 261 return 1 262 return 0 263 264 def doocclusion(self,samples,shaderinfo): 265 # not really very scientific, or good in any way... 266 oc=0.0 267 for i in range(0,samples): 268 ray=vec(random.randrange(-100,100),random.randrange(-100,100),random.randrange(-100,100)) 269 ray.norm() 270 ray=ray*2.5 271 if self.isoccluded(ray,shaderinfo): 272 oc=oc+1 273 oc=oc/float(samples) 274 return 1-oc 275 276 def getcolour(self,ray,shaderinfo): 277 depth=shaderinfo["depth"] 278 col=vec(0,0,0) 279 if depth>0: 280 test=line(shaderinfo["position"],shaderinfo["position"]+ray) 281 obj=shaderinfo["thisobj"] 282 objects=shaderinfo["objects"] 283 newshaderinfo = copy.copy(shaderinfo) 284 newshaderinfo["ray"]=test 285 newshaderinfo["depth"]=depth-1 286 # todo - depth test 287 for ob in objects: 288 if ob is not obj: 289 intersects,position,normal = ob.intersect(test) 290 if intersects is not "none": 291 newshaderinfo["thisobj"]=ob 292 newshaderinfo["position"]=position 293 newshaderinfo["normal"]=normal 294 col=col+ob.shader.shade(newshaderinfo) 295 return col 296 297 def docolourbleed(self,samples,shaderinfo): 298 # not really very scientific, or good in any way... 299 col=vec(0,0,0) 300 for i in range(0,samples): 301 ray=vec(random.randrange(-100,100),random.randrange(-100,100),random.randrange(-100,100)) 302 ray.norm() 303 ray=ray*5 304 col=col+self.getcolour(ray,shaderinfo) 305 col=col/float(samples) 306 return col 307 308 def shade(self,shaderinfo): 309 col=vec(0,0,0) 310 for lite in shaderinfo["lights"]: 311 col=col+lite.light(shaderinfo) 312 return col 313 314 class world: 315 def __init__(self,width,height): 316 self.lights=[] 317 self.objects=[] 318 self.cameratype="persp" 319 self.width=width 320 self.height=height 321 self.backplane=2000.0 322 self.imageplane=5.0 323 self.aspect=self.width/float(self.height) 324 325 def render_row(self, channel, sy): 326 327 """ 328 Render the given row, using the 'channel' provided to communicate 329 result data back to the coordinating process, and using 'sy' as the row 330 position. A tuple containing 'sy' and a list of result numbers is 331 returned by this function via the given 'channel'. 332 """ 333 334 row = [] 335 for sx in range(0,self.width): 336 x=2*(0.5-sx/float(self.width))*self.aspect 337 y=2*(0.5-sy/float(self.height)) 338 if self.cameratype=="ortho": 339 ray = line(vec(x,y,0),vec(x,y,self.backplane)) 340 else: 341 ray = line(vec(0,0,0),vec(x,y,self.imageplane)) 342 ray.end=ray.end*self.backplane 343 344 col=vec(0,0,0) 345 depth=self.backplane 346 shaderinfo={"ray":ray,"lights":self.lights,"objects":self.objects,"depth":2} 347 348 for obj in self.objects: 349 intersects,position,normal = obj.intersect(ray) 350 if intersects is not "none": 351 if position.z<depth and position.z>0: 352 depth=position.z 353 shaderinfo["thisobj"]=obj 354 shaderinfo["position"]=position 355 shaderinfo["normal"]=normal 356 col=obj.shader.shade(shaderinfo) 357 row.append(col) 358 359 channel.send((sy, row)) 360 361 def render(self, filename, limit): 362 363 """ 364 Render the image with many processes, saving it to 'filename', using the 365 given process 'limit' to constrain the number of processes used. 366 """ 367 368 image = Image.new("RGB", (self.width, self.height)) 369 draw = ImageDraw.Draw(image) 370 total = self.width * self.height 371 count = 0 372 373 queue = pprocess.Queue(limit=limit) 374 render_row = queue.manage(self.render_row) 375 376 for y in range(0, self.height): 377 render_row(y) 378 379 for sy, row in queue: 380 for sx, col in enumerate(row): 381 draw.point((sx,sy),fill=(col.x*255,col.y*255,col.z*255)) 382 count = count + 1 383 384 percent = int((count/float(total))*100) 385 sys.stdout.write(("\010" * 9) + "%3d%% %3d" % (percent, sy)) 386 sys.stdout.flush() 387 388 image.save(filename) 389 390 # vim: tabstop=4 expandtab shiftwidth=4