From 47af7a9023c426576abe1b140395ab121050d7a4 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 22 Oct 2023 16:58:58 +0100 Subject: [PATCH] Add IPostProcessWorldShader for custom effect render passes. --- OpenRA.Game/Graphics/PlatformInterfaces.cs | 1 + .../Graphics/RenderPostProcessPassVertex.cs | 37 ++++++++++++ OpenRA.Game/Graphics/ShaderBindings.cs | 7 ++- OpenRA.Game/Graphics/WorldRenderer.cs | 28 ++++++++++ OpenRA.Game/Traits/TraitsInterfaces.cs | 10 ++++ .../Traits/World/RenderPostProcessPassBase.cs | 56 +++++++++++++++++++ OpenRA.Platforms.Default/OpenGL.cs | 5 ++ OpenRA.Platforms.Default/Texture.cs | 13 +++++ .../ThreadedGraphicsContext.cs | 7 +++ glsl/postprocess.vert | 12 ++++ 10 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs create mode 100644 OpenRA.Mods.Common/Traits/World/RenderPostProcessPassBase.cs create mode 100644 glsl/postprocess.vert diff --git a/OpenRA.Game/Graphics/PlatformInterfaces.cs b/OpenRA.Game/Graphics/PlatformInterfaces.cs index 76c9001231..429fddb2e8 100644 --- a/OpenRA.Game/Graphics/PlatformInterfaces.cs +++ b/OpenRA.Game/Graphics/PlatformInterfaces.cs @@ -157,6 +157,7 @@ namespace OpenRA { void SetData(byte[] colors, int width, int height); void SetFloatData(float[] data, int width, int height); + void SetDataFromReadBuffer(Rectangle rect); byte[] GetData(); Size Size { get; } TextureScaleFilter ScaleFilter { get; set; } diff --git a/OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs b/OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs new file mode 100644 index 0000000000..3e61dd3b3d --- /dev/null +++ b/OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs @@ -0,0 +1,37 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * 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.Runtime.InteropServices; + +namespace OpenRA.Graphics +{ + [StructLayout(LayoutKind.Sequential)] + public readonly struct RenderPostProcessPassVertex + { + public readonly float X, Y; + + public RenderPostProcessPassVertex(float x, float y) + { + X = x; Y = y; + } + } + + public sealed class RenderPostProcessPassShaderBindings : ShaderBindings + { + public RenderPostProcessPassShaderBindings(string name) + : base("postprocess", "postprocess_" + name) { } + + public override ShaderVertexAttribute[] Attributes { get; } = new[] + { + new ShaderVertexAttribute("aVertexPosition", 2, 0) + }; + } +} diff --git a/OpenRA.Game/Graphics/ShaderBindings.cs b/OpenRA.Game/Graphics/ShaderBindings.cs index 71adb20666..eff4531c8a 100644 --- a/OpenRA.Game/Graphics/ShaderBindings.cs +++ b/OpenRA.Game/Graphics/ShaderBindings.cs @@ -39,11 +39,14 @@ namespace OpenRA.Graphics public abstract ShaderVertexAttribute[] Attributes { get; } protected ShaderBindings(string name) + : this(name, name) { } + + protected ShaderBindings(string vertexName, string fragmentName) { Stride = Attributes.Sum(a => a.Components * 4); - VertexShaderName = name; + VertexShaderName = vertexName; VertexShaderCode = GetShaderCode(VertexShaderName + ".vert"); - FragmentShaderName = name; + FragmentShaderName = fragmentName; FragmentShaderCode = GetShaderCode(FragmentShaderName + ".frag"); } diff --git a/OpenRA.Game/Graphics/WorldRenderer.cs b/OpenRA.Game/Graphics/WorldRenderer.cs index d61352b2af..e9ed250d49 100644 --- a/OpenRA.Game/Graphics/WorldRenderer.cs +++ b/OpenRA.Game/Graphics/WorldRenderer.cs @@ -45,6 +45,8 @@ namespace OpenRA.Graphics readonly List renderablesBuffer = new(); readonly IRenderer[] renderers; + readonly IRenderPostProcessPass[] postProcessPasses; + readonly ITexture postProcessTexture; internal WorldRenderer(ModData modData, World world) { @@ -71,6 +73,10 @@ namespace OpenRA.Graphics terrainRenderer = world.WorldActor.TraitOrDefault(); debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault()); + + postProcessPasses = world.WorldActor.TraitsImplementing().ToArray(); + if (postProcessPasses.Length > 0) + postProcessTexture = Game.Renderer.Context.CreateTexture(); } public void BeginFrame() @@ -284,6 +290,8 @@ namespace OpenRA.Graphics if (enableDepthBuffer) Game.Renderer.ClearDepthBuffer(); + ApplyPostProcessing(PostProcessPassType.AfterActors); + World.ApplyToActorsWithTrait((actor, trait) => { if (actor.IsInWorld && !actor.Disposed) @@ -293,6 +301,8 @@ namespace OpenRA.Graphics if (enableDepthBuffer) Game.Renderer.ClearDepthBuffer(); + ApplyPostProcessing(PostProcessPassType.AfterWorld); + World.ApplyToActorsWithTrait((actor, trait) => trait.RenderShroud(this)); if (enableDepthBuffer) @@ -306,9 +316,27 @@ namespace OpenRA.Graphics foreach (var r in g) r.Render(this); + ApplyPostProcessing(PostProcessPassType.AfterShroud); + Game.Renderer.Flush(); } + void ApplyPostProcessing(PostProcessPassType type) + { + var size = Game.Renderer.WorldFrameBufferSize; + var rect = new Rectangle(0, 0, size.Width, size.Height); + foreach (var pass in postProcessPasses) + { + if (pass.Type != type || !pass.Enabled) + continue; + + // Make a copy of the world texture to avoid reading and writing on the same buffer + Game.Renderer.Flush(); + postProcessTexture.SetDataFromReadBuffer(rect); + pass.Draw(this, postProcessTexture); + } + } + public void DrawAnnotations() { Game.Renderer.EnableAntialiasingFilter(); diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 7bb928dea1..dc8bdbcfb4 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -459,6 +459,16 @@ namespace OpenRA.Traits bool SpatiallyPartitionable { get; } } + public enum PostProcessPassType { AfterShroud, AfterWorld, AfterActors } + + [RequireExplicitImplementation] + public interface IRenderPostProcessPass + { + PostProcessPassType Type { get; } + bool Enabled { get; } + void Draw(WorldRenderer wr, ITexture worldTexture); + } + [Flags] public enum SelectionPriorityModifiers { diff --git a/OpenRA.Mods.Common/Traits/World/RenderPostProcessPassBase.cs b/OpenRA.Mods.Common/Traits/World/RenderPostProcessPassBase.cs new file mode 100644 index 0000000000..92569fc065 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/RenderPostProcessPassBase.cs @@ -0,0 +1,56 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * 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 OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public abstract class RenderPostProcessPassBase : IRenderPostProcessPass + { + readonly Renderer renderer; + readonly IShader shader; + readonly IVertexBuffer buffer; + readonly PostProcessPassType type; + + protected RenderPostProcessPassBase(string name, PostProcessPassType type) + { + this.type = type; + renderer = Game.Renderer; + shader = renderer.CreateShader(new RenderPostProcessPassShaderBindings(name)); + var vertices = new RenderPostProcessPassVertex[] + { + new(-1, -1), + new(1, -1), + new(1, 1), + new(1, 1), + new(-1, 1), + new(-1, -1) + }; + + buffer = renderer.CreateVertexBuffer(6); + buffer.SetData(ref vertices, 6); + } + + PostProcessPassType IRenderPostProcessPass.Type => type; + bool IRenderPostProcessPass.Enabled => Enabled; + void IRenderPostProcessPass.Draw(WorldRenderer wr, ITexture worldTexture) + { + shader.PrepareRender(); + shader.SetTexture("WorldTexture", worldTexture); + PrepareRender(wr, shader); + renderer.DrawBatch(buffer, shader, 0, 6, PrimitiveType.TriangleList); + } + + protected abstract bool Enabled { get; } + protected abstract void PrepareRender(WorldRenderer wr, IShader shader); + } +} diff --git a/OpenRA.Platforms.Default/OpenGL.cs b/OpenRA.Platforms.Default/OpenGL.cs index c204b01226..caa7f33d41 100644 --- a/OpenRA.Platforms.Default/OpenGL.cs +++ b/OpenRA.Platforms.Default/OpenGL.cs @@ -442,6 +442,10 @@ namespace OpenRA.Platforms.Default int width, int height, int border, int format, int type, IntPtr pixels); public static TexImage2D glTexImage2D { get; private set; } + public delegate void CopyTexImage2D(int target, int level, int internalFormat, + int x, int y, int width, int height, int border); + public static CopyTexImage2D glCopyTexImage2D { get; private set; } + public delegate void GetTexImage(int target, int level, int format, int type, IntPtr pixels); public static GetTexImage glGetTexImage { get; private set; } @@ -607,6 +611,7 @@ namespace OpenRA.Platforms.Default glBindTexture = Bind("glBindTexture"); glActiveTexture = Bind("glActiveTexture"); glTexImage2D = Bind("glTexImage2D"); + glCopyTexImage2D = Bind("glCopyTexImage2D"); glTexParameteri = Bind("glTexParameteri"); glTexParameterf = Bind("glTexParameterf"); diff --git a/OpenRA.Platforms.Default/Texture.cs b/OpenRA.Platforms.Default/Texture.cs index 26523fbd4f..f154281269 100644 --- a/OpenRA.Platforms.Default/Texture.cs +++ b/OpenRA.Platforms.Default/Texture.cs @@ -111,6 +111,19 @@ namespace OpenRA.Platforms.Default } } + public void SetDataFromReadBuffer(Rectangle rect) + { + VerifyThreadAffinity(); + if (!Exts.IsPowerOf2(rect.Width) || !Exts.IsPowerOf2(rect.Height)) + throw new InvalidDataException($"Non-power-of-two rectangle {rect.Width}x{rect.Height}"); + + PrepareTexture(); + + var glInternalFormat = OpenGL.Profile == GLProfile.Embedded ? OpenGL.GL_BGRA : OpenGL.GL_RGBA8; + OpenGL.glCopyTexImage2D(OpenGL.GL_TEXTURE_2D, 0, glInternalFormat, rect.X, rect.Y, rect.Width, rect.Height, 0); + OpenGL.CheckGLError(); + } + public byte[] GetData() { VerifyThreadAffinity(); diff --git a/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs b/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs index cc6c2494ee..61b3449f09 100644 --- a/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs +++ b/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs @@ -648,6 +648,7 @@ namespace OpenRA.Platforms.Default readonly Func setData2; readonly Action setData3; readonly Func setData4; + readonly Action setData5; readonly Action dispose; public ThreadedTexture(ThreadedGraphicsContext device, ITextureInternal texture) @@ -663,6 +664,7 @@ namespace OpenRA.Platforms.Default setData2 = tuple => { setData1(tuple); return null; }; setData3 = tuple => { var t = ((float[], int, int))tuple; texture.SetFloatData(t.Item1, t.Item2, t.Item3); }; setData4 = tuple => { setData3(tuple); return null; }; + setData5 = rect => texture.SetDataFromReadBuffer((Rectangle)rect); dispose = texture.Dispose; } @@ -725,6 +727,11 @@ namespace OpenRA.Platforms.Default } } + public void SetDataFromReadBuffer(Rectangle rect) + { + device.Post(setData5, rect); + } + public void Dispose() { device.Post(dispose); diff --git a/glsl/postprocess.vert b/glsl/postprocess.vert new file mode 100644 index 0000000000..ab804cbd07 --- /dev/null +++ b/glsl/postprocess.vert @@ -0,0 +1,12 @@ +#version {VERSION} + +#if __VERSION__ == 120 +attribute vec2 aVertexPosition; +#else +in vec2 aVertexPosition; +#endif + +void main() +{ + gl_Position = vec4(aVertexPosition, 0, 1); +}