Double sided materials are rendered in DXEngine with rendering the same object twice - first with front material and then with back material (in this case the normal is flipped in the shader and reverse the triangle culling).
This is done by adding two RenderableMeshPrimitive to rendering queues - the second has IsBackFaceMaterial set to true.
In the current version of DXEngine (v2.3) the MeshObjectNode does not support that. This will be available in the next version.
Here is the source code of the new MeshObjectNode with new IsBackFaceMaterial property:
Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Reflection;
using Ab3d.DirectX.Materials;
using SharpDX;
namespace Ab3d.DirectX
{
// NOTE: Class is sealed for better performance - this way the virtual method can be directly called (without need to check virtual methods table first)
/// <summary>
/// MeshObjectNode is an <see cref="ObjectNode"/> that can render an object that is defined by object derived from <see cref="MeshBase"/> class.
/// </summary>
[Obfuscation(Feature = "control flow", Exclude = true, ApplyToMembers = true)]
public sealed class MeshObjectNode : ObjectNode
{
private bool _isMaterialInitialized;
/// <summary>
/// Gets a Mesh that defines the 3D object geometry.
/// </summary>
public MeshBase Mesh { get; private set; }
private Material[] _materials;
/// <summary>
/// Gets or sets an array of Material that can be set to override the materials defined in the Mesh.
/// </summary>
public Material[] Materials
{
get { return _materials; }
set
{
if (_materials == value)
return;
_materials = value;
_isMaterialInitialized = false;
}
}
/// <summary>
/// IsBackFaceMaterial specifies if the material is used to render front (IsBackFaceMaterial == false) or back (IsBackFaceMaterial == true) faces.
/// </summary>
public bool IsBackFaceMaterial;
/// <summary>
/// Initializes a new instance of the <see cref="MeshObjectNode"/> class.
/// </summary>
/// <param name="meshBase">MeshBase object that defines the 3D object geometry</param>
public MeshObjectNode(MeshBase meshBase)
: this(meshBase, (Material[])null )
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MeshObjectNode"/> class.
/// </summary>
/// <param name="meshBase">MeshBase object that defines the 3D object geometry</param>
/// <param name="material">material used by this ObjectNode</param>
public MeshObjectNode(MeshBase meshBase, Material material)
: this(meshBase, new Material[] { material })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MeshObjectNode"/> class.
/// </summary>
/// <param name="mesh">MeshBase object that defines the 3D object geometry</param>
/// <param name="materials">material used by this ObjectNode</param>
/// <exception cref="System.ArgumentNullException">mesh;mesh is null.</exception>
public MeshObjectNode(MeshBase mesh, Material[] materials)
{
if (mesh == null)
throw new ArgumentNullException("mesh", "mesh is null.");
Mesh = mesh;
mesh.AddRef();
Materials = materials;
if (materials != null && materials.Length > 0)
{
for (var i = 0; i < materials.Length; i++)
materials[i].AddRef();
}
this.Name = mesh.Name;
this.Bounds = mesh.Bounds;
LockChildNodes(); // Prevent adding any ChildNodes
}
/// <summary>
/// Updates the bounds of this SceneNode if the dirty flags indicates that the bounds could be changed of if the forceUpdate parameter is set to true
/// </summary>
/// <param name="forceUpdate">if true than bounds are updated regardless of the SceneNode's dirty flags</param>
/// <returns>true if bounds were changed</returns>
public override bool UpdateBounds(bool forceUpdate = false)
{
bool changeNeeded = forceUpdate || (this.DirtyFlags & (SceneNodeDirtyFlags.MeshChanged |
SceneNodeDirtyFlags.MeshPositionsChanged |
SceneNodeDirtyFlags.TransformChanged |
SceneNodeDirtyFlags.WorldMatrixChanged)) != 0;
if (!changeNeeded)
return false;
bool isChanged;
if (Mesh != null && Mesh.Bounds != null && !Mesh.Bounds.IsEmpty)
{
this.Bounds = Mesh.Bounds;
isChanged = true;
}
else
{
isChanged = false;
}
if (isChanged)
{
if (this.Transform != null && !this.Transform.IsIdentity && Bounds != null)
Bounds.Transform(this.Transform);
NotifySceneNodeChange(SceneNodeDirtyFlags.BoundsChanged); // Mark this SceneNode as BoundsChanged
NotifyAllParentSceneNodesChange(SceneNodeDirtyFlags.ChildBoundsChanged); // Notify all parent SceneNodes that this child was changed
}
return isChanged; // we have changed the bounds
}
/// <inheritdoc />
protected override void OnInitializeResources(DXScene dxScene)
{
var dxDevice = parentDXScene.DXDevice;
// Initialize Mesh
if (!Mesh.IsInitialized)
Mesh.InitializeResources(dxDevice);
// Initialize materials
EnsureInitializedMaterial(dxDevice);
base.OnInitializeResources(dxScene);
}
/// <inheritdoc />
public override void CollectRenderableObjects()
{
if (parentDXScene == null)
return;
EnsureInitializedMaterial(parentDXScene.DXDevice);
if (!this.IsActive || Mesh.VertexBufferBindings == null)
return;
Material usedMaterial;
if (this.Materials != null && this.Materials.Length > 0)
usedMaterial = this.Materials[0];
else if (Mesh.Materials != null && Mesh.Materials.Length > 0)
usedMaterial = Mesh.Materials[0];
else
usedMaterial = null;
if (usedMaterial != null)
{
// First create new IRenderableObject (RenderableMeshPrimitive in our case)
RenderableMeshPrimitive newRenderableMeshPrimitive;
if (this.IsWorldMatrixIdentity)
newRenderableMeshPrimitive = new RenderableMeshPrimitive(Mesh, usedMaterial);
else
newRenderableMeshPrimitive = new RenderableMeshPrimitive(Mesh, usedMaterial, ref WorldMatrix);
newRenderableMeshPrimitive.IsBackFaceMaterial = this.IsBackFaceMaterial;
// Now select the RenderingQueue where the new IRenderableObject will be added to
// Check if material HasTransparency - in that case put it into TransparentRenderingQueue, else move it to GeometryRenderingQueue
var transparentMaterial = usedMaterial as ITransparentMaterial;
RenderingQueue renderingQueue;
if (transparentMaterial != null && transparentMaterial.HasTransparency)
renderingQueue = parentDXScene.TransparentRenderingQueue;
else
renderingQueue = parentDXScene.GeometryRenderingQueue;
if (renderingQueue != null)
renderingQueue.Add(newRenderableMeshPrimitive);
}
}
private void EnsureInitializedMaterial(DXDevice dxDevice)
{
if (_isMaterialInitialized)
return;
if (Materials != null)
{
int materialsCount = Materials.Length;
for (int i = 0; i < materialsCount; i++)
{
var oneMaterial = Materials[i];
if (oneMaterial != null && !oneMaterial.IsInitialized)
oneMaterial.InitializeResources(dxDevice);
}
}
_isMaterialInitialized = true;
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
{
Mesh.Dispose();
Mesh = null;
if (Materials != null && Materials.Length > 0)
{
for (var i = 0; i < Materials.Length; i++)
Materials[i].Dispose();
}
Materials = null;
Bounds = Bounds.Empty;
_isMaterialInitialized = false;
}
base.Dispose(disposing);
}
}
}
This source has updated CollectRenderableObjects method with new line:
newRenderableMeshPrimitive.IsBackFaceMaterial = this.IsBackFaceMaterial;
To render two-sided material create two instances of MeshObjectNode and set IsBackFaceMaterial to true on the second.
NOTE:
To use this source with DXEngine v2.3, you will need to change the IsWorldMatrixIdentity to isWorldMatrixIdentity and WorldMatrix to worldMatrix (using CamelCase becase in v2.3 those two properties are protected and not public as in a never version)