From 879cea84c413a986b7fe2039b7930c613660cc63 Mon Sep 17 00:00:00 2001 From: NINI Date: Mon, 5 Oct 2026 23:10:01 +0900 Subject: [PATCH] fix(presentation): keep outlined group numbers readable through scenery --- .../Editor/PresentationAssets.cs | 1 + .../Tests/PlayMode/ContentPlayTests.cs | 98 +++++++++++++++++++ .../Presentation/Materials/NumberOutline.mat | 5 +- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/Assets/MagnetismHeroes/Editor/PresentationAssets.cs b/Assets/MagnetismHeroes/Editor/PresentationAssets.cs index a7f79bc..ac8aded 100644 --- a/Assets/MagnetismHeroes/Editor/PresentationAssets.cs +++ b/Assets/MagnetismHeroes/Editor/PresentationAssets.cs @@ -73,6 +73,7 @@ namespace MagnetismHeroes.Editor numberMaterial.SetFloat("_OutlineWidth", .12f); numberMaterial.SetColor("_OutlineColor", Color.white); AssetDatabase.CreateAsset(numberMaterial, Root + "Materials/NumberOutline.mat"); } + numberMaterial.shader = Shader.Find("TextMeshPro/Mobile/Distance Field Overlay"); numberMaterial.EnableKeyword("OUTLINE_ON"); EditorUtility.SetDirty(numberMaterial); number.fontSharedMaterial = numberMaterial; PrefabUtility.SaveAsPrefabAsset(badge, Root + "Prefabs/GroupBadge.prefab"); } diff --git a/Assets/MagnetismHeroes/Tests/PlayMode/ContentPlayTests.cs b/Assets/MagnetismHeroes/Tests/PlayMode/ContentPlayTests.cs index 05fed1d..cee329c 100644 --- a/Assets/MagnetismHeroes/Tests/PlayMode/ContentPlayTests.cs +++ b/Assets/MagnetismHeroes/Tests/PlayMode/ContentPlayTests.cs @@ -55,6 +55,104 @@ namespace MagnetismHeroes.Tests yield return null; } [UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; } + private float RendererVisibleFraction(Renderer renderer, string evidenceName = null) + { + var camera=_app.Presenter.Camera; var priorTarget=camera.targetTexture; var priorActive=RenderTexture.active; + var target=RenderTexture.GetTemporary(1280,720,24); var image=new Texture2D(1280,720,TextureFormat.RGB24,false); + bool enabled=renderer.enabled; + try + { + camera.targetTexture=target; + Color32[] Read(bool visible, string suffix) + { + renderer.enabled=visible; camera.Render(); RenderTexture.active=target; + image.ReadPixels(new Rect(0,0,1280,720),0,0); image.Apply(); + if (evidenceName!=null) File.WriteAllBytes(Path.Combine(_evidence,evidenceName+suffix+".png"),image.EncodeToPNG()); + return image.GetPixels32(); + } + var on=Read(true,"-on"); + var bounds=renderer.bounds; var min=Vector2.one; var max=Vector2.zero; + for (int x=0;x<2;x++) for (int y=0;y<2;y++) for (int z=0;z<2;z++) + { + var point=camera.WorldToViewportPoint(new Vector3(x==0 ? bounds.min.x : bounds.max.x,y==0 ? bounds.min.y : bounds.max.y,z==0 ? bounds.min.z : bounds.max.z)); + min=Vector2.Min(min,point); max=Vector2.Max(max,point); + } + int left=Mathf.Clamp(Mathf.FloorToInt(min.x*1280)-1,0,1279), right=Mathf.Clamp(Mathf.CeilToInt(max.x*1280)+1,0,1280); + int bottom=Mathf.Clamp(Mathf.FloorToInt(min.y*720)-1,0,719), top=Mathf.Clamp(Mathf.CeilToInt(max.y*720)+1,0,720); + var off=Read(false,"-off"); + var others=Object.FindObjectsByType(FindObjectsSortMode.None).Where(other=>other!=renderer && other.enabled).ToArray(); + Color32[] referenceOn,referenceOff; + try { foreach (var other in others) other.enabled=false; referenceOn=Read(true,"-reference-on"); referenceOff=Read(false,"-reference-off"); } + finally { foreach (var other in others) other.enabled=true; } + int referencePixels=0,visiblePixels=0; + bool Different(Color32 first,Color32 second) => System.Math.Abs(first.r-second.r)>5 || System.Math.Abs(first.g-second.g)>5 || System.Math.Abs(first.b-second.b)>5; + for (int y=bottom;y()) + { + float fraction=RendererVisibleFraction(label.GetComponent(),"r11-split-number-"+label.text); + Assert.That(fraction, Is.GreaterThanOrEqualTo(.95f),$"Number {label.text} must contribute visible glyph pixels through the unchanged Garden frame."); + } + Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot())), Is.EqualTo(board)); + } + [UnityTest] public IEnumerator OneThroughTenLabelsKeepTheirMembershipAndPixelsThroughOpaqueDecorationAfterOrbit() + { + _app.StartStage(20); yield return null; + var blocker=GameObject.CreatePrimitive(PrimitiveType.Quad); + var material=new Material(Shader.Find("Universal Render Pipeline/Unlit")); material.color=Color.white; + blocker.GetComponent().sharedMaterial=material; + try + { + for (int scenario=1;scenario<=11;scenario++) + { + int count=System.Math.Min(scenario,10); + var balls=Enumerable.Range(0,count).Select(i=>new BallState {id="arbitrary-"+(count-i),position=new Position3((i%4-1.5f)*1.1f,.8f+i/4*.7f,i%3*.4f)}).ToArray(); + var groups=scenario==11 ? new[] {new GroupState {id="arbitrary-a",ballIds=balls.Take(5).Select(ball=>ball.id).ToArray()},new GroupState {id="arbitrary-b",ballIds=balls.Skip(5).Select(ball=>ball.id).ToArray()}} + : new[] {new GroupState {id="arbitrary",ballIds=balls.Select(ball=>ball.id).ToArray()}}; + var board=BoardRules.CreateFreePlay(new FreePlayState {sourceStageId=20,balls=balls,groups=groups}); + _app.Presenter.Render(board); var savedCamera=_app.Orbit.Snapshot(); var saved=JsonUtility.ToJson(board.ToFreePlayState(savedCamera)); + foreach (int yaw in new[] {0,95,210}) + { + var rotation=Quaternion.Euler(-15,yaw,0); + _app.Orbit.Apply(new CameraState {target=new Position3(0,.6f,0),distance=11,rotation=new Rotation4(rotation.x,rotation.y,rotation.z,rotation.w)}); + var camera=_app.Presenter.Camera; blocker.transform.SetPositionAndRotation(camera.transform.position+camera.transform.forward*2,camera.transform.rotation); blocker.transform.localScale=Vector3.one*20; + _app.Presenter.SendMessage("LateUpdate"); + var labels=_app.Presenter.GetComponentsInChildren(); + Assert.That(labels.Select(label=>label.text), Is.EqualTo(board.Groups.Select(group=>group.ballIds.Length.ToString()))); + foreach (var label in labels) Assert.That(RendererVisibleFraction(label.GetComponent()), Is.GreaterThanOrEqualTo(.95f),$"count{count}/yaw{yaw}/label{label.text}"); + Assert.That(RendererVisibleFraction(_app.Presenter.GetComponentInChildren()), Is.Zero,"Faces retain physical depth occlusion."); + Assert.That(RendererVisibleFraction(_app.Presenter.Balls.Values.First().GetComponentInChildren()), Is.Zero,"Balls retain physical depth occlusion."); + } + Assert.That(JsonUtility.ToJson(board.ToFreePlayState(savedCamera)), Is.EqualTo(saved)); + } + } + finally { Object.Destroy(blocker); Object.Destroy(material); } + } [UnityTest] public IEnumerator CountingRefreshPreservesViewsAndClearsMarkersInSameFreeBoard() { _app.StartStage(20); yield return null; diff --git a/Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/NumberOutline.mat b/Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/NumberOutline.mat index 4d868a0..17184e8 100644 --- a/Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/NumberOutline.mat +++ b/Assets/MagnetismHeroes/UnityRuntime/Presentation/Materials/NumberOutline.mat @@ -8,10 +8,11 @@ Material: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: NumberOutline - m_Shader: {fileID: 4800000, guid: fe393ace9b354375a9cb14cdbbc28be4, type: 3} + m_Shader: {fileID: 4800000, guid: a02a7d8c237544f1962732b55a9aebf1, type: 3} m_Parent: {fileID: 0} m_ModifiedSerializedProperties: 0 - m_ValidKeywords: [] + m_ValidKeywords: + - OUTLINE_ON m_InvalidKeywords: [] m_LightmapFlags: 4 m_EnableInstancingVariants: 0