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.
- pyworldgen-0.1.0/PKG-INFO +460 -0
- pyworldgen-0.1.0/README.md +416 -0
- pyworldgen-0.1.0/pyproject.toml +72 -0
- pyworldgen-0.1.0/setup.cfg +4 -0
- pyworldgen-0.1.0/src/pyworldgen/__init__.py +73 -0
- pyworldgen-0.1.0/src/pyworldgen/biomes/__init__.py +60 -0
- pyworldgen-0.1.0/src/pyworldgen/biomes/cave.py +353 -0
- pyworldgen-0.1.0/src/pyworldgen/biomes/flat.py +110 -0
- pyworldgen-0.1.0/src/pyworldgen/biomes/forest.py +697 -0
- pyworldgen-0.1.0/src/pyworldgen/biomes/landscape.py +542 -0
- pyworldgen-0.1.0/src/pyworldgen/compose/__init__.py +23 -0
- pyworldgen-0.1.0/src/pyworldgen/compose/forest_overlay.py +57 -0
- pyworldgen-0.1.0/src/pyworldgen/compose/palette.py +70 -0
- pyworldgen-0.1.0/src/pyworldgen/compose/regions.py +77 -0
- pyworldgen-0.1.0/src/pyworldgen/compose/world.py +198 -0
- pyworldgen-0.1.0/src/pyworldgen/core/__init__.py +67 -0
- pyworldgen-0.1.0/src/pyworldgen/core/aabb.py +104 -0
- pyworldgen-0.1.0/src/pyworldgen/core/coords.py +82 -0
- pyworldgen-0.1.0/src/pyworldgen/core/hash.py +215 -0
- pyworldgen-0.1.0/src/pyworldgen/core/math_types.py +83 -0
- pyworldgen-0.1.0/src/pyworldgen/core/rng.py +37 -0
- pyworldgen-0.1.0/src/pyworldgen/fields/__init__.py +5 -0
- pyworldgen-0.1.0/src/pyworldgen/fields/base.py +81 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/__init__.py +27 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/biomes.py +39 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/caves.py +78 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/features.py +43 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/pass_base.py +39 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/scheduler.py +48 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/structures.py +20 -0
- pyworldgen-0.1.0/src/pyworldgen/generation/terrain.py +46 -0
- pyworldgen-0.1.0/src/pyworldgen/io/__init__.py +50 -0
- pyworldgen-0.1.0/src/pyworldgen/io/obj.py +159 -0
- pyworldgen-0.1.0/src/pyworldgen/io/pointcloud_io.py +93 -0
- pyworldgen-0.1.0/src/pyworldgen/io/serialization.py +98 -0
- pyworldgen-0.1.0/src/pyworldgen/io/voxel_io.py +105 -0
- pyworldgen-0.1.0/src/pyworldgen/mesh/__init__.py +18 -0
- pyworldgen-0.1.0/src/pyworldgen/mesh/cube_faces.py +21 -0
- pyworldgen-0.1.0/src/pyworldgen/mesh/dense_mesher.py +150 -0
- pyworldgen-0.1.0/src/pyworldgen/mesh/greedy_mesher.py +123 -0
- pyworldgen-0.1.0/src/pyworldgen/mesh/mesh.py +76 -0
- pyworldgen-0.1.0/src/pyworldgen/mesh/naive_mesher.py +54 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/__init__.py +95 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/_grad.py +54 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/cellular.py +76 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/fractal.py +87 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/gabor.py +67 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/opensimplex.py +98 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/perlin.py +64 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/phasor.py +44 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/simplex.py +104 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/warp.py +67 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/wave.py +61 -0
- pyworldgen-0.1.0/src/pyworldgen/noise/wavelet.py +128 -0
- pyworldgen-0.1.0/src/pyworldgen/pointcloud/__init__.py +12 -0
- pyworldgen-0.1.0/src/pyworldgen/pointcloud/cloud.py +155 -0
- pyworldgen-0.1.0/src/pyworldgen/registry.py +147 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/__init__.py +122 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/animation.py +86 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/backends.py +124 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/camera.py +130 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/colormap.py +54 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/paths.py +145 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/scene.py +114 -0
- pyworldgen-0.1.0/src/pyworldgen/rendering/software.py +169 -0
- pyworldgen-0.1.0/src/pyworldgen/roads/__init__.py +38 -0
- pyworldgen-0.1.0/src/pyworldgen/roads/cost.py +61 -0
- pyworldgen-0.1.0/src/pyworldgen/roads/generate.py +262 -0
- pyworldgen-0.1.0/src/pyworldgen/roads/geometry.py +82 -0
- pyworldgen-0.1.0/src/pyworldgen/roads/network.py +105 -0
- pyworldgen-0.1.0/src/pyworldgen/roads/solver.py +161 -0
- pyworldgen-0.1.0/src/pyworldgen/roads/terrain.py +94 -0
- pyworldgen-0.1.0/src/pyworldgen/runtime/__init__.py +6 -0
- pyworldgen-0.1.0/src/pyworldgen/runtime/chunk_streamer.py +33 -0
- pyworldgen-0.1.0/src/pyworldgen/runtime/navigation.py +61 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/__init__.py +46 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/bvh.py +110 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/dda.py +91 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/fps.py +111 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/graph.py +90 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/kdtree.py +101 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/octree.py +67 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/quadtree.py +78 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/rtree.py +150 -0
- pyworldgen-0.1.0/src/pyworldgen/spatial/spatial_hash.py +67 -0
- pyworldgen-0.1.0/src/pyworldgen/voxel/__init__.py +51 -0
- pyworldgen-0.1.0/src/pyworldgen/voxel/blocks.py +57 -0
- pyworldgen-0.1.0/src/pyworldgen/voxel/lighting.py +25 -0
- pyworldgen-0.1.0/src/pyworldgen/world/__init__.py +20 -0
- pyworldgen-0.1.0/src/pyworldgen/world/block_storage.py +58 -0
- pyworldgen-0.1.0/src/pyworldgen/world/chunk.py +73 -0
- pyworldgen-0.1.0/src/pyworldgen/world/chunk_map.py +37 -0
- pyworldgen-0.1.0/src/pyworldgen/world/edits.py +49 -0
- pyworldgen-0.1.0/src/pyworldgen/world/queries.py +69 -0
- pyworldgen-0.1.0/src/pyworldgen/world/world.py +34 -0
- pyworldgen-0.1.0/src/pyworldgen.egg-info/PKG-INFO +460 -0
- pyworldgen-0.1.0/src/pyworldgen.egg-info/SOURCES.txt +117 -0
- pyworldgen-0.1.0/src/pyworldgen.egg-info/dependency_links.txt +1 -0
- pyworldgen-0.1.0/src/pyworldgen.egg-info/requires.txt +26 -0
- pyworldgen-0.1.0/src/pyworldgen.egg-info/top_level.txt +1 -0
- pyworldgen-0.1.0/tests/test_biomes_cave.py +67 -0
- pyworldgen-0.1.0/tests/test_biomes_flat.py +39 -0
- pyworldgen-0.1.0/tests/test_biomes_forest.py +109 -0
- pyworldgen-0.1.0/tests/test_biomes_landscape.py +59 -0
- pyworldgen-0.1.0/tests/test_compose.py +102 -0
- pyworldgen-0.1.0/tests/test_core_hash.py +88 -0
- pyworldgen-0.1.0/tests/test_export_formats.py +163 -0
- pyworldgen-0.1.0/tests/test_fields.py +60 -0
- pyworldgen-0.1.0/tests/test_generation.py +69 -0
- pyworldgen-0.1.0/tests/test_io_runtime.py +111 -0
- pyworldgen-0.1.0/tests/test_mesh.py +28 -0
- pyworldgen-0.1.0/tests/test_noise.py +122 -0
- pyworldgen-0.1.0/tests/test_pointcloud.py +142 -0
- pyworldgen-0.1.0/tests/test_registry.py +60 -0
- pyworldgen-0.1.0/tests/test_rendering.py +176 -0
- pyworldgen-0.1.0/tests/test_roads.py +245 -0
- pyworldgen-0.1.0/tests/test_spatial.py +125 -0
- pyworldgen-0.1.0/tests/test_visualization_scripts.py +90 -0
- 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.
|