Flight Sim using Ltrees

Jan 27, 2010 at 10:52 PM

I am working on a game based around simulation of bird flight. I am using a slightly customised version of Ltrees for the tree rendering;





I have stabilised the leaf billboards against roll, modified the lighting, implemented runtime generation and substitution of sprites for distant trees, and made the library use one giant vertex and index buffer for all trees instead of one set of buffers per tree.

Thanks for the great library Asger!

Jan 28, 2010 at 9:19 AM

Looks cool! I love seeing my work being put to good use. Maybe I should add the other type of free billboard as an option - others would probably enjoy it too.

Out of curiosity, what is the advantage of using one huge buffer instead of one for each mesh? Or maybe that's just a software architectural choice in your engine?

Jan 28, 2010 at 12:49 PM

Thanks. The main advantage of unified buffers is the ability to draw a whole clump of trees with three draw calls, instead of three draw calls per tree (trunks/leaves/edges). This reduces setup and call overhead. This works together with the technique I am using to share tree meshes without putting cloned trees too close together. With 50,000 or so trees on the map, it is not practical to have a unique mesh for every tree or generate them in realtime as the player flies through a forest at 100 kph.

Feb 10, 2010 at 3:38 AM

Here is another feature you might like to consider adding. I have created 'snowy' versions of some of the trees with snow drawn on to the top half of the leaf texture. However this conflicts with the randomisation of leaf sprite angle. I have hacked in a 'RandomiseLeafOrientation' property, and set it to false for the 'snowy' trees (and true for all the normal trees).

Feb 11, 2010 at 4:57 AM
Edited Feb 11, 2010 at 4:59 AM

The wind animation was not working properly on the Xbox 360; the leaves were moving much more than the trunk. I tracked this down to an incompatibility between the LeafVertex structure layout and the Xbox GPU - apparently on many GPUs vertex structures have to be aligned on a 64 bit boundary, also unless you specify [StructLayout(LayoutKind.Sequential)] the compiler may insert padding that messes up the byte offsets in the vertex definition. In this case it seems to have been causing normal values to be read as bone indices.

