Python, Pygame, Image manipulation: Restretch a loaded png, to be the texture for an isometric Tile

牧云@^-^@ 提交于 2019-12-13 07:48:48

问题


I'm a 17 year old programmer, trying to program an isometric game in python, with pygame. After finishing a tile engine, working with not good looking, gimp-drawn PNG's, I wondered, if it would be possible to render some Tiles by texture. I hope I provided all what's needed to understand, what's my issue and please excuse my not perfect English.

Simply what I want to do, is to generate a 128 by 128 Pixel width Image of an Isometric Tile, using the following picture as texture for all three sides of the Block:

(Links here because I'm not yet allowed to put pictures in, due to it's my first post)

To explain better, what I'm trying to do, I have drawn this little picture:

I have already searched the Internet for about 2 hours and didn't come to a solution, except for the top part of the Tile, here is what I already got in Code:

This is the Image Manipulation Module, the transformToRightPart() is the method where I need help:

import pygame

class Image(object):
    '''
    Use this Module to create Tiles by Texture to use them later in the Tileengine.
    It is important to run pygame.init() before creating objects of this class!
    Contains unfinished Elements!
    '''
    def __init__(self, path):
        self.loadFromPath(path)

    def getIMG(self):
        assert self.originalIMG is not None, "No picture to return"
        if not self.IMG == None:
            return self.IMG
        else:
            return self.originalIMG

    def loadFromPath(self, path):
        '''
        Don't do convert() or convert_alpha() here,
        as Objects of this class are created during the loading process,
        with no pygame.display() created.
        '''
        self.originalIMG = pygame.image.load(path)
        self.IMG = None

    def transformToTopPart(self):
        '''
        Transforms the loaded Image to the Top Part of an Isometric Tile, with the Dimensions 2:1,
        said in Pixels: 128 px Width by 64 px Height.
        '''
        self.IMG = pygame.transform.rotate(self.originalIMG, 45)
        self.IMG = pygame.transform.scale(self.IMG, (128, 64))

    def transformToRightPart(self):
        '''
        TODO!! Don't ask how (X.X)
        Transforms the loaded Image to the right Part of an Isometric Tile.
        '''
        assert False, "This method isn't finished, try something different ;)"

    def transformToLeftPart(self):
        '''
        Transforms the loaded Image to the left Part of an Isometric Tile.
        Due to the nice geometric fact, that the shape of the left part,
        is just the flipped right part shape and we don't lose quality by flipping,
        we do this little trick, to enshorten the code.
        '''
        self.originalIMG = pygame.transform.flip(self.originalIMG, True, False)
        self.transformToRightPart()
        self.IMG = pygame.transform.flip(self.IMG, True, False)
        self.originalIMG = pygame.transform.flip(self.originalIMG, True, False)

And this is the Module, which creates a window with the tile to render:

import pygame, sys

from ImageManipulation import Image
from pygame.locals import *

if __name__ == '__main__':
    pygame.init()
    FPS=20
    fpsClock = pygame.time.Clock()
    picture = Image("Stone_Floor_texture.png")
    picture.transformToTopPart()
    DISPLAY = pygame.display.set_mode((400,400),0,32)
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
        DISPLAY.blit(picture.getIMG(),(0,0))
        pygame.display.update()
        fpsClock.tick(FPS)

The output of the code looks like this:

What I'm trying to achieve is, that it looks, something like this:


回答1:


Big thanks to Spektre for all the effort he made, trying to help me, but all in all, after two days of over-thinking the problem and bug-fixing, I came up with a solution myself. It might not be as fast or efficient as targeting the pixels directly in an array, like Spektre did in his c++ example, but it is a way, you're only dependencies are pygame, and it is easy to understand.

What did I do? - I wrote two functions, the first getting a surface containing only a single column of another surface, with an index, referring to the x position of the column. And the second, calculating a coefficient, how far down each row should get moved, if the last row should get shifted down a certain amount of pixels and then returning a surface with the shifted picture.

Here is the magic Code:

import pygame

from pygame.locals import *
from pygame import Surface

def getColumn(surface, index):
    assert index <= surface.get_width(), "index can't be bigger, than surface width"
    height = surface.get_height()
    subsurf = Surface((1,height)) # Create Surface 1 px by picture-height high, to store the output in
    subsurf.blit(surface.subsurface(pygame.Rect( (index,0),(1,height) )),(0,0)) # Blit a one pixel width subsurface with x Position at index of the image to subsurf
    return subsurf

def shiftRightDown(surface, pixels):
    size = surface.get_size()
    newSize = (size[0], size[1]+pixels)
    coeff = pixels / size[0]
    returnSurface = Surface(newSize)
    for i in range(size[1]): # here happens the magic
        returnSurface.blit(getColumn(surface, i), (i,0+int(i*coeff)))
    return returnSurface

After all, big respect to Spektres coding skills, even though I'm to dumb to understand anything from the c plus plus example, as I'm a total beginner.




