pyworldgen 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. pyworldgen-0.1.0/PKG-INFO +460 -0
  2. pyworldgen-0.1.0/README.md +416 -0
  3. pyworldgen-0.1.0/pyproject.toml +72 -0
  4. pyworldgen-0.1.0/setup.cfg +4 -0
  5. pyworldgen-0.1.0/src/pyworldgen/__init__.py +73 -0
  6. pyworldgen-0.1.0/src/pyworldgen/biomes/__init__.py +60 -0
  7. pyworldgen-0.1.0/src/pyworldgen/biomes/cave.py +353 -0
  8. pyworldgen-0.1.0/src/pyworldgen/biomes/flat.py +110 -0
  9. pyworldgen-0.1.0/src/pyworldgen/biomes/forest.py +697 -0
  10. pyworldgen-0.1.0/src/pyworldgen/biomes/landscape.py +542 -0
  11. pyworldgen-0.1.0/src/pyworldgen/compose/__init__.py +23 -0
  12. pyworldgen-0.1.0/src/pyworldgen/compose/forest_overlay.py +57 -0
  13. pyworldgen-0.1.0/src/pyworldgen/compose/palette.py +70 -0
  14. pyworldgen-0.1.0/src/pyworldgen/compose/regions.py +77 -0
  15. pyworldgen-0.1.0/src/pyworldgen/compose/world.py +198 -0
  16. pyworldgen-0.1.0/src/pyworldgen/core/__init__.py +67 -0
  17. pyworldgen-0.1.0/src/pyworldgen/core/aabb.py +104 -0
  18. pyworldgen-0.1.0/src/pyworldgen/core/coords.py +82 -0
  19. pyworldgen-0.1.0/src/pyworldgen/core/hash.py +215 -0
  20. pyworldgen-0.1.0/src/pyworldgen/core/math_types.py +83 -0
  21. pyworldgen-0.1.0/src/pyworldgen/core/rng.py +37 -0
  22. pyworldgen-0.1.0/src/pyworldgen/fields/__init__.py +5 -0
  23. pyworldgen-0.1.0/src/pyworldgen/fields/base.py +81 -0
  24. pyworldgen-0.1.0/src/pyworldgen/generation/__init__.py +27 -0
  25. pyworldgen-0.1.0/src/pyworldgen/generation/biomes.py +39 -0
  26. pyworldgen-0.1.0/src/pyworldgen/generation/caves.py +78 -0
  27. pyworldgen-0.1.0/src/pyworldgen/generation/features.py +43 -0
  28. pyworldgen-0.1.0/src/pyworldgen/generation/pass_base.py +39 -0
  29. pyworldgen-0.1.0/src/pyworldgen/generation/scheduler.py +48 -0
  30. pyworldgen-0.1.0/src/pyworldgen/generation/structures.py +20 -0
  31. pyworldgen-0.1.0/src/pyworldgen/generation/terrain.py +46 -0
  32. pyworldgen-0.1.0/src/pyworldgen/io/__init__.py +50 -0
  33. pyworldgen-0.1.0/src/pyworldgen/io/obj.py +159 -0
  34. pyworldgen-0.1.0/src/pyworldgen/io/pointcloud_io.py +93 -0
  35. pyworldgen-0.1.0/src/pyworldgen/io/serialization.py +98 -0
  36. pyworldgen-0.1.0/src/pyworldgen/io/voxel_io.py +105 -0
  37. pyworldgen-0.1.0/src/pyworldgen/mesh/__init__.py +18 -0
  38. pyworldgen-0.1.0/src/pyworldgen/mesh/cube_faces.py +21 -0
  39. pyworldgen-0.1.0/src/pyworldgen/mesh/dense_mesher.py +150 -0
  40. pyworldgen-0.1.0/src/pyworldgen/mesh/greedy_mesher.py +123 -0
  41. pyworldgen-0.1.0/src/pyworldgen/mesh/mesh.py +76 -0
  42. pyworldgen-0.1.0/src/pyworldgen/mesh/naive_mesher.py +54 -0
  43. pyworldgen-0.1.0/src/pyworldgen/noise/__init__.py +95 -0
  44. pyworldgen-0.1.0/src/pyworldgen/noise/_grad.py +54 -0
  45. pyworldgen-0.1.0/src/pyworldgen/noise/cellular.py +76 -0
  46. pyworldgen-0.1.0/src/pyworldgen/noise/fractal.py +87 -0
  47. pyworldgen-0.1.0/src/pyworldgen/noise/gabor.py +67 -0
  48. pyworldgen-0.1.0/src/pyworldgen/noise/opensimplex.py +98 -0
  49. pyworldgen-0.1.0/src/pyworldgen/noise/perlin.py +64 -0
  50. pyworldgen-0.1.0/src/pyworldgen/noise/phasor.py +44 -0
  51. pyworldgen-0.1.0/src/pyworldgen/noise/simplex.py +104 -0
  52. pyworldgen-0.1.0/src/pyworldgen/noise/warp.py +67 -0
  53. pyworldgen-0.1.0/src/pyworldgen/noise/wave.py +61 -0
  54. pyworldgen-0.1.0/src/pyworldgen/noise/wavelet.py +128 -0
  55. pyworldgen-0.1.0/src/pyworldgen/pointcloud/__init__.py +12 -0
  56. pyworldgen-0.1.0/src/pyworldgen/pointcloud/cloud.py +155 -0
  57. pyworldgen-0.1.0/src/pyworldgen/registry.py +147 -0
  58. pyworldgen-0.1.0/src/pyworldgen/rendering/__init__.py +122 -0
  59. pyworldgen-0.1.0/src/pyworldgen/rendering/animation.py +86 -0
  60. pyworldgen-0.1.0/src/pyworldgen/rendering/backends.py +124 -0
  61. pyworldgen-0.1.0/src/pyworldgen/rendering/camera.py +130 -0
  62. pyworldgen-0.1.0/src/pyworldgen/rendering/colormap.py +54 -0
  63. pyworldgen-0.1.0/src/pyworldgen/rendering/paths.py +145 -0
  64. pyworldgen-0.1.0/src/pyworldgen/rendering/scene.py +114 -0
  65. pyworldgen-0.1.0/src/pyworldgen/rendering/software.py +169 -0
  66. pyworldgen-0.1.0/src/pyworldgen/roads/__init__.py +38 -0
  67. pyworldgen-0.1.0/src/pyworldgen/roads/cost.py +61 -0
  68. pyworldgen-0.1.0/src/pyworldgen/roads/generate.py +262 -0
  69. pyworldgen-0.1.0/src/pyworldgen/roads/geometry.py +82 -0
  70. pyworldgen-0.1.0/src/pyworldgen/roads/network.py +105 -0
  71. pyworldgen-0.1.0/src/pyworldgen/roads/solver.py +161 -0
  72. pyworldgen-0.1.0/src/pyworldgen/roads/terrain.py +94 -0
  73. pyworldgen-0.1.0/src/pyworldgen/runtime/__init__.py +6 -0
  74. pyworldgen-0.1.0/src/pyworldgen/runtime/chunk_streamer.py +33 -0
  75. pyworldgen-0.1.0/src/pyworldgen/runtime/navigation.py +61 -0
  76. pyworldgen-0.1.0/src/pyworldgen/spatial/__init__.py +46 -0
  77. pyworldgen-0.1.0/src/pyworldgen/spatial/bvh.py +110 -0
  78. pyworldgen-0.1.0/src/pyworldgen/spatial/dda.py +91 -0
  79. pyworldgen-0.1.0/src/pyworldgen/spatial/fps.py +111 -0
  80. pyworldgen-0.1.0/src/pyworldgen/spatial/graph.py +90 -0
  81. pyworldgen-0.1.0/src/pyworldgen/spatial/kdtree.py +101 -0
  82. pyworldgen-0.1.0/src/pyworldgen/spatial/octree.py +67 -0
  83. pyworldgen-0.1.0/src/pyworldgen/spatial/quadtree.py +78 -0
  84. pyworldgen-0.1.0/src/pyworldgen/spatial/rtree.py +150 -0
  85. pyworldgen-0.1.0/src/pyworldgen/spatial/spatial_hash.py +67 -0
  86. pyworldgen-0.1.0/src/pyworldgen/voxel/__init__.py +51 -0
  87. pyworldgen-0.1.0/src/pyworldgen/voxel/blocks.py +57 -0
  88. pyworldgen-0.1.0/src/pyworldgen/voxel/lighting.py +25 -0
  89. pyworldgen-0.1.0/src/pyworldgen/world/__init__.py +20 -0
  90. pyworldgen-0.1.0/src/pyworldgen/world/block_storage.py +58 -0
  91. pyworldgen-0.1.0/src/pyworldgen/world/chunk.py +73 -0
  92. pyworldgen-0.1.0/src/pyworldgen/world/chunk_map.py +37 -0
  93. pyworldgen-0.1.0/src/pyworldgen/world/edits.py +49 -0
  94. pyworldgen-0.1.0/src/pyworldgen/world/queries.py +69 -0
  95. pyworldgen-0.1.0/src/pyworldgen/world/world.py +34 -0
  96. pyworldgen-0.1.0/src/pyworldgen.egg-info/PKG-INFO +460 -0
  97. pyworldgen-0.1.0/src/pyworldgen.egg-info/SOURCES.txt +117 -0
  98. pyworldgen-0.1.0/src/pyworldgen.egg-info/dependency_links.txt +1 -0
  99. pyworldgen-0.1.0/src/pyworldgen.egg-info/requires.txt +26 -0
  100. pyworldgen-0.1.0/src/pyworldgen.egg-info/top_level.txt +1 -0
  101. pyworldgen-0.1.0/tests/test_biomes_cave.py +67 -0
  102. pyworldgen-0.1.0/tests/test_biomes_flat.py +39 -0
  103. pyworldgen-0.1.0/tests/test_biomes_forest.py +109 -0
  104. pyworldgen-0.1.0/tests/test_biomes_landscape.py +59 -0
  105. pyworldgen-0.1.0/tests/test_compose.py +102 -0
  106. pyworldgen-0.1.0/tests/test_core_hash.py +88 -0
  107. pyworldgen-0.1.0/tests/test_export_formats.py +163 -0
  108. pyworldgen-0.1.0/tests/test_fields.py +60 -0
  109. pyworldgen-0.1.0/tests/test_generation.py +69 -0
  110. pyworldgen-0.1.0/tests/test_io_runtime.py +111 -0
  111. pyworldgen-0.1.0/tests/test_mesh.py +28 -0
  112. pyworldgen-0.1.0/tests/test_noise.py +122 -0
  113. pyworldgen-0.1.0/tests/test_pointcloud.py +142 -0
  114. pyworldgen-0.1.0/tests/test_registry.py +60 -0
  115. pyworldgen-0.1.0/tests/test_rendering.py +176 -0
  116. pyworldgen-0.1.0/tests/test_roads.py +245 -0
  117. pyworldgen-0.1.0/tests/test_spatial.py +125 -0
  118. pyworldgen-0.1.0/tests/test_visualization_scripts.py +90 -0
  119. pyworldgen-0.1.0/tests/test_world_storage.py +46 -0
