Adding a Shader to a ModelVisual3D
#1
Hello,

I am evaluating AB4D to improve my WPF 3D application.

I want to apply a custom vertex/pixel shader to my ModelVisual3D that contain my 3d model (not all scene). Is there a simple way to do it?

Thank you
Leonardo
#2
Ab3d.DXEngine support using custom shaders that are used to render some 3D objects.

When I wanted to answer your question, I have found out that there is no good sample on how to on how to create a custom material that uses custom shader and can have different property values for different models. There are a few sample on how to use custom shaders (for example Customizations/CustomFogShaderEffectSample in the main DXEngine samples project and the ShaderFactory project) but they only use standard material properties or have some global properties (fog settings).

Because good customizability is one of the major features of DXEngine and because support for custom shaders is needed for that, I have decided to write a new sample that will show how to create a custom material that use custom effect with custom shaders.

I have updated the samples in the evaluation and commercial versions. For those how already have that installed, you can download the zip that is attached to this post and add and overwrite the files from zip over the existing sample files.


The proces of using custom shaders is best described in the sample, but you can also follow the following guide:

To use custom shaders, you first need to prepare the custom Effect and custom Material classes.

Custom effect class is created by deriving a new class from Ab3d.DirectX.Effect - effect defines how a material that uses this effect will be rendered. Then you need to override the following methods: OnInitializeResources (create shaders and other resources), OnApplyPerFrameSettings, ApplyMaterial (setup per-frame and per-object resources), Dispose.

To store the custom material properties that are different for each 3D model, you also need to create a custom Material class - a new class that is derived from Ab3d.DirectX.Material - this class can also implement some standard material interfaces (see what is available in the Ab3d.DirectX.Materials namespace). You can also define your own interface and define your custom properties there. This material object is then passed to the ApplyMaterial method in your Effect class - there you can read your custom material properties and set the shader constant buffers accordingly.

To tell DXEngine to use your custom effect to render your custom material you need to set the material's Effect property to an instance of your effect.

The best way to do this is in material's overridden OnInitializeResources method. Here you set the Effect property to an instance of your custom effect. Here you can also register your custom effect. The following code does this:
Code:
/// <summary>
/// Initializes resources.
/// </summary>
/// <param name="dxDevice">Parent DXDevice used to initialize resources</param>
protected override void OnInitializeResources(DXDevice dxDevice)
{
    // Set this.Effect to an instance of MyEffect.
    // This will tell DXEngine to use MyEffect to render this material.
    // An instance of MyEffect is get by using GetEffect method on EffectsManager.
    // This will also register MyEffect on EffectsManager if it was not registered before (using Activator to create an instance of MyEffect)
    this.Effect = dxDevice.EffectsManager.GetEffect<MyEffect>();
}

As you see from code comments, the GetEffect method is used to get an instance of MyEffect. This also registers and creates an instance of MyEffect on EffectsManager. If you need to have more control over the creation of MyEffect instance, you can use the following code (calling GetEffect with createNewEffectInstanceIfNotFound parameter set to false):

Code:
/// <summary>
/// Initializes resources.
/// </summary>
/// <param name="dxDevice">Parent DXDevice used to initialize resources</param>
protected override void OnInitializeResources(DXDevice dxDevice)
{
    // Set this.Effect to an instance of MyEffect.
    // This will tell DXEngine to use MyEffect to render this material.
    var myEffect = dxDevice.EffectsManager.GetEffect<MyEffect>(createNewEffectInstanceIfNotFound: false);

    if (myEffect == null)
    {
        myEffect = new MyEffect(myEffectSettings);
        dxDevice.EffectsManager.RegisterEffect(myEffect);
    }

    this.Effect = myEffect;
}

Another option is to register your effect in the DXEngine initialization process:

Code:
MainDXViewportView.DXSceneDeviceCreated += delegate(object sender, EventArgs args)
{
    var dxScene = MainDXViewportView.DXScene;

    if (dxScene == null) // Probably using WPF 3D rendering
        return;

    // Create a new instance of MyEffect
    var myEffect = new MyEffect();

    // Register effect with EffectsManager - this will also initialize the effect with calling OnInitializeResources method in the MyEffect class
    dxScene.DXDevice.EffectsManager.RegisterEffect(myEffect);
};

