fix(presentation): keep outlined group numbers readable through scenery

This commit is contained in:
NINI
2026-10-05 23:10:01 +09:00
parent 2970c2dcfe
commit 879cea84c4
3 changed files with 102 additions and 2 deletions
@@ -73,6 +73,7 @@ namespace MagnetismHeroes.Editor
numberMaterial.SetFloat("_OutlineWidth", .12f); numberMaterial.SetColor("_OutlineColor", Color.white); numberMaterial.SetFloat("_OutlineWidth", .12f); numberMaterial.SetColor("_OutlineColor", Color.white);
AssetDatabase.CreateAsset(numberMaterial, Root + "Materials/NumberOutline.mat"); 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; number.fontSharedMaterial = numberMaterial;
PrefabUtility.SaveAsPrefabAsset(badge, Root + "Prefabs/GroupBadge.prefab"); PrefabUtility.SaveAsPrefabAsset(badge, Root + "Prefabs/GroupBadge.prefab");
} }
@@ -55,6 +55,104 @@ namespace MagnetismHeroes.Tests
yield return null; yield return null;
} }
[UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); 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<Renderer>(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<top;y++) for (int x=left;x<right;x++)
{
int i=y*1280+x;
// Compare the same unobscured dark glyph/body pixels; a surviving edge cannot establish legibility.
if (Different(referenceOn[i],referenceOff[i]) && referenceOn[i].r+referenceOn[i].g+referenceOn[i].b<480)
{ referencePixels++; if (Different(on[i],off[i])) visiblePixels++; }
}
Assert.That(referencePixels, Is.GreaterThan(0),"The identical unobscured renderer must contribute a reference mask.");
if (evidenceName!=null) File.WriteAllText(Path.Combine(_evidence,evidenceName+"-pixel-roi.txt"),$"renderer={renderer.name};rect={left},{bottom},{right-left},{top-bottom};reference={referencePixels};visible={visiblePixels}");
return (float)visiblePixels/referencePixels;
}
finally { renderer.enabled=enabled; camera.targetTexture=priorTarget; RenderTexture.active=priorActive; RenderTexture.ReleaseTemporary(target); Object.Destroy(image); }
}
[UnityTest] public IEnumerator SplitFiveNumbersRemainRenderedBehindTheActualGardenFrame()
{
var camera=new CameraState {target=new Position3(0,.6f,0),distance=11,rotation=new Rotation4(-.022557568f,.96225017f,.08418597f,.2578342f)};
_store.Saved.latestFreePlay=new FreePlayState {sourceStageId=9,camera=camera,
balls=new[] {
new BallState {id="s09-b01",position=new Position3(-1.0351909f,.8f,.60371524f)},
new BallState {id="s09-b02",position=new Position3(-.03519098f,.8f,.60371524f)},
new BallState {id="s09-b03",position=new Position3(-3.9800465f,.8f,1.649168f)},
new BallState {id="s09-b04",position=new Position3(.964809f,.8f,.60371524f)},
new BallState {id="s09-b05",position=new Position3(-2.9800465f,.8f,1.649168f)}},
groups=new[] {new GroupState {id="s09-g01",ballIds=new[] {"s09-b01","s09-b02","s09-b04"}},new GroupState {id="s09-g01-split",ballIds=new[] {"s09-b03","s09-b05"}}}};
_app.Initialize(_store); _app.ContinueFreePlay(); yield return null; _app.Voice.Clear();
var board=JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()));
Capture("r11-split-five-frame");
foreach (var label in _app.GetComponentsInChildren<TMPro.TextMeshPro>())
{
float fraction=RendererVisibleFraction(label.GetComponent<Renderer>(),"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<Renderer>().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<TMPro.TextMeshPro>();
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<Renderer>()), Is.GreaterThanOrEqualTo(.95f),$"count{count}/yaw{yaw}/label{label.text}");
Assert.That(RendererVisibleFraction(_app.Presenter.GetComponentInChildren<SpriteRenderer>()), Is.Zero,"Faces retain physical depth occlusion.");
Assert.That(RendererVisibleFraction(_app.Presenter.Balls.Values.First().GetComponentInChildren<MeshRenderer>()), 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() [UnityTest] public IEnumerator CountingRefreshPreservesViewsAndClearsMarkersInSameFreeBoard()
{ {
_app.StartStage(20); yield return null; _app.StartStage(20); yield return null;
@@ -8,10 +8,11 @@ Material:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_Name: NumberOutline m_Name: NumberOutline
m_Shader: {fileID: 4800000, guid: fe393ace9b354375a9cb14cdbbc28be4, type: 3} m_Shader: {fileID: 4800000, guid: a02a7d8c237544f1962732b55a9aebf1, type: 3}
m_Parent: {fileID: 0} m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0 m_ModifiedSerializedProperties: 0
m_ValidKeywords: [] m_ValidKeywords:
- OUTLINE_ON
m_InvalidKeywords: [] m_InvalidKeywords: []
m_LightmapFlags: 4 m_LightmapFlags: 4
m_EnableInstancingVariants: 0 m_EnableInstancingVariants: 0