How are the textures generated in "Project Hex"?

OK, it looks like GeoMipTerrain won’t cut it for my current project, so I’m “borrowing” some ideas from Shamus Young’s Project Hex (http://www.shamusyoung.com/twentysidedtale/?p=9844 ). Specifically, how he textures his terrain. There are multiple terrain types (grass, stone, sand, etc.), which overlap at their boundaries. I can only think of two ways he could have done it, neither of which makes sense to me:

A. Using multiple texture layers, one for each terrain type, and overlaying them.

B. Procedurally generating one massive texture to be stretched over the entire terrain mesh.

It seems like the performance of either of these would be way too low, but I don’t know. Am I missing something here?

EDIT:

Yeah, he is using one massive procedural texture. It says so right in the article. Sorry for clogging up the board…I’ll come back and post my results if I get it working.

OK, I’m back!

I have most of it working. But I can’t figure out how to get the alpha values of the texture splats to display correctly. This is probably an idiosyncrasy of the PNMImage or Texture classes. My code is below if anyone wants to use it or (hopefully) give me a hand.

EDIT: It was the “gaussianFilterFrom()” function call that I used to resize the texture atlas. It was setting the RGB color of every transparent pixel to white. I replaced that line with a call to “boxFilterFrom()” and it worked fine.

import direct.directbase.DirectStart
from panda3d.core import *


def terrainTexture(
	terrain, 					#Generated GeoMipTerrain object
	atlas, 						#Texture object containing texture atlas
	subimages, 					#How many splats are in the texture atlas
	textureSize=(1024,1024), 	#How big should the procedural texture be
	splatScale=2.0				#Splat size as a multiple of splat spacing
):
	#Check dimensions of textures, since it's not done automatically.
	for size in [textureSize, (atlas.getXSize(), atlas.getYSize()) ]:
		for dimension in size:
			if dimension & (dimension-1) != 0:
				print "One of the dimensions of one of your textures is not a power of two.  Returning None."
				return None
			
	#Load texture atlas
	atlasImage=PNMImage()
	atlas.store(atlasImage)

	#How far apart splats will be, given one for each heightmap value.
	splatSpacing = (
		#~ terrain.heightfield().getXSize(), 
		#~ terrain.heightfield().getYSize()  
		(textureSize[0] / terrain.heightfield().getXSize() )+1, 
		(textureSize[1] / terrain.heightfield().getYSize() )+1 
	)

	#Make new image object to hold scaled-up copy of atlas
	splatImage=PNMImage( 
		int(splatScale * splatSpacing[0] * subimages), 
		int(splatScale * splatSpacing[1])
	)

	#Resize atlas and store in new image
	splatImage.boxFilterFrom(1.0, atlasImage)

	#Record new size of splats after scaling
	splatWidth=splatImage.getXSize()/subimages
	splatHeight=splatImage.getYSize()

	#Make terrain texture and paint splats over it
	terrainTextureImage=PNMImage(*textureSize)
	for layer in range(subimages):
		for x in range(terrain.heightfield().getXSize()):
			for y in range(terrain.heightfield().getYSize()):
				
				elevation=terrain.heightfield().getXel(x,y).getX()
				
				#The terrain's vertical range is divided into the same number of "zones"
				#as there are subimages in the atlas.
				currentSubimage=int( (elevation*subimages) )
				offset=currentSubimage*splatWidth
	
				#Higher layers overlap lower layers.  Within a layer, splats with greater x or 
				#y coordinates overlap others.
				if currentSubimage != layer:continue
				
				#Splats are centered over the heightmap points
				toPoint = ( 
					(x*splatSpacing[0])-(splatWidth/2),  
					(y*splatSpacing[1])-(splatHeight/2)
				)
				
				fromPoint = (offset, 0)
				
				terrainTextureImage.copySubImage(
					splatImage, 
					toPoint[0], toPoint[1], 
					fromPoint[0], fromPoint[1], 
					splatWidth, splatHeight
				)
			
	terrainTexture=Texture()
	terrainTexture.load(terrainTextureImage)
	return terrainTexture


terrain=GeoMipTerrain('terrain')
terrain.setHeightfield(Filename('assets/heightmap.png'))
terrain.generate()
terrain.getRoot().reparentTo(render)
terrain.getRoot().setSz(10)
atlasTexture=loader.loadTexture('assets/atlas.png')

terrain.getRoot().setTexture(terrainTexture(terrain, atlasTexture, 4))

run()