From 747dfbdf9183bb7c97fca40943f3842e59df8a94 Mon Sep 17 00:00:00 2001 From: "Marcus Str." Date: Tue, 24 Sep 2024 23:20:36 +0200 Subject: [PATCH] Possible initial public release. After extensive testing, code amended to incorporate Laminar Research tools to build usable meshes for X-Plane, along with the correct files needed for the scenery. Normal maps also generating correctly. New class xp_scenery which performs generation of the mesh. Structure of files unified so that multiple tiles can be held in one single folder. Dependency for wand removed. --- defines.py | 42 +++++----- layergen.py | 29 +++++-- og.py | 13 +-- orthographic.py | 78 ++++++++++++------ photogen.py | 32 ++++++-- tiledb.py | 2 +- xp_dsfgen.py | 211 ------------------------------------------------ xp_normalmap.py | 37 ++++----- xp_scenery.py | 182 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 330 insertions(+), 296 deletions(-) delete mode 100644 xp_dsfgen.py create mode 100644 xp_scenery.py diff --git a/defines.py b/defines.py index 824cd01..cbf8db3 100644 --- a/defines.py +++ b/defines.py @@ -72,27 +72,31 @@ mstr_shadow_casters = [ # Whether or not to generate X-Plane Scenery files mstr_xp_genscenery = True -# X-Plane specific -mstr_xp_dsftool = "M:/Developer/Projects/orthographic/bin/DSFTool.exe" -mstr_xp_folder = "M:/Flight Sim/Simulator/11/" -# Whether or not you want normal maps for certain geographical elements -# to make them appear more realistic. -mstr_xp_generate_normal_maps = True +# Generate normal maps for X-Plane scenery? +# Strong recommendation: yes +mstr_xp_scn_normalmaps = True -# If you set the above to true, you can -# 1) define for which features to generate normal maps for, and -# 2) the specularity for each feature. -# A specularity value of 100 is fully specular, useful for water -# A specularity value of 10 is more useful for inland features. -# Sand may also reflect some of its surface, so possibly 25 is good there. +# Paths to required X-Plane scenery tools +mstr_xp_meshtool = "/home/marcus/Developer/Projects/orthographic/bin/MeshTool" +mstr_xp_ddstool = "/home/marcus/Developer/Projects/orthographic/bin/DDSTool" +mstr_xp_xessrc = "https://dev.x-plane.com/update/misc/MeshTool/" + +# If you set the above to true, you can define for which features you +# want to generate normal maps for. The below is my recommendation for +# good-looking orthos in the simulator. mstr_xp_normal_maps = [ - ("landuse", "farmland", 10), - ("landuse", "forest", 10), - ("leisure", "nature_reserve", 10), - ("natural", "water", 100), - ("water", "pond", 100), - ("water", "river", 100), - ("water", "lake", 100) + ("landuse", "farmland"), + ("landuse", "meadow"), + ("landuse", "orchard"), + ("landuse", "forest"), + ("natural", "wetland"), + ("natural", "bare_rock"), + ("natural", "scrub"), + ("natural", "heath"), + ("natural", "sand"), + ("natural", "desert"), + ("leisure", "nature_reserve"), + ("building", "*") ] # How much of a tile we need for each zoom level. The higher diff --git a/layergen.py b/layergen.py index 9b47552..e04bbce 100644 --- a/layergen.py +++ b/layergen.py @@ -430,10 +430,10 @@ class mstr_layergen: # Depending on if scenery for XP should be made, AND if normal maps should be made, we would # need to make them at this exact point if mstr_xp_genscenery == True: - if mstr_xp_generate_normal_maps == True: + if mstr_xp_scn_normalmaps == True: nm = False for n in mstr_xp_normal_maps: - if n[0] == self._tag and n[1] == self._value: + if n[0] == self._tag and (n[1] == self._value or n[1] == "*"): nm = True break if nm == True: @@ -533,8 +533,27 @@ class mstr_layergen: self._tiledb.commit_query() self._tiledb.close_db() + + # Create a water mask we need to remove from the DDS later + if (self._tag == "natural" and self._value == "water") or (self._tag == "water" and self._value == "lake") or (self._tag == "water" and self._value == "pond") or (self._tag == "water" and self._value == "river"): + mstr_msg("layergen", "Generating inland water mask") + inl_mask = Image.new("RGBA", (self._imgsize, self._imgsize), (255,255,255,255)) + lyr_pix = layer_comp.load() + inl_pix = inl_mask.load() + for y in range(self._imgsize): + for x in range(self._imgsize): + l = lyr_pix[x,y] + if l[3] > 0: + b = 255 - l[3] + inl_pix[x,y] = (b,b,b,255) + inl_mask.save(mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer_mask.png") + mstr_msg("layergen", "Inland water mask generated and saved") + # --------------------------------------------------------------------------------------- + # --------------------------------------------------------------------------------------- + # --------------------------------------------------------------------------------------- + # If we encounter one of these road-specific tags, we need to proceed differently. if self._isline == True or self._tag == "building": @@ -651,7 +670,7 @@ class mstr_layergen: layer_comp_pix[x, y] = ( r,g,b,a[3] ) # We will do some super magic here to let houses look more realistic - if self._tag == "building": + if self._tag == "building" or self._value == "cemetery": vls = [ "detached", "hotel", "farm", "semidetached_house", "apartments", "civic", "office", "retail", "industrial", "house", "school", "yes" ] if self._value in vls: # Generate a new image @@ -769,10 +788,10 @@ class mstr_layergen: # Depending on if scenery for XP should be made, AND if normal maps should be made, we would # need to make them at this exact point if mstr_xp_genscenery == True: - if mstr_xp_generate_normal_maps == True: + if mstr_xp_scn_normalmaps == True: nm = False for n in mstr_xp_normal_maps: - if n[0] == self._tag and n[1] == self._value: + if n[0] == self._tag and (n[1] == self._value or n[1] == "*"): nm = True break if nm == True: diff --git a/og.py b/og.py index a115f01..3050a04 100644 --- a/og.py +++ b/og.py @@ -1,10 +1,4 @@ -import sys -import os -from orthographic import * -from log import * -from defines import * - # ------------------------------------------------------------------- # ORTHOGRAPHIC # Your personal aerial satellite. Always on. At any altitude.* @@ -17,6 +11,13 @@ from defines import * # ------------------------------------------------------------------- +import sys +import os +from orthographic import * +from log import * +from defines import * + + # Print a welcome message print(" ") print(" ---------------------------------------------------------------- ") diff --git a/orthographic.py b/orthographic.py index 8dc2090..3080ca7 100644 --- a/orthographic.py +++ b/orthographic.py @@ -15,12 +15,11 @@ import os import glob from defines import * from log import * +from osmxml import * from maskgen import * from layergen import * from photogen import * -from osmxml import * -from tilegen import * -from xp_dsfgen import * +from xp_scenery import * # The main class which handles the rest @@ -91,30 +90,45 @@ class mstr_orthographic: os.makedirs(self._output + "/_cache") mstr_msg("orthographic", "Created _cache folder.") - # Generate the Tiles folder for the finished products - if not os.path.exists(self._output + "/Tiles"): - os.makedirs(self._output + "/Tiles") - mstr_msg("orthographic", "Created Tiles folder.") - # Generate the Tiles/lat-lng folder for the finished tile - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld): - os.makedirs(self._output + "/Tiles/z_orthographic_"+ self._latlngfld) - mstr_msg("orthographic", "Created Tiles sub folder: " + self._latlngfld) + if not os.path.exists(self._output + "/z_orthographic"): + os.makedirs(self._output + "/z_orthographic") + mstr_msg("orthographic", "Created z_orthographic folder") # Generate the orthos folder - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/orthos"): - os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld +"/orthos") + if not os.path.exists(self._output + "/z_orthographic/orthos"): + os.makedirs(self._output + "/z_orthographic/orthos") mstr_msg("orthographic", "Created tile orthos folder") + if not os.path.exists(self._output + "/z_orthographic/orthos" + self._latlngfld): + os.makedirs(self._output + "/z_orthographic/orthos/" + self._latlngfld) + # Generate the database folder + if not os.path.exists(self._output + "/z_orthographic/data"): + os.makedirs(self._output + "/z_orthographic/data") + mstr_msg("orthographic", "Created tile database folder") + if not os.path.exists(self._output + "/z_orthographic/data/" + self._latlngfld): + os.makedirs(self._output + "/z_orthographic/data/" + self._latlngfld) + + # X-Plane specific if mstr_xp_genscenery == True: - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/terrain"): - os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/terrain") - mstr_msg("orthographic", "Created X-Plane tile terrain folder") - - if mstr_xp_generate_normal_maps == True: - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/normals"): - os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/normals") - mstr_msg("orthographic", "Created X-Plane tile normals folder") + btnum = self.find_earthnavdata_number() + btstr = self.latlng_folder(btnum) + if not os.path.exists(self._output + "/z_orthographic/terrain"): + os.makedirs(self._output + "/z_orthographic/terrain") + mstr_msg("orthographic", "[X-Plane] Created terrain files folder") + if not os.path.exists(self._output + "/z_orthographic/terrain/" + self._latlngfld): + os.makedirs(self._output + "/z_orthographic/terrain/" + self._latlngfld) + if not os.path.exists(self._output + "/z_orthographic/Earth nav data"): + os.makedirs(self._output + "/z_orthographic/Earth nav data") + mstr_msg("orthographic", "[X-Plane] Created Earth nav folder") + if not os.path.exists(self._output + "/z_orthographic/Earth nav data/" + btstr): + os.makedirs(self._output + "/z_orthographic/Earth nav data/" + btstr) + if mstr_xp_scn_normalmaps == True: + if not os.path.exists(self._output + "/z_orthographic/normals"): + os.makedirs(self._output + "/z_orthographic/normals") + mstr_msg("orthographic", "[X-Plane] created tile normal maps folder") + if not os.path.exists(self._output + "/z_orthographic/normals/" + self._latlngfld): + os.makedirs(self._output + "/z_orthographic/normals/" + self._latlngfld) # The tile is constructed of many smaller parts. We walk through the # smallest possible, from which the bigger ones are later built. @@ -162,7 +176,7 @@ class mstr_orthographic: mstr_msg("orthographic", "Adjusted bounding box for XML object") # Determine what to do... maybe work was interrupted - if os.path.isfile(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(cur_tile_y) + "_" + str(cur_tile_x) + ".dds") == False: + if os.path.isfile(mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(cur_tile_y) + "_" + str(cur_tile_x) + ".dds") == False: # Let the user know mstr_msg("orthographic", "Generating missing orthophoto " + str(cur_tile_y) + "-" + str(cur_tile_x)) @@ -248,12 +262,15 @@ class mstr_orthographic: # Complete scenery if mstr_xp_genscenery == True: - dsf = mstr_xp_dsfgen(self._lat, self._long, mlat, mlng, self._vstep) - dsf.build_dsf_for_tile() - mstr_msg("orthographic", "X-Plane scenery completed") + scn = mstr_xp_scenery(self._lat, self._long, mlat, mlng, self._vstep, self._latlngfld) + scn.acquire_elevation_data() + scn.acquire_xes_data() + scn.build_mesh_script() + scn.build_mesh() + mstr_msg("orthographic", "[X-Plane] Mesh built, and scenery completed") mstr_msg("orthographic", "Final step completed.") - mstr_msg("orthographic", "Tile data in: " + self._output + "/Tiles/z_orthographic_" + self._latlngfld) + mstr_msg("orthographic", "Tile data in: " + self._output + "z_orthographic/" + self._latlngfld) print("") mstr_msg("orthographic", "Thanks for using Orthographic! -- Best, Marcus") print("") @@ -335,3 +352,12 @@ class mstr_orthographic: return fstr + + # Find the next "by-ten" numbers for the current latitude and longitude + def find_earthnavdata_number(self): + earthnavdata = [] + lat = abs(int(self._lat / 10) * 10) + lng = abs(int(self._long / 10) * 10) + earthnavdata.append(lat) + earthnavdata.append(lng) + return earthnavdata \ No newline at end of file diff --git a/photogen.py b/photogen.py index 5703755..ed5e415 100644 --- a/photogen.py +++ b/photogen.py @@ -4,7 +4,6 @@ from PIL import Image, ImageFilter from defines import * from layergen import * from log import * -from wand import image # ------------------------------------------------------------------- # ORTHOGRAPHIC @@ -111,6 +110,25 @@ class mstr_photogen: if p[0] == 255 and p[1] == 0 and p[2] == 255: t = (0,0,0,0) ocean_pix[x,y] = t + + # Now cut out inland water + water_layers = ( + ["natural", "water"], + ["water", "lake"], + ["water", "pond"], + ["water", "river"] + ) + for l in water_layers: + fn = mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_" + l[0] + "-" + l[1] + "_layer_mask.png" + if os.path.isfile(fn) == True: + wtr = Image.open(fn) + wtr_pix = wtr.load() + tilepix = self._tile.load() + for y in range(wtr.height): + for x in range(wtr.width): + wp = wtr_pix[x,y] + if wp[0] == 0: + tilepix[x,y] = (0,0,0,0) # We are now in posession of the final image. @@ -118,15 +136,13 @@ class mstr_photogen: self._tile = self._tile.resize((mstr_photores, mstr_photores), Image.Resampling.BILINEAR) # This we can save accordingly. - self._tile.convert('RGB').save(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") - + self._tile.save(mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) + ".png") # Now we convert this into a DDS - with image.Image(filename=mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") as img: - img.compression = "dxt1" - img.save(filename=mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".dds") - # TODO: CUT OUT OCEANS AND SEAS IN DDS - os.remove(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") + _tmpfn = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) + os.system(mstr_xp_ddstool + " --png2dxt1 " + _tmpfn + ".png " + _tmpfn + ".dds" ) + + os.remove(mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) + ".png") diff --git a/tiledb.py b/tiledb.py index 69e24d9..c43185c 100644 --- a/tiledb.py +++ b/tiledb.py @@ -25,7 +25,7 @@ class mstr_tiledb: self._latlngfld = latlngfld # The db file will be created, should it not exist - self._conn = sqlite3.connect(mstr_datafolder + "Tiles/z_orthographic_" + latlngfld + "/data.db") + self._conn = sqlite3.connect(mstr_datafolder + "z_orthographic/data/" + latlngfld + "/data.db") self._crs = self._conn.cursor() #mstr_msg("tiledb", "Database object initiated") diff --git a/xp_dsfgen.py b/xp_dsfgen.py deleted file mode 100644 index b05483f..0000000 --- a/xp_dsfgen.py +++ /dev/null @@ -1,211 +0,0 @@ - -# ------------------------------------------------------------------- -# 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 -# ------------------------------------------------------------------- -# xp_dsfgen.py -# This class is coming into play at the very end of the tile -# generation process, and builds the DSF (Distributable Scenery -# Format) file for X-Plane. -# -# For this, you will need DSFTool which I cannot re-distribute. -# -# You can download it for free from X-Plane's website. -# Place them somewhere convenient, and point to them in the -# xp_ variables in defines.py -# ------------------------------------------------------------------- - -import os -import glob -import math -from random import randrange -from log import * - -class mstr_xp_dsfgen: - # Instantiate with Lat/Lng, as usual - def __init__(self, lat, lng, mlat, mlng, vstep): - self._latitude = lat - self._longitude = lng - self._maxlat = mlat - self._maxlng = mlng - self._tmpdsf = mstr_datafolder + "_cache/tiledsf.txt" - self._vstep = vstep - self._dsfstring = "" - mstr_msg("xp_dsfgen", "[X-Plane] DSFgen initialized") - self.build_header() - - - # Construct header of DSF txt - def build_header(self): - self._dsfstring = self._dsfstring + "PROPERTY sim/west " + str(int(self._longitude)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/east " + str(int(self._longitude + 1)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/south " + str(int(self._latitude)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/north " + str(int(self._latitude+1)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/planet earth\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/creation_agent Orthographic\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/overlay 1\n" # <- DO NOT REMOVE THIS!! - self._dsfstring = self._dsfstring + "PROPERTY sim/require_facade 6/0\n" - mstr_msg("xp_dsfgen", "[X-Plane] DSF header built") - - - # Write the text file - def write_dsf_txt(self): - mstr_msg("xp_dsfgen", "[X-Plane] Writing DSF txt file") - with open(self._tmpdsf, 'w') as textfile: - textfile.write(self._dsfstring) - - - # Convert the DSF into actual, usable data for X-Plane - def convert_dsf_text(self): - mstr_msg("xp_dsfgen", "[X-Plane] Converting DSF information into X-Plane DSF file") - # Find separator - sep = "" - if os.name == "nt": - sep = "\\" - if os.name == "posix": - sep = "/" - - datafolder = mstr_datafolder.replace("/", sep) - - # First, create the Earth nav data folder should it not exist - end_base = datafolder + "Tiles/z_orthographic_" + self.xplane_latlng_folder([self._latitude, self._longitude]) + sep + "Earth nav data" - - # Create the appropriate rounded folder - end_round = self.xplane_latlng_folder(self.find_earthnavdata_number()) - if not os.path.exists(end_base): - os.makedirs(end_base) - if not os.path.exists(end_base + sep + end_round): - os.makedirs(end_base + sep + end_round) - - # Get the file name for the DSF - end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude]) - - # Perform conversion - os.system(mstr_xp_dsftool + " --text2dsf " + datafolder + "_cache" + sep + "tiledsf.txt \"" + end_base + sep + end_round + sep + end_latlng + ".dsf\"") - mstr_msg("xp_dsfgen", "[X-Plane] DSF conversion complete") - - - # Find the next "by-ten" numbers for the current latitude and longitude - def find_earthnavdata_number(self): - earthnavdata = [] - lat = abs(int(self._latitude / 10) * 10) - lng = abs(int(self._longitude / 10) * 10) - earthnavdata.append(lat) - earthnavdata.append(lng) - return earthnavdata - - # Find with of a longitude (needed for DSF and .pol files) - def _findWidthOfLongitude(self, lat): - dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km - return round(dm * 1000, 3) - - # Find diameter of an ortho - def find_ortho_diameter(self, side): - sq = side * side - sqsum = sq + sq - dm = math.sqrt(sqsum) - - - # Construct an X-Plane compatible folder name for latitude and longitude - def xplane_latlng_folder(self, numbers): - fstr = "" - if numbers[0] >= 0: fstr = "+" - if numbers[0] < 0: fstr = "-" - if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) - if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) - - if numbers[1] >= 0: fstr = fstr + "+" - if numbers[1] < 0: fstr = fstr + "-" - if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) - if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) - if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) - - return fstr - - - # Build the complete DSF. - # This is the main function to call. - def build_dsf_for_tile(self): - mstr_msg("xp_dsfgen", "[X-Plane] Building DSF file") - # Add the polygon definition file entries - for v in range(1, self._maxlat+1): - for h in range(1, self._maxlng+1): - self._dsfstring = self._dsfstring + "POLYGON_DEF terrain/"+str(v)+"_"+str(h)+".pol\n" - - # Add the definitions for each ortho tile - curpol = 0 - cur_lat = self._latitude - cur_lng = self._longitude - for v in range(1, self._maxlat+1): - for h in range(1, self._maxlng+1): - bbox = [ cur_lat, cur_lng, cur_lat+self._vstep-0.00001, cur_lng + mstr_zl_18-0.00001 ] - self._dsfstring = self._dsfstring + "BEGIN_POLYGON "+str(curpol)+" 65535 4\n" - self._dsfstring = self._dsfstring + "BEGIN_WINDING\n" - - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[1]) + " " + str(bbox[0]) + " 0.000000000 0.000000000\n" - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[3]) + " " + str(bbox[0]) + " 1.000000000 0.000000000\n" - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[3]) + " " + str(bbox[2]) + " 1.000000000 1.000000000\n" - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[1]) + " " + str(bbox[2]) + " 0.000000000 1.000000000\n" - - self._dsfstring = self._dsfstring + "END_WINDING\n" - self._dsfstring = self._dsfstring + "END_POLYGON\n" - - # Adjust forward - cur_lng = cur_lng + mstr_zl_18 - curpol = curpol + 1 - - # Adjust up, reset longitude - cur_lng = self._longitude - cur_lat = cur_lat + self._vstep - - # OK... we can now save this - self.write_dsf_txt() - - # Generate the single .pol files now - mstr_msg("xp_dsfgen", "[X-Plane] Beginning generation of terrain/*.pol files") - cur_lat = self._latitude - cur_lng = self._longitude - lclat = cur_lat + (self._vstep/2) - lclng = cur_lng + (mstr_zl_18/2) - for v in range(1, self._maxlat+1): - for h in range(1, self._maxlng+1): - dm = self._findWidthOfLongitude(cur_lng) * mstr_zl_18 - dg = self.find_ortho_diameter(dm) - polstr = "" - polstr = polstr + "A\n" - polstr = polstr + "850\n" - polstr = polstr + "DRAPED_POLYGON\n" - polstr = polstr + "\n" - polstr = polstr + "TEXTURE_NOWRAP ../orthos/"+str(v)+"_"+str(h)+".dds\n" - - # Check for existence of a normal map - # If there is one, we will add that too - end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude]) - if os.path.isfile(mstr_datafolder + "Tiles/z_orthographic_"+end_latlng+"/normals/" + str(v) + "_" + str(h) + ".png") == True: - polstr = polstr + "TEXTURE_NORMAL 1 ../normals/"+str(v)+"_"+str(h)+".png\n" - - polstr = polstr + "SCALE "+str(dm)+" "+str(dm)+"\n" - polstr = polstr + "LOAD_CENTER "+str(lclat)+" " +str(lclng)+" " +str(dg)+ " " +str(mstr_photores)+"\n" - polstr = polstr + "LAYER_GROUP TERRAIN 1\n" - - # Save this content - with open(mstr_datafolder + "Tiles/z_orthographic_"+end_latlng+"/terrain/"+str(v)+"_"+str(h)+".pol", 'w') as textfile: - textfile.write(polstr) - - # Adjust forward - cur_lng = cur_lng + mstr_zl_18 - lclng = cur_lng + (mstr_zl_18/2) - - cur_lng = self._longitude - lclng = cur_lng + (mstr_zl_18/2) - - cur_lat = cur_lat + self._vstep - lclat = cur_lat + (self._vstep/2) - - mstr_msg("xp_dsfgen", "[X-Plane] Converting DSF txt") - self.convert_dsf_text() - - mstr_msg("xp_dsfgen", "[X-Plane] DSF for tile completed") diff --git a/xp_normalmap.py b/xp_normalmap.py index d38ffb5..1394def 100644 --- a/xp_normalmap.py +++ b/xp_normalmap.py @@ -84,20 +84,12 @@ class mstr_xp_normalmap: # The Big Mac. Generate the normal map def generate_normal_map_for_layer(self, image): mstr_msg("xp_normalmap", "[X-Plane] Beginning normal map generation") - nmp = Image.new("RGBA", (image.width, image.height), (128,128,255,0)) # no specularity, but default color - #nmp = Image.new("RGBA", (image.width, image.height), (0,0,0,0)) + # No specularity, no reflectivity - but standard color + # Blue (reflectivity) and alpha (specularity) need to be 1 - but can be adjusted as needed + nmp = Image.new("RGBA", (image.width, image.height), (128,128,1,1)) org = image.load() nmp_pix = nmp.load() - # Find out which alpha value to use - alpha = 0 - for n in mstr_xp_normal_maps: - if n[0] == self._tag and n[1] == self._value: - v = n[2] / 100 - a = v * 255.0 - alpha = int(a) - - # Let's try some shenanigans w = image.width h = image.height @@ -132,16 +124,13 @@ class mstr_xp_normalmap: v = (dx, dy, dz) nrm = self.normalize_vector(v) - # Invert height for our Orthos - but only in some circumstances - # We want water to go "down" - if (self._tag != "natural" and self._value != "water") or (self._tag != "water" and self._value != "lake") or (self._tag != "water" and self._value != "pond") or (self._tag != "water" and self._value != "river"): - if nrm[1] > 0: - nrm[1] = 0 - (abs(nrm[1])) - else: - nrm[1] = abs(nrm[1]) + if nrm[1] > 0: + nrm[1] = 0 - (abs(nrm[1])) + else: + nrm[1] = abs(nrm[1]) # Set pixel - nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), int(self.map_component(nrm[2])), alpha) + nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), int(self.map_component(nrm[2])), 1) mstr_msg("xp_normalmap", "[X-Plane] Normal map generated") return nmp @@ -157,7 +146,7 @@ class mstr_xp_normalmap: nrm = self.generate_normal_map_for_layer(lyr) # Normal map final file name - nrmfln = mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/normals/" + str(self._tv) + "_" + str(self._th) + ".png" + nrmfln = mstr_datafolder + "z_orthographic/normals/" + self._latlngfld + "/" + str(self._tv) + "_" + str(self._th) + ".png" # Check for existence of normal map file ex = os.path.isfile(nrmfln) @@ -170,5 +159,13 @@ class mstr_xp_normalmap: if ex == True: nrmmap = Image.open(nrmfln) nrmmap.alpha_composite(nrm) + + # Specularity blending correction + nrmmap_pix = nrmmap.load() + for y in range(nrmmap.height): + for x in range(nrmmap.width): + c = nrmmap_pix[x,y] + nrmmap_pix[x,y] = (c[0], c[1], c[2], 1) nrmmap.save(nrmfln) + mstr_msg("xp_normalmap", "[X-Plane] Normal map saved") diff --git a/xp_scenery.py b/xp_scenery.py new file mode 100644 index 0000000..269b8aa --- /dev/null +++ b/xp_scenery.py @@ -0,0 +1,182 @@ + +# ------------------------------------------------------------------- +# 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 +# ------------------------------------------------------------------- +# xp_scenery.py +# This class builds an elevation mesh from provided DEM data, and +# generates all required files that are needed to provide a usable +# scenery package for X-Plane. +# ------------------------------------------------------------------- + +import os +import math +import urllib.request +from defines import * +from log import * + +class mstr_xp_scenery: + # Set required variables + def __init__(self, lat, lng, mlat, mlng, vstep, latlngfld): + mstr_msg("xp_scenery", "[X-Plane] Scenery generator instantiated") + self._lat = lat + self._lng = lng + self._mlat = mlat + self._mlng = mlng + self._vstep = vstep + self._latlngfld = latlngfld + self._demfn = self.build_dem_filename() + + + # Build the correct file name for the elevation model + def build_dem_filename(self, xes=False): + fn = "" + if self._lat > 0: + fn = fn + "N" + else: + fn = fn + "S" + + if abs(self._lat) < 10: fn = fn + "0" + str(self._lat) + if abs(self._lat) >= 10 and abs(self._lat) <= 90: fn = fn + str(self._lat) + + if self._lng > 0: + fn = fn + "E" + else: + fn = fn + "W" + + if abs(self._lng) < 10: fn = fn + "00" + str(self._lng) + if abs(self._lng) >= 10 and abs(self._lng) <= 99: fn = fn + "0" + str(self._lng) + if abs(self._lng) >= 100 : fn = fn + str(self._lng) + + if xes == False: + fn = fn + ".hgt" + if xes == True: + fn = fn + ".xes" + + mstr_msg("xp_scenery", "[X-Plane] DEM file name constructed: " + fn) + + return fn + + # Generate the mesh script for the ortho photos + def build_mesh_script(self): + scr = mstr_datafolder + "z_orthographic/data/meshscript.txt" + # Before we blast all these lines into the file, we need to make sure they do not exist already + write_lines = True + + if os.path.isfile(scr) == True: + fnlines = [] + with open(scr) as textfile: + fnlines = textfile.readlines() + + for line in fnlines: + l = line.split(" ") + if l[2] == str(self._lng) and l[3] == str(self._lat): + write_lines = False + break + else: + open(scr, 'a').close() + + # If we did not find the initial corner coordinate in the script, we can go ahead + if write_lines == True: + mstr_msg("xp_scenery", "[X-Plane] Writing mesh script file") + # We basically run through all tiles and note down the position of the orthos + # as needed by X-Plane. + cur_lat = self._lat + cur_lng = self._lng + for lat in range(1, self._mlat+1): + for lng in range(1, self._mlng+1): + # The '1' after 'ORTHOPHOTO' defines we want water underneath transparent parts of the DDS texture/ortho. + # This ensures that even if the mesh does not include information for there being a water body, + # we will get 100% correct representation of the water bodies. + scrtxt = "ORTHOPHOTO 1 " + str(cur_lng) + " " + str(cur_lat) + " " + str(round(cur_lng+mstr_zl_18, 6)) + " " + str(cur_lat) + " " + str(round(cur_lng+mstr_zl_18, 6)) + " " + str(round(cur_lat+self._vstep, 6)) + " " + str(cur_lng) + " " + str(round(cur_lat+self._vstep, 6)) + " terrain/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + ".ter\n" + + with open(scr, 'a') as textfile: + textfile.write(scrtxt) + + cur_lng = round(cur_lng + mstr_zl_18, 6) + + cur_lng = self._lng + cur_lat = round(cur_lat + self._vstep, 6) + mstr_msg("xp_scenery", "[X-Plane] Mesh script completed") + + + # Find the next "by-ten" numbers for the current latitude and longitude + def find_earthnavdata_number(self): + earthnavdata = [] + lat = abs(int(self._lat / 10) * 10) + lng = abs(int(self._lng / 10) * 10) + earthnavdata.append(lat) + earthnavdata.append(lng) + return earthnavdata + + + # Construct an X-Plane compatible folder name for latitude and longitude + def xplane_latlng_folder(self, numbers): + fstr = "" + if numbers[0] >= 0: fstr = "+" + if numbers[0] < 0: fstr = "-" + if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) + if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) + + if numbers[1] >= 0: fstr = fstr + "+" + if numbers[1] < 0: fstr = fstr + "-" + if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) + if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) + if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) + + return fstr + + + # Acquires the elevation data as needed - + # you can either acquire the older STRM set with lower resolution, + # or a modern LIDAR scan with higher resolution. However, the + # LIDAR files are much larger and generates a more taxing mesh. + # If you are testing, acquire the low resolution file. + # Both versions are coming from my repository at marstr.online . + def acquire_elevation_data(self): + mstr_msg("xp_scenery", "[X-Plane] Acquiring DEM model data") + url = "https://marstr.online/dem/" + + url = url + self._demfn + + urllib.request.urlretrieve(url, mstr_datafolder + "_cache/" + self._demfn) + mstr_msg("xp_scenery", "[X-Plane] DEM data acquired") + + + # Download the X-Plane definition file from my server + def acquire_xes_data(self): + mstr_msg("xp_scenery", "[X-Plane] Acquiring XES file") + url = "https://marstr.online/xes/" + xesfn = self.build_dem_filename(True) + + xp_folder = self.xplane_latlng_folder([self._lat, self._lng]) + url = url + xp_folder + ".xes" + + urllib.request.urlretrieve(url, mstr_datafolder + "_cache/" + xesfn) + mstr_msg("xp_scenery", "[X-Plane] XES data acquired") + + + # This builds the entire mesh in one go + def build_mesh(self): + end_bt = self.find_earthnavdata_number() + btlfn = str(self.xplane_latlng_folder(end_bt)) + xp_folder = self.xplane_latlng_folder([self._lat, self._lng]) + scr = mstr_datafolder + "z_orthographic/data/meshscript.txt" + wd = mstr_datafolder + "z_orthographic/data" + dsf = mstr_datafolder + "z_orthographic/Earth nav data/" + btlfn + "/" + xp_folder + xesfn = self.build_dem_filename(True) + + # The main command to build the mesh + cmd = mstr_xp_meshtool + " \"" + scr + "\" \"" + mstr_datafolder + "_cache/" + xesfn + "\"" + " \"" + mstr_datafolder + "_cache/" + self._demfn + "\" \"" + wd + "\" \"" + dsf + ".dsf\"" + + os.system(cmd) + + +# Individual testing +#scn = mstr_xp_scenery(51, 7, 101, 64, 0.010102, "+51+007") +#scn.acquire_xes_data() +#scn.build_mesh_script() +#scn.build_mesh() \ No newline at end of file -- 2.30.2