Working in my passion project temporarily named VCG I had to implement painting and it was surprisingly hard to figure out how to get working because most information out there is for Godot 3 and some functions and variables related to this have changed so all that information is deprecated now. Anyway, finally got it working after banging my head on it for a few days, here you have it.
To do this we need to get the UV position that matches the 3D world position we’re clicking, blend the brush texture onto the mesh’s material texture, and then update the texture and material.
Starting with getting these coordinates I used Alfred Baudisch’s example, which helped a lot! Had to update a few lines from the changes that happened between Godot 3 and 4, so the updated version of his script would be: pastebin.
Now that we can get the coordinates using
get_uv_coords(point, normal, transform = true) we can paint with our brush using blend_rect().
extends Node var img : Image var tex : ImageTexture var mesh : GeometryInstance3D = null var rect : Rect2i var brush : Image = Image.new() var brushSize var pixels = 256 func _create_image(): # your image and your brush need to have the same format, in my case it's RGBA8 img = Image.create(pixels, pixels, false, Image.FORMAT_RGBA8) img.fill(Color(0.82, 0.6, 0.451)) # this is the color I want my background to be brushSize = brush.get_height() / 2 rect = Rect2i(Vector2i(brushSize,brushSize), Vector2i(brushSize,brushSize)) tex = ImageTexture.create_from_image(img) mesh.material_override.set("shader_parameter/Mask_texture", tex) func _get_mesh(): for child in get_children(): if child is GeometryInstance3D: mesh = child func _ready(): brush = brush.load("res://Art/Textures/T_TomatoSauce_brushes.png") _get_mesh() _create_image() func random_brush_splat(): # choose a random sprite from my 2x2 atlas texture var index : Vector2i = Vector2i(0,0) if randi() % 2: index = Vector2i(brushSize, 0) if randi() % 2: index = Vector2i(index.x, brushSize) rect = Rect2i(Vector2i(index.x, index.y), Vector2i(brushSize,brushSize)) func draw_on_sprite(coords): coords = (coords * pixels) - Vector2(brushSize, brushSize)/2 random_brush_splat() img.blend_rect (brush, rect, Vector2i(coords.x,coords.y)) tex.update(img) mesh.material_override.set("shader_parameter/Mask_texture", tex)
For some reason you have to set the material texture every time you update it, both the texture file and the material itself. This was driving me insane because I knew the texture was updating if I checked the pixel but it wasn’t changing at all in my viewport.
And in this case I’ll do you one better: flipbook brush using the function
random_brush_splat(). So there’s variation on the texture and it doesn’t look like you’re dragging your Windows 98 error window.
So now we call that
draw_on_sprite(coords) function from my mouse event function that comes from an Area3D node that I have on my mesh I want to paint.
func _on_area_3d_input_event(camera, event, position, normal, _shape_idx): if TouchInput.checkClick(event).y and coords != position: # dragging # drawing tomato sauce if coords.distance_squared_to(position) > 0.04: # give some space between splats coords = position if (PlayerVariables.ingredientHeld != null): if PlayerVariables.ingredientHeld is Tomato: $pizza_dough_flat/Area3D.set_mesh ($pizza_dough_flat/Pizza_dough) var uv_coords = $pizza_dough_flat/Area3D.get_uv_coords(position, normal) if (uv_coords != null): $pizza_dough_flat.draw_on_sprite(uv_coords )
For those curious, this is my
class_name Touch_input extends Node var is_dragging : bool = false var is_click : bool = false func checkClick(event): is_click = false is_dragging = false if (InputEventScreenTouch and !event.is_pressed()) and (Input.is_action_pressed("click")): # dragging is_dragging = true elif (event is InputEventMouseButton): # tap - avoids auto double-tap is_click = true return Vector2(is_click, is_dragging)
It’s in an autoloaded script, they work in a similar way that static classes do in Unity.
And there you have it folks.