ZG Connect logoZG Connect

Faza 1: Priprema podataka prije Unity importa

Prije nego što bilo što uđe u Unity, izvorni datasetovi pripremaju se potpuno odvojeno, izvan Unity projekta: teren (DEM/heightmap), teksture terena (satelitski ortofoto i/ili OSM splatmap) i 3D zgrade. Svi pipelinei završavaju u istom formatu — skup pločica (tileova) poravnatih na 1×1 km mrežu, spremnih za uvoz.

  • Track A — Teren (visine): digitalni model reljefa (DMR) → GeoTIFF → 1×1 km tile-ovi → 16-bit RAW heightmap.
  • Track B — Zgrade: izvorni 3D model zgrada → Blender → FBX/GLB → Unity → segmentacija u 1×1 km tile-ove.
  • Track C — Teksture terena: satelitski/zračni ortofoto (DGU) i/ili semantički OSM splatmap → 1×1 km tile teksture.
  • Track D — Cestovna mreža: OSM ceste (Overpass) → centerline JSON po 1×1 km tileu → (naknadno) 3D road mesh.

Sva tri traga koriste isti tile identifikacijski sustav (koordinate donjeg-lijevog kuta tilea u metrima, npr. 550000_5068000), a Track A (heightmap) je zapravo izvor grida — i Track B i Track C čitaju metadata.json iz Tracka A da bi znali točne granice svakog tilea, umjesto da svaki pipeline zasebno definira koordinate.


Track A: Teren — DEM u Unity heightmap

Skripte: D:\_unity projects\ZG Connect\_resources\_height\ (geotiff_2_tiff_tiles.py, geotiff_tiles_2_raw.py)

Izvor podataka

Polazna točka je Digitalni model reljefa Grada Zagreba (ZAGREB_DMR_2022), preuzet sa Zagrebačkog GeoHUB-a (geohub-zagreb.hub.arcgis.com):

ZAGREB_DMR_2022 https://geohub-zagreb.hub.arcgis.com/datasets/5f8d5bb987e6441ab3554cab26318552

Dataset se preuzima kao ESRI geodatabase (ZAGREB_DMR_2022.gdb, isporučen kao .gdb.zip). Iz njega se u GIS alatu (izvan ovog repozitorija) izvozi jedinstveni spojeni raster geo_tiff_zagreb.tif koji pokriva cijelo područje grada — to je ulaz za prvi Python korak.

Napomena — koji GIS alat/postupak koristiš za export .gdb.tif (QGIS, ArcGIS Pro...) trenutno nije dio dokumentiranog pipelinea. Reci mi pa ću to dodati.

Korak 1: geotiff_2_tiff_tiles.py — rezanje na 1×1 km tile-ove

Uzima jedan veliki GeoTIFF (cijeli DMR grada) i reže ga na mrežu 1×1 km pločica, poravnatu na apsolutne koordinate (svaki tile počinje na cijeloj tisućici metara u izvornom CRS-u). Svaka pločica se resampla bilinearno na 1025×1025 px — jedan piksel viška po osi (1024 + 1) je namjerno, standardna Unity Terrain heightmap konvencija koja omogućuje besšavno spajanje susjednih terena (zadnji red/stupac piksela jednog tilea preklapa se s prvim redom/stupcem susjednog).

Tile-ovi kojima je udio nevažećih (NoData) piksela iznad praga smatraju se praznima i preskaču se — DMR ne pokriva cijeli pravokutni bounding box grada (rubovi, rijeke i sl.).

Parametar Vrijednost / opis
INPUT_TIF Putanja do spojenog izvornog GeoTIFF-a (cijeli grad).
OUTPUT_DIR Mapa u koju se pišu pojedinačni tile GeoTIFF-ovi + tiles_metadata.json.
TILE_SIZE_METERS Veličina tilea u metrima — 1000 (1 km), poravnano s ostatkom pipelinea.
OUTPUT_RESOLUTION Rezolucija svakog tile rastera u pikselima — 1025 (1024 + 1 rub za bešavno spajanje).
EMPTY_INVALID_RATIO_THRESHOLD Ako je udio NoData piksela u tileu ≥ ova vrijednost (0.95), tile se u potpunosti preskače.
NODATA_THRESHOLD Sve vrijednosti veće od ovoga (1e20) tretiraju se kao NoData — hvata "sulude" float vrijednosti iz izvora (stvarni NoData je bio 3.4e+38).

Za svaki uspješno exportani tile skripta piše ime po konvenciji zg_dmr_{left}_{bottom}_1km_1025.tif i u tiles_metadata.json bilježi granice (left/right/ bottom/top), udio nevažećih piksela te min/max/mean visinu unutar tilea (informativno, ne koristi se za normalizaciju — vidi sljedeći korak).

Korak 2: geotiff_tiles_2_raw.py — konverzija u Unity RAW heightmap

Uzima sve .tif tile-ove iz prethodnog koraka i za svaki piše 16-bit unsigned RAW datoteku u formatu koji Unity Terrain izravno razumije pri importu heightmapa (Terrain → Import Raw..., little-endian, Windows byte order).

Ključna točka ovog koraka: normalizacija visine radi se na globalnom rasponu (MIN_HEIGHTMAX_HEIGHT), ne po pojedinom tileu. Kad bi se svaki tile normalizirao prema svom lokalnom min/max, susjedni tereni ne bi imali usklađenu skalu visine i nastali bi vidljivi "stepenici" na granicama tileova.

