mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 22:35:42 +00:00
Skip same-faction borders during faction highlighting (#6200)
When hovering a faction in the Factions table or viewing a faction notification, borders between provinces of the same faction are now suppressed so the faction's territory appears as one contiguous highlighted region. Adds a cross-province ID map (computed once at startup alongside the distance map) that stores which province is on the other side of each pixel's nearest border, plus a 256x1 highlight group lookup texture that the shader checks to skip highlight borders between same-group provinces. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,8 @@ namespace eagle {
|
||||
SetDefaultProvinceColor(commandProvince);
|
||||
}
|
||||
|
||||
provinceBorderManager.SetHighlightGroup(OverrideTargetedProvinces);
|
||||
|
||||
foreach (var pid in OverrideTargetedProvinces) {
|
||||
FlashProvince(pid, _targetColor, TargetedBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
@@ -243,6 +245,7 @@ namespace eagle {
|
||||
TargetedBorderCycle);
|
||||
}
|
||||
} else {
|
||||
provinceBorderManager.ClearHighlightGroup();
|
||||
if (SelectedProvinceId is ProvinceId sPid) {
|
||||
FlashProvince(sPid, _selectionColor, SelectedBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
|
||||
+65
-14
@@ -6,19 +6,24 @@ namespace eagle {
|
||||
/// Each pixel stores the Euclidean distance (Chamfer approximation, in texels) to
|
||||
/// the nearest province boundary, clamped to 255. The shader uses this to render
|
||||
/// variable-width borders with a single texture sample per fragment.
|
||||
///
|
||||
/// Also generates a cross-province ID map: for each pixel, the province ID on the
|
||||
/// other side of the nearest border. The shader uses this to suppress highlight
|
||||
/// borders between provinces in the same highlight group.
|
||||
/// </summary>
|
||||
public class ProvinceBorderDistanceGenerator : MonoBehaviour {
|
||||
public ProvinceIDLoader provinceIDLoader;
|
||||
public Material provinceMapMaterial;
|
||||
|
||||
private Texture2D _distanceTexture;
|
||||
private Texture2D _crossProvinceTexture;
|
||||
|
||||
void Start() {
|
||||
var mapBytes = provinceIDLoader.DecompressedBytes;
|
||||
int w = provinceIDLoader.mapWidth;
|
||||
int h = provinceIDLoader.mapHeight;
|
||||
|
||||
var dist = ComputeDistanceMap(mapBytes, w, h);
|
||||
var (dist, crossIds) = ComputeDistanceMap(mapBytes, w, h);
|
||||
|
||||
_distanceTexture = new Texture2D(w, h, TextureFormat.R8, false);
|
||||
_distanceTexture.filterMode = FilterMode.Bilinear;
|
||||
@@ -26,13 +31,21 @@ namespace eagle {
|
||||
_distanceTexture.LoadRawTextureData(dist);
|
||||
_distanceTexture.Apply();
|
||||
|
||||
_crossProvinceTexture = new Texture2D(w, h, TextureFormat.R8, false);
|
||||
_crossProvinceTexture.filterMode = FilterMode.Point;
|
||||
_crossProvinceTexture.wrapMode = TextureWrapMode.Clamp;
|
||||
_crossProvinceTexture.LoadRawTextureData(crossIds);
|
||||
_crossProvinceTexture.Apply();
|
||||
|
||||
provinceMapMaterial.SetTexture("_BorderDistanceMap", _distanceTexture);
|
||||
provinceMapMaterial.SetTexture("_CrossProvinceIDMap", _crossProvinceTexture);
|
||||
}
|
||||
|
||||
private static byte[] ComputeDistanceMap(byte[] ids, int w, int h) {
|
||||
private static (byte[] dist, byte[] crossIds) ComputeDistanceMap(byte[] ids, int w, int h) {
|
||||
const float DIAG = 1.414f;
|
||||
int size = w * h;
|
||||
var dist = new float[size];
|
||||
var crossIds = new byte[size];
|
||||
|
||||
// Init: border pixels = 0, interior = large value
|
||||
for (int y = 0; y < h; y++) {
|
||||
@@ -43,24 +56,29 @@ namespace eagle {
|
||||
// Ocean/invalid pixels are always border
|
||||
if (id == 0 || id == 0xFF) {
|
||||
dist[idx] = 0f;
|
||||
crossIds[idx] = id;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 8 neighbors for different province ID
|
||||
bool isBorder = false;
|
||||
byte crossId = id;
|
||||
for (int dy = -1; dy <= 1 && !isBorder; dy++) {
|
||||
for (int dx = -1; dx <= 1 && !isBorder; dx++) {
|
||||
if (dx == 0 && dy == 0) continue;
|
||||
int nx = x + dx, ny = y + dy;
|
||||
if (nx < 0 || nx >= w || ny < 0 || ny >= h) {
|
||||
isBorder = true;
|
||||
crossId = 0;
|
||||
} else if (ids[ny * w + nx] != id) {
|
||||
isBorder = true;
|
||||
crossId = ids[ny * w + nx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dist[idx] = isBorder ? 0f : 255f;
|
||||
crossIds[idx] = crossId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +89,27 @@ namespace eagle {
|
||||
if (dist[idx] == 0f) continue;
|
||||
|
||||
float d = dist[idx];
|
||||
if (x > 0) d = Mathf.Min(d, dist[idx - 1] + 1f);
|
||||
if (y > 0) d = Mathf.Min(d, dist[(y - 1) * w + x] + 1f);
|
||||
if (x > 0 && y > 0) d = Mathf.Min(d, dist[(y - 1) * w + x - 1] + DIAG);
|
||||
if (x < w - 1 && y > 0) d = Mathf.Min(d, dist[(y - 1) * w + x + 1] + DIAG);
|
||||
int bestIdx = idx;
|
||||
|
||||
if (x > 0 && dist[idx - 1] + 1f < d) {
|
||||
d = dist[idx - 1] + 1f;
|
||||
bestIdx = idx - 1;
|
||||
}
|
||||
if (y > 0 && dist[(y - 1) * w + x] + 1f < d) {
|
||||
d = dist[(y - 1) * w + x] + 1f;
|
||||
bestIdx = (y - 1) * w + x;
|
||||
}
|
||||
if (x > 0 && y > 0 && dist[(y - 1) * w + x - 1] + DIAG < d) {
|
||||
d = dist[(y - 1) * w + x - 1] + DIAG;
|
||||
bestIdx = (y - 1) * w + x - 1;
|
||||
}
|
||||
if (x < w - 1 && y > 0 && dist[(y - 1) * w + x + 1] + DIAG < d) {
|
||||
d = dist[(y - 1) * w + x + 1] + DIAG;
|
||||
bestIdx = (y - 1) * w + x + 1;
|
||||
}
|
||||
|
||||
dist[idx] = d;
|
||||
if (bestIdx != idx) { crossIds[idx] = crossIds[bestIdx]; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,24 +120,41 @@ namespace eagle {
|
||||
if (dist[idx] == 0f) continue;
|
||||
|
||||
float d = dist[idx];
|
||||
if (x < w - 1) d = Mathf.Min(d, dist[idx + 1] + 1f);
|
||||
if (y < h - 1) d = Mathf.Min(d, dist[(y + 1) * w + x] + 1f);
|
||||
if (x < w - 1 && y < h - 1) d = Mathf.Min(d, dist[(y + 1) * w + x + 1] + DIAG);
|
||||
if (x > 0 && y < h - 1) d = Mathf.Min(d, dist[(y + 1) * w + x - 1] + DIAG);
|
||||
int bestIdx = idx;
|
||||
|
||||
if (x < w - 1 && dist[idx + 1] + 1f < d) {
|
||||
d = dist[idx + 1] + 1f;
|
||||
bestIdx = idx + 1;
|
||||
}
|
||||
if (y < h - 1 && dist[(y + 1) * w + x] + 1f < d) {
|
||||
d = dist[(y + 1) * w + x] + 1f;
|
||||
bestIdx = (y + 1) * w + x;
|
||||
}
|
||||
if (x < w - 1 && y < h - 1 && dist[(y + 1) * w + x + 1] + DIAG < d) {
|
||||
d = dist[(y + 1) * w + x + 1] + DIAG;
|
||||
bestIdx = (y + 1) * w + x + 1;
|
||||
}
|
||||
if (x > 0 && y < h - 1 && dist[(y + 1) * w + x - 1] + DIAG < d) {
|
||||
d = dist[(y + 1) * w + x - 1] + DIAG;
|
||||
bestIdx = (y + 1) * w + x - 1;
|
||||
}
|
||||
|
||||
dist[idx] = d;
|
||||
if (bestIdx != idx) { crossIds[idx] = crossIds[bestIdx]; }
|
||||
}
|
||||
}
|
||||
|
||||
// Pack into byte array
|
||||
var result = new byte[size];
|
||||
// Pack into byte arrays
|
||||
var distResult = new byte[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
result[i] = (byte)Mathf.Min(Mathf.RoundToInt(dist[i]), 255);
|
||||
distResult[i] = (byte)Mathf.Min(Mathf.RoundToInt(dist[i]), 255);
|
||||
}
|
||||
return result;
|
||||
return (distResult, crossIds);
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_distanceTexture != null) { Destroy(_distanceTexture); }
|
||||
if (_crossProvinceTexture != null) { Destroy(_crossProvinceTexture); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
@@ -5,6 +6,8 @@ namespace eagle {
|
||||
/// Manages 256x1 lookup textures for per-province border highlighting.
|
||||
/// Border color and width are set per province and uploaded each frame
|
||||
/// when dirty, mirroring ProvinceColorManager's batched approach.
|
||||
/// Also manages a highlight-group lookup so the shader can suppress
|
||||
/// highlight borders between provinces in the same group (e.g. same faction).
|
||||
/// </summary>
|
||||
public class ProvinceBorderManager : MonoBehaviour {
|
||||
[Header("Material")]
|
||||
@@ -16,9 +19,12 @@ namespace eagle {
|
||||
|
||||
private Texture2D _borderColorLookup;
|
||||
private Texture2D _borderWidthLookup;
|
||||
private Texture2D _highlightGroupLookup;
|
||||
private Color[] _borderColors;
|
||||
private Color[] _borderWidths;
|
||||
private Color[] _highlightGroups;
|
||||
private bool _dirty;
|
||||
private bool _groupDirty;
|
||||
|
||||
void Awake() {
|
||||
_borderColorLookup = new Texture2D(256, 1, TextureFormat.RGBA32, false);
|
||||
@@ -29,20 +35,29 @@ namespace eagle {
|
||||
_borderWidthLookup.filterMode = FilterMode.Point;
|
||||
_borderWidthLookup.wrapMode = TextureWrapMode.Clamp;
|
||||
|
||||
_highlightGroupLookup = new Texture2D(256, 1, TextureFormat.RGBA32, false);
|
||||
_highlightGroupLookup.filterMode = FilterMode.Point;
|
||||
_highlightGroupLookup.wrapMode = TextureWrapMode.Clamp;
|
||||
|
||||
_borderColors = new Color[256];
|
||||
_borderWidths = new Color[256];
|
||||
_highlightGroups = new Color[256];
|
||||
for (int i = 0; i < 256; i++) {
|
||||
_borderColors[i] = Color.clear;
|
||||
_borderWidths[i] = Color.clear;
|
||||
_highlightGroups[i] = Color.clear;
|
||||
}
|
||||
|
||||
_borderColorLookup.SetPixels(_borderColors);
|
||||
_borderColorLookup.Apply();
|
||||
_borderWidthLookup.SetPixels(_borderWidths);
|
||||
_borderWidthLookup.Apply();
|
||||
_highlightGroupLookup.SetPixels(_highlightGroups);
|
||||
_highlightGroupLookup.Apply();
|
||||
|
||||
provinceMapMaterial.SetTexture("_BorderColorLookup", _borderColorLookup);
|
||||
provinceMapMaterial.SetTexture("_BorderWidthLookup", _borderWidthLookup);
|
||||
provinceMapMaterial.SetTexture("_HighlightGroupLookup", _highlightGroupLookup);
|
||||
provinceMapMaterial.SetFloat("_DefaultBorderWidth", defaultBorderWidth);
|
||||
provinceMapMaterial.SetColor("_DefaultBorderColor", defaultBorderColor);
|
||||
}
|
||||
@@ -61,6 +76,20 @@ namespace eagle {
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
public void SetHighlightGroup(IList<int> provinceIds) {
|
||||
for (int i = 0; i < 256; i++) { _highlightGroups[i] = Color.clear; }
|
||||
var groupColor = new Color(1f / 255f, 0, 0, 0);
|
||||
foreach (var id in provinceIds) {
|
||||
if (id >= 0 && id <= 255) { _highlightGroups[id] = groupColor; }
|
||||
}
|
||||
_groupDirty = true;
|
||||
}
|
||||
|
||||
public void ClearHighlightGroup() {
|
||||
for (int i = 0; i < 256; i++) { _highlightGroups[i] = Color.clear; }
|
||||
_groupDirty = true;
|
||||
}
|
||||
|
||||
void LateUpdate() {
|
||||
if (_dirty) {
|
||||
_borderColorLookup.SetPixels(_borderColors);
|
||||
@@ -69,11 +98,17 @@ namespace eagle {
|
||||
_borderWidthLookup.Apply();
|
||||
_dirty = false;
|
||||
}
|
||||
if (_groupDirty) {
|
||||
_highlightGroupLookup.SetPixels(_highlightGroups);
|
||||
_highlightGroupLookup.Apply();
|
||||
_groupDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_borderColorLookup != null) Destroy(_borderColorLookup);
|
||||
if (_borderWidthLookup != null) Destroy(_borderWidthLookup);
|
||||
if (_highlightGroupLookup != null) Destroy(_highlightGroupLookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -19,6 +19,10 @@ Shader "Eagle/ProvinceMap"
|
||||
_DefaultBorderWidth ("Default Border Width", Float) = 0.35
|
||||
_DefaultBorderColor ("Default Border Color", Color) = (0.15, 0.1, 0.05, 0.8)
|
||||
|
||||
// Same-faction border suppression
|
||||
_CrossProvinceIDMap ("Cross Province ID Map", 2D) = "black" {}
|
||||
_HighlightGroupLookup ("Highlight Group Lookup (256x1)", 2D) = "black" {}
|
||||
|
||||
// UI clipping support
|
||||
_StencilComp ("Stencil Comparison", Float) = 8
|
||||
_Stencil ("Stencil ID", Float) = 0
|
||||
@@ -90,6 +94,8 @@ Shader "Eagle/ProvinceMap"
|
||||
sampler2D _BorderDistanceMap;
|
||||
sampler2D _BorderColorLookup;
|
||||
sampler2D _BorderWidthLookup;
|
||||
sampler2D _CrossProvinceIDMap;
|
||||
sampler2D _HighlightGroupLookup;
|
||||
float4 _ProvinceIDMap_ST;
|
||||
float4 _OceanTex_ST;
|
||||
float4 _BorderDistanceMap_TexelSize; // Unity auto-provides (1/w, 1/h, w, h)
|
||||
@@ -161,11 +167,20 @@ Shader "Eagle/ProvinceMap"
|
||||
float effectiveHighlightWidth = highlightWidthPx * texelsPerPixel;
|
||||
fixed4 highlightColor = tex2D(_BorderColorLookup, float2(provinceId, 0.5));
|
||||
|
||||
// Check if the province across the nearest border is in the
|
||||
// same highlight group (e.g. same faction). When both sides
|
||||
// belong to the same group the highlight border is suppressed
|
||||
// so the faction's territory looks like one contiguous region.
|
||||
float crossProvId = tex2D(_CrossProvinceIDMap, i.uv).r;
|
||||
float myGroup = tex2D(_HighlightGroupLookup, float2(provinceId, 0.5)).r;
|
||||
float crossGroup = tex2D(_HighlightGroupLookup, float2(crossProvId, 0.5)).r;
|
||||
bool sameGroup = myGroup > 0.001 && abs(myGroup - crossGroup) < 0.001;
|
||||
|
||||
// Scale anti-aliasing transitions
|
||||
float defaultTransition = 0.5 * texelsPerPixel;
|
||||
float highlightTransition = 1.5 * texelsPerPixel;
|
||||
|
||||
if (effectiveHighlightWidth > 0.5 && dist <= effectiveHighlightWidth)
|
||||
if (effectiveHighlightWidth > 0.5 && dist <= effectiveHighlightWidth && !sameGroup)
|
||||
{
|
||||
float edge = 1.0 - saturate((dist - effectiveHighlightWidth + highlightTransition) / highlightTransition);
|
||||
color = lerp(fillColor, highlightColor, highlightColor.a * edge);
|
||||
|
||||
+36
-9
File diff suppressed because one or more lines are too long
+9
-9
@@ -6,28 +6,28 @@ EditorUserSettings:
|
||||
serializedVersion: 4
|
||||
m_ConfigSettings:
|
||||
RecentlyUsedSceneGuid-0:
|
||||
value: 5b02025f520c5a5f0b0d552640770c4946151a2e7d7a75697c7e4f66e1b2306c
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-1:
|
||||
value: 540206545d510c590f595d2143770f45444f1b73757e72357c791836e7e5373a
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-2:
|
||||
RecentlyUsedSceneGuid-1:
|
||||
value: 06525702540c0f595c57547545770c48104e487d7d787062787e4864b7b7606b
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-3:
|
||||
RecentlyUsedSceneGuid-2:
|
||||
value: 5a000356000d085a555c0d7343770b4241151e2b7e7070647f2d4e35bae46339
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-4:
|
||||
RecentlyUsedSceneGuid-3:
|
||||
value: 0507035e5c0558095e5a5974477b064047454b7e2d2a2534282f4d62b0b3606d
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-5:
|
||||
RecentlyUsedSceneGuid-4:
|
||||
value: 0703555650055e0b58565d7a41770643441641797b7b7263757a4931b1e46c6a
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-5:
|
||||
value: 015207020753505e590c597411770a4215164d282a7e7633757f496be4b0316c
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-6:
|
||||
value: 51070104510d505f5c565e2314750e44134e4c7a2a2a71357c714d62b0e2643c
|
||||
value: 5b02025f520c5a5f0b0d552640770c4946151a2e7d7a75697c7e4f66e1b2306c
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-7:
|
||||
value: 015207020753505e590c597411770a4215164d282a7e7633757f496be4b0316c
|
||||
value: 51070104510d505f5c565e2314750e44134e4c7a2a2a71357c714d62b0e2643c
|
||||
flags: 0
|
||||
RecentlyUsedScenePath-0:
|
||||
value: 224247031146467c0c0309321c22465e0319113e35
|
||||
|
||||
Reference in New Issue
Block a user