| Line | Revision | Contents |
| 1 | 167 | """Quadratic geometry types (cone, sphere, cylinder, etc) |
| 2 | ||
| 3 | This implementation does not use the gluQuadric objects, it |
|
| 4 | does direct creation via numpy operations. |
|
| 5 | """ |
|
| 6 | 1 | from OpenGL.GL import * |
| 7 | from vrml import field, protofunctions, node |
|
| 8 | from vrml.vrml97 import basenodes, nodetypes |
|
| 9 | 139 | from vrml import cache |
| 10 | from OpenGLContext.scenegraph import boundingvolume |
|
| 11 | 163 | from OpenGLContext.arrays import * |
| 12 | 164 | from OpenGLContext import vectorutilities |
| 13 | 163 | from OpenGL.arrays import vbo |
| 14 | 1 | |
| 15 | 164 | |
| 16 | def mesh_indices( zstep,ystep, xstep=1 ): |
|
| 17 | # now the indices, same as all quadratics |
|
| 18 | indices = zeros( (zstep-1,ystep-1,6),dtype='H' ) |
|
| 19 | # all indices now render the first rectangle... |
|
| 20 | indices[:] = (0,0+ystep,0+ystep+xstep, 0,0+ystep+xstep,0+xstep) |
|
| 21 | xoffsets = arange(0,ystep-1,1,dtype='H').reshape( (-1,1)) |
|
| 22 | indices += xoffsets |
|
| 23 | yoffsets = arange(0,zstep-1,1,dtype='H').reshape( (-1,1,1)) |
|
| 24 | indices += (yoffsets * ystep) |
|
| 25 | return indices |
|
| 26 | ||
| 27 | 1 | class Quadric( nodetypes.Geometry, node.Node ): |
| 28 | """Base-class for the various quadratic-type geometry classes""" |
|
| 29 | def render ( |
|
| 30 | self, |
|
| 31 | visible = 1, # can skip normals and textures if not |
|
| 32 | lit = 1, # can skip normals if not |
|
| 33 | textured = 1, # can skip textureCoordinates if not |
|
| 34 | transparent = 0, |
|
| 35 | mode = None, # the renderpass object for which we compile |
|
| 36 | ): |
|
| 37 | """Render the geometry""" |
|
| 38 | 166 | vbos = mode.cache.getData(self) |
| 39 | if not vbos: |
|
| 40 | vbos = self.compile( mode = mode ) |
|
| 41 | if vbos is None: |
|
| 42 | 1 | return 1 |
| 43 | 166 | coords,indices,count = vbos |
| 44 | glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS) |
|
| 45 | glPushAttrib(GL_ALL_ATTRIB_BITS) |
|
| 46 | 1 | try: |
| 47 | 166 | coords.bind() |
| 48 | glEnableClientState(GL_VERTEX_ARRAY) |
|
| 49 | glVertexPointer( 3, GL_FLOAT,32,coords) |
|
| 50 | if visible: |
|
| 51 | if textured: |
|
| 52 | glEnableClientState(GL_TEXTURE_COORD_ARRAY) |
|
| 53 | glTexCoordPointer( 3, GL_FLOAT,32,coords+12) |
|
| 54 | if lit: |
|
| 55 | glEnableClientState(GL_NORMAL_ARRAY) |
|
| 56 | glNormalPointer( GL_FLOAT,32,coords+20 ) |
|
| 57 | # TODO: sort for transparent geometry... |
|
| 58 | indices.bind() |
|
| 59 | # Can loop loading matrix and calling just this function |
|
| 60 | # for each sphere you want to render... |
|
| 61 | # include both scale and position in the matrix... |
|
| 62 | glDrawElements( |
|
| 63 | GL_TRIANGLES, count, GL_UNSIGNED_SHORT, indices |
|
| 64 | ) |
|
| 65 | 1 | finally: |
| 66 | 166 | glPopAttrib() |
| 67 | glPopClientAttrib() |
|
| 68 | coords.unbind() |
|
| 69 | indices.unbind() |
|
| 70 | def compile( self, mode=None ): |
|
| 71 | """Compile this sphere for use on mode""" |
|
| 72 | raise NotImplementedError( """Haven't implemented %s compilation yet"""%(self.__class__.__name__,)) |
|
| 73 | 1 | |
| 74 | ||
| 75 | 166 | class Sphere( basenodes.Sphere, Quadric ): |
| 76 | 1 | """Sphere geometry rendered with GLU quadratic calls""" |
| 77 | 163 | _unitSphere = None |
| 78 | def compile( self, mode=None ): |
|
| 79 | """Compile this sphere for use on mode""" |
|
| 80 | if self._unitSphere is None: |
|
| 81 | # create a unitsphere instance for all instances |
|
| 82 | Sphere._unitSphere = self.sphere( pi/32 ) |
|
| 83 | coords,indices = self._unitSphere |
|
| 84 | coords = copy( coords ) |
|
| 85 | coords[:,0:3] *= self.radius |
|
| 86 | vbos = vbo.VBO(coords), vbo.VBO(indices,target = 'GL_ELEMENT_ARRAY_BUFFER' ), len(indices) |
|
| 87 | 227 | if hasattr(mode,'cache'): |
| 88 | holder = mode.cache.holder( self, vbos ) |
|
| 89 | holder.depend( self, 'radius' ) |
|
| 90 | 163 | return vbos |
| 91 | |
|
| 92 | @classmethod |
|
| 93 | def sphere( cls, phi=pi/8.0, latAngle=pi, longAngle=(pi*2) ): |
|
| 94 | """Create arrays for rendering a unit-sphere |
|
| 95 | |
|
| 96 | phi -- angle between points on the sphere (stacks/slices) |
|
| 97 | |
|
| 98 | Note: creates 'H' type indices... |
|
| 99 | """ |
|
| 100 | latsteps = arange( 0,latAngle+0.000003, phi ) |
|
| 101 | longsteps = arange( 0,longAngle+0.000003, phi ) |
|
| 102 | return cls._partialSphere( latsteps,longsteps ) |
|
| 103 | ||
| 104 | @classmethod |
|
| 105 | def _partialSphere( cls, latsteps, longsteps ): |
|
| 106 | """Create a partial-sphere data-set for latsteps and longsteps""" |
|
| 107 | ystep = len(longsteps) |
|
| 108 | zstep = len(latsteps) |
|
| 109 | xstep = 1 |
|
| 110 | coords = zeros((zstep,ystep,8), 'f') |
|
| 111 | coords[:,:,0] = sin(longsteps) |
|
| 112 | coords[:,:,1] = cos(latsteps).reshape( (-1,1)) |
|
| 113 | coords[:,:,2] = cos(longsteps) |
|
| 114 | coords[:,:,3] = longsteps/(2*pi) |
|
| 115 | coords[:,:,4] = latsteps.reshape( (-1,1))/ pi |
|
| 116 | |
|
| 117 | # now scale by sin of y's |
|
| 118 | scale = sin(latsteps).reshape( (-1,1)) |
|
| 119 | coords[:,:,0] *= scale |
|
| 120 | coords[:,:,2] *= scale |
|
| 121 | coords[:,:,5:8] = coords[:,:,0:3] # normals |
|
| 122 | |
|
| 123 | 164 | indices = mesh_indices( zstep, ystep ) |
| 124 | 163 | |
| 125 | # now optimize/simplify the data-set... |
|
| 126 | new_indices = [] |
|
| 127 | |
|
| 128 | for (i,iSet) in enumerate(indices ): |
|
| 129 | angle = latsteps[i] |
|
| 130 | nextAngle = latsteps[i+1] |
|
| 131 | if allclose(angle%(pi*2),0): |
|
| 132 | iSet = iSet.reshape( (-1,3))[::2] |
|
| 133 | elif allclose(nextAngle%(pi),0): |
|
| 134 | iSet = iSet.reshape( (-1,3))[1::2] |
|
| 135 | else: |
|
| 136 | iSet = iSet.reshape( (-1,3)) |
|
| 137 | new_indices.append( iSet ) |
|
| 138 | indices = concatenate( new_indices ) |
|
| 139 | return coords.reshape((-1,8)), indices.reshape((-1,)) |
|
| 140 | |
|
| 141 | 1 | def boundingVolume( self, mode=None ): |
| 142 | """Create a bounding-volume object for this node |
|
| 143 | ||
| 144 | In this case we use the AABoundingBox, despite |
|
| 145 | the presence of the bounding sphere implementation. |
|
| 146 | This is just a preference issue, I'm using |
|
| 147 | AABoundingBox everywhere else, and want the sphere |
|
| 148 | to interoperate properly. |
|
| 149 | """ |
|
| 150 | current = boundingvolume.getCachedVolume( self ) |
|
| 151 | if current: |
|
| 152 | return current |
|
| 153 | return boundingvolume.cacheVolume( |
|
| 154 | self, |
|
| 155 | boundingvolume.AABoundingBox( |
|
| 156 | size = (self.radius*2,self.radius*2,self.radius*2), |
|
| 157 | ), |
|
| 158 | ( (self, 'radius'), ), |
|
| 159 | ) |
|
| 160 | |
|
| 161 | 166 | class Cone( basenodes.Cone, Quadric ): |
| 162 | 1 | """Cone geometry rendered with GLU quadratic calls""" |
| 163 | 164 | def compile( self, mode=None ): |
| 164 | """Compile this sphere for use on mode""" |
|
| 165 | coords,indices = self.cone( self.height, self.bottomRadius, self.bottom, self.side ) |
|
| 166 | vbos = vbo.VBO(coords), vbo.VBO(indices,target = 'GL_ELEMENT_ARRAY_BUFFER' ), len(indices) |
|
| 167 | holder = mode.cache.holder( self, vbos ) |
|
| 168 | holder.depend( self, 'bottomRadius' ) |
|
| 169 | holder.depend( self, 'height' ) |
|
| 170 | return vbos |
|
| 171 | |
|
| 172 | @classmethod |
|
| 173 | def cone( |
|
| 174 | 166 | cls, height=2.0, radius=1.0, bottom=True, side=True, |
| 175 | 167 | phi = pi/16, longAngle=(pi*2), top=False, cylinder=False |
| 176 | 164 | ): |
| 177 | """Generate a VBO data-set to render a cone""" |
|
| 178 | tip = (0,height/2.0,0) |
|
| 179 | longsteps = arange( 0,longAngle+0.000003, phi ) |
|
| 180 | ystep = len(longsteps) |
|
| 181 | zstep = 0 |
|
| 182 | 167 | if top and cylinder: |
| 183 | 166 | zstep += 2 |
| 184 | 164 | if side: |
| 185 | zstep += 2 |
|
| 186 | if bottom: |
|
| 187 | zstep += 2 |
|
| 188 | # need top-ring coords and 2 sets for |
|
| 189 | coords = zeros( (zstep,ystep,8), 'f') |
|
| 190 | 166 | coords[:,:,0] = sin(longsteps) * radius |
| 191 | coords[:,:,2] = cos(longsteps) * radius |
|
| 192 | 164 | coords[:,:,3] = longsteps/(2*pi) |
| 193 | 167 | def fill_disk( area, ycoord, normal=(0,-1,0), degenerate=1 ): |
| 194 | 164 | """fill in disk elements for given area""" |
| 195 | 167 | other = not degenerate |
| 196 | 164 | # disk texture coordinates |
| 197 | 167 | area[:,:,1] = ycoord |
| 198 | 164 | # x and z are 0 at center |
| 199 | 167 | area[degenerate,:,0] = 0.0 |
| 200 | area[degenerate,:,2] = 0.0 |
|
| 201 | area[other,:,3] = sin( longsteps ) / 2.0 + .5 |
|
| 202 | area[other,:,4] = cos( longsteps ) / 2.0 + .5 |
|
| 203 | area[degenerate,:,3:5] = .5 |
|
| 204 | 164 | # normal for the disk is all the same... |
| 205 | 167 | area[:,:,5:8] = normal |
| 206 | 166 | def fill_sides( area ): |
| 207 | """Fill in side-of-cylinder/cone components""" |
|
| 208 | 167 | if not cylinder: |
| 209 | area[0,:,0:3] = (0,height/2.0,0) |
|
| 210 | else: |
|
| 211 | area[0,:,1] = height/2.0 |
|
| 212 | area[1,:,1] = -height/2.0 |
|
| 213 | 166 | area[0,:,4] = 0 |
| 214 | area[1,:,4] = 1.0 |
|
| 215 | 164 | # normals for the sides... |
| 216 | 166 | area[0:2,:-1,5:8] = vectorutilities.normalise( |
| 217 | 164 | vectorutilities.crossProduct( |
| 218 | 166 | area[0,:-1,0:3] - area[1,:-1,0:3], |
| 219 | area[1,:-1,0:3] - area[1,1:,0:3] |
|
| 220 | 164 | ) |
| 221 | ) |
|
| 222 | 166 | area[0:2,-1,5:8] = area[0:2,0,5:8] |
| 223 | 167 | |
| 224 | offset = 0 |
|
| 225 | tocompress = {} |
|
| 226 | if top and cylinder: |
|
| 227 | fill_disk( coords[offset:offset+2],height/2.0,(0,1,0), degenerate=0 ) |
|
| 228 | tocompress[offset] = 0 |
|
| 229 | offset += 2 |
|
| 230 | 166 | if side: |
| 231 | 167 | fill_sides( coords[offset:offset+2] ) |
| 232 | offset += 2 |
|
| 233 | if bottom: |
|
| 234 | # disk texture coordinates |
|
| 235 | fill_disk( coords[offset:offset+2], -height/2.0, (0,-1,0), degenerate=1 ) |
|
| 236 | tocompress[offset] = 1 |
|
| 237 | offset += 2 |
|
| 238 | |
|
| 239 | 164 | # now the indices, same as all quadratics |
| 240 | indices = mesh_indices( zstep, ystep ) |
|
| 241 | 167 | new_indices = [] |
| 242 | for (i,iSet) in enumerate( indices ): |
|
| 243 | iSet = iSet.reshape( (-1,3) ) |
|
| 244 | if i in tocompress: |
|
| 245 | if not tocompress[i]: |
|
| 246 | iSet = iSet[::2] |
|
| 247 | else: |
|
| 248 | iSet = iSet[1::2] |
|
| 249 | new_indices.append( iSet ) |
|
| 250 | 164 | # compress out degenerate indices if present... |
| 251 | indices = concatenate( new_indices ) |
|
| 252 | return coords.reshape( (-1,8)), indices.reshape( (-1,)) |
|
| 253 | |
|
| 254 | 1 | def boundingVolume( self, mode=None ): |
| 255 | """Create a bounding-volume object for this node |
|
| 256 | ||
| 257 | In this case we use the AABoundingBox, despite |
|
| 258 | the presence of the bounding sphere implementation. |
|
| 259 | This is just a preference issue, I'm using |
|
| 260 | AABoundingBox everywhere else, and want the sphere |
|
| 261 | to interoperate properly. |
|
| 262 | """ |
|
| 263 | current = boundingvolume.getCachedVolume( self ) |
|
| 264 | if current: |
|
| 265 | return current |
|
| 266 | radius = self.bottomRadius |
|
| 267 | return boundingvolume.cacheVolume( |
|
| 268 | self, |
|
| 269 | boundingvolume.AABoundingBox( |
|
| 270 | size = (radius*2,self.height,radius*2), |
|
| 271 | ), |
|
| 272 | ( (self, 'bottomRadius'), (self,'height') ), |
|
| 273 | ) |
|
| 274 | ||
| 275 | ||
| 276 | ||
| 277 | class Cylinder( basenodes.Cylinder, Quadric ): |
|
| 278 | """Cylinder geometry rendered with GLU quadratic calls""" |
|
| 279 | 167 | def compile( self, mode=None ): |
| 280 | """Compile this sphere for use on mode""" |
|
| 281 | coords,indices = Cone.cone( |
|
| 282 | self.height, self.radius, self.bottom, self.side, |
|
| 283 | top=self.top, cylinder=True, |
|
| 284 | ) |
|
| 285 | vbos = vbo.VBO(coords), vbo.VBO(indices,target = 'GL_ELEMENT_ARRAY_BUFFER' ), len(indices) |
|
| 286 | holder = mode.cache.holder( self, vbos ) |
|
| 287 | holder.depend( self, 'radius' ) |
|
| 288 | holder.depend( self, 'height' ) |
|
| 289 | return vbos |
|
| 290 | 1 | def boundingVolume( self, mode=None ): |
| 291 | """Create a bounding-volume object for this node |
|
| 292 | ||
| 293 | In this case we use the AABoundingBox, despite |
|
| 294 | the presence of the bounding sphere implementation. |
|
| 295 | This is just a preference issue, I'm using |
|
| 296 | AABoundingBox everywhere else, and want the sphere |
|
| 297 | to interoperate properly. |
|
| 298 | """ |
|
| 299 | current = boundingvolume.getCachedVolume( self ) |
|
| 300 | if current: |
|
| 301 | return current |
|
| 302 | radius = self.radius |
|
| 303 | return boundingvolume.cacheVolume( |
|
| 304 | self, |
|
| 305 | boundingvolume.AABoundingBox( |
|
| 306 | size = (radius*2,self.height,radius*2), |
|
| 307 | ), |
|
| 308 | ( (self, 'radius'), (self,'height') ), |
|
| 309 | ) |
|
| 310 | 164 | |
| 311 | if __name__ == "__main__": |
|
| 312 | c = Cone.cone() |
|
| 313 | |
Loggerhead 1.10 is a web-based interface for Bazaar branches