def getTextureRAM(mesh): total_image_area = 0 for cimg in mesh.images: pilimg = cimg.pilimage if pilimg: total_image_area += pilimg.size[0] * pilimg.size[1] * len( pilimg.getbands()) else: #PIL doesn't support DDS, so if loading failed, try and load it as a DDS with panda3d imgdata = cimg.data #if we can't even load the image's data, can't convert if imgdata is None: continue try: from panda3d.core import Texture from panda3d.core import StringStream except ImportError: #if panda3d isn't installed and PIL failed, can't convert continue t = Texture() try: success = t.readDds(StringStream(imgdata)) except: success = 1 if success == 0: #failed to load as DDS, so let's give up continue total_image_area += t.getXSize() * t.getYSize() * 3 return total_image_area
def getTextureRAM(mesh): total_image_area = 0 for cimg in mesh.images: pilimg = cimg.pilimage if pilimg: total_image_area += pilimg.size[0] * pilimg.size[1] * len(pilimg.getbands()) else: # PIL doesn't support DDS, so if loading failed, try and load it as a DDS with panda3d imgdata = cimg.data # if we can't even load the image's data, can't convert if imgdata is None: continue try: from panda3d.core import Texture from panda3d.core import StringStream except ImportError: # if panda3d isn't installed and PIL failed, can't convert continue t = Texture() try: success = t.readDds(StringStream(imgdata)) except: success = 1 if success == 0: # failed to load as DDS, so let's give up continue total_image_area += t.getXSize() * t.getYSize() * 3 return total_image_area
class Typist(object): TARGETS = { 'paper': { 'model': 'paper', 'textureRoot': 'Front', 'scale': Point3(0.85, 0.85, 1), 'hpr': Point3(0, 0, 0), } } def __init__(self, base, typewriterNP, underDeskClip, sounds): self.base = base self.sounds = sounds self.underDeskClip = underDeskClip self.typeIndex = 0 self.typewriterNP = typewriterNP self.rollerAssemblyNP = typewriterNP.find("**/roller assembly") assert self.rollerAssemblyNP self.rollerNP = typewriterNP.find("**/roller") assert self.rollerNP self.carriageNP = typewriterNP.find("**/carriage") assert self.carriageNP self.baseCarriagePos = self.carriageNP.getPos() self.carriageBounds = self.carriageNP.getTightBounds() self.font = base.loader.loadFont('Harting.ttf', pointSize=32) self.pnmFont = PNMTextMaker(self.font) self.fontCharSize, _, _ = fonts.measureFont(self.pnmFont, 32) print "font char size: ", self.fontCharSize self.pixelsPerLine = int(round(self.pnmFont.getLineHeight())) self.target = None """ panda3d.core.NodePath """ self.targetRoot = None """ panda3d.core.NodePath """ self.paperY = 0.0 """ range from 0 to 1 """ self.paperX = 0.0 """ range from 0 to 1 """ self.createRollerBase() self.tex = None self.texImage = None self.setupTexture() self.scheduler = Scheduler() task = self.base.taskMgr.add(self.tick, 'timerTask') task.setDelay(0.01) def tick(self, task): self.scheduler.tick(globalClock.getRealTime()) return task.cont def setupTexture(self): """ This is the overlay/decal/etc. which contains the typed characters. The texture size and the font size are currently tied together. :return: """ self.texImage = PNMImage(1024, 1024) self.texImage.addAlpha() self.texImage.fill(1.0) self.texImage.alphaFill(1.0) self.tex = Texture('typing') self.tex.setMagfilter(Texture.FTLinear) self.tex.setMinfilter(Texture.FTLinear) self.typingStage = TextureStage('typing') self.typingStage.setMode(TextureStage.MModulate) self.tex.load(self.texImage) # ensure we can quickly update subimages self.tex.setKeepRamImage(True) # temp for drawing chars self.chImage = PNMImage(*self.fontCharSize) def drawCharacter(self, ch, px, py): """ Draw a character onto the texture :param ch: :param px: paperX :param py: paperY :return: the paper-relative size of the character """ h = self.fontCharSize[1] if ch != ' ': # position -> pixel, applying margins x = int(self.tex.getXSize() * (px * 0.8 + 0.1)) y = int(self.tex.getYSize() * (py * 0.8 + 0.1)) # always draw onto the paper, to capture # incremental character overstrikes self.pnmFont.generateInto(ch, self.texImage, x, y) if False: #print ch,"to",x,y,"w=",g.getWidth() self.tex.load(self.texImage) else: # copy an area (presumably) encompassing the character g = self.pnmFont.getGlyph(ord(ch)) cx, cy = self.fontCharSize # a glyph is minimally sized and "moves around" in its text box # (think ' vs. ,), so it has been drawn somewhere relative to # the 'x' and 'y' we wanted. x += g.getLeft() y -= g.getTop() self.chImage.copySubImage( self.texImage, 0, 0, # from x, y, # to cx, cy # size ) self.tex.loadSubImage(self.chImage, x, y) # toggle for a typewriter that uses non-proportional spacing #w = self.paperCharWidth(g.getWidth()) w = self.paperCharWidth() else: w = self.paperCharWidth() return w, h def start(self): self.target = None self.setTarget('paper') self.hookKeyboard() def createRollerBase(self): """ The paper moves such that it is tangent to the roller. This nodepath keeps a coordinate space relative to that, so that the paper can be positioned from (0,0,0) to (0,0,1) to "roll" it along the roller. """ bb = self.rollerNP.getTightBounds() #self.rollerNP.showTightBounds() self.paperRollerBase = self.rollerAssemblyNP.attachNewNode( 'rollerBase') self.paperRollerBase.setHpr(0, -20, 0) print "roller:", bb rad = abs(bb[0].y - bb[1].y) / 2 center = Vec3(-(bb[0].x + bb[1].x) / 2 - 0.03, (bb[0].y - bb[1].y) / 2, (bb[0].z + bb[1].z) / 2) self.paperRollerBase.setPos(center) def setTarget(self, name): if self.target: self.target.removeNode() # load and transform the model target = self.TARGETS[name] self.target = self.base.loader.loadModel(target['model']) #self.target.setScale(target['scale']) self.target.setHpr(target['hpr']) # put it in the world self.target.reparentTo(self.paperRollerBase) rbb = self.rollerNP.getTightBounds() tbb = self.target.getTightBounds() rs = (rbb[1] - rbb[0]) ts = (tbb[1] - tbb[0]) self.target.setScale(rs.x / ts.x, 1, 1) # apply the texture self.targetRoot = self.target if 'textureRoot' in target: self.targetRoot = self.target.find("**/" + target['textureRoot']) assert self.targetRoot self.targetRoot.setTexture(self.typingStage, self.tex) #self.setupTargetClip() # reset self.paperX = self.paperY = 0. newPos = self.calcPaperPos(self.paperY) self.target.setPos(newPos) self.moveCarriage() def setupTargetClip(self): """ The target is fed in to the typewriter but until we invent "geom curling", it shouldn't be visible under the typewriter under the desk. The @underDeskClip node has a world-relative bounding box, which we can convert to the target-relative bounding box, and pass to a shader that can clip the nodes. """ shader = Shader.make( Shader.SLGLSL, """ #version 120 attribute vec4 p3d_MultiTexCoord0; attribute vec4 p3d_MultiTexCoord1; void main() { gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; gl_TexCoord[0] = p3d_MultiTexCoord0; gl_TexCoord[1] = p3d_MultiTexCoord1; } """, """ #version 120 uniform sampler2D baseTex; uniform sampler2D charTex; const vec4 zero = vec4(0, 0, 0, 0); const vec4 one = vec4(1, 1, 1, 1); const vec4 half = vec4(0.5, 0.5, 0.5, 0); void main() { vec4 baseColor = texture2D(baseTex, gl_TexCoord[0].st); vec4 typeColor = texture2D(charTex, gl_TexCoord[1].st); gl_FragColor = baseColor * typeColor; }""") self.target.setShader(shader) baseTex = self.targetRoot.getTexture() print "Base Texture:", baseTex self.target.setShaderInput("baseTex", baseTex) self.target.setShaderInput("charTex", self.tex) def hookKeyboard(self): """ Hook events so we can respond to keypresses. """ self.base.buttonThrowers[0].node().setKeystrokeEvent('keystroke') self.base.accept('keystroke', self.schedTypeCharacter) self.base.accept('backspace', self.schedBackspace) self.base.accept('arrow_up', lambda: self.schedAdjustPaper(-5)) self.base.accept('arrow_up-repeat', lambda: self.schedAdjustPaper(-1)) self.base.accept('arrow_down', lambda: self.schedAdjustPaper(5)) self.base.accept('arrow_down-repeat', lambda: self.schedAdjustPaper(1)) self.base.accept('arrow_left', lambda: self.schedAdjustCarriage(-1)) self.base.accept('arrow_left-repeat', lambda: self.schedAdjustCarriage(-1)) self.base.accept('arrow_right', lambda: self.schedAdjustCarriage(1)) self.base.accept('arrow_right-repeat', lambda: self.schedAdjustCarriage(1)) def paperCharWidth(self, pixels=None): if not pixels: pixels = self.fontCharSize[0] return float(pixels) / self.tex.getXSize() def paperLineHeight(self): return float(self.fontCharSize[1] * 1.2) / self.tex.getYSize() def schedScroll(self): if self.scheduler.isQueueEmpty(): self.schedRollPaper(1) self.schedResetCarriage() def schedBackspace(self): if self.scheduler.isQueueEmpty(): def doit(): if self.paperX > 0: self.schedAdjustCarriage(-1) self.scheduler.schedule(0.01, doit) def createMoveCarriageInterval(self, newX, curX=None): if curX is None: curX = self.paperX here = self.calcCarriage(curX) there = self.calcCarriage(newX) posInterval = LerpPosInterval(self.carriageNP, abs(newX - curX), there, startPos=here, blendType='easeIn') posInterval.setDoneEvent('carriageReset') def isReset(): self.paperX = newX self.base.acceptOnce('carriageReset', isReset) return posInterval def schedResetCarriage(self): if self.paperX > 0.1: self.sounds['pullback'].play() invl = self.createMoveCarriageInterval(0) self.scheduler.scheduleInterval(0, invl) def calcCarriage(self, paperX): """ Calculate where the carriage should be offset based on the position on the paper :param paperX: 0...1 :return: pos for self.carriageNP """ x = (0.5 - paperX) * 0.69 * 0.8 + 0.01 bb = self.carriageBounds return self.baseCarriagePos + Point3(x * (bb[1].x - bb[0].x), 0, 0) def moveCarriage(self): pos = self.calcCarriage(self.paperX) self.carriageNP.setPos(pos) def schedMoveCarriage(self, curX, newX): if self.scheduler.isQueueEmpty(): #self.scheduler.schedule(0.1, self.moveCarriage) invl = self.createMoveCarriageInterval(newX, curX=curX) invl.start() def schedAdjustCarriage(self, bx): if self.scheduler.isQueueEmpty(): def doit(): self.paperX = max( 0.0, min(1.0, self.paperX + bx * self.paperCharWidth())) self.moveCarriage() self.scheduler.schedule(0.1, doit) def calcPaperPos(self, paperY): # center over roller, peek out a little z = paperY * 0.8 - 0.5 + 0.175 bb = self.target.getTightBounds() return Point3(-0.5, 0, z * (bb[1].z - bb[0].z)) def createMovePaperInterval(self, newY): here = self.calcPaperPos(self.paperY) there = self.calcPaperPos(newY) posInterval = LerpPosInterval(self.target, abs(newY - self.paperY), there, startPos=here, blendType='easeInOut') posInterval.setDoneEvent('scrollDone') def isDone(): self.paperY = newY self.base.acceptOnce('scrollDone', isDone) return posInterval def schedAdjustPaper(self, by): if self.scheduler.isQueueEmpty(): def doit(): self.schedRollPaper(by) self.scheduler.schedule(0.1, doit) def schedRollPaper(self, by): """ Position the paper such that @percent of it is rolled over roller :param percent: :return: """ def doit(): self.sounds['scroll'].play() newY = min(1.0, max(0.0, self.paperY + self.paperLineHeight() * by)) invl = self.createMovePaperInterval(newY) invl.start() self.scheduler.schedule(0.1, doit) def schedTypeCharacter(self, keyname): # filter for visibility if ord(keyname) == 13: self.schedScroll() elif ord(keyname) >= 32 and ord(keyname) != 127: if self.scheduler.isQueueEmpty(): curX, curY = self.paperX, self.paperY self.typeCharacter(keyname, curX, curY) def typeCharacter(self, ch, curX, curY): newX = curX w, h = self.drawCharacter(ch, curX, curY) newX += w if ch != ' ': # alternate typing sound #self.typeIndex = (self.typeIndex+1) % 3 self.typeIndex = random.randint(0, 2) self.sounds['type' + str(self.typeIndex + 1)].play() else: self.sounds['advance'].play() if newX >= 1: self.sounds['bell'].play() newX = 1 self.schedMoveCarriage(self.paperX, newX) # move first, to avoid overtype self.paperX = newX
class MyApp(ShowBase): def __init__(self): ShowBase.__init__(self) # Load the environment model. self.scene = self.loader.loadModel("models/environment") # Reparent the model to render. self.scene.reparentTo(self.render) # Apply scale and position transforms on the model. self.scene.setScale(0.25, 0.25, 0.25) self.scene.setPos(-8, 42, 0) # Needed for camera image self.dr = self.camNode.getDisplayRegion(0) # Needed for camera depth image winprops = WindowProperties.size(self.win.getXSize(), self.win.getYSize()) fbprops = FrameBufferProperties() fbprops.setDepthBits(1) self.depthBuffer = self.graphicsEngine.makeOutput( self.pipe, "depth buffer", -2, fbprops, winprops, GraphicsPipe.BFRefuseWindow, self.win.getGsg(), self.win) self.depthTex = Texture() self.depthTex.setFormat(Texture.FDepthComponent) self.depthBuffer.addRenderTexture(self.depthTex, GraphicsOutput.RTMCopyRam, GraphicsOutput.RTPDepth) lens = self.cam.node().getLens() # the near and far clipping distances can be changed if desired # lens.setNear(5.0) # lens.setFar(500.0) self.depthCam = self.makeCamera(self.depthBuffer, lens=lens, scene=render) self.depthCam.reparentTo(self.cam) # TODO: Scene is rendered twice: once for rgb and once for depth image. # How can both images be obtained in one rendering pass? def get_camera_image(self, requested_format=None): """ Returns the camera's image, which is of type uint8 and has values between 0 and 255. The 'requested_format' argument should specify in which order the components of the image must be. For example, valid format strings are "RGBA" and "BGRA". By default, Panda's internal format "BGRA" is used, in which case no data is copied over. """ tex = self.dr.getScreenshot() if requested_format is None: data = tex.getRamImage() else: data = tex.getRamImageAs(requested_format) image = np.frombuffer( data, np.uint8) # use data.get_data() instead of data in python 2 image.shape = (tex.getYSize(), tex.getXSize(), tex.getNumComponents()) image = np.flipud(image) return image def get_camera_depth_image(self): """ Returns the camera's depth image, which is of type float32 and has values between 0.0 and 1.0. """ data = self.depthTex.getRamImage() depth_image = np.frombuffer(data, np.float32) depth_image.shape = (self.depthTex.getYSize(), self.depthTex.getXSize(), self.depthTex.getNumComponents()) depth_image = np.flipud(depth_image) return depth_image
def optimizeTextures(mesh): previous_images = [] for cimg in mesh.images: previous_images.append(cimg.path) pilimg = cimg.pilimage #PIL doesn't support DDS, so if loading failed, try and load it as a DDS with panda3d if pilimg is None: imgdata = cimg.data #if we can't even load the image's data, can't convert if imgdata is None: print("Couldn't load image data", file=sys.stderr) continue try: from panda3d.core import Texture from panda3d.core import StringStream from panda3d.core import PNMImage except ImportError: #if panda3d isn't installed and PIL failed, can't convert print('Tried loading image with PIL and DDS and both failed', file=sys.stderr) continue t = Texture() success = t.readDds(StringStream(imgdata)) if success == 0: #failed to load as DDS, so let's give up print('Tried loading image as DDS and failed', file=sys.stderr) continue #convert DDS to PNG outdata = t.getRamImageAs('RGB').getData() try: im = Image.fromstring('RGB', (t.getXSize(), t.getYSize()), outdata) im.load() except IOError: #Any problem with panda3d might generate an invalid image buffer, so don't convert this print('Problem loading DDS file with PIL', file=sys.stderr) continue pilimg = im if pilimg.format == 'JPEG': #PIL image is already in JPG format so don't convert continue if 'A' in pilimg.getbands(): alpha = numpy.array(pilimg.split()[-1].getdata()) if not numpy.any(alpha < 255): alpha = None #this means that none of the pixels are using alpha, so convert to RGB pilimg = pilimg.convert('RGB') if 'A' in pilimg.getbands(): #save textures with an alpha channel in PNG output_format = 'PNG' output_extension = '.png' output_options = {'optimize':True} else: if pilimg.format != 'RGB': pilimg = pilimg.convert("RGB") #otherwise save as JPEG since it gets output_format = 'JPEG' output_extension = '.jpg' output_options = {'quality':95, 'optimize':True} if cimg.path.lower()[-len(output_extension):] != output_extension: dot = cimg.path.rfind('.') before_ext = cimg.path[0:dot] if dot != -1 else cimg.path while before_ext + output_extension in previous_images: before_ext = before_ext + '-x' cimg.path = before_ext + output_extension previous_images.append(cimg.path) outbuf = StringIO() try: pilimg.save(outbuf, output_format, **output_options) except IOError as ex: print(ex) cimg.data = outbuf.getvalue()
class Panda3dCameraSensor(object): def __init__(self, base, color=True, depth=False, size=None, near_far=None, hfov=None, title=None): if size is None: size = (640, 480) if near_far is None: near_far = (0.01, 10000.0) if hfov is None: hfov = 60 winprops = WindowProperties.size(*size) winprops.setTitle(title or 'Camera Sensor') fbprops = FrameBufferProperties() # Request 8 RGB bits, 8 alpha bits, and a depth buffer. fbprops.setRgbColor(True) fbprops.setRgbaBits(8, 8, 8, 8) fbprops.setDepthBits(24) self.graphics_engine = GraphicsEngine(base.pipe) window_type = base.config.GetString('window-type', 'onscreen') flags = GraphicsPipe.BFFbPropsOptional if window_type == 'onscreen': flags = flags | GraphicsPipe.BFRequireWindow elif window_type == 'offscreen': flags = flags | GraphicsPipe.BFRefuseWindow self.buffer = self.graphics_engine.makeOutput(base.pipe, "camera sensor buffer", -100, fbprops, winprops, flags) if not color and not depth: raise ValueError("At least one of color or depth should be True") if color: self.color_tex = Texture("color_texture") self.buffer.addRenderTexture(self.color_tex, GraphicsOutput.RTMCopyRam, GraphicsOutput.RTPColor) else: self.color_tex = None if depth: self.depth_tex = Texture("depth_texture") self.buffer.addRenderTexture(self.depth_tex, GraphicsOutput.RTMCopyRam, GraphicsOutput.RTPDepth) else: self.depth_tex = None self.cam = base.makeCamera(self.buffer, scene=base.render, camName='camera_sensor') self.lens = self.cam.node().getLens() self.lens.setFov(hfov) self.lens.setFilmSize( *size) # this also defines the units of the focal length self.lens.setNearFar(*near_far) def observe(self): for _ in range(self.graphics_engine.getNumWindows()): self.graphics_engine.renderFrame() self.graphics_engine.syncFrame() images = [] if self.color_tex: data = self.color_tex.getRamImageAs('RGBA') if sys.version_info < (3, 0): data = data.get_data() image = np.frombuffer(data, np.uint8) image.shape = (self.color_tex.getYSize(), self.color_tex.getXSize(), self.color_tex.getNumComponents()) image = np.flipud(image) image = image[ ..., : -1] # remove alpha channel; if alpha values are needed, set alpha bits to 8 images.append(image) if self.depth_tex: depth_data = self.depth_tex.getRamImage() if sys.version_info < (3, 0): depth_data = depth_data.get_data() depth_image_size = self.depth_tex.getYSize( ) * self.depth_tex.getXSize() * self.depth_tex.getNumComponents() if len(depth_data) == 2 * depth_image_size: dtype = np.float16 elif len(depth_data) == 3 * depth_image_size: dtype = np.float24 elif len(depth_data) == 4 * depth_image_size: dtype = np.float32 else: raise ValueError( "Depth data has %d bytes but the size of the depth image is %d" % (len(depth_data), depth_image_size)) depth_image = np.frombuffer(depth_data, dtype) depth_image.shape = (self.depth_tex.getYSize(), self.depth_tex.getXSize(), self.depth_tex.getNumComponents()) depth_image = np.flipud(depth_image) depth_image = depth_image.astype( np.float32, copy=False) # copy only if necessary images.append(depth_image) return tuple(images)
def getMipMaps(mesh): mipmaps = {} for effect in mesh.effects: for prop in effect.supported: propval = getattr(effect, prop) if isinstance(propval, collada.material.Map): image_name = propval.sampler.surface.image.path image_data = propval.sampler.surface.image.data try: im = Image.open(StringIO(image_data)) im.load() except IOError: from panda3d.core import Texture from panda3d.core import StringStream from panda3d.core import PNMImage #PIL failed, so lets try DDS reader with panda3d t = Texture(image_name) success = t.readDds(StringStream(image_data)) if success == 0: raise FilterException("Failed to read image file %s" % image_name) #convert DDS to PNG outdata = t.getRamImageAs('RGBA').getData() try: im = Image.fromstring('RGBA', (t.getXSize(), t.getYSize()), outdata) im.load() except IOError: raise FilterException("Failed to read image file %s" % image_name) #Keep JPG in same format since JPG->PNG is pretty bad if im.format == 'JPEG': output_format = 'JPEG' output_extension = 'jpg' output_options = {'quality': 95, 'optimize': True} else: output_format = 'PNG' output_extension = 'png' output_options = {'optimize': True} #store a copy to the original image so we can resize from it directly each time orig_im = im width, height = im.size #round down to power of 2 width = int(math.pow(2, int(math.log(width, 2)))) height = int(math.pow(2, int(math.log(height, 2)))) pil_images = [] while True: im = orig_im.resize((width, height), Image.ANTIALIAS) pil_images.insert(0, im) if width == 1 and height == 1: break width = max(width / 2, 1) height = max(height / 2, 1) tar_buf = StringIO() tar = tarfile.TarFile(fileobj=tar_buf, mode='w') cur_offset = 0 byte_ranges = [] for i, pil_img in enumerate(pil_images): buf = StringIO() pil_img.save(buf, output_format, **output_options) file_len = buf.tell() cur_name = '%dx%d.%s' % (pil_img.size[0], pil_img.size[1], output_extension) tar_info = tarfile.TarInfo(name=cur_name) tar_info.size = file_len buf.seek(0) tar.addfile(tarinfo=tar_info, fileobj=buf) #tar files have a 512 byte header cur_offset += 512 file_start = cur_offset byte_ranges.append({ 'offset': file_start, 'length': file_len, 'width': pil_img.size[0], 'height': pil_img.size[1] }) #file lengths are rounded up to nearest 512 multiple file_len = 512 * ((file_len + 512 - 1) / 512) cur_offset += file_len tar.close() mipmaps[propval.sampler.surface.image.path] = ( tar_buf.getvalue(), byte_ranges) return mipmaps
class Typist(object): TARGETS = { 'paper': { 'model': 'paper', 'textureRoot': 'Front', 'scale': Point3(0.85, 0.85, 1), 'hpr' : Point3(0, 0, 0), } } def __init__(self, base, typewriterNP, underDeskClip, sounds): self.base = base self.sounds = sounds self.underDeskClip = underDeskClip self.typeIndex = 0 self.typewriterNP = typewriterNP self.rollerAssemblyNP = typewriterNP.find("**/roller assembly") assert self.rollerAssemblyNP self.rollerNP = typewriterNP.find("**/roller") assert self.rollerNP self.carriageNP = typewriterNP.find("**/carriage") assert self.carriageNP self.baseCarriagePos = self.carriageNP.getPos() self.carriageBounds = self.carriageNP.getTightBounds() self.font = base.loader.loadFont('Harting.ttf', pointSize=32) self.pnmFont = PNMTextMaker(self.font) self.fontCharSize, _, _ = fonts.measureFont(self.pnmFont, 32) print "font char size: ",self.fontCharSize self.pixelsPerLine = int(round(self.pnmFont.getLineHeight())) self.target = None """ panda3d.core.NodePath """ self.targetRoot = None """ panda3d.core.NodePath """ self.paperY = 0.0 """ range from 0 to 1 """ self.paperX = 0.0 """ range from 0 to 1 """ self.createRollerBase() self.tex = None self.texImage = None self.setupTexture() self.scheduler = Scheduler() task = self.base.taskMgr.add(self.tick, 'timerTask') task.setDelay(0.01) def tick(self, task): self.scheduler.tick(globalClock.getRealTime()) return task.cont def setupTexture(self): """ This is the overlay/decal/etc. which contains the typed characters. The texture size and the font size are currently tied together. :return: """ self.texImage = PNMImage(1024, 1024) self.texImage.addAlpha() self.texImage.fill(1.0) self.texImage.alphaFill(1.0) self.tex = Texture('typing') self.tex.setMagfilter(Texture.FTLinear) self.tex.setMinfilter(Texture.FTLinear) self.typingStage = TextureStage('typing') self.typingStage.setMode(TextureStage.MModulate) self.tex.load(self.texImage) # ensure we can quickly update subimages self.tex.setKeepRamImage(True) # temp for drawing chars self.chImage = PNMImage(*self.fontCharSize) def drawCharacter(self, ch, px, py): """ Draw a character onto the texture :param ch: :param px: paperX :param py: paperY :return: the paper-relative size of the character """ h = self.fontCharSize[1] if ch != ' ': # position -> pixel, applying margins x = int(self.tex.getXSize() * (px * 0.8 + 0.1)) y = int(self.tex.getYSize() * (py * 0.8 + 0.1)) # always draw onto the paper, to capture # incremental character overstrikes self.pnmFont.generateInto(ch, self.texImage, x, y) if False: #print ch,"to",x,y,"w=",g.getWidth() self.tex.load(self.texImage) else: # copy an area (presumably) encompassing the character g = self.pnmFont.getGlyph(ord(ch)) cx, cy = self.fontCharSize # a glyph is minimally sized and "moves around" in its text box # (think ' vs. ,), so it has been drawn somewhere relative to # the 'x' and 'y' we wanted. x += g.getLeft() y -= g.getTop() self.chImage.copySubImage( self.texImage, 0, 0, # from x, y, # to cx, cy # size ) self.tex.loadSubImage(self.chImage, x, y) # toggle for a typewriter that uses non-proportional spacing #w = self.paperCharWidth(g.getWidth()) w = self.paperCharWidth() else: w = self.paperCharWidth() return w, h def start(self): self.target = None self.setTarget('paper') self.hookKeyboard() def createRollerBase(self): """ The paper moves such that it is tangent to the roller. This nodepath keeps a coordinate space relative to that, so that the paper can be positioned from (0,0,0) to (0,0,1) to "roll" it along the roller. """ bb = self.rollerNP.getTightBounds() #self.rollerNP.showTightBounds() self.paperRollerBase = self.rollerAssemblyNP.attachNewNode('rollerBase') self.paperRollerBase.setHpr(0, -20, 0) print "roller:",bb rad = abs(bb[0].y - bb[1].y) / 2 center = Vec3(-(bb[0].x+bb[1].x)/2 - 0.03, (bb[0].y-bb[1].y)/2, (bb[0].z+bb[1].z)/2) self.paperRollerBase.setPos(center) def setTarget(self, name): if self.target: self.target.removeNode() # load and transform the model target = self.TARGETS[name] self.target = self.base.loader.loadModel(target['model']) #self.target.setScale(target['scale']) self.target.setHpr(target['hpr']) # put it in the world self.target.reparentTo(self.paperRollerBase) rbb = self.rollerNP.getTightBounds() tbb = self.target.getTightBounds() rs = (rbb[1] - rbb[0]) ts = (tbb[1] - tbb[0]) self.target.setScale(rs.x / ts.x, 1, 1) # apply the texture self.targetRoot = self.target if 'textureRoot' in target: self.targetRoot = self.target.find("**/" + target['textureRoot']) assert self.targetRoot self.targetRoot.setTexture(self.typingStage, self.tex) #self.setupTargetClip() # reset self.paperX = self.paperY = 0. newPos = self.calcPaperPos(self.paperY) self.target.setPos(newPos) self.moveCarriage() def setupTargetClip(self): """ The target is fed in to the typewriter but until we invent "geom curling", it shouldn't be visible under the typewriter under the desk. The @underDeskClip node has a world-relative bounding box, which we can convert to the target-relative bounding box, and pass to a shader that can clip the nodes. """ shader = Shader.make( Shader.SLGLSL, """ #version 120 attribute vec4 p3d_MultiTexCoord0; attribute vec4 p3d_MultiTexCoord1; void main() { gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; gl_TexCoord[0] = p3d_MultiTexCoord0; gl_TexCoord[1] = p3d_MultiTexCoord1; } """, """ #version 120 uniform sampler2D baseTex; uniform sampler2D charTex; const vec4 zero = vec4(0, 0, 0, 0); const vec4 one = vec4(1, 1, 1, 1); const vec4 half = vec4(0.5, 0.5, 0.5, 0); void main() { vec4 baseColor = texture2D(baseTex, gl_TexCoord[0].st); vec4 typeColor = texture2D(charTex, gl_TexCoord[1].st); gl_FragColor = baseColor * typeColor; }""" ) self.target.setShader(shader) baseTex = self.targetRoot.getTexture() print "Base Texture:",baseTex self.target.setShaderInput("baseTex", baseTex) self.target.setShaderInput("charTex", self.tex) def hookKeyboard(self): """ Hook events so we can respond to keypresses. """ self.base.buttonThrowers[0].node().setKeystrokeEvent('keystroke') self.base.accept('keystroke', self.schedTypeCharacter) self.base.accept('backspace', self.schedBackspace) self.base.accept('arrow_up', lambda: self.schedAdjustPaper(-5)) self.base.accept('arrow_up-repeat', lambda: self.schedAdjustPaper(-1)) self.base.accept('arrow_down', lambda:self.schedAdjustPaper(5)) self.base.accept('arrow_down-repeat', lambda:self.schedAdjustPaper(1)) self.base.accept('arrow_left', lambda: self.schedAdjustCarriage(-1)) self.base.accept('arrow_left-repeat', lambda: self.schedAdjustCarriage(-1)) self.base.accept('arrow_right', lambda:self.schedAdjustCarriage(1)) self.base.accept('arrow_right-repeat', lambda:self.schedAdjustCarriage(1)) def paperCharWidth(self, pixels=None): if not pixels: pixels = self.fontCharSize[0] return float(pixels) / self.tex.getXSize() def paperLineHeight(self): return float(self.fontCharSize[1] * 1.2) / self.tex.getYSize() def schedScroll(self): if self.scheduler.isQueueEmpty(): self.schedRollPaper(1) self.schedResetCarriage() def schedBackspace(self): if self.scheduler.isQueueEmpty(): def doit(): if self.paperX > 0: self.schedAdjustCarriage(-1) self.scheduler.schedule(0.01, doit) def createMoveCarriageInterval(self, newX, curX=None): if curX is None: curX = self.paperX here = self.calcCarriage(curX) there = self.calcCarriage(newX) posInterval = LerpPosInterval( self.carriageNP, abs(newX - curX), there, startPos = here, blendType='easeIn') posInterval.setDoneEvent('carriageReset') def isReset(): self.paperX = newX self.base.acceptOnce('carriageReset', isReset) return posInterval def schedResetCarriage(self): if self.paperX > 0.1: self.sounds['pullback'].play() invl = self.createMoveCarriageInterval(0) self.scheduler.scheduleInterval(0, invl) def calcCarriage(self, paperX): """ Calculate where the carriage should be offset based on the position on the paper :param paperX: 0...1 :return: pos for self.carriageNP """ x = (0.5 - paperX) * 0.69 * 0.8 + 0.01 bb = self.carriageBounds return self.baseCarriagePos + Point3(x * (bb[1].x-bb[0].x), 0, 0) def moveCarriage(self): pos = self.calcCarriage(self.paperX) self.carriageNP.setPos(pos) def schedMoveCarriage(self, curX, newX): if self.scheduler.isQueueEmpty(): #self.scheduler.schedule(0.1, self.moveCarriage) invl = self.createMoveCarriageInterval(newX, curX=curX) invl.start() def schedAdjustCarriage(self, bx): if self.scheduler.isQueueEmpty(): def doit(): self.paperX = max(0.0, min(1.0, self.paperX + bx * self.paperCharWidth())) self.moveCarriage() self.scheduler.schedule(0.1, doit) def calcPaperPos(self, paperY): # center over roller, peek out a little z = paperY * 0.8 - 0.5 + 0.175 bb = self.target.getTightBounds() return Point3(-0.5, 0, z * (bb[1].z-bb[0].z)) def createMovePaperInterval(self, newY): here = self.calcPaperPos(self.paperY) there = self.calcPaperPos(newY) posInterval = LerpPosInterval( self.target, abs(newY - self.paperY), there, startPos = here, blendType='easeInOut') posInterval.setDoneEvent('scrollDone') def isDone(): self.paperY = newY self.base.acceptOnce('scrollDone', isDone) return posInterval def schedAdjustPaper(self, by): if self.scheduler.isQueueEmpty(): def doit(): self.schedRollPaper(by) self.scheduler.schedule(0.1, doit) def schedRollPaper(self, by): """ Position the paper such that @percent of it is rolled over roller :param percent: :return: """ def doit(): self.sounds['scroll'].play() newY = min(1.0, max(0.0, self.paperY + self.paperLineHeight() * by)) invl = self.createMovePaperInterval(newY) invl.start() self.scheduler.schedule(0.1, doit) def schedTypeCharacter(self, keyname): # filter for visibility if ord(keyname) == 13: self.schedScroll() elif ord(keyname) >= 32 and ord(keyname) != 127: if self.scheduler.isQueueEmpty(): curX, curY = self.paperX, self.paperY self.typeCharacter(keyname, curX, curY) def typeCharacter(self, ch, curX, curY): newX = curX w, h = self.drawCharacter(ch, curX, curY) newX += w if ch != ' ': # alternate typing sound #self.typeIndex = (self.typeIndex+1) % 3 self.typeIndex = random.randint(0, 2) self.sounds['type' + str(self.typeIndex+1)].play() else: self.sounds['advance'].play() if newX >= 1: self.sounds['bell'].play() newX = 1 self.schedMoveCarriage(self.paperX, newX) # move first, to avoid overtype self.paperX = newX
class MyApp(ShowBase): def __init__(self): ShowBase.__init__(self) # Load the environment model. self.setup_environment() #self.scene = self.loader.loadModel("models/environment") # Reparent the model to render. #self.scene.reparentTo(self.render) # Apply scale and position transforms on the model. #self.scene.setScale(0.25, 0.25, 0.25) #self.scene.setPos(-8, 42, 0) # Needed for camera image self.dr = self.camNode.getDisplayRegion(0) # Needed for camera depth image winprops = WindowProperties.size(self.win.getXSize(), self.win.getYSize()) fbprops = FrameBufferProperties() fbprops.setDepthBits(1) self.depthBuffer = self.graphicsEngine.makeOutput( self.pipe, "depth buffer", -2, fbprops, winprops, GraphicsPipe.BFRefuseWindow, self.win.getGsg(), self.win) self.depthTex = Texture() self.depthTex.setFormat(Texture.FDepthComponent) self.depthBuffer.addRenderTexture(self.depthTex, GraphicsOutput.RTMCopyRam, GraphicsOutput.RTPDepth) lens = self.cam.node().getLens() lens.setFov(90.0, 90.0) # the near and far clipping distances can be changed if desired # lens.setNear(5.0) # lens.setFar(500.0) self.depthCam = self.makeCamera(self.depthBuffer, lens=lens, scene=self.render) self.depthCam.reparentTo(self.cam) # TODO: Scene is rendered twice: once for rgb and once for depth image. # How can both images be obtained in one rendering pass? self.render.setAntialias(AntialiasAttrib.MAuto) def setup_environment(self): # encapsulate some stuff # set up ambient lighting self.alight = AmbientLight('alight') self.alight.setColor(VBase4(0.1, 0.1, 0.1, 1)) self.alnp = self.render.attachNewNode(self.alight) self.render.setLight(self.alnp) # set up a point light self.plight = PointLight('plight') self.plight.setColor(VBase4(0.8, 0.8, 0.8, 1)) self.plnp = self.render.attachNewNode(self.plight) self.plnp.setPos(0, 0, 100) self.render.setLight(self.plnp) # set up terrain model self.terr_material = Material() self.terr_material.setShininess(1.0) self.terr_material.setAmbient(VBase4(0, 0, 0, 0)) self.terr_material.setDiffuse(VBase4(1, 1, 1, 1)) self.terr_material.setEmission(VBase4(0, 0, 0, 0)) self.terr_material.setSpecular(VBase4(0, 0, 0, 0)) # general scaling self.trrHorzSc = 4.0 self.trrVertSc = 4.0 # was 4.0 # Create sky #terrctr = self.trrHorzSc*65.0 #self.setup_skybox(terrctr,800.0,2.0,0.3) self.skysphere = self.loader.loadModel("sky-forest/SkySphere.bam") self.skysphere.setBin('background', 1) self.skysphere.setDepthWrite(0) self.skysphere.reparentTo(self.render) # Load some textures self.grsTxtSc = 5 self.numTreeTexts = 7 # ground texture self.txtGrass = self.loader.loadTexture('tex/ground005.png') self.txtGrass.setWrapU(Texture.WM_mirror) self.txtGrass.setWrapV(Texture.WM_mirror) self.txtGrass.setMagfilter(Texture.FTLinear) self.txtGrass.setMinfilter(Texture.FTLinearMipmapLinear) # set up terrain texture stages self.TS1 = TextureStage('terrtext') self.TS1.setSort(0) self.TS1.setMode(TextureStage.MReplace) # Set up the GeoMipTerrain self.terrain = GeoMipTerrain("myDynamicTerrain") img = PNMImage(Filename('tex/bowl_height_map.png')) self.terrain.setHeightfield(img) self.terrain.setBruteforce(0) self.terrain.setAutoFlatten(GeoMipTerrain.AFMMedium) # Set terrain properties self.terrain.setBlockSize(32) self.terrain.setNear(50) self.terrain.setFar(500) self.terrain.setFocalPoint(self.camera) # Store the root NodePath for convenience self.root = self.terrain.getRoot() self.root.clearTexture() self.root.setTwoSided(0) self.root.setCollideMask(BitMask32.bit(0)) self.root.setSz(self.trrVertSc) self.root.setSx(self.trrHorzSc) self.root.setSy(self.trrHorzSc) self.root.setMaterial(self.terr_material) self.root.setTexture(self.TS1, self.txtGrass) self.root.setTexScale(self.TS1, self.grsTxtSc, self.grsTxtSc) offset = 0.5 * img.getXSize() * self.trrHorzSc - 0.5 self.root.setPos(-offset, -offset, 0) self.terrain.generate() self.root.reparentTo(self.render) # load tree billboards self.txtTreeBillBoards = [] for a in range(self.numTreeTexts): fstr = 'trees/tree' + '%03d' % (a + 991) self.txtTreeBillBoards.append( \ self.loader.loadTexture(fstr + '-color.png', fstr + '-opacity.png')) self.txtTreeBillBoards[a].setMinfilter( Texture.FTLinearMipmapLinear) #self.placePlantOnTerrain('trees',300,0,20,20,self.trrHorzSc,self.trrVertSc, \ # self.numTreeTexts,self.txtTreeBillBoards,'scene-def/trees.txt') self.setup_house() self.setup_vehicle() self.taskMgr.add(self.skysphereTask, "SkySphere Task") def setup_house(self): # place farmhouse on terrain self.house = ModelNode('house1') self.loadModelOntoTerrain(self.render, self.terrain, self.house, 43.0, 0.275, 0.0, 0.0, self.trrHorzSc, self.trrVertSc, 'models/FarmHouse', Vec3(0, 0, 0), Point3(-12.0567, -29.1724, 0.0837742), Point3(12.2229, 21.1915, 21.3668)) def setup_vehicle(self): # place HMMWV on terrain self.hmmwv = ModelNode('hmmwv1') self.loadModelOntoTerrain(self.render, self.terrain, self.hmmwv, 33.0, 1.0, 20.0, 24.0, self.trrHorzSc, self.trrVertSc, 'models/hmmwv', Vec3(0, -90, 0), Point3(-1.21273, -2.49153, -1.10753), Point3(1.21273, 2.49153, 1.10753)) def setup_skybox(self, terrctr=645.0, boxsz=1000.0, aspect=1.0, uplift=0.0): vsz = boxsz / aspect self.bckgtx = [] self.bckgtx.append(self.loader.loadTexture('sky/Back2.png')) self.bckgtx.append(self.loader.loadTexture('sky/Right2.png')) self.bckgtx.append(self.loader.loadTexture('sky/Front2.png')) self.bckgtx.append(self.loader.loadTexture('sky/Left2.png')) self.bckgtx.append(self.loader.loadTexture('sky/Up.png')) for a in range(4): self.bckg = CardMaker('bkcard') lr = Point3(0.5 * boxsz, 0.5 * boxsz, -0.5 * vsz) ur = Point3(0.5 * boxsz, 0.5 * boxsz, 0.5 * vsz) ul = Point3(-0.5 * boxsz, 0.5 * boxsz, 0.5 * vsz) ll = Point3(-0.5 * boxsz, 0.5 * boxsz, -0.5 * vsz) self.bckg.setFrame(ll, lr, ur, ul) self.bckg.setHasNormals(0) self.bckg.setHasUvs(1) #self.bckg.setUvRange(self.bckgtx[a]) bkcrd = self.render.attachNewNode(self.bckg.generate()) bkcrd.setTexture(self.bckgtx[a]) self.bckgtx[a].setWrapU(Texture.WMClamp) self.bckgtx[a].setWrapV(Texture.WMClamp) bkcrd.setLightOff() bkcrd.setFogOff() bkcrd.setHpr(90.0 * a, 0, 0) cz = 0.5 * boxsz * uplift #print 'set card at:', terrctr,terrctr,cz, ' with points: ', lr,ur,ul,ll bkcrd.setPos(terrctr, terrctr, cz) self.top = CardMaker('bkcard') lr = Point3(0.5 * boxsz, -0.5 * boxsz, 0) ur = Point3(0.5 * boxsz, 0.5 * boxsz, 0) ul = Point3(-0.5 * boxsz, 0.5 * boxsz, 0) ll = Point3(-0.5 * boxsz, -0.5 * boxsz, 0) self.top.setFrame(ll, lr, ur, ul) self.top.setHasNormals(0) self.top.setHasUvs(1) #self.top.setUvRange(self.bckgtx[4]) bkcrd = self.render.attachNewNode(self.bckg.generate()) bkcrd.setTexture(self.bckgtx[4]) self.bckgtx[4].setWrapU(Texture.WMClamp) self.bckgtx[4].setWrapV(Texture.WMClamp) bkcrd.setLightOff() bkcrd.setFogOff() bkcrd.setHpr(0, 90, 90) bkcrd.setPos(terrctr, terrctr, 0.5 * vsz + 0.5 * boxsz * uplift) def placePlantOnTerrain(self, itemStr, itemCnt, Mode, typItemWidth, typItemHeight, trrHorzSc, trrVertSc, numTxtTypes, txtList, planFileName): # Billboarding plants crd = CardMaker('mycard') crd.setColor(0.5, 0.5, 0.5, 1) ll = Point3(-0.5 * typItemWidth, 0, 0) lr = Point3(0.5 * typItemWidth, 0, 0) ur = Point3(0.5 * typItemWidth, 0, typItemHeight) ul = Point3(-0.5 * typItemWidth, 0, typItemHeight) crd.setFrame(ll, lr, ur, ul) crd.setHasNormals(False) crd.setHasUvs(True) # generate/save/load locations try: plan_data_fp = open(planFileName, 'r') item_list = [] for line in plan_data_fp: toks = line.split(',') px = float(toks[0].strip(' ')) py = float(toks[1].strip(' ')) ang = float(toks[2].strip(' ')) dht = float(toks[3].strip(' ')) scl = float(toks[4].strip(' ')) idx = int(toks[5].strip(' ')) item_list.append((px, py, ang, dht, scl, idx)) plan_data_fp.close() print 'loaded ', itemStr, ' data file of size:', len(item_list) except IOError: # generate list and try to save item_list = [] for a in range(itemCnt): px = random.randrange(-self.trrHorzSc * 64, self.trrHorzSc * 64) py = random.randrange(-self.trrHorzSc * 64, self.trrHorzSc * 64) ang = 180 * random.random() dht = 0.0 scl = 0.75 + 0.25 * (random.random() + random.random()) idx = random.randrange(0, numTxtTypes) item_list.append([px, py, ang, dht, scl, idx]) try: plan_data_fp = open(planFileName, 'w') for c in item_list: print >> plan_data_fp, c[0], ',', c[1], ',', c[2], ',', c[ 3], ',', c[4], ',', c[5] plan_data_fp.close() print 'saved ', itemStr, ' data of size: ', len(item_list) except IOError: print 'unable to store ', itemStr, ' data of size: ', len( item_list) # define each plant for c in item_list: px = c[0] py = c[1] ang = c[2] dht = c[3] scl = c[4] idx = c[5] if idx >= numTxtTypes: idx = 0 if Mode > 0: for b in range(Mode): crdNP = self.render.attachNewNode(crd.generate()) crdNP.setTexture(txtList[idx]) crdNP.setScale(scl) crdNP.setTwoSided(True) ht = self.terrain.getElevation(px / trrHorzSc, py / trrHorzSc) crdNP.setPos(px, py, ht * trrVertSc + dht) crdNP.setHpr(ang + (180 / Mode) * b, 0, 0) crdNP.setTransparency(TransparencyAttrib.MAlpha) crdNP.setLightOff() else: # set up item as defined crd.setUvRange(txtList[idx]) crdNP = self.render.attachNewNode(crd.generate()) crdNP.setBillboardAxis() crdNP.setTexture(txtList[idx]) crdNP.setScale(scl) ht = self.terrain.getElevation(px / trrHorzSc, py / trrHorzSc) crdNP.setPos(px, py, ht * trrVertSc) crdNP.setTransparency(TransparencyAttrib.MAlpha) crdNP.setLightOff() def loadModelOntoTerrain(self, render_node, terr_obj, model_obj, hdg, scl, xctr, yctr, terr_horz_sc, terr_vert_sc, model_path, rotA, minP, maxP): # load model onto terrain hdg_rads = hdg * math.pi / 180.0 model_obj = self.loader.loadModel(model_path) rotAll = rotA rotAll.setX(rotAll.getX() + hdg) model_obj.setHpr(rotA) model_obj.setLightOff() # if model changes, these will have to be recomputed # minP = Point3(0,0,0) # maxP = Point3(0,0,0) # model_obj.calcTightBounds(minP,maxP) print minP print maxP htl = [] maxzofs = -1000.0 for xi in [minP[0], maxP[0]]: for yi in [minP[1], maxP[1]]: tx = xctr + scl * xi * math.cos(hdg_rads) ty = yctr + scl * yi * math.sin(hdg_rads) tht = self.terrain.getElevation(tx / terr_horz_sc, ty / terr_horz_sc) print 'tx=', tx, ', ty=', ty, ', tht=', tht htl.append(tht * terr_vert_sc - minP.getZ()) for hi in htl: if hi > maxzofs: maxzofs = hi print maxzofs model_obj.setPos(xctr, yctr, maxzofs) model_obj.setHpr(rotAll) model_obj.setScale(scl) model_obj.reparentTo(render_node) return maxzofs, minP, maxP def get_camera_image(self, requested_format=None): """ Returns the camera's image, which is of type uint8 and has values between 0 and 255. The 'requested_format' argument should specify in which order the components of the image must be. For example, valid format strings are "RGBA" and "BGRA". By default, Panda's internal format "BGRA" is used, in which case no data is copied over. """ tex = self.dr.getScreenshot() if requested_format is None: data = tex.getRamImage() else: data = tex.getRamImageAs(requested_format) image = np.frombuffer( data.get_data(), np.uint8) # use data.get_data() instead of data in python 2 image.shape = (tex.getYSize(), tex.getXSize(), tex.getNumComponents()) image = np.flipud(image) return image def get_camera_depth_image(self): """ Returns the camera's depth image, which is of type float32 and has values between 0.0 and 1.0. """ data = self.depthTex.getRamImage() depth_image = np.frombuffer(data.get_data(), np.float32) depth_image.shape = (self.depthTex.getYSize(), self.depthTex.getXSize(), self.depthTex.getNumComponents()) depth_image = np.flipud(depth_image) ''' Surface position can be inferred by calculating backward from the depth buffer. Each pixel on the screen represents a ray from the camera into the scene, and the depth value in the pixel indicates a distance along the ray. Because of this, it is not actually necessary to store surface position explicitly - it is only necessary to store depth values. Of course, OpenGL does that for free. So the framebuffer now needs to store surface normal, diffuse color, and depth value (to infer surface position). In practice, most ordinary framebuffers can only store color and depth - they don't have any place to store a third value. So we need to use a special offscreen buffer with an "auxiliary" bitplane. The auxiliary bitplane stores the surface normal. So then, there's the final postprocessing pass. This involves combining the diffuse color texture, the surface normal texture, the depth texture, and the light parameters into a final rendered output. The light parameters are passed into the postprocessing shader as constants, not as textures. If there are a lot of lights, things get interesting. You use one postprocessing pass per light. Each pass only needs to scan those framebuffer pixels that are actually in range of the light in question. To traverse only the pixels that are affected by the light, just render the illuminated area's convex bounding volume. The shader to store the diffuse color and surface normal is trivial. But the final postprocessing shader is a little complicated. What makes it tricky is that it needs to regenerate the original surface position from the screen position and depth value. The math for that deserves some explanation. We need to take a clip-space coordinate and depth-buffer value (ClipX,ClipY,ClipZ,ClipW) and unproject it back to a view-space (ViewX,ViewY,ViewZ) coordinate. Lighting is then done in view-space. Okay, so here's the math. Panda uses the projection matrix to transform view-space into clip-space. But in practice, the projection matrix for a perspective camera always contains four nonzero constants, and they're always in the same place: -- here are the non-zero elements of the projection matrix -- A 0 0 0 0 0 B 1 0 C 0 0 0 0 D 0 -- precompute these from above projection matrix -- ''' proj = self.cam.node().getLens().getProjectionMat() proj_x = 0.5 * proj.getCell(3, 2) / proj.getCell(0, 0) proj_y = 0.5 * proj.getCell(3, 2) proj_z = 0.5 * proj.getCell(3, 2) / proj.getCell(2, 1) proj_w = -0.5 - 0.5 * proj.getCell(1, 2) ''' -- now for each pixel compute viewpoint coordinates -- viewx = (screenx * projx) / (depth + projw) viewy = (1 * projy) / (depth + projw) viewz = (screeny * projz) / (depth + projw) ''' grid = np.mgrid[0:depth_image.shape[0], 0:depth_image.shape[1]] ygrid = np.float32(np.squeeze( grid[0, :, :])) / float(depth_image.shape[0] - 1) ygrid -= 0.5 xgrid = np.float32(np.squeeze( grid[1, :, :])) / float(depth_image.shape[1] - 1) xgrid -= 0.5 xview = 2.0 * xgrid * proj_x zview = 2.0 * ygrid * proj_z denom = np.squeeze(depth_image) + proj_w xview = xview / denom yview = proj_y / denom zview = zview / denom sqrng = xview**2 + yview**2 + zview**2 range_image = np.sqrt(sqrng) range_image_1 = np.expand_dims(range_image, axis=2) return depth_image, range_image_1 def compute_sample_pattern(self, limg_shape, res_factor): # assume velocity is XYZ and we are looking +X up and towards -Z pattern = [] lens = self.cam.node().getLens() sx = self.win.getXSize() sy = self.win.getYSize() ifov_vert = 2.0 * math.tan( 0.5 * math.radians(lens.getVfov())) / float(sy - 1) ifov_horz = 2.0 * math.tan( 0.5 * math.radians(lens.getHfov())) / float(sx - 1) #ifov_vert = lens.getVfov() / float(sy-1) #ifov_horz = lens.getHfov() / float(sy-1) for ldr_row in range(limg_shape[0]): theta = -10.0 - 41.33 * ( float(ldr_row) / float(limg_shape[0] - 1) - 0.5) for ldr_col in range(limg_shape[1]): psi = 60.0 * (float(ldr_col) / float(limg_shape[1] - 1) - 0.5) cpsi = math.cos(math.radians(psi)) vert_ang = theta / cpsi img_row_flt = (0.5 * float(sy - 1) - (math.tan(math.radians(vert_ang)) / ifov_vert)) #img_row_flt = 0.5*(sy-1) - (vert_ang / ifov_vert) if img_row_flt < 0: print('img_row_flt=%f' % img_row_flt) img_row_flt = 0.0 if img_row_flt >= sy: print('img_row_flt=%f' % img_row_flt) img_row_flt = float(sy - 1) img_col_flt = (0.5 * float(sx - 1) + (math.tan(math.radians(psi)) / ifov_horz)) #img_col_flt = 0.5*(sx-1) + (psi / ifov_horz) if img_col_flt < 0: print('img_col_flt=%f' % img_col_flt) img_col_flt = 0.0 if img_col_flt >= sx: print('img_col_flt=%f' % img_col_flt) img_col_flt = float(sx - 1) pattern.append((ldr_row, ldr_col, img_row_flt, img_col_flt)) return pattern def find_sorted_ladar_returns(self, rangearr, intensarr, ks_m): my_range = rangearr.copy() my_inten = intensarr.copy() ''' pixels data is organized by: [0] starting range of this return [1] ending range of this return [2] peak range of this return [3] total intensity of this return ''' int_mult = len(my_inten) pixels = map( list, zip(my_range.tolist(), my_range.tolist(), my_range.tolist(), my_inten.tolist())) spix = sorted(pixels, key=lambda x: x[0]) done = False while not done: mxpi = len(spix) if mxpi > 2: mindel = 1e20 mnidx = None for pidx in range(mxpi - 1): rdel = spix[pidx + 1][0] - spix[pidx][1] # must be within ks_m meters in range to merge if (rdel < ks_m) and (rdel < mindel): mindel = rdel mnidx = pidx # merge best two returns if mnidx is not None: # new range span for testing against neighbors spix[mnidx][1] = spix[mnidx + 1][1] # new peak range is range of max contributor if spix[mnidx + 1][3] > spix[mnidx][3]: spix[mnidx][2] = spix[mnidx + 1][2] # intensity of return is sum of contributors spix[mnidx][3] += spix[mnidx + 1][3] # remove one of the two merged del spix[mnidx + 1] else: done = True else: done = True # now eliminate all but max and last returns max_idx = None max_val = 0.0 for ci, pix in enumerate(spix): if pix[3] > max_val: max_val = pix[3] / int_mult max_idx = ci # if they are the same, return only one if spix[-1][3] >= spix[max_idx][3]: return [spix[-1]] else: return [spix[max_idx], spix[-1]] def sample_range_image(self, rng_img, int_img, limg_shape, vel_cam, pps, ldr_err, pattern): # depth image is set up as 512 x 512 and is 62.5 degrees vertical FOV # the center row is vertical, but we want to sample from the # region corresponding to HDL-32 FOV: from +10 to -30 degrees detailed_sensor_model = False fwd_vel = vel_cam[1] beam_div = 0.002 lens = self.cam.node().getLens() #sx = self.win.getXSize() sy = self.win.getYSize() ifov_vert = 2.0 * math.tan( 0.5 * math.radians(lens.getVfov())) / float(sy - 1) #ifov_horz = 2.0*math.tan(0.5*math.radians(lens.getHfov()))/float(sx-1) #ifov = math.radians(self.cam.node().getLens().getVfov() / self.win.getYSize()) sigma = beam_div / ifov_vert hs = int(2.0 * sigma + 1.0) gprof = gauss_kern(sigma, hs, normalize=False) rimg = np.zeros(limg_shape, dtype=np.float32) iimg = np.zeros(limg_shape, dtype=np.float32) margin = 10.0 for pidx, relation in enumerate(pattern): # get the usual scan pattern sample ldr_row, ldr_col, img_row_flt, img_col_flt = relation if ((img_row_flt > -margin) and (img_col_flt > -margin) and (img_row_flt < rng_img.shape[0] + margin) and (img_col_flt < rng_img.shape[1] + margin)): # within reasonable distance from image limits img_row = int(round(img_row_flt)) img_col = int(round(img_col_flt)) # motion compensation trng = np.float32(rng_img[img_row, img_col]) if trng > 0.0: # TODO: change this back to False done = True ic = 0 while not done: old_trng = trng del_row = pidx * fwd_vel / (ifov_vert * trng * pps) if (abs(del_row) > 1e-1) and (ic < 10): img_row_f = img_row_flt + del_row img_row = int(round(img_row_f)) trng = np.float32(rng_img[img_row, img_col]) ic += 1 if abs(trng - old_trng) < 0.5: done = True else: done = True # simple sensor processing: just sample from large images rimg[ldr_row, ldr_col] = np.float32(rng_img[img_row, img_col]) iimg[ldr_row, ldr_col] = np.float32(int_img[img_row, img_col]) if detailed_sensor_model: # detailed model subsamples whole beam width gpatch = copy_patch_centered((img_row, img_col), hs, int_img, 0.0) gpatch = np.float32(gpatch) gpatch *= gprof rpatch = copy_patch_centered((img_row, img_col), hs, rng_img, 0.0) rpatch = np.squeeze(rpatch) valid = rpatch > 1e-3 if np.count_nonzero(valid) > 0: rpatch_ts = rpatch[valid] gpatch_ts = gpatch[valid] returns = self.find_sorted_ladar_returns( rpatch_ts, gpatch_ts, 2.5) # for now we just take first return rimg[ldr_row, ldr_col] = returns[0][2] iimg[ldr_row, ldr_col] = returns[0][3] else: rimg[ldr_row, ldr_col] = 0.0 iimg[ldr_row, ldr_col] = np.float32(int_img[img_row, img_col]) rimg += ldr_err * np.random.standard_normal(rimg.shape) return rimg, iimg def skysphereTask(self, task): if self.base is not None: self.skysphere.setPos(self.base.camera, 0, 0, 0) self.terrain.generate() return task.cont
class TextureHeightmapBase(Heightmap): def __init__(self, name, width, height, height_scale, u_scale, v_scale, median, interpolator): Heightmap.__init__(self, name, width, height, height_scale, u_scale, v_scale, median, interpolator) self.texture = None self.texture_offset = LVector2() self.texture_scale = LVector2(1, 1) self.tex_id = str(width) + ':' + str(height) def reset(self): self.texture = None def get_texture_offset(self, patch): return self.texture_offset def get_texture_scale(self, patch): return self.texture_scale return self.heightmap_ready def set_height(self, x, y, height): pass def get_height(self, x, y): if self.texture_peeker is None: print("No peeker") traceback.print_stack() return 0.0 new_x = x * self.texture_scale[0] + self.texture_offset[0] * self.width new_y = y * self.texture_scale[1] + self.texture_offset[1] * self.height new_x = min(new_x, self.width - 1) new_y = min(new_y, self.height - 1) height = self.interpolator.get_value(self.texture_peeker, new_x, new_y) return height * self.height_scale # + self.offset def create_heightmap(self, shape, callback=None, cb_args=()): return self.load(shape, callback, cb_args) def load(self, shape, callback, cb_args=()): if self.texture is None: self.texture = Texture() self.texture.set_wrap_u(Texture.WMClamp) self.texture.set_wrap_v(Texture.WMClamp) self.interpolator.configure_texture(self.texture) self.do_load(shape, self.heightmap_ready_cb, (callback, cb_args)) else: if callback is not None: callback(self, *cb_args) def heightmap_ready_cb(self, texture, callback, cb_args): #print("READY", self.patch.str_id()) self.texture_peeker = self.texture.peek() # if self.texture_peeker is None: # print("NOT READY !!!") self.heightmap_ready = True data = self.texture.getRamImage() if sys.version_info[0] < 3: buf = data.getData() np_buffer = numpy.fromstring(buf, dtype=numpy.float32) else: np_buffer = numpy.frombuffer(data, numpy.float32) np_buffer.shape = (self.texture.getYSize(), self.texture.getXSize(), self.texture.getNumComponents()) self.min_height = np_buffer.min() self.max_height = np_buffer.max() self.mean_height = np_buffer.mean() if callback is not None: callback(self, *cb_args) def do_load(self, shape, callback, cb_args): pass
class HeightmapPatch: cachable = True def __init__(self, parent, x0, y0, x1, y1, width, height, scale=1.0, coord=TexCoord.Cylindrical, face=-1, border=1): self.parent = parent self.x0 = x0 self.y0 = y0 self.x1 = x1 self.y1 = y1 self.scale = scale self.width = width self.height = height self.coord = coord self.face = face self.border = border self.r_width = self.width + self.border * 2 self.r_height = self.height + self.border * 2 self.r_x0 = self.x0 - float( self.border) / self.width * (self.x1 - self.x0) self.r_x1 = self.x1 + float( self.border) / self.width * (self.x1 - self.x0) self.r_y0 = self.y0 - float( self.border) / self.height * (self.y1 - self.y0) self.r_y1 = self.y1 + float( self.border) / self.height * (self.y1 - self.y0) self.dx = self.r_x1 - self.r_x0 self.dy = self.r_y1 - self.r_y0 self.lod = None self.patch = None self.heightmap_ready = False self.texture = None self.texture_peeker = None self.callback = None self.cloned = False self.texture_offset = LVector2() self.texture_scale = LVector2(1, 1) self.min_height = None self.max_height = None self.mean_height = None @classmethod def create_from_patch(cls, noise, parent, x, y, scale, lod, density, coord=TexCoord.Cylindrical, face=-1): #TODO: Should be move to Patch/Tile if coord == TexCoord.Cylindrical: r_div = 1 << lod s_div = 2 << lod x0 = float(x) / s_div y0 = float(y) / r_div x1 = float(x + 1) / s_div y1 = float(y + 1) / r_div elif coord == TexCoord.NormalizedCube or coord == TexCoord.SqrtCube: div = 1 << lod r_div = div s_div = div x0 = float(x) / div y0 = float(y) / div x1 = float(x + 1) / div y1 = float(y + 1) / div else: div = 1 << lod r_div = div s_div = div size = 1.0 / (1 << lod) x0 = (x) * scale y0 = (y) * scale x1 = (x + size) * scale y1 = (y + size) * scale patch = cls(noise, parent, x0, y0, x1, y1, width=density, height=density, scale=scale, coord=coord, face=face, border=1) patch.patch_lod = lod patch.lod = lod patch.lod_scale_x = scale / s_div patch.lod_scale_y = scale / r_div patch.density = density patch.x = x patch.y = y return patch def copy_from(self, heightmap_patch): self.cloned = True self.lod = heightmap_patch.lod self.texture = heightmap_patch.texture self.texture_peeker = heightmap_patch.texture_peeker self.heightmap_ready = heightmap_patch.heightmap_ready self.min_height = heightmap_patch.min_height self.max_height = heightmap_patch.max_height self.mean_height = heightmap_patch.mean_height def calc_sub_patch(self): self.copy_from(self.parent_heightmap) delta = self.patch.lod - self.lod scale = 1 << delta if self.patch.coord != TexCoord.Flat: x_tex = int(self.x / scale) * scale y_tex = int(self.y / scale) * scale x_delta = float(self.x - x_tex) / scale y_delta = float(self.y - y_tex) / scale else: x_tex = int(self.x * scale) / scale y_tex = int(self.y * scale) / scale x_delta = float(self.x - x_tex) y_delta = float(self.y - y_tex) self.texture_offset = LVector2(x_delta, y_delta) self.texture_scale = LVector2(1.0 / scale, 1.0 / scale) def is_ready(self): return self.heightmap_ready def set_height(self, x, y, height): pass def get_height(self, x, y): if self.texture_peeker is None: print("No peeker", self.patch.str_id(), self.patch.instance_ready) traceback.print_stack() return 0.0 new_x = x * self.texture_scale[0] + self.texture_offset[0] * self.width new_y = y * self.texture_scale[1] + self.texture_offset[1] * self.height new_x = min(new_x, self.width - 1) new_y = min(new_y, self.height - 1) height = self.parent.interpolator.get_value(self.texture_peeker, new_x, new_y) #TODO: This should be done in PatchedHeightmap.get_height() return height * self.parent.height_scale # + self.parent.offset def get_height_uv(self, u, v): return self.get_height(u * self.width, v * self.height) def load(self, patch, callback, cb_args=()): if self.texture is None: self.texture = Texture() self.texture.set_wrap_u(Texture.WMClamp) self.texture.set_wrap_v(Texture.WMClamp) self.parent.interpolator.configure_texture(self.texture) self.do_load(patch, callback, cb_args) else: if callback is not None: callback(self, *cb_args) def heightmap_ready_cb(self, texture, callback, cb_args): if texture is not None: self.texture = texture #print("READY", self.patch.str_id(), texture, self.texture) self.texture_peeker = texture.peek() # if self.texture_peeker is None: # print("NOT READY !!!") self.heightmap_ready = True data = self.texture.getRamImage() #TODO: should be completed and refactored signed = False component_type = texture.getComponentType() if component_type == Texture.T_float: buffer_type = numpy.float32 scale = 1.0 elif component_type == Texture.T_unsigned_byte: if signed: buffer_type = numpy.int8 scale = 128.0 else: buffer_type = numpy.uint8 scale = 255.0 elif component_type == Texture.T_unsigned_short: if signed: buffer_type = numpy.int16 scale = 32768.0 else: buffer_type = numpy.uint16 scale = 65535.0 if sys.version_info[0] < 3: buf = data.getData() np_buffer = numpy.fromstring(buf, dtype=buffer_type) else: np_buffer = numpy.frombuffer(data, buffer_type) np_buffer.shape = (self.texture.getYSize(), self.texture.getXSize(), self.texture.getNumComponents()) self.min_height = np_buffer.min() / scale self.max_height = np_buffer.max() / scale self.mean_height = np_buffer.mean() / scale else: if self.parent_heightmap is not None: self.calc_sub_patch() else: print("Make default texture for heightmap") texture = Texture() texture.setup_2d_texture(1, 1, Texture.T_float, Texture.F_r32) texture.set_clear_color(LColor(0, 0, 0, 0)) texture.make_ram_image() self.heightmap_ready_cb(texture, None, None) if callback is not None: callback(self, *cb_args) def do_load(self, patch, callback, cb_args): pass
def getMipMaps(mesh): mipmaps = {} for effect in mesh.effects: for prop in effect.supported: propval = getattr(effect, prop) if isinstance(propval, collada.material.Map): image_name = propval.sampler.surface.image.path image_data = propval.sampler.surface.image.data try: im = Image.open(StringIO(image_data)) im.load() except IOError: from panda3d.core import Texture from panda3d.core import StringStream from panda3d.core import PNMImage #PIL failed, so lets try DDS reader with panda3d t = Texture(image_name) success = t.readDds(StringStream(image_data)) if success == 0: raise FilterException("Failed to read image file %s" % image_name) #convert DDS to PNG outdata = t.getRamImageAs('RGBA').getData() try: im = Image.fromstring('RGBA', (t.getXSize(), t.getYSize()), outdata) im.load() except IOError: raise FilterException("Failed to read image file %s" % image_name) #Keep JPG in same format since JPG->PNG is pretty bad if im.format == 'JPEG': output_format = 'JPEG' output_extension = 'jpg' output_options = {'quality': 95, 'optimize':True} else: output_format = 'PNG' output_extension = 'png' output_options = {'optimize':True} #store a copy to the original image so we can resize from it directly each time orig_im = im width, height = im.size #round down to power of 2 width = int(math.pow(2, int(math.log(width, 2)))) height = int(math.pow(2, int(math.log(height, 2)))) pil_images = [] while True: im = orig_im.resize((width, height), Image.ANTIALIAS) pil_images.insert(0, im) if width == 1 and height == 1: break width = max(width / 2, 1) height = max(height / 2, 1) tar_buf = StringIO() tar = tarfile.TarFile(fileobj=tar_buf, mode='w') cur_offset = 0 byte_ranges = [] for i, pil_img in enumerate(pil_images): buf = StringIO() pil_img.save(buf, output_format, **output_options) file_len = buf.tell() cur_name = '%dx%d.%s' % (pil_img.size[0], pil_img.size[1], output_extension) tar_info = tarfile.TarInfo(name=cur_name) tar_info.size=file_len buf.seek(0) tar.addfile(tarinfo=tar_info, fileobj=buf) #tar files have a 512 byte header cur_offset += 512 file_start = cur_offset byte_ranges.append({'offset':file_start, 'length':file_len, 'width':pil_img.size[0], 'height':pil_img.size[1]}) #file lengths are rounded up to nearest 512 multiple file_len = 512 * ((file_len + 512 - 1) / 512) cur_offset += file_len tar.close() mipmaps[propval.sampler.surface.image.path] = (tar_buf.getvalue(), byte_ranges) return mipmaps
class ShaderHeightmapPatch(HeightmapPatch): tex_generators = {} cachable = False def __init__(self, noise, parent, x0, y0, x1, y1, width, height, scale=1.0, coord=TexCoord.Cylindrical, face=0, border=1): HeightmapPatch.__init__(self, parent, x0, y0, x1, y1, width, height, scale, coord, face, border) self.shader = None self.texture = None self.texture_peeker = None self.noise = noise self.tex_generator = None self.callback = None self.cloned = False self.texture_offset = LVector2() self.texture_scale = LVector2(1, 1) self.min_height = None self.max_height = None self.mean_height = None def copy_from(self, heightmap_patch): self.cloned = True self.lod = heightmap_patch.lod self.texture = heightmap_patch.texture self.texture_peeker = heightmap_patch.texture_peeker self.heightmap_ready = heightmap_patch.heightmap_ready self.min_height = heightmap_patch.min_height self.max_height = heightmap_patch.max_height self.mean_height = heightmap_patch.mean_height def get_height(self, x, y): if self.texture_peeker is None: print("No peeker", self.patch.str_id(), self.patch.instance_ready) traceback.print_stack() return 0.0 new_x = x * self.texture_scale[0] + self.texture_offset[0] * self.width new_y = ( (self.height - 1) - y) * self.texture_scale[1] + self.texture_offset[1] * self.height new_x = min(new_x, self.width - 1) new_y = min(new_y, self.height - 1) height = self.parent.interpolator.get_value(self.texture_peeker, new_x, new_y) #TODO: This should be done in PatchedHeightmap.get_height() return height * self.parent.height_scale # + self.parent.offset def generate(self, callback, cb_args=()): if self.texture is None: self.texture = Texture() self.texture.set_wrap_u(Texture.WMClamp) self.texture.set_wrap_v(Texture.WMClamp) self.parent.interpolator.configure_texture(self.texture) self._make_heightmap(callback, cb_args) else: if callback is not None: callback(self, *cb_args) def heightmap_ready_cb(self, texture, callback, cb_args): #print("READY", self.patch.str_id()) self.texture_peeker = self.texture.peek() # if self.texture_peeker is None: # print("NOT READY !!!") self.heightmap_ready = True data = self.texture.getRamImage() if sys.version_info[0] < 3: buf = data.getData() np_buffer = numpy.fromstring(buf, dtype=numpy.float32) else: np_buffer = numpy.frombuffer(data, numpy.float32) np_buffer.shape = (self.texture.getYSize(), self.texture.getXSize(), self.texture.getNumComponents()) self.min_height = np_buffer.min() self.max_height = np_buffer.max() self.mean_height = np_buffer.mean() if callback is not None: callback(self, *cb_args) def _make_heightmap(self, callback, cb_args): if not self.width in ShaderHeightmapPatch.tex_generators: ShaderHeightmapPatch.tex_generators[self.width] = GeneratorPool( settings.patch_pool_size) if settings.encode_float: texture_format = Texture.F_rgba else: texture_format = Texture.F_r32 ShaderHeightmapPatch.tex_generators[self.width].make_buffer( self.width, self.height, texture_format) tex_generator = ShaderHeightmapPatch.tex_generators[self.width] if self.shader is None: self.shader = NoiseShader(coord=self.coord, noise_source=self.noise, noise_target=FloatTarget(), offset=(self.x0, self.y0, 0.0), scale=(self.lod_scale_x, self.lod_scale_y, 1.0)) self.shader.global_frequency = self.parent.global_frequency self.shader.global_scale = self.parent.global_scale self.shader.create_and_register_shader(None, None) tex_generator.generate(self.shader, self.face, self.texture, self.heightmap_ready_cb, (callback, cb_args))