194 lines
5.6 KiB
GDScript
194 lines
5.6 KiB
GDScript
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))
|