Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
chsh2 committed Jan 9, 2024
1 parent 0f7462f commit 493434c
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 101 deletions.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# Texture VFX Control: Blender Add-on

WIP
This Blender add-on provides with shader-based effects on image/sequence/movie textures. It enables users to perform video composition directly in the 3D space without using the compositor or the sequencer.

The functionalities of this add-on include:

- Texture Playback Control
- Setting the speed and loop mode of sequence/movie textures
- VFX Shaders
- Blur
- Chroma Key
- Outline


## Requirements

Blender 3.3+ or Blender 4.0

## Installation

WIP

## Usage

WIP

### Playback Control Driver

### VFX Shader Node Groups

## Credits
2 changes: 1 addition & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description" : "Shader-based video playback control and composition VFXs",
"blender" : (3, 3, 0),
"version" : (0, 1, 0),
"location" : "Node Editor",
"location" : "View3D > Object > Quick Effects, or Shader Editor > Add",
"warning" : "This addon is still in an early stage of development",
"doc_url": "",
"wiki_url": "",
Expand Down
Empty file added operators/__init__.py
Empty file.
19 changes: 4 additions & 15 deletions op_driver.py → operators/op_playback.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import bpy
from .utils import get_target_node

def add_driver_variable(driver, id, data_path, name):
var = driver.variables.new()
var.name = name
var.type = 'SINGLE_PROP'
var.targets[0].id = id
var.targets[0].data_path = f'["{data_path}"]'
from ..utils.driver import add_driver_variable
from ..utils.node import get_target_node

class AddTexturePlaybackDriverOperator(bpy.types.Operator):
"""Map the playback of a movie/sequence texture to a custom float property by adding a driver to the offset value"""
Expand Down Expand Up @@ -114,13 +108,8 @@ def add_driver_to_node(node):
add_driver_variable(playback_driver.driver, obj, datapath_start, 's')
add_driver_variable(playback_driver.driver, obj, datapath_playhead, 'p')
add_driver_variable(playback_driver.driver, obj, datapath_duration, 'd')

scene_var = playback_driver.driver.variables.new()
scene_var.name = 't'
scene_var.type = 'SINGLE_PROP'
scene_var.targets[0].id_type = 'SCENE'
scene_var.targets[0].id = bpy.context.scene
scene_var.targets[0].data_path = 'frame_current'
add_driver_variable(playback_driver.driver, bpy.context.scene, 'frame_current', 't',
id_type='SCENE', custom_property=False)

playback_driver.driver.expression = 'min(max(floor(p*d)+s-t, s-t), s+d-t-1)'

Expand Down
121 changes: 95 additions & 26 deletions op_shader_nodes.py → operators/op_shader_nodes.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import bpy
from .utils import append_node_group, get_target_node, copy_driver

def add_node_group(node_tree, name, location=(0,0)) -> bpy.types.ShaderNodeGroup:
node = node_tree.nodes.new('ShaderNodeGroup')
node.node_tree = append_node_group(name)
node.location = location
return node

def add_uv_input(node_tree, location=(0,0)) -> bpy.types.ShaderNodeUVMap:
node = node_tree.nodes.new('ShaderNodeUVMap')
node.location = location
return node
import math
from ..utils.asset import append_node_group
from ..utils.node import *
from ..utils.driver import copy_driver, add_driver_variable

