Implemented multi-pass nodes and fixed blur. Various other fixes.

* Added a "constant wave" in the pattern node
* Updated graphEdit to detect and forbid loops
* Modified code that renders to texture to update a texture instead of returning one (so we avoid updating everything and rely on everything being updated automatically wrt textures)
* base library is loaded from filesystem (instead of package) if available
This commit is contained in:
Rodolphe Suescun 2018-08-12 19:25:18 +02:00
parent 2f8be1a142
commit 18015aec93
15 changed files with 211 additions and 101 deletions

View File

@ -13,6 +13,10 @@ vec3 rand3(vec2 x) {
dot(x, vec2(13.254, 5.867)))) * 43758.5453);
}
float wave_constant(float x) {
return 1.0;
}
float wave_sin(float x) {
return 0.5-0.5*cos(3.1415928*2.0*x);
}

View File

@ -8,7 +8,7 @@ signal save_path_changed
signal graph_changed
func _ready():
pass
OS.low_processor_usage_mode = true
func get_source(node, port):
for c in get_connection_list():
@ -21,24 +21,35 @@ func add_node(node, global_position = null):
node.offset = (scroll_offset + global_position - rect_global_position) / zoom
node.connect("close_request", self, "remove_node", [ node ])
func connect_node(from, from_slot, to, to_slot):
var source_list = [ from ]
while !source_list.empty():
var source = source_list.pop_front()
if source == to:
#print("cannot connect %s to %s (%s)" % [from, to, source])
return false
for c in get_connection_list():
if c.to == source and source_list.find(c.from) == -1:
source_list.append(c.from)
var disconnect = get_source(to, to_slot)
if disconnect != null:
.disconnect_node(disconnect.node, disconnect.slot, to, to_slot)
.connect_node(from, from_slot, to, to_slot)
send_changed_signal()
return true
func disconnect_node(from, from_slot, to, to_slot):
.disconnect_node(from, from_slot, to, to_slot)
send_changed_signal();
func remove_node(node):
var node_name = node.name
for c in get_connection_list():
if c.from == node_name or c.to == node_name:
disconnect_node(c.from, c.from_port, c.to, c.to_port)
send_changed_signal()
node.queue_free()
send_changed_signal()
func _on_GraphEdit_connection_request(from, from_slot, to, to_slot):
var disconnect = get_source(to, to_slot)
if disconnect != null:
disconnect_node(disconnect.node, disconnect.slot, to, to_slot)
connect_node(from, from_slot, to, to_slot)
send_changed_signal();
func _on_GraphEdit_disconnection_request(from, from_slot, to, to_slot):
disconnect_node(from, from_slot, to, to_slot)
send_changed_signal();
# Global operations on graph
@ -96,7 +107,7 @@ func create_node(data, global_position = null):
i += 1
add_node(node, global_position)
node.deserialize(data)
send_changed_signal()
send_changed_signal()
func load_file():
var dialog = FileDialog.new()
@ -120,7 +131,6 @@ func do_load_file(filename):
for c in data.connections:
connect_node(c.from, c.from_port, c.to, c.to_port)
set_save_path(filename)
send_changed_signal()
set_need_save(false)
func save_file():
@ -164,6 +174,8 @@ func send_changed_signal():
func do_send_changed_signal():
emit_signal("graph_changed")
# Drag and drop
func can_drop_data(position, data):
return typeof(data) == TYPE_DICTIONARY and data.has('type')
@ -180,8 +192,8 @@ func setup_material(shader_material, textures, shader_code):
var render_queue = []
func render_to_viewport(node, size, method, args):
render_queue.append( { shader=node.generate_shader(), textures=node.get_textures(), size=size, method=method, args=args } )
func render_shader_to_viewport(shader, textures, size, method, args):
render_queue.append( { shader=shader, textures=textures, size=size, method=method, args=args } )
if render_queue.size() == 1:
while !render_queue.empty():
var job = render_queue.front()
@ -190,8 +202,9 @@ func render_to_viewport(node, size, method, args):
$SaveViewport/ColorRect.rect_size = Vector2(job.size, job.size)
var shader_material = $SaveViewport/ColorRect.material
shader_material.shader.code = job.shader
for k in job.textures.keys():
shader_material.set_shader_param(k+"_tex", job.textures[k])
if job.textures != null:
for k in job.textures.keys():
shader_material.set_shader_param(k+"_tex", job.textures[k])
$SaveViewport.render_target_update_mode = Viewport.UPDATE_ONCE
$SaveViewport.update_worlds()
yield(get_tree(), "idle_frame")
@ -200,6 +213,9 @@ func render_to_viewport(node, size, method, args):
callv(job.method, job.args)
render_queue.pop_front()
func render_to_viewport(node, size, method, args):
render_shader_to_viewport(node.generate_shader(), node.get_textures(), size, method, args)
func export_texture(node, filename, size = 256):
if node == null:
return null
@ -210,15 +226,16 @@ func do_export_texture(filename):
var viewport_image = viewport_texture.get_data()
viewport_image.save_png(filename)
func precalculate_texture(node, size, object, method, args):
func precalculate_node(node, size, target_texture, object, method, args):
if node == null:
return null
render_to_viewport(node, size, "do_precalculate_texture", [ object, method, args ])
func do_precalculate_texture(object, method, args):
func precalculate_shader(shader, textures, size, target_texture, object, method, args):
render_shader_to_viewport(shader, textures, size, "do_precalculate_texture", [ target_texture, object, method, args ])
func do_precalculate_texture(target_texture, object, method, args):
var viewport_texture = $SaveViewport.get_texture()
var texture = ImageTexture.new()
texture.create_from_image(viewport_texture.get_data())
args.append(texture)
target_texture.create_from_image(viewport_texture.get_data())
object.callv(method, args)

