Exemplo n.º 1
0
    def palettize(self, palette_img, create_rgb=True):
        """
        Runs all palettization procedures on the given PaletteImage.
        Creates an RGB version of the palette if requested.
            :palette_img: The PaletteImage you want to palettize.
            :create_rgb: Would you like to create an RGB variant of the palette? True by default.
        """
        # We need to know the palette's prior size
        if not palette_img.size_known:
            raise PalettizerException(
                "Palette image's size is not known, this is a fatal error!")

        palette_name = palette_img.basename

        if self.debug:
            print(
                f'[{palette_img.page.group.dirname}] Compiling {palette_name}..'
            )

        # Our max distortion is 1, as we do not want to resize the palette to be smaller accidentally
        max_distortion = 1

        # This array holds all of our PNMImage source textures
        imgs = []

        # We're going to check all of the source textures
        # so that we can load all of them and determine our palette's final size
        for placement in palette_img.placements:
            sources = placement.texture.sources

            # We need to know the size of all source textures
            # If a source texture's size is not known, that means that it has been defined but did not exist on the disk when creating textures.boo
            for source in sources:
                if not source.size_known:
                    print(
                        f'Warning: Size is not known for source {source.filename}, please consider fixing the texture path and re-running egg-palettize'
                    )

            # Source (original) full resolution texture size
            source = sources[0]
            x_size = source.x_size
            y_size = source.y_size

            # Prior texture position and size
            tex_position = placement.position
            tex_x_size = tex_position.x_size
            tex_y_size = tex_position.y_size

            # DISTORTER
            # Fold the points until they scream out
            # DISTORTER
            # Change the way it sounds

            # We need to calculate the maximum distortion for both our X and Y (U/V) coordinates
            if x_size != tex_x_size:
                x_distortion = x_size / tex_x_size

                if x_distortion > max_distortion:
                    max_distortion = x_distortion

            if y_size != tex_y_size:
                y_distortion = y_size / tex_y_size

                if y_distortion > max_distortion:
                    max_distortion = y_distortion

            # If we have more than one source, that means our texture
            # has been defined multiple times for the same group...
            # Panda3D uses the first source texture only, so that's what we'll do.
            # But we'll make sure to print a warning either way!
            if len(sources) > 1:
                source2 = sources[1]

                if source2.x_size != x_size or source2.y_size != y_size:
                    print(
                        f'Two different source textures are defined for {palette_name}: {source.filename} ({x_size} {y_size}) vs {source2.filename} ({source2.x_size} {source2.y_size})'
                    )

            # Time to load the source file from Pandora!
            img = self.read_texture(source.filename)

            # Add the source image to our list
            imgs.append(img)

        # Well, time to calculate the palette's final size!
        # We will multiply the current size with the highest distortion.
        # We do NOT calculate X and Y distortion separately.
        # Doing so would destroy the aspect ratio of the palette.
        current_x_size = palette_img.x_size
        current_y_size = palette_img.y_size
        new_x_size = round(current_x_size * max_distortion)
        new_y_size = round(current_y_size * max_distortion)

        # Power of two time!
        # It's easier for the game engine to load, as it does not have to scale it automatically.
        new_x_size, new_y_size = self.scale_power_of_2(new_x_size, new_y_size)

        # We've changed the palette size. It is necessary to recalculate our texture distortion.
        x_distortion = new_x_size / current_x_size
        y_distortion = new_y_size / current_y_size

        # Create our palette image with four channels.
        # We will cut down the last channel when necessary.
        # Having a fourth, empty channel would only increase the file size.
        new_image = PNMImage(new_x_size, new_y_size, 4)
        new_image.alpha_fill(1)

        # Textures with alpha always have four channels set (three for RGB and one for Alpha).
        has_alpha = palette_img.properties.effective_channels in (2, 4)
        rgb_only = palette_img.properties.format == TextureGlobals.F_alpha
        alpha_image = None

        # If necessary and possible, create an alpha image as well.
        # Textures with alpha always have four channels set (three for RGB and one for Alpha).
        if create_rgb and has_alpha and not rgb_only:
            alpha_image = PNMImage(new_x_size, new_y_size, 1)
            alpha_image.set_type(PalettizeGlobals.RGB_TYPE)

        for i, placement in enumerate(palette_img.placements):
            # Find the loaded source image from before...
            texture_img = imgs[i]

            # Calculate the placement of our image!
            tex_position = placement.placed

            # Determine the upper left and lower right corners
            # with some matrix magic.
            transform = placement.compute_tex_matrix()
            ul = transform.xform_point(LTexCoordd(0.0, 1.0))
            lr = transform.xform_point(LTexCoordd(1.0, 0.0))

            # Calculate the top, left, bottom and right corners.
            top = int(math.floor((1.0 - ul[1]) * new_y_size + 0.5))
            left = int(math.floor(ul[0] * new_x_size + 0.5))
            bottom = int(math.floor((1.0 - lr[1]) * new_y_size + 0.5))
            right = int(math.floor(lr[0] * new_x_size + 0.5))

            tex_x_size = right - left
            tex_y_size = bottom - top
            org_x_size = round(tex_position.x_size * x_distortion)
            org_y_size = round(tex_position.y_size * y_distortion)
            tex_x = round(tex_position.x * x_distortion)
            tex_y = round(tex_position.y * y_distortion)

            # Resize our image to the desired size.
            texture_img = self.resize_image(texture_img, tex_x_size,
                                            tex_y_size)

            for y in range(tex_y, tex_y + org_y_size):
                sy = y - top

                # UV wrapping modes - V component (for Y texture coordinate)
                if placement.placed.wrap_v == TextureGlobals.WM_clamp:
                    sy = max(min(sy, tex_y_size - 1), 0)
                elif placement.placed.wrap_v == TextureGlobals.WM_mirror:
                    sy = (tex_y_size * 2) - 1 - (
                        (-sy - 1) %
                        (tex_y_size * 2)) if sy < 0 else sy % (tex_y_size * 2)
                    sy = sy if sy < tex_y_size else 2 * tex_y_size - sy - 1
                elif placement.placed.wrap_v == TextureGlobals.WM_mirror_once:
                    sy = sy if sy < tex_y_size else 2 * tex_y_size - sy - 1

                    # Repeat texture
                    sy = tex_y_size - 1 - (
                        (-sy - 1) % tex_y_size) if sy < 0 else sy % tex_y_size
                elif placement.placed.wrap_v == TextureGlobals.WM_border_color:
                    if sy < 0 or sy >= tex_y_size:
                        continue
                else:
                    # Repeat texture
                    sy = tex_y_size - 1 - (
                        (-sy - 1) % tex_y_size) if sy < 0 else sy % tex_y_size

                for x in range(tex_x, tex_x + org_x_size):
                    sx = x - left

                    # UV wrapping modes - U component (for X texture coordinate)
                    if placement.placed.wrap_u == TextureGlobals.WM_clamp:
                        sx = max(min(sx, tex_x_size - 1), 0)
                    elif placement.placed.wrap_u == TextureGlobals.WM_mirror:
                        sx = (tex_x_size * 2) - 1 - (
                            (-sx - 1) %
                            (tex_x_size * 2)) if sx < 0 else sx % (tex_x_size *
                                                                   2)
                        sx = sx if sx < tex_x_size else 2 * tex_x_size - sx - 1
                    elif placement.placed.wrap_u == TextureGlobals.WM_mirror_once:
                        sx = sx if sx >= 0 else ~sx

                        # Repeat texture
                        sx = tex_x_size - 1 - (
                            (-sx - 1) %
                            tex_x_size) if sx < 0 else sx % tex_x_size
                    elif placement.placed.wrap_u == TextureGlobals.WM_border_color:
                        if sx < 0 or sx >= tex_x_size:
                            continue
                    else:
                        # Repeat texture
                        sx = tex_x_size - 1 - (
                            (-sx - 1) %
                            tex_x_size) if sx < 0 else sx % tex_x_size

                    new_image.set_xel(x, y, texture_img.get_xel(sx, sy))
                    new_image.set_alpha(x, y, texture_img.get_alpha(sx, sy))

                    if alpha_image:
                        alpha_image.set_gray(x, y,
                                             texture_img.get_alpha(sx, sy))

        return new_image, alpha_image, has_alpha, rgb_only
