diff --git a/OpenRA.Game/Graphics/Renderable.cs b/OpenRA.Game/Graphics/Renderable.cs index b2e84114ce..a3ec62dbd6 100644 --- a/OpenRA.Game/Graphics/Renderable.cs +++ b/OpenRA.Game/Graphics/Renderable.cs @@ -41,6 +41,7 @@ namespace OpenRA.Graphics IRenderable WithZOffset(int newOffset); IRenderable WithPos(WPos pos); void Render(WorldRenderer wr); + void RenderDebugGeometry(WorldRenderer wr); } public struct SpriteRenderable : IRenderable @@ -83,5 +84,11 @@ namespace OpenRA.Graphics { sprite.DrawAt(wr.ScreenPxPosition(pos) - pxCenter, palette.Index, scale); } + + public void RenderDebugGeometry(WorldRenderer wr) + { + var offset = wr.ScreenPxPosition(pos) - pxCenter; + Game.Renderer.WorldLineRenderer.DrawRect(offset, offset + sprite.size, Color.Red); + } } } diff --git a/OpenRA.Game/Graphics/VoxelRenderable.cs b/OpenRA.Game/Graphics/VoxelRenderable.cs index 168d3b0513..a2517c1680 100644 --- a/OpenRA.Game/Graphics/VoxelRenderable.cs +++ b/OpenRA.Game/Graphics/VoxelRenderable.cs @@ -102,5 +102,131 @@ namespace OpenRA.Graphics v.Voxel.Draw(vr, lightAmbientColor, lightDiffuseColor, palette.Index, normalsPalette.Index); Game.Renderer.DisableDepthBuffer(); } + + public void RenderDebugGeometry(WorldRenderer wr) + { + var draw = voxels.Where(v => v.DisableFunc == null || !v.DisableFunc()); + var scaleTransform = Util.ScaleMatrix(scale, scale, scale); + var pxOrigin = wr.ScreenPosition(pos); + + // Correct for bogus light source definition + var shadowTransform = Util.MakeFloatMatrix(new WRot(new WAngle(256) - lightSource.Pitch, + WAngle.Zero, lightSource.Yaw + new WAngle(512)).AsMatrix()); + + var invShadowTransform = Util.MatrixInverse(shadowTransform); + var cameraTransform = Util.MakeFloatMatrix(camera.AsMatrix()); + + // TODO: Generalize this once we support sloped terrain + var groundNormal = new float[] {0,0,1,1}; + var groundPos = new float[] {0, 0, 0.5f*(wr.ScreenPosition(pos).Y - wr.ScreenZPosition(pos, 0)), 1}; + var shadowGroundNormal = Util.MatrixVectorMultiply(shadowTransform, groundNormal); + var shadowGroundPos = Util.MatrixVectorMultiply(shadowTransform, groundPos); + + // 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 draw) + { + var bounds = v.Voxel.Bounds(v.FrameFunc()); + var worldTransform = v.RotationFunc().Reverse().Aggregate(scaleTransform, + (x,y) => Util.MatrixMultiply(x, Util.MakeFloatMatrix(y.AsMatrix()))); + + var worldBounds = Util.MatrixAABBMultiply(worldTransform, bounds); + var screenBounds = Util.MatrixAABBMultiply(cameraTransform, worldBounds); + + // Aggregate bounds rect + var pxOffset = wr.ScreenVector(v.OffsetFunc()); + var pxPos = pxOrigin + new float2(pxOffset[0], pxOffset[1]); + tl = float2.Min(tl, pxPos + new float2(screenBounds[0], screenBounds[1])); + br = float2.Max(br, pxPos + new float2(screenBounds[3], screenBounds[4])); + + // Box to render the shadow image from + var shadowBounds = Util.MatrixAABBMultiply(shadowTransform, worldBounds); + var shadowPxOffset = Util.MatrixVectorMultiply(shadowTransform, pxOffset); + + stl = float2.Min(stl, new float2(shadowPxOffset[0] + shadowBounds[0], shadowPxOffset[1] + shadowBounds[1])); + sbr = float2.Max(sbr, new float2(shadowPxOffset[0] + shadowBounds[3], shadowPxOffset[1] + shadowBounds[4])); + + // Draw voxel bounding box + var screenTransform = Util.MatrixMultiply(cameraTransform, worldTransform); + DrawBoundsBox(pxPos, screenTransform, bounds, Color.Yellow); + } + + // Inflate rects by 1px each side to ensure rendering is within bounds + var pad = new float2(1,1); + tl -= pad; + br += pad; + stl -= pad; + sbr += pad; + + // 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 screenCorners = new float2[4]; + for (var j = 0; j < 4; j++) + { + // Project to ground plane + corners[j][2] -= (corners[j][2] - shadowGroundPos[2]) + + (corners[j][1] - shadowGroundPos[1])*shadowGroundNormal[1]/shadowGroundNormal[2] + + (corners[j][0] - shadowGroundPos[0])*shadowGroundNormal[0]/shadowGroundNormal[2]; + + // Rotate to camera-space + corners[j] = Util.MatrixVectorMultiply(shadowScreenTransform, corners[j]); + screenCorners[j] = pxOrigin + new float2(corners[j][0], corners[j][1]); + } + + // Draw transformed shadow sprite rect + var c = Color.Purple; + Game.Renderer.WorldLineRenderer.DrawLine(screenCorners[1], screenCorners[3], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(screenCorners[3], screenCorners[0], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(screenCorners[0], screenCorners[2], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(screenCorners[2], screenCorners[1], c, c); + + // Draw sprite rect + Game.Renderer.WorldLineRenderer.DrawRect(tl, br, Color.Red); + } + + static void DrawBoundsBox(float2 pxPos, float[] transform, float[] bounds, Color c) + { + // Corner offsets + var ix = new uint[] {0,0,0,0,3,3,3,3}; + var iy = new uint[] {1,1,4,4,1,1,4,4}; + var iz = new uint[] {2,5,2,5,2,5,2,5}; + + var corners = new float2[8]; + for (var i = 0; i < 8; i++) + { + var vec = new float[] {bounds[ix[i]], bounds[iy[i]], bounds[iz[i]], 1}; + var screen = Util.MatrixVectorMultiply(transform, vec); + corners[i] = pxPos + new float2(screen[0], screen[1]); + } + + Game.Renderer.WorldLineRenderer.DrawLine(corners[0], corners[1], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[1], corners[3], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[3], corners[2], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[2], corners[0], c, c); + + Game.Renderer.WorldLineRenderer.DrawLine(corners[4], corners[5], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[5], corners[7], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[7], corners[6], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[6], corners[4], c, c); + + Game.Renderer.WorldLineRenderer.DrawLine(corners[0], corners[4], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[1], corners[5], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[2], corners[6], c, c); + Game.Renderer.WorldLineRenderer.DrawLine(corners[3], corners[7], c, c); + } } } diff --git a/OpenRA.Game/Graphics/WorldRenderer.cs b/OpenRA.Game/Graphics/WorldRenderer.cs index 87eef096f2..676d7fd931 100644 --- a/OpenRA.Game/Graphics/WorldRenderer.cs +++ b/OpenRA.Game/Graphics/WorldRenderer.cs @@ -37,6 +37,7 @@ namespace OpenRA.Graphics internal readonly ShroudRenderer shroudRenderer; internal readonly HardwarePalette palette; internal Cache palettes; + Lazy devTrait; internal WorldRenderer(World world) { @@ -51,6 +52,8 @@ namespace OpenRA.Graphics terrainRenderer = new TerrainRenderer(world, this); shroudRenderer = new ShroudRenderer(world); + + devTrait = Lazy.New(() => world.LocalPlayer != null ? world.LocalPlayer.PlayerActor.Trait() : null); } PaletteReference CreatePaletteReference(string name) @@ -74,14 +77,21 @@ namespace OpenRA.Graphics bounds.TopLeftAsCPos().ToPPos(), bounds.BottomRightAsCPos().ToPPos()); - actors.SelectMany(a => a.Render(this)) - .OrderBy(r => r, comparer) - .Do(rr => rr.Render(this)); + var worldRenderables = actors.SelectMany(a => a.Render(this)) + .OrderBy(r => r, comparer); // Effects are drawn on top of all actors + var effectRenderables = world.Effects.SelectMany(e => e.Render(this)); + // TODO: Allow effects to be interleaved with actors - world.Effects.SelectMany(e => e.Render(this)) - .Do(rr => rr.Render(this)); + worldRenderables.Do(rr => rr.Render(this)); + effectRenderables.Do(rr => rr.Render(this)); + + if (devTrait.Value != null && devTrait.Value.ShowDebugGeometry) + { + worldRenderables.Do(rr => rr.RenderDebugGeometry(this)); + effectRenderables.Do(rr => rr.RenderDebugGeometry(this)); + } } public void Draw() @@ -219,6 +229,13 @@ namespace OpenRA.Graphics return new int2((int)Math.Round(px.X), (int)Math.Round(px.Y)); } + // For scaling vectors to pixel sizes in the voxel renderer + public float[] ScreenVector(WVec vec) + { + var c = Game.CellSize/1024f; + return new float[] {c*vec.X, c*vec.Y, c*vec.Z, 1}; + } + public float ScreenZPosition(WPos pos, int zOffset) { return (pos.Y + pos.Z + zOffset)*Game.CellSize/1024f; } } } diff --git a/OpenRA.Game/Traits/Player/DeveloperMode.cs b/OpenRA.Game/Traits/Player/DeveloperMode.cs index 769d53eca0..51b33a0211 100644 --- a/OpenRA.Game/Traits/Player/DeveloperMode.cs +++ b/OpenRA.Game/Traits/Player/DeveloperMode.cs @@ -22,6 +22,7 @@ namespace OpenRA.Traits public bool UnlimitedPower; public bool BuildAnywhere; public bool ShowMuzzles; + public bool ShowDebugGeometry; public object Create (ActorInitializer init) { return new DeveloperMode(this); } } @@ -39,6 +40,7 @@ namespace OpenRA.Traits // Client size only public bool ShowMuzzles; + public bool ShowDebugGeometry; public DeveloperMode(DeveloperModeInfo info) { @@ -50,6 +52,7 @@ namespace OpenRA.Traits UnlimitedPower = info.UnlimitedPower; BuildAnywhere = info.BuildAnywhere; ShowMuzzles = info.ShowMuzzles; + ShowDebugGeometry = info.ShowDebugGeometry; } public void ResolveOrder (Actor self, Order order) diff --git a/OpenRA.Mods.RA/Widgets/Logic/CheatsLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/CheatsLogic.cs index 68f1d6bea1..a4cb298ffa 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/CheatsLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/CheatsLogic.cs @@ -13,6 +13,7 @@ using System.Drawing; using System.Reflection; using System.Linq; using OpenRA; +using OpenRA.Graphics; using OpenRA.Traits; using OpenRA.Widgets; using XRandom = OpenRA.Thirdparty.Random; @@ -68,6 +69,13 @@ namespace OpenRA.Mods.RA.Widgets.Logic showMuzzlesCheckbox.OnClick = () => devTrait.ShowMuzzles ^= true; } + var showGeometryCheckbox = widget.GetOrNull("SHOW_GEOMETRY"); + if (showGeometryCheckbox != null) + { + showGeometryCheckbox.IsChecked = () => devTrait.ShowDebugGeometry; + showGeometryCheckbox.OnClick = () => devTrait.ShowDebugGeometry ^= true; + } + var allTechCheckbox = widget.GetOrNull("ENABLE_TECH"); if (allTechCheckbox != null) { diff --git a/mods/cnc/chrome/cheats.yaml b/mods/cnc/chrome/cheats.yaml index 7f68f3c389..665d7985aa 100644 --- a/mods/cnc/chrome/cheats.yaml +++ b/mods/cnc/chrome/cheats.yaml @@ -98,4 +98,10 @@ Container@CHEATS_PANEL: Y:235 Height:20 Width:200 - Text:Show Muzzle Positions \ No newline at end of file + Text:Show Muzzle Positions + Checkbox@SHOW_GEOMETRY: + X:290 + Y:265 + Height:20 + Width:200 + Text:Show Render Geometry \ No newline at end of file diff --git a/mods/ra/chrome/cheats.yaml b/mods/ra/chrome/cheats.yaml index b07fee7405..ffa5f7507c 100644 --- a/mods/ra/chrome/cheats.yaml +++ b/mods/ra/chrome/cheats.yaml @@ -3,7 +3,7 @@ Background@CHEATS_PANEL: X:(WINDOW_RIGHT - WIDTH)/2 Y:(WINDOW_BOTTOM - HEIGHT)/2 Width:350 - Height:475 + Height:505 Visible:false Children: Label@LABEL_TITLE: @@ -87,21 +87,27 @@ Background@CHEATS_PANEL: Height:20 Width:200 Text:Show Muzzle Positions + Checkbox@SHOW_GEOMETRY: + X:30 + Y:380 + Height:20 + Width:200 + Text:Show Render Geometry Checkbox@DESYNC_ARMED: X:30 - Y:382 + Y:412 Width:20 Height:20 Button@DESYNC: X:60 - Y:380 + Y:410 Width:150 Height:20 Text: Force Desync Height:25 Button@CLOSE: X:30 - Y:420 + Y:450 Width:PARENT_RIGHT - 60 Height:25 Text:Close