The HardwarePalette will now grow its palette buffer and texture in power-of-2 increments. This avoids it having to allocate memory for a full 256x256 texture up front. In practice the default mods use 22 or 23 palettes so a 32x256 texture is used. This means both the buffer and texture save neatly on memory. Additionally, HardwarePalette.ApplyModifiers sees a nice speedup as it has to transfer a much smaller amount of memory from the buffer to the texture. To facilitate this change, the MaxPalettes constant is no more. Instead the PaletteReference deals with the calculation of the index and this is passed into the appropriate methods.
352 lines
12 KiB
C#
352 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)
|
|
{
|
|
this.Sprite = sprite;
|
|
ShadowSprite = shadowSprite;
|
|
ProjectedShadowBounds = projectedShadowBounds;
|
|
ShadowDirection = shadowDirection;
|
|
}
|
|
}
|
|
|
|
public sealed class VoxelRenderer : IDisposable
|
|
{
|
|
// 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);
|
|
|
|
readonly Renderer renderer;
|
|
readonly IShader shader;
|
|
|
|
readonly Dictionary<Sheet, IFrameBuffer> mappedBuffers = new Dictionary<Sheet, IFrameBuffer>();
|
|
readonly Stack<KeyValuePair<Sheet, IFrameBuffer>> unmappedBuffers = new Stack<KeyValuePair<Sheet, IFrameBuffer>>();
|
|
readonly List<Pair<Sheet, Action>> doRender = new List<Pair<Sheet, Action>>();
|
|
|
|
SheetBuilder sheetBuilder;
|
|
|
|
public VoxelRenderer(Renderer renderer, IShader shader)
|
|
{
|
|
this.renderer = renderer;
|
|
this.shader = shader;
|
|
}
|
|
|
|
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[] { stl.X, stl.Y, 0, 1 },
|
|
new[] { sbr.X, sbr.Y, 0, 1 },
|
|
new[] { sbr.X, stl.Y, 0, 1 },
|
|
new[] { 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.TextureMidIndex, normals.TextureMidIndex);
|
|
|
|
// Disable shadow normals by forcing zero diffuse and identity ambient light
|
|
Render(rd, Util.MatrixMultiply(shadow, t), lightDirection,
|
|
ShadowAmbient, ShadowDiffuse, shadowPalette.TextureMidIndex, normals.TextureMidIndex);
|
|
}
|
|
}
|
|
}));
|
|
|
|
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,
|
|
float colorPaletteTextureMidIndex, float normalsPaletteTextureMidIndex)
|
|
{
|
|
shader.SetTexture("DiffuseTexture", renderData.Sheet.GetTexture());
|
|
shader.SetVec("PaletteRows", colorPaletteTextureMidIndex, normalsPaletteTextureMidIndex);
|
|
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();
|
|
}
|
|
}
|
|
}
|