@@ -0,0 +1,460 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyworldgen
3
+ Version: 0.1.0
4
+ Summary: Deterministic procedural world generation: biomes, noise, roads, and smooth multi-biome composition.
5
+ Author-email: YOUR NAME <you@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YOUR-GH-USER/pyworldgen
8
+ Project-URL: Repository, https://github.com/YOUR-GH-USER/pyworldgen
9
+ Project-URL: Issues, https://github.com/YOUR-GH-USER/pyworldgen/issues
10
+ Project-URL: Changelog, https://github.com/YOUR-GH-USER/pyworldgen/blob/main/CHANGELOG.md
11
+ Keywords: procedural-generation,worldgen,noise,perlin,simplex,terrain,voxel,biomes
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
21
+ Classifier: Topic :: Games/Entertainment
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: numpy>=1.23
25
+ Requires-Dist: scipy>=1.9
26
+ Requires-Dist: matplotlib>=3.6
27
+ Requires-Dist: pillow>=9.0
28
+ Requires-Dist: imageio>=2.0
29
+ Provides-Extra: render
30
+ Requires-Dist: pyvista>=0.43; extra == "render"
31
+ Provides-Extra: fps
32
+ Requires-Dist: fpsample>=1.0; extra == "fps"
33
+ Provides-Extra: viz
34
+ Requires-Dist: scikit-image>=0.20; extra == "viz"
35
+ Provides-Extra: geometry
36
+ Requires-Dist: shapely>=2; extra == "geometry"
37
+ Requires-Dist: trimesh>=4; extra == "geometry"
38
+ Requires-Dist: mapbox-earcut>=1; extra == "geometry"
39
+ Provides-Extra: all
40
+ Requires-Dist: pyworldgen[fps,geometry,render,viz]; extra == "all"
41
+ Provides-Extra: dev
42
+ Requires-Dist: pytest>=7; extra == "dev"
43
+ Requires-Dist: opensimplex>=0.4; extra == "dev"
44
+
45
+ # pyworldgen
46
+
47
+ A unified, deterministic procedural biome-generation library. It brings three
48
+ previously separate code bases under one roof and makes them share a single
49
+ foundation:
50
+
51
+ - a chunked voxel **world** with a generation-pass framework (formerly `procgen3d`);
52
+ - pure-function **biome** generators — caves, forests, landscapes (formerly `biomegen_lab`);
53
+ - the streamed implicit **cave** with its cross-language hash.
54
+
55
+
56
+ ## The idea that unifies everything
57
+
58
+ The three code bases looked different on the surface — one was an array-backed
59
+ chunk engine, the others were pure mathematical fields — but they all obey the
60
+ same principle: **a compact deterministic source of truth is sampled into
61
+ arrays, which are exported as views.** A seed plus parameters *is* the world;
62
+ voxel grids, meshes, point clouds and saved chunks are all regenerable views of
63
+ that source. `pyworldgen` makes that principle explicit and builds three shared
64
+ mechanisms on top of it.
65
+
66
+ ### 1. One canonical cross-language hash
67
+
68
+ Every generator's randomness now flows through a single module,
69
+ `pyworldgen.core.hash`. It is an integer multiply-and-mix hash (xxHash-style
70
+ constants, `Math.imul` semantics) whose only floating-point work happens *after*
71
+ hashing, so a world is reproducible bit-for-bit from a seed on any backend —
72
+ Python, JavaScript, GLSL, C++. The shader idiom `frac(sin(x)*43758.5453)` is
73
+ deliberately avoided because `sin` disagrees on its last bits across platforms
74
+ and the hash amplifies that into a different world.
75
+
76
+ This replaced **three duplicated hash implementations**. In doing so it fixed a
77
+ real latent bug: the original `cavegen` and `landscapegen` copies performed the
78
+ 32-bit multiply in signed `int64` and relied on silent overflow wraparound,
79
+ while `forestgen` used `uint64`. The three did not always agree. The unified
80
+ module standardises on the correct `uint64` path (masked to 32 bits), and a test
81
+ pins the output against an independent pure-Python reference of the same
82
+ construction so the NumPy path can never drift from the cross-language contract.
83
+
84
+ ### 2. The field abstraction — where the two frameworks meet
85
+
86
+ `pyworldgen.fields` is the conceptual bridge. A biome can be expressed as a pure
87
+ function `is_solid(x, y, z)` with no stored grid (this is exactly what the
88
+ implicit cave is). The same function can also *fill chunks* in the array-backed
89
+ world:
90
+
91
+ - `voxelize(field, ...)` samples a field into a dense occupancy array;
92
+ - `fill_chunk_from_field(field, chunk, ...)` samples it straight into a chunk;
93
+ - `FieldSolidPass` wraps a field as a generation pass.
94
+
95
+ So a streamed implicit cave drops into the same scheduler as hand-authored
96
+ terrain, and can be meshed, saved and streamed like any other chunk. The cave's
97
+ spec↔world coordinate swap lives inside its own `is_solid`, so the generic fill
98
+ logic stays agnostic.
99
+
100
+ ### 3. Compact source, heavy views
101
+
102
+ Seeds, parameters, tree graphs and river polylines are the source of truth.
103
+ Meshes and OBJ files are produced on demand from them. For the pure fields you
104
+ often don't need to store geometry at all — only measured or hand-edited data
105
+ needs persisting.
106
+
107
+ ## Layer map
108
+
109
+ | Layer | Lines | What's in it |
110
+ |---|---|---|
111
+ | `core` | 527 | canonical hash + noise, RNG seeding, vec/AABB/ray, chunk coordinates |
112
+ | `spatial` | 833 | SpatialHashGrid, BVH, voxel DDA, Quadtree, Octree, Graph+Dijkstra, **KDTree**, **RTree**, **farthest point sampling** |
113
+ | `fields` | 71 | `SolidField3D` protocol, voxelize, chunk fill |
114
+ | `voxel` | 99 | block registry, sunlight pass |
115
+ | `world` | 315 | block storage, chunk, chunk map, deferred edits, world, queries |
116
+ | `mesh` | 392 | naive + greedy chunk meshers, **dense-volume mesher** |
117
+ | `generation` | 294 | pass base, dependency-checked scheduler, terrain/biome/cave/feature/structure passes |
118
+ | `runtime` | 95 | chunk streaming, grid A* navigation |
119
+ | `biomes` | 1456 | the signature generators — cave, forest, landscape, **flat terrain** |
120
+ | `roads` | 723 | **weighted anisotropic least-cost roads (Galin et al.): path segment masks, line-integral cost, bridges, curvature, terrain grading** |
121
+ | `compose` | 356 | **multi-biome composition: partition-of-unity blending, smooth transitions, integrated forest + roads** |
122
+ | `pointcloud` | 164 | **mesh surface sampling, FPS downsampling, point→voxel rasterisation** |
123
+ | `rendering` | 762 | **numpy pinhole camera, orbit/keyframe paths, software renderer, PyVista/matplotlib backends, GIF/MP4 export** |
124
+ | `io` | 445 | chunk (de)serialization, region store, OBJ / **MagicaVoxel .vox** / **PLY / XYZ** export |
125
+
126
+ The two spatial structures the reference "core structures" guide lists but
127
+ neither original project had — a **KDTree** (nearest-neighbour over static
128
+ points) and an STR-bulk-loaded **RTree** (map/polygon window queries) — were
129
+ added and tested against brute force.
130
+
131
+ ## The three biomes and their invariants
132
+
133
+ Each generator preserves the defining property of its original, and each has a
134
+ `validate_*` function and tests that assert the invariant directly:
135
+
136
+ - **Implicit cave** (`biomes.cave`) — a pure positional field with a
137
+ *guaranteed always-open* wandering main corridor, unioned with worm tunnels
138
+ and caverns, roughened walls and optional speleothems. The corridor's openness
139
+ is strictly positive along its axis, so the whole streamed length stays
140
+ navigable. Bridges to the world via `ImplicitCave`, a valid `SolidField3D`.
141
+ - **Forest** (`biomes.forest`) — a Voronoi canopy graph. Every canopy cell has
142
+ exactly one owner tree, so crowns never overlap *by construction*; each tree is
143
+ a recursive root→trunk→branch→terminal graph with bottom-up branch radii
144
+ (`r_parent^γ = Σ r_child^γ`) that stays inside its owned cell. An optional
145
+ `canopy_diffusion` (default `0.0`, i.e. hard power-diagram edges) domain-warps
146
+ the ownership so neighbouring crowns interlock with organic, diffuse boundaries
147
+ — still exactly one owner per cell, and `0.0` reproduces the original output.
148
+ - **Landscape** (`biomes.landscape`) — a river-valley plain. Every river bed is
149
+ monotonically downhill from spring to mouth, enforced with
150
+ `np.minimum.accumulate` rather than hoped for; tributaries join their main
151
+ river, valleys and channels are carved, ponds fill low ground, and materials
152
+ are classified water→upland.
153
+ - **Flat terrain** (`biomes.flat`) — the neutral default: a heightfield that is
154
+ perfectly flat unless you add tilt or undulation. It exposes the same
155
+ `height`/`x`/`y`/`resolution` interface as the landscape, so roads, the terrain
156
+ mesher, and biome composition all work on it unchanged.
157
+
158
+ ## Roads
159
+
160
+ Roads (`pyworldgen.roads`) are laid over any terrain (`FlatTerrain`,
161
+ `LandscapeWorld`, or a raw height grid) as **weighted anisotropic least-cost
162
+ paths**, following Galin et al., *Procedural Generation of Roads* (Eurographics
163
+ 2010). The routing of a single connection is a faithful port of the paper; laying
164
+ a *network* of hubs (minimum spanning tree plus a few loop edges) is the
165
+ pyworldgen extension on top of it.
166
+
167
+ The paper's two central ideas are both implemented:
168
+
169
+ - **Path segment masks `M_k`** (`path_segment_mask`) — instead of 4/8-connectivity
170
+ (which limits road directions to multiples of 45°, the "limit-on-direction
171
+ problem"), each grid point connects to every `(i, j) ∈ [-k, k]²` with
172
+ `gcd(|i|, |j|) = 1`. The mask sizes reproduce the paper's Table 1 exactly
173
+ (`k=1`→8 directions, `k=3`→32, `k=5`→80), and the tests assert it. Larger `k`
174
+ gives finer angle resolution and straighter roads; `mask_radius` (default 3)
175
+ selects it.
176
+ - **Line-integral segment cost** with **transfer functions** — because a mask
177
+ segment spans several cells, its cost is the integral of a local cost sampled
178
+ along it. Each characteristic (slope *along the travel direction*, water depth)
179
+ passes through a transfer function with a threshold `κ₀` beyond which the cost
180
+ is `∞` (a forbidden region): grades steeper than `max_grade` can't be climbed,
181
+ water deeper than `max_water_depth` can't be forded.
182
+
183
+ On top of that: **bridges** — deep water is crossed at a `bridge_cost` surcharge
184
+ rather than forbidden (a long mask segment over a river *is* a bridge), and the
185
+ ribbon geometry lifts the bridge deck onto the water surface; **curvature** —
186
+ setting `curvature_weight > 0` augments the search state with the incoming
187
+ direction (the paper's oriented Ω×[0,2π] grid) so sharp turns are penalised
188
+ (opt-in, and slower, as in the paper's Table 3); **corridor reuse** — later roads
189
+ are discounted along earlier ones so the network bundles; and **excavation /
190
+ embankment** — `carve_road_terrain` cuts/fills the ground to the road within the
191
+ asphalt half-width and blends back to the terrain over a shoulder region (§6.2),
192
+ leaving bridge spans untouched.
193
+
194
+ ```python
195
+ from pyworldgen.biomes.landscape import generate_landscape
196
+ from pyworldgen.roads import generate_road_network, road_ribbon_mesh, carve_road_terrain
197
+
198
+ land = generate_landscape(seed="rhine") # rivers to bridge
199
+ roads = generate_road_network(land, seed="roads", num_hubs=11, mask_radius=4)
200
+
201
+ roads.road_mask # (ny, nx) boolean road footprint
202
+ roads.bridge_mask() # road cells carried over water on a bridge
203
+ verts, faces = road_ribbon_mesh(roads) # deck lifted over water, ready for OBJ / render
204
+ graded = carve_road_terrain(roads) # terrain with cuts, fills and shoulders
205
+ ```
206
+
207
+ `bf.generate("roads", seed=..., num_hubs=...)` is a convenience that lays roads
208
+ over a flat base. The result is a `RoadNetwork`: hubs, smoothed centreline
209
+ segments, the rasterised mask, the water it bridges, and helpers for 3D polylines
210
+ and ribbon meshes. Routed roads are much straighter than 8-connectivity (checked
211
+ against the mask table) and follow the terrain at a far gentler grade than a
212
+ straight line — both asserted in the tests.
213
+
214
+ Honest gaps versus the paper: smoothing uses Chaikin corner-cutting rather than
215
+ clothoid splines (§6.1), tunnels are not implemented (only bridges), and the
216
+ stochastic bridge/tunnel sampling of §5.4 is not used — all noted as future work.
217
+
218
+ ## Composition: many biomes in one world
219
+
220
+ `pyworldgen.compose` blends several biomes into a single coherent world with
221
+ **smooth transitions**, then lays the forest and roads over the *same* composed
222
+ terrain. The algorithm is a partition-of-unity blend:
223
+
224
+ 1. **Region seeds + classification.** Scatter region centres (seeded dart-throw)
225
+ and classify each into a biome by its *niche* in a low-frequency
226
+ (elevation, moisture) control field — a Whittaker-style assignment, so
227
+ mountains cluster on high ground and water in the basins.
228
+ 2. **Smooth weights.** Each biome type is a *global* terrain profile `h_T(x, y)`.
229
+ For every cell, a domain-warped Gaussian gives a weight to each seed; weights
230
+ are summed per biome and normalised so they **sum to 1 everywhere** (a
231
+ partition of unity). This is what makes the result smooth — every blended
232
+ field is a convex combination of continuous fields, so there are no seams.
233
+ 3. **Blend.** `height = Σ_T W_T · h_T`, and likewise water depth, a tree-density
234
+ field, and biome colour. Transitions read as foothills between mountains and
235
+ plains, and shorelines around lakes.
236
+ 4. **Integrated overlays.** Forest is scattered on the composed terrain, gated by
237
+ the blended density field (so it fades out at biome edges) and re-draped onto
238
+ the composed height; roads are routed over the composed `(height, water_depth)`
239
+ — which is exactly the interface the road generator already consumes, so they
240
+ bridge the composed lakes and avoid the composed mountains with no special
241
+ casing.
242
+
243
+ ```python
244
+ from pyworldgen.compose import compose_world
245
+
246
+ world = compose_world(seed="atlas", num_regions=16, num_hubs=12)
247
+ world.height, world.water_depth # blended terrain (same interface as a biome)
248
+ world.weights["forest"] # per-biome partition-of-unity weight field
249
+ world.color, world.dominant # blended biome colour / dominant-biome map
250
+ world.trees, world.roads # forest + road network on the composed terrain
251
+ ```
252
+
253
+ `bf.generate("composite", seed=...)` returns a `CompositeWorld`, which exposes the
254
+ `height`/`water_depth`/`x`/`y`/`resolution` terrain interface — so the terrain and
255
+ water meshers and the road generator all consume it directly. The built-in
256
+ palette is water, plains, forest, hills and mountains; a custom `palette` of
257
+ `BiomeType`s (each a terrain profile, colour, tree density and niche) can be
258
+ passed in. The weights are verified in the tests to be a true partition of unity
259
+ with no hard seams, trees to sit exactly on the composed terrain and never in
260
+ water, and mountains to dominate higher ground than water.
261
+
262
+ The current palette models water as lakes/wetlands (basins filled to a level);
263
+ per-region river hydrology from the landscape generator can be slotted in through
264
+ the same `(height, water_depth)` interface as a future extension.
265
+
266
+ ## Meshes, voxels and point clouds
267
+
268
+ Every biome can be exported in all three geometry representations, because each
269
+ is just another *view* of the compact source. The three are interchangeable:
270
+
271
+ - **Voxels.** The cave is native voxels (a `DenseVoxelVolume`); the chunked
272
+ world is voxels too. Export either to **MagicaVoxel `.vox`** (readable by
273
+ MagicaVoxel, Blender, Godot, three.js) or a portable `.npz`.
274
+ - **Meshes.** A `DenseVoxelVolume` meshes with a vectorised naive culled
275
+ mesher (`volume_to_mesh`); the forest and landscape mesh from their graph /
276
+ height field. All go to Wavefront `.obj`.
277
+ - **Point clouds.** `sample_mesh_surface` scatters points over any mesh with
278
+ **area-weighted** density (uniform regardless of tessellation), each carrying
279
+ its triangle's normal. The cloud thins to an even, blue-noise-like subset with
280
+ **farthest point sampling**, then exports to `.ply` or `.xyz`.
281
+
282
+ So the pipeline **biome → mesh → point cloud → FPS-downsampled cloud** works for
283
+ any biome:
284
+
285
+ ```python
286
+ from pyworldgen.mesh import volume_to_mesh
287
+ from pyworldgen.pointcloud import sample_mesh_surface
288
+ from pyworldgen.io import volume_to_vox, mesh_to_obj, write_ply
289
+
290
+ cave = bf.generate("implicit_cave", seed="lux", length=90.0)
291
+
292
+ volume_to_vox("cave.vox", cave) # voxels
293
+ mesh = volume_to_mesh(cave)
294
+ mesh_to_obj("cave.obj", mesh) # mesh
295
+
296
+ cloud = sample_mesh_surface(mesh, 40_000, seed=1) # dense surface cloud
297
+ even = cloud.farthest_point_downsample(4_096) # evenly spread subset
298
+ write_ply("cave_points.ply", even) # point cloud (with normals)
299
+ ```
300
+
301
+ Farthest point sampling ships with both a self-contained, deterministic NumPy
302
+ backend built on the same layer as the KD-tree and the
303
+ [`fpsample`](https://pypi.org/project/fpsample/) Rust QuickFPS/NPDU backend for
304
+ very large clouds. The NumPy backend is the default. The reverse direction,
305
+ point cloud → voxels, is `points_to_occupancy`, so a mesh-native biome (forest,
306
+ landscape) can also be voxelised via a surface sampling.
307
+
308
+ ## Rendering and fly-throughs
309
+
310
+ Any geometry view can be rendered to images and animations. A render is just
311
+ another view of the source, so the same biome flows all the way through:
312
+ source → mesh / point cloud → camera → frames.
313
+
314
+ - **Camera.** A numpy pinhole camera (`Camera.look_at`, `project`, `lerp`) with
315
+ intrinsics from field of view — the standard self-driving pinhole model,
316
+ without a torch/Lie-group dependency, so interpolating a path is a plain lerp.
317
+ - **Paths.** `orbit` / `turntable` spin around a point; `flythrough` interpolates
318
+ a smooth path through `Keyframe` waypoints; `frame_scene` and `orbit_scene`
319
+ auto-fit the camera to a scene's bounding box.
320
+ - **Renderers.** The default `SoftwareRenderer` is pure numpy — a z-buffered
321
+ point splatter (depth / normal / colour shading) and a flat-shaded triangle
322
+ rasteriser. `PyVistaRenderer` provides solid VTK renders, and
323
+ `MatplotlibRenderer` provides projected scatter renders.
324
+ - **Output.** `save_png`, `save_gif` (Pillow), `save_mp4` (imageio), plus a
325
+ dependency-free PPM writer.
326
+
327
+ A turntable fly-through of a biome is one call:
328
+
329
+ ```python
330
+ import pyworldgen as bf
331
+ import pyworldgen.rendering as rr
332
+ from pyworldgen.mesh import volume_to_mesh
333
+ from pyworldgen.pointcloud import sample_mesh_surface
334
+
335
+ cave = bf.generate("implicit_cave", seed="lux", length=70.0)
336
+ cloud = sample_mesh_surface(volume_to_mesh(cave), 60_000, seed=1)
337
+
338
+ frames = rr.render_turntable(cloud, n_frames=48, elevation=18) # fly-around
339
+ rr.save_gif(frames, "cave_orbit.gif", fps=20)
340
+
341
+ # solid mesh still from a chosen angle
342
+ still = rr.SoftwareRenderer().render(volume_to_mesh(cave),
343
+ rr.frame_scene(cave, azimuth=40, elevation=30))
344
+ rr.save_png(still, "cave.png")
345
+ ```
346
+
347
+ Point-cloud fly-throughs are fast (tens of milliseconds per frame); the triangle
348
+ rasteriser is best for solid stills. For very large meshes and extra polish,
349
+ pass `renderer=rr.PyVistaRenderer()`.
350
+
351
+ ## Quick start
352
+
353
+ ```python
354
+ import pyworldgen as bf
355
+
356
+ bf.list_generators() # ['composite', 'flat', 'forest', 'implicit_cave', 'landscape', 'roads']
357
+
358
+ forest = bf.generate("forest", seed="grenoble", target_tree_count=60)
359
+ cave = bf.generate("implicit_cave", seed="luxembourg", length=90.0)
360
+ land = bf.generate("landscape", seed="rhine")
361
+ ```
362
+
363
+ Drive the chunked world with a pipeline of passes:
364
+
365
+ ```python
366
+ from pyworldgen.world import World
367
+ from pyworldgen.core.coords import ChunkCoord
368
+ from pyworldgen.generation import (
369
+ GenerationScheduler, SimpleTerrainPass, SimpleBiomePass,
370
+ SimpleCavePass, SimpleTreeFeaturePass, ApplyDeferredEditsPass,
371
+ )
372
+ from pyworldgen.voxel.lighting import SimpleSunlightPass
373
+
374
+ world = World.create("demo")
375
+ scheduler = GenerationScheduler([
376
+ SimpleTerrainPass(), SimpleBiomePass(), SimpleCavePass(),
377
+ SimpleTreeFeaturePass(), ApplyDeferredEditsPass(), SimpleSunlightPass(),
378
+ ])
379
+ chunk = world.chunks.get_or_create(ChunkCoord(0, 0, 0))
380
+ scheduler.generate_chunk(world, chunk)
381
+ ```
382
+
383
+ Stream a whole implicit cave into chunks through the field bridge:
384
+
385
+ ```python
386
+ from pyworldgen.biomes.cave import ImplicitCave
387
+ from pyworldgen.generation import FieldSolidPass, GenerationScheduler
388
+
389
+ cave = ImplicitCave.from_seed("luxembourg")
390
+ scheduler = GenerationScheduler([FieldSolidPass(cave, name="cave")])
391
+ ```
392
+
393
+
394
+ ## Visualization scripts
395
+
396
+ Camera-based stills, multi-view sheets, turntables, fly-throughs, biome debug maps, and a noise gallery are available in `scripts/`.
397
+
398
+ ```bash
399
+ python scripts/visualize_all.py --quick --out sample_visualizations
400
+ python scripts/visualize_all.py --quick --animations --out sample_visualizations
401
+ python scripts/render_biome.py composite --seed atlas --quick
402
+ ```
403
+
404
+ ## Install and dependencies
405
+
406
+ ```bash
407
+ pip install -e . # installs runtime dependencies
408
+ pip install -e ".[dev]" # adds pytest
409
+ ```
410
+
411
+ Runtime dependencies include `numpy`, `scipy`, `fpsample`, `matplotlib`,
412
+ `pillow`, `imageio`, and `pyvista`. `scikit-image` (`[viz]`) and
413
+ `shapely`/`trimesh`/`mapbox-earcut` (`[geometry]`) remain extras for marching
414
+ cubes and structure-generation extension points.
415
+
416
+ ## Testing
417
+
418
+
419
+ Coverage spans the hash's cross-language bit-contract and the absence of integer
420
+ overflow, the spatial structures (KDTree/RTree checked against brute force),
421
+ block storage and world queries, both meshers plus the dense-volume mesher, the
422
+ field↔chunk bridge, the generation scheduler's dependency validation and
423
+ reproducibility, all three biome invariants, serialization round-trips, grid A*,
424
+ the geometry exporters, and the rendering layer — farthest point sampling is
425
+ checked for exact behaviour on a 1-D line and for spreading points better than a
426
+ random subset; surface sampling for lying on the mesh and for area-weighting; the
427
+ `.vox`, `.ply` and `.xyz` writers by parsing their output back; the camera's
428
+ projection axes, orbit radius, fly-through endpoints, the renderer's z-buffer
429
+ (nearest wins), and PNG/GIF/PPM output; and the roads layer — the path segment
430
+ mask sizes against the paper's Table 1, staircase removal (a `(3,1)` run with
431
+ zero direction changes), least-cost routing that detours around a ridge and
432
+ follows terrain at a gentler grade than a straight line, bridges that cross deep
433
+ water only when allowed, curvature that reduces turns, terrain grading that
434
+ touches ground near roads but not far from them, network connectivity across all
435
+ hubs, endpoint pinning, and determinism, plus the flat terrain being flat by
436
+ default; and the composition layer — weights that form a true partition of unity
437
+ with no hard colour seams, coherent niche classification (mountains above water),
438
+ trees draped exactly on the composed terrain and never in water, roads consuming
439
+ the composed terrain interface, and determinism.
440
+
441
+ ## What was ported compactly (transparency)
442
+
443
+ To keep the merge coherent and tested, some elaborate paths from the originals
444
+ were folded down rather than dropped:
445
+
446
+ - The forest's per-species table is folded into a single parameter set;
447
+ multi-species is a documented extension point (per-site branch depth/radii).
448
+ - The landscape's multi-layer hydrology solver is expressed as a single carve
449
+ pass driven by the drainage graph; catchment/erosion iteration is an
450
+ extension point.
451
+ - Marching-cubes cave meshing and the CLI from the originals are kept out of the
452
+ tested core; geometry output is covered by the OBJ, MagicaVoxel `.vox` and
453
+ PLY/XYZ exporters and a vectorised naive voxel mesher (rather than marching
454
+ cubes), and the rendering layer ships a self-contained numpy renderer plus
455
+ PyVista/matplotlib backends.
456
+ - Legacy grid/morphology cave variants (which needed `scipy`) are omitted in
457
+ favour of the faithful implicit field.
458
+
459
+ The road/building/story structure generators from the PCG reference are not
460
+ implemented here; the graph and spatial layers they would build on are in place.