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 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()
Exemple #3
0
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
Exemple #4
0
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))