Remove TerrainLayer and integrate terrain handling into TerritoryWebGLRenderer

- Deleted TerrainLayer class to streamline rendering architecture.
- Updated GameRenderer to remove TerrainLayer instantiation.
- Enhanced TerritoryWebGLRenderer to manage terrain textures and rendering directly.
- Added terrain texture handling and shader updates for improved visual fidelity.
- Introduced terrainView method in GameImpl and GameMap for terrain data access.
This commit is contained in:
scamiv
2026-01-12 02:31:22 +01:00
parent 9710bf3526
commit 957a4fff42
6 changed files with 239 additions and 147 deletions
-2
View File
@@ -37,7 +37,6 @@ import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { UILayer } from "./layers/UILayer";
import { UnitDisplay } from "./layers/UnitDisplay";
@@ -248,7 +247,6 @@ export function createRenderer(
// Try to group layers by the return value of shouldTransform.
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
new RailroadLayer(game, eventBus, transformHandler),
structureLayer,
@@ -1,77 +0,0 @@
import { Theme } from "../../../core/configuration/Config";
import { GameView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
export class TerrainLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private imageData: ImageData;
private theme: Theme;
constructor(
private game: GameView,
private transformHandler: TransformHandler,
) {}
shouldTransform(): boolean {
return true;
}
tick() {
if (this.game.config().theme() !== this.theme) {
this.redraw();
}
}
init() {
console.log("redrew terrain layer");
this.redraw();
}
redraw(): void {
this.canvas = document.createElement("canvas");
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
const context = this.canvas.getContext("2d", { alpha: false });
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.imageData = this.context.createImageData(
this.canvas.width,
this.canvas.height,
);
this.initImageData();
this.context.putImageData(this.imageData, 0, 0);
}
initImageData() {
this.theme = this.game.config().theme();
this.game.forEachTile((tile) => {
const terrainColor = this.theme.terrainColor(this.game, tile);
// TODO: isn't tileref and index the same?
const index = this.game.y(tile) * this.game.width() + this.game.x(tile);
const offset = index * 4;
this.imageData.data[offset] = terrainColor.rgba.r;
this.imageData.data[offset + 1] = terrainColor.rgba.g;
this.imageData.data[offset + 2] = terrainColor.rgba.b;
this.imageData.data[offset + 3] = 255;
});
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.transformHandler.scale < 1) {
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = "low";
} else {
context.imageSmoothingEnabled = false;
}
context.drawImage(
this.canvas,
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height(),
);
}
}
@@ -36,6 +36,7 @@ export class TerritoryWebGLRenderer {
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;
@@ -87,6 +88,7 @@ export class TerritoryWebGLRenderer {
viewScale: WebGLUniformLocation | null;
viewOffset: WebGLUniformLocation | null;
state: WebGLUniformLocation | null;
terrain: WebGLUniformLocation | null;
latestState: WebGLUniformLocation | null;
palette: WebGLUniformLocation | null;
relations: WebGLUniformLocation | null;
@@ -121,6 +123,7 @@ export class TerritoryWebGLRenderer {
hoverPulseSpeed: WebGLUniformLocation | null;
time: WebGLUniformLocation | null;
viewerId: WebGLUniformLocation | null;
darkMode: WebGLUniformLocation | null;
};
private readonly mapWidth: number;
@@ -202,6 +205,7 @@ export class TerritoryWebGLRenderer {
this.jfaVao = null;
this.jfaVertexBuffer = null;
this.stateTexture = null;
this.terrainTexture = null;
this.paletteTexture = null;
this.relationTexture = null;
this.patternTexture = null;
@@ -246,6 +250,7 @@ export class TerritoryWebGLRenderer {
viewScale: null,
viewOffset: null,
state: null,
terrain: null,
latestState: null,
palette: null,
relations: null,
@@ -280,6 +285,7 @@ export class TerritoryWebGLRenderer {
hoverPulseSpeed: null,
time: null,
viewerId: null,
darkMode: null,
};
return;
}
@@ -292,6 +298,7 @@ export class TerritoryWebGLRenderer {
this.jfaVao = null;
this.jfaVertexBuffer = null;
this.stateTexture = null;
this.terrainTexture = null;
this.paletteTexture = null;
this.relationTexture = null;
this.patternTexture = null;
@@ -336,6 +343,7 @@ export class TerritoryWebGLRenderer {
viewScale: null,
viewOffset: null,
state: null,
terrain: null,
latestState: null,
palette: null,
relations: null,
@@ -370,6 +378,7 @@ export class TerritoryWebGLRenderer {
hoverPulseSpeed: null,
time: null,
viewerId: null,
darkMode: null,
};
return;
}
@@ -428,6 +437,7 @@ export class TerritoryWebGLRenderer {
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"),
@@ -477,6 +487,7 @@ export class TerritoryWebGLRenderer {
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).
@@ -530,6 +541,7 @@ export class TerritoryWebGLRenderer {
gl.bindVertexArray(null);
this.stateTexture = gl.createTexture();
this.terrainTexture = gl.createTexture();
this.paletteTexture = gl.createTexture();
this.relationTexture = gl.createTexture();
this.patternTexture = gl.createTexture();
@@ -590,6 +602,26 @@ export class TerritoryWebGLRenderer {
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.terrainView(),
);
this.uploadPalette();
gl.activeTexture(gl.TEXTURE4);
@@ -965,6 +997,9 @@ export class TerritoryWebGLRenderer {
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);
}
@@ -1642,6 +1677,10 @@ export class TerritoryWebGLRenderer {
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"
@@ -1717,6 +1756,12 @@ export class TerritoryWebGLRenderer {
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);
@@ -2413,9 +2458,11 @@ export class TerritoryWebGLRenderer {
nOwner = ownerAt(texCoord + ivec2(0, -1));
isBorder = isBorder || (nOwner != owner);
outSeed = isBorder ? vec2(texCoord) : vec2(-1.0, -1.0);
}
`;
// Seed in map-space at the *tile center* so we can later interpret the
// boundary as half a tile away (distance-to-edge = distance-to-center - 0.5).
outSeed = isBorder ? (vec2(texCoord) + vec2(0.5)) : vec2(-1.0, -1.0);
}
`;
const vertexShader = this.compileShader(
gl,
@@ -2478,17 +2525,17 @@ export class TerritoryWebGLRenderer {
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));
if (dist < bestDist) {
bestDist = dist;
bestSeed = seed;
}
}
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);
@@ -2498,8 +2545,9 @@ export class TerritoryWebGLRenderer {
);
int step = int(u_step + 0.5);
vec2 bestSeed = seedAt(texCoord);
float bestDist = bestSeed.x < 0.0 ? 1e20 : length(bestSeed - vec2(texCoord));
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);
@@ -2643,6 +2691,7 @@ export class TerritoryWebGLRenderer {
precision highp usampler2D;
uniform usampler2D u_state;
uniform usampler2D u_terrain;
uniform usampler2D u_latestState;
uniform sampler2D u_palette;
uniform usampler2D u_relations;
@@ -2681,6 +2730,7 @@ export class TerritoryWebGLRenderer {
uniform float u_hoverPulseStrength;
uniform float u_hoverPulseSpeed;
uniform float u_time;
uniform bool u_darkMode;
out vec4 outColor;
@@ -2693,6 +2743,110 @@ export class TerritoryWebGLRenderer {
return texelFetch(u_state, clamped, 0).r & 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 prevOwnerAtTex(ivec2 texCoord) {
ivec2 clamped = clamp(
texCoord,
@@ -2816,14 +2970,16 @@ export class TerritoryWebGLRenderer {
return color * (isLightTile ? LIGHT_FACTOR : DARK_FACTOR);
}
void main() {
ivec2 fragCoord = ivec2(gl_FragCoord.xy);
vec2 viewCoord = vec2(
float(fragCoord.x),
u_viewResolution.y - 1.0 - float(fragCoord.y)
);
vec2 mapHalf = u_mapResolution * 0.5;
vec2 mapCoord = (viewCoord - mapHalf) / u_viewScale + u_viewOffset + mapHalf;
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 ||
@@ -2833,6 +2989,8 @@ export class TerritoryWebGLRenderer {
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 = texelFetch(u_state, texCoord, 0).r;
@@ -2872,6 +3030,7 @@ export class TerritoryWebGLRenderer {
}
}
// Border detection: check if any neighbor has a different owner.
bool isBorder = false;
bool hasFriendlyRelation = false;
bool hasEmbargoRelation = false;
@@ -2909,9 +3068,13 @@ export class TerritoryWebGLRenderer {
}
}
// Get terrain for background rendering (needed for both normal and alt view)
uint terrain = terrainAtTex(texCoord);
vec3 baseTerrainColor = terrainColor(terrain);
if (u_alternativeView) {
vec3 color = vec3(0.0);
float a = 0.0;
// Start with terrain as base
vec3 color = baseTerrainColor;
if (owner != 0u) {
uint relationAlt = relationCode(owner, uint(u_viewerId));
vec4 altColor = u_altNeutral;
@@ -2922,8 +3085,9 @@ export class TerritoryWebGLRenderer {
} else if (isEmbargo(relationAlt)) {
altColor = u_altEnemy;
}
color = altColor.rgb;
a = isBorder ? 1.0 : 0.0;
// Blend territory on top of terrain, borders fully opaque
float territoryAlpha = isBorder ? 1.0 : u_alpha;
color = mix(baseTerrainColor, altColor.rgb, territoryAlpha);
}
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
float pulse = u_hoverPulseStrength > 0.0
@@ -2932,22 +3096,24 @@ export class TerritoryWebGLRenderer {
: 1.0;
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
}
outColor = vec4(color * a, a);
outColor = vec4(color, 1.0);
return;
}
vec3 fillColor = vec3(0.0);
float fillAlpha = 0.0;
// 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) {
fillColor = u_fallout.rgb;
fillAlpha = u_alpha;
// 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(
@@ -2977,13 +3143,13 @@ export class TerritoryWebGLRenderer {
borderAlpha = baseBorder.a;
} else {
bool isPrimary = patternIsPrimary(owner, texCoord);
fillColor = isPrimary ? base.rgb : baseBorder.rgb;
fillAlpha = u_alpha;
vec3 patternColor = isPrimary ? base.rgb : baseBorder.rgb;
// Blend territory fill on top of terrain
fillColor = mix(baseTerrainColor, patternColor, u_alpha);
}
}
vec3 contestedFillColor = fillColor;
float contestedFillAlpha = fillAlpha;
vec3 color = fillColor;
bool useContestedFill = false;
if (contested && latestOwner != 0u) {
useContestedFill = true;
@@ -3003,26 +3169,24 @@ export class TerritoryWebGLRenderer {
}
float strength = contestStrength(contestId);
float noise = blueNoise(texCoord);
contestedFillColor = noise < strength ? latestOwnerBase : defenderBase;
contestedFillAlpha = u_alpha;
vec3 contestColor = noise < strength ? latestOwnerBase : defenderBase;
// Blend contested fill on top of terrain
color = mix(baseTerrainColor, contestColor, u_alpha);
}
vec3 color = useContestedFill ? contestedFillColor : fillColor;
float a = useContestedFill ? contestedFillAlpha : fillAlpha;
if (!smoothActive && isBorder && owner != 0u) {
color = borderColor;
a = borderAlpha;
// Blend border on top of terrain
color = mix(baseTerrainColor, borderColor, borderAlpha);
}
if (smoothActive) {
vec3 oldColor = vec3(0.0);
float oldAlpha = 0.0;
// Compute old color blended on terrain
vec3 oldColor = baseTerrainColor;
if (oldOwner == 0u) {
if (hasFallout) {
oldColor = u_fallout.rgb;
oldAlpha = u_alpha;
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(
@@ -3031,23 +3195,24 @@ export class TerritoryWebGLRenderer {
0
);
bool oldPrimary = patternIsPrimary(oldOwner, texCoord);
oldColor = oldPrimary ? oldBase.rgb : oldBorder.rgb;
oldAlpha = u_alpha;
vec3 oldPatternColor = oldPrimary ? oldBase.rgb : oldBorder.rgb;
oldColor = mix(baseTerrainColor, oldPatternColor, u_alpha);
}
vec2 seedOld = jfaSeedOldAtTex(texCoord);
float oldDistance =
seedOld.x < 0.0 ? 1e6 : length(seedOld - mapCoord);
float oldDistance = seedOld.x < 0.0
? 1e6
: max(length(seedOld - mapCoord) - 0.5, 0.0);
vec2 seedNew = jfaSeedNewAtTex(texCoord);
float newDistance =
seedNew.x < 0.0 ? 1e6 : length(seedNew - mapCoord);
float newDistance = seedNew.x < 0.0
? 1e6
: max(length(seedNew - mapCoord) - 0.5, 0.0);
float maxDistance = max(oldDistance + newDistance, 0.001);
float edge = u_smoothProgress * maxDistance;
float phi = oldDistance - edge;
float showNew = step(phi, 0.0);
color = mix(oldColor, color, showNew);
a = mix(oldAlpha, a, showNew);
const float FRONT_HALF_WIDTH = 0.5;
float distToFront = abs(phi);
@@ -3083,30 +3248,24 @@ export class TerritoryWebGLRenderer {
}
}
bColor = applyDefended(bColor, isDefended, texCoord);
color = mix(color, bColor, frontBandAlpha);
a = mix(a, borderBase.a, frontBandAlpha);
// Blend border on top (borders are opaque)
color = mix(color, bColor, frontBandAlpha * borderBase.a);
}
}
}
bool pendingOwnerChange = latestOwner != owner;
if (pendingOwnerChange && !useContestedFill && !u_alternativeView) {
vec3 hintColor = vec3(1.0);
vec3 hintColor = baseTerrainColor;
if (latestOwner != 0u) {
hintColor = texelFetch(
vec3 latestColor = texelFetch(
u_palette,
ivec2(int(latestOwner) * 2, 0),
0
).rgb;
hintColor = mix(baseTerrainColor, latestColor, u_alpha * 0.12);
}
const float HINT_ALPHA_RATIO = 0.12;
float hintAlpha = u_alpha * HINT_ALPHA_RATIO;
if (a < hintAlpha) {
a = hintAlpha;
color = hintColor;
} else {
color = mix(color, hintColor, 0.08);
}
color = mix(color, hintColor, 0.5);
}
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
@@ -3117,7 +3276,8 @@ export class TerritoryWebGLRenderer {
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
}
outColor = vec4(color * a, a);
// Output fully opaque since we render terrain as background
outColor = vec4(color, 1.0);
}
`;
+3
View File
@@ -984,6 +984,9 @@ export class GameImpl implements Game {
tileStateView(): Uint16Array {
return this._map.tileStateView();
}
terrainView(): Uint8Array {
return this._map.terrainView();
}
numTilesWithFallout(): number {
return this._map.numTilesWithFallout();
}
+5
View File
@@ -30,6 +30,7 @@ export interface GameMap {
isDefended(ref: TileRef): boolean;
setDefended(ref: TileRef, value: boolean): void;
tileStateView(): Uint16Array;
terrainView(): Uint8Array;
isOnEdgeOfMap(ref: TileRef): boolean;
isBorder(ref: TileRef): boolean;
neighbors(ref: TileRef): TileRef[];
@@ -231,6 +232,10 @@ export class GameMapImpl implements GameMap {
return this.state;
}
terrainView(): Uint8Array {
return this.terrain;
}
isOnEdgeOfMap(ref: TileRef): boolean {
const x = this.x(ref);
const y = this.y(ref);
+3
View File
@@ -888,6 +888,9 @@ export class GameView implements GameMap {
tileStateView(): Uint16Array {
return this._map.tileStateView();
}
terrainView(): Uint8Array {
return this._map.terrainView();
}
isBorder(ref: TileRef): boolean {
return this._map.isBorder(ref);
}