]> marstr Code Repo - orthographic/commitdiff
X-Plane DSF file generator, X-Plane normal map generator. Normal maps can be turned...
authormarstr <marcus@marstr.online>
Mon, 9 Sep 2024 21:14:43 +0000 (23:14 +0200)
committermarstr <marcus@marstr.online>
Mon, 9 Sep 2024 21:14:43 +0000 (23:14 +0200)
17 files changed:
Textures/building/common/brd/b5.png [new file with mode: 0644]
Textures/building/common/ptc/b5_p1.png [new file with mode: 0644]
Textures/building/common/ptc/b5_p2.png [new file with mode: 0644]
Textures/natural/desert/brd/b1.png [new file with mode: 0644]
Textures/natural/desert/ptc/b1_p1.png [new file with mode: 0644]
Textures/natural/desert/ptc/b1_p2.png [new file with mode: 0644]
Textures/natural/sand/brd/b1.png [new file with mode: 0644]
Textures/natural/sand/ptc/b1_p1.png [new file with mode: 0644]
Textures/natural/sand/ptc/b1_p2.png [new file with mode: 0644]
Textures/natural/water/normal_template.png [new file with mode: 0644]
defines.py
layergen.py
orthographic.py
photogen.py
wedgen.py [deleted file]
xp_dsfgen.py
xp_normalmap.py [new file with mode: 0644]

diff --git a/Textures/building/common/brd/b5.png b/Textures/building/common/brd/b5.png
new file mode 100644 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..e65cf6b
Binary files /dev/null and b/Textures/natural/water/normal_template.png differ
index fc77d12bcdceafc8918077cdab2379988ad15862..a746dacb2ccb6056a986c42c666576ceb8ef1310 100644 (file)
@@ -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"),
index fdd993223d14ee985ea918486e5372a88c8574d5..fd334af52cb4bbc9d9c97de40a78a949be73ddfe 100644 (file)
@@ -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()
+
index 4beb2bb484afcc181f3c3e090e7ad35b3c4b7d71..09024dbbeea3e235318a4da78c40b085e3be7a85 100644 (file)
@@ -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
index 9c19206897bb99a006bdba3758fb6ad41bda8295..99b8bb0eafc7741bc3ecff3e74f171115df75b26 100644 (file)
@@ -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 (file)
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
index e4ccfd82a2ceccfff546e5fa7269b559c537e1f4..4ee910ace742837b16c1345cab4a61ba4aba0524 100644 (file)
 # 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 (file)
index 0000000..3f1850b
--- /dev/null
@@ -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")