diff --git a/guide/engine/src/components/climate-sim/HexGLRenderer.tsx b/guide/engine/src/components/climate-sim/HexGLRenderer.tsx index 45fc09af..39383094 100644 --- a/guide/engine/src/components/climate-sim/HexGLRenderer.tsx +++ b/guide/engine/src/components/climate-sim/HexGLRenderer.tsx @@ -87,7 +87,54 @@ function buildPolarHexGrid( } } - // Build geometry: each hex = center + 6 corners = 6 triangles + return buildHexGeometry(tiles, hexR) +} + +/** Update hex mesh vertex colors from simulation data. Shared by equator and polar meshes. */ +function updateHexMeshColors( + colorAttr: THREE.BufferAttribute, + dataCoords: Array<{ col: number; row: number }>, + texB: Float32Array, +): void { + const vertsPerHex = 7 + for (let h = 0; h < dataCoords.length; h++) { + const { col, row } = dataCoords[h]! + const texIdx = (row * MAP_W + col) * 4 + const terrainEnc = texB[texIdx + 2] ?? 0 + const [r, g, b] = terrainColorJS(terrainEnc) + for (let v = 0; v < vertsPerHex; v++) { + const ci = (h * vertsPerHex + v) * 3 + colorAttr.array[ci] = r + colorAttr.array[ci + 1] = g + colorAttr.array[ci + 2] = b + } + } + colorAttr.needsUpdate = true +} + +// ── equator hex mesh builder ────────────────────────────────────────────── +// Standard flat hex grid: 40 cols × 24 rows. Same hexCenter() positioning +// as the GLSL shader, ensuring identical tile mapping. +function buildEquatorHexGrid(): PolarHexData { + const hexR = HEX_W / 2 + const tiles: Array<{ x: number; y: number; col: number; row: number }> = [] + + for (let row = 0; row < MAP_H; row++) { + for (let col = 0; col < MAP_W; col++) { + const cx = col * HEX_W * 0.75 + HEX_W / 2 + const cy = row * HEX_H + (col % 2 === 1 ? HEX_H / 2 : 0) + HEX_H / 2 + tiles.push({ x: cx, y: cy, col, row }) + } + } + + return buildHexGeometry(tiles, hexR) +} + +/** Shared geometry builder: takes positioned tiles and creates a BufferGeometry. */ +function buildHexGeometry( + tiles: Array<{ x: number; y: number; col: number; row: number }>, + hexR: number, +): PolarHexData { const hexCount = tiles.length const vertsPerHex = 7 const positions = new Float32Array(hexCount * vertsPerHex * 3) @@ -97,13 +144,9 @@ function buildPolarHexGrid( for (let h = 0; h < hexCount; h++) { const tile = tiles[h]! const base = h * vertsPerHex - - // Center vertex positions[base * 3] = tile.x positions[base * 3 + 1] = tile.y positions[base * 3 + 2] = 0 - - // 6 corner vertices (flat-top hex) for (let v = 0; v < 6; v++) { const a = (Math.PI / 3) * v const vi = base + 1 + v @@ -111,8 +154,6 @@ function buildPolarHexGrid( positions[vi * 3 + 1] = tile.y + hexR * Math.sin(a) positions[vi * 3 + 2] = 0 } - - // 6 triangles (fan from center) for (let v = 0; v < 6; v++) { indices.push(base, base + 1 + v, base + 1 + (v + 1) % 6) } @@ -122,33 +163,7 @@ function buildPolarHexGrid( geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)) geometry.setIndex(indices) - - const dataCoords = tiles.map((t) => ({ col: t.col, row: t.row })) - return { geometry, dataCoords, hexCount } -} - -/** Update polar mesh vertex colors from the simulation data textures. */ -function updatePolarColors( - colorAttr: THREE.BufferAttribute, - dataCoords: Array<{ col: number; row: number }>, - texA: Float32Array, - texB: Float32Array, -): void { - const vertsPerHex = 7 - for (let h = 0; h < dataCoords.length; h++) { - const { col, row } = dataCoords[h]! - const texIdx = (row * MAP_W + col) * 4 - const terrainEnc = texB[texIdx + 2] ?? 0 - const [r, g, b] = terrainColorJS(terrainEnc) - // Set all 7 vertices of this hex to the same color - for (let v = 0; v < vertsPerHex; v++) { - const ci = (h * vertsPerHex + v) * 3 - colorAttr.array[ci] = r - colorAttr.array[ci + 1] = g - colorAttr.array[ci + 2] = b - } - } - colorAttr.needsUpdate = true + return { geometry, dataCoords: tiles.map((t) => ({ col: t.col, row: t.row })), hexCount } } function hexCenter(col: number, row: number): [number, number] { @@ -173,18 +188,25 @@ interface PolarMeshState { numRings: number } +interface HexMeshState { + mesh: THREE.Mesh + dataCoords: Array<{ col: number; row: number }> +} + interface GLObjects { renderer: THREE.WebGLRenderer camera: THREE.OrthographicCamera fieldScene: THREE.Scene overlayScene: THREE.Scene polarScene: THREE.Scene + equatorScene: THREE.Scene texA: THREE.DataTexture texB: THREE.DataTexture texC: THREE.DataTexture refTexA: THREE.DataTexture fieldMaterial: THREE.ShaderMaterial fieldMesh: THREE.Mesh + equatorHex: HexMeshState polarNorth: PolarMeshState polarSouth: PolarMeshState windGeo: THREE.BufferGeometry @@ -234,6 +256,8 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h const overlayScene = new THREE.Scene() const polarScene = new THREE.Scene() polarScene.background = new THREE.Color(0x04030a) + const equatorScene = new THREE.Scene() + equatorScene.background = new THREE.Color(0x08060f) const mkTex = (data: Float32Array): THREE.DataTexture => { const t = new THREE.DataTexture(data, MAP_W, MAP_H, THREE.RGBAFormat, THREE.FloatType) @@ -320,13 +344,23 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h polarScene.add(mesh) // Initialize colors from current snapshot const colorAttr = geometry.getAttribute('color') as THREE.BufferAttribute - updatePolarColors(colorAttr, dataCoords, snapshot.texA, snapshot.texB) + updateHexMeshColors(colorAttr, dataCoords, snapshot.texB) return { mesh, dataCoords, numRings: rings } } let polarNorth = makePolarMesh('north', DEFAULT_POLAR_RINGS) let polarSouth = makePolarMesh('south', DEFAULT_POLAR_RINGS) + // ── equator hex mesh (same geometry approach as polar, flat grid layout) ── + const equatorData = buildEquatorHexGrid() + const equatorMesh = new THREE.Mesh(equatorData.geometry, polarMat) + equatorScene.add(equatorMesh) + updateHexMeshColors( + equatorData.geometry.getAttribute('color') as THREE.BufferAttribute, + equatorData.dataCoords, snapshot.texB, + ) + const equatorHex: HexMeshState = { mesh: equatorMesh, dataCoords: equatorData.dataCoords } + const snapshotRef = { current: snapshot } buildLeyLines(leyGroup, snapshot.ley_edges) buildWonderMarkers(wonderGeo, snapshot.wonder_positions) @@ -356,19 +390,21 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h renderer.autoClear = true const currentView = gl.current?.activeView ?? 0 if (currentView === 0) { - renderer.render(fieldScene, camera) + // Equator: geometry hex mesh + overlays + renderer.render(equatorScene, camera) renderer.autoClear = false renderer.render(overlayScene, camera) } else { + // Polar: geodesic hex mesh renderer.render(polarScene, camera) } } tick() gl.current = { - renderer, camera, fieldScene, overlayScene, polarScene, + renderer, camera, fieldScene, overlayScene, polarScene, equatorScene, texA, texB, texC, refTexA, fieldMaterial, fieldMesh, - polarNorth, polarSouth, + equatorHex, polarNorth, polarSouth, windGeo, windPositions, windVelocities, windPoints, leyGroup, wonderGeo, wonderPoints, animId, startTime, snapshotRef, activeView, @@ -383,6 +419,7 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h refTexA.dispose() fieldGeo.dispose() fieldMaterial.dispose() + equatorData.geometry.dispose() polarNorth.mesh.geometry.dispose() polarSouth.mesh.geometry.dispose() polarMat.dispose() @@ -406,11 +443,13 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h } buildLeyLines(g.leyGroup, snapshot.ley_edges) buildWonderMarkers(g.wonderGeo, snapshot.wonder_positions) - // Update polar mesh colors from new data + // Update all hex mesh colors from new data (equator + both poles use same function) + const colorE = g.equatorHex.mesh.geometry.getAttribute('color') as THREE.BufferAttribute + updateHexMeshColors(colorE, g.equatorHex.dataCoords, snapshot.texB) const colorN = g.polarNorth.mesh.geometry.getAttribute('color') as THREE.BufferAttribute - updatePolarColors(colorN, g.polarNorth.dataCoords, snapshot.texA, snapshot.texB) + updateHexMeshColors(colorN, g.polarNorth.dataCoords, snapshot.texB) const colorS = g.polarSouth.mesh.geometry.getAttribute('color') as THREE.BufferAttribute - updatePolarColors(colorS, g.polarSouth.dataCoords, snapshot.texA, snapshot.texB) + updateHexMeshColors(colorS, g.polarSouth.dataCoords, snapshot.texB) }, [snapshot]) // ── update reference texture for delta layer ──────────────────────────── @@ -457,12 +496,12 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h setCameraView(g, cx, cy, halfW, halfH) } - /** Fit the camera to the equator map with current zoom */ + /** Fit the camera to the equator hex mesh with current zoom */ const fitEquatorCamera = (g: GLObjects): void => { const w = mapPxWRef.current const h = mapPxHRef.current - const halfW = (w / 2) * zoomRef.current - const halfH = (h / 2) * zoomRef.current + const halfW = (w / 2) / zoomRef.current + const halfH = (h / 2) / zoomRef.current setCameraView(g, w / 2, h / 2, halfW, halfH) } @@ -511,7 +550,7 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h mesh.visible = old.mesh.visible g.polarScene.add(mesh) const colorAttr = geometry.getAttribute('color') as THREE.BufferAttribute - updatePolarColors(colorAttr, dataCoords, g.snapshotRef.current.texA, g.snapshotRef.current.texB) + updateHexMeshColors(colorAttr, dataCoords, g.snapshotRef.current.texB) return { mesh, dataCoords, numRings: rings } }