orthographic/xp_normalmap.py

145 lines
5.4 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):
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, 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
# 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))
if water: nmp = Image.new("RGBA", (image.width, image.height), (128, 128, 255, 0))
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
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
# 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"
mstr_msg("xp_normalmap", "[X-Plane] Normal map generated")
return nrm