Skip to content
Snippets Groups Projects
hdr.py 10.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • ### 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
    
    from bpy.props import FloatProperty, FloatVectorProperty
    
    import gpu
    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);
    
      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) * vec4(exposure, exposure, exposure, 1.0f);
    
    }'''
    
    # 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
    
        texture = gpu.texture.from_image(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})
    
        with gpu.matrix.push_pop():
            gpu.matrix.translate(position)
            gpu.matrix.scale(scale)
    
            shader.bind()
    
            shader.uniform_sampler("image", texture)
    
            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: FloatProperty(name="Exposure", default=1.0)
        scale: FloatProperty(name="Scale", default=1.0)
        offset: FloatVectorProperty(name="Offset", default=(0.0, 0.0), size=2, subtype='COORDINATES')
    
    
        @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
    
            # 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'}