|
Target Version: DirectX 9.0 Effects Part 1:
|
Rendering techniques are decoupled from the engine, meaning less hard coded rendering code and greater flexibility, as new visual effects can be added without modifying application code. | |
An effect file can contain several implementations of a given technique to allow for various hardware, and as we will see, we can readily validate techniques against hardware to find the correct implementation to use. | |
By separating the programming of visual effects from the application code, effect files can potentially become part of the art pipeline, accompanying incoming geometry and associated media with the specifics of how they are to be rendered. |
Effect files are made up of three basic components:
Parameter declarations:
These are named values that may be set by the application prior to rendering,
to be used by the functions and techniques defined in the effect file.
These are how the application will communicate values necessary for rendering,
such as transformation matrices and lighting parameters.
Techniques:
A technique is a definition of how to render an object. It includes all
rendering and texture state settings that need to be set prior to rendering,
as well as vertex and pixel shader declarations. A technique may consist
of one or more rendering passes to support multi-pass rendering, each pass
containing its own state and shader definitions.
Functions:
Functions are written in High Level Shader Language (HLSL), a C-style language
which can be used as an alternative to writing shaders in assembly language.
Functions can be compiled into vertex or pixel shaders, may be called from
other functions, or used in other ways we'll see later on (for example to
calculate texel values for automatic generation of procedural textures!)
Let's take a quick look at an example of an effect file. We'll keep it moderately simple, no shaders here, but it will provide examples of parameters and techniques as described above.
The first part of your file has several parameter declarations, values that our application will set prior to rendering
File: blend.fx |
texture t0;
// first texture to blend texture t1; // second texture to blend float 4x4 world; // world matrix float 4x4 camera; // view matrix |
Next we will define a technique that will set the world and view transformation states, and blend the two textures specified by the application in a single pass:
technique blend_singlepass { pass p0 { Texture[0]=<t0>; ColorOp[0]=SelectArg1; ColorArg1[0]=Texture; Texture[1]=<t1>; ColorOp[1]=Add; ColorArg1[1]=Texture; ColorArg2[1]=Current; ColorOp[2]=Disable; WorldTransform=<world>; ViewTransform=<camera>; } } |
As you will note, all of the states and enumerated types used here are shortened versions of the constants you are used to seeing in D3D. For example,
ColorOp[0]=SelectArg1;
is equivalent to this operation:
pDevice->SetTextureStageState(0,D3DTSS_COLOROP,D3DTOP_SELECTARG1);
You can find a listing of the available states as referenced in the effect file format here or in the DX9 docs under "Effect States".
To continue our effect file, let's say we want to be able to support older hardware that would require multi-pass alpha blending to blend two textures. To allow for this, we'll add a second technique to the file, right after the first:
technique blend_multipass { pass p0 { AlphaBlendEnable=False; Texture[0]=<t0>; ColorOp[0]=SelectArg1; ColorArg1[0]=Texture; ColorOp[1]=Disable; WorldTransform=<world>; ViewTransform=<camera>; } pass p1 { AlphaBlendEnable=True; SrcBlend=One; DestBlend=One; Texture[0]=<t1>; ColorOp[0]=SelectArg1; ColorArg1[0]=Texture; ColorOp[1]=Disable; WorldTransform=<world>; ViewTransform=<camera>; } } |
In this second technique, we have two pass blocks, while the previous only had one. This is because the object will need to be rendered twice, using different settings each time. We will see shortly how this is handled in the section "Rendering with a Technique".
Loading the effect file can be performed using the D3DXCreateEffectFromFile() function. In its simplest form, we can load an effect file like this:
LPD3DEFFECT pEffect;
if
(FAILED(hr=D3DXCreateEffectFromFile(pDevice,"blend.fx",NULL,NULL,0,NULL,&pEffect,NULL)))
return hr;
On success, pEffect points to an ID3DXEffect instance, from which we can access the parameters and techniques of the effect file.
Once we have loaded our effect file, we can access any of its techniques by name if we wish. However, we need to know what techniques the hardware our program is running on can support. There are a couple of methods provided by the effect interface that can help us here: ValidateTechnique and FindNextValidTechnique. We could use ValidateTechnique to explicitly test the available techniques, for example:
D3DXHANDLE
hTech=pEffect->GetTechniqueByName("blend_singlepass");
if (FAILED(ValidateTechnique(hTech)))
hTech=pEffect->GetTechniqueByName("blend_multipass");
However, this is a bit cumbersome as our application has to know the names of the techniques in the loaded file and check them explicitly. A more flexible method would be to use FindNextValidTechnique(), as it does not require such explicit code and will allow for any number of techniques in the effect file:
D3DXHANDLE hTech;
if (FAILED(hr=pEffect->FindNextValidTechnique(NULL,&hTech)))
return hr;
This method will find the first valid technique in the file. If you arrange your techniques with your preferred technique first followed by fallback techniques, this will return the most desirable technique that will run on the current hardware.
Prior to rendering geometry with a given technique, you must first call the effect's SetTechnique() method with the technique handle. Next, call the Begin() method to prepare for rendering with this technique and determine the number of passes that are required for rendering:
if (SUCCEEDED(pEffect->SetTechnique(hTech)))
{
UINT passes;
if (SUCCEEDED(pEffect->Begin(&passes,0))) {
At this point, passes will contain the number of rendering passes required by the technique, for example 1 if blend_singlepass were used or 2 if blend_multipass were selected.
Effect parameters are set using Set* methods of the effect class corresponding to different parameter types. Each of these functions takes a D3DXHANDLE and a new value, however a string containing the name of the parameter can also be passed in place of the handle. For example:
pEffect->SetTexture("t0",pTexture1);
pEffect->SetTexture("t1",pTexture2);
pEffect->SetMatrix("world",&worldMatrix);
pEffect->SetMatrix("camera",&viewMatrix);
This can be handy for setting effects that won't be changed regularly. However, for parameters that are changed regularly it is preferable to get the parameter's handle when you first load the effect, then use the handle for setting the parameter:
// get handles once after loading effect
D3DXHANDLE hWorld,hCamera;
hWorld=pEffect->GetParameterByName(NULL,"world");
hCamera=pEffect->GetParameterByName(NULL,"camera");
...
// re-use handles each time parameters set
pEffect->SetMatrix(hWorld,&worldMatrix);
pEffect->SetMatrix(hCamera,&viewMatrix);
To render geometry once Begin() has successfully been called, you will need to loop through the number of passes returned by Begin(), and for each pass call the Pass() function and render your geometry. When completed, call the effect's End() method:
for (int
i=0;i<passes;i++) {
pEffect->Pass(i);
// render geometry here
// e.g.
DrawIndexedPrimitive(....
// or
pMesh->DrawSubset(...
}
pEffect->End();
}
Stay tuned for more installments in this series of articles on Effects. In the meantime, to get a real taste of effect files, run the EffectEdit sample in the DirectX 9 SDK. There are several sample effect files provided that show a lot more features than we've covered here, and you can edit the loaded effect file while this sample is running and see the results of your changes immediately!
Visitors Since 1/1/2000:
|