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.

This commit is contained in:
marstr 2024-09-09 23:14:43 +02:00
parent bbc924984a
commit a5e850feec
17 changed files with 384 additions and 143 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

View File

@ -71,8 +71,26 @@ mstr_xp_genscenery = True
# X-Plane specific # X-Plane specific
mstr_xp_dsftool = "M:/Developer/Projects/orthographic/bin/DSFTool.exe" 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/" 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 # 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 # the zoom level, the smaller the area to generate a mask of - but also
@ -108,9 +126,6 @@ mstr_ortho_layers = [
("highway", "path", 3), ("highway", "path", 3),
("natural", "bare_rock", "natural", "bare_rock"), ("natural", "bare_rock", "natural", "bare_rock"),
("natural", "grassland", "landuse", "meadow"), ("natural", "grassland", "landuse", "meadow"),
("natural", "wetland", "natural", "wetland"),
("natural", "scrub", "natural", "scrub"),
("natural", "heath", "natural", "heath"),
("leisure", "park", "leisure", "green"), ("leisure", "park", "leisure", "green"),
("leisure", "dog_park", "leisure", "green"), ("leisure", "dog_park", "leisure", "green"),
("leisure", "garden", "leisure", "green"), ("leisure", "garden", "leisure", "green"),
@ -132,6 +147,9 @@ mstr_ortho_layers = [
("waterway", "stream", 10), ("waterway", "stream", 10),
("leisure", "nature_reserve", "landuse", "forest"), ("leisure", "nature_reserve", "landuse", "forest"),
("landuse", "forest", "landuse", "forest"), ("landuse", "forest", "landuse", "forest"),
("natural", "wetland", "natural", "wetland"),
("natural", "scrub", "natural", "scrub"),
("natural", "heath", "natural", "heath"),
# Z-Order 3 # Z-Order 3
("natural", "water", "natural", "water"), ("natural", "water", "natural", "water"),
("natural", "bay", "natural", "beach"), ("natural", "bay", "natural", "beach"),

View File

@ -22,6 +22,7 @@ from log import *
from tiledb import * from tiledb import *
from osmxml import * from osmxml import *
from functions import * from functions import *
from xp_normalmap import *
class mstr_layergen: class mstr_layergen:
@ -53,6 +54,10 @@ class mstr_layergen:
self._maxlng = maxlatlng[1] self._maxlng = maxlatlng[1]
mstr_msg("layergen", "Maximum latitude and longitude tile numbers received") 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 # 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, # before the actual crop of farm field itself. This generates this "border" layer,
# and returns it. # 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" ) 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.") 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()

View File

@ -25,6 +25,17 @@ from tilegen import *
# The main class which handles the rest # The main class which handles the rest
class mstr_orthographic: 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, # 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 # 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 # 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.") mstr_msg("orthographic", "Created Tiles folder.")
# Generate the Tiles/lat-lng folder for the finished tile # Generate the Tiles/lat-lng folder for the finished tile
if not os.path.exists(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long)): if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld):
os.makedirs(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long)) os.makedirs(self._output + "/Tiles/z_orthographic_"+ self._latlngfld)
mstr_msg("orthographic", "Created Tiles sub folder: " +str(self._lat)+"_"+str(self._long)) 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 # The tile is constructed of many smaller parts. We walk through the
# smallest possible, from which the bigger ones are later built. # 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") mstr_msg("orthographic", "Adjusted bounding box for XML object")
# Determine what to do... maybe work was interrupted # 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 # Let the user know
mstr_msg("orthographic", "Generating missing orthophoto " + str(cur_tile_y) + "-" + str(cur_tile_x)) 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. # itself, and finally, compose the ortho photo.
mstr_msg("orthographic", "Beginning generation of layers") 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 curlyr = 1
for layer in layers: for layer in layers:
# Let the user know # Let the user know
@ -186,6 +202,7 @@ class mstr_orthographic:
# Generate the layer # Generate the layer
lg = mstr_layergen(layer[0], layer[1], self._lat, cur_tile_y, self._long, cur_tile_x, layer[2]) 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_max_latlng_tile(maxlatlng)
lg.set_latlng_folder(self._latlngfld)
lg.genlayer() lg.genlayer()
curlyr = curlyr+1 curlyr = curlyr+1
mstr_msg("orthographic", "All layers created") 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") mstr_msg("orthographic", "A total of " + str(len(layers)) + " layers were found")
return layers 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))

View File

@ -4,6 +4,7 @@ from PIL import Image, ImageFilter
from defines import * from defines import *
from layergen import * from layergen import *
from log import * from log import *
from wand import image
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# ORTHOGRAPHIC # ORTHOGRAPHIC
@ -33,6 +34,7 @@ class mstr_photogen:
if mstr_photores == 4096: self._imgsize = 6000 if mstr_photores == 4096: self._imgsize = 6000
# Empty image where everything goes into # Empty image where everything goes into
self._tile = Image.new("RGBA", (self._imgsize, self._imgsize)) self._tile = Image.new("RGBA", (self._imgsize, self._imgsize))
self._latlngfld = self.latlng_folder([lat,lng])
mstr_msg("photogen", "Photogen initialized") mstr_msg("photogen", "Photogen initialized")
@ -104,7 +106,14 @@ class mstr_photogen:
self._tile = self._tile.resize((mstr_photores, mstr_photores), Image.Resampling.BILINEAR) self._tile = self._tile.resize((mstr_photores, mstr_photores), Image.Resampling.BILINEAR)
# This we can save accordingly. # 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 # This checks the final image for empty patches. Should one be
@ -148,4 +157,21 @@ class mstr_photogen:
# exact pixel positions. # exact pixel positions.
mask.save( mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_tile-completion.png" ) 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") 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

View File

@ -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()

View File

@ -11,11 +11,9 @@
# generation process, and builds the DSF (Distributable Scenery # generation process, and builds the DSF (Distributable Scenery
# Format) file for X-Plane. # Format) file for X-Plane.
# #
# For this, you will need two tools which I cannot re-distribute: # For this, you will need DSFTool which I cannot re-distribute.
# - DSFTool
# - DDSTool
# #
# 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 # Place them somewhere convenient, and point to them in the
# xp_ variables in defines.py # xp_ variables in defines.py
# ------------------------------------------------------------------- # -------------------------------------------------------------------
@ -25,18 +23,18 @@ import glob
import math import math
from random import randrange from random import randrange
from log import * from log import *
from tiledb import *
class mstr_xp_dsfgen: class mstr_xp_dsfgen:
# Instantiate with Lat/Lng, as usual # Instantiate with Lat/Lng, as usual
def __init__(self, lat, lng, amtdg): def __init__(self, lat, lng, mlat, mlng, vstep):
self._latitude = lat self._latitude = lat
self._longitude = lng self._longitude = lng
self._tiledb = mstr_tiledb(lat, lng) self._maxlat = mlat
self._maxlng = mlng
self._tmpdsf = mstr_datafolder + "_cache/tiledsf.txt" self._tmpdsf = mstr_datafolder + "_cache/tiledsf.txt"
self._vstep = vstep
self._dsfstring = "" self._dsfstring = ""
self._amtdg = amtdg mstr_msg("xp_dsfgen", "[X-Plane] DSFgen initialized")
mstr_msg("xp_dsfgen", "DSFgen initialized")
self.build_header() 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/north " + str(int(self._latitude+1)) + "\n"
self._dsfstring = self._dsfstring + "PROPERTY sim/planet earth\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/creation_agent Orthographic\n"
mstr_msg("xp_dsfgen", "Header built") 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")
# 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")
# Write the text file # Write the text file
def write_dsf_txt(self): 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) textfile.write(self._dsfstring)
# Convert the DSF into actual, usable data for X-Plane # Convert the DSF into actual, usable data for X-Plane
def convert_dsf_text(self): def convert_dsf_text(self):
mstr_msg("xp_dsfgen", "[X-Plane] Converting DSF information into X-Plane DSF file")
# Find separator # Find separator
sep = "" sep = ""
if os.name == "nt": if os.name == "nt":
@ -94,7 +71,7 @@ class mstr_xp_dsfgen:
datafolder = mstr_datafolder.replace("/", sep) datafolder = mstr_datafolder.replace("/", sep)
# First, create the Earth nav data folder should it not exist # 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 # Create the appropriate rounded folder
end_round = self.xplane_latlng_folder(self.find_earthnavdata_number()) 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]) end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude])
# Perform conversion # 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 # Find the next "by-ten" numbers for the current latitude and longitude
@ -119,6 +97,17 @@ class mstr_xp_dsfgen:
earthnavdata.append(lng) earthnavdata.append(lng)
return earthnavdata 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 # Construct an X-Plane compatible folder name for latitude and longitude
def xplane_latlng_folder(self, numbers): def xplane_latlng_folder(self, numbers):
@ -136,23 +125,92 @@ class mstr_xp_dsfgen:
return fstr return fstr
# Pick some forest type from X-Plane
def pick_forest_type(self):
ftype = 0
# Where forests live in X-Plane. # Build the complete DSF.
rootfolder = mstr_xp_folder + "Resources/default scenery/1000 forests/" # This is the main function to call.
forests = glob.glob(rootfolder + "mixed_*.for") def build_dsf_for_tile(self):
ftype = randrange(1, len(forests)-1) mstr_msg("xp_dsfgen", "[X-Plane] Building DSF file")
fstring = forests[ftype] # Add the polygon definition file entries
fstring = fstring.replace(mstr_xp_folder + "Resources/default scenery/1000 forests\\", "") 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"
return fstring # 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")
dsf = mstr_xp_dsfgen(51, 7, 1) # Testing
dsf.build_polygon_defs() dsf = mstr_xp_dsfgen(51, 7, 101, 63, 0.01010)
dsf.write_dsf_txt() dsf.build_dsf_for_tile()
dsf.convert_dsf_text()

163
xp_normalmap.py Normal file
View File

@ -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")