Parametar Vrijednost / opis
INPUT_TIF_DIR Mapa s 1×1 km GeoTIFF tile-ovima iz koraka 1.
OUTPUT_RAW_DIR Mapa u koju se pišu .raw datoteke + tiles_unity_metadata.json.
RESOLUTION 1025 — mora odgovarati rezoluciji iz koraka 1.
TILE_SIZE_METERS 1000 — veličina tilea u metrima.
MIN_HEIGHT / MAX_HEIGHT Globalni raspon visina (u metrima) preko svih tileova zajedno (trenutno 95.01040.0) — koristi se za 0..1 normalizaciju prije zapisa u 16-bit. Mora ostati isti za cijeli grad radi bešavnog spajanja terena.
MAX_INVALID_RATIO_TO_EXPORT Tile se preskače ako mu je udio NoData piksela veći od ove vrijednosti. Za pun export cijelog grada postavljeno na 0.95 (skripta u komentaru predlaže niže vrijednosti, npr. 0.100.25, za brže testne exporte s manje rubnih/praznih tileova).
UNITY_ORIGIN_X / UNITY_ORIGIN_Y Ishodište Unity koordinatnog sustava izraženo u izvornim (metarskim, CRS) koordinatama. Oduzima se od apsolutnih koordinata svakog tilea da se dobije relativna Unity pozicija (unity_position.x/z). Trenutno 442000 / 5051000.

NoData rupe unutar tilea (rubni/djelomično prazni tile-ovi koji ipak prođu prag) popunjavaju se najjednostavnijom mogućom strategijom — vrijednošću MIN_HEIGHT — dovoljno stabilno za Unity import, ali ne i geografski točno; za takve rubne tileove treba računati s vizualnim artefaktima.

Izlaz i veza s Unity importom

Rezultat je mapa .raw heightmap datoteka (zg_dmr_{left}_{bottom}_1km_1025.raw) plus tiles_unity_metadata.json — JSON s dva dijela:

  • settings — globalne postavke cijelog seta (rezolucija, veličina tilea, globalni min/max visine, Unity origin, prag nevažećih piksela).
  • tiles[] — po tile: naziv RAW datoteke, geografske granice, unity_position (već preračunata relativna pozicija), terrain_size (x/y/z u metrima — visina terena y je MAX_HEIGHT - MIN_HEIGHT), rezolucija heightmapa i statistika nevažećih/graničnih vrijednosti.

Ovaj JSON format je 1:1 ono što Unity-side importer očekuje kao HeightmapMetadataJson / HeightmapSettingsJson / HeightmapTileJson (ZGConnectJsonModels.cs) — dakle izlaz ovog Python koraka je izravan, strojno čitljiv ulaz za ZGConnectImporter u sljedećoj fazi (klasični Unity import), bez ručnog prepisivanja parametara.


Track B: Zgrade — preuzimanje i segmentacija 3D zgrada

Ova faza pretvara izvorni 3D model zgrada Grada Zagreba (vanjski, "sirovi" izvor) u skup manjih GLB datoteka poravnatih na 1×1 km mrežu (tile grid) koje se kasnije koriste za klasični Unity import ili za realtime streaming pipeline.

Pregled koraka

  1. Preuzimanje izvornog dataseta — 3D model zgrada s Grad Zagreb open data portala.
  2. Blender — import izvornog modela i export u FBX ili GLB.
  3. Unity import — FBX/GLB se uvozi u scenu kao "sirova" hijerarhija zgrada.
  4. Ručno poravnanje — building root se pozicionira/poravnava nad terenom u Scene view-u.
  5. Building Associator — grupira pojedinačne zgrade u 1×1 km tile grupe i upisuje ih u CityDataset.
  6. Building Exporter — exporta svaku tile grupu kao zasebni .glb + prateći .json (metapodaci lokacije).

Korak 1: Preuzimanje izvornog dataseta

Izvorni 3D model zgrada Grada Zagreba preuzima se s Zagrebačkog portala otvorenih podataka:

ZG3D 2022 — 3D model Grada Zagreba https://data.zagreb.hr/dataset/zg3d-2022-3d-model-gz

Korak 2: Blender

Preuzeti model se otvori u Blenderu (import), i odmah izveze (export) u FBX ili GLB format — bez dodatne obrade unutar Blendera. Ovaj korak je čisto konverzija formata radi kompatibilnosti s Unity importerom.

Korak 3–4: Unity import i poravnanje

FBX iz Blendera uvozi se standardnim Unity FBX importerom kao hijerarhija GameObjecta — svaka pojedinačna zgrada je jedan direktni child zajedničkog "buildings root" objekta. Cijela unutarnja hijerarhija zgrade (sub-meshevi, LOD čvorovi i sl.) tretira se kao jedna cjelina i kasnije se pomiče zajedno.

Prije segmentacije, taj root objekt treba ručno poravnati u Scene view-u tako da footprinti zgrada odgovaraju terrain tileovima (isti koordinatni sustav kao i terrain grid iz prijašnje faze pripreme podataka).

Korak 5: Building Associator

Meni: ZG Connect → Building Associator Skripta: ZGConnectBuildingAssociatorWindow.cs

Alat grupira sve zgrade (direktne childove building roota) u 1×1 km tile-parent objekte (TileBuildings_{tileId}) na temelju pozicije svake zgrade unutar mreže definirane u CityDataset assetu, i upisuje referencu u dataset (CityTileRecord.buildingsSceneObject).

Veličina tilea nije hardkodirana na 1 km — čita se iz CityDataset.tileSizeMeters (default vrijednost je 1000, tj. 1000 m = 1 km). Klasifikacija zgrade u tile radi se po X/Z koordinatama centra njezinog bounding boxa (svi rendereri u hijerarhiji), podijeljenim s veličinom tilea — cijelobrojno dijeljenje daje grid koordinatu tilea kojoj zgrada pripada.

Parametri prozora:

Parametar Opis
Buildings Root Root GameObject čiji su direktni childovi pojedinačne zgrade. Mora biti ručno poravnat s terenom prije pokretanja.
Dataset CityDataset asset koji definira tile mrežu (veličinu tilea, popis tileova s granicama).
Snap to terrain surface Ako je uključeno, nakon grupiranja svaka zgrada dobiva raycast prema dolje i njezina Y pozicija se postavlja na točku dodira s terenom. Zahtijeva aktivne terrain collidere.
Disable groups after wiring Postavlja sve tile-grupe zgrada na neaktivne nakon povezivanja. Preporučeno uključeno — streaming sustav očekuje da sve grupe kreću ugašene i uključuju se runtime po potrebi.

