BlenderKit: fixes for HDRs

Now HDRs are clearly separated between true HDR files and simple 360 photos.
the 360 photos are hidden in search filter by default.
Also fixed version check for HDRs and several little tweaks to UI
This commit is contained in:
Vilem Duha 2021-08-12 16:15:06 +02:00
parent 2fb1729cab
commit 31abe54983
8 changed files with 162 additions and 60 deletions

View File

@ -976,16 +976,19 @@ class BlenderKitBrushSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
texture_resolution_max: IntProperty(name="Texture Resolution Max", description="texture resolution maximum",
default=0)
evs_cap: IntProperty(name="EV cap", description="EVs dynamic range",
default=0)
true_hdr: BoolProperty(name="Real HDR", description="Image has High dynamic range.",default=False)
class BlenderKitBrushUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
mode: EnumProperty(
name="Mode",
items=(
('IMAGE', 'Texture paint', "Texture brush"),
('SCULPT', 'Sculpt', 'Sculpt brush'),
('VERTEX', 'Vertex paint', 'Vertex paint brush'),
('WEIGHT', 'Weight paint', 'Weight paint brush'),
("IMAGE", "Texture paint", "Texture brush"),
("SCULPT", "Sculpt", "Sculpt brush"),
("VERTEX", "Vertex paint", "Vertex paint brush"),
("WEIGHT", "Weight paint", "Weight paint brush"),
),
description="Mode where the brush works",
default="SCULPT",
@ -1514,6 +1517,13 @@ class BlenderKitHDRSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
update=search.search_update
)
true_hdr: BoolProperty(
name='Real HDRs only',
description='Search only for real HDRs, this means images that have a range higher than 0-1 in their pixels.',
default=True,
update=search.search_update
)
class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
search_keywords: StringProperty(

View File

@ -17,7 +17,7 @@
# ##### END GPL LICENSE BLOCK #####
from blenderkit import paths, append_link, utils, ui, colors, tasks_queue, rerequests, resolutions, ui_panels
from blenderkit import paths, append_link, utils, ui, colors, tasks_queue, rerequests, resolutions, ui_panels, search
import threading
import time
@ -693,7 +693,11 @@ def delete_unfinished_file(file_name):
def download_asset_file(asset_data, resolution='blend', api_key = ''):
# this is a simple non-threaded way to download files for background resolution genenration tool
file_name = paths.get_download_filepaths(asset_data, resolution)[0] # prefer global dir if possible.
file_names = paths.get_download_filepaths(asset_data, resolution) # prefer global dir if possible.
if len(file_names) == 0:
return None
file_name = file_names[0]
if check_existing(asset_data, resolution=resolution):
# this sends the thread for processing, where another check should occur, since the file might be corrupted.
@ -704,6 +708,7 @@ def download_asset_file(asset_data, resolution='blend', api_key = ''):
with open(file_name, "wb") as f:
print("Downloading %s" % file_name)
headers = utils.get_headers(api_key)
res_file_info, resolution = paths.get_res_file(asset_data, resolution)
response = requests.get(res_file_info['url'], stream=True)
total_length = response.headers.get('Content-Length')
@ -1308,12 +1313,23 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
# or from the scene.
asset_base_id = self.asset_base_id
au = s.get('assets used')
if au == None:
s['assets used'] = {}
if asset_base_id in s.get('assets used'):
# already used assets have already download link and especially file link.
asset_data = s['assets used'][asset_base_id].to_dict()
au = s.get('assets used')
if au == None:
s['assets used'] = {}
if asset_base_id in s.get('assets used'):
# already used assets have already download link and especially file link.
asset_data = s['assets used'][asset_base_id].to_dict()
else:
#when not in scene nor in search results, we need to get it from the server
params = {
'asset_base_id': self.asset_base_id
}
preferences = bpy.context.preferences.addons['blenderkit'].preferences
results = search.get_search_simple(params, page_size=1, max_results=1,
api_key=preferences.api_key)
asset_data = search.parse_result(results[0])
return asset_data
def execute(self, context):

View File

@ -2,6 +2,7 @@ import bpy
import os
import time
def get_orig_render_settings():
rs = bpy.context.scene.render
ims = rs.image_settings
@ -33,7 +34,8 @@ def set_orig_render_settings(orig_settings):
vs.view_transform = orig_settings['view_transform']
def img_save_as(img, filepath='//', file_format='JPEG', quality=90, color_mode='RGB', compression=15, view_transform = 'Raw', exr_codec = 'DWAA'):
def img_save_as(img, filepath='//', file_format='JPEG', quality=90, color_mode='RGB', compression=15,
view_transform='Raw', exr_codec='DWAA'):
'''Uses Blender 'save render' to save images - BLender isn't really able so save images with other methods correctly.'''
ors = get_orig_render_settings()
@ -49,11 +51,11 @@ def img_save_as(img, filepath='//', file_format='JPEG', quality=90, color_mode='
ims.exr_codec = exr_codec
vs.view_transform = view_transform
img.save_render(filepath=bpy.path.abspath(filepath), scene=bpy.context.scene)
set_orig_render_settings(ors)
def set_colorspace(img, colorspace):
'''sets image colorspace, but does so in a try statement, because some people might actually replace the default
colorspace settings, and it literally can't be guessed what these people use, even if it will mostly be the filmic addon.
@ -66,11 +68,22 @@ def set_colorspace(img, colorspace):
except:
print(f'Colorspace {colorspace} not found.')
def analyze_image_is_true_hdr(image):
import numpy
scene = bpy.context.scene
ui_props = scene.blenderkitUI
size = image.size
imageWidth = size[0]
imageHeight = size[1]
tempBuffer = numpy.empty(imageWidth * imageHeight * 4, dtype=numpy.float32)
image.pixels.foreach_get(tempBuffer)
image.blenderkit.true_hdr = numpy.amax(tempBuffer) > 1.05
def generate_hdr_thumbnail():
import numpy
scene = bpy.context.scene
ui_props = scene.blenderkitUI
hdr_image = ui_props.hdr_upload_image#bpy.data.images.get(ui_props.hdr_upload_image)
hdr_image = ui_props.hdr_upload_image # bpy.data.images.get(ui_props.hdr_upload_image)
base, ext = os.path.splitext(hdr_image.filepath)
thumb_path = base + '.jpg'
@ -90,6 +103,8 @@ def generate_hdr_thumbnail():
hdr_image.pixels.foreach_get(tempBuffer)
hdr_image.blenderkit.true_hdr = numpy.amax(tempBuffer) > 1.05
inew.filepath = thumb_path
set_colorspace(inew, 'Linear')
inew.pixels.foreach_set(tempBuffer)
@ -103,29 +118,31 @@ def generate_hdr_thumbnail():
def find_color_mode(image):
if not isinstance(image, bpy.types.Image):
raise(TypeError)
raise (TypeError)
else:
depth_mapping = {
8: 'BW',
24: 'RGB',
32: 'RGBA',#can also be bw.. but image.channels doesn't work.
32: 'RGBA', # can also be bw.. but image.channels doesn't work.
96: 'RGB',
128: 'RGBA',
}
return depth_mapping.get(image.depth,'RGB')
return depth_mapping.get(image.depth, 'RGB')
def find_image_depth(image):
if not isinstance(image, bpy.types.Image):
raise(TypeError)
raise (TypeError)
else:
depth_mapping = {
8: '8',
24: '8',
32: '8',#can also be bw.. but image.channels doesn't work.
32: '8', # can also be bw.. but image.channels doesn't work.
96: '16',
128: '16',
}
return depth_mapping.get(image.depth,'8')
return depth_mapping.get(image.depth, '8')
def can_erase_alpha(na):
alpha = na[3::4]
@ -148,6 +165,7 @@ def is_image_black(na):
print('image can have alpha channel dropped')
return rgbsum == 0
def is_image_bw(na):
r = na[::4]
g = na[1::4]
@ -186,7 +204,8 @@ def numpytoimage(a, iname, width=0, height=0, channels=3):
if image.name[:len(iname)] == iname and image.size[0] == width and image.size[1] == height:
i = image
if i is None:
i = bpy.data.images.new(iname, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False, tiled=False)
i = bpy.data.images.new(iname, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False,
tiled=False)
# dropping this re-shaping code - just doing flat array for speed and simplicity
# d = a.shape[0] * a.shape[1]
@ -220,6 +239,7 @@ def imagetonumpy_flat(i):
# print('\ntime of image to numpy ' + str(time.time() - t))
return na
def imagetonumpy(i):
t = time.time()
@ -273,18 +293,19 @@ def get_rgb_mean(i):
# return(rmedian,gmedian, bmedian)
return (rmean, gmean, bmean)
def check_nmap_mean_ok(i):
'''checks if normal map values are in standard range.'''
rmean,gmean,bmean = get_rgb_mean(i)
rmean, gmean, bmean = get_rgb_mean(i)
#we could/should also check blue, but some ogl substance exports have 0-1, while 90% nmaps have 0.5 - 1.
nmap_ok = 0.45< rmean < 0.55 and .45 < gmean < .55
# we could/should also check blue, but some ogl substance exports have 0-1, while 90% nmaps have 0.5 - 1.
nmap_ok = 0.45 < rmean < 0.55 and .45 < gmean < .55
return nmap_ok
def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
def check_nmap_ogl_vs_dx(i, mask=None, generated_test_images=False):
'''
checks if normal map is directX or OpenGL.
Returns - String value - DirectX and OpenGL
@ -293,8 +314,6 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
width = i.size[0]
height = i.size[1]
rmean, gmean, bmean = get_rgb_mean(i)
na = imagetonumpy(i)
@ -306,8 +325,8 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
green_y_comparison = numpy.zeros((width, height), numpy.float32)
if generated_test_images:
red_x_comparison_img = numpy.empty((width, height, 4), numpy.float32) #images for debugging purposes
green_y_comparison_img = numpy.empty((width, height, 4), numpy.float32)#images for debugging purposes
red_x_comparison_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes
green_y_comparison_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes
ogl = numpy.zeros((width, height), numpy.float32)
dx = numpy.zeros((width, height), numpy.float32)
@ -318,21 +337,21 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
for y in range(0, height):
for x in range(0, width):
#try to mask with UV mask image
if mask is None or mask[x,y,3]>0:
# try to mask with UV mask image
if mask is None or mask[x, y, 3] > 0:
last_height_x = ogl[max(x - 1, 0), min(y, height - 1)]
last_height_y = ogl[max(x,0), min(y - 1,height-1)]
last_height_y = ogl[max(x, 0), min(y - 1, height - 1)]
diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5)))
diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5)))
calc_height = (last_height_x + last_height_y) \
- diff_x - diff_y
calc_height = calc_height /2
- diff_x - diff_y
calc_height = calc_height / 2
ogl[x, y] = calc_height
if generated_test_images:
rgb = calc_height *.1 +.5
ogl_img[x,y] = [rgb,rgb,rgb,1]
rgb = calc_height * .1 + .5
ogl_img[x, y] = [rgb, rgb, rgb, 1]
# green channel
last_height_x = dx[max(x - 1, 0), min(y, height - 1)]
@ -348,7 +367,6 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
rgb = calc_height * .1 + .5
dx_img[x, y] = [rgb, rgb, rgb, 1]
ogl_std = ogl.std()
dx_std = dx.std()
@ -362,7 +380,6 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
else:
print('this is probably an OpenGL texture')
if generated_test_images:
# red_x_comparison_img = red_x_comparison_img.swapaxes(0,1)
# red_x_comparison_img = red_x_comparison_img.flatten()
@ -383,9 +400,10 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
numpytoimage(dx_img, 'DirectX', width=width, height=height, channels=1)
if abs(ogl_std) > abs(dx_std):
return 'DirectX'
return 'DirectX'
return 'OpenGL'
def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=False, do_downscale=False):
'''checks the image and saves it to drive with possibly reduced channels.
Also can remove the image from the asset if the image is pure black
@ -396,7 +414,7 @@ def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=Fa
'''
colorspace = teximage.colorspace_settings.name
teximage.colorspace_settings.name = 'Non-Color'
#teximage.colorspace_settings.name = 'sRGB' color correction mambo jambo.
# teximage.colorspace_settings.name = 'sRGB' color correction mambo jambo.
JPEG_QUALITY = 90
# is_image_black(na)
@ -429,7 +447,7 @@ def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=Fa
image_depth = find_image_depth(teximage)
ims.color_mode = find_color_mode(teximage)
#image_depth = str(max(min(int(teximage.depth / 3), 16), 8))
# image_depth = str(max(min(int(teximage.depth / 3), 16), 8))
print('resulting depth set to:', image_depth)
fp = input_filepath
@ -469,8 +487,6 @@ def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=Fa
if do_downscale:
downscale(teximage)
# it's actually very important not to try to change the image filepath and packed file filepath before saving,
# blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes.
teximage.save_render(filepath=bpy.path.abspath(fp), scene=bpy.context.scene)

