#region Copyright & License Information /* * Copyright 2007-2021 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, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; using OpenRA.Primitives; namespace OpenRA.Mods.Common.Graphics { public class ModelRenderable : IPalettedRenderable, IModifyableRenderable { readonly IEnumerable models; readonly WPos pos; readonly int zOffset; readonly WRot camera; readonly WRot lightSource; readonly float[] lightAmbientColor; readonly float[] lightDiffuseColor; readonly PaletteReference palette; readonly PaletteReference normalsPalette; readonly PaletteReference shadowPalette; readonly float scale; readonly float alpha; readonly float3 tint; readonly TintModifiers tintModifiers; public ModelRenderable( IEnumerable models, WPos pos, int zOffset, in WRot camera, float scale, in WRot lightSource, float[] lightAmbientColor, float[] lightDiffuseColor, PaletteReference color, PaletteReference normals, PaletteReference shadow) : this(models, pos, zOffset, camera, scale, lightSource, lightAmbientColor, lightDiffuseColor, color, normals, shadow, 1f, float3.Ones, TintModifiers.None) { } public ModelRenderable( IEnumerable models, WPos pos, int zOffset, in WRot camera, float scale, in WRot lightSource, float[] lightAmbientColor, float[] lightDiffuseColor, PaletteReference color, PaletteReference normals, PaletteReference shadow, float alpha, in float3 tint, TintModifiers tintModifiers) { this.models = models; this.pos = pos; this.zOffset = zOffset; this.scale = scale; this.camera = camera; this.lightSource = lightSource; this.lightAmbientColor = lightAmbientColor; this.lightDiffuseColor = lightDiffuseColor; palette = color; normalsPalette = normals; shadowPalette = shadow; this.alpha = alpha; this.tint = tint; this.tintModifiers = tintModifiers; } public WPos Pos => pos; public PaletteReference Palette => palette; public int ZOffset => zOffset; public bool IsDecoration => false; public float Alpha => alpha; public float3 Tint => tint; public TintModifiers TintModifiers => tintModifiers; public IPalettedRenderable WithPalette(PaletteReference newPalette) { return new ModelRenderable( models, pos, zOffset, camera, scale, lightSource, lightAmbientColor, lightDiffuseColor, newPalette, normalsPalette, shadowPalette, alpha, tint, tintModifiers); } public IRenderable WithZOffset(int newOffset) { return new ModelRenderable( models, pos, newOffset, camera, scale, lightSource, lightAmbientColor, lightDiffuseColor, palette, normalsPalette, shadowPalette, alpha, tint, tintModifiers); } public IRenderable OffsetBy(in WVec vec) { return new ModelRenderable( models, pos + vec, zOffset, camera, scale, lightSource, lightAmbientColor, lightDiffuseColor, palette, normalsPalette, shadowPalette, alpha, tint, tintModifiers); } public IRenderable AsDecoration() { return this; } public IModifyableRenderable WithAlpha(float newAlpha) { return new ModelRenderable( models, pos, zOffset, camera, scale, lightSource, lightAmbientColor, lightDiffuseColor, palette, normalsPalette, shadowPalette, newAlpha, tint, tintModifiers); } public IModifyableRenderable WithTint(in float3 newTint, TintModifiers newTintModifiers) { return new ModelRenderable( models, pos, zOffset, camera, scale, lightSource, lightAmbientColor, lightDiffuseColor, palette, normalsPalette, shadowPalette, alpha, newTint, newTintModifiers); } public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return new FinalizedModelRenderable(wr, this); } class FinalizedModelRenderable : IFinalizedRenderable { readonly ModelRenderable model; readonly ModelRenderProxy renderProxy; public FinalizedModelRenderable(WorldRenderer wr, ModelRenderable model) { this.model = model; var draw = model.models.Where(v => v.IsVisible); var map = wr.World.Map; var groundOrientation = map.TerrainOrientation(map.CellContaining(model.pos)); renderProxy = Game.Renderer.WorldModelRenderer.RenderAsync( wr, draw, model.camera, model.scale, groundOrientation, model.lightSource, model.lightAmbientColor, model.lightDiffuseColor, model.palette, model.normalsPalette, model.shadowPalette); } public void Render(WorldRenderer wr) { var map = wr.World.Map; var groundPos = model.pos - new WVec(0, 0, map.DistanceAboveTerrain(model.pos).Length); var tileScale = map.Grid.Type == MapGridType.RectangularIsometric ? 1448f : 1024f; var groundZ = map.Grid.TileSize.Height * (groundPos.Z - model.pos.Z) / tileScale; var pxOrigin = wr.Screen3DPosition(model.pos); // HACK: We don't have enough texture channels to pass the depth data to the shader // so for now just offset everything forward so that the back corner is rendered at pos. pxOrigin -= new float3(0, 0, Screen3DBounds(wr).Z.X); // HACK: The previous hack isn't sufficient for the ramp type that is half flat and half // sloped towards the camera. Offset it by another half cell to avoid clipping. var cell = map.CellContaining(model.pos); if (map.Ramp.Contains(cell) && map.Ramp[cell] == 7) pxOrigin += new float3(0, 0, 0.5f * map.Grid.TileSize.Height); var shadowOrigin = pxOrigin - groundZ * (new float2(renderProxy.ShadowDirection, 1)); var psb = renderProxy.ProjectedShadowBounds; var sa = shadowOrigin + psb[0]; var sb = shadowOrigin + psb[2]; var sc = shadowOrigin + psb[1]; var sd = shadowOrigin + psb[3]; var wrsr = Game.Renderer.WorldRgbaSpriteRenderer; var t = model.tint; if (wr.TerrainLighting != null && (model.tintModifiers & TintModifiers.IgnoreWorldTint) == 0) t *= wr.TerrainLighting.TintAt(model.pos); // Shader interprets negative alpha as a flag to use the tint colour directly instead of multiplying the sprite colour var a = model.alpha; if ((model.tintModifiers & TintModifiers.ReplaceColor) != 0) a *= -1; wrsr.DrawSprite(renderProxy.ShadowSprite, sa, sb, sc, sd, t, a); wrsr.DrawSprite(renderProxy.Sprite, pxOrigin - 0.5f * renderProxy.Sprite.Size, 1f, t, a); } public void RenderDebugGeometry(WorldRenderer wr) { var groundPos = model.pos - new WVec(0, 0, wr.World.Map.DistanceAboveTerrain(model.pos).Length); var groundZ = wr.World.Map.Grid.TileSize.Height * (groundPos.Z - model.pos.Z) / 1024f; var pxOrigin = wr.Screen3DPosition(model.pos); var shadowOrigin = pxOrigin - groundZ * (new float2(renderProxy.ShadowDirection, 1)); // Draw sprite rect var offset = pxOrigin + renderProxy.Sprite.Offset - 0.5f * renderProxy.Sprite.Size; var tl = wr.Viewport.WorldToViewPx(offset.XY); var br = wr.Viewport.WorldToViewPx((offset + renderProxy.Sprite.Size).XY); Game.Renderer.RgbaColorRenderer.DrawRect(tl, br, 1, Color.Red); // Draw transformed shadow sprite rect var c = Color.Purple; var psb = renderProxy.ProjectedShadowBounds; Game.Renderer.RgbaColorRenderer.DrawPolygon(new float2[] { wr.Viewport.WorldToViewPx(shadowOrigin + psb[1]), wr.Viewport.WorldToViewPx(shadowOrigin + psb[3]), wr.Viewport.WorldToViewPx(shadowOrigin + psb[0]), wr.Viewport.WorldToViewPx(shadowOrigin + psb[2]) }, 1, c); // Draw bounding box var draw = model.models.Where(v => v.IsVisible); var scaleTransform = OpenRA.Graphics.Util.ScaleMatrix(model.scale, model.scale, model.scale); var cameraTransform = OpenRA.Graphics.Util.MakeFloatMatrix(model.camera.AsMatrix()); foreach (var v in draw) { var bounds = v.Model.Bounds(v.FrameFunc()); var rotation = OpenRA.Graphics.Util.MakeFloatMatrix(v.RotationFunc().AsMatrix()); var worldTransform = OpenRA.Graphics.Util.MatrixMultiply(scaleTransform, rotation); var pxPos = pxOrigin + wr.ScreenVectorComponents(v.OffsetFunc()); var screenTransform = OpenRA.Graphics.Util.MatrixMultiply(cameraTransform, worldTransform); DrawBoundsBox(wr, pxPos, screenTransform, bounds, 1, Color.Yellow); } } static readonly uint[] CornerXIndex = new uint[] { 0, 0, 0, 0, 3, 3, 3, 3 }; static readonly uint[] CornerYIndex = new uint[] { 1, 1, 4, 4, 1, 1, 4, 4 }; static readonly uint[] CornerZIndex = new uint[] { 2, 5, 2, 5, 2, 5, 2, 5 }; static void DrawBoundsBox(WorldRenderer wr, in float3 pxPos, float[] transform, float[] bounds, float width, Color c) { var cr = Game.Renderer.RgbaColorRenderer; var corners = new float2[8]; for (var i = 0; i < 8; i++) { var vec = new[] { bounds[CornerXIndex[i]], bounds[CornerYIndex[i]], bounds[CornerZIndex[i]], 1 }; var screen = OpenRA.Graphics.Util.MatrixVectorMultiply(transform, vec); corners[i] = wr.Viewport.WorldToViewPx(pxPos + new float3(screen[0], screen[1], screen[2])); } // Front face cr.DrawPolygon(new[] { corners[0], corners[1], corners[3], corners[2] }, width, c); // Back face cr.DrawPolygon(new[] { corners[4], corners[5], corners[7], corners[6] }, width, c); // Horizontal edges cr.DrawLine(corners[0], corners[4], width, c); cr.DrawLine(corners[1], corners[5], width, c); cr.DrawLine(corners[2], corners[6], width, c); cr.DrawLine(corners[3], corners[7], width, c); } public Rectangle ScreenBounds(WorldRenderer wr) { return Screen3DBounds(wr).Bounds; } (Rectangle Bounds, float2 Z) Screen3DBounds(WorldRenderer wr) { var pxOrigin = wr.ScreenPosition(model.pos); var draw = model.models.Where(v => v.IsVisible); var scaleTransform = OpenRA.Graphics.Util.ScaleMatrix(model.scale, model.scale, model.scale); var cameraTransform = OpenRA.Graphics.Util.MakeFloatMatrix(model.camera.AsMatrix()); var minX = float.MaxValue; var minY = float.MaxValue; var minZ = float.MaxValue; var maxX = float.MinValue; var maxY = float.MinValue; var maxZ = float.MinValue; foreach (var v in draw) { var bounds = v.Model.Bounds(v.FrameFunc()); var rotation = OpenRA.Graphics.Util.MakeFloatMatrix(v.RotationFunc().AsMatrix()); var worldTransform = OpenRA.Graphics.Util.MatrixMultiply(scaleTransform, rotation); var pxPos = pxOrigin + wr.ScreenVectorComponents(v.OffsetFunc()); var screenTransform = OpenRA.Graphics.Util.MatrixMultiply(cameraTransform, worldTransform); for (var i = 0; i < 8; i++) { var vec = new float[] { bounds[CornerXIndex[i]], bounds[CornerYIndex[i]], bounds[CornerZIndex[i]], 1 }; var screen = OpenRA.Graphics.Util.MatrixVectorMultiply(screenTransform, vec); minX = Math.Min(minX, pxPos.X + screen[0]); minY = Math.Min(minY, pxPos.Y + screen[1]); minZ = Math.Min(minZ, pxPos.Z + screen[2]); maxX = Math.Max(maxX, pxPos.X + screen[0]); maxY = Math.Max(maxY, pxPos.Y + screen[1]); maxZ = Math.Max(minZ, pxPos.Z + screen[2]); } } return (Rectangle.FromLTRB((int)minX, (int)minY, (int)maxX, (int)maxY), new float2(minZ, maxZ)); } } } }