Exemplo n.º 2
0
    def convert_texture(self, texture, model_path=None):
        if not self.model_path:
            self.print_exc('ERROR: No model path specified in ImageConverter.')
            return

        tex_path = texture[0]
        tex_basename = os.path.splitext(os.path.basename(tex_path))[0]

        if not os.path.isabs(tex_path):
            if '../' in tex_path and model_path:
                # This texture path is using relative paths.
                # We assume that the working directory is the model's directory
                tex_path = os.path.join(os.path.dirname(model_path), tex_path)
            else:
                tex_path = os.path.join(self.model_path, tex_path)

        tex_path = tex_path.replace('\\', os.sep).replace('/', os.sep)

        if not os.path.exists(tex_path):
            self.print_exc('ERROR: Could not convert {}: Missing RGB texture!'.format(tex_path))
            return

        png_tex_path = os.path.join(os.path.dirname(tex_path), tex_basename + '.png')
        png_tex_path = png_tex_path.replace('\\', os.sep).replace('/', os.sep)

        print('Converting to PNG...', png_tex_path)

        if len(texture) == 1:
            # Only one texture, we can save this immediately
            if tex_path.lower().endswith('.rgb'):
                output_img = PNMImage()
                output_img.read(Filename.from_os_specific(tex_path))

                if output_img.num_channels in (1, 2) and 'golf_ball' not in tex_path and 'roll-o-dex' not in tex_path: # HACK: Toontown
                    output_img.set_color_type(4)

                    for i in range(output_img.get_x_size()):
                        for j in range(output_img.get_y_size()):
                            output_img.set_alpha(i, j, output_img.get_gray(i, j))
            else:
                output_img = self.read_texture(tex_path, alpha=False)
        elif len(texture) == 2:
            img = self.read_texture(tex_path, alpha=True)

            # Two textures: the second one should be a RGB file
            alpha_path = texture[1]

            if not os.path.isabs(alpha_path):
                if '../' in alpha_path and model_path:
                    # This texture path is using relative paths.
                    # We assume that the working directory is the model's directory
                    alpha_path = os.path.join(os.path.dirname(model_path), alpha_path)
                else:
                    alpha_path = os.path.join(self.model_path, alpha_path)

            alpha_path = alpha_path.replace('\\', os.sep).replace('/', os.sep)

            if not os.path.exists(alpha_path):
                self.print_exc('ERROR: Could not convert {} with alpha {}: Missing alpha texture!'.format(tex_path, alpha_path))
                return

            alpha_img = PNMImage()
            alpha_img.read(Filename.from_os_specific(alpha_path))

            alpha_img = self.resize_image(alpha_img, img.get_x_size(), img.get_y_size())

            output_img = PNMImage(img.get_x_size(), img.get_y_size(), 4)
            output_img.alpha_fill(1)

            output_img.copy_sub_image(img, 0, 0, 0, 0, img.get_x_size(), img.get_y_size())

            for i in range(img.get_x_size()):
                for j in range(img.get_y_size()):
                    output_img.set_alpha(i, j, alpha_img.get_gray(i, j))

        output_img.write(Filename.from_os_specific(png_tex_path))
