perf(climate-sim): ⚡ Optimize HexGLRenderer with new fragment shaders for faster climate simulation rendering
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
28773e2864
commit
f65a169f8c
2 changed files with 140 additions and 17 deletions
|
|
@ -305,12 +305,35 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h
|
|||
const wonderPoints = new THREE.Points(wonderGeo, wonderMat)
|
||||
overlayScene.add(wonderPoints)
|
||||
|
||||
// ── polar hex meshes (geodesic ring layout) ──────────────────────────
|
||||
const DEFAULT_POLAR_RINGS = 8
|
||||
const polarMat = new THREE.ShaderMaterial({
|
||||
vertexShader: POLAR_VERT,
|
||||
fragmentShader: POLAR_FRAG,
|
||||
vertexColors: true,
|
||||
})
|
||||
|
||||
function makePolarMesh(pole: 'north' | 'south', rings: number): PolarMeshState {
|
||||
const { geometry, dataCoords } = buildPolarHexGrid(pole, rings, mapPxW, mapPxH)
|
||||
const mesh = new THREE.Mesh(geometry, polarMat)
|
||||
mesh.visible = false
|
||||
polarScene.add(mesh)
|
||||
// Initialize colors from current snapshot
|
||||
const colorAttr = geometry.getAttribute('color') as THREE.BufferAttribute
|
||||
updatePolarColors(colorAttr, dataCoords, snapshot.texA, snapshot.texB)
|
||||
return { mesh, dataCoords, numRings: rings }
|
||||
}
|
||||
|
||||
let polarNorth = makePolarMesh('north', DEFAULT_POLAR_RINGS)
|
||||
let polarSouth = makePolarMesh('south', DEFAULT_POLAR_RINGS)
|
||||
|
||||
const snapshotRef = { current: snapshot }
|
||||
buildLeyLines(leyGroup, snapshot.ley_edges)
|
||||
buildWonderMarkers(wonderGeo, snapshot.wonder_positions)
|
||||
|
||||
const startTime = performance.now()
|
||||
let animId = 0
|
||||
let activeView = 0 // 0=equator, 1=north, 2=south
|
||||
|
||||
const tick = (): void => {
|
||||
animId = requestAnimationFrame(tick)
|
||||
|
|
@ -331,18 +354,24 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h
|
|||
}
|
||||
|
||||
renderer.autoClear = true
|
||||
renderer.render(fieldScene, camera)
|
||||
renderer.autoClear = false
|
||||
renderer.render(overlayScene, camera)
|
||||
const currentView = gl.current?.activeView ?? 0
|
||||
if (currentView === 0) {
|
||||
renderer.render(fieldScene, camera)
|
||||
renderer.autoClear = false
|
||||
renderer.render(overlayScene, camera)
|
||||
} else {
|
||||
renderer.render(polarScene, camera)
|
||||
}
|
||||
}
|
||||
tick()
|
||||
|
||||
gl.current = {
|
||||
renderer, camera, fieldScene, overlayScene,
|
||||
texA, texB, texC, refTexA, fieldMaterial,
|
||||
renderer, camera, fieldScene, overlayScene, polarScene,
|
||||
texA, texB, texC, refTexA, fieldMaterial, fieldMesh,
|
||||
polarNorth, polarSouth,
|
||||
windGeo, windPositions, windVelocities, windPoints,
|
||||
leyGroup, wonderGeo, wonderPoints,
|
||||
animId, startTime, snapshotRef,
|
||||
animId, startTime, snapshotRef, activeView,
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
@ -354,6 +383,9 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h
|
|||
refTexA.dispose()
|
||||
fieldGeo.dispose()
|
||||
fieldMaterial.dispose()
|
||||
polarNorth.mesh.geometry.dispose()
|
||||
polarSouth.mesh.geometry.dispose()
|
||||
polarMat.dispose()
|
||||
windGeo.dispose()
|
||||
wonderGeo.dispose()
|
||||
}
|
||||
|
|
@ -374,6 +406,11 @@ 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
|
||||
const colorN = g.polarNorth.mesh.geometry.getAttribute('color') as THREE.BufferAttribute
|
||||
updatePolarColors(colorN, g.polarNorth.dataCoords, snapshot.texA, snapshot.texB)
|
||||
const colorS = g.polarSouth.mesh.geometry.getAttribute('color') as THREE.BufferAttribute
|
||||
updatePolarColors(colorS, g.polarSouth.dataCoords, snapshot.texA, snapshot.texB)
|
||||
}, [snapshot])
|
||||
|
||||
// ── update reference texture for delta layer ────────────────────────────
|
||||
|
|
@ -392,31 +429,118 @@ export function HexGLRenderer({ snapshot, referenceSnapshot, layerMask, width, h
|
|||
g.windPoints.visible = (layerMask & (1 << 3)) !== 0
|
||||
}, [layerMask])
|
||||
|
||||
// ── update view mode uniform for polar / equator projection ─────────────
|
||||
// ── camera zoom helper ─────────────────────────────────────────────────
|
||||
// Sets the orthographic camera to show a region centered on (cx, cy) with given half-extents.
|
||||
// Maintains the canvas aspect ratio.
|
||||
const setCameraView = (g: GLObjects, cx: number, cy: number, halfW: number, halfH: number): void => {
|
||||
g.camera.left = cx - halfW
|
||||
g.camera.right = cx + halfW
|
||||
g.camera.top = cy + halfH
|
||||
g.camera.bottom = cy - halfH
|
||||
g.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
const mapPxWRef = useRef(MAP_W * HEX_W * 0.75 + HEX_W * 0.25)
|
||||
const mapPxHRef = useRef(MAP_H * HEX_H + HEX_H * 0.5)
|
||||
const zoomRef = useRef(1.0) // 1.0 = fit-to-view, <1 = zoomed in, >1 = zoomed out
|
||||
|
||||
/** Fit the camera to the polar mesh radius with current zoom */
|
||||
const fitPolarCamera = (g: GLObjects, numRings: number): void => {
|
||||
const hexR = HEX_W / 2
|
||||
const hexInradius = hexR * Math.sqrt(3) / 2
|
||||
const meshRadius = numRings * hexInradius * 2 + hexR // radius in pixels + margin
|
||||
const cx = mapPxWRef.current / 2
|
||||
const cy = mapPxHRef.current / 2
|
||||
const aspect = mapPxWRef.current / mapPxHRef.current
|
||||
const halfH = meshRadius * 1.15 * zoomRef.current
|
||||
const halfW = halfH * aspect
|
||||
setCameraView(g, cx, cy, halfW, halfH)
|
||||
}
|
||||
|
||||
/** Fit the camera to the equator map 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
|
||||
setCameraView(g, w / 2, h / 2, halfW, halfH)
|
||||
}
|
||||
|
||||
// ── switch between equator field mesh and polar hex meshes ──────────────
|
||||
useEffect(() => {
|
||||
const g = gl.current
|
||||
if (!g) return
|
||||
const viewInt = viewCenter === 'north' ? 1 : viewCenter === 'south' ? 2 : 0
|
||||
g.fieldMaterial.uniforms.uViewCenter.value = viewInt
|
||||
// Hide overlays in polar mode — they render in world-pixel space, not projected
|
||||
g.activeView = viewInt
|
||||
zoomRef.current = 1.0 // reset zoom on view switch
|
||||
|
||||
// Show/hide meshes
|
||||
g.fieldMesh.visible = viewInt === 0
|
||||
g.polarNorth.mesh.visible = viewInt === 1
|
||||
g.polarSouth.mesh.visible = viewInt === 2
|
||||
|
||||
// Overlays only in equator mode
|
||||
g.leyGroup.visible = viewInt === 0
|
||||
g.wonderPoints.visible = viewInt === 0
|
||||
g.windPoints.visible = viewInt === 0 && (g.fieldMaterial.uniforms.uLayerMask.value & (1 << 3)) !== 0
|
||||
|
||||
// Set camera to fit the current view
|
||||
if (viewInt === 0) {
|
||||
fitEquatorCamera(g)
|
||||
} else {
|
||||
const polar = viewInt === 1 ? g.polarNorth : g.polarSouth
|
||||
fitPolarCamera(g, polar.numRings)
|
||||
}
|
||||
}, [viewCenter])
|
||||
|
||||
// ── mouse wheel zoom for polar view ───────────────────────────────────────
|
||||
// ── mouse wheel zoom (works for all views) ────────────────────────────────
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const MIN_ROWS = 3
|
||||
const MAX_ROWS = Math.floor(MAP_H / 2)
|
||||
const MIN_RINGS = 3
|
||||
const MAX_RINGS = Math.floor(MAP_H / 2)
|
||||
const mapPxW = mapPxWRef.current
|
||||
const mapPxH = mapPxHRef.current
|
||||
|
||||
const rebuildPolar = (g: GLObjects, pole: 'north' | 'south', rings: number): PolarMeshState => {
|
||||
const old = pole === 'north' ? g.polarNorth : g.polarSouth
|
||||
g.polarScene.remove(old.mesh)
|
||||
old.mesh.geometry.dispose()
|
||||
const { geometry, dataCoords } = buildPolarHexGrid(pole, rings, mapPxW, mapPxH)
|
||||
const mesh = new THREE.Mesh(geometry, old.mesh.material)
|
||||
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)
|
||||
return { mesh, dataCoords, numRings: rings }
|
||||
}
|
||||
|
||||
const ZOOM_STEP = 0.1
|
||||
const MIN_ZOOM = 0.3
|
||||
const MAX_ZOOM = 2.0
|
||||
|
||||
const onWheel = (e: WheelEvent): void => {
|
||||
const g = gl.current
|
||||
if (!g || g.fieldMaterial.uniforms.uViewCenter.value === 0) return
|
||||
if (!g) return
|
||||
e.preventDefault()
|
||||
const current = g.fieldMaterial.uniforms.uPolarRows.value as number
|
||||
const delta = e.deltaY > 0 ? 1 : -1 // scroll down = zoom out (more rows)
|
||||
g.fieldMaterial.uniforms.uPolarRows.value = Math.max(MIN_ROWS, Math.min(MAX_ROWS, current + delta))
|
||||
|
||||
if (g.activeView === 0) {
|
||||
// Equator: camera zoom
|
||||
const delta = e.deltaY > 0 ? ZOOM_STEP : -ZOOM_STEP
|
||||
zoomRef.current = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoomRef.current + delta))
|
||||
fitEquatorCamera(g)
|
||||
} else {
|
||||
// Polar: change ring count + fit camera
|
||||
const polar = g.activeView === 1 ? g.polarNorth : g.polarSouth
|
||||
const pole = g.activeView === 1 ? 'north' as const : 'south' as const
|
||||
const delta = e.deltaY > 0 ? 1 : -1
|
||||
const newRings = Math.max(MIN_RINGS, Math.min(MAX_RINGS, polar.numRings + delta))
|
||||
if (newRings === polar.numRings) return
|
||||
const newState = rebuildPolar(g, pole, newRings)
|
||||
if (pole === 'north') g.polarNorth = newState
|
||||
else g.polarSouth = newState
|
||||
fitPolarCamera(g, newRings)
|
||||
}
|
||||
}
|
||||
canvas.addEventListener('wheel', onWheel, { passive: false })
|
||||
return () => canvas.removeEventListener('wheel', onWheel)
|
||||
|
|
|
|||
|
|
@ -457,7 +457,6 @@ void main() {
|
|||
// ── polar hex mesh shaders ────────────────────────────────────────────────
|
||||
// Vertex colors are computed in JS from the data textures each frame.
|
||||
export const POLAR_VERT = /* glsl */ `
|
||||
attribute vec3 color;
|
||||
varying vec3 vColor;
|
||||
void main() {
|
||||
vColor = color;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue