@@ -89,83 +89,172 @@ def shader_param_ensure(node, param):
8989 return sock
9090
9191
92- def update_script_node (node , report ):
93- """compile and update shader script node"""
92+ def osl_param_ensure_property (ccam , param ):
93+ import idprop
94+
95+ if param .isoutput or param .isclosure :
96+ return None
97+
98+ # Get metadata for the parameter to control UI display
99+ metadata = {meta .name : meta .value for meta in param .metadata }
100+ if 'label' not in metadata :
101+ metadata ['label' ] = param .name
102+
103+ datatype = None
104+ if param .type .basetype == param .type .basetype .INT :
105+ datatype = int
106+ elif param .type .basetype == param .type .basetype .FLOAT :
107+ datatype = float
108+ elif param .type .basetype == param .type .basetype .STRING :
109+ datatype = str
110+
111+ # OSl doesn't have boolean as a type, but we do
112+ if (datatype == int ) and (metadata .get ('widget' ) in ('boolean' , 'checkBox' )):
113+ datatype = bool
114+ default = param .value if isinstance (param .value , tuple ) else [param .value ]
115+ default = [datatype (v ) for v in default ]
116+
117+ name = param .name
118+ if name in ccam :
119+ # If the parameter already exists, only reset its value if its type
120+ # or array length changed
121+ cur_data = ccam [name ]
122+ if isinstance (cur_data , idprop .types .IDPropertyArray ):
123+ cur_length = len (cur_data )
124+ cur_type = type (cur_data [0 ])
125+ else :
126+ cur_length = 1
127+ cur_type = type (cur_data )
128+ do_replace = datatype != cur_type or len (default ) != cur_length
129+ else :
130+ # Parameter doesn't exist yet, so set it from the defaults
131+ do_replace = True
132+
133+ if do_replace :
134+ ccam [name ] = tuple (default ) if len (default ) > 1 else default [0 ]
135+
136+ ui = ccam .id_properties_ui (name )
137+ ui .clear ()
138+
139+ # Determine subtype (no unit support for now)
140+ if param .type .vecsemantics == param .type .vecsemantics .COLOR :
141+ ui .update (subtype = 'COLOR' )
142+ elif metadata .get ('slider' ):
143+ ui .update (subtype = 'FACTOR' )
144+
145+ # Map OSL metadata to Blender names
146+ option_map = {
147+ 'help' : 'description' ,
148+ 'sensitivity' : 'step' , 'digits' : 'precision' ,
149+ 'min' : 'min' , 'max' : 'max' ,
150+ 'slidermin' : 'soft_min' , 'slidermax' : 'soft_max' ,
151+ }
152+ if 'sensitivity' in metadata :
153+ # Blender divides this value by 100 by convention, so counteract that.
154+ metadata ['sensitivity' ] *= 100
155+ for option , value in metadata .items ():
156+ if option in option_map :
157+ ui .update (** {option_map [option ]: value })
158+
159+ return name
160+
161+
162+ def update_external_script (report , filepath , library ):
163+ """compile and update OSL script"""
94164 import os
95165 import shutil
166+
167+ oso_file_remove = False
168+
169+ script_path = bpy .path .abspath (filepath , library = library )
170+ script_path_noext , script_ext = os .path .splitext (script_path )
171+
172+ if script_ext == ".oso" :
173+ # it's a .oso file, no need to compile
174+ ok , oso_path = True , script_path
175+ elif script_ext == ".osl" :
176+ # compile .osl file
177+ ok , oso_path = osl_compile (script_path , report )
178+ oso_file_remove = True
179+
180+ if ok :
181+ # copy .oso from temporary path to .osl directory
182+ dst_path = script_path_noext + ".oso"
183+ try :
184+ shutil .copy2 (oso_path , dst_path )
185+ except :
186+ report ({'ERROR' }, "Failed to write .oso file next to external .osl file at " + dst_path )
187+ elif os .path .dirname (filepath ) == "" :
188+ # module in search path
189+ oso_path = filepath
190+ ok = True
191+ else :
192+ # unknown
193+ report ({'ERROR' }, "External shader script must have .osl or .oso extension, or be a module name" )
194+ ok = False
195+
196+ return ok , oso_path , oso_file_remove
197+
198+
199+ def update_internal_script (report , script ):
200+ """compile and update shader script node"""
201+ import os
96202 import tempfile
97- import hashlib
98203 import pathlib
204+ import hashlib
205+
206+ bytecode = None
207+ bytecode_hash = None
208+
209+ osl_path = bpy .path .abspath (script .filepath , library = script .library )
210+
211+ if script .is_in_memory or script .is_dirty or script .is_modified or not os .path .exists (osl_path ):
212+ # write text datablock contents to temporary file
213+ osl_file = tempfile .NamedTemporaryFile (mode = 'w' , suffix = ".osl" , delete = False )
214+ osl_file .write (script .as_string ())
215+ osl_file .write ("\n " )
216+ osl_file .close ()
217+
218+ ok , oso_path = osl_compile (osl_file .name , report )
219+ os .remove (osl_file .name )
220+ else :
221+ # compile text datablock from disk directly
222+ ok , oso_path = osl_compile (osl_path , report )
223+
224+ if ok :
225+ # read bytecode
226+ try :
227+ bytecode = pathlib .Path (oso_path ).read_text ()
228+ md5 = hashlib .md5 (usedforsecurity = False )
229+ md5 .update (bytecode .encode ())
230+ bytecode_hash = md5 .hexdigest ()
231+ except :
232+ import traceback
233+ traceback .print_exc ()
234+
235+ report ({'ERROR' }, "Can't read OSO bytecode to store in node at %r" % oso_path )
236+ ok = False
237+
238+ return ok , oso_path , bytecode , bytecode_hash
239+
240+
241+ def update_script_node (node , report ):
242+ """compile and update shader script node"""
243+ import os
99244 import oslquery
100245
101246 oso_file_remove = False
102247
103248 if node .mode == 'EXTERNAL' :
104249 # compile external script file
105- script_path = bpy .path .abspath (node .filepath , library = node .id_data .library )
106- script_path_noext , script_ext = os .path .splitext (script_path )
107-
108- if script_ext == ".oso" :
109- # it's a .oso file, no need to compile
110- ok , oso_path = True , script_path
111- elif script_ext == ".osl" :
112- # compile .osl file
113- ok , oso_path = osl_compile (script_path , report )
114- oso_file_remove = True
115-
116- if ok :
117- # copy .oso from temporary path to .osl directory
118- dst_path = script_path_noext + ".oso"
119- try :
120- shutil .copy2 (oso_path , dst_path )
121- except :
122- report ({'ERROR' }, "Failed to write .oso file next to external .osl file at " + dst_path )
123- elif os .path .dirname (node .filepath ) == "" :
124- # module in search path
125- oso_path = node .filepath
126- ok = True
127- else :
128- # unknown
129- report ({'ERROR' }, "External shader script must have .osl or .oso extension, or be a module name" )
130- ok = False
131-
132- if ok :
133- node .bytecode = ""
134- node .bytecode_hash = ""
250+ ok , oso_path , oso_file_remove = update_external_script (report , node .filepath , node .id_data .library )
135251
136252 elif node .mode == 'INTERNAL' and node .script :
137253 # internal script, we will store bytecode in the node
138- script = node .script
139- osl_path = bpy .path .abspath (script .filepath , library = script .library )
140-
141- if script .is_in_memory or script .is_dirty or script .is_modified or not os .path .exists (osl_path ):
142- # write text datablock contents to temporary file
143- osl_file = tempfile .NamedTemporaryFile (mode = 'w' , suffix = ".osl" , delete = False )
144- osl_file .write (script .as_string ())
145- osl_file .write ("\n " )
146- osl_file .close ()
147-
148- ok , oso_path = osl_compile (osl_file .name , report )
149- os .remove (osl_file .name )
150- else :
151- # compile text datablock from disk directly
152- ok , oso_path = osl_compile (osl_path , report )
153-
154- if ok :
155- # read bytecode
156- try :
157- bytecode = pathlib .Path (oso_path ).read_text ()
158- md5 = hashlib .md5 (usedforsecurity = False )
159- md5 .update (bytecode .encode ())
160-
161- node .bytecode = bytecode
162- node .bytecode_hash = md5 .hexdigest ()
163- except :
164- import traceback
165- traceback .print_exc ()
166-
167- report ({'ERROR' }, "Can't read OSO bytecode to store in node at %r" % oso_path )
168- ok = False
254+ ok , oso_path , bytecode , bytecode_hash = update_internal_script (report , node .script )
255+ if bytecode :
256+ node .bytecode = bytecode
257+ node .bytecode_hash = bytecode_hash
169258
170259 else :
171260 report ({'WARNING' }, "No text or file specified in node, nothing to compile" )
@@ -198,3 +287,55 @@ def update_script_node(node, report):
198287 pass
199288
200289 return ok
290+
291+
292+ def update_custom_camera_shader (cam , report ):
293+ """compile and update custom camera shader"""
294+ import os
295+ import oslquery
296+
297+ oso_file_remove = False
298+
299+ custom_props = cam .cycles_custom
300+ if cam .custom_mode == 'EXTERNAL' :
301+ # compile external script file
302+ ok , oso_path , oso_file_remove = update_external_script (report , cam .custom_filepath , cam .library )
303+
304+ elif cam .custom_mode == 'INTERNAL' and cam .custom_shader :
305+ # internal script, we will store bytecode in the node
306+ ok , oso_path , bytecode , bytecode_hash = update_internal_script (report , cam .custom_shader )
307+ if bytecode :
308+ cam .custom_bytecode = bytecode
309+ cam .custom_bytecode_hash = bytecode_hash
310+ cam .update_tag ()
311+
312+ else :
313+ report ({'WARNING' }, "No text or file specified in node, nothing to compile" )
314+ return
315+
316+ if ok :
317+ if query := oslquery .OSLQuery (oso_path ):
318+ # Ensure that all parameters have a matching property
319+ used_params = set ()
320+ for param in query .parameters :
321+ if name := osl_param_ensure_property (custom_props , param ):
322+ used_params .add (name )
323+
324+ # Clean up unused parameters
325+ for prop in list (custom_props .keys ()):
326+ if prop not in used_params :
327+ del custom_props [prop ]
328+ else :
329+ ok = False
330+ report ({'ERROR' }, tip_ ("OSL query failed to open %s" ) % oso_path )
331+ else :
332+ report ({'ERROR' }, "Custom Camera shader compilation failed, see console for errors" )
333+
334+ # remove temporary oso file
335+ if oso_file_remove :
336+ try :
337+ os .remove (oso_path )
338+ except :
339+ pass
340+
341+ return ok
0 commit comments