SNAPTOTHETERRAIN / Random Retro Terrain Generator

Hello
Finally i finished the first version of

SNAPTOTHETERRAIN
Subdivided Terrain Multitexture Random Retro Generator v.1.0

dependencies: PIL library

good for newbies, so am i :slight_smile:

here is the complete source with media included

http://www.handeozerden.com/snaptothegrid/panda/snaptotheterrain_v01.zip

these are some screenshots:


here is the code, with explanations of the project inside:

main.py

"""
SNAPTOTHETERRAIN
Subdivided Terrain Multitexture Random Generator v.1.0 by SnapToTheGrid
--------------------------------------------------------------------------------
Main: 
    Initializes application, general setup, key events
Files:
    Contains several file management usefull functions (only custom_listdir for the moment)
    This function is used for getting a cool list of random tile textures
DiamondMap: 
   Generates a random procedural heighmap of given size(**2+1) usign Diamond Square algorythm and saves it to disk
   If wanted, splits the heightmap in x*y subimages and saves them to disk
   strange: if we use jpg, when we join the 3d terrains, elevations in the borders dont fit,using bmp is ok , that took me crazy!
Terrain: 
   Based on treeterrain script, adapted to my necessities (thanks Treeform!)
   options:
      -Retro True/False: 
         Divides the elevations by int or float, giving a general aspect retro flatted or normal
      -subdivide=None: 
         Generates a multitexture terrain with fixed resolution from a given heightmap image
      -subdivide=N:
         Generates x*y multitexture terrains with fixed resolution from each subheightmap and locates them in x*y grid
         Improves performance a bit, i think the nodepaths outside the camera's view are not computed in the render :)
--------------------------------------------------------------------------------
Todo:
   - improve performance in some way
   - find a way to not having to locate the texture layers 0.02 units over, with some lights don't looks nice
   - maybe change the system and generate a single colormap texture using PIL functions (?)
     That WILL improve performance and i could use geoMipTerrain too, mhmmmm :)
   - collision function for getting kind of getElevation() of the terrain in a given x,y point -> ideas to make it work fast(?)
   - avatar+camera+a* pathfinding algorythm
--------------------------------------------------------------------------------
"""

#import panda modules
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject

#import python modules
import sys

#import custom modules
import DiamondMap,Terrain

#define constants
TILEPATH="media/tiles/"
HPATH="media/hmaps/"
HFILENAME="hmap"
CFILENAME="tile128"
SIZE=65 # has to be a 2 exponent number plus one
ALT=1.0
RETRO=True
SUBDIVIDE=3


class Main(DirectObject):
    
    #===========================================================================
    """initialize"""
    def __init__(self):
        print "____________________________________________________"
        print "Class Main"
        #setup application
        #-----------------------
        base.setFrameRateMeter(True)
        self.toggleWireFrame()
        self.keys()
        # Initialize application
        #-----------------------
        #create a random heighmap jpg file of given size and split it into given subdivisions
        map=DiamondMap.DiamondMap(path=HPATH,filename=HFILENAME,size=SIZE,subdivide=SUBDIVIDE)
        #create multitextured 3d terrain from heighmap data
        terrain=Terrain.Terrain(heightmap=HPATH+HFILENAME,
                                colormap=TILEPATH+CFILENAME,
                                tilepath=TILEPATH,
                                alt=ALT,
                                retro=RETRO,
                                subdivide=SUBDIVIDE)
        
    #===========================================================================
    """key functions"""
    def keys(self):
        print "Keys"
        self.accept('f',self.toggleWireFrame)
        self.accept('t',self.toggleTexture)
        self.accept('p',self.snapShot)
        self.accept('escape',sys.exit) 
    def snapShot(self):
        base.screenshot("Snapshot") 
    def toggleWireFrame(self):
        base.toggleWireframe()
    def toggleTexture(self):
        base.toggleTexture()
    

main=Main()
run()

DiamondMap.py

"""
DiamondMap: 
   Based on a (i think) Kris Schnee schrieb script that i found in the net, originally implemented in pygame, thanks!
   Generates a random procedural heighmap of given size(**2+1) usign Diamond Square algorythm and saves it to disk
   If wanted, splits the heightmap in x*y subimages and saves them to disk
"""
#import python modules
import Image,ImageDraw
import random

