orthographic/xp_normalmap.py

164 lines
5.7 KiB
Python

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