From 55e58c2fe4d5f9551131fb3e8d182130e2e3f2a8 Mon Sep 17 00:00:00 2001 From: marstr Date: Thu, 5 Sep 2024 22:49:39 +0200 Subject: [PATCH] Initial code to generate the WED (X-Plane World Editor) XML file at the end of all tile generation. --- defines.py | 22 +++++++++--------- layergen.py | 33 +++++++++++++++++--------- maskgen.py | 11 +++++++++ orthographic.py | 8 +------ osmxml.py | 22 +++++++++++------- repoinfo | 7 ++++++ tiledb.py | 6 +++++ wedgen.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 wedgen.py diff --git a/defines.py b/defines.py index a8fc5af..f2a756c 100644 --- a/defines.py +++ b/defines.py @@ -50,7 +50,7 @@ mstr_show_log = True # We will simply things to achieve good-looking results. # You can, however, disable the shadow rendering layer here. mstr_shadow_enabled = True -mstr_shadow_strength = 0.65 +mstr_shadow_strength = 0.85 mstr_shadow_shift = 24 # The tags that cast shadows mstr_shadow_casters = [ @@ -169,15 +169,15 @@ mstr_mask_blur = [ ("landuse", "greenfield", 30), ("landuse", "orchard", 30), ("landuse", "meadow", 30), - ("barrier", "hedge", 12), - ("landuse", "recreation_ground", 30), + ("barrier", "hedge", 5), + ("landuse", "recreation_ground", 20), ("landuse", "vineyard", 30), ("natural", "grassland", 30), ("natural", "wetland", 30), - ("natural", "scrub", 30), - ("natural", "heath", 30), + ("natural", "scrub", 20), + ("natural", "heath", 20), ("leisure", "park", 30), - ("leisure", "golf_course", 35), + ("leisure", "golf_course", 25), ("leisure", "dog_park", 35), ("leisure", "garden", 20), ("leisure", "sports_centre", 5), @@ -190,15 +190,15 @@ mstr_mask_blur = [ ("landuse", "military", 30), # Z-Order 3 ("natural", "bare_rock", 25), - ("natural", "water", 20), + ("natural", "water", 4), ("natural", "bay", 30), ("natural", "beach", 30), - ("water", "lake", 20), - ("water", "pond", 20), - ("water", "river", 20), + ("water", "lake", 10), + ("water", "pond", 10), + ("water", "river", 10), ("waterway", "river", 10), ("waterway", "stream", 10), - ("amenity", "parking", 10), + ("amenity", "parking", 3), ("highway", "pedestrian", 12), # Z-Order 4 ("highway", "motorway", 5), diff --git a/layergen.py b/layergen.py index 664514a..d3e157e 100644 --- a/layergen.py +++ b/layergen.py @@ -68,7 +68,7 @@ class mstr_layergen: src = -1 if len(brd) == 1: src=1 if len(brd) >= 2: - src = randrange(1, len(brd)) + src = randrange(1, len(brd)+1) ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png") # Load in the sources to work with @@ -84,7 +84,7 @@ class mstr_layergen: imgid = 0 if len(ptc_src) == 1: imgid = 0 if len(ptc_src) >= 2: - imgid = randrange(1, len(ptc_src)) - 1 + imgid = randrange(1, len(ptc_src)+1) - 1 l = 0 - int(ptc_src[imgid].width / 2) r = layer.width - int(ptc_src[imgid].width / 2) t = 0 - int(ptc_src[imgid].height / 2) @@ -294,7 +294,7 @@ class mstr_layergen: if src == -1: if len(brd) == 1: src=1 if len(brd) >= 2: - src = randrange(1, len(brd)) + src = randrange(1, len(brd)+1) ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png") @@ -359,7 +359,7 @@ class mstr_layergen: imgid = 0 if len(ptc_src) == 1: imgid = 0 if len(ptc_src) >= 2: - imgid = randrange(1, len(ptc_src)) - 1 + imgid = randrange(1, len(ptc_src)+1) - 1 l = 0 - int(ptc_src[imgid].width / 2) r = layer.width - int(ptc_src[imgid].width / 2) t = 0 - int(ptc_src[imgid].height / 2) @@ -371,7 +371,7 @@ class mstr_layergen: # Here we need to do some magic to make some features look more natural if (self._tag == "landuse" and self._value == "meadow") or (self._tag == "natural" and self._value == "grassland") or (self._tag == "natural" and self._value == "heath"): amt = randrange(1,5) - for i in range(1, amt): + for i in range(1, amt+1): ptc = randrange(1, 14) img = Image.open(mstr_datafolder + "Textures/tile/completion/p" + str(ptc)+".png") lx = randrange( int(layer.width/20), layer.width - (int(layer.width/20)) - img.width ) @@ -409,6 +409,12 @@ class mstr_layergen: layer_border = self.genborder(osm_edge, "landuse", "meadow") layer_comp.alpha_composite(layer_border) + # Edges for waters + if self._tag == "natural" and self._value == "water": + osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) + osm_edge = osm_edge.filter(ImageFilter.MaxFilter) + osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=2)) + layer_comp.alpha_composite(osm_edge) # Store layer if self._is_completion == False: @@ -612,6 +618,10 @@ class mstr_layergen: # Find a color range d = randrange(1,21) + # Bring in some variety by making the one or other pixel darker + dp = randrange(1, 3) + if dp == 2: + d = d + 20 # Adjust this pixel c = (bld_clr[cidx][1]-d, bld_clr[cidx][2]-d, bld_clr[cidx][3]-d, 255) # Set pixel @@ -641,7 +651,7 @@ class mstr_layergen: shf_x2 = x-randrange(1, 21) shf_y2 = y-randrange(1, 21) if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_y <= self._imgsize-1 and shf_y >= 0: - st = random.uniform(0.85, 1.0) + st = random.uniform(0.65, 0.85) ca = 255 * st aa = int(ca) d = randrange(1,26) @@ -653,7 +663,8 @@ class mstr_layergen: layer_comp = details # New edge osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) - osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=1)) + osm_edge = osm_edge.filter(ImageFilter.MaxFilter) + osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=2)) # Blur the image layer_comp = layer_comp.filter(ImageFilter.GaussianBlur(radius=1)) osm_edge.alpha_composite(layer_comp) @@ -678,7 +689,7 @@ class mstr_layergen: if shf_x > 0 and shf_x < self._imgsize and shf_y > 0 and shf_y < self._imgsize: # Pick a number of trees to place numtrees = randrange(1, 16) - for i in range(1, numtrees): + for i in range(1, numtrees+1): # Pick some file pick = str(randrange(1, 11)) tree = Image.open(mstr_datafolder + "Textures/building/area/p" + pick + ".png") @@ -704,11 +715,11 @@ class mstr_layergen: for y in range(self._imgsize-1): for x in range(self._imgsize-1): m = mask_pix[x,y] - shf_x = x + randrange(1, mstr_shadow_shift) - shf_x2 = x + randrange(1, mstr_shadow_shift) + shf_x = x + randrange(1, mstr_shadow_shift + 1) + shf_x2 = x + randrange(1, mstr_shadow_shift + 1) if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_x2 <= self._imgsize-1 and shf_x2 >= 0: a = mask_pix[x,y][3] - st = random.uniform(0.45, mstr_shadow_strength) + st = random.uniform(0.6, mstr_shadow_strength) ca = a * st aa = int(ca) shadow_pix[shf_x, y] = (0,0,0,aa) diff --git a/maskgen.py b/maskgen.py index 52a992b..4a0d0b4 100644 --- a/maskgen.py +++ b/maskgen.py @@ -27,6 +27,7 @@ from defines import * from log import * from PIL import Image, ImageFilter, ImageDraw, ImagePath from random import randrange +from tiledb import * import random class mstr_maskgen: @@ -42,6 +43,9 @@ class mstr_maskgen: self._vstep = vstep self._scale = 1 / math.cos(math.radians(self._box[0])) self._isline = isline + self._tiledb = mstr_tiledb(self._box[0], self._box[2]) + if xml != None: + self._xml = xml #mstr_msg("maskgen", "Intialized mask gen.") @@ -135,6 +139,10 @@ class mstr_maskgen: if len(latlng) == 2: # For some reason, sometimes the array is empty. Make sure we have two data points. if len(latlng) == 2: + # Insert a WED data point should this be a forest: + if self._tag == "landuse" and self._value == "forest": + self._tiledb.insert_wed_datapoint(1, 1, latlng[0], latlng[1]) + # Project the pixel, and add to the polygon shape. p_lat = self.project_pixel(latlng[0], bbox[1]) p_lng = self.project_pixel(latlng[1], bbox[3]) @@ -183,4 +191,7 @@ class mstr_maskgen: # Inform mstr_msg("maskgen", "Mask built.") + # Close the DB + self._tiledb.close_db() + diff --git a/orthographic.py b/orthographic.py index 34a331c..a860a01 100644 --- a/orthographic.py +++ b/orthographic.py @@ -98,12 +98,6 @@ class mstr_orthographic: os.makedirs(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long)) mstr_msg("orthographic", "Created Tiles sub folder: " +str(self._lat)+"_"+str(self._long)) - # Note down diameter of entire tile - #tile_dm = round(self._findWidthOfLongitude(), 3) - #mstr_msg("orthographic", "Tile diameter: " + str(tile_dm) + "m") - #dm_of_18 = round(tile_dm * mstr_zl_18, 3) - #mstr_msg("orthographic", "Diameter of ZL 18 tile: " + str(dm_of_18) + "m") - # The tile is constructed of many smaller parts. We walk through the # smallest possible, from which the bigger ones are later built. bb_lat = self._lat @@ -181,7 +175,7 @@ class mstr_orthographic: mstr_msg("orthographic", "Processing layer " + str(curlyr) + " of " + str(len(layers))) # Generate the mask - mg = mstr_maskgen( [self._lat, cur_tile_y, self._long, cur_tile_x], self._vstep, layer[0], layer[1], layer[2] ) + mg = mstr_maskgen( [self._lat, cur_tile_y, self._long, cur_tile_x], self._vstep, layer[0], layer[1], layer[2]) mg._build_mask() # Generate the layer diff --git a/osmxml.py b/osmxml.py index 2a43a39..faf529a 100644 --- a/osmxml.py +++ b/osmxml.py @@ -37,7 +37,7 @@ class mstr_osmxml: # Acquire XMLs in chunks, then store them - def acquire_osm(self, v, h): + def acquire_osm(self, v, h, asobject=False): mstr_msg("osmxml", "Acquiring OSM data for " + str(self._lat)+","+str(self._lng)+" - "+str(self._curB_lat)+","+str(self._curB_lng)) # We will use our self-hosted API for this. @@ -55,12 +55,17 @@ class mstr_osmxml: } r = requests.post(mstr_osm_endpoint, json=data) - xml = mstr_datafolder + "_cache/tile.xml" - if os.path.isfile(xml): - os.remove(xml) - with open(xml, 'wb') as textfile: + xmlf = mstr_datafolder + "_cache/tile.xml" + if os.path.isfile(xmlf): + os.remove(xmlf) + with open(xmlf, 'wb') as textfile: textfile.write(r.content) + # Provide the object directly + if asobject == True: + xml_doc = xml.dom.minidom.parse("_cache/tile.xml") + return xml_doc + # Get all nodes from the specified OSM file def acquire_nodes(self, xmlfile): @@ -108,7 +113,7 @@ class mstr_osmxml: icao.append(v) # Return list of found airports return icao - + # Finds the surface type of a runway in the current data chunk. # If no surface type is specified, the runway will be rendered similar @@ -139,7 +144,7 @@ class mstr_osmxml: # Return the found surface type return surface - + # It turns out that some features hide themselves in the relations section. # I figured this out during testing, and almost going insane over the @@ -163,4 +168,5 @@ class mstr_osmxml: tgd.append(i.getAttribute("k")) tgd.append(i.getAttribute("v")) rls.append((rp.getAttribute("ref"), tgd)) - return rls \ No newline at end of file + return rls + \ No newline at end of file diff --git a/repoinfo b/repoinfo index e50ef2f..4b37800 100644 --- a/repoinfo +++ b/repoinfo @@ -40,6 +40,8 @@ Orthographic does its very best to approximate good-looking ortho photos, as if It is, at the end of the day, for a flight simulator. Orthographic can strike the right balance between accuracy of the part of the world you fly in, and looks. +Prior to publishing, there have been many iterations and hours of testing the generation of buildings. While the current state is far from perfect, it is as best I can make it - and I think we can agree that it looks good enough... for flight simulators. With the right tools in place, buildings will most likely be placed on many of these locations anyways. + [section]Where it excels[/section] @@ -67,12 +69,15 @@ Apart from that I am aware that the code is most likely not the best and can be - Current Python version - Pillow for Python +- SQLite must be available to Python. On Windows it is by default, you may need to install the sqlite3 developer libraries on your distribution, then compile Python to automatically avail of this built-in module. + [section]Configuration[/section] - Open the file defines.py - Change mstr_datafolder to where you want the data of Orthographic to be stored and placed - Change mstr_airport_radius to an amount in zoom level 18 tiles. Areas around airports with ICAO code will get aerials with zoom level 18. I recommend to leave the default at 5 +- You can turn console logging on or off by changing the variable mstr_show_log In this file you can also define how large you want the final products to be - you can choose between 4k resolution (2048px) or 16k resolution (4096). The choice you make here should be based on 1) how much detail you want, and 2) how much VRAM your GPU has. The larger a texture, the more VRAM is needed, and the more processing is required by the GPU. I personally go with 2048, as I have a RTX2060, so my VRAM is limited. When at altitudes of 10,000 feet and above, you will most definitely not notice the difference between 4096 and 2048. @@ -82,6 +87,8 @@ Change the mstr_photores variable to 4096 if you want the maximum possible. Just a note: 4096 also uses 4x more hard drive space. 4k uses about 2MB per image, where as 16k uses about 8-10MB per image. This may not seem much for one image, but keep in mind we are talking about quite a considerable number of images. To get an idea - if you have it, look into any folder of an Ortho4XP tile in your X-Plane folder, and check the size of the "textures" folder. +Also in defines.py, you will find the single layers, along with their corresponding mask blurring values. It is my strong recommendation NOT to change these values, as I have taken a lot of time to fine-tune these values. + [section]Running![/section] diff --git a/tiledb.py b/tiledb.py index 805c3cd..c933b90 100644 --- a/tiledb.py +++ b/tiledb.py @@ -40,6 +40,7 @@ class mstr_tiledb: self._conn.execute("CREATE TABLE IF NOT EXISTS tiledata (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);") self._conn.execute("CREATE TABLE IF NOT EXISTS airports (icao TEXT, tile_v INTEGER, tile_h INTEGER, latitude REAL, longitude REAL);") self._conn.execute("CREATE TABLE IF NOT EXISTS completion (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);") + self._conn.execute("CREATE TABLE IF NOT EXISTS weddata (datatype INTEGER, datagroup INTEGER, latitude REAL, longitude REAL);") #mstr_msg("tiledb", "Tables created") @@ -55,6 +56,11 @@ class mstr_tiledb: def insert_icao(self, icao, tv, th, lat, lng): self._conn.execute("INSERT INTO airports VALUES ('"+icao+"', "+str(tv)+", "+str(th)+", "+str(lat)+", "+str(lng)+");") + # Inserts a data point for WED generation + def insert_wed_datapoint(self, dtype, dgroup, lat, lng): + self._conn.execute("INSERT INTO weddata VALUES( "+str(dtype)+","+str(dgroup)+","+str(lat)+","+str(lng)+" );") + self.commit_query() + # Commit a query or a number of queries def commit_query(self): diff --git a/wedgen.py b/wedgen.py new file mode 100644 index 0000000..d22aeae --- /dev/null +++ b/wedgen.py @@ -0,0 +1,61 @@ + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# wedgen.py +# Generates the XML for X-Plane's WED, which you need to use the +# orthos in the flight simulator. +# ------------------------------------------------------------------- + +import xml.dom.minidom +from defines import * +from log import * +from tiledb import * + + +class mstr_wedgen: + def __init__(self, lat, lng): + self._lat = lat + self._lng = lng + self._curID = 2 + self._tiledb = mstr_tiledb(lat, lng) + + + # Creates the XML file for WED in one go + def create_WED_XML(self): + + # Generates a very basic skeleton for the World Editor + root = xml.dom.minidom.Document() + xmlobj = root.createElement("doc") + root.appendChild(xmlobj) + + objs = root.createElement("objects") + xmlobj.appendChild(objs) + + rtobj = root.createElement("object") + rtobj.setAttribute("class", "WED_Root") + rtobj.setAttribute("id", "1") + rtobj.setAttribute("parent_id", "0") + + chdobj = root.createElement("children") + rtobj.appendChild(chdobj) + + hiera = root.createElement("hierarchy") + hiera.setAttribute("name", "root") + rtobj.appendChild(hiera) + + # We now open the DB and check for everything that needs to be added, + # predominantely forests + + + # I believe this needs to be in + prefs = root.createElement("prefs") + xmlobj.appendChild(prefs) + + file_handle = open(mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/earth.wed.xml","w") + root.writexml(file_handle) + file_handle.close() \ No newline at end of file -- 2.30.2