Commit 6c6d1a9b63 allowed several units to OSL camera shaders'
parameters. Only meters 'm' were supported as distance units, because
others cannot be converted automatically into the shader' unit.
This commit allows using 'mm' in addition to 'm', and makes the
Blender property use a 'DISTANCE_CAMERA' subtype. This assumes that
someone using 'mm' specifically wants to use the value for camera
parameters, which means it can be used as is, without any conversion
in the shader.
The camera templates were updated to use a focal length in mm.
Pull Request: https://projects.blender.org/blender/blender/pulls/147347
376 lines
13 KiB
Python
376 lines
13 KiB
Python
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from __future__ import annotations
|
|
|
|
import bpy
|
|
import _cycles
|
|
|
|
from bpy.app.translations import pgettext_rpt as rpt_
|
|
|
|
|
|
def osl_compile(input_path, report):
|
|
"""compile .osl file with given filepath to temporary .oso file"""
|
|
import tempfile
|
|
output_file = tempfile.NamedTemporaryFile(mode='w', suffix=".oso", delete=False)
|
|
output_path = output_file.name
|
|
output_file.close()
|
|
|
|
ok = _cycles.osl_compile(input_path, output_path)
|
|
|
|
if ok:
|
|
report({'INFO'}, "OSL shader compilation succeeded")
|
|
|
|
return ok, output_path
|
|
|
|
|
|
def shader_param_type_default(param, is_bool):
|
|
if param.isclosure:
|
|
return 'NodeSocketShader', None
|
|
elif param.type.vecsemantics == param.type.vecsemantics.COLOR:
|
|
return 'NodeSocketColor', (param.value[0], param.value[1], param.value[2], 1.0)
|
|
elif param.type.vecsemantics in [
|
|
param.type.vecsemantics.POINT,
|
|
param.type.vecsemantics.VECTOR,
|
|
param.type.vecsemantics.NORMAL,
|
|
]:
|
|
return 'NodeSocketVector', param.value
|
|
elif param.type.aggregate == param.type.aggregate.SCALAR:
|
|
if param.type.basetype == param.type.basetype.INT:
|
|
if is_bool:
|
|
return 'NodeSocketBool', bool(param.value)
|
|
else:
|
|
return 'NodeSocketInt', int(param.value)
|
|
elif param.type.basetype == param.type.basetype.FLOAT:
|
|
return 'NodeSocketFloat', float(param.value)
|
|
elif param.type.basetype == param.type.basetype.STRING:
|
|
return 'NodeSocketString', str(param.value)
|
|
|
|
return None, None
|
|
|
|
|
|
def shader_param_ensure(node, param):
|
|
# Skip unsupported types
|
|
if param.varlenarray or param.isstruct or param.type.arraylen > 1:
|
|
return None
|
|
|
|
metadata = {meta.name: meta.value for meta in param.metadata}
|
|
|
|
is_bool = metadata.get('widget') in ['boolean', 'checkBox']
|
|
hide_value = (param.value is None) or (metadata.get('widget') == 'null')
|
|
label = metadata.get('label', param.name)
|
|
|
|
socket_type, default = shader_param_type_default(param, is_bool)
|
|
if not socket_type:
|
|
return None
|
|
|
|
sockets = node.outputs if param.isoutput else node.inputs
|
|
if param.name in sockets:
|
|
sock = sockets[param.name]
|
|
|
|
if sock.bl_idname != socket_type:
|
|
# Type doesn't match, delete the socket and recreate it below
|
|
sockets.remove(sock)
|
|
else:
|
|
# Update properties if needed
|
|
if sock.name != label:
|
|
sock.name = label
|
|
if not param.isoutput and sock.hide_value != hide_value:
|
|
sock.hide_value = hide_value
|
|
|
|
# We have a matching socket, no need to create one
|
|
return sock
|
|
|
|
sock = sockets.new(type=socket_type, name=label, identifier=param.name)
|
|
if default is not None:
|
|
sock.default_value = default
|
|
sock.hide_value = hide_value
|
|
return sock
|
|
|
|
|
|
def osl_param_ensure_property(ccam, param):
|
|
import idprop
|
|
|
|
if param.isoutput or param.isclosure:
|
|
return None
|
|
|
|
# Get metadata for the parameter to control UI display
|
|
metadata = {meta.name: meta.value for meta in param.metadata}
|
|
if 'label' not in metadata:
|
|
metadata['label'] = param.name
|
|
|
|
datatype = None
|
|
if param.type.basetype == param.type.basetype.INT:
|
|
datatype = int
|
|
elif param.type.basetype == param.type.basetype.FLOAT:
|
|
datatype = float
|
|
elif param.type.basetype == param.type.basetype.STRING:
|
|
datatype = str
|
|
|
|
# OSl doesn't have boolean as a type, but we do
|
|
if (datatype == int) and (metadata.get('widget') in ('boolean', 'checkBox')):
|
|
datatype = bool
|
|
default = param.value if isinstance(param.value, tuple) else [param.value]
|
|
default = [datatype(v) for v in default]
|
|
|
|
name = param.name
|
|
if name in ccam:
|
|
# If the parameter already exists, only reset its value if its type
|
|
# or array length changed
|
|
cur_data = ccam[name]
|
|
if isinstance(cur_data, idprop.types.IDPropertyArray):
|
|
cur_length = len(cur_data)
|
|
cur_type = type(cur_data[0])
|
|
else:
|
|
cur_length = 1
|
|
cur_type = type(cur_data)
|
|
do_replace = datatype != cur_type or len(default) != cur_length
|
|
else:
|
|
# Parameter doesn't exist yet, so set it from the defaults
|
|
do_replace = True
|
|
|
|
if do_replace:
|
|
ccam[name] = tuple(default) if len(default) > 1 else default[0]
|
|
|
|
ui = ccam.id_properties_ui(name)
|
|
ui.clear()
|
|
ui.update(default=tuple(default) if len(default) > 1 else default[0])
|
|
|
|
# Determine subtype (limited unit support for now)
|
|
if param.type.vecsemantics == param.type.vecsemantics.COLOR:
|
|
ui.update(subtype='COLOR')
|
|
elif param.type.vecsemantics == param.type.vecsemantics.POINT:
|
|
ui.update(subtype='TRANSLATION')
|
|
elif param.type.vecsemantics == param.type.vecsemantics.NORMAL:
|
|
ui.update(subtype='DIRECTION')
|
|
elif datatype is str and metadata.get('widget') == 'filename':
|
|
ui.update(subtype='FILE_PATH')
|
|
elif datatype is float and metadata.get('unit') == 'radians':
|
|
ui.update(subtype='ANGLE')
|
|
elif datatype is float and metadata.get('unit') == 'm':
|
|
ui.update(subtype='DISTANCE')
|
|
elif datatype is float and metadata.get('unit') == 'mm':
|
|
ui.update(subtype='DISTANCE_CAMERA')
|
|
elif datatype is float and metadata.get('unit') in ('s', 'sec'):
|
|
ui.update(subtype='TIME_ABSOLUTE')
|
|
elif metadata.get('slider'):
|
|
ui.update(subtype='FACTOR')
|
|
elif datatype is int and metadata.get('widget') == 'mapper':
|
|
options = metadata.get('options', "")
|
|
options = options.split("|")
|
|
option_items = []
|
|
for option in options:
|
|
if ":" not in option:
|
|
continue
|
|
item, index = option.split(":")
|
|
# Ensure that the index can be converted to an integer
|
|
try:
|
|
int(index)
|
|
except ValueError:
|
|
continue
|
|
option_items.append((str(index), bpy.path.display_name(item), ""))
|
|
ui.update(items=option_items)
|
|
|
|
# Map OSL metadata to Blender names
|
|
option_map = {
|
|
'help': 'description',
|
|
'sensitivity': 'step', 'digits': 'precision',
|
|
'min': 'min', 'max': 'max',
|
|
'slidermin': 'soft_min', 'slidermax': 'soft_max',
|
|
}
|
|
if 'sensitivity' in metadata:
|
|
# Blender divides this value by 100 by convention, so counteract that.
|
|
metadata['sensitivity'] *= 100
|
|
for option, value in metadata.items():
|
|
if option in option_map:
|
|
ui.update(**{option_map[option]: value})
|
|
|
|
return name
|
|
|
|
|
|
def update_external_script(report, filepath, library):
|
|
"""compile and update OSL script"""
|
|
import os
|
|
import shutil
|
|
|
|
oso_file_remove = False
|
|
|
|
script_path = bpy.path.abspath(filepath, library=library)
|
|
script_path_noext, script_ext = os.path.splitext(script_path)
|
|
|
|
if script_ext == ".oso":
|
|
# it's a .oso file, no need to compile
|
|
ok, oso_path = True, script_path
|
|
elif script_ext == ".osl":
|
|
# compile .osl file
|
|
ok, oso_path = osl_compile(script_path, report)
|
|
oso_file_remove = True
|
|
|
|
if ok:
|
|
# copy .oso from temporary path to .osl directory
|
|
dst_path = script_path_noext + ".oso"
|
|
try:
|
|
shutil.copy2(oso_path, dst_path)
|
|
except:
|
|
report({'ERROR'}, "Failed to write .oso file next to external .osl file at " + dst_path)
|
|
elif os.path.dirname(filepath) == "":
|
|
# module in search path
|
|
oso_path = filepath
|
|
ok = True
|
|
else:
|
|
# unknown
|
|
report({'ERROR'}, "External shader script must have .osl or .oso extension, or be a module name")
|
|
ok = False
|
|
|
|
return ok, oso_path, oso_file_remove
|
|
|
|
|
|
def update_internal_script(report, script):
|
|
"""compile and update shader script node"""
|
|
import os
|
|
import tempfile
|
|
import pathlib
|
|
import hashlib
|
|
|
|
bytecode = None
|
|
bytecode_hash = None
|
|
|
|
osl_path = bpy.path.abspath(script.filepath, library=script.library)
|
|
|
|
if script.is_in_memory or script.is_dirty or script.is_modified or not os.path.exists(osl_path):
|
|
# write text datablock contents to temporary file
|
|
osl_file = tempfile.NamedTemporaryFile(mode='w', suffix=".osl", delete=False)
|
|
osl_file.write(script.as_string())
|
|
osl_file.write("\n")
|
|
osl_file.close()
|
|
|
|
ok, oso_path = osl_compile(osl_file.name, report)
|
|
os.remove(osl_file.name)
|
|
else:
|
|
# compile text datablock from disk directly
|
|
ok, oso_path = osl_compile(osl_path, report)
|
|
|
|
if ok:
|
|
# read bytecode
|
|
try:
|
|
bytecode = pathlib.Path(oso_path).read_text()
|
|
md5 = hashlib.md5(usedforsecurity=False)
|
|
md5.update(bytecode.encode())
|
|
bytecode_hash = md5.hexdigest()
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
report({'ERROR'}, "Cannot read OSO bytecode to store in node at {!r}".format(oso_path))
|
|
ok = False
|
|
|
|
return ok, oso_path, bytecode, bytecode_hash
|
|
|
|
|
|
def update_script_node(node, report):
|
|
"""compile and update shader script node"""
|
|
import os
|
|
import oslquery
|
|
|
|
oso_file_remove = False
|
|
|
|
if node.mode == 'EXTERNAL':
|
|
# compile external script file
|
|
ok, oso_path, oso_file_remove = update_external_script(report, node.filepath, node.id_data.library)
|
|
if ok:
|
|
# Clear old internal bytecode, and also trigger node update if it was already cleared.
|
|
node.bytecode = ""
|
|
node.bytecode_hash = ""
|
|
|
|
elif node.mode == 'INTERNAL' and node.script:
|
|
# internal script, we will store bytecode in the node
|
|
ok, oso_path, bytecode, bytecode_hash = update_internal_script(report, node.script)
|
|
if bytecode:
|
|
node.bytecode = bytecode
|
|
node.bytecode_hash = bytecode_hash
|
|
|
|
else:
|
|
report({'WARNING'}, "No text or file specified in node, nothing to compile")
|
|
return
|
|
|
|
if ok:
|
|
if query := oslquery.OSLQuery(oso_path):
|
|
# Ensure that all parameters have a matching socket
|
|
used_sockets = set()
|
|
for param in query.parameters:
|
|
if sock := shader_param_ensure(node, param):
|
|
used_sockets.add(sock)
|
|
|
|
# Remove unused sockets
|
|
for sockets in (node.inputs, node.outputs):
|
|
for identifier in [sock.identifier for sock in sockets]:
|
|
if sockets[identifier] not in used_sockets:
|
|
sockets.remove(sockets[identifier])
|
|
else:
|
|
ok = False
|
|
report({'ERROR'}, rpt_("OSL query failed to open %s") % oso_path)
|
|
else:
|
|
report({'ERROR'}, "OSL script compilation failed, see console for errors")
|
|
|
|
# remove temporary oso file
|
|
if oso_file_remove:
|
|
try:
|
|
os.remove(oso_path)
|
|
except:
|
|
pass
|
|
|
|
return ok
|
|
|
|
|
|
def update_custom_camera_shader(cam, report):
|
|
"""compile and update custom camera shader"""
|
|
import os
|
|
import oslquery
|
|
|
|
oso_file_remove = False
|
|
|
|
custom_props = cam.cycles_custom
|
|
if cam.custom_mode == 'EXTERNAL':
|
|
# compile external script file
|
|
ok, oso_path, oso_file_remove = update_external_script(report, cam.custom_filepath, cam.library)
|
|
|
|
elif cam.custom_mode == 'INTERNAL' and cam.custom_shader:
|
|
# internal script, we will store bytecode in the node
|
|
ok, oso_path, bytecode, bytecode_hash = update_internal_script(report, cam.custom_shader)
|
|
if bytecode:
|
|
cam.custom_bytecode = bytecode
|
|
cam.custom_bytecode_hash = bytecode_hash
|
|
cam.update_tag()
|
|
|
|
else:
|
|
report({'WARNING'}, "No text or file specified in node, nothing to compile")
|
|
return
|
|
|
|
if ok:
|
|
if query := oslquery.OSLQuery(oso_path):
|
|
# Ensure that all parameters have a matching property
|
|
used_params = set()
|
|
for param in query.parameters:
|
|
if name := osl_param_ensure_property(custom_props, param):
|
|
used_params.add(name)
|
|
|
|
# Clean up unused parameters
|
|
for prop in list(custom_props.keys()):
|
|
if prop not in used_params:
|
|
del custom_props[prop]
|
|
else:
|
|
ok = False
|
|
report({'ERROR'}, rpt_("OSL query failed to open %s") % oso_path)
|
|
else:
|
|
report({'ERROR'}, "Custom Camera shader compilation failed, see console for errors")
|
|
|
|
# remove temporary oso file
|
|
if oso_file_remove:
|
|
try:
|
|
os.remove(oso_path)
|
|
except:
|
|
pass
|
|
|
|
return ok
|