View File

@ -243,7 +243,7 @@ COLOR = vec4(colorize_3_0_rgb, 1.0);
render_priority = 0
shader = SubResource( 2 )
[node name="GraphEdit" type="GraphEdit"]
[node name="GraphEdit" type="GraphEdit" index="0"]
self_modulate = Color( 1, 1, 1, 0 )
anchor_left = 0.0
@ -267,8 +267,6 @@ _sections_unfolded = [ "Material", "Mouse", "Visibility" ]
[node name="Material" parent="." index="0" instance=ExtResource( 2 )]
margin_right = 111.0
margin_bottom = 118.0
theme = SubResource( 1 )
_sections_unfolded = [ "Anchor", "Margin", "Mouse", "Theme", "slot", "slot/2", "slot/3", "slot/4", "slot/5" ]
@ -331,9 +329,9 @@ wait_time = 0.1
one_shot = true
autostart = false
[connection signal="connection_request" from="." to="." method="_on_GraphEdit_connection_request"]
[connection signal="connection_request" from="." to="." method="connect_node"]
[connection signal="disconnection_request" from="." to="." method="_on_GraphEdit_disconnection_request"]
[connection signal="disconnection_request" from="." to="." method="disconnect_node"]
[connection signal="timeout" from="Timer" to="." method="do_send_changed_signal"]

View File

@ -26,20 +26,25 @@ func get_drag_data(position):
func _ready():
var root = create_item()
add_library("res://addons/procedural_material/library/base.json")
var lib_path = OS.get_executable_path()
lib_path = lib_path.left(max(lib_path.rfind("\\"), lib_path.rfind("/"))+1)+"library/base.json"
if !add_library(lib_path):
add_library("res://addons/procedural_material/library/base.json")
add_library("user://library/user.json")
func add_library(filename):
var root = get_root()
var file = File.new()
if file.open(filename, File.READ) != OK:
return
return false
var lib = parse_json(file.get_as_text())
file.close()
if lib != null && lib.has("lib"):
for m in lib.lib:
m.library = filename
add_item(m, m.tree_item)
return true
return false
func add_item(item, item_name, item_parent = null):
if item_parent == null:

View File