Here the DXSceneDeviceCreated event is used to execute the code after the DXDevice is created.

You also need to override the Dispose method where you dispose the Effect:

Code:
protected override void Dispose(bool disposing)
{
    if (Effect != null)
    {
        Effect.Dispose(); // Decrease reference counting in the Effect object
        Effect = null;
    }

    base.Dispose(disposing);
}


Now you can create your 3D objects and assign your custom material to the objects. To do this, you need to assign an instance of your custom material to a WPF material that is used by 3D object - for example:

Code:
var myCustomDXMaterial = new MyCustomDXMaterial()
{
    MyColor = Colors.Green,
};

// Create standard WPF material and set the myCustomDXMaterial to be used when the model is rendered in DXEngine.
var myCustomDiffuseMaterial = new DiffuseMaterial();
myCustomDiffuseMaterial.SetUsedDXMaterial(myCustomDXMaterial);

myGeometryModel3D.Material = myCustomDiffuseMaterial;

(I am planning to simplify this in the future so you will not need to create a WPF material and will be able to assign your material directly to the GeometryModel3D).


An even better picture of how shaders are used in DXEngine can be get with purchasing "Effects source code" for $299 - this will give you fill source code of all hlsl files and Effect files used in DXEngine.


Attached Files
.zip   MeshNormalsMaterialSample.zip (Size: 25.46 KB / Downloads: 3)
Andrej Benedik
#3
Thank you very much Andrej. I am studying the new sample.

Leonardo
#4
I have understood your example, but I am thinking to simpler API. Any change to have a Material like this?

<ShaderMaterial PixelShader="mypixelshader.hlsl" VertexShader="myvertexshader.hlsl" />

With automatic shader compilation/caching and a way to pass parameters of Shaders with Data Binding from XAML?

Thank you
Leonardo
#5
I agree that it would be great to have such simple way to define new materials.

But because shaders are usually not defined very often, I think that it is worth investing a little bit more time to provide a fixed Effect and fixed Material implementation (instead of having a dynamic shader and material). 

After that your application will be able to benefit from the the following advantages:

  1. Much better performance. Setting material properties on a fixed constant buffer (a struct that defines the data for DirectX shaders) is much faster then setting properties on a dynamically compiled shader where you need to use lookup tables to find each property. Also using standard properties is much much faster then using DependecyProperties. 

  2. Faster initialization time - with using dynamic shaders, your application will need to first load the SharpDX.D3DCompiler assembly and a native compiler dll and then compile the hlsl code of the shaders. This will need to be done at each application startup. When using precompiled shaders the shader load times are much shorter.

  3. With using precompiled shaders and a fixed Effect class, you will not need to reference SharpDX.D3DCompiler and worry about the native d3dcompiler_47.dll library that will need to be deployed with your application (see comments in the ShaderFactors sample project in the ShaderBuilderWindow.xaml.cs file).

  4. Because almost all the classes that are used in WPF 3D are not extendable (the only exception is ModelVisual3D class), this means that it is not possible to define your own material classes and have a nice looking XAML. To provide custom materials and material properties you need to use some WPF tricks that are only possible in code behind (for example using SetValue method to add custom properties to WPF objects).
Of course the performance considerations depend on the user case. If you are showing only 100 models, than this is not a problem. But if you are showing thousands of 
objects, then the code that sets up material is performance critical - ideally it is called 60 times per second for each object and for each material property. 


But because one of the main purpose of Ab3d.DXEngine is to provide easy to use access to DirectX 11, I am planning to write a sample that will show how to use DirectX 11 Effects framework - this provides a way to have a more dynamic-like access to shader properties. I currently do not have time to do that but have that quite high on my DXEngine todo list.
You may also check some other opinion on advantages and disadvantages of using a more dynamic approach (with Effects library) vs. static approach (standard constant buffers) on the following discussion:
https://gamedev.stackexchange.com/questi...-framework
Andrej Benedik
  


Forum Jump:


Users browsing this thread:
1 Guest(s)