Files
2026-10-10 16:07:53 +08:00

194 lines
5.6 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name FacePainter
extends Node
## 面部画笔:直接往一张 Image 的字节缓冲上盖笔刷印章。
##
## 为什么不用 Control._draw():Godot 4 的 canvas 已经没有「擦除」混合模式了
## (CanvasItemMaterial 只有 mix / add / sub / mul / premult,也没有 draw_set_blend_mode),
## 橡皮没法靠 blend_mode 实现。自己管像素反而更直接:
## 画 = 标准的 source-over alpha 混合;擦 = 把目标的 alpha 乘掉;存档 = 就是存这张图。
var image: Image
var texture: ImageTexture
var canvas_size: int = 512
## 当前笔刷。绘画面板改这几个值,下一笔生效。
var brush_color: Color = Color.BLACK
var brush_width: float = 12.0
var erasing: bool = false
## 像素缓冲(RGBA8)。所有绘制都改它,一笔画完再一次性推给显卡。
var _buf: PackedByteArray
## 圆形笔刷的覆盖度,边长 _stamp_d
var _cov: PackedFloat32Array
var _stamp_d: int = 0
var _stamp_rgb: Vector3 = Vector3.ZERO
var _last_stamp: Vector2 = Vector2.INF
func _init(size: int = 512) -> void:
canvas_size = size
func _ready() -> void:
_buf = PackedByteArray()
_buf.resize(canvas_size * canvas_size * 4)
_sync()
# ---------------- 画笔接口 ----------------
func begin_stroke(pos: Vector2) -> void:
_rebuild_stamp()
_last_stamp = Vector2.INF
_stamp_at(pos)
_last_stamp = pos
_sync()
func extend_stroke(pos: Vector2) -> void:
if not _last_stamp.is_finite():
_stamp_at(pos)
_last_stamp = pos
_sync()
return
var spacing := maxf(1.0, brush_width * 0.25)
var dist := _last_stamp.distance_to(pos)
if dist < spacing:
return
# 鼠标动得快时两点之间得补章,否则线是一串断点
var steps := int(ceil(dist / spacing))
for i in range(1, steps + 1):
_stamp_at(_last_stamp.lerp(pos, float(i) / float(steps)))
_last_stamp = pos
_sync()
func end_stroke() -> void:
_last_stamp = Vector2.INF
func clear() -> void:
_buf.fill(0)
_last_stamp = Vector2.INF
_sync()
## 拍一张当前笔画层的快照。绘画面板打开时存一份,点「取消」时还原
func snapshot() -> PackedByteArray:
return _buf.duplicate()
## 还原到快照。尺寸对不上就忽略(换过画布大小)
func restore(data: PackedByteArray) -> void:
if data.size() != _buf.size():
return
_buf = data.duplicate()
_last_stamp = Vector2.INF
_sync()
# ---------------- 存档 ----------------
## 笔画层存成一张透明底 PNG
func save_png(path: String) -> void:
_sync()
image.save_png(path)
## 读回笔画层。尺寸对不上就当没读到(换了画布大小的话)
func load_png(path: String) -> void:
if not FileAccess.file_exists(path):
return
set_image(Image.load_from_file(path))
## 从一张 Image 直接载入笔画层 —— 联机收到别人的脸时走这条,不用落盘
func set_image(img: Image) -> void:
if img == null:
return
if img.get_width() != canvas_size or img.get_height() != canvas_size:
push_warning("笔画层尺寸是 %s,画布是 %d,忽略" % [img.get_size(), canvas_size])
return
img.convert(Image.FORMAT_RGBA8)
_buf = img.get_data()
_last_stamp = Vector2.INF
_sync()
## 当前笔画层的 PNG 字节 —— 联机同步用,不落盘
func to_png_bytes() -> PackedByteArray:
_sync()
return image.save_png_to_buffer()
# ---------------- 内部 ----------------
## 把字节缓冲变成 Image 再推给显卡。一笔画完调一次 —— 512² 一次就是 1MB,别每盖一个印章都来
func _sync() -> void:
image = Image.create_from_data(canvas_size, canvas_size, false, Image.FORMAT_RGBA8, _buf)
if texture == null:
texture = ImageTexture.create_from_image(image)
else:
texture.update(image)
## 按当前粗细和颜色重造笔刷。边缘留 1 像素羽化 —— 硬边的线很难看。
func _rebuild_stamp() -> void:
var r := maxi(2, int(round(brush_width * 0.5)))
var d := r * 2
_stamp_d = d
_stamp_rgb = Vector3(brush_color.r, brush_color.g, brush_color.b)
_cov.resize(d * d)
for y in d:
for x in d:
var dx := float(x) - float(r) + 0.5
var dy := float(y) - float(r) + 0.5
var dist := sqrt(dx * dx + dy * dy) / float(r)
_cov[y * d + x] = clampf((1.0 - dist) * float(r), 0.0, 1.0)
## 在 pos 处盖一个印章。越界像素直接跳过,所以画到边上也不会出错。
func _stamp_at(pos: Vector2) -> void:
var d := _stamp_d
# d 恒为偶数(上方是按 r * 2 算出来的),这里的整除不会丢精度。
# 所以只抑制警告、不改算法 —— 换成浮点反而会引入半像素偏移。
@warning_ignore("integer_division")
var r := d / 2
var cx := int(round(pos.x)) - r
var cy := int(round(pos.y)) - r
var sr := _stamp_rgb.x * 255.0
var sg := _stamp_rgb.y * 255.0
var sb := _stamp_rgb.z * 255.0
for sy in d:
var ty := cy + sy
if ty < 0 or ty >= canvas_size:
continue
var row := ty * canvas_size * 4
var srow := sy * d
for sx in d:
var tx := cx + sx
if tx < 0 or tx >= canvas_size:
continue
var cov := _cov[srow + sx]
if cov <= 0.001:
continue
var o := row + tx * 4
if erasing:
# 擦:把 alpha 按覆盖度乘掉。RGB 留着不管 —— shader 只看 alpha
_buf[o + 3] = int(float(_buf[o + 3]) * (1.0 - cov))
continue
var sa := cov
var da := float(_buf[o + 3]) / 255.0
var na := sa + da * (1.0 - sa)
if na <= 0.0001:
continue
var ws := sa / na
var wd := da * (1.0 - sa) / na
_buf[o] = int(clampf(sr * ws + float(_buf[o]) * wd, 0.0, 255.0))
_buf[o + 1] = int(clampf(sg * ws + float(_buf[o + 1]) * wd, 0.0, 255.0))
_buf[o + 2] = int(clampf(sb * ws + float(_buf[o + 2]) * wd, 0.0, 255.0))
_buf[o + 3] = int(clampf(na * 255.0, 0.0, 255.0))