Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
bl_info = {
"name": "Cycles Render Engine",
"author": "",
"blender": (2, 80, 0),
"description": "Cycles renderer integration",
"warning": "",
"doc_url": "https://docs.blender.org/manual/en/latest/render/cycles/",
"tracker_url": "",
"support": 'OFFICIAL',
"category": "Render"}
# Support 'reload' case.
if "bpy" in locals():
import importlib
if "engine" in locals():
importlib.reload(engine)
if "version_update" in locals():
importlib.reload(version_update)
if "ui" in locals():
importlib.reload(ui)
if "operators" in locals():
importlib.reload(operators)
if "properties" in locals():
importlib.reload(properties)
if "presets" in locals():
importlib.reload(presets)
if "maketx" in locals():
importlib.reload(maketx)
import bpy
from . import (
engine,
version_update,
)
class CyclesRender(bpy.types.RenderEngine):
bl_idname = 'CYCLES'
bl_label = "Cycles"
bl_use_eevee_viewport = True
bl_use_preview = True
bl_use_exclude_layers = True
bl_use_spherical_stereo = True
bl_use_custom_freestyle = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.session = None
def __del__(self):
engine.free(self)
# final render
def update(self, data, depsgraph):
if not self.session:
if self.is_preview:
cscene = bpy.context.scene.cycles
use_osl = cscene.shading_system
engine.create(self, data, preview_osl=use_osl)
else:
engine.create(self, data)
engine.reset(self, data, depsgraph)
def render(self, depsgraph):
engine.render(self, depsgraph)
def render_frame_finish(self):
engine.render_frame_finish(self)
def draw(self, context, depsgraph):
engine.draw(self, depsgraph, context.space_data)
def bake(self, depsgraph, obj, pass_type, pass_filter, width, height):
engine.bake(self, depsgraph, obj, pass_type, pass_filter, width, height)
# viewport render
def view_update(self, context, depsgraph):
if not self.session:
# When starting a new render session in viewport (by switching
# viewport to Rendered shading) unpause the render. The way to think
# of it is: artist requests render, so we start to render.
# Do it for both original and evaluated scene so that Cycles
# immediately reacts to un-paused render.
cscene = context.scene.cycles
cscene_eval = depsgraph.scene_eval.cycles
if cscene.preview_pause or cscene_eval.preview_pause:
cscene.preview_pause = False
cscene_eval.preview_pause = False
engine.create(self, context.blend_data,
context.region, context.space_data, context.region_data)
engine.reset(self, context.blend_data, depsgraph)
engine.sync(self, depsgraph, context.blend_data)
def view_draw(self, context, depsgraph):
engine.view_draw(self, depsgraph, context.region, context.space_data, context.region_data)
def update_script_node(self, node):
if engine.with_osl():
from . import osl
osl.update_script_node(node, self.report)
else:
self.report({'ERROR'}, "OSL support disabled in this build")
def update_custom_camera(self, cam):
if engine.with_osl():
from . import osl
osl.update_custom_camera_shader(cam, self.report)
else:
self.report({'ERROR'}, "OSL support disabled in this build")
def update_render_passes(self, scene, srl):
engine.register_passes(self, scene, srl)
def engine_exit():
engine.exit()
classes = (
CyclesRender,
)
cli_commands = []
def register():
from bpy.utils import register_class
from . import ui
from . import operators
from . import properties
from . import presets
from .maketx import maketx_command
import atexit
# Make sure we only registered the callback once.
atexit.unregister(engine_exit)
atexit.register(engine_exit)
engine.init()
properties.register()
ui.register()
operators.register()
presets.register()
for cls in classes:
register_class(cls)
bpy.app.handlers.version_update.append(version_update.do_versions)
cli_commands.append(bpy.utils.register_cli_command("maketx", maketx_command))
def unregister():
from bpy.utils import unregister_class
from . import ui
from . import operators
from . import properties
from . import presets
bpy.app.handlers.version_update.remove(version_update.do_versions)
for cmd in cli_commands:
bpy.utils.unregister_cli_command(cmd)
cli_commands.clear()
ui.unregister()
operators.unregister()
properties.unregister()
presets.unregister()
for cls in classes:
unregister_class(cls)

View File