class DiamondMap:
    def __init__(self,path,filename,size,subdivide=None):
        print "____________________________________________________"
        print "Class DiamondMap"
        heightmap=self.initMap(size,False)
        heightmap =self.randomMap(heightmap,False,(0.0,0.0,0.0,0.0))
        im=self.drawMap(heightmap)
        im.save(path+filename+".bmp","BMP")
        print filename+".bmp saved in "+path, im.size
        if subdivide!=None:
            self.cropImage(path,filename,subdivide)
    #===========================================================================
    def initMap(self,size,random_data=False):
        """Generate random static or a blank map."""
        heightmap = []
        if random_data:
            #random noise background
            for x in range(size):
                heightmap.append([])
                for y in range(size):
                    heightmap[-1].append( random.random() )
        else:
            #black background
            for x in range(size):
                heightmap.append([])
                for y in range(size):
                    heightmap[-1].append(0.0)
        return heightmap

    #===========================================================================
    def randomMap(self,heightmap,show_in_progress=False,seed_values=(.5,.5,.5,.5)):
        """Make procedural terrain using the D/S Algorithm."""
        size = len(heightmap)
        ## Seed the corners.
        corners = ((0,0),(size-1,0),(0,size-1),(size-1,size-1))
        for n in range(4):
            point = corners[n]
            heightmap[point[0]][point[1]] = seed_values[n]
    
        ## Starting values.
        size_minus_1 = size - 1 ## For edge-wrapping purposes.
        cell_size = size-1 ## Examine squares of this size within the heightmap.
        cell_size_half = cell_size / 2
        iteration = 0 ## How many times have we done the algorithm? (just FYI)
        chaos = 1.0 ## Total possible variation of a height from expected average.
        chaos_half = chaos * .5 ## Possible variation up or down.
        diamond_chaos_half = (chaos/1.414)*.5 ## Reduced by sqrt(2) for D step.
        ## (Wouldn't "Diamond Chaos Half" be a cool anime series title?)
    
        while cell_size > 1: ## For actual use.
            ## Begin the algorithm.
            #print "Iteration: "+str(iteration)
            #print "Chaos: "+str(chaos)+" C.Half: "+str(chaos_half)+" D.C.Half: "+str(diamond_chaos_half)
            iteration += 1
    
            """Find the "anchor points" that mark the upper-left corner
            of each cell."""
            for anchor_y in range(0,size-1,cell_size):
                for anchor_x in range(0,size-1,cell_size):
    
                    ## Calculate the center of the cell.
                    cx = anchor_x + cell_size_half
                    cy = anchor_y + cell_size_half
    
                    ## The "Diamond" phase.
    
                    ## Find the center's diagonal "neighbors."
                    neighbors = ([cx-cell_size_half,cy-cell_size_half],
                                 [cx+cell_size_half,cy-cell_size_half],
                                 [cx-cell_size_half,cy+cell_size_half],
                                 [cx+cell_size_half,cy+cell_size_half])
    
                    ## Correct for points outside the map.
                    for n in range(4):
                        neighbor = neighbors[n]
                        if neighbor[0] < 0:
                            neighbors[n][0] += size
                        elif neighbor[0] > size:
                            neighbors[n][0] -= size
                        if neighbor[1] < 0:
                            neighbors[n][1] += size
                        elif neighbor[1] > size:
                            neighbors[n][1] -= size
                    average = sum([heightmap[n[0]][n[1]] for n in neighbors]) * .25
                    h = average - chaos_half + (random.random() * chaos)
    ##                h = average ## Test: No randomness.
                    h = max(0.0,min(1.0,h))
                    heightmap[cx][cy] = h
    
                    ## The "Square" phase.
    
                    ## Calculate four "edge points" surrounding the center.
                    edge_points = ((cx,cy-cell_size_half),
                                   (cx-cell_size_half,cy),
                                   (cx+cell_size_half,cy),
                                   (cx,cy+cell_size_half))
                    for point in edge_points:
                        neighbors = [[point[0],point[1]-cell_size_half],
                                     [point[0]-cell_size_half,point[1]],
                                     [point[0]+cell_size_half,point[1]],
                                     [point[0],point[1]+cell_size_half]]
    
                        ## Correct for points outside the map.
                        for n in range(4):
                            neighbor = neighbors[n]
                            if neighbor[0] < 0:
                                neighbors[n][0] += size
                            elif neighbor[0] > size_minus_1:
                                neighbors[n][0] -= size
                            if neighbor[1] < 0:
                                neighbors[n][1] += size
                            elif neighbor[1] > size_minus_1:
                                neighbors[n][1] -= size
    
                        average = sum([heightmap[n[0]][n[1]] for n in neighbors]) * .25
                        h = average - chaos_half + (random.random() * chaos)
                        h = max(0.0,min(1.0,h))
    ##                    h = average ## Test: No randomness.
                        heightmap[point[0]][point[1]] = h
    
            ## End of iteration. Reduce cell size and chaos.
            cell_size /= 2
            cell_size_half /= 2
            chaos *= .5
            chaos_half = chaos * .5
            diamond_chaos_half = (chaos / 1.414) * .25
    
            #if show_in_progress:
                #DisplayAsImage(heightmap)
    
        return heightmap
    #---------------------------------------------------------------------------
    def drawMap(self,heightmap):
        """Draws the heighmap in the image
        The range of values is assumed to be 0.0 to 1.0, in a 2D, square array.
        The range is converted to greyscale values of 0 to 255."""
        
        #print "drawMap"
        #get size
        size=len(heightmap)
        #create image
        im=Image.new(mode='RGB', size=(size,size), color=(0,0,0))
        print "Image: ",im.format, im.size, im.mode
        draw = ImageDraw.Draw(im)
        #draw map
        for y in range(size):
            for x in range(size): 
                h = int(heightmap[x][y] * 255)*1
                try:
                    #print heightmap[x][y]
                    draw.point((x,y), fill=(h,h,h))
                except:
                    print "Error on x,y: "+str((x,y))+"; map --> 0-255 value: "+str((heightmap[x][y],h))    
                
        return im
         
    #---------------------------------------------------------------------------                  
    def cropImage(self,path,filename,subdivide=2):
        """crops the image in x*y subimages"""
        """strange: if we use jpg when we join the 3d terrains elevations dont fit,using bmp is ok"""
        im = Image.open(path+filename+".bmp")
        regions=[]
        a=0
        b=0
        dx=im.size[0]/subdivide
        dy=im.size[1]/subdivide
        for y in range(0,subdivide):
            regions.append([])
            for x in range(0,subdivide):
                #add 2 pixels, dont ask me why but this mut be done in order for 3d terrains join well
                ddx=2
                ddy=2
                #if x==subdivide:
                    #ddx=0
                #if y==subdivide:
                    #ddy=0
                box = (a, b, ddx+a+dx, ddy+b+dy) 
                regions[y].append(im.crop(box))
                subfilename=filename+"_"+str(x)+"_"+str(y)+".bmp"
                regions[y][x].save(path+subfilename, "BMP")
                print "  "+subfilename+" saved in "+path, box
                a+=dx
                if a>=dx*subdivide: 
                    a=0
                    b+=dy
    
        return regions
        
