mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
3874 lines
134 KiB
TypeScript
3874 lines
134 KiB
TypeScript
import { base64url } from "jose";
|
|
import { DefaultPattern } from "../../../core/CosmeticSchemas";
|
|
import { Theme } from "../../../core/configuration/Config";
|
|
import { TileRef } from "../../../core/game/GameMap";
|
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
|
import { UserSettings } from "../../../core/game/UserSettings";
|
|
import { FrameProfiler } from "../FrameProfiler";
|
|
|
|
type DirtySpan = { minX: number; maxX: number };
|
|
|
|
export interface TerritoryWebGLCreateResult {
|
|
renderer: TerritoryWebGLRenderer | null;
|
|
reason?: string;
|
|
}
|
|
|
|
export interface HoverHighlightOptions {
|
|
color?: { r: number; g: number; b: number };
|
|
strength?: number;
|
|
pulseStrength?: number;
|
|
pulseSpeed?: number;
|
|
}
|
|
|
|
const PATTERN_STRIDE_BYTES = 1052;
|
|
|
|
// WebGL2 territory renderer that shades tiles from packed tile state
|
|
// (Uint16Array) using palette, relation, and pattern textures.
|
|
export class TerritoryWebGLRenderer {
|
|
public readonly canvas: HTMLCanvasElement;
|
|
|
|
private contestEnabled = false;
|
|
private contestPatternMode: 0 | 1 | 2 = 0; // 0=blueNoise(strength), 1=checkerboard(50/50), 2=bayer4x4(strength)
|
|
private debugDisableStaticBorders = false;
|
|
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;
|
|
private readonly vao: WebGLVertexArrayObject | null;
|
|
private readonly vertexBuffer: WebGLBuffer | null;
|
|
private readonly jfaVao: WebGLVertexArrayObject | null;
|
|
private readonly jfaVertexBuffer: WebGLBuffer | null;
|
|
private readonly stateTexture: WebGLTexture | null;
|
|
private readonly terrainTexture: WebGLTexture | null;
|
|
private readonly paletteTexture: WebGLTexture | null;
|
|
private readonly relationTexture: WebGLTexture | null;
|
|
private readonly patternTexture: WebGLTexture | null;
|
|
private readonly contestOwnersTexture: WebGLTexture | null;
|
|
private readonly contestIdsTexture: WebGLTexture | null;
|
|
private readonly contestTimesTexture: WebGLTexture | null;
|
|
private readonly contestStrengthsTexture: WebGLTexture | null;
|
|
private readonly prevOwnerTexture: WebGLTexture | null;
|
|
private readonly olderOwnerTexture: WebGLTexture | null;
|
|
private readonly stateFramebuffer: WebGLFramebuffer | null;
|
|
private readonly prevStateFramebuffer: WebGLFramebuffer | null;
|
|
private readonly olderStateFramebuffer: WebGLFramebuffer | null;
|
|
private readonly jfaTextureA: WebGLTexture | null;
|
|
private readonly jfaTextureB: WebGLTexture | null;
|
|
private readonly jfaFramebufferA: WebGLFramebuffer | null;
|
|
private readonly jfaFramebufferB: WebGLFramebuffer | null;
|
|
private readonly jfaResultOlderTexture: WebGLTexture | null;
|
|
private readonly jfaResultOldTexture: WebGLTexture | null;
|
|
private readonly jfaResultNewTexture: WebGLTexture | null;
|
|
private readonly jfaResultOlderFramebuffer: WebGLFramebuffer | null;
|
|
private readonly jfaResultOldFramebuffer: WebGLFramebuffer | null;
|
|
private readonly jfaResultNewFramebuffer: WebGLFramebuffer | null;
|
|
private readonly jfaSeedProgram: WebGLProgram | null;
|
|
private readonly jfaProgram: WebGLProgram | null;
|
|
private readonly changeMaskProgram: WebGLProgram | null;
|
|
private readonly changeMaskTextureOlder: WebGLTexture | null;
|
|
private readonly changeMaskTextureOld: WebGLTexture | null;
|
|
private readonly changeMaskTextureNew: WebGLTexture | null;
|
|
private readonly changeMaskFramebufferOlder: WebGLFramebuffer | null;
|
|
private readonly changeMaskFramebufferOld: WebGLFramebuffer | null;
|
|
private readonly changeMaskFramebufferNew: WebGLFramebuffer | null;
|
|
private readonly jfaSeedUniforms: {
|
|
resolution: WebGLUniformLocation | null;
|
|
owner: WebGLUniformLocation | null;
|
|
};
|
|
private readonly jfaUniforms: {
|
|
resolution: WebGLUniformLocation | null;
|
|
step: WebGLUniformLocation | null;
|
|
seeds: WebGLUniformLocation | null;
|
|
};
|
|
private readonly changeMaskUniforms: {
|
|
resolution: WebGLUniformLocation | null;
|
|
oldTexture: WebGLUniformLocation | null;
|
|
newTexture: WebGLUniformLocation | null;
|
|
};
|
|
private readonly uniforms: {
|
|
mapResolution: WebGLUniformLocation | null;
|
|
viewResolution: WebGLUniformLocation | null;
|
|
viewScale: WebGLUniformLocation | null;
|
|
viewOffset: WebGLUniformLocation | null;
|
|
state: WebGLUniformLocation | null;
|
|
terrain: WebGLUniformLocation | null;
|
|
latestState: WebGLUniformLocation | null;
|
|
palette: WebGLUniformLocation | null;
|
|
relations: WebGLUniformLocation | null;
|
|
patterns: WebGLUniformLocation | null;
|
|
contestEnabled: WebGLUniformLocation | null;
|
|
contestPatternMode: WebGLUniformLocation | null;
|
|
debugDisableStaticBorders: WebGLUniformLocation | null;
|
|
debugDisableAllBorders: WebGLUniformLocation | null;
|
|
seedSamplingMode: WebGLUniformLocation | null;
|
|
debugStripeFixedColors: WebGLUniformLocation | null;
|
|
motionMode: WebGLUniformLocation | null;
|
|
contestOwners: WebGLUniformLocation | null;
|
|
contestIds: WebGLUniformLocation | null;
|
|
contestTimes: WebGLUniformLocation | null;
|
|
contestStrengths: WebGLUniformLocation | null;
|
|
jfaAvailable: WebGLUniformLocation | null;
|
|
contestNow: WebGLUniformLocation | null;
|
|
contestDuration: WebGLUniformLocation | null;
|
|
prevOwner: WebGLUniformLocation | null;
|
|
jfaSeedsOld: WebGLUniformLocation | null;
|
|
jfaSeedsNew: WebGLUniformLocation | null;
|
|
smoothProgress: WebGLUniformLocation | null;
|
|
changeMask: WebGLUniformLocation | null;
|
|
smoothEnabled: WebGLUniformLocation | null;
|
|
patternStride: WebGLUniformLocation | null;
|
|
patternRows: WebGLUniformLocation | null;
|
|
fallout: WebGLUniformLocation | null;
|
|
altSelf: WebGLUniformLocation | null;
|
|
altAlly: WebGLUniformLocation | null;
|
|
altNeutral: WebGLUniformLocation | null;
|
|
altEnemy: WebGLUniformLocation | null;
|
|
alpha: WebGLUniformLocation | null;
|
|
alternativeView: WebGLUniformLocation | null;
|
|
hoveredPlayerId: WebGLUniformLocation | null;
|
|
hoverHighlightStrength: WebGLUniformLocation | null;
|
|
hoverHighlightColor: WebGLUniformLocation | null;
|
|
hoverPulseStrength: WebGLUniformLocation | null;
|
|
hoverPulseSpeed: WebGLUniformLocation | null;
|
|
time: WebGLUniformLocation | null;
|
|
viewerId: WebGLUniformLocation | null;
|
|
darkMode: WebGLUniformLocation | null;
|
|
};
|
|
|
|
private readonly mapWidth: number;
|
|
private readonly mapHeight: number;
|
|
private viewWidth: number;
|
|
private viewHeight: number;
|
|
private viewScale = 1;
|
|
private viewOffsetX = 0;
|
|
private viewOffsetY = 0;
|
|
|
|
private readonly state: Uint16Array;
|
|
private contestOwnersState: Uint16Array;
|
|
private contestIdsState: Uint16Array;
|
|
private contestTimesState: Uint16Array;
|
|
private contestStrengthsState: Uint16Array;
|
|
private readonly dirtyRows: Map<number, DirtySpan> = new Map();
|
|
private readonly contestDirtyRows: Map<number, DirtySpan> = new Map();
|
|
private needsFullUpload = true;
|
|
private needsContestFullUpload = true;
|
|
private needsContestTimesUpload = true;
|
|
private needsContestStrengthsUpload = true;
|
|
private alternativeView = false;
|
|
private paletteWidth = 0;
|
|
// Defaults are overridden by setHoverHighlightOptions() from TerritoryLayer.
|
|
private hoverHighlightStrength = 0.3;
|
|
// Defaults are overridden by setHoverHighlightOptions() from TerritoryLayer.
|
|
private hoverHighlightColor: [number, number, number] = [1, 1, 1];
|
|
// Defaults are overridden by setHoverHighlightOptions() from TerritoryLayer.
|
|
private hoverPulseStrength = 0.25;
|
|
// Defaults are overridden by setHoverHighlightOptions() from TerritoryLayer.
|
|
private hoverPulseSpeed = Math.PI * 2;
|
|
private hoveredPlayerId = -1;
|
|
private hoverStartTime = 0;
|
|
private static readonly HOVER_DURATION_MS = 5000;
|
|
private animationStartTime = Date.now();
|
|
private contestNow = 0;
|
|
private contestDurationTicks = 0;
|
|
private smoothProgress = 1;
|
|
private smoothEnabled = true;
|
|
private jfaSupported = false;
|
|
private jfaDisabledReason: string | null = null;
|
|
private jfaDirty = false;
|
|
private jfaHistoryInitialized = false;
|
|
private changeMaskDirty = false;
|
|
private changeMaskHistoryInitialized = false;
|
|
private prevStateCopySupported = false;
|
|
private jfaSteps: number[] = [];
|
|
private interpolationPair: "prevCurrent" | "olderPrev" = "prevCurrent";
|
|
private readonly userSettings = new UserSettings();
|
|
private readonly patternBytesCache = new Map<string, Uint8Array>();
|
|
|
|
private constructor(
|
|
private readonly game: GameView,
|
|
private readonly theme: Theme,
|
|
state: Uint16Array,
|
|
) {
|
|
this.canvas = document.createElement("canvas");
|
|
this.mapWidth = game.width();
|
|
this.mapHeight = game.height();
|
|
this.viewWidth = this.mapWidth;
|
|
this.viewHeight = this.mapHeight;
|
|
this.canvas.width = this.viewWidth;
|
|
this.canvas.height = this.viewHeight;
|
|
|
|
this.state = state;
|
|
this.contestOwnersState = new Uint16Array(state.length * 2);
|
|
this.contestIdsState = new Uint16Array(state.length);
|
|
this.contestTimesState = new Uint16Array(1);
|
|
this.contestStrengthsState = new Uint16Array(1);
|
|
|
|
this.gl = this.canvas.getContext("webgl2", {
|
|
premultipliedAlpha: true,
|
|
antialias: false,
|
|
preserveDrawingBuffer: true,
|
|
});
|
|
|
|
if (!this.gl) {
|
|
this.program = null;
|
|
this.vao = null;
|
|
this.vertexBuffer = null;
|
|
this.jfaVao = null;
|
|
this.jfaVertexBuffer = null;
|
|
this.stateTexture = null;
|
|
this.terrainTexture = null;
|
|
this.paletteTexture = null;
|
|
this.relationTexture = null;
|
|
this.patternTexture = null;
|
|
this.contestOwnersTexture = null;
|
|
this.contestIdsTexture = null;
|
|
this.contestTimesTexture = null;
|
|
this.contestStrengthsTexture = null;
|
|
this.prevOwnerTexture = null;
|
|
this.olderOwnerTexture = null;
|
|
this.stateFramebuffer = null;
|
|
this.prevStateFramebuffer = null;
|
|
this.olderStateFramebuffer = null;
|
|
this.jfaTextureA = null;
|
|
this.jfaTextureB = null;
|
|
this.jfaFramebufferA = null;
|
|
this.jfaFramebufferB = null;
|
|
this.jfaResultOlderTexture = null;
|
|
this.jfaResultOldTexture = null;
|
|
this.jfaResultNewTexture = null;
|
|
this.jfaResultOlderFramebuffer = null;
|
|
this.jfaResultOldFramebuffer = null;
|
|
this.jfaResultNewFramebuffer = null;
|
|
this.jfaSeedProgram = null;
|
|
this.jfaProgram = null;
|
|
this.changeMaskProgram = null;
|
|
this.changeMaskTextureOlder = null;
|
|
this.changeMaskTextureOld = null;
|
|
this.changeMaskTextureNew = null;
|
|
this.changeMaskFramebufferOlder = null;
|
|
this.changeMaskFramebufferOld = null;
|
|
this.changeMaskFramebufferNew = null;
|
|
this.jfaSeedUniforms = { resolution: null, owner: null };
|
|
this.jfaUniforms = { resolution: null, step: null, seeds: null };
|
|
this.changeMaskUniforms = {
|
|
resolution: null,
|
|
oldTexture: null,
|
|
newTexture: null,
|
|
};
|
|
this.uniforms = {
|
|
mapResolution: null,
|
|
viewResolution: null,
|
|
viewScale: null,
|
|
viewOffset: null,
|
|
state: null,
|
|
terrain: null,
|
|
latestState: null,
|
|
palette: null,
|
|
relations: null,
|
|
patterns: null,
|
|
contestEnabled: null,
|
|
contestPatternMode: null,
|
|
debugDisableStaticBorders: null,
|
|
debugDisableAllBorders: null,
|
|
seedSamplingMode: null,
|
|
debugStripeFixedColors: null,
|
|
motionMode: null,
|
|
contestOwners: null,
|
|
contestIds: null,
|
|
contestTimes: null,
|
|
contestStrengths: null,
|
|
jfaAvailable: null,
|
|
contestNow: null,
|
|
contestDuration: null,
|
|
prevOwner: null,
|
|
jfaSeedsOld: null,
|
|
jfaSeedsNew: null,
|
|
smoothProgress: null,
|
|
changeMask: null,
|
|
smoothEnabled: null,
|
|
patternStride: null,
|
|
patternRows: null,
|
|
fallout: null,
|
|
altSelf: null,
|
|
altAlly: null,
|
|
altNeutral: null,
|
|
altEnemy: null,
|
|
alpha: null,
|
|
alternativeView: null,
|
|
hoveredPlayerId: null,
|
|
hoverHighlightStrength: null,
|
|
hoverHighlightColor: null,
|
|
hoverPulseStrength: null,
|
|
hoverPulseSpeed: null,
|
|
time: null,
|
|
viewerId: null,
|
|
darkMode: null,
|
|
};
|
|
return;
|
|
}
|
|
|
|
const gl = this.gl;
|
|
this.program = this.createProgram(gl);
|
|
if (!this.program) {
|
|
this.vao = null;
|
|
this.vertexBuffer = null;
|
|
this.jfaVao = null;
|
|
this.jfaVertexBuffer = null;
|
|
this.stateTexture = null;
|
|
this.terrainTexture = null;
|
|
this.paletteTexture = null;
|
|
this.relationTexture = null;
|
|
this.patternTexture = null;
|
|
this.contestOwnersTexture = null;
|
|
this.contestIdsTexture = null;
|
|
this.contestTimesTexture = null;
|
|
this.contestStrengthsTexture = null;
|
|
this.prevOwnerTexture = null;
|
|
this.olderOwnerTexture = null;
|
|
this.stateFramebuffer = null;
|
|
this.prevStateFramebuffer = null;
|
|
this.olderStateFramebuffer = null;
|
|
this.jfaTextureA = null;
|
|
this.jfaTextureB = null;
|
|
this.jfaFramebufferA = null;
|
|
this.jfaFramebufferB = null;
|
|
this.jfaResultOlderTexture = null;
|
|
this.jfaResultOldTexture = null;
|
|
this.jfaResultNewTexture = null;
|
|
this.jfaResultOlderFramebuffer = null;
|
|
this.jfaResultOldFramebuffer = null;
|
|
this.jfaResultNewFramebuffer = null;
|
|
this.jfaSeedProgram = null;
|
|
this.jfaProgram = null;
|
|
this.changeMaskProgram = null;
|
|
this.changeMaskTextureOlder = null;
|
|
this.changeMaskTextureOld = null;
|
|
this.changeMaskTextureNew = null;
|
|
this.changeMaskFramebufferOlder = null;
|
|
this.changeMaskFramebufferOld = null;
|
|
this.changeMaskFramebufferNew = null;
|
|
this.jfaSeedUniforms = { resolution: null, owner: null };
|
|
this.jfaUniforms = { resolution: null, step: null, seeds: null };
|
|
this.changeMaskUniforms = {
|
|
resolution: null,
|
|
oldTexture: null,
|
|
newTexture: null,
|
|
};
|
|
this.uniforms = {
|
|
mapResolution: null,
|
|
viewResolution: null,
|
|
viewScale: null,
|
|
viewOffset: null,
|
|
state: null,
|
|
terrain: null,
|
|
latestState: null,
|
|
palette: null,
|
|
relations: null,
|
|
patterns: null,
|
|
contestEnabled: null,
|
|
contestPatternMode: null,
|
|
debugDisableStaticBorders: null,
|
|
debugDisableAllBorders: null,
|
|
seedSamplingMode: null,
|
|
debugStripeFixedColors: null,
|
|
motionMode: null,
|
|
contestOwners: null,
|
|
contestIds: null,
|
|
contestTimes: null,
|
|
contestStrengths: null,
|
|
jfaAvailable: null,
|
|
contestNow: null,
|
|
contestDuration: null,
|
|
prevOwner: null,
|
|
jfaSeedsOld: null,
|
|
jfaSeedsNew: null,
|
|
smoothProgress: null,
|
|
changeMask: null,
|
|
smoothEnabled: null,
|
|
patternStride: null,
|
|
patternRows: null,
|
|
fallout: null,
|
|
altSelf: null,
|
|
altAlly: null,
|
|
altNeutral: null,
|
|
altEnemy: null,
|
|
alpha: null,
|
|
alternativeView: null,
|
|
hoveredPlayerId: null,
|
|
hoverHighlightStrength: null,
|
|
hoverHighlightColor: null,
|
|
hoverPulseStrength: null,
|
|
hoverPulseSpeed: null,
|
|
time: null,
|
|
viewerId: null,
|
|
darkMode: null,
|
|
};
|
|
return;
|
|
}
|
|
|
|
this.jfaSupported = !!gl.getExtension("EXT_color_buffer_float");
|
|
if (!this.jfaSupported) {
|
|
this.jfaDisabledReason = "EXT_color_buffer_float unavailable";
|
|
}
|
|
this.jfaSeedProgram = this.jfaSupported
|
|
? this.createJfaSeedProgram(gl)
|
|
: null;
|
|
this.jfaProgram = this.jfaSupported ? this.createJfaProgram(gl) : null;
|
|
this.changeMaskProgram = this.jfaSupported
|
|
? this.createChangeMaskProgram(gl)
|
|
: null;
|
|
if (!this.jfaSeedProgram || !this.jfaProgram) {
|
|
this.jfaSupported = false;
|
|
this.jfaDisabledReason ??= "JFA shaders unavailable";
|
|
}
|
|
this.jfaSeedUniforms = this.jfaSeedProgram
|
|
? {
|
|
resolution: gl.getUniformLocation(
|
|
this.jfaSeedProgram,
|
|
"u_resolution",
|
|
),
|
|
owner: gl.getUniformLocation(this.jfaSeedProgram, "u_ownerTexture"),
|
|
}
|
|
: { resolution: null, owner: null };
|
|
this.jfaUniforms = this.jfaProgram
|
|
? {
|
|
resolution: gl.getUniformLocation(this.jfaProgram, "u_resolution"),
|
|
step: gl.getUniformLocation(this.jfaProgram, "u_step"),
|
|
seeds: gl.getUniformLocation(this.jfaProgram, "u_seeds"),
|
|
}
|
|
: { resolution: null, step: null, seeds: null };
|
|
this.changeMaskUniforms = this.changeMaskProgram
|
|
? {
|
|
resolution: gl.getUniformLocation(
|
|
this.changeMaskProgram,
|
|
"u_resolution",
|
|
),
|
|
oldTexture: gl.getUniformLocation(
|
|
this.changeMaskProgram,
|
|
"u_oldTexture",
|
|
),
|
|
newTexture: gl.getUniformLocation(
|
|
this.changeMaskProgram,
|
|
"u_newTexture",
|
|
),
|
|
}
|
|
: { resolution: null, oldTexture: null, newTexture: null };
|
|
|
|
this.uniforms = {
|
|
mapResolution: gl.getUniformLocation(this.program, "u_mapResolution"),
|
|
viewResolution: gl.getUniformLocation(this.program, "u_viewResolution"),
|
|
viewScale: gl.getUniformLocation(this.program, "u_viewScale"),
|
|
viewOffset: gl.getUniformLocation(this.program, "u_viewOffset"),
|
|
state: gl.getUniformLocation(this.program, "u_state"),
|
|
terrain: gl.getUniformLocation(this.program, "u_terrain"),
|
|
latestState: gl.getUniformLocation(this.program, "u_latestState"),
|
|
palette: gl.getUniformLocation(this.program, "u_palette"),
|
|
relations: gl.getUniformLocation(this.program, "u_relations"),
|
|
patterns: gl.getUniformLocation(this.program, "u_patterns"),
|
|
contestEnabled: gl.getUniformLocation(this.program, "u_contestEnabled"),
|
|
contestPatternMode: gl.getUniformLocation(
|
|
this.program,
|
|
"u_contestPatternMode",
|
|
),
|
|
debugDisableStaticBorders: gl.getUniformLocation(
|
|
this.program,
|
|
"u_debugDisableStaticBorders",
|
|
),
|
|
debugDisableAllBorders: gl.getUniformLocation(
|
|
this.program,
|
|
"u_debugDisableAllBorders",
|
|
),
|
|
seedSamplingMode: gl.getUniformLocation(
|
|
this.program,
|
|
"u_seedSamplingMode",
|
|
),
|
|
debugStripeFixedColors: gl.getUniformLocation(
|
|
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"),
|
|
contestStrengths: gl.getUniformLocation(
|
|
this.program,
|
|
"u_contestStrengths",
|
|
),
|
|
jfaAvailable: gl.getUniformLocation(this.program, "u_jfaAvailable"),
|
|
contestNow: gl.getUniformLocation(this.program, "u_contestNow"),
|
|
contestDuration: gl.getUniformLocation(
|
|
this.program,
|
|
"u_contestDurationTicks",
|
|
),
|
|
prevOwner: gl.getUniformLocation(this.program, "u_prevOwner"),
|
|
jfaSeedsOld: gl.getUniformLocation(this.program, "u_jfaSeedsOld"),
|
|
jfaSeedsNew: gl.getUniformLocation(this.program, "u_jfaSeedsNew"),
|
|
smoothProgress: gl.getUniformLocation(this.program, "u_smoothProgress"),
|
|
changeMask: gl.getUniformLocation(this.program, "u_changeMask"),
|
|
smoothEnabled: gl.getUniformLocation(this.program, "u_smoothEnabled"),
|
|
patternStride: gl.getUniformLocation(this.program, "u_patternStride"),
|
|
patternRows: gl.getUniformLocation(this.program, "u_patternRows"),
|
|
fallout: gl.getUniformLocation(this.program, "u_fallout"),
|
|
altSelf: gl.getUniformLocation(this.program, "u_altSelf"),
|
|
altAlly: gl.getUniformLocation(this.program, "u_altAlly"),
|
|
altNeutral: gl.getUniformLocation(this.program, "u_altNeutral"),
|
|
altEnemy: gl.getUniformLocation(this.program, "u_altEnemy"),
|
|
alpha: gl.getUniformLocation(this.program, "u_alpha"),
|
|
alternativeView: gl.getUniformLocation(this.program, "u_alternativeView"),
|
|
hoveredPlayerId: gl.getUniformLocation(this.program, "u_hoveredPlayerId"),
|
|
hoverHighlightStrength: gl.getUniformLocation(
|
|
this.program,
|
|
"u_hoverHighlightStrength",
|
|
),
|
|
hoverHighlightColor: gl.getUniformLocation(
|
|
this.program,
|
|
"u_hoverHighlightColor",
|
|
),
|
|
hoverPulseStrength: gl.getUniformLocation(
|
|
this.program,
|
|
"u_hoverPulseStrength",
|
|
),
|
|
hoverPulseSpeed: gl.getUniformLocation(this.program, "u_hoverPulseSpeed"),
|
|
time: gl.getUniformLocation(this.program, "u_time"),
|
|
viewerId: gl.getUniformLocation(this.program, "u_viewerId"),
|
|
darkMode: gl.getUniformLocation(this.program, "u_darkMode"),
|
|
};
|
|
|
|
// Vertex data: two triangles covering the full view (pixel-perfect).
|
|
const vertices = new Float32Array([
|
|
0,
|
|
0,
|
|
this.viewWidth,
|
|
0,
|
|
0,
|
|
this.viewHeight,
|
|
0,
|
|
this.viewHeight,
|
|
this.viewWidth,
|
|
0,
|
|
this.viewWidth,
|
|
this.viewHeight,
|
|
]);
|
|
|
|
this.vao = gl.createVertexArray();
|
|
this.vertexBuffer = gl.createBuffer();
|
|
gl.bindVertexArray(this.vao);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
|
|
const posLoc = gl.getAttribLocation(this.program, "a_position");
|
|
gl.enableVertexAttribArray(posLoc);
|
|
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 2 * 4, 0);
|
|
gl.bindVertexArray(null);
|
|
|
|
const mapVertices = new Float32Array([
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
0,
|
|
0,
|
|
this.mapHeight,
|
|
0,
|
|
this.mapHeight,
|
|
this.mapWidth,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
]);
|
|
this.jfaVao = gl.createVertexArray();
|
|
this.jfaVertexBuffer = gl.createBuffer();
|
|
gl.bindVertexArray(this.jfaVao);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.jfaVertexBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, mapVertices, gl.STATIC_DRAW);
|
|
gl.enableVertexAttribArray(posLoc);
|
|
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 2 * 4, 0);
|
|
gl.bindVertexArray(null);
|
|
|
|
this.stateTexture = gl.createTexture();
|
|
this.terrainTexture = gl.createTexture();
|
|
this.paletteTexture = gl.createTexture();
|
|
this.relationTexture = gl.createTexture();
|
|
this.patternTexture = gl.createTexture();
|
|
this.contestOwnersTexture = gl.createTexture();
|
|
this.contestIdsTexture = gl.createTexture();
|
|
this.contestTimesTexture = gl.createTexture();
|
|
this.contestStrengthsTexture = gl.createTexture();
|
|
this.prevOwnerTexture = gl.createTexture();
|
|
this.olderOwnerTexture = gl.createTexture();
|
|
this.stateFramebuffer = gl.createFramebuffer();
|
|
this.prevStateFramebuffer = gl.createFramebuffer();
|
|
this.olderStateFramebuffer = gl.createFramebuffer();
|
|
this.jfaTextureA = this.jfaSupported ? gl.createTexture() : null;
|
|
this.jfaTextureB = this.jfaSupported ? gl.createTexture() : null;
|
|
this.jfaFramebufferA = this.jfaSupported ? gl.createFramebuffer() : null;
|
|
this.jfaFramebufferB = this.jfaSupported ? gl.createFramebuffer() : null;
|
|
this.jfaResultOlderTexture = this.jfaSupported ? gl.createTexture() : null;
|
|
this.jfaResultOldTexture = this.jfaSupported ? gl.createTexture() : null;
|
|
this.jfaResultNewTexture = this.jfaSupported ? gl.createTexture() : null;
|
|
this.jfaResultOlderFramebuffer = this.jfaSupported
|
|
? gl.createFramebuffer()
|
|
: null;
|
|
this.jfaResultOldFramebuffer = this.jfaSupported
|
|
? gl.createFramebuffer()
|
|
: null;
|
|
this.jfaResultNewFramebuffer = this.jfaSupported
|
|
? gl.createFramebuffer()
|
|
: null;
|
|
this.changeMaskTextureOlder = this.jfaSupported ? gl.createTexture() : null;
|
|
this.changeMaskTextureOld = this.jfaSupported ? gl.createTexture() : null;
|
|
this.changeMaskTextureNew = this.jfaSupported ? gl.createTexture() : null;
|
|
this.changeMaskFramebufferOlder = this.jfaSupported
|
|
? gl.createFramebuffer()
|
|
: null;
|
|
this.changeMaskFramebufferOld = this.jfaSupported
|
|
? gl.createFramebuffer()
|
|
: null;
|
|
this.changeMaskFramebufferNew = this.jfaSupported
|
|
? gl.createFramebuffer()
|
|
: null;
|
|
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
|
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.R16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.state,
|
|
);
|
|
|
|
// Terrain texture (immutable, only uploaded once)
|
|
gl.activeTexture(gl.TEXTURE14);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.terrainTexture);
|
|
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.R8UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_BYTE,
|
|
game.terrainDataView(),
|
|
);
|
|
|
|
this.uploadPalette();
|
|
|
|
gl.activeTexture(gl.TEXTURE4);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestOwnersTexture);
|
|
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.RG16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RG_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestOwnersState,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE5);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestIdsTexture);
|
|
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.R16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestIdsState,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE6);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestTimesTexture);
|
|
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.R16UI,
|
|
this.contestTimesState.length,
|
|
1,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestTimesState,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE11);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestStrengthsTexture);
|
|
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.R16UI,
|
|
this.contestStrengthsState.length,
|
|
1,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestStrengthsState,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE7);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.prevOwnerTexture);
|
|
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.R16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.state,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE13);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.olderOwnerTexture);
|
|
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.R16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.state,
|
|
);
|
|
|
|
if (
|
|
this.stateFramebuffer &&
|
|
this.prevStateFramebuffer &&
|
|
this.olderStateFramebuffer &&
|
|
this.stateTexture &&
|
|
this.prevOwnerTexture &&
|
|
this.olderOwnerTexture
|
|
) {
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.stateFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.stateTexture,
|
|
0,
|
|
);
|
|
const stateStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.prevStateFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.prevOwnerTexture,
|
|
0,
|
|
);
|
|
const prevStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.olderStateFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.olderOwnerTexture,
|
|
0,
|
|
);
|
|
const olderStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
this.prevStateCopySupported =
|
|
stateStatus === gl.FRAMEBUFFER_COMPLETE &&
|
|
prevStatus === gl.FRAMEBUFFER_COMPLETE &&
|
|
olderStatus === gl.FRAMEBUFFER_COMPLETE;
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
}
|
|
|
|
if (
|
|
this.jfaSupported &&
|
|
this.jfaTextureA &&
|
|
this.jfaTextureB &&
|
|
this.jfaFramebufferA &&
|
|
this.jfaFramebufferB &&
|
|
this.jfaResultOlderTexture &&
|
|
this.jfaResultOldTexture &&
|
|
this.jfaResultNewTexture &&
|
|
this.jfaResultOlderFramebuffer &&
|
|
this.jfaResultOldFramebuffer &&
|
|
this.jfaResultNewFramebuffer
|
|
) {
|
|
gl.activeTexture(gl.TEXTURE9);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.jfaTextureA);
|
|
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.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RG16F,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RG,
|
|
gl.HALF_FLOAT,
|
|
null,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE10);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.jfaTextureB);
|
|
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.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RG16F,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RG,
|
|
gl.HALF_FLOAT,
|
|
null,
|
|
);
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaFramebufferA);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.jfaTextureA,
|
|
0,
|
|
);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaFramebufferB);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.jfaTextureB,
|
|
0,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE12);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOlderTexture);
|
|
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.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RG16F,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RG,
|
|
gl.HALF_FLOAT,
|
|
null,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE10);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOldTexture);
|
|
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.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RG16F,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RG,
|
|
gl.HALF_FLOAT,
|
|
null,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE11);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultNewTexture);
|
|
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.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RG16F,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RG,
|
|
gl.HALF_FLOAT,
|
|
null,
|
|
);
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaResultOlderFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.jfaResultOlderTexture,
|
|
0,
|
|
);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.jfaResultOldTexture,
|
|
0,
|
|
);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaResultNewFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.jfaResultNewTexture,
|
|
0,
|
|
);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
this.jfaSteps = this.buildJfaSteps(this.mapWidth, this.mapHeight);
|
|
this.jfaDirty = true;
|
|
}
|
|
|
|
if (
|
|
this.jfaSupported &&
|
|
this.changeMaskTextureOlder &&
|
|
this.changeMaskTextureOld &&
|
|
this.changeMaskTextureNew &&
|
|
this.changeMaskFramebufferOlder &&
|
|
this.changeMaskFramebufferOld &&
|
|
this.changeMaskFramebufferNew
|
|
) {
|
|
const initMaskTex = (tex: WebGLTexture) => {
|
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
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.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.R8UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_BYTE,
|
|
null,
|
|
);
|
|
};
|
|
|
|
gl.activeTexture(gl.TEXTURE13);
|
|
initMaskTex(this.changeMaskTextureOlder);
|
|
initMaskTex(this.changeMaskTextureOld);
|
|
initMaskTex(this.changeMaskTextureNew);
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.changeMaskFramebufferOlder);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.changeMaskTextureOlder,
|
|
0,
|
|
);
|
|
gl.clearBufferuiv(gl.COLOR, 0, new Uint32Array([0, 0, 0, 0]));
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.changeMaskFramebufferOld);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.changeMaskTextureOld,
|
|
0,
|
|
);
|
|
gl.clearBufferuiv(gl.COLOR, 0, new Uint32Array([0, 0, 0, 0]));
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.changeMaskFramebufferNew);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
this.changeMaskTextureNew,
|
|
0,
|
|
);
|
|
gl.clearBufferuiv(gl.COLOR, 0, new Uint32Array([0, 0, 0, 0]));
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
this.changeMaskDirty = true;
|
|
}
|
|
|
|
gl.useProgram(this.program);
|
|
gl.uniform1i(this.uniforms.state, 0);
|
|
if (this.uniforms.terrain) {
|
|
gl.uniform1i(this.uniforms.terrain, 14);
|
|
}
|
|
if (this.uniforms.latestState) {
|
|
gl.uniform1i(this.uniforms.latestState, 12);
|
|
}
|
|
gl.uniform1i(this.uniforms.palette, 1);
|
|
gl.uniform1i(this.uniforms.relations, 2);
|
|
gl.uniform1i(this.uniforms.patterns, 3);
|
|
gl.uniform1i(this.uniforms.contestOwners, 4);
|
|
gl.uniform1i(this.uniforms.contestIds, 5);
|
|
gl.uniform1i(this.uniforms.contestTimes, 6);
|
|
gl.uniform1i(this.uniforms.contestStrengths, 11);
|
|
gl.uniform1i(this.uniforms.prevOwner, 7);
|
|
gl.uniform1i(this.uniforms.jfaSeedsOld, 8);
|
|
gl.uniform1i(this.uniforms.jfaSeedsNew, 9);
|
|
if (this.uniforms.changeMask) {
|
|
gl.uniform1i(this.uniforms.changeMask, 13);
|
|
}
|
|
|
|
if (this.uniforms.mapResolution) {
|
|
gl.uniform2f(this.uniforms.mapResolution, this.mapWidth, this.mapHeight);
|
|
}
|
|
if (this.uniforms.viewResolution) {
|
|
gl.uniform2f(
|
|
this.uniforms.viewResolution,
|
|
this.viewWidth,
|
|
this.viewHeight,
|
|
);
|
|
}
|
|
if (this.uniforms.viewScale) {
|
|
gl.uniform1f(this.uniforms.viewScale, this.viewScale);
|
|
}
|
|
if (this.uniforms.viewOffset) {
|
|
gl.uniform2f(
|
|
this.uniforms.viewOffset,
|
|
this.viewOffsetX,
|
|
this.viewOffsetY,
|
|
);
|
|
}
|
|
if (this.uniforms.alpha) {
|
|
gl.uniform1f(this.uniforms.alpha, 150 / 255);
|
|
}
|
|
if (this.uniforms.fallout) {
|
|
const f = this.theme.falloutColor().rgba;
|
|
gl.uniform4f(
|
|
this.uniforms.fallout,
|
|
f.r / 255,
|
|
f.g / 255,
|
|
f.b / 255,
|
|
f.a ?? 1,
|
|
);
|
|
}
|
|
if (this.uniforms.altSelf) {
|
|
const c = this.theme.selfColor().rgba;
|
|
gl.uniform4f(
|
|
this.uniforms.altSelf,
|
|
c.r / 255,
|
|
c.g / 255,
|
|
c.b / 255,
|
|
c.a ?? 1,
|
|
);
|
|
}
|
|
if (this.uniforms.altAlly) {
|
|
const c = this.theme.allyColor().rgba;
|
|
gl.uniform4f(
|
|
this.uniforms.altAlly,
|
|
c.r / 255,
|
|
c.g / 255,
|
|
c.b / 255,
|
|
c.a ?? 1,
|
|
);
|
|
}
|
|
if (this.uniforms.altNeutral) {
|
|
const c = this.theme.neutralColor().rgba;
|
|
gl.uniform4f(
|
|
this.uniforms.altNeutral,
|
|
c.r / 255,
|
|
c.g / 255,
|
|
c.b / 255,
|
|
c.a ?? 1,
|
|
);
|
|
}
|
|
if (this.uniforms.altEnemy) {
|
|
const c = this.theme.enemyColor().rgba;
|
|
gl.uniform4f(
|
|
this.uniforms.altEnemy,
|
|
c.r / 255,
|
|
c.g / 255,
|
|
c.b / 255,
|
|
c.a ?? 1,
|
|
);
|
|
}
|
|
if (this.uniforms.viewerId) {
|
|
const viewerId = this.game.myPlayer()?.smallID() ?? 0;
|
|
gl.uniform1i(this.uniforms.viewerId, viewerId);
|
|
}
|
|
if (this.uniforms.viewResolution) {
|
|
gl.uniform2f(
|
|
this.uniforms.viewResolution,
|
|
this.viewWidth,
|
|
this.viewHeight,
|
|
);
|
|
}
|
|
if (this.uniforms.viewScale) {
|
|
gl.uniform1f(this.uniforms.viewScale, this.viewScale);
|
|
}
|
|
if (this.uniforms.viewOffset) {
|
|
gl.uniform2f(
|
|
this.uniforms.viewOffset,
|
|
this.viewOffsetX,
|
|
this.viewOffsetY,
|
|
);
|
|
}
|
|
if (this.uniforms.alternativeView) {
|
|
gl.uniform1i(this.uniforms.alternativeView, 0);
|
|
}
|
|
if (this.uniforms.hoveredPlayerId) {
|
|
gl.uniform1f(this.uniforms.hoveredPlayerId, -1);
|
|
}
|
|
if (this.uniforms.hoverHighlightStrength) {
|
|
gl.uniform1f(
|
|
this.uniforms.hoverHighlightStrength,
|
|
this.hoverHighlightStrength,
|
|
);
|
|
}
|
|
if (this.uniforms.hoverHighlightColor) {
|
|
const [r, g, b] = this.hoverHighlightColor;
|
|
gl.uniform3f(this.uniforms.hoverHighlightColor, r, g, b);
|
|
}
|
|
if (this.uniforms.hoverPulseStrength) {
|
|
gl.uniform1f(this.uniforms.hoverPulseStrength, this.hoverPulseStrength);
|
|
}
|
|
if (this.uniforms.hoverPulseSpeed) {
|
|
gl.uniform1f(this.uniforms.hoverPulseSpeed, this.hoverPulseSpeed);
|
|
}
|
|
if (this.uniforms.jfaAvailable) {
|
|
gl.uniform1i(this.uniforms.jfaAvailable, this.jfaSupported ? 1 : 0);
|
|
}
|
|
if (this.uniforms.contestNow) {
|
|
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
|
|
}
|
|
if (this.uniforms.contestDuration) {
|
|
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationTicks);
|
|
}
|
|
if (this.uniforms.smoothProgress) {
|
|
gl.uniform1f(this.uniforms.smoothProgress, this.smoothProgress);
|
|
}
|
|
if (this.uniforms.smoothEnabled) {
|
|
gl.uniform1i(this.uniforms.smoothEnabled, this.smoothEnabled ? 1 : 0);
|
|
}
|
|
|
|
if (
|
|
this.jfaSupported &&
|
|
this.jfaResultOldTexture &&
|
|
this.jfaResultNewTexture
|
|
) {
|
|
gl.activeTexture(gl.TEXTURE8);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOldTexture);
|
|
gl.activeTexture(gl.TEXTURE9);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultNewTexture);
|
|
}
|
|
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
gl.viewport(0, 0, this.viewWidth, this.viewHeight);
|
|
}
|
|
|
|
static create(game: GameView, theme: Theme): TerritoryWebGLCreateResult {
|
|
const state = game.tileStateView();
|
|
const expected = game.width() * game.height();
|
|
if (state.length !== expected) {
|
|
return {
|
|
renderer: null,
|
|
reason: "Tile state buffer size mismatch; WebGL renderer disabled.",
|
|
};
|
|
}
|
|
|
|
const renderer = new TerritoryWebGLRenderer(game, theme, state);
|
|
if (!renderer.isValid()) {
|
|
return {
|
|
renderer: null,
|
|
reason: "WebGL2 not available; WebGL renderer disabled.",
|
|
};
|
|
}
|
|
return { renderer };
|
|
}
|
|
|
|
isValid(): boolean {
|
|
return !!this.gl && !!this.program && !!this.vao;
|
|
}
|
|
|
|
dispose(): void {
|
|
if (this.gl) {
|
|
this.gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
}
|
|
this.canvas.remove();
|
|
}
|
|
|
|
setAlternativeView(enabled: boolean) {
|
|
this.alternativeView = enabled;
|
|
}
|
|
|
|
setViewSize(width: number, height: number) {
|
|
const nextWidth = Math.max(1, Math.floor(width));
|
|
const nextHeight = Math.max(1, Math.floor(height));
|
|
if (nextWidth === this.viewWidth && nextHeight === this.viewHeight) {
|
|
return;
|
|
}
|
|
this.viewWidth = nextWidth;
|
|
this.viewHeight = nextHeight;
|
|
this.canvas.width = nextWidth;
|
|
this.canvas.height = nextHeight;
|
|
if (!this.gl || !this.vertexBuffer) {
|
|
return;
|
|
}
|
|
const gl = this.gl;
|
|
const vertices = new Float32Array([
|
|
0,
|
|
0,
|
|
this.viewWidth,
|
|
0,
|
|
0,
|
|
this.viewHeight,
|
|
0,
|
|
this.viewHeight,
|
|
this.viewWidth,
|
|
0,
|
|
this.viewWidth,
|
|
this.viewHeight,
|
|
]);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
if (this.program) {
|
|
gl.useProgram(this.program);
|
|
if (this.uniforms.viewResolution) {
|
|
gl.uniform2f(
|
|
this.uniforms.viewResolution,
|
|
this.viewWidth,
|
|
this.viewHeight,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
setViewTransform(scale: number, offsetX: number, offsetY: number) {
|
|
this.viewScale = scale;
|
|
this.viewOffsetX = offsetX;
|
|
this.viewOffsetY = offsetY;
|
|
}
|
|
|
|
setHoveredPlayerId(playerSmallId: number | null) {
|
|
const encoded = playerSmallId ?? -1;
|
|
if (encoded !== this.hoveredPlayerId) {
|
|
this.hoveredPlayerId = encoded;
|
|
this.hoverStartTime = encoded >= 0 ? Date.now() : 0;
|
|
}
|
|
}
|
|
|
|
setHoverHighlightOptions(options: HoverHighlightOptions) {
|
|
if (options.strength !== undefined) {
|
|
this.hoverHighlightStrength = Math.max(0, Math.min(1, options.strength));
|
|
}
|
|
if (options.color) {
|
|
this.hoverHighlightColor = [
|
|
options.color.r / 255,
|
|
options.color.g / 255,
|
|
options.color.b / 255,
|
|
];
|
|
}
|
|
if (options.pulseStrength !== undefined) {
|
|
this.hoverPulseStrength = Math.max(0, Math.min(1, options.pulseStrength));
|
|
}
|
|
if (options.pulseSpeed !== undefined) {
|
|
this.hoverPulseSpeed = Math.max(0, options.pulseSpeed);
|
|
}
|
|
}
|
|
|
|
setContestEnabled(enabled: boolean) {
|
|
if (this.contestEnabled === enabled) {
|
|
return;
|
|
}
|
|
this.contestEnabled = enabled;
|
|
if (this.contestEnabled) {
|
|
this.needsContestFullUpload = true;
|
|
this.needsContestTimesUpload = true;
|
|
this.needsContestStrengthsUpload = true;
|
|
} else {
|
|
this.contestDirtyRows.clear();
|
|
}
|
|
}
|
|
|
|
setContestPatternMode(mode: "blueNoise" | "checkerboard" | "bayer4x4") {
|
|
if (mode === "checkerboard") this.contestPatternMode = 1;
|
|
else if (mode === "bayer4x4") this.contestPatternMode = 2;
|
|
else this.contestPatternMode = 0;
|
|
}
|
|
|
|
setDebugDisableStaticBorders(disabled: boolean) {
|
|
this.debugDisableStaticBorders = disabled;
|
|
}
|
|
|
|
setDebugDisableAllBorders(disabled: boolean) {
|
|
this.debugDisableAllBorders = disabled;
|
|
}
|
|
|
|
setSeedSamplingMode(mode: "none" | "2x2" | "3x3") {
|
|
this.seedSamplingMode = mode === "none" ? 0 : mode === "2x2" ? 1 : 2;
|
|
}
|
|
|
|
setDebugStripeFixedColors(enabled: boolean) {
|
|
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;
|
|
}
|
|
const x = tile % this.mapWidth;
|
|
const y = Math.floor(tile / this.mapWidth);
|
|
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);
|
|
}
|
|
}
|
|
|
|
setContestTile(
|
|
tile: TileRef,
|
|
defenderOwner: number,
|
|
attackerOwner: number,
|
|
componentId: number,
|
|
attackerEver: boolean,
|
|
) {
|
|
if (!this.contestEnabled) {
|
|
return;
|
|
}
|
|
const offset = tile * 2;
|
|
const defenderValue = defenderOwner & 0xffff;
|
|
const attackerValue = attackerOwner & 0xffff;
|
|
const idValue = (componentId & 0x7fff) | (attackerEver ? 0x8000 : 0);
|
|
if (
|
|
this.contestOwnersState[offset] === defenderValue &&
|
|
this.contestOwnersState[offset + 1] === attackerValue &&
|
|
this.contestIdsState[tile] === idValue
|
|
) {
|
|
return;
|
|
}
|
|
this.contestOwnersState[offset] = defenderValue;
|
|
this.contestOwnersState[offset + 1] = attackerValue;
|
|
this.contestIdsState[tile] = idValue;
|
|
if (this.needsContestFullUpload) {
|
|
return;
|
|
}
|
|
const x = tile % this.mapWidth;
|
|
const y = Math.floor(tile / this.mapWidth);
|
|
const span = this.contestDirtyRows.get(y);
|
|
if (span === undefined) {
|
|
this.contestDirtyRows.set(y, { minX: x, maxX: x });
|
|
} else {
|
|
span.minX = Math.min(span.minX, x);
|
|
span.maxX = Math.max(span.maxX, x);
|
|
}
|
|
}
|
|
|
|
clearContestTile(tile: TileRef) {
|
|
this.setContestTile(tile, 0, 0, 0, false);
|
|
}
|
|
|
|
setContestTime(componentId: number, nowPacked: number) {
|
|
if (!this.contestEnabled) {
|
|
return;
|
|
}
|
|
if (componentId <= 0) {
|
|
return;
|
|
}
|
|
this.ensureContestTimeCapacity(componentId);
|
|
const packed = nowPacked & 0xffff;
|
|
if (this.contestTimesState[componentId] === packed) {
|
|
return;
|
|
}
|
|
this.contestTimesState[componentId] = packed;
|
|
this.needsContestTimesUpload = true;
|
|
}
|
|
|
|
ensureContestTimeCapacity(componentId: number) {
|
|
if (componentId < this.contestTimesState.length) {
|
|
return;
|
|
}
|
|
let nextLength = Math.max(1, this.contestTimesState.length);
|
|
while (nextLength <= componentId) {
|
|
nextLength *= 2;
|
|
}
|
|
const nextState = new Uint16Array(nextLength);
|
|
nextState.set(this.contestTimesState);
|
|
this.contestTimesState = nextState;
|
|
this.needsContestTimesUpload = true;
|
|
}
|
|
|
|
setContestStrength(componentId: number, strength: number) {
|
|
if (!this.contestEnabled) {
|
|
return;
|
|
}
|
|
if (componentId <= 0) {
|
|
return;
|
|
}
|
|
this.ensureContestStrengthCapacity(componentId);
|
|
const clamped = Math.max(0, Math.min(1, strength));
|
|
const packed = Math.round(clamped * 65535) & 0xffff;
|
|
if (this.contestStrengthsState[componentId] === packed) {
|
|
return;
|
|
}
|
|
this.contestStrengthsState[componentId] = packed;
|
|
this.needsContestStrengthsUpload = true;
|
|
}
|
|
|
|
ensureContestStrengthCapacity(componentId: number) {
|
|
if (componentId < this.contestStrengthsState.length) {
|
|
return;
|
|
}
|
|
let nextLength = Math.max(1, this.contestStrengthsState.length);
|
|
while (nextLength <= componentId) {
|
|
nextLength *= 2;
|
|
}
|
|
const nextState = new Uint16Array(nextLength);
|
|
nextState.set(this.contestStrengthsState);
|
|
this.contestStrengthsState = nextState;
|
|
this.needsContestStrengthsUpload = true;
|
|
}
|
|
|
|
setContestNow(nowPacked: number, durationTicks: number) {
|
|
if (!this.contestEnabled) {
|
|
return;
|
|
}
|
|
this.contestNow = nowPacked | 0;
|
|
this.contestDurationTicks = Math.max(0, durationTicks);
|
|
}
|
|
|
|
snapshotStateForSmoothing() {
|
|
if (
|
|
!this.gl ||
|
|
!this.prevStateCopySupported ||
|
|
!this.stateFramebuffer ||
|
|
!this.prevStateFramebuffer ||
|
|
!this.olderStateFramebuffer
|
|
) {
|
|
return;
|
|
}
|
|
const gl = this.gl;
|
|
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.prevStateFramebuffer);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.olderStateFramebuffer);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.stateFramebuffer);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.prevStateFramebuffer);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
|
|
if (
|
|
this.jfaSupported &&
|
|
this.jfaResultOlderFramebuffer &&
|
|
this.jfaResultOldFramebuffer &&
|
|
this.jfaResultNewFramebuffer
|
|
) {
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOlderFramebuffer);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultNewFramebuffer);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
}
|
|
|
|
if (
|
|
this.jfaSupported &&
|
|
this.changeMaskFramebufferOlder &&
|
|
this.changeMaskFramebufferOld &&
|
|
this.changeMaskFramebufferNew
|
|
) {
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.changeMaskFramebufferOld);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.changeMaskFramebufferOlder);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.changeMaskFramebufferNew);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.changeMaskFramebufferOld);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
}
|
|
this.jfaDirty = true;
|
|
this.changeMaskDirty = true;
|
|
}
|
|
|
|
setSmoothProgress(progress: number) {
|
|
this.smoothProgress = Math.max(0, Math.min(1, progress));
|
|
}
|
|
|
|
setSmoothEnabled(enabled: boolean) {
|
|
this.smoothEnabled =
|
|
enabled &&
|
|
this.jfaSupported &&
|
|
this.prevStateCopySupported &&
|
|
!!this.changeMaskProgram &&
|
|
!!this.changeMaskTextureOld &&
|
|
!!this.changeMaskTextureNew &&
|
|
!!this.jfaResultOldTexture &&
|
|
!!this.jfaResultNewTexture;
|
|
}
|
|
|
|
setInterpolationPair(pair: "prevCurrent" | "olderPrev") {
|
|
this.interpolationPair = pair;
|
|
}
|
|
|
|
markAllDirty() {
|
|
this.needsFullUpload = true;
|
|
this.dirtyRows.clear();
|
|
this.needsContestFullUpload = true;
|
|
this.needsContestTimesUpload = true;
|
|
this.needsContestStrengthsUpload = true;
|
|
this.contestDirtyRows.clear();
|
|
this.jfaDirty = true;
|
|
this.changeMaskDirty = true;
|
|
}
|
|
|
|
refreshPalette() {
|
|
if (!this.gl || !this.paletteTexture || !this.relationTexture) {
|
|
return;
|
|
}
|
|
this.uploadPalette();
|
|
}
|
|
|
|
render() {
|
|
if (!this.gl || !this.program || !this.vao) {
|
|
return;
|
|
}
|
|
const gl = this.gl;
|
|
|
|
const uploadStateSpan = FrameProfiler.start();
|
|
this.uploadStateTexture();
|
|
FrameProfiler.end("TerritoryWebGLRenderer:uploadState", uploadStateSpan);
|
|
|
|
if (this.contestEnabled) {
|
|
const uploadContestSpan = FrameProfiler.start();
|
|
this.uploadContestTexture();
|
|
FrameProfiler.end(
|
|
"TerritoryWebGLRenderer:uploadContests",
|
|
uploadContestSpan,
|
|
);
|
|
|
|
const uploadContestTimesSpan = FrameProfiler.start();
|
|
this.uploadContestTimesTexture();
|
|
FrameProfiler.end(
|
|
"TerritoryWebGLRenderer:uploadContestTimes",
|
|
uploadContestTimesSpan,
|
|
);
|
|
|
|
const uploadContestStrengthsSpan = FrameProfiler.start();
|
|
this.uploadContestStrengthsTexture();
|
|
FrameProfiler.end(
|
|
"TerritoryWebGLRenderer:uploadContestStrengths",
|
|
uploadContestStrengthsSpan,
|
|
);
|
|
}
|
|
|
|
if (this.jfaSupported) {
|
|
this.updateChangeMask();
|
|
this.updateJfa();
|
|
}
|
|
|
|
const renderSpan = FrameProfiler.start();
|
|
gl.viewport(0, 0, this.viewWidth, this.viewHeight);
|
|
gl.useProgram(this.program);
|
|
gl.bindVertexArray(this.vao);
|
|
|
|
const canUseOlderPair =
|
|
this.interpolationPair === "olderPrev" &&
|
|
!!this.prevOwnerTexture &&
|
|
!!this.olderOwnerTexture &&
|
|
!!this.jfaResultOldTexture &&
|
|
!!this.jfaResultOlderTexture;
|
|
const renderPair = canUseOlderPair ? "olderPrev" : "prevCurrent";
|
|
|
|
const toStateTexture =
|
|
renderPair === "olderPrev" ? this.prevOwnerTexture : this.stateTexture;
|
|
const fromStateTexture =
|
|
renderPair === "olderPrev"
|
|
? this.olderOwnerTexture
|
|
: this.prevOwnerTexture;
|
|
|
|
if (toStateTexture) {
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, toStateTexture);
|
|
}
|
|
if (this.paletteTexture) {
|
|
gl.activeTexture(gl.TEXTURE1);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture);
|
|
}
|
|
if (this.relationTexture) {
|
|
gl.activeTexture(gl.TEXTURE2);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.relationTexture);
|
|
}
|
|
if (this.patternTexture) {
|
|
gl.activeTexture(gl.TEXTURE3);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.patternTexture);
|
|
}
|
|
if (this.contestOwnersTexture) {
|
|
gl.activeTexture(gl.TEXTURE4);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestOwnersTexture);
|
|
}
|
|
if (this.contestIdsTexture) {
|
|
gl.activeTexture(gl.TEXTURE5);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestIdsTexture);
|
|
}
|
|
if (this.contestTimesTexture) {
|
|
gl.activeTexture(gl.TEXTURE6);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestTimesTexture);
|
|
}
|
|
if (fromStateTexture) {
|
|
gl.activeTexture(gl.TEXTURE7);
|
|
gl.bindTexture(gl.TEXTURE_2D, fromStateTexture);
|
|
}
|
|
|
|
const seedsOld =
|
|
renderPair === "olderPrev"
|
|
? this.jfaResultOlderTexture
|
|
: this.jfaResultOldTexture;
|
|
const seedsNew =
|
|
renderPair === "olderPrev"
|
|
? this.jfaResultOldTexture
|
|
: this.jfaResultNewTexture;
|
|
if (seedsOld) {
|
|
gl.activeTexture(gl.TEXTURE8);
|
|
gl.bindTexture(gl.TEXTURE_2D, seedsOld);
|
|
}
|
|
if (seedsNew) {
|
|
gl.activeTexture(gl.TEXTURE9);
|
|
gl.bindTexture(gl.TEXTURE_2D, seedsNew);
|
|
}
|
|
|
|
if (this.stateTexture) {
|
|
gl.activeTexture(gl.TEXTURE12);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
|
}
|
|
if (this.terrainTexture) {
|
|
gl.activeTexture(gl.TEXTURE14);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.terrainTexture);
|
|
}
|
|
|
|
const changeMaskTexture =
|
|
renderPair === "olderPrev"
|
|
? this.changeMaskTextureOld
|
|
: this.changeMaskTextureNew;
|
|
if (changeMaskTexture) {
|
|
gl.activeTexture(gl.TEXTURE13);
|
|
gl.bindTexture(gl.TEXTURE_2D, changeMaskTexture);
|
|
}
|
|
if (this.contestStrengthsTexture) {
|
|
gl.activeTexture(gl.TEXTURE11);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestStrengthsTexture);
|
|
}
|
|
if (this.uniforms.viewResolution) {
|
|
gl.uniform2f(
|
|
this.uniforms.viewResolution,
|
|
this.viewWidth,
|
|
this.viewHeight,
|
|
);
|
|
}
|
|
if (this.uniforms.viewScale) {
|
|
gl.uniform1f(this.uniforms.viewScale, this.viewScale);
|
|
}
|
|
if (this.uniforms.viewOffset) {
|
|
gl.uniform2f(
|
|
this.uniforms.viewOffset,
|
|
this.viewOffsetX,
|
|
this.viewOffsetY,
|
|
);
|
|
}
|
|
if (this.uniforms.alternativeView) {
|
|
gl.uniform1i(this.uniforms.alternativeView, this.alternativeView ? 1 : 0);
|
|
}
|
|
if (this.uniforms.hoveredPlayerId) {
|
|
// Disable highlight after 5 seconds
|
|
const now = Date.now();
|
|
const elapsed = now - this.hoverStartTime;
|
|
const activeHoverId =
|
|
this.hoveredPlayerId >= 0 &&
|
|
elapsed < TerritoryWebGLRenderer.HOVER_DURATION_MS
|
|
? this.hoveredPlayerId
|
|
: -1;
|
|
gl.uniform1f(this.uniforms.hoveredPlayerId, activeHoverId);
|
|
}
|
|
if (this.uniforms.hoverHighlightStrength) {
|
|
gl.uniform1f(
|
|
this.uniforms.hoverHighlightStrength,
|
|
this.hoverHighlightStrength,
|
|
);
|
|
}
|
|
if (this.uniforms.hoverHighlightColor) {
|
|
const [r, g, b] = this.hoverHighlightColor;
|
|
gl.uniform3f(this.uniforms.hoverHighlightColor, r, g, b);
|
|
}
|
|
if (this.uniforms.hoverPulseStrength) {
|
|
gl.uniform1f(this.uniforms.hoverPulseStrength, this.hoverPulseStrength);
|
|
}
|
|
if (this.uniforms.hoverPulseSpeed) {
|
|
gl.uniform1f(this.uniforms.hoverPulseSpeed, this.hoverPulseSpeed);
|
|
}
|
|
if (this.uniforms.time) {
|
|
const currentTime = (Date.now() - this.animationStartTime) / 1000.0;
|
|
gl.uniform1f(this.uniforms.time, currentTime);
|
|
}
|
|
if (this.uniforms.viewerId) {
|
|
const viewerId = this.game.myPlayer()?.smallID() ?? 0;
|
|
gl.uniform1i(this.uniforms.viewerId, viewerId);
|
|
}
|
|
if (this.uniforms.contestEnabled) {
|
|
gl.uniform1i(this.uniforms.contestEnabled, this.contestEnabled ? 1 : 0);
|
|
}
|
|
if (this.uniforms.contestPatternMode) {
|
|
gl.uniform1i(this.uniforms.contestPatternMode, this.contestPatternMode);
|
|
}
|
|
if (this.uniforms.debugDisableStaticBorders) {
|
|
gl.uniform1i(
|
|
this.uniforms.debugDisableStaticBorders,
|
|
this.debugDisableStaticBorders ? 1 : 0,
|
|
);
|
|
}
|
|
if (this.uniforms.debugDisableAllBorders) {
|
|
gl.uniform1i(
|
|
this.uniforms.debugDisableAllBorders,
|
|
this.debugDisableAllBorders ? 1 : 0,
|
|
);
|
|
}
|
|
if (this.uniforms.seedSamplingMode) {
|
|
gl.uniform1i(this.uniforms.seedSamplingMode, this.seedSamplingMode);
|
|
}
|
|
if (this.uniforms.debugStripeFixedColors) {
|
|
gl.uniform1i(
|
|
this.uniforms.debugStripeFixedColors,
|
|
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);
|
|
}
|
|
if (this.uniforms.contestDuration) {
|
|
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationTicks);
|
|
}
|
|
if (this.uniforms.smoothProgress) {
|
|
gl.uniform1f(this.uniforms.smoothProgress, this.smoothProgress);
|
|
}
|
|
if (this.uniforms.smoothEnabled) {
|
|
gl.uniform1i(this.uniforms.smoothEnabled, this.smoothEnabled ? 1 : 0);
|
|
}
|
|
if (this.uniforms.darkMode) {
|
|
gl.uniform1i(
|
|
this.uniforms.darkMode,
|
|
this.userSettings.darkMode() ? 1 : 0,
|
|
);
|
|
}
|
|
|
|
gl.clearColor(0, 0, 0, 0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
gl.bindVertexArray(null);
|
|
FrameProfiler.end("TerritoryWebGLRenderer:draw", renderSpan);
|
|
}
|
|
|
|
getDebugStats() {
|
|
return {
|
|
mapWidth: this.mapWidth,
|
|
mapHeight: this.mapHeight,
|
|
viewWidth: this.viewWidth,
|
|
viewHeight: this.viewHeight,
|
|
viewScale: this.viewScale,
|
|
viewOffsetX: this.viewOffsetX,
|
|
viewOffsetY: this.viewOffsetY,
|
|
smoothEnabled: this.smoothEnabled,
|
|
smoothProgress: this.smoothProgress,
|
|
jfaSupported: this.jfaSupported,
|
|
jfaDisabledReason: this.jfaDisabledReason,
|
|
jfaDirty: this.jfaDirty,
|
|
prevStateCopySupported: this.prevStateCopySupported,
|
|
contestDurationTicks: this.contestDurationTicks,
|
|
contestNow: this.contestNow,
|
|
hoveredPlayerId: this.hoveredPlayerId,
|
|
};
|
|
}
|
|
|
|
private uploadStateTexture(): { rows: number; bytes: number } {
|
|
if (!this.gl || !this.stateTexture) return { rows: 0, bytes: 0 };
|
|
const gl = this.gl;
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
|
|
|
const bytesPerPixel = Uint16Array.BYTES_PER_ELEMENT;
|
|
let rowsUploaded = 0;
|
|
let bytesUploaded = 0;
|
|
|
|
if (this.needsFullUpload) {
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.R16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.state,
|
|
);
|
|
this.needsFullUpload = false;
|
|
this.dirtyRows.clear();
|
|
rowsUploaded = this.mapHeight;
|
|
bytesUploaded = this.mapWidth * this.mapHeight * bytesPerPixel;
|
|
return { rows: rowsUploaded, bytes: bytesUploaded };
|
|
}
|
|
|
|
if (this.dirtyRows.size === 0) {
|
|
return { rows: 0, bytes: 0 };
|
|
}
|
|
|
|
for (const [y, span] of this.dirtyRows) {
|
|
const width = span.maxX - span.minX + 1;
|
|
const offset = y * this.mapWidth + span.minX;
|
|
const rowSlice = this.state.subarray(offset, offset + width);
|
|
gl.texSubImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
span.minX,
|
|
y,
|
|
width,
|
|
1,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
rowSlice,
|
|
);
|
|
rowsUploaded++;
|
|
bytesUploaded += width * bytesPerPixel;
|
|
}
|
|
this.dirtyRows.clear();
|
|
return { rows: rowsUploaded, bytes: bytesUploaded };
|
|
}
|
|
|
|
private uploadContestTexture(): { rows: number; bytes: number } {
|
|
if (!this.gl || !this.contestOwnersTexture || !this.contestIdsTexture) {
|
|
return { rows: 0, bytes: 0 };
|
|
}
|
|
const gl = this.gl;
|
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
|
|
const bytesPerOwnerPixel = Uint16Array.BYTES_PER_ELEMENT * 2;
|
|
const bytesPerIdPixel = Uint16Array.BYTES_PER_ELEMENT;
|
|
let rowsUploaded = 0;
|
|
let bytesUploaded = 0;
|
|
|
|
if (this.needsContestFullUpload) {
|
|
gl.activeTexture(gl.TEXTURE4);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestOwnersTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RG16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RG_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestOwnersState,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE5);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestIdsTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.R16UI,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestIdsState,
|
|
);
|
|
|
|
this.needsContestFullUpload = false;
|
|
this.contestDirtyRows.clear();
|
|
rowsUploaded = this.mapHeight;
|
|
bytesUploaded =
|
|
this.mapWidth * this.mapHeight * (bytesPerOwnerPixel + bytesPerIdPixel);
|
|
return { rows: rowsUploaded, bytes: bytesUploaded };
|
|
}
|
|
|
|
if (this.contestDirtyRows.size === 0) {
|
|
return { rows: 0, bytes: 0 };
|
|
}
|
|
|
|
for (const [y, span] of this.contestDirtyRows) {
|
|
const width = span.maxX - span.minX + 1;
|
|
const ownerOffset = (y * this.mapWidth + span.minX) * 2;
|
|
const ownerSlice = this.contestOwnersState.subarray(
|
|
ownerOffset,
|
|
ownerOffset + width * 2,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE4);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestOwnersTexture);
|
|
gl.texSubImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
span.minX,
|
|
y,
|
|
width,
|
|
1,
|
|
gl.RG_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
ownerSlice,
|
|
);
|
|
|
|
const idOffset = y * this.mapWidth + span.minX;
|
|
const idSlice = this.contestIdsState.subarray(idOffset, idOffset + width);
|
|
gl.activeTexture(gl.TEXTURE5);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestIdsTexture);
|
|
gl.texSubImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
span.minX,
|
|
y,
|
|
width,
|
|
1,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
idSlice,
|
|
);
|
|
|
|
rowsUploaded++;
|
|
bytesUploaded += width * (bytesPerOwnerPixel + bytesPerIdPixel);
|
|
}
|
|
this.contestDirtyRows.clear();
|
|
return { rows: rowsUploaded, bytes: bytesUploaded };
|
|
}
|
|
|
|
private uploadContestTimesTexture(): { rows: number; bytes: number } {
|
|
if (!this.gl || !this.contestTimesTexture) {
|
|
return { rows: 0, bytes: 0 };
|
|
}
|
|
if (!this.needsContestTimesUpload) {
|
|
return { rows: 0, bytes: 0 };
|
|
}
|
|
const gl = this.gl;
|
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
gl.activeTexture(gl.TEXTURE6);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestTimesTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.R16UI,
|
|
this.contestTimesState.length,
|
|
1,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestTimesState,
|
|
);
|
|
this.needsContestTimesUpload = false;
|
|
const bytes = this.contestTimesState.length * Uint16Array.BYTES_PER_ELEMENT;
|
|
return { rows: 1, bytes };
|
|
}
|
|
|
|
private uploadContestStrengthsTexture(): { rows: number; bytes: number } {
|
|
if (!this.gl || !this.contestStrengthsTexture) {
|
|
return { rows: 0, bytes: 0 };
|
|
}
|
|
if (!this.needsContestStrengthsUpload) {
|
|
return { rows: 0, bytes: 0 };
|
|
}
|
|
const gl = this.gl;
|
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
gl.activeTexture(gl.TEXTURE11);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.contestStrengthsTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.R16UI,
|
|
this.contestStrengthsState.length,
|
|
1,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_SHORT,
|
|
this.contestStrengthsState,
|
|
);
|
|
this.needsContestStrengthsUpload = false;
|
|
const bytes =
|
|
this.contestStrengthsState.length * Uint16Array.BYTES_PER_ELEMENT;
|
|
return { rows: 1, bytes };
|
|
}
|
|
|
|
private updateChangeMask() {
|
|
if (
|
|
!this.gl ||
|
|
!this.jfaSupported ||
|
|
!this.changeMaskDirty ||
|
|
!this.changeMaskProgram ||
|
|
!this.changeMaskFramebufferNew ||
|
|
!this.changeMaskFramebufferOld ||
|
|
!this.changeMaskFramebufferOlder ||
|
|
!this.prevOwnerTexture ||
|
|
!this.stateTexture ||
|
|
!this.jfaVao
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const gl = this.gl;
|
|
const prevBlend = gl.isEnabled(gl.BLEND);
|
|
gl.disable(gl.BLEND);
|
|
gl.viewport(0, 0, this.mapWidth, this.mapHeight);
|
|
gl.bindVertexArray(this.jfaVao);
|
|
|
|
gl.useProgram(this.changeMaskProgram);
|
|
if (this.changeMaskUniforms.resolution) {
|
|
gl.uniform2f(
|
|
this.changeMaskUniforms.resolution,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
);
|
|
}
|
|
if (this.changeMaskUniforms.oldTexture) {
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.prevOwnerTexture);
|
|
gl.uniform1i(this.changeMaskUniforms.oldTexture, 0);
|
|
}
|
|
if (this.changeMaskUniforms.newTexture) {
|
|
gl.activeTexture(gl.TEXTURE1);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
|
gl.uniform1i(this.changeMaskUniforms.newTexture, 1);
|
|
}
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.changeMaskFramebufferNew);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
if (!this.changeMaskHistoryInitialized) {
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.changeMaskFramebufferNew);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.changeMaskFramebufferOld);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.changeMaskFramebufferOlder);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
this.changeMaskHistoryInitialized = true;
|
|
}
|
|
|
|
this.changeMaskDirty = false;
|
|
|
|
if (prevBlend) {
|
|
gl.enable(gl.BLEND);
|
|
}
|
|
}
|
|
|
|
private updateJfa() {
|
|
if (
|
|
!this.gl ||
|
|
!this.jfaSupported ||
|
|
!this.jfaSeedProgram ||
|
|
!this.jfaProgram ||
|
|
!this.jfaFramebufferA ||
|
|
!this.jfaFramebufferB ||
|
|
!this.jfaTextureA ||
|
|
!this.jfaTextureB ||
|
|
!this.stateTexture ||
|
|
!this.jfaResultNewFramebuffer ||
|
|
!this.jfaResultNewTexture ||
|
|
!this.jfaVao
|
|
) {
|
|
return;
|
|
}
|
|
if (!this.jfaDirty) {
|
|
return;
|
|
}
|
|
const gl = this.gl;
|
|
const prevBlend = gl.isEnabled(gl.BLEND);
|
|
gl.disable(gl.BLEND);
|
|
gl.viewport(0, 0, this.mapWidth, this.mapHeight);
|
|
gl.bindVertexArray(this.jfaVao);
|
|
|
|
const runJfa = (
|
|
ownerTexture: WebGLTexture,
|
|
resultFramebuffer: WebGLFramebuffer,
|
|
) => {
|
|
gl.useProgram(this.jfaSeedProgram);
|
|
if (this.jfaSeedUniforms.resolution) {
|
|
gl.uniform2f(
|
|
this.jfaSeedUniforms.resolution,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
);
|
|
}
|
|
if (this.jfaSeedUniforms.owner) {
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, ownerTexture);
|
|
gl.uniform1i(this.jfaSeedUniforms.owner, 0);
|
|
}
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaFramebufferA);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
|
|
let readTex = this.jfaTextureA;
|
|
let readFbo = this.jfaFramebufferA;
|
|
let writeFbo = this.jfaFramebufferB;
|
|
let writeTex = this.jfaTextureB;
|
|
for (const step of this.jfaSteps) {
|
|
gl.useProgram(this.jfaProgram);
|
|
if (this.jfaUniforms.resolution) {
|
|
gl.uniform2f(
|
|
this.jfaUniforms.resolution,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
);
|
|
}
|
|
if (this.jfaUniforms.step) {
|
|
gl.uniform1f(this.jfaUniforms.step, step);
|
|
}
|
|
if (this.jfaUniforms.seeds) {
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, readTex);
|
|
gl.uniform1i(this.jfaUniforms.seeds, 0);
|
|
}
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, writeFbo);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
|
|
const tempTex = readTex;
|
|
readTex = writeTex;
|
|
writeTex = tempTex;
|
|
const tempFbo = readFbo;
|
|
readFbo = writeFbo;
|
|
writeFbo = tempFbo;
|
|
}
|
|
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, readFbo);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, resultFramebuffer);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
};
|
|
|
|
runJfa(this.stateTexture, this.jfaResultNewFramebuffer);
|
|
|
|
this.jfaDirty = false;
|
|
|
|
if (
|
|
!this.jfaHistoryInitialized &&
|
|
this.jfaResultOlderFramebuffer &&
|
|
this.jfaResultOldFramebuffer
|
|
) {
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultNewFramebuffer);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOlderFramebuffer);
|
|
gl.blitFramebuffer(
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
0,
|
|
0,
|
|
this.mapWidth,
|
|
this.mapHeight,
|
|
gl.COLOR_BUFFER_BIT,
|
|
gl.NEAREST,
|
|
);
|
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
this.jfaHistoryInitialized = true;
|
|
}
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
if (prevBlend) {
|
|
gl.enable(gl.BLEND);
|
|
}
|
|
gl.bindVertexArray(null);
|
|
}
|
|
|
|
private buildJfaSteps(width: number, height: number): number[] {
|
|
const maxDim = Math.max(width, height);
|
|
let step = 1;
|
|
while (step < maxDim) {
|
|
step <<= 1;
|
|
}
|
|
step >>= 1;
|
|
const steps: number[] = [];
|
|
while (step >= 1) {
|
|
steps.push(step);
|
|
step >>= 1;
|
|
}
|
|
return steps;
|
|
}
|
|
|
|
private uploadPalette() {
|
|
if (
|
|
!this.gl ||
|
|
!this.paletteTexture ||
|
|
!this.relationTexture ||
|
|
!this.patternTexture ||
|
|
!this.program
|
|
)
|
|
return;
|
|
const gl = this.gl;
|
|
const players = this.game.playerViews().filter((p) => p.isPlayer());
|
|
|
|
const maxId = players.reduce((max, p) => Math.max(max, p.smallID()), 0) + 1;
|
|
this.paletteWidth = Math.max(maxId, 1);
|
|
|
|
const paletteData = new Uint8Array(this.paletteWidth * 8);
|
|
const relationData = new Uint8Array(this.paletteWidth * this.paletteWidth);
|
|
const patternData = new Uint8Array(
|
|
this.paletteWidth * PATTERN_STRIDE_BYTES,
|
|
);
|
|
|
|
const patternsEnabled = this.userSettings.territoryPatterns();
|
|
const defaultPatternBytes = this.getPatternBytes(
|
|
DefaultPattern.patternData,
|
|
);
|
|
|
|
for (const p of players) {
|
|
const id = p.smallID();
|
|
const territoryRgba = p.territoryColor().rgba;
|
|
paletteData[id * 8] = territoryRgba.r;
|
|
paletteData[id * 8 + 1] = territoryRgba.g;
|
|
paletteData[id * 8 + 2] = territoryRgba.b;
|
|
paletteData[id * 8 + 3] = Math.round((territoryRgba.a ?? 1) * 255);
|
|
|
|
const borderRgba = p.borderColor().rgba;
|
|
paletteData[id * 8 + 4] = borderRgba.r;
|
|
paletteData[id * 8 + 5] = borderRgba.g;
|
|
paletteData[id * 8 + 6] = borderRgba.b;
|
|
paletteData[id * 8 + 7] = Math.round((borderRgba.a ?? 1) * 255);
|
|
|
|
const patternBytes =
|
|
patternsEnabled && p.cosmetics.pattern
|
|
? this.getPatternBytes(p.cosmetics.pattern.patternData)
|
|
: defaultPatternBytes;
|
|
const offset = id * PATTERN_STRIDE_BYTES;
|
|
patternData.set(patternBytes.slice(0, PATTERN_STRIDE_BYTES), offset);
|
|
}
|
|
|
|
for (let ownerId = 0; ownerId < this.paletteWidth; ownerId++) {
|
|
const owner = this.safePlayerBySmallId(ownerId);
|
|
for (let otherId = 0; otherId < this.paletteWidth; otherId++) {
|
|
const other = this.safePlayerBySmallId(otherId);
|
|
relationData[ownerId * this.paletteWidth + otherId] =
|
|
this.resolveRelationCode(owner, other);
|
|
}
|
|
}
|
|
|
|
gl.useProgram(this.program);
|
|
|
|
gl.activeTexture(gl.TEXTURE1);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture);
|
|
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.RGBA8,
|
|
this.paletteWidth * 2,
|
|
1,
|
|
0,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
paletteData,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE2);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.relationTexture);
|
|
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.R8UI,
|
|
this.paletteWidth,
|
|
this.paletteWidth,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_BYTE,
|
|
relationData,
|
|
);
|
|
|
|
gl.activeTexture(gl.TEXTURE3);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.patternTexture);
|
|
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.R8UI,
|
|
PATTERN_STRIDE_BYTES,
|
|
this.paletteWidth,
|
|
0,
|
|
gl.RED_INTEGER,
|
|
gl.UNSIGNED_BYTE,
|
|
patternData,
|
|
);
|
|
|
|
if (this.uniforms.patternStride) {
|
|
gl.uniform1i(this.uniforms.patternStride, PATTERN_STRIDE_BYTES);
|
|
}
|
|
if (this.uniforms.patternRows) {
|
|
gl.uniform1i(this.uniforms.patternRows, this.paletteWidth);
|
|
}
|
|
}
|
|
|
|
private resolveRelationCode(
|
|
owner: PlayerView | null,
|
|
other: PlayerView | null,
|
|
): number {
|
|
if (!owner || !other || !owner.isPlayer() || !other.isPlayer()) {
|
|
return 0;
|
|
}
|
|
|
|
let code = 0;
|
|
if (owner.smallID() === other.smallID()) {
|
|
code |= 4;
|
|
}
|
|
if (owner.isFriendly(other) || other.isFriendly(owner)) {
|
|
code |= 1;
|
|
}
|
|
if (owner.hasEmbargo(other)) {
|
|
code |= 2;
|
|
}
|
|
return code;
|
|
}
|
|
|
|
private safePlayerBySmallId(id: number): PlayerView | null {
|
|
const player = this.game.playerBySmallID(id);
|
|
return player instanceof PlayerView ? player : null;
|
|
}
|
|
|
|
private getPatternBytes(patternData: string): Uint8Array {
|
|
const cached = this.patternBytesCache.get(patternData);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
try {
|
|
const bytes = base64url.decode(patternData);
|
|
this.patternBytesCache.set(patternData, bytes);
|
|
return bytes;
|
|
} catch (error) {
|
|
const fallback = base64url.decode(DefaultPattern.patternData);
|
|
this.patternBytesCache.set(patternData, fallback);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
private createJfaSeedProgram(
|
|
gl: WebGL2RenderingContext,
|
|
): WebGLProgram | null {
|
|
const vertexShaderSource = `#version 300 es
|
|
precision highp float;
|
|
layout(location = 0) in vec2 a_position;
|
|
uniform vec2 u_resolution;
|
|
void main() {
|
|
vec2 zeroToOne = a_position / u_resolution;
|
|
vec2 clipSpace = zeroToOne * 2.0 - 1.0;
|
|
clipSpace.y = -clipSpace.y;
|
|
gl_Position = vec4(clipSpace, 0.0, 1.0);
|
|
}
|
|
`;
|
|
|
|
const fragmentShaderSource = `#version 300 es
|
|
precision highp float;
|
|
precision highp usampler2D;
|
|
|
|
uniform usampler2D u_ownerTexture;
|
|
uniform vec2 u_resolution;
|
|
|
|
out vec2 outSeed;
|
|
|
|
uint ownerAt(ivec2 texCoord) {
|
|
ivec2 clamped = clamp(
|
|
texCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_resolution.x) - 1, int(u_resolution.y) - 1)
|
|
);
|
|
return texelFetch(u_ownerTexture, clamped, 0).r & 0xFFFu;
|
|
}
|
|
|
|
void main() {
|
|
ivec2 fragCoord = ivec2(gl_FragCoord.xy);
|
|
ivec2 texCoord = ivec2(
|
|
fragCoord.x,
|
|
int(u_resolution.y) - 1 - fragCoord.y
|
|
);
|
|
|
|
uint owner = ownerAt(texCoord);
|
|
bool isBorder = false;
|
|
vec2 edgeDir = vec2(0.0);
|
|
uint nOwner = ownerAt(texCoord + ivec2(1, 0));
|
|
if (nOwner != owner) { isBorder = true; edgeDir += vec2(1.0, 0.0); }
|
|
nOwner = ownerAt(texCoord + ivec2(-1, 0));
|
|
if (nOwner != owner) { isBorder = true; edgeDir += vec2(-1.0, 0.0); }
|
|
nOwner = ownerAt(texCoord + ivec2(0, 1));
|
|
if (nOwner != owner) { isBorder = true; edgeDir += vec2(0.0, 1.0); }
|
|
nOwner = ownerAt(texCoord + ivec2(0, -1));
|
|
if (nOwner != owner) { isBorder = true; edgeDir += vec2(0.0, -1.0); }
|
|
|
|
vec2 edgeOffset = vec2(
|
|
edgeDir.x == 0.0 ? 0.0 : (edgeDir.x > 0.0 ? 0.5 : -0.5),
|
|
edgeDir.y == 0.0 ? 0.0 : (edgeDir.y > 0.0 ? 0.5 : -0.5)
|
|
);
|
|
|
|
// Seed at the border edge (tile center +/- 0.5) so the front can move
|
|
// even when the border tile itself stays the same.
|
|
outSeed = isBorder
|
|
? (vec2(texCoord) + vec2(0.5) + edgeOffset)
|
|
: vec2(-1.0, -1.0);
|
|
}
|
|
`;
|
|
|
|
const vertexShader = this.compileShader(
|
|
gl,
|
|
gl.VERTEX_SHADER,
|
|
vertexShaderSource,
|
|
);
|
|
const fragmentShader = this.compileShader(
|
|
gl,
|
|
gl.FRAGMENT_SHADER,
|
|
fragmentShaderSource,
|
|
);
|
|
if (!vertexShader || !fragmentShader) {
|
|
return null;
|
|
}
|
|
|
|
const program = gl.createProgram();
|
|
if (!program) return null;
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
console.error(
|
|
"[TerritoryWebGLRenderer] JFA seed link error",
|
|
gl.getProgramInfoLog(program),
|
|
);
|
|
gl.deleteProgram(program);
|
|
return null;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
private createJfaProgram(gl: WebGL2RenderingContext): WebGLProgram | null {
|
|
const vertexShaderSource = `#version 300 es
|
|
precision highp float;
|
|
layout(location = 0) in vec2 a_position;
|
|
uniform vec2 u_resolution;
|
|
void main() {
|
|
vec2 zeroToOne = a_position / u_resolution;
|
|
vec2 clipSpace = zeroToOne * 2.0 - 1.0;
|
|
clipSpace.y = -clipSpace.y;
|
|
gl_Position = vec4(clipSpace, 0.0, 1.0);
|
|
}
|
|
`;
|
|
|
|
const fragmentShaderSource = `#version 300 es
|
|
precision highp float;
|
|
|
|
uniform sampler2D u_seeds;
|
|
uniform vec2 u_resolution;
|
|
uniform float u_step;
|
|
|
|
out vec2 outSeed;
|
|
|
|
vec2 seedAt(ivec2 coord) {
|
|
// coord is in texCoord space (Y-flipped from fragCoord)
|
|
// JFA texture was written at fragCoord positions, so flip back
|
|
ivec2 jfaCoord = ivec2(coord.x, int(u_resolution.y) - 1 - coord.y);
|
|
ivec2 clamped = clamp(
|
|
jfaCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_resolution.x) - 1, int(u_resolution.y) - 1)
|
|
);
|
|
return texelFetch(u_seeds, clamped, 0).rg;
|
|
}
|
|
|
|
void considerSeed(ivec2 coord, ivec2 texCoord, inout vec2 bestSeed, inout float bestDist) {
|
|
vec2 seed = seedAt(coord);
|
|
if (seed.x < 0.0) {
|
|
return;
|
|
}
|
|
float dist = length(seed - (vec2(texCoord) + vec2(0.5)));
|
|
if (dist < bestDist) {
|
|
bestDist = dist;
|
|
bestSeed = seed;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
ivec2 fragCoord = ivec2(gl_FragCoord.xy);
|
|
ivec2 texCoord = ivec2(
|
|
fragCoord.x,
|
|
int(u_resolution.y) - 1 - fragCoord.y
|
|
);
|
|
int step = int(u_step + 0.5);
|
|
|
|
vec2 bestSeed = seedAt(texCoord);
|
|
vec2 texPos = vec2(texCoord) + vec2(0.5);
|
|
float bestDist = bestSeed.x < 0.0 ? 1e20 : length(bestSeed - texPos);
|
|
|
|
considerSeed(texCoord + ivec2(-step, -step), texCoord, bestSeed, bestDist);
|
|
considerSeed(texCoord + ivec2(0, -step), texCoord, bestSeed, bestDist);
|
|
considerSeed(texCoord + ivec2(step, -step), texCoord, bestSeed, bestDist);
|
|
considerSeed(texCoord + ivec2(-step, 0), texCoord, bestSeed, bestDist);
|
|
considerSeed(texCoord + ivec2(step, 0), texCoord, bestSeed, bestDist);
|
|
considerSeed(texCoord + ivec2(-step, step), texCoord, bestSeed, bestDist);
|
|
considerSeed(texCoord + ivec2(0, step), texCoord, bestSeed, bestDist);
|
|
considerSeed(texCoord + ivec2(step, step), texCoord, bestSeed, bestDist);
|
|
|
|
outSeed = bestSeed;
|
|
}
|
|
`;
|
|
|
|
const vertexShader = this.compileShader(
|
|
gl,
|
|
gl.VERTEX_SHADER,
|
|
vertexShaderSource,
|
|
);
|
|
const fragmentShader = this.compileShader(
|
|
gl,
|
|
gl.FRAGMENT_SHADER,
|
|
fragmentShaderSource,
|
|
);
|
|
if (!vertexShader || !fragmentShader) {
|
|
return null;
|
|
}
|
|
|
|
const program = gl.createProgram();
|
|
if (!program) return null;
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
console.error(
|
|
"[TerritoryWebGLRenderer] JFA link error",
|
|
gl.getProgramInfoLog(program),
|
|
);
|
|
gl.deleteProgram(program);
|
|
return null;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
private createChangeMaskProgram(
|
|
gl: WebGL2RenderingContext,
|
|
): WebGLProgram | null {
|
|
const vertexShaderSource = `#version 300 es
|
|
precision highp float;
|
|
layout(location = 0) in vec2 a_position;
|
|
uniform vec2 u_resolution;
|
|
void main() {
|
|
vec2 zeroToOne = a_position / u_resolution;
|
|
vec2 clipSpace = zeroToOne * 2.0 - 1.0;
|
|
clipSpace.y = -clipSpace.y;
|
|
gl_Position = vec4(clipSpace, 0.0, 1.0);
|
|
}
|
|
`;
|
|
|
|
const fragmentShaderSource = `#version 300 es
|
|
precision highp float;
|
|
precision highp usampler2D;
|
|
|
|
uniform usampler2D u_oldTexture;
|
|
uniform usampler2D u_newTexture;
|
|
uniform vec2 u_resolution;
|
|
|
|
layout(location = 0) out uint outMask;
|
|
|
|
uint ownerAt(usampler2D tex, ivec2 texCoord) {
|
|
ivec2 clamped = clamp(
|
|
texCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_resolution.x) - 1, int(u_resolution.y) - 1)
|
|
);
|
|
return texelFetch(tex, clamped, 0).r & 0xFFFu;
|
|
}
|
|
|
|
void main() {
|
|
ivec2 fragCoord = ivec2(gl_FragCoord.xy);
|
|
ivec2 texCoord = ivec2(
|
|
fragCoord.x,
|
|
int(u_resolution.y) - 1 - fragCoord.y
|
|
);
|
|
|
|
bool changed = ownerAt(u_oldTexture, texCoord) != ownerAt(u_newTexture, texCoord);
|
|
changed = changed || (ownerAt(u_oldTexture, texCoord + ivec2(1, 0)) != ownerAt(u_newTexture, texCoord + ivec2(1, 0)));
|
|
changed = changed || (ownerAt(u_oldTexture, texCoord + ivec2(-1, 0)) != ownerAt(u_newTexture, texCoord + ivec2(-1, 0)));
|
|
changed = changed || (ownerAt(u_oldTexture, texCoord + ivec2(0, 1)) != ownerAt(u_newTexture, texCoord + ivec2(0, 1)));
|
|
changed = changed || (ownerAt(u_oldTexture, texCoord + ivec2(0, -1)) != ownerAt(u_newTexture, texCoord + ivec2(0, -1)));
|
|
|
|
outMask = changed ? 1u : 0u;
|
|
}
|
|
`;
|
|
|
|
const vertexShader = this.compileShader(
|
|
gl,
|
|
gl.VERTEX_SHADER,
|
|
vertexShaderSource,
|
|
);
|
|
const fragmentShader = this.compileShader(
|
|
gl,
|
|
gl.FRAGMENT_SHADER,
|
|
fragmentShaderSource,
|
|
);
|
|
if (!vertexShader || !fragmentShader) {
|
|
return null;
|
|
}
|
|
|
|
const program = gl.createProgram();
|
|
if (!program) return null;
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
console.error(
|
|
"[TerritoryWebGLRenderer] change mask link error",
|
|
gl.getProgramInfoLog(program),
|
|
);
|
|
gl.deleteProgram(program);
|
|
return null;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
private createProgram(gl: WebGL2RenderingContext): WebGLProgram | null {
|
|
const vertexShaderSource = `#version 300 es
|
|
precision highp float;
|
|
layout(location = 0) in vec2 a_position;
|
|
uniform vec2 u_viewResolution;
|
|
void main() {
|
|
vec2 zeroToOne = a_position / u_viewResolution;
|
|
vec2 clipSpace = zeroToOne * 2.0 - 1.0;
|
|
clipSpace.y = -clipSpace.y;
|
|
gl_Position = vec4(clipSpace, 0.0, 1.0);
|
|
}
|
|
`;
|
|
|
|
const fragmentShaderSource = `#version 300 es
|
|
precision highp float;
|
|
precision highp usampler2D;
|
|
|
|
uniform usampler2D u_state;
|
|
uniform usampler2D u_terrain;
|
|
uniform usampler2D u_latestState;
|
|
uniform sampler2D u_palette;
|
|
uniform usampler2D u_relations;
|
|
uniform usampler2D u_patterns;
|
|
uniform bool u_contestEnabled;
|
|
uniform int u_contestPatternMode; // 0=blueNoise(strength), 1=checkerboard(50/50), 2=bayer4x4(strength)
|
|
uniform bool u_debugDisableStaticBorders;
|
|
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;
|
|
uniform usampler2D u_contestStrengths;
|
|
uniform bool u_jfaAvailable;
|
|
uniform int u_contestNow;
|
|
uniform float u_contestDurationTicks;
|
|
uniform usampler2D u_prevOwner;
|
|
uniform usampler2D u_changeMask;
|
|
uniform sampler2D u_jfaSeedsOld;
|
|
uniform sampler2D u_jfaSeedsNew;
|
|
uniform float u_smoothProgress;
|
|
uniform bool u_smoothEnabled;
|
|
uniform int u_patternStride;
|
|
uniform int u_patternRows;
|
|
uniform int u_viewerId;
|
|
uniform vec2 u_mapResolution;
|
|
uniform vec2 u_viewResolution;
|
|
uniform float u_viewScale;
|
|
uniform vec2 u_viewOffset;
|
|
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 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 bool u_darkMode;
|
|
|
|
out vec4 outColor;
|
|
|
|
uint stateAtTex(ivec2 texCoord) {
|
|
ivec2 clamped = clamp(
|
|
texCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1)
|
|
);
|
|
return texelFetch(u_state, clamped, 0).r;
|
|
}
|
|
|
|
uint ownerAtTex(ivec2 texCoord) {
|
|
return stateAtTex(texCoord) & 0xFFFu;
|
|
}
|
|
|
|
// Terrain bit layout: bit7=land, bit6=shoreline, bit5=ocean, bits0-4=magnitude
|
|
uint terrainAtTex(ivec2 texCoord) {
|
|
ivec2 clamped = clamp(
|
|
texCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1)
|
|
);
|
|
return texelFetch(u_terrain, clamped, 0).r;
|
|
}
|
|
|
|
bool isLand(uint terrain) {
|
|
return (terrain & 0x80u) != 0u; // bit 7
|
|
}
|
|
|
|
bool isShoreline(uint terrain) {
|
|
return (terrain & 0x40u) != 0u; // bit 6
|
|
}
|
|
|
|
bool isOcean(uint terrain) {
|
|
return (terrain & 0x20u) != 0u; // bit 5
|
|
}
|
|
|
|
uint getMagnitude(uint terrain) {
|
|
return terrain & 0x1Fu; // bits 0-4
|
|
}
|
|
|
|
// Compute terrain color based on type, magnitude, and theme
|
|
// Colors match PastelTheme (light) and PastelThemeDark exactly
|
|
vec3 terrainColor(uint terrain) {
|
|
uint mag = getMagnitude(terrain);
|
|
float fmag = float(mag);
|
|
|
|
if (isLand(terrain)) {
|
|
if (isShoreline(terrain)) {
|
|
// Shore/beach - land adjacent to water
|
|
// Light: rgb(204,203,158), Dark: rgb(134,133,88)
|
|
return u_darkMode
|
|
? vec3(134.0/255.0, 133.0/255.0, 88.0/255.0)
|
|
: vec3(204.0/255.0, 203.0/255.0, 158.0/255.0);
|
|
}
|
|
if (mag < 10u) {
|
|
// Plains (mag 0-9)
|
|
// Light: rgb(190, 220-2*mag, 138), Dark: rgb(140, 170-2*mag, 88)
|
|
return u_darkMode
|
|
? vec3(140.0/255.0, (170.0 - 2.0*fmag)/255.0, 88.0/255.0)
|
|
: vec3(190.0/255.0, (220.0 - 2.0*fmag)/255.0, 138.0/255.0);
|
|
} else if (mag < 20u) {
|
|
// Highland (mag 10-19)
|
|
// Light: rgb(200+2*mag, 183+2*mag, 138+2*mag)
|
|
// Dark: rgb(150+2*mag, 133+2*mag, 88+2*mag)
|
|
return u_darkMode
|
|
? vec3((150.0 + 2.0*fmag)/255.0, (133.0 + 2.0*fmag)/255.0, (88.0 + 2.0*fmag)/255.0)
|
|
: vec3((200.0 + 2.0*fmag)/255.0, (183.0 + 2.0*fmag)/255.0, (138.0 + 2.0*fmag)/255.0);
|
|
} else {
|
|
// Mountain (mag 20-30)
|
|
// Light: rgb(230+mag/2, 230+mag/2, 230+mag/2)
|
|
// Dark: rgb(180+mag/2, 180+mag/2, 180+mag/2)
|
|
float base = u_darkMode ? 180.0 : 230.0;
|
|
float val = (base + fmag/2.0) / 255.0;
|
|
return vec3(val, val, val);
|
|
}
|
|
} else {
|
|
// Water
|
|
if (isShoreline(terrain)) {
|
|
// Shoreline water - lighter, adjacent to land
|
|
// Light: rgb(100,143,255), Dark: rgb(50,50,50)
|
|
return u_darkMode
|
|
? vec3(50.0/255.0, 50.0/255.0, 50.0/255.0)
|
|
: vec3(100.0/255.0, 143.0/255.0, 255.0/255.0);
|
|
}
|
|
if (isOcean(terrain)) {
|
|
// Ocean - depth-adjusted
|
|
// Light base: rgb(70,132,180), adjusted by +1-min(mag,10)
|
|
// Dark base: rgb(14,11,30), adjusted by +9-mag for mag<10
|
|
float depthAdj = float(min(mag, 10u));
|
|
if (u_darkMode) {
|
|
// Dark: rgb(14+9-mag, 11+9-mag, 30+9-mag) for mag<10, else rgb(14,11,30)
|
|
if (mag < 10u) {
|
|
return vec3(
|
|
(14.0 + 9.0 - fmag)/255.0,
|
|
(11.0 + 9.0 - fmag)/255.0,
|
|
(30.0 + 9.0 - fmag)/255.0
|
|
);
|
|
}
|
|
return vec3(14.0/255.0, 11.0/255.0, 30.0/255.0);
|
|
} else {
|
|
// Light: rgb(70-10+11-min(mag,10), 132-10+11-min(mag,10), 180-10+11-min(mag,10))
|
|
// = rgb(71-depthAdj, 133-depthAdj, 181-depthAdj)
|
|
return vec3(
|
|
(71.0 - depthAdj)/255.0,
|
|
(133.0 - depthAdj)/255.0,
|
|
(181.0 - depthAdj)/255.0
|
|
);
|
|
}
|
|
} else {
|
|
// Lake - use same as shoreline water for simplicity
|
|
// Light: rgb(100,143,255), Dark: rgb(50,50,50)
|
|
return u_darkMode
|
|
? vec3(50.0/255.0, 50.0/255.0, 50.0/255.0)
|
|
: vec3(100.0/255.0, 143.0/255.0, 255.0/255.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
uint prevStateAtTex(ivec2 texCoord) {
|
|
ivec2 clamped = clamp(
|
|
texCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1)
|
|
);
|
|
return texelFetch(u_prevOwner, clamped, 0).r;
|
|
}
|
|
|
|
uint prevOwnerAtTex(ivec2 texCoord) {
|
|
return prevStateAtTex(texCoord) & 0xFFFu;
|
|
}
|
|
|
|
vec2 jfaSeedOldAtTex(ivec2 texCoord) {
|
|
// JFA texture was written with fragCoord (bottom-left origin), but we're reading with
|
|
// texCoord (top-left origin, same as state texture). Need to flip Y to match.
|
|
// JFA row 0 = fragCoord.y=0 = stateTexCoord.y=height-1 = bottom of map
|
|
// To read data for texCoord.y=0 (top), we need JFA row height-1
|
|
ivec2 flipped = ivec2(texCoord.x, int(u_mapResolution.y) - 1 - texCoord.y);
|
|
ivec2 clamped = clamp(
|
|
flipped,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1)
|
|
);
|
|
return texelFetch(u_jfaSeedsOld, clamped, 0).rg;
|
|
}
|
|
|
|
vec2 jfaSeedNewAtTex(ivec2 texCoord) {
|
|
// JFA texture was written with fragCoord (bottom-left origin), but we're reading with
|
|
// texCoord (top-left origin, same as state texture). Need to flip Y to match.
|
|
ivec2 flipped = ivec2(texCoord.x, int(u_mapResolution.y) - 1 - texCoord.y);
|
|
ivec2 clamped = clamp(
|
|
flipped,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1)
|
|
);
|
|
return texelFetch(u_jfaSeedsNew, clamped, 0).rg;
|
|
}
|
|
|
|
// Best-of-NxN seed sampling to reduce tile-boundary discontinuities.
|
|
// Returns the seed (from OLD JFA) that is closest to mapCoord.
|
|
vec2 bestSeedOld(vec2 mapCoord) {
|
|
ivec2 base = ivec2(floor(mapCoord));
|
|
float bestDist = 1e9;
|
|
vec2 bestSeed = vec2(-1.0);
|
|
|
|
int radius = u_seedSamplingMode == 2 ? 1 : 0; // 3x3 vs 2x2
|
|
int end = u_seedSamplingMode == 2 ? 2 : 2; // 3x3: -1..+1, 2x2: 0..+1
|
|
int start = u_seedSamplingMode == 2 ? -1 : 0;
|
|
|
|
for (int dy = start; dy < end; dy++) {
|
|
for (int dx = start; dx < end; dx++) {
|
|
ivec2 sampleTex = base + ivec2(dx, dy);
|
|
vec2 seed = jfaSeedOldAtTex(sampleTex);
|
|
if (seed.x >= 0.0) {
|
|
float d = distance(mapCoord, seed);
|
|
if (d < bestDist) {
|
|
bestDist = d;
|
|
bestSeed = seed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bestSeed;
|
|
}
|
|
|
|
// Best-of-NxN seed sampling for NEW JFA.
|
|
vec2 bestSeedNew(vec2 mapCoord) {
|
|
ivec2 base = ivec2(floor(mapCoord));
|
|
float bestDist = 1e9;
|
|
vec2 bestSeed = vec2(-1.0);
|
|
|
|
int radius = u_seedSamplingMode == 2 ? 1 : 0;
|
|
int end = u_seedSamplingMode == 2 ? 2 : 2;
|
|
int start = u_seedSamplingMode == 2 ? -1 : 0;
|
|
|
|
for (int dy = start; dy < end; dy++) {
|
|
for (int dx = start; dx < end; dx++) {
|
|
ivec2 sampleTex = base + ivec2(dx, dy);
|
|
vec2 seed = jfaSeedNewAtTex(sampleTex);
|
|
if (seed.x >= 0.0) {
|
|
float d = distance(mapCoord, seed);
|
|
if (d < bestDist) {
|
|
bestDist = d;
|
|
bestSeed = seed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bestSeed;
|
|
}
|
|
|
|
uvec2 contestOwnersAtTex(ivec2 texCoord) {
|
|
ivec2 clamped = clamp(
|
|
texCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1)
|
|
);
|
|
return texelFetch(u_contestOwners, clamped, 0).rg;
|
|
}
|
|
|
|
uint contestIdRawAtTex(ivec2 texCoord) {
|
|
ivec2 clamped = clamp(
|
|
texCoord,
|
|
ivec2(0, 0),
|
|
ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1)
|
|
);
|
|
return texelFetch(u_contestIds, clamped, 0).r;
|
|
}
|
|
|
|
float contestStrength(uint contestId) {
|
|
if (contestId == 0u) {
|
|
return 0.5;
|
|
}
|
|
uint strengthRaw = texelFetch(
|
|
u_contestStrengths,
|
|
ivec2(int(contestId), 0),
|
|
0
|
|
).r;
|
|
return clamp(float(strengthRaw) / 65535.0, 0.0, 1.0);
|
|
}
|
|
|
|
float blueNoise(ivec2 texCoord) {
|
|
vec2 p = vec2(texCoord);
|
|
float x = fract(0.06711056 * p.x + 0.00583715 * p.y);
|
|
return fract(52.9829189 * x);
|
|
}
|
|
|
|
float bayer4x4(ivec2 texCoord) {
|
|
// Classic 4x4 Bayer matrix values 0..15 mapped to (0.5/16 .. 15.5/16)
|
|
int x = texCoord.x & 3;
|
|
int y = texCoord.y & 3;
|
|
int idx = (y << 2) | x;
|
|
int v = 0;
|
|
// Row-major:
|
|
// 0 8 2 10
|
|
// 12 4 14 6
|
|
// 3 11 1 9
|
|
// 15 7 13 5
|
|
if (idx == 0) v = 0;
|
|
else if (idx == 1) v = 8;
|
|
else if (idx == 2) v = 2;
|
|
else if (idx == 3) v = 10;
|
|
else if (idx == 4) v = 12;
|
|
else if (idx == 5) v = 4;
|
|
else if (idx == 6) v = 14;
|
|
else if (idx == 7) v = 6;
|
|
else if (idx == 8) v = 3;
|
|
else if (idx == 9) v = 11;
|
|
else if (idx == 10) v = 1;
|
|
else if (idx == 11) v = 9;
|
|
else if (idx == 12) v = 15;
|
|
else if (idx == 13) v = 7;
|
|
else if (idx == 14) v = 13;
|
|
else v = 5;
|
|
return (float(v) + 0.5) / 16.0;
|
|
}
|
|
|
|
bool contestPickAttacker(ivec2 texCoord, float strength) {
|
|
if (u_contestPatternMode == 1) {
|
|
// Checkerboard is always 50/50 (ignores strength)
|
|
return ((texCoord.x + texCoord.y) & 1) == 0;
|
|
}
|
|
if (u_contestPatternMode == 2) {
|
|
return bayer4x4(texCoord) < strength;
|
|
}
|
|
return blueNoise(texCoord) < strength;
|
|
}
|
|
|
|
uint relationCode(uint owner, uint other) {
|
|
if (owner == 0u || other == 0u) {
|
|
return 0u;
|
|
}
|
|
return texelFetch(u_relations, ivec2(int(owner), int(other)), 0).r;
|
|
}
|
|
|
|
bool isFriendly(uint code) {
|
|
return (code & 1u) != 0u;
|
|
}
|
|
|
|
bool isEmbargo(uint code) {
|
|
return (code & 2u) != 0u;
|
|
}
|
|
|
|
bool isSelf(uint code) {
|
|
return (code & 4u) != 0u;
|
|
}
|
|
|
|
uint patternByte(uint owner, uint offset) {
|
|
int x = int(offset);
|
|
int y = int(owner);
|
|
if (x < 0 || x >= u_patternStride || y < 0 || y >= u_patternRows) {
|
|
return 0u;
|
|
}
|
|
return texelFetch(u_patterns, ivec2(x, y), 0).r;
|
|
}
|
|
|
|
bool patternIsPrimary(uint owner, ivec2 texCoord) {
|
|
uint version = patternByte(owner, 0u);
|
|
if (version != 0u) {
|
|
return true;
|
|
}
|
|
uint b1 = patternByte(owner, 1u);
|
|
uint b2 = patternByte(owner, 2u);
|
|
uint scale = b1 & 7u;
|
|
uint width = (((b2 & 3u) << 5) | ((b1 >> 3) & 31u)) + 2u;
|
|
uint height = ((b2 >> 2) & 63u) + 2u;
|
|
if (width == 0u || height == 0u) {
|
|
return true;
|
|
}
|
|
uint px = (uint(texCoord.x) >> scale) % width;
|
|
uint py = (uint(texCoord.y) >> scale) % height;
|
|
uint idx = py * width + px;
|
|
uint byteIndex = idx >> 3;
|
|
uint bitIndex = idx & 7u;
|
|
uint byteVal = patternByte(owner, 3u + byteIndex);
|
|
return (byteVal & (1u << bitIndex)) == 0u;
|
|
}
|
|
|
|
vec3 applyDefended(vec3 color, bool defended, ivec2 texCoord) {
|
|
if (!defended) {
|
|
return color;
|
|
}
|
|
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
|
const float LIGHT_FACTOR = 1.2;
|
|
const float DARK_FACTOR = 0.8;
|
|
return color * (isLightTile ? LIGHT_FACTOR : DARK_FACTOR);
|
|
}
|
|
|
|
vec3 applyBorderTint(vec3 color, bool hasFriendly, bool hasEmbargo) {
|
|
const float BORDER_TINT_RATIO = 0.35;
|
|
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0);
|
|
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0);
|
|
if (hasFriendly) {
|
|
color = color * (1.0 - BORDER_TINT_RATIO) +
|
|
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
|
|
}
|
|
if (hasEmbargo) {
|
|
color = color * (1.0 - BORDER_TINT_RATIO) +
|
|
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
|
|
}
|
|
return color;
|
|
}
|
|
|
|
void main() {
|
|
// gl_FragCoord.xy is already at pixel center (0.5, 0.5 ...).
|
|
// Use the pixel center to avoid half-pixel snapping/offset artifacts,
|
|
// especially noticeable on the interpolated JFA border/front.
|
|
vec2 viewCoord = vec2(
|
|
gl_FragCoord.x - 0.5,
|
|
u_viewResolution.y - gl_FragCoord.y - 0.5
|
|
);
|
|
vec2 mapHalf = u_mapResolution * 0.5;
|
|
vec2 mapCoord = (viewCoord - mapHalf) / u_viewScale + u_viewOffset + mapHalf;
|
|
if (
|
|
mapCoord.x < 0.0 ||
|
|
mapCoord.y < 0.0 ||
|
|
mapCoord.x >= u_mapResolution.x ||
|
|
mapCoord.y >= u_mapResolution.y
|
|
) {
|
|
outColor = vec4(0.0);
|
|
return;
|
|
}
|
|
// Tile centers are at (0.5, 1.5, 2.5, ...). Floor gives the tile index.
|
|
// Original ivec2(mapCoord) is equivalent but less explicit.
|
|
ivec2 texCoord = ivec2(mapCoord);
|
|
|
|
uint state = stateAtTex(texCoord);
|
|
uint owner = state & 0xFFFu;
|
|
bool hasFallout = (state & 0x2000u) != 0u;
|
|
bool isDefended = (state & 0x1000u) != 0u;
|
|
uint latestState = texelFetch(u_latestState, texCoord, 0).r;
|
|
uint latestOwner = latestState & 0xFFFu;
|
|
uint oldState = prevStateAtTex(texCoord);
|
|
uint oldOwner = oldState & 0xFFFu;
|
|
bool oldHasFallout = (oldState & 0x2000u) != 0u;
|
|
bool oldIsDefended = (oldState & 0x1000u) != 0u;
|
|
// ChangeMask was written with Y-flipped coords, so flip when reading
|
|
ivec2 changeMaskCoord = ivec2(texCoord.x, int(u_mapResolution.y) - 1 - texCoord.y);
|
|
uint changeMask = texelFetch(u_changeMask, changeMaskCoord, 0).r;
|
|
|
|
// Expand the animation region by 1 tile (halo) so the *outer* border edge can move smoothly.
|
|
// If we only animate "changed" tiles, the leading edge stays pinned to tile coordinates because
|
|
// neighbor pixels are still rendered from the static FROM snapshot.
|
|
uint affectedMask = changeMask;
|
|
ivec2 cm;
|
|
cm = ivec2(clamp(texCoord.x + 1, 0, int(u_mapResolution.x) - 1), texCoord.y);
|
|
affectedMask |= texelFetch(u_changeMask, ivec2(cm.x, int(u_mapResolution.y) - 1 - cm.y), 0).r;
|
|
cm = ivec2(clamp(texCoord.x - 1, 0, int(u_mapResolution.x) - 1), texCoord.y);
|
|
affectedMask |= texelFetch(u_changeMask, ivec2(cm.x, int(u_mapResolution.y) - 1 - cm.y), 0).r;
|
|
cm = ivec2(texCoord.x, clamp(texCoord.y + 1, 0, int(u_mapResolution.y) - 1));
|
|
affectedMask |= texelFetch(u_changeMask, ivec2(cm.x, int(u_mapResolution.y) - 1 - cm.y), 0).r;
|
|
cm = ivec2(texCoord.x, clamp(texCoord.y - 1, 0, int(u_mapResolution.y) - 1));
|
|
affectedMask |= texelFetch(u_changeMask, ivec2(cm.x, int(u_mapResolution.y) - 1 - cm.y), 0).r;
|
|
bool smoothActive = u_smoothEnabled &&
|
|
u_smoothProgress < 1.0 &&
|
|
!u_alternativeView &&
|
|
u_jfaAvailable &&
|
|
affectedMask != 0u;
|
|
|
|
uint contestIdRaw = 0u;
|
|
const uint CONTEST_ID_MASK = 0x7FFFu;
|
|
uint contestId = 0u;
|
|
uvec2 contestOwners = uvec2(0u);
|
|
uint defender = 0u;
|
|
bool contested = false;
|
|
if (u_contestEnabled) {
|
|
contestIdRaw = contestIdRawAtTex(texCoord);
|
|
contestId = contestIdRaw & CONTEST_ID_MASK;
|
|
contestOwners = contestOwnersAtTex(texCoord);
|
|
defender = contestOwners.r & 0xFFFu;
|
|
|
|
if (contestId != 0u) {
|
|
uint lastTime = texelFetch(u_contestTimes, ivec2(int(contestId), 0), 0).r;
|
|
const uint CONTEST_WRAP = 32768u;
|
|
uint nowTime = uint(u_contestNow);
|
|
uint elapsed = nowTime >= lastTime
|
|
? (nowTime - lastTime)
|
|
: (CONTEST_WRAP - lastTime + nowTime);
|
|
contested = float(elapsed) < u_contestDurationTicks;
|
|
}
|
|
}
|
|
|
|
// Border detection: check if any neighbor has a different owner.
|
|
bool isBorder = false;
|
|
bool hasFriendlyRelation = false;
|
|
bool hasEmbargoRelation = false;
|
|
if (!smoothActive) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Get terrain for background rendering (needed for both normal and alt view)
|
|
uint terrain = terrainAtTex(texCoord);
|
|
vec3 baseTerrainColor = terrainColor(terrain);
|
|
|
|
if (u_alternativeView) {
|
|
// Alt view: terrain + borders only, no territory fill
|
|
vec3 color = baseTerrainColor;
|
|
if (owner == 0u && hasFallout) {
|
|
color = mix(baseTerrainColor, u_fallout.rgb, u_alpha);
|
|
}
|
|
if (!u_debugDisableAllBorders && !u_debugDisableStaticBorders && owner != 0u && isBorder) {
|
|
// Only draw borders, not territory fill
|
|
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;
|
|
}
|
|
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, 1.0);
|
|
return;
|
|
}
|
|
|
|
// Normal view: blend territory on top of terrain
|
|
vec3 fillColor = baseTerrainColor;
|
|
vec3 borderColor = vec3(0.0);
|
|
float borderAlpha = 0.0;
|
|
vec3 ownerBase = vec3(0.0);
|
|
vec4 ownerBorder = vec4(0.0);
|
|
|
|
if (owner == 0u) {
|
|
// Unowned tile - show terrain (or fallout if irradiated)
|
|
if (hasFallout) {
|
|
// Blend fallout on top of terrain
|
|
fillColor = mix(baseTerrainColor, u_fallout.rgb, u_alpha);
|
|
}
|
|
// Otherwise fillColor is already baseTerrainColor
|
|
} else {
|
|
vec4 base = texelFetch(u_palette, ivec2(int(owner) * 2, 0), 0);
|
|
vec4 baseBorder = texelFetch(
|
|
u_palette,
|
|
ivec2(int(owner) * 2 + 1, 0),
|
|
0
|
|
);
|
|
ownerBase = base.rgb;
|
|
ownerBorder = baseBorder;
|
|
bool isPrimary = patternIsPrimary(owner, texCoord);
|
|
vec3 patternColor = isPrimary ? base.rgb : baseBorder.rgb;
|
|
// Blend territory fill on top of terrain
|
|
fillColor = mix(baseTerrainColor, patternColor, u_alpha);
|
|
|
|
if (isBorder && !smoothActive) {
|
|
vec3 bColor = applyBorderTint(
|
|
baseBorder.rgb,
|
|
hasFriendlyRelation,
|
|
hasEmbargoRelation
|
|
);
|
|
borderColor = applyDefended(bColor, isDefended, texCoord);
|
|
borderAlpha = baseBorder.a;
|
|
}
|
|
}
|
|
|
|
vec3 color = fillColor;
|
|
bool useContestedFill = false;
|
|
if (contested && latestOwner != 0u) {
|
|
useContestedFill = true;
|
|
vec3 latestOwnerBase = texelFetch(
|
|
u_palette,
|
|
ivec2(int(latestOwner) * 2, 0),
|
|
0
|
|
).rgb;
|
|
vec3 defenderBase = latestOwnerBase;
|
|
if (defender != 0u) {
|
|
vec4 defenderColor = texelFetch(
|
|
u_palette,
|
|
ivec2(int(defender) * 2, 0),
|
|
0
|
|
);
|
|
defenderBase = defenderColor.rgb;
|
|
}
|
|
float strength = contestStrength(contestId);
|
|
bool pickAttacker = contestPickAttacker(texCoord, strength);
|
|
vec3 contestColor = pickAttacker ? latestOwnerBase : defenderBase;
|
|
// Blend contested fill on top of terrain
|
|
color = mix(baseTerrainColor, contestColor, u_alpha);
|
|
}
|
|
|
|
if (!u_debugDisableAllBorders && !u_debugDisableStaticBorders && !smoothActive && isBorder && owner != 0u) {
|
|
// Blend border on top of the current fill
|
|
color = mix(color, borderColor, borderAlpha);
|
|
}
|
|
|
|
if (smoothActive) {
|
|
// DEBUG: uncomment ONE line to visualize issues
|
|
// color = vec3(1.0, 0.0, 1.0); outColor = vec4(color, 1.0); return; // magenta = smoothActive tiles
|
|
// vec2 ds = jfaSeedOldAtTex(texCoord); color = vec3(ds.x >= 0.0 ? 0.0 : 1.0, jfaSeedNewAtTex(texCoord).x >= 0.0 ? 0.0 : 1.0, 0.0); outColor = vec4(color, 1.0); return; // seed validity
|
|
|
|
// Compute old color blended on terrain
|
|
vec3 oldColor = baseTerrainColor;
|
|
if (oldOwner == 0u) {
|
|
if (oldHasFallout) {
|
|
oldColor = mix(baseTerrainColor, u_fallout.rgb, u_alpha);
|
|
}
|
|
// Otherwise oldColor is already baseTerrainColor
|
|
} else {
|
|
vec4 oldBase = texelFetch(u_palette, ivec2(int(oldOwner) * 2, 0), 0);
|
|
vec4 oldBorder = texelFetch(
|
|
u_palette,
|
|
ivec2(int(oldOwner) * 2 + 1, 0),
|
|
0
|
|
);
|
|
bool oldPrimary = patternIsPrimary(oldOwner, texCoord);
|
|
vec3 oldPatternColor = oldPrimary ? oldBase.rgb : oldBorder.rgb;
|
|
oldColor = mix(baseTerrainColor, oldPatternColor, u_alpha);
|
|
}
|
|
|
|
// JFA-based animation with tile-sized pixelated look
|
|
// Movement is pixel-smooth but edges remain hard/blocky like stable borders
|
|
// Use best-of-NxN seed sampling when enabled to reduce tile-boundary discontinuities.
|
|
// Use seeds picked at the TILE CENTER to avoid seed flipping inside a tile
|
|
// (which can cause direction/timing glitches). Distances still use mapCoord
|
|
// for smooth within-tile variation.
|
|
vec2 tileCenter = floor(mapCoord) + 0.5;
|
|
vec2 seedOld = u_seedSamplingMode == 0
|
|
? jfaSeedOldAtTex(texCoord)
|
|
: bestSeedOld(tileCenter);
|
|
vec2 seedNew = u_seedSamplingMode == 0
|
|
? jfaSeedNewAtTex(texCoord)
|
|
: bestSeedNew(tileCenter);
|
|
|
|
bool hasOldSeed = seedOld.x >= 0.0;
|
|
bool hasNewSeed = seedNew.x >= 0.0;
|
|
|
|
// CORRECT MODEL (no blending, no "future"):
|
|
// - We are interpolating between a *pair* of snapshots (from/to), selected by "renderPair" on CPU.
|
|
// - u_prevOwner is the FROM snapshot (texture unit 7).
|
|
// - u_state is the TO snapshot (texture unit 0).
|
|
// - u_jfaSeedsOld/u_jfaSeedsNew + u_changeMask also match that pair.
|
|
//
|
|
// We render:
|
|
// 1) Old snapshot at the true map coords (static).
|
|
// 2) New snapshot slid in from the old border position toward the new border position.
|
|
// No blending: the slid-in new snapshot overwrites old ONLY where changeMask indicates change.
|
|
|
|
float t = clamp(u_smoothProgress, 0.0, 1.0);
|
|
|
|
// --- Old layer (FROM snapshot), at texCoord ---
|
|
uint fromState = oldState;
|
|
uint fromOwner = oldOwner;
|
|
|
|
// Fill for FROM owner
|
|
vec3 fromColor = baseTerrainColor;
|
|
if (fromOwner != 0u) {
|
|
vec4 fromBase = texelFetch(u_palette, ivec2(int(fromOwner) * 2, 0), 0);
|
|
vec4 fromBorderBase = texelFetch(
|
|
u_palette,
|
|
ivec2(int(fromOwner) * 2 + 1, 0),
|
|
0
|
|
);
|
|
bool fromPrimary = patternIsPrimary(fromOwner, texCoord);
|
|
vec3 fromPatternColor = fromPrimary ? fromBase.rgb : fromBorderBase.rgb;
|
|
fromColor = mix(baseTerrainColor, fromPatternColor, u_alpha);
|
|
} else if (oldHasFallout) {
|
|
// preserve fallout tint when unowned
|
|
fromColor = mix(baseTerrainColor, u_fallout.rgb, u_alpha);
|
|
}
|
|
|
|
// Border for FROM owner (tile-width, stable look)
|
|
bool fromIsBorder = false;
|
|
uint fromOther = 0u;
|
|
uint nFrom;
|
|
nFrom = texelFetch(u_prevOwner, texCoord + ivec2(1, 0), 0).r & 0xFFFu;
|
|
if (nFrom != fromOwner) { fromIsBorder = true; if (nFrom != 0u) fromOther = nFrom; }
|
|
nFrom = texelFetch(u_prevOwner, texCoord + ivec2(-1, 0), 0).r & 0xFFFu;
|
|
if (nFrom != fromOwner) { fromIsBorder = true; if (nFrom != 0u) fromOther = nFrom; }
|
|
nFrom = texelFetch(u_prevOwner, texCoord + ivec2(0, 1), 0).r & 0xFFFu;
|
|
if (nFrom != fromOwner) { fromIsBorder = true; if (nFrom != 0u) fromOther = nFrom; }
|
|
nFrom = texelFetch(u_prevOwner, texCoord + ivec2(0, -1), 0).r & 0xFFFu;
|
|
if (nFrom != fromOwner) { fromIsBorder = true; if (nFrom != 0u) fromOther = nFrom; }
|
|
|
|
if (!u_debugDisableAllBorders && !u_debugDisableStaticBorders && fromIsBorder && fromOwner != 0u) {
|
|
vec4 borderBase = texelFetch(u_palette, ivec2(int(fromOwner) * 2 + 1, 0), 0);
|
|
bool fromFriendly = false;
|
|
bool fromEmbargo = false;
|
|
if (fromOther != 0u) {
|
|
uint rel = relationCode(fromOwner, fromOther);
|
|
fromFriendly = isFriendly(rel);
|
|
fromEmbargo = isEmbargo(rel);
|
|
}
|
|
vec3 bColor = applyBorderTint(
|
|
borderBase.rgb,
|
|
fromFriendly,
|
|
fromEmbargo
|
|
);
|
|
bColor = applyDefended(bColor, oldIsDefended, texCoord);
|
|
fromColor = bColor;
|
|
}
|
|
|
|
// Start with FROM layer
|
|
color = fromColor;
|
|
|
|
// Draw a *constant-width* moving border stripe between the FROM and TO snapshots.
|
|
// Use a planar front (not radial) that moves coherently across tiles based on
|
|
// 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 = vec2(1.0, 0.0);
|
|
vec2 frontOrigin = seedOld;
|
|
float frontPos = 0.0;
|
|
vec2 shift = vec2(0.0);
|
|
|
|
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 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).
|
|
float frontDist = frontPos - s;
|
|
|
|
// Compute the sliding position: sample owners at the position where the front currently is.
|
|
// This ensures owner checks happen at the sliding position, not static.
|
|
vec2 slideOffsetFront = (frontPos - s) * dir; // Offset from current position to front position
|
|
vec2 slideCoordFront = mapCoord + slideOffsetFront;
|
|
ivec2 slideTexFront = clamp(ivec2(slideCoordFront), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
|
|
// Sample owners at the sliding position
|
|
uint slideState = texelFetch(u_state, slideTexFront, 0).r;
|
|
uint slideOwner = slideState & 0xFFFu;
|
|
bool slideHasFallout = (slideState & 0x2000u) != 0u;
|
|
bool slideIsDefended = (slideState & 0x1000u) != 0u;
|
|
|
|
// Check if we're on a border at the sliding position (this is where the border currently is)
|
|
bool slideIsBorder = false;
|
|
bool slideHasFriendly = false;
|
|
bool slideHasEmbargo = false;
|
|
uint slideOther = 0u;
|
|
uint nSlide;
|
|
ivec2 nSlideTex;
|
|
nSlideTex = clamp(slideTexFront + ivec2(1, 0), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nSlide = texelFetch(u_state, nSlideTex, 0).r & 0xFFFu;
|
|
if (nSlide != slideOwner) { slideIsBorder = true; if (nSlide != 0u) { slideOther = nSlide; uint rel = relationCode(slideOwner, nSlide); slideHasFriendly = slideHasFriendly || isFriendly(rel); slideHasEmbargo = slideHasEmbargo || isEmbargo(rel); } }
|
|
nSlideTex = clamp(slideTexFront + ivec2(-1, 0), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nSlide = texelFetch(u_state, nSlideTex, 0).r & 0xFFFu;
|
|
if (nSlide != slideOwner) { slideIsBorder = true; if (nSlide != 0u) { slideOther = nSlide; uint rel = relationCode(slideOwner, nSlide); slideHasFriendly = slideHasFriendly || isFriendly(rel); slideHasEmbargo = slideHasEmbargo || isEmbargo(rel); } }
|
|
nSlideTex = clamp(slideTexFront + ivec2(0, 1), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nSlide = texelFetch(u_state, nSlideTex, 0).r & 0xFFFu;
|
|
if (nSlide != slideOwner) { slideIsBorder = true; if (nSlide != 0u) { slideOther = nSlide; uint rel = relationCode(slideOwner, nSlide); slideHasFriendly = slideHasFriendly || isFriendly(rel); slideHasEmbargo = slideHasEmbargo || isEmbargo(rel); } }
|
|
nSlideTex = clamp(slideTexFront + ivec2(0, -1), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nSlide = texelFetch(u_state, nSlideTex, 0).r & 0xFFFu;
|
|
if (nSlide != slideOwner) { slideIsBorder = true; if (nSlide != 0u) { slideOther = nSlide; uint rel = relationCode(slideOwner, nSlide); slideHasFriendly = slideHasFriendly || isFriendly(rel); slideHasEmbargo = slideHasEmbargo || isEmbargo(rel); } }
|
|
|
|
// Check if we're on a border in the FROM state (retreating side)
|
|
uint fromSlideState = prevStateAtTex(slideTexFront);
|
|
uint fromSlideOwner = fromSlideState & 0xFFFu;
|
|
bool fromSlideDefended = (fromSlideState & 0x1000u) != 0u;
|
|
bool fromIsBorderAtSlide = false;
|
|
uint fromOtherAtSlide = 0u;
|
|
uint nFromSlide;
|
|
ivec2 nFromSlideTex;
|
|
nFromSlideTex = clamp(slideTexFront + ivec2(1, 0), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFromSlide = texelFetch(u_prevOwner, nFromSlideTex, 0).r & 0xFFFu;
|
|
if (nFromSlide != fromSlideOwner) { fromIsBorderAtSlide = true; if (nFromSlide != 0u) fromOtherAtSlide = nFromSlide; }
|
|
nFromSlideTex = clamp(slideTexFront + ivec2(-1, 0), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFromSlide = texelFetch(u_prevOwner, nFromSlideTex, 0).r & 0xFFFu;
|
|
if (nFromSlide != fromSlideOwner) { fromIsBorderAtSlide = true; if (nFromSlide != 0u) fromOtherAtSlide = nFromSlide; }
|
|
nFromSlideTex = clamp(slideTexFront + ivec2(0, 1), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFromSlide = texelFetch(u_prevOwner, nFromSlideTex, 0).r & 0xFFFu;
|
|
if (nFromSlide != fromSlideOwner) { fromIsBorderAtSlide = true; if (nFromSlide != 0u) fromOtherAtSlide = nFromSlide; }
|
|
nFromSlideTex = clamp(slideTexFront + ivec2(0, -1), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFromSlide = texelFetch(u_prevOwner, nFromSlideTex, 0).r & 0xFFFu;
|
|
if (nFromSlide != fromSlideOwner) { fromIsBorderAtSlide = true; if (nFromSlide != 0u) fromOtherAtSlide = nFromSlide; }
|
|
|
|
// Draw border stripe: check both expanding (TO) and retreating (FROM) sides
|
|
float stripeWidth = u_debugDisableAllBorders ? 0.0 : 0.5;
|
|
bool isStripe = abs(frontDist) <= stripeWidth;
|
|
bool drawExpandingBorder =
|
|
isStripe && slideIsBorder && slideOwner != 0u && frontDist > 0.0;
|
|
bool drawRetreatingBorder =
|
|
isStripe && fromIsBorderAtSlide && fromSlideOwner != 0u && frontDist <= 0.0;
|
|
|
|
if (!u_debugDisableAllBorders && (drawExpandingBorder || drawRetreatingBorder)) {
|
|
uint stripeOwner = drawExpandingBorder ? slideOwner : fromSlideOwner;
|
|
uint stripeOther = drawExpandingBorder ? slideOther : fromOtherAtSlide;
|
|
|
|
if (u_debugStripeFixedColors) {
|
|
// Debug mode: Use fixed colors
|
|
if (drawExpandingBorder) {
|
|
// Expanding: bright red
|
|
color = vec3(1.0, float(stripeOwner) / 255.0, 0.0);
|
|
} else {
|
|
// Retreating: bright blue
|
|
color = vec3(0.0, float(stripeOwner) / 255.0, 1.0);
|
|
}
|
|
} else {
|
|
// Normal mode: Use actual border colors
|
|
if (stripeOwner != 0u) {
|
|
vec4 borderBase = texelFetch(
|
|
u_palette,
|
|
ivec2(int(stripeOwner) * 2 + 1, 0),
|
|
0
|
|
);
|
|
bool stripeFriendly = false;
|
|
bool stripeEmbargo = false;
|
|
if (stripeOther != 0u) {
|
|
uint rel = relationCode(stripeOwner, stripeOther);
|
|
stripeFriendly = isFriendly(rel);
|
|
stripeEmbargo = isEmbargo(rel);
|
|
}
|
|
bool stripeDefended = drawExpandingBorder
|
|
? slideIsDefended
|
|
: fromSlideDefended;
|
|
vec3 bColor = applyBorderTint(
|
|
borderBase.rgb,
|
|
stripeFriendly,
|
|
stripeEmbargo
|
|
);
|
|
bColor = applyDefended(bColor, stripeDefended, slideTexFront);
|
|
color = bColor;
|
|
}
|
|
}
|
|
} else if (frontDist > stripeWidth) {
|
|
// Front has passed; show the new fill/border at the shifted position
|
|
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;
|
|
uint fillOwner = fillState & 0xFFFu;
|
|
bool fillHasFallout = (fillState & 0x2000u) != 0u;
|
|
bool fillIsDefended = (fillState & 0x1000u) != 0u;
|
|
|
|
bool fillIsBorder = false;
|
|
bool fillHasFriendly = false;
|
|
bool fillHasEmbargo = false;
|
|
uint fillOther = 0u;
|
|
uint nFill;
|
|
ivec2 nFillTex;
|
|
nFillTex = clamp(slideTexFill + ivec2(1, 0), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFill = texelFetch(u_state, nFillTex, 0).r & 0xFFFu;
|
|
if (nFill != fillOwner) { fillIsBorder = true; if (nFill != 0u) { fillOther = nFill; uint rel = relationCode(fillOwner, nFill); fillHasFriendly = fillHasFriendly || isFriendly(rel); fillHasEmbargo = fillHasEmbargo || isEmbargo(rel); } }
|
|
nFillTex = clamp(slideTexFill + ivec2(-1, 0), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFill = texelFetch(u_state, nFillTex, 0).r & 0xFFFu;
|
|
if (nFill != fillOwner) { fillIsBorder = true; if (nFill != 0u) { fillOther = nFill; uint rel = relationCode(fillOwner, nFill); fillHasFriendly = fillHasFriendly || isFriendly(rel); fillHasEmbargo = fillHasEmbargo || isEmbargo(rel); } }
|
|
nFillTex = clamp(slideTexFill + ivec2(0, 1), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFill = texelFetch(u_state, nFillTex, 0).r & 0xFFFu;
|
|
if (nFill != fillOwner) { fillIsBorder = true; if (nFill != 0u) { fillOther = nFill; uint rel = relationCode(fillOwner, nFill); fillHasFriendly = fillHasFriendly || isFriendly(rel); fillHasEmbargo = fillHasEmbargo || isEmbargo(rel); } }
|
|
nFillTex = clamp(slideTexFill + ivec2(0, -1), ivec2(0), ivec2(int(u_mapResolution.x) - 1, int(u_mapResolution.y) - 1));
|
|
nFill = texelFetch(u_state, nFillTex, 0).r & 0xFFFu;
|
|
if (nFill != fillOwner) { fillIsBorder = true; if (nFill != 0u) { fillOther = nFill; uint rel = relationCode(fillOwner, nFill); fillHasFriendly = fillHasFriendly || isFriendly(rel); fillHasEmbargo = fillHasEmbargo || isEmbargo(rel); } }
|
|
|
|
vec3 toColor = baseTerrainColor;
|
|
if (fillOwner != 0u) {
|
|
vec4 toBase = texelFetch(u_palette, ivec2(int(fillOwner) * 2, 0), 0);
|
|
vec4 toBorderBase = texelFetch(
|
|
u_palette,
|
|
ivec2(int(fillOwner) * 2 + 1, 0),
|
|
0
|
|
);
|
|
bool toPrimary = patternIsPrimary(fillOwner, slideTexFill);
|
|
vec3 toPatternColor = toPrimary ? toBase.rgb : toBorderBase.rgb;
|
|
toColor = mix(baseTerrainColor, toPatternColor, u_alpha);
|
|
if (!u_debugDisableAllBorders && !u_debugDisableStaticBorders && fillIsBorder) {
|
|
vec3 bColor = applyBorderTint(
|
|
toBorderBase.rgb,
|
|
fillHasFriendly,
|
|
fillHasEmbargo
|
|
);
|
|
bColor = applyDefended(bColor, fillIsDefended, slideTexFill);
|
|
toColor = bColor;
|
|
}
|
|
} else if (fillHasFallout) {
|
|
toColor = mix(baseTerrainColor, u_fallout.rgb, u_alpha);
|
|
}
|
|
|
|
color = toColor;
|
|
}
|
|
// If frontDist < -stripeWidth, we're ahead of the front, so keep fromColor (already set).
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
bool pendingOwnerChange = latestOwner != owner;
|
|
if (pendingOwnerChange && !useContestedFill && !u_alternativeView) {
|
|
vec3 hintColor = baseTerrainColor;
|
|
if (latestOwner != 0u) {
|
|
vec3 latestColor = texelFetch(
|
|
u_palette,
|
|
ivec2(int(latestOwner) * 2, 0),
|
|
0
|
|
).rgb;
|
|
hintColor = mix(baseTerrainColor, latestColor, u_alpha * 0.12);
|
|
}
|
|
color = mix(color, hintColor, 0.5);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Output fully opaque since we render terrain as background
|
|
outColor = vec4(color, 1.0);
|
|
}
|
|
`;
|
|
|
|
const vertexShader = this.compileShader(
|
|
gl,
|
|
gl.VERTEX_SHADER,
|
|
vertexShaderSource,
|
|
);
|
|
const fragmentShader = this.compileShader(
|
|
gl,
|
|
gl.FRAGMENT_SHADER,
|
|
fragmentShaderSource,
|
|
);
|
|
if (!vertexShader || !fragmentShader) {
|
|
return null;
|
|
}
|
|
|
|
const program = gl.createProgram();
|
|
if (!program) return null;
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
console.error(
|
|
"[TerritoryWebGLRenderer] link error",
|
|
gl.getProgramInfoLog(program),
|
|
);
|
|
gl.deleteProgram(program);
|
|
return null;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
private compileShader(
|
|
gl: WebGL2RenderingContext,
|
|
type: number,
|
|
source: string,
|
|
): WebGLShader | null {
|
|
const shader = gl.createShader(type);
|
|
if (!shader) return null;
|
|
gl.shaderSource(shader, source);
|
|
gl.compileShader(shader);
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
console.error(
|
|
"[TerritoryWebGLRenderer] shader error",
|
|
gl.getShaderInfoLog(shader),
|
|
);
|
|
gl.deleteShader(shader);
|
|
return null;
|
|
}
|
|
return shader;
|
|
}
|
|
}
|