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
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
In case others haven’t seen yet: This is what can be done with ngPlant:
http://povrayscrapbook.awardspace.com/
http://yorik.orgfree.com/greenhouse.html
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):
http://www.dachau.net/users/pfrogner/kiefer.jpg
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: https://discourse.panda3d.org/viewtopic.php?t=3114
(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:
http://www.vrvis.at/vr/billboardclouds/
http://www.ogre3d.org/wiki/index.php/SoC2006_BillboardClouds
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
# _____________________________________________________________________________
# MODEL
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 ) )
else:
parentNode = data
for grpIdx in xrange( plant.GetGroupCount( ) ):
grp = plant.GetGroup( grpIdx )
# filter
if lod:
if not grp.IsLODVisRangeEnabled( ):
continue
_ngout, _ngin = grp.GetLODVisRange( )
if _lod <= _ngout or _lod >= _ngin:
continue
else:
if grp.IsLODVisRangeEnabled( ):
continue
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 )
else:
iBuffer = grp.GetIndexBuffer( _ngp.TRIANGLE_LIST )
makeTriangleVertices( )
makeTrianglePolys( grpNode, tex, texDbl )
#grpNode.meshTriangles( EggGroup.TConvex )
# _____________________________________________________________________________
# NORMAL MODE (TRIANGLES)
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
# _____________________________________________________________________________
# BILLBOARD MODE
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
# _____________________________________________________________________________
# SINGLE QUAD FOR SNAPSHOT
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
# _____________________________________________________________________________
# MAIN
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
//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;
vtx_position.xyz += rightVector * ( vtx_texcoord0.y - 0.5f ) * width;
vtx_position.xyz += 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( k_lightvec.xyz );
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;
}