extends Node2D # ============================================================================ # 探针 P3 —— NoiseTexture2D / FastNoiseLite 平铺语义(架构 §11 P3) # 依据:管线报告 §6.3;作用于 §5.2 纸纹 pass(双尺度 noise 有序抖动的 tileable 前提) # 实测手册:docs/engineering/2026-10-19-f-w1-probe-p3-manual.md # 结论回填:docs/engineering/probes/P3-result.md # # 验证:seamless = true 是否真正可平铺 # - 屏上:左右两个 2×2 tile 网格(红线 = tile 边界 @256px)肉直视读接缝 # - 按 S:生成边界差图 PNG(user://p3_diff_*.png)+ 控制台统计 # * wrap_* = |左边缘-右边缘| / |上边缘-下边缘|(平铺接缝强度) # * grad_mean = 相邻像素平均差(自然梯度基线) # * seamless 若有效:wrap ≈ grad(同数量级);若无效:wrap >> grad # # 知识缺口:FastNoiseLite/NoiseTexture2D 属性名(seamless / seamless_blend_ratio / # TYPE_SIMPLEX / FRACTAL_FBM)为 4.x 稳定名;4.7.2 若报错按实际属性名修正并记录。 # ============================================================================ const NOISE_SIZE := 256 # 每 tile 256×256;面板 512×512 = 2×2 平铺 const PANEL_SIZE := 512 var _textures: Array = [] func _ready() -> void: _textures = [_make_texture(true), _make_texture(false)] var positions := [Vector2(96, 160), Vector2(672, 160)] var titles := ["seamless = true", "seamless = false"] for i in _textures.size(): var tr := TextureRect.new() tr.name = "Panel%d" % i tr.texture = _textures[i] tr.position = positions[i] tr.size = Vector2(PANEL_SIZE, PANEL_SIZE) tr.stretch_mode = TextureRect.STRETCH_TILE tr.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(tr) var overlay := SeamOverlay.new() overlay.position = positions[i] add_child(overlay) var t := Label.new() t.name = "PanelTitle%d" % i t.text = "%s(红线 = tile 边界;面板内 2×2 平铺)" % titles[i] t.position = Vector2(int(positions[i].x), 128) t.add_theme_color_override("font_color", Color.WHITE) add_child(t) var hud := Label.new() hud.name = "HUDHelp" hud.text = "探针 P3 | 对比两侧红线处接缝(错位/亮度跳变=异常)| S 生成检查图到 user:// 并打印统计 | Esc 退出" hud.position = Vector2(96, 648) hud.add_theme_color_override("font_color", Color.WHITE) add_child(hud) _print_environment() func _make_texture(seamless: bool) -> NoiseTexture2D: var fnl := FastNoiseLite.new() fnl.seed = 20261019 fnl.noise_type = FastNoiseLite.TYPE_SIMPLEX fnl.frequency = 0.02 fnl.fractal_type = FastNoiseLite.FRACTAL_FBM fnl.fractal_octaves = 3 fnl.fractal_lacunarity = 2.0 fnl.fractal_gain = 0.5 var nt := NoiseTexture2D.new() nt.width = NOISE_SIZE nt.height = NOISE_SIZE nt.noise = fnl nt.seamless = seamless nt.seamless_blend_ratio = 0.25 nt.generate_mipmaps = false return nt func _input(event: InputEvent) -> void: var key_event := event as InputEventKey if key_event != null and key_event.pressed and not key_event.echo: match key_event.keycode: KEY_S: _generate_check_images() KEY_ESCAPE: get_tree().quit() func _generate_check_images() -> void: var names := ["seamless", "periodic"] print("=== P3 检查图与统计 ===") for i in _textures.size(): var nt: NoiseTexture2D = _textures[i] var img := nt.get_image() if img == null: push_warning("P3: NoiseTexture2D.get_image() 返回 null(%s)。回退:系统截图(Win+Shift+S)+ 肉眼判读。" % names[i]) continue var w := img.get_width() var h := img.get_height() # 边界差图(×8 放大):wrap 接缝越亮 = 平铺越不连续 var strip_v := Image.create(w, h, false, Image.FORMAT_RGBA8) var strip_h := Image.create(w, h, false, Image.FORMAT_RGBA8) for y in h: var dv := _amp(absf(img.get_pixel(0, y).r - img.get_pixel(w - 1, y).r)) for x in w: strip_v.set_pixel(x, y, Color(dv, dv, dv, 1.0)) for x in w: var dh := _amp(absf(img.get_pixel(x, 0).r - img.get_pixel(x, h - 1).r)) for y in h: strip_h.set_pixel(x, y, Color(dh, dh, dh, 1.0)) strip_v.save_png("user://p3_diff_v_%s.png" % names[i]) strip_h.save_png("user://p3_diff_h_%s.png" % names[i]) var stats := _edge_stats(img) print("%s: wrap_v mean=%.5f max=%.5f | wrap_h mean=%.5f max=%.5f | grad_mean=%.5f → wrap/grad v=%.2fx h=%.2fx" % [ names[i], stats.wrap_v_mean, stats.wrap_v_max, stats.wrap_h_mean, stats.wrap_h_max, stats.grad_mean, stats.wrap_v_mean / maxf(stats.grad_mean, 1e-9), stats.wrap_h_mean / maxf(stats.grad_mean, 1e-9)]) print("PNG 位置:%APPDATA%/Godot/app_userdata/<项目名>/p3_diff_*.png") print("判读:seamless 行 wrap/grad ≈ 1-3x = 可用;periodic 行 wrap/grad ≫ 1 = 对照组生效(说明指标有效)") func _amp(d: float) -> float: return clampf(d * 8.0, 0.0, 1.0) func _edge_stats(img: Image) -> Dictionary: var w := img.get_width() var h := img.get_height() var wrap_v := 0.0 var wrap_v_max := 0.0 for y in h: var d := absf(img.get_pixel(0, y).r - img.get_pixel(w - 1, y).r) wrap_v += d wrap_v_max = maxf(wrap_v_max, d) var wrap_h := 0.0 var wrap_h_max := 0.0 for x in w: var d := absf(img.get_pixel(x, 0).r - img.get_pixel(x, h - 1).r) wrap_h += d wrap_h_max = maxf(wrap_h_max, d) var grad := 0.0 for y in h: for x in w - 1: grad += absf(img.get_pixel(x, y).r - img.get_pixel(x + 1, y).r) return { "wrap_v_mean": wrap_v / h, "wrap_v_max": wrap_v_max, "wrap_h_mean": wrap_h / w, "wrap_h_max": wrap_h_max, "grad_mean": grad / float((w - 1) * h), } func _print_environment() -> void: var v := Engine.get_version_info() print("=== P3 probe 环境 ===") print("godot: %d.%d.%d %s" % [int(v.major), int(v.minor), int(v.patch), str(v.status)]) print("rendering_method: ", ProjectSettings.get_setting("rendering/renderer/rendering_method")) print("adapter: ", RenderingServer.get_video_adapter_name()) print("noise: FastNoiseLite simplex fbm oct3 f0.02 | tile 256×256 | blend_ratio 0.25") class SeamOverlay extends Node2D: const TILE := 256 const PANEL := 512 func _draw() -> void: var c := Color(1.0, 0.2, 0.2, 0.6) draw_line(Vector2(TILE, 0), Vector2(TILE, PANEL), c, 1.0) draw_line(Vector2(0, TILE), Vector2(PANEL, TILE), c, 1.0) draw_rect(Rect2(Vector2.ZERO, Vector2(PANEL, PANEL)), c, false, 1.0)