Node Wrangler: Improved accuracy on Align Nodes operator #104551
@ -586,11 +586,10 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
|
|||||||
either 0x100 or 0x200, tintcolor will be processed if colorchunks are present"""
|
either 0x100 or 0x200, tintcolor will be processed if colorchunks are present"""
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
# Copyright 2005 Bob Holcomb
|
# Copyright 2005 Bob Holcomb
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import struct
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import time
|
||||||
import math
|
import math
|
||||||
|
import struct
|
||||||
import mathutils
|
import mathutils
|
||||||
from bpy_extras.image_utils import load_image
|
from bpy_extras.image_utils import load_image
|
||||||
from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
|
from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
|
||||||
@ -293,16 +293,12 @@ def add_texture_to_material(image, contextWrapper, pct, extend, alpha, scale, of
|
|||||||
img_wrap.rotation[2] = angle
|
img_wrap.rotation[2] = angle
|
||||||
|
|
||||||
if extend == 'mirror':
|
if extend == 'mirror':
|
||||||
# 3DS mirror flag can be emulated by these settings (at least so it seems)
|
img_wrap.extension = 'MIRROR'
|
||||||
# TODO: bring back 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
|
|
||||||
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:
|
||||||
|
@ -2944,7 +2944,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