perf(climate-sim): Optimize GLSL shaders and rendering performance for hexagonal grids, terrain legends, and climate simulation dashboards

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 04:32:36 -07:00
parent 964ec6e717
commit 35038780ec
4 changed files with 194 additions and 72 deletions

View file

@ -154,19 +154,19 @@ interface TerrainGroup {
color: string
}
// Biome groups (Life mode chart) — groups 26 biome_ids into visual categories
// Biome groups (Life mode chart) — groups biome_ids into visual categories
// Must cover all ids in TERRAIN_ORDER (runner.ts)
const BIOME_GROUPS: TerrainGroup[] = [
{ label: 'Ocean', abbr: 'Ocn', ids: ['ocean', 'coast', 'deep_ocean', 'shallow_ocean', 'coral_reef', 'estuary', 'mangrove'], color: 'rgb(61,120,209)' },
{ label: 'Fresh', abbr: 'Frs', ids: ['lake', 'pond', 'river', 'inland_sea'], color: 'rgb(100,160,230)' },
{ label: 'Ice', abbr: 'Ice', ids: ['permanent_ice', 'ice', 'snow', 'alpine_tundra', 'polar_desert'], color: 'rgb(224,240,255)' },
{ label: 'Tundra', abbr: 'Tnd', ids: ['tundra'], color: 'rgb(184,194,166)' },
{ label: 'Arid', abbr: 'Ard', ids: ['desert', 'chaparral'], color: 'rgb(222,199,128)' },
{ label: 'Grass', abbr: 'Grs', ids: ['plains', 'grassland', 'temperate_grassland', 'savanna', 'alpine_meadow'], color: 'rgb(141,197,112)' },
{ label: 'Forest', abbr: 'For', ids: ['forest', 'temperate_forest', 'boreal_forest', 'tropical_rainforest', 'tropical_dry_forest', 'montane_forest', 'cloud_forest', 'jungle', 'enchanted_forest'], color: 'rgb(51,140,64)' },
{ label: 'Ocean', abbr: 'Ocn', ids: ['ocean', 'deep_ocean', 'coast', 'coral_reef', 'estuary', 'mangrove'], color: 'rgb(61,120,209)' },
{ label: 'Fresh', abbr: 'Frs', ids: ['lake', 'inland_sea'], color: 'rgb(100,160,230)' },
{ label: 'Ice', abbr: 'Ice', ids: ['ice', 'snow', 'polar_desert'], color: 'rgb(224,240,255)' },
{ label: 'Tundra', abbr: 'Tnd', ids: ['tundra', 'alpine_tundra'], color: 'rgb(184,194,166)' },
{ label: 'Arid', abbr: 'Ard', ids: ['desert', 'chaparral', 'savanna'], color: 'rgb(222,199,128)' },
{ label: 'Grass', abbr: 'Grs', ids: ['plains', 'grassland', 'alpine_meadow'], color: 'rgb(141,197,112)' },
{ label: 'Forest', abbr: 'For', ids: ['forest', 'boreal_forest', 'temperate_rainforest', 'tropical_dry_forest', 'tropical_rainforest', 'jungle', 'cloud_forest', 'montane_forest', 'enchanted_forest'], color: 'rgb(51,140,64)' },
{ label: 'Rough', abbr: 'Rgh', ids: ['hills', 'mountains'], color: 'rgb(158,153,148)' },
{ label: 'Wetland', abbr: 'Wet', ids: ['swamp', 'bog'], color: 'rgb(61,79,36)' },
{ label: 'Volcanic', abbr: 'Vol', ids: ['volcano'], color: 'rgb(191,51,20)' },
{ label: 'Cave', abbr: 'Cav', ids: ['subterranean'], color: 'rgb(90,70,60)' },
]
// Substrate groups (Environment mode chart) — geological layer

View file