Akcije: - Preview (no scene changes) — samo analizira i prikazuje koliko zgrada pada u koji tile, bez izmjene scene. Prikazuje i broj "unassigned" zgrada (centar im je izvan svih tile granica — znak da poravnanje nije točno). - Associate Buildings — stvarno grupira zgrade u TileBuildings_{tileId} objekte i upisuje ih u dataset. - Associate First Tile Only (test) — testni pokušaj na samo jednom tileu (radi provjere prije pune obrade); preporuka je nakon toga napraviti Undo (Ctrl+Z) i tek onda pokrenuti punu asocijaciju.

Zgrade čiji centar ne upada ni u jedan tile završavaju u posebnoj grupi TileBuildings_Unassigned, uz upozorenje u konzoli — signal da treba provjeriti poravnanje.

Korak 6: Building Exporter

Meni: ZG Connect → Building Exporter Skripta: ZGConnectBuildingExporterWindow.cs (koristi ZGConnectGlbExportUtility.cs)

Exporta svaku tile-grupu (rezultat prethodnog koraka) u zaseban .glb fajl, plus opcionalni prateći .json s metapodacima lokacije. Zahtijeva instaliran UnityGLTF paket (com.khronos.unitygltf) — alat ga detektira reflekcijom pri pokretanju i javlja grešku ako paket nije instaliran.

Parametri prozora:

Parametar Opis
City Dataset Isti dataset korišten u Building Associatoru.
Buildings Root Fallback referenca — Unity ne može trajno spremiti referencu na scenski objekt unutar .asset datoteke, pa se ona gubi nakon svake rekompilacije skripti. Exporter zato po potrebi traži tile-grupe po imenu (TileBuildings_{tileId}) ispod ovog roota.
Output Folder Mapa u koju se pišu .glb/.json parovi, jedan par po tileu.
Write metadata JSON per tile Uz svaki .glb piše i .json s imenima zgrada i njihovim lokalnim transformacijama (pozicija/rotacija/scale) — most za kasnije spajanje s vanjskim atributnim podacima po zgradi.
Skip already exported tiles Ako buildings_{tileId}.glb već postoji u output mapi, taj tile se preskače. Omogućuje nastavak prekinutog exporta bez ponovne obrade već gotovih tileova.

Klikom na "Export N Tile(s) to GLB" alat prolazi kroz sve tileove koji imaju povezanu grupu zgrada, prikazuje progress bar (s mogućnošću otkazivanja) i po završetku javlja koliko je tileova exportano/preskočeno/palo s greškom.

Izlazna struktura

Za svaki tile s zgradama nastaje par datoteka:

buildings_{tileId}.glb
buildings_{tileId}.json

tileId je isti identifikator koji koristi terrain streaming sustav (npr. 550000_5068000), pa se building tileovi mogu jednoznačno povezati s odgovarajućim terrain tileom.

Sadržaj .json datoteke (osnovna varijanta, iz ZGConnectBuildingExporterWindow):

{
  "tileId": "550000_5068000",
  "tileSizeMeters": 1000,
  "tileOriginUnity": { "x": 0.0, "y": 0.0, "z": 0.0 },
  "buildingCount": 42,
  "buildings": [
    {
      "name": "zagreb_Part_1619",
      "localPosition": { "x": 0, "y": 0, "z": 0 },
      "localRotation": { "x": 0, "y": 0, "z": 0 },
      "localScale": { "x": 1, "y": 1, "z": 1 }
    }
  ]
}

Numerički ID ugniježđen u imenu zgrade (npr. zagreb_Part_16191619) kasnije služi kao ključ za spajanje s vanjskim atributnim podacima o zgradi (adresa, broj katova, površina, godina izgradnje...) iz otvorenih podataka Grada Zagreba — to spajanje nije dio ove faze, radi se naknadno.

Postoji i bogatija varijanta istog JSON-a (BuildingsMetadataJson, kroz BuildingTileMetadataWriter) koja uz transformacije nosi i material-slot ključeve, GPS koordinate i OSM match podatke po zgradi — ta se generira u kasnijem koraku obrade površina zgrada (surface baking), ne u ovom sirovom exportu. Dokumentirat ćemo je kad dođemo do te faze.

Automatska obrada pri re-importu (BuildingImportSettings.cs)

Kad se exportani .glb file kasnije uvozi natrag u Unity pod putanju Assets/Buildings/buildings_*, AssetPostprocessor skripta BuildingImportSettings.cs automatski postavlja import parametre modela:

Postavka Vrijednost Razlog
importAnimation isključeno zgrade nemaju animacije
importBlendShapes isključeno nema blend shapeova
importCameras / importLights isključeno GLB ne nosi scensku opremu koja bi trebala ući u Unity
generateSecondaryUV isključeno nema potrebe za lightmap UV-ovima
isReadable uključeno mesh mora biti čitljiv za kasniju obradu fasada/krovova (UV processing)
meshCompression Low zadržava preciznost geometrije
materialImportMode None materijali se ne uvoze automatski iz GLB-a, dodjeljuju se kroz ZG Connect pipeline

Pomoćni alat: BatchImport (kopiranje u projekt)

Meni: ZG Connect → Import Building Tiles Skripta: BatchImport.cs

Jednostavan alat koji otvara dijalog za odabir vanjske mape s buildings_*.glb/.json parovima, kopira sve te datoteke u Assets/Buildings/, i pokreće jedan zajednički (batch) Unity import umjesto pojedinačnog importa po datoteci — brže za veći broj tileova.


Track C: Teksture terena — ortofoto i OSM splatmap

Skripte: Assets/ZGConnect/Python_tools/ (download_dgu_ortho_offset_thirds.py, download_dgu_ortho_2048.py, download_basemap.py, osm_splatmap_generator.py, osm_ortho_splatmap_generator.py, make_texture_atlas.py)