@ -1,6 +1,7 @@
tool
extends GraphNode
# A class that provides the shader node interface for a node port
class OutPort:
var node = null
var port = null
@ -130,7 +131,7 @@ func get_shader_code(uv, slot = 0):
func get_textures():
var list = {}
for i in range(5):
for i in range(get_connection_input_count()):
var source = get_source(i)
if source != null:
var source_list = source.get_textures()
@ -149,17 +150,13 @@ func deserialize_element(e):
return Color(e.r, e.g, e.b, e.a)
return e
func generate_shader(slot = 0):
func do_generate_shader(src_code):
var code
code = "shader_type canvas_item;\n\n"
code = "shader_type canvas_item;\n"
var file = File.new()
file.open("res://addons/procedural_material/common.shader", File.READ)
code += file.get_as_text()
code += "\n"
for c in get_parent().get_children():
if c is GraphNode:
c.reset()
var src_code = get_shader_code("UV", slot)
var shader_code = src_code.defs
shader_code += "void fragment() {\n"
shader_code += src_code.code
@ -168,6 +165,13 @@ func generate_shader(slot = 0):
#print("GENERATED SHADER:\n"+shader_code)
code += shader_code
return code
func generate_shader(slot = 0):
# Reset all nodes
for c in get_parent().get_children():
if c is GraphNode:
c.reset()
return do_generate_shader(get_shader_code("UV", slot))
func serialize():
var type = get_script().resource_path
@ -189,13 +193,22 @@ func deserialize(data):
set(variable, value)
update_property_widgets()
# Render targets again for multipass filters
func rerender_targets():
for c in get_parent().get_connection_list():
if c.from == name:
var node = get_parent().get_node(c.to)
if node != null and node is GraphNode:
node._rerender()
func _rerender():
rerender_targets()
# Generic code for convolution nodes
func get_shader_code_convolution(convolution, uv):
func get_shader_code_convolution(src, convolution, uv):
var rv = { defs="", code="" }
var src = get_source()
if src == null:
return rv
var variant_index = generated_variants.find(uv)
var need_defs = false
if generated_variants.empty():

View File

