diff options
author | Damien Picard <dam.pic@free.fr> | 2019-12-09 14:02:34 +0300 |
---|---|---|
committer | Damien Picard <dam.pic@free.fr> | 2019-12-09 14:03:58 +0300 |
commit | efbc5e5db7c73ae43ddc11abf8db8bc8f98c9945 (patch) | |
tree | 2f5a3fce49e89a35264abf0d2576deaf932ae446 /sun_position | |
parent | c5f0bbde29a9406c33ac84a04aa1ab6b2a27ff35 (diff) |
sun_position: move to release: T69936
Diffstat (limited to 'sun_position')
-rw-r--r-- | sun_position/__init__.py | 85 | ||||
-rw-r--r-- | sun_position/geo.py | 192 | ||||
-rw-r--r-- | sun_position/hdr.py | 303 | ||||
-rw-r--r-- | sun_position/north.py | 110 | ||||
-rw-r--r-- | sun_position/properties.py | 276 | ||||
-rw-r--r-- | sun_position/sun_calc.py | 589 | ||||
-rw-r--r-- | sun_position/ui_sun.py | 293 |
7 files changed, 1848 insertions, 0 deletions
diff --git a/sun_position/__init__.py b/sun_position/__init__.py new file mode 100644 index 00000000..de2dd858 --- /dev/null +++ b/sun_position/__init__.py @@ -0,0 +1,85 @@ +### 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) diff --git a/sun_position/geo.py b/sun_position/geo.py new file mode 100644 index 00000000..6d49f2ad --- /dev/null +++ b/sun_position/geo.py @@ -0,0 +1,192 @@ +#!/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 diff --git a/sun_position/hdr.py b/sun_position/hdr.py new file mode 100644 index 00000000..257daf58 --- /dev/null +++ b/sun_position/hdr.py @@ -0,0 +1,303 @@ +### 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'} diff --git a/sun_position/north.py b/sun_position/north.py new file mode 100644 index 00000000..44b4ee09 --- /dev/null +++ b/sun_position/north.py @@ -0,0 +1,110 @@ +### 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 = ''' + uniform mat4 u_ViewProjectionMatrix; + + in vec3 position; + + flat out vec2 v_StartPos; + out vec4 v_VertPos; + + void main() + { + vec4 pos = u_ViewProjectionMatrix * vec4(position, 1.0f); + gl_Position = pos; + v_StartPos = (pos / pos.w).xy; + v_VertPos = pos; + } + ''' + + fragment_shader = ''' + uniform vec4 u_Color; + + flat in vec2 v_StartPos; + in vec4 v_VertPos; + + uniform vec2 u_Resolution; + + void main() + { + vec4 vertPos_2d = v_VertPos / v_VertPos.w; + vec2 dir = (vertPos_2d.xy - v_StartPos.xy) * u_Resolution; + float dist = length(dir); + + if (step(sin(dist / 5.0f), 0.0) == 1) discard; + + gl_FragColor = u_Color; + } + ''' + + shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + + def draw_north_callback(): + # ------------------------------------------------------------------ + # Set up the compass needle using the current north offset angle + # less 90 degrees. This forces the unit circle to begin at the + # 12 O'clock instead of 3 O'clock position. + # ------------------------------------------------------------------ + sun_props = bpy.context.scene.sun_pos_properties + + color = (0.2, 0.6, 1.0, 0.7) + radius = 100 + angle = -(sun_props.north_offset - math.pi / 2) + x = math.cos(angle) * radius + y = math.sin(angle) * radius + + coords = Vector((x, y, 0)), Vector((0, 0, 0)) # Start & end of needle + + batch = batch_for_shader( + shader, 'LINE_STRIP', + {"position": coords}, + ) + shader.bind() + + matrix = bpy.context.region_data.perspective_matrix + shader.uniform_float("u_ViewProjectionMatrix", matrix) + shader.uniform_float("u_Resolution", (bpy.context.region.width, bpy.context.region.height)) + shader.uniform_float("u_Color", color) + bgl.glLineWidth(2.0) + batch.draw(shader) + + + _handle = None + + + def north_update(self, context): + global _handle + if self.show_north and _handle is None: + _handle = bpy.types.SpaceView3D.draw_handler_add(draw_north_callback, (), 'WINDOW', 'POST_VIEW') + elif _handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(_handle, 'WINDOW') + _handle = None + context.area.tag_redraw() diff --git a/sun_position/properties.py b/sun_position/properties.py new file mode 100644 index 00000000..5fbf1540 --- /dev/null +++ b/sun_position/properties.py @@ -0,0 +1,276 @@ +### 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 +from bpy.types import AddonPreferences, PropertyGroup +from bpy.props import (StringProperty, EnumProperty, IntProperty, + FloatProperty, BoolProperty, PointerProperty) + +from .sun_calc import sun_update, parse_coordinates +from .north import north_update + +from math import pi +from datetime import datetime +TODAY = datetime.today() + +############################################################################ +# Sun panel properties +############################################################################ + + +class SunPosProperties(PropertyGroup): + usage_mode: EnumProperty( + name="Usage mode", + description="Operate in normal mode or environment texture mode", + items=( + ('NORMAL', "Normal", ""), + ('HDR', "Sun + HDR texture", ""), + ), + default='NORMAL', + update=sun_update) + + use_daylight_savings: BoolProperty( + description="Daylight savings time adds 1 hour to standard time", + default=False, + update=sun_update) + + use_refraction: BoolProperty( + description="Show apparent sun position due to refraction", + default=True, + update=sun_update) + + show_north: BoolProperty( + description="Draw line pointing north", + default=False, + update=north_update) + + north_offset: FloatProperty( + name="North Offset", + description="Rotate the scene to choose North direction", + unit="ROTATION", + soft_min=-pi, soft_max=pi, step=10.0, default=0.0, + update=sun_update) + + latitude: FloatProperty( + name="Latitude", + description="Latitude: (+) Northern (-) Southern", + soft_min=-90.0, soft_max=90.0, + step=5, precision=3, + default=0.0, + update=sun_update) + + longitude: FloatProperty( + name="Longitude", + description="Longitude: (-) West of Greenwich (+) East of Greenwich", + soft_min=-180.0, soft_max=180.0, + step=5, precision=3, + default=0.0, + update=sun_update) + + co_parser: StringProperty( + name="Enter coordinates", + description="Enter coordinates from an online map", + update=parse_coordinates) + + month: IntProperty( + name="Month", + min=1, max=12, default=TODAY.month, + update=sun_update) + + day: IntProperty( + name="Day", + min=1, max=31, default=TODAY.day, + update=sun_update) + + year: IntProperty( + name="Year", + min=1800, max=4000, default=TODAY.year, + update=sun_update) + + use_day_of_year: BoolProperty( + description="Use a single value for day of year", + name="Use day of year", + default=False, + update=sun_update) + + day_of_year: IntProperty( + name="Day of year", + min=1, max=366, default=1, + update=sun_update) + + UTC_zone: FloatProperty( + name="UTC zone", + description="Time zone: Difference from Greenwich, England in hours", + precision=1, + min=-14.0, max=13, step=50, default=0.0, + update=sun_update) + + time: FloatProperty( + name="Time", + description="Time of the day", + precision=4, + soft_min=0.0, soft_max=23.9999, step=1.0, default=12.0, + update=sun_update) + + sun_distance: FloatProperty( + name="Distance", + description="Distance to sun from origin", + unit="LENGTH", + min=0.0, soft_max=3000.0, step=10.0, default=50.0, + update=sun_update) + + use_sun_object: BoolProperty( + description="Enable sun positioning of light object", + default=False, + update=sun_update) + + sun_object: PointerProperty( + type=bpy.types.Object, + description="Sun object to set in the scene", + poll=lambda self, obj: obj.type == 'LIGHT', + update=sun_update) + + use_object_collection: BoolProperty( + description="Allow a collection of objects to be positioned", + default=False, + update=sun_update) + + object_collection: PointerProperty( + type=bpy.types.Collection, + description="Collection of objects used for analemma", + update=sun_update) + + object_collection_type: EnumProperty( + name="Display type", + description="Show object group on ecliptic or as analemma", + items=( + ('ECLIPTIC', "On the Ecliptic", ""), + ('ANALEMMA', "As Analemma", ""), + ), + default='ECLIPTIC', + update=sun_update) + + use_sky_texture: BoolProperty( + description="Enable use of Cycles' " + "sky texture. World nodes must be enabled, " + "then set color to Sky Texture", + default=False, + update=sun_update) + + sky_texture: StringProperty( + default="Sky Texture", + name="Sky Texture", + description="Name of sky texture to be used", + update=sun_update) + + hdr_texture: StringProperty( + default="Environment Texture", + name="Environment Texture", + description="Name of texture to use. World nodes must be enabled " + "and color set to Environment Texture", + update=sun_update) + + hdr_azimuth: FloatProperty( + name="Rotation", + description="Rotation angle of sun and environment texture", + unit="ROTATION", + step=10.0, + default=0.0, precision=3, + update=sun_update) + + hdr_elevation: FloatProperty( + name="Elevation", + description="Elevation angle of sun", + unit="ROTATION", + step=10.0, + default=0.0, precision=3, + update=sun_update) + + bind_to_sun: BoolProperty( + description="If true, Environment texture moves with sun", + default=False, + update=sun_update) + + time_spread: FloatProperty( + name="Time Spread", + description="Time period in which to spread object group", + precision=4, + soft_min=1.0, soft_max=24.0, step=1.0, default=23.0, + update=sun_update) + + +############################################################################ +# Preference panel properties +############################################################################ + + +class SunPosAddonPreferences(AddonPreferences): + bl_idname = __package__ + + show_time_place: BoolProperty( + description="Show time/place presets", + default=False) + + show_object_collection: BoolProperty( + description="Use object collection", + default=True, + update=sun_update) + + show_dms: BoolProperty( + description="Show lat/long degrees, minutes, seconds labels", + default=True) + + show_north: BoolProperty( + description="Show north offset choice and slider", + default=True, + update=sun_update) + + show_refraction: BoolProperty( + description="Show sun refraction choice", + default=True, + update=sun_update) + + show_az_el: BoolProperty( + description="Show azimuth and solar elevation info", + default=True) + + show_daylight_savings: BoolProperty( + description="Show daylight savings time choice", + default=True, + update=sun_update) + + show_rise_set: BoolProperty( + description="Show sunrise and sunset", + default=True) + + def draw(self, context): + layout = self.layout + + box = layout.box() + col = box.column() + + col.label(text="Show or use:") + flow = col.grid_flow(columns=0, even_columns=True, even_rows=False, align=False) + flow.prop(self, "show_time_place", text="Time/place presets") + flow.prop(self, "show_object_collection", text="Use collection") + flow.prop(self, "show_dms", text="D° M' S\"") + flow.prop(self, "show_north", text="North offset") + flow.prop(self, "show_refraction", text="Refraction") + flow.prop(self, "show_az_el", text="Azimuth, elevation") + flow.prop(self, "show_daylight_savings", text="Daylight savings time") + flow.prop(self, "show_rise_set", text="Sunrise, sunset") diff --git a/sun_position/sun_calc.py b/sun_position/sun_calc.py new file mode 100644 index 00000000..0813fd12 --- /dev/null +++ b/sun_position/sun_calc.py @@ -0,0 +1,589 @@ +### 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 +from bpy.app.handlers import persistent +from mathutils import Euler +import math +from math import degrees, radians, pi +import datetime +from .geo import parse_position + + +############################################################################ +# +# SunClass is used for storing intermediate sun calculations. +# +############################################################################ + +class SunClass: + + class TazEl: + time = 0.0 + azimuth = 0.0 + elevation = 0.0 + + class CLAMP: + elevation = 0.0 + azimuth = 0.0 + az_start_sun = 0.0 + az_start_env = 0.0 + + sunrise = TazEl() + sunset = TazEl() + solar_noon = TazEl() + rise_set_ok = False + + bind = CLAMP() + bind_to_sun = False + + latitude = 0.0 + longitude = 0.0 + elevation = 0.0 + azimuth = 0.0 + + month = 0 + day = 0 + year = 0 + day_of_year = 0 + time = 0.0 + + UTC_zone = 0 + sun_distance = 0.0 + use_daylight_savings = False + + +sun = SunClass() + + +def sun_update(self, context): + update_time(context) + move_sun(context) + +def parse_coordinates(self, context): + error_message = "ERROR: Could not parse coordinates" + sun_props = context.scene.sun_pos_properties + + if sun_props.co_parser: + parsed_co = parse_position(sun_props.co_parser) + + if parsed_co is not None and len(parsed_co) == 2: + sun_props.latitude, sun_props.longitude = parsed_co + elif sun_props.co_parser != error_message: + sun_props.co_parser = error_message + + # Clear prop + if sun_props.co_parser not in {'', error_message}: + sun_props.co_parser = '' + +@persistent +def sun_handler(scene): + update_time(bpy.context) + move_sun(bpy.context) + + +############################################################################ +# +# move_sun() will cycle through all the selected objects +# and call set_sun_position and set_sun_rotations +# to place them in the sky. +# +############################################################################ + + +def move_sun(context): + addon_prefs = context.preferences.addons[__package__].preferences + sun_props = context.scene.sun_pos_properties + + if sun_props.usage_mode == "HDR": + nt = context.scene.world.node_tree.nodes + env_tex = nt.get(sun_props.hdr_texture) + + if sun.bind_to_sun != sun_props.bind_to_sun: + # bind_to_sun was just toggled + sun.bind_to_sun = sun_props.bind_to_sun + sun.bind.az_start_sun = sun_props.hdr_azimuth + if env_tex: + sun.bind.az_start_env = env_tex.texture_mapping.rotation.z + + if env_tex and sun_props.bind_to_sun: + az = sun_props.hdr_azimuth - sun.bind.az_start_sun + sun.bind.az_start_env + env_tex.texture_mapping.rotation.z = az + + if sun_props.sun_object: + sun.theta = math.pi / 2 - sun_props.hdr_elevation + sun.phi = -sun_props.hdr_azimuth + + obj = sun_props.sun_object + set_sun_position(obj, sun_props.sun_distance) + rotation_euler = Euler((sun_props.hdr_elevation - pi/2, + 0, -sun_props.hdr_azimuth)) + + set_sun_rotations(obj, rotation_euler) + return + + local_time = sun_props.time + zone = -sun_props.UTC_zone + sun.use_daylight_savings = sun_props.use_daylight_savings + if sun.use_daylight_savings: + zone -= 1 + + north_offset = degrees(sun_props.north_offset) + + if addon_prefs.show_rise_set: + calc_sunrise_sunset(rise=True) + calc_sunrise_sunset(rise=False) + + get_sun_position(local_time, sun_props.latitude, sun_props.longitude, + north_offset, zone, sun_props.month, sun_props.day, sun_props.year, + sun_props.sun_distance) + + if sun_props.use_sky_texture and sun_props.sky_texture: + sky_node = bpy.context.scene.world.node_tree.nodes.get(sun_props.sky_texture) + if sky_node is not None and sky_node.type == "TEX_SKY": + locX = math.sin(sun.phi) * math.sin(-sun.theta) + locY = math.sin(sun.theta) * math.cos(sun.phi) + locZ = math.cos(sun.theta) + sky_node.texture_mapping.rotation.z = 0.0 + sky_node.sun_direction = locX, locY, locZ + + # Sun object + if ((sun_props.use_sun_object or sun_props.usage_mode == 'HDR') + and sun_props.sun_object + and sun_props.sun_object.name in context.view_layer.objects): + obj = sun_props.sun_object + set_sun_position(obj, sun_props.sun_distance) + rotation_euler = Euler((math.radians(sun.elevation - 90), 0, + math.radians(-sun.az_north))) + set_sun_rotations(obj, rotation_euler) + + # Sun collection + if (addon_prefs.show_object_collection + and sun_props.use_object_collection + and sun_props.object_collection): + sun_objects = sun_props.object_collection.objects + object_count = len(sun_objects) + if sun_props.object_collection_type == 'ECLIPTIC': + # Ecliptic + if object_count > 1: + time_increment = sun_props.time_spread / (object_count - 1) + local_time = local_time + time_increment * (object_count - 1) + else: + time_increment = sun_props.time_spread + + for obj in sun_objects: + get_sun_position(local_time, sun_props.latitude, + sun_props.longitude, north_offset, zone, + sun_props.month, sun_props.day, + sun_props.year, sun_props.sun_distance) + set_sun_position(obj, sun_props.sun_distance) + local_time -= time_increment + obj.rotation_euler = ( + (math.radians(sun.elevation - 90), 0, + math.radians(-sun.az_north))) + else: + # Analemma + day_increment = 365 / object_count + day = sun_props.day_of_year + day_increment * (object_count - 1) + for obj in sun_objects: + dt = (datetime.date(sun_props.year, 1, 1) + + datetime.timedelta(day - 1)) + get_sun_position(local_time, sun_props.latitude, + sun_props.longitude, north_offset, zone, + dt.month, dt.day, sun_props.year, + sun_props.sun_distance) + set_sun_position(obj, sun_props.sun_distance) + day -= day_increment + obj.rotation_euler = ( + (math.radians(sun.elevation - 90), 0, + math.radians(-sun.az_north))) + +def update_time(context): + sun_props = context.scene.sun_pos_properties + + if not sun_props.use_day_of_year: + dt = datetime.date(sun_props.year, sun_props.month, sun_props.day) + day_of_year = dt.timetuple().tm_yday + if sun_props.day_of_year != day_of_year: + sun_props.day_of_year = day_of_year + sun.day = sun_props.day + sun.month = sun_props.month + sun.day_of_year = day_of_year + else: + dt = (datetime.date(sun_props.year, 1, 1) + + datetime.timedelta(sun_props.day_of_year - 1)) + sun.day = dt.day + sun.month = dt.month + sun.day_of_year = sun_props.day_of_year + if sun_props.day != dt.day: + sun_props.day = dt.day + if sun_props.month != dt.month: + sun_props.month = dt.month + sun.year = sun_props.year + sun.longitude = sun_props.longitude + sun.latitude = sun_props.latitude + sun.UTC_zone = sun_props.UTC_zone + + +def format_time(the_time, daylight_savings, longitude, UTC_zone=None): + if UTC_zone is not None: + if daylight_savings: + UTC_zone += 1 + the_time -= UTC_zone + + the_time %= 24 + + hh = int(the_time) + mm = (the_time - int(the_time)) * 60 + ss = int((mm - int(mm)) * 60) + + return ("%02i:%02i:%02i" % (hh, mm, ss)) + + +def format_hms(the_time): + hh = str(int(the_time)) + min = (the_time - int(the_time)) * 60 + sec = int((min - int(min)) * 60) + mm = "0" + str(int(min)) if min < 10 else str(int(min)) + ss = "0" + str(sec) if sec < 10 else str(sec) + + return (hh + ":" + mm + ":" + ss) + + +def format_lat_long(lat_long, is_latitude): + hh = str(abs(int(lat_long))) + min = abs((lat_long - int(lat_long)) * 60) + sec = abs(int((min - int(min)) * 60)) + mm = "0" + str(int(min)) if min < 10 else str(int(min)) + ss = "0" + str(sec) if sec < 10 else str(sec) + if lat_long == 0: + coord_tag = " " + else: + if is_latitude: + coord_tag = " N" if lat_long > 0 else " S" + else: + coord_tag = " E" if lat_long > 0 else " W" + + return hh + "° " + mm + "' " + ss + '"' + coord_tag + + + + +############################################################################ +# +# Calculate the actual position of the sun based on input parameters. +# +# The sun positioning algorithms below 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 calculations of this script can be verified with those of NOAA's +# using the Azimuth and Solar Elevation displayed in the SunPos_Panel. +# NOAA's web site is: +# http://www.esrl.noaa.gov/gmd/grad/solcalc +############################################################################ + + +def get_sun_position(local_time, latitude, longitude, north_offset, + utc_zone, month, day, year, distance): + + addon_prefs = bpy.context.preferences.addons[__package__].preferences + sun_props = bpy.context.scene.sun_pos_properties + + longitude *= -1 # for internal calculations + utc_time = local_time + utc_zone # Set Greenwich Meridian Time + + if latitude > 89.93: # Latitude 90 and -90 gives + latitude = radians(89.93) # erroneous results so nudge it + elif latitude < -89.93: + latitude = radians(-89.93) + else: + latitude = radians(latitude) + + t = julian_time_from_y2k(utc_time, year, month, day) + + e = radians(obliquity_correction(t)) + L = apparent_longitude_of_sun(t) + solar_dec = sun_declination(e, L) + eqtime = calc_equation_of_time(t) + + time_correction = (eqtime - 4 * longitude) + 60 * utc_zone + true_solar_time = ((utc_time - utc_zone) * 60.0 + time_correction) % 1440 + + hour_angle = true_solar_time / 4.0 - 180.0 + if hour_angle < -180.0: + hour_angle += 360.0 + + csz = (math.sin(latitude) * math.sin(solar_dec) + + math.cos(latitude) * math.cos(solar_dec) * + math.cos(radians(hour_angle))) + if csz > 1.0: + csz = 1.0 + elif csz < -1.0: + csz = -1.0 + + zenith = math.acos(csz) + + az_denom = math.cos(latitude) * math.sin(zenith) + + if abs(az_denom) > 0.001: + az_rad = ((math.sin(latitude) * + math.cos(zenith)) - math.sin(solar_dec)) / az_denom + if abs(az_rad) > 1.0: + az_rad = -1.0 if (az_rad < 0.0) else 1.0 + azimuth = 180.0 - degrees(math.acos(az_rad)) + if hour_angle > 0.0: + azimuth = -azimuth + else: + azimuth = 180.0 if (latitude > 0.0) else 0.0 + + if azimuth < 0.0: + azimuth = azimuth + 360.0 + + exoatm_elevation = 90.0 - degrees(zenith) + + if sun_props.use_refraction: + if exoatm_elevation > 85.0: + refraction_correction = 0.0 + else: + te = math.tan(radians(exoatm_elevation)) + if exoatm_elevation > 5.0: + refraction_correction = ( + 58.1 / te - 0.07 / (te ** 3) + 0.000086 / (te ** 5)) + elif (exoatm_elevation > -0.575): + s1 = (-12.79 + exoatm_elevation * 0.711) + s2 = (103.4 + exoatm_elevation * (s1)) + s3 = (-518.2 + exoatm_elevation * (s2)) + refraction_correction = 1735.0 + exoatm_elevation * (s3) + else: + refraction_correction = -20.774 / te + + refraction_correction = refraction_correction / 3600 + solar_elevation = 90.0 - (degrees(zenith) - refraction_correction) + + else: + solar_elevation = 90.0 - degrees(zenith) + + solar_azimuth = azimuth + solar_azimuth += north_offset + + sun.az_north = solar_azimuth + + sun.theta = math.pi / 2 - radians(solar_elevation) + sun.phi = radians(solar_azimuth) * -1 + sun.azimuth = azimuth + sun.elevation = solar_elevation + + +def set_sun_position(obj, distance): + locX = math.sin(sun.phi) * math.sin(-sun.theta) * distance + locY = math.sin(sun.theta) * math.cos(sun.phi) * distance + locZ = math.cos(sun.theta) * distance + + #---------------------------------------------- + # Update selected object in viewport + #---------------------------------------------- + obj.location = locX, locY, locZ + + +def set_sun_rotations(obj, rotation_euler): + rotation_quaternion = rotation_euler.to_quaternion() + obj.rotation_quaternion = rotation_quaternion + + if obj.rotation_mode in {'XZY', 'YXZ', 'YZX', 'ZXY','ZYX'}: + obj.rotation_euler = rotation_quaternion.to_euler(obj.rotation_mode) + else: + obj.rotation_euler = rotation_euler + + rotation_axis_angle = obj.rotation_quaternion.to_axis_angle() + obj.rotation_axis_angle = (rotation_axis_angle[1], + *rotation_axis_angle[0]) + + +def calc_sunrise_set_UTC(rise, jd, latitude, longitude): + t = calc_time_julian_cent(jd) + eq_time = calc_equation_of_time(t) + solar_dec = calc_sun_declination(t) + hour_angle = calc_hour_angle_sunrise(latitude, solar_dec) + if not rise: + hour_angle = -hour_angle + delta = longitude + degrees(hour_angle) + time_UTC = 720 - (4.0 * delta) - eq_time + return time_UTC + + +def calc_sun_declination(t): + e = radians(obliquity_correction(t)) + L = apparent_longitude_of_sun(t) + solar_dec = sun_declination(e, L) + return solar_dec + + +def calc_hour_angle_sunrise(lat, solar_dec): + lat_rad = radians(lat) + HAarg = (math.cos(radians(90.833)) / + (math.cos(lat_rad) * math.cos(solar_dec)) + - math.tan(lat_rad) * math.tan(solar_dec)) + if HAarg < -1.0: + HAarg = -1.0 + elif HAarg > 1.0: + HAarg = 1.0 + HA = math.acos(HAarg) + return HA + + +def calc_solar_noon(jd, longitude, timezone, dst): + t = calc_time_julian_cent(jd - longitude / 360.0) + eq_time = calc_equation_of_time(t) + noon_offset = 720.0 - (longitude * 4.0) - eq_time + newt = calc_time_julian_cent(jd + noon_offset / 1440.0) + eq_time = calc_equation_of_time(newt) + + nv = 780.0 if dst else 720.0 + noon_local = (nv- (longitude * 4.0) - eq_time + (timezone * 60.0)) % 1440 + sun.solar_noon.time = noon_local / 60.0 + + +def calc_sunrise_sunset(rise): + zone = -sun.UTC_zone + + jd = get_julian_day(sun.year, sun.month, sun.day) + time_UTC = calc_sunrise_set_UTC(rise, jd, sun.latitude, sun.longitude) + new_time_UTC = calc_sunrise_set_UTC(rise, jd + time_UTC / 1440.0, + sun.latitude, sun.longitude) + time_local = new_time_UTC + (-zone * 60.0) + tl = time_local / 60.0 + get_sun_position(tl, sun.latitude, sun.longitude, 0.0, + zone, sun.month, sun.day, sun.year, + sun.sun_distance) + if sun.use_daylight_savings: + time_local += 60.0 + tl = time_local / 60.0 + tl %= 24.0 + if rise: + sun.sunrise.time = tl + sun.sunrise.azimuth = sun.azimuth + sun.sunrise.elevation = sun.elevation + calc_solar_noon(jd, sun.longitude, -zone, sun.use_daylight_savings) + get_sun_position(sun.solar_noon.time, sun.latitude, sun.longitude, + 0.0, zone, sun.month, sun.day, sun.year, + sun.sun_distance) + sun.solar_noon.elevation = sun.elevation + else: + sun.sunset.time = tl + sun.sunset.azimuth = sun.azimuth + sun.sunset.elevation = sun.elevation + +########################################################################## +## Get the elapsed julian time since 1/1/2000 12:00 gmt +## Y2k epoch (1/1/2000 12:00 gmt) is Julian day 2451545.0 +########################################################################## + + +def julian_time_from_y2k(utc_time, year, month, day): + century = 36525.0 # Days in Julian Century + epoch = 2451545.0 # Julian Day for 1/1/2000 12:00 gmt + jd = get_julian_day(year, month, day) + return ((jd + (utc_time / 24)) - epoch) / century + + +def get_julian_day(year, month, day): + if month <= 2: + year -= 1 + month += 12 + A = math.floor(year / 100) + B = 2 - A + math.floor(A / 4.0) + jd = (math.floor((365.25 * (year + 4716.0))) + + math.floor(30.6001 * (month + 1)) + day + B - 1524.5) + return jd + + +def calc_time_julian_cent(jd): + t = (jd - 2451545.0) / 36525.0 + return t + + +def sun_declination(e, L): + return (math.asin(math.sin(e) * math.sin(L))) + + +def calc_equation_of_time(t): + epsilon = obliquity_correction(t) + ml = radians(mean_longitude_sun(t)) + e = eccentricity_earth_orbit(t) + m = radians(mean_anomaly_sun(t)) + y = math.tan(radians(epsilon) / 2.0) + y = y * y + sin2ml = math.sin(2.0 * ml) + cos2ml = math.cos(2.0 * ml) + sin4ml = math.sin(4.0 * ml) + sinm = math.sin(m) + sin2m = math.sin(2.0 * m) + etime = (y * sin2ml - 2.0 * e * sinm + 4.0 * e * y * + sinm * cos2ml - 0.5 * y ** 2 * sin4ml - 1.25 * e ** 2 * sin2m) + return (degrees(etime) * 4) + + +def obliquity_correction(t): + ec = obliquity_of_ecliptic(t) + omega = 125.04 - 1934.136 * t + return (ec + 0.00256 * math.cos(radians(omega))) + + +def obliquity_of_ecliptic(t): + return ((23.0 + 26.0 / 60 + (21.4480 - 46.8150) / 3600 * t - + (0.00059 / 3600) * t ** 2 + (0.001813 / 3600) * t ** 3)) + + +def true_longitude_of_sun(t): + return (mean_longitude_sun(t) + equation_of_sun_center(t)) + + +def calc_sun_apparent_long(t): + o = true_longitude_of_sun(t) + omega = 125.04 - 1934.136 * t + lamb = o - 0.00569 - 0.00478 * math.sin(radians(omega)) + return lamb + + +def apparent_longitude_of_sun(t): + return (radians(true_longitude_of_sun(t) - 0.00569 - 0.00478 * + math.sin(radians(125.04 - 1934.136 * t)))) + + +def mean_longitude_sun(t): + return (280.46646 + 36000.76983 * t + 0.0003032 * t ** 2) % 360 + + +def equation_of_sun_center(t): + m = radians(mean_anomaly_sun(t)) + c = ((1.914602 - 0.004817 * t - 0.000014 * t ** 2) * math.sin(m) + + (0.019993 - 0.000101 * t) * math.sin(m * 2) + + 0.000289 * math.sin(m * 3)) + return c + + +def mean_anomaly_sun(t): + return (357.52911 + t * (35999.05029 - 0.0001537 * t)) + + +def eccentricity_earth_orbit(t): + return (0.016708634 - 0.000042037 * t - 0.0000001267 * t ** 2) diff --git a/sun_position/ui_sun.py b/sun_position/ui_sun.py new file mode 100644 index 00000000..dc809c8e --- /dev/null +++ b/sun_position/ui_sun.py @@ -0,0 +1,293 @@ +### 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 +from bpy.types import Operator, Menu +from bl_operators.presets import AddPresetBase +import os + +from .sun_calc import (format_lat_long, format_time, format_hms, sun) + + +# ------------------------------------------------------------------- +# Choice list of places, month and day at 12:00 noon +# ------------------------------------------------------------------- + + +class SUNPOS_MT_Presets(Menu): + bl_label = "Sun Position Presets" + preset_subdir = "operator/sun_position" + preset_operator = "script.execute_preset" + draw = Menu.draw_preset + + +class SUNPOS_OT_AddPreset(AddPresetBase, Operator): + '''Add Sun Position preset''' + bl_idname = "world.sunpos_add_preset" + bl_label = "Add Sun Position preset" + preset_menu = "SUNPOS_MT_Presets" + + # variable used for all preset values + preset_defines = [ + "sun_props = bpy.context.scene.sun_pos_properties" + ] + + # properties to store in the preset + preset_values = [ + "sun_props.day", + "sun_props.month", + "sun_props.time", + "sun_props.year", + "sun_props.UTC_zone", + "sun_props.use_daylight_savings", + "sun_props.latitude", + "sun_props.longitude", + ] + + # where to store the preset + preset_subdir = "operator/sun_position" + + +class SUNPOS_OT_DefaultPresets(Operator): + '''Copy Sun Position default presets''' + bl_idname = "world.sunpos_default_presets" + bl_label = "Copy Sun Position default presets" + + def execute(self, context): + preset_dirpath = bpy.utils.user_resource('SCRIPTS', path="presets/operator/sun_position", create=True) + # [month, day, time, UTC, lat, lon, dst] + presets = {"chongqing.py": [10, 1, 7.18, 8, 29.5583, 106.567, False], + "sao_paulo.py": [9, 7, 12.0, -3, -23.55, -46.6333, False], + "kinshasa.py": [6, 30, 12.0, 1, -4.325, 15.3222, False], + "london.py": [6, 11, 12.0, 0, 51.5072, -0.1275, True], + "new_york.py": [7, 4, 12.0, -5, 40.6611, -73.9439, True], + "sydney.py": [1, 26, 17.6, 10, -33.865, 151.209, False]} + + script = '''import bpy +sun_props = bpy.context.scene.sun_pos_properties + +sun_props.month = {:d} +sun_props.day = {:d} +sun_props.time = {:f} +sun_props.UTC_zone = {:d} +sun_props.latitude = {:f} +sun_props.longitude = {:f} +sun_props.use_daylight_savings = {} +''' + + for path, p in presets.items(): + print(p) + with open(os.path.join(preset_dirpath, path), 'w') as f: + f.write(script.format(*p)) + + return {'FINISHED'} + +# ------------------------------------------------------------------- +# +# Draw the Sun Panel, sliders, et. al. +# +# ------------------------------------------------------------------- + +class SUNPOS_PT_Panel(bpy.types.Panel): + bl_idname = "SUNPOS_PT_world" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "world" + bl_label = "Sun Position" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + sp = context.scene.sun_pos_properties + p = context.preferences.addons[__package__].preferences + layout = self.layout + self.draw_panel(context, sp, p, layout) + + def draw_panel(self, context, sp, p, layout): + self.layout.label(text="Usage mode:") + self.layout.prop(sp, "usage_mode", expand=True) + if sp.usage_mode == "HDR": + self.draw_environ_mode_panel(context, sp, p, layout) + else: + self.draw_normal_mode_panel(context, sp, p, layout) + + def draw_environ_mode_panel(self, context, sp, p, layout): + box = self.layout.box() + flow = box.grid_flow(row_major=True, columns=0, even_columns=True, + even_rows=False, align=False) + + col = flow.column() + col.label(text="Environment texture:") + col.prop_search(sp, "hdr_texture", + context.scene.world.node_tree, "nodes", text="") + col.separator() + + col = flow.column() + col.label(text="Sun object:") + col.prop_search(sp, "sun_object", + context.view_layer, "objects", text="") + col.separator() + + col = flow.column(align=True) + col.prop(sp, "sun_distance") + if not sp.bind_to_sun: + col.prop(sp, "hdr_elevation") + col.prop(sp, "hdr_azimuth") + col.separator() + + col = flow.column(align=True) + row1 = col.row() + if sp.bind_to_sun: + prop_text="Release binding" + else: + prop_text="Bind Texture to Sun " + row1.prop(sp, "bind_to_sun", toggle=True, icon="CONSTRAINT", + text=prop_text) + + row = col.row() + row.enabled = not sp.bind_to_sun + row.operator("world.sunpos_show_hdr", icon='LIGHT_SUN') + + def draw_normal_mode_panel(self, context, sp, p, layout): + if p.show_time_place: + row = layout.row(align=True) + row.menu(SUNPOS_MT_Presets.__name__, text=SUNPOS_MT_Presets.bl_label) + row.operator(SUNPOS_OT_AddPreset.bl_idname, text="", icon='ADD') + row.operator(SUNPOS_OT_AddPreset.bl_idname, text="", icon='REMOVE').remove_active = True + row.operator(SUNPOS_OT_DefaultPresets.bl_idname, text="", icon='FILE_REFRESH') + + box = self.layout.box() + flow = box.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.prop(sp, "use_sky_texture", text="Cycles sky") + if sp.use_sky_texture: + col.prop_search(sp, "sky_texture", context.scene.world.node_tree, + "nodes", text="") + col.separator() + + col = flow.column() + col.prop(sp, "use_sun_object", text="Use object") + if sp.use_sun_object: + col.prop(sp, "sun_object", text="") + col.separator() + + col = flow.column() + if p.show_object_collection: + col.prop(sp, "use_object_collection", text="Use collection") + if sp.use_object_collection: + col.prop(sp, "object_collection", text="") + if sp.object_collection: + col.prop(sp, "object_collection_type") + if sp.object_collection_type == 'ECLIPTIC': + col.prop(sp, "time_spread") + + box = self.layout.box() + + col = box.column(align=True) + col.label(text="Enter coordinates:") + col.prop(sp, "co_parser", text='', icon='URL') + + box.separator() + + flow = box.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + + col = flow.column(align=True) + col.prop(sp, "latitude") + if p.show_dms: + row = col.row() + row.alignment = 'RIGHT' + row.label(text=format_lat_long(sp.latitude, True)) + + col = flow.column(align=True) + col.prop(sp, "longitude") + if p.show_dms: + row = col.row() + row.alignment = 'RIGHT' + row.label(text=format_lat_long(sp.longitude, False)) + col.separator() + + if p.show_north: + col = flow.column(align=True) + col.prop(sp, "show_north", text="Show North", toggle=True) + col.prop(sp, "north_offset") + col.separator() + + if p.show_az_el: + col = flow.column(align=True) + row = col.row() + row.alignment = 'RIGHT' + row.label(text="Azimuth: " + + str(round(sun.azimuth, 3)) + "°") + row = col.row() + row.alignment = 'RIGHT' + row.label(text="Elevation: " + + str(round(sun.elevation, 3)) + "°") + col.separator() + + if p.show_refraction: + col = flow.column() + col.prop(sp, "use_refraction", text="Show refraction") + col.separator() + + col = flow.column() + col.prop(sp, "sun_distance") + + + box = self.layout.box() + flow = box.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + + col = flow.column(align=True) + col.prop(sp, "use_day_of_year", + icon='SORTTIME') + if sp.use_day_of_year: + col.prop(sp, "day_of_year") + else: + col.prop(sp, "month") + col.prop(sp, "day") + col.prop(sp, "year") + col.separator() + + col = flow.column(align=True) + col.prop(sp, "time") + col.prop(sp, "UTC_zone") + if p.show_daylight_savings: + col.prop(sp, "use_daylight_savings", text="Daylight Savings") + col.separator() + + lt = format_time(sp.time, + p.show_daylight_savings and sp.use_daylight_savings, + sp.longitude) + ut = format_time(sp.time, + p.show_daylight_savings and sp.use_daylight_savings, + sp.longitude, + sp.UTC_zone) + col = flow.column(align=True) + col.alignment = 'CENTER' + col.label(text="Local: " + lt, icon='TIME') + col.label(text=" UTC: " + ut, icon='PREVIEW_RANGE') + col.separator() + + col = flow.column(align=True) + col.alignment = 'CENTER' + if p.show_rise_set: + sr = format_hms(sun.sunrise.time) + ss = format_hms(sun.sunset.time) + tsr = "Sunrise: " + sr + tss = " Sunset: " + ss + col.label(text=tsr, icon='LIGHT_SUN') + col.label(text=tss, icon='SOLO_ON') |