diff --git a/Resources/EnginePrototypes/Shaders/stockshaders.yml b/Resources/EnginePrototypes/Shaders/stockshaders.yml index 63e2dcf6890..f7744e8de86 100644 --- a/Resources/EnginePrototypes/Shaders/stockshaders.yml +++ b/Resources/EnginePrototypes/Shaders/stockshaders.yml @@ -17,3 +17,8 @@ id: ColorPicker kind: source path: "/Shaders/color_picker.swsl" + +- type: shader + id: NormalRotator + kind: source + path: "/Shaders/Internal/normal-rotator.swsl" diff --git a/Resources/Shaders/Internal/color.swsl b/Resources/Shaders/Internal/color.swsl new file mode 100644 index 00000000000..1625f72a84a --- /dev/null +++ b/Resources/Shaders/Internal/color.swsl @@ -0,0 +1,5 @@ +uniform highp vec4 InputColor; + +void fragment() { + COLOR = InputColor; +} diff --git a/Resources/Shaders/Internal/light_shared.swsl b/Resources/Shaders/Internal/light_shared.swsl index fbd2fdb20ea..a546ae9b351 100644 --- a/Resources/Shaders/Internal/light_shared.swsl +++ b/Resources/Shaders/Internal/light_shared.swsl @@ -6,16 +6,24 @@ const highp float LIGHTING_HEIGHT = 1.0; const highp float g_MinVariance = 0.0; +const highp float COLOR_POWER = 0.4586603; + varying highp vec2 worldPosition; uniform highp vec4 lightColor; // Position of the light, in world coordinates. uniform highp vec2 lightCenter; +uniform highp vec2 eyeCenter; +uniform highp vec2 eyeZoom; uniform highp float lightRange; uniform highp float lightPower; uniform highp float lightSoftness; uniform highp float lightIndex; +uniform highp int useNormals; +uniform highp float globalRotation; +uniform highp float maskRotation; uniform sampler2D shadowMap; +uniform sampler2D normalMap; void vertex() { @@ -26,6 +34,11 @@ void vertex() VERTEX = transformed.xy; } +highp vec2 rotateVector(highp vec2 vector, highp float theta) +{ + return vector * cos(theta) + vector.yx * vec2(-1, 1) * sin(theta); +} + highp float shadowContrib(highp vec2 diff) { highp float dist = length(diff); @@ -53,6 +66,28 @@ void fragment() val *= lightPower; val *= mask; - COLOR = vec4(lightColor.rgb, val * occlusion); + highp float run = 1.0; + + if (useNormals == 1) + { + highp vec2 lightDir = rotateVector(normalize(diff), globalRotation); + highp vec2 newUV = rotateVector(UV - vec2(0.5, 0.5) + rotateVector((lightCenter - eyeCenter) / vec2(2, -2) / lightRange, maskRotation), -globalRotation); + newUV *= (projectionMatrix * vec3(1.0, 1.0, 0.0)).xy * vec2(1.0, -1.0) / eyeZoom * lightRange; + newUV += vec2(0.5, 0.5); + + highp vec4 origSample = texture2D(normalMap, newUV); + if (origSample.xyzw == vec4(0.0, 0.0, 0.0, 1.0)) + discard; + + // origSample is on the range 0 - 1 in all components, + // but every color component has been raised to the log_0.5(ln(2)/pi)th power (yes i know) by some unknown mechanism + // so we first raise it to the log_(ln2/pi)(0.5)th power (yes, i know) to reset it + highp vec3 normalSample = pow(origSample.xyz, vec3(COLOR_POWER)) * vec3(2.0, 2.0, 1.0) - vec3(1.0, 1.0, 0.0); + run = dot(normalize(vec3(lightDir, LIGHTING_HEIGHT)), normalSample * vec3(-1.0, -1.0, 1.0)); + if (run <= 0.0) + discard; + } + + COLOR = vec4(lightColor.xyz, val * occlusion * run); } diff --git a/Resources/Shaders/Internal/normal-rotator.swsl b/Resources/Shaders/Internal/normal-rotator.swsl new file mode 100644 index 00000000000..49d52bad844 --- /dev/null +++ b/Resources/Shaders/Internal/normal-rotator.swsl @@ -0,0 +1,12 @@ +uniform highp float rotation; +const highp float COLOR_POWER = 0.4586603; +const highp float COLOR_POWERNT = 2.1802625; + +void fragment() +{ + highp vec4 Col = zTexture(UV); + Col = vec4(pow(Col.xy, vec2(COLOR_POWER)) - vec2(0.5, 0.5), Col.zw); + Col = vec4(Col.xy * cos(rotation) + vec2(-1.0, 1.0) * Col.yx * sin(rotation), Col.zw); + Col = vec4(pow(Col.xy + vec2(0.5, 0.5), vec2(COLOR_POWERNT)), Col.zw); + COLOR = Col; +} diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index a4bdcae91b7..b96c90a9e9e 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -12,6 +12,7 @@ using Robust.Shared.Animations; using Robust.Shared.ComponentTrees; using Robust.Shared.GameObjects; +using Robust.Shared.Graphics; using Robust.Shared.Graphics.RSI; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -58,6 +59,8 @@ public sealed partial class SpriteComponent : Component, IComponentDebug, ISeria [DataField("visible")] private bool _visible = true; + private const string NormalShaderPrototype = "NormalRotator"; + // VV convenience variable to examine layer objects using layer keys [ViewVariables] private Dictionary _mappedLayers => LayerMap.ToDictionary(x => x.Key, x => Layers[x.Value]); @@ -786,6 +789,9 @@ public void LayerSetData(int index, PrototypeLayerData layerDatum) } } + prototypes.TryIndex(NormalShaderPrototype, out var normalProto); + layer.NormalShader = normalProto!.Instance(); + layer.RenderingStrategy = layerDatum.RenderingStrategy ?? layer.RenderingStrategy; layer.Cycle = layerDatum.Cycle; @@ -1289,7 +1295,7 @@ public bool SnapCardinals [ViewVariables(VVAccess.ReadWrite)] public bool NoRotation { get => _screenLock; set => _screenLock = value; } - internal void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, Direction? overrideDirection) + internal void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, Direction? overrideDirection, bool normal = false) { var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs angle = angle.Reduced().FlipPositive(); // Reduce the angles to fix math shenanigans @@ -1324,16 +1330,16 @@ internal void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation switch (layer.RenderingStrategy) { case LayerRenderingStrategy.NoRotation: - layer.Render(drawingHandle, ref transformNoRot, angle, overrideDirection); + layer.Render(drawingHandle, ref transformNoRot, angle, overrideDirection, (float)eyeRotation, normal: normal); break; case LayerRenderingStrategy.SnapToCardinals: - layer.Render(drawingHandle, ref transformSnap, angle, overrideDirection); + layer.Render(drawingHandle, ref transformSnap, angle, overrideDirection, (float)eyeRotation, normal: normal); break; case LayerRenderingStrategy.Default: - layer.Render(drawingHandle, ref transformDefault, angle, overrideDirection); + layer.Render(drawingHandle, ref transformDefault, angle, overrideDirection, (float)eyeRotation, normal: normal); break; case LayerRenderingStrategy.UseSpriteStrategy: - layer.Render(drawingHandle, ref transformSprite, angle, overrideDirection); + layer.Render(drawingHandle, ref transformSprite, angle, overrideDirection, (float)eyeRotation, normal: normal); break; default: Logger.Error($"Tried to render a layer with unknown rendering stragegy: {layer.RenderingStrategy}"); @@ -1346,7 +1352,7 @@ internal void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation { foreach (var layer in Layers) { - layer.Render(drawingHandle, ref transformSprite, angle, overrideDirection); + layer.Render(drawingHandle, ref transformSprite, angle, overrideDirection, (float)eyeRotation, normal: normal); } } } @@ -1492,9 +1498,9 @@ public enum DirectionOffset : byte public sealed class Layer : ISpriteLayer, ISerializationHooks { [ViewVariables] private readonly SpriteComponent _parent; - [ViewVariables] public string? ShaderPrototype; [ViewVariables] public ShaderInstance? Shader; + /* no vv 4 u */ public ShaderInstance? NormalShader; [ViewVariables] public Texture? Texture; private RSI? _rsi; @@ -2017,7 +2023,7 @@ public static RsiDirection GetDirection(RsiDirectionType dirType, Angle angle) /// /// Render a layer. This assumes that the input angle is between 0 and 2pi. /// - internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection) + internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection, float eyeRotation = 0f, bool normal = false) { if (!Visible || Blank) return; @@ -2042,7 +2048,7 @@ internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatri var transformMatrix = Matrix3x2.Multiply(layerMatrix, spriteMatrix); drawingHandle.SetTransform(in transformMatrix); - RenderTexture(drawingHandle, texture); + RenderTexture(drawingHandle, texture, eyeRotation, normal: normal); } else { @@ -2072,18 +2078,24 @@ internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatri } } - private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture) + private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture, float textureRotation = 0f, bool normal = false) { - if (Shader != null) + if (normal && NormalShader is {} normalShader) + { + NormalShader = normalShader.Mutable ? normalShader : normalShader.Duplicate(); + drawingHandle.UseShader(NormalShader); + NormalShader.SetParameter("rotation", textureRotation + (float)drawingHandle.GetTransform().Rotation()); + } + else if (Shader != null) drawingHandle.UseShader(Shader); - var layerColor = _parent.color * Color; + var layerColor = normal ? Color.White : _parent.color * Color; var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter; var quad = Box2.FromDimensions(textureSize/-2, textureSize); - drawingHandle.DrawTextureRectRegion(texture, quad, layerColor); + drawingHandle.DrawTextureRectRegion(texture, quad, layerColor, normal: normal); - if (Shader != null) + if (Shader != null || normal) drawingHandle.UseShader(null); } diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index e92f0df258c..5ef7d184811 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -46,12 +46,12 @@ public sealed partial class SpriteSystem : EntitySystem private ISawmill _sawmill = default!; - internal void Render(EntityUid uid, SpriteComponent sprite, DrawingHandleWorld drawingHandle, Angle eyeRotation, in Angle worldRotation, in Vector2 worldPosition) + internal void Render(EntityUid uid, SpriteComponent sprite, DrawingHandleWorld drawingHandle, Angle eyeRotation, in Angle worldRotation, in Vector2 worldPosition, bool normal = false) { if (!sprite.IsInert) _queuedFrameUpdate.Add(uid); - sprite.RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, sprite.EnableDirectionOverride ? sprite.DirectionOverride : null); + sprite.RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, sprite.EnableDirectionOverride ? sprite.DirectionOverride : null, normal: normal); } public override void Initialize() diff --git a/Robust.Client/Graphics/AtlasTexture.cs b/Robust.Client/Graphics/AtlasTexture.cs index 6a983f8b04e..87358ee72d3 100644 --- a/Robust.Client/Graphics/AtlasTexture.cs +++ b/Robust.Client/Graphics/AtlasTexture.cs @@ -12,7 +12,7 @@ namespace Robust.Client.Graphics [PublicAPI] public sealed class AtlasTexture : Texture { - public AtlasTexture(Texture texture, UIBox2 subRegion) : base((Vector2i) subRegion.Size) + public AtlasTexture(Texture texture, UIBox2 subRegion, int? width = null) : base((Vector2i) subRegion.Size) { DebugTools.Assert(SubRegion.Right < texture.Width); DebugTools.Assert(SubRegion.Bottom < texture.Height); @@ -20,6 +20,7 @@ public AtlasTexture(Texture texture, UIBox2 subRegion) : base((Vector2i) subRegi DebugTools.Assert(SubRegion.Top >= 0); SubRegion = subRegion; + RegionWidth = width ?? (int)subRegion.Width; SourceTexture = texture; } @@ -33,6 +34,11 @@ public AtlasTexture(Texture texture, UIBox2 subRegion) : base((Vector2i) subRegi /// public UIBox2 SubRegion { get; } + /// + /// The width of the texture we came from, used for offsetting to get normals + /// + public int RegionWidth { get; } + public override Color GetPixel(int x, int y) { DebugTools.Assert(x < SubRegion.Right); diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index 5c0a3efff5d..9e51861c871 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using OpenToolkit.Graphics.OpenGL4; +using Robust.Shared; +using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.Graphics; @@ -27,8 +29,14 @@ internal partial class Clyde private List> _grids = new(); - private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye) + private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye, bool normal = false) { + if (normal && (!_lightManager.Enabled + || !_lightManager.DrawLighting + || !_cfg.GetCVar(CVars.LightNormals) + || !_resourceCache.GetNormalsEnabled())) + return; + var mapId = eye.Position.MapId; if (!_mapManager.MapExists(mapId)) { @@ -55,12 +63,16 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun { SetTexture(TextureUnit.Texture0, _tileDefinitionManager.TileTextureAtlas); SetTexture(TextureUnit.Texture1, _lightingReady ? viewport.LightRenderTarget.Texture : _stockTextureWhite); - gridProgram = ActivateShaderInstance(_defaultShader.Handle).Item1; + + gridProgram = ActivateShaderInstance((normal ? _colorShader : _defaultShader).Handle).Item1; + if (normal) + gridProgram.SetUniformMaybe("InputColor", new Color(0.5f, 0.5f, 1f)); SetupGlobalUniformsImmediate(gridProgram, (ClydeTexture) _tileDefinitionManager.TileTextureAtlas); gridProgram.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0); gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1); - gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1)); + if (!normal) + gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1)); } var transform = _entityManager.GetComponent(mapGrid); diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs index c70f8ef5c8d..51e9abbbfd7 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs @@ -16,6 +16,7 @@ using Robust.Shared.Maths; using Robust.Shared.Profiling; using Robust.Shared.Utility; +using Color = Robust.Shared.Maths.Color; namespace Robust.Client.Graphics.Clyde { @@ -247,8 +248,14 @@ private List GetOverlaysForSpace(OverlaySpace space) return ScreenBufferTexture; } - private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye) + private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye, bool normal = false) { + if (normal && (!_lightManager.Enabled + || !_lightManager.DrawLighting + || !_cfg.GetCVar(CVars.LightNormals) + || !_resourceCache.GetNormalsEnabled())) + return; + var mapId = eye.Position.MapId; if (mapId == MapId.Nullspace) return; @@ -353,7 +360,13 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world } } - spriteSystem.Render(entry.Uid, entry.Sprite, _renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos); + spriteSystem.Render(entry.Uid, + entry.Sprite, + _renderHandle.DrawingHandleWorld, + eye.Rotation, + in entry.WorldRot, + in entry.WorldPos, + normal: normal); if (entry.Sprite.PostShader != null && entityPostRenderTarget != null) { @@ -481,6 +494,19 @@ private void RenderViewport(Viewport viewport) if (eye.Position.MapId != MapId.Nullspace) { + // prob not needed + using (DebugGroup("GridNormals")) + using (_prof.Group("GridNormals")) + { + _drawGrids(viewport, worldAABB, worldBounds, eye, normal: true); + } + + using (DebugGroup("EntityNormals")) + using (_prof.Group("EntityNormals")) + { + DrawEntities(viewport, worldBounds, worldAABB, eye, normal: true); + } + using (DebugGroup("Lights")) using (_prof.Group("Lights")) { diff --git a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs index 80008a627f3..486739d8370 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs @@ -419,6 +419,8 @@ private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 w SetTexture(TextureUnit.Texture1, ShadowTexture); lightShader.SetUniformTextureMaybe("shadowMap", TextureUnit.Texture1); + SetTexture(TextureUnit.Texture2, viewport.RenderTarget.Texture); + lightShader.SetUniformTextureMaybe("normalMap", TextureUnit.Texture2); GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); CheckGlError(); @@ -489,6 +491,14 @@ private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 w lightShader.SetUniformMaybe("lightIndex", component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1); + lightShader.SetUniformMaybe("globalRotation", (float)eye.Rotation.Theta + (float)rotation.Theta - + (float)_transformSystem.GetWorldRotation(mapUid).Theta); + lightShader.SetUniformMaybe("maskRotation", component.MaskAutoRotate ? (float)rot : 0); + + lightShader.SetUniformMaybe("eyeZoom", eye.Zoom / viewport.RenderScale); + lightShader.SetUniformMaybe("eyeCenter", eye.Position.Position + eye.Offset); + lightShader.SetUniformMaybe("useNormals", (_cfg.GetCVar(CVars.LightNormals) && _resourceCache.GetNormalsEnabled()) ? (int)1 : (int)0); // me when shaders do not support boolean values + var offset = new Vector2(component.Radius, component.Radius); Matrix3x2 matrix; diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs index 57b4c038eac..991efbb635c 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs @@ -84,9 +84,9 @@ public void DrawTextureScreen(Texture texture, Vector2 bl, Vector2 br, Vector2 t /// A color to multiply the texture by when shading. /// The four corners of the texture sub region in px. public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr, - Color modulate, in UIBox2? subRegion) + Color modulate, in UIBox2? subRegion, bool normal = false) { - var clydeTexture = ExtractTexture(texture, in subRegion, out var csr); + var clydeTexture = ExtractTexture(texture, in subRegion, out var csr, normal: normal); var sr = WorldTextureBoundsToUV(clydeTexture, csr); @@ -102,14 +102,17 @@ internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr) /// /// Converts a subRegion (px) into texture coords (0-1) of a given texture (cells of the textureAtlas). /// - internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr) + internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr, bool normal = false) { if (texture is AtlasTexture atlas) { + int horizontalNormalOffset = atlas.RegionWidth; texture = atlas.SourceTexture; if (subRegion.HasValue) { var offset = atlas.SubRegion.TopLeft; + if (normal) + offset += Vector2.UnitX * horizontalNormalOffset; sr = new UIBox2( subRegion.Value.TopLeft + offset, subRegion.Value.BottomRight + offset); @@ -117,6 +120,11 @@ internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegio else { sr = atlas.SubRegion; + if (normal) + { + var offset = Vector2.UnitX * horizontalNormalOffset; + sr = new UIBox2(sr.TopLeft + offset, sr.BottomRight + offset); + } } } else @@ -512,13 +520,14 @@ public override void DrawRect(in Box2Rotated rect, Color color, bool filled = tr /// The four vertices of the quad in object space (or world if the transform is identity.). /// A color to multiply the texture by when shading. /// The four corners of the texture sub region in px. + /// If the normal for this texture should be rendered. public override void DrawTextureRectRegion(Texture texture, Box2 quad, - Color? modulate = null, UIBox2? subRegion = null) + Color? modulate = null, UIBox2? subRegion = null, bool normal = false) { - var color = (modulate ?? Color.White) * Modulate; + var color = normal ? Color.White : (modulate ?? Color.White) * Modulate; _renderHandle.DrawTextureWorld(texture, quad.BottomLeft, quad.BottomRight, - quad.TopLeft, quad.TopRight, color, in subRegion); + quad.TopLeft, quad.TopRight, color, in subRegion, normal: normal); } /// @@ -531,12 +540,12 @@ public override void DrawTextureRectRegion(Texture texture, Box2 quad, /// A color to multiply the texture by when shading. /// The four corners of the texture sub region in px. public override void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, - Color? modulate = null, UIBox2? subRegion = null) + Color? modulate = null, UIBox2? subRegion = null, bool normal = false) { - var color = (modulate ?? Color.White) * Modulate; + var color = normal ? (modulate ?? Color.White) * Modulate : Color.White; _renderHandle.DrawTextureWorld(texture, quad.BottomLeft, quad.BottomRight, - quad.TopLeft, quad.TopRight, color, in subRegion); + quad.TopLeft, quad.TopRight, color, in subRegion, normal: normal); } public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture, diff --git a/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs b/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs index 148e5d11244..2db0760ae90 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs @@ -19,6 +19,8 @@ internal partial class Clyde { [ViewVariables] private ClydeShaderInstance _defaultShader = default!; + [ViewVariables] + private ClydeShaderInstance _colorShader = default!; private string _shaderLibrary = default!; @@ -159,9 +161,12 @@ private void LoadStockShaders() var defaultLoadedShader = _resourceCache .GetResource("/Shaders/Internal/default-sprite.swsl"); - _defaultShader = (ClydeShaderInstance) InstanceShader(defaultLoadedShader); + var colorShader = _resourceCache + .GetResource("/Shaders/Internal/color.swsl"); + _colorShader = (ClydeShaderInstance) InstanceShader(colorShader); + _queuedShaderInstance = _defaultShader; } diff --git a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs index 9fba50a0506..8e5f0c67acd 100644 --- a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs +++ b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs @@ -43,7 +43,7 @@ protected DrawingHandleWorld(Texture white) : base(white) /// A color to multiply the texture by when shading. /// The four corners of the texture sub region in px. public abstract void DrawTextureRectRegion(Texture texture, Box2 quad, - Color? modulate = null, UIBox2? subRegion = null); + Color? modulate = null, UIBox2? subRegion = null, bool normal = false); /// /// Draws a sprite to the world. The coordinate system is right handed. @@ -56,7 +56,7 @@ public abstract void DrawTextureRectRegion(Texture texture, Box2 quad, /// A color to multiply the texture by when shading. /// The four corners of the texture sub region in px. public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, - Color? modulate = null, UIBox2? subRegion = null); + Color? modulate = null, UIBox2? subRegion = null, bool normal = false); private Box2 GetQuad(Texture texture, Vector2 position) { diff --git a/Robust.Client/ResourceManagement/BaseResource.cs b/Robust.Client/ResourceManagement/BaseResource.cs index 4bc1b30fb8b..8e88ff34a43 100644 --- a/Robust.Client/ResourceManagement/BaseResource.cs +++ b/Robust.Client/ResourceManagement/BaseResource.cs @@ -27,6 +27,9 @@ public virtual void Dispose() /// public abstract void Load(IDependencyCollection dependencies, ResPath path); + public virtual void Load(IDependencyCollection dependencies, ResPath path, bool normalLoading) + => Load(dependencies, path); + public virtual void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default) { diff --git a/Robust.Client/ResourceManagement/IResourceCache.cs b/Robust.Client/ResourceManagement/IResourceCache.cs index dd13909bbec..9226977938e 100644 --- a/Robust.Client/ResourceManagement/IResourceCache.cs +++ b/Robust.Client/ResourceManagement/IResourceCache.cs @@ -13,6 +13,7 @@ namespace Robust.Client.ResourceManagement; /// public interface IResourceCache : IResourceManager { + bool GetNormalsEnabled(); T GetResource(string path, bool useFallback = true) where T : BaseResource, new(); diff --git a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs index de628adb1bf..c15e39a044e 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs @@ -45,6 +45,7 @@ public void PreloadTextures() private void PreloadTextures(ISawmill sawmill) { + NormalsEnabled = _configurationManager.GetCVar(CVars.LightNormals); sawmill.Debug("Preloading textures..."); var sw = Stopwatch.StartNew(); var resList = GetTypeData().Resources; @@ -132,7 +133,7 @@ private void PreloadRsis(ISawmill sawmill) { try { - RSIResource.LoadPreTexture(_manager, data); + RSIResource.LoadPreTexture(_manager, data, _configurationManager.GetCVar(CVars.LightNormals)); } catch (Exception e) { @@ -221,6 +222,7 @@ private void PreloadRsis(ISawmill sawmill) var height = offset.Y + deltaY; var croppedSheet = new Image(maxSize, height); sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default); + FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet); void FinalizeMetaAtlas(int toIndex, Image sheet) diff --git a/Robust.Client/ResourceManagement/ResourceCache.cs b/Robust.Client/ResourceManagement/ResourceCache.cs index cdcea4fccfe..cb46f69fe88 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Robust.Client.Audio; +using Robust.Shared; using Robust.Shared.ContentPack; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -19,6 +20,12 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt { private readonly Dictionary _cachedResources = new(); private readonly Dictionary _fallbacks = new(); + public bool NormalsEnabled { get; private set; } + + public bool GetNormalsEnabled() + { + return NormalsEnabled; + } public T GetResource(string path, bool useFallback = true) where T : BaseResource, new() { @@ -37,7 +44,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt try { var dependencies = IoCManager.Instance!; - resource.Load(dependencies, path); + resource.Load(dependencies, path, _configurationManager.GetCVar(CVars.LightNormals)); cache.Resources[path] = resource; return resource; } @@ -82,7 +89,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt try { var dependencies = IoCManager.Instance!; - _resource.Load(dependencies, path); + _resource.Load(dependencies, path, _configurationManager.GetCVar(CVars.LightNormals)); resource = _resource; cache.Resources[path] = resource; return true; diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs index f7b9bda7324..c808b7907ff 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs @@ -3,7 +3,6 @@ using System.Linq; using Robust.Client.Graphics; using Robust.Client.Utility; -using Robust.Shared.ContentPack; using Robust.Shared.Graphics; using Robust.Shared.Graphics.RSI; using Robust.Shared.IoC; @@ -12,6 +11,8 @@ using Robust.Shared.Utility; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using TerraFX.Interop.Windows; +using IResourceManager = Robust.Shared.ContentPack.IResourceManager; namespace Robust.Client.ResourceManagement { @@ -36,10 +37,13 @@ public sealed class RSIResource : BaseResource public const uint MAXIMUM_RSI_VERSION = RsiLoading.MAXIMUM_RSI_VERSION; public override void Load(IDependencyCollection dependencies, ResPath path) + => Load(dependencies, path, false); + + public override void Load(IDependencyCollection dependencies, ResPath path, bool normalLoading) { var loadStepData = new LoadStepData {Path = path}; var manager = dependencies.Resolve(); - LoadPreTexture(manager, loadStepData); + LoadPreTexture(manager, loadStepData, normalLoading); LoadTexture(dependencies.Resolve(), loadStepData); LoadPostTexture(loadStepData); LoadFinish(dependencies.Resolve(), loadStepData); @@ -55,7 +59,106 @@ internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData) loadStepData.LoadParameters); } - internal static void LoadPreTexture(IResourceManager manager, LoadStepData data) + internal static void ConvertBumpToNormal(Image bumpmap, Vector2i blocksize, out Image normal, float factor = 2f) + { + int Width = bumpmap.Width; + int Height = bumpmap.Height; + normal = new Image(Width, Height); + int x, y; + for (x = 0; x < Width; x++) + for (y = 0; y < Height; y++) + { + int XFactor = x / blocksize.X; + int YFactor = y / blocksize.Y; + + int XL = x - 1; + int YL = y - 1; + int XR = x + 1; + int YR = y + 1; + + float Left = 0f, Right = 0f, Up = 0f, Down = 0f; + + if (XL - XFactor * blocksize.X >= 0) + Left = bumpmap[XL, y].R / 255f; + if (XR - XFactor * blocksize.X < blocksize.X) + Right = bumpmap[XR, y].R / 255f; + if (YL - YFactor * blocksize.Y >= 0) + Up = bumpmap[x, YL].R / 255f; + if (YR - YFactor * blocksize.Y < blocksize.Y) + Down = bumpmap[x, YR].R / 255f; + + // very good math + // cross product of a certain two vectors + var Cross = new Vector3((Left - Right) * factor, (Down - Up) * factor, 1f); + Cross.Normalize(); + + var NewColor = new Rgba32((Cross.X + 1) * 0.5f, (Cross.Y + 1) * 0.5f, Cross.Z, bumpmap[x, y].A); + normal[x, y] = NewColor; + } + } + + internal static void CreatePlaceholderBump(Image original, Vector2i blocksize, out Image bumpmap) + { + int x, y; + int Width = original.Width; + int Height = original.Height; + bumpmap = new Image(Width, Height); + for (x = 0; x < Width; x++) + for (y = 0; y < Height; y++) + { + if (original[x, y].A == 0) + { + bumpmap[x, y] = new Rgba32(0f, 0f, 0f); + continue; + } + + int XFactor = x / blocksize.X; + int YFactor = y / blocksize.Y; + + int XL = x - 1; + int YL = y - 1; + int XR = x + 1; + int YR = y + 1; + + byte Left = 0, Right = 0, Up = 0, Down = 0; + + if (XL - XFactor * blocksize.X >= 0) + Left = bumpmap[XL, y].A; + if (XR - XFactor * blocksize.X < blocksize.X) + Right = bumpmap[XR, y].A; + if (YL - YFactor * blocksize.Y >= 0) + Up = bumpmap[x, YL].A; + if (YR - YFactor * blocksize.Y < blocksize.Y) + Down = bumpmap[x, YR].A; + + if (Left == 0 || Right == 0 || Up == 0 || Down == 0) + { + bumpmap[x, y] = new Rgba32(0.5f, 0.5f, 0.5f, original[x, y].A); + } + else + { + bumpmap[x, y] = new Rgba32(1f, 1f, 1f, original[x, y].A); + } + } + } + + internal static void CreatePlaceholderNormal(Image original, Vector2i blocksize, out Image normalmap, bool? flat = false) + { + if (flat ?? false) + { + normalmap = new Image(original.Width, original.Height); + int x, y; + for (x = 0; x < original.Width; x++) + for (y = 0; y < original.Height; y++) + normalmap[x, y] = new Rgba32(0.5f, 0.5f, 1f); + return; + } + CreatePlaceholderBump(original, blocksize, out var bumpmap); + ConvertBumpToNormal(bumpmap, blocksize, out normalmap); + bumpmap.Dispose(); + } + + internal static void LoadPreTexture(IResourceManager manager, LoadStepData data, bool normalLoading = false) { var manifestPath = data.Path / "meta.json"; RsiLoading.RsiMetadata metadata; @@ -92,14 +195,50 @@ internal static void LoadPreTexture(IResourceManager manager, LoadStepData data) var stateObject = metadata.States[index]; // Load image from disk. var texPath = data.Path / (stateObject.StateId + ".png"); + var normalPath = data.Path / (stateObject.NormalId + ".png"); + var bumpPath = data.Path / (stateObject.BumpId + ".png"); using (var stream = manager.ContentFileRead(texPath)) { - reg.Src = Image.Load(stream); + var texture = Image.Load(stream); + Image normalImage; + if (!normalLoading) normalImage = texture; + else if (stateObject.NormalId is {}) + { + using (var normalStream = manager.ContentFileRead(normalPath)) + { + normalImage = Image.Load(normalStream); + } + } + else + { + if (stateObject.BumpId is {}) + { + using (var bumpStream = manager.ContentFileRead(bumpPath)) + { + var bump = Image.Load(bumpStream); + ConvertBumpToNormal(bump, stateObject.Size, out normalImage); + bump.Dispose(); + } + } + else + { + CreatePlaceholderNormal(texture, stateObject.Size, out normalImage, metadata.FlatNormal ?? stateObject.FlatNormal ?? false); + } + } + if (normalLoading) + for (int nX = 0; nX < texture.Width; nX++) + for (int nY = 0; nY < texture.Height; nY++) + { + var T = normalImage[nX, nY]; + T.A = texture[nX, nY].A; + normalImage[nX, nY] = T; + } + + reg.Src = (texture, normalImage); } - - if (reg.Src.Width % frameSize.X != 0 || reg.Src.Height % frameSize.Y != 0) + if (reg.Src.Item1.Width % frameSize.X != 0 || reg.Src.Item1.Height % frameSize.Y != 0) { - var regDims = $"{reg.Src.Width}x{reg.Src.Height}"; + var regDims = $"{reg.Src.Item1.Width}x{reg.Src.Item1.Height}"; var iconDims = $"{frameSize.X}x{frameSize.Y}"; throw new RSILoadException($"State '{stateObject.StateId}' image size ({regDims}) is not a multiple of the icon size ({iconDims})."); } @@ -144,7 +283,8 @@ internal static void LoadPreTexture(IResourceManager manager, LoadStepData data) var dimensionX = (int) MathF.Ceiling(MathF.Sqrt(totalFrameCount)); var dimensionY = (int) MathF.Ceiling((float) totalFrameCount / dimensionX); - var sheet = new Image(dimensionX * frameSize.X, dimensionY * frameSize.Y); + var sheetHalfWidth = dimensionX * frameSize.X; + var sheet = new Image(sheetHalfWidth * (normalLoading ? 2 : 1), dimensionY * frameSize.Y); var sheetIndex = 0; for (var index = 0; index < toAtlas.Length; index++) @@ -153,7 +293,7 @@ internal static void LoadPreTexture(IResourceManager manager, LoadStepData data) // Blit all the frames over. for (var i = 0; i < reg.TotalFrameCount; i++) { - var srcWidth = (reg.Src.Width / frameSize.X); + var srcWidth = (reg.Src.Item1.Width / frameSize.X); var srcColumn = i % srcWidth; var srcRow = i / srcWidth; var srcPos = (srcColumn * frameSize.X, srcRow * frameSize.Y); @@ -161,10 +301,13 @@ internal static void LoadPreTexture(IResourceManager manager, LoadStepData data) var sheetColumn = (sheetIndex + i) % dimensionX; var sheetRow = (sheetIndex + i) / dimensionX; var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); + var sheetNPos = (sheetColumn * frameSize.X + sheetHalfWidth, sheetRow * frameSize.Y); var srcBox = UIBox2i.FromDimensions(srcPos, frameSize); - reg.Src.Blit(srcBox, sheet, sheetPos); + reg.Src.Item1.Blit(srcBox, sheet, sheetPos); + if (normalLoading) + reg.Src.Item2.Blit(srcBox, sheet, sheetNPos); } sheetIndex += reg.TotalFrameCount; @@ -173,11 +316,14 @@ internal static void LoadPreTexture(IResourceManager manager, LoadStepData data) for (var i = 0; i < toAtlas.Length; i++) { ref var reg = ref toAtlas[i]; - reg.Src.Dispose(); + reg.Src.Item1.Dispose(); + if (normalLoading) + reg.Src.Item2.Dispose(); } data.Rsi = rsi; data.AtlasSheet = sheet; + data.SheetNormalOffset = sheetHalfWidth; data.AtlasList = toAtlas; data.FrameSize = frameSize; data.DimX = dimensionX; @@ -212,7 +358,7 @@ internal static void LoadPostTexture(LoadStepData data) var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); dirOffsets[j] = sheetPos; - dirOutput[j] = new AtlasTexture(texture, UIBox2.FromDimensions(data.AtlasOffset + sheetPos, frameSize)); + dirOutput[j] = new AtlasTexture(texture, UIBox2.FromDimensions(data.AtlasOffset + sheetPos, frameSize), data.SheetNormalOffset); } } @@ -379,6 +525,7 @@ internal sealed class LoadStepData public bool Bad; public ResPath Path = default!; public Image AtlasSheet = default!; + public int SheetNormalOffset = default!; public int DimX; public StateReg[] AtlasList = default!; public Vector2i FrameSize; @@ -392,7 +539,7 @@ internal sealed class LoadStepData internal struct StateReg { - public Image Src; + public (Image, Image) Src; public Texture[][] Output; public int[][] Indices; public Vector2i[][] Offsets; diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 0a0f0d4fc50..e1e58b409e6 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -879,6 +879,12 @@ protected CVars() public static readonly CVarDef LightBlurFactor = CVarDef.Create("light.blur_factor", 0.001f, CVar.CLIENTONLY | CVar.ARCHIVE); + /// + /// If we render and load normals. + /// + public static readonly CVarDef LightNormals = + CVarDef.Create("light.normals", false, CVar.CLIENTONLY | CVar.ARCHIVE); + /* * Lookup */ diff --git a/Robust.Shared/Resources/RsiLoading.cs b/Robust.Shared/Resources/RsiLoading.cs index 3854f80ce14..c2e326827be 100644 --- a/Robust.Shared/Resources/RsiLoading.cs +++ b/Robust.Shared/Resources/RsiLoading.cs @@ -41,6 +41,9 @@ internal static RsiMetadata LoadRsiMetadata(Stream manifestFile) { var stateObject = manifestJson.States[stateI]; var stateName = stateObject.Name; + var normalName = stateObject.Normal; + var bumpName = stateObject.Bump; + var flatNormal = stateObject.FlatNormal; int dirValue; if (stateObject.Directions is { } dirVal) @@ -91,7 +94,7 @@ internal static RsiMetadata LoadRsiMetadata(Stream manifestFile) } } - states[stateI] = new StateMetadata(stateName, dirValue, delays); + states[stateI] = new StateMetadata(stateName, normalName, bumpName, flatNormal, size, dirValue, delays); } var textureParams = TextureLoadParameters.Default; @@ -104,7 +107,7 @@ internal static RsiMetadata LoadRsiMetadata(Stream manifestFile) }; } - return new RsiMetadata(size, states, textureParams, manifestJson.MetaAtlas); + return new RsiMetadata(size, states, manifestJson.FlatNormal, textureParams, manifestJson.MetaAtlas); } public static void Warmup() @@ -114,10 +117,11 @@ public static void Warmup() JsonSerializer.Deserialize(warmupJson, SerializerOptions); } - internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, TextureLoadParameters loadParameters, bool metaAtlas) + internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, bool? flatNormal, TextureLoadParameters loadParameters, bool metaAtlas) { public readonly Vector2i Size = size; public readonly StateMetadata[] States = states; + public readonly bool? FlatNormal = flatNormal; public readonly TextureLoadParameters LoadParameters = loadParameters; public readonly bool MetaAtlas = metaAtlas; } @@ -125,12 +129,20 @@ internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, Texture internal sealed class StateMetadata { public readonly string StateId; + public readonly string? NormalId; + public readonly string? BumpId; + public readonly bool? FlatNormal; + public readonly Vector2i Size; public readonly int DirCount; public readonly float[][] Delays; - public StateMetadata(string stateId, int dirCount, float[][] delays) + public StateMetadata(string stateId, string? normalId, string? bumpId, bool? flatNormal, Vector2i size, int dirCount, float[][] delays) { StateId = stateId; + NormalId = normalId; + BumpId = bumpId; + FlatNormal = flatNormal; + Size = size; DirCount = dirCount; Delays = delays; @@ -144,11 +156,12 @@ public StateMetadata(string stateId, int dirCount, float[][] delays) private sealed record RsiJsonMetadata( Vector2i Size, StateJsonMetadata[] States, + bool? FlatNormal, RsiJsonLoad? Load, bool MetaAtlas = true); [UsedImplicitly] - private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays); + private sealed record StateJsonMetadata(string Name, string? Normal, string? Bump, bool? FlatNormal, int? Directions, float[][]? Delays); [UsedImplicitly] private sealed record RsiJsonLoad(bool Srgb = true);