diff --git a/lathe/lathe.py b/lathe/lathe.py index 9f5b433..0e95772 100644 --- a/lathe/lathe.py +++ b/lathe/lathe.py @@ -7,28 +7,24 @@ import random import time from io import TextIOWrapper -from typing import Generator # import third-party modules import numpy as np import typer + +# import internal modules +from mylogger import err_con, log, std_con from numpy.typing import NDArray from rich.logging import RichHandler from rich.table import Table -from typing_extensions import Annotated - -# import internal modules -from mylogger import log, std_con from terrain import sample_octaves +from typing_extensions import Annotated from util import Mesh, MeshArray, create_mesh, now, rescale, save_world from viz import viz # Define globals. RADIUS: int = 6378100 # Radius of the world in meters. The approximate radius of Earth is 6378100 m. -FEATURE_SIZE: np.float64 = ( - RADIUS * 0.25 -) # Determines the "coherence" of the random noise; affects the size of discrete landmasses. A good size for Earth-like continents is 0.5× the radius. ZMIN: int = round( number=-(RADIUS * 0.0015) ) # The lowest elevation in the world, in meters. The deepest oceanic trench on Earth is approximately -10,300 m. @@ -93,12 +89,6 @@ def main( help="The radius of the world in meters. (Earth radius is approximately 6378100 m.)" ), ] = RADIUS, - feature_size: Annotated[ - float, - typer.Option( - help="Determines the coherence of noise; affects the size of discrete landmasses. A good size for Earth-like continents is approximately 50% of the radius." - ), - ] = FEATURE_SIZE, recursion: Annotated[ int, typer.Option( @@ -129,7 +119,7 @@ def main( help="Sets the global sea level by defining a relative percent of the range from min to max altitude." ), ] = OCEAN_PERCENT, - zscale: Annotated[ + scale: Annotated[ int, typer.Option( help="Sets a scaling factor for elevations to make them visible in the plot. Does not change the actual elevation values." @@ -175,11 +165,28 @@ def main( std_con.print("Setting world seed and parameters.\r\n") - if seed == 0: - seed_rng: Generator = np.random.default_rng() - world_seed: int = seed_rng.integers(low=0, high=999999) - else: - world_seed: int = seed + try: + if seed == 0: + world_seed = 0 + elif seed < 1: + world_seed = 0 + err_con.print( + "Seed must be an integer between 1 and 255. Setting to random seed. \r\n" + ) + elif seed > 255: + world_seed = 0 + err_con.print( + "Seed must be an integer between 1 and 255. Setting to random seed. \r\n" + ) + else: + world_seed = seed + except ValueError as e: + err_con.print("Seed must be an integer between 1 and 256. \r\n", f"{e}") + finally: + if world_seed == 0: + world_seed_string = str("Random") + else: + world_seed_string = str(world_seed) zrange: float = zmax - zmin @@ -190,9 +197,8 @@ def main( world_params = { "name": name, "timestamp": now(), - "seed": int(world_seed), + "seed": world_seed_string, "radius": radius, - "feature size": feature_size, "recursion": recursion, "octaves": octaves, "ztilt": ztilt, @@ -209,18 +215,18 @@ def main( params_table.title = "World Parameters" params_table.title_style = "bold magenta" params_table.add_row("Name", name) - params_table.add_row("World Seed", str(object=world_seed)) - params_table.add_row("Radius", str(object=radius)) - params_table.add_row("Feature Size", str(object=feature_size)) - params_table.add_row("Recursion Factor", str(object=recursion)) - params_table.add_row("Octaves (#)", str(object=octaves)) - params_table.add_row("Axial Tilt", str(object=ztilt)) - params_table.add_row("Lowest Elevation", str(object=zmin)) - params_table.add_row("Highest Elevation", str(object=zmax)) - params_table.add_row("Elevation Range", str(object=zrange)) - params_table.add_row("Ocean Percent", str(object=ocean_percent)) - params_table.add_row("Sea Level", str(object=ocean_point)) - params_table.add_row("Log Display", str(object=loglevel)) + params_table.add_row("Timestamp", str(now())) + params_table.add_row("World Seed", world_seed_string) + params_table.add_row("Radius", str(radius)) + params_table.add_row("Recursion Factor", str(recursion)) + params_table.add_row("Octaves (#)", str(octaves)) + params_table.add_row("Axial Tilt", str(ztilt)) + params_table.add_row("Lowest Elevation", str(zmin)) + params_table.add_row("Highest Elevation", str(zmax)) + params_table.add_row("Elevation Range", str(zrange)) + params_table.add_row("Ocean Percent", str(ocean_percent)) + params_table.add_row("Sea Level", str(ocean_point)) + params_table.add_row("Log Display", str(loglevel)) std_con.print(params_table) @@ -254,7 +260,6 @@ def main( init_strength=INIT_STRENGTH, roughness=ROUGHNESS, persistence=PERSISTENCE, - feature_size=feature_size, radius=radius, seed=world_seed, ) @@ -282,7 +287,7 @@ def main( log.debug(msg="Generating elevation scalars.") elevation_scalars: NDArray[np.float64] = ( ((raw_elevations) + radius) / radius - ) * zscale + ) * scale log.debug(msg="") log.debug(msg="Elevation Scalars:") @@ -326,7 +331,7 @@ def main( world_mesh=world_mesh, # scalars="Elevations", radius=radius, - zscale=zscale, + zscale=scale, zmin=zmin, zmax=zmax, ) diff --git a/lathe/terrain.py b/lathe/terrain.py index c6b0801..a1ced79 100644 --- a/lathe/terrain.py +++ b/lathe/terrain.py @@ -3,14 +3,28 @@ import numpy as np import opensimplex as osi -from mylogger import std_con +from mylogger import err_con, std_con from numba import prange from numpy.typing import NDArray def sample_noise( - points, roughness, strength, feature_size, radius + points: NDArray[np.float64], + roughness: float, + strength: float, + radius: int, ) -> NDArray[np.float64]: + """Samples 4-dimension simplex noise at each point in the input array. + + Args: + points (NDArray[np.float64]): A 3d array of points. + roughness (float): The roughness (frequency) of the noise. + strength (float): The strength (persistence) of the noise. + radius (int): The radius of the world. + + Returns: + elevations: A NDArray[np.float64] of elevations for each point in the input array. + """ elevations = np.ones(len(points), dtype=np.float64) rough_verts = points * roughness @@ -19,42 +33,44 @@ def sample_noise( x=rough_verts[v][0], y=rough_verts[v][1], z=rough_verts[v][2], - w=1 / feature_size, + w=1, ) - # ?: Adding +1 to elevation moves negative values in the 0-1 range. Multiplying by 0.5 drags any values > 1 back into the 0-1 range. I'm not sure if multiplying by the radius is the proper thing to do in my next implementation. - - # return (elevations + 1) * 0.5 * strength * radius return elevations * strength * radius + def sample_octaves( - points, - octaves, - init_roughness, - init_strength, - roughness, - persistence, - feature_size, - radius, - seed, + points: NDArray[np.float64], + octaves: int, + init_roughness: float, + init_strength: float, + roughness: float, + persistence: float, + radius: int, + seed: int, ) -> NDArray[np.float64]: - """Samples multiple octaves of noise to generate elevations. + """Iterates through the noise function multiple times to create a more complex noise pattern. - Arguments: - points -- _description_ - octaves -- _description_ - init_roughness -- _description_ - init_strength -- _description_ - roughness -- _description_ - persistence -- _description_ - radius -- _description_ + Args: + points (NDArray[np.float64]): A 3d array of points. + octaves (int): The number of times to iterate through the noise function. + init_roughness (float): A starting value for the roughness. + init_strength (float): A starting value for the strength. + roughness (float): A multiplier for the roughness. + persistence (float): A multiplier for the strength. + radius (int): The radius of the world. + seed (int): The seed for the noise generator. Returns: - Array of elevations. + elevations: A NDArray[np.float64] of elevations for each point in the input array. """ # Initialize noise generator seed. - osi.seed(seed) + + if seed == 0: + osi.random_seed() + else: + osi.seed(seed) # Initialize elevations array. elevations = np.ones(shape=len(points), dtype=np.float64) @@ -62,15 +78,14 @@ def sample_octaves( # NOTE: In my separate-sampling experiment, rough/strength pairs of (1.6, 0.4) (5, 0.2) and (24, 0.02) were good for 3 octaves. The final 3 results were added and then multiplied by 0.4 for i in prange(octaves): + std_con.print(f"Octave: {i+1} ", "\r\n") elevations += sample_noise( points=points, roughness=init_roughness / radius, strength=init_strength / radius, - feature_size=feature_size, radius=radius, ) init_roughness *= roughness init_strength *= persistence - std_con.print(f"Octave: {i} ", "\r\n") return elevations