Pose Library: Update to use the asset shelf (when enabled) #104546
@ -7,6 +7,7 @@ from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import time
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
import mathutils
|
import mathutils
|
||||||
@ -586,10 +587,12 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
|
|||||||
|
|
||||||
mapflags = 0
|
mapflags = 0
|
||||||
|
|
||||||
# no perfect mapping for mirror modes - 3DS only has uniform mirror w. repeat=2
|
|
||||||
if texslot.extension == 'EXTEND':
|
if texslot.extension == 'EXTEND':
|
||||||
mapflags |= 0x1
|
mapflags |= 0x1
|
||||||
|
|
||||||
|
if texslot.extension == 'MIRROR':
|
||||||
|
mapflags |= 0x2
|
||||||
|
|
||||||
if texslot.extension == 'CLIP':
|
if texslot.extension == 'CLIP':
|
||||||
mapflags |= 0x10
|
mapflags |= 0x10
|
||||||
|
|
||||||
@ -647,6 +650,7 @@ def make_material_chunk(material, image):
|
|||||||
"""Make a material chunk out of a blender material.
|
"""Make a material chunk out of a blender material.
|
||||||
Shading method is required for 3ds max, 0 for wireframe.
|
Shading method is required for 3ds max, 0 for wireframe.
|
||||||
0x1 for flat, 0x2 for gouraud, 0x3 for phong and 0x4 for metal."""
|
0x1 for flat, 0x2 for gouraud, 0x3 for phong and 0x4 for metal."""
|
||||||
|
|
||||||
material_chunk = _3ds_chunk(MATERIAL)
|
material_chunk = _3ds_chunk(MATERIAL)
|
||||||
name = _3ds_chunk(MATNAME)
|
name = _3ds_chunk(MATNAME)
|
||||||
shading = _3ds_chunk(MATSHADING)
|
shading = _3ds_chunk(MATSHADING)
|
||||||
@ -739,7 +743,7 @@ def make_material_chunk(material, image):
|
|||||||
diffuse = []
|
diffuse = []
|
||||||
|
|
||||||
for link in wrap.material.node_tree.links:
|
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]
|
diffuse = [link.from_node.image]
|
||||||
|
|
||||||
if diffuse:
|
if diffuse:
|
||||||
@ -1033,8 +1037,8 @@ def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
|
|||||||
matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
|
matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
|
||||||
obj_matrix = matrix.transposed().to_3x3()
|
obj_matrix = matrix.transposed().to_3x3()
|
||||||
|
|
||||||
if ob.parent is None:
|
if ob.parent is None or ob.parent.name not in translation:
|
||||||
obj_translate = translation[ob.name]
|
obj_translate = matrix.to_translation()
|
||||||
|
|
||||||
else: # Calculate child matrix translation relative to parent
|
else: # Calculate child matrix translation relative to parent
|
||||||
obj_translate = translation[ob.name].cross(-1 * translation[ob.parent.name])
|
obj_translate = translation[ob.name].cross(-1 * translation[ob.parent.name])
|
||||||
@ -1185,26 +1189,23 @@ def save(operator,
|
|||||||
global_matrix=None,
|
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."""
|
"""Save the Blender scene to a 3ds file."""
|
||||||
|
|
||||||
# Time the export
|
# Time the export
|
||||||
duration = time.time()
|
duration = time.time()
|
||||||
|
|
||||||
|
scene = context.scene
|
||||||
|
layer = context.view_layer
|
||||||
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
if global_matrix is None:
|
if global_matrix is None:
|
||||||
global_matrix = mathutils.Matrix()
|
global_matrix = mathutils.Matrix()
|
||||||
|
|
||||||
if bpy.ops.object.mode_set.poll():
|
if bpy.ops.object.mode_set.poll():
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
scene = context.scene
|
|
||||||
layer = context.view_layer
|
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
|
||||||
|
|
||||||
# Initialize the main chunk (primary):
|
# Initialize the main chunk (primary):
|
||||||
primary = _3ds_chunk(PRIMARY)
|
primary = _3ds_chunk(PRIMARY)
|
||||||
|
|
||||||
# Add version chunk:
|
# Add version chunk:
|
||||||
version_chunk = _3ds_chunk(VERSION)
|
version_chunk = _3ds_chunk(VERSION)
|
||||||
version_chunk.add_variable("version", _3ds_uint(3))
|
version_chunk.add_variable("version", _3ds_uint(3))
|
||||||
@ -1240,9 +1241,9 @@ def save(operator,
|
|||||||
mesh_objects = []
|
mesh_objects = []
|
||||||
|
|
||||||
if use_selection:
|
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:
|
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']
|
empty_objects = [ob for ob in objects if ob.type == 'EMPTY']
|
||||||
light_objects = [ob for ob in objects if ob.type == 'LIGHT']
|
light_objects = [ob for ob in objects if ob.type == 'LIGHT']
|
||||||
@ -1250,7 +1251,6 @@ def save(operator,
|
|||||||
|
|
||||||
for ob in objects:
|
for ob in objects:
|
||||||
# get derived objects
|
# get derived objects
|
||||||
# free, derived = create_derived_objects(scene, ob)
|
|
||||||
derived_dict = bpy_extras.io_utils.create_derived_objects(depsgraph, [ob])
|
derived_dict = bpy_extras.io_utils.create_derived_objects(depsgraph, [ob])
|
||||||
derived = derived_dict.get(ob)
|
derived = derived_dict.get(ob)
|
||||||
|
|
||||||
@ -1302,20 +1302,17 @@ def save(operator,
|
|||||||
if f.material_index >= ma_ls_len:
|
if f.material_index >= ma_ls_len:
|
||||||
f.material_index = 0
|
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:
|
# Make material chunks for all materials used in the meshes:
|
||||||
for ma_image in materialDict.values():
|
for ma_image in materialDict.values():
|
||||||
object_info.add_subchunk(make_material_chunk(ma_image[0], ma_image[1]))
|
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:
|
# Give all objects a unique ID and build a dictionary from object name to object id:
|
||||||
# name_to_id = {}
|
# name_to_id = {}
|
||||||
|
|
||||||
translation = {} # collect translation for transformation matrix
|
|
||||||
|
|
||||||
for ob, data, matrix in mesh_objects:
|
for ob, data, matrix in mesh_objects:
|
||||||
translation[ob.name] = ob.location
|
translation[ob.name] = ob.location
|
||||||
# name_to_id[ob.name]= len(name_to_id)
|
# name_to_id[ob.name]= len(name_to_id)
|
||||||
|
@ -294,15 +294,15 @@ def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, of
|
|||||||
|
|
||||||
if extend == 'mirror':
|
if extend == 'mirror':
|
||||||
# 3DS mirror flag can be emulated by these settings (at least so it seems)
|
# 3DS mirror flag can be emulated by these settings (at least so it seems)
|
||||||
# TODO: bring back mirror
|
img_wrap.extension = 'MIRROR'
|
||||||
pass
|
|
||||||
# texture.repeat_x = texture.repeat_y = 2
|
|
||||||
# texture.use_mirror_x = texture.use_mirror_y = True
|
|
||||||
elif extend == 'decal':
|
elif extend == 'decal':
|
||||||
# 3DS' decal mode maps best to Blenders EXTEND
|
# 3DS' decal mode maps best to Blenders EXTEND
|
||||||
img_wrap.extension = 'EXTEND'
|
img_wrap.extension = 'EXTEND'
|
||||||
|
|
||||||
elif extend == 'noWrap':
|
elif extend == 'noWrap':
|
||||||
img_wrap.extension = 'CLIP'
|
img_wrap.extension = 'CLIP'
|
||||||
|
|
||||||
if alpha == 'alpha':
|
if alpha == 'alpha':
|
||||||
for link in links:
|
for link in 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 == 'MIX_RGB':
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
'name': 'glTF 2.0 format',
|
'name': 'glTF 2.0 format',
|
||||||
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
'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),
|
'blender': (3, 5, 0),
|
||||||
'location': 'File > Import-Export',
|
'location': 'File > Import-Export',
|
||||||
'description': 'Import-Export as glTF 2.0',
|
'description': 'Import-Export as glTF 2.0',
|
||||||
|
@ -169,6 +169,12 @@ class PrimitiveCreator:
|
|||||||
attr['gltf_attribute_name'] = 'COLOR_0'
|
attr['gltf_attribute_name'] = 'COLOR_0'
|
||||||
attr['get'] = self.get_function()
|
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:
|
else:
|
||||||
# Custom attributes
|
# Custom attributes
|
||||||
# Keep only attributes that starts with _
|
# Keep only attributes that starts with _
|
||||||
@ -186,6 +192,12 @@ class PrimitiveCreator:
|
|||||||
attr['gltf_attribute_name'] = keep_attribute.attr_name.upper()
|
attr['gltf_attribute_name'] = keep_attribute.attr_name.upper()
|
||||||
attr['get'] = self.get_function()
|
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)
|
self.blender_attributes.append(attr)
|
||||||
|
|
||||||
# Manage POSITION
|
# Manage POSITION
|
||||||
|
@ -225,8 +225,8 @@ class VExportTree:
|
|||||||
|
|
||||||
###### Manage children ######
|
###### Manage children ######
|
||||||
|
|
||||||
# standard children
|
# standard children (of object, or of instance collection)
|
||||||
if blender_bone is None and blender_object.is_instancer is False:
|
if blender_bone is None:
|
||||||
for child_object in blender_children[blender_object]:
|
for child_object in blender_children[blender_object]:
|
||||||
if child_object.parent_bone:
|
if child_object.parent_bone:
|
||||||
# Object parented to bones
|
# Object parented to bones
|
||||||
|
@ -204,7 +204,9 @@ class ExportImage:
|
|||||||
def __encode_from_image(self, image: bpy.types.Image, export_settings) -> bytes:
|
def __encode_from_image(self, image: bpy.types.Image, export_settings) -> bytes:
|
||||||
# See if there is an existing file we can use.
|
# See if there is an existing file we can use.
|
||||||
data = None
|
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:
|
if image.packed_file is not None:
|
||||||
data = image.packed_file.data
|
data = image.packed_file.data
|
||||||
else:
|
else:
|
||||||
|
@ -2875,7 +2875,7 @@ class NWResetNodes(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
node_active = context.active_node
|
node_active = context.active_node
|
||||||
node_selected = context.selected_nodes
|
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
|
# Check if one node is selected at least
|
||||||
if not (len(node_selected) > 0):
|
if not (len(node_selected) > 0):
|
||||||
|
@ -7,7 +7,7 @@ bl_info = {
|
|||||||
"name": "Storypencil - Storyboard Tools",
|
"name": "Storypencil - Storyboard Tools",
|
||||||
"description": "Storyboard tools",
|
"description": "Storyboard tools",
|
||||||
"author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas, Samuel Bernou",
|
"author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas, Samuel Bernou",
|
||||||
"version": (1, 1, 3),
|
"version": (1, 1, 4),
|
||||||
"blender": (3, 3, 0),
|
"blender": (3, 3, 0),
|
||||||
"location": "",
|
"location": "",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
|
@ -106,10 +106,17 @@ class STORYPENCIL_OT_RenderAction(Operator):
|
|||||||
|
|
||||||
# Create list of selected strips because the selection is changed when adding new strips
|
# Create list of selected strips because the selection is changed when adding new strips
|
||||||
Strips = []
|
Strips = []
|
||||||
|
Metas = []
|
||||||
for sq in sequences:
|
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 only_selected is False or sq.select is True:
|
||||||
Strips.append(sq)
|
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
|
# Sort strips
|
||||||
Strips = sorted(Strips, key=lambda strip: strip.frame_start)
|
Strips = sorted(Strips, key=lambda strip: strip.frame_start)
|
||||||
@ -133,8 +140,38 @@ class STORYPENCIL_OT_RenderAction(Operator):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Videos = []
|
Videos = []
|
||||||
Sheets = []
|
# Render Meta Strips (Only video)
|
||||||
# Read all strips and render the output
|
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:
|
for sq in Strips:
|
||||||
strip_name = sq.name
|
strip_name = sq.name
|
||||||
strip_scene = sq.scene
|
strip_scene = sq.scene
|
||||||
@ -187,12 +224,6 @@ class STORYPENCIL_OT_RenderAction(Operator):
|
|||||||
self.format_to4(frame_nrr)
|
self.format_to4(frame_nrr)
|
||||||
|
|
||||||
filepath = os.path.join(root_folder, framename)
|
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
|
scene.render.filepath = filepath
|
||||||
|
|
||||||
# Render Frame
|
# Render Frame
|
||||||
|
@ -19,6 +19,7 @@ class STORYPENCIL_OT_NewScene(Operator):
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
scene_name: bpy.props.StringProperty(default="Scene")
|
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
|
# Poll
|
||||||
@ -39,6 +40,7 @@ class STORYPENCIL_OT_NewScene(Operator):
|
|||||||
layout = self.layout
|
layout = self.layout
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.prop(self, "scene_name", text="Scene Name")
|
col.prop(self, "scene_name", text="Scene Name")
|
||||||
|
col.prop(self, "num_strips", text="Repeat")
|
||||||
|
|
||||||
def format_to3(self, value):
|
def format_to3(self, value):
|
||||||
return f"{value:03}"
|
return f"{value:03}"
|
||||||
@ -50,32 +52,38 @@ class STORYPENCIL_OT_NewScene(Operator):
|
|||||||
scene_prv = context.scene
|
scene_prv = context.scene
|
||||||
cfra_prv = scene_prv.frame_current
|
cfra_prv = scene_prv.frame_current
|
||||||
scene_base = scene_prv.storypencil_base_scene
|
scene_base = scene_prv.storypencil_base_scene
|
||||||
|
repeat = self.num_strips
|
||||||
|
|
||||||
# Set context to base scene and duplicate
|
offset = 0
|
||||||
context.window.scene = scene_base
|
for i in range(repeat):
|
||||||
bpy.ops.scene.new(type='FULL_COPY')
|
# Set context to base scene and duplicate
|
||||||
scene_new = context.window.scene
|
context.window.scene = scene_base
|
||||||
new_name = scene_prv.storypencil_name_prefix + \
|
bpy.ops.scene.new(type='FULL_COPY')
|
||||||
self.scene_name + scene_prv.storypencil_name_suffix
|
scene_new = context.window.scene
|
||||||
id = 0
|
new_name = scene_prv.storypencil_name_prefix + \
|
||||||
while new_name in bpy.data.scenes:
|
self.scene_name + scene_prv.storypencil_name_suffix
|
||||||
id += 1
|
id = 0
|
||||||
new_name = scene_prv.storypencil_name_prefix + self.scene_name + \
|
while new_name in bpy.data.scenes:
|
||||||
scene_prv.storypencil_name_suffix + '.' + self.format_to3(id)
|
id += 1
|
||||||
|
new_name = scene_prv.storypencil_name_prefix + self.scene_name + \
|
||||||
|
scene_prv.storypencil_name_suffix + '.' + self.format_to3(id)
|
||||||
|
|
||||||
scene_new.name = new_name
|
scene_new.name = new_name
|
||||||
# Set duration of new scene
|
# Set duration of new scene
|
||||||
scene_new.frame_end = scene_new.frame_start + \
|
scene_new.frame_end = scene_new.frame_start + \
|
||||||
scene_prv.storypencil_scene_duration - 1
|
scene_prv.storypencil_scene_duration - 1
|
||||||
|
|
||||||
# Back to original scene
|
# Back to original scene
|
||||||
context.window.scene = scene_prv
|
context.window.scene = scene_prv
|
||||||
scene_prv.frame_current = cfra_prv
|
scene_prv.frame_current = cfra_prv
|
||||||
bpy.ops.sequencer.scene_strip_add(
|
bpy.ops.sequencer.scene_strip_add(
|
||||||
frame_start=cfra_prv, scene=scene_new.name)
|
frame_start=cfra_prv + offset, scene=scene_new.name)
|
||||||
|
|
||||||
scene_new.update_tag()
|
# Add offset for repeat
|
||||||
scene_prv.update_tag()
|
offset += scene_new.frame_end - scene_new.frame_start + 1
|
||||||
|
|
||||||
|
scene_new.update_tag()
|
||||||
|
scene_prv.update_tag()
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
349
vdm_brush_baker/__init__.py
Normal file
349
vdm_brush_baker/__init__.py
Normal 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
|
67
vdm_brush_baker/bakematerial.py
Normal file
67
vdm_brush_baker/bakematerial.py
Normal 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]
|
Loading…
Reference in New Issue
Block a user