@ -4,10 +4,69 @@ extends "res://addons/procedural_material/node_base.gd"
var sigma = 1.0
var epsilon = 0.005
var input_shader = ""
var input_texture
var mid_texture
var final_texture
func _ready():
initialize_properties([ $HBoxContainer1/sigma, $HBoxContainer2/epsilon ])
input_texture = ImageTexture.new()
mid_texture = ImageTexture.new()
final_texture = ImageTexture.new()
initialize_properties([ $HBoxContainer1/epsilon, $HBoxContainer2/sigma ])
func get_gaussian_blur_shader(v):
var shader_code
var kernel_size = 10
var kernel = []
kernel.resize(2*kernel_size+1)
shader_code = "shader_type canvas_item;\n"
shader_code += "uniform sampler2D input_tex;\n"
shader_code += "void fragment() {\n"
shader_code += "vec3 color = vec3(0.0);"
var sum = 0
for x in range(-kernel_size, kernel_size+1):
var coef = exp(-0.5*(pow((x)/sigma, 2.0))) / (2.0*PI*sigma*sigma)
kernel[x+kernel_size] = coef
sum += coef
for x in range(-kernel_size, kernel_size+1):
shader_code += "color += %.9f*textureLod(input_tex, UV+vec2(%.9f, %.9f), %.9f).rgb;\n" % [ kernel[x+kernel_size] / sum, x*v.x, x*v.y, epsilon ]
shader_code += "COLOR = vec4(color, 1.0);\n"
shader_code += "}\n"
return shader_code;
func _rerender():
get_parent().precalculate_shader(input_shader, get_source().get_textures(), 4096, input_texture, self, "pass_1", [])
func pass_1():
get_parent().precalculate_shader(get_gaussian_blur_shader(Vector2(epsilon, 0)), { input=input_texture}, 4096, mid_texture, self, "pass_2", [])
func pass_2():
get_parent().precalculate_shader(get_gaussian_blur_shader(Vector2(0, epsilon)), { input=mid_texture}, 4096, final_texture, self, "rerender_targets", [])
func get_textures():
var list = {}
list[name] = final_texture
return list
func _get_shader_code(uv):
var rv = { defs="", code="" }
var src = get_source()
if src == null:
return rv
input_shader = do_generate_shader(src.get_shader_code("UV"))
_rerender()
if generated_variants.empty():
rv.defs = "uniform sampler2D "+name+"_tex;\n"
var variant_index = generated_variants.find(uv)
if variant_index == -1:
variant_index = generated_variants.size()
generated_variants.append(uv)
rv.code = "vec3 "+name+"_"+str(variant_index)+"_rgb = texture("+name+"_tex, "+uv+").rgb;\n"
rv.rgb = name+"_"+str(variant_index)+"_rgb"
return rv
func __get_shader_code(uv):
var convolution = {
kernel=[
0, 0, 0, 0, 0,

View File

@ -5,7 +5,7 @@
[sub_resource type="Theme" id=1]
[node name="Blur" type="GraphNode" index="0"]
[node name="Blur" type="GraphNode"]
anchor_left = 0.0
anchor_top = 0.0
@ -77,13 +77,13 @@ mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 3
size_flags_vertical = 4
text = "Sigma:"
text = "Grid:"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
_sections_unfolded = [ "Size Flags" ]
[node name="sigma" type="SpinBox" parent="HBoxContainer1" index="1"]
[node name="epsilon" type="SpinBox" parent="HBoxContainer1" index="1"]
anchor_left = 0.0
anchor_top = 0.0
@ -98,11 +98,11 @@ mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
min_value = 0.05
max_value = 5.0
step = 0.05
min_value = 0.001
max_value = 0.01
step = 0.001
page = 0.0
value = 1.0
value = 0.005
exp_edit = false
rounded = false
editable = true
@ -143,13 +143,13 @@ mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 3
size_flags_vertical = 4
text = "Grid:"
text = "Sigma:"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
_sections_unfolded = [ "Size Flags" ]
[node name="epsilon" type="SpinBox" parent="HBoxContainer2" index="1"]
[node name="sigma" type="SpinBox" parent="HBoxContainer2" index="1"]
anchor_left = 0.0
anchor_top = 0.0
@ -164,11 +164,11 @@ mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
min_value = 0.001
max_value = 0.01
step = 0.001
min_value = 0.05
max_value = 20.0
step = 0.05
page = 0.0
value = 0.005
value = 1.0
exp_edit = false
rounded = false
editable = true

View File

@ -7,29 +7,39 @@ var roughness
var emission_energy
var normal_scale
var ao_light_affect
var depth_scale
var depth_scale
var texture_albedo = null
var texture_metallic = null
var texture_roughness = null
var texture_emission = null
var texture_normal_map = null
var texture_ambient_occlusion = null
var texture_depth_map = null
var current_material_list = []
var generated_textures = {}
const TEXTURE_LIST = [
{ port= 0, texture= "albedo" },
{ port= 1, texture= "metallic" },
{ port= 2, texture= "roughness" },
{ port= 3, texture= "emission" },
{ port= 4, texture= "normal_map" },
{ port= 5, texture= "ambient_occlusion" },
{ port= 6, texture= "depth_map" }
{ port=0, texture="albedo" },
{ port=1, texture="metallic" },
{ port=2, texture="roughness" },
{ port=3, texture="emission" },
{ port=4, texture="normal_map" },
{ port=5, texture="ambient_occlusion" },
{ port=6, texture="depth_map" }
]
func _ready():
for t in TEXTURE_LIST:
generated_textures[t.texture] = { shader=null, source=null, texture=null}
initialize_properties([ $Albedo/albedo_color, $Metallic/metallic, $Roughness/roughness, $Emission/emission_energy, $NormalMap/normal_scale, $AmbientOcclusion/ao_light_affect, $DepthMap/depth_scale ])
func _rerender():
var has_textures = false
for t in TEXTURE_LIST:
var shader = generated_textures[t.texture].shader
if shader != null:
var input_textures = null
if generated_textures[t.texture].source != null:
input_textures = generated_textures[t.texture].source.get_textures()
get_parent().precalculate_shader(shader, input_textures, 1024, generated_textures[t.texture].texture, self, "do_update_materials", [ current_material_list ])
has_textures = true
if !has_textures:
do_update_materials(current_material_list)
func _get_shader_code(uv):
var rv = { defs="", code="", f="0.0" }
var src = get_source()
@ -37,55 +47,53 @@ func _get_shader_code(uv):
rv = src.get_shader_code(uv)
return rv
func _get_state_variables():
return [ ]
func update_materials(material_list):
current_material_list = material_list
var has_textures = false
for t in TEXTURE_LIST:
var source = get_source(t.port)
if source == null:
set("texture_"+t.texture, null)
generated_textures[t.texture].shader = null
generated_textures[t.texture].source = null
if generated_textures[t.texture].texture != null:
generated_textures[t.texture].texture = null
else:
get_parent().precalculate_texture(source, 1024, self, "store_texture", [ t.texture, material_list ])
has_textures = true
if !has_textures:
do_update_materials(material_list)
func store_texture(texture_name, material_list, texture):
set("texture_"+texture_name, texture)
do_update_materials(material_list)
generated_textures[t.texture].shader = source.generate_shader()
generated_textures[t.texture].source = source
if generated_textures[t.texture].texture == null:
generated_textures[t.texture].texture = ImageTexture.new()
_rerender()
func do_update_materials(material_list):
for m in material_list:
if m is SpatialMaterial:
m.albedo_color = albedo_color
m.albedo_texture = texture_albedo
m.albedo_texture = generated_textures.albedo.texture
m.metallic = metallic
m.metallic_texture = texture_metallic
m.metallic_texture = generated_textures.metallic.texture
m.roughness = roughness
m.roughness_texture = texture_roughness
if texture_emission != null:
m.roughness_texture = generated_textures.roughness.texture
if generated_textures.emission.texture != null:
m.emission_enabled = true
m.emission_energy = emission_energy
m.emission_texture = texture_emission
m.emission_texture = generated_textures.emission.texture
else:
m.emission_enabled = false
if texture_normal_map != null:
if generated_textures.normal_map.texture != null:
m.normal_enabled = true
m.normal_texture = texture_normal_map
m.normal_texture = generated_textures.normal_map.texture
else:
m.normal_enabled = false
if texture_ambient_occlusion != null:
if generated_textures.ambient_occlusion.texture != null:
m.ao_enabled = true
m.ao_light_affect = ao_light_affect
m.ao_texture = texture_ambient_occlusion
m.ao_texture = generated_textures.ambient_occlusion.texture
else:
m.ao_enabled = false
if texture_depth_map != null:
if generated_textures.depth_map.texture != null:
m.depth_enabled = true
m.depth_scale = depth_scale
m.depth_texture = texture_depth_map
m.depth_texture = generated_textures.depth_map.texture
else:
m.depth_enabled = false

View File

@ -5,7 +5,7 @@
[sub_resource type="Theme" id=1]
[node name="Material" type="GraphNode" index="0"]
[node name="Material" type="GraphNode"]
anchor_left = 0.0
anchor_top = 0.0

View File

@ -23,6 +23,9 @@ func _ready():
initialize_properties([ $amount ])
func _get_shader_code(uv):
var src = get_source()
if src == null:
return { defs="", code="" }
var convolution = CONVOLUTION
convolution.scale_before_normalize = amount
return get_shader_code_convolution(convolution, uv)
return get_shader_code_convolution(src, convolution, uv)

View File

@ -7,7 +7,7 @@ var x_scale = 4.0
var y_wave = 0
var y_scale = 4.0
const WAVE_FCT = [ "wave_sin", "wave_triangle", "wave_square", "fract" ]
const WAVE_FCT = [ "wave_sin", "wave_triangle", "wave_square", "fract", "wave_constant" ]
const MIX_FCT = [ "mix_multiply", "mix_add", "mix_max", "mix_min", "mix_xor", "mix_pow" ]
func _ready():

View File

@ -5,7 +5,7 @@
[sub_resource type="Theme" id=1]
[node name="Pattern" type="GraphNode"]
[node name="Pattern" type="GraphNode" index="0"]
anchor_left = 0.0
anchor_top = 0.0
@ -181,7 +181,7 @@ group = null
text = "Sine"
flat = false
align = 0
items = [ "Sine", null, false, 0, null, "Triangle", null, false, 1, null, "Square", null, false, 2, null, "Sawtooth", null, false, 3, null ]
items = [ "Sine", null, false, 0, null, "Triangle", null, false, 1, null, "Square", null, false, 2, null, "Sawtooth", null, false, 3, null, "Constant", null, false, 4, null ]
selected = 0
_sections_unfolded = [ "Rect" ]
@ -276,7 +276,7 @@ group = null
text = "Sine"
flat = false
align = 0
items = [ "Sine", null, false, 0, null, "Triangle", null, false, 1, null, "Square", null, false, 2, null, "Sawtooth", null, false, 3, null ]
items = [ "Sine", null, false, 0, null, "Triangle", null, false, 1, null, "Square", null, false, 2, null, "Sawtooth", null, false, 3, null, "Constant", null, false, 4, null ]
selected = 0
_sections_unfolded = [ "Rect" ]

View File

@ -8,9 +8,12 @@ const ENVIRONMENTS = [
]
func _ready():
var m = $MaterialPreview/Objects/Cube.get_surface_material(0).duplicate()
var m
m = $MaterialPreview/Objects/Cube.get_surface_material(0).duplicate()
$MaterialPreview/Objects/Cube.set_surface_material(0, m)
$MaterialPreview/Objects/Cylinder.set_surface_material(0, m)
m = $MaterialPreview/Objects/Sphere.get_surface_material(0).duplicate()
$MaterialPreview/Objects/Sphere.set_surface_material(0, m)
$ObjectRotate.play("rotate")
$Preview2D.material = $Preview2D.material.duplicate(true)
_on_Environment_item_selected($Config/Environment.selected)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 325 KiB

View File

@ -1 +1 @@
{"connections":[{"from":"voronoi_0","from_port":0,"to":"blend_0","to_port":0},{"from":"voronoi_0","from_port":1,"to":"blend_0","to_port":1},{"from":"perlin_0","from_port":0,"to":"blend_0","to_port":2},{"from":"blend_0","from_port":0,"to":"colorize_0","to_port":0},{"from":"colorize_0","from_port":0,"to":"Material","to_port":0},{"from":"normal_map_0","from_port":0,"to":"Material","to_port":4},{"from":"perlin_0","from_port":0,"to":"colorize_1","to_port":0},{"from":"colorize_1","from_port":0,"to":"Material","to_port":1},{"from":"perlin_0","from_port":0,"to":"colorize_2","to_port":0},{"from":"colorize_2","from_port":0,"to":"Material","to_port":2},{"from":"perlin_1","from_port":0,"to":"warp_0","to_port":1},{"from":"voronoi_1","from_port":1,"to":"warp_0","to_port":0},{"from":"warp_0","from_port":0,"to":"normal_map_0","to_port":0},{"from":"warp_0","from_port":0,"to":"colorize_3","to_port":0},{"from":"colorize_3","from_port":0,"to":"Material","to_port":5}],"nodes":[{"amount":0.5,"name":"normal_map_0","node_position":{"x":531,"y":432},"type":"normal_map"},{"gradient":[{"b":0,"g":0,"pos":0,"r":0},{"b":0.260417,"g":0.260417,"pos":1,"r":0.260417}],"name":"colorize_1","node_position":{"x":533,"y":343},"type":"colorize"},{"amount":0.5,"blend_type":0,"name":"blend_0","node_position":{"x":327,"y":411},"type":"blend"},{"gradient":[{"b":0.391927,"g":0.523519,"pos":0,"r":0.583333},{"b":0.240885,"g":0.276693,"pos":0.345455,"r":0.3125},{"b":0.391927,"g":0.523519,"pos":0.654545,"r":0.583333},{"b":0.240885,"g":0.276693,"pos":0.945455,"r":0.3125}],"name":"colorize_0","node_position":{"x":530,"y":171},"type":"colorize"},{"gradient":[{"b":0.364583,"g":0.364583,"pos":0,"r":0.364583},{"b":1,"g":1,"pos":1,"r":1}],"name":"colorize_2","node_position":{"x":526,"y":258},"type":"colorize"},{"intensity":1,"name":"voronoi_0","node_position":{"x":117,"y":448},"scale_x":4,"scale_y":4,"type":"voronoi"},{"iterations":6,"name":"perlin_0","node_position":{"x":105,"y":305},"persistence":0.85,"scale_x":4,"scale_y":4,"type":"perlin"},{"iterations":3,"name":"perlin_1","node_position":{"x":102,"y":166},"persistence":0.65,"scale_x":4,"scale_y":4,"type":"perlin"},{"intensity":0.85,"name":"voronoi_1","node_position":{"x":115,"y":63},"scale_x":4,"scale_y":4,"type":"voronoi"},{"name":"Material","node_position":{"x":768,"y":239},"type":"material"},{"amount":0.3,"name":"warp_0","node_position":{"x":317,"y":139},"type":"warp"},{"gradient":[{"b":1,"g":1,"pos":0.127273,"r":1},{"b":0,"g":0,"pos":0.236364,"r":0}],"name":"colorize_3","node_position":{"x":327,"y":238},"type":"colorize"}]}
{"connections":[{"from":"voronoi_0","from_port":0,"to":"blend_0","to_port":0},{"from":"voronoi_0","from_port":1,"to":"blend_0","to_port":1},{"from":"perlin_0","from_port":0,"to":"blend_0","to_port":2},{"from":"blend_0","from_port":0,"to":"colorize_0","to_port":0},{"from":"colorize_0","from_port":0,"to":"Material","to_port":0},{"from":"normal_map_0","from_port":0,"to":"Material","to_port":4},{"from":"perlin_0","from_port":0,"to":"colorize_1","to_port":0},{"from":"colorize_1","from_port":0,"to":"Material","to_port":1},{"from":"perlin_0","from_port":0,"to":"colorize_2","to_port":0},{"from":"colorize_2","from_port":0,"to":"Material","to_port":2},{"from":"perlin_1","from_port":0,"to":"warp_0","to_port":1},{"from":"voronoi_1","from_port":1,"to":"warp_0","to_port":0},{"from":"warp_0","from_port":0,"to":"normal_map_0","to_port":0}],"nodes":[{"amount":0.5,"name":"normal_map_0","node_position":{"x":531,"y":432},"type":"normal_map"},{"gradient":[{"b":0,"g":0,"pos":0,"r":0},{"b":0.260417,"g":0.260417,"pos":1,"r":0.260417}],"name":"colorize_1","node_position":{"x":533,"y":343},"type":"colorize"},{"gradient":[{"b":0.391927,"g":0.523519,"pos":0,"r":0.583333},{"b":0.240885,"g":0.276693,"pos":0.345455,"r":0.3125},{"b":0.391927,"g":0.523519,"pos":0.645455,"r":0.583333},{"b":0.240885,"g":0.276693,"pos":0.945455,"r":0.3125}],"name":"colorize_0","node_position":{"x":530,"y":171},"type":"colorize"},{"gradient":[{"b":0.364583,"g":0.364583,"pos":0,"r":0.364583},{"b":1,"g":1,"pos":1,"r":1}],"name":"colorize_2","node_position":{"x":526,"y":258},"type":"colorize"},{"iterations":6,"name":"perlin_0","node_position":{"x":105,"y":305},"persistence":0.85,"scale_x":4,"scale_y":4,"type":"perlin"},{"iterations":3,"name":"perlin_1","node_position":{"x":102,"y":166},"persistence":0.65,"scale_x":4,"scale_y":4,"type":"perlin"},{"intensity":0.85,"name":"voronoi_1","node_position":{"x":115,"y":63},"scale_x":4,"scale_y":4,"type":"voronoi"},{"albedo_color":{"a":1,"b":1,"g":1,"r":1,"type":"Color"},"ao_light_affect":1,"depth_scale":1,"emission_energy":1,"metallic":1,"name":"Material","node_position":{"x":768,"y":239},"normal_scale":1,"roughness":1,"type":"material"},{"amount":0.3,"name":"warp_0","node_position":{"x":317,"y":139},"type":"warp"},{"intensity":1,"name":"voronoi_0","node_position":{"x":117,"y":448},"scale_x":4,"scale_y":4,"type":"voronoi"},{"amount":0.5,"blend_type":0,"name":"blend_0","node_position":{"x":327,"y":411},"type":"blend"}]}