#-------------------------------------------------------------------------------        
#map=DiamondMap("media/hmaps/","hmap",129,subdivide=3)

Terrain.py

"""
Terrain: 
   -based on Treeform script named 'treeterain.py', never could done it without it :)
   -generates a list of random tile texture files (uses custom_listdir function from Files class)
   -generates x*y terrain meshes and locates them in x*y
   -this module can work alone if heightmap data is already generated
"""
#import panda modules
if __name__ == "__main__":
    import direct.directbase.DirectStart
from pandac.PandaModules import NodePath,GeomVertexData,GeomVertexFormat,Geom,GeomVertexWriter,GeomTriangles,GeomNode,Vec3
from pandac.PandaModules import Texture,DepthTestAttrib
#import python modules
import Image
from random import *
#import custom modules
import Files

################################################################################

class Terrain:
    def __init__(self,heightmap,colormap,tilepath,alt=1.0,retro=False,subdivide=None):
        print "____________________________________________________"
        print "Class Terrain"
        #get 10 random texture files for use as tile textures
        dir=Files.custom_listdir(path=tilepath+"random/",extfilter=("",".bmp",".jpg",".png",".tga"),extreturn=False)
        tiles = [ tile for tile in sample(dir,12) ]
        #set first texture to be a sea tile (optional)
        tiles[0]=tilepath+"blue64.jpg"
        
        #terrain generation
        #------------------
        #create root nodePath that contains all terrains
        root= NodePath("root")
        root.setSz(alt)
        size= Image.open(heightmap+".bmp").size
        root.setPos(-(size[0]-2)/2.0,-(size[1]-2)/2.0,0)
        root.reparentTo(render)
        
        #single terrain
        if subdivide==None:
            #generate a terrain mesh from heightmap file
            terrain=TerrainMesh(root,heightmap,colormap,tiles,alt,retro,subdivide,(0,0))
        #multiple subdivided terrains
        else:
            #generate a grid of x*y terrains
            terrains=[]
            for x in range(0,subdivide):
                for y in range(0,subdivide):
                    subheightmap=heightmap+"_"+str(x)+"_"+str(y)
                    terrains.append(TerrainMesh(root,subheightmap,colormap,tiles,alt,retro,subdivide,(x,y)))
            
        #
        #rescale and center root terrain
        
        #self.root.setPos(-self.size/2.0,-self.size/2.0,0)
        #
        render.analyze()

