diff --git a/addons/material_maker/engine/gen_base.gd b/addons/material_maker/engine/gen_base.gd index e665262..17793a5 100644 --- a/addons/material_maker/engine/gen_base.gd +++ b/addons/material_maker/engine/gen_base.gd @@ -53,6 +53,9 @@ func toggle_editable() -> bool: func is_template() -> bool: return model != null +func get_template_name() -> bool: + return model + func is_editable() -> bool: return false diff --git a/addons/material_maker/nodes/tones.mmg b/addons/material_maker/nodes/tones.mmg new file mode 100644 index 0000000..be161f0 --- /dev/null +++ b/addons/material_maker/nodes/tones.mmg @@ -0,0 +1,122 @@ +{ + "name": "levels", + "node_position": { + "x": 0, + "y": 0 + }, + "parameters": { + "in_max": { + "a": 1, + "b": 1, + "g": 1, + "r": 1, + "type": "Color" + }, + "in_mid": { + "a": 0.5, + "b": 0.5, + "g": 0.5, + "r": 0.5, + "type": "Color" + }, + "in_min": { + "a": 0, + "b": 0, + "g": 0, + "r": 0, + "type": "Color" + }, + "out_max": { + "a": 1, + "b": 1, + "g": 1, + "r": 1, + "type": "Color" + }, + "out_min": { + "a": 0, + "b": 0, + "g": 0, + "r": 0, + "type": "Color" + } + }, + "shader_model": { + "code": "", + "global": "vec4 adjust_levels(vec4 input, vec4 in_min, vec4 in_mid, vec4 in_max, vec4 out_min, vec4 out_max) {\n\tinput = clamp((input-in_min)/(in_max-in_min), 0.0, 1.0);\n\tin_mid = (in_mid-in_min)/(in_max-in_min);\n\tvec4 dark = step(in_mid, input);\n\tinput = 0.5*mix(input/(in_mid), 1.0+(input-in_mid)/(1.0-in_mid), dark);\n\treturn out_min+input*(out_max-out_min);\n}\n", + "inputs": [ + { + "default": "vec4(1.0)", + "label": "", + "name": "input", + "type": "rgba" + } + ], + "instance": "", + "name": "Levels", + "outputs": [ + { + "rgba": "adjust_levels($input($uv), $in_min, $in_mid, $in_max, $out_min, $out_max)", + "type": "rgba" + } + ], + "parameters": [ + { + "default": { + "a": 0, + "b": 0, + "g": 0, + "r": 0 + }, + "label": "", + "name": "in_min", + "type": "color" + }, + { + "default": { + "a": 0.498039, + "b": 0.498039, + "g": 0.498039, + "r": 0.498039 + }, + "label": "", + "name": "in_mid", + "type": "color" + }, + { + "default": { + "a": 1, + "b": 1, + "g": 1, + "r": 1 + }, + "label": "", + "name": "in_max", + "type": "color" + }, + { + "default": { + "a": 1, + "b": 0, + "g": 0, + "r": 0 + }, + "label": "", + "name": "out_min", + "type": "color" + }, + { + "default": { + "a": 1, + "b": 1, + "g": 1, + "r": 1 + }, + "label": "", + "name": "out_max", + "type": "color" + } + ] + }, + "type": "shader" +} \ No newline at end of file diff --git a/material_maker/doc/images/node_filter_tones.png b/material_maker/doc/images/node_filter_tones.png new file mode 100644 index 0000000..a2acbf2 Binary files /dev/null and b/material_maker/doc/images/node_filter_tones.png differ diff --git a/material_maker/doc/node_filter_tones.rst b/material_maker/doc/node_filter_tones.rst new file mode 100644 index 0000000..e4cbb26 --- /dev/null +++ b/material_maker/doc/node_filter_tones.rst @@ -0,0 +1,38 @@ +Tones node +~~~~~~~~~~ + +The **Tones** node provides an easy interface to adjust the tones of its input. It can be applied +homogeneously to the R, G and B channels, or separately on R, G, B and A. + +The nodes shows an histogram of all channels of its input, and 3 cursors to modify the input +adjustment at the top, and 2 cursors to modify the output adjustment. + +.. image:: images/node_filter_tones.png + :align: center + +Inputs +++++++ + +The **Tones** node requires an RGBA input texture. + +Outputs ++++++++ + +The **Tones** node provides a single RGBA texture. + +Parameters +++++++++++ + +At the top of the node, a control can be used to select the active channel (Luminance, Red, +Green, Blue and Alpha). + +The button can be used to adjust automatically the Tones to the node's input to obtain better +contrast. + +The 3 cursors at the top of the histogram can be used to select the input colors that will be +remapped to black (value = 0 for single channel), mid-grey (value = 0.5) and white (value = 1). +Values between those defined by cursors are interpolated linearly, and all values are clamped +between 0 and 1. + +The 2 bottom cursors define the output color for black (value = 0 for single channel) and +white (value = 1). \ No newline at end of file diff --git a/material_maker/doc/nodes_filter.rst b/material_maker/doc/nodes_filter.rst index 06d11e9..1d04094 100644 --- a/material_maker/doc/nodes_filter.rst +++ b/material_maker/doc/nodes_filter.rst @@ -9,6 +9,7 @@ The filter nodes accept one or several inputs and generate one or several images node_filter_invert node_filter_brightness_contrast node_filter_adjust_hsv + node_filter_tones node_filter_greyscale node_filter_colorize node_filter_combine diff --git a/material_maker/graph_edit.gd b/material_maker/graph_edit.gd index 4c2e658..ba166a1 100644 --- a/material_maker/graph_edit.gd +++ b/material_maker/graph_edit.gd @@ -166,7 +166,7 @@ func clear_material() -> void: func update_graph(generators, connections) -> Array: var rv = [] for g in generators: - var node = node_factory.create_node(g.get_type()) + var node = node_factory.create_node(g.get_template_name() if g.is_template() else "", g.get_type()) if node != null: node.name = "node_"+g.name add_node(node) diff --git a/material_maker/icons/icons.svg b/material_maker/icons/icons.svg index ab74fb5..f4a511c 100644 --- a/material_maker/icons/icons.svg +++ b/material_maker/icons/icons.svg @@ -55,9 +55,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="40.459032" - inkscape:cx="12.31719" - inkscape:cy="41.781829" + inkscape:zoom="7.152214" + inkscape:cx="122.70346" + inkscape:cy="36.247815" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" @@ -605,5 +605,42 @@ sodipodi:end="0.0018129957" sodipodi:open="true" d="m 15.411452,304.10377 a 7.4148879,7.4346347 0 0 1 -7.4502054,7.39166 7.4148879,7.4346347 0 0 1 -7.37939334,-7.46273 7.4148879,7.4346347 0 0 1 7.43561054,-7.40642 7.4148879,7.4346347 0 0 1 7.3941002,7.44808" /> + + + + + diff --git a/material_maker/library/base.json b/material_maker/library/base.json index a2962cc..b792392 100644 --- a/material_maker/library/base.json +++ b/material_maker/library/base.json @@ -2289,6 +2289,50 @@ "tree_item": "Filter/AdjustHSV", "type": "adjust_hsv" }, + { + "collapsed": true, + "icon": "filter_tones", + "name": "tones", + "parameters": { + "in_max": { + "a": 1, + "b": 1, + "g": 1, + "r": 1, + "type": "Color" + }, + "in_mid": { + "a": 0.5, + "b": 0.5, + "g": 0.5, + "r": 0.5, + "type": "Color" + }, + "in_min": { + "a": 0, + "b": 0, + "g": 0, + "r": 0, + "type": "Color" + }, + "out_max": { + "a": 1, + "b": 1, + "g": 1, + "r": 1, + "type": "Color" + }, + "out_min": { + "a": 0, + "b": 0, + "g": 0, + "r": 0, + "type": "Color" + } + }, + "tree_item": "Filter/Tones", + "type": "tones" + }, { "collapsed": true, "icon": "filter_greyscale", diff --git a/material_maker/library/base/filter_tones.png b/material_maker/library/base/filter_tones.png new file mode 100644 index 0000000..b2809d0 Binary files /dev/null and b/material_maker/library/base/filter_tones.png differ diff --git a/material_maker/main_window.gd b/material_maker/main_window.gd index 0434639..be7af06 100644 --- a/material_maker/main_window.gd +++ b/material_maker/main_window.gd @@ -462,7 +462,7 @@ func make_selected_nodes_editable() -> void: var selected_nodes = get_selected_nodes() if !selected_nodes.empty(): for n in selected_nodes: - if n.generator.toggle_editable(): + if n.generator.toggle_editable() and n.has_method("update_node"): n.update_node() func add_to_user_library() -> void: diff --git a/material_maker/node_factory.gd b/material_maker/node_factory.gd index 12a908e..7548ff4 100644 --- a/material_maker/node_factory.gd +++ b/material_maker/node_factory.gd @@ -2,13 +2,16 @@ extends Node var includes -func create_node(type) -> Node: +func create_node(model : String, type : String) -> Node: + var node_type = null var node = null - var file_name = "res://material_maker/nodes/"+type+".tscn" + var file_name = "res://material_maker/nodes/"+model+".tscn" + if ! ResourceLoader.exists(file_name): + file_name = "res://material_maker/nodes/"+type+".tscn" if ResourceLoader.exists(file_name): - var node_type = load(file_name) - if node_type != null: - node = node_type.instance() + node_type = load(file_name) + if node_type != null: + node = node_type.instance() if node == null: node = preload("res://material_maker/nodes/generic.tscn").instance() return node diff --git a/material_maker/nodes/tones.gd b/material_maker/nodes/tones.gd new file mode 100644 index 0000000..aff0e33 --- /dev/null +++ b/material_maker/nodes/tones.gd @@ -0,0 +1,170 @@ +extends MMGraphNodeBase + +class Cursor: + extends Control + + var color : Color + var top : bool = true + var position : float + + const WIDTH : int = 8 + const HEIGHT : int = 8 + + func _init(c, p, t = true): + color = c + position = p + top = t + + func _ready() -> void: + rect_position.y = -2 if top else get_parent().rect_size.y+2-HEIGHT + set_value(position) + rect_size = Vector2(WIDTH, HEIGHT) + + func _draw() -> void: + var polygon : PoolVector2Array + if top: + polygon = PoolVector2Array([Vector2(0, 0), Vector2(WIDTH/2, HEIGHT), Vector2(WIDTH, 0), Vector2(0, 0)]) + else: + polygon = PoolVector2Array([Vector2(0, HEIGHT), Vector2(WIDTH/2, 0), Vector2(WIDTH, HEIGHT), Vector2(0, HEIGHT)]) + var c = color + c.a = 1.0 + draw_colored_polygon(polygon, c) + var outline_color = 0.0 if position > 0.5 else 1.0 + draw_polyline(polygon, Color(outline_color, outline_color, outline_color), 1.0, true) + + func _gui_input(ev) -> void: + if ev is InputEventMouseMotion && (ev.button_mask & 1) != 0: + rect_position.x += ev.relative.x + rect_position.x = min(max(-0.5*WIDTH, rect_position.x), get_parent().rect_size.x-0.5*WIDTH) + update_value((rect_position.x+0.5*WIDTH)/get_parent().rect_size.x) + + func update_value(p : float) -> void: + if p != position: + set_value(p) + get_parent().get_parent().update_value(self, position) + update() + + func set_value(v : float): + position = v + rect_position.x = position * get_parent().rect_size.x - 0.5*WIDTH + +var cursor_in_min : Cursor +var cursor_in_mid : Cursor +var cursor_in_max : Cursor +var cursor_out_min : Cursor +var cursor_out_max : Cursor + +func _ready() -> void: + var slot_color = mm_io_types.types["rgba"].color + var slot_type = mm_io_types.types["rgba"].slot_type + set_slot(0, true, slot_type, slot_color, true, slot_type, slot_color) + cursor_in_min = Cursor.new(Color(0.0, 0.0, 0.0), 0.0) + $Histogram.add_child(cursor_in_min) + cursor_in_mid = Cursor.new(Color(0.5, 0.5, 0.5), 0.5) + $Histogram.add_child(cursor_in_mid) + cursor_in_max = Cursor.new(Color(1.0, 1.0, 1.0), 1.0) + $Histogram.add_child(cursor_in_max) + cursor_out_min = Cursor.new(Color(0.0, 0.0, 0.0), 0.0, false) + $Histogram.add_child(cursor_out_min) + cursor_out_max = Cursor.new(Color(1.0, 1.0, 1.0), 1.0, false) + $Histogram.add_child(cursor_out_max) + +func set_generator(g) -> void: + .set_generator(g) + generator.connect("parameter_changed", self, "on_parameter_changed") + _on_Mode_item_selected(0) + +func on_parameter_changed(p, v) -> void: + if p == "__input_changed__": + var source = generator.get_source(0) + var result = source.generator.render(source.output_index, 128, true) + while result is GDScriptFunctionState: + result = yield(result, "completed") + result.copy_to_texture($Histogram.get_image_texture()) + result.release() + +func get_parameter(n : String) -> float: + var value = generator.get_parameter(n) + match $Bar/Mode.selected: + 1: + return value.r + 2: + return value.g + 3: + return value.b + 4: + return value.a + return (value.r+value.g+value.b)/3.0 + +func _on_Mode_item_selected(_id): + cursor_in_min.set_value(get_parameter("in_min")) + cursor_in_mid.set_value(get_parameter("in_mid")) + cursor_in_max.set_value(get_parameter("in_max")) + cursor_out_min.set_value(get_parameter("out_min")) + cursor_out_max.set_value(get_parameter("out_max")) + +func set_parameter(n : String, v : float, d : float) -> void: + var value = generator.get_parameter(n) + match $Bar/Mode.selected: + 0: + value.r = v + value.g = v + value.b = v + value.a = d + 1: + value.r = v + 2: + value.g = v + 3: + value.b = v + 4: + value.a = v + generator.set_parameter(n, value) + +func update_value(control : Cursor, value : float) -> void: + match control: + cursor_in_min: + set_parameter("in_min", value, 0) + cursor_in_mid: + set_parameter("in_mid", value, 0.5) + cursor_in_max: + set_parameter("in_max", value, 1) + cursor_out_min: + set_parameter("out_min", value, 0) + cursor_out_max: + set_parameter("out_max", value, 1) + get_parent().send_changed_signal() + +func _on_Auto_pressed(): + var histogram = $Histogram.get_histogram_texture().get_data() + histogram.lock() + var in_min : int = -1 + var in_mid : int = -1 + var in_mid_value : float = 0 + var in_max : int = -1 + var histogram_size = histogram.get_size().x + for i in range(histogram_size): + var color : Color = histogram.get_pixel(i, 0) + var value : float + match $Bar/Mode.selected: + 0: + value = (color.r+color.g+color.b)/3.0 + 1: + value = color.r + 2: + value = color.g + 3: + value = color.b + 4: + value = color.a + if value > 0.0: + if in_min == -1: + in_min = i + in_max = i + if in_mid_value < value: + in_mid = i + in_mid_value = value + histogram.unlock() + cursor_in_min.update_value(in_min/(histogram_size-1)) + cursor_in_mid.update_value(in_mid/(histogram_size-1)) + cursor_in_max.update_value(in_max/(histogram_size-1)) diff --git a/material_maker/nodes/tones.tscn b/material_maker/nodes/tones.tscn new file mode 100644 index 0000000..bd5f634 --- /dev/null +++ b/material_maker/nodes/tones.tscn @@ -0,0 +1,103 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://material_maker/nodes/tones.gd" type="Script" id=1] +[ext_resource path="res://material_maker/widgets/histogram/histogram.tscn" type="PackedScene" id=2] +[ext_resource path="res://material_maker/icons/icons.svg" type="Texture" id=3] + +[sub_resource type="AtlasTexture" id=1] +flags = 4 +atlas = ExtResource( 3 ) +region = Rect2( 16, 80, 16, 16 ) + +[node name="Tones" type="GraphNode"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 2.75074 +margin_top = 1.78928 +margin_right = -1053.25 +margin_bottom = -529.211 +title = "Tones" +show_close = true +slot/0/left_enabled = true +slot/0/left_type = 0 +slot/0/left_color = Color( 0, 0.396078, 1, 1 ) +slot/0/right_enabled = true +slot/0/right_type = 0 +slot/0/right_color = Color( 0, 0.415686, 1, 1 ) +slot/1/left_enabled = false +slot/1/left_type = 0 +slot/1/left_color = Color( 1, 1, 1, 1 ) +slot/1/right_enabled = false +slot/1/right_type = 0 +slot/1/right_color = Color( 1, 1, 1, 1 ) +slot/2/left_enabled = false +slot/2/left_type = 0 +slot/2/left_color = Color( 1, 1, 1, 1 ) +slot/2/right_enabled = false +slot/2/right_type = 0 +slot/2/right_color = Color( 1, 1, 1, 1 ) +slot/3/left_enabled = false +slot/3/left_type = 0 +slot/3/left_color = Color( 1, 1, 1, 1 ) +slot/3/right_enabled = false +slot/3/right_type = 0 +slot/3/right_color = Color( 1, 1, 1, 1 ) +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Bar" type="HBoxContainer" parent="."] +margin_left = 16.0 +margin_top = 24.0 +margin_right = 208.0 +margin_bottom = 44.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Mode" type="OptionButton" parent="Bar"] +margin_right = 172.0 +margin_bottom = 20.0 +size_flags_horizontal = 3 +text = "Luminance" +items = [ "Luminance", null, false, 0, null, "Red", null, false, 1, null, "Green", null, false, 2, null, "Blue", null, false, 3, null, "Alpha", null, false, 4, null ] +selected = 0 + +[node name="Auto" type="TextureButton" parent="Bar"] +margin_left = 176.0 +margin_top = 2.0 +margin_right = 192.0 +margin_bottom = 18.0 +hint_tooltip = "Set levels automatically" +size_flags_vertical = 4 +texture_normal = SubResource( 1 ) + +[node name="Spacer1" type="Control" parent="."] +margin_left = 16.0 +margin_top = 45.0 +margin_right = 208.0 +margin_bottom = 49.0 +rect_min_size = Vector2( 0, 4 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Histogram" parent="." instance=ExtResource( 2 )] +margin_left = 16.0 +margin_top = 50.0 +margin_right = 208.0 +margin_bottom = 178.0 +rect_min_size = Vector2( 192, 128 ) + +[node name="Spacer2" type="Control" parent="."] +margin_left = 16.0 +margin_top = 179.0 +margin_right = 208.0 +margin_bottom = 183.0 +rect_min_size = Vector2( 0, 4 ) +__meta__ = { +"_edit_use_anchors_": false +} +[connection signal="item_selected" from="Bar/Mode" to="." method="_on_Mode_item_selected"] +[connection signal="pressed" from="Bar/Auto" to="." method="_on_Auto_pressed"] diff --git a/material_maker/widgets/histogram/histogram.gd b/material_maker/widgets/histogram/histogram.gd new file mode 100644 index 0000000..d2ae909 --- /dev/null +++ b/material_maker/widgets/histogram/histogram.gd @@ -0,0 +1,10 @@ +extends Control + +func update_histogram() -> void: + pass + +func get_image_texture() -> ImageTexture: + return $ViewportImage/ColorRect.material.get_shader_param("tex") + +func get_histogram_texture() -> ImageTexture: + return $Control.material.get_shader_param("tex") diff --git a/material_maker/widgets/histogram/histogram.tscn b/material_maker/widgets/histogram/histogram.tscn new file mode 100644 index 0000000..02670ed --- /dev/null +++ b/material_maker/widgets/histogram/histogram.tscn @@ -0,0 +1,157 @@ +[gd_scene load_steps=14 format=2] + +[ext_resource path="res://material_maker/widgets/histogram/histogram.gd" type="Script" id=1] + +[sub_resource type="Shader" id=1] +resource_local_to_scene = true +code = "shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D tex; + +void fragment() { + COLOR = texture(tex, UV); +} +" + +[sub_resource type="ImageTexture" id=2] +resource_local_to_scene = true + +[sub_resource type="ShaderMaterial" id=3] +resource_local_to_scene = true +shader = SubResource( 1 ) +shader_param/tex = SubResource( 2 ) + +[sub_resource type="Shader" id=4] +code = "shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D tex; + +void fragment() { + vec4 sum = vec4(0.0); + for (int i = 0; i < 128; ++i) { + sum += step(abs(texture(tex, vec2(UV.x, float(i)/127.0))-UV.y), vec4(0.02)); + } + COLOR = sum/255.0; +} +" + +[sub_resource type="ViewportTexture" id=5] +viewport_path = NodePath("ViewportImage") + +[sub_resource type="ShaderMaterial" id=6] +resource_local_to_scene = true +shader = SubResource( 4 ) +shader_param/tex = SubResource( 5 ) + +[sub_resource type="Shader" id=7] +code = "shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D tex; + +void fragment() { + vec4 sum = vec4(0.0); + for (int i = 0; i < 128; ++i) { + sum += texture(tex, vec2(float(i)/127.0, UV.x)); + } + COLOR = sum/255.0; +}" + +[sub_resource type="ViewportTexture" id=8] +viewport_path = NodePath("ViewportHistogram1") + +[sub_resource type="ShaderMaterial" id=9] +resource_local_to_scene = true +shader = SubResource( 7 ) +shader_param/tex = SubResource( 8 ) + +[sub_resource type="Shader" id=10] +code = "shader_type canvas_item; + +uniform sampler2D tex; +render_mode blend_disabled; + +void fragment() { + if (abs(fract(UV.y+0.05)) < 0.1) { + COLOR = vec4(vec3(UV.x), 1.0); + } else { + vec4 highest = vec4(0.0); + for (int i = 0; i < 128; ++i) { + highest = max(highest, texture(tex, vec2(float(i)/128.0, 0.0))); + } + vec4 value = step(vec4(0.95-UV.y)*highest/0.9, texture(tex, vec2(UV.x, 0.0))); + float alpha = step(0.1, dot(value, vec4(1.0))); + COLOR = vec4(mix(value.rgb, vec3(0.5), 0.3*value.a), alpha); + } +}" + +[sub_resource type="ViewportTexture" id=11] +flags = 4 +viewport_path = NodePath("ViewportHistogram2") + +[sub_resource type="ShaderMaterial" id=12] +resource_local_to_scene = true +shader = SubResource( 10 ) +shader_param/tex = SubResource( 11 ) + +[node name="Histogram" type="Control"] +margin_right = 64.0 +margin_bottom = 64.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ViewportImage" type="Viewport" parent="."] +size = Vector2( 256, 256 ) +transparent_bg = true +hdr = false +usage = 0 +render_target_v_flip = true +render_target_update_mode = 3 + +[node name="ColorRect" type="ColorRect" parent="ViewportImage"] +material = SubResource( 3 ) +margin_right = 256.0 +margin_bottom = 128.0 +rect_min_size = Vector2( 256, 256 ) + +[node name="ViewportHistogram1" type="Viewport" parent="."] +size = Vector2( 128, 128 ) +own_world = true +transparent_bg = true +hdr = false +usage = 0 +render_target_v_flip = true +render_target_update_mode = 3 + +[node name="ColorRect" type="ColorRect" parent="ViewportHistogram1"] +material = SubResource( 6 ) +margin_right = 128.0 +margin_bottom = 128.0 +rect_min_size = Vector2( 128, 128 ) + +[node name="ViewportHistogram2" type="Viewport" parent="."] +size = Vector2( 128, 2 ) +transparent_bg = true +disable_3d = true +keep_3d_linear = true +usage = 0 +render_target_v_flip = true +render_target_update_mode = 3 + +[node name="ColorRect" type="ColorRect" parent="ViewportHistogram2"] +material = SubResource( 9 ) +margin_right = 128.0 +margin_bottom = 2.0 +rect_min_size = Vector2( 128, 2 ) + +[node name="Control" type="ColorRect" parent="."] +material = SubResource( 12 ) +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": false +}