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. The row position 'sy', 330 provided for the first row, will subsequently be read from the 331 'channel' for the rendering of new rows. A tuple containing 'sy' and a 332 list of result numbers is returned by this function via the given 333 'channel'. 334 """ 335 336 while 1: 337 338 row = [] 339 for sx in range(0,self.width): 340 x=2*(0.5-sx/float(self.width))*self.aspect 341 y=2*(0.5-sy/float(self.height)) 342 if self.cameratype=="ortho": 343 ray = line(vec(x,y,0),vec(x,y,self.backplane)) 344 else: 345 ray = line(vec(0,0,0),vec(x,y,self.imageplane)) 346 ray.end=ray.end*self.backplane 347 348 col=vec(0,0,0) 349 depth=self.backplane 350 shaderinfo={"ray":ray,"lights":self.lights,"objects":self.objects,"depth":2} 351 352 for obj in self.objects: 353 intersects,position,normal = obj.intersect(ray) 354 if intersects is not "none": 355 if position.z<depth and position.z>0: 356 depth=position.z 357 shaderinfo["thisobj"]=obj 358 shaderinfo["position"]=position 359 shaderinfo["normal"]=normal 360 col=obj.shader.shade(shaderinfo) 361 row.append(col) 362 363 channel.send((sy, row)) 364 t = channel.receive() 365 if t is None: 366 break 367 (sy,), kw = t 368 369 def render(self, filename, limit): 370 371 """ 372 Render the image with many processes, saving it to 'filename', using the 373 given process 'limit' to constrain the number of processes used. 374 """ 375 376 image = Image.new("RGB", (self.width, self.height)) 377 exchange = PyGmyExchange(self.width, self.height, image, limit=limit, reuse=1) 378 render_row = exchange.manage(self.render_row) 379 380 for y in range(0, self.height): 381 render_row(y) 382 383 exchange.finish() 384 image.save(filename) 385 386 class PyGmyExchange(pprocess.Exchange): 387 388 "A convenience class for parallelisation." 389 390 def __init__(self, width, height, image, *args, **kw): 391 392 """ 393 Initialise the exchange, adding extra PyGmy-specific data such as the 394 'width' and 'height' of the eventual 'image'. 395 """ 396 397 pprocess.Exchange.__init__(self, *args, **kw) 398 self.draw = ImageDraw.Draw(image) 399 self.total = width * height 400 self.count = 0 401 402 def store_data(self, channel): 403 404 "Store the data arriving on the given 'channel'." 405 406 sy, row = channel.receive() 407 for sx, col in enumerate(row): 408 self.draw.point((sx,sy),fill=(col.x*255,col.y*255,col.z*255)) 409 self.count = self.count + 1 410 411 percent = int((self.count/float(self.total))*100) 412 sys.stdout.write(("\010" * 9) + "%3d%% %3d" % (percent, sy)) 413 sys.stdout.flush() 414 415 # vim: tabstop=4 expandtab shiftwidth=4