################################################################################
"""
TerrainMesh:
   -this mainly is a copy from  Treeform script named 'treeterain.py', simplified and adapted...
   -Generates a terrain mesh with fixed resolution from a given heightmap
   -Generates extra meshes and assign them tile textures depending on height
"""

class TerrainMesh:
    def __init__(self,root,heightmap,colormap,tiles,alt=1.0,retro=False,subdivide=None,loc=(0,0)):
        print "____________________________________________________"
        print "Class TerrainMesh"
        #record vars
        self.root=root
        self.retro=retro
        self.subdivide=subdivide
        self.loc=loc
        #generate vertex data list
        self.data,self.size = self.fromFile(heightmap) #print self.data
        #generate terrain base with black texture
        terrain = self.generateMesh(0,colormap+".jpg",True) 
        #generate terrain layers with texture tiles
        for i in range(0,len(tiles)):
            terrain = self.generateMesh(i,tiles[i],False)
        
        
    #===========================================================================    
    def fromFile(self,heightmap):
        """Generates data from given heightmap image file"""
        #open heightmap image
        hmap = Image.open(heightmap+".bmp").convert("L") 
        #get heightmap image size 
        xs,ys = hmap.size
        #remove 2 rows from size if is one of he last terrains in x or y grid
        if self.subdivide!=None:
            if self.loc[0]==self.subdivide-1:
                xs=hmap.size[0]-2
            if self.loc[1]==self.subdivide-1: 
                ys=hmap.size[1]-2
        #get divisor
        cdiv=10
        if self.retro:
            hdiv=10
        else:
            hdiv=10.0
        #generate data list: [[heightpixel,colorpixel],...] #(hmap.getpixel((x,ys-y-1))/hdiv, hmap.getpixel((x,ys-y-1))/cdiv ) 
        return [[(hmap.getpixel((x,y))/hdiv, hmap.getpixel((x,y))/cdiv ) for y in range(ys)] for x in range(xs)],hmap.size
         
    #===========================================================================
    def generateMesh(self,i,tile,base=False):   
        """ generate a terrain """
        data = self.data
        #set transparency of vertex
        def tp(n): 
            if base==False:
                if i == n:
                    list = [1,1,1,.5]
                else:
                    list = [1,1,1,0]
            else:
                list = [1,1,1,1]
            return list
        #create vdata
        vdata = GeomVertexData('terrain', GeomVertexFormat.getV3c4t2(), Geom.UHStatic)
        vertex = GeomVertexWriter(vdata, 'vertex')
        uv = GeomVertexWriter(vdata, 'texcoord')
        color = GeomVertexWriter(vdata, 'color')
        #create vertices
        number = 0
        hm = 1
        for x in range(1,len(data)-1):
            for y in range(1,len(data[x])-1):
                #get vertex data
                v1 = Vec3(x,y,data[x][y][0])
                c1 = data[x][y][1]
                v2 = Vec3(x+1,y,data[x+1][y][0])
                c2 = data[x+1][y][1]
                v3 = Vec3(x+1,y+1,data[x+1][y+1][0])
                c3 = data[x+1][y+1][1]
                v4 = Vec3(x,y+1,data[x][y+1][0])
                c4 = data[x][y+1][1]
                #discard vertex that we don't want
                if base==False:
                    if c1 != i and c2 != i and c3 != i and c4 != i :
                        continue 
                c1,c2,c3,c4 = tp(c1),tp(c2),tp(c3),tp(c4) 
                #
                #vertex color/lighting (?) what is this (?)
                #
                #avg = (c1[3]+c2[3]+c3[3]+c4[3])/4.
                #c1[3]/=avg
                #c2[3]/=avg
                #c3[3]/=avg
                #c4[3]/=avg
                #
                #add vertices
                #
                vertex.addData3f(v1)
                color.addData4f(*c1)  
                uv.addData2f(0,0)
                vertex.addData3f(v2)
                color.addData4f(*c2)
                uv.addData2f(1,0)               
                vertex.addData3f(v3)
                color.addData4f(*c3)
                uv.addData2f(1,1)
                #
                vertex.addData3f(v1)
                color.addData4f(*c1)
                uv.addData2f(0,0)
                vertex.addData3f(v3)
                color.addData4f(*c3)
                uv.addData2f(1,1)
                vertex.addData3f(v4)
                color.addData4f(*c4)
                uv.addData2f(0,1)
                #
                number = number + 2
        #add triangles       
        prim = GeomTriangles(Geom.UHStatic)
        for n in range(number):
            prim.addVertices(n*3,n*3+1,n*3+2)
        prim.closePrimitive()
        #create geom and geomnode
        geom = Geom(vdata)
        geom.addPrimitive(prim)
        geomnode = GeomNode('gnode')
        geomnode.addGeom(geom)
        #create terrain node and parent it to parent
        terain = NodePath("terain")
        terain.attachNewNode(geomnode)
        terain.reparentTo(self.root)
        #set render order
        terain.setBin("",1)
        #load and assign texture
        tx = loader.loadTexture(tile)
        tx.setMinfilter(Texture.FTLinearMipmapLinear)
        #set texture blend
        if base==False:
            terain.setDepthTest(DepthTestAttrib.MLessEqual)
            terain.setDepthWrite(False)
            terain.setTransparency(True) 
        terain.setTexture(tx)
        #get location
        dx=self.loc[0]*(-2+self.size[0])/1.0
        dy=self.loc[1]*(-2+self.size[1])/1.0
        #locate terrain
        if base==False:
            #we have to locate texture meshes a bit up in order to see them, not very nice, ideas(?)
            #terain.setPos(0,0,.02) 
            terain.setPos(dx,dy,.02)       
        else:
            #terain.setPos(0,0,0) 
            terain.setPos(dx,dy,0) 
        #return terrain nodePath
        return terain
    