Teren u Unityju treba i vizualnu teksturu, ne samo visinu. Postoje dva različita pristupa koja se mogu koristiti odvojeno ili zajedno (hibridno) — oba čitaju tile grid izravno iz metadata.json koji je proizveo Track A (heightmap), pa su tile granice uvijek 1:1 usklađene s terenom:

  • Ortofoto (satelitska/zračna snimka) — jedna velika fotografska tekstura po tileu (Unity "Ortho basemap"). Vizualno točno, ali bez semantike (ne zna se je li piksel trava, cesta ili zgrada).
  • OSM splatmap — teren dobiva do 8 materijalnih slojeva (Unity terrain layer blending) na temelju stvarnog tipa površine iz OpenStreetMap podataka (trava, šuma, urbano, asfalt...). Semantički točno, tile-teksture se ponavljaju (tiling) umjesto jedne velike fotografije.
  • Hibrid — kombinacija oba signala za prirodniji rezultat.

C1: Ortofoto — preuzimanje s DGU WMS servisa

Zračne/satelitske ortofoto snimke preuzimaju se s WMS servisa Državne geodetske uprave (DGU)geoportal.dgu.hr, sloj OI.OrthoimageCoverage.

download_dgu_ortho_offset_thirds.py — glavni per-tile downloader. Čita tile grid iz heightmap metadata.json (Track A) i za svaki tile skida jednu 2048×2048 px WMS snimku poravnatu točno na granice tog tilea.

Parametar Opis
METADATA_JSON Putanja do metadata.json iz Track A — izvor tile grida (koordinate svakog tilea).
OUTPUT_DIR Mapa u koju se pišu preuzete ortofoto teksture.
WMS_URL / LAYER_NAME / CRS DGU WMS endpoint, sloj i koordinatni sustav (EPSG:3765). U datoteci postoje zakomentirane alternative (npr. orthophoto_lidar_2022_2023 sloj) — trenutno aktivan je orthophoto_1000.
TILE_SIZE_METERS 1000 — mora odgovarati Track A gridu.
TEXTURE_RESOLUTION 2048 px po tileu.
MAX_INVALID_RATIO Prag za preskakanje rubnih/djelomično praznih tileova — 0.95 za pun export, 0.10 predloženo za brzi test.
DOWNLOAD_ALL_TILES / MAX_TILES_TO_DOWNLOAD Ako je DOWNLOAD_ALL_TILES = False, skida se samo prvih N tileova — korisno za brzi test prije punog exporta.
SPECIFIC_TILES Popis točno određenih tileova ((left, bottom) parovi) za ciljano ponovno preuzimanje, umjesto punog seta.
SKIP_EXISTING Preskače tileove koji već imaju preuzetu teksturu — nastavak prekinutog exporta.
SLEEP_BETWEEN_REQUESTS / REQUEST_TIMEOUT Pristojno ograničavanje brzine zahtjeva prema DGU serveru.

download_dgu_ortho_2048.py — poseban, manji alat: ne skida po tileu, nego preuzima jednu pregledno-selekcijsku sliku cijelog grada (2048×2048, sastavljenu od 4×4 WMS podzahtjeva) — koristi se kao referentna/preview slika za odabir područja, ne kao teren tekstura.

C2: Alternativni izvor — stilizirani web-map (download_basemap.py)

Umjesto zračne fotografije, ovaj alat generira teren teksturu iz web mapa (npr. Carto "Voyager, no labels" stil) — vizualno čist prikaz s cestama/objektima, ali bez fotorealizma. Podržava dva moda:

Mod (SOURCE) Opis
slippy Skida standardne web-map tileove (zoom 17–20) sa CDN-a (TILE_URL, npr. Carto), sastavlja ih i reže na granice terrain tilea. Zahtijeva internet po pokretanju; tileovi se keširaju lokalno (TILE_CACHE_DIR) i dijele između susjednih tileova.
cache Renderira vektorske OSM podatke offline, iz istog regionalnog Overpass JSON cachea koji koriste osm_vegetation_masks.py i download_osm_overview.py — bez ovisnosti o tile-serveru. SAMPLE_RESOLUTION > OUTPUT_RESOLUTION omogućuje supersampling (renderiraj veće, smanji LANCZOS filterom za glađi rezultat).

Ostali parametri: ZOOM (razina detalja za slippy mod), OUTPUT_RESOLUTION (finalna veličina teksture po tileu), FILE_PREFIX, SKIP_EXISTING, ROAD_WIDTH_SCALE (debljina cesta u cache modu). Izlaz je isti oblik kao ortofoto — jedna tekstura po tileu + metadata.json spreman za Dataset Import Manager.

C3: OSM splatmap — semantičko teksturiranje terena

osm_splatmap_generator.py generira do 8 materijalnih slojeva po tileu izravno iz OpenStreetMap vektorskih podataka, umjesto jedne fotografije. Izvor OSM podataka je isti PBF file kao u ostatku projekta: https://download.geofabrik.de/europe/croatia-latest.osm.pbf

Trenutna definicija slojeva (LAYERS u skripti):

Sloj OSM izvor (primjeri tagova) Veličina ponavljanja teksture
grass landuse=grass/meadow/farmland/..., natural=grassland/heath, leisure=park/garden/golf_course 4 m
forest landuse=forest/wood, natural=wood/forest/scrub 8 m
urban_light landuse=residential/commercial/retail/... 6 m
urban_dark landuse=industrial/construction/quarry/landfill 6 m
sand natural=beach/sand/dune, landuse=beach 4 m
rock natural=rock/bare_rock/scree/cliff/shingle 8 m
asphalt highway=motorway/trunk/primary/.../service (linije, bufferirane ~9 m), landuse=railway 3 m
dirt highway=footway/path/pedestrian/steps/track/bridleway (pješačke staze, bufferirane ~3 m) 2 m

