perf(climate-sim): Optimize WebGL buffer updates for hexagonal grid rendering in HexGLRenderer

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 00:14:19 -07:00
parent 0adb923687
commit ac5cc84e99

View file

@ -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 }
}