Exemplo n.º 3
0
class GPUFFT:
    """ This is a collection of compute shaders to generate the inverse
    fft efficiently on the gpu, with butterfly FFT and precomputed weights """
    def __init__(self, size, source_tex, normalization_factor):
        """ Creates a new fft instance. The source texture has to specified
        from the begining, as the shaderAttributes are pregenerated for
        performance reasons """

        self.size = size
        self.log2_size = int(math.log(size, 2))
        self.normalization_factor = normalization_factor

        # Create a ping and a pong texture, because we can't write to the
        # same texture while reading to it (that would lead to unexpected
        # behaviour, we could solve that by using an appropriate thread size,
        # but it works fine so far)
        self.ping_texture = Texture("FFTPing")
        self.ping_texture.setup_2d_texture(self.size, self.size,
                                           Texture.TFloat, Texture.FRgba32)
        self.pong_texture = Texture("FFTPong")
        self.pong_texture.setup_2d_texture(self.size, self.size,
                                           Texture.TFloat, Texture.FRgba32)
        self.source_tex = source_tex

        for tex in [self.ping_texture, self.pong_texture, source_tex]:
            tex.set_minfilter(Texture.FTNearest)
            tex.set_magfilter(Texture.FTNearest)
            tex.set_wrap_u(Texture.WMClamp)
            tex.set_wrap_v(Texture.WMClamp)

        # Pregenerate weights & indices for the shaders
        self._compute_weighting()

        # Pre generate the shaders, we have 2 passes: Horizontal and Vertical
        # which both execute log2(N) times with varying radii
        self.horizontal_fft_shader = Shader.load_compute(
            Shader.SLGLSL, "/$$rp/rpcore/water/shader/horizontal_fft.compute")
        self.horizontal_fft = NodePath("HorizontalFFT")
        self.horizontal_fft.set_shader(self.horizontal_fft_shader)
        self.horizontal_fft.set_shader_input("precomputedWeights",
                                             self.weights_lookup_tex)
        self.horizontal_fft.set_shader_input("N", LVecBase2i(self.size))

        self.vertical_fft_shader = Shader.load_compute(
            Shader.SLGLSL, "/$$rp/rpcore/water/shader/vertical_fft.compute")
        self.vertical_fft = NodePath("VerticalFFT")
        self.vertical_fft.set_shader(self.vertical_fft_shader)
        self.vertical_fft.set_shader_input("precomputedWeights",
                                           self.weights_lookup_tex)
        self.vertical_fft.set_shader_input("N", LVecBase2i(self.size))

        # Create a texture where the result is stored
        self.result_texture = Texture("Result")
        self.result_texture.setup2dTexture(self.size, self.size,
                                           Texture.TFloat, Texture.FRgba16)
        self.result_texture.set_minfilter(Texture.FTLinear)
        self.result_texture.set_magfilter(Texture.FTLinear)

        # Prepare the shader attributes, so we don't have to regenerate them
        # every frame -> That is VERY slow (3ms per fft instance)
        self._prepare_attributes()

    def get_result_texture(self):
        """ Returns the result texture, only contains valid data after execute
        was called at least once """
        return self.result_texture

    def _generate_indices(self, storage_a, storage_b):
        """ This method generates the precompute indices, see
        http://cnx.org/content/m12012/latest/image1.png """
        num_iter = self.size
        offset = 1
        step = 0
        for i in range(self.log2_size):
            num_iter = num_iter >> 1
            step = offset
            for j in range(self.size):
                goLeft = (j // step) % 2 == 1
                index_a, index_b = 0, 0
                if goLeft:
                    index_a, index_b = j - step, j
                else:
                    index_a, index_b = j, j + step

                storage_a[i][j] = index_a
                storage_b[i][j] = index_b
            offset = offset << 1

    def _generate_weights(self, storage):
        """ This method generates the precomputed weights """

        # Using a custom pi variable should force the calculations to use
        # high precision (I hope so)
        pi = 3.141592653589793238462643383
        num_iter = self.size // 2
        num_k = 1
        resolution_float = float(self.size)
        for i in range(self.log2_size):
            start = 0
            end = 2 * num_k
            for b in range(num_iter):
                K = 0
                for k in range(start, end, 2):
                    fK = float(K)
                    f_num_iter = float(num_iter)
                    weight_a = Vec2(
                        math.cos(2.0 * pi * fK * f_num_iter /
                                 resolution_float),
                        -math.sin(
                            2.0 * pi * fK * f_num_iter / resolution_float))
                    weight_b = Vec2(
                        -math.cos(
                            2.0 * pi * fK * f_num_iter / resolution_float),
                        math.sin(2.0 * pi * fK * f_num_iter /
                                 resolution_float))
                    storage[i][k // 2] = weight_a
                    storage[i][k // 2 + num_k] = weight_b
                    K += 1
                start += 4 * num_k
                end = start + 2 * num_k

            num_iter = num_iter >> 1
            num_k = num_k << 1

    def _reverse_row(self, indices):
        """ Reverses the bits in the given row. This is required for inverse
        fft (actually we perform a normal fft, but reversing the bits gives
        us an inverse fft) """
        mask = 0x1
        for j in range(self.size):
            val = 0x0
            temp = int(indices[j])  # Int is required, for making a copy
            for i in range(self.log2_size):
                t = mask & temp
                val = (val << 1) | t
                temp = temp >> 1
            indices[j] = val

    def _compute_weighting(self):
        """ Precomputes the weights & indices, and stores them in a texture """
        indices_a = [[0 for i in range(self.size)]
                     for k in range(self.log2_size)]
        indices_b = [[0 for i in range(self.size)]
                     for k in range(self.log2_size)]
        weights = [[Vec2(0.0) for i in range(self.size)]
                   for k in range(self.log2_size)]

        # Pre-Generating indices ..
        self._generate_indices(indices_a, indices_b)
        self._reverse_row(indices_a[0])
        self._reverse_row(indices_b[0])

        # Pre-Generating weights .."
        self._generate_weights(weights)

        # Create storage for the weights & indices
        self.weights_lookup = PNMImage(self.size, self.log2_size, 4)
        self.weights_lookup.setMaxval((2**16) - 1)
        self.weights_lookup.fill(0.0)

        # Populate storage
        for x in range(self.size):
            for y in range(self.log2_size):
                index_a = indices_a[y][x]
                index_b = indices_b[y][x]
                weight = weights[y][x]

                self.weights_lookup.set_red(x, y, index_a / float(self.size))
                self.weights_lookup.set_green(x, y, index_b / float(self.size))
                self.weights_lookup.set_blue(x, y, weight.x * 0.5 + 0.5)
                self.weights_lookup.set_alpha(x, y, weight.y * 0.5 + 0.5)

        # Convert storage to texture so we can use it in a shader
        self.weights_lookup_tex = Texture("Weights Lookup")
        self.weights_lookup_tex.load(self.weights_lookup)
        self.weights_lookup_tex.set_format(Texture.FRgba16)
        self.weights_lookup_tex.set_minfilter(Texture.FTNearest)
        self.weights_lookup_tex.set_magfilter(Texture.FTNearest)
        self.weights_lookup_tex.set_wrap_u(Texture.WMClamp)
        self.weights_lookup_tex.set_wrap_v(Texture.WMClamp)

    def _prepare_attributes(self):
        """ Prepares all shaderAttributes, so that we have a list of
        ShaderAttributes we can simply walk through in the update method,
        that is MUCH faster than using set_shader_input, as each call to
        set_shader_input forces the generation of a new ShaderAttrib """
        self.attributes = []
        textures = [self.ping_texture, self.pong_texture]

        current_index = 0
        firstPass = True

        # Horizontal
        for step in range(self.log2_size):
            source = textures[current_index]
            dest = textures[1 - current_index]

            if firstPass:
                source = self.source_tex
                firstPass = False

            index = self.log2_size - step - 1
            self.horizontal_fft.set_shader_input("source", source)
            self.horizontal_fft.set_shader_input("dest", dest)
            self.horizontal_fft.set_shader_input("butterflyIndex",
                                                 LVecBase2i(index))
            self._queue_shader(self.horizontal_fft)
            current_index = 1 - current_index

        # Vertical
        for step in range(self.log2_size):
            source = textures[current_index]
            dest = textures[1 - current_index]
            is_last_pass = step == self.log2_size - 1
            if is_last_pass:
                dest = self.result_texture
            index = self.log2_size - step - 1
            self.vertical_fft.set_shader_input("source", source)
            self.vertical_fft.set_shader_input("dest", dest)
            self.vertical_fft.set_shader_input("isLastPass", is_last_pass)
            self.vertical_fft.set_shader_input("normalizationFactor",
                                               self.normalization_factor)
            self.vertical_fft.set_shader_input("butterflyIndex",
                                               LVecBase2i(index))
            self._queue_shader(self.vertical_fft)

            current_index = 1 - current_index

    def execute(self):
        """ Executes the inverse fft once """
        for attr in self.attributes:
            self._execute_shader(attr)

    def _queue_shader(self, node):
        """ Internal method to fetch the ShaderAttrib of a node and store it
        in the update queue """
        sattr = node.getAttrib(ShaderAttrib)
        self.attributes.append(sattr)

    def _execute_shader(self, sattr):
        """ Internal method to execute a shader by a given ShaderAttrib """
        Globals.base.graphicsEngine.dispatch_compute(
            (self.size // 16, self.size // 16, 1), sattr,
            Globals.base.win.get_gsg())