Files
test2/intern/cycles/blender/addon/osl.py
Damien Picard 9d0266280f Cycles: OSL Camera: Allow mm as parameter unit, use as cam distance
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
2025-10-13 02:52:16 +02:00

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