2025-12-01
This commit is contained in:
@@ -16,16 +16,161 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import blf
|
||||
import gpu
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
from bpy import app
|
||||
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .image_utils import IMG
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
cached_images = {}
|
||||
|
||||
cached_gpu_textures = {}
|
||||
|
||||
_cached_image_shader: Optional[gpu.types.GPUShader] = None
|
||||
|
||||
|
||||
VERTEX_SHADER_LEGACY = """
|
||||
uniform mat4 ModelViewProjectionMatrix;
|
||||
in vec2 pos;
|
||||
in vec2 texCoord;
|
||||
out vec2 uv;
|
||||
|
||||
void main()
|
||||
{
|
||||
uv = texCoord;
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
FRAGMENT_SHADER_LEGACY = """
|
||||
in vec2 uv;
|
||||
out vec4 fragColor;
|
||||
uniform sampler2D image;
|
||||
uniform float transparency;
|
||||
uniform int color_space_mode;
|
||||
|
||||
vec3 linear_to_srgb(vec3 linear_color)
|
||||
{
|
||||
vec3 cutoff = vec3(0.0031308);
|
||||
vec3 lower = linear_color * 12.92;
|
||||
vec3 higher = 1.055 * pow(max(linear_color, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
|
||||
return mix(lower, higher, step(cutoff, linear_color));
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 color = texture(image, uv);
|
||||
if (color_space_mode == 1) {
|
||||
color.rgb = linear_to_srgb(color.rgb);
|
||||
}
|
||||
color.a *= transparency;
|
||||
fragColor = color;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def create_image_shader_info():
|
||||
"""Return GPU shader info for the runtime image shader."""
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.vertex_in(0, "VEC2", "pos")
|
||||
shader_info.vertex_in(1, "VEC2", "texCoord")
|
||||
|
||||
stage_iface = gpu.types.GPUStageInterfaceInfo("uv_iface")
|
||||
stage_iface.smooth("VEC2", "uv")
|
||||
shader_info.vertex_out(stage_iface)
|
||||
|
||||
shader_info.push_constant("MAT4", "ModelViewProjectionMatrix")
|
||||
shader_info.push_constant("FLOAT", "transparency")
|
||||
shader_info.push_constant("INT", "color_space_mode")
|
||||
shader_info.sampler(0, "FLOAT_2D", "image")
|
||||
|
||||
shader_info.fragment_out(0, "VEC4", "fragColor")
|
||||
shader_info.vertex_source(
|
||||
"""
|
||||
void main()
|
||||
{
|
||||
uv = texCoord;
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
)
|
||||
shader_info.fragment_source(
|
||||
"""
|
||||
void main()
|
||||
{
|
||||
vec4 color = texture(image, uv);
|
||||
if (color_space_mode == 1) {
|
||||
vec3 cutoff = vec3(0.0031308);
|
||||
vec3 lower = color.rgb * 12.92;
|
||||
vec3 higher = 1.055 * pow(max(color.rgb, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
|
||||
color.rgb = mix(lower, higher, step(cutoff, color.rgb));
|
||||
}
|
||||
color.a *= transparency;
|
||||
fragColor = color;
|
||||
}
|
||||
"""
|
||||
)
|
||||
return shader_info
|
||||
|
||||
|
||||
def create_image_shader():
|
||||
"""Return a cached shader that supports transparency across Blender versions.
|
||||
Features:
|
||||
- sRGB conversion for UI overlays
|
||||
- transparency
|
||||
"""
|
||||
global _cached_image_shader
|
||||
|
||||
if _cached_image_shader is not None:
|
||||
return _cached_image_shader
|
||||
|
||||
shader = None
|
||||
|
||||
create_info_supported = (
|
||||
hasattr(gpu, "shader")
|
||||
and hasattr(gpu.shader, "create_from_info")
|
||||
and hasattr(gpu.types, "GPUShaderCreateInfo")
|
||||
)
|
||||
|
||||
if create_info_supported:
|
||||
try:
|
||||
shader_info = create_image_shader_info()
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to create image shader")
|
||||
shader = None
|
||||
|
||||
if shader is None:
|
||||
try:
|
||||
shader = gpu.types.GPUShader(VERTEX_SHADER_LEGACY, FRAGMENT_SHADER_LEGACY)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to create image shader")
|
||||
|
||||
if shader is None:
|
||||
# fallback to builtin shader
|
||||
# mainly for MacOS builds that have issues with custom shaders
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_IMAGE")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("IMAGE")
|
||||
|
||||
_cached_image_shader = shader
|
||||
return shader
|
||||
|
||||
|
||||
def draw_rect(x, y, width, height, color):
|
||||
"""Used for drawing 2D rectangle backgrounds."""
|
||||
xmax = x + width
|
||||
ymax = y + height
|
||||
points = (
|
||||
@@ -48,6 +193,31 @@ def draw_rect(x, y, width, height, color):
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_rect_outline(x, y, width, height, color, line_width=1.0):
|
||||
"""Used for drawing 2D rectangle outlines."""
|
||||
xmax = x + width
|
||||
ymax = y + height
|
||||
coords = (
|
||||
(x, y), # (x, y)
|
||||
(x, ymax), # (x, y)
|
||||
(xmax, ymax), # (x, y)
|
||||
(xmax, y), # (x, y)
|
||||
)
|
||||
indices = ((0, 1), (1, 2), (2, 3), (3, 0))
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "LINES", {"pos": coords}, indices=indices)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(line_width)
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_line2d(x1, y1, x2, y2, width, color):
|
||||
"""Used for drawing line from dragged thumbnail to the 3D bounding box."""
|
||||
coords = ((x1, y1), (x2, y2))
|
||||
@@ -115,30 +285,59 @@ def draw_lines(vertices, indices, color):
|
||||
|
||||
|
||||
def draw_rect_3d(coords, color):
|
||||
"""Used for drawing 3D rectangle backgrounds."""
|
||||
indices = [(0, 1, 2), (2, 3, 0)]
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "TRIS", {"pos": coords}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
cached_images = {}
|
||||
def _resolve_color_space_mode() -> int:
|
||||
"""Return shader color conversion mode for the current drawing context.
|
||||
|
||||
area over non-3D means UI overlay, so we need to apply sRGB conversion."""
|
||||
area = getattr(bpy.context, "area", None)
|
||||
if area is None:
|
||||
return 0
|
||||
|
||||
# Blender 5.0+ node editors already expect linear data, so avoid extra conversion there
|
||||
node_editor_types = {"NODE_EDITOR", "VIEW_3D"}
|
||||
|
||||
if area.type in node_editor_types:
|
||||
return 0
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batch=None):
|
||||
# draw_rect(x,y, width, height, (.5,0,0,.5))
|
||||
def draw_image_runtime(
|
||||
x: float,
|
||||
y: float,
|
||||
width: float,
|
||||
height: float,
|
||||
image: Union[bpy.types.Image, IMG],
|
||||
transparency: Optional[float] = 1.0,
|
||||
crop: Tuple[float, float, float, float] = (0, 0, 1, 1),
|
||||
batch: Optional[gpu.types.GPUBatch] = None,
|
||||
) -> Optional[gpu.types.GPUBatch]:
|
||||
"""Draws an image at given location with given size.
|
||||
|
||||
try:
|
||||
image.name
|
||||
except:
|
||||
print("Image is invalid- draw function")
|
||||
return
|
||||
Returns:
|
||||
The batch object if successful, or None if the image is invalid.
|
||||
"""
|
||||
if not image.name or not image.filepath:
|
||||
return None
|
||||
|
||||
ci = cached_images.get(image.filepath)
|
||||
image_shader = create_image_shader()
|
||||
|
||||
texture = None
|
||||
ci = cached_images.get(image.filepath + "GPU_TEXTURE")
|
||||
if ci is not None:
|
||||
if (
|
||||
ci["x"] == x
|
||||
@@ -149,6 +348,7 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
batch = ci["batch"]
|
||||
image_shader = ci["image_shader"]
|
||||
texture = ci["texture"]
|
||||
|
||||
if not batch:
|
||||
coords = [(x, y), (x + width, y), (x, y + height), (x + width, y + height)]
|
||||
|
||||
@@ -161,16 +361,14 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
|
||||
indices = [(0, 1, 2), (2, 1, 3)]
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
image_shader = gpu.shader.from_builtin("2D_IMAGE")
|
||||
else:
|
||||
image_shader = gpu.shader.from_builtin("IMAGE")
|
||||
batch = batch_for_shader(
|
||||
image_shader, "TRIS", {"pos": coords, "texCoord": uvs}, indices=indices
|
||||
)
|
||||
texture = gpu.texture.from_image(image)
|
||||
|
||||
texture = path_to_gpu_texture(image.filepath)
|
||||
|
||||
# tell shader to use the image that is bound to image unit 0
|
||||
cached_images[image.filepath] = {
|
||||
cached_images[image.filepath + "GPU_TEXTURE"] = {
|
||||
"x": x,
|
||||
"y": y,
|
||||
"width": width,
|
||||
@@ -179,19 +377,56 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
"image_shader": image_shader,
|
||||
"texture": texture,
|
||||
}
|
||||
# send image to gpu if it isn't there already
|
||||
if image.gl_load():
|
||||
raise Exception()
|
||||
|
||||
# texture = gpu.texture.from_image(image)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
batch.draw(image_shader)
|
||||
if batch is None:
|
||||
return None
|
||||
|
||||
if image_shader and texture:
|
||||
color_space_mode = _resolve_color_space_mode()
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
|
||||
# may not be available in simple shader
|
||||
try:
|
||||
# set floats
|
||||
image_shader.uniform_float("transparency", transparency)
|
||||
|
||||
# set color space mode
|
||||
image_shader.uniform_int("color_space_mode", color_space_mode)
|
||||
batch.draw(image_shader)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return batch
|
||||
|
||||
|
||||
def path_to_gpu_texture(path: str) -> Optional[gpu.types.GPUTexture]:
|
||||
"""Convert a Blender image to a GPU texture.
|
||||
|
||||
Returns:
|
||||
The GPU texture if successful, or None if the image is invalid.
|
||||
"""
|
||||
# check if exists and is file [prevent exception for missing files]
|
||||
if path in cached_gpu_textures:
|
||||
return cached_gpu_textures[path]
|
||||
|
||||
if not os.path.exists(path) or not os.path.isfile(path):
|
||||
# do not spam log with warnings, just return None
|
||||
return None
|
||||
img = bpy.data.images.load(path, check_existing=False)
|
||||
img.gl_load()
|
||||
|
||||
tex = gpu.texture.from_image(img)
|
||||
cached_gpu_textures[path] = tex
|
||||
|
||||
# # Clean up Blender image
|
||||
bpy.data.images.remove(img)
|
||||
return tex
|
||||
|
||||
|
||||
def get_text_size(font_id=0, text="", text_size=16, dpi=72):
|
||||
if app.version < (4, 0, 0):
|
||||
blf.size(font_id, text_size, dpi)
|
||||
|
||||
Reference in New Issue
Block a user