Commit eb3e843c authored by Damien Picard's avatar Damien Picard
Browse files

sun_position: remove from contrib: T69936

parent 6e651fab
### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# --------------------------------------------------------------------------
# The sun positioning algorithms are based on the National Oceanic
# and Atmospheric Administration's (NOAA) Solar Position Calculator
# which rely on calculations of Jean Meeus' book "Astronomical Algorithms."
# Use of NOAA data and products are in the public domain and may be used
# freely by the public as outlined in their policies at
# www.nws.noaa.gov/disclaimer.php
# --------------------------------------------------------------------------
# The geo parser script is by Maximilian Högner, released
# under the GNU GPL license:
# http://hoegners.de/Maxi/geo/
# --------------------------------------------------------------------------
# <pep8 compliant>
bl_info = {
"name": "Sun Position",
"author": "Michael Martin",
"version": (3, 1, 0),
"blender": (2, 80, 0),
"location": "World > Sun Position",
"description": "Show sun position with objects and/or sky texture",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
"Scripts/3D_interaction/Sun_Position",
"tracker_url": "https://projects.blender.org/tracker/"
"index.php?func=detail&aid=29714",
"category": "Lighting"}
if "bpy" in locals():
import importlib
importlib.reload(properties)
importlib.reload(ui_sun)
importlib.reload(hdr)
else:
from . import properties, ui_sun, hdr
import bpy
def register():
bpy.utils.register_class(properties.SunPosProperties)
bpy.types.Scene.sun_pos_properties = (
bpy.props.PointerProperty(type=properties.SunPosProperties,
name="Sun Position",
description="Sun Position Settings"))
bpy.utils.register_class(properties.SunPosAddonPreferences)
bpy.utils.register_class(ui_sun.SUNPOS_OT_AddPreset)
bpy.utils.register_class(ui_sun.SUNPOS_OT_DefaultPresets)
bpy.utils.register_class(ui_sun.SUNPOS_MT_Presets)
bpy.utils.register_class(ui_sun.SUNPOS_PT_Panel)
bpy.utils.register_class(hdr.SUNPOS_OT_ShowHdr)
bpy.app.handlers.frame_change_post.append(sun_calc.sun_handler)
def unregister():
bpy.utils.unregister_class(hdr.SUNPOS_OT_ShowHdr)
bpy.utils.unregister_class(ui_sun.SUNPOS_PT_Panel)
bpy.utils.unregister_class(ui_sun.SUNPOS_MT_Presets)
bpy.utils.unregister_class(ui_sun.SUNPOS_OT_DefaultPresets)
bpy.utils.unregister_class(ui_sun.SUNPOS_OT_AddPreset)
bpy.utils.unregister_class(properties.SunPosAddonPreferences)
del bpy.types.Scene.sun_pos_properties
bpy.utils.unregister_class(properties.SunPosProperties)
bpy.app.handlers.frame_change_post.remove(sun_calc.sun_handler)
#!/usr/bin/env python
#
# geo.py is a python module with no dependencies on extra packages,
# providing some convenience functions for working with geographic
# coordinates
#
# Copyright (C) 2010 Maximilian Hoegner <hp.maxi@hoegners.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
### Part one - Functions for dealing with points on a sphere ###
### Part two - A tolerant parser for position strings ###
import re
class Parser:
""" A parser class using regular expressions. """
def __init__(self):
self.patterns = {}
self.raw_patterns = {}
self.virtual = {}
def add(self, name, pattern, virtual=False):
""" Adds a new named pattern (regular expression) that can reference previously added patterns by %(pattern_name)s.
Virtual patterns can be used to make expressions more compact but don't show up in the parse tree. """
self.raw_patterns[name] = "(?:" + pattern + ")"
self.virtual[name] = virtual
try:
self.patterns[name] = ("(?:" + pattern + ")") % self.patterns
except KeyError as e:
raise (Exception, "Unknown pattern name: %s" % str(e))
def parse(self, pattern_name, text):
""" Parses 'text' with pattern 'pattern_name' and returns parse tree """
# build pattern with subgroups
sub_dict = {}
subpattern_names = []
for s in re.finditer("%\(.*?\)s", self.raw_patterns[pattern_name]):
subpattern_name = s.group()[2:-2]
if not self.virtual[subpattern_name]:
sub_dict[subpattern_name] = "(" + self.patterns[
subpattern_name] + ")"
subpattern_names.append(subpattern_name)
else:
sub_dict[subpattern_name] = self.patterns[subpattern_name]
pattern = "^" + (self.raw_patterns[pattern_name] % sub_dict) + "$"
# do matching
m = re.match(pattern, text)
if m == None:
return None
# build tree recursively by parsing subgroups
tree = {"TEXT": text}
for i in range(len(subpattern_names)):
text_part = m.group(i + 1)
if not text_part == None:
subpattern = subpattern_names[i]
tree[subpattern] = self.parse(subpattern, text_part)
return tree
position_parser = Parser()
position_parser.add("direction_ns", r"[NSns]")
position_parser.add("direction_ew", r"[EOWeow]")
position_parser.add("decimal_separator", r"[\.,]", True)
position_parser.add("sign", r"[+-]")
position_parser.add("nmea_style_degrees", r"[0-9]{2,}")
position_parser.add("nmea_style_minutes",
r"[0-9]{2}(?:%(decimal_separator)s[0-9]*)?")
position_parser.add(
"nmea_style", r"%(sign)s?\s*%(nmea_style_degrees)s%(nmea_style_minutes)s")
position_parser.add(
"number",
r"[0-9]+(?:%(decimal_separator)s[0-9]*)?|%(decimal_separator)s[0-9]+")
position_parser.add("plain_degrees", r"(?:%(sign)s\s*)?%(number)s")
position_parser.add("degree_symbol", r"°", True)
position_parser.add("minutes_symbol", r"'|′|`|´", True)
position_parser.add("seconds_symbol",
r"%(minutes_symbol)s%(minutes_symbol)s|″|\"",
True)
position_parser.add("degrees", r"%(number)s\s*%(degree_symbol)s")
position_parser.add("minutes", r"%(number)s\s*%(minutes_symbol)s")
position_parser.add("seconds", r"%(number)s\s*%(seconds_symbol)s")
position_parser.add(
"degree_coordinates",
"(?:%(sign)s\s*)?%(degrees)s(?:[+\s]*%(minutes)s)?(?:[+\s]*%(seconds)s)?|(?:%(sign)s\s*)%(minutes)s(?:[+\s]*%(seconds)s)?|(?:%(sign)s\s*)%(seconds)s"
)
position_parser.add(
"coordinates_ns",
r"%(nmea_style)s|%(plain_degrees)s|%(degree_coordinates)s")
position_parser.add(
"coordinates_ew",
r"%(nmea_style)s|%(plain_degrees)s|%(degree_coordinates)s")
position_parser.add(
"position", """\
\s*%(direction_ns)s\s*%(coordinates_ns)s[,;\s]*%(direction_ew)s\s*%(coordinates_ew)s\s*|\
\s*%(direction_ew)s\s*%(coordinates_ew)s[,;\s]*%(direction_ns)s\s*%(coordinates_ns)s\s*|\
\s*%(coordinates_ns)s\s*%(direction_ns)s[,;\s]*%(coordinates_ew)s\s*%(direction_ew)s\s*|\
\s*%(coordinates_ew)s\s*%(direction_ew)s[,;\s]*%(coordinates_ns)s\s*%(direction_ns)s\s*|\
\s*%(coordinates_ns)s[,;\s]+%(coordinates_ew)s\s*\
""")
def get_number(b):
""" Takes appropriate branch of parse tree and returns float. """
s = b["TEXT"].replace(",", ".")
return float(s)
def get_coordinate(b):
""" Takes appropriate branch of the parse tree and returns degrees as a float. """
r = 0.
if b.get("nmea_style"):
if b["nmea_style"].get("nmea_style_degrees"):
r += get_number(b["nmea_style"]["nmea_style_degrees"])
if b["nmea_style"].get("nmea_style_minutes"):
r += get_number(b["nmea_style"]["nmea_style_minutes"]) / 60.
if b["nmea_style"].get(
"sign") and b["nmea_style"]["sign"]["TEXT"] == "-":
r *= -1.
elif b.get("plain_degrees"):
r += get_number(b["plain_degrees"]["number"])
if b["plain_degrees"].get(
"sign") and b["plain_degrees"]["sign"]["TEXT"] == "-":
r *= -1.
elif b.get("degree_coordinates"):
if b["degree_coordinates"].get("degrees"):
r += get_number(b["degree_coordinates"]["degrees"]["number"])
if b["degree_coordinates"].get("minutes"):
r += get_number(b["degree_coordinates"]["minutes"]["number"]) / 60.
if b["degree_coordinates"].get("seconds"):
r += get_number(
b["degree_coordinates"]["seconds"]["number"]) / 3600.
if b["degree_coordinates"].get(
"sign") and b["degree_coordinates"]["sign"]["TEXT"] == "-":
r *= -1.
return r
def parse_position(s):
""" Takes a (utf8-encoded) string describing a position and returns a tuple of floats for latitude and longitude in degrees.
Tries to be as tolerant as possible with input. Returns None if parsing doesn't succeed. """
parse_tree = position_parser.parse("position", s)
if parse_tree == None: return None
lat_sign = +1.
if parse_tree.get(
"direction_ns") and parse_tree["direction_ns"]["TEXT"] in ("S",
"s"):
lat_sign = -1.
lon_sign = +1.
if parse_tree.get(
"direction_ew") and parse_tree["direction_ew"]["TEXT"] in ("W",
"w"):
lon_sign = -1.
lat = lat_sign * get_coordinate(parse_tree["coordinates_ns"])
lon = lon_sign * get_coordinate(parse_tree["coordinates_ew"])
return lat, lon
### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# -*- coding: utf-8 -*-
import bpy
import gpu
import bgl
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
from math import sqrt, pi, atan2, asin
vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
/* Keep in sync with intern/opencolorio/gpu_shader_display_transform_vertex.glsl */
in vec2 texCoord;
in vec2 pos;
out vec2 texCoord_interp;
void main()
{
gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f);
gl_Position.z = 1.0;
texCoord_interp = texCoord;
}'''
fragment_shader = '''
in vec2 texCoord_interp;
out vec4 fragColor;
uniform sampler2D image;
uniform float exposure;
void main()
{
fragColor = texture(image, texCoord_interp) * exposure;
}'''
# shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
def draw_callback_px(self, context):
nt = context.scene.world.node_tree.nodes
env_tex_node = nt.get(context.scene.sun_pos_properties.hdr_texture)
image = env_tex_node.image
if self.area != context.area:
return
if image.gl_load():
raise Exception()
bottom = 0
top = context.area.height
right = context.area.width
position = Vector((right, top)) / 2 + self.offset
scale = Vector((context.area.width, context.area.width / 2)) * self.scale
shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
coords = ((-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5))
uv_coords = ((0, 0), (1, 0), (1, 1), (0, 1))
batch = batch_for_shader(shader, 'TRI_FAN',
{"pos" : coords,
"texCoord" : uv_coords})
bgl.glActiveTexture(bgl.GL_TEXTURE0)
bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode)
with gpu.matrix.push_pop():
gpu.matrix.translate(position)
gpu.matrix.scale(scale)
shader.bind()
shader.uniform_int("image", 0)
shader.uniform_float("exposure", self.exposure)
batch.draw(shader)
# Crosshair
# vertical
coords = ((self.mouse_position[0], bottom), (self.mouse_position[0], top))
colors = ((1,)*4,)*2
shader = gpu.shader.from_builtin('2D_FLAT_COLOR')
batch = batch_for_shader(shader, 'LINES',
{"pos": coords, "color": colors})
shader.bind()
batch.draw(shader)
# horizontal
if bottom <= self.mouse_position[1] <= top:
coords = ((0, self.mouse_position[1]), (context.area.width, self.mouse_position[1]))
batch = batch_for_shader(shader, 'LINES',
{"pos": coords, "color": colors})
shader.bind()
batch.draw(shader)
class SUNPOS_OT_ShowHdr(bpy.types.Operator):
"""Tooltip"""
bl_idname = "world.sunpos_show_hdr"
bl_label = "Sync Sun to Texture"
exposure = 1.0
@classmethod
def poll(self, context):
sun_props = context.scene.sun_pos_properties
return sun_props.hdr_texture and sun_props.sun_object is not None
def update(self, context, event):
sun_props = context.scene.sun_pos_properties
mouse_position_abs = Vector((event.mouse_x, event.mouse_y))
# Get current area
for area in context.screen.areas:
# Compare absolute mouse position to area bounds
if (area.x < mouse_position_abs.x < area.x + area.width
and area.y < mouse_position_abs.y < area.y + area.height):
self.area = area
if area.type == 'VIEW_3D':
# Redraw all areas
area.tag_redraw()
if self.area.type == 'VIEW_3D':
self.top = self.area.height
self.right = self.area.width
nt = context.scene.world.node_tree.nodes
env_tex = nt.get(sun_props.hdr_texture)
# Mouse position relative to window
self.mouse_position = Vector((mouse_position_abs.x - self.area.x,
mouse_position_abs.y - self.area.y))
self.selected_point = (self.mouse_position - self.offset - Vector((self.right, self.top))/2) / self.scale
u = self.selected_point.x / self.area.width + 0.5
v = (self.selected_point.y) / (self.area.width / 2) + 0.5
# Set elevation and azimuth from selected point
if env_tex.projection == 'EQUIRECTANGULAR':
el = v * pi - pi/2
az = u * pi*2 - pi/2 + env_tex.texture_mapping.rotation.z
# Clamp elevation
el = max(el, -pi/2)
el = min(el, pi/2)
sun_props.hdr_elevation = el
sun_props.hdr_azimuth = az
elif env_tex.projection == 'MIRROR_BALL':
# Formula from intern/cycles/kernel/kernel_projection.h
# Point on sphere
dir = Vector()
# Normalize to -1, 1
dir.x = 2.0 * u - 1.0
dir.z = 2.0 * v - 1.0
# Outside bounds
if (dir.x * dir.x + dir.z * dir.z > 1.0):
dir = Vector()
else:
dir.y = -sqrt(max(1.0 - dir.x * dir.x - dir.z * dir.z, 0.0))
# Reflection
i = Vector((0.0, -1.0, 0.0))
dir = 2.0 * dir.dot(i) * dir - i
# Convert vector to euler
el = asin(dir.z)
az = atan2(dir.x, dir.y) + env_tex.texture_mapping.rotation.z
sun_props.hdr_elevation = el
sun_props.hdr_azimuth = az
else:
self.report({'ERROR'}, 'Unknown projection')
return {'CANCELLED'}
def pan(self, context, event):
self.offset += Vector((event.mouse_region_x - self.mouse_prev_x,
event.mouse_region_y - self.mouse_prev_y))
self.mouse_prev_x, self.mouse_prev_y = event.mouse_region_x, event.mouse_region_y
def modal(self, context, event):
self.area.tag_redraw()
if event.type == 'MOUSEMOVE':
if self.is_panning:
self.pan(context, event)
self.update(context, event)
# Confirm
elif event.type in {'LEFTMOUSE', 'RET'}:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
for area in context.screen.areas:
area.tag_redraw()
# Bind the environment texture to the sun
context.scene.sun_pos_properties.bind_to_sun = True
context.workspace.status_text_set(None)
return {'FINISHED'}
# Cancel
elif event.type in {'RIGHTMOUSE', 'ESC'}:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
for area in context.screen.areas:
area.tag_redraw()
# Reset previous values
context.scene.sun_pos_properties.hdr_elevation = self.initial_elevation
context.scene.sun_pos_properties.hdr_azimuth = self.initial_azimuth
context.workspace.status_text_set(None)
return {'CANCELLED'}
# Set exposure or zoom
elif event.type == 'WHEELUPMOUSE':
# Exposure
if event.ctrl:
self.exposure *= 1.1
# Zoom
else:
self.scale *= 1.1
self.offset -= (self.mouse_position - (Vector((self.right, self.top)) / 2 + self.offset)) / 10.0
self.update(context, event)
elif event.type == 'WHEELDOWNMOUSE':
# Exposure
if event.ctrl:
self.exposure /= 1.1
# Zoom
else:
self.scale /= 1.1
self.offset += (self.mouse_position - (Vector((self.right, self.top)) / 2 + self.offset)) / 11.0
self.update(context, event)
# Toggle pan
elif event.type == 'MIDDLEMOUSE':
if event.value == 'PRESS':
self.mouse_prev_x, self.mouse_prev_y = event.mouse_region_x, event.mouse_region_y
self.is_panning = True
elif event.value == 'RELEASE':
self.is_panning = False
else:
return {'PASS_THROUGH'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
self.is_panning = False
self.mouse_prev_x = 0.0
self.mouse_prev_y = 0.0
self.offset = Vector((0.0, 0.0))
self.scale = 1.0
# Get at least one 3D View
area_3d = None
for a in context.screen.areas:
if a.type == 'VIEW_3D':
area_3d = a
break
if area_3d is None:
self.report({'ERROR'}, 'Could not find 3D View')
return {'CANCELLED'}
nt = context.scene.world.node_tree.nodes
env_tex_node = nt.get(context.scene.sun_pos_properties.hdr_texture)
if env_tex_node.type != "TEX_ENVIRONMENT":
self.report({'ERROR'}, 'Please select an Environment Texture node')
return {'CANCELLED'}
self.area = context.area
self.mouse_position = event.mouse_region_x, event.mouse_region_y
self.initial_elevation = context.scene.sun_pos_properties.hdr_elevation
self.initial_azimuth = context.scene.sun_pos_properties.hdr_azimuth
context.workspace.status_text_set("Enter/LMB: confirm, Esc/RMB: cancel, MMB: pan, mouse wheel: zoom, Ctrl + mouse wheel: set exposure")
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px,
(self, context), 'WINDOW', 'POST_PIXEL')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
import bpy
import bgl
import math
import gpu
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
if bpy.app.background: # ignore north line in background mode
def north_update(self, context):
pass
else:
vertex_shader