From: marstr Date: Mon, 9 Sep 2024 21:14:43 +0000 (+0200) Subject: X-Plane DSF file generator, X-Plane normal map generator. Normal maps can be turned... X-Git-Url: https://marstr.online/code/gitweb.cgi?a=commitdiff_plain;h=a5e850feec02d98a0871ef6aefae28244e208fa2;p=orthographic X-Plane DSF file generator, X-Plane normal map generator. Normal maps can be turned on or off in defines.py . Normal map generation implemented in layergen. Nearing initial public release. Added sand and desert source material. Changed layer order for some parts. --- diff --git a/Textures/building/common/brd/b5.png b/Textures/building/common/brd/b5.png new file mode 100644 index 0000000..51c5d0e Binary files /dev/null and b/Textures/building/common/brd/b5.png differ diff --git a/Textures/building/common/ptc/b5_p1.png b/Textures/building/common/ptc/b5_p1.png new file mode 100644 index 0000000..552ad4a Binary files /dev/null and b/Textures/building/common/ptc/b5_p1.png differ diff --git a/Textures/building/common/ptc/b5_p2.png b/Textures/building/common/ptc/b5_p2.png new file mode 100644 index 0000000..627ddac Binary files /dev/null and b/Textures/building/common/ptc/b5_p2.png differ diff --git a/Textures/natural/desert/brd/b1.png b/Textures/natural/desert/brd/b1.png new file mode 100644 index 0000000..f6a39a3 Binary files /dev/null and b/Textures/natural/desert/brd/b1.png differ diff --git a/Textures/natural/desert/ptc/b1_p1.png b/Textures/natural/desert/ptc/b1_p1.png new file mode 100644 index 0000000..bc6f47e Binary files /dev/null and b/Textures/natural/desert/ptc/b1_p1.png differ diff --git a/Textures/natural/desert/ptc/b1_p2.png b/Textures/natural/desert/ptc/b1_p2.png new file mode 100644 index 0000000..38d69c3 Binary files /dev/null and b/Textures/natural/desert/ptc/b1_p2.png differ diff --git a/Textures/natural/sand/brd/b1.png b/Textures/natural/sand/brd/b1.png new file mode 100644 index 0000000..bea8f7d Binary files /dev/null and b/Textures/natural/sand/brd/b1.png differ diff --git a/Textures/natural/sand/ptc/b1_p1.png b/Textures/natural/sand/ptc/b1_p1.png new file mode 100644 index 0000000..d5aff49 Binary files /dev/null and b/Textures/natural/sand/ptc/b1_p1.png differ diff --git a/Textures/natural/sand/ptc/b1_p2.png b/Textures/natural/sand/ptc/b1_p2.png new file mode 100644 index 0000000..2ac6764 Binary files /dev/null and b/Textures/natural/sand/ptc/b1_p2.png differ diff --git a/Textures/natural/water/normal_template.png b/Textures/natural/water/normal_template.png new file mode 100644 index 0000000..e65cf6b Binary files /dev/null and b/Textures/natural/water/normal_template.png differ diff --git a/defines.py b/defines.py index fc77d12..a746dac 100644 --- a/defines.py +++ b/defines.py @@ -71,8 +71,26 @@ mstr_xp_genscenery = True # X-Plane specific mstr_xp_dsftool = "M:/Developer/Projects/orthographic/bin/DSFTool.exe" -mstr_xp_ddstool = "M:/Developer/Projects/orthographic/bin/DDSTool.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 + +# 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. +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) +] # How much of a tile we need for each zoom level. The higher # the zoom level, the smaller the area to generate a mask of - but also @@ -108,9 +126,6 @@ mstr_ortho_layers = [ ("highway", "path", 3), ("natural", "bare_rock", "natural", "bare_rock"), ("natural", "grassland", "landuse", "meadow"), - ("natural", "wetland", "natural", "wetland"), - ("natural", "scrub", "natural", "scrub"), - ("natural", "heath", "natural", "heath"), ("leisure", "park", "leisure", "green"), ("leisure", "dog_park", "leisure", "green"), ("leisure", "garden", "leisure", "green"), @@ -132,6 +147,9 @@ mstr_ortho_layers = [ ("waterway", "stream", 10), ("leisure", "nature_reserve", "landuse", "forest"), ("landuse", "forest", "landuse", "forest"), + ("natural", "wetland", "natural", "wetland"), + ("natural", "scrub", "natural", "scrub"), + ("natural", "heath", "natural", "heath"), # Z-Order 3 ("natural", "water", "natural", "water"), ("natural", "bay", "natural", "beach"), diff --git a/layergen.py b/layergen.py index fdd9932..fd334af 100644 --- a/layergen.py +++ b/layergen.py @@ -22,6 +22,7 @@ from log import * from tiledb import * from osmxml import * from functions import * +from xp_normalmap import * class mstr_layergen: @@ -53,6 +54,10 @@ class mstr_layergen: self._maxlng = maxlatlng[1] mstr_msg("layergen", "Maximum latitude and longitude tile numbers received") + # Set latlng folder + def set_latlng_folder(self, latlngfld): + self._latlngfld = latlngfld + # This generates a "border" image, for example farmland usually has a small space of grass # before the actual crop of farm field itself. This generates this "border" layer, # and returns it. @@ -749,3 +754,11 @@ class mstr_layergen: layer_comp.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer.png" ) mstr_msg("layergen", "Layer image finalized and saved.") + + # 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: + nrm = mstr_xp_normalmap(self._latitude, self._longitude, self._tag, self._value, self._lat_number, self._lng_number, self._latlngfld) + nrm.build_normalmap() + diff --git a/orthographic.py b/orthographic.py index 4beb2bb..09024db 100644 --- a/orthographic.py +++ b/orthographic.py @@ -25,6 +25,17 @@ from tilegen import * # The main class which handles the rest class mstr_orthographic: + # Constructor of class. Takes longitude and latitude. + def __init__(self, lat, lng, outfolder, pwd): + self._lat = lat + self._long = lng + self._output = outfolder + self._pwd = pwd + self._vstep = self._findVerticalStepping() + self._latlngfld = self.latlng_folder([lat,lng]) + mstr_msg("orthographic", "Initiated with LAT: " + str(lat) + ", LNG: " + str(lng)) + + # It did happen that the generation of photos crashed as, for some reason, # a file in _cache was apparently used by another process (hint: it was # not). I therefore need this test before deleting a file in _cache, so @@ -94,9 +105,24 @@ class mstr_orthographic: mstr_msg("orthographic", "Created Tiles folder.") # Generate the Tiles/lat-lng folder for the finished tile - if not os.path.exists(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long)): - os.makedirs(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long)) - mstr_msg("orthographic", "Created Tiles sub folder: " +str(self._lat)+"_"+str(self._long)) + 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) + + # Generate the orthos folder + if not os.path.exists(self._output + "/Tiles/z_orthograpic_" + self._latlngfld + "/orthos"): + os.makedirs(self._output + "/Tiles/z_orthograpic_" + self._latlngfld +"/orthos") + mstr_msg("orthographic", "Created tile orthos folder") + + if mstr_xp_genscenery == True: + if not os.path.exists(self._output + "/Tiles/z_orthograpic_" + self._latlngfld + "/terrain"): + os.makedirs(self._output + "/Tiles/z_orthograpic_" + 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_orthograpic_" + self._latlngfld + "/normals"): + os.makedirs(self._output + "/Tiles/z_orthograpic_" + self._latlngfld + "/normals") + mstr_msg("orthographic", "Created X-Plane tile normals folder") # The tile is constructed of many smaller parts. We walk through the # smallest possible, from which the bigger ones are later built. @@ -144,7 +170,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/" + str(self._lat) + "_" + str(self._long) + "/textures/" + str(cur_tile_y) + "_" + str(cur_tile_x) + ".jpg") == False: + if os.path.isfile(mstr_datafolder + "Tiles/" + self._latlngfld + "/orthos/" + str(cur_tile_y) + "_" + str(cur_tile_x) + ".jpg") == False: # Let the user know mstr_msg("orthographic", "Generating missing orthophoto " + str(cur_tile_y) + "-" + str(cur_tile_x)) @@ -162,16 +188,6 @@ class mstr_orthographic: # itself, and finally, compose the ortho photo. mstr_msg("orthographic", "Beginning generation of layers") - # Generate the Tiles/lat-lng folder for the finished tile - if not os.path.exists(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long) + "/Textures"): - os.makedirs(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long)+"/Textures") - mstr_msg("orthographic", "Created tile textures folder") - - # Generate the Tiles/terrain folder for the finished tile - if not os.path.exists(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long) + "/terrain"): - os.makedirs(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long)+"/terrain") - mstr_msg("orthographic", "Created tile terrain folder") - curlyr = 1 for layer in layers: # Let the user know @@ -186,6 +202,7 @@ class mstr_orthographic: # Generate the layer lg = mstr_layergen(layer[0], layer[1], self._lat, cur_tile_y, self._long, cur_tile_x, layer[2]) lg.set_max_latlng_tile(maxlatlng) + lg.set_latlng_folder(self._latlngfld) lg.genlayer() curlyr = curlyr+1 mstr_msg("orthographic", "All layers created") @@ -293,14 +310,21 @@ class mstr_orthographic: mstr_msg("orthographic", "A total of " + str(len(layers)) + " layers were found") return layers + + # Construct a folder name for latitude and longitude + def 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 - # Constructor of class. Takes longitude and latitude. - def __init__(self, lat, lng, outfolder, pwd): - self._lat = lat - self._long = lng - self._output = outfolder - self._pwd = pwd - self._vstep = self._findVerticalStepping() - mstr_msg("orthographic", "Initiated with LAT: " + str(lat) + ", LNG: " + str(lng)) \ No newline at end of file diff --git a/photogen.py b/photogen.py index 9c19206..99b8bb0 100644 --- a/photogen.py +++ b/photogen.py @@ -4,6 +4,7 @@ from PIL import Image, ImageFilter from defines import * from layergen import * from log import * +from wand import image # ------------------------------------------------------------------- # ORTHOGRAPHIC @@ -33,6 +34,7 @@ class mstr_photogen: if mstr_photores == 4096: self._imgsize = 6000 # Empty image where everything goes into self._tile = Image.new("RGBA", (self._imgsize, self._imgsize)) + self._latlngfld = self.latlng_folder([lat,lng]) mstr_msg("photogen", "Photogen initialized") @@ -104,7 +106,14 @@ 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/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + str(self._ty) + "_" + str(self._tx) + ".jpg", format='JPEG', subsampling=0, quality=100) + self._tile.convert('RGB').save(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + 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(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") # This checks the final image for empty patches. Should one be @@ -148,4 +157,21 @@ class mstr_photogen: # exact pixel positions. mask.save( mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_tile-completion.png" ) - mstr_msg("photogen", "Generated and saved empty space mask") \ No newline at end of file + mstr_msg("photogen", "Generated and saved empty space mask") + + + # Construct a folder name for latitude and longitude + def 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 \ No newline at end of file diff --git a/wedgen.py b/wedgen.py deleted file mode 100644 index d22aeae..0000000 --- a/wedgen.py +++ /dev/null @@ -1,61 +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 -# ------------------------------------------------------------------- -# 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 diff --git a/xp_dsfgen.py b/xp_dsfgen.py index e4ccfd8..4ee910a 100644 --- a/xp_dsfgen.py +++ b/xp_dsfgen.py @@ -11,11 +11,9 @@ # generation process, and builds the DSF (Distributable Scenery # Format) file for X-Plane. # -# For this, you will need two tools which I cannot re-distribute: -# - DSFTool -# - DDSTool +# For this, you will need DSFTool which I cannot re-distribute. # -# You can download both of these for free from X-Plane's website. +# 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 # ------------------------------------------------------------------- @@ -25,18 +23,18 @@ import glob import math from random import randrange from log import * -from tiledb import * class mstr_xp_dsfgen: # Instantiate with Lat/Lng, as usual - def __init__(self, lat, lng, amtdg): + def __init__(self, lat, lng, mlat, mlng, vstep): self._latitude = lat self._longitude = lng - self._tiledb = mstr_tiledb(lat, lng) + self._maxlat = mlat + self._maxlng = mlng self._tmpdsf = mstr_datafolder + "_cache/tiledsf.txt" + self._vstep = vstep self._dsfstring = "" - self._amtdg = amtdg - mstr_msg("xp_dsfgen", "DSFgen initialized") + mstr_msg("xp_dsfgen", "[X-Plane] DSFgen initialized") self.build_header() @@ -48,42 +46,21 @@ class mstr_xp_dsfgen: 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" - mstr_msg("xp_dsfgen", "Header built") - - - # Let's now walk through the single polygon definitions - def build_polygon_defs(self): - mstr_msg("xp_dsfgen", "Walking through forest polygons") - - # Pick the kind of forest we work with - for f in range(1, self._amtdg+1): - #rws = self._tiledb.perform_query("SELECT * FROM xp_scenery WHERE datagroup="+str(f)+";") - frs = self.pick_forest_type() - self._dsfstring = self._dsfstring + "POLYGON_DEF lib/g8/"+frs+"\n" - - # Put in the data - curpoly=0 - for f in range(1, self._amtdg+1): - rws = self._tiledb.perform_query("SELECT * FROM xpscenery WHERE datagroup="+str(f)+";") - self._dsfstring = self._dsfstring + "BEGIN_POLYGON "+str(curpoly)+" 255 2\n" - self._dsfstring = self._dsfstring + "BEGIN_WINDING\n" - for r in rws: - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(r[2]) + " " + str(r[1]) + "\n" - self._dsfstring = self._dsfstring + "END_WINDING\n" - self._dsfstring = self._dsfstring + "END_POLYGON\n" - curpoly = curpoly + 1 - - mstr_msg("xp_dsfgen", "Forest definitions complete") + 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): - with open(mstr_datafolder + "_cache/dsf.txt", 'w') as textfile: + 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": @@ -94,7 +71,7 @@ class mstr_xp_dsfgen: datafolder = mstr_datafolder.replace("/", sep) # First, create the Earth nav data folder should it not exist - end_base = datafolder + "Tiles" + sep + str(self._latitude) + "_" + str(self._longitude) + sep + "Earth nav data" + 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()) @@ -107,7 +84,8 @@ class mstr_xp_dsfgen: end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude]) # Perform conversion - os.system(mstr_xp_dsftool + " --text2dsf " + datafolder + "_cache" + sep + "dsf.txt \"" + end_base + sep + end_round + sep + end_latlng + ".dsf\"") + 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 @@ -119,6 +97,17 @@ class mstr_xp_dsfgen: 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): @@ -136,23 +125,92 @@ class mstr_xp_dsfgen: return fstr - - # Pick some forest type from X-Plane - def pick_forest_type(self): - ftype = 0 - - # Where forests live in X-Plane. - rootfolder = mstr_xp_folder + "Resources/default scenery/1000 forests/" - forests = glob.glob(rootfolder + "mixed_*.for") - ftype = randrange(1, len(forests)-1) - fstring = forests[ftype] - fstring = fstring.replace(mstr_xp_folder + "Resources/default scenery/1000 forests\\", "") - - return fstring - - -dsf = mstr_xp_dsfgen(51, 7, 1) -dsf.build_polygon_defs() -dsf.write_dsf_txt() -dsf.convert_dsf_text() \ No newline at end of file + # 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") + + +# Testing +dsf = mstr_xp_dsfgen(51, 7, 101, 63, 0.01010) +dsf.build_dsf_for_tile() \ No newline at end of file diff --git a/xp_normalmap.py b/xp_normalmap.py new file mode 100644 index 0000000..3f1850b --- /dev/null +++ b/xp_normalmap.py @@ -0,0 +1,163 @@ + +# ------------------------------------------------------------------- +# 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_normalmap.py +# For some geographical features, we will add a normal map for +# additional realism in the orthos, without using additional meshes +# or geometry. +# +# Source converted to Python from this C++ StackOverflow: +# https://stackoverflow.com/a/2368794 +# ------------------------------------------------------------------- + + +import numpy as np +import math +import os +from PIL import Image, ImageFilter +from defines import * +from log import * + + +class mstr_xp_normalmap: + + # Only a few params + def __init__(self, lat, lng, tag, value, tv, th, latlngfld): + self._lat = lat + self._lng = lng + self._tag = tag + self._value = value + self._latlngfld = latlngfld + self._tv = tv + self._th = th + mstr_msg("xp_normalmap", "[X-Plane] Normal Map generator initialized") + + + # Load the layer image and resize it to 1/4th its size - + # then provide it + def load_layer(self): + qtr = int(mstr_photores / 4) + image = Image.open(mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._lng) + "_" + self._tag + "_" + self._value + "_layer.png") + image = image.resize((qtr,qtr), Image.Resampling.LANCZOS) + mstr_msg("xp_normalmap", "[X-Plane] Layer image loaded") + return image + + + # A few mathematical calls we need + # -------------------------------------------------------- + def intensity(pixel): + avg = (pixel[0] + pixel[1] + pixel[2]) / 3 + pavg = 255.0 / avg + return pavg + + + def clamp(px, mpx): + if px > mpx-1: + return mpx-1 + else: + if px < 0: + return 0 + else: + return px + + + def map_component(px): + return (px + 1.0) * (255.0 / 2.0) + + + def normalize_vector(v): + vc = np.array([v[0], v[1], v[2]]) + norm = np.linalg.norm(vc) + nv = vc / norm + return nv + # -------------------------------------------------------- + + + # 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,255)) + 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 + for y in range(image.width): + for x in range(image.height): + # Neighboring pixels + px_t = org[ self.clamp(x, image.width), self.clamp(y+1, image.height) ] + px_tr = org[ self.clamp(x+1, image.width), self.clamp(y+1, image.height) ] + px_r = org[ self.clamp(x+1, image.width), self.clamp(y, image.height) ] + px_br = org[ self.clamp(x+1, image.width), self.clamp(y+1, image.height) ] + px_b = org[ self.clamp(x, image.width), self.clamp(y-1, image.height) ] + px_bl = org[ self.clamp(x-1, image.width), self.clamp(y-1, image.height) ] + px_l = org[ self.clamp(x-1, image.width), self.clamp(y, image.height) ] + px_tl = org[ self.clamp(x-1, image.width), self.clamp(y+1, image.height) ] + + # Intensities of pixels + it_t = self.intensity(px_t) + it_tr = self.intensity(px_tr) + it_r = self.intensity(px_r) + it_br = self.intensity(px_br) + it_b = self.intensity(px_b) + it_bl = self.intensity(px_bl) + it_l = self.intensity(px_l) + it_tl = self.intensity(px_tl) + + # Sobel filter + dx = (it_tr + 2.0 * it_r + it_br) - (it_tl + 2.0 * it_l + it_bl) + dy = (it_bl + 2.0 * it_b + it_br) - (it_tl + 2.0 * it_t + it_tr) + dz = 10 # This is usually a good value for strength + v = (dx, dy, dz) + nrm = self.normalize_vector(v) + + # Invert height for our Orthos + 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) + + mstr_msg("xp_normalmap", "[X-Plane] Normal map generated") + return nmp + + + # The funnction to call. Blends with the existing map, or creates a new one + def build_normalmap(self): + mstr_msg("xp_normalmap", "[X-Plane] Building normal map") + # The layer image + lyr = self.load_layer() + + # Make the normal map for the layer + 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" + + # Check for existence of normal map file + ex = os.path.isfile(nrmfln) + + # Does not exist? Just save + if ex == False: + nrm.save(nrmfln) + + # Exists? Open it, composite both, save + if ex == True: + nrmmap = Image.open(nrmfln) + nrmmap.alpha_composite(nrm) + nrmmap.save(nrmfln) + mstr_msg("xp_normalmap", "[X-Plane] Normal map saved")