Skip to content

feat: add tectonic plate simulation for physically-based heightmap generation#1362

Open
Dobidop wants to merge 9 commits intoAzgaar:masterfrom
Dobidop:master
Open

feat: add tectonic plate simulation for physically-based heightmap generation#1362
Dobidop wants to merge 9 commits intoAzgaar:masterfrom
Dobidop:master

Conversation

@Dobidop
Copy link
Copy Markdown

@Dobidop Dobidop commented Mar 22, 2026

Disclaimer

This has been mostly been created using Claude Code opus because I could never create this by myself, but I have tested it as best as I can. I made this for myself mostly, but I'll just lift this as a pull request just in case you want to merge it.

Summary

Adds a complete tectonic plate simulation system that generates heightmaps through physically-modeled plate tectonics on a spherical icosphere mesh, then projects the results onto the flat map grid. Includes an interactive plate editor with paint mode for manual plate reshaping. There is also a sea level parameter for the presets, but this isn't exposed in the UI currently.

  • Tectonic Generator (tectonic-generator.ts, 1,390 lines): A 13-step simulation pipeline that seeds plates on an icosphere, grows them via randomized frontier BFS, assigns velocities as 3D tangent vectors, classifies boundary types (convergent/divergent/transform), computes boundary elevations based on real plate interaction physics (continental collision, subduction trenches, rift valleys, mid-ocean ridges), adds continental shelves, hotspot volcanoes, hydraulic erosion, and fractal noise, then projects from spherical to equirectangular grid using a bucketed spatial index for efficient lookup
  • Tectonic Editor (tectonic-editor.js, 648 lines): Interactive UI for selecting plates, toggling oceanic/continental type, adjusting velocity via sliders or by dragging arrow handles directly on the map, and a paint mode with configurable brush radius to reassign cells between plates. Supports preview regeneration and full map rebuild
  • 4 preset templates: Tectonic (default), Pangea (supercontinent), Archipelago (island-heavy), and Rift (deep valleys and tall mountains), each with tuned parameters for plate count, continental ratio, collision intensity, erosion passes, and sea level

Key design decisions

  • Icosphere mesh for the simulation sphere rather than a lat/lon grid, avoiding polar distortion and ensuring uniform cell sizes across the surface
  • Bucketed spatial indexing (72×36 bins at 5° resolution) for sphere-to-grid projection, reducing O(n²) nearest-face lookups to O(1) average
  • Asymmetric elevation diffusion: boundary cells retain 92% of their value (preserving mountain sharpness) while interior cells blend 60/40 with neighbors (smoothing valleys)
  • Coastal fractal pass applied post-projection to create irregular coastlines with bays and peninsulas
  • Regeneration preserves plate state: editing plates and regenerating only recalculates elevation-dependent steps, keeping the same seed for deterministic noise/erosion

Boundary physics model

Boundary Type Effect Real-world analogue
Continental-Continental convergent +40–80 elevation Himalayas
Ocean-Continental convergent (ocean side) -12–22 (trench) Mariana Trench
Ocean-Continental convergent (land side) +30–65 (volcanic arc) Andes
Ocean-Ocean convergent +20–40 (island arc) Japan
Continental rift (divergent) -18–33 East African Rift
Ocean rift (divergent) +5–10 (mid-ocean ridge) Mid-Atlantic Ridge
Transform ±5 random San Andreas Fault

Files changed

File Change
src/modules/tectonic-generator.ts New — core simulation engine
src/types/TectonicMetadata.ts New — TypeScript interfaces for plates, boundaries, config, metadata
public/modules/ui/tectonic-editor.js New — interactive plate editor with paint mode
public/config/tectonic-templates.js New — 4 preset template configurations
src/modules/heightmap-generator.ts Added fromTectonic() integration method
public/modules/dynamic/heightmap-selection.js Template list now includes tectonic templates
src/index.html Added editor dialog HTML, script tags, Edit Tectonics button
public/modules/ui/tools.js Route click to tectonic editor
src/types/global.ts Added global type augmentation

Known limitations

  • d3.mouse() is used in the editor (deprecated in D3 v7+) — should be migrated to d3.pointer() if the project upgrades D3
  • Velocity Z-component is zeroed when editing via UI drag, losing spherical detail (acceptable since the 2D projection dominates the final output)
  • Plate count limited to 255 (Uint8Array storage) — more than sufficient for realistic maps

