@tool class_name GrassMultiMesh extends MultiMeshInstance2D @export_group("Visuals") @export var texture_sheet : Texture2D: set(value): texture_sheet = value texture = value @export var blade_size : Vector2 = Vector2(16, 16) @export var texture_sheet_size : Vector2 = Vector2(64, 16) @export var grass_tint : Color = Color.WHITE: set(value): grass_tint = value material.set_shader_parameter("modulate", grass_tint) @export_group("Editor") @export var editing : bool = false: set(value): editing = value notify_property_list_changed() @export var brush_radius : float = 50.0 @export var brush_density : int = 5 @export_storage var grass_data : Array[Dictionary] = [] enum GrassState {NORMAL = 0, CUT = 1} var _blade_half_height: float func _ready() -> void: if not multimesh: multimesh = MultiMesh.new() multimesh.transform_format = MultiMesh.TRANSFORM_2D multimesh.use_colors = true multimesh.use_custom_data = true multimesh.mesh = _create_mesh_rect() if not material: material = ShaderMaterial.new() if ResourceLoader.exists("res://shaders/grass_shader.gdshader"): material.shader = load("res://shaders/grass_shader.gdshader") if material is ShaderMaterial: material.set_shader_parameter("frame_size", blade_size) material.set_shader_parameter("sheet_size", texture_sheet_size) material.set_shader_parameter("modulate", grass_tint) if material.get_shader_parameter("noise_tex") == null: var noise = FastNoiseLite.new() noise.frequency = 0.005 var noise_tex = NoiseTexture2D.new() noise_tex.noise = noise noise_tex.seamless = true material.set_shader_parameter("noise_tex", noise_tex) if texture_sheet: texture = texture_sheet _blade_half_height = blade_size.y / 2.0 _rebuild_multimesh() func _process(_delta): if editing and Engine.is_editor_hint(): queue_redraw() if Input.is_key_pressed(KEY_K): _brush_paint() elif Input.is_key_pressed(KEY_E): _brush_erase() func interact_with_grass(global_target_pos: Vector2, rect_size: Vector2, new_state: int): if global_position.distance_squared_to(global_target_pos) > 500 * 500: return var local_target = to_local(global_target_pos) var half_w = rect_size.x * 0.5 var half_h = rect_size.y * 0.5 var left = local_target.x - half_w var right = local_target.x + half_w var top = local_target.y - half_h var bottom = local_target.y + half_h for i in range(multimesh.instance_count): var t_origin = multimesh.get_instance_transform_2d(i).origin if t_origin.x > left and t_origin.x < right and t_origin.y > top and t_origin.y < bottom: var custom_data = multimesh.get_instance_custom_data(i) if int(custom_data.b) == new_state: continue custom_data.b = float(new_state) multimesh.set_instance_custom_data(i, custom_data) grass_data[i]["state"] = new_state func _create_mesh_rect() -> QuadMesh: var mesh = QuadMesh.new() mesh.size = blade_size return mesh func _rebuild_multimesh() -> void: multimesh.instance_count = grass_data.size() var pivot_offset = Vector2(0, -_blade_half_height) for i in range(grass_data.size()): var d = grass_data[i] var xform = Transform2D().translated(d.pos).rotated(d.get("rot", 0.0)).translated(pivot_offset) multimesh.set_instance_transform_2d(i, xform) var custom = Color( randf(), float(d.get("tex_idx", 0)), float(d.get("state", 0)), 0.0 ) multimesh.set_instance_custom_data(i, custom) func _brush_paint() -> void: var mouse_pos = get_local_mouse_position() var columns = int(texture_sheet_size.x / blade_size.x) if columns == 0: columns = 1 for i in range(brush_density): var angle = randf() * TAU var rad = sqrt(randf()) * brush_radius var pos = mouse_pos + Vector2(cos(angle), sin(angle)) * rad var new_blade = { "pos": pos, "tex_idx": randi() % columns, "state": GrassState.NORMAL, "rot": 0# randf_range(-0.2, 0.2) } grass_data.append(new_blade) _rebuild_multimesh() func _brush_erase() -> void: var mouse_pos = get_local_mouse_position() var r_sq = brush_radius * brush_radius for i in range(grass_data.size() - 1, -1, -1): if grass_data[i].pos.distance_squared_to(mouse_pos) < r_sq: grass_data.remove_at(i) _rebuild_multimesh() func _draw() -> void: if editing and Engine.is_editor_hint(): draw_arc(get_local_mouse_position(), brush_radius, 0, TAU, 32, Color.YELLOW, 2.0)