def get_fx_chain_info(tex_node, fx_types=['tfx_ChromaKey']):
"""
Expand Down Expand Up @@ -283,10 +275,10 @@ class OutlineOperator(bpy.types.Operator):
default = (1.0,1.0,.0,1.0),
min = 0.0, max = 1.0, size = 4,
)
outline_size: bpy.props.FloatVectorProperty(
outline_size: bpy.props.FloatProperty(
name = "Size",
default = (0.5, 0.5),
min = 0, soft_max = 5, size = 2, step = 1
default = 0.5,
min = 0, soft_max = 5, step = 1
)
outline_outer: bpy.props.BoolProperty(
name = "Outer Contour",
Expand All @@ -300,15 +292,45 @@ class OutlineOperator(bpy.types.Operator):
name='Blur',
default=0, min=0, max=0.1, step=1
)

directional: bpy.props.BoolProperty(
name='Directional',
default = False
)
direction: bpy.props.EnumProperty(
name='Direction',
items=[('FIXED', 'Fixed Angle', ''),
('REF', 'Following a Light', '')],
default='FIXED'
)
angle: bpy.props.FloatProperty(
name='Angle',
default=0.25*math.pi, min=-2*math.pi, max=2*math.pi,
unit='ROTATION',
)
light_name: bpy.props.StringProperty(
name='Source',
default='',
search=lambda self, context, edit_text: [obj.name for obj in context.scene.objects]
)

def draw(self, context):
layout = self.layout
layout.prop(self, 'outline_color')
layout.prop(self, 'outline_size')
layout.prop(self, 'outline_outer')
layout.prop(self, 'outline_inner')
row = layout.row()
row.prop(self, 'outline_outer')
row.prop(self, 'outline_inner')
layout.prop(self, 'blur_strength')

layout.prop(self, 'directional')
if self.directional:
layout.prop(self, 'direction')
if self.direction == 'FIXED':
layout.prop(self, 'angle')
else:
layout.label(text="Light Information:")
box = layout.box()
box.prop(self, 'light_name')

def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)

Expand All @@ -323,7 +345,9 @@ def execute(self, context):
if not tex_node:
self.report({"WARNING"}, "Cannot find any eligible texture node to perform the operation.")
return {'FINISHED'}

if tex_node.extension == 'CLIP':
tex_node.extension = 'EXTEND'

# Duplicate nodes for edge detection
node_chain, inside_links, output_color_links, output_alpha_links, \
output_color_socket, output_alpha_socket = get_fx_chain_info(tex_node)
Expand Down Expand Up @@ -359,18 +383,63 @@ def execute(self, context):
post_node = add_node_group(target_node_tree, 'tfx_OutlinePost',
location=(node_chain[-1].location[0] + 200,
node_chain[-1].location[1]))
post_node.inputs['Color'].default_value = self.outline_color
post_node.inputs['Inner'].default_value = float(self.outline_inner)
post_node.inputs['Outer'].default_value = float(self.outline_outer)

# Set offset values considering different factors
ratio = tex_node.image.size[0] / tex_node.image.size[1]
converted_outline_size = [self.outline_size / 100.0, self.outline_size * ratio / 100.0] if ratio < 1 \
else [self.outline_size / ratio / 100.0, self.outline_size / 100.0]
if self.directional and self.direction == 'FIXED':
pre_node.inputs['U_Offset'].default_value = converted_outline_size[0] * math.cos(self.angle)
pre_node.inputs['V_Offset'].default_value = converted_outline_size[1] * math.sin(self.angle)
else:
pre_node.inputs['U_Offset'].default_value = converted_outline_size[0]
pre_node.inputs['V_Offset'].default_value = converted_outline_size[1]

# If use a light source, add extra node groups and drivers
if self.directional and self.direction == 'REF':
light_obj = context.scene.objects[self.light_name] if self.light_name in context.scene.objects else None
if light_obj:
light_node = add_node_group(target_node_tree, 'tfx_LightVector',
location=(pre_node.location[0] - 200,
pre_node.location[1] - 150))
# Set light direction: currently only supports point light
target_node_tree.links.new(light_node.outputs['X'], pre_node.inputs['U_Offset'])
target_node_tree.links.new(light_node.outputs['Y'], pre_node.inputs['V_Offset'])
light_loc_drivers = light_node.inputs['Light'].driver_add('default_value')
for i,dr in enumerate(light_loc_drivers):
dr.driver.type = 'SCRIPTED'
add_driver_variable(dr.driver, light_obj, f'location[{i}]', 'var', custom_property=False)
dr.driver.expression = 'var'
# Set light strength
light_node.inputs['Scale'].default_value[0] = converted_outline_size[0]
light_node.inputs['Scale'].default_value[1] = converted_outline_size[1]

# Set light color: disabled for now
if False and light_obj.type == 'LIGHT':
light_color_drivers = post_node.inputs['Color'].driver_add('default_value')
for i,dr in enumerate(light_color_drivers):
dr.driver.type = 'SCRIPTED'
if i < 3:
add_driver_variable(dr.driver, light_obj.data, f'color[{i}]', 'var',
id_type='LIGHT', custom_property=False)
dr.driver.expression = 'var'
else:
dr.driver.expression = '1.0'

# Connect to the FX nodes
target_node_tree.links.new(uv_socket, pre_node.inputs['UV'])
for tag in ['U+', 'U-', 'V+', 'V-']:
target_node_tree.links.new(pre_node.outputs[tag], offset_uv_inputs[tag])
target_node_tree.links.new(offset_alpha_outputs[tag], post_node.inputs[tag])
target_node_tree.links.new(output_color_socket, post_node.inputs['Image'])
target_node_tree.links.new(output_alpha_socket, post_node.inputs['Alpha'])
pre_node.inputs['U_Offset'].default_value = self.outline_size[0] / 100.0
pre_node.inputs['V_Offset'].default_value = self.outline_size[1] / 100.0
post_node.inputs['Color'].default_value = self.outline_color
post_node.inputs['Inner'].default_value = float(self.outline_inner)
post_node.inputs['Outer'].default_value = float(self.outline_outer)

if self.directional:
target_node_tree.links.new(output_alpha_socket, post_node.inputs['U-'])
target_node_tree.links.new(output_alpha_socket, post_node.inputs['V-'])

# Reconnect output sockets
for link in output_color_links:
target_node_tree.links.new(post_node.outputs['Image'], link.to_socket)
Expand Down
Binary file modified res/assets.blend
Binary file not shown.
58 changes: 0 additions & 58 deletions utils.py

This file was deleted.

25 changes: 25 additions & 0 deletions utils/asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import bpy
import os

def get_library_blend_file():
file_name = '../res/assets.blend'
script_file = os.path.realpath(__file__)
directory = os.path.dirname(script_file)
file_path = os.path.join(directory, file_name)
return file_path

def append_node_group(node_group_name):
if node_group_name in bpy.data.node_groups:
return bpy.data.node_groups[node_group_name]

mode = bpy.context.mode
bpy.ops.object.mode_set(mode='OBJECT')
file_path = get_library_blend_file()
inner_path = 'NodeTree'
bpy.ops.wm.append(
filepath=os.path.join(file_path, inner_path, node_group_name),
directory=os.path.join(file_path, inner_path),
filename=node_group_name
)
bpy.ops.object.mode_set(mode=mode)
return bpy.data.node_groups[node_group_name]
23 changes: 23 additions & 0 deletions utils/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import bpy

def add_driver_variable(driver, id, data_path, name, id_type='OBJECT', custom_property = True):
var = driver.variables.new()
var.name = name
var.type = 'SINGLE_PROP'
var.targets[0].id_type = id_type
var.targets[0].id = id
var.targets[0].data_path = f'["{data_path}"]' if custom_property else data_path

def copy_driver(src, dst):
"""
Copy all attributes of the source driver to an empty destination driver
"""
dst.type = src.type
for src_var in src.variables:
dst_var = dst.variables.new()
dst_var.name = src_var.name
dst_var.type = src_var.type
dst_var.targets[0].id_type = src_var.targets[0].id_type
dst_var.targets[0].id = src_var.targets[0].id
dst_var.targets[0].data_path = src_var.targets[0].data_path
dst.expression = src.expression
32 changes: 32 additions & 0 deletions utils/node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import bpy
from ..utils.asset import append_node_group

def get_target_node(node_tree,
filter = lambda node: (node.type == 'TEX_IMAGE' and node.image)
):
"""
Find the shader node to perform operations.
First, check the active node. If it is not qualified, search through all nodes to find the first eligible one
"""
nodes = node_tree.nodes
if nodes.active and filter(nodes.active):
return nodes.active, node_tree
# Search recursively for node groups
elif nodes.active and nodes.active.type=='GROUP' and nodes.active.node_tree:
return get_target_node(nodes.active.node_tree, filter)
else:
for node in nodes:
if filter(node):
return node, node_tree
return None, None

def add_node_group(node_tree, name, location=(0,0)) -> bpy.types.ShaderNodeGroup:
node = node_tree.nodes.new('ShaderNodeGroup')
node.node_tree = append_node_group(name)
node.location = location
return node

def add_uv_input(node_tree, location=(0,0)) -> bpy.types.ShaderNodeUVMap:
node = node_tree.nodes.new('ShaderNodeUVMap')
node.location = location
return node

0 comments on commit 493434c

Please sign in to comment.