Pose Library: Update to use the asset shelf (when enabled) #104546

Merged
Julian Eisel merged 33 commits from asset-shelf into main 2023-08-04 15:00:21 +02:00
12 changed files with 529 additions and 63 deletions
Showing only changes of commit a0704bd58b - Show all commits

View File

@ -7,6 +7,7 @@ from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
"""
import bpy
import time
import math
import struct
import mathutils
@ -586,10 +587,12 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
mapflags = 0
# no perfect mapping for mirror modes - 3DS only has uniform mirror w. repeat=2
if texslot.extension == 'EXTEND':
mapflags |= 0x1
if texslot.extension == 'MIRROR':
mapflags |= 0x2
if texslot.extension == 'CLIP':
mapflags |= 0x10
@ -647,6 +650,7 @@ def make_material_chunk(material, image):
"""Make a material chunk out of a blender material.
Shading method is required for 3ds max, 0 for wireframe.
0x1 for flat, 0x2 for gouraud, 0x3 for phong and 0x4 for metal."""
material_chunk = _3ds_chunk(MATERIAL)
name = _3ds_chunk(MATNAME)
shading = _3ds_chunk(MATSHADING)
@ -739,7 +743,7 @@ def make_material_chunk(material, image):
diffuse = []
for link in wrap.material.node_tree.links:
if link.from_node.type == 'TEX_IMAGE' and link.to_node.type == 'MIX_RGB':
if link.from_node.type == 'TEX_IMAGE' and link.to_node.type in {'MIX', 'MIX_RGB'}:
diffuse = [link.from_node.image]
if diffuse:
@ -1033,8 +1037,8 @@ def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
obj_matrix = matrix.transposed().to_3x3()
if ob.parent is None:
obj_translate = translation[ob.name]
if ob.parent is None or ob.parent.name not in translation:
obj_translate = matrix.to_translation()
else: # Calculate child matrix translation relative to parent
obj_translate = translation[ob.name].cross(-1 * translation[ob.parent.name])
@ -1185,26 +1189,23 @@ def save(operator,
global_matrix=None,
):
import time
# from bpy_extras.io_utils import create_derived_objects, free_derived_objects
"""Save the Blender scene to a 3ds file."""
# Time the export
duration = time.time()
scene = context.scene
layer = context.view_layer
depsgraph = context.evaluated_depsgraph_get()
if global_matrix is None:
global_matrix = mathutils.Matrix()
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='OBJECT')
scene = context.scene
layer = context.view_layer
depsgraph = context.evaluated_depsgraph_get()
# Initialize the main chunk (primary):
primary = _3ds_chunk(PRIMARY)
# Add version chunk:
version_chunk = _3ds_chunk(VERSION)
version_chunk.add_variable("version", _3ds_uint(3))
@ -1240,9 +1241,9 @@ def save(operator,
mesh_objects = []
if use_selection:
objects = [ob for ob in scene.objects if not ob.hide_viewport and ob.select_get(view_layer=layer)]
objects = [ob for ob in scene.objects if ob.visible_get(view_layer=layer) and ob.select_get(view_layer=layer)]
else:
objects = [ob for ob in scene.objects if not ob.hide_viewport]
objects = [ob for ob in scene.objects if ob.visible_get(view_layer=layer)]
empty_objects = [ob for ob in objects if ob.type == 'EMPTY']
light_objects = [ob for ob in objects if ob.type == 'LIGHT']
@ -1250,7 +1251,6 @@ def save(operator,
for ob in objects:
# get derived objects
# free, derived = create_derived_objects(scene, ob)
derived_dict = bpy_extras.io_utils.create_derived_objects(depsgraph, [ob])
derived = derived_dict.get(ob)
@ -1302,20 +1302,17 @@ def save(operator,
if f.material_index >= ma_ls_len:
f.material_index = 0
# ob_derived_eval.to_mesh_clear()
# if free:
# free_derived_objects(ob)
# Make material chunks for all materials used in the meshes:
for ma_image in materialDict.values():
object_info.add_subchunk(make_material_chunk(ma_image[0], ma_image[1]))
# Collect translation for transformation matrix
translation = {}
# Give all objects a unique ID and build a dictionary from object name to object id:
# name_to_id = {}
translation = {} # collect translation for transformation matrix
for ob, data, matrix in mesh_objects:
translation[ob.name] = ob.location
# name_to_id[ob.name]= len(name_to_id)

View File

@ -294,15 +294,15 @@ def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, of
if extend == 'mirror':
# 3DS mirror flag can be emulated by these settings (at least so it seems)
# TODO: bring back mirror
pass
# texture.repeat_x = texture.repeat_y = 2
# texture.use_mirror_x = texture.use_mirror_y = True
img_wrap.extension = 'MIRROR'
elif extend == 'decal':
# 3DS' decal mode maps best to Blenders EXTEND
img_wrap.extension = 'EXTEND'
elif extend == 'noWrap':
img_wrap.extension = 'CLIP'
if alpha == 'alpha':
for link in links:
if link.from_node.type == 'TEX_IMAGE' and link.to_node.type == 'MIX_RGB':

View File

@ -4,7 +4,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (3, 6, 15),
"version": (3, 6, 18),
'blender': (3, 5, 0),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',

View File

@ -169,6 +169,12 @@ class PrimitiveCreator:
attr['gltf_attribute_name'] = 'COLOR_0'
attr['get'] = self.get_function()
# Seems we sometime can have name collision about attributes
# Avoid crash and ignoring one of duplicated attribute name
if 'COLOR_0' in [a['gltf_attribute_name'] for a in self.blender_attributes]:
print_console('WARNING', 'Attribute (vertex color) collision name: ' + blender_attribute.name + ", ignoring one of them")
continue
else:
# Custom attributes
# Keep only attributes that starts with _
@ -186,6 +192,12 @@ class PrimitiveCreator:
attr['gltf_attribute_name'] = keep_attribute.attr_name.upper()
attr['get'] = self.get_function()
# Seems we sometime can have name collision about attributes
# Avoid crash and ignoring one of duplicated attribute name
if attr['gltf_attribute_name'] in [a['gltf_attribute_name'] for a in self.blender_attributes]:
print_console('WARNING', 'Attribute collision name: ' + blender_attribute.name + ", ignoring one of them")
continue
self.blender_attributes.append(attr)
# Manage POSITION

View File

@ -225,8 +225,8 @@ class VExportTree:
###### Manage children ######
# standard children
if blender_bone is None and blender_object.is_instancer is False:
# standard children (of object, or of instance collection)
if blender_bone is None:
for child_object in blender_children[blender_object]:
if child_object.parent_bone:
# Object parented to bones

View File

@ -204,7 +204,9 @@ class ExportImage:
def __encode_from_image(self, image: bpy.types.Image, export_settings) -> bytes:
# See if there is an existing file we can use.
data = None
if image.source == 'FILE' and not image.is_dirty:
# Sequence image can't be exported, but it avoid to crash to check that default image exists
# Else, it can crash when trying to access a non existing image
if image.source in ['FILE', 'SEQUENCE'] and not image.is_dirty:
if image.packed_file is not None:
data = image.packed_file.data
else:

View File

@ -2875,7 +2875,7 @@ class NWResetNodes(bpy.types.Operator):
def execute(self, context):
node_active = context.active_node
node_selected = context.selected_nodes
node_ignore = ["FRAME", "REROUTE", "GROUP"]
node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
# Check if one node is selected at least
if not (len(node_selected) > 0):

View File

@ -7,7 +7,7 @@ bl_info = {
"name": "Storypencil - Storyboard Tools",
"description": "Storyboard tools",
"author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas, Samuel Bernou",
"version": (1, 1, 3),
"version": (1, 1, 4),
"blender": (3, 3, 0),
"location": "",
"warning": "",

View File

@ -106,9 +106,16 @@ class STORYPENCIL_OT_RenderAction(Operator):
# Create list of selected strips because the selection is changed when adding new strips
Strips = []
Metas = []
for sq in sequences:
if sq.type == 'SCENE':
if sq.type in ('SCENE', 'META'):
if only_selected is False or sq.select is True:
if sq.type == 'META' and is_video_output:
Metas.append(sq)
continue
if sq.type == 'SCENE' and is_video_output and sq.parent_meta():
continue
if sq.type == 'SCENE':
Strips.append(sq)
# Sort strips
@ -133,8 +140,38 @@ class STORYPENCIL_OT_RenderAction(Operator):
try:
Videos = []
Sheets = []
# Read all strips and render the output
# Render Meta Strips (Only video)
for meta in Metas:
meta_name = meta.name
scene.frame_start = int(meta.frame_start + meta.frame_offset_start)
scene.frame_end = int(meta.frame_start + meta.frame_final_duration - 1)
print("Meta:" + meta_name)
print("Video From:", scene.frame_start,
"To", scene.frame_end)
# Video
filepath = os.path.join(rootpath, meta_name)
if image_settings.file_format == 'FFMPEG':
ext = self.video_ext[scene.render.ffmpeg.format]
else:
ext = '.avi'
if not filepath.endswith(ext):
filepath += ext
scene.render.use_file_extension = False
scene.render.filepath = filepath
# Render Animation
bpy.ops.render.render(animation=True)
# Add video to add meta strip later
if scene.storypencil_add_render_strip:
Videos.append(
[filepath, meta.frame_start + meta.frame_offset_start])
# Read all scene strips and render the output (No META)
for sq in Strips:
strip_name = sq.name
strip_scene = sq.scene
@ -187,12 +224,6 @@ class STORYPENCIL_OT_RenderAction(Operator):
self.format_to4(frame_nrr)
filepath = os.path.join(root_folder, framename)
sheet = os.path.realpath(filepath)
sheet = bpy.path.ensure_ext(
sheet, self.image_ext[image_settings.file_format])
Sheets.append([sheet, keyframe])
scene.render.filepath = filepath
# Render Frame

View File

@ -19,6 +19,7 @@ class STORYPENCIL_OT_NewScene(Operator):
bl_options = {'REGISTER', 'UNDO'}
scene_name: bpy.props.StringProperty(default="Scene")
num_strips: bpy.props.IntProperty(default=1, min=1, max=128, description="Number of scenes to add")
# ------------------------------
# Poll
@ -39,6 +40,7 @@ class STORYPENCIL_OT_NewScene(Operator):
layout = self.layout
col = layout.column()
col.prop(self, "scene_name", text="Scene Name")
col.prop(self, "num_strips", text="Repeat")
def format_to3(self, value):
return f"{value:03}"
@ -50,7 +52,10 @@ class STORYPENCIL_OT_NewScene(Operator):
scene_prv = context.scene
cfra_prv = scene_prv.frame_current
scene_base = scene_prv.storypencil_base_scene
repeat = self.num_strips
offset = 0
for i in range(repeat):
# Set context to base scene and duplicate
context.window.scene = scene_base
bpy.ops.scene.new(type='FULL_COPY')
@ -72,7 +77,10 @@ class STORYPENCIL_OT_NewScene(Operator):
context.window.scene = scene_prv
scene_prv.frame_current = cfra_prv
bpy.ops.sequencer.scene_strip_add(
frame_start=cfra_prv, scene=scene_new.name)
frame_start=cfra_prv + offset, scene=scene_new.name)
# Add offset for repeat
offset += scene_new.frame_end - scene_new.frame_start + 1
scene_new.update_tag()
scene_prv.update_tag()

349
vdm_brush_baker/__init__.py Normal file
View File

@ -0,0 +1,349 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2023 Robin Hohnsbeen
from mathutils import Vector
from datetime import datetime
from pathlib import Path
import os
import bpy
from . import bakematerial
import importlib
importlib.reload(bakematerial)
bl_info = {
'name': 'VDM Brush Baker',
'author': 'Robin Hohnsbeen',
'description': 'Bake vector displacement brushes easily from a plane',
'blender': (3, 5, 0),
'version': (1, 0, 2),
'location': 'Sculpt Mode: View3D > Sidebar > Tool Tab',
'warning': '',
'category': 'Baking'
}
class vdm_brush_baker_addon_data(bpy.types.PropertyGroup):
draft_brush_name: bpy.props.StringProperty(
name='Name',
default='NewVDMBrush',
description='The name that will be used for the brush and texture')
render_resolution: bpy.props.EnumProperty(items={
('128', '128 px', 'Render with 128 x 128 pixels', 1),
('256', '256 px', 'Render with 256 x 256 pixels', 2),
('512', '512 px', 'Render with 512 x 512 pixels', 3),
('1024', '1024 px', 'Render with 1024 x 1024 pixels', 4),
('2048', '2048 px', 'Render with 2048 x 2048 pixels', 5),
},
default='512', name='Map Resolution')
compression: bpy.props.EnumProperty(items={
('none', 'None', '', 1),
('zip', 'ZIP (lossless)', '', 2),
},
default='zip', name='Compression')
color_depth: bpy.props.EnumProperty(items={
('16', '16', '', 1),
('32', '32', '', 2),
},
default='16',
name='Color Depth',
description='A color depth of 32 can give better results but leads to far bigger file sizes. 16 should be good if the sculpt doesn\'t extend "too far" from the original plane')
render_samples: bpy.props.IntProperty(name='Render Samples',
default=64,
min=2,
max=4096)
def get_addon_data() -> vdm_brush_baker_addon_data:
return bpy.context.scene.VDMBrushBakerAddonData
def get_output_path(filename):
save_path = bpy.path.abspath('/tmp')
if bpy.data.is_saved:
save_path = os.path.dirname(bpy.data.filepath)
save_path = os.path.join(save_path, 'output_vdm', filename)
if bpy.data.is_saved:
return bpy.path.relpath(save_path)
else:
return save_path
class PT_VDMBaker(bpy.types.Panel):
"""
The main panel of the add-on which contains a button to create a sculpting plane and a button to bake the vector displacement map.
It also has settings for name (image, texture and brush at once), resolution, compression and color depth.
"""
bl_label = 'VDM Brush Baker'
bl_idname = 'Editor_PT_LayoutPanel'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Tool'
def draw(self, context):
layout = self.layout
addon_data = get_addon_data()
layout.use_property_split = True
layout.use_property_decorate = False
layout.operator(create_sculpt_plane.bl_idname, icon='ADD')
layout.separator()
is_occupied, brush_name = get_new_brush_name()
button_text = 'Overwrite VDM Brush' if is_occupied else 'Render and Create VDM Brush'
createvdmlayout = layout.row()
createvdmlayout.enabled = context.active_object is not None and context.active_object.type == 'MESH'
createvdmlayout.operator(
create_vdm_brush.bl_idname, text=button_text, icon='BRUSH_DATA')
if is_occupied:
layout.label(
text='Name Taken: Brush will be overwritten.', icon='INFO')
col = layout.column()
col.alert = is_occupied
col.prop(addon_data, 'draft_brush_name')
settings_layout = layout.column(align=True)
settings_layout.label(text='Settings')
layout_box = settings_layout.box()
col = layout_box.column()
col.prop(addon_data, 'render_resolution')
col = layout_box.column()
col.prop(addon_data, 'compression')
col = layout_box.column()
col.prop(addon_data, 'color_depth')
col = layout_box.column()
col.prop(addon_data, 'render_samples')
layout.separator()
def get_new_brush_name():
addon_data = get_addon_data()
is_name_occupied = False
for custom_brush in bpy.data.brushes:
if custom_brush.name == addon_data.draft_brush_name:
is_name_occupied = True
break
if addon_data.draft_brush_name != '':
return is_name_occupied, addon_data.draft_brush_name
else:
date = datetime.now()
dateformat = date.strftime('%b-%d-%Y-%H-%M-%S')
return False, f'vdm-{dateformat}'
class create_sculpt_plane(bpy.types.Operator):
"""
Creates a grid with 128 vertices per side plus two multires subdivisions.
It uses 'Preserve corners' so further subdivisions can be made while the corners of the grid stay pointy.
"""
bl_idname = 'sculptplane.create'
bl_label = 'Create Sculpting Plane'
bl_description = 'Creates a plane with a multires modifier to sculpt on'
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.mesh.primitive_grid_add(x_subdivisions=128, y_subdivisions=128, size=2,
enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
new_grid = bpy.context.active_object
multires = new_grid.modifiers.new('MultiresVDM', type='MULTIRES')
multires.boundary_smooth = 'PRESERVE_CORNERS'
bpy.ops.object.multires_subdivide(
modifier='MultiresVDM', mode='CATMULL_CLARK')
bpy.ops.object.multires_subdivide(
modifier='MultiresVDM', mode='CATMULL_CLARK') # 512 vertices per one side
return {'FINISHED'}
class create_vdm_brush(bpy.types.Operator):
"""
This operator will bake a vector displacement map from the active object and create a texture and brush datablock.
"""
bl_idname = 'vdmbrush.create'
bl_label = 'Create vdm brush from plane'
bl_description = 'Creates a vector displacement map from your sculpture and creates a brush with it'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'MESH'
def execute(self, context):
if context.active_object is None or context.active_object.type != 'MESH':
return {'CANCELLED'}
vdm_plane = context.active_object
addon_data = get_addon_data()
new_brush_name = addon_data.draft_brush_name
reference_brush_name = addon_data.draft_brush_name
is_occupied, brush_name = get_new_brush_name()
if len(addon_data.draft_brush_name) == 0 or is_occupied:
addon_data.draft_brush_name = brush_name
# Saving user settings
scene = context.scene
default_render_engine = scene.render.engine
default_view_transform = scene.view_settings.view_transform
default_display_device = scene.display_settings.display_device
default_file_format = scene.render.image_settings.file_format
default_color_mode = scene.render.image_settings.color_mode
default_codec = scene.render.image_settings.exr_codec
default_denoise = scene.cycles.use_denoising
default_compute_device = scene.cycles.device
default_scene_samples = scene.cycles.samples
default_plane_location = vdm_plane.location.copy()
default_plane_rotation = vdm_plane.rotation_euler.copy()
default_mode = bpy.context.object.mode
bpy.ops.object.mode_set(mode='OBJECT')
vdm_bake_material = bakematerial.get_vdm_bake_material()
try:
# Prepare baking
scene.render.engine = 'CYCLES'
scene.cycles.samples = addon_data.render_samples
scene.cycles.use_denoising = False
scene.cycles.device = 'GPU'
old_image_name = f'{reference_brush_name}'
if old_image_name in bpy.data.images:
bpy.data.images[old_image_name].name = 'Old VDM texture'
# Removing the image right away can cause a crash when sculpt mode is exited.
# bpy.data.images.remove(bpy.data.images[old_image_name])
vdm_plane.data.materials.clear()
vdm_plane.data.materials.append(vdm_bake_material)
vdm_plane.location = Vector([0, 0, 0])
vdm_plane.rotation_euler = (0, 0, 0)
vdm_texture_node = vdm_bake_material.node_tree.nodes['VDMTexture']
render_resolution = int(addon_data.render_resolution)
bpy.ops.object.select_all(action='DESELECT')
vdm_plane.select_set(True)
output_path = get_output_path(f'{new_brush_name}.exr')
vdm_texture_image = bpy.data.images.new(
name=new_brush_name, width=render_resolution, height=render_resolution, alpha=False, float_buffer=True)
vdm_bake_material.node_tree.nodes.active = vdm_texture_node
vdm_texture_image.filepath_raw = output_path
scene.render.image_settings.file_format = 'OPEN_EXR'
scene.render.image_settings.color_mode = 'RGB'
scene.render.image_settings.exr_codec = 'NONE'
if addon_data.compression == 'zip':
scene.render.image_settings.exr_codec = 'ZIP'
scene.render.image_settings.color_depth = addon_data.color_depth
vdm_texture_image.use_half_precision = addon_data.color_depth == '16'
vdm_texture_image.colorspace_settings.is_data = True
vdm_texture_image.colorspace_settings.name = 'Non-Color'
vdm_texture_node.image = vdm_texture_image
vdm_texture_node.select = True
# Bake
bpy.ops.object.bake(type='EMIT')
# save as render so we have more control over compression settings
vdm_texture_image.save_render(
filepath=bpy.path.abspath(output_path), scene=scene, quality=0)
# Removes the dirty flag, so the image doesn't have to be saved again by the user.
vdm_texture_image.pack()
vdm_texture_image.unpack(method='REMOVE')
except BaseException as Err:
self.report({"ERROR"}, f"{Err}")
finally:
scene.render.image_settings.file_format = default_file_format
scene.render.image_settings.color_mode = default_color_mode
scene.render.image_settings.exr_codec = default_codec
scene.cycles.samples = default_scene_samples
scene.display_settings.display_device = default_display_device
scene.view_settings.view_transform = default_view_transform
scene.cycles.use_denoising = default_denoise
scene.cycles.device = default_compute_device
scene.render.engine = default_render_engine
vdm_plane.data.materials.clear()
vdm_plane.location = default_plane_location
vdm_plane.rotation_euler = default_plane_rotation
# Needs to be in sculpt mode to set 'AREA_PLANE' mapping on new brush.
bpy.ops.object.mode_set(mode='SCULPT')
# Texture
vdm_texture: bpy.types.Texture = None
if bpy.data.textures.find(reference_brush_name) != -1:
vdm_texture = bpy.data.textures[reference_brush_name]
else:
vdm_texture = bpy.data.textures.new(
name=new_brush_name, type='IMAGE')
vdm_texture.extension = 'EXTEND'
vdm_texture.image = vdm_texture_image
vdm_texture.name = new_brush_name
# Brush
new_brush: bpy.types.Brush = None
if bpy.data.brushes.find(reference_brush_name) != -1:
new_brush = bpy.data.brushes[reference_brush_name]
self.report({'INFO'}, f'Changed draw brush \'{new_brush.name}\'')
else:
new_brush = bpy.data.brushes.new(
name=new_brush_name, mode='SCULPT')
self.report(
{'INFO'}, f'Created new draw brush \'{new_brush.name}\'')
new_brush.texture = vdm_texture
new_brush.texture_slot.map_mode = 'AREA_PLANE'
new_brush.stroke_method = 'ANCHORED'
new_brush.name = new_brush_name
new_brush.use_color_as_displacement = True
new_brush.strength = 1.0
new_brush.hardness = 0.9
bpy.ops.object.mode_set(mode = default_mode)
if bpy.context.object.mode == 'SCULPT':
context.tool_settings.sculpt.brush = new_brush
return {'FINISHED'}
registered_classes = [
PT_VDMBaker,
vdm_brush_baker_addon_data,
create_vdm_brush,
create_sculpt_plane
]
def register():
for registered_class in registered_classes:
bpy.utils.register_class(registered_class)
bpy.types.Scene.VDMBrushBakerAddonData = bpy.props.PointerProperty(
type=vdm_brush_baker_addon_data)
def unregister():
for registered_class in registered_classes:
bpy.utils.unregister_class(registered_class)
del bpy.types.Scene.VDMBrushBakerAddonData

View File

@ -0,0 +1,67 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2023 Robin Hohnsbeen
import bpy
def get_vdm_bake_material():
"""Creates a material that is used to bake the displacement from a plane against its UVs.
Returns:
material: Baking material
"""
material_name = 'VDM_baking_material'
if material_name not in bpy.data.materials:
new_material = bpy.data.materials.new(name=material_name)
new_material.use_nodes = True
nodes = new_material.node_tree.nodes
nodes.remove(nodes['Principled BSDF'])
material_output = nodes['Material Output']
# Create relevant nodes
combine_node = nodes.new('ShaderNodeCombineXYZ')
separate_node1 = nodes.new('ShaderNodeSeparateXYZ')
separate_node2 = nodes.new('ShaderNodeSeparateXYZ')
vector_subtract_node = nodes.new('ShaderNodeVectorMath')
vector_subtract_node.operation = 'SUBTRACT'
vector_multiply_node = nodes.new('ShaderNodeVectorMath')
vector_multiply_node.operation = 'MULTIPLY'
vector_multiply_node.inputs[1].default_value = [2.0, 2.0, 2.0]
vector_add_node = nodes.new('ShaderNodeVectorMath')
vector_add_node.operation = 'ADD'
vector_add_node.inputs[1].default_value = [-0.5, -0.5, -0.5]
tex_coord_node = nodes.new('ShaderNodeTexCoord')
image_node = nodes.new('ShaderNodeTexImage')
image_node.name = "VDMTexture"
# Connect nodes
tree = new_material.node_tree
tree.links.new(combine_node.outputs[0], material_output.inputs[0])
tree.links.new(separate_node1.outputs[0], combine_node.inputs[0])
tree.links.new(separate_node1.outputs[1], combine_node.inputs[1])
tree.links.new(
vector_subtract_node.outputs[0], separate_node1.inputs[0])
tree.links.new(
vector_multiply_node.outputs[0], vector_subtract_node.inputs[1])
tree.links.new(
vector_add_node.outputs[0], vector_multiply_node.inputs[0])
tree.links.new(tex_coord_node.outputs[2], vector_add_node.inputs[0])
tree.links.new(
tex_coord_node.outputs[3], vector_subtract_node.inputs[0])
tree.links.new(tex_coord_node.outputs[3], separate_node2.inputs[0])
tree.links.new(separate_node2.outputs[2], combine_node.inputs[2])
return bpy.data.materials[material_name]