ngplant wrapper

based on a word in this thread ( i got the idea of wrapping ngplant into panda3d, it’s not complete, and not very well tested, but works for me.

Maybe… I’ll include this into the jungle engine once :slight_smile: … (if computers get 100 times faster)…

Download it here:

You will also need to download and install the python library of ngplant.

I think we can do better with Panda3D than just modifying the ngp2obj script that comes with ngPlant.

  • create an egg file via the egg library, not .obj. Egg is way more powerful, and it is the native Panda3D format. So your should write something like

  • look out for billboard mode using grpMat.Billboard. Get precomputed billboard vertices with grp.GetVAttrBufferI( _ngp.ATTR_BILLBOARD_POS ). I also suggest using the API in indexed mode, give way more simple code.

  • make multi-LOD models inside one egg. This is why I have been talking about EggSwitchConditionDistance in the thread you refer.

  • make use of ngshot.exe for creating full-plant images. Very useful for maximum LOD.

Ask for my code if you want to do better, most things are there already. :slight_smile:


I’d love to see your code :slight_smile:

However i dont really understand why you would want to write an egg file instead of directly creating a model in panda3d. There is allways the possibility of nodePath.writeBamFile( ‘filename’ ) if you want it on the disk. I’d think this might be faster if you want to use the model directly in a scene (not having checked for any lod related stuff).

You might have mistaken some stuff i left in the package, that i’d use the ngplant2obj converter, i just built my converter based on this code and forgot to rename it.

Anyway it’s a great tool which could be very interesting for a lot of projects (minimizing model size & vegetation variety)

Sure, here it is. Far from finished, but halfway functional at least.

Reasons why I prefer .egg (and not .bam):

(1) .egg is rather version-independent. .bam models have to be re-created for each new version of Panda3D.

(2) A.F.A.I.K. there is some optimization stuff going on when loading .egg or converting .egg to .bam. I don’t know any details, though. If using vertex writers then these optimization possibilities can’t be used.

(3) .egg can be modified more easily if e.g. you want to change mipmap filters for textures later, add lod switches, or add gameplay related stuff like collision meshes. For a tree I reckon it would be enough to just add a tube for the stem. Everything else (branches, leaves) could be walk-through.

(4) Creating multiple trees. I think this is possible with vertex writers too, but you have to store the geom, and not recreate mesh data for every tree instance.

One advantage of creating the mesh at run-time (like you do) would be that you could create dozens of similar trees from only one ngPlant model, by choosing different seeds. If going via .egg files I have to create one model per seed.

Right. I didn’t read the code careful enough. Sorry :slight_smile:

Yes, it is. I hope it won’t go the same way EarthSculptor, which is commercial since release 1.0. Only maps below 257x257 are supported in the free version :frowning:

In case others haven’t seen yet: This is what can be done with ngPlant:

Here are some screenshots from what I can do right now, using the ngPlant billboard mode and a small vertex shader (code at the bottom of this post):

Shortcomings of the (unfinished) code below:

(1) LOD is not working so far. It required EggSwitchConditionDistance, which is not available at the Python API so far. I hope this will be fixed with the next release. See this thread too:

(2) Billboard have a size (width, height) in ngPlant. For now the size is hard coded in the shader. I’m pretty sure there is a way to store this information in the .egg model (tags perhaps?). Todo.

(3) Using ngshot.exe it is possible to create images which show the whole plant/tree. For far-away trees this is perhaps the fastest way of rendering: a single billboard. So far ngshot has to be called manually. Todo: call from script, with proper parameters.

(4) Lighting: A lot of realism comes from good lighting. I think self-shadowing would be very nice to have. A simple approach could be to make billboards/polygons inside the tree darker to mimic self-shadowing. Todo.

Finally, the ultimate goal would be to have forests with several hundred of tree rendered. Maybe billboard clouds (decomposing complex models into a small number of billboards) are the right way to do this:

Anyway, rendering hundreds of Panda3D billboards is not very fast in Panda3D. So even if I use only one billboard per tree, it will be slow to render a forest with say 1000 trees in Panda3D. Maybe there is some way to speed this up. Perhaps by subdividing a “vegetation layer” into small chunks, and do billboard computations per chunk (in C++ or shader ???). I guess you are the one with the most experience here, Hypnos, because you have written the JungleEngine already.

from pandac.PandaModules import EggComment
from pandac.PandaModules import EggData
from pandac.PandaModules import EggGroup
from pandac.PandaModules import EggPolygon
from pandac.PandaModules import EggRenderMode
#from pandac.PandaModules import EggSwitchConditionDistance
from pandac.PandaModules import EggTexture
from pandac.PandaModules import EggVertex
from pandac.PandaModules import EggVertexPool
from pandac.PandaModules import Filename
from pandac.PandaModules import Point2D
from pandac.PandaModules import Point3D
from pandac.PandaModules import Vec3D

import os.path
import _ngp

version = '0.1'

_p = Point3D( 0, 0, 2 )

plant = None
data = None
vp = None
vpool = None

iBuffer = None
vBuffer = None
nBuffer = None
tBuffer = None
bBuffer = None

# _____________________________________________________________________________

def startModel( ngpfile, seed=123 ):
    global plant
    global data
    global vp

    plant = _ngp.PlantInstance( ngpfile, seed )

    vp = EggVertexPool( 'plant' )
    comment = EggComment( '', 'Created with ngp2egg version %s' % version )

    data = EggData( )
    data.setCoordinateSystem( 1 ) # CS_zup_right
    data.addChild( comment )
    data.addChild( vp )

def finishModel( eggfile ):
    data.writeEgg( Filename( eggfile ) )

def createModel( lod=None ):
    if lod:
        _lod, _in, _out = lod
        parentName = 'plant_%f' % _lod
        parentNode = EggGroup( parentName )
        ###parentNode.setLod( EggSwitchConditionDistance( _in, _out, _p ) )
        parentNode = data

    for grpIdx in xrange( plant.GetGroupCount( ) ):
        grp = plant.GetGroup( grpIdx )

        # filter
        if lod:
            if not grp.IsLODVisRangeEnabled( ):
            _ngout, _ngin = grp.GetLODVisRange( )
            if _lod <= _ngout or _lod >= _ngin:
            if grp.IsLODVisRangeEnabled( ):

        createGroup( parentNode, grp, grpIdx )

def createGroup( parentNode, grp, grpIdx ):
    global vpool
    global iBuffer
    global vBuffer
    global nBuffer
    global tBuffer
    global bBuffer

    grpMat = grp.GetMaterial( )

    vpool = { }

    # create group node
    grpName = ( grpMat.Billboard and 'leave.%i' or 'branch.%i' ) % grpIdx
    grpNode = EggGroup( grpName )
    parentNode.addChild( grpNode )

    # texture
    texDbl = grpMat.DoubleSided
    texBase = os.path.splitext( os.path.basename( grpMat.TexName ) )[0]
    texFn = texBase + '.png'

    tex = EggTexture( texBase, texFn )
    tex.setAlphaMode( EggRenderMode.AMMsMask )
    tex.setMagfilter( EggTexture.FTLinearMipmapLinear )
    tex.setMinfilter( EggTexture.FTNearestMipmapLinear )

    # mesh
    vBuffer = grp.GetVAttrBufferI( _ngp.ATTR_VERTEX )
    nBuffer = grp.GetVAttrBufferI( _ngp.ATTR_NORMAL )
    tBuffer = grp.GetVAttrBufferI( _ngp.ATTR_TEXCOORD0 )
    bBuffer = grp.GetVAttrBufferI( _ngp.ATTR_BILLBOARD_POS )

    if grpMat.Billboard:
        iBuffer = grp.GetIndexBuffer( _ngp.TRIANGLE_LIST )

        makeBillboardVertices( )
        makeBillboardPolys( grpNode, tex, texDbl )

        width, height = grp.GetBillboardSize( )
        grpNode.setTag( 'billboardWidth', '%6.4f' % width )
        grpNode.setTag( 'billboardHeight', '%6.4f' % height )

        iBuffer = grp.GetIndexBuffer( _ngp.TRIANGLE_LIST )

        makeTriangleVertices( )
        makeTrianglePolys( grpNode, tex, texDbl )

        #grpNode.meshTriangles( EggGroup.TConvex )

# _____________________________________________________________________________

def makeTriangle( grpNode, tex, texDbl ):
    poly = EggPolygon( )
    poly.setTexture( tex )
    ###poly.setBfaceFlag( texDbl )

    poly.addVertex( vpool[ iBuffer.pop( 0 ) ] )
    poly.addVertex( vpool[ iBuffer.pop( 0 ) ] )
    poly.addVertex( vpool[ iBuffer.pop( 0 ) ] )

    grpNode.addChild( poly )

def makeTrianglePolys( grpNode, tex, texDbl ):
    while iBuffer:
        makeTriangle( grpNode, tex, texDbl )

def makeTriangleVertices( ):
    assert len( vBuffer ) == len( nBuffer ) == len( tBuffer )

    for idx in range( len( vBuffer ) ):
        x, y, z    = vBuffer[ idx ]
        nx, ny, nz = nBuffer[ idx ]
        u, v       = tBuffer[ idx ]

        x, y, z = ( x, -z, y )       # Transform:  ngPlant ==> Panda3D
        nx, ny, nz = ( nx, -nz, ny ) # Transform:  ngPlant ==> Panda3D

        vertex = EggVertex( )
        vertex.setPos( Point3D( x, y, z ) )
        vertex.setNormal( Vec3D( nx, ny, nz ) )
        vertex.setUv( Point2D( u, v ) )

        vp.addVertex( vertex )

        vpool[ idx ] = vertex

# _____________________________________________________________________________

def makeBillboard( grpNode, tex, texDbl ):
    poly = EggPolygon( )
    poly.setTexture( tex )

    v1 = vpool[ iBuffer.pop( 0 ) ]
    v2 = vpool[ iBuffer.pop( 0 ) ]
    v3 = vpool[ iBuffer.pop( 0 ) ]
    v4 = vpool[ iBuffer.pop( 0 ) ]
    v5 = vpool[ iBuffer.pop( 0 ) ]
    v6 = vpool[ iBuffer.pop( 0 ) ]

    poly.addVertex( v1 )
    poly.addVertex( v2 )
    poly.addVertex( v6 )
    poly.addVertex( v3 )

    grpNode.addChild( poly )

def makeBillboardPolys( grpNode, tex, texDbl ):
    while iBuffer:
        makeBillboard( grpNode, tex, texDbl )

def makeBillboardVertices( ):
    assert len( bBuffer ) == len( nBuffer ) == len( tBuffer )

    for idx in range( len( bBuffer ) ):
        x, y, z    = bBuffer[ idx ]
        u, v       = tBuffer[ idx ]

        x, y, z = ( x, -z, y ) # Transform:  ngPlant ==> Panda3D

        x += idx * 0.001
        y += idx * 0.001
        z += idx * 0.001

        vertex = EggVertex( )
        vertex.setPos( Point3D( x, y, z ) )
        vertex.setNormal( Vec3D( 0, 0, 1 ) )
        vertex.setUv( Point2D( u, v ) )

        vp.addVertex( vertex )

        vpool[ idx ] = vertex

# _____________________________________________________________________________

def createSnapshot( texFn, lod=None ):
    # group node
    grpNode = EggGroup( 'far' )
    grpNode.setBillboardType( EggGroup.BTAxis )
    data.addChild( grpNode )

    # lod (optional)
    if lod:
        _in, _out = lod
        ###grpNode.setLod( EggSwitchConditionDistance( _in, _out, _p ) )

    # texture
    texBase = texFn.split( '.' )[0]

    tex = EggTexture( texBase, texFn )
    tex.setAlphaMode( EggRenderMode.AMMsMask )
    tex.setMagfilter( EggTexture.FTLinearMipmapLinear )
    tex.setMinfilter( EggTexture.FTNearestMipmapLinear )

    # vertices
    size = plant.GetBoundingBox( )[1][1] # upper, y-coordinate
    dx = 0.5 * size
    dz = size

    v1 = makeQuadVertex( -dx,  0, 0, 0 )
    v2 = makeQuadVertex(  dx,  0, 1, 0 )
    v3 = makeQuadVertex(  dx, dz, 1, 1 )
    v4 = makeQuadVertex( -dx, dz, 0, 1 )

    # polygon
    poly = EggPolygon( )
    poly.setTexture( tex )

    poly.addVertex( v1 )
    poly.addVertex( v2 )
    poly.addVertex( v3 )
    poly.addVertex( v4 )

    grpNode.addChild( poly )

def makeQuadVertex( x, z, u, v ):
    vertex = EggVertex( )
    vertex.setPos( Point3D( x, 0, z ) )
    ###vertex.setNormal( Vec3D( 0, 0, 1 ) )
    vertex.setUv( Point2D( u, v ) )

    vp.addVertex( vertex )
    return vertex

# _____________________________________________________________________________

if __name__ == '__main__':

    startModel( 'kiefer.ngp' )
    createModel( )
    finishModel( 'kiefer.egg' )

    #startModel( 'kiefer.ngp' )
    #createModel( ( 0.1, 10,  0 ) )                        # _lod, _in, _out
    #createModel( ( 0.9, 30, 10 ) )                        # _lod, _in, _out
    #createSnapshot( 'kiefer_billboard.png', ( 30, 10 ) )  # _in, _out
    #finishModel( 'kiefer.egg' )

And here is the (preliminary) shader I use for rendering the leave billboard:


//Cg profile arbvp1 arbfp1

void vshader( in float4 vtx_position : POSITION,
              in float3 vtx_normal : NORMAL,
              in float3 vtx_texcoord0 : TEXCOORD0,
              in uniform float4x4 mat_modelproj,
              in uniform float4x4 mat_modelview,
              in uniform float4 k_lightvec,
              out float l_brightness,
              out float3 l_texcoord0 : TEXCOORD0,
              out float4 l_position : POSITION )
    float3 upVector    = mat_modelview._m00_m01_m02;
    float3 rightVector = mat_modelview._m10_m11_m12;

    // todo:
    // - size as parameter, like lightvec
    // - offsets for uv could be better ???

    float width = 3.0f;
    float height = 3.0f; += rightVector * ( vtx_texcoord0.y - 0.5f ) * width; += upVector * ( vtx_texcoord0.x - 0.5f ) * height;

    l_texcoord0 = vtx_texcoord0;
    l_position = mul( mat_modelproj, vtx_position );

    // lighting
    float3 N = normalize( vtx_normal );
    float3 L = normalize( );
    l_brightness = max( dot( -N, L ), 0.0f ) * 0.7f + 0.3f;

void fshader( in float3 l_texcoord0 : TEXCOORD0,
              in uniform sampler2D tex_0 : TEXUNIT0,
              in float l_brightness,
              out float4 o_color : COLOR )
    o_color = tex2D( tex_0, l_texcoord0.xy );

    // lighting
    //o_color = o_color * l_brightness;

Sorry for digging that thread out. Im really a beginner in panda3d, linux and python. When I tried to run the of Hypnos, which should as far as i understand display the palm.ngp in the panda engine with a rotating camera. However, when i try to run it i get the following error:

Traceback (most recent call last):
File “”, line 6, in
import _ngp
ImportError: No module named _ngp
robin@PMI37Ubuntu:~/Dokumente/Freizeit/3d und games/toolz/panda-ngplant/src$ python
Traceback (most recent call last):
File “”, line 6, in
import _ngp
ImportError: No module named _ngp

I have located file, which I think is required in the /proc/10313/cwd/pywrapper folder. How can I tell python to find this file?

I know that that is more a pyton than a panda problem, but I haven’t found a solution with searching on the internet and would be very glad if someone could help me.