mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 22:51:57 +00:00
Add tick interpolation support to TerritoryLayer and WebGLRenderer
- Introduced tick management in TerritoryLayer with lastTickSeen, tickParity, and tickDurationMs properties. - Updated tick method to handle tile updates based on the current tick and manage WebGL rendering timing. - Enhanced TerritoryRendererStrategy interface with new methods for updating tile arrival and setting tick timing. - Implemented arrival phase and tick parity handling in TerritoryWebGLRenderer, including texture updates for changed tiles. - Improved shader logic to manage visibility of tiles based on tick progress and arrival phase.
This commit is contained in:
@@ -70,6 +70,9 @@ export class TerritoryLayer implements Layer {
|
||||
private lastMyPlayerSmallId: number | null = null;
|
||||
private useWebGL: boolean;
|
||||
private webglSupported = true;
|
||||
private lastTickSeen = 0;
|
||||
private tickParity = 0;
|
||||
private tickDurationMs = 100;
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
@@ -97,6 +100,28 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
tick() {
|
||||
const tickProfile = FrameProfiler.start();
|
||||
|
||||
const currentTick = this.game.ticks();
|
||||
if (currentTick !== this.lastTickSeen) {
|
||||
this.lastTickSeen = currentTick;
|
||||
this.tickParity ^= 1;
|
||||
const tickStartMs = performance.now();
|
||||
const changedTiles = this.game.recentlyUpdatedTiles();
|
||||
if (this.territoryRenderer?.isWebGL()) {
|
||||
const webglRenderer = this.territoryRenderer as WebglTerritoryRenderer;
|
||||
webglRenderer.updateArrivalForChangedTiles(
|
||||
changedTiles,
|
||||
this.tickParity,
|
||||
);
|
||||
webglRenderer.setTickTiming(
|
||||
currentTick,
|
||||
tickStartMs,
|
||||
this.tickDurationMs,
|
||||
this.tickParity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.spawnHighlight();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ export interface TerritoryRendererStrategy {
|
||||
setHoverHighlightOptions(options: HoverHighlightOptions): void;
|
||||
refreshPalette(): void;
|
||||
clearTile(tile: TileRef): void;
|
||||
updateArrivalForChangedTiles(tiles: TileRef[], tickParity: number): void;
|
||||
setTickTiming(
|
||||
tick: number,
|
||||
startMs: number,
|
||||
durationMs: number,
|
||||
parity: number,
|
||||
): void;
|
||||
}
|
||||
|
||||
export class CanvasTerritoryRenderer implements TerritoryRendererStrategy {
|
||||
@@ -235,6 +242,14 @@ export class CanvasTerritoryRenderer implements TerritoryRendererStrategy {
|
||||
this.alternativeImageData.data[offset + 3] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
updateArrivalForChangedTiles(): void {
|
||||
// No tick intrapolation for canvas renderer.
|
||||
}
|
||||
|
||||
setTickTiming(): void {
|
||||
// No tick intrapolation for canvas renderer.
|
||||
}
|
||||
}
|
||||
|
||||
export class WebglTerritoryRenderer implements TerritoryRendererStrategy {
|
||||
@@ -298,4 +313,17 @@ export class WebglTerritoryRenderer implements TerritoryRendererStrategy {
|
||||
clearTile(): void {
|
||||
// No-op for WebGL; canvas alpha clearing is not used.
|
||||
}
|
||||
|
||||
updateArrivalForChangedTiles(tiles: TileRef[], tickParity: number): void {
|
||||
this.renderer.updateArrivalForChangedTiles(this.game, tiles, tickParity);
|
||||
}
|
||||
|
||||
setTickTiming(
|
||||
tick: number,
|
||||
startMs: number,
|
||||
durationMs: number,
|
||||
parity: number,
|
||||
): void {
|
||||
this.renderer.setTickTiming(tick, startMs, durationMs, parity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ export class TerritoryWebGLRenderer {
|
||||
borderDefendedFriendlyDark: WebGLUniformLocation | null;
|
||||
borderDefendedEmbargoLight: WebGLUniformLocation | null;
|
||||
borderDefendedEmbargoDark: WebGLUniformLocation | null;
|
||||
// Tick intrapolation uniforms
|
||||
prevState: WebGLUniformLocation | null;
|
||||
arrivalPhase: WebGLUniformLocation | null;
|
||||
tickProgress: WebGLUniformLocation | null;
|
||||
tickParity: WebGLUniformLocation | null;
|
||||
};
|
||||
|
||||
private readonly state: Uint16Array;
|
||||
@@ -75,6 +80,23 @@ export class TerritoryWebGLRenderer {
|
||||
private hoveredPlayerId = -1;
|
||||
private animationStartTime = Date.now();
|
||||
|
||||
// Tick intrapolation buffers (client-side only, WebGL path).
|
||||
private readonly prevState: Uint16Array;
|
||||
private readonly arrivalPhase: Uint8Array;
|
||||
private readonly changeParity: Uint8Array;
|
||||
|
||||
// Packed texel buffer backing the arrivalPhase texture (RG8: phase, parity).
|
||||
private readonly arrivalPhaseTexData: Uint8Array;
|
||||
|
||||
// Corresponding textures.
|
||||
private readonly arrivalPhaseTexture: WebGLTexture | null = null;
|
||||
|
||||
// Tick timing for shader.
|
||||
private lastTickId = 0;
|
||||
private lastTickStartMs = 0;
|
||||
private tickDurationMs = 100;
|
||||
private tickParity = 0;
|
||||
|
||||
private constructor(
|
||||
private readonly game: GameView,
|
||||
private readonly theme: Theme,
|
||||
@@ -85,6 +107,12 @@ export class TerritoryWebGLRenderer {
|
||||
this.canvas.height = game.height();
|
||||
|
||||
this.state = new Uint16Array(sharedState);
|
||||
// Allocate intrapolation buffers.
|
||||
const numTiles = this.canvas.width * this.canvas.height;
|
||||
this.prevState = new Uint16Array(numTiles);
|
||||
this.arrivalPhase = new Uint8Array(numTiles);
|
||||
this.changeParity = new Uint8Array(numTiles);
|
||||
this.arrivalPhaseTexData = new Uint8Array(numTiles * 2);
|
||||
|
||||
this.gl = this.canvas.getContext("webgl2", {
|
||||
premultipliedAlpha: true,
|
||||
@@ -127,6 +155,10 @@ export class TerritoryWebGLRenderer {
|
||||
borderDefendedFriendlyDark: null,
|
||||
borderDefendedEmbargoLight: null,
|
||||
borderDefendedEmbargoDark: null,
|
||||
prevState: null,
|
||||
arrivalPhase: null,
|
||||
tickProgress: null,
|
||||
tickParity: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -167,6 +199,10 @@ export class TerritoryWebGLRenderer {
|
||||
borderDefendedFriendlyDark: null,
|
||||
borderDefendedEmbargoLight: null,
|
||||
borderDefendedEmbargoDark: null,
|
||||
prevState: null,
|
||||
arrivalPhase: null,
|
||||
tickProgress: null,
|
||||
tickParity: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -226,6 +262,10 @@ export class TerritoryWebGLRenderer {
|
||||
this.program,
|
||||
"u_borderDefendedEmbargoDark",
|
||||
),
|
||||
prevState: null,
|
||||
arrivalPhase: gl.getUniformLocation(this.program, "u_arrivalPhase"),
|
||||
tickProgress: gl.getUniformLocation(this.program, "u_tickProgress"),
|
||||
tickParity: gl.getUniformLocation(this.program, "u_tickParity"),
|
||||
};
|
||||
|
||||
// Vertex data: two triangles covering the full map (pixel-perfect).
|
||||
@@ -259,6 +299,14 @@ export class TerritoryWebGLRenderer {
|
||||
this.paletteTexture = gl.createTexture();
|
||||
this.relationTexture = gl.createTexture();
|
||||
|
||||
// Initialize intrapolation buffers from current game state.
|
||||
this.initPrevStateFromGame();
|
||||
|
||||
// Create texture for arrivalPhase.
|
||||
const arrivalTex = gl.createTexture();
|
||||
this.arrivalPhaseTexture = arrivalTex;
|
||||
|
||||
// State texture (current map state from SAB).
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
@@ -278,12 +326,37 @@ export class TerritoryWebGLRenderer {
|
||||
this.state,
|
||||
);
|
||||
|
||||
// Arrival phase texture (use RG8, R=phase, G=parity flag).
|
||||
if (this.arrivalPhaseTexture) {
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.arrivalPhaseTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RG8,
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
0,
|
||||
gl.RG,
|
||||
gl.UNSIGNED_BYTE,
|
||||
this.arrivalPhaseTexData,
|
||||
);
|
||||
}
|
||||
|
||||
this.uploadPalette();
|
||||
|
||||
gl.useProgram(this.program);
|
||||
gl.uniform1i(this.uniforms.state, 0);
|
||||
gl.uniform1i(this.uniforms.palette, 1);
|
||||
gl.uniform1i(this.uniforms.relations, 2);
|
||||
if (this.uniforms.arrivalPhase) {
|
||||
gl.uniform1i(this.uniforms.arrivalPhase, 4);
|
||||
}
|
||||
|
||||
if (this.uniforms.resolution) {
|
||||
gl.uniform2f(
|
||||
@@ -438,6 +511,128 @@ export class TerritoryWebGLRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize prevState buffer from the current GameView map state.
|
||||
* Only called once during construction.
|
||||
*/
|
||||
private initPrevStateFromGame() {
|
||||
const numTiles = this.canvas.width * this.canvas.height;
|
||||
for (let tile = 0; tile < numTiles; tile++) {
|
||||
const ownerId = this.game.ownerID(tile as TileRef);
|
||||
const hasFallout = this.game.hasFallout(tile as TileRef);
|
||||
const isDefended = this.game.isDefended(tile as TileRef);
|
||||
let state = ownerId & 0x0fff;
|
||||
if (isDefended) {
|
||||
state |= 1 << 12;
|
||||
}
|
||||
if (hasFallout) {
|
||||
state |= 1 << 13;
|
||||
}
|
||||
this.prevState[tile] = state;
|
||||
this.arrivalPhase[tile] = 0xff;
|
||||
this.changeParity[tile] = 0;
|
||||
const idx = tile * 2;
|
||||
this.arrivalPhaseTexData[idx] = this.arrivalPhase[tile];
|
||||
this.arrivalPhaseTexData[idx + 1] = this.changeParity[tile];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update arrival phase and parity for tiles that changed this tick.
|
||||
* prevState is used as "old" state for animated tiles and is not
|
||||
* overwritten here for those tiles.
|
||||
*/
|
||||
updateArrivalForChangedTiles(
|
||||
game: GameView,
|
||||
tiles: TileRef[],
|
||||
tickParity: number,
|
||||
) {
|
||||
const maxRadius = 8;
|
||||
const width = this.canvas.width;
|
||||
const height = this.canvas.height;
|
||||
|
||||
for (const tile of tiles) {
|
||||
const prevStateWord = this.prevState[tile];
|
||||
const prevOwner = prevStateWord & 0x0fff;
|
||||
const newStateWord = this.state[tile];
|
||||
const newOwner = newStateWord & 0x0fff;
|
||||
|
||||
// Only animate owner changes for now.
|
||||
if (prevOwner === newOwner) {
|
||||
// Keep prevState in sync so future owner-change detection is correct.
|
||||
this.prevState[tile] = newStateWord;
|
||||
continue;
|
||||
}
|
||||
|
||||
const x0 = game.x(tile);
|
||||
const y0 = game.y(tile);
|
||||
let minDistSq = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (let dy = -maxRadius; dy <= maxRadius; dy++) {
|
||||
const y = y0 + dy;
|
||||
if (y < 0 || y >= height) continue;
|
||||
for (let dx = -maxRadius; dx <= maxRadius; dx++) {
|
||||
const x = x0 + dx;
|
||||
if (x < 0 || x >= width) continue;
|
||||
const distSq = dx * dx + dy * dy;
|
||||
if (distSq > maxRadius * maxRadius) continue;
|
||||
const t2 = game.ref(x, y);
|
||||
const prevOwnerHere = this.prevState[t2] & 0x0fff;
|
||||
if (prevOwnerHere === newOwner && distSq < minDistSq) {
|
||||
minDistSq = distSq;
|
||||
if (minDistSq <= 1) break;
|
||||
}
|
||||
}
|
||||
if (minDistSq <= 1) break;
|
||||
}
|
||||
|
||||
let phaseByte = 0xff;
|
||||
if (minDistSq !== Number.POSITIVE_INFINITY) {
|
||||
const dist = Math.sqrt(minDistSq);
|
||||
const clipped = Math.min(dist, maxRadius);
|
||||
const phase = maxRadius > 0 ? clipped / maxRadius : 1.0;
|
||||
phaseByte = Math.max(0, Math.min(255, Math.round(phase * 255)));
|
||||
}
|
||||
|
||||
this.arrivalPhase[tile] = phaseByte;
|
||||
// Store parity as 0 or 255 so the sampler2D normalized channel is easy to threshold.
|
||||
this.changeParity[tile] = tickParity ? 255 : 0;
|
||||
|
||||
const idx = tile * 2;
|
||||
this.arrivalPhaseTexData[idx] = this.arrivalPhase[tile];
|
||||
this.arrivalPhaseTexData[idx + 1] = this.changeParity[tile];
|
||||
|
||||
// Update the previous-state snapshot to reflect the newly applied state,
|
||||
// so subsequent ticks treat this owner as the "old" owner.
|
||||
this.prevState[tile] = newStateWord;
|
||||
}
|
||||
|
||||
// Mark all potentially affected rows dirty so the textures will be updated.
|
||||
for (const tile of tiles) {
|
||||
const x = tile % this.canvas.width;
|
||||
const y = Math.floor(tile / this.canvas.width);
|
||||
const span = this.dirtyRows.get(y);
|
||||
if (span === undefined) {
|
||||
this.dirtyRows.set(y, { minX: x, maxX: x });
|
||||
} else {
|
||||
span.minX = Math.min(span.minX, x);
|
||||
span.maxX = Math.max(span.maxX, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTickTiming(
|
||||
tick: number,
|
||||
startMs: number,
|
||||
durationMs: number,
|
||||
parity: number,
|
||||
) {
|
||||
this.lastTickId = tick;
|
||||
this.lastTickStartMs = startMs;
|
||||
this.tickDurationMs = durationMs;
|
||||
this.tickParity = parity;
|
||||
}
|
||||
|
||||
markTile(tile: TileRef) {
|
||||
if (this.needsFullUpload) {
|
||||
return;
|
||||
@@ -501,6 +696,18 @@ export class TerritoryWebGLRenderer {
|
||||
if (this.uniforms.hoverPulseSpeed) {
|
||||
gl.uniform1f(this.uniforms.hoverPulseSpeed, this.hoverPulseSpeed);
|
||||
}
|
||||
if (this.uniforms.tickProgress) {
|
||||
const now = performance.now();
|
||||
const dt = Math.max(0, now - this.lastTickStartMs);
|
||||
const progress =
|
||||
this.tickDurationMs > 0
|
||||
? Math.max(0, Math.min(1, dt / this.tickDurationMs))
|
||||
: 1;
|
||||
gl.uniform1f(this.uniforms.tickProgress, progress);
|
||||
}
|
||||
if (this.uniforms.tickParity) {
|
||||
gl.uniform1i(this.uniforms.tickParity, this.tickParity);
|
||||
}
|
||||
if (this.uniforms.time) {
|
||||
const currentTime = (Date.now() - this.animationStartTime) / 1000.0;
|
||||
gl.uniform1f(this.uniforms.time, currentTime);
|
||||
@@ -543,10 +750,13 @@ export class TerritoryWebGLRenderer {
|
||||
this.dirtyRows.clear();
|
||||
rowsUploaded = this.canvas.height;
|
||||
bytesUploaded = this.canvas.width * this.canvas.height * bytesPerPixel;
|
||||
// Full upload: also refresh arrivalPhase texture.
|
||||
this.uploadArrivalPhaseTexture(true);
|
||||
return { rows: rowsUploaded, bytes: bytesUploaded };
|
||||
}
|
||||
|
||||
if (this.dirtyRows.size === 0) {
|
||||
// No state changes; still keep prev/arrival textures as-is.
|
||||
return { rows: 0, bytes: 0 };
|
||||
}
|
||||
|
||||
@@ -568,10 +778,58 @@ export class TerritoryWebGLRenderer {
|
||||
rowsUploaded++;
|
||||
bytesUploaded += width * bytesPerPixel;
|
||||
}
|
||||
|
||||
// Apply same row set to arrivalPhase.
|
||||
this.uploadArrivalPhaseTexture(false);
|
||||
|
||||
this.dirtyRows.clear();
|
||||
return { rows: rowsUploaded, bytes: bytesUploaded };
|
||||
}
|
||||
|
||||
private uploadArrivalPhaseTexture(full: boolean) {
|
||||
if (!this.gl || !this.arrivalPhaseTexture) return;
|
||||
const gl = this.gl;
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.arrivalPhaseTexture);
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
|
||||
if (full) {
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RG8,
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
0,
|
||||
gl.RG,
|
||||
gl.UNSIGNED_BYTE,
|
||||
this.arrivalPhaseTexData,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dirtyRows.size === 0) return;
|
||||
|
||||
for (const [y, span] of this.dirtyRows) {
|
||||
const width = span.maxX - span.minX + 1;
|
||||
const startPixel = y * this.canvas.width + span.minX;
|
||||
const startByte = startPixel * 2;
|
||||
const endByte = (startPixel + width) * 2;
|
||||
const rowSlice = this.arrivalPhaseTexData.subarray(startByte, endByte);
|
||||
gl.texSubImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
span.minX,
|
||||
y,
|
||||
width,
|
||||
1,
|
||||
gl.RG,
|
||||
gl.UNSIGNED_BYTE,
|
||||
rowSlice,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats upload metrics into a human-readable string for logging/debugging.
|
||||
* Used for performance monitoring of WebGL texture uploads, bucketing values
|
||||
@@ -713,36 +971,39 @@ export class TerritoryWebGLRenderer {
|
||||
`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
precision mediump float;
|
||||
precision highp usampler2D;
|
||||
|
||||
uniform usampler2D u_state;
|
||||
uniform sampler2D u_palette;
|
||||
uniform usampler2D u_relations;
|
||||
uniform int u_viewerId;
|
||||
uniform vec2 u_resolution;
|
||||
uniform vec4 u_fallout;
|
||||
uniform vec4 u_altSelf;
|
||||
uniform vec4 u_altAlly;
|
||||
uniform vec4 u_altNeutral;
|
||||
uniform vec4 u_altEnemy;
|
||||
uniform float u_alpha;
|
||||
uniform vec4 u_borderNeutral;
|
||||
uniform vec4 u_borderFriendly;
|
||||
uniform vec4 u_borderEmbargo;
|
||||
uniform vec4 u_borderDefendedNeutralLight;
|
||||
uniform vec4 u_borderDefendedNeutralDark;
|
||||
uniform vec4 u_borderDefendedFriendlyLight;
|
||||
uniform vec4 u_borderDefendedFriendlyDark;
|
||||
uniform vec4 u_borderDefendedEmbargoLight;
|
||||
uniform vec4 u_borderDefendedEmbargoDark;
|
||||
uniform bool u_alternativeView;
|
||||
uniform float u_hoveredPlayerId;
|
||||
uniform vec3 u_hoverHighlightColor;
|
||||
uniform float u_hoverHighlightStrength;
|
||||
uniform float u_hoverPulseStrength;
|
||||
uniform float u_hoverPulseSpeed;
|
||||
uniform float u_time;
|
||||
precision mediump float;
|
||||
precision highp usampler2D;
|
||||
|
||||
uniform usampler2D u_state;
|
||||
uniform sampler2D u_palette;
|
||||
uniform usampler2D u_relations;
|
||||
uniform int u_viewerId;
|
||||
uniform vec2 u_resolution;
|
||||
uniform vec4 u_fallout;
|
||||
uniform vec4 u_altSelf;
|
||||
uniform vec4 u_altAlly;
|
||||
uniform vec4 u_altNeutral;
|
||||
uniform vec4 u_altEnemy;
|
||||
uniform float u_alpha;
|
||||
uniform vec4 u_borderNeutral;
|
||||
uniform vec4 u_borderFriendly;
|
||||
uniform vec4 u_borderEmbargo;
|
||||
uniform vec4 u_borderDefendedNeutralLight;
|
||||
uniform vec4 u_borderDefendedNeutralDark;
|
||||
uniform vec4 u_borderDefendedFriendlyLight;
|
||||
uniform vec4 u_borderDefendedFriendlyDark;
|
||||
uniform vec4 u_borderDefendedEmbargoLight;
|
||||
uniform vec4 u_borderDefendedEmbargoDark;
|
||||
uniform bool u_alternativeView;
|
||||
uniform float u_hoveredPlayerId;
|
||||
uniform vec3 u_hoverHighlightColor;
|
||||
uniform float u_hoverHighlightStrength;
|
||||
uniform float u_hoverPulseStrength;
|
||||
uniform float u_hoverPulseSpeed;
|
||||
uniform float u_time;
|
||||
uniform sampler2D u_arrivalPhase;
|
||||
uniform float u_tickProgress;
|
||||
uniform int u_tickParity;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
@@ -774,72 +1035,123 @@ export class TerritoryWebGLRenderer {
|
||||
return (code & 4u) != 0u;
|
||||
}
|
||||
|
||||
void main() {
|
||||
ivec2 fragCoord = ivec2(gl_FragCoord.xy);
|
||||
// gl_FragCoord origin is bottom-left; flip Y to match top-left oriented buffers.
|
||||
ivec2 texCoord = ivec2(fragCoord.x, int(u_resolution.y) - 1 - fragCoord.y);
|
||||
void main() {
|
||||
ivec2 fragCoord = ivec2(gl_FragCoord.xy);
|
||||
// gl_FragCoord origin is bottom-left; flip Y to match top-left oriented buffers.
|
||||
ivec2 texCoord = ivec2(fragCoord.x, int(u_resolution.y) - 1 - fragCoord.y);
|
||||
|
||||
uint state = texelFetch(u_state, texCoord, 0).r;
|
||||
uint owner = state & 0xFFFu;
|
||||
bool hasFallout = (state & 0x2000u) != 0u; // bit 13
|
||||
bool isDefended = (state & 0x1000u) != 0u; // bit 12
|
||||
uint state = texelFetch(u_state, texCoord, 0).r;
|
||||
uint owner = state & 0xFFFu;
|
||||
bool hasFallout = (state & 0x2000u) != 0u; // bit 13
|
||||
bool isDefended = (state & 0x1000u) != 0u; // bit 12
|
||||
|
||||
if (owner == 0u) {
|
||||
if (hasFallout) {
|
||||
vec3 color = u_fallout.rgb;
|
||||
float a = u_alpha;
|
||||
vec4 arrival = texture(u_arrivalPhase, (vec2(texCoord) + 0.5) / u_resolution);
|
||||
float arrivalPhase = arrival.r;
|
||||
int changeParity = arrival.g > 0.5 ? 1 : 0;
|
||||
|
||||
if (owner == 0u) {
|
||||
if (hasFallout) {
|
||||
vec3 color = u_fallout.rgb;
|
||||
float a = u_alpha;
|
||||
outColor = vec4(color * a, a);
|
||||
} else {
|
||||
outColor = vec4(0.0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Border detection via neighbor comparison and relation checks
|
||||
bool isBorder = false;
|
||||
bool hasFriendlyRelation = false;
|
||||
bool hasEmbargoRelation = false;
|
||||
uint nOwner = ownerAtTex(texCoord + ivec2(1, 0));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
nOwner = ownerAtTex(texCoord + ivec2(-1, 0));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
nOwner = ownerAtTex(texCoord + ivec2(0, 1));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
nOwner = ownerAtTex(texCoord + ivec2(0, -1));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
|
||||
if (u_alternativeView) {
|
||||
uint relationAlt = relationCode(owner, uint(u_viewerId));
|
||||
vec4 altColor = u_altNeutral;
|
||||
if (isSelf(relationAlt)) {
|
||||
altColor = u_altSelf;
|
||||
} else if (isFriendly(relationAlt)) {
|
||||
altColor = u_altAlly;
|
||||
} else if (isEmbargo(relationAlt)) {
|
||||
altColor = u_altEnemy;
|
||||
}
|
||||
float a = isBorder ? 1.0 : 0.0;
|
||||
vec3 color = altColor.rgb;
|
||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||
float pulse = u_hoverPulseStrength > 0.0
|
||||
? (1.0 - u_hoverPulseStrength) +
|
||||
u_hoverPulseStrength * (0.5 + 0.5 * sin(u_time * u_hoverPulseSpeed))
|
||||
: 1.0;
|
||||
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
|
||||
}
|
||||
outColor = vec4(color * a, a);
|
||||
} else {
|
||||
outColor = vec4(0.0);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Border detection via neighbor comparison and relation checks
|
||||
bool isBorder = false;
|
||||
bool hasFriendlyRelation = false;
|
||||
bool hasEmbargoRelation = false;
|
||||
uint nOwner = ownerAtTex(texCoord + ivec2(1, 0));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
nOwner = ownerAtTex(texCoord + ivec2(-1, 0));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
nOwner = ownerAtTex(texCoord + ivec2(0, 1));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
nOwner = ownerAtTex(texCoord + ivec2(0, -1));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
uint rel = relationCode(owner, nOwner);
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
// Current owner color
|
||||
vec4 base = texelFetch(u_palette, ivec2(int(owner) * 2, 0), 0); // territory color
|
||||
vec4 baseBorder = texelFetch(u_palette, ivec2(int(owner) * 2 + 1, 0), 0); // base border color
|
||||
vec3 color = base.rgb;
|
||||
float a = u_alpha;
|
||||
|
||||
if (u_alternativeView) {
|
||||
uint relationAlt = relationCode(owner, uint(u_viewerId));
|
||||
vec4 altColor = u_altNeutral;
|
||||
if (isSelf(relationAlt)) {
|
||||
altColor = u_altSelf;
|
||||
} else if (isFriendly(relationAlt)) {
|
||||
altColor = u_altAlly;
|
||||
} else if (isEmbargo(relationAlt)) {
|
||||
altColor = u_altEnemy;
|
||||
if (isBorder) {
|
||||
// Start with base border color and apply relation tint
|
||||
vec3 borderColor = baseBorder.rgb;
|
||||
|
||||
// Apply relation-based tinting (same logic as PlayerView.borderColor)
|
||||
const float BORDER_TINT_RATIO = 0.35;
|
||||
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0); // green
|
||||
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0); // red
|
||||
|
||||
if (hasFriendlyRelation) { // friendly
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
if (hasEmbargoRelation) { // embargo
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
|
||||
// Apply defended checkerboard pattern
|
||||
if (isDefended) {
|
||||
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
||||
const float LIGHT_FACTOR = 1.2;
|
||||
const float DARK_FACTOR = 0.8;
|
||||
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
|
||||
}
|
||||
|
||||
color = borderColor;
|
||||
a = baseBorder.a; // Already in 0-1 range from RGBA8 texture
|
||||
}
|
||||
float a = isBorder ? 1.0 : 0.0;
|
||||
vec3 color = altColor.rgb;
|
||||
|
||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||
float pulse = u_hoverPulseStrength > 0.0
|
||||
? (1.0 - u_hoverPulseStrength) +
|
||||
@@ -847,56 +1159,20 @@ export class TerritoryWebGLRenderer {
|
||||
: 1.0;
|
||||
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
|
||||
}
|
||||
outColor = vec4(color * a, a);
|
||||
return;
|
||||
}
|
||||
|
||||
vec4 base = texelFetch(u_palette, ivec2(int(owner) * 2, 0), 0); // territory color
|
||||
vec4 baseBorder = texelFetch(u_palette, ivec2(int(owner) * 2 + 1, 0), 0); // base border color
|
||||
vec3 color = base.rgb;
|
||||
float a = u_alpha;
|
||||
vec4 currColor = vec4(color * a, a);
|
||||
|
||||
if (isBorder) {
|
||||
// Start with base border color and apply relation tint
|
||||
vec3 borderColor = baseBorder.rgb;
|
||||
|
||||
// Apply relation-based tinting (same logic as PlayerView.borderColor)
|
||||
const float BORDER_TINT_RATIO = 0.35;
|
||||
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0); // green
|
||||
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0); // red
|
||||
|
||||
if (hasFriendlyRelation) { // friendly
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
if (hasEmbargoRelation) { // embargo
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
// Simple arrival-based gating: tiles that changed this tick
|
||||
// remain invisible until their local arrivalPhase is reached.
|
||||
bool tileChangedThisTick = (changeParity == u_tickParity) && (arrivalPhase < 1.0);
|
||||
if (tileChangedThisTick && u_tickProgress < arrivalPhase) {
|
||||
outColor = vec4(0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply defended checkerboard pattern
|
||||
if (isDefended) {
|
||||
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
||||
const float LIGHT_FACTOR = 1.2;
|
||||
const float DARK_FACTOR = 0.8;
|
||||
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
|
||||
}
|
||||
|
||||
color = borderColor;
|
||||
a = baseBorder.a; // Already in 0-1 range from RGBA8 texture
|
||||
outColor = currColor;
|
||||
}
|
||||
|
||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||
float pulse = u_hoverPulseStrength > 0.0
|
||||
? (1.0 - u_hoverPulseStrength) +
|
||||
u_hoverPulseStrength * (0.5 + 0.5 * sin(u_time * u_hoverPulseSpeed))
|
||||
: 1.0;
|
||||
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
|
||||
}
|
||||
|
||||
outColor = vec4(color * a, a);
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
||||
const vertexShader = this.compileShader(
|
||||
gl,
|
||||
|
||||
Reference in New Issue
Block a user