Napomena — ovo je trenutna implementacija u kodu i razlikuje se od ranijeg arhitekturnog nacrta (terrain_splatmap_architecture.md, osm_splatmap_pipeline.md): water više nije terrain-layer nego se voda prikazuje kao zaseban GameObject, a dodan je dirt sloj za pješačke staze. Kad preklapanja postoje, pobjeđuje sloj s višim prioritetom (LAYER_PRIORITY, redoslijed od najnižeg: grass → dirt → asphalt → sand → rock → urban_light → urban_dark → forest). Piksel bez ikakvog OSM podatka ne dobiva automatski grass fallback u ovoj verziji (za razliku od starog nacrta) — ostaje neoznačen, osim ako se koristi hibridna (Track C-hibrid) varijanta.

Parametar Opis
TILES_METADATA_JSON metadata.json iz Track A — izvor tile grida.
OSM_PBF Putanja do preuzetog croatia-latest.osm.pbf.
OUTPUT_DIR Izlazna mapa (osm_tiled/ po defaultu).
SPLAT_RESOLUTION Rezolucija splatmap teksture po tileu — 512 px (~2 m/px) je zadano, 256 brže/manje, 1024 oštrije/sporije.
TILE_CRS EPSG:3765 — mora odgovarati ostatku pipelinea.

Rezultat: za svaki tile 2 RGBA PNG datoteke ({tileId}_splat0.png, {tileId}_splat1.png — 8 slojeva pakiranih u 2×4 kanala) plus metadata.json u formatu type: "tiled" koji Unity importer (ZG Connect Dataset Import Manager) izravno prepoznaje. Materijalne teksture za svaki sloj (npr. grass.png, asphalt.png...) ne generira skripta — te se ručno dodaju u OUTPUT_DIR/textures/ prije importa u Unity.

C4: Hibrid — OSM + ortofoto (osm_ortho_splatmap_generator.py)

Kombinira oba signala po pikselu radi prirodnijeg rezultata: OSM poligoni daju pouzdanu klasifikaciju tipa površine (oštri rubovi), a ortofoto boja (analiza zelenila, svjetline, zasićenja) dodaje prirodnu varijaciju i, što je važnije, popunjava područja gdje OSM nema podataka — umjesto da takvi pikseli ostanu neoznačeni ili automatski postanu trava.

Parametar Opis
TILES_METADATA_JSON Isti tile grid izvor kao i ostali koraci.
OSM_PBF OSM vektorski podaci (isti PBF).
ORTHO_METADATA_JSON / ORTHO_DIR Već preuzeti ortofoto set (izlaz iz C1) — ortofoto signal se izvlači odavde, ne skida se ponovno.
OUTPUT_DIR Zaseban izlaz (osm_ortho_tiled/) — ne miješa se s čistim OSM outputom.
ORTHO_INFLUENCE Koliko ortofoto boja modulira rezultat unutar OSM poligona. 0.0 = čisti OSM (identično C3), 1.0 = čista ortofoto klasifikacija. Preporučeni početak: 0.20 (80% OSM + 20% ortofoto).
ORTHO_INFLUENCE_UNCLASSIFIED Isto, ali za piksele izvan svih OSM poligona — postavljeno više (1.00) jer zamjenjuje "sve je trava" fallback stvarnom klasifikacijom iz boje.

Izlazni format identičan je čistom OSM splatmapu (C3) — može se uvesti na isti način kroz Dataset Import Manager, samo iz drugog output foldera.

Pomoćni alat: make_texture_atlas.py

Ne generira teren teksture, nego pakira postojeći set per-tile PNG-ova (bilo ortofoto, bilo pojedinačni splatmap kanal) u jednu veliku atlas sliku + JSON s UV koordinatama svakog tilea unutar atlasa (i top-left i bottom-left konvencija, potonja pogodna izravno za Unity materijale/meshove). Koristan za pregledne/UI karte ili minimap prikaze, ne ulazi u sam terrain-layer runtime pipeline. Čita bilo koji metadata.json s tiles[].texture_file poljem — radi i s ortofoto i s OSM tiled outputom.

Integracija u Unity

Bez obzira na odabranu varijantu (C1 ortofoto, C2 stilizirani basemap, C3 OSM splatmap, C4 hibrid), izlaz je uvijek mapa s teksturama + metadata.json spreman za:

Tools → ZG Connect → Dataset Import Manager → Scan for Data Sources → Import

Ortofoto/basemap izvori uvoze se kao Ortho basemap tip (jedna TerrainLayer po tileu); OSM splatmap i hibrid izvori uvoze se kao Tiled basemap tip (shared TerrainLayer setovi + per- tile alphamap). Jedan te isti tile može imati oba tipa basemapa registrirana istovremeno — BasemapSwitcher komponenta omogućuje runtime prebacivanje između njih bez ponovnog importa.



Track D: Cestovna mreža — preuzimanje OSM cesta u JSON po tileu

Skripta: osm_road_extractor.py (nova, samostalna — pogledaj napomenu ispod)

Ranije je export cesta bio "sakriven" unutar osm_vegetation_masks.py (skripta čije je glavno ime i svrha vegetacijske maske, a ceste je radila kao nusprodukt istog OSM upita). Na tvoj zahtjev, taj dio je izdvojen u zasebnu, samostalnu skriptuosm_road_extractor.py — koja radi isključivo preuzimanje i export cesta, bez ikakve ovisnosti o vegetacijskom dijelu (nema više numpy/Pillow/rasterio/scipy u zahtjevima, samo requests, pyproj, shapely). Kod je restrukturiran, ali logika ekstrakcije, širine cesta i formata izlaznog JSON-a identična je originalu — postojeći {tile_id}_roads.json fajlovi i sve što ih dalje koristi (osm_road_surfaces.py) ostaju kompatibilni.

