145 lines
5.4 KiB
Python
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
|