@@ -0,0 +1,78 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
# Fit to match default projective camera with focal_length 50 and sensor_width 36.
default_fisheye_polynomial = [
-1.1735143712967577e-05,
-0.019988736953434998,
-3.3525322965709175e-06,
3.099275275886036e-06,
-2.6064646454854524e-08,
]
# Utilities to generate lens polynomials to match built-in camera types, only here
# for reference at the moment, not used by the code.
def create_grid(sensor_height, sensor_width):
import numpy as np
if sensor_height is None:
sensor_height = sensor_width / (16 / 9) # Default aspect ration 16:9
uu, vv = np.meshgrid(np.linspace(0, 1, 100), np.linspace(0, 1, 100))
uu = (uu - 0.5) * sensor_width
vv = (vv - 0.5) * sensor_height
rr = np.sqrt(uu ** 2 + vv ** 2)
return rr
def fisheye_lens_polynomial_from_projective(focal_length=50, sensor_width=36, sensor_height=None):
import numpy as np
rr = create_grid(sensor_height, sensor_width)
polynomial = np.polyfit(rr.flat, (-np.arctan(rr / focal_length)).flat, 4)
return list(reversed(polynomial))
def fisheye_lens_polynomial_from_projective_fov(fov, sensor_width=36, sensor_height=None):
import numpy as np
f = sensor_width / 2 / np.tan(fov / 2)
return fisheye_lens_polynomial_from_projective(f, sensor_width, sensor_height)
def fisheye_lens_polynomial_from_equisolid(lens=10.5, sensor_width=36, sensor_height=None):
import numpy as np
rr = create_grid(sensor_height, sensor_width)
x = rr.reshape(-1)
x = np.stack([x**i for i in [1, 2, 3, 4]])
y = (-2 * np.arcsin(rr / (2 * lens))).reshape(-1)
polynomial = np.linalg.lstsq(x.T, y.T, rcond=None)[0]
return [0] + list(polynomial)
def fisheye_lens_polynomial_from_equidistant(fov=180, sensor_width=36, sensor_height=None):
import numpy as np
return [0, -np.radians(fov) / sensor_width, 0, 0, 0]
def fisheye_lens_polynomial_from_distorted_projective_polynomial(
k1, k2, k3, focal_length=50, sensor_width=36, sensor_height=None,
):
import numpy as np
rr = create_grid(sensor_height, sensor_width)
r2 = (rr / focal_length) ** 2
r4 = r2 * r2
r6 = r4 * r2
r_coeff = 1 + k1 * r2 + k2 * r4 + k3 * r6
polynomial = np.polyfit(rr.flat, (-np.arctan(rr / focal_length * r_coeff)).flat, 4)
return list(reversed(polynomial))
def fisheye_lens_polynomial_from_distorted_projective_divisions(
k1, k2, focal_length=50, sensor_width=36, sensor_height=None,
):
import numpy as np
rr = create_grid(sensor_height, sensor_width)
r2 = (rr / focal_length) ** 2
r4 = r2 * r2
r_coeff = 1 + k1 * r2 + k2 * r4
polynomial = np.polyfit(rr.flat, (-np.arctan(rr / focal_length / r_coeff)).flat, 4)
return list(reversed(polynomial))

View File

