Rendering of trees

This script renders trees similar to the ones that are generated by SpeedTree. The basic trick is to have some kind of “billboard” effekt on every single leave of a tree. Facing every leave towards the camera gives a more volumetric look of the tree, from every direction. The billboard effect is created by a simple vetex shader that expands groups of four (almost) identical vertices using a pre-rotated quad. Information about which corner of the quad a vertex is is stored in the third texture coordinate.

http://www.dachau.net/users/pfrogner/pandaTree1.jpg
http://www.dachau.net/users/pfrogner/pandaTree2.jpg

Demo-Tree.py:

import direct.directbase.DirectStart

from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import WindowProperties
from pandac.PandaModules import Vec4
from pandac.PandaModules import Vec3
from pandac.PandaModules import TextureStage
from direct.task.Task import Task


class BaseWorld( DirectObject ):

    def __init__( self ):

        # Base setup
        base.setBackgroundColor( 0, 0, 0 )
        base.enableParticles( )
        base.disableMouse( )
        base.win.movePointer( 0, 100, 100 )

        props = WindowProperties( )
        props.setCursorHidden( True )
        base.win.requestProperties( props )

        # Camera control
        self.heading = 0
        self.pitch = 0
        self.last = 0
        self.focus = Vec3( 0, -40, 10 )
        self.mouseButton = [ 0, 0, 0 ]

        # Input
        self.accept( 'f1', self.toggleWireframe )
        self.accept( 'f2', self.toggleTexture )
        self.accept( 'f3', self.screenshot )
        self.accept( 'escape', self.exit )

        self.accept( 'mouse1', self.setMouseButton, [ 0, 1 ] )
        self.accept( 'mouse1-up', self.setMouseButton, [ 0, 0 ] )
        self.accept( 'mouse2', self.setMouseButton, [ 1, 1 ] )
        self.accept( 'mouse2-up', self.setMouseButton, [ 1, 0 ] )
        self.accept( 'mouse3', self.setMouseButton, [ 2, 1 ] )
        self.accept( 'mouse3-up', self.setMouseButton, [ 2, 0 ] )

        # Content
        self.setupWorld( )

        # Task
        taskMgr.add( self.updateCamera, 'updateCamera' )

    def setupWorld( self ):
        pass

    def updateCamera( self, task ):

        md = base.win.getPointer( 0 )
        x = md.getX( )
        y = md.getY( )

        if base.win.movePointer( 0, 100, 100 ):
            self.heading = self.heading - ( x - 100 ) * 0.2
            self.pitch = self.pitch - ( y - 100 ) * 0.2
        if ( self.pitch < -80 ): self.pitch = -80
        if ( self.pitch >  80 ): self.pitch =  80
        base.camera.setHpr( self.heading, self.pitch, 0 )

        dir = base.camera.getMat( ).getRow3( 1 )
        elapsed = task.time - self.last

        if ( self.mouseButton[ 0 ] ):
            self.focus = self.focus + dir * elapsed * 30
        if ( self.mouseButton[ 1 ] ) or ( self.mouseButton[ 2 ] ):
            self.focus = self.focus - dir * elapsed * 30
        base.camera.setPos( self.focus - ( dir * 5 ) )

        self.focus = base.camera.getPos( ) + ( dir * 5 )
        self.last = task.time

        return Task.cont

    def setMouseButton( self, button, value ):
        self.mouseButton[ button ] = value

    def toggleWireframe( self ):
        base.toggleWireframe( )

    def toggleTexture( self ):
        base.toggleTexture( )

    def screenshot( self ):
        base.screenshot( 'screenshots/Demo' )

    def exit( self ):
        taskMgr.stop( )


