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_point(self, channel, sx, sy): 326 327 """ 328 Render the given point, using the 'channel' provided to communicate 329 result data back to the coordinating process, and using 'sx' and 'sy' as 330 the point position. A tuple containing 'sx', 'sy' and a result is 331 returned by this function via the given 'channel'. 332 """ 333 334 x=2*(0.5-sx/float(self.width))*self.aspect 335 y=2*(0.5-sy/float(self.height)) 336 if self.cameratype=="ortho": 337 ray = line(vec(x,y,0),vec(x,y,self.backplane)) 338 else: 339 ray = line(vec(0,0,0),vec(x,y,self.imageplane)) 340 ray.end=ray.end*self.backplane 341 342 col=vec(0,0,0) 343 depth=self.backplane 344 shaderinfo={"ray":ray,"lights":self.lights,"objects":self.objects,"depth":2} 345 346 for obj in self.objects: 347 intersects,position,normal = obj.intersect(ray) 348 if intersects is not "none": 349 if position.z<depth and position.z>0: 350 depth=position.z 351 shaderinfo["thisobj"]=obj 352 shaderinfo["position"]=position 353 shaderinfo["normal"]=normal 354 col=obj.shader.shade(shaderinfo) 355 356 channel.send((sx, sy, col)) 357 358 def render(self, filename, limit): 359 360 """ 361 Render the image with many processes, saving it to 'filename', using the 362 given process 'limit' to constrain the number of processes used. 363 """ 364 365 image = Image.new("RGB", (self.width,self.height)) 366 exchange = PyGmyExchange(limit=limit) 367 exchange.draw = ImageDraw.Draw(image) 368 exchange.total = self.width*self.height 369 exchange.count = 0 370 371 for y in range(0, self.height): 372 for x in range(0,self.width): 373 channel = pprocess.start(self.render_point, x, y) 374 exchange.add_wait(channel) 375 376 exchange.finish() 377 image.save(filename) 378 379 class PyGmyExchange(pprocess.Exchange): 380 381 "A convenience class for parallelisation." 382 383 def store_data(self, channel): 384 385 "Store the data arriving on the given 'channel'." 386 387 sx, sy, col = channel.receive() 388 self.draw.point((sx,sy),fill=(col.x*255,col.y*255,col.z*255)) 389 self.count = self.count + 1 390 391 percent = int((self.count/float(self.total))*100) 392 sys.stdout.write(("\010" * 13) + "%3d%% %3d %3d" % (percent, sx, sy)) 393 sys.stdout.flush() 394 395 # vim: tabstop=4 expandtab shiftwidth=4