Skripta radi jedan regionalni Overpass upit (samo za highway elemente — manji i brži upit nego kombinirani vegetacija+ceste upit iz originala, jer ne traži i forest/park/grass/ exclusion tagove), rezultat kešira lokalno, i zatim reže/exporta cestovne centerline po 1×1 km tileu, bez ikakvog dodatnog mrežnog poziva po tileu.

Napomena o provenijenciji — u projektu postoji i mapa _dataset/road_topology/ s drugačijim formatom ({tileId}_road_topology.json, sadrži već trianguliran poligon mesh), te kompajlirani osm_road_topology.cpython-314.pyc u __pycache__/ bez pripadajućeg .py izvornika bilo gdje u projektu. Izgleda kao ostatak ranije/napuštene varijante pipelinea koja je zamijenjena trenutnim tokom (osm_road_extractor.pyosm_road_surfaces.py, opisano dolje). Ako ti ta stara mapa/pristup više ne treba, može se tretirati kao arhiva; reci ako je ipak želiš dokumentirati zasebno.

Izvor podataka i način preuzimanja

Skripta ne skida podatke po tileu pojedinačno, nego radi jedan Overpass upit za cijelu regiju (uniju granica svih tileova iz heightmap grida), rezultat kešira u osm_overpass_cache.json (isti model preuzimanja kao download_osm_overview.py), i tek onda lokalno (bez dodatnih mrežnih poziva) reže/klasificira podatke po pojedinačnom tileu. Upit ide na jedan od javnih Overpass API endpointa (overpass-api.de, overpass.kumi.systems — s mogućnošću ručnog dodavanja alternativnog endpointa).

Klasifikacija cesta i debljina

Cesta se prepoznaje po highway OSM tagu koji odgovara regexu — obuhvaćeni tipovi: motorway, trunk, primary, secondary, tertiary, unclassified, residential, service, living_street, pedestrian, footway, path, cycleway, track.

Svakom tipu ceste dodijeljena je zadana širina (width_m) — koristi se i za generiranje "exclusion" buffera (za vegetacijske maske) i kao geometrijski podatak u izlaznom JSON-u:

highway tip Širina (m)
motorway 12.0
trunk 10.0
primary 8.0
secondary 7.0
tertiary 6.0
residential 5.0
service 4.0
footway 2.0
path 1.5
ostali (default) 5.0

Segmentacija po tileu

Sve ceste iz regionalnog upita režu se (clip_roads_for_tile) na granice svakog pojedinog 1×1 km tilea. Ako cesta prelazi granicu tilea, dio unutar tilea postaje zaseban segment — osm_way_id originalne OSM ceste se pritom zadržava, pa se segmenti iz susjednih tileova kasnije mogu ponovno "sašiti" po tom ID-u (to radi osm_road_surfaces.py u sljedećem koraku).

Izlazni format — {tile_id}_roads.json

{
  "format_version": 1,
  "tile_id": "442000_5061000",
  "crs": "EPSG:3765",
  "left": 442000, "bottom": 5061000, "right": 443000, "top": 5062000,
  "unity_origin": { "easting": 459479, "northing": 5074937 },
  "segment_count": 4,
  "segments": [
    {
      "osm_way_id": 93532653,
      "highway": "tertiary",
      "width_m": 6.0,
      "points_epsg": [[443000.0, 5061962.88], [442994.27, 5061970.99], ...],
      "points_unity": [[-16479.0, -12974.12], [-16484.73, -12966.0], ...]
    }
  ]
}

Svaki segment nosi koordinate osi ceste dvaput — u apsolutnim EPSG:3765 metrima (points_epsg) i već preračunate u Unity-relativne koordinate (points_unity, oduzet unity_origin) — tako da Unity strana ne mora sama raditi projekciju.

Parametri (CLI)

Parametar Opis
--metadata Heightmap metadata.json (Track A) — izvor tile grida. Alternativa: --tiles (stariji vegetation_tiles.json format iz Unity Vegetation Tiles Exportera).
--out Izlazna mapa za {tile_id}_roads.json fajlove (default _dataset/road_segments, isto mjesto kao i prije).
--tile Obradi samo jedan tile (po tile_id), umjesto svih.
--crs Override koordinatnog sustava (default EPSG:3765).
--max-invalid-ratio Preskoči tileove čiji je invalid_ratio (iz heightmap metadata) iznad zadane vrijednosti — isti mehanizam kao u Track A.
--region-min-e/max-e/min-n/max-n Opcionalni filter — obradi samo tileove unutar zadanog EPSG podskupa regije.
--skip-existing Preskoči tile ako {tile_id}_roads.json već postoji — nastavak prekinutog exporta.
--refresh-overpass Ignorira regionalni Overpass cache i ponovno skida podatke.
--cache-file Putanja do regionalnog OSM JSON cachea (default <out>/osm_roads_overpass_cache.json).
--user-agent Override HTTP User-Agent zaglavlja prema Overpass serveru.
--overpass-url Dodaj alternativni Overpass endpoint (parametar se može ponoviti za više endpointa).
--quiet Minimalni ispis (samo greške i sažetak na kraju).

Primjeri pokretanja:

python osm_road_extractor.py \
    --metadata "_dataset/hightmaps_raw_1km_1025/metadata.json" \
    --out "_dataset/road_segments"

python osm_road_extractor.py --metadata ... --out ... --tile 455000_5071000

python osm_road_extractor.py --metadata ... --out ... \
    --region-min-e 455000 --region-max-e 457000 --region-min-n 5071000 --region-max-n 5073000

D2: osm_road_surfaces.py — pretvaranje centerlina u 3D mesh ceste

{tile_id}_roads.json iz prethodnog koraka (D1) je ulaz, ne krajnji rezultat. osm_road_surfaces.py je zaseban, znatno opsežniji alat koji te centerline pretvara u stvarnu 3D geometriju ceste — asfaltnu plohu (i po želji pločnike) koja prati oblik terena, spremnu za GLB import u Unity. Za razliku od D1 (koji radi po 1×1 km tileu i ovisi samo o OSM podacima), ovaj korak prvo gradi jednu regionalnu geometriju za cijeli grad i tek je onda reže po tileovima — to je ključno za ispravno ponašanje na granicama tileova (cesta se ne smije "prekinuti" na rubu tile grida).

