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 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))