Textures, FrameBuffers and VertexBuffers allocated by the Sdl2 Renderer were only being released via finalizers. This could lead to OpenGL out of memory errors since resources may not be cleaned up in a timely manner. To avoid this, IDisposable has been implemented and transitively applied to classes that use these resources. As a side-effect some static state is no longer static, particularly in Renderer, in order to facilitate this change and just for nicer design in general. Also dispose some bitmaps.
354 lines
12 KiB
C#
354 lines
12 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation. For more information,
|
|
* see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using OpenRA.Primitives;
|
|
|
|
namespace OpenRA.Graphics
|
|
{
|
|
public class VoxelRenderProxy
|
|
{
|
|
public readonly Sprite Sprite;
|
|
public readonly Sprite ShadowSprite;
|
|
public readonly float ShadowDirection;
|
|
public readonly float2[] ProjectedShadowBounds;
|
|
|
|
public VoxelRenderProxy(Sprite sprite, Sprite shadowSprite, float2[] projectedShadowBounds, float shadowDirection)
|
|
{
|
|
Sprite = sprite;
|
|
ShadowSprite = shadowSprite;
|
|
ProjectedShadowBounds = projectedShadowBounds;
|
|
ShadowDirection = shadowDirection;
|
|
}
|
|
}
|
|
|
|
public sealed class VoxelRenderer : IDisposable
|
|
{
|
|
Renderer renderer;
|
|
IShader shader;
|
|
|
|
SheetBuilder sheetBuilder;
|
|
Dictionary<Sheet, IFrameBuffer> mappedBuffers;
|
|
Stack<KeyValuePair<Sheet, IFrameBuffer>> unmappedBuffers;
|
|
List<Pair<Sheet, Action>> doRender;
|
|
|
|
// Static constants
|
|
static readonly float[] shadowDiffuse = new float[] {0,0,0};
|
|
static readonly float[] shadowAmbient = new float[] {1,1,1};
|
|
static readonly float2 spritePadding = new float2(2, 2);
|
|
static readonly float[] zeroVector = new float[] {0,0,0,1};
|
|
static readonly float[] zVector = new float[] {0,0,1,1};
|
|
static readonly float[] flipMtx = Util.ScaleMatrix(1, -1, 1);
|
|
static readonly float[] shadowScaleFlipMtx = Util.ScaleMatrix(2, -2, 2);
|
|
|
|
public VoxelRenderer(Renderer renderer, IShader shader)
|
|
{
|
|
this.renderer = renderer;
|
|
this.shader = shader;
|
|
|
|
mappedBuffers = new Dictionary<Sheet, IFrameBuffer>();
|
|
unmappedBuffers = new Stack<KeyValuePair<Sheet, IFrameBuffer>>();
|
|
doRender = new List<Pair<Sheet, Action>>();
|
|
}
|
|
|
|
public void SetPalette(ITexture palette)
|
|
{
|
|
shader.SetTexture("Palette", palette);
|
|
}
|
|
|
|
public void SetViewportParams(Size screen, float zoom, int2 scroll)
|
|
{
|
|
var a = 2f / renderer.SheetSize;
|
|
var view = new float[]
|
|
{
|
|
a, 0, 0, 0,
|
|
0, -a, 0, 0,
|
|
0, 0, -2*a, 0,
|
|
-1, 1, 0, 1
|
|
};
|
|
|
|
shader.SetMatrix("View", view);
|
|
}
|
|
|
|
public VoxelRenderProxy RenderAsync(WorldRenderer wr, IEnumerable<VoxelAnimation> voxels, WRot camera, float scale,
|
|
float[] groundNormal, WRot lightSource, float[] lightAmbientColor, float[] lightDiffuseColor,
|
|
PaletteReference color, PaletteReference normals, PaletteReference shadowPalette)
|
|
{
|
|
// Correct for inverted y-axis
|
|
var scaleTransform = Util.ScaleMatrix(scale, scale, scale);
|
|
|
|
// Correct for bogus light source definition
|
|
var lightYaw = Util.MakeFloatMatrix(new WRot(WAngle.Zero, WAngle.Zero, -lightSource.Yaw).AsMatrix());
|
|
var lightPitch = Util.MakeFloatMatrix(new WRot(WAngle.Zero, -lightSource.Pitch, WAngle.Zero).AsMatrix());
|
|
var shadowTransform = Util.MatrixMultiply(lightPitch, lightYaw);
|
|
|
|
var invShadowTransform = Util.MatrixInverse(shadowTransform);
|
|
var cameraTransform = Util.MakeFloatMatrix(camera.AsMatrix());
|
|
var invCameraTransform = Util.MatrixInverse(cameraTransform);
|
|
|
|
// Sprite rectangle
|
|
var tl = new float2(float.MaxValue, float.MaxValue);
|
|
var br = new float2(float.MinValue, float.MinValue);
|
|
|
|
// Shadow sprite rectangle
|
|
var stl = new float2(float.MaxValue, float.MaxValue);
|
|
var sbr = new float2(float.MinValue, float.MinValue);
|
|
|
|
foreach (var v in voxels)
|
|
{
|
|
// Convert screen offset back to world coords
|
|
var offsetVec = Util.MatrixVectorMultiply(invCameraTransform, wr.ScreenVector(v.OffsetFunc()));
|
|
var offsetTransform = Util.TranslationMatrix(offsetVec[0], offsetVec[1], offsetVec[2]);
|
|
|
|
var worldTransform = v.RotationFunc().Aggregate(Util.IdentityMatrix(),
|
|
(x,y) => Util.MatrixMultiply(Util.MakeFloatMatrix(y.AsMatrix()), x));
|
|
worldTransform = Util.MatrixMultiply(scaleTransform, worldTransform);
|
|
worldTransform = Util.MatrixMultiply(offsetTransform, worldTransform);
|
|
|
|
var bounds = v.Voxel.Bounds(v.FrameFunc());
|
|
var worldBounds = Util.MatrixAABBMultiply(worldTransform, bounds);
|
|
var screenBounds = Util.MatrixAABBMultiply(cameraTransform, worldBounds);
|
|
var shadowBounds = Util.MatrixAABBMultiply(shadowTransform, worldBounds);
|
|
|
|
// Aggregate bounds rects
|
|
tl = float2.Min(tl, new float2(screenBounds[0], screenBounds[1]));
|
|
br = float2.Max(br, new float2(screenBounds[3], screenBounds[4]));
|
|
stl = float2.Min(stl, new float2(shadowBounds[0], shadowBounds[1]));
|
|
sbr = float2.Max(sbr, new float2(shadowBounds[3], shadowBounds[4]));
|
|
}
|
|
|
|
// Inflate rects to ensure rendering is within bounds
|
|
tl -= spritePadding;
|
|
br += spritePadding;
|
|
stl -= spritePadding;
|
|
sbr += spritePadding;
|
|
|
|
// Corners of the shadow quad, in shadow-space
|
|
var corners = new float[][]
|
|
{
|
|
new float[] {stl.X, stl.Y, 0, 1},
|
|
new float[] {sbr.X, sbr.Y, 0, 1},
|
|
new float[] {sbr.X, stl.Y, 0, 1},
|
|
new float[] {stl.X, sbr.Y, 0, 1}
|
|
};
|
|
|
|
var shadowScreenTransform = Util.MatrixMultiply(cameraTransform, invShadowTransform);
|
|
var shadowGroundNormal = Util.MatrixVectorMultiply(shadowTransform, groundNormal);
|
|
var screenCorners = new float2[4];
|
|
for (var j = 0; j < 4; j++)
|
|
{
|
|
// Project to ground plane
|
|
corners[j][2] = -(corners[j][1]*shadowGroundNormal[1]/shadowGroundNormal[2] +
|
|
corners[j][0]*shadowGroundNormal[0]/shadowGroundNormal[2]);
|
|
|
|
// Rotate to camera-space
|
|
corners[j] = Util.MatrixVectorMultiply(shadowScreenTransform, corners[j]);
|
|
screenCorners[j] = new float2(corners[j][0], corners[j][1]);
|
|
}
|
|
|
|
// Shadows are rendered at twice the resolution to reduce artefacts
|
|
Size spriteSize, shadowSpriteSize;
|
|
int2 spriteOffset, shadowSpriteOffset;
|
|
CalculateSpriteGeometry(tl, br, 1, out spriteSize, out spriteOffset);
|
|
CalculateSpriteGeometry(stl, sbr, 2, out shadowSpriteSize, out shadowSpriteOffset);
|
|
|
|
var sprite = sheetBuilder.Allocate(spriteSize, spriteOffset);
|
|
var shadowSprite = sheetBuilder.Allocate(shadowSpriteSize, shadowSpriteOffset);
|
|
var sb = sprite.bounds;
|
|
var ssb = shadowSprite.bounds;
|
|
var spriteCenter = new float2(sb.Left + sb.Width / 2, sb.Top + sb.Height / 2);
|
|
var shadowCenter = new float2(ssb.Left + ssb.Width / 2, ssb.Top + ssb.Height / 2);
|
|
|
|
var translateMtx = Util.TranslationMatrix(spriteCenter.X - spriteOffset.X, renderer.SheetSize - (spriteCenter.Y - spriteOffset.Y), 0);
|
|
var shadowTranslateMtx = Util.TranslationMatrix(shadowCenter.X - shadowSpriteOffset.X, renderer.SheetSize - (shadowCenter.Y - shadowSpriteOffset.Y), 0);
|
|
var correctionTransform = Util.MatrixMultiply(translateMtx, flipMtx);
|
|
var shadowCorrectionTransform = Util.MatrixMultiply(shadowTranslateMtx, shadowScaleFlipMtx);
|
|
|
|
doRender.Add(Pair.New<Sheet, Action>(sprite.sheet, () =>
|
|
{
|
|
foreach (var v in voxels)
|
|
{
|
|
// Convert screen offset to world offset
|
|
var offsetVec = Util.MatrixVectorMultiply(invCameraTransform, wr.ScreenVector(v.OffsetFunc()));
|
|
var offsetTransform = Util.TranslationMatrix(offsetVec[0], offsetVec[1], offsetVec[2]);
|
|
|
|
var rotations = v.RotationFunc().Aggregate(Util.IdentityMatrix(),
|
|
(x,y) => Util.MatrixMultiply(Util.MakeFloatMatrix(y.AsMatrix()), x));
|
|
var worldTransform = Util.MatrixMultiply(scaleTransform, rotations);
|
|
worldTransform = Util.MatrixMultiply(offsetTransform, worldTransform);
|
|
|
|
var transform = Util.MatrixMultiply(cameraTransform, worldTransform);
|
|
transform = Util.MatrixMultiply(correctionTransform, transform);
|
|
|
|
var shadow = Util.MatrixMultiply(shadowTransform, worldTransform);
|
|
shadow = Util.MatrixMultiply(shadowCorrectionTransform, shadow);
|
|
|
|
var lightTransform = Util.MatrixMultiply(Util.MatrixInverse(rotations), invShadowTransform);
|
|
|
|
var frame = v.FrameFunc();
|
|
for (uint i = 0; i < v.Voxel.Limbs; i++)
|
|
{
|
|
var rd = v.Voxel.RenderData(i);
|
|
var t = v.Voxel.TransformationMatrix(i, frame);
|
|
|
|
// Transform light vector from shadow -> world -> limb coords
|
|
var lightDirection = ExtractRotationVector(Util.MatrixMultiply(Util.MatrixInverse(t), lightTransform));
|
|
|
|
Render(rd, Util.MatrixMultiply(transform, t), lightDirection,
|
|
lightAmbientColor, lightDiffuseColor, color.Index, normals.Index);
|
|
|
|
// Disable shadow normals by forcing zero diffuse and identity ambient light
|
|
Render(rd, Util.MatrixMultiply(shadow, t), lightDirection,
|
|
shadowAmbient, shadowDiffuse, shadowPalette.Index, normals.Index);
|
|
}
|
|
}
|
|
}));
|
|
|
|
var screenLightVector = Util.MatrixVectorMultiply(invShadowTransform, zVector);
|
|
screenLightVector = Util.MatrixVectorMultiply(cameraTransform, screenLightVector);
|
|
return new VoxelRenderProxy(sprite, shadowSprite, screenCorners, -screenLightVector[2]/screenLightVector[1]);
|
|
}
|
|
|
|
static void CalculateSpriteGeometry(float2 tl, float2 br, float scale, out Size size, out int2 offset)
|
|
{
|
|
var width = (int)(scale*(br.X - tl.X));
|
|
var height = (int)(scale*(br.Y - tl.Y));
|
|
offset = (0.5f*scale*(br + tl)).ToInt2();
|
|
|
|
// Width and height must be even to avoid rendering glitches
|
|
if ((width & 1) == 1)
|
|
width += 1;
|
|
if ((height & 1) == 1)
|
|
height += 1;
|
|
|
|
size = new Size(width, height);
|
|
}
|
|
|
|
static float[] ExtractRotationVector(float[] mtx)
|
|
{
|
|
var tVec = Util.MatrixVectorMultiply(mtx, zVector);
|
|
var tOrigin = Util.MatrixVectorMultiply(mtx, zeroVector);
|
|
tVec[0] -= tOrigin[0]*tVec[3]/tOrigin[3];
|
|
tVec[1] -= tOrigin[1]*tVec[3]/tOrigin[3];
|
|
tVec[2] -= tOrigin[2]*tVec[3]/tOrigin[3];
|
|
|
|
// Renormalize
|
|
var w = (float)Math.Sqrt(tVec[0]*tVec[0] + tVec[1]*tVec[1] + tVec[2]*tVec[2]);
|
|
tVec[0] /= w;
|
|
tVec[1] /= w;
|
|
tVec[2] /= w;
|
|
tVec[3] = 1f;
|
|
|
|
return tVec;
|
|
}
|
|
|
|
void Render(VoxelRenderData renderData,
|
|
float[] t, float[] lightDirection,
|
|
float[] ambientLight, float[] diffuseLight,
|
|
int colorPalette, int normalsPalette)
|
|
{
|
|
shader.SetTexture("DiffuseTexture", renderData.Sheet.GetTexture());
|
|
shader.SetVec("PaletteRows", (colorPalette + 0.5f) / HardwarePalette.MaxPalettes,
|
|
(normalsPalette + 0.5f) / HardwarePalette.MaxPalettes);
|
|
shader.SetMatrix("TransformMatrix", t);
|
|
shader.SetVec("LightDirection", lightDirection, 4);
|
|
shader.SetVec("AmbientLight", ambientLight, 3);
|
|
shader.SetVec("DiffuseLight", diffuseLight, 3);
|
|
|
|
shader.Render(() => renderer.DrawBatch(Game.modData.VoxelLoader.VertexBuffer, renderData.Start, renderData.Count, PrimitiveType.QuadList));
|
|
}
|
|
|
|
public void BeginFrame()
|
|
{
|
|
foreach (var kv in mappedBuffers)
|
|
unmappedBuffers.Push(kv);
|
|
mappedBuffers.Clear();
|
|
|
|
sheetBuilder = new SheetBuilder(SheetType.BGRA, AllocateSheet);
|
|
doRender.Clear();
|
|
}
|
|
|
|
IFrameBuffer EnableFrameBuffer(Sheet s)
|
|
{
|
|
var fbo = mappedBuffers[s];
|
|
Game.Renderer.Flush();
|
|
fbo.Bind();
|
|
|
|
Game.Renderer.Device.EnableDepthBuffer();
|
|
return fbo;
|
|
}
|
|
|
|
void DisableFrameBuffer(IFrameBuffer fbo)
|
|
{
|
|
Game.Renderer.Flush();
|
|
Game.Renderer.Device.DisableDepthBuffer();
|
|
fbo.Unbind();
|
|
}
|
|
|
|
public void EndFrame()
|
|
{
|
|
if (doRender.Count == 0)
|
|
return;
|
|
|
|
Sheet currentSheet = null;
|
|
IFrameBuffer fbo = null;
|
|
foreach (var v in doRender)
|
|
{
|
|
// Change sheet
|
|
if (v.First != currentSheet)
|
|
{
|
|
if (fbo != null)
|
|
DisableFrameBuffer(fbo);
|
|
|
|
currentSheet = v.First;
|
|
fbo = EnableFrameBuffer(currentSheet);
|
|
}
|
|
|
|
v.Second();
|
|
}
|
|
|
|
DisableFrameBuffer(fbo);
|
|
}
|
|
|
|
public Sheet AllocateSheet()
|
|
{
|
|
// Reuse cached fbo
|
|
if (unmappedBuffers.Count > 0)
|
|
{
|
|
var kv = unmappedBuffers.Pop();
|
|
mappedBuffers.Add(kv.Key, kv.Value);
|
|
return kv.Key;
|
|
}
|
|
|
|
var size = new Size(renderer.SheetSize, renderer.SheetSize);
|
|
var framebuffer = renderer.Device.CreateFrameBuffer(size);
|
|
var sheet = new Sheet(framebuffer.Texture);
|
|
mappedBuffers.Add(sheet, framebuffer);
|
|
|
|
return sheet;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var kvp in mappedBuffers.Concat(unmappedBuffers))
|
|
{
|
|
kvp.Key.Dispose();
|
|
kvp.Value.Dispose();
|
|
}
|
|
|
|
mappedBuffers.Clear();
|
|
unmappedBuffers.Clear();
|
|
}
|
|
}
|
|
}
|