mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:00:43 +00:00
Add ocean color override to graphics settings (#4269)
## What Adds a **Terrain** section to the graphics settings modal with a color picker and a hex-code text field (paste a `#rrggbb` code) for the **ocean** (deep water) color. ## Details - The picked color sets the *shallow-water base*; the existing per-depth brightness gradient is preserved (deeper water still darkens). - Only deep water is affected — shoreline water and land are untouched. - Follows the same override pattern as every other graphics setting: the default lives in `render-settings.json` (`terrain.oceanColor`), the override is a field in `GraphicsOverrides`, and `applyGraphicsOverrides` copies it into the live `RenderSettings`. - Rebased on #4271 (settings resolved before renderer construction): the terrain texture **bakes the resolved ocean color at construction**, so a saved override shows on load with no special-casing. Terrain is baked into a GPU texture rather than read per-frame, so a *live* change still triggers an explicit `view.rebuildTerrain()`. - Resetting graphics overrides clears it back to the default ocean color. ## Testing Verified live in a headless singleplayer game: - A **saved** ocean override renders green deep-water on load, baked at construction with no settings-change event fired. - A mid-game color change recolors the deep ocean instantly, gradient preserved, shoreline/land untouched. `tsc` and ESLint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -540,6 +540,8 @@
|
|||||||
"name_cull_desc": "Hide names smaller than this size",
|
"name_cull_desc": "Hide names smaller than this size",
|
||||||
"name_cull_label": "Minimum name size",
|
"name_cull_label": "Minimum name size",
|
||||||
"name_scale_label": "Name Scale",
|
"name_scale_label": "Name Scale",
|
||||||
|
"ocean_color_desc": "Base color of ocean.",
|
||||||
|
"ocean_color_label": "Ocean color",
|
||||||
"rail_distance_desc": "How far zoomed out train tracks remain visible",
|
"rail_distance_desc": "How far zoomed out train tracks remain visible",
|
||||||
"rail_distance_label": "Train track draw distance",
|
"rail_distance_label": "Train track draw distance",
|
||||||
"rail_thickness_desc": "How wide train tracks are drawn",
|
"rail_thickness_desc": "How wide train tracks are drawn",
|
||||||
@@ -551,6 +553,7 @@
|
|||||||
"section_map": "Map",
|
"section_map": "Map",
|
||||||
"section_name_labels": "Name Labels",
|
"section_name_labels": "Name Labels",
|
||||||
"section_structure_icons": "Structure Icons",
|
"section_structure_icons": "Structure Icons",
|
||||||
|
"section_terrain": "Terrain",
|
||||||
"territory_alpha_desc": "How opaque the territory fill is (lower lets terrain show through)",
|
"territory_alpha_desc": "How opaque the territory fill is (lower lets terrain show through)",
|
||||||
"territory_alpha_label": "Territory opacity",
|
"territory_alpha_label": "Territory opacity",
|
||||||
"territory_sat_desc": "How vivid the territory fill colors are (lower mutes them)",
|
"territory_sat_desc": "How vivid the territory fill colors are (lower mutes them)",
|
||||||
|
|||||||
@@ -521,14 +521,18 @@ async function createClientGame(
|
|||||||
// graphics-override change (covers a theme switch such as colorblind mode).
|
// graphics-override change (covers a theme switch such as colorblind mode).
|
||||||
const onGraphicsChanged = (): void => {
|
const onGraphicsChanged = (): void => {
|
||||||
regenerateRenderSettings();
|
regenerateRenderSettings();
|
||||||
|
// Terrain is baked into a GPU texture rather than read per-frame, so a
|
||||||
|
// terrain-color override (e.g. ocean) needs an explicit texture rebuild.
|
||||||
|
view.rebuildTerrain();
|
||||||
// A graphics override can switch the active theme (e.g. colorblind mode),
|
// A graphics override can switch the active theme (e.g. colorblind mode),
|
||||||
// so re-theme existing players and re-upload the palette to recolor their
|
// so re-theme existing players and re-upload the palette to recolor their
|
||||||
// territory fills/borders live.
|
// territory fills/borders live.
|
||||||
gameView.refreshPlayerColors();
|
gameView.refreshPlayerColors();
|
||||||
webglBuilder.refreshPalette(gameView);
|
webglBuilder.refreshPalette(gameView);
|
||||||
};
|
};
|
||||||
// No initial regenerate needed — the renderer was constructed with the
|
// No initial regenerate or terrain rebuild needed — the renderer was
|
||||||
// resolved settings above.
|
// constructed with the resolved settings above, so the terrain texture
|
||||||
|
// already bakes any saved ocean-color override.
|
||||||
globalThis.addEventListener(
|
globalThis.addEventListener(
|
||||||
`${USER_SETTINGS_CHANGED_EVENT}:${GRAPHICS_KEY}`,
|
`${USER_SETTINGS_CHANGED_EVENT}:${GRAPHICS_KEY}`,
|
||||||
onGraphicsChanged,
|
onGraphicsChanged,
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ const RAIL_THICKNESS_MIN = 0.5;
|
|||||||
const RAIL_THICKNESS_MAX = 3;
|
const RAIL_THICKNESS_MAX = 3;
|
||||||
const RAIL_THICKNESS_STEP = 0.1;
|
const RAIL_THICKNESS_STEP = 0.1;
|
||||||
|
|
||||||
|
const HEX_COLOR_RE = /^#?([0-9a-fA-F]{6})$/;
|
||||||
|
|
||||||
export class ShowGraphicsSettingsModalEvent {
|
export class ShowGraphicsSettingsModalEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly isVisible: boolean = true,
|
public readonly isVisible: boolean = true,
|
||||||
@@ -334,6 +336,29 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
|||||||
this.patchStructure({ iconSize: value });
|
this.patchStructure({ iconSize: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private patchTerrain(patch: Partial<GraphicsOverrides["terrain"]>) {
|
||||||
|
const current = this.userSettings.graphicsOverrides();
|
||||||
|
this.userSettings.setGraphicsOverrides({
|
||||||
|
...current,
|
||||||
|
terrain: { ...current.terrain, ...patch },
|
||||||
|
});
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentOceanColor(): string {
|
||||||
|
return (
|
||||||
|
this.userSettings.graphicsOverrides().terrain?.oceanColor ??
|
||||||
|
renderDefaults.terrain.oceanColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onOceanColorChange(event: Event) {
|
||||||
|
const value = (event.target as HTMLInputElement).value.trim();
|
||||||
|
const match = HEX_COLOR_RE.exec(value);
|
||||||
|
if (!match) return; // ignore partial/invalid hex while typing
|
||||||
|
this.patchTerrain({ oceanColor: `#${match[1].toLowerCase()}` });
|
||||||
|
}
|
||||||
|
|
||||||
private currentClassicIcons(): boolean {
|
private currentClassicIcons(): boolean {
|
||||||
return (
|
return (
|
||||||
this.userSettings.graphicsOverrides().structure?.classicIcons ?? true
|
this.userSettings.graphicsOverrides().structure?.classicIcons ?? true
|
||||||
@@ -459,6 +484,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
|||||||
const coordinateGridOpacity = this.currentCoordinateGridOpacity();
|
const coordinateGridOpacity = this.currentCoordinateGridOpacity();
|
||||||
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
|
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
|
||||||
const railThickness = this.currentRailThickness();
|
const railThickness = this.currentRailThickness();
|
||||||
|
const oceanColor = this.currentOceanColor();
|
||||||
const colorblind = this.currentColorblind();
|
const colorblind = this.currentColorblind();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -919,6 +945,39 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
|
||||||
|
>
|
||||||
|
${translateText("graphics_setting.section_terrain")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium">
|
||||||
|
${translateText("graphics_setting.ocean_color_label")}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-400">
|
||||||
|
${translateText("graphics_setting.ocean_color_desc")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
.value=${oceanColor}
|
||||||
|
placeholder=${renderDefaults.terrain.oceanColor}
|
||||||
|
spellcheck="false"
|
||||||
|
@change=${this.onOceanColorChange}
|
||||||
|
class="w-24 px-2 py-1 bg-slate-900 border border-slate-500 rounded-sm text-sm text-white font-mono"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
.value=${oceanColor}
|
||||||
|
@input=${this.onOceanColorChange}
|
||||||
|
class="w-10 h-8 bg-transparent border border-slate-500 rounded-sm cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
|
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export const GraphicsOverridesSchema = z
|
|||||||
colorblind: z.boolean(),
|
colorblind: z.boolean(),
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial(),
|
||||||
|
terrain: z
|
||||||
|
.object({
|
||||||
|
// "#rrggbb" hex string; overrides the base ocean (deep water) color.
|
||||||
|
oceanColor: z.string(),
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ export class MapRenderer {
|
|||||||
applyTerrainDelta(refs: readonly number[], terrainBytes: Uint8Array): void {
|
applyTerrainDelta(refs: readonly number[], terrainBytes: Uint8Array): void {
|
||||||
this.renderer?.applyTerrainDelta(refs, terrainBytes);
|
this.renderer?.applyTerrainDelta(refs, terrainBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rebuild the terrain texture from current settings (e.g. ocean color). */
|
||||||
|
rebuildTerrain(): void {
|
||||||
|
this.renderer?.rebuildTerrain();
|
||||||
|
}
|
||||||
updateAttackRings(rings: AttackRingInput[]): void {
|
updateAttackRings(rings: AttackRingInput[]): void {
|
||||||
this.renderer?.updateAttackRings(rings);
|
this.renderer?.updateAttackRings(rings);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ export function applyGraphicsOverrides(
|
|||||||
if (overrides.passEnabled?.fx !== undefined) {
|
if (overrides.passEnabled?.fx !== undefined) {
|
||||||
settings.passEnabled.fx = overrides.passEnabled.fx;
|
settings.passEnabled.fx = overrides.passEnabled.fx;
|
||||||
}
|
}
|
||||||
|
if (overrides.terrain?.oceanColor !== undefined) {
|
||||||
|
settings.terrain.oceanColor = overrides.terrain.oceanColor;
|
||||||
|
}
|
||||||
if (overrides.name?.darkNames !== undefined) {
|
if (overrides.name?.darkNames !== undefined) {
|
||||||
const dark = overrides.name.darkNames;
|
const dark = overrides.name.darkNames;
|
||||||
// Dark: black fill + player-colored outline. Force outline RGB to black
|
// Dark: black fill + player-colored outline. Force outline RGB to black
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ export interface RenderSettings {
|
|||||||
bar: boolean;
|
bar: boolean;
|
||||||
nameDebug: boolean;
|
nameDebug: boolean;
|
||||||
};
|
};
|
||||||
|
terrain: {
|
||||||
|
/**
|
||||||
|
* Base (shallowest) color of deep water as a "#rrggbb" hex string. The
|
||||||
|
* per-depth brightness gradient is preserved relative to this color.
|
||||||
|
*/
|
||||||
|
oceanColor: string;
|
||||||
|
};
|
||||||
falloutBloom: {
|
falloutBloom: {
|
||||||
broilSpeedCold: number;
|
broilSpeedCold: number;
|
||||||
broilSpeedHot: number;
|
broilSpeedHot: number;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import { UnitPass } from "./passes/UnitPass";
|
|||||||
import { WorldTextPass } from "./passes/WorldTextPass";
|
import { WorldTextPass } from "./passes/WorldTextPass";
|
||||||
import type { RenderSettings } from "./RenderSettings";
|
import type { RenderSettings } from "./RenderSettings";
|
||||||
import { AffiliationPalette } from "./utils/Affiliation";
|
import { AffiliationPalette } from "./utils/Affiliation";
|
||||||
import { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
|
import { getPaletteSize, hexToRgb } from "./utils/ColorUtils";
|
||||||
import {
|
import {
|
||||||
createTexture2D,
|
createTexture2D,
|
||||||
toScreen,
|
toScreen,
|
||||||
@@ -213,8 +213,13 @@ export class GPURenderer {
|
|||||||
this.camera = new Camera(mapW, mapH);
|
this.camera = new Camera(mapW, mapH);
|
||||||
|
|
||||||
// --- Terrain (static) ---
|
// --- Terrain (static) ---
|
||||||
const terrainRGBA = buildTerrainRGBA(terrainBytes, mapW, mapH);
|
this.terrainPass = new TerrainPass(
|
||||||
this.terrainPass = new TerrainPass(gl, terrainRGBA, mapW, mapH);
|
gl,
|
||||||
|
terrainBytes,
|
||||||
|
mapW,
|
||||||
|
mapH,
|
||||||
|
hexToRgb(this.settings.terrain.oceanColor) ?? undefined,
|
||||||
|
);
|
||||||
|
|
||||||
// --- Shared palette texture (RGBA32F, 4096×2) ---
|
// --- Shared palette texture (RGBA32F, 4096×2) ---
|
||||||
this.paletteData = paletteData;
|
this.paletteData = paletteData;
|
||||||
@@ -817,6 +822,17 @@ export class GPURenderer {
|
|||||||
this.railroadPass.applyTerrainDelta(refs, terrainBytes);
|
this.railroadPass.applyTerrainDelta(refs, terrainBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild the terrain texture from the current `settings.terrain` colors.
|
||||||
|
* Terrain is baked into a GPU texture rather than read per-frame, so a
|
||||||
|
* settings change needs this explicit rebuild.
|
||||||
|
*/
|
||||||
|
rebuildTerrain(): void {
|
||||||
|
this.terrainPass.setOceanColor(
|
||||||
|
hexToRgb(this.settings.terrain.oceanColor) ?? undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
applyConquestEvents(events: ConquestFx[]): void {
|
applyConquestEvents(events: ConquestFx[]): void {
|
||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
this.fxPass.applyConquestEvents(events);
|
this.fxPass.applyConquestEvents(events);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import terrainFragSrc from "../shaders/terrain/terrain.frag.glsl?raw";
|
import terrainFragSrc from "../shaders/terrain/terrain.frag.glsl?raw";
|
||||||
import terrainVertSrc from "../shaders/terrain/terrain.vert.glsl?raw";
|
import terrainVertSrc from "../shaders/terrain/terrain.vert.glsl?raw";
|
||||||
import { encodeTerrainTile } from "../utils/ColorUtils";
|
import { buildTerrainRGBA, encodeTerrainTile } from "../utils/ColorUtils";
|
||||||
import {
|
import {
|
||||||
createMapQuad,
|
createMapQuad,
|
||||||
createProgram,
|
createProgram,
|
||||||
@@ -28,16 +28,22 @@ export class TerrainPass {
|
|||||||
private vao: WebGLVertexArrayObject;
|
private vao: WebGLVertexArrayObject;
|
||||||
private uCamera: WebGLUniformLocation;
|
private uCamera: WebGLUniformLocation;
|
||||||
private mapW: number;
|
private mapW: number;
|
||||||
|
private mapH: number;
|
||||||
|
// Base ocean (deep water) color; reused by applyTerrainDelta and rebuilds.
|
||||||
|
private oceanColor: readonly [number, number, number] | undefined;
|
||||||
// Scratch buffer for 1×1 sub-uploads; reused across applyTerrainDelta calls.
|
// Scratch buffer for 1×1 sub-uploads; reused across applyTerrainDelta calls.
|
||||||
private readonly pixelScratch = new Uint8Array(4);
|
private readonly pixelScratch = new Uint8Array(4);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private gl: WebGL2RenderingContext,
|
private gl: WebGL2RenderingContext,
|
||||||
terrainRGBA: Uint8Array,
|
private terrainBytes: Uint8Array,
|
||||||
mapW: number,
|
mapW: number,
|
||||||
mapH: number,
|
mapH: number,
|
||||||
|
oceanColor?: readonly [number, number, number],
|
||||||
) {
|
) {
|
||||||
this.mapW = mapW;
|
this.mapW = mapW;
|
||||||
|
this.mapH = mapH;
|
||||||
|
this.oceanColor = oceanColor;
|
||||||
this.program = createProgram(
|
this.program = createProgram(
|
||||||
gl,
|
gl,
|
||||||
shaderSrc(terrainVertSrc, { MAP_W: mapW, MAP_H: mapH }),
|
shaderSrc(terrainVertSrc, { MAP_W: mapW, MAP_H: mapH }),
|
||||||
@@ -51,13 +57,35 @@ export class TerrainPass {
|
|||||||
internalFormat: gl.RGBA8,
|
internalFormat: gl.RGBA8,
|
||||||
format: gl.RGBA,
|
format: gl.RGBA,
|
||||||
type: gl.UNSIGNED_BYTE,
|
type: gl.UNSIGNED_BYTE,
|
||||||
data: terrainRGBA,
|
data: buildTerrainRGBA(terrainBytes, mapW, mapH, oceanColor),
|
||||||
filter: gl.NEAREST, // pixel-crisp at all zoom levels
|
filter: gl.NEAREST, // pixel-crisp at all zoom levels
|
||||||
});
|
});
|
||||||
|
|
||||||
this.vao = createMapQuad(gl, mapW, mapH);
|
this.vao = createMapQuad(gl, mapW, mapH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the base ocean color and re-upload the whole terrain texture.
|
||||||
|
* Called when the user changes the ocean color in graphics settings.
|
||||||
|
*/
|
||||||
|
setOceanColor(oceanColor?: readonly [number, number, number]): void {
|
||||||
|
this.oceanColor = oceanColor;
|
||||||
|
const gl = this.gl;
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.tex);
|
||||||
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||||
|
gl.texSubImage2D(
|
||||||
|
gl.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.mapW,
|
||||||
|
this.mapH,
|
||||||
|
gl.RGBA,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
buildTerrainRGBA(this.terrainBytes, this.mapW, this.mapH, oceanColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a subset of terrain tiles in-place (e.g. land→water from a water
|
* Update a subset of terrain tiles in-place (e.g. land→water from a water
|
||||||
* nuke). `bytes[i]` is the new terrain byte for `refs[i]` (parallel arrays).
|
* nuke). `bytes[i]` is the new terrain byte for `refs[i]` (parallel arrays).
|
||||||
@@ -73,7 +101,7 @@ export class TerrainPass {
|
|||||||
const ref = refs[i];
|
const ref = refs[i];
|
||||||
const x = ref % this.mapW;
|
const x = ref % this.mapW;
|
||||||
const y = (ref - x) / this.mapW;
|
const y = (ref - x) / this.mapW;
|
||||||
encodeTerrainTile(bytes[i], this.pixelScratch, 0);
|
encodeTerrainTile(bytes[i], this.pixelScratch, 0, this.oceanColor);
|
||||||
gl.texSubImage2D(
|
gl.texSubImage2D(
|
||||||
gl.TEXTURE_2D,
|
gl.TEXTURE_2D,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"bar": true,
|
"bar": true,
|
||||||
"nameDebug": false
|
"nameDebug": false
|
||||||
},
|
},
|
||||||
|
"terrain": {
|
||||||
|
"oceanColor": "#4785b5"
|
||||||
|
},
|
||||||
"falloutBloom": {
|
"falloutBloom": {
|
||||||
"broilSpeedCold": 0.0018,
|
"broilSpeedCold": 0.0018,
|
||||||
"broilSpeedHot": 0,
|
"broilSpeedHot": 0,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
* Float32Array(PALETTE_SIZE × 2 × 4) to the GPURenderer constructor.
|
* Float32Array(PALETTE_SIZE × 2 × 4) to the GPURenderer constructor.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import renderDefaults from "../render-settings.json";
|
||||||
|
|
||||||
/** Must cover 12-bit smallID range (0-4095). */
|
/** Must cover 12-bit smallID range (0-4095). */
|
||||||
const PALETTE_SIZE = 4096;
|
const PALETTE_SIZE = 4096;
|
||||||
|
|
||||||
@@ -17,6 +19,23 @@ export function getPaletteSize(): number {
|
|||||||
|
|
||||||
// ---------- Terrain ----------
|
// ---------- Terrain ----------
|
||||||
|
|
||||||
|
/** Parse a "#rrggbb" (or "rrggbb") hex string into an RGB tuple, or null. */
|
||||||
|
export function hexToRgb(hex: string): [number, number, number] | null {
|
||||||
|
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex.trim());
|
||||||
|
if (!m) return null;
|
||||||
|
const n = parseInt(m[1], 16);
|
||||||
|
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default base (shallowest, magnitude 0) color for deep water. Derived from
|
||||||
|
* the `terrain.oceanColor` default in render-settings.json (the single source
|
||||||
|
* of truth); used as a fallback when no override color is supplied.
|
||||||
|
*/
|
||||||
|
const DEEP_WATER_BASE: readonly [number, number, number] = hexToRgb(
|
||||||
|
renderDefaults.terrain.oceanColor,
|
||||||
|
)!;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute a static RGBA8 texture from raw terrain bytes.
|
* Compute a static RGBA8 texture from raw terrain bytes.
|
||||||
* The single source of truth for terrain colors.
|
* The single source of truth for terrain colors.
|
||||||
@@ -32,6 +51,7 @@ export function encodeTerrainTile(
|
|||||||
tb: number,
|
tb: number,
|
||||||
out: Uint8Array,
|
out: Uint8Array,
|
||||||
offset: number,
|
offset: number,
|
||||||
|
oceanColor?: readonly [number, number, number],
|
||||||
): void {
|
): void {
|
||||||
const isLand = (tb & 0x80) !== 0;
|
const isLand = (tb & 0x80) !== 0;
|
||||||
const isShoreline = (tb & 0x40) !== 0;
|
const isShoreline = (tb & 0x40) !== 0;
|
||||||
@@ -68,12 +88,14 @@ export function encodeTerrainTile(
|
|||||||
g = 143;
|
g = 143;
|
||||||
b = 255;
|
b = 255;
|
||||||
} else {
|
} else {
|
||||||
// Deep water
|
// Deep water — darkens with depth (magnitude). The base color sets the
|
||||||
|
// shallowest (brightest) shade; the per-depth gradient is preserved by
|
||||||
|
// subtracting the depth from each channel.
|
||||||
const m = Math.min(magnitude, 10);
|
const m = Math.min(magnitude, 10);
|
||||||
const off = 11 - m;
|
const base = oceanColor ?? DEEP_WATER_BASE;
|
||||||
r = Math.max(0, 70 - 10 + off);
|
r = Math.max(0, base[0] - m);
|
||||||
g = Math.max(0, 132 - 10 + off);
|
g = Math.max(0, base[1] - m);
|
||||||
b = Math.max(0, 180 - 10 + off);
|
b = Math.max(0, base[2] - m);
|
||||||
}
|
}
|
||||||
|
|
||||||
out[offset] = r;
|
out[offset] = r;
|
||||||
@@ -86,10 +108,11 @@ export function buildTerrainRGBA(
|
|||||||
terrainBytes: Uint8Array,
|
terrainBytes: Uint8Array,
|
||||||
w: number,
|
w: number,
|
||||||
h: number,
|
h: number,
|
||||||
|
oceanColor?: readonly [number, number, number],
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
const pixels = new Uint8Array(w * h * 4);
|
const pixels = new Uint8Array(w * h * 4);
|
||||||
for (let i = 0; i < w * h; i++) {
|
for (let i = 0; i < w * h; i++) {
|
||||||
encodeTerrainTile(terrainBytes[i], pixels, i * 4);
|
encodeTerrainTile(terrainBytes[i], pixels, i * 4, oceanColor);
|
||||||
}
|
}
|
||||||
return pixels;
|
return pixels;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user