I've attached my code for C# and Sharpdx, there is some code missing in here, but the basic function is now working. still some bugs, but if you want to post a 256 char message to the screen then it does it. This function uses Codehead's font generator file format.
Code was hacked together in a day. I need to go through, remove redundant code, formatting, refactoring etc.
Whole bunch of functionality I have not implemented as you can see. Will break this into 2 classes and move some functions around which will naturally shrink the code base.
No warranties offered :)
using System;
using SharpDX;
using D3D11 = SharpDX.Direct3D11;
using DXGI = SharpDX.DXGI;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
namespace Insane3d
{
public struct WriterTextureObject
{
public D3D11.ShaderResourceView m_d3dTexture;
public int m_textureWidth;
public int m_textureHeight;
public byte m_bpp;
public byte m_charOffset;
public byte[] m_charWidths;
public int m_cellWidth;
public int m_cellHeight;
public int m_columnCount;
}
public class WriterString
{
private bool m_updated;
private int m_fontIndex;
public string m_message;
public float m_x; // in -1.0 to 1.0 co-ords
public float m_y; // in -1.0 to 1.0 co-ords
public bool m_uniformSpacing; // use this if you want the text render to apply cell width rather than char width.
public string m_uid;
public int m_startingIndex;
public int m_runLength;
public bool m_enabled;
public bool Updated
{
get { return m_updated; }
set { m_updated = value; }
}
public int FontIndex
{
get { return m_fontIndex; }
set { m_fontIndex = value; }
}
}
public class DirectXWriter : IDisposable
{
List<WriterTextureObject> m_objects;
List<WriterString> m_strings;
D3DBufferContainer m_staticStrings; // we wont use buffer containers/ we will use our own triangle buffer.
float m_screenWidth;
float m_screenHeight;
static int s_maxEntities = 64;
static int s_maxCharacters = 128;
static int s_verticesPerChar = 6;
D3D11.Device m_device;
D3D11.DeviceContext m_context;
D3D11.Effect m_effect;
bool m_update;
D3D11.Buffer m_D3DtriangleVertexBuffer;
List<VertexPointMaster> m_staticVertexList;
D3D11.VertexBufferBinding[] m_binding;
/*----------------------------------------------------------------------
*
*
*
*
*----------------------------------------------------------------------*/
/// <summary>
/// Constructor
/// </summary>
/// <param name="a_parentDevice"></param>
/// <param name="a_context"></param>
/// <param name="a_effect"></param>
public DirectXWriter(D3D11.Device a_parentDevice, D3D11.DeviceContext a_context, D3D11.Effect a_effect)
{
m_device = a_parentDevice;
m_context = a_context;
m_effect = a_effect;
m_update = false;
m_objects = new List<WriterTextureObject>();
m_staticVertexList = new List<VertexPointMaster>();
m_screenWidth = 1920;
m_screenHeight = 1080;
m_staticStrings = new D3DBufferContainer();
m_strings = new List<WriterString>();
m_binding = new D3D11.VertexBufferBinding[2];
DirectXHelper.CreateInitialD3DBuffer(m_device, m_context, ref m_D3DtriangleVertexBuffer, s_maxCharacters * s_verticesPerChar * s_maxEntities, ref m_binding);
}
/// <summary>
/// Main Rendering Function
/// </summary>
public void Render()
{
if (m_update == true)
{
CreateD3DBufferFromData();
m_update = false;
}
RenderBuffers();
}
/*----------------------------------------------------------------------
*
*
*
*
*----------------------------------------------------------------------*/
/// <summary>
/// Takes in a list of vertices and creates a direct3d buffer, if the buffer exists and is too small, it will resize it.
/// </summary>
void CreateD3DBufferFromData()
{
bool recreateBinding = false;
int vertCount = 0;
int startCount = 0;
int offsetCount = 0;
Debug.Assert(s_maxCharacters * s_verticesPerChar * s_maxEntities > 0);
if (m_D3DtriangleVertexBuffer != null)
{
if (m_D3DtriangleVertexBuffer.Description.SizeInBytes < s_maxCharacters * s_verticesPerChar * s_maxEntities * GlobalStatics.s_vertexSize)
{
m_D3DtriangleVertexBuffer.Dispose();
m_D3DtriangleVertexBuffer = null;
}
else
{
DataStream astream;
var dataBox = m_context.MapSubresource(m_D3DtriangleVertexBuffer, D3D11.MapMode.WriteDiscard, D3D11.MapFlags.None, out astream);
for (int k = 0; k < m_strings.Count; k++)
{
offsetCount = k * s_maxCharacters * s_verticesPerChar;
vertCount = startCount + m_strings[k].m_runLength;
if (m_strings[k].m_enabled && m_strings[k].Updated)
{
astream.Position = offsetCount * GlobalStatics.s_vertexSize;
for (int i = startCount; i < vertCount; i++)
{
astream.Write(m_staticVertexList[i].m_vertex);
astream.Write(m_staticVertexList[i].getNormal());
astream.Write(m_staticVertexList[i].m_texture);
astream.Write(m_staticVertexList[i].getBlendVector());
}
m_strings[k].Updated = false;
}
startCount = startCount + m_strings[k].m_runLength;
}
m_context.UnmapSubresource(m_D3DtriangleVertexBuffer, 0); //to update the data on GPU
astream.Dispose();
}
}
if (m_D3DtriangleVertexBuffer == null)
{
// now write out the buffer correctly
var stream = new DataStream(s_maxCharacters * s_verticesPerChar * s_maxEntities * GlobalStatics.s_vertexSize, true, true);
for (int k = 0; k < m_strings.Count; k++)
{
offsetCount = k * s_maxCharacters * s_verticesPerChar;
vertCount = startCount + m_strings[k].m_runLength;
if (m_strings[k].m_enabled)
{
stream.Position = offsetCount * GlobalStatics.s_vertexSize;
for (int i = startCount; i < vertCount; i++)
{
stream.Write(m_staticVertexList[i].m_vertex);
stream.Write(m_staticVertexList[i].getNormal());
stream.Write(m_staticVertexList[i].m_texture);
stream.Write(m_staticVertexList[i].getBlendVector());
}
}
startCount = startCount + m_strings[k].m_runLength;
}
stream.Position = 0;
m_D3DtriangleVertexBuffer = new D3D11.Buffer(m_device, stream, new D3D11.BufferDescription
{
BindFlags = D3D11.BindFlags.VertexBuffer,
CpuAccessFlags = D3D11.CpuAccessFlags.Write,
OptionFlags = D3D11.ResourceOptionFlags.None,
SizeInBytes = (int)stream.Length,
Usage = D3D11.ResourceUsage.Dynamic
});
recreateBinding = true;
stream.Dispose();
}
if (recreateBinding)
{
if (m_binding[0].Buffer != null)
{
m_binding[0].Buffer.Dispose();
}
m_binding[0] = new D3D11.VertexBufferBinding(m_D3DtriangleVertexBuffer, GlobalStatics.s_vertexSize, 0);
}
}
/*----------------------------------------------------------------------
*
*
*
*
*----------------------------------------------------------------------*/
/// <summary>
/// Function that creates the geometry for the string in screen space. Returns a UID than can be used to update the string contents and position.
/// </summary>
/// <param name="a_message">String to be presented on screen</param>
/// <param name="a_uid">Pass in blank if new string entry, or pass in previous UID to overwrite current item</param>
/// <param name="a_fontIndex">Index of font set you want to use</param>
/// <param name="a_x">Position in screen space of a 1920 x 1080 window</param>
/// <param name="a_y">Position in screen space of a 1920 x 1080 window</param>
/// <returns></returns>
public string updateString(string a_message, string a_uid, int a_fontIndex, int a_x, int a_y) // pass in a blank UID will make it new
{
m_update = true;
WriterString newstring = new WriterString();
int foundIndex = -1;
newstring.m_uid = a_uid;
if (a_uid != "")
{
for (int i = 0; i < m_strings.Count; i++)
{
if (m_strings[i].m_uid == a_uid)
{
foundIndex = i;
newstring = m_strings[i];
i = m_strings.Count;
}
}
}
else
{
newstring = new WriterString();
foundIndex = m_strings.Count;
m_strings.Add(newstring);
}
string uid = createStringGeometry(newstring, a_x, a_y, 5);
newstring.m_message = a_message;
newstring.m_runLength = newstring.m_message.Length * s_verticesPerChar;
newstring.m_startingIndex = foundIndex * s_maxCharacters * s_verticesPerChar;
newstring.m_enabled = true;
newstring.Updated = true;
newstring.FontIndex = a_fontIndex;
m_strings[foundIndex] = newstring;
return uid;
}
// we need to do some work to make the max length of a string 256 for instance.
// this means that in the vertex buffer, we can then allocate static lengths.
// the run length and starting index then can be aligned to 256 for example
// that means that when a string is updated, we only have to update part of the buffer, not rebuild it completely.
/// <summary>
/// Private function that creates all the vertex data for the triangles. Triangles are using Screen space co-ordinates.
/// </summary>
/// <param name="newstring"></param>
/// <param name="a_x"></param>
/// <param name="a_y"></param>
/// <param name="a_scale"></param>
/// <returns></returns>
string createStringGeometry(WriterString newstring, int a_x, int a_y, float a_scale)
{
newstring.m_uid = Helper.GetUniqueKey(8);
int x = a_x;
if (newstring.m_message.Length > s_maxCharacters)
{
newstring.m_message = newstring.m_message.Substring(0, s_maxCharacters);
}
for (int i = 0; i < newstring.m_message.Length ; i++)
{
x = createCharQuad(newstring.m_message[i], x, a_y, ref m_staticVertexList, 0, a_scale);
}
return newstring.m_uid;
}
/*----------------------------------------------------------------------
*
*
*
*
*----------------------------------------------------------------------*/
int createCharQuad(char a_char, int a_x, int a_y, ref List<VertexPointMaster> a_list, int a_fontIndex, float a_scale)
{
int index = (byte)a_char;
WriterTextureObject text = m_objects[a_fontIndex];
float scaledWidth;
if (index == 0)
{
scaledWidth = (text.m_charWidths[index] / 2 * a_scale);
}
else
{
scaledWidth = (text.m_charWidths[index] * a_scale);
}
index = index - text.m_charOffset;
Debug.Assert(index >= 0 && index < 256);
float topX, topy;
float botX, boty;
int textIndex = index % text.m_columnCount;
// u coords
topX = textIndex * text.m_cellWidth;
botX = topX + text.m_charWidths[index];
topX = topX / text.m_textureWidth;
botX = botX / text.m_textureWidth;
int result = a_x + (int) scaledWidth + 15; // 15 is fudge factor for spacing, will do something more orietated to char set later.
// v coords
textIndex = index / text.m_columnCount;
topy = textIndex * text.m_cellHeight;
boty = topy + text.m_cellHeight;
topy = topy / text.m_textureHeight;
boty = boty / text.m_textureHeight;
float topxCoord, topyCoord; //top left corner
float botxCoord, botyCoord; // bottom right corner
topxCoord = convertPixelXCoordToScreen(a_x);
topyCoord = convertPixelYCoordToScreen(a_y);
botxCoord = convertPixelXCoordToScreen(a_x + text.m_charWidths[index] * a_scale) ;
botyCoord = convertPixelYCoordToScreen(a_y + text.m_cellHeight * a_scale) ;
VertexPointMaster vp1;
// tri 1 - Upper left.
vp1 = new VertexPointMaster();
vp1.setVertex(new Vector3(topxCoord, topyCoord, 0));
vp1.setTexture(new Vector2(topX, topy));
vp1.setTexturePrimary(a_fontIndex);
vp1.setDepthBlend(1); // we might want to make this semi transparent later or play with colours etc.
a_list.Add(vp1);
// tri 1 - Upper right.
vp1 = new VertexPointMaster();
vp1.setVertex(new Vector3(botxCoord, topyCoord,0));
vp1.setTexture(new Vector2(botX, topy));
vp1.setTexturePrimary(a_fontIndex);
vp1.setDepthBlend(1);
a_list.Add(vp1);
// tri 1 - Bottom left.
vp1 = new VertexPointMaster();
vp1.setVertex(new Vector3(topxCoord, botyCoord,0));
vp1.setTexture(new Vector2(topX, boty));
vp1.setTexturePrimary(a_fontIndex);
vp1.setDepthBlend(1);
a_list.Add(vp1);
// tri 2 - Bottom left.
vp1 = new VertexPointMaster();
vp1.setVertex(new Vector3(topxCoord, botyCoord, 0));
vp1.setTexture(new Vector2(topX, boty));
vp1.setTexturePrimary(a_fontIndex);
vp1.setDepthBlend(1); // we might want to make this semi transparent later or play with colours etc.
a_list.Add(vp1);
// tri 2 - Upper right
vp1 = new VertexPointMaster();
vp1.setVertex(new Vector3(botxCoord, topyCoord, 0));
vp1.setTexture(new Vector2(botX, topy));
vp1.setTexturePrimary(a_fontIndex);
vp1.setDepthBlend(1); // we might want to make this semi transparent later or play with colours etc.
a_list.Add(vp1);
// tri 2 - Bottom right.
vp1 = new VertexPointMaster();
vp1.setVertex(new Vector3(botxCoord, botyCoord, 0));
vp1.setTexture(new Vector2(botX, boty));
vp1.setTexturePrimary(a_fontIndex);
vp1.setDepthBlend(1); // we might want to make this semi transparent later or play with colours etc.
a_list.Add(vp1);
return result; // returns the width of the char added in pixels
}
/*----------------------------------------------------------------------
*
*
*
*
*----------------------------------------------------------------------*/
void RenderBuffers()
{
GlobalStatics.DXManager.setZBufferState(false);
D3D11.BlendState blend = GlobalStatics.DXManager.getBlendState(0);
m_context.OutputMerger.SetBlendState(blend);
DirectXHelper.LoadVertexBuffertoGPU(m_context, m_binding[0], GlobalStatics.DXManager.s_bufferLayout, SharpDX.Direct3D.PrimitiveTopology.TriangleList);
for (int i = 0; i < m_strings.Count; i++)
{
if (m_strings[i].m_enabled)
{
//1. set the texture, then get the immediate context updated. (ALA set the variables prior to getting the state of the byte code such as texture type and matrices
//2. get the bytecode and apply to the immediate context.
//3. pass vertex data directive to immediate context for render
GlobalStatics.s_primaryTexture.SetResource(m_objects[0].m_d3dTexture);
m_effect.GetTechniqueByName("TextWriter").GetPassByIndex(0).Apply(m_context);
m_context.Draw(m_strings[i].m_runLength, m_strings[i].m_startingIndex);
}
}
m_effect.GetTechniqueByIndex(0).GetPassByIndex(0).Apply(m_context);
GlobalStatics.DXManager.setZBufferState(true);
}
/*----------------------------------------------------------------------
*
*
*
*
*----------------------------------------------------------------------*/
public bool LoadFont(string a_filename, bool a_mipmapped)
{
WriterTextureObject obj = new WriterTextureObject();
D3D11.Texture2D tex;
BinaryReader reader = new BinaryReader(File.Open(a_filename, FileMode.Open));
// Check header - should read 0xBF 0xF2 (BFF2)
byte h0 = reader.ReadByte();
byte h1 = reader.ReadByte();
if (h0 != 0xBF || h1 != 0xF2)
{
reader.Close();
return false;
}
// Get image dimensions
obj.m_textureWidth = reader.ReadInt32();
obj.m_textureHeight = reader.ReadInt32();
// Get cell dimensions
obj.m_cellWidth = reader.ReadInt32();
obj.m_cellHeight = reader.ReadInt32();
// Sanity check (prevent divide by zero)
if (obj.m_cellWidth <= 0 || obj.m_cellHeight <= 0)
{
throw new IOException("Invalid header content");
}
// Pre-calculate column count
obj.m_columnCount = obj.m_textureWidth / obj.m_cellWidth;
// Get colour depth
obj.m_bpp = reader.ReadByte();
// Get base offset
obj.m_charOffset = reader.ReadByte();
// Read width information
obj.m_charWidths = new byte[256];
for (int wLoop = 0; wLoop < 256; ++wLoop)
{
obj.m_charWidths[wLoop] = reader.ReadByte();
}
// Get bitmap
int bitLen = (obj.m_textureHeight * obj.m_textureWidth) * (obj.m_bpp / 8);
byte[] bits = new byte[bitLen];
bits = reader.ReadBytes(bitLen);
reader.Close();
D3D11.Texture2DDescription desc = new D3D11.Texture2DDescription()
{
ArraySize = 1,
BindFlags = D3D11.BindFlags.ShaderResource,
CpuAccessFlags = D3D11.CpuAccessFlags.Write,
Format = DXGI.Format.B8G8R8A8_UNorm,
Height = obj.m_textureHeight,
MipLevels = 1,
SampleDescription = new DXGI.SampleDescription(1, 0),
Usage = D3D11.ResourceUsage.Dynamic,
Width = obj.m_textureWidth
};
if (a_mipmapped)
{
desc.Usage = D3D11.ResourceUsage.Default;
desc.CpuAccessFlags = D3D11.CpuAccessFlags.None; // its none because we load the data into the texture as a full resource update, not update the contents after with map resource.
desc.Format = DXGI.Format.R8G8B8A8_UNorm_SRgb;
desc.MipLevels = 0;
desc.OptionFlags = D3D11.ResourceOptionFlags.GenerateMipMaps;
desc.SampleDescription.Count = 1;
desc.SampleDescription.Quality = 0;
desc.BindFlags = D3D11.BindFlags.RenderTarget | D3D11.BindFlags.ShaderResource;
}
if (desc.MipLevels != 0)
{
// if we arent doing mip maps, then load the resource directly
DataStream buffer = new DataStream(obj.m_textureHeight * obj.m_textureWidth * 4, true, true);
load24BitBitmapInto32BitTexture(bits, buffer, obj);
DataRectangle rect = new DataRectangle(buffer.DataPointer, obj.m_textureWidth * 4);
tex = new D3D11.Texture2D(m_device, desc, rect); /// this creates a texture and populates it,
obj.m_d3dTexture = new D3D11.ShaderResourceView(m_device, tex);
}
else
{
tex = new D3D11.Texture2D(m_device, desc); /// we need to create a blank one then populate it with the mip mapped texture.
int stride = obj.m_textureWidth * 4;
var buffer = new DataStream(obj.m_textureHeight * stride, true, true);
load24BitBitmapInto32BitTexture(bits, buffer, obj);
DataBox box = new DataBox(buffer.DataPointer, stride, 1);
m_device.ImmediateContext.UpdateSubresource(box, tex, D3D11.Resource.CalculateSubResourceIndex(0, 0, 4));
buffer.Dispose();
D3D11.ShaderResourceViewDescription srvDesc = new D3D11.ShaderResourceViewDescription();
srvDesc.Format = desc.Format;
srvDesc.Dimension = SharpDX.Direct3D.ShaderResourceViewDimension.Texture2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = -1;
obj.m_d3dTexture = new D3D11.ShaderResourceView(m_device, tex, srvDesc);
}
if (a_mipmapped)
{
m_context.GenerateMips(obj.m_d3dTexture);
}
m_objects.Add(obj);
return true;
}
void load24BitBitmapInto32BitTexture(byte[] a_bits, DataStream a_buffer, WriterTextureObject a_obj)
{
for (int i = 0; i < a_bits.Length; i += 3)
{
a_buffer.WriteByte(a_bits[i]);
a_buffer.WriteByte(a_bits[i + 1]);
a_buffer.WriteByte(a_bits[i + 2]);
// if i % 3 = 2 then add an alpha channel byte
if (a_bits[i] > 0)
{
a_buffer.WriteByte(a_bits[i]); //need this to convert to full 32 bit
}
else
{
a_buffer.WriteByte(0); //need this to convert to full 32 bit
}
}
}
float convertPixelXCoordToScreen(float a_x)
{
float val = (a_x / m_screenWidth / 2.0f) - 1.0f;
return val;
}
float convertPixelYCoordToScreen(float a_y)
{
float val = 1.0f - (a_y / m_screenHeight / 2.0f);
return val;
}
float convertWidthToScreen(float a_width)
{
float val = a_width / m_screenWidth / 2;
return val;
}
float convertHeightToScreen(float a_width)
{
float val = a_width / m_screenHeight / 2;
return val;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (m_D3DtriangleVertexBuffer != null)
{
m_D3DtriangleVertexBuffer.Dispose();
}
if (m_staticStrings != null)
{
m_staticStrings.Dispose();
}
// TODO: dispose managed state (managed objects).
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~DirectXWriter() {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
}
}