Last active
April 12, 2023 15:01
-
-
Save halby24/bdf17e8283cc657b18ae96d1a08b5525 to your computer and use it in GitHub Desktop.
Blenderのブラシサイズを右クリックで変えるやつ (3.5対応)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ***** BEGIN GPL LICENSE BLOCK ***** | |
# | |
# Copyright © 2019 Jean Ayer(vrav) | |
# | |
# 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/>. | |
# | |
# ***** END GPL LICENSE BLOCK ***** | |
bl_info = { | |
"name": "Brush Strength/Radius QuickSet", | |
"description": "Alter brush radius or strength in the 3D view.", | |
"author": "Jean Ayer (vrav), modified by Toudou++.", # legal / tracker name: Cody Burrow | |
"version": (0, 8, 4), | |
"blender": (2, 82, 0), | |
"location": "User Preferences > Input > assign 'brush.modal_quickset'", | |
"warning": "Automatically assigns brush.modal_quickset to RMB in sculpt mode", | |
"category": "Paint" | |
} | |
# brush_quickset.py | |
# brush.modal_quickset for hotkeys | |
# Brush QuickSet from search menu | |
# Modify sculpt/paint brush radius and strength in a streamlined manner. | |
# To use, assign a hotkey to brush.modal_quickset in a paint or sculpt mode. | |
# Recommended RMB, but any key can be used in a hold-and-release manner. | |
# What does it do? When you activate the modal operator, you can drag the | |
# mouse along either axis to affect brush radius or strength. Which axis | |
# affects which is configurable, amongst other things detailed below. | |
# Operator Options: | |
# - Axis Order: Whether X or Y affects brush size, etc. | |
# - Key Action: Hotkey activity (press or release) can apply or cancel. | |
# - Numeric: Show strength value when adjusted; can pick size | |
# - Slider: Represent strength with a visual slider; can pick size | |
# - Pixel Deadzone: Distance before an axis starts affecting the brush. | |
# - Size Sensitivity: Multiplier for quicker or slower radius adjustment. | |
# - Graphic: Represent strength via transparent brush overlay | |
# - Lock Axis: Allow only one value to be altered at a time | |
# Known limitations: | |
# - Not available for painting in the image editor. | |
# - Holding ctrl does not snap to values, probably should. | |
from mathutils import Color | |
import bpy | |
import blf | |
import bgl | |
import gpu | |
from gpu_extras.batch import batch_for_shader | |
vertex_shader = ''' | |
uniform mat4 ModelViewProjectionMatrix; | |
in vec2 pos; | |
in vec4 color; | |
out vec4 col; | |
void main() | |
{ | |
gl_Position = ModelViewProjectionMatrix * vec4(pos, 0.0, 1.0); | |
col = color; | |
} | |
''' | |
fragment_shader = ''' | |
in vec4 col; | |
void main() | |
{ | |
gl_FragColor = col; | |
} | |
''' | |
rectpoints = ( | |
(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0) | |
) | |
circlepoints = ( | |
( 0.0 , 1.0 ), | |
( -0.19509 , 0.980785 ), | |
( -0.382683 , 0.92388 ), | |
( -0.55557 , 0.83147 ), | |
( -0.707107 , 0.707107 ), | |
( -0.83147 , 0.55557 ), | |
( -0.92388 , 0.382683 ), | |
( -0.980785 , 0.19509 ), | |
( -1.0 , 0.0 ), | |
( -0.980785 , -0.19509 ), | |
( -0.92388 , -0.382683 ), | |
( -0.83147 , -0.55557 ), | |
( -0.707107 , -0.707107 ), | |
( -0.55557 , -0.83147 ), | |
( -0.382683 , -0.92388 ), | |
( -0.19509 , -0.980785 ), | |
( 0.0 , -1.0 ), | |
( 0.195091 , -0.980785 ), | |
( 0.382684 , -0.923879 ), | |
( 0.555571 , -0.831469 ), | |
( 0.707107 , -0.707106 ), | |
( 0.83147 , -0.55557 ), | |
( 0.92388 , -0.382683 ), | |
( 0.980785 , -0.195089 ), | |
( 1.0 , 0.0 ), | |
( 0.980785 , 0.195091 ), | |
( 0.923879 , 0.382684 ), | |
( 0.831469 , 0.555571 ), | |
( 0.707106 , 0.707108 ), | |
( 0.555569 , 0.83147 ), | |
( 0.382682 , 0.92388 ), | |
( 0.195089 , 0.980786 ), | |
) | |
circleindices = ( | |
( 1 , 0 , 31 ), | |
( 1 , 31 , 30 ), | |
( 2 , 1 , 30 ), | |
( 15 , 13 , 18 ), | |
( 30 , 29 , 28 ), | |
( 3 , 30 , 28 ), | |
( 4 , 3 , 28 ), | |
( 27 , 5 , 28 ), | |
( 3 , 2 , 30 ), | |
( 5 , 27 , 26 ), | |
( 6 , 5 , 26 ), | |
( 6 , 26 , 25 ), | |
( 7 , 6 , 25 ), | |
( 7 , 25 , 24 ), | |
( 8 , 7 , 24 ), | |
( 8 , 24 , 23 ), | |
( 9 , 8 , 23 ), | |
( 9 , 23 , 22 ), | |
( 10 , 9 , 22 ), | |
( 10 , 22 , 21 ), | |
( 11 , 10 , 21 ), | |
( 11 , 21 , 20 ), | |
( 12 , 11 , 20 ), | |
( 12 , 20 , 19 ), | |
( 13 , 12 , 19 ), | |
( 13 , 19 , 18 ), | |
( 17 , 15 , 18 ), | |
( 14 , 13 , 15 ), | |
( 15 , 17 , 16 ), | |
( 5 , 4 , 28 ), | |
) | |
def draw_callback_px(self, context): | |
# circle graphic, text, and slider | |
unify_settings = bpy.context.tool_settings.unified_paint_settings | |
strength = unify_settings.strength if self.uni_str else self.brush.strength | |
size = unify_settings.size if self.uni_size else self.brush.size | |
vertices = [] | |
colors = [] | |
indices = [] | |
text = "" | |
font_id = 0 | |
do_text = False | |
if self.graphic: | |
# circle inside brush | |
starti = len(vertices) | |
for x, y in circlepoints: | |
vertices.append((int(size * x) + self.cur[0], int(size * y) + self.cur[1])) | |
colors.append((256, 0, 0, strength * 0.25)) | |
for i in circleindices: | |
indices.append((starti + i[0], starti + i[1], starti + i[2])) | |
if self.text != 'NONE' and self.doingstr: | |
if self.text == 'MEDIUM': | |
fontsize = 11 | |
elif self.text == 'LARGE': | |
fontsize = 22 | |
else: | |
fontsize = 8 | |
blf.size(font_id, fontsize, 72) | |
blf.shadow(font_id, 0, 0.0, 0.0, 0.0, 1.0) | |
blf.enable(font_id, blf.SHADOW) | |
if strength < 0.001: | |
text = "0.001" | |
else: | |
text = str(strength)[0:5] | |
textsize = blf.dimensions(font_id, text) | |
xpos = self.start[0] + self.offset[0] | |
ypos = self.start[1] + self.offset[1] | |
blf.position(font_id, xpos, ypos, 0) | |
# rectangle behind text | |
starti = len(vertices) | |
# rectpoints: (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0) | |
for x, y in rectpoints: | |
vertices.append((int(textsize[0] * x) + xpos, int(textsize[1] * y) + ypos)) | |
colors.append((self.backcolor.r, self.backcolor.g, self.backcolor.b, 0.5)) | |
indices.extend(( | |
(starti, starti+1, starti+2), (starti+2, starti, starti+3) | |
)) | |
do_text = True | |
if self.slider != 'NONE' and self.doingstr: | |
xpos = self.start[0] + self.offset[0] - self.sliderwidth + (32 if self.text == 'MEDIUM' else 64 if self.text == 'LARGE' else 23) | |
ypos = self.start[1] + self.offset[1] - self.sliderheight# + (1 if self.slider != 'SMALL' else 0) | |
if strength < 1.0: | |
sliderscale = strength | |
elif strength > 5.0: | |
sliderscale = strength / 10 | |
elif strength > 2.0: | |
sliderscale = strength / 5 | |
else: | |
sliderscale = strength / 2 | |
# slider back rect | |
starti = len(vertices) | |
# rectpoints: (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0) | |
for x, y in rectpoints: | |
vertices.append((int(self.sliderwidth * x) + xpos, int(self.sliderheight * y) + ypos - 1)) | |
colors.append((self.backcolor.r, self.backcolor.g, self.backcolor.b, 0.5)) | |
indices.extend(( | |
(starti, starti+1, starti+2), (starti+2, starti, starti+3) | |
)) | |
# slider front rect | |
starti = len(vertices) | |
# rectpoints: (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0) | |
for x, y in rectpoints: | |
vertices.append((int(self.sliderwidth * x * sliderscale) + xpos, int(self.sliderheight * y * 0.75) + ypos)) | |
colors.append((self.frontcolor.r, self.frontcolor.g, self.frontcolor.b, 0.8)) | |
indices.extend(( | |
(starti, starti+1, starti+2), (starti+2, starti, starti+3) | |
)) | |
shader = gpu.types.GPUShader(vertex_shader, fragment_shader) | |
batch = batch_for_shader(shader, 'TRIS', {"pos":vertices, "color":colors}, indices=indices) | |
bgl.glEnable(bgl.GL_BLEND) | |
shader.bind() | |
batch.draw(shader) | |
bgl.glDisable(bgl.GL_BLEND) | |
if do_text: | |
blf.draw(font_id, text) | |
blf.disable(font_id, blf.SHADOW) | |
def applyChanges(self): | |
unify_settings = bpy.context.tool_settings.unified_paint_settings | |
if self.doingstr: | |
if self.uni_str: | |
modrate = self.strmod * 0.0025 | |
newval = unify_settings.strength + modrate | |
if 10.0 > newval > 0.0: | |
unify_settings.strength = newval | |
self.strmod_total += modrate | |
else: | |
modrate = self.strmod * 0.0025 | |
newval = self.brush.strength + modrate | |
if 10.0 > newval > 0.0: | |
self.brush.strength = newval | |
self.strmod_total += modrate | |
if self.doingrad: | |
if self.uni_size: | |
newval = unify_settings.size + self.radmod | |
if 2000 > newval > 0: | |
unify_settings.size = int(newval) | |
self.radmod_total += self.radmod | |
else: | |
newval = self.brush.size + self.radmod | |
if 2000 > newval > 0: | |
self.brush.size = int(newval) | |
self.radmod_total += self.radmod | |
def revertChanges(self): | |
unify_settings = bpy.context.tool_settings.unified_paint_settings | |
if self.doingstr: | |
if self.uni_str: | |
unify_settings.strength -= self.strmod_total | |
else: | |
self.brush.strength -= self.strmod_total | |
if self.doingrad: | |
if self.uni_size: | |
unify_settings.size -= self.radmod_total | |
else: | |
self.brush.size -= self.radmod_total | |
class PAINT_OT_brush_modal_quickset(bpy.types.Operator): | |
bl_idname = "brush.modal_quickset" | |
bl_label = "Brush QuickSet" | |
axisaffect : bpy.props.EnumProperty( | |
name = "Axis Order", | |
description = "Which axis affects which brush property", | |
items = [('YSTR', 'X: Radius, Y: Strength', ''), | |
('YRAD', 'Y: Radius, X: Strength', '')], | |
default = 'YRAD') | |
keyaction : bpy.props.EnumProperty( | |
name = "Key Action", | |
description = "Hotkey second press or initial release behaviour", | |
items = [('IGNORE', 'Key Ignored', ''), | |
('CANCEL', 'Key Cancels', ''), | |
('FINISH', 'Key Applies', '')], | |
default = 'FINISH') | |
text : bpy.props.EnumProperty( | |
name = "Numeric", | |
description = "Text display; only shows when strength adjusted", | |
items = [('NONE', 'None', ''), | |
('LARGE', 'Large', ''), | |
('MEDIUM', 'Medium', ''), | |
('SMALL', 'Small', '')], | |
default = 'MEDIUM') | |
slider : bpy.props.EnumProperty( | |
name = "Slider", | |
description = "Slider display for strength visualization", | |
items = [('NONE', 'None', ''), | |
('LARGE', 'Large', ''), | |
('MEDIUM', 'Medium', ''), | |
('SMALL', 'Small', '')], | |
default = 'MEDIUM') | |
deadzone : bpy.props.IntProperty( | |
name = "Pixel Deadzone", | |
description = "Screen distance after which movement has effect", | |
default = 16, | |
min = 0) | |
sens : bpy.props.FloatProperty( | |
name = "Sensitivity", | |
description = "Multiplier to affect brush settings by", | |
default = 1.0, | |
min = 0.1, | |
max = 2.0) | |
graphic : bpy.props.BoolProperty( | |
name = "Graphic", | |
description = "Transparent circle to visually represent strength", | |
default = True) | |
lock : bpy.props.BoolProperty( | |
name = "Lock Axis", | |
description = "When adjusting one value, lock the other", | |
default = True) | |
@classmethod | |
def poll(cls, context): | |
return (context.area.type == 'VIEW_3D' | |
and context.mode in {'SCULPT', 'PAINT_WEIGHT', 'PAINT_VERTEX', 'PAINT_TEXTURE'}) | |
def modal(self, context, event): | |
sens = (self.sens * 0.5) if event.shift else (self.sens) | |
self.cur = (event.mouse_region_x, event.mouse_region_y) | |
diff = (self.cur[0] - self.prev[0], self.cur[1] - self.prev[1]) | |
if self.axisaffect == 'YRAD': | |
# Y corresponds to radius | |
if not self.doingrad: | |
if self.lock: | |
if not self.doingstr and abs(self.cur[1] - self.start[1]) > self.deadzone: | |
self.doingrad = True | |
self.radmod = diff[1] * sens | |
elif abs(self.cur[1] - self.start[1]) > self.deadzone: | |
self.doingrad = True | |
self.radmod = diff[1] * sens | |
else: | |
self.radmod = diff[1] * sens | |
if not self.doingstr: | |
if self.lock: | |
if not self.doingrad and abs(self.cur[0] - self.start[0]) > self.deadzone: | |
self.doingstr = True | |
self.strmod = diff[0] * sens | |
elif abs(self.cur[0] - self.start[0]) > self.deadzone: | |
self.doingstr = True | |
self.strmod = diff[0] * sens | |
else: | |
self.strmod = diff[0] * sens | |
else: | |
# Y corresponds to strength | |
if not self.doingrad: | |
if self.lock: | |
if not self.doingstr and abs(self.cur[0] - self.start[0]) > self.deadzone: | |
self.doingrad = True | |
self.radmod = diff[0] * sens | |
elif abs(self.cur[0] - self.start[0]) > self.deadzone: | |
self.doingrad = True | |
self.radmod = diff[0] * sens | |
else: | |
self.radmod = diff[0] * sens | |
if not self.doingstr: | |
if self.lock: | |
if not self.doingrad and abs(self.cur[1] - self.start[1]) > self.deadzone: | |
self.doingstr = True | |
self.strmod = diff[1] * sens | |
elif abs(self.cur[1] - self.start[1]) > self.deadzone: | |
self.doingstr = True | |
self.strmod = diff[1] * sens | |
else: | |
self.strmod = diff[1] * sens | |
context.area.tag_redraw() | |
if event.type in {'LEFTMOUSE'} or self.action == 1: | |
# apply changes, finished | |
if hasattr(self, '_handle'): | |
context.space_data.draw_handler_remove(self._handle, 'WINDOW') | |
del self._handle | |
applyChanges(self) | |
return {'FINISHED'} | |
elif event.type in {'ESC'} or self.action == -1: | |
# do nothing, return to previous settings | |
if hasattr(self, '_handle'): | |
context.space_data.draw_handler_remove(self._handle, 'WINDOW') | |
del self._handle | |
revertChanges(self) | |
return {'CANCELLED'} | |
elif self.keyaction != 'IGNORE' and event.type in {self.hotkey} and event.value == 'RELEASE': | |
# if key action enabled, prepare to exit | |
if self.keyaction == 'FINISH': | |
if hasattr(self, '_handle'): | |
context.space_data.draw_handler_remove(self._handle, 'WINDOW') | |
del self._handle | |
self.action = 1 | |
elif self.keyaction == 'CANCEL': | |
if hasattr(self, '_handle'): | |
context.space_data.draw_handler_remove(self._handle, 'WINDOW') | |
del self._handle | |
self.action = -1 | |
return {'RUNNING_MODAL'} | |
else: | |
# continuation | |
applyChanges(self) | |
self.prev = self.cur | |
return {'RUNNING_MODAL'} | |
return {'CANCELLED'} | |
def invoke(self, context, event): | |
if bpy.context.mode == 'SCULPT': | |
self.brush = context.tool_settings.sculpt.brush | |
elif bpy.context.mode == 'PAINT_TEXTURE': | |
self.brush = context.tool_settings.image_paint.brush | |
elif bpy.context.mode == 'PAINT_VERTEX': | |
self.brush = context.tool_settings.vertex_paint.brush | |
elif bpy.context.mode == 'PAINT_WEIGHT': | |
self.brush = context.tool_settings.weight_paint.brush | |
else: | |
self.report({'WARNING'}, "Mode invalid - only paint or sculpt") | |
return {'CANCELLED'} | |
self.hotkey = event.type | |
if self.hotkey == 'NONE': | |
self.keyaction = 'IGNORE' | |
self.action = 0 | |
unify_settings = context.tool_settings.unified_paint_settings | |
self.uni_size = unify_settings.use_unified_size | |
self.uni_str = unify_settings.use_unified_strength | |
self.doingrad = False | |
self.doingstr = False | |
self.start = (event.mouse_region_x, event.mouse_region_y) | |
self.prev = self.start | |
self.radmod_total = 0.0 | |
self.strmod_total = 0.0 | |
self.radmod = 0.0 | |
self.strmod = 0.0 | |
# self._handle = context.space_data.draw_handler_add(draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') | |
if self.graphic: | |
if not hasattr(self, '_handle'): | |
self._handle = context.space_data.draw_handler_add(draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') | |
self.brushcolor = self.brush.cursor_color_add | |
if self.brush.sculpt_capabilities.has_secondary_color and self.brush.direction in {'SUBTRACT','DEEPEN','MAGNIFY','PEAKS','CONTRAST','DEFLATE'}: | |
self.brushcolor = self.brush.cursor_color_subtract | |
if self.text != 'NONE': | |
if not hasattr(self, '_handle'): | |
self._handle = context.space_data.draw_handler_add(draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') | |
self.offset = (30, -37) | |
self.backcolor = Color((1.0, 1.0, 1.0)) - context.preferences.themes['Default'].view_3d.space.text_hi | |
if self.slider != 'NONE': | |
if not hasattr(self, '_handle'): | |
self._handle = context.space_data.draw_handler_add(draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') | |
if self.slider == 'LARGE': | |
self.sliderheight = 16 | |
self.sliderwidth = 180 | |
elif self.slider == 'MEDIUM': | |
self.sliderheight = 8 | |
self.sliderwidth = 80 | |
else: | |
self.sliderheight = 3 | |
self.sliderwidth = 60 | |
if not hasattr(self, 'offset'): | |
self.offset = (30, -37) | |
if not hasattr(self, 'backcolor'): | |
self.backcolor = Color((1.0, 1.0, 1.0)) - context.preferences.themes['Default'].view_3d.space.text_hi | |
self.frontcolor = context.preferences.themes['Default'].view_3d.space.text_hi | |
# enter modal operation | |
context.window_manager.modal_handler_add(self) | |
return {'RUNNING_MODAL'} | |
def register(): | |
bpy.utils.register_class(PAINT_OT_brush_modal_quickset) | |
cfg = bpy.context.window_manager.keyconfigs.addon | |
if not cfg.keymaps.__contains__('Sculpt'): | |
cfg.keymaps.new('Sculpt', space_type='EMPTY', region_type='WINDOW') | |
kmi = cfg.keymaps['Sculpt'].keymap_items | |
kmi.new('brush.modal_quickset', 'RIGHTMOUSE', 'PRESS') | |
def unregister(): | |
bpy.utils.unregister_class(PAINT_OT_brush_modal_quickset) | |
cfg = bpy.context.window_manager.keyconfigs.addon | |
if cfg.keymaps.__contains__('Sculpt'): | |
for kmi in cfg.keymaps['Sculpt'].keymap_items: | |
if kmi.idname == 'brush.modal_quickset': | |
if kmi.value == 'PRESS' and kmi.type == 'RIGHTMOUSE': | |
cfg.keymaps['Sculpt'].keymap_items.remove(kmi) | |
break |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment