架构基线:docs/architecture/2026-10-19-F-architecture.md(v1.0) 范围:WRATH OF THE DEAD《怨返》单人挂机 · GDScript · 1280×720 · 12 班底数据驱动 - 骨架:project.godot(4.7 + GL Compatibility)、架构 §2.1 完整目录树(含占位说明)、 .gitignore/.gitattributes(LF 纪律)、README.md、CLAUDE.md、export_presets.cfg 占位、 pipeline/README.md(3ds Max 侧契约) - 数据管线段 1:data/sheets/generators.csv(GDD §3.1 十二班底逐值抄录) + data/schema/generators.schema.json(v0)+ src/tools/sheet_importer.gd (import_all/import_sheet/validate + ImportReport + .tres stub + 行号/列名/原因失败策略) + src/tools/import_main.gd(CI CLI 入口)+ data/build/generators.json(字节级确定性产物) - 探针:P1(SCREEN_TEXTURE/CanvasLayer 排序)与 P3(NoiseTexture2D seamless)场景代码与 截图/统计工具;实测手册与结论模板在工作区 docs/engineering/(随仓库外文档树) - CI:数据门激活(import_all + schema validate + build 漂移门);单测/sim/导出 W3+ 注释占位; 4.7.2 headless CLI 行为标注待本机验证 待用户侧:P1/P3 实测回填结论;P6 字体许可 / P7 GPU 实测;look-dev 游魂初版(Max 侧)
168 lines
6.2 KiB
GDScript
168 lines
6.2 KiB
GDScript
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)
|