Pregled cijelog pipelinea (regionalni, ne po tileu)

  1. Učitavanje — pročita sve {tile}_roads.json fajlove iz --roads-dir (izlaz D1).
  2. Sašivanje (stitching) — segmenti iste OSM ceste (isti osm_way_id), razrezani na granicama susjednih tileova u D1, ponovno se spajaju u jednu neprekinutu liniju (chain_segments_for_way) tako da se krajnje točke susjednih segmenata poklapaju (tolerancija COORD_TOLERANCE_M = 0.05 m).
  3. Klasifikacija materijala — svaka sašivena cesta dobiva asphalt ili dirt na temelju highway taga: motorway/trunk/primary/secondary/tertiary/unclassified/residential/ service/living_street/pedestrian → asfalt; footway/path/steps/track/bridleway/cycleway → dirt (zemljana staza). Nepoznati tip po defaultu pada u asphalt.
  4. Regionalna geometrija po materijalu — svaka cesta se bufferira na punu širinu (width_m / 2 sa svake strane linije), sve trake istog materijala spajaju se boolean unijom (shapely.unary_union) u jednu (multi)poligonsku plohu za cijeli grad. Na raskrižjima se potom radi fillet (zaobljavanje) — buffer prema van pa prema unutra istog iznosa — da se uklone oštri "spike" artefakti na spojevima cesta.
  5. Regionalni cache — ovaj korak (2–4) je spor za cijeli grad (~15 min), pa se rezultat sprema u {out}/.surface_cache/ (asfalt i dirt geometrija kao WKB, plus JSON s parametrima koji su korišteni). Sljedeće pokretanje s istim --fillet-m/--buffer-join/ --simplify-m čita iz cachea umjesto da sve ponovno računa (~10 min za svih 858 tileova, samo export bez regionalnog koraka). --refresh-surfaces forsira ponovni izračun — nužno nakon promjene tih triju parametara jer se cache inače tiho koristi zastario.
  6. Rezanje po tileu — regionalna asfalt/dirt geometrija reže se na granice svakog 1×1 km tilea (clip_surface_to_tile). Dirt plohe se dodatno oduzimaju od asfaltnih (cesta uvijek "pobjeđuje" stazu na preklopu).
  7. Uzorkovanje visine terena — za svaki vrh poligona (ruba ceste) očita se visina izravno iz Track A RAW heightmap datoteka (HeightmapIndex, bilinearna interpolacija, ista normalizacija min_height/max_height kao u Track A) — cesta time prati stvarni oblik terena umjesto da lebdi na fiksnoj visini.
  8. Stabilizacija visine ruba — da rub ceste ne bi imao nagle "bow-tie" uvrnute trokute tamo gdje teren naglo skače, primjenjuje se ograničenje maksimalnog nagiba između susjednih točaka ruba (--max-slope) i nekoliko prolaza Laplaceovog zaglađivanja (--height-smooth-iters) prije nego što se ta visina "zaključa" i koristi za mesh.
  9. Triangulacija — poligon plohe (top face) triangulira se pomoću mapbox-earcut biblioteke (ista tehnika kao za krovove zgrada u Building pipelineu).
  10. Ekstruzija u "slab" — od trianguliranog gornjeg lica gradi se puni 3D volumen: gornja ploha (na visini terena + --surface-offset-m), donja ploha na istoj X/Z ali niža za --slab-thickness-m (dirt/pločnik) odnosno --road-slab-thickness-m (asfalt), i vertikalni bočni zidovi duž vanjskog ruba — osim na rubovima koji se poklapaju s granicom tilea (is_tile_clip_edge), da se izbjegnu nepotrebni "zidovi" točno na spoju dva susjedna tilea.
  11. Pločnici (opcionalno, --sidewalks) — uz asfaltni rub gradi se ravna traka pločnika prema van (--sidewalk-width-m), čija unutarnja visina rubnih vrhova dijeli točno istu vrijednost s rubom ceste (nema procjepa/koraka), a pločnik se ne generira gdje bi se preklopio s dirt stazom niti na tile rubovima.
  12. GLB export — sve plohe istog materijala (asfalt/dirt/pločnik) spajaju se u jedan mesh primitive (merge_mesh_data) i pišu u minimalni, ručno pisan GLB writer (bez vanjske GLTF biblioteke), s bojama materijala definiranim u kodu (MATERIAL_COLORS).
  13. Metapodaci — uz GLB, po tileu se piše {tile_id}_roads_surface.json: broj poligona/ vrhova/trokuta, popis materijala, i potpun snapshot svih bake parametara korištenih za taj export (korisno za reprodukciju/debug).

CLI parametri