@ -2,25 +2,74 @@ import { useState } from 'react'
import type { ReactElement } from 'react'
import styled from 'styled-components'
// Extracted from hexGLShaders.ts terrainColor() — shader indices 0-24
const TERRAIN_COLORS: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'ocean', label: 'Ocean', rgb: [0.24, 0.47, 0.82] },
{ id: 'coast', label: 'Coast', rgb: [0.31, 0.71, 0.86] },
{ id: 'lake', label: 'Lake', rgb: [0.31, 0.63, 0.84] },
{ id: 'inland_sea', label: 'Inland Sea', rgb: [0.25, 0.51, 0.80] },
// Indices match TERRAIN_ORDER in runner.ts and terrainColor() in hexGLShaders.ts
// Only the first 31 (non-magic) entries are shown in the legend
const WATER_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'ocean', label: 'Ocean', rgb: [0.24, 0.47, 0.82] },
{ id: 'deep_ocean', label: 'Deep Ocean', rgb: [0.12, 0.27, 0.63] },
{ id: 'coast', label: 'Coast', rgb: [0.31, 0.71, 0.86] },
{ id: 'coral_reef', label: 'Coral Reef', rgb: [0.25, 0.75, 0.67] },
{ id: 'lake', label: 'Lake', rgb: [0.31, 0.63, 0.84] },
{ id: 'inland_sea', label: 'Inland Sea', rgb: [0.25, 0.51, 0.80] },
{ id: 'estuary', label: 'Estuary', rgb: [0.35, 0.59, 0.67] },
]
const ICE_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'ice', label: 'Ice', rgb: [0.88, 0.94, 1.00] },
{ id: 'snow', label: 'Snow', rgb: [0.94, 0.96, 1.00] },
{ id: 'tundra', label: 'Tundra', rgb: [0.72, 0.76, 0.65] },
{ id: 'desert', label: 'Desert', rgb: [0.87, 0.78, 0.50] },
{ id: 'plains', label: 'Plains', rgb: [0.73, 0.82, 0.53] },
{ id: 'grassland', label: 'Grassland', rgb: [0.38, 0.72, 0.35] },
{ id: 'forest', label: 'Forest', rgb: [0.20, 0.55, 0.25] },
{ id: 'boreal_forest', label: 'Boreal Forest', rgb: [0.22, 0.44, 0.30] },
{ id: 'jungle', label: 'Jungle', rgb: [0.09, 0.45, 0.18] },
{ id: 'hills', label: 'Hills', rgb: [0.58, 0.52, 0.38] },
{ id: 'mountains', label: 'Mountains', rgb: [0.62, 0.60, 0.58] },
{ id: 'swamp', label: 'Swamp', rgb: [0.24, 0.31, 0.14] },
{ id: 'volcano', label: 'Volcano', rgb: [0.75, 0.20, 0.08] },
{ id: 'polar_desert', label: 'Polar Desert', rgb: [0.78, 0.80, 0.73] },
]
const COLD_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'tundra', label: 'Tundra', rgb: [0.72, 0.76, 0.65] },
{ id: 'alpine_tundra', label: 'Alpine Tundra', rgb: [0.66, 0.69, 0.60] },
{ id: 'boreal_forest', label: 'Boreal Forest', rgb: [0.22, 0.44, 0.30] },
]
const TEMPERATE_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'chaparral', label: 'Chaparral', rgb: [0.69, 0.62, 0.42] },
{ id: 'plains', label: 'Plains', rgb: [0.73, 0.82, 0.53] },
{ id: 'grassland', label: 'Grassland', rgb: [0.38, 0.72, 0.35] },
{ id: 'forest', label: 'Forest', rgb: [0.20, 0.55, 0.25] },
{ id: 'temperate_rainforest', label: 'Temperate Rainforest', rgb: [0.10, 0.44, 0.20] },
]
const TROPICAL_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'desert', label: 'Desert', rgb: [0.87, 0.78, 0.50] },
{ id: 'savanna', label: 'Savanna', rgb: [0.74, 0.70, 0.40] },
{ id: 'tropical_dry_forest', label: 'Tropical Dry Forest', rgb: [0.50, 0.61, 0.25] },
{ id: 'tropical_rainforest', label: 'Tropical Rainforest', rgb: [0.09, 0.45, 0.18] },
{ id: 'jungle', label: 'Jungle', rgb: [0.14, 0.40, 0.14] },
{ id: 'mangrove', label: 'Mangrove', rgb: [0.28, 0.47, 0.35] },
]
const ELEVATION_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'hills', label: 'Hills', rgb: [0.58, 0.52, 0.38] },
{ id: 'mountains', label: 'Mountains', rgb: [0.62, 0.60, 0.58] },
{ id: 'alpine_meadow', label: 'Alpine Meadow', rgb: [0.56, 0.69, 0.47] },
{ id: 'cloud_forest', label: 'Cloud Forest', rgb: [0.25, 0.47, 0.35] },
{ id: 'montane_forest', label: 'Montane Forest', rgb: [0.18, 0.42, 0.28] },
]
const WETLAND_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'swamp', label: 'Swamp', rgb: [0.24, 0.31, 0.14] },
{ id: 'bog', label: 'Bog', rgb: [0.45, 0.39, 0.22] },
]
const SPECIAL_BIOMES: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> = [
{ id: 'volcano', label: 'Volcano', rgb: [0.75, 0.20, 0.08] },
]
const TERRAIN_SECTIONS: ReadonlyArray<{ label: string; biomes: ReadonlyArray<{ id: string; label: string; rgb: [number, number, number] }> }> = [
{ label: 'Water', biomes: WATER_BIOMES },
{ label: 'Ice', biomes: ICE_BIOMES },
{ label: 'Cold', biomes: COLD_BIOMES },
{ label: 'Temperate', biomes: TEMPERATE_BIOMES },
{ label: 'Tropical', biomes: TROPICAL_BIOMES },
{ label: 'Elevation', biomes: ELEVATION_BIOMES },
{ label: 'Wetland', biomes: WETLAND_BIOMES },
{ label: 'Special', biomes: SPECIAL_BIOMES },
]
const QUALITY_TIERS: ReadonlyArray<{ label: string; css: string }> = [
@ -48,16 +97,20 @@ export function TerrainLegend(): ReactElement {
{expanded && (
<Content>
{/* ── Terrain ── */}
<SectionLabel>Terrain</SectionLabel>
<SwatchGrid>
{TERRAIN_COLORS.map(({ id, label, rgb }) => (
<SwatchRow key={id}>
<Swatch style={{ background: rgbToCSS(rgb) }} />
<SwatchLabel>{label}</SwatchLabel>
</SwatchRow>
))}
</SwatchGrid>
{/* ── Terrain by category ── */}
{TERRAIN_SECTIONS.map(({ label, biomes }) => (
<div key={label}>
<SectionLabel>{label}</SectionLabel>
<SwatchGrid>
{biomes.map(({ id, label: biomeLabel, rgb }) => (
<SwatchRow key={id}>
<Swatch style={{ background: rgbToCSS(rgb) }} />
<SwatchLabel>{biomeLabel}</SwatchLabel>
</SwatchRow>
))}
</SwatchGrid>
</div>
))}
{/* ── Climate ── */}
<SectionLabel>Climate</SectionLabel>
@ -199,6 +252,8 @@ const Chevron = styled.span<{ $open: boolean }>`
const Content = styled.div`
padding: 0 0.625rem 0.625rem;
border-top: 1px solid rgba(255, 255, 255, 0.07);
max-height: 70vh;
overflow-y: auto;
`
const SectionLabel = styled.div`

View file

@ -27,34 +27,58 @@ uniform float uPolarRows; // how many rows from the pole are visible (mouse-whe
uniform float uTime;
// ── terrain colour lookup ──────────────────────────────────────────────────
// Indices must match TERRAIN_ORDER in runner.ts (40 entries)
vec3 terrainColor(float encoded) {
int idx = int(encoded * 24.0 + 0.5);
int idx = int(encoded * 39.0 + 0.5);
// Water
if (idx == 0) return vec3(0.24, 0.47, 0.82); // ocean
if (idx == 1) return vec3(0.31, 0.71, 0.86); // coast
if (idx == 2) return vec3(0.31, 0.63, 0.84); // lake
if (idx == 3) return vec3(0.25, 0.51, 0.80); // inland_sea
if (idx == 4) return vec3(0.88, 0.94, 1.00); // ice
if (idx == 5) return vec3(0.94, 0.96, 1.00); // snow
if (idx == 6) return vec3(0.72, 0.76, 0.65); // tundra
if (idx == 7) return vec3(0.87, 0.78, 0.50); // desert
if (idx == 8) return vec3(0.73, 0.82, 0.53); // plains
if (idx == 9) return vec3(0.38, 0.72, 0.35); // grassland
if (idx == 10) return vec3(0.20, 0.55, 0.25); // forest
if (idx == 11) return vec3(0.22, 0.44, 0.30); // boreal_forest
if (idx == 12) return vec3(0.09, 0.45, 0.18); // jungle
if (idx == 13) return vec3(0.42, 0.85, 0.55); // enchanted_forest
if (idx == 14) return vec3(0.58, 0.52, 0.38); // hills
if (idx == 15) return vec3(0.62, 0.60, 0.58); // mountains
if (idx == 16) return vec3(0.24, 0.31, 0.14); // swamp
if (idx == 17) return vec3(0.75, 0.20, 0.08); // volcano
// Natural wonders — geological/biological formations
if (idx == 18) return vec3(0.85, 0.70, 1.00); // mana_node (crystal glow)
if (idx == 19) return vec3(0.60, 0.95, 0.70); // ley_nexus (verdant pulse)
if (idx == 20) return vec3(0.70, 0.70, 0.85); // lodestone_spire (magnetic steel)
if (idx == 21) return vec3(0.75, 0.85, 1.00); // crystal_cavern (ice-blue shimmer)
if (idx == 22) return vec3(0.55, 0.40, 0.25); // worldroot (ancient bark)
if (idx == 23) return vec3(0.40, 0.80, 0.75); // primordial_spring (mineral teal)
return vec3(0.30, 0.50, 0.80); // abyssal_vortex (deep ocean glow)
if (idx == 1) return vec3(0.12, 0.27, 0.63); // deep_ocean
if (idx == 2) return vec3(0.31, 0.71, 0.86); // coast
if (idx == 3) return vec3(0.25, 0.75, 0.67); // coral_reef
if (idx == 4) return vec3(0.31, 0.63, 0.84); // lake
if (idx == 5) return vec3(0.25, 0.51, 0.80); // inland_sea
if (idx == 6) return vec3(0.35, 0.59, 0.67); // estuary
// Ice / Polar
if (idx == 7) return vec3(0.88, 0.94, 1.00); // ice
if (idx == 8) return vec3(0.94, 0.96, 1.00); // snow
if (idx == 9) return vec3(0.78, 0.80, 0.73); // polar_desert
// Cold
if (idx == 10) return vec3(0.72, 0.76, 0.65); // tundra
if (idx == 11) return vec3(0.66, 0.69, 0.60); // alpine_tundra
if (idx == 12) return vec3(0.22, 0.44, 0.30); // boreal_forest
// Temperate
if (idx == 13) return vec3(0.69, 0.62, 0.42); // chaparral
if (idx == 14) return vec3(0.73, 0.82, 0.53); // plains
if (idx == 15) return vec3(0.38, 0.72, 0.35); // grassland
if (idx == 16) return vec3(0.20, 0.55, 0.25); // forest
if (idx == 17) return vec3(0.10, 0.44, 0.20); // temperate_rainforest
// Warm / Tropical
if (idx == 18) return vec3(0.87, 0.78, 0.50); // desert
if (idx == 19) return vec3(0.74, 0.70, 0.40); // savanna
if (idx == 20) return vec3(0.50, 0.61, 0.25); // tropical_dry_forest
if (idx == 21) return vec3(0.09, 0.45, 0.18); // tropical_rainforest
if (idx == 22) return vec3(0.14, 0.40, 0.14); // jungle
if (idx == 23) return vec3(0.28, 0.47, 0.35); // mangrove
// Elevation
if (idx == 24) return vec3(0.58, 0.52, 0.38); // hills
if (idx == 25) return vec3(0.62, 0.60, 0.58); // mountains
if (idx == 26) return vec3(0.56, 0.69, 0.47); // alpine_meadow
if (idx == 27) return vec3(0.25, 0.47, 0.35); // cloud_forest
if (idx == 28) return vec3(0.18, 0.42, 0.28); // montane_forest
// Wetland
if (idx == 29) return vec3(0.24, 0.31, 0.14); // swamp
if (idx == 30) return vec3(0.45, 0.39, 0.22); // bog
// Special
if (idx == 31) return vec3(0.75, 0.20, 0.08); // volcano
// Magic compat (hidden from legend)
if (idx == 32) return vec3(0.42, 0.85, 0.55); // enchanted_forest
if (idx == 33) return vec3(0.85, 0.70, 1.00); // mana_node
if (idx == 34) return vec3(0.60, 0.95, 0.70); // ley_nexus
if (idx == 35) return vec3(0.70, 0.70, 0.85); // lodestone_spire
if (idx == 36) return vec3(0.75, 0.85, 1.00); // crystal_cavern
if (idx == 37) return vec3(0.55, 0.40, 0.25); // worldroot
if (idx == 38) return vec3(0.40, 0.80, 0.75); // primordial_spring
return vec3(0.30, 0.50, 0.80); // abyssal_vortex (fallback)
}
// ── gradient helpers ───────────────────────────────────────────────────────
@ -330,7 +354,7 @@ void main() {
// Marine health overlay (coast/ocean tiles only — reef > 0 is a proxy)
if (showMarine) {
int tidx = int(terrainEnc * 24.0 + 0.5);
int tidx = int(terrainEnc * 39.0 + 0.5);
if (tidx <= 3) {
col = mix(col, marineColor(reef), 0.80);
}
@ -362,7 +386,7 @@ void main() {
}
if (showFish) {
// Blue dots on water tiles with fish
int tidx2 = int(terrainEnc * 24.0 + 0.5);
int tidx2 = int(terrainEnc * 39.0 + 0.5);
if (tidx2 <= 3 && reef > 0.01) {
col = mix(col, vec3(0.20, 0.50, 0.95), 0.7 * reef);
}

View file

@ -10,18 +10,61 @@ const MAP_H = GRID_HEIGHT
// ── terrain color lookup ──────────────────────────────────────────────────
// Mirrors the GLSL terrainColor() function for vertex coloring.
// Indices must match TERRAIN_ORDER in runner.ts (39 entries).
const TERRAIN_COLORS: [number, number, number][] = [
[0.24, 0.47, 0.82], [0.31, 0.71, 0.86], [0.31, 0.63, 0.84], [0.25, 0.51, 0.80],
[0.88, 0.94, 1.00], [0.94, 0.96, 1.00], [0.72, 0.76, 0.65], [0.87, 0.78, 0.50],
[0.73, 0.82, 0.53], [0.38, 0.72, 0.35], [0.20, 0.55, 0.25], [0.22, 0.44, 0.30],
[0.09, 0.45, 0.18], [0.42, 0.85, 0.55], [0.58, 0.52, 0.38], [0.62, 0.60, 0.58],
[0.24, 0.31, 0.14], [0.75, 0.20, 0.08], [0.85, 0.70, 1.00], [0.60, 0.95, 0.70],
[0.70, 0.70, 0.85], [0.75, 0.85, 1.00], [0.55, 0.40, 0.25], [0.40, 0.80, 0.75],
[0.30, 0.50, 0.80],
// Water
[0.24, 0.47, 0.82], // 0 ocean
[0.12, 0.27, 0.63], // 1 deep_ocean
[0.31, 0.71, 0.86], // 2 coast
[0.25, 0.75, 0.67], // 3 coral_reef
[0.31, 0.63, 0.84], // 4 lake
[0.25, 0.51, 0.80], // 5 inland_sea
[0.35, 0.59, 0.67], // 6 estuary
// Ice / Polar
[0.88, 0.94, 1.00], // 7 ice
[0.94, 0.96, 1.00], // 8 snow
[0.78, 0.80, 0.73], // 9 polar_desert
// Cold
[0.72, 0.76, 0.65], // 10 tundra
[0.66, 0.69, 0.60], // 11 alpine_tundra
[0.22, 0.44, 0.30], // 12 boreal_forest
// Temperate
[0.69, 0.62, 0.42], // 13 chaparral
[0.73, 0.82, 0.53], // 14 plains
[0.38, 0.72, 0.35], // 15 grassland
[0.20, 0.55, 0.25], // 16 forest
[0.10, 0.44, 0.20], // 17 temperate_rainforest
// Warm / Tropical
[0.87, 0.78, 0.50], // 18 desert
[0.74, 0.70, 0.40], // 19 savanna
[0.50, 0.61, 0.25], // 20 tropical_dry_forest
[0.09, 0.45, 0.18], // 21 tropical_rainforest
[0.14, 0.40, 0.14], // 22 jungle
[0.28, 0.47, 0.35], // 23 mangrove
// Elevation
[0.58, 0.52, 0.38], // 24 hills
[0.62, 0.60, 0.58], // 25 mountains
[0.56, 0.69, 0.47], // 26 alpine_meadow
[0.25, 0.47, 0.35], // 27 cloud_forest
[0.18, 0.42, 0.28], // 28 montane_forest
// Wetland
[0.24, 0.31, 0.14], // 29 swamp
[0.45, 0.39, 0.22], // 30 bog
// Special
[0.75, 0.20, 0.08], // 31 volcano
// Magic compat
[0.42, 0.85, 0.55], // 32 enchanted_forest
[0.85, 0.70, 1.00], // 33 mana_node
[0.60, 0.95, 0.70], // 34 ley_nexus
[0.70, 0.70, 0.85], // 35 lodestone_spire
[0.75, 0.85, 1.00], // 36 crystal_cavern
[0.55, 0.40, 0.25], // 37 worldroot
[0.40, 0.80, 0.75], // 38 primordial_spring
[0.30, 0.50, 0.80], // 39 abyssal_vortex
]
export function terrainColorJS(encoded: number): [number, number, number] {
const idx = Math.round(encoded * 24)
const idx = Math.round(encoded * (TERRAIN_COLORS.length - 1))
return TERRAIN_COLORS[Math.min(idx, TERRAIN_COLORS.length - 1)] ?? TERRAIN_COLORS[0]!
}