orthographic/xp_normalmap.py

164 lines
5.9 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")
# A few mathematical calls we need
# --------------------------------------------------------
def intensity(self, pixel):
avg = (pixel[0] + pixel[1] + pixel[2]) / 3
if avg > 0:
pavg = 255.0 / avg
else:
pavg = 0
return pavg
def clamp(self, px, mpx):
if px > mpx-1:
return mpx-1
else:
if px < 0:
return 0
else:
return px
def map_component(self, px):
return (px + 1.0) * (255.0 / 2.0)
def normalize_vector(self, 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")
# No specularity, no reflectivity - but standard color
# Blue (reflectivity) and alpha (specularity) need to be 1 - but can be adjusted as needed
# Resize original
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))
org = image.load()
nmp_pix = nmp.load()
# Let's try some shenanigans
w = image.width
h = image.height
for y in range(h):
for x in range(w):
p = org[x,y]
if p[3] > 0: # Only do something if there is something to do in layer
# Neighboring pixels
px_t = org[ self.clamp(x, w), self.clamp(y+1, h) ]
px_tr = org[ self.clamp(x+1, w), self.clamp(y+1, h) ]
px_r = org[ self.clamp(x+1, w), self.clamp(y, h) ]
px_br = org[ self.clamp(x+1, w), self.clamp(y+1, h) ]
px_b = org[ self.clamp(x, w), self.clamp(y-1, h) ]
px_bl = org[ self.clamp(x-1, w), self.clamp(y-1, h) ]
px_l = org[ self.clamp(x-1, w), self.clamp(y, h) ]
px_tl = org[ self.clamp(x-1, w), self.clamp(y+1, h) ]
# 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)
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])), 255 - int(self.map_component(nrm[2])), 1)
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, layer):
mstr_msg("xp_normalmap", "[X-Plane] Building normal map")
# Make the normal map for the layer
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)
mstr_msg("xp_normalmap", "[X-Plane] Normal map saved")