1 # SPDX-FileCopyrightText: 2019-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # -*- coding: utf-8 -*-
8 from bpy
.props
import FloatProperty
, FloatVectorProperty
9 from bpy
.app
.translations
import pgettext_iface
as iface_
11 from gpu_extras
.batch
import batch_for_shader
12 from mathutils
import Vector
13 from math
import sqrt
, pi
, atan2
, asin
16 if not bpy
.app
.background
: # ignore drawing in background mode
17 image_shader
= gpu
.shader
.from_builtin('IMAGE_COLOR')
18 line_shader
= gpu
.shader
.from_builtin('FLAT_COLOR')
21 def draw_callback_px(self
, context
):
22 nt
= context
.scene
.world
.node_tree
.nodes
23 env_tex_node
= nt
.get(context
.scene
.sun_pos_properties
.hdr_texture
)
24 image
= env_tex_node
.image
25 texture
= gpu
.texture
.from_image(image
)
27 if self
.area
!= context
.area
:
31 top
= context
.area
.height
32 right
= context
.area
.width
34 position
= Vector((right
, top
)) / 2 + self
.offset
35 scale
= Vector((context
.area
.width
, context
.area
.width
/ 2)) * self
.scale
37 coords
= ((-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5))
38 uv_coords
= ((0, 0), (1, 0), (1, 1), (0, 1))
39 batch
= batch_for_shader(image_shader
, 'TRI_FAN',
40 {"pos": coords
, "texCoord": uv_coords
})
42 with gpu
.matrix
.push_pop():
43 gpu
.matrix
.translate(position
)
44 gpu
.matrix
.scale(scale
)
47 image_shader
.uniform_sampler("image", texture
)
48 image_shader
.uniform_float("color", (self
.exposure
, self
.exposure
, self
.exposure
, 1.0))
49 batch
.draw(image_shader
)
53 coords
= ((self
.mouse_position
[0], bottom
), (self
.mouse_position
[0], top
))
54 colors
= ((1,) * 4,) * 2
55 batch
= batch_for_shader(line_shader
, 'LINES',
56 {"pos": coords
, "color": colors
})
58 batch
.draw(line_shader
)
61 if bottom
<= self
.mouse_position
[1] <= top
:
62 coords
= ((0, self
.mouse_position
[1]), (context
.area
.width
, self
.mouse_position
[1]))
63 batch
= batch_for_shader(line_shader
, 'LINES',
64 {"pos": coords
, "color": colors
})
66 batch
.draw(line_shader
)
69 class SUNPOS_OT_ShowHdr(bpy
.types
.Operator
):
70 """Select the location of the Sun in any 3D viewport and keep it in sync with the environment"""
71 bl_idname
= "world.sunpos_show_hdr"
72 bl_label
= "Pick Sun in Viewport"
74 exposure
: FloatProperty(name
="Exposure", default
=1.0)
75 scale
: FloatProperty(name
="Scale", default
=1.0)
76 offset
: FloatVectorProperty(name
="Offset", default
=(0.0, 0.0), size
=2, subtype
='COORDINATES')
79 def poll(self
, context
):
80 sun_props
= context
.scene
.sun_pos_properties
81 if sun_props
.sun_object
is None:
82 self
.poll_message_set("Please select a Sun object")
84 if not sun_props
.hdr_texture
:
85 self
.poll_message_set("Please select an Environment Texture node")
87 if sun_props
.bind_to_sun
:
88 self
.poll_message_set("The environment texture is already bound to the Sun object")
91 nt
= context
.scene
.world
.node_tree
.nodes
92 env_tex_node
= nt
.get(context
.scene
.sun_pos_properties
.hdr_texture
)
93 if (env_tex_node
is None
94 or env_tex_node
.type != "TEX_ENVIRONMENT"
95 or env_tex_node
.image
is None):
96 self
.poll_message_set("Please select a valid Environment Texture node")
100 def update(self
, context
, event
):
101 sun_props
= context
.scene
.sun_pos_properties
102 mouse_position_abs
= Vector((event
.mouse_x
, event
.mouse_y
))
105 for area
in context
.screen
.areas
:
106 # Compare absolute mouse position to area bounds
107 if (area
.x
< mouse_position_abs
.x
< area
.x
+ area
.width
108 and area
.y
< mouse_position_abs
.y
< area
.y
+ area
.height
):
110 if area
.type == 'VIEW_3D':
114 if self
.area
.type == 'VIEW_3D':
115 self
.top
= self
.area
.height
116 self
.right
= self
.area
.width
118 nt
= context
.scene
.world
.node_tree
.nodes
119 env_tex
= nt
.get(sun_props
.hdr_texture
)
121 # Mouse position relative to window
122 self
.mouse_position
= Vector((mouse_position_abs
.x
- self
.area
.x
,
123 mouse_position_abs
.y
- self
.area
.y
))
125 self
.selected_point
= (self
.mouse_position
127 - Vector((self
.right
, self
.top
)) / 2) / self
.scale
128 u
= self
.selected_point
.x
/ self
.area
.width
+ 0.5
129 v
= (self
.selected_point
.y
) / (self
.area
.width
/ 2) + 0.5
131 # Set elevation and azimuth from selected point
132 if env_tex
.projection
== 'EQUIRECTANGULAR':
134 az
= u
* pi
*2 - pi
/2 + env_tex
.texture_mapping
.rotation
.z
140 sun_props
.hdr_elevation
= el
141 sun_props
.hdr_azimuth
= az
142 elif env_tex
.projection
== 'MIRROR_BALL':
143 # Formula from intern/cycles/kernel/kernel_projection.h
148 dir.x
= 2.0 * u
- 1.0
149 dir.z
= 2.0 * v
- 1.0
152 if (dir.x
* dir.x
+ dir.z
* dir.z
> 1.0):
156 dir.y
= -sqrt(max(1.0 - dir.x
* dir.x
- dir.z
* dir.z
, 0.0))
159 i
= Vector((0.0, -1.0, 0.0))
161 dir = 2.0 * dir.dot(i
) * dir - i
163 # Convert vector to euler
165 az
= atan2(dir.x
, dir.y
) + env_tex
.texture_mapping
.rotation
.z
166 sun_props
.hdr_elevation
= el
167 sun_props
.hdr_azimuth
= az
170 self
.report({'ERROR'}, 'Unknown projection')
173 def pan(self
, context
, event
):
174 self
.offset
+= Vector((event
.mouse_region_x
- self
.mouse_prev_x
,
175 event
.mouse_region_y
- self
.mouse_prev_y
))
176 self
.mouse_prev_x
, self
.mouse_prev_y
= event
.mouse_region_x
, event
.mouse_region_y
178 def modal(self
, context
, event
):
179 self
.area
.tag_redraw()
180 if event
.type == 'MOUSEMOVE':
182 self
.pan(context
, event
)
183 self
.update(context
, event
)
186 elif event
.type in {'LEFTMOUSE', 'RET'}:
187 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._handle
, 'WINDOW')
188 for area
in context
.screen
.areas
:
190 # Bind the environment texture to the sun
191 context
.scene
.sun_pos_properties
.bind_to_sun
= True
192 context
.workspace
.status_text_set(None)
196 elif event
.type in {'RIGHTMOUSE', 'ESC'}:
197 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._handle
, 'WINDOW')
198 for area
in context
.screen
.areas
:
200 # Reset previous values
201 context
.scene
.sun_pos_properties
.hdr_elevation
= self
.initial_elevation
202 context
.scene
.sun_pos_properties
.hdr_azimuth
= self
.initial_azimuth
203 context
.workspace
.status_text_set(None)
206 # Set exposure or zoom
207 elif event
.type == 'WHEELUPMOUSE':
214 self
.offset
-= (self
.mouse_position
- (Vector((self
.right
, self
.top
)) / 2 + self
.offset
)) / 10.0
215 self
.update(context
, event
)
216 elif event
.type == 'WHEELDOWNMOUSE':
223 self
.offset
+= (self
.mouse_position
- (Vector((self
.right
, self
.top
)) / 2 + self
.offset
)) / 11.0
224 self
.update(context
, event
)
227 elif event
.type == 'MIDDLEMOUSE':
228 if event
.value
== 'PRESS':
229 self
.mouse_prev_x
, self
.mouse_prev_y
= event
.mouse_region_x
, event
.mouse_region_y
230 self
.is_panning
= True
231 elif event
.value
== 'RELEASE':
232 self
.is_panning
= False
235 return {'PASS_THROUGH'}
237 return {'RUNNING_MODAL'}
239 def invoke(self
, context
, event
):
240 self
.is_panning
= False
241 self
.mouse_prev_x
= 0.0
242 self
.mouse_prev_y
= 0.0
244 # Get at least one 3D View
246 for a
in context
.screen
.areas
:
247 if a
.type == 'VIEW_3D':
252 self
.report({'ERROR'}, 'Could not find 3D View')
255 nt
= context
.scene
.world
.node_tree
.nodes
256 env_tex_node
= nt
.get(context
.scene
.sun_pos_properties
.hdr_texture
)
257 if env_tex_node
is None or env_tex_node
.type != "TEX_ENVIRONMENT":
258 self
.report({'ERROR'}, 'Please select an Environment Texture node')
261 self
.area
= context
.area
263 self
.mouse_position
= event
.mouse_region_x
, event
.mouse_region_y
265 self
.initial_elevation
= context
.scene
.sun_pos_properties
.hdr_elevation
266 self
.initial_azimuth
= context
.scene
.sun_pos_properties
.hdr_azimuth
268 context
.workspace
.status_text_set(
269 iface_("Enter/LMB: confirm, Esc/RMB: cancel, MMB: pan, "
270 "mouse wheel: zoom, Ctrl + mouse wheel: set exposure"))
272 self
._handle
= bpy
.types
.SpaceView3D
.draw_handler_add(
273 draw_callback_px
, (self
, context
), 'WINDOW', 'POST_PIXEL'
275 context
.window_manager
.modal_handler_add(self
)
277 return {'RUNNING_MODAL'}