Add motion mode selection to TerritoryLayer and TerritoryWebGLRenderer

- Introduced a dropdown UI in TerritoryLayer for selecting motion modes (euclidean, axisSnap, manhattan, chebyshev).
- Updated TerritoryWebGLRenderer to handle the new motion mode settings, enhancing rendering behavior based on selected modes.
- Enhanced shader logic to incorporate motion mode effects, improving visual representation of territory transitions.
- Updated logging to include the current motion mode for better tracking of rendering performance.
This commit is contained in:
scamiv
2026-01-13 20:40:23 +01:00
parent e0d0c001d8
commit 6b813b4c91
2 changed files with 119 additions and 10 deletions
@@ -93,6 +93,8 @@ export class TerritoryLayer implements Layer {
"blueNoise";
private debugDisableStaticBorders = false;
private debugDisableAllBorders = false;
private motionMode: "euclidean" | "axisSnap" | "manhattan" | "chebyshev" =
"euclidean";
private seedSamplingMode: "none" | "2x2" | "3x3" = "2x2";
private debugStripeFixedColors = false;
@@ -726,6 +728,48 @@ export class TerritoryLayer implements Layer {
seedSamplingRow.appendChild(seedSamplingSelect);
root.appendChild(seedSamplingRow);
// Motion mode dropdown
const motionModeRow = document.createElement("label");
motionModeRow.style.display = "flex";
motionModeRow.style.alignItems = "center";
motionModeRow.style.gap = "6px";
motionModeRow.style.marginTop = "6px";
const motionModeText = document.createElement("span");
motionModeText.textContent = "motion mode";
const motionModeSelect = document.createElement("select");
motionModeSelect.style.background = "rgba(0,0,0,0.5)";
motionModeSelect.style.color = "#fff";
motionModeSelect.style.border = "1px solid rgba(255,255,255,0.2)";
motionModeSelect.style.borderRadius = "4px";
motionModeSelect.style.padding = "2px 4px";
const motionModes: Array<
"euclidean" | "axisSnap" | "manhattan" | "chebyshev"
> = ["euclidean", "axisSnap", "manhattan", "chebyshev"];
for (const m of motionModes) {
const opt = document.createElement("option");
opt.value = m;
opt.textContent = m;
motionModeSelect.appendChild(opt);
}
motionModeSelect.value = this.motionMode;
motionModeSelect.addEventListener("change", () => {
const v = motionModeSelect.value as
| "euclidean"
| "axisSnap"
| "manhattan"
| "chebyshev";
this.motionMode = v;
this.territoryRenderer?.setMotionMode(v);
this.territoryRenderer?.markAllDirty();
});
motionModeRow.appendChild(motionModeText);
motionModeRow.appendChild(motionModeSelect);
root.appendChild(motionModeRow);
// Debug: fixed stripe colors
const stripeColorsRow = document.createElement("label");
stripeColorsRow.style.display = "flex";
@@ -834,6 +878,7 @@ export class TerritoryLayer implements Layer {
this.debugDisableAllBorders,
);
this.territoryRenderer.setSeedSamplingMode(this.seedSamplingMode);
this.territoryRenderer.setMotionMode(this.motionMode);
this.territoryRenderer.setDebugStripeFixedColors(
this.debugStripeFixedColors,
);
@@ -1558,6 +1603,7 @@ export class TerritoryLayer implements Layer {
`smooth: ${stats.smoothEnabled ? "on" : "off"} ${stats.smoothProgress.toFixed(2)} pair ${this.lastInterpolationPair}`,
`tick: ${this.tickNumberCurrent ?? "-"} prev ${this.tickNumberPrev ?? "-"}`,
`delayMs: ${this.interpolationDelayMs.toFixed(0)}`,
`motionMode: ${this.motionMode}`,
`tripleBuf: ${this.tripleBufferEnabled ? "on" : "off"}`,
`delayMode: ${this.interpolationDelayMode}${this.interpolationDelayMode === "ema" ? ` (ema=${this.tickIntervalEmaMs.toFixed(0)}ms)` : ""}`,
`smoothPrereq: prevCopy ${stats.prevStateCopySupported ? "yes" : "no"}`,
@@ -33,6 +33,7 @@ export class TerritoryWebGLRenderer {
private debugDisableAllBorders = false;
private seedSamplingMode: 0 | 1 | 2 = 1; // 0=none(single texel), 1=2x2, 2=3x3
private debugStripeFixedColors = false; // Use fixed debug colors for moving stripe
private motionMode: 0 | 1 | 2 | 3 = 0; // 0=euclidean, 1=axisSnap, 2=manhattan, 3=chebyshev
private readonly gl: WebGL2RenderingContext | null;
private readonly program: WebGLProgram | null;
@@ -104,6 +105,7 @@ export class TerritoryWebGLRenderer {
debugDisableAllBorders: WebGLUniformLocation | null;
seedSamplingMode: WebGLUniformLocation | null;
debugStripeFixedColors: WebGLUniformLocation | null;
motionMode: WebGLUniformLocation | null;
contestOwners: WebGLUniformLocation | null;
contestIds: WebGLUniformLocation | null;
contestTimes: WebGLUniformLocation | null;
@@ -273,6 +275,7 @@ export class TerritoryWebGLRenderer {
debugDisableAllBorders: null,
seedSamplingMode: null,
debugStripeFixedColors: null,
motionMode: null,
contestOwners: null,
contestIds: null,
contestTimes: null,
@@ -371,6 +374,7 @@ export class TerritoryWebGLRenderer {
debugDisableAllBorders: null,
seedSamplingMode: null,
debugStripeFixedColors: null,
motionMode: null,
contestOwners: null,
contestIds: null,
contestTimes: null,
@@ -485,6 +489,7 @@ export class TerritoryWebGLRenderer {
this.program,
"u_debugStripeFixedColors",
),
motionMode: gl.getUniformLocation(this.program, "u_motionMode"),
contestOwners: gl.getUniformLocation(this.program, "u_contestOwners"),
contestIds: gl.getUniformLocation(this.program, "u_contestIds"),
contestTimes: gl.getUniformLocation(this.program, "u_contestTimes"),
@@ -1346,6 +1351,13 @@ export class TerritoryWebGLRenderer {
this.debugStripeFixedColors = enabled;
}
setMotionMode(mode: "euclidean" | "axisSnap" | "manhattan" | "chebyshev") {
if (mode === "axisSnap") this.motionMode = 1;
else if (mode === "manhattan") this.motionMode = 2;
else if (mode === "chebyshev") this.motionMode = 3;
else this.motionMode = 0;
}
markTile(tile: TileRef) {
if (this.needsFullUpload) {
return;
@@ -1843,6 +1855,9 @@ export class TerritoryWebGLRenderer {
this.debugStripeFixedColors ? 1 : 0,
);
}
if (this.uniforms.motionMode) {
gl.uniform1i(this.uniforms.motionMode, this.motionMode);
}
if (this.uniforms.contestNow) {
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
}
@@ -2812,6 +2827,7 @@ export class TerritoryWebGLRenderer {
uniform bool u_debugDisableAllBorders;
uniform int u_seedSamplingMode; // 0=none(single texel), 1=2x2, 2=3x3
uniform bool u_debugStripeFixedColors; // Use fixed debug colors for moving stripe
uniform int u_motionMode; // 0=euclidean, 1=axisSnap, 2=manhattan, 3=chebyshev
uniform usampler2D u_contestOwners;
uniform usampler2D u_contestIds;
uniform usampler2D u_contestTimes;
@@ -3541,19 +3557,66 @@ export class TerritoryWebGLRenderer {
// the displacement direction from old->new seeds.
if (affectedMask != 0u && hasOldSeed && hasNewSeed) {
vec2 disp = seedNew - seedOld;
vec2 absDisp = abs(disp);
vec2 dispSign = vec2(disp.x >= 0.0 ? 1.0 : -1.0, disp.y >= 0.0 ? 1.0 : -1.0);
float dispLen = length(disp);
if (dispLen > 1e-4) {
vec2 dir = disp / dispLen;
vec2 dir = vec2(1.0, 0.0);
vec2 frontOrigin = seedOld;
float frontPos = 0.0;
vec2 shift = vec2(0.0);
// Project mapCoord onto the displacement direction, measured from seedOld.
if (u_motionMode == 1) {
bool xDom = absDisp.x >= absDisp.y;
dir = xDom ? vec2(dispSign.x, 0.0) : vec2(0.0, dispSign.y);
float len = xDom ? absDisp.x : absDisp.y;
frontOrigin = seedOld;
frontPos = t * len;
shift = dir * (len * (1.0 - t));
} else if (u_motionMode == 2) {
bool xDom = absDisp.x >= absDisp.y;
vec2 axisX = vec2(dispSign.x, 0.0);
vec2 axisY = vec2(0.0, dispSign.y);
vec2 axis1 = xDom ? axisX : axisY;
vec2 axis2 = xDom ? axisY : axisX;
float len1 = xDom ? absDisp.x : absDisp.y;
float len2 = xDom ? absDisp.y : absDisp.x;
float total = len1 + len2;
float split = total > 1e-4 ? len1 / total : 0.5;
if (t <= split) {
float t1 = split > 1e-4 ? t / split : 1.0;
dir = axis1;
frontOrigin = seedOld;
frontPos = t1 * len1;
shift = axis1 * (len1 * (1.0 - t1)) + axis2 * len2;
} else {
float t2 = (t - split) / max(1.0 - split, 1e-4);
dir = axis2;
frontOrigin = seedOld + axis1 * len1;
frontPos = t2 * len2;
shift = axis2 * (len2 * (1.0 - t2));
}
} else if (u_motionMode == 3) {
float maxAbs = max(absDisp.x, absDisp.y);
float p = t * maxAbs;
vec2 remaining = max(absDisp - vec2(p), vec2(0.0));
shift = dispSign * remaining;
bool xDom = absDisp.x >= absDisp.y;
dir = xDom ? vec2(dispSign.x, 0.0) : vec2(0.0, dispSign.y);
frontOrigin = seedOld;
frontPos = t * maxAbs;
} else {
dir = disp / dispLen;
frontOrigin = seedOld;
frontPos = t * dispLen;
shift = disp * (1.0 - t);
}
// Project mapCoord onto the displacement direction, measured from frontOrigin.
// This gives us a global coordinate along the motion axis.
// At t=0, front should be near seedOld (s 0).
// At t=1, front should be near seedNew (s ≈ dispLen).
float s = dot(mapCoord - seedOld, dir);
// Front position moves from old border to new border along the motion axis.
// Seeds are placed at border edges, so no extra offset is needed.
float frontPos = t * dispLen;
// At t=0, front should be near frontOrigin (s ~ 0).
// At t=1, front should be near frontOrigin + dir * frontPos.
float s = dot(mapCoord - frontOrigin, dir);
// Signed distance from the moving front plane.
// Positive means the front has passed this point (new territory side).
@@ -3662,7 +3725,7 @@ export class TerritoryWebGLRenderer {
}
} else if (frontDist > stripeWidth) {
// Front has passed; show the new fill/border at the shifted position
vec2 slideCoordFill = mapCoord - dir * (dispLen * (1.0 - t));
vec2 slideCoordFill = mapCoord - shift;
ivec2 slideTexFill = clamp(ivec2(slideCoordFill), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
uint fillState = texelFetch(u_state, slideTexFill, 0).r;