In the end I merged LeafVertex and TreeVertex into this structure;


        public Vector3     Position;          // Trunk vertex position, or center of the leaf.
        public Color       Color;             // Color tint of the leaf; set to black for trunks.
        public HalfVector4 TextureCoordinate; // Texture coordinate (xy) plus for leaves view space offset (zw).
        public HalfVector4 Normal;            // Normal vector to use in lighting calculations (xyz), plus bone index (w).

        public const int SizeInBytes = 8 * 4;

        public static readonly VertexElement[] VertexElements = {
            new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),
            new VertexElement(0, 3 * 4, VertexElementFormat.Color, VertexElementMethod.Default, VertexElementUsage.Color, 0),
            new VertexElement(0, 4 * 4, VertexElementFormat.HalfVector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
            new VertexElement(0, 6 * 4, VertexElementFormat.HalfVector4, VertexElementMethod.Default, VertexElementUsage.Normal, 0),


and also merged the tree trunk and leaf shaders so that I could render everything in two passes (opaque and edge blending) instead of three. This version slightly reduces memory use and integrates well with the mechanism I am using to crossfade the proper trees with the billboards as they move out of 'complex draw' range.

Feb 11, 2010 at 8:01 AM

How did you draw the trunk and opaque leaves in one pass? Their vertex shaders are completely different.

Thanks for telling me about the XBox issue. In the future I will have to look up some reference for compatibility issues with ATI cards and XBoxes. I don't want it to end up like some website that only works in internet explorer.

Feb 11, 2010 at 3:14 PM
Edited Feb 11, 2010 at 3:23 PM

I use the Color parameter to switch between leaf and trunk vertex shaders, and a mode variable to switch between leaf and trunk pixel shaders. I've been merging a lot of my shaders like this as I have been optimising the game - it seems that in XNA the overhead of doing more DrawPrimitives calls is far higher than the overhead of some additional tests in the shaders. Here is the shader I am using for the Ltree output;



// ----- Technique: Tree ------
// Combined trunk and leaf shader, loosely based on Ltrees code.

struct TreeVTP {
    float4 Position          : POSITION0;
    float2 TextureCoordinate : TEXCOORD0;
    float3 Color             : COLOR0;
    float  Fog               : TEXCOORD1;
    float  Mode              : TEXCOORD2;

TreeVTP TreeVertexShader(float4 inPos : POSITION0, float4 color : COLOR0,
                         float4 textureCoord : TEXCOORD0, float4 normal : NORMAL0) {

    TreeVTP output;	
    float4x4 bone = xBones[normal.w];
    float4 viewPos = mul(mul(inPos, bone), xWorldView);
    if(color.r == 0) {

		// Trunk lighting.
		normal = normalize(mul(mul(normal.xyz, bone), xWorld));
		float dlStrength = dot(normal, -xLightDirection);
		output.Color = dlStrength > 0.0f ? xAmbientLight + (xDirectionalLight * dlStrength) : xAmbientLight;
		output.Mode = 0.0f;
    } else {

		// Leaf lighting and billboarding.
	        viewPos.xyz += ((textureCoord.z * xLeafRight) + (textureCoord.w * xLeafUp)) * xLeafScale;   
		float3 lightDirection = -mul(xLightDirection, xView);
		float treeNormalLight = saturate(dot(normal.xyz, lightDirection));
		float leafNormalLight = saturate(dot(cross(xLeafRight, xLeafUp), lightDirection));
		float4 light = xDirectionalLight * ((0.5f * treeNormalLight) + (0.4f * leafNormalLight) + 0.3f);
		output.Color = (xAmbientLight + light) * color;
		output.Mode = 1.0f;
    output.Position = mul(viewPos, xProjection);
    float fog = length(viewPos);
    fog = 1.0f - saturate(fog * xFogScale); output.Fog = fog * fog;
    output.TextureCoordinate = textureCoord.xy;	
    return output;


float4 TreeOpaquePixelShader(TreeVTP PSin) : COLOR0 {

	float4 col; float a;
	if(PSin.Mode == 0.0f) {
		col = tex2D(TrunkSampler, PSin.TextureCoordinate);
		a = xTreeFade;
	} else {
		a = tex2Dbias(LeafSampler, float4(PSin.TextureCoordinate.xy, 1, -1)).a * xTreeFade;
		col = tex2D(LeafSampler, PSin.TextureCoordinate);	
        clip(col.a - 0.8f);
        return float4(lerp(xFogColour.rgb, col.rgb * PSin.Color, PSin.Fog), a);

} float4 TreeBlendPixelShader(TreeVTP PSin) : COLOR0 { float4 col; float a; if(PSin.Mode == 0.0f) { col = tex2D(TrunkSampler, PSin.TextureCoordinate); a = xTreeFade; } else { a = tex2Dbias(LeafSampler, float4(PSin.TextureCoordinate.xy, 1, -1)).a * xTreeFade; col = tex2D(LeafSampler, PSin.TextureCoordinate); } clip(0.8f - a); clip(a - 0.05f); return float4(lerp(xFogColour.rgb, col.rgb * PSin.Color, PSin.Fog), a); } technique Tree { pass Opaque { VertexShader = compile vs_3_0 TreeVertexShader(); PixelShader = compile ps_3_0 TreeOpaquePixelShader(); } pass Blended { VertexShader = compile vs_3_0 TreeVertexShader(); PixelShader = compile ps_3_0 TreeBlendPixelShader(); } }


Technically I should be using the alpha component of the colour to determine if the vertex is a trunk or a leaf, because that isn't used by the rendering code, whereas in theory someone could make a leaf with a red value of zero. Obviously not a problem when I control all the tree definitions.


In the vertex & index buffers I have a simple version of the tree trunk, then the leaf clouds, then a complex version of the tree trunk. This allows me to do an opaque pass on the simple trunk + leaves at longer ranges or the leaves + complex trunk at closer ranges, in one draw call.


I don't suppose you have a tree definition for a palm tree do you? I've been trying to write one but I can't get it to look right. :)