@@ -0,0 +1,277 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
def _configure_argument_parser():
import argparse
# No help because it conflicts with general Python scripts argument parsing
parser = argparse.ArgumentParser(description="Cycles Addon argument parser",
add_help=False)
parser.add_argument("--cycles-print-stats",
help="Print rendering statistics to stderr",
action='store_true')
parser.add_argument("--cycles-device",
help="Set the device to use for Cycles, overriding user preferences and the scene setting."
"Valid options are 'CPU', 'CUDA', 'OPTIX', 'HIP', 'ONEAPI', or 'METAL'."
"Additionally, you can append '+CPU' to any GPU type for hybrid rendering.",
default=None)
return parser
def _parse_command_line():
import sys
argv = sys.argv
if "--" not in argv:
return
parser = _configure_argument_parser()
args, _ = parser.parse_known_args(argv[argv.index("--") + 1:])
if args.cycles_print_stats:
import _cycles
_cycles.enable_print_stats()
if args.cycles_device:
import _cycles
if not _cycles.set_device_override(args.cycles_device):
sys.exit(1)
def init():
import bpy
import _cycles
import os.path
path = os.path.dirname(__file__)
user_path = os.path.dirname(os.path.abspath(bpy.utils.user_resource('CONFIG', path='')))
_cycles.init(path, user_path, bpy.app.background)
_parse_command_line()
def exit():
import _cycles
_cycles.exit()
def create(engine, data, region=None, v3d=None, rv3d=None, preview_osl=False):
import _cycles
import bpy
data = data.as_pointer()
prefs = bpy.context.preferences.as_pointer()
screen = 0
if region:
screen = region.id_data.as_pointer()
region = region.as_pointer()
if v3d:
screen = screen or v3d.id_data.as_pointer()
v3d = v3d.as_pointer()
if rv3d:
screen = screen or rv3d.id_data.as_pointer()
rv3d = rv3d.as_pointer()
engine.session = _cycles.create(engine.as_pointer(), prefs, data, screen, region, v3d, rv3d, preview_osl)
def free(engine):
if hasattr(engine, "session"):
if engine.session:
import _cycles
_cycles.free(engine.session)
del engine.session
def render(engine, depsgraph):
import _cycles
if hasattr(engine, "session"):
_cycles.render(engine.session, depsgraph.as_pointer())
def render_frame_finish(engine):
if not engine.session:
return
import _cycles
_cycles.render_frame_finish(engine.session)
def draw(engine, depsgraph, space_image):
if not engine.session:
return
depsgraph_ptr = depsgraph.as_pointer()
space_image_ptr = space_image.as_pointer()
screen_ptr = space_image.id_data.as_pointer()
import _cycles
_cycles.draw(engine.session, depsgraph_ptr, screen_ptr, space_image_ptr)
def bake(engine, depsgraph, obj, pass_type, pass_filter, width, height):
import _cycles
session = getattr(engine, "session", None)
if session is not None:
_cycles.bake(engine.session, depsgraph.as_pointer(), obj.as_pointer(), pass_type, pass_filter, width, height)
def reset(engine, data, depsgraph):
import _cycles
import bpy
prefs = bpy.context.preferences
if prefs.experimental.use_cycles_debug and prefs.view.show_developer_ui:
_cycles.debug_flags_update(depsgraph.scene.as_pointer())
else:
_cycles.debug_flags_reset()
data = data.as_pointer()
depsgraph = depsgraph.as_pointer()
_cycles.reset(engine.session, data, depsgraph)
def sync(engine, depsgraph, data):
import _cycles
_cycles.sync(engine.session, depsgraph.as_pointer())
def view_draw(engine, depsgraph, region, v3d, rv3d):
import _cycles
depsgraph = depsgraph.as_pointer()
v3d = v3d.as_pointer()
rv3d = rv3d.as_pointer()
# draw render image
_cycles.view_draw(engine.session, depsgraph, v3d, rv3d)
def available_devices():
import _cycles
return _cycles.available_devices()
def with_osl():
import _cycles
return _cycles.with_osl
def osl_version():
import _cycles
return _cycles.osl_version
def with_path_guiding():
import _cycles
return _cycles.with_path_guiding
def system_info():
import _cycles
return _cycles.system_info()
def list_render_passes(scene, srl):
import _cycles
from bpy.app.translations import pgettext_n as n_
crl = srl.cycles
# Combined pass.
yield ("Combined", "RGBA", 'COLOR')
# Keep alignment for readability.
# autopep8: off
# Data passes.
if srl.use_pass_z: yield (n_("Depth"), "Z", 'VALUE')
if srl.use_pass_mist: yield (n_("Mist"), "Z", 'VALUE')
if srl.use_pass_position: yield (n_("Position"), "XYZ", 'VECTOR')
if srl.use_pass_normal: yield (n_("Normal"), "XYZ", 'VECTOR')
if srl.use_pass_vector: yield (n_("Vector"), "XYZW", 'VECTOR')
if srl.use_pass_uv: yield (n_("UV"), "UVA", 'VECTOR')
if srl.use_pass_object_index: yield (n_("Object Index"), "X", 'VALUE')
if srl.use_pass_material_index: yield (n_("Material Index"), "X", 'VALUE')
# Light passes.
if srl.use_pass_diffuse_direct: yield (n_("Diffuse Direct"), "RGB", 'COLOR')
if srl.use_pass_diffuse_indirect: yield (n_("Diffuse Indirect"), "RGB", 'COLOR')
if srl.use_pass_diffuse_color: yield (n_("Diffuse Color"), "RGB", 'COLOR')
if srl.use_pass_glossy_direct: yield (n_("Glossy Direct"), "RGB", 'COLOR')
if srl.use_pass_glossy_indirect: yield (n_("Glossy Indirect"), "RGB", 'COLOR')
if srl.use_pass_glossy_color: yield (n_("Glossy Color"), "RGB", 'COLOR')
if srl.use_pass_transmission_direct: yield (n_("Transmission Direct"), "RGB", 'COLOR')
if srl.use_pass_transmission_indirect: yield (n_("Transmission Indirect"), "RGB", 'COLOR')
if srl.use_pass_transmission_color: yield (n_("Transmission Color"), "RGB", 'COLOR')
if crl.use_pass_volume_direct: yield (n_("Volume Direct"), "RGB", 'COLOR')
if crl.use_pass_volume_indirect: yield (n_("Volume Indirect"), "RGB", 'COLOR')
if crl.use_pass_volume_scatter: yield (n_("Volume Scatter"), "RGB", 'COLOR')
if crl.use_pass_volume_transmit: yield (n_("Volume Transmit"), "RGB", 'COLOR')
if crl.use_pass_volume_majorant: yield (n_("Volume Majorant"), "Z", 'VALUE')
if srl.use_pass_emit: yield (n_("Emission"), "RGB", 'COLOR')
if srl.use_pass_environment: yield (n_("Environment"), "RGB", 'COLOR')
if srl.use_pass_ambient_occlusion: yield (n_("Ambient Occlusion"), "RGB", 'COLOR')
if crl.use_pass_shadow_catcher: yield (n_("Shadow Catcher"), "RGB", 'COLOR')
# autopep8: on
# Debug passes.
if crl.pass_debug_sample_count:
yield (n_("Debug Sample Count"), "X", 'VALUE')
if crl.pass_render_time:
# Only yield the pass if rendering on CPU
if scene.cycles.device == 'CPU':
yield (n_("Render Time"), "X", "VALUE")
# Cryptomatte passes.
# NOTE: Name channels are lowercase RGBA so that compression rules check in OpenEXR DWA code
# uses lossless compression. Reportedly this naming is the only one which works good from the
# interoperability point of view. Using XYZW naming is not portable.
crypto_depth = (min(16, srl.pass_cryptomatte_depth) + 1) // 2
if srl.use_pass_cryptomatte_object:
for i in range(0, crypto_depth):
yield ("CryptoObject" + '{:02d}'.format(i), "rgba", 'COLOR')
if srl.use_pass_cryptomatte_material:
for i in range(0, crypto_depth):
yield ("CryptoMaterial" + '{:02d}'.format(i), "rgba", 'COLOR')
if srl.use_pass_cryptomatte_asset:
for i in range(0, crypto_depth):
yield ("CryptoAsset" + '{:02d}'.format(i), "rgba", 'COLOR')
# Denoising passes.
if scene.cycles.use_denoising and crl.use_denoising:
yield (n_("Noisy Image"), "RGBA", 'COLOR')
if crl.use_pass_shadow_catcher:
yield (n_("Noisy Shadow Catcher"), "RGB", 'COLOR')
if crl.denoising_store_passes:
yield (n_("Denoising Albedo"), "RGB", 'COLOR')
yield (n_("Denoising Specular Albedo"), "RGB", 'COLOR')
yield (n_("Denoising Normal"), "XYZ", 'VECTOR')
yield (n_("Denoising Roughness"), "X", 'VALUE')
yield (n_("Denoising Depth"), "Z", 'VALUE')
# Custom AOV passes.
for aov in srl.aovs:
if not aov.is_valid:
continue
if aov.type == 'VALUE':
yield (aov.name, "X", 'VALUE')
else:
yield (aov.name, "RGBA", 'COLOR')
# Light groups.
for lightgroup in srl.lightgroups:
yield ("Combined_%s" % lightgroup.name, "RGB", 'COLOR')
# Path guiding debug passes.
if _cycles.with_debug and scene.cycles.use_guiding:
yield (n_("Guiding Color"), "RGB", 'COLOR')
yield (n_("Guiding Probability"), "X", 'VALUE')
yield (n_("Guiding Average Roughness"), "X", 'VALUE')
def register_passes(engine, scene, view_layer):
for name, channelids, channeltype in list_render_passes(scene, view_layer):
engine.register_pass(scene, view_layer, name, len(channelids), channelids, channeltype)

