Storing a model as a texture.

EDIT: Cleaned up the code quite a bit, changed triangle creation to avoid wrapping around the edge of the shape map

Thanks Azraiyl, your primitive idea got me started doing this and your code was a huge help.

This class is builds a geom object from a three color image. Similar to a height map for terrain it uses the RGB of the image to get the XYZ of each of the models vertices.

SculptMaker.py

import math
import Image

from pandac.PandaModules import Geom
from pandac.PandaModules import GeomNode
from pandac.PandaModules import GeomPoints
from pandac.PandaModules import GeomTriangles
from pandac.PandaModules import GeomVertexData
from pandac.PandaModules import GeomVertexFormat
from pandac.PandaModules import GeomVertexWriter
from pandac.PandaModules import NodePath

class Sculpt:
    def __init__(self, name, smap, tint):
        self.name = name
        self.smap = Image.open(smap)
        self.dimx = self.smap.size[0]
        self.dimy = self.smap.size[1]
        self.tint = tint

    def generate(self):
        format = GeomVertexFormat.getV3n3c4t2()
        data = GeomVertexData("Data", format, Geom.UHDynamic)
        vertices = GeomVertexWriter(data, "vertex")
        normal = GeomVertexWriter(data, 'normal')
        colors = GeomVertexWriter(data, "color")
        texcoord = GeomVertexWriter(data, 'texcoord')
        triangles = GeomTriangles(Geom.UHDynamic)

        self.vcount = 0
        
        uv_unum = 1.0 / (self.dimx - 1)
        uv_vnum = 1.0 / (self.dimy - 1)

        u = 0.0
        self.uvu = []
        for i in range(self.dimx):
            self.uvu.append(u)
            u = u + uv_unum
        
        v = 0.0
        self.uvv = []
        for i in range(self.dimy):
            self.uvv.append(v)
            v = v + uv_vnum

        x = 0
        y = 0
        while y < self.dimy:
            while x < self.dimx:
                red, green, blue = self.smap.getpixel((x, y))
                r = float(red) / 256.0 - 0.5
                g = float(green) / 256.0 - 0.5
                b = float(blue) / 256.0 - 0.5
                vertices.addData3f(r, g, b)
                normal.addData3f(r, g, b)
                colors.addData4f(self.tint)
                texcoord.addData2f(self.uvu[x], self.uvv[y])
                x = x + 1
                self.vcount = self.vcount + 1
            x = 0
            y = y + 1
        
        self.vcount = self.vcount - 1

        def makeFace(vert1, vert2, vert3, vert4):
            triangles.addVertices(vert1, vert2, vert3)
            triangles.addVertices(vert2, vert4, vert3)
            triangles.closePrimitive()

        q = 0
        a = self.dimx
        qcount = self.dimx

        while qcount<self.vcount:
            w = q + 1
            s = a + 1
            if ((w)%self.dimy == 0):
                q = q + 1
                a = a + 1
            else:
                makeFace(q,a,w,s)
                q = q + 1
                a = a + 1
            qcount = qcount + 1
        makeFace(q,a,w,s)

        geom = Geom(data)
        geom.addPrimitive(triangles)

        node = GeomNode(self.name)
        node.addGeom(geom)

        return NodePath(node)

To use it just import it into your script like usual:

from SculptMaker import *

The Sculpt function takes 3 variables, name (a string), image map (the image for the model shape) and tint (four floats for the color of the mesh).

example:

import sys

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject

from pandac.PandaModules import *
from SculptMaker import *

base.setBackgroundColor(0.0, 0.0, 0.0)

class Master(DirectObject):
    def __init__(self):
        self.accept('f1', self.wire)
        self.accept('f2', self.tex)
        self.accept("escape", sys.exit)

    def tex(self):
        base.toggleTexture()

    def wire(self):
        base.toggleWireframe()

    cube = Sculpt("Test", "scu-easle.png", (1,1,1,1)).generate()
    ts = TextureStage('ts')
    uvmap = loader.loadTexture('wood-edge.png')
    uvmap.setWrapU(Texture.WMRepeat)
    uvmap.setWrapV(Texture.WMRepeat)
    cube.setTexture(ts, uvmap)
    cube.setTexOffset(ts, 0.0, 0.52);
    cube.setTexRotate(ts, 90);
    cube.setTexScale(ts, 4, 78)
    cube.reparentTo(render)
    cube.setScale(.75,.75,2)

    # Create Ambient Light
    ambientLight = AmbientLight('ambientLight')
    ambientLight.setColor(Vec4(0.189, 0.189, 0.189, 0))
    ambientLightNP = render.attachNewNode(ambientLight.upcastToPandaNode())
    render.setLight(ambientLightNP)

    # Directional light 01
    directionalLight = DirectionalLight('directionalLight')
    directionalLight.setColor(Vec4(0.889, 0.912, 0.778, 1))
    directionalLightNP = render.attachNewNode(directionalLight)
    directionalLightNP.setHpr(160, -20, 0)
    render.setLight(directionalLightNP)

m = Master()
run()

Here’s a screenshot of what this example creates:

And these are the source images for the map and texture:
map:

texture:

Hope some folks find it useful, needs work of course but I think it’s off to a good start.
Enjoy!

Wow. That’s pretty cool, thanks!

When Panda gets support for Geometry Shaders it would even be possible to do this entirely on the GPU :slight_smile:

Not that this is entirely relevant, but fyi Second Life uses this exact approach for users to import custom meshes into their platform.

Yep, there approach is what made me want to do this. The blender scripts here http://www.dominodesigns.info/second_life/blender_scripts.html are excellent for creating them. I’ve yet to figure out how to make the different “stitching” methods a reality but I’m sure I can get there eventually.

The beautiful upside to this method is size. Obj, egg, collada are all rather large files, if one plans to send mesh data via a network this is a fantastic way to do it.

If I could figure out a way to combine this with the geomipterrain then I imaging doing really interesting landscapes with caves and over hangs would be possible.

I could not run it because I did not have PIL installed, now I think it’ll work.

I just had rewrite import Image to from PIL import Image

ok, it did not work.

panda v. 1.6.1.:

Black screen(no tracebacks)

Now I think: ain’t it PNMImage?

if it is so what’s the problem?

It’s not that old to be incomplatible with 1.6.1!

Well, I use v1.6.1 so I know it works with that version. Also this class doesn’t use PNMImage at all, it uses the python image library (PIL) to read images. Perhaps you are trying to run the class as a script? If you are running the example code I posted and all you see is black then most likely the camera needs to be repositioned. In that sample there is no camera control specified so both the camera and the object are at 0,0,0.