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 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 31 def sq(a): 32 return a*a 33 34 class vec: 35 def __init__(self, x, y, z): 36 self.x=float(x) 37 self.y=float(y) 38 self.z=float(z) 39 40 def __add__(self,other): 41 return vec(self.x+other.x,self.y+other.y,self.z+other.z) 42 43 def __sub__(self,other): 44 return vec(self.x-other.x,self.y-other.y,self.z-other.z) 45 46 def __mul__(self,amount): 47 return vec(self.x*amount,self.y*amount,self.z*amount) 48 49 def __div__(self,amount): 50 return vec(self.x/amount,self.y/amount,self.z/amount) 51 52 def __neg__(self): 53 return vec(-self.x,-self.y,-self.z) 54 55 def dot(self,other): 56 return (self.x*other.x)+(self.y*other.y)+(self.z*other.z) 57 58 def cross(self,other): 59 return vec(self.y*other.z - self.z*other.y, 60 self.z*other.x - self.x*other.z, 61 self.x*other.y - self.y*other.x) 62 63 def dist(self,other): 64 return sqrt((other.x-self.x)*(other.x-self.x)+ 65 (other.y-self.y)*(other.y-self.y)+ 66 (other.z-self.z)*(other.z-self.z)) 67 68 def sq(self): 69 return sq(self.x)+sq(self.y)+sq(self.z) 70 71 def mag(self): 72 return self.dist(vec(0,0,0)) 73 74 def norm(self): 75 mag=self.mag() 76 if mag!=0: 77 self.x=self.x/mag 78 self.y=self.y/mag 79 self.z=self.z/mag 80 81 def reflect(self,normal): 82 vdn=self.dot(normal)*2 83 return self-normal*vdn 84 85 class line: 86 def __init__(self, start, end): 87 self.start=start 88 self.end=end 89 90 def vec(self): 91 return self.end-self.start 92 93 def closestpoint(self, point): 94 l=self.end-self.start 95 l2=point-self.start 96 t=l.dot(l2) 97 if t<=0: return self.start 98 if t>l.mag(): return self.end 99 return self.start+l*t 100 101 class renderobject: 102 def __init__(self, shader): 103 self.shader=shader 104 105 def intersect(self,l): 106 return "none",vec(0,0,0),vec(0,0,0) # type, position, normal 107 108 class plane(renderobject): 109 def __init__(self,plane,dist,shader): 110 renderobject.__init__(self,shader) 111 self.plane=plane 112 self.dist=dist 113 114 def intersect(self,l): 115 vd=self.plane.dot(l.vec()) 116 if vd==0: return "none",vec(0,0,0),vec(0,0,0) 117 v0 = -(self.plane.dot(l.start)+self.dist) 118 t = v0/vd 119 if t<0 or t>1: return "none",vec(0,0,0),vec(0,0,0) 120 return "one",l.start+(l.vec()*t),self.plane 121 122 123 class sphere(renderobject): 124 def __init__(self, pos, radius, shader): 125 renderobject.__init__(self,shader) 126 self.pos=pos 127 self.radius=radius 128 129 def disttoline(self,l): 130 return self.pos.dist(l.closestpoint(self.pos)) 131 132 def intersect(self,l): 133 lvec=l.vec() 134 a = sq(lvec.x)+sq(lvec.y)+sq(lvec.z) 135 136 b = 2*(lvec.x*(l.start.x-self.pos.x)+ \ 137 lvec.y*(l.start.y-self.pos.y)+ \ 138 lvec.z*(l.start.z-self.pos.z)) 139 140 c = self.pos.sq()+l.start.sq() - \ 141 2*(self.pos.x*l.start.x+self.pos.y*l.start.y+self.pos.z*l.start.z)-sq(self.radius) 142 143 i = b*b-4*a*c 144 145 intersectiontype="none" 146 pos=vec(0,0,0) 147 norm=vec(0,0,0) 148 t=0 149 150 if i>0 : 151 if i==0: 152 intersectiontype="one" 153 t = -b/(2*a); 154 else: 155 intersectiontype="two" 156 t = (-b - sqrt( b*b - 4*a*c )) / (2*a) 157 # just bother with one for the moment 158 # t2= (-b + sqrt( b*b - 4*a*c )) / (2*a) 159 160 if t>0 and t<1: 161 pos = l.start+lvec*t 162 norm=pos-self.pos 163 norm.norm() 164 else: 165 intersectiontype="none" 166 167 return intersectiontype,pos,norm 168 169 def intersects(self,l): 170 return self.disttoline(l)<self.radius 171 172 class light: 173 def __init__(self): 174 pass 175 176 def checkshadow(self, obj, objects,l): 177 # shadowing built into the lights (is this right?) 178 for ob in objects: 179 if ob is not obj: 180 intersects,pos,norm = ob.intersect(l) 181 if intersects is not "none": 182 return 1 183 return 0 184 185 def light(self, obj, objects, pos, normal): 186 pass 187 188 class parallellight(light): 189 def __init__(self, direction, col): 190 direction.norm() 191 self.direction=direction 192 self.col=col 193 194 def inshadow(self, obj, objects, pos): 195 # create a longish line towards the light 196 l = line(pos,pos+self.direction*1000) 197 return self.checkshadow(obj,objects,l) 198 199 def light(self, shaderinfo): 200 if self.inshadow(shaderinfo["thisobj"],shaderinfo["objects"],shaderinfo["position"]): return vec(0,0,0) 201 return self.col*self.direction.dot(shaderinfo["normal"]) 202 203 class pointlight(light): 204 def __init__(self, position, col): 205 self.position=position 206 self.col=col 207 208 def inshadow(self, obj, objects, pos): 209 l = line(pos,self.position) 210 return self.checkshadow(obj,objects,l) 211 212 def light(self, shaderinfo): 213 if self.inshadow(shaderinfo["thisobj"],shaderinfo["objects"],shaderinfo["position"]): return vec(0,0,0) 214 direction = shaderinfo["position"]-self.position; 215 direction.norm() 216 direction=-direction 217 return self.col*direction.dot(shaderinfo["normal"]) 218 219 class shader: 220 def __init__(self): 221 pass 222 223 # a load of helper functions for shaders, need much improvement 224 225 def getreflected(self,shaderinfo): 226 depth=shaderinfo["depth"] 227 col=vec(0,0,0) 228 if depth>0: 229 lray=copy.copy(shaderinfo["ray"]) 230 ray=lray.vec() 231 normal=copy.copy(shaderinfo["normal"]) 232 ray=ray.reflect(normal) 233 reflected=line(shaderinfo["position"],shaderinfo["position"]+ray) 234 obj=shaderinfo["thisobj"] 235 objects=shaderinfo["objects"] 236 newshaderinfo = copy.copy(shaderinfo) 237 newshaderinfo["ray"]=reflected 238 newshaderinfo["depth"]=depth-1 239 # todo - depth test 240 for ob in objects: 241 if ob is not obj: 242 intersects,position,normal = ob.intersect(reflected) 243 if intersects is not "none": 244 newshaderinfo["thisobj"]=ob 245 newshaderinfo["position"]=position 246 newshaderinfo["normal"]=normal 247 col=col+ob.shader.shade(newshaderinfo) 248 return col 249 250 def isoccluded(self,ray,shaderinfo): 251 dist=ray.mag() 252 test=line(shaderinfo["position"],shaderinfo["position"]+ray) 253 obj=shaderinfo["thisobj"] 254 objects=shaderinfo["objects"] 255 # todo - depth test 256 for ob in objects: 257 if ob is not obj: 258 intersects,position,normal = ob.intersect(test) 259 if intersects is not "none": 260 return 1 261 return 0 262 263 def doocclusion(self,samples,shaderinfo): 264 # not really very scientific, or good in any way... 265 oc=0.0 266 for i in range(0,samples): 267 ray=vec(random.randrange(-100,100),random.randrange(-100,100),random.randrange(-100,100)) 268 ray.norm() 269 ray=ray*2.5 270 if self.isoccluded(ray,shaderinfo): 271 oc=oc+1 272 oc=oc/float(samples) 273 return 1-oc 274 275 def getcolour(self,ray,shaderinfo): 276 depth=shaderinfo["depth"] 277 col=vec(0,0,0) 278 if depth>0: 279 test=line(shaderinfo["position"],shaderinfo["position"]+ray) 280 obj=shaderinfo["thisobj"] 281 objects=shaderinfo["objects"] 282 newshaderinfo = copy.copy(shaderinfo) 283 newshaderinfo["ray"]=test 284 newshaderinfo["depth"]=depth-1 285 # todo - depth test 286 for ob in objects: 287 if ob is not obj: 288 intersects,position,normal = ob.intersect(test) 289 if intersects is not "none": 290 newshaderinfo["thisobj"]=ob 291 newshaderinfo["position"]=position 292 newshaderinfo["normal"]=normal 293 col=col+ob.shader.shade(newshaderinfo) 294 return col 295 296 def docolourbleed(self,samples,shaderinfo): 297 # not really very scientific, or good in any way... 298 col=vec(0,0,0) 299 for i in range(0,samples): 300 ray=vec(random.randrange(-100,100),random.randrange(-100,100),random.randrange(-100,100)) 301 ray.norm() 302 ray=ray*5 303 col=col+self.getcolour(ray,shaderinfo) 304 col=col/float(samples) 305 return col 306 307 def shade(self,shaderinfo): 308 col=vec(0,0,0) 309 for lite in shaderinfo["lights"]: 310 col=col+lite.light(shaderinfo) 311 return col 312 313 class world: 314 def __init__(self,width,height): 315 self.lights=[] 316 self.objects=[] 317 self.cameratype="persp" 318 self.width=width 319 self.height=height 320 self.backplane=2000.0 321 self.imageplane=5.0 322 self.aspect=self.width/float(self.height) 323 324 def render_row(self, channel, sy): 325 326 """ 327 Render the given row, using the 'channel' provided to communicate 328 result data back to the coordinating process, and using 'sy' as the row 329 position. A tuple containing 'sy' and a list of result numbers is 330 returned by this function via the given 'channel'. 331 """ 332 333 row = [] 334 for sx in range(0,self.width): 335 x=2*(0.5-sx/float(self.width))*self.aspect 336 y=2*(0.5-sy/float(self.height)) 337 if self.cameratype=="ortho": 338 ray = line(vec(x,y,0),vec(x,y,self.backplane)) 339 else: 340 ray = line(vec(0,0,0),vec(x,y,self.imageplane)) 341 ray.end=ray.end*self.backplane 342 343 col=vec(0,0,0) 344 depth=self.backplane 345 shaderinfo={"ray":ray,"lights":self.lights,"objects":self.objects,"depth":2} 346 347 for obj in self.objects: 348 intersects,position,normal = obj.intersect(ray) 349 if intersects is not "none": 350 if position.z<depth and position.z>0: 351 depth=position.z 352 shaderinfo["thisobj"]=obj 353 shaderinfo["position"]=position 354 shaderinfo["normal"]=normal 355 col=obj.shader.shade(shaderinfo) 356 row.append(col) 357 358 channel.send((sy, row)) 359 360 def render(self, filename, limit): 361 362 """ 363 Render the image with many processes, saving it to 'filename', using the 364 given process 'limit' to constrain the number of processes used. 365 """ 366 367 image = Image.new("RGB", (self.width,self.height)) 368 exchange = PyGmyExchange(limit=limit) 369 exchange.draw = ImageDraw.Draw(image) 370 exchange.total = self.width*self.height 371 exchange.count = 0 372 373 for y in range(0, self.height): 374 channel = pprocess.start(self.render_row, y) 375 exchange.add_wait(channel) 376 377 exchange.finish() 378 image.save(filename) 379 380 class PyGmyExchange(pprocess.Exchange): 381 382 "A convenience class for parallelisation." 383 384 def store_data(self, channel): 385 386 "Store the data arriving on the given 'channel'." 387 388 sy, row = channel.receive() 389 sx = 0 390 for col in row: 391 self.draw.point((sx,sy),fill=(col.x*255,col.y*255,col.z*255)) 392 self.count = self.count + 1 393 sx += 1 394 395 percentstr = str(int((self.count/float(self.total))*100))+"%" 396 print "\b\b\b"+percentstr 397 398 # vim: tabstop=4 expandtab shiftwidth=4