View File

@@ -0,0 +1,106 @@
# SPDX-FileCopyrightText: 2026 Blender Authors
#
# SPDX-License-Identifier: Apache-2.0
import pathlib
import sys
def argparse_create():
import argparse
parser = argparse.ArgumentParser(
prog=pathlib.Path(sys.argv[0]).name + " --command maketx",
description=(
"Generate Cycles texture cache (.tx) files.\n"
"\n"
"When a file path is given, generate a tx file for that image.\n"
"When no file path is given, a blend file must be loaded and tx\n"
"files are generated for all images used by shader node trees."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"filepath",
type=str,
nargs="?",
default=None,
help="Source image file path. If omitted, generate for all images in the blend file.",
)
parser.add_argument(
"--colorspace",
dest="colorspace",
type=str,
default="auto",
help="Color space name. Auto-detected if omitted. Only used with a file path.",
)
parser.add_argument(
"--alpha-type",
dest="alpha_type",
type=str,
default="auto",
choices=["straight", "premultiplied", "channel_packed", "none", "auto"],
help="Alpha type. Auto-detected if omitted. Only used with a file path.",
)
parser.add_argument(
"--cache-dir",
dest="cache_dir",
type=str,
default="",
help="Output directory for tx files, relative to source image or absolute.",
)
return parser
def maketx_command(argv):
parser = argparse_create()
args = parser.parse_args(argv)
if args.filepath is not None:
return maketx_file(args)
else:
return maketx_blend(args, parser)
def maketx_file(args):
import _cycles
filepath = str(pathlib.Path(args.filepath).absolute())
try:
out_filepath = _cycles.maketx(
filepath,
colorspace=args.colorspace,
alpha_type=args.alpha_type,
cache_dir=args.cache_dir,
)
except RuntimeError as ex:
sys.stderr.write("Error: {:s}\n".format(str(ex)))
return 1
print(out_filepath)
return 0
def maketx_blend(args, parser):
import bpy
if not bpy.data.filepath:
parser.error("No image file path given and no blend file loaded.")
if args.colorspace != "auto":
parser.error("--colorspace is only used with an image file path argument.")
if args.alpha_type != "auto":
parser.error("--alpha-type is only used with an image file path argument.")
if args.cache_dir:
bpy.context.preferences.filepaths.texture_cache_directory = args.cache_dir
bpy.ops.render.generate_texture_cache()
return 0

View File

@@ -0,0 +1,167 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import bpy
from bpy.types import Operator
from bpy.props import StringProperty
from bpy.app.translations import pgettext_tip as tip_
class CYCLES_OT_use_shading_nodes(Operator):
"""Enable nodes on a light"""
bl_idname = "cycles.use_shading_nodes"
bl_label = "Use Nodes"
@classmethod
def poll(cls, context):
return getattr(context, "light", False)
def execute(self, context):
if context.light:
context.light.use_nodes = True
return {'FINISHED'}
class CYCLES_OT_denoise_animation(Operator):
"Denoise rendered animation sequence using current scene and view " \
"layer settings. Requires denoising data passes and output to " \
"OpenEXR multilayer files"
bl_idname = "cycles.denoise_animation"
bl_label = "Denoise Animation"
input_filepath: StringProperty(
name='Input Filepath',
description='File path for image to denoise. If not specified, uses the render file path and frame range from the scene',
default='',
subtype='FILE_PATH')
output_filepath: StringProperty(
name='Output Filepath',
description='If not specified, renders will be denoised in-place',
default='',
subtype='FILE_PATH')
def execute(self, context):
import os
preferences = context.preferences
scene = context.scene
view_layer = context.view_layer
in_filepath = self.input_filepath
out_filepath = self.output_filepath
in_filepaths = []
out_filepaths = []
if in_filepath != '':
# Denoise a single file
if out_filepath == '':
out_filepath = in_filepath
in_filepaths.append(in_filepath)
out_filepaths.append(out_filepath)
else:
# Denoise animation sequence with expanded frames matching
# Blender render output file naming.
in_filepath = scene.render.filepath
if out_filepath == '':
out_filepath = in_filepath
# Backup since we will overwrite the scene path temporarily
original_filepath = scene.render.filepath
for frame in range(scene.frame_start, scene.frame_end + 1):
scene.render.filepath = in_filepath
filepath = scene.render.frame_path(frame=frame)
in_filepaths.append(filepath)
if not os.path.isfile(filepath):
scene.render.filepath = original_filepath
err_msg = tip_("Frame '%s' not found, animation must be complete") % filepath
self.report({'ERROR'}, err_msg)
return {'CANCELLED'}
scene.render.filepath = out_filepath
filepath = scene.render.frame_path(frame=frame)
out_filepaths.append(filepath)
scene.render.filepath = original_filepath
# Run denoiser
# TODO: support cancel and progress reports.
import _cycles
try:
_cycles.denoise(preferences.as_pointer(),
scene.as_pointer(),
view_layer.as_pointer(),
input=in_filepaths,
output=out_filepaths)
except Exception as e:
self.report({'ERROR'}, str(e))
return {'FINISHED'}
self.report({'INFO'}, "Denoising completed")
return {'FINISHED'}
class CYCLES_OT_merge_images(Operator):
"Combine OpenEXR multi-layer images rendered with different sample " \
"ranges into one image with reduced noise"
bl_idname = "cycles.merge_images"
bl_label = "Merge Images"
input_filepath1: StringProperty(
name='Input Filepath',
description='File path for image to merge',
default='',
subtype='FILE_PATH')
input_filepath2: StringProperty(
name='Input Filepath',
description='File path for image to merge',
default='',
subtype='FILE_PATH')
output_filepath: StringProperty(
name='Output Filepath',
description='File path for merged image',
default='',
subtype='FILE_PATH')
def execute(self, context):
in_filepaths = [self.input_filepath1, self.input_filepath2]
out_filepath = self.output_filepath
import _cycles
try:
_cycles.merge(input=in_filepaths, output=out_filepath)
except Exception as e:
self.report({'ERROR'}, str(e))
return {'FINISHED'}
return {'FINISHED'}
classes = (
CYCLES_OT_use_shading_nodes,
CYCLES_OT_denoise_animation,
CYCLES_OT_merge_images
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)

View File

@@ -0,0 +1,375 @@
# 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'}, rpt_("Failed to write .oso file next to external .osl file at {:s}").format(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

View File

@@ -0,0 +1,136 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from bl_operators.presets import AddPresetBase
from bpy.types import Operator
class AddPresetIntegrator(AddPresetBase, Operator):
'''Add an Integrator Preset'''
bl_idname = "render.cycles_integrator_preset_add"
bl_label = "Add Integrator Preset"
preset_menu = "CYCLES_PT_integrator_presets"
preset_defines = [
"cycles = bpy.context.scene.cycles"
]
preset_values = [
"cycles.max_bounces",
"cycles.diffuse_bounces",
"cycles.glossy_bounces",
"cycles.transmission_bounces",
"cycles.volume_bounces",
"cycles.transparent_max_bounces",
"cycles.caustics_reflective",
"cycles.caustics_refractive",
"cycles.blur_glossy",
"cycles.use_fast_gi",
"cycles.ao_bounces",
"cycles.ao_bounces_render",
]
preset_subdir = "cycles/integrator"
class AddPresetSampling(AddPresetBase, Operator):
'''Add a Sampling Preset'''
bl_idname = "render.cycles_sampling_preset_add"
bl_label = "Add Sampling Preset"
preset_menu = "CYCLES_PT_sampling_presets"
preset_defines = [
"cycles = bpy.context.scene.cycles"
]
preset_values = [
"cycles.use_adaptive_sampling",
"cycles.samples",
"cycles.adaptive_threshold",
"cycles.adaptive_min_samples",
"cycles.time_limit",
"cycles.use_denoising",
"cycles.denoiser",
"cycles.denoising_input_passes",
"cycles.denoising_prefilter",
"cycles.denoising_quality",
]
preset_subdir = "cycles/sampling"
class AddPresetViewportSampling(AddPresetBase, Operator):
'''Add a Viewport Sampling Preset'''
bl_idname = "render.cycles_viewport_sampling_preset_add"
bl_label = "Add Viewport Sampling Preset"
preset_menu = "CYCLES_PT_viewport_sampling_presets"
preset_defines = [
"cycles = bpy.context.scene.cycles"
]
preset_values = [
"cycles.use_preview_adaptive_sampling",
"cycles.preview_samples",
"cycles.preview_adaptive_threshold",
"cycles.preview_adaptive_min_samples",
"cycles.use_preview_denoising",
"cycles.preview_denoiser",
"cycles.preview_denoising_input_passes",
"cycles.preview_denoising_prefilter",
"cycles.preview_denoising_quality",
"cycles.preview_denoising_start_sample",
]
preset_subdir = "cycles/viewport_sampling"
class AddPresetPerformance(AddPresetBase, Operator):
'''Add an Performance Preset'''
bl_idname = "render.cycles_performance_preset_add"
bl_label = "Add Performance Preset"
preset_menu = "CYCLES_PT_performance_presets"
preset_defines = [
"render = bpy.context.scene.render",
"cycles = bpy.context.scene.cycles",
]
preset_values = [
"render.threads_mode",
"render.use_persistent_data",
"cycles.debug_use_spatial_splits",
"cycles.debug_use_compact_bvh",
"cycles.debug_use_hair_bvh",
"cycles.debug_bvh_time_steps",
"cycles.tile_size",
]
preset_subdir = "cycles/performance"
classes = (
AddPresetIntegrator,
AddPresetSampling,
AddPresetViewportSampling,
AddPresetPerformance,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
if __name__ == "__main__":
register()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,330 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import bpy
from bpy.app.handlers import persistent
def custom_bake_remap(scene):
"""
Remap bake types into the new types and set the flags accordingly
"""
bake_lookup = (
'COMBINED',
'AO',
'SHADOW',
'NORMAL',
'UV',
'EMIT',
'ENVIRONMENT',
'DIFFUSE_DIRECT',
'DIFFUSE_INDIRECT',
'DIFFUSE_COLOR',
'GLOSSY_DIRECT',
'GLOSSY_INDIRECT',
'GLOSSY_COLOR',
'TRANSMISSION_DIRECT',
'TRANSMISSION_INDIRECT',
'TRANSMISSION_COLOR')
diffuse_direct_idx = bake_lookup.index('DIFFUSE_DIRECT')
cscene = scene.cycles
# Old bake type
bake_type_idx = cscene.get("bake_type")
if bake_type_idx is None:
cscene.bake_type = 'COMBINED'
return
# File doesn't need versioning
if bake_type_idx < diffuse_direct_idx:
return
# File needs versioning
bake_type = bake_lookup[bake_type_idx]
cscene.bake_type, end = bake_type.split('_')
if end == 'DIRECT':
scene.render.bake.use_pass_indirect = False
scene.render.bake.use_pass_color = False
elif end == 'INDIRECT':
scene.render.bake.use_pass_direct = False
scene.render.bake.use_pass_color = False
elif end == 'COLOR':
scene.render.bake.use_pass_direct = False
scene.render.bake.use_pass_indirect = False
@persistent
def do_versions(self):
if bpy.context.preferences.version <= (2, 78, 1):
prop = bpy.context.preferences.addons[__package__].preferences
system = bpy.context.preferences.system
if not prop.is_property_set("compute_device_type"):
# Device might not currently be available so this can fail
try:
if system.legacy_compute_device_type == 1:
prop.compute_device_type = 'NONE' # Was OpenCL
elif system.legacy_compute_device_type == 2:
prop.compute_device_type = 'CUDA'
else:
prop.compute_device_type = 'NONE'
except:
pass
# Init device list for UI
prop.get_devices(prop.compute_device_type)
if bpy.context.preferences.version <= (3, 0, 40):
# Disable OpenCL device
prop = bpy.context.preferences.addons[__package__].preferences
if prop.is_property_set("compute_device_type") and prop['compute_device_type'] == 4:
prop.compute_device_type = 'NONE'
# We don't modify startup file because it assumes to
# have all the default values only.
if not bpy.data.is_saved:
return
# Map of versions used by libraries.
library_versions = {}
library_versions[bpy.data.version] = [None]
for library in bpy.data.libraries:
library_versions.setdefault(library.version, []).append(library)
# Do versioning per library, since they might have different versions.
max_need_versioning = (5, 2, 8)
for version, libraries in library_versions.items():
if version > max_need_versioning:
continue
# Scenes
for scene in bpy.data.scenes:
if scene.library not in libraries:
continue
# Auto tiling is always enabled now
if version <= (5, 0, 77):
cscene = scene.cycles
if not cscene.use_auto_tile:
cscene.use_auto_tile = True
cscene.tile_size = 8192
# Clamp Direct/Indirect separation in 270
if version <= (2, 70, 0):
cscene = scene.cycles
sample_clamp = cscene.get("sample_clamp", False)
if (sample_clamp and
not cscene.is_property_set("sample_clamp_direct") and
not cscene.is_property_set("sample_clamp_indirect")):
cscene.sample_clamp_direct = sample_clamp
cscene.sample_clamp_indirect = sample_clamp
# Change of Volume Bounces in 271
if version <= (2, 71, 0):
cscene = scene.cycles
if not cscene.is_property_set("volume_bounces"):
cscene.volume_bounces = 1
# Caustics Reflective/Refractive separation in 272
if version <= (2, 72, 0):
cscene = scene.cycles
if (
cscene.get("no_caustics", False) and
not cscene.is_property_set("caustics_reflective") and
not cscene.is_property_set("caustics_refractive")
):
cscene.caustics_reflective = False
cscene.caustics_refractive = False
# Baking types changed
if version <= (2, 76, 6):
custom_bake_remap(scene)
# Several default changes for 2.77
if version <= (2, 76, 8):
cscene = scene.cycles
# Samples
if not cscene.is_property_set("samples"):
cscene.samples = 10
# Preview Samples
if not cscene.is_property_set("preview_samples"):
cscene.preview_samples = 10
# Filter
if cscene.get("filter_type", -1) == -1:
cscene.pixel_filter_type = 'GAUSSIAN'
if version <= (2, 76, 10):
cscene = scene.cycles
if not cscene.is_property_set("pixel_filter_type"):
filter_type_int = cscene.get("filter_type", -1)
if filter_type_int == 0:
cscene.pixel_filter_type = 'BOX'
elif filter_type_int == 1:
cscene.pixel_filter_type = 'GAUSSIAN'
if version <= (2, 78, 2):
cscene = scene.cycles
if not cscene.is_property_set("light_sampling_threshold"):
cscene.light_sampling_threshold = 0.0
if version <= (2, 79, 0):
cscene = scene.cycles
# Default changes
if not cscene.is_property_set("blur_glossy"):
cscene.blur_glossy = 0.0
if not cscene.is_property_set("sample_clamp_indirect"):
cscene.sample_clamp_indirect = 0.0
if version <= (2, 92, 4):
if scene.render.engine == 'CYCLES':
for view_layer in scene.view_layers:
cview_layer = view_layer.cycles
view_layer.use_pass_cryptomatte_object = cview_layer.get("use_pass_crypto_object", False)
view_layer.use_pass_cryptomatte_material = cview_layer.get("use_pass_crypto_material", False)
view_layer.use_pass_cryptomatte_asset = cview_layer.get("use_pass_crypto_asset", False)
view_layer.pass_cryptomatte_depth = cview_layer.get("pass_crypto_depth", 6)
if version <= (2, 93, 7):
if scene.render.engine == 'CYCLES':
for view_layer in scene.view_layers:
cview_layer = view_layer.cycles
for caov in cview_layer.get("aovs", []):
aov_name = caov.get("name", "AOV")
if aov_name in view_layer.aovs:
continue
baov = view_layer.aovs.add()
baov.name = caov.get("name", "AOV")
baov.type = "COLOR" if caov.get("type", 1) == 1 else "VALUE"
if version <= (2, 93, 16):
cscene = scene.cycles
ao_bounces = cscene.get("ao_bounces", 0)
ao_bounces_render = cscene.get("ao_bounces_render", 0)
if scene.render.use_simplify and (ao_bounces or ao_bounces_render):
cscene.use_fast_gi = True
cscene.ao_bounces = ao_bounces
cscene.ao_bounces_render = ao_bounces_render
else:
cscene.ao_bounces = 1
cscene.ao_bounces_render = 1
if version <= (3, 0, 25):
cscene = scene.cycles
# Default changes.
if not cscene.is_property_set("samples"):
cscene.samples = 128
if not cscene.is_property_set("preview_samples"):
cscene.preview_samples = 32
if not cscene.is_property_set("use_adaptive_sampling"):
cscene.use_adaptive_sampling = False
cscene.use_preview_adaptive_sampling = False
if not cscene.is_property_set("use_denoising"):
cscene.use_denoising = False
if not cscene.is_property_set("use_preview_denoising"):
cscene.use_preview_denoising = False
if not cscene.is_property_set("sampling_pattern") or \
cscene.get('sampling_pattern') >= 2:
cscene.sampling_pattern = 'TABULATED_SOBOL'
# Removal of square samples.
cscene = scene.cycles
use_square_samples = cscene.get("use_square_samples", False)
if use_square_samples:
cscene.samples *= cscene.samples
cscene.preview_samples *= cscene.preview_samples
for layer in scene.view_layers:
layer.samples *= layer.samples
cscene["use_square_samples"] = False
if version <= (3, 5, 3):
cscene = scene.cycles
# Disable light tree for existing scenes.
if not cscene.is_property_set("use_light_tree"):
cscene.use_light_tree = False
# Sampling pattern settings are hidden behind a debug menu. Switch to the
# default faster and fully featured (Supports Scrambling Distance)
# Tabulated Sobol.
cscene.sampling_pattern = 'TABULATED_SOBOL'
if version <= (4, 2, 52):
cscene = scene.cycles
# Previous versions defaulted to Tabulated Sobol unless debugging options
# were enabled, so keep this behavior instead of suddenly defaulting to
# blue noise if the file happens to contain a different option for the enum.
cscene.sampling_pattern = 'TABULATED_SOBOL'
# Lamps
for light in bpy.data.lights:
if light.library not in libraries:
continue
if version <= (2, 76, 5):
clight = light.cycles
# MIS
if not clight.is_property_set("use_multiple_importance_sampling"):
clight.use_multiple_importance_sampling = False
# Worlds
for world in bpy.data.worlds:
if world.library not in libraries:
continue
if version <= (2, 76, 9):
cworld = world.cycles
# World MIS Resolution
if not cworld.is_property_set("sample_map_resolution"):
cworld.sample_map_resolution = 256
if version <= (2, 79, 4) or \
(version >= (2, 80, 0) and version <= (2, 80, 18)):
cworld = world.cycles
# World MIS
if not cworld.is_property_set("sampling_method"):
if cworld.get("sample_as_light", True):
cworld.sampling_method = 'MANUAL'
else:
cworld.sampling_method = 'NONE'
# Materials
for mat in bpy.data.materials:
if mat.library not in libraries:
continue
if version <= (2, 76, 5):
cmat = mat.cycles
# Volume Sampling
if not cmat.is_property_set("volume_sampling"):
cmat.volume_sampling = 'DISTANCE'
if version <= (2, 79, 2):
cmat = mat.cycles
if cmat.get("displacement_method", -1) == -1:
cmat['displacement_method'] = 0
# Change default to bump again.
if version <= (2, 79, 6) or \
(version >= (2, 80, 0) and version <= (2, 80, 41)):
cmat = mat.cycles
if cmat.get("displacement_method", -1) == -1:
cmat['displacement_method'] = 1
if version <= (3, 5, 3):
cmat = mat.cycles
if not cmat.get("sample_as_light", True):
cmat.emission_sampling = 'NONE'