Unigine整合Myra UI Library全紀錄(2):渲染
TextureQuadBatcher
由于Unigine沒有SpriteBatch類似物,需要手動實現一個。當然用Unigine.Ffp直接來搞也可以,只不過效率就會差一些了。
因為我打算同時用Myra和ImGui.NET,因此這里偷了個懶,去借用Unigine示例里整合ImGui.NET用的Shader/Material了。不打算用ImGui的可以去把unigine-imgui-csharp-integration-sample\data\imgui.basemat拷貝到自己項目的data目錄下。
接下來按照它這個Shader使用頂點的方式,定義頂點格式(其實就是ImGui的頂點格式):
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct VertexLayout(VertexPositionColorTexture vertexData)
{
public vec2 Position = new(vertexData.Position.X, vertexData.Position.Y);
public vec2 TexCoord = new(vertexData.TextureCoordinate.X, vertexData.TextureCoordinate.Y);
public uint Color = vertexData.Color.PackedValue;
}
VertexPositionColorTexture是Myra傳遞過來的頂點數據格式。
接下來聲明幾個會用到的常量和變量:
const int MaxSprites = 2048;
const int MaxVertices = MaxSprites * 4;
const int MaxIndices = MaxSprites * 6;
readonly MeshDynamic quadMesh;
readonly Material quadMaterial;
Texture? lastTexture;
readonly VertexLayout[] vertexData = new VertexLayout[MaxVertices];
int vertexCount;
指定一次最多繪制2048個圖元,這個數量已經很多了,再多會導致Mesh的Index尺寸超過65536,效率就會有所降低(Unigine的Index是4字節int)。
MeshDynamic是Unigine的動態Mesh對象,創建并指定頂點格式的過程也很簡單:
quadMesh = new MeshDynamic(MeshDynamic.USAGE_DYNAMIC_VERTEX);
var vertexFormat = new MeshDynamic.Attribute[3];
vertexFormat[0].type = MeshDynamic.TYPE_FLOAT;
vertexFormat[0].offset = 0;
vertexFormat[0].size = 2;
vertexFormat[1].type = MeshDynamic.TYPE_FLOAT;
vertexFormat[1].offset = 8;
vertexFormat[1].size = 2;
vertexFormat[2].type = MeshDynamic.TYPE_UCHAR;
vertexFormat[2].offset = 16;
vertexFormat[2].size = 4;
quadMesh.SetVertexFormat(vertexFormat);
注意在創建的時候,指定USAGE_DYNAMIC_VERTEX,而不是USAGE_DYNAMIC_ALL。由于Myra會讓我們繪制的全都是單純的Quad,因此Index可以完全不動,提前創建好就不再更改了:
var indexData = new int[MaxIndices];
for (int i = 0, j = 0; i < MaxIndices; i += 6, j += 4) {
indexData[i + 0] = j + 0;
indexData[i + 1] = j + 1;
indexData[i + 2] = j + 2;
indexData[i + 3] = j + 3;
indexData[i + 4] = j + 2;
indexData[i + 5] = j + 1;
}
quadMesh.SetIndicesArray(indexData);
quadMesh.FlushIndices();
順便把Material也創建好:
quadMaterial = Materials.FindManualMaterial("imgui").Inherit();
基本的數據都準備好了之后,開始制作繪制Quad的過程。這里先采用和Xna的SpriteBatch類似的Begin/Draw/End結構。首先是Begin:
public void Begin(TextureFiltering textureFiltering)
{
//設置渲染狀態
RenderState.SaveState();
RenderState.ClearStates();
RenderState.SetBlendFunc(RenderState.BLEND_ONE, RenderState.BLEND_ONE_MINUS_SRC_ALPHA);
RenderState.PolygonCull = RenderState.CULL_NONE;
RenderState.DepthFunc = RenderState.DEPTH_NONE;
//用正交投影矩陣渲染
var clientRenderSize = WindowManager.MainWindow.ClientRenderSize;
float left = 0;
float right = clientRenderSize.x;
float top = 0;
float bottom = clientRenderSize.y;
var orthoProj = new mat4 {
m00 = 2.0f / (right - left),
m03 = (right + left) / (left - right),
m11 = 2.0f / (top - bottom),
m13 = (top + bottom) / (bottom - top),
m22 = 0.5f,
m23 = 0.5f,
m33 = 1.0f
};
Renderer.Projection = orthoProj;
//選定為當前渲染的Shader
var shader = quadMaterial.GetShaderForce("imgui");
var pass = quadMaterial.GetRenderPass("imgui");
Renderer.SetShaderParameters(pass, shader, quadMaterial, false);
//選定為當前渲染Mesh
quadMesh.Bind();
}
一目了然,沒什么好說的。要注意的就是RenderState.SetBlendFunc()這里,是One加上OneMinusSrcAlpha的模式,和傳統Alpha混合的SrcAlpha加OneMinusSrcAlpha模式不同。因為Myra使用的是Pre-Multiplied Alpha。
順便把End也寫了:
public void End()
{
Flush();
//恢復渲染狀態
quadMesh.Unbind();
RenderState.RestoreState();
}
之后是和Myra對接的部分:
public void DrawQuad(Texture texture, ref VertexPositionColorTexture topLeft, ref VertexPositionColorTexture topRight, ref VertexPositionColorTexture bottomLeft, ref VertexPositionColorTexture bottomRight)
{
if (texture != lastTexture || vertexCount >= MaxVertices) {
Flush();
lastTexture = texture;
}
vertexData[vertexCount++] = new VertexLayout(topLeft);
vertexData[vertexCount++] = new VertexLayout(topRight);
vertexData[vertexCount++] = new VertexLayout(bottomLeft);
vertexData[vertexCount++] = new VertexLayout(bottomRight);
}
其實就是將Myra傳遞過來的數據緩存起來,當Texture發生了改變,或者頂點數量超過緩沖區上限了之后,再輸出。
最后就是最重要的輸出部分了,然而這部分反而代碼很簡單:
public void Flush()
{
if (vertexCount == 0 || lastTexture == null) {
return;
}
//應用頂點數據
quadMesh.ClearVertex();
unsafe {
fixed (void* pVertexData = vertexData) {
quadMesh.SetVertexArray((nint)pVertexData, vertexCount);
}
}
quadMesh.FlushVertex();
//繪制
RenderState.SetTexture(RenderState.BIND_FRAGMENT, 0, lastTexture);
quadMesh.RenderSurface(MeshDynamic.MODE_TRIANGLES, 0, 0, vertexCount / 4 * 6);
//重置計數
vertexCount = 0;
}
Unigine提供的SetVertexArray不完整,因此這里多了一塊unsafe。
繪制部分沒啥好說的:設置紋理,輸出三角形,通過vertexCount / 4 * 6計算得到繪制的Index總數量。
IMyraRenderer
MyraRenderer支持兩種模式,Sprite模式:給Xna的SpriteBatch類似物使用。Quad模式:直接繪制頂點。Unigine自然要使用Quad模式:
RendererType IMyraRenderer.RendererType => RendererType.Quad;
之后聲明幾個后面要用到的變量,并將其初始化:
readonly TextureQuadBatcher quadBatcher = new();
Rectangle currentScissor;
bool isBeginCalled;
public MyraRenderer()
{
var clientRenderSize = WindowManager.MainWindow.ClientRenderSize;
currentScissor = new Rectangle(0, 0, clientRenderSize.x, clientRenderSize.y);
}
然后實現Myra的Scissor:
Rectangle IMyraRenderer.Scissor
{
get => currentScissor;
set {
if (value != currentScissor) {
Flush();
currentScissor = value;
var clientRenderSize = WindowManager.MainWindow.ClientRenderSize;
int y = clientRenderSize.y - (currentScissor.Y + currentScissor.Height); //ScissorTest是右手坐標系,Y軸從屏幕下方往上數
RenderState.SetScissorTest((float)currentScissor.X / clientRenderSize.x, (float)y / clientRenderSize.y, (float)currentScissor.Width / clientRenderSize.x, (float)currentScissor.Height / clientRenderSize.y);
}
}
}
每次Scissor變化的時候,都要將已有的緩存刷新,再調用RenderState.SetScissorTest。由于Unigine是右手坐標系,屏幕左下角是(0.0f,0.0f),右上角是(1.0f,1.0f)。而Myra傳遞過來的是傳統的屏幕像素坐標,左上角為(0,0)右下角是(ClientRenderSize.x,ClientRenderSize.y),因此這里要對坐標系進行轉換。
剩下的幾個接口就很簡單了,把相應的參數傳給TextureQuadBatcher就可以。
void IMyraRenderer.Begin(TextureFiltering textureFiltering)
{
quadBatcher.Begin(textureFiltering);
isBeginCalled = true;
}
void IMyraRenderer.End()
{
quadBatcher.End();
isBeginCalled = false;
}
void IMyraRenderer.DrawSprite(object texture, Vector2 pos, Rectangle? src, FSColor color, float rotation, Vector2 scale, float depth)
{
//ignored
}
void IMyraRenderer.DrawQuad(object texture, ref VertexPositionColorTexture topLeft, ref VertexPositionColorTexture topRight, ref VertexPositionColorTexture bottomLeft, ref VertexPositionColorTexture bottomRight)
{
quadBatcher.DrawQuad((Texture)texture, ref topLeft, ref topRight, ref bottomLeft, ref bottomRight);
}
void Flush()
{
if (isBeginCalled) {
quadBatcher.Flush();
}
}
DrawSprite/DrawQuad二者只需實現其一,前面選擇了哪個模式就實現哪個模式即可。
如此一來渲染的部分就實現完成了。這并不是效率最高的實現方式,但概念上最簡單。目前先這么做,先讓程序跑起來再優化。

浙公網安備 33010602011771號