Test plan

  • Generate maps with each of the 4 tectonic templates and verify realistic continent/ocean distribution
  • Open tectonic editor, select plates, modify velocity via sliders and drag handles
  • Use paint mode to reassign cells between plates, verify overlay updates correctly
  • Click "Regenerate Preview" and verify height overlay reflects plate changes
  • Click "Apply to Map" and verify full map rebuild (rivers, cultures, states, etc.)
  • Verify editor cleanup on close (overlay removed, no stale SVG elements)
  • Test with different grid sizes (small/medium/large maps)
Seia 2026-03-22-15-08 image

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 22, 2026

Deploy Preview for afmg ready!

Name Link
🔨 Latest commit 61e76eb
🔍 Latest deploy log https://app.netlify.com/projects/afmg/deploys/69bffaa474f0480008f30afb
😎 Deploy Preview https://deploy-preview-1362--afmg.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new tectonic-plate-based heightmap generator and a corresponding UI editor, integrating the new tectonic templates into the existing heightmap selection/randomization flow.

Changes:

  • Introduces a full tectonic simulation pipeline on an icosphere mesh and projects results onto the map grid
  • Adds an in-app “Tectonics” editor (plate selection, velocity editing, paint reassignment, preview regen, apply-to-map)
  • Registers 4 tectonic presets and wires them into template selection + random template picking

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/types/global.ts Adds global typing for tectonicTemplates
src/types/TectonicMetadata.ts New tectonic metadata/config/types + global declarations
src/modules/tectonic-generator.ts New tectonic plate simulation + projection + metadata generation
src/modules/heightmap-generator.ts Integrates tectonic generation via fromTectonic()
src/index.html Adds Tectonics button, editor dialog markup, and script/template includes
public/modules/ui/tools.js Routes new toolbar button to editTectonics()
public/modules/ui/tectonic-editor.js New tectonic editor UI/overlay/paint tooling
public/modules/ui/options.js Includes tectonic templates in random template selection
public/modules/dynamic/heightmap-selection.js Shows tectonic templates in selection UI and renders previews
public/config/tectonic-templates.js New preset template definitions + probabilities
.claude/settings.local.json New Claude local settings file added to repo