################################################################################
if __name__ == "__main__":
    terrain=Terrain(heightmap="hmaps/h.jpg",colormap="hmaps/black.jpg",tilepath="terraintiles/")
    run()

Files.py

"""
Files:
    Contains several file management usefull functions
"""
import os

#===========================================================================
def custom_listdir(path,extfilter,extreturn=False):
    """   
    #usage: dir=custom_listdir(path="terraintiles/",extfilter=(".bmp",".jpg",".png",".tga"),extreturn=False)
    #Returns a list with given directory by 
        #-showing directories first 
        #-showing files by ordering the names alphabetically.
        #-removing files with unwanted extensions from the list
        #-returns another list with the file extensions if extreturn=True.
    #Shure there's a super more pythonic way to do this, sorry the mess!
    """
    dir = sorted([d for d in os.listdir(path) if os.path.isdir(path + os.path.sep + d)])
    dir.extend(sorted([f for f in os.listdir(path) if os.path.isfile(path + os.path.sep + f)]))
    #filter extensions
    unwanted=[]
    ext=[]
    dirok=[]
    extok=[]
    for i in range(0,len(dir)):
        #add path
        dir[i]=path+dir[i]
        #get extension
        ext.append(dir[i][dir[i].find("."):len(dir[i])]) 
        #add unwanted files to unwanted list
        n=0
        for e in range(0,len(extfilter)):   
            if ext[i].lower()<>extfilter[e].lower(): 
                n+=1   
        if n==len(extfilter):
            unwanted.append(dir[i])
    
    #make new lists without the unwanted files
    dirok=[]
    extok=[]
    for i in range(0,len(dir)):
        n=0
        for u in range(0,len(unwanted)):
            if dir[i]<>unwanted[u]:
                n+=1
        if n==len(unwanted):
            dirok.append(dir[i])
            extok.append(ext[i]) 
        
    #return dir list 
    if extreturn==True:
        return dirok,extok
    else:
        return dirok
    

