using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.Rendering.Universal; using UnityEngine.TestTools; using Object = UnityEngine.Object; namespace BumpCombat.Tests { public sealed class ComparisonSpriteFlipRenderPlayModeTests { private const int FlipRenderTestLayer = 31; [UnityTest] public IEnumerator SpriteRendererFlipXMirrorsAsymmetricRenderedSprite() { const int width = 80; const int height = 40; const int textureWidth = 8; const int textureHeight = 4; GameObject cameraObject = null; GameObject spriteObject = null; Texture2D source = null; Texture2D capture = null; RenderTexture target = null; Material material = null; Sprite sprite = null; try { Shader shader = Shader.Find("BumpCombat/Comparison Sprite Depth"); Assert.That(shader, Is.Not.Null); material = new Material(shader) { enableInstancing = true }; source = new Texture2D(textureWidth, textureHeight, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp, }; Color32[] sourcePixels = new Color32[textureWidth * textureHeight]; for (int i = 0; i < sourcePixels.Length; i++) sourcePixels[i] = new Color32(0, 0, 0, 0); sourcePixels[0] = new Color32(255, 32, 32, 255); sourcePixels[1] = new Color32(255, 96, 32, 255); sourcePixels[textureWidth + 2] = new Color32(32, 255, 32, 255); sourcePixels[textureWidth * 2 + 5] = new Color32(32, 64, 255, 255); sourcePixels[textureWidth * 3 + 7] = new Color32(255, 255, 32, 255); source.SetPixels32(sourcePixels); source.Apply(false, false); sprite = Sprite.Create(source, new Rect(0, 0, textureWidth, textureHeight), new Vector2(.5f, .5f), 2f); target = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32) { filterMode = FilterMode.Point, antiAliasing = 1, }; target.Create(); cameraObject = new GameObject("Comparison flip render test camera"); Camera camera = cameraObject.AddComponent(); cameraObject.AddComponent().SetRenderer(1); camera.orthographic = true; camera.orthographicSize = 1f; camera.aspect = (float)width / height; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.black; camera.targetTexture = target; camera.cullingMask = 1 << FlipRenderTestLayer; camera.transform.position = new Vector3(0f, 0f, -10f); spriteObject = new GameObject("Asymmetric flip test sprite"); spriteObject.layer = FlipRenderTestLayer; SpriteRenderer renderer = spriteObject.AddComponent(); renderer.sprite = sprite; renderer.sharedMaterial = material; renderer.flipX = false; capture = new Texture2D(width, height, TextureFormat.RGBA32, false); yield return null; Color[] forward = Capture(camera, target, capture); renderer.flipX = true; yield return null; Color[] flipped = Capture(camera, target, capture); int mismatchedPixels = 0; int forwardBrightPixels = 0; int flippedBrightPixels = 0; int flipChangedPixels = 0; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { Color expected = forward[y * width + (width - 1 - x)]; Color actual = flipped[y * width + x]; Color sameCoordinateForward = forward[y * width + x]; if (IsBright(forward[y * width + x])) forwardBrightPixels++; if (IsBright(actual)) flippedBrightPixels++; if (ColorDistance(sameCoordinateForward, actual) > .02f) flipChangedPixels++; if (Mathf.Abs(expected.r - actual.r) + Mathf.Abs(expected.g - actual.g) + Mathf.Abs(expected.b - actual.b) + Mathf.Abs(expected.a - actual.a) > .02f) mismatchedPixels++; } Assert.That(forwardBrightPixels, Is.GreaterThan(0), "The forward image must contain rendered sprite pixels."); Assert.That(flippedBrightPixels, Is.GreaterThan(0), "The flipped image must contain rendered sprite pixels."); Assert.That(flipChangedPixels, Is.GreaterThan(0), "Flipping this asymmetric sprite must change actual pixels."); Assert.That(mismatchedPixels, Is.Zero, "SpriteRenderer.flipX must mirror the rendered pixels, not merely expose a flip flag."); } finally { if (spriteObject != null) Object.Destroy(spriteObject); if (cameraObject != null) Object.Destroy(cameraObject); if (sprite != null) Object.Destroy(sprite); if (source != null) Object.Destroy(source); if (capture != null) Object.Destroy(capture); if (target != null) { target.Release(); Object.Destroy(target); } if (material != null) Object.Destroy(material); } } [UnityTest] public IEnumerator SharedSpriteMaterialStillRendersMeshAndLineGeometry() { const int width = 80; const int height = 80; GameObject cameraObject = null; GameObject meshObject = null; GameObject lineObject = null; Texture2D capture = null; RenderTexture target = null; Material material = null; Mesh mesh = null; try { Shader shader = Shader.Find("BumpCombat/Comparison Sprite Depth"); Assert.That(shader, Is.Not.Null); material = new Material(shader); target = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32); target.Create(); cameraObject = new GameObject("Shared material render test camera"); Camera camera = cameraObject.AddComponent(); cameraObject.AddComponent().SetRenderer(1); camera.orthographic = true; camera.orthographicSize = 2f; camera.aspect = 1f; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.black; camera.targetTexture = target; camera.cullingMask = 1 << FlipRenderTestLayer; camera.transform.position = new Vector3(0f, 0f, -10f); meshObject = new GameObject("Ground-like MeshRenderer"); meshObject.layer = FlipRenderTestLayer; mesh = new Mesh { vertices = new[] { new Vector3(-.6f, .1f, 0f), new Vector3(.6f, .1f, 0f), new Vector3(-.6f, 1.3f, 0f), new Vector3(.6f, 1.3f, 0f), }, uv = new[] { Vector2.zero, Vector2.right, Vector2.up, Vector2.one }, colors = new[] { Color.white, Color.white, Color.white, Color.white }, triangles = new[] { 0, 2, 1, 2, 3, 1 }, }; mesh.RecalculateBounds(); meshObject.AddComponent().sharedMesh = mesh; meshObject.AddComponent().sharedMaterial = material; lineObject = new GameObject("Ground-like LineRenderer"); lineObject.layer = FlipRenderTestLayer; LineRenderer line = lineObject.AddComponent(); line.useWorldSpace = true; line.positionCount = 2; line.startWidth = line.endWidth = .12f; line.startColor = line.endColor = Color.red; line.sharedMaterial = material; line.SetPosition(0, new Vector3(-1.2f, -1f, 0f)); line.SetPosition(1, new Vector3(1.2f, -1f, 0f)); capture = new Texture2D(width, height, TextureFormat.RGBA32, false); yield return null; Color[] pixels = Capture(camera, target, capture); int meshPixels = 0; int linePixels = 0; foreach (Color pixel in pixels) { if (pixel.r > .8f && pixel.g > .8f && pixel.b > .8f) meshPixels++; if (pixel.r > .8f && pixel.g < .3f && pixel.b < .3f) linePixels++; } Assert.That(meshPixels, Is.GreaterThan(10), "The shared shader must keep opaque floor/mesh geometry visible with default SpriteProps."); Assert.That(linePixels, Is.GreaterThan(3), "LineRenderer geometry using the shared comparison material must remain visible."); } finally { if (meshObject != null) Object.Destroy(meshObject); if (lineObject != null) Object.Destroy(lineObject); if (cameraObject != null) Object.Destroy(cameraObject); if (capture != null) Object.Destroy(capture); if (mesh != null) Object.Destroy(mesh); if (target != null) { target.Release(); Object.Destroy(target); } if (material != null) Object.Destroy(material); } } private static Color[] Capture(Camera camera, RenderTexture target, Texture2D capture) { RenderTexture previous = RenderTexture.active; camera.Render(); RenderTexture.active = target; capture.ReadPixels(new Rect(0f, 0f, target.width, target.height), 0, 0); capture.Apply(false, false); Color[] pixels = capture.GetPixels(); RenderTexture.active = previous; return pixels; } private static bool IsBright(Color color) => color.r > .25f || color.g > .25f || color.b > .25f; private static float ColorDistance(Color a, Color b) => Mathf.Abs(a.r - b.r) + Mathf.Abs(a.g - b.g) + Mathf.Abs(a.b - b.b) + Mathf.Abs(a.a - b.a); } }