1 ### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 from bpy
.app
.handlers
import persistent
21 from mathutils
import Euler
23 from math
import degrees
, radians
, pi
25 from .geo
import parse_position
28 ############################################################################
30 # SunClass is used for storing intermediate sun calculations.
32 ############################################################################
68 use_daylight_savings
= False
74 def sun_update(self
, context
):
78 def parse_coordinates(self
, context
):
79 error_message
= "ERROR: Could not parse coordinates"
80 sun_props
= context
.scene
.sun_pos_properties
82 if sun_props
.co_parser
:
83 parsed_co
= parse_position(sun_props
.co_parser
)
85 if parsed_co
is not None and len(parsed_co
) == 2:
86 sun_props
.latitude
, sun_props
.longitude
= parsed_co
87 elif sun_props
.co_parser
!= error_message
:
88 sun_props
.co_parser
= error_message
91 if sun_props
.co_parser
not in {'', error_message
}:
92 sun_props
.co_parser
= ''
95 def sun_handler(scene
):
96 update_time(bpy
.context
)
100 ############################################################################
102 # move_sun() will cycle through all the selected objects
103 # and call set_sun_position and set_sun_rotations
104 # to place them in the sky.
106 ############################################################################
109 def move_sun(context
):
110 addon_prefs
= context
.preferences
.addons
[__package__
].preferences
111 sun_props
= context
.scene
.sun_pos_properties
113 if sun_props
.usage_mode
== "HDR":
114 nt
= context
.scene
.world
.node_tree
.nodes
115 env_tex
= nt
.get(sun_props
.hdr_texture
)
117 if sun
.bind_to_sun
!= sun_props
.bind_to_sun
:
118 # bind_to_sun was just toggled
119 sun
.bind_to_sun
= sun_props
.bind_to_sun
120 sun
.bind
.az_start_sun
= sun_props
.hdr_azimuth
122 sun
.bind
.az_start_env
= env_tex
.texture_mapping
.rotation
.z
124 if env_tex
and sun_props
.bind_to_sun
:
125 az
= sun_props
.hdr_azimuth
- sun
.bind
.az_start_sun
+ sun
.bind
.az_start_env
126 env_tex
.texture_mapping
.rotation
.z
= az
128 if sun_props
.sun_object
:
129 sun
.theta
= math
.pi
/ 2 - sun_props
.hdr_elevation
130 sun
.phi
= -sun_props
.hdr_azimuth
132 obj
= sun_props
.sun_object
133 set_sun_position(obj
, sun_props
.sun_distance
)
134 rotation_euler
= Euler((sun_props
.hdr_elevation
- pi
/2,
135 0, -sun_props
.hdr_azimuth
))
137 set_sun_rotations(obj
, rotation_euler
)
140 local_time
= sun_props
.time
141 zone
= -sun_props
.UTC_zone
142 sun
.use_daylight_savings
= sun_props
.use_daylight_savings
143 if sun
.use_daylight_savings
:
146 north_offset
= degrees(sun_props
.north_offset
)
148 if addon_prefs
.show_rise_set
:
149 calc_sunrise_sunset(rise
=True)
150 calc_sunrise_sunset(rise
=False)
152 get_sun_position(local_time
, sun_props
.latitude
, sun_props
.longitude
,
153 north_offset
, zone
, sun_props
.month
, sun_props
.day
, sun_props
.year
,
154 sun_props
.sun_distance
)
156 if sun_props
.use_sky_texture
and sun_props
.sky_texture
:
157 sky_node
= bpy
.context
.scene
.world
.node_tree
.nodes
.get(sun_props
.sky_texture
)
158 if sky_node
is not None and sky_node
.type == "TEX_SKY":
159 locX
= math
.sin(sun
.phi
) * math
.sin(-sun
.theta
)
160 locY
= math
.sin(sun
.theta
) * math
.cos(sun
.phi
)
161 locZ
= math
.cos(sun
.theta
)
162 sky_node
.texture_mapping
.rotation
.z
= 0.0
163 sky_node
.sun_direction
= locX
, locY
, locZ
166 if ((sun_props
.use_sun_object
or sun_props
.usage_mode
== 'HDR')
167 and sun_props
.sun_object
168 and sun_props
.sun_object
.name
in context
.view_layer
.objects
):
169 obj
= sun_props
.sun_object
170 set_sun_position(obj
, sun_props
.sun_distance
)
171 rotation_euler
= Euler((math
.radians(sun
.elevation
- 90), 0,
172 math
.radians(-sun
.az_north
)))
173 set_sun_rotations(obj
, rotation_euler
)
176 if (addon_prefs
.show_object_collection
177 and sun_props
.use_object_collection
178 and sun_props
.object_collection
):
179 sun_objects
= sun_props
.object_collection
.objects
180 object_count
= len(sun_objects
)
181 if sun_props
.object_collection_type
== 'DIURNAL':
184 time_increment
= sun_props
.time_spread
/ (object_count
- 1)
185 local_time
= local_time
+ time_increment
* (object_count
- 1)
187 time_increment
= sun_props
.time_spread
189 for obj
in sun_objects
:
190 get_sun_position(local_time
, sun_props
.latitude
,
191 sun_props
.longitude
, north_offset
, zone
,
192 sun_props
.month
, sun_props
.day
,
193 sun_props
.year
, sun_props
.sun_distance
)
194 set_sun_position(obj
, sun_props
.sun_distance
)
195 local_time
-= time_increment
196 obj
.rotation_euler
= (
197 (math
.radians(sun
.elevation
- 90), 0,
198 math
.radians(-sun
.az_north
)))
201 day_increment
= 365 / object_count
202 day
= sun_props
.day_of_year
+ day_increment
* (object_count
- 1)
203 for obj
in sun_objects
:
204 dt
= (datetime
.date(sun_props
.year
, 1, 1) +
205 datetime
.timedelta(day
- 1))
206 get_sun_position(local_time
, sun_props
.latitude
,
207 sun_props
.longitude
, north_offset
, zone
,
208 dt
.month
, dt
.day
, sun_props
.year
,
209 sun_props
.sun_distance
)
210 set_sun_position(obj
, sun_props
.sun_distance
)
212 obj
.rotation_euler
= (
213 (math
.radians(sun
.elevation
- 90), 0,
214 math
.radians(-sun
.az_north
)))
216 def update_time(context
):
217 sun_props
= context
.scene
.sun_pos_properties
219 if sun_props
.use_day_of_year
:
220 dt
= (datetime
.date(sun_props
.year
, 1, 1) +
221 datetime
.timedelta(sun_props
.day_of_year
- 1))
224 sun
.day_of_year
= sun_props
.day_of_year
225 if sun_props
.day
!= dt
.day
:
226 sun_props
.day
= dt
.day
227 if sun_props
.month
!= dt
.month
:
228 sun_props
.month
= dt
.month
230 dt
= datetime
.date(sun_props
.year
, sun_props
.month
, sun_props
.day
)
231 day_of_year
= dt
.timetuple().tm_yday
232 if sun_props
.day_of_year
!= day_of_year
:
233 sun_props
.day_of_year
= day_of_year
234 sun
.day
= sun_props
.day
235 sun
.month
= sun_props
.month
236 sun
.day_of_year
= day_of_year
237 sun
.year
= sun_props
.year
238 sun
.longitude
= sun_props
.longitude
239 sun
.latitude
= sun_props
.latitude
240 sun
.UTC_zone
= sun_props
.UTC_zone
243 def format_time(the_time
, daylight_savings
, longitude
, UTC_zone
=None):
244 if UTC_zone
is not None:
252 mm
= (the_time
- int(the_time
)) * 60
253 ss
= int((mm
- int(mm
)) * 60)
255 return ("%02i:%02i:%02i" % (hh
, mm
, ss
))
258 def format_hms(the_time
):
259 hh
= str(int(the_time
))
260 min = (the_time
- int(the_time
)) * 60
261 sec
= int((min - int(min)) * 60)
262 mm
= "0" + str(int(min)) if min < 10 else str(int(min))
263 ss
= "0" + str(sec
) if sec
< 10 else str(sec
)
265 return (hh
+ ":" + mm
+ ":" + ss
)
268 def format_lat_long(lat_long
, is_latitude
):
269 hh
= str(abs(int(lat_long
)))
270 min = abs((lat_long
- int(lat_long
)) * 60)
271 sec
= abs(int((min - int(min)) * 60))
272 mm
= "0" + str(int(min)) if min < 10 else str(int(min))
273 ss
= "0" + str(sec
) if sec
< 10 else str(sec
)
278 coord_tag
= " N" if lat_long
> 0 else " S"
280 coord_tag
= " E" if lat_long
> 0 else " W"
282 return hh
+ "° " + mm
+ "' " + ss
+ '"' + coord_tag
287 ############################################################################
289 # Calculate the actual position of the sun based on input parameters.
291 # The sun positioning algorithms below are based on the National Oceanic
292 # and Atmospheric Administration's (NOAA) Solar Position Calculator
293 # which rely on calculations of Jean Meeus' book "Astronomical Algorithms."
294 # Use of NOAA data and products are in the public domain and may be used
295 # freely by the public as outlined in their policies at
296 # www.nws.noaa.gov/disclaimer.php
298 # The calculations of this script can be verified with those of NOAA's
299 # using the Azimuth and Solar Elevation displayed in the SunPos_Panel.
300 # NOAA's web site is:
301 # http://www.esrl.noaa.gov/gmd/grad/solcalc
302 ############################################################################
305 def get_sun_position(local_time
, latitude
, longitude
, north_offset
,
306 utc_zone
, month
, day
, year
, distance
):
308 addon_prefs
= bpy
.context
.preferences
.addons
[__package__
].preferences
309 sun_props
= bpy
.context
.scene
.sun_pos_properties
311 longitude
*= -1 # for internal calculations
312 utc_time
= local_time
+ utc_zone
# Set Greenwich Meridian Time
314 if latitude
> 89.93: # Latitude 90 and -90 gives
315 latitude
= radians(89.93) # erroneous results so nudge it
316 elif latitude
< -89.93:
317 latitude
= radians(-89.93)
319 latitude
= radians(latitude
)
321 t
= julian_time_from_y2k(utc_time
, year
, month
, day
)
323 e
= radians(obliquity_correction(t
))
324 L
= apparent_longitude_of_sun(t
)
325 solar_dec
= sun_declination(e
, L
)
326 eqtime
= calc_equation_of_time(t
)
328 time_correction
= (eqtime
- 4 * longitude
) + 60 * utc_zone
329 true_solar_time
= ((utc_time
- utc_zone
) * 60.0 + time_correction
) % 1440
331 hour_angle
= true_solar_time
/ 4.0 - 180.0
332 if hour_angle
< -180.0:
335 csz
= (math
.sin(latitude
) * math
.sin(solar_dec
) +
336 math
.cos(latitude
) * math
.cos(solar_dec
) *
337 math
.cos(radians(hour_angle
)))
343 zenith
= math
.acos(csz
)
345 az_denom
= math
.cos(latitude
) * math
.sin(zenith
)
347 if abs(az_denom
) > 0.001:
348 az_rad
= ((math
.sin(latitude
) *
349 math
.cos(zenith
)) - math
.sin(solar_dec
)) / az_denom
350 if abs(az_rad
) > 1.0:
351 az_rad
= -1.0 if (az_rad
< 0.0) else 1.0
352 azimuth
= 180.0 - degrees(math
.acos(az_rad
))
356 azimuth
= 180.0 if (latitude
> 0.0) else 0.0
359 azimuth
= azimuth
+ 360.0
361 exoatm_elevation
= 90.0 - degrees(zenith
)
363 if sun_props
.use_refraction
:
364 if exoatm_elevation
> 85.0:
365 refraction_correction
= 0.0
367 te
= math
.tan(radians(exoatm_elevation
))
368 if exoatm_elevation
> 5.0:
369 refraction_correction
= (
370 58.1 / te
- 0.07 / (te
** 3) + 0.000086 / (te
** 5))
371 elif (exoatm_elevation
> -0.575):
372 s1
= (-12.79 + exoatm_elevation
* 0.711)
373 s2
= (103.4 + exoatm_elevation
* (s1
))
374 s3
= (-518.2 + exoatm_elevation
* (s2
))
375 refraction_correction
= 1735.0 + exoatm_elevation
* (s3
)
377 refraction_correction
= -20.774 / te
379 refraction_correction
= refraction_correction
/ 3600
380 solar_elevation
= 90.0 - (degrees(zenith
) - refraction_correction
)
383 solar_elevation
= 90.0 - degrees(zenith
)
385 solar_azimuth
= azimuth
386 solar_azimuth
+= north_offset
388 sun
.az_north
= solar_azimuth
390 sun
.theta
= math
.pi
/ 2 - radians(solar_elevation
)
391 sun
.phi
= radians(solar_azimuth
) * -1
392 sun
.azimuth
= azimuth
393 sun
.elevation
= solar_elevation
396 def set_sun_position(obj
, distance
):
397 locX
= math
.sin(sun
.phi
) * math
.sin(-sun
.theta
) * distance
398 locY
= math
.sin(sun
.theta
) * math
.cos(sun
.phi
) * distance
399 locZ
= math
.cos(sun
.theta
) * distance
401 #----------------------------------------------
402 # Update selected object in viewport
403 #----------------------------------------------
404 obj
.location
= locX
, locY
, locZ
407 def set_sun_rotations(obj
, rotation_euler
):
408 rotation_quaternion
= rotation_euler
.to_quaternion()
409 obj
.rotation_quaternion
= rotation_quaternion
411 if obj
.rotation_mode
in {'XZY', 'YXZ', 'YZX', 'ZXY','ZYX'}:
412 obj
.rotation_euler
= rotation_quaternion
.to_euler(obj
.rotation_mode
)
414 obj
.rotation_euler
= rotation_euler
416 rotation_axis_angle
= obj
.rotation_quaternion
.to_axis_angle()
417 obj
.rotation_axis_angle
= (rotation_axis_angle
[1],
418 *rotation_axis_angle
[0])
421 def calc_sunrise_set_UTC(rise
, jd
, latitude
, longitude
):
422 t
= calc_time_julian_cent(jd
)
423 eq_time
= calc_equation_of_time(t
)
424 solar_dec
= calc_sun_declination(t
)
425 hour_angle
= calc_hour_angle_sunrise(latitude
, solar_dec
)
427 hour_angle
= -hour_angle
428 delta
= longitude
+ degrees(hour_angle
)
429 time_UTC
= 720 - (4.0 * delta
) - eq_time
433 def calc_sun_declination(t
):
434 e
= radians(obliquity_correction(t
))
435 L
= apparent_longitude_of_sun(t
)
436 solar_dec
= sun_declination(e
, L
)
440 def calc_hour_angle_sunrise(lat
, solar_dec
):
441 lat_rad
= radians(lat
)
442 HAarg
= (math
.cos(radians(90.833)) /
443 (math
.cos(lat_rad
) * math
.cos(solar_dec
))
444 - math
.tan(lat_rad
) * math
.tan(solar_dec
))
449 HA
= math
.acos(HAarg
)
453 def calc_solar_noon(jd
, longitude
, timezone
, dst
):
454 t
= calc_time_julian_cent(jd
- longitude
/ 360.0)
455 eq_time
= calc_equation_of_time(t
)
456 noon_offset
= 720.0 - (longitude
* 4.0) - eq_time
457 newt
= calc_time_julian_cent(jd
+ noon_offset
/ 1440.0)
458 eq_time
= calc_equation_of_time(newt
)
460 nv
= 780.0 if dst
else 720.0
461 noon_local
= (nv
- (longitude
* 4.0) - eq_time
+ (timezone
* 60.0)) % 1440
462 sun
.solar_noon
.time
= noon_local
/ 60.0
465 def calc_sunrise_sunset(rise
):
468 jd
= get_julian_day(sun
.year
, sun
.month
, sun
.day
)
469 time_UTC
= calc_sunrise_set_UTC(rise
, jd
, sun
.latitude
, sun
.longitude
)
470 new_time_UTC
= calc_sunrise_set_UTC(rise
, jd
+ time_UTC
/ 1440.0,
471 sun
.latitude
, sun
.longitude
)
472 time_local
= new_time_UTC
+ (-zone
* 60.0)
473 tl
= time_local
/ 60.0
474 get_sun_position(tl
, sun
.latitude
, sun
.longitude
, 0.0,
475 zone
, sun
.month
, sun
.day
, sun
.year
,
477 if sun
.use_daylight_savings
:
479 tl
= time_local
/ 60.0
482 sun
.sunrise
.time
= tl
483 sun
.sunrise
.azimuth
= sun
.azimuth
484 sun
.sunrise
.elevation
= sun
.elevation
485 calc_solar_noon(jd
, sun
.longitude
, -zone
, sun
.use_daylight_savings
)
486 get_sun_position(sun
.solar_noon
.time
, sun
.latitude
, sun
.longitude
,
487 0.0, zone
, sun
.month
, sun
.day
, sun
.year
,
489 sun
.solar_noon
.elevation
= sun
.elevation
492 sun
.sunset
.azimuth
= sun
.azimuth
493 sun
.sunset
.elevation
= sun
.elevation
495 ##########################################################################
496 ## Get the elapsed julian time since 1/1/2000 12:00 gmt
497 ## Y2k epoch (1/1/2000 12:00 gmt) is Julian day 2451545.0
498 ##########################################################################
501 def julian_time_from_y2k(utc_time
, year
, month
, day
):
502 century
= 36525.0 # Days in Julian Century
503 epoch
= 2451545.0 # Julian Day for 1/1/2000 12:00 gmt
504 jd
= get_julian_day(year
, month
, day
)
505 return ((jd
+ (utc_time
/ 24)) - epoch
) / century
508 def get_julian_day(year
, month
, day
):
512 A
= math
.floor(year
/ 100)
513 B
= 2 - A
+ math
.floor(A
/ 4.0)
514 jd
= (math
.floor((365.25 * (year
+ 4716.0))) +
515 math
.floor(30.6001 * (month
+ 1)) + day
+ B
- 1524.5)
519 def calc_time_julian_cent(jd
):
520 t
= (jd
- 2451545.0) / 36525.0
524 def sun_declination(e
, L
):
525 return (math
.asin(math
.sin(e
) * math
.sin(L
)))
528 def calc_equation_of_time(t
):
529 epsilon
= obliquity_correction(t
)
530 ml
= radians(mean_longitude_sun(t
))
531 e
= eccentricity_earth_orbit(t
)
532 m
= radians(mean_anomaly_sun(t
))
533 y
= math
.tan(radians(epsilon
) / 2.0)
535 sin2ml
= math
.sin(2.0 * ml
)
536 cos2ml
= math
.cos(2.0 * ml
)
537 sin4ml
= math
.sin(4.0 * ml
)
539 sin2m
= math
.sin(2.0 * m
)
540 etime
= (y
* sin2ml
- 2.0 * e
* sinm
+ 4.0 * e
* y
*
541 sinm
* cos2ml
- 0.5 * y
** 2 * sin4ml
- 1.25 * e
** 2 * sin2m
)
542 return (degrees(etime
) * 4)
545 def obliquity_correction(t
):
546 ec
= obliquity_of_ecliptic(t
)
547 omega
= 125.04 - 1934.136 * t
548 return (ec
+ 0.00256 * math
.cos(radians(omega
)))
551 def obliquity_of_ecliptic(t
):
552 return ((23.0 + 26.0 / 60 + (21.4480 - 46.8150) / 3600 * t
-
553 (0.00059 / 3600) * t
** 2 + (0.001813 / 3600) * t
** 3))
556 def true_longitude_of_sun(t
):
557 return (mean_longitude_sun(t
) + equation_of_sun_center(t
))
560 def calc_sun_apparent_long(t
):
561 o
= true_longitude_of_sun(t
)
562 omega
= 125.04 - 1934.136 * t
563 lamb
= o
- 0.00569 - 0.00478 * math
.sin(radians(omega
))
567 def apparent_longitude_of_sun(t
):
568 return (radians(true_longitude_of_sun(t
) - 0.00569 - 0.00478 *
569 math
.sin(radians(125.04 - 1934.136 * t
))))
572 def mean_longitude_sun(t
):
573 return (280.46646 + 36000.76983 * t
+ 0.0003032 * t
** 2) % 360
576 def equation_of_sun_center(t
):
577 m
= radians(mean_anomaly_sun(t
))
578 c
= ((1.914602 - 0.004817 * t
- 0.000014 * t
** 2) * math
.sin(m
) +
579 (0.019993 - 0.000101 * t
) * math
.sin(m
* 2) +
580 0.000289 * math
.sin(m
* 3))
584 def mean_anomaly_sun(t
):
585 return (357.52911 + t
* (35999.05029 - 0.0001537 * t
))
588 def eccentricity_earth_orbit(t
):
589 return (0.016708634 - 0.000042037 * t
- 0.0000001267 * t
** 2)