Files
OpenRA/OpenRA.Game/Graphics/VoxelRenderer.cs
2014-08-21 11:27:52 +02:00

342 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 class VoxelRenderer
{
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.Texture);
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;
}
}
}