Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ Generated terrain retains per-provider URLs, checksums, attribution and datum as
- Optional regional bathymetry and permitted numeric exports retain their own terms.

The code license does not replace dataset terms. See [GEBCO terms](https://www.gebco.net/data-products/gridded-bathymetry-data), [Copernicus DEM collection](https://spacedata.copernicus.eu/collections/copernicus-digital-elevation-model), and [OpenStreetMap copyright](https://www.openstreetmap.org/copyright).

Packaged seabed maps: [Aerial Beach 01 by Rob Tuytel, Poly Haven](https://polyhaven.com/a/aerial_beach_01), [CC0-1.0](https://polyhaven.com/license). The original 2K diffuse, OpenGL normal and roughness maps are included with source URLs and checksums in `src/posim_terrain/assets/seabed/manifest.json`.
4 changes: 4 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ python -m unittest discover -s tests
```

코드: [Apache-2.0](LICENSE). 데이터에는 각 자료의 이용 조건과 출처 표시가 적용됩니다.

## 해저면 표현

해저면은 [Rob Tuytel의 Aerial Beach 01](https://polyhaven.com/a/aerial_beach_01)에서 제공한 2K 컬러·OpenGL 노멀·거칠기 맵을 사용합니다. 라이선스는 [CC0](https://polyhaven.com/license)입니다. 질감은 30 m마다 반복되며 격자선이 포함되어 있지 않습니다. 이미지 URL과 SHA-256 해시는 `src/posim_terrain/assets/seabed/manifest.json`에 기록되어 있습니다. 실행 중 질감 사용에는 네트워크 연결이 필요하지 않습니다. 이미지는 일반적인 모래를 표현하며 실제 지형 형상은 수치 수심 자료로 정합니다.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ python -m unittest discover -s tests
```

Code: [Apache-2.0](LICENSE). Datasets retain their own terms and attribution.

## Seabed appearance

Seabed visuals use the packaged 2K diffuse, OpenGL normal and roughness maps from [Aerial Beach 01 by Rob Tuytel](https://polyhaven.com/a/aerial_beach_01), licensed [CC0](https://polyhaven.com/license). The texture repeats every 30 m and contains no baked grid. Asset URLs and SHA-256 hashes are recorded in `src/posim_terrain/assets/seabed/manifest.json`. Runtime texture use requires no network access. The imagery represents generic sand; numeric bathymetry defines the actual terrain.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ dependencies = ["numpy>=1.24", "scipy>=1.10", "pyproj>=3.5", "Pillow>=10", "rast
where = ["src"]

[tool.setuptools.package-data]
posim_terrain = ["data/*"]
posim_terrain = ["data/*", "assets/seabed/*"]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/posim_terrain/assets/seabed/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"asset": "Aerial Beach 01",
"author": "Rob Tuytel",
"source": "https://polyhaven.com/a/aerial_beach_01",
"license": "CC0-1.0",
"license_url": "https://polyhaven.com/license",
"scale_metres": 30,
"files": {
"diff": {
"file": "aerial_beach_01_diff_2k.jpg",
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/aerial_beach_01/aerial_beach_01_diff_2k.jpg",
"sha256": "1803d7e2861b304bea6d2e124dcd4c657fcb93843b84c17bb2db98ce48a87e4e"
},
"nor_gl": {
"file": "aerial_beach_01_nor_gl_2k.jpg",
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/aerial_beach_01/aerial_beach_01_nor_gl_2k.jpg",
"sha256": "0f35185b7225fe2027897bb7e4586c7deb048817524f1ed104fdae7ce80bdc06"
},
"rough": {
"file": "aerial_beach_01_rough_2k.jpg",
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/aerial_beach_01/aerial_beach_01_rough_2k.jpg",
"sha256": "318b440c109de7411f9e7443f38ae3cf873f34e90afd762c931bae3b23426da2"
}
}
}
5 changes: 4 additions & 1 deletion src/posim_terrain/coastal_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,11 @@ def request(cells):
shutil.rmtree(lock,ignore_errors=True)
log = (root()/'warmer.log').open('ab')
try:
subprocess.Popen([sys.executable,"-m","posim_terrain.coastal_detail",'--warm'],
process=subprocess.Popen([sys.executable,"-m","posim_terrain.coastal_detail",'--warm'],
stdout=log,stderr=log,start_new_session=True)
if os.getenv('WWOS_NATIVE_OWNER'):
from process_group import register_group
register_group(process,os.environ['WWOS_DATA_DIR'],os.environ['WWOS_NATIVE_OWNER'])
except OSError:pass
finally:log.close()

Expand Down
44 changes: 27 additions & 17 deletions src/posim_terrain/seabed_visual.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Synthetic metric sand/grid visual; never changes measured collision heights."""
"""Textured sand visual with runtime measurement overlays; never changes measured collision heights."""
from pathlib import Path
import json
import hashlib
Expand Down Expand Up @@ -27,18 +27,24 @@ def renderer_albedo(path):
finally:temporary.unlink(missing_ok=True)
return target

SEABED_SCALE_M = 30
SEABED_ASSETS = Path(__file__).parent/'assets'/'seabed'

def texture(path):
size=512
rng=np.random.default_rng(20261002)
y,x=np.indices((size,size))
grain=rng.normal(0,8,(size,size)) + 6*np.sin(x*.17+2*np.sin(y*.027))
pixels=np.clip(np.array([164,145,104])[None,None,:]+grain[...,None],0,255).astype(np.uint8)
image=Image.fromarray(pixels)
draw=ImageDraw.Draw(image)
for p in range(0,size,128):
draw.line((p,0,p,size),fill=(18,18,18),width=4)
draw.line((0,p,size,p),fill=(18,18,18),width=4)
image.save(path)
"""Install CC0 sand without baked measurement lines; preserve old callers."""
with Image.open(SEABED_ASSETS/'aerial_beach_01_diff_2k.jpg') as source:
source.save(path)

def sand_assets(out):
import shutil
result={}
for channel in ('diff','nor_gl','rough'):
source=SEABED_ASSETS/f'aerial_beach_01_{channel}_2k.jpg'
digest=hashlib.sha256(source.read_bytes()).hexdigest()[:16]
target=Path(out)/f'seabed-sand-{channel}-{digest}.jpg'
if not target.is_file():shutil.copyfile(source,target)
result[channel]=target
return result

def shoreline_pixels(pixels,elevation):
"""Blend terrain in a narrow height band around mean sea level.
Expand Down Expand Up @@ -81,9 +87,9 @@ def shoreline_albedo(out,xs,ys,heights,land_texture):
ys[-1]-(np.arange(ny)+.5)*height/ny)
if not land_texture:
with Image.open(source) as image: sand=np.asarray(image.convert('RGB'))
# Match the existing absolute-metre, 4 m repeating seabed UVs.
u=np.floor(np.mod(xx/4,1)*sand.shape[1]).astype(int)
v=np.floor(np.mod(-yy/4,1)*sand.shape[0]).astype(int)
# Match the existing absolute-metre, 30 m repeating seabed UVs.
u=np.floor(np.mod(xx/SEABED_SCALE_M,1)*sand.shape[1]).astype(int)
v=np.floor(np.mod(-yy/SEABED_SCALE_M,1)*sand.shape[0]).astype(int)
base=sand[v,u]
graded=shoreline_pixels(base,surface_height(xs,ys,heights,xx,yy))
if land_texture and np.array_equal(graded,base):return renderer_albedo(source)
Expand All @@ -99,6 +105,7 @@ def decorate_link(link,out,xs,ys,heights,*,normals=None,land_texture=None,prefix
from .terrain import write_mesh
out=Path(out)
texture(out/'seabed-grid.png')
sand=sand_assets(out)
for visual in list(link.findall('visual')):link.remove(visual)
h=np.asarray(heights)
a,b,c,d=h[:-1,:-1],h[:-1,1:],h[1:,:-1],h[1:,1:]
Expand All @@ -109,13 +116,13 @@ def decorate_link(link,out,xs,ys,heights,*,normals=None,land_texture=None,prefix
land=maximum>=0
shallow_seabed=shallow&~land
albedo=shoreline_albedo(out,xs,ys,h,land_texture) if land.any() else out/land_texture['file']
parts=[('seabed',~(land|shallow_seabed),4,(0,0),'seabed-grid.png'),
parts=[('seabed',~(land|shallow_seabed),SEABED_SCALE_M,(0,0),sand['diff']),
('land',land,(xs[-1]-xs[0],ys[-1]-ys[0]),(-xs[0],-ys[0]),albedo)]
if shallow_seabed.any():
parts.append(('shallow',shallow_seabed,(xs[-1]-xs[0],ys[-1]-ys[0]),(-xs[0],-ys[0]),
shoreline_albedo(out,xs,ys,h,None)))
else:
parts=[('seabed',~shallow,4,(0,0),'seabed-grid.png')]
parts=[('seabed',~shallow,SEABED_SCALE_M,(0,0),sand['diff'])]
if shallow.any():
parts.append(('shallow',shallow,(xs[-1]-xs[0],ys[-1]-ys[0]),(-xs[0],-ys[0]),
shoreline_albedo(out,xs,ys,h,None)))
Expand All @@ -131,6 +138,9 @@ def decorate_link(link,out,xs,ys,heights,*,normals=None,land_texture=None,prefix
albedo=out/image
ET.SubElement(metal,'albedo_map').text=str(albedo.resolve())
ET.SubElement(metal,'roughness').text='1'
if name=='seabed':
ET.SubElement(metal,'normal_map').text=str(sand['nor_gl'].resolve())
ET.SubElement(metal,'roughness_map').text=str(sand['rough'].resolve())
ET.SubElement(metal,'metalness').text='0'

def decorate(tree,out,xs,ys,heights,*,land_texture=None):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_coastal_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
ROOT=Path(__file__).resolve().parents[1]
import posim_terrain.coastal_detail as detail
from posim_terrain.terrain import projection
from posim_terrain.seabed_visual import decorate_link,prepare,shoreline_pixels
from posim_terrain.seabed_visual import decorate_link,prepare,shoreline_pixels,sand_assets


class CoastalSafety(unittest.TestCase):
Expand Down Expand Up @@ -97,7 +97,7 @@ def test_shoreline_limits_and_higher_land_keep_original_texture(self):
visual=link.find('visual');land=imagery and height>=0
self.assertEqual(visual.get('name'),'land' if land else 'seabed')
albedo=Path(visual.findtext('material/pbr/metal/albedo_map'))
self.assertEqual(albedo.read_bytes(),(out/('land-satellite.png' if land else 'seabed-grid.png')).read_bytes())
self.assertEqual(albedo.read_bytes(),(out/'land-satellite.png' if land else sand_assets(out)['diff']).read_bytes())
self.assertFalse((out/'land-shoreline.png').exists())
self.assertFalse((out/'seabed-shoreline.png').exists())

Expand Down
28 changes: 28 additions & 0 deletions tests/test_seabed_material.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Offline texture identity, physical UV scale and unchanged collision data."""
from pathlib import Path
import hashlib,json,tempfile,unittest,xml.etree.ElementTree as ET
import numpy as np
from PIL import Image
from posim_terrain.seabed_visual import SEABED_ASSETS,SEABED_SCALE_M,decorate_link,texture,sand_assets
class SeabedMaterialTests(unittest.TestCase):
def test_packaged_maps_match_attribution_and_hashes(self):
manifest=json.loads((SEABED_ASSETS/'manifest.json').read_text())
self.assertEqual(manifest['license'],'CC0-1.0');self.assertEqual(manifest['scale_metres'],SEABED_SCALE_M)
for asset in manifest['files'].values():
path=SEABED_ASSETS/asset['file'];self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(),asset['sha256'])
with Image.open(path) as image:self.assertEqual(image.size,(2048,2048))
def test_sand_has_no_baked_grid_and_material_keeps_collision(self):
with tempfile.TemporaryDirectory() as tmp:
out=Path(tmp);texture(out/'sand.png')
with Image.open(out/'sand.png') as actual,Image.open(SEABED_ASSETS/'aerial_beach_01_diff_2k.jpg') as expected:np.testing.assert_array_equal(actual,expected)
link=ET.fromstring('<link name="terrain"><collision name="survey"><geometry><mesh><uri>survey.dae</uri></mesh></geometry></collision></link>')
before=ET.tostring(link.find('collision'));axis=np.array([0.,30.,60.]);h=np.full((3,3),-10.)
decorate_link(link,out,axis,axis,h)
self.assertEqual(ET.tostring(link.find('collision')),before);np.testing.assert_array_equal(h,-10.)
metal=link.find('visual/material/pbr/metal');assets=sand_assets(out)
for field,key in [('albedo_map','diff'),('normal_map','nor_gl'),('roughness_map','rough')]:self.assertEqual(Path(metal.findtext(field)),assets[key].resolve())
ns={'c':'http://www.collada.org/2005/11/COLLADASchema'};mesh=ET.parse(link.findtext('visual/geometry/mesh/uri'))
arrays=mesh.findall('.//c:source',ns);uv=next(s for s in arrays if 'uv' in s.get('id','').lower() or 'texcoord' in s.get('id','').lower())
values=np.fromstring(uv.findtext('c:float_array',namespaces=ns),sep=' ').reshape(-1,2)
self.assertAlmostEqual(float(np.ptp(values[:,0])),2.)
if __name__=='__main__':unittest.main()