View File

@ -310,7 +310,6 @@ def get_download_filepaths(asset_data, resolution='blend', can_return_others = F
'''Get all possible paths of the asset and resolution. Usually global and local directory.'''
dirs = get_download_dirs(asset_data['assetType'])
res_file, resolution = get_res_file(asset_data, resolution, find_closest_with_url = can_return_others)
name_slug = slugify(asset_data['name'])
asset_folder_name = f"{name_slug}_{asset_data['id']}"

View File

@ -1133,9 +1133,12 @@ def build_query_HDR():
props = bpy.context.window_manager.blenderkit_HDR
query = {
"asset_type": 'hdr',
# "engine": props.search_engine,
# "adult": props.search_adult,
}
if props.true_hdr:
query["trueHDR"] = props.true_hdr
build_query_common(query, props)
return query
@ -1283,6 +1286,8 @@ def get_search_simple(parameters, filepath=None, page_size=100, max_results=1000
requeststring += f'+{p}:{parameters[p]}'
requeststring += '&page_size=' + str(page_size)
requeststring += '&dict_parameters=1'
bk_logger.debug(requeststring)
response = rerequests.get(requeststring, headers=headers) # , params = rparameters)
# print(response.json())
@ -1425,6 +1430,8 @@ def update_filters():
sprops.search_polycount
elif ui_props.asset_type == 'MATERIAL':
sprops.use_filters = fcommon
elif ui_props.asset_type == 'HDR':
sprops.use_filters = sprops.true_hdr
def search_update(self, context):

View File

@ -870,6 +870,31 @@ class VIEW3D_PT_blenderkit_advanced_material_search(Panel):
row.prop(props, "search_file_size_max", text='Max')
layout.prop(props, "quality_limit", slider=True)
class VIEW3D_PT_blenderkit_advanced_HDR_search(Panel):
bl_category = "BlenderKit"
bl_idname = "VIEW3D_PT_blenderkit_advanced_HDR_search"
bl_parent_id = "VIEW3D_PT_blenderkit_unified"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_label = "Search filters"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
s = context.scene
ui_props = s.blenderkitUI
return ui_props.down_up == 'SEARCH' and ui_props.asset_type == 'HDR'
def draw(self, context):
wm = context.window_manager
props = wm.blenderkit_HDR
layout = self.layout
layout.separator()
layout.prop(props, "own_only")
layout.prop(props, "true_hdr")
class VIEW3D_PT_blenderkit_categories(Panel):
bl_category = "BlenderKit"
@ -2016,7 +2041,6 @@ class ClosePopupButton(bpy.types.Operator):
def win_close(self):
VK_ESCAPE = 0x1B
ctypes.windll.user32.keybd_event(VK_ESCAPE)
print('hit escape')
return True
def mouse_trick(self, context, x, y):
@ -2250,6 +2274,8 @@ def header_search_draw(self, context):
elif ui_props.asset_type == 'MATERIAL':
layout.popover(panel="VIEW3D_PT_blenderkit_advanced_material_search", text="", icon_value=icon_id)
elif ui_props.asset_type == 'HDR':
layout.popover(panel="VIEW3D_PT_blenderkit_advanced_HDR_search", text="", icon_value=icon_id)
def ui_message(title, message):
@ -2271,6 +2297,7 @@ classes = (
VIEW3D_PT_blenderkit_unified,
VIEW3D_PT_blenderkit_advanced_model_search,
VIEW3D_PT_blenderkit_advanced_material_search,
VIEW3D_PT_blenderkit_advanced_HDR_search,
VIEW3D_PT_blenderkit_categories,
VIEW3D_PT_blenderkit_import_settings,
VIEW3D_PT_blenderkit_model_properties,

View File

@ -63,7 +63,6 @@ def get_app_version():
return '%i.%i.%i' % (ver[0], ver[1], ver[2])
def add_version(data):
app_version = get_app_version()
addon_version = version_checker.get_addon_version()
@ -444,6 +443,9 @@ def get_upload_data(caller=None, context=None, asset_type=None):
return None, None
props = image.blenderkit
image_utils.analyze_image_is_true_hdr(image)
# props.name = brush.name
base, ext = os.path.splitext(image.filepath)
thumb_path = base + '.jpg'
@ -460,8 +462,8 @@ def get_upload_data(caller=None, context=None, asset_type=None):
# mat analytics happen here, since they don't take up any time...
upload_params = {
"textureResolutionMax": props.texture_resolution_max
"textureResolutionMax": props.texture_resolution_max,
"trueHDR": props.true_hdr
}
upload_data = {
@ -660,11 +662,8 @@ class FastMetadata(bpy.types.Operator):
update=update_free_full
)
####################
@classmethod
def poll(cls, context):
scene = bpy.context.scene
@ -729,7 +728,7 @@ class FastMetadata(bpy.types.Operator):
asset_data = dict(sr[ui_props.active_index])
else:
active_asset = utils.get_active_asset_by_type(asset_type = self.asset_type)
active_asset = utils.get_active_asset_by_type(asset_type=self.asset_type)
asset_data = active_asset.get('asset_data')
if not can_edit_asset(asset_data=asset_data):
@ -1081,6 +1080,9 @@ def start_upload(self, context, asset_type, reupload, upload_set):
if 'THUMBNAIL' in upload_set:
if asset_type == 'HDR':
image_utils.generate_hdr_thumbnail()
# get upload data because the image utils function sets true_hdr
export_data, upload_data = get_upload_data(caller=self, context=context, asset_type=asset_type)
elif not os.path.exists(export_data["thumbnail_path"]):
props.upload_state = 'Thumbnail not found'
props.uploading = False
@ -1214,9 +1216,12 @@ class UploadOperator(Operator):
layout.prop(self, 'thumbnail')
if props.asset_base_id != '' and not self.reupload:
layout.label(text="Really upload as new? ")
layout.label(text="Do this only when you create a new asset from an old one.")
layout.label(text="For updates of thumbnail or model use reupload.")
utils.label_multiline(layout, text="Really upload as new?\n"
"Do this only when you create\n"
"a new asset from an old one.\n"
"For updates of thumbnail or model use reupload.\n",
width=400, icon='ERROR')
if props.is_private == 'PUBLIC':
if self.asset_type == 'MODEL':
@ -1229,6 +1234,22 @@ class UploadOperator(Operator):
'- Check if it has all textures and renders as expected\n'
'- Check if it has correct size in world units (for models)'
, width=400)
elif self.asset_type == 'HDR':
if not props.true_hdr:
utils.label_multiline(layout, text="This image isn't HDR,\n"
"It has a low dynamic range.\n"
"BlenderKit library accepts 360 degree images\n"
"however the default filter setting for search\n"
"is to show only true HDR images\n"
, icon='ERROR', width=400)
utils.label_multiline(layout, text='You marked the asset as public.\n'
'This means it will be validated by our team.\n\n'
'Please test your upload after it finishes:\n'
'- Open a new file\n'
'- Find the asset and download it\n'
'- Check if it works as expected\n'
, width=400)
else:
utils.label_multiline(layout, text='You marked the asset as public.\n'
'This means it will be validated by our team.\n\n'
@ -1239,12 +1260,17 @@ class UploadOperator(Operator):
, width=400)
def invoke(self, context, event):
props = utils.get_upload_props()
if not utils.user_logged_in():
ui_panels.draw_not_logged_in(self, message='To upload assets you need to login/signup.')
return {'CANCELLED'}
if self.asset_type == 'HDR':
props = utils.get_upload_props()
# getting upload data for images ensures true_hdr check so users can be informed about their handling
# simple 360 photos or renders with LDR are hidden by default..
export_data, upload_data = get_upload_data(asset_type='HDR')
# if props.is_private == 'PUBLIC':
return context.window_manager.invoke_props_dialog(self)
# else:

View File

@ -838,10 +838,11 @@ def user_is_owner(asset_data=None):
def asset_from_newer_blender_version(asset_data):
bver = bpy.app.version
aver = asset_data['sourceAppVersion'].split('.')
# print(aver,bver)
#print(aver,bver)
bver_f = bver[0] + bver[1] * .01 + bver[2] * .0001
aver_f = int(aver[0]) + int(aver[1]) * .01 + int(aver[2]) * .0001
return aver_f>bver_f
if len(aver)>=3:
aver_f = int(aver[0]) + int(aver[1]) * .01 + int(aver[2]) * .0001
return aver_f>bver_f
def guard_from_crash():
'''