class World( BaseWorld ):

    def setupWorld( self ):

        self.accept( 'f4', self.resetCamera )

        tex = loader.loadTexture( 'models/treeLeaf.png' )
        ts = TextureStage( 'leaf' )

        # Sky box
        self.skyNP = loader.loadModelCopy( 'models/skyBox' )
        self.skyNP.reparentTo( render )
        self.skyNP.setScale( 100, 100, 100 )
        self.skyNP.setPos( 0, 0, 0 )

        # Trunk
        self.trunkNP = loader.loadModelCopy( 'models/treeTrunk' )
        self.trunkNP.reparentTo( render )
        self.trunkNP.setPos( 0, 0, 0 )

        # Leaves
        self.leafNP = loader.loadModelCopy( 'models/treeLeaves' )
        self.leafNP.reparentTo( render )
        self.leafNP.setTransparency( True )
        self.leafNP.setTexture( ts, tex )
        self.leafNP.setPos( 0, 0, 0 )
        self.leafNP.setShader( loader.loadShader( 'leaf.sha' ) )

        # Task
        taskMgr.add( self.updateWorld, 'updateWorld' )

    def updateWorld( self, task ):

        right = render.getRelativeVector( camera, Vec3.right( ) )
        right = Vec4( right.getX( ), right.getY( ), right.getZ( ), 0 )

        up = render.getRelativeVector( camera, Vec3.up( ) )
        up = Vec4( up.getX( ), up.getY( ), up.getZ( ), 0 )

        v0 =  right + up
        v1 = -right + up
        v2 = -right - up
        v3 =  right - up

        self.leafNP.setShaderInput( 'v0', v0 )
        self.leafNP.setShaderInput( 'v1', v1 )
        self.leafNP.setShaderInput( 'v2', v2 )
        self.leafNP.setShaderInput( 'v3', v3 )

        return Task.cont

    def resetCamera( self ):
        self.heading = 0
        self.pitch = 0
        self.focus = Vec3( 0, -10, 0 )


if base.win.getGsg( ).getSupportsBasicShaders( ):
    world = World( )
    run( )
else:
    print 'Shaders not supported!'

leaf.sha:

//Cg

//Cg profile arbvp1 arbfp1

void vshader( float4 vtx_position : POSITION,
              float3 vtx_texcoord0 : TEXCOORD0,
              uniform float4x4 mat_modelproj,
              uniform float4 k_v0,
              uniform float4 k_v1,
              uniform float4 k_v2,
              uniform float4 k_v3,
              out float3 l_texcoord0 : TEXCOORD0,
              out float4 l_position : POSITION )
{
    float4 offset = float4( 0, 0, 0, 0 );
    if ( vtx_texcoord0.z == 0.0 ) {
        offset = k_v0;
    } else if ( vtx_texcoord0.z == 1.0 ) {
        offset = k_v1;
    } else if ( vtx_texcoord0.z == 2.0 ) {
        offset = k_v2;
    } else if ( vtx_texcoord0.z == 3.0 ) {
        offset = k_v3;
    }

    l_texcoord0 = vtx_texcoord0;
    l_position = mul( mat_modelproj, vtx_position + offset );
}

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

A zip-file containing the full demo can be downloaded here:
http://www.dachau.net/users/pfrogner/Demo-Tree.zip

The code is not yet final, and there are still a few things I’d like to improve. Perhaps someone knows advice.

(1) When creating more than one tree this way, I would have to use setShaderInput( ) on every tree node each frame. Since the parameters don’t depend on the tree, but only on the camera orientation, it would be nice to set the parameters only once per frame, for the shader.

(2) I am new to CG, and this has been my first shader. Instead of the ugly “if… else” lines I would like to do access to an array using texcoord0.z as index:

float4 quad[4] = { k_v0, k_v1, k_v2, k_v3 };
offset = quad[ texcoord0.z ];

enn0x

hm… perhaps…try to…
-use random rotation of the leaves within a certan range
-use some 2d planes with tree-like painting on it add twigs
-darken the leaves on bottom and the inside of the tree to fake self-shadowing. (no sharp step but smooth blending between them)

hm… that’s all that came to my mind without thinking^^

That is interesting, and thx for the post…nice work indeed and thx for taking time to do that and sharing.

I would like of course to see animation, but that is picking as atm it looks nice at least and I imagine your working on that anyway.

Planta is another alternative, but its not out yet.

OPentree is another one , but ive never used it…

cheers
neighborlee