function Node_Strand_Create(_x, _y, _group = noone) : Node(_x, _y, _group) constructor { name = "Strand Create"; color = COLORS.node_blend_strand; icon = THEME.strandSim; setDimension(96, 48);; update_on_frame = true; manual_ungroupable = false; newInput(0, nodeValue_Enum_Scroll("Type", self, 0, [ "Point", "Path", "Mesh" ])); newInput(1, nodeValue_Int("Density", self, 8, "How many strands to generate.")); newInput(2, nodeValue_Vec2("Length", self, [ 4, 4 ])); newInput(3, nodeValue_Int("Segment", self, 4)); newInput(4, nodeValue_Float("Elasticity", self, 0.05, "Length preservation, the higher the value the easier it is to stretch each segment.")) .setDisplay(VALUE_DISPLAY.slider); newInput(5, nodeValue_PathNode("Path", self, noone)); newInput(6, nodeValue_Vec2("Position", self, [ 0, 0 ])); newInput(7, nodeValue_Enum_Button("Side", self, 0, [ "Inner", "Outer", "Both" ])); newInput(8, nodeValue_Float("Spring", self, 0.8, "Angular stiffness, the higher the value the easier it is to bend each segment.")) .setDisplay(VALUE_DISPLAY.slider); newInput(9, nodeValue_Float("Structure", self, 0.2, "The ability to keep its original shape.")) .setDisplay(VALUE_DISPLAY.slider); newInput(10, nodeValue_Int("Seed", self, seed_random(6))) .setDisplay(VALUE_DISPLAY._default, { side_button : button(function() { randomize(); inputs[10].setValue(seed_random(6)); }).setIcon(THEME.icon_random, 0, COLORS._main_icon) }); newInput(11, nodeValue_Float("Curl frequency", self, 0)); newInput(12, nodeValue_Float("Curliness", self, 1)) .setDisplay(VALUE_DISPLAY.slider); newInput(13, nodeValue("Mesh", self, CONNECT_TYPE.input, VALUE_TYPE.mesh, noone)); newInput(14, nodeValue_Enum_Scroll("Distribution", self, 0, [ "Uniform", "Random" ])); newInput(15, nodeValue_Trigger("Bake hair", self, false, "Prevent strand reseting to apply manual modification. Unbaking will remove all changes.")) .setDisplay(VALUE_DISPLAY.button, { name: "Bake", UI : true, onClick: function() { attributes.use_groom = !attributes.use_groom; if(attributes.use_groom) groomed = strands.clone(); strandUpdate(true); } }); newInput(16, nodeValue_Bool("View fix hair", self, false)); newInput(17, nodeValue_Vec2("Root strength", self, [-1, -1])) .setTooltip("The force required to break strand from its root. Set to -1 to make strand infinitely strong."); outputs[0] = nodeValue_Output("Strand", self, VALUE_TYPE.strands, noone); input_display_list = [ 10, ["Generation", false], 0, 1, 5, 6, 7, 13, 14, 15, ["Strand", false], 2, 3, 4, 8, 9, 17, ["Curl", false], 11, 12, ["Preview", true], 16, ]; attributes.use_groom = false; groomed = new StrandMesh(); strands = new StrandMesh(); #region ---- tools ---- tool_push = new NodeTool( "Push", THEME.strand_push ) .addSetting("Radius", VALUE_TYPE.float, function(val) { tool_push.attribute.radius = val; }, "radius", 6) .addSetting("Strength", VALUE_TYPE.float, function(val) { tool_push.attribute.strength = val; }, "strength", 0.2) .addSetting("Falloff", VALUE_TYPE.float, function(val) { tool_push.attribute.fall = val; }, "fall", 0.1) .addSetting("Fix length", VALUE_TYPE.boolean, function() { tool_push.attribute.fix = !tool_push.attribute.fix; }, "fix", false) tool_comb = new NodeTool( "Comb", THEME.strand_comb ) .addSetting("Width", VALUE_TYPE.float, function(val) { tool_comb.attribute.width = val; }, "width", 8) .addSetting("Thick", VALUE_TYPE.float, function(val) { tool_comb.attribute.thick = val; }, "thick", 4) .addSetting("Strength", VALUE_TYPE.float, function(val) { tool_comb.attribute.strength = val; }, "strength", 0.75) tool_stretch = new NodeTool( "Stretch", THEME.strand_stretch ) .addSetting("Radius", VALUE_TYPE.float, function(val) { tool_stretch.attribute.radius = val; }, "radius", 6) .addSetting("Strength", VALUE_TYPE.float, function(val) { tool_stretch.attribute.strength = val; }, "strength", 0.5) .addSetting("Falloff", VALUE_TYPE.float, function(val) { tool_stretch.attribute.fall = val; }, "fall", 0.1) tool_cut = new NodeTool( "Shorten", THEME.strand_cut ) .addSetting("Radius", VALUE_TYPE.float, function(val) { tool_cut.attribute.radius = val; }, "radius", 6) .addSetting("Strength", VALUE_TYPE.float, function(val) { tool_cut.attribute.strength = val; }, "strength", 0.5) .addSetting("Falloff", VALUE_TYPE.float, function(val) { tool_cut.attribute.fall = val; }, "fall", 0.1) tool_grab = new NodeTool( "Grab", THEME.strand_grab ) .addSetting("Radius", VALUE_TYPE.float, function(val) { tool_grab.attribute.radius = val; }, "radius", 4) .addSetting("Strength", VALUE_TYPE.float, function(val) { tool_grab.attribute.strength = val; }, "strength", 1) .addSetting("Falloff", VALUE_TYPE.float, function(val) { tool_grab.attribute.fall = val; }, "fall", 0.2) groomTools = [ tool_push, tool_comb, tool_stretch, tool_cut, tool_grab, ]; tool_dragging = noone; tool_mx = 0; tool_my = 0; tool_dmx = 0; tool_dmy = 0; tool_dir = 0; tool_dir_fix = 0; tool_dir_to = 0; tool_grabbing = []; #endregion static drawOverlay = function(hover, active, _x, _y, _s, _mx, _my, _snx, _sny) { #region var _typ = getInputData(0); var _pre = getInputData(16); if(!attributes.use_groom) strands.draw(_x, _y, _s, _pre); tools = attributes.use_groom? groomTools : -1; if(_typ == 0) { if(tool_dragging == noone) inputs[6].drawOverlay(hover, active, _x, _y, _s, _mx, _my, _snx, _sny); } else if(_typ == 1) { var _pth = getInputData(5); var _sid = getInputData(7); if(!struct_has(_pth, "getPointRatio")) return; var lines = struct_has(_pth, "getLineCount")? _pth.getLineCount() : 1; draw_set_color(COLORS._main_accent); var ox, oy, nx, ny; for( var l = 0; l < lines; l++ ) for( var i = 0; i < 32; i++ ) { var _p = _pth.getPointRatio(i / 32, l); nx = _x + _p.x * _s; ny = _y + _p.y * _s; if(i) draw_line(ox, oy, nx, ny); ox = nx; oy = ny; } } else if(_typ == 2) { var _msh = getInputData(13); if(_msh == noone) return; draw_set_color(COLORS._main_accent); _msh.draw(_x, _y, _s); } if(!attributes.use_groom) return; groomed.draw(_x, _y, _s, _pre, true); var __mx = (_mx - _x) / _s; var __my = (_my - _y) / _s; if(tool_dmx != __mx || tool_dmy != __my) { tool_dir_to = point_direction(tool_dmx, tool_dmy, __mx, __my); tool_dir = lerp_angle(tool_dir, tool_dir_to, 10); } if(tool_dragging == tool_push) { var rad = tool_push.attribute.radius; var fall = tool_push.attribute.fall; var fix = tool_push.attribute.fix; var stn = tool_push.attribute.strength; var dx = __mx - tool_mx; var dy = __my - tool_my; if(dx != 0 || dy != 0) for( var i = 0, n = array_length(groomed.hairs); i < n; i++ ) { var h = groomed.hairs[i]; for( var j = 1; j < array_length(h.points); j++ ) { var p = h.points[j]; var d = point_distance(p.x, p.y, __mx, __my); if(d > rad * (1 + fall)) continue; var fl = 1 - clamp((d - rad * (1 + fall)) / (rad * fall * 2), 0, 1); p.x += dx * stn * fl; p.y += dy * stn * fl; } } tool_mx = __mx; tool_my = __my; if(mouse_release(mb_left)) { groomed.freeze(fix); tool_dragging = noone; } } else if(tool_dragging == tool_comb) { var wid = tool_comb.attribute.width; var thk = tool_comb.attribute.thick; var stn = tool_comb.attribute.strength; stn = power(stn, 2); var p0x = __mx + lengthdir_x(wid, tool_dir_fix + 90); var p0y = __my + lengthdir_y(wid, tool_dir_fix + 90); var p1x = __mx + lengthdir_x(wid, tool_dir_fix - 90); var p1y = __my + lengthdir_y(wid, tool_dir_fix - 90); if(tool_dmx != __mx || tool_dmy != __my) for( var i = 0, n = array_length(groomed.hairs); i < n; i++ ) { var h = groomed.hairs[i]; var op, np; for( var j = 0; j < array_length(h.points); j++ ) { np = h.points[j]; np.targetAngle = tool_dir_fix; var dst = distance_to_line(np.x, np.y, p0x, p0y, p1x, p1y); if(j) { var dir = np.storeAngle; var dis = np.storeDistance; var ang = dst < thk? lerp_angle_direct(dir, tool_dir_fix, lerp(stn, 1, (1 - dst) / thk)) : dir; np.storeAngle = ang; np.x = op.x + lengthdir_x(dis, np.storeAngle); np.y = op.y + lengthdir_y(dis, np.storeAngle); } op = np; } } if(mouse_release(mb_left)) { groomed.freeze(true); tool_dragging = noone; } } else if(tool_dragging == tool_stretch || tool_dragging == tool_cut) { var rad = tool_dragging.attribute.radius; var fall = tool_dragging.attribute.fall; var stn = tool_dragging.attribute.strength; stn = tool_dragging == tool_stretch? stn / game_get_speed(gamespeed_fps) : stn / 10; for( var i = 0, n = array_length(groomed.hairs); i < n; i++ ) { var h = groomed.hairs[i]; var op, np; var amo = array_length(h.points); var l = []; for( var j = 0; j < amo; j++ ) { np = h.points[j]; if(j) l[j] = point_distance(op.x, op.y, np.x, np.y); op = np; } for( var j = 0; j < amo; j++ ) { var ind = tool_dragging == tool_stretch? j : amo - 1 - j; np = h.points[ind]; if(j) { var dir = point_direction(op.x, op.y, np.x, np.y); var dis = l[ind]; var mds = point_distance(__mx, __my, (op.x + np.x) / 2, (op.y + np.y) / 2); if(mds < rad * (1 + fall)) { var fl = clamp((rad * (1 + fall) - mds) / (rad * fall * 2), 0, 1); var st = dis * (1 + fl * stn * (tool_dragging == tool_stretch? 1 : -1)); if(tool_dragging == tool_stretch) l[j] = st; else l[amo - 1 - j] = st; //print(string(st) + ": " + string(dis) + ", " + // string(1 + fl * stn * (tool_dragging == 2? 1 : -1))); } } op = np; } for( var j = 0; j < amo; j++ ) { np = h.points[j]; if(j) { var dr = point_direction(op.x, op.y, np.x, np.y); np.x = op.x + lengthdir_x(l[j], dr); np.y = op.y + lengthdir_y(l[j], dr); } op = np; } } if(mouse_release(mb_left)) { groomed.freeze(false); tool_dragging = noone; } } else if(tool_dragging == tool_grab) { var rad = tool_grab.attribute.radius; var stn = tool_grab.attribute.strength; var dx = __mx - tool_mx; var dy = __my - tool_my; if(dx != 0 || dy != 0) for( var i = 0, n = array_length(tool_grabbing); i < n; i++ ) { var h = tool_grabbing[i][0]; var p = tool_grabbing[i][1]; var inf = tool_grabbing[i][2]; p.ikx = p.x + dx * inf; p.iky = p.y + dy * inf; h.FABRIK(4); } tool_mx = __mx; tool_my = __my; if(mouse_release(mb_left)) { groomed.freeze(true); tool_dragging = noone; } } if(isUsingTool(0)) { var rad = tool_push.attribute.radius; var fall = tool_push.attribute.fall; draw_set_color(COLORS._main_accent); draw_circle_prec(_mx, _my, rad * _s, true); draw_circle_dash(_mx, _my, rad * _s * (1 - fall), true); draw_circle_dash(_mx, _my, rad * _s * (1 + fall), true); if(mouse_press(mb_left, active)) { tool_dragging = tool_push; tool_mx = (_mx - _x) / _s; tool_my = (_my - _y) / _s; } } else if(isUsingTool(1)) { if(tool_dragging == noone) tool_dir_fix = tool_dir; var wid = tool_comb.attribute.width; var thk = tool_comb.attribute.thick; var p0x = _mx + lengthdir_x(wid * _s, tool_dir_fix + 90); var p0y = _my + lengthdir_y(wid * _s, tool_dir_fix + 90); var p1x = _mx + lengthdir_x(wid * _s, tool_dir_fix - 90); var p1y = _my + lengthdir_y(wid * _s, tool_dir_fix - 90); draw_set_color(COLORS._main_accent); draw_line(p0x, p0y, p1x, p1y); var _p0x = p0x + lengthdir_x(thk * _s, tool_dir_fix); var _p0y = p0y + lengthdir_y(thk * _s, tool_dir_fix); var _p1x = p1x + lengthdir_x(thk * _s, tool_dir_fix); var _p1y = p1y + lengthdir_y(thk * _s, tool_dir_fix); draw_line_dashed(_p0x, _p0y, _p1x, _p1y); var __p0x = p0x - lengthdir_x(thk * _s, tool_dir_fix); var __p0y = p0y - lengthdir_y(thk * _s, tool_dir_fix); var __p1x = p1x - lengthdir_x(thk * _s, tool_dir_fix); var __p1y = p1y - lengthdir_y(thk * _s, tool_dir_fix); draw_line_dashed(__p0x, __p0y, __p1x, __p1y); draw_line_dashed(_p0x, _p0y, __p0x, __p0y); draw_line_dashed(_p1x, _p1y, __p1x, __p1y); if(mouse_press(mb_left, active)) { groomed.store(); tool_dragging = tool_comb; tool_mx = (_mx - _x) / _s; tool_my = (_my - _y) / _s; } } else if(isUsingTool(2) || isUsingTool(3)) { var rad = isUsingTool(2)? tool_stretch.attribute.radius : tool_cut.attribute.radius; var fall = isUsingTool(2)? tool_stretch.attribute.fall : tool_cut.attribute.fall; draw_set_color(COLORS._main_accent); draw_circle_prec(_mx, _my, rad * _s, true); draw_circle_dash(_mx, _my, rad * _s * (1 - fall), true); draw_circle_dash(_mx, _my, rad * _s * (1 + fall), true); if(mouse_press(mb_left, active)) { tool_dragging = isUsingTool(2)? tool_stretch : tool_cut; tool_mx = (_mx - _x) / _s; tool_my = (_my - _y) / _s; } } else if(isUsingTool(4)) { var rad = tool_grab.attribute.radius; var fall = tool_grab.attribute.fall; draw_set_color(COLORS._main_accent); draw_circle_prec(_mx, _my, rad * _s, true); draw_circle_dash(_mx, _my, rad * _s * (1 - fall), true); draw_circle_dash(_mx, _my, rad * _s * (1 + fall), true); if(mouse_press(mb_left, active)) { tool_dragging = tool_grab; tool_mx = (_mx - _x) / _s; tool_my = (_my - _y) / _s; tool_grabbing = []; for( var i = 0, n = array_length(groomed.hairs); i < n; i++ ) { var h = groomed.hairs[i]; var p = h.points[array_length(h.points) - 1]; var d = point_distance(p.x, p.y, tool_mx, tool_my); if(d > rad * (1 + fall)) continue; var fl = clamp((rad * (1 + fall) - d) / (rad * fall * 2), 0, 1); array_push(tool_grabbing, [ h, p, fl ]); } } } tool_dmx = __mx; tool_dmy = __my; } #endregion static step = function() { #region var _typ = getInputData(0); inputs[ 5].setVisible(_typ == 1, _typ == 1); inputs[ 7].setVisible(_typ == 1); inputs[13].setVisible(_typ == 2, _typ == 2); inputs[14].setVisible(_typ != 2); inputs[15].editWidget.text = attributes.use_groom? "Unbake" : "Bake"; inputs[15].editWidget.blend = attributes.use_groom? COLORS._main_value_negative : COLORS._main_value_positive; } #endregion static strandUpdate = function(willReset = false) { #region var _typ = getInputData(0); var _den = getInputData(1); var _len = getInputData(2); var _seg = getInputData(3); _seg = _seg + 1; var _ten = getInputData(4); _ten = 1 - _ten; var _pth = getInputData(5); var _pos = getInputData(6); var _sid = getInputData(7); var _spr = getInputData(8); var _ang = getInputData(9); var _sed = getInputData(10); var _crF = getInputData(11); var _crS = getInputData(12); var _msh = getInputData(13); var _rnd = getInputData(14); var _rot = getInputData(17); var sx, sy, prog, dir; if(willReset) { if(attributes.use_groom) { strands = groomed.clone(); outputs[0].setValue(strands); return; } strands = new StrandMesh(); } outputs[0].setValue(strands); if(_typ == 0) strands.loop = true; else if(_typ == 1) { if(!struct_has(_pth, "getPointRatio")) return; var _p0 = _pth.getPointRatio(0); var _p1 = _pth.getPointRatio(0.999); strands.loop = abs(_p0.x - _p1.x) < 1 && abs(_p0.y - _p1.y) < 1; } else if(_typ == 2) { if(_msh == noone) return; strands.mesh = _msh; strands.loop = false; } var lines = 1; if(_typ == 1 && struct_has(_pth, "getLineCount")) lines = _pth.getLineCount(); var ind = 0; for( var k = 0; k < lines; k++ ) for( var i = 0; i < _den; i++ ) { prog = i / _den; if(_typ == 0) { sx = _pos[0]; sy = _pos[1]; dir = _rnd? random1D(_sed, 0, 360) : 360 * prog; _sed++; } else if(_typ == 1) { var rat = _rnd? random1D(_sed) : prog; _sed++; rat = clamp(rat, 0.01, 0.99); var _p = _pth.getPointRatio(rat, k); sx = _pos[0] + _p.x; sy = _pos[1] + _p.y; var _p0 = _pth.getPointRatio(clamp(rat - 0.001, 0, 1)); var _p1 = _pth.getPointRatio(clamp(rat + 0.001, 0, 1)); dir = point_direction(_p0.x, _p0.y, _p1.x, _p1.y) + 90; if(_sid == 1) dir += 180; else if(_sid == 2) dir += 180 * (_rnd? choose(0, 1) : i % 2); } else if(_typ == 2) { var _p = strands.mesh.getRandomPoint(_sed); _sed += 5; sx = _pos[0] + _p.x; sy = _pos[1] + _p.y; dir = irandom(360); } if(willReset || array_safe_get_fast(strands.hairs, i, noone) == noone) { var h = new Strand(sx, sy, _seg, random_range(_len[0], _len[1]), dir, _crF, _crS); h.rootStrength = random1D(h.id, _rot[0], _rot[1]); strands.hairs[ind] = h; } if(ind >= array_length(strands.hairs)) return; var h = strands.hairs[ind]; h.setOrigin(sx, sy); h.tension = _ten; h.spring = _spr; h.angularTension = _ang; h.curl_freq = _crF; h.curl_size = _crS; ind++; } } #endregion static update = function(frame = CURRENT_FRAME) { #region strandUpdate(IS_FIRST_FRAME); } #endregion static onDrawNode = function(xx, yy, _mx, _my, _s, _hover, _focus) { #region var bbox = drawGetBbox(xx, yy, _s); draw_sprite_fit(s_node_strandSim_create, 0, bbox.xc, bbox.yc, bbox.w, bbox.h); } #endregion static attributeSerialize = function() { #region var att = {}; att.use_groom = attributes.use_groom; att.fixStrand = groomed.serialize(); return att; } #endregion static attributeDeserialize = function(attr) { #region struct_append(attributes, attr); if(struct_has(attr, "fixStrand")) groomed.deserialize(attr.fixStrand); attributes.use_groom = struct_try_get(attr, "use_groom", false); } #endregion }