回答2:


Well I did this by simply copy the texture pixels into sprite using plane projections (basis vectors of each side) + some rescaling as the texture does not correspond with your sprite resolution. I did it in C++ so here my commented code (you can extract the equations from it):

// [constants]
const int sxs=128;              // target sprite resolution [pixels]
const int sys=128;
const int height=32;            // height/thickness of your tile [pixels]
const DWORD cback=0x00FFFFFF;   // background color (or invisible for the sprite)

// [variables]
DWORD **ptxr,**pspr;            // direct pixel access pointers (any 32bit variable type)
Graphics::TBitmap *txr,*spr;    // VCL bitmaps
int txs,tys,x,y,x0,y0,xx,yy,th;

// [init]
// create VCL bitmaps (can ignore this)
txr=new Graphics::TBitmap; // input texture
spr=new Graphics::TBitmap; // output sprite
// load texture
txr->LoadFromFile("texture.bmp");
txs=txr->Width;
tys=txr->Height;

// prepare sprite resolution
spr->SetSize(sxs,sys);
// allow direct pixel access
txr->HandleType=bmDIB; txr->PixelFormat=pf32bit; ptxr=new DWORD*[tys]; for (y=0;y<tys;y++) ptxr[y]=(DWORD*)txr->ScanLine[y];
spr->HandleType=bmDIB; spr->PixelFormat=pf32bit; pspr=new DWORD*[sys]; for (y=0;y<sys;y++) pspr[y]=(DWORD*)spr->ScanLine[y];

// [render sprite]
th=height*(txs-1)/(sxs-1);  // height of tile in texture [pixels]
// clear
for (y=0;y<sys;y++)
 for (x=0;x<sxs;x++)
  pspr[y][x]=cback;
// top side
x0=0; y0=(sys*3/4)-height;
for (y=0;y<tys;y++)
 for (x=0;x<txs;x++)
    {
    // isometric projection of top side
    xx=x0+(x+y)*(sxs-1)/((txs-1)*2);
    yy=y0+(x-y)*(sxs-1)/((txs-1)*4);
    // copy pixel from texture to sorite
    if ((xx>=0)&&(xx<sxs)&&(yy>=0)&&(yy<sys))
     pspr[yy][xx]=ptxr[y][x];
    }
// left side
x0=0; y0=(sys*3/4)-height;
for (y=0;(y<tys)&&(y<th);y++)
 for (x=0;x<txs;x++)
    {
    // isometric projection of top side
    xx=x0+(x      )*(sxs-1)/((txs-1)*2);
    yy=y0+(x+(4*y))*(sxs-1)/((txs-1)*4);
    // copy pixel from texture to sorite
    if ((xx>=0)&&(xx<sxs)&&(yy>=0)&&(yy<sys))
     pspr[yy][xx]=ptxr[y][x];
    }
// right side
x0=sxs/2; y0=sys-height-1;
for (y=0;(y<txs)&&(y<th);y++) // x,y are swapped to avoid connection seems
 for (x=0;x<tys;x++)
    {
    // isometric projection of top side
    xx=x0+(+x      )*(sxs-1)/((txs-1)*2);
    yy=y0+(-x+(4*y))*(sxs-1)/((txs-1)*4);
    // copy pixel from texture to sorite
    if ((xx>=0)&&(xx<sxs)&&(yy>=0)&&(yy<sys))
     pspr[yy][xx]=ptxr[x][y];
    }

// here do your stuff with your sprite spr I render source and resulting images into bitmap to show on screen
// you can ignoe this
bmp->SetSize(txs+5+sxs,max(tys,sys));
bmp->Canvas->Brush->Color=clBtnFace;
bmp->Canvas->FillRect(TRect(0,0,bmp->Width,bmp->Height));
bmp->Canvas->Draw(0,0,txr);
bmp->Canvas->Draw(txs+5,0,spr);

// [exit]
// release memory
delete[] ptxr;
delete[] pspr;
if (txr) delete txr; txr=NULL;
if (spr) delete spr; spr=NULL;

The texture must be square otherwise the right side rendering will have access violation troubles not to mention visible seams ...

Here output sprite example of this code:

Now how it works:

ignore the VCL init/load/exit stuff handling images as the important stuff is just the rendering.

Each part consist of setting start point (red square) and convert texture x,y coordinates into offset from that start point in plane projection basis vectors (the black arrows).

And the offset is also multiplied by the resolution ratio between texture and sprite to handle their different sizes.

Look here to understand the direct pixel access I used:

  • Display an array of color in C

PS

You can add lighting to enhance 3D look ... This is how it looks when top side is 100% left side is 75% and right side is 50% of intensity:

simulating light coming from above left side



来源:https://stackoverflow.com/questions/44619304/python-pygame-image-manipulation-restretch-a-loaded-png-to-be-the-texture-fo

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!