#dir=custom_listdir(path="tiles/",extfilter=("",".bmp",".jpg",".png",".tga"),extreturn=False)
#for i in range(0,len(dir)):
    #print dir[i]

Thanks Treeform for the ‘treeterrain’ amazing script
Thanks pro-rsoft for advice

That’s it, enjoy retro-lovers! hope someone founds it usefull/helpfull!
Any feedback, comments, ideas, help will be apreciated :slight_smile:

c.

1 Like

I love random maps. Looks great :O)

Just downloaded this, very nice! I like retro terrains/environments of the Nintendo Mario/Zelda style, you know, blocky green grass and little blocky bushes and trees and buildings.

Have you ever played with the open source game engine Sauerbraten? It allows map/geometry editing to be done dynamically in-game, and they are making an RPG with it that looks much like the graphical style I am talking about. It uses an octree that the player can modify dynamically. I always thought it would be fun to write something like this for Panda, to allow the player to construct simple, blocky, cartoony but nice environments with terrains, trees, buildings etc. dynamically in-game as in sauerbraten.

You could then combine that with some code for steering characters that I posted a while ago, and some pathfinding code, and you could pretty easily have characters walking around this kin of environment with some degree of intelligence. Then, take it from there, there’s lots of kinds of game you could create.

i’m not really into modern superrealistic first person shooters he he i prefer tiny/cute environments with cool low res characers really breathing life…

so that’s the intention, construct patienly some world system with a spark of intelligence into it, for me that’s more important than top graphics, aldough of course i have to find a way to improve them and make the whole thing look a lot better, just in the beggining of the process.

Now im going to figure out mouse picking on the terrain, then i’ll implement an a-star algorythm for path finding, and that will generate a bunch of AI people to get born in the world

afterwards i’ll decide the percentage of Strategic or RPG enfasis, will see…

Thanks for the comments, i’m really starting to like this panda thing :slight_smile:

Does anybody knows a really fast way of mousepicking on the 3d terrain mesh?

c.

Might be worth taking a look at the game “darwinia” for research.

In fact i played that game some time ago : )
beautifull siplicity, nice audio clicks 'n cuts
a cool designed curiosity!

aldough the general sensation while playing was a bit cold, wich is a feeling that leads you to less addiction ; )

c

You might be correct there. As much as I enjoyed that game I lost interest eventually.

Hey seriously, i’m getting mad on finding the way of making this procedural generated terrain pickable, i’m able to make pickable loaded models calling the picker script:

e.g. -> self.picker.makePickable(self.camera.target)

wich assigns a pickable tag to the nodePath

eg -> def makePickable(self, newObj):
newObj.setTag(“pickable”,“true”)

but i CAN’T make that with any nodePath related to the generated terrain,
None of these works:

self.picker.makePickable(self.terrain.terrainNode)
self.picker.makePickable(self.terrain.terrainMesh)
self.picker.makePickable(self.terrain.gNode)

I searched on the forums and still havent found a way…

Anyone could give me a hand on that?
thanks.

Reminds me of the old sim city style terrains (2k/3k).

Did you ever get it working?
Sammual

It seems he figured out a way here:
discourse.panda3d.org/viewtopic … highlight=

exactly,
the whole point is i was generating the terrain triangles in a wrong orientation, looking down and not up, i changed the order of the vertices and it worked then.

anyway, i changed that later and i wrote a linear interpolation function for calculating the collisions with the terrain, like mindstormm says you can find it in the retro world demo

work has keep me away of panda for so long now, soon will be time to get into it again :slight_smile:

c

Would you be able to post the updated code?

Thanks,
Sammual

mhm i would really grab the code in

http://www.panda3d.net/phpbb2/viewtopic.php?t=4541&highlight=

in the Terrain.py script

this issue is fixed and everything is a bit optimised there

how do i get this to work? whenever i try to run it using the command prompt, it says that there is no image module…

how do i fix this?

As stated above, you need to install PIL (Python Image Library).

i’ve downloaded the Python image library installer, but whenever i run the installer, i get a message saying the program has encountered an error and it needs to close…

it does this every time i push “install” and it always does it immediately at that point, and doesnt even show the loading bar at all for the installer…

i’m wondering if there is a way to install the PIL manually? or from a different installer exe…

i wonder if i’m the only person having a problem with this…