The SpeedTree SDK can be configured to have N render passes, each one defined by a name string. In the reference application, we define Forward, Depth Only (shadow casting), and Deferred in MyRenderPassNames.h
. These names are passed in an array to each model object in the SDK via a call to pModel→SetRenderPasses().
This is demonstrated in CMyPopulate::InitBaseTreeGraphics()
in MyPopulate.cpp
. Right after this code, there is a function call to establish a render config callback for each model (the reference application uses the same callback for all models). The callback is invoked several times for each model, each time with a different configuration to allow the developers to set as many or as few state changes as they need. For example, it’s possible to use only two render configurations for the entire forest: one for 3D tree and grass models, another for the billboard models. Alternatively, it’s possible to establish a different render state/shader for each LOD and each geometry type within each LOD. The former will generate fewer draw calls/state changes and the latter more (but with potentially less general and shorter shaders).
Listing 1 below shows our reference application’s render configuration callback, located in MyPopulate.cpp
. Note that there is some specialization in this callback, but not a lot. There are separate shaders for grass, trees (each uses a different vertex definition), and billboards. There’s also a distinction for the depth-only pass to allow significantly shorter shaders to be used. The SDK intelligently sorts by these states to minimize state changes in the whole-forest draw loop.
Listing 1. Reference Application Example Callback
void MyRenderStateCallback(const SDrawCallDetails* pDrawCallDetails, SClientPipeline* pClientPipeline) { // get application pointer assert(g_pAppData); // global pointer to reference application const SMyCmdLineOptions& sCmdLineOptions = g_pApp->GetCmdLineOptions( ); const CMyConfigFile& cConfigFile = g_pApp->GetConfig( ); // pass info based on the name of the render pass const st_bool c_bForwardLitPass = (strcmp(pDrawCallDetails->m_pPassName, c_pMyRenderPassForwardLit) == 0); const st_bool c_bDeferredPass = (strcmp(pDrawCallDetails->m_pPassName, c_pMyRenderPassDeferred) == 0); const st_bool c_bDepthOnlyPass = (strcmp(pDrawCallDetails->m_pPassName, c_pMyRenderPassDepthOnly) == 0); const st_bool c_bFogOnMesh = pDrawCallDetails->m_bFogOn3dMesh && c_bForwardLitPass; // render state pClientPipeline->m_sRenderState.m_eFaceCulling = pDrawCallDetails->m_bBillboard ? CULL_FACE_BACK : CULL_FACE_NONE; pClientPipeline->m_sRenderState.m_bAlphaToCoverage = !c_bDepthOnlyPass && (sCmdLineOptions.m_nNumSamples > 1); pClientPipeline->m_sRenderState.m_bMultisampling = !c_bDepthOnlyPass && (sCmdLineOptions.m_nNumSamples > 1); pClientPipeline->m_sRenderState.m_nNumMsaaSamples = c_bDepthOnlyPass ? 1 : sCmdLineOptions.m_nNumSamples; pClientPipeline->m_sRenderState.m_bPolygonOffset = c_bDepthOnlyPass; // figure out the shader path base on the render type const st_char* c_pRenderType = "/forward/"; if (c_bDeferredPass) c_pRenderType = "/deferred/"; else if (c_bDepthOnlyPass) c_pRenderType = "/depth/"; pClientPipeline->m_strShaderPath = cConfigFile.m_sWorld.m_strShaderPath + c_pRenderType + CPipeline::GetCompiledShaderFolder( ); CFixedString& strVertexShader = pClientPipeline->m_strVertexShaderFilename; CFixedString& strPixelShader = pClientPipeline->m_strPixelShaderFilename; // when shader names are assigned, the "_vs" and "_ps" prefixes are left off since they'll be added // by the shader loading system if (c_bDepthOnlyPass) { if (pDrawCallDetails->m_bBillboard) strVertexShader = strPixelShader = "billboards_depth"; else if (pDrawCallDetails->m_bGrassModel) strVertexShader = strPixelShader = "grass_depth"; else strVertexShader = strPixelShader = "3d_trees_depth"; } else { if (pDrawCallDetails->m_bBillboard) strVertexShader = strPixelShader = "billboards"; else if (pDrawCallDetails->m_bGrassModel) strVertexShader = strPixelShader = c_bFogOnMesh ? "grass_fogged" : "grass"; else strVertexShader = strPixelShader = c_bFogOnMesh ? "3d_trees_fogged" : "3d_trees"; } if (c_bDepthOnlyPass) pClientPipeline->m_sRenderState.m_aeRenderTargetTypes[0] = RENDER_TARGET_TYPE_DEPTH; else { pClientPipeline->m_sRenderState.m_aeRenderTargetTypes[0] = RENDER_TARGET_TYPE_COLOR; if (sCmdLineOptions.m_bDeferred) pClientPipeline->m_sRenderState.m_aeRenderTargetTypes[1] = RENDER_TARGET_TYPE_COLOR; pClientPipeline->m_sRenderState.m_nNumRenderTargets = sCmdLineOptions.m_bDeferred ? 2 : 1; } }
The callback is given an SDrawCallDetails
object to base its decisions on, which is in Listing 2.
Listing 2. SDrawCallDetails Declaration
struct SDrawCallDetails { st_int32 m_nPassIndex; const st_char* m_pPassName; const st_char* m_pStsdkFilename; SMaterial m_sMaterial; st_int32 m_nLodIndex; CLodInfo m_cLod; st_bool m_bBillboard; st_bool m_bGrassModel; st_bool m_bFogOn3dMesh; st_int32 m_nDrawCallIndex; SDrawCall m_sDrawCall; const CCore* m_pTree; };
The output of the callback function is an SClientPipeline
object (Listing 3) which simply contains shader filenames, a shader path, and a render state object.
Listing 3. SClientPipeline Declaration
struct SClientPipeline { CFixedString m_strVertexShaderFilename; CFixedString m_strPixelShaderFilename; CFixedString m_strShaderPath; SRenderState m_sRenderState; };