# 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
("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"),
("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"),
from tiledb import *
from osmxml import *
from functions import *
+from xp_normalmap import *
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.
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()
+
# 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
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.
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))
# 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
# 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")
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
from defines import *
from layergen import *
from log import *
+from wand import image
# -------------------------------------------------------------------
# ORTHOGRAPHIC
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")
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
# 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
+++ /dev/null
-
-# -------------------------------------------------------------------
-# 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
# 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
# -------------------------------------------------------------------
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()
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":
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())
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
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):
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
--- /dev/null
+
+# -------------------------------------------------------------------
+# 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")