Parametar Default Opis
--metadata HEIGHTMAP_METADATA (config) Heightmap metadata.json (Track A) — izvor tile grida.
--roads-dir ROADS_DIR (config) Mapa s {tile}_roads.json fajlovima — izlaz D1 (osm_road_extractor.py).
--heightmap-dir HEIGHTMAP_DIR (config) Mapa s zg_dmr_* RAW heightmap datotekama (Track A) — izvor za uzorkovanje visine.
--out OUTPUT_DIR (config) Izlazna mapa za GLB + surface JSON + .surface_cache/.
--tile Obradi samo jedan tile (po tile_id).
--crs EPSG:3765 Oznaka koordinatnog sustava (informativna, geometrija je već u tom CRS-u iz D1).
--fillet-m 2.0 Radijus zaobljavanja na raskrižjima cesta (uklanja oštre spike-ove na spojevima). Dio regionalnog cachea — promjena zahtijeva --refresh-surfaces.
--surface-offset-m 0.05 Vertikalni pomak gornje plohe iznad terena (Unity Y) — sprječava z-fighting s terenom ispod.
--slab-thickness-m 0.20 Debljina vertikalnog "slaba" za dirt staze i pločnike.
--road-slab-thickness-m 0.35 Debljina slaba za asfaltnu cestu (deblje nego dirt/pločnik — dodatna zaštita protiv z-fightinga na bočnim zidovima).
--buffer-join 3 (bevel) Stil spoja pri bufferiranju linije u traku: 1 = round, 2 = mitre (oštri špic — sklon artefaktima), 3 = bevel (preporučeno). Dio regionalnog cachea.
--simplify-m 0.15 Douglas-Peucker pojednostavljenje ruba poligona prije triangulacije (0 = isključeno) — manje "sliver" (izduženih, gotovo degeneriranih) trokuta.
--max-slope 0.35 Maksimalni odnos uspon/razmak (rise/run) između susjednih točaka na rubu — sprječava uvrnute ("bow-tie") trokute gdje teren naglo skače.
--height-smooth-iters 2 Broj prolaza Laplaceovog zaglađivanja visine ruba prije zaključavanja.
--min-triangle-area 0.02 Odbacuje (kulira) trokute manje od ove površine (3D, u m²) — čisti degenerirane ostatke nakon triangulacije/simplifikacije.
--edge-segment-m 2.0 Maksimalna duljina ruba prije subdivizije poligonskog prstena — gladi krivine ceste i smanjuje artefakte na dugim ravnim segmentima preko zakrivljenog terena.
--sidewalks isključeno Generira ravne pločnike uz asfaltni rub (bez rampinga).
--sidewalk-width-m 1.5 Širina pločnika, mjerena prema van od asfaltnog ruba.
--skip-existing isključeno Preskoči tile ako {tile_id}_roads_surface.json već postoji — nastavak prekinutog exporta.
--refresh-surfaces isključeno Prisili ponovni izračun regionalne union/fillet geometrije umjesto čitanja iz .surface_cache/obavezno nakon promjene --fillet-m ili --buffer-join.
--region-min-e/max-e/min-n/max-n Opcionalni filter — obradi samo tileove unutar zadanog EPSG podskupa regije.
--quiet isključeno Minimalni ispis.

Primjeri pokretanja

# Prvi run (~15 min za regionalni union) + export svih pločica
python osm_road_surfaces.py \
    --metadata "_dataset/hightmaps_raw_1km_1025/metadata.json" \
    --roads-dir "_dataset/road_segments" \
    --heightmap-dir "_dataset/hightmaps_raw_1km_1025" \
    --out "_dataset/road_surfaces"

# Ponovni export (brzo, koristi .surface_cache u --out)
python osm_road_surfaces.py --out "_dataset/road_surfaces" --skip-existing

# Jedna pločica
python osm_road_surfaces.py --tile 473000_5080000 --out "_dataset/road_surfaces"

# Prilagodba ceste (slab, fillet, kvaliteta mesha)
python osm_road_surfaces.py --tile 473000_5080000 \
    --fillet-m 2.0 --surface-offset-m 0.05 --slab-thickness-m 0.20 \
    --buffer-join 3 --simplify-m 0.15 --max-slope 0.35 --height-smooth-iters 2

# Ceste + pločnici (asphalt + sidewalk materijali u GLB-u)
python osm_road_surfaces.py --out "_dataset/road_surfaces" --sidewalks \
    --sidewalk-width-m 1.5 --road-slab-thickness-m 0.35

# Forsiraj rebuild regionalnog union/fillet cachea
# (obavezno nakon promjene --fillet-m ili --buffer-join)
python osm_road_surfaces.py --refresh-surfaces \
    --metadata "_dataset/hightmaps_raw_1km_1025/metadata.json" \
    --roads-dir "_dataset/road_segments" \
    --heightmap-dir "_dataset/hightmaps_raw_1km_1025" \
    --out "_dataset/road_surfaces"

Izlaz po pločici

{tile_id}_roads.glb           — mesh (asphalt, dirt, opcionalno sidewalk kao zasebni primitivi)
{tile_id}_roads_surface.json  — vertex/triangle count, popis materijala, potpuni bake parametri

{tile_id}_roads_surface.json primjer strukture:

{
  "format_version": 2,
  "tile_id": "473000_5080000",
  "left": 473000, "bottom": 5080000, "right": 474000, "top": 5081000,
  "has_mesh": true,
  "glb_file": "473000_5080000_roads.glb",
  "materials": ["asphalt", "dirt"],
  "polygon_count": 14,
  "sidewalk_polygon_count": 0,
  "vertex_count": 3821,
  "triangle_count": 5104,
  "bake": {
    "fillet_m": 2.0, "surface_offset_m": 0.05, "slab_thickness_m": 0.2,
    "road_slab_thickness_m": 0.35, "simplify_m": 0.15, "max_slope": 0.35,
    "height_smooth_iters": 2, "min_triangle_area_m2": 0.02, "buffer_join": 3,
    "sidewalks": false, "sidewalk_width_m": 1.5, "edge_segment_m": 2.0
  }
}

Ako za neki tile nema geometrije ceste (npr. tile bez ijedne ceste), i dalje se piše metadata JSON s "has_mesh": false (radi ispravnog rada --skip-existing), ali bez GLB datoteke.


Sljedeći korak

Sve četiri pripreme (Track A–D) rezultiraju setovima 1×1 km tileova s pripadajućim JSON metapodacima — teren kao .raw heightmap + tiles_unity_metadata.json (Track A), zgrade kao buildings_{tileId}.glb + .json (Track B), teksture terena kao ortofoto/splatmap PNG-ovi + metadata.json (Track C), ceste kao {tileId}_roads.json centerline (Track D, uz opcionalni GLB mesh iz osm_road_surfaces.py). Svi ulaze kao ulaz u sljedeću fazu: klasični Unity import (ZGConnectImporter, Dataset Import Manager, obrada fasada/krovova) ili realtime streaming pipeline.