Comment on lines +621 to +627
fromTectonic(graph: any, config: TectonicConfig): Uint8Array {
this.setGraph(graph);
const generator = new TectonicPlateGenerator(this.grid!, config);
const result = generator.generate();
this.heights = result.heights;
window.tectonicMetadata = result.metadata;
window.tectonicGenerator = generator;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fromTectonic always mutates global state (window.tectonicMetadata / window.tectonicGenerator). This method is also used by the heightmap selection UI to render previews, which can leave stale tectonic globals pointing at a preview graph (and make the Tectonics editor appear available on non-tectonic maps). Consider adding a side-effect-free path for preview generation (e.g., an optional flag to skip setting globals, or a separate previewFromTectonic method) and only persist globals during actual map generation.

Suggested change
fromTectonic(graph: any, config: TectonicConfig): Uint8Array {
this.setGraph(graph);
const generator = new TectonicPlateGenerator(this.grid!, config);
const result = generator.generate();
this.heights = result.heights;
window.tectonicMetadata = result.metadata;
window.tectonicGenerator = generator;
fromTectonic(
graph: any,
config: TectonicConfig,
options?: { persistGlobals?: boolean },
): Uint8Array {
const { persistGlobals = true } = options || {};
this.setGraph(graph);
const generator = new TectonicPlateGenerator(this.grid!, config);
const result = generator.generate();
this.heights = result.heights;
if (persistGlobals) {
window.tectonicMetadata = result.metadata;
window.tectonicGenerator = generator;
}

Copilot uses AI. Check for mistakes.
Comment on lines +202 to 210
sections[0].innerHTML = allTemplateKeys
.map(key => {
const name = heightmapTemplates[key].name;
const isTectonic = typeof tectonicTemplates !== "undefined" && key in tectonicTemplates;
const name = isTectonic ? tectonicTemplates[key].name : heightmapTemplates[key].name;
Math.random = aleaPRNG(initialSeed);
const heights = HeightmapGenerator.fromTemplate(graph, key);
const heights = isTectonic
? HeightmapGenerator.fromTectonic(graph, tectonicTemplates[key].config)
: HeightmapGenerator.fromTemplate(graph, key);

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heightmap selection screen generates previews by calling HeightmapGenerator.fromTectonic(...) for each tectonic template. Because fromTectonic currently stores window.tectonicGenerator / window.tectonicMetadata, opening the selection dialog can overwrite the active map's tectonic state (or create tectonic state when the current map isn't tectonic). Consider using a preview-only API that doesn't touch globals, or explicitly clearing/restoring those globals around preview rendering.

Copilot uses AI. Check for mistakes.
boundaryType,
roughness,
isOceanic: isOceanicArr,
plates: this.plates,
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metadata includes plates: this.plates, but each TectonicPlate contains a potentially huge cells: Set<number> (sphere faces). This duplicates large in-memory state already held by the generator and makes tectonicMetadata expensive to keep around (and not serializable if ever persisted). If the UI only needs grid-level arrays, consider omitting plates (or stripping cells) from TectonicMetadata and accessing plate details via tectonicGenerator.getPlates() instead.

Suggested change
plates: this.plates,

Copilot uses AI. Check for mistakes.
Comment on lines +650 to +652
if (!pair.cells.includes(i)) {
pair.cells.push(i);
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detectBoundaries adds each face i to a single plate-pair and then breaks, so pair.cells can't contain duplicates of i. The if (!pair.cells.includes(i)) check is therefore redundant and adds an O(n) scan on every boundary insert. Consider removing the includes check (or using a Set if you later need de-duplication).

Suggested change
if (!pair.cells.includes(i)) {
pair.cells.push(i);
}
pair.cells.push(i);

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +59
var tectonicMetadata: TectonicMetadata | null;
var tectonicGenerator: import("../modules/tectonic-generator").TectonicPlateGenerator | null;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tectonicMetadata / tectonicGenerator are used via window.* in TS code, but here they're declared as global var bindings. In this codebase, globals are typically typed by augmenting interface Window (e.g., src/utils/commonUtils.ts:374-392). Consider switching this to a declare global { interface Window { ... } } augmentation (or updating usages to the bare global identifiers) to avoid TS property errors and keep typings consistent.

Suggested change
var tectonicMetadata: TectonicMetadata | null;
var tectonicGenerator: import("../modules/tectonic-generator").TectonicPlateGenerator | null;
interface Window {
tectonicMetadata: TectonicMetadata | null;
tectonicGenerator: import("../modules/tectonic-generator").TectonicPlateGenerator | null;
}

Copilot uses AI. Check for mistakes.
boundaries: this.boundaries
};

this.logDiagnostics(heights);
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectAndFinalize unconditionally calls this.logDiagnostics(heights), which prints a very large ASCII report to the console on every generation/regeneration. This is likely to spam the console and impact performance in normal usage. Consider guarding diagnostics behind a debug flag (e.g., window.DEBUG?.tectonics) or removing it for production builds.

Suggested change
this.logDiagnostics(heights);
if (typeof window !== "undefined" && (window as any).DEBUG?.tectonics) {
this.logDiagnostics(heights);
}

Copilot uses AI. Check for mistakes.
Comment on lines +887 to +895
while (queue.length > 0) {
const cell = queue.shift()!;
for (const neighbor of this.sphere.neighbors[cell]) {
if (visited[neighbor]) continue;
visited[neighbor] = 1;
change[neighbor] = change[cell] * (0.7 + this.rng() * 0.15);
if (change[neighbor] > 1) queue.push(neighbor);
}
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addHotspots, the BFS loop uses queue.shift() to pop items. On larger spheres this can become O(n²) due to array reindexing, and hotspot spread can touch many faces. Consider iterating with an index pointer (like let idx=0; while (idx < queue.length) { const cell = queue[idx++]; ... }) or using a small queue implementation.

Copilot uses AI. Check for mistakes.
<script defer src="modules/ui/tools.js?v=1.113.3"></script>
<script defer src="modules/ui/world-configurator.js?v=1.105.4"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.113.0"></script>
<script defer src="modules/ui/tectonic-editor.js?v=1.0.0"></script>
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new script tag uses a hard-coded cache-busting version ?v=1.0.0, while nearby scripts use the app's current v=1.11x.x pattern. If these query params are relied on for cache invalidation, this can cause stale assets after deploy. Consider aligning the version parameter with the repo's existing convention (or using the same global/versioned build mechanism as other UI modules).

Suggested change
<script defer src="modules/ui/tectonic-editor.js?v=1.0.0"></script>
<script defer src="modules/ui/tectonic-editor.js?v=1.113.0"></script>

Copilot uses AI. Check for mistakes.
@Azgaar
Copy link
Copy Markdown
Owner

Azgaar commented Mar 22, 2026

Hey! This is a cool concept, but I don't really understand how to use it and it produces very weird maps... Probably could be worked on to make a clearer UI and make it usable for common users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants