From 8a9dfd05f45f795537d48245fbd5c0a71559ebb3 Mon Sep 17 00:00:00 2001 From: "Marcus Str." Date: Sat, 30 Nov 2024 22:12:36 +0100 Subject: [PATCH] Normal map moved to end of ortho pipeline. Water is no longer rendered underneath, but makes use of normal maps. Physics model in orthos fixed. Testing of normals outstanding. --- layergen.py | 5 +- photogen.py | 51 ++++++------ xp_normalmap.py | 47 ++++------- xp_scenery.py | 214 +++++++++--------------------------------------- 4 files changed, 83 insertions(+), 234 deletions(-) diff --git a/layergen.py b/layergen.py index cb72595..98d3267 100644 --- a/layergen.py +++ b/layergen.py @@ -22,7 +22,6 @@ from log import * from tileinfo import * from osmxml import * from functions import * -from xp_normalmap import * class mstr_layergen: @@ -409,6 +408,7 @@ class mstr_layergen: # Depending on if scenery for XP should be made, AND if normal maps should be made, we would # need to make them at this exact point + """ if mstr_xp_genscenery == True: if mstr_xp_scn_normalmaps == True and self._is_completion == False: nm = False @@ -419,6 +419,7 @@ class mstr_layergen: if nm == True: nrm = mstr_xp_normalmap(self._latitude, self._longitude, self._tag, self._value, self._lat_number, self._lng_number, self._latlngfld) nrm.build_normalmap(layer_comp) + """ # Let's try our hand at pseudo shadows @@ -886,6 +887,7 @@ class mstr_layergen: # Depending on if scenery for XP should be made, AND if normal maps should be made, we would # need to make them at this exact point + """ if mstr_xp_genscenery == True: if mstr_xp_scn_normalmaps == True and self._is_completion == False: nm = False @@ -896,6 +898,7 @@ class mstr_layergen: if nm == True: nrm = mstr_xp_normalmap(self._latitude, self._longitude, self._tag, self._value, self._lat_number, self._lng_number, self._latlngfld) nrm.build_normalmap(layer_comp) + """ # Return image diff --git a/photogen.py b/photogen.py index 54b016b..c50bb86 100644 --- a/photogen.py +++ b/photogen.py @@ -5,6 +5,7 @@ from defines import * from layergen import * from log import * from functions import * +from xp_normalmap import * # ------------------------------------------------------------------- # ORTHOGRAPHIC @@ -29,9 +30,7 @@ class mstr_photogen: self._tx = tx self._maxlatlng = [ maxlat, maxlng ] # Define layer size depending on what is wanted - self._imgsize = 0 - if mstr_photores == 2048: self._imgsize = 2048 - if mstr_photores == 4096: self._imgsize = 6000 + self._imgsize = mstr_photores # Empty image where everything goes into self._tile = Image.new("RGBA", (self._imgsize, self._imgsize)) self._latlngfld = self.latlng_folder([lat,lng]) @@ -148,29 +147,6 @@ class mstr_photogen: corrpix[x,y] = nc if c[3] == 0: corrpix[x,y] = (0,0,0,0) - - # Now cut out inland water - for w in waterlayers: - wtr_pix = w.load() - for y in range(w.height): - for x in range(w.width): - v = wtr_pix[x,y] - if v[3] >= 50: - c = (0,0,0,0) - corrpix[x,y] = c - - """ - ddsf_water = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) + "_water.png" - if os.path.isfile(ddsf_water) == True: - wtr = Image.open(ddsf_water) - wtr_pix = wtr.load() - for y in range(wtr.height): - for x in range(wtr.width): - v = wtr_pix[x,y] - if v <= 50: - c = (0,0,0,0) - corrpix[x,y] = c - """ # We are now in posession of the final image. @@ -187,6 +163,29 @@ class mstr_photogen: os.remove(mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) + ".png") + # Now generate the normal map for this ortho. + # But only if this is enabled. + if mstr_xp_genscenery and mstr_xp_scn_normalmaps: + # Generate the normal normal map first (hah) + nrm = mstr_xp_normalmap() + nrmimg = nrm.generate_normal_map_for_layer(self._tile, False) + + # Now we need to walk through the water layers and generate a combined normal map + wtrlyr = Image.new("RGBA", (self._imgsize, self._imgsize)) + for w in waterlayers: + wtrlyr.alpha_composite(w) + wtrlyr = wtrlyr.resize((int(mstr_photores/4), int(mstr_photores/4)), Image.Resampling.BILINEAR) + wtrimg = nrm.generate_normal_map_for_layer(wtrlyr, True) + + # Blend + nrmimg.alpha_composite(wtrimg) + + # Save + nrmfln = mstr_datafolder + "z_orthographic/normals/" + self._latlngfld + "/" + str(self._ty) + "_" + str( + self._tx) + ".png" + nrmimg.save(nrmfln) + + # This checks the final image for empty patches. Should one be diff --git a/xp_normalmap.py b/xp_normalmap.py index 9590c55..fcefc90 100644 --- a/xp_normalmap.py +++ b/xp_normalmap.py @@ -27,14 +27,7 @@ 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 + def __init__(self): mstr_msg("xp_normalmap", "[X-Plane] Normal Map generator initialized") @@ -46,7 +39,7 @@ class mstr_xp_normalmap: pavg = 255.0 / avg else: pavg = 0 - return pavg + return pavg * 3 def clamp(self, px, mpx): @@ -72,7 +65,7 @@ class mstr_xp_normalmap: # The Big Mac. Generate the normal map - def generate_normal_map_for_layer(self, image): + def generate_normal_map_for_layer(self, image, water=False): mstr_msg("xp_normalmap", "[X-Plane] Beginning normal map generation") # No specularity, no reflectivity - but standard color # Blue (reflectivity) and alpha (specularity) need to be 1 - but can be adjusted as needed @@ -81,6 +74,9 @@ class mstr_xp_normalmap: image = image.resize((int(mstr_photores/4), int(mstr_photores/4)), Image.Resampling.BILINEAR) nmp = Image.new("RGBA", (image.width, image.height), (128,128,1,1)) + + if water: nmp = Image.new("RGBA", (image.width, image.height), (128, 128, 255, 0)) + org = image.load() nmp_pix = nmp.load() @@ -124,7 +120,10 @@ class mstr_xp_normalmap: nrm[1] = abs(nrm[1]) # Set pixel - nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), 255 - int(self.map_component(nrm[2])), 1) + if water: + nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), int(self.map_component(nrm[2])), int(self.map_component(nrm[2]))) + if not water: + nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), 255 - int(self.map_component(nrm[2])), 1) mstr_msg("xp_normalmap", "[X-Plane] Normal map generated") return nmp @@ -138,26 +137,8 @@ class mstr_xp_normalmap: nrm = self.generate_normal_map_for_layer(layer) # Normal map final file name - nrmfln = mstr_datafolder + "z_orthographic/normals/" + self._latlngfld + "/" + str(self._tv) + "_" + str(self._th) + ".png" - - # Check for existence of normal map file - ex = os.path.isfile(nrmfln) - - # 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) - - # Specularity blending correction - nrmmap_pix = nrmmap.load() - for y in range(nrmmap.height): - for x in range(nrmmap.width): - c = nrmmap_pix[x,y] - nrmmap_pix[x,y] = (c[0], c[1], c[2], 1) - nrmmap.save(nrmfln) + #nrmfln = mstr_datafolder + "z_orthographic/normals/" + self._latlngfld + "/" + str(self._tv) + "_" + str(self._th) + ".png" - mstr_msg("xp_normalmap", "[X-Plane] Normal map saved") + mstr_msg("xp_normalmap", "[X-Plane] Normal map generated") + + return nrm diff --git a/xp_scenery.py b/xp_scenery.py index ca07034..2bfeee1 100644 --- a/xp_scenery.py +++ b/xp_scenery.py @@ -34,6 +34,8 @@ class mstr_xp_scenery: self._dsfstring = "" self._demdata = None # To be populated when the mesh is built self._demcoord = None # Also to be populated when mesh is built + self._waterdata = [] # So that we know where to implement water + #self.load_water_data() # Build the correct file name for the elevation model @@ -66,12 +68,30 @@ class mstr_xp_scenery: return fn + # Load the water data before we generate the mesh + def load_water_data(self): + fn = mstr_datafolder + "z_orthographic/data/" + self._latlngfld + "/wtrfile" + with open(fn) as file: + for line in file: + ln = line.replace(" ", "_") + ln = ln.replace("\n", "") + ln = ln.replace("\r", "") + self._waterdata.append(ln) + + + # Check if ortho has water + def does_ortho_have_water(self, ortho): + wtr = False + if ortho in self._waterdata: wtr = True + return wtr + + + # Build the DSF for the ortho photo overlay def build_and_convert_dsf(self): end = self.find_earthnavdata_number() llf = self.xplane_latlng_folder(end) meshtxt = mstr_datafolder + "_cache/mesh_"+self._latlngfld+".txt" - scr = mstr_datafolder + "z_orthographic/data/" + self._latlngfld + "/meshscript.txt" cmd = mstr_xp_dsftool + " --text2dsf " + meshtxt + " '" + mstr_datafolder + "z_orthographic/Earth nav data/" + llf + "/" + self._latlngfld + ".dsf'" os.system(cmd) @@ -160,9 +180,8 @@ class mstr_xp_scenery: terstr = terstr + "TERRAIN\r\n" terstr = terstr + "\r\n" terstr = terstr + "LOAD_CENTER " + str(cnt_x) + " " + str(cnt_y) + " " + str(dmt) + " 2048\r\n" - #terstr = terstr + "BASE_TEX_NOWRAP ../../orthos/" + self._latlngfld + "/" + str(lat)+"_"+str(lng)+".dds\r\n" - terstr = terstr + "TEXTURE_NOWRAP ../../orthos/" + self._latlngfld + "/" + str(lat)+"_"+str(lng)+".dds\r\n" - if mstr_xp_scn_normalmaps == True: + terstr = terstr + "BASE_TEX_NOWRAP ../../orthos/" + self._latlngfld + "/" + str(lat)+"_"+str(lng)+".dds\r\n" + if mstr_xp_scn_normalmaps: terstr = terstr + "NORMAL_TEX 1.0 ../../normals/" + self._latlngfld + "/" + str(lat)+"_"+str(lng)+".png\r\n" terfln = mstr_datafolder + "z_orthographic/terrain/" + self._latlngfld + "/" + str(lat)+"_"+str(lng)+".ter" @@ -223,103 +242,34 @@ class mstr_xp_scenery: with open(meshtxt, 'w') as textfile: textfile.write(dsf_str) + dsf_str = "" + + # Orthos for lat in range(1, self._mlat+1): for lng in range(1, self._mlng+1): # Write the line only if an ortho exists of course. ddsf = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + ".dds" - ddsf_water = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + "_water.png" - if os.path.isfile(ddsf) == True: - dsfstr = "TERRAIN_DEF terrain/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + ".ter\r\n" - - # Let's check if this tile needs water beneath - if os.path.isfile(ddsf_water) == True: - dsfstr = dsfstr + "TERRAIN_DEF terrain_Water\r\n" + if os.path.isfile(ddsf): + dsf_str = dsf_str + "TERRAIN_DEF terrain/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + ".ter\r\n" - with open(meshtxt, 'a') as textfile: - textfile.write(dsfstr) + with open(meshtxt, 'a') as textfile: + textfile.write(dsf_str) # OK. So. Let's build the mesh. - - """ - # First, the ground water mesh - with open(meshtxt, 'a') as textfile: - textfile.write("BEGIN_PATCH 0 0.000000 -1.000000 1 5\r\n") - - # Vertical row (Latitude Row) - for lat_r in range(0, len(self._demcoord)-2): - - # Horizontal row (Longitude Column) - for lng_c in range(0, len(self._demcoord)-2): - - # Lat/lng coordinate - lat_crd = self._demcoord[lat_r][lng_c][0] - lat_crd_t = self._demcoord[lat_r+1][lng_c][0] - lng_crd = self._demcoord[lat_r][lng_c][1] - lng_crd_r = self._demcoord[lat_r][lng_c+1][1] - - # Coords of triangle vertices - # 0 - Longitude - # 1 - Latitude - # 2 - Height in m - t1_v1 = [ lng_crd_r, lat_crd, 0 ] - t1_v2 = [ lng_crd, lat_crd_t, 0 ] - t1_v3 = [ lng_crd_r, lat_crd_t, 0 ] - t2_v1 = [ lng_crd, lat_crd_t, 0 ] - t2_v2 = [ lng_crd_r, lat_crd, 0 ] - t2_v3 = [ lng_crd, lat_crd, 0 ] - - - t1_v1 = [ lng_crd_r, lat_crd, self._demcoord[lat_r][lng_c+1][2] ] - t1_v2 = [ lng_crd, lat_crd_t, self._demcoord[lat_r+1][lng_c][2] ] - t1_v3 = [ lng_crd_r, lat_crd_t, self._demcoord[lat_r+1][lng_c+1][2] ] - t2_v1 = [ lng_crd, lat_crd_t, self._demcoord[lat_r+1][lng_c][2] ] - t2_v2 = [ lng_crd_r, lat_crd, self._demcoord[lat_r][lng_c+1][2] ] - t2_v3 = [ lng_crd, lat_crd, self._demcoord[lat_r][lng_c][2] ] - - - # Write down the two triangles - t_str = "" - t_str = t_str + "BEGIN_PRIMITIVE 0\r\n" - t_str = t_str + "PATCH_VERTEX " + str(t1_v1[0]) + " " + str(t1_v1[1]) + " " + str(t1_v1[2]) + " 0.000015 0.000015\r\n" - t_str = t_str + "PATCH_VERTEX " + str(t1_v2[0]) + " " + str(t1_v2[1]) + " " + str(t1_v2[2]) + " 0.000015 0.000015\r\n" - t_str = t_str + "PATCH_VERTEX " + str(t1_v3[0]) + " " + str(t1_v3[1]) + " " + str(t1_v3[2]) + " 0.000015 0.000015\r\n" - t_str = t_str + "END_PRIMITIVE 0\r\n" - t_str = t_str + "BEGIN_PRIMITIVE 0\r\n" - t_str = t_str + "PATCH_VERTEX " + str(t2_v1[0]) + " " + str(t2_v1[1]) + " " + str(t2_v1[2]) + " 0.000015 0.000015\r\n" - t_str = t_str + "PATCH_VERTEX " + str(t2_v2[0]) + " " + str(t2_v2[1]) + " " + str(t2_v2[2]) + " 0.000015 0.000015\r\n" - t_str = t_str + "PATCH_VERTEX " + str(t2_v3[0]) + " " + str(t2_v3[1]) + " " + str(t2_v3[2]) + " 0.000015 0.000015\r\n" - t_str = t_str + "END_PRIMITIVE 0\r\n" - - # Send to the file - with open(meshtxt, 'a') as textfile: - textfile.write(t_str) - - t_str = "" - - # Water mesh ends - with open(meshtxt, 'a') as textfile: - textfile.write("END PATCH\r\n") - """ # Current patch curpatch = 0 for lat in range(1, self._mlat+1): for lng in range(1, self._mlng+1): - # Create the patch only if the matching ortho exists. # This way we make sure that we hit the same order as the .ter files. # We can also detect which lat and lng coord we are on. - #ddsf = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/1_1.dds" - ddsf = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + ".dds" - ddsf_water = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + "_water.png" - if os.path.isfile(ddsf) == True: - - scangrid = self.find_height_scan_start_end_points([ self._lat+((lat-1)*self._vstep), self._lng+((lng-1)*mstr_zl_18) ]) - #sloped = self.build_sloped_scangrid(scangrid) + ddsf = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(lat) + "_" + str(lng) + ".dds" + if os.path.isfile(ddsf): # Base coords for this ortho base_lat = self._lat + ((lat-1) * self._vstep) @@ -335,12 +285,6 @@ class mstr_xp_scenery: latstep = self._vstep/odiv lngstep = mstr_zl_18 /odiv uv_step = 1 / odiv - - # Height values - hgt_bl = 0 - hgt_br = 0 - hgt_tr = 0 - hgt_tl = 0 # Generate the ortho tile for y in range(0,odiv): @@ -408,11 +352,6 @@ class mstr_xp_scenery: t_str = "" - # Height value: - # hgtindex = self.find_height_for_coord([lat, lng]) - # height = self._demcoord[hgtindex[0]][hgtindex[1]][2] - - # End this patch with open(meshtxt, 'a') as textfile: textfile.write("END PATCH\r\n") @@ -422,15 +361,12 @@ class mstr_xp_scenery: # Let's check if this tile needs water beneath - needs_water = False - if os.path.isfile(ddsf_water) == True: needs_water = True - - - if needs_water == True: + """ + if self.does_ortho_have_water(str(lat) + "_" + str(lng)): # Begin a new patch with open(meshtxt, 'a') as textfile: - textfile.write("BEGIN_PATCH " + str(curpatch) + " 0.000000 -1.000000 1 5\r\n") + textfile.write("BEGIN_PATCH " + str(curpatch) + " 0.000000 -1.000000 2 5\r\n") # Generate the ortho tile for y in range(0,odiv): @@ -461,10 +397,10 @@ class mstr_xp_scenery: hgt_br_idx = self.find_height_for_coord([lat_b, lng_r]) hgt_tr_idx = self.find_height_for_coord([lat_t, lng_r]) hgt_tl_idx = self.find_height_for_coord([lat_t, lng_l]) - hgt_bl = round(self._demcoord[ hgt_bl_idx[0] ][ hgt_bl_idx[1] ][2] - .1, 6) - hgt_br = round(self._demcoord[ hgt_br_idx[0] ][ hgt_br_idx[1] ][2] - .1, 6) - hgt_tr = round(self._demcoord[ hgt_tr_idx[0] ][ hgt_tr_idx[1] ][2] - .1, 6) - hgt_tl = round(self._demcoord[ hgt_tl_idx[0] ][ hgt_tl_idx[1] ][2] - .1, 6) + hgt_bl = round(self._demcoord[ hgt_bl_idx[0] ][ hgt_bl_idx[1] ][2] - .01, 6) + hgt_br = round(self._demcoord[ hgt_br_idx[0] ][ hgt_br_idx[1] ][2] - .01, 6) + hgt_tr = round(self._demcoord[ hgt_tr_idx[0] ][ hgt_tr_idx[1] ][2] - .01, 6) + hgt_tl = round(self._demcoord[ hgt_tl_idx[0] ][ hgt_tl_idx[1] ][2] - .01, 6) # Coords of triangle vertices # 0 - Longitude @@ -500,6 +436,7 @@ class mstr_xp_scenery: # Increase patch number curpatch = curpatch + 1 + """ @@ -561,76 +498,5 @@ class mstr_xp_scenery: if startend[2] < 0: startend[2] = 0 if startend[3] > len(self._demdata)-1: startend[3] = startend[3] = len(self._demdata)-1 - - """ - t = self._lat - while t < startcoord[0]: - t = t + self._vstep - startend[0] = startend[0]+1 - - t = self._lat - while t < startcoord[0]+self._vstep: - t = t + self._vstep - startend[1] = startend[1]+1 - - t = self._lng - while t < startcoord[1]: - t = t + mstr_zl_18 - startend[2] = startend[2]+1 - - t = self._lng - while t < startcoord[1]+mstr_zl_18: - t = t + mstr_zl_18 - startend[3] = startend[3]+1 - - # Some corrections - startend[0] = startend[0]-1 - if startend[0] < 0: startend[0] = 0 - - startend[1] = startend[1]+1 - if startend[1] > len(self._demdata)-1: startend[1] = startend[1] = len(self._demdata)-1 - - startend[2] = startend[2]-1 - if startend[2] < 0: startend[2] = 0 - - startend[3] = startend[3]+1 - if startend[3] > len(self._demdata)-1: startend[3] = startend[3] = len(self._demdata)-1 - """ - return startend - - # Function to subdivide between two vectors - def subdivide_vectors(self, v1, v2, subdivisions): - #return np.linspace(v1, v2, subdivisions + 2, axis=0) # +2 to include endpoints - return numpy.linspace(v1, v2, subdivisions + 2, axis=0) # +2 to include endpoints - - - # This build a scangrid with increased resolution, extrapolated from existing points. - # With this we can accurately depict the height of a point a long the slope of the ground mesh. - def build_sloped_scangrid(self, grid): - - # Contains the data as defined by the grid passed in - tmp_dem = [] - - # Acquire original grid data - for l in range(grid[2], grid[3]+1): - row = [] - for c in range(grid[0], grid[1]+1): - row.append(self._demcoord[l][c]) - tmp_dem.append(row) - - # Subdivide the array - subdivisions = 10 - result = [] - for i in range(len(tmp_dem) - 1): - for j in range(len(tmp_dem[i]) - 1): - subdivided = self.subdivide_vectors(tmp_dem[i][j], tmp_dem[i][j + 1], subdivisions) - if i > 0: # Avoid duplicating start point - subdivided = subdivided[1:] - result.append(subdivided) - - # Combine all subdivisions into one array - result = numpy.vstack(result) - - return result \ No newline at end of file -- 2.30.2