test(render): 双帧像素取证+判据源探针(迭代 30 卡① T2-④)——board_shot_probe 每关抓微光开(生产默认态)/关(golden 基线保护面)双帧:前置 UiKit.motion_scale=0.5 冻结接触带呼吸相位(静态带色,双帧差异唯一定位在类型微光开关),_check_dual_frames 逐格窗口矩形+到格缘距离判据(de < cell÷8——带外含砖心零侵入=禁整格染色/禁越圈)+每生效格须有实绘像素(微光不空转)+开关快照须在关开关前取;读数落 board_dual_frame_readings.json(报告读数表源)。身份解析接生产同源面(main.gd:251 同构:板在守卫+Active 相位守卫+query_reward_identity 只读代理——裸视图不接则作用集恒空、PASS 假绿,本批实跑两次 glow=0 的根因即此;附带排 Callable.bind 动态派发 Object 硬校验坑,改成员现值+直接方法引用)。新建判据源探针 type_glow_shot_probe(夹具面五组齐现:glow_cells=5、groups=[1-5]、changed_outside=0、相邻组实测 CR 最弱对 1.232、组4/组5=1.423)——真实面随板摇号(双帧 glow=2/4/4),五组齐现互辨取证在夹具面承载,两面分工不混。off 帧对 2026-09-30 代 golden 三基线漂移 0.13/0.11/0.10%(≤2%,接触带静态化预期面,契约 6 未触发、基线未重制);render_gate.sh 整闸全绿(含菜单 golden 0.14%)。

This commit is contained in:
DKChan committed 2026-10-17 10:18:55 +08:00
1 parent 80ac80ef1a
commit e5c03eb3ab
4 files changed
+405 -6

No files matched your search

+137 -6
View File
@@ -7,11 +7,22 @@
# 护栏:headless fail-fast(shot_probe 同款);空纹理取证 fail——取证段失败=闸红,
# 零字节/空纹理不允许静默落盘;board_meta.json 取样坐标落盘(棋盘视图全局矩形——
# golden 前置裁剪坐标源,禁硬编码像素)。
# 迭代 30 卡① T2-④ 双帧取证:每关抓 微光开(生产默认态·Q4 丙案)/关(golden 基线保护面)
# 两帧——① 开帧逐像素判定「非生效格零变化」(禁整格染色/禁越圈;生效格内只准落在内缘
# 带 = cell÷8)② 每个生效格须有实绘像素(微光不空转)③ 关帧即 golden 对比帧(原名落盘,
# 基线 2026-09-30 代不重制——契约 1 off 态基线)。隔离纪律 = 与 type_glow_shot_probe 同款:
# 前置 UiKit.motion_scale=0.5 冻结接触带呼吸相位(静态带色),两帧差异唯一定位在类型微光。
extends Node
const AutoPlayer := preload("res://smoke/auto_player.gd")
const ActionEconomy := preload("res://core/action_economy.gd")
var _meta := {} # 棋盘视图实测几何(全局 rect——tools/golden_board_crop.py 取样坐标源)
var _problems: Array = []
var _level_readings := {} # 关 → 双帧比对读数(实现报告读数表源)
var _cur_settlement = null # 当关 settlement(身份回调读取面——仿生产 run_scope.settlement 现值;
# 不用 Callable.bind 传参:bind 的 Callable 动态派发时对类型化参数做 Object 硬校验,
# 报「Cannot convert argument 2 from Object to Vector2i」——直接方法引用+成员现值无此坑)
func _ready() -> void:
@@ -30,6 +41,11 @@ func _ready() -> void:
push_error("[board_shot] 表加载失败")
get_tree().quit(1)
return
# 双帧单一变量隔离(迭代 30 卡①):冻结接触带呼吸相位——motion_scale<1.0 ⇒ _apply_contact_bands
# 施静态色(减动效停格路径)。必须在 rebuild 前置位(rebuild/set_dig_glow_enabled 按当时相位
# 重施带色)。golden 侧影响:静态带色=呼吸周期中值,与基线捕获相的像素差最坏 ≈灰度24 于
# 接触带像素(占比 <0.4%),≤2% 判据不受影响;停格同时令「非生效格零变化」可精确判定。
UiKit.motion_scale = 0.5
var ui_font: Font = load("res://presentation/views/ui_kit.gd").load_font(load("res://presentation/views/ui_kit.gd").FONT_UI)
var targets := [[1, "L1_dungeon"], [2, "L2_underdark"], [3, "L3_deep_underdark"]]
for t in targets:
@@ -43,6 +59,7 @@ func _ready() -> void:
get_tree().quit(1)
return
var settlement = runner.current_battle()
_cur_settlement = settlement
for i in range(18):
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
var kind := String(act.get("kind", "stop"))
@@ -58,7 +75,18 @@ func _ready() -> void:
if ui_font != null:
view.set_world_font(ui_font)
add_child(view)
# 身份解析接线(生产同源——main.gd:251 set_identity_resolver(_identity_lookup)):必须在
# rebuild / set_dig_glow_enabled 前接好,二者尾链 _recompute_glow 同步读身份建作用集。
# 裸视图不接 = _identity_cb 无效 ⇒ info 恒 null ⇒ 作用集恒空(2026-10-17 首两次实跑
# 三关 glow=0 根因——双帧比对空转、PASS 假绿)。现值经 _cur_settlement 成员读入
# (仿生产 run_scope.settlement;bind 传参有 Callable 类型校验坑,见成员注)。
view.set_identity_resolver(_probe_identity_lookup)
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
# 进挖掘相位(生产态 = hud_presenter.set_dig_glow_enabled(not settling) 接线——裸视图默认
# _dig_glow=false,接触带与类型微光同源门控都不渲染;golden 基线 2026-09-30 代即捕获于
# 该裸态。双帧取证要求生产默认态,故显式开——两帧均含静态接触带(motion_scale=0.5 停格),
# 差异唯一定位在类型微光开关)
view.set_dig_glow_enabled(true)
# 抓帧前等入场动画完结(迭代 19 卡① S2 观察项根因修复 2026-10-08):rebuild 有全板淡入
# motion(0.2)+R0 涟漪逐格延迟显形(motion(0.04×离板心距离)+motion(0.12),最远格 ≈0.56s),
# 原制 6 个 process_frame 热机后 ≈0.1s=捕在涟漪中途——逐跑帧时序抖动使揭示 alpha 漂移,
@@ -66,21 +94,124 @@ func _ready() -> void:
await get_tree().create_timer(1.2).timeout
await get_tree().process_frame
await RenderingServer.frame_post_draw
var img := get_viewport().get_texture().get_image()
if img == null or img.is_empty():
var on_img := get_viewport().get_texture().get_image()
if on_img == null or on_img.is_empty():
push_error("[board_shot] FAIL — 抓帧空纹理: %s(Q7 取证语义=闸红)" % tag)
get_tree().quit(1)
return
var on_path := "%s/board_%s_glow_on.png" % [out, tag]
on_img.save_png(on_path)
# 作用集快照必须在关开关之前取(set_type_glow_enabled(false) 会清空 _glow 重算)
var glow_cells: Dictionary = (view.type_glow_probe().get("cells", {}) as Dictionary).duplicate()
# 帧 B = 微光关(golden 基线保护面——与基线同文件名落盘,golden 对比取本帧)
view.set_type_glow_enabled(false)
await get_tree().process_frame
await RenderingServer.frame_post_draw
var off_img := get_viewport().get_texture().get_image()
if off_img == null or off_img.is_empty():
push_error("[board_shot] FAIL — 抓帧空纹理: %s_off(Q7 取证语义=闸红)" % tag)
get_tree().quit(1)
return
var path := "%s/board_%s.png" % [out, tag]
img.save_png(path)
off_img.save_png(path)
_meta[tag] = {"x": view.global_position.x, "y": view.global_position.y,
"w": view.size.x, "h": view.size.y}
print("[board_shot] saved %s (%dx%d)" % [path, img.get_width(), img.get_height()])
print("[board_shot] saved %s (%dx%d) + %s" % [path, off_img.get_width(), off_img.get_height(), on_path])
_check_dual_frames(tag, view, glow_cells, off_img, on_img)
view.queue_free()
await get_tree().process_frame
var mf := FileAccess.open(out.path_join("board_meta.json"), FileAccess.WRITE)
if mf != null:
mf.store_string(JSON.stringify(_meta))
mf.close()
print("[board_shot] done")
get_tree().quit(0)
var rf := FileAccess.open(out.path_join("board_dual_frame_readings.json"), FileAccess.WRITE)
if rf != null:
rf.store_string(JSON.stringify(_level_readings, " "))
rf.close()
if _problems.is_empty():
print("[board_shot] PASS — 三关双帧取证:非生效格零变化/生效格内缘带外零侵入/生效格实绘非空;golden 帧=微光关态(契约 1 基线保护面)")
get_tree().quit(0)
else:
for p in _problems:
push_error("[board_shot] FAIL — " + p)
print("[board_shot] FAIL — " + "; ".join(_problems))
get_tree().quit(1)
# 身份解析回调(生产同源面——main.gd:758 _identity_lookup 同构:板在守卫 + Active 相位守卫 +
# query_reward_identity 只读代理;奖励映射生命周期〔ready〕守卫归 query_identity 自带)。
# 相位为何是 Active:每轮 settle_batch 末尾 _exit_distribution 已 notify_round_start 兑现下轮
# AP ⇒ 18 步 novice 驱动后相位回 Active,即生产「挖掘期看板」态;本回调零玩法数据写入
# (🔴 契约 1——只读投影面)。
func _probe_identity_lookup(c: Vector2i):
if _cur_settlement == null or _cur_settlement.minesweeper.current_board() == null:
return null
if String(_cur_settlement.action_economy.phase) != ActionEconomy.PHASE_ACTIVE:
return null
return _cur_settlement.query_reward_identity(c)
# 双帧逐像素比对(迭代 30 卡① T2-④-①②):所有变化像素必须落在生效格的内缘带内
# (de < cell÷8——带外含砖心零侵入=禁整格染色/禁越圈);每个生效格须有实绘像素(不空转)。
func _check_dual_frames(tag: String, view, glow_cells: Dictionary, off_img: Image, on_img: Image) -> void:
var board = view.board()
if board == null:
_problems.append("%s 双帧比对取板失败(view.board() 为 null)" % tag)
return
var w: int = off_img.get_width()
var h: int = off_img.get_height()
var cell: float = view.cell_size
var band_px: float = cell * 0.125
# 逐格窗口矩形 + 到格缘距离判据(与 type_glow_shot_probe 同口径)
var rects := {}
for gy in range(board.height):
for gx in range(board.width):
var c := Vector2i(gx, gy)
rects[c] = view.cell_rect_probe(c)
var changed_total := 0
var changed_outside := 0
var first_outside := ""
var glow_hits := {}
for c in glow_cells:
glow_hits[c] = 0
for gy in range(board.height):
for gx in range(board.width):
var c2 := Vector2i(gx, gy)
var r: Rect2 = rects[c2]
if r.size.x <= 0.0:
continue
var is_glow: bool = glow_cells.has(c2)
var x0: int = int(floor(r.position.x))
var y0: int = int(floor(r.position.y))
var x1: int = int(ceil(r.position.x + r.size.x))
var y1: int = int(ceil(r.position.y + r.size.y))
for yy in range(maxi(0, y0), mini(h, y1)):
for xx in range(maxi(0, x0), mini(w, x1)):
var de: int = mini(mini(xx - x0, x1 - 1 - xx), mini(yy - y0, y1 - 1 - yy))
var p1: Color = off_img.get_pixel(xx, yy)
var p2: Color = on_img.get_pixel(xx, yy)
if p1.r == p2.r and p1.g == p2.g and p1.b == p2.b and p1.a == p2.a:
continue
changed_total += 1
if is_glow:
if float(de) < band_px:
glow_hits[c2] = int(glow_hits[c2]) + 1
else:
changed_outside += 1
if first_outside == "":
first_outside = "%s 带外(de=%d≥%.0f)" % [str(c2), de, band_px]
else:
changed_outside += 1
if first_outside == "":
first_outside = "%s 非生效格" % str(c2)
_level_readings[tag] = {"glow_cells": glow_cells.size(), "changed_total": changed_total,
"changed_outside": changed_outside, "band_px": band_px}
print("[board_shot] %s 双帧读数 glow=%d changed=%d outside=%d band=%.0fpx"
% [tag, glow_cells.size(), changed_total, changed_outside, band_px])
if changed_outside != 0:
_problems.append("%s 非生效格/带外像素变化 %d 个(首例 %s——禁整格染色/禁越圈)" % [tag, changed_outside, first_outside])
if glow_cells.size() > 0 and changed_total == 0:
_problems.append("%s 微光开帧整板零变化(微光未生效)" % tag)
for c in glow_cells:
if int(glow_hits[c]) == 0:
_problems.append("%s 生效格 %s 零实绘像素(微光空转)" % [tag, str(c)])
+261
View File
@@ -0,0 +1,261 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 未开格类型微光渲染取证探针(迭代 30 卡① T2 像素侧——V4 禁侵/V5 不压破/V6 五组互辨)。
# 必须**窗口化**运行(headless 无渲染面,抓帧恒空):
# Godot_console.exe --path . res://smoke/type_glow_shot_probe.tscn -- <输出目录>
# 流程:起一板(真实 LevelRunner + settle_batch 驱动 18 步 novice——自然揭示数字/旗)→ 注入夹具身份回调
# (九 cell_type 循环 ⇒ 五组同屏齐现)→ BoardView 真渲染:
# 帧 A = 微光关(基线态)→ type_glow_off.png
# 帧 B = 微光开(生产默认态)→ type_glow_on.png
# 逐像素比对两帧:① 所有变化像素必须落在生效格的内缘带内(非生效格零变化=禁整格染色/禁越圈)
# ② 五组带内取样两两灰度对比对 CR ≥ GRAY_CR_MIN(色弱互辨通道)。
# 零存档触碰(无 save_manager 引用)。取证件落调用方给的目录(库外),不入 git。
extends Node
const AutoPlayer := preload("res://smoke/auto_player.gd")
# 夹具:九 cell_type 全值域(地图战斗资源九型枚举同源)
const FIXTURE_TYPES := ["attack", "defense", "blast", "heal", "weaken",
"pierce", "magic", "action", "gp"]
# 8 邻方位 → 九型确定性指派(V6 五组同屏齐现的取证前提——组间互辨要求一帧内五组俱在)。
# 方位序:NW N NE W E SW S SE;探针起板后 N/NE/E 三位被 novice 挖开(恒定 seed 确定性),
# 故五组各占一个存活方位:NW=BUFF/W=PIERCE/SW=AOE/S=MAGIC/SE=HEAL,其余三位补 BUFF。
# 同屏组数不足 5 = 取证失败(见 _report 硬断言)——幸存集若变,探针报红而非静默少测。
const FIXTURE_DIR_TYPES := ["attack", "gp", "defense", "pierce",
"weaken", "blast", "magic", "heal"]
# 判据阈值(provisional——首版取严;实现报告登记,复核转正归战斗一屏样张批)
const CORE_PAD_FRAC := 0.20
const GRAY_CR_MIN := 1.20
var _hero_pos := Vector2i.ZERO
func _ready() -> void:
if DisplayServer.get_name() == "headless":
print("[type_glow_shot] FAIL — 本探针必须窗口化运行(headless 无渲染面,截图必空)")
get_tree().quit(1)
return
var args := OS.get_cmdline_user_args()
var out := args[0] if args.size() >= 1 and args[0] != "" else "user://type-glow-shots"
DirAccess.make_dir_recursive_absolute(out)
await get_tree().process_frame
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", true):
push_error("[type_glow_shot] 表加载失败")
get_tree().quit(1)
return
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(421001)
var runner = load("res://core/level_runner.gd").new(db, rng)
if not runner.start_level(1, 421001, {}, 0):
push_error("[type_glow_shot] 开局失败")
get_tree().quit(1)
return
var settlement = runner.current_battle()
for i in range(18):
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
var kind := String(act.get("kind", "stop"))
if kind == "stop":
break
var op := kind
if kind == "flag":
op = "flag_place"
settlement.minesweeper.settle_batch([{"action": op, "cell": act.get("cell")}])
var view = load("res://presentation/views/board_view.gd").new()
view.size = Vector2(900, 900)
view.position = Vector2(40, 40)
add_child(view)
# 冻结接触带呼吸(减动效路径 motion_scale<1.0 ⇒ _apply_contact_bands 施静态色):
# 两帧比对要隔离「类型微光开关」这一单一变量,英雄格接触带相位漂移会污染非生效格零变化判据。
# 必须在 rebuild/set_dig_glow_enabled 之前置位——那两处会按当时相位重施带色。
UiKit.motion_scale = 0.5
var ui_font: Font = load("res://presentation/views/ui_kit.gd").load_font(load("res://presentation/views/ui_kit.gd").FONT_UI)
if ui_font != null:
view.set_world_font(ui_font)
# 夹具身份回调(返回形状与 query_identity 同构;8 邻按方位确定性指派,五组同屏齐现)
view.set_identity_resolver(_fixture_identity)
_hero_pos = settlement.minesweeper.hero_pos()
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
# 挖掘相位:接触带呼吸与类型微光同源门控(_dig_glow);此处显式开以承取证
view.set_dig_glow_enabled(true)
# 作用集快照必须在关开关之前取(set_type_glow_enabled(false) 会清空 _glow 重算)
var glow_snapshot: Dictionary = view.type_glow_probe()
# 抓帧前等入场动画完结(board_shot_probe 同款时间制 1.2s——锚定动画完结态)
await get_tree().create_timer(1.2).timeout
await get_tree().process_frame
await RenderingServer.frame_post_draw
var on_img := get_viewport().get_texture().get_image()
if on_img == null or on_img.is_empty():
push_error("[type_glow_shot] FAIL — 抓帧空纹理(取证失败=闸红)")
get_tree().quit(1)
return
on_img.save_png(out.path_join("type_glow_on.png"))
view.set_type_glow_enabled(false)
await get_tree().process_frame
await RenderingServer.frame_post_draw
var off_img := get_viewport().get_texture().get_image()
if off_img == null or off_img.is_empty():
push_error("[type_glow_shot] FAIL — 抓帧空纹理(取证失败=闸红)")
get_tree().quit(1)
return
off_img.save_png(out.path_join("type_glow_off.png"))
_report(view, glow_snapshot, off_img, on_img, out)
# 逐像素比对与读数:V4(非生效格零变化+砖心零侵入)/V5(灰度分类一致)/V6(五组互辨)
# glow_snapshot = 关开关前的作用集快照(set_type_glow_enabled(false) 会清空 _glow 重算,故必须外传)
func _report(view, glow_snapshot: Dictionary, off_img: Image, on_img: Image, out: String) -> void:
var glow_cells: Dictionary = glow_snapshot.get("cells", {})
var bad: Array[String] = []
var readings := {}
if not bool(glow_snapshot.get("enabled", false)):
bad.append("微光开关态非默认开")
if glow_cells.is_empty():
bad.append("微光作用集为空(英雄 8 邻未开格未派生出组号)")
var board = view.board()
var w: int = off_img.get_width()
var h: int = off_img.get_height()
var cell: float = view.cell_size
var band_px: float = cell * 0.125
var core_pad: float = cell * CORE_PAD_FRAC
var changed_total := 0
var changed_outside := 0
var by_group := {}
if board != null:
for gy in range(board.height):
for gx in range(board.width):
var c := Vector2i(gx, gy)
var r: Rect2 = view.cell_rect_probe(c)
if r.size.x <= 0.0:
continue
var is_glow: bool = glow_cells.has(c)
var g: int = int(glow_cells[c]) if is_glow else 0
var x0: int = int(floor(r.position.x))
var y0: int = int(floor(r.position.y))
var x1: int = int(ceil(r.position.x + r.size.x))
var y1: int = int(ceil(r.position.y + r.size.y))
var gsum := 0.0
var gn := 0
for yy in range(maxi(0, y0), mini(h, y1)):
for xx in range(maxi(0, x0), mini(w, x1)):
var de: int = mini(mini(xx - x0, x1 - 1 - xx), mini(yy - y0, y1 - 1 - yy))
var p1: Color = off_img.get_pixel(xx, yy)
var p2: Color = on_img.get_pixel(xx, yy)
if is_glow and float(de) < band_px:
gsum += _lum(p2)
gn += 1
if p1.r == p2.r and p1.g == p2.g and p1.b == p2.b and p1.a == p2.a:
continue
changed_total += 1
if (not is_glow) or float(de) >= core_pad - 1.0:
changed_outside += 1
if is_glow and gn > 0 and not by_group.has(g):
by_group[g] = gsum / float(gn)
readings["glow_cells"] = glow_cells.size()
readings["groups"] = _sorted_keys(by_group)
readings["changed_total"] = changed_total
readings["changed_outside"] = changed_outside
readings["band_lum"] = by_group
var rects := {}
for c in glow_cells:
var rr: Rect2 = view.cell_rect_probe(c)
rects[str(c)] = [rr.position.x, rr.position.y, rr.size.x, rr.size.y]
readings["rects"] = rects
if changed_total == 0:
bad.append("开微光后整帧零像素变化(微光未生效)")
if changed_outside != 0:
bad.append("非生效格像素变化 %d 个(禁整格染色/禁越圈)" % changed_outside)
if by_group.size() < 5:
bad.append("同屏组数 %d < 5(V6 五组并列互辨的取证前提不成立)" % by_group.size())
var ks: Array = _sorted_keys(by_group)
var pairs: Array[String] = []
var worst_cr := 99.0
var worst_pair := ""
for i in range(ks.size()):
for j in range(i + 1, ks.size()):
# WCAG 相对亮度对比对:band 均值先经 sRGB→线性反伽马(+0.05 偏移定义在线性域),
# 与 tools/shot_checker.py 的 WCAG 段同口径(色弱互辨通道的机检同款公式)。
var la: float = _to_linear(float(by_group[ks[i]]))
var lb: float = _to_linear(float(by_group[ks[j]]))
var cr: float = (maxf(la, lb) + 0.05) / (minf(la, lb) + 0.05)
pairs.append("组%d/组%d CR=%.3f ΔL*=%.1f" % [ks[i], ks[j], cr, absf(_lstar(la) - _lstar(lb))])
if cr < worst_cr:
worst_cr = cr
worst_pair = "组%d/组%d" % [ks[i], ks[j]]
readings["pairs"] = " ".join(pairs)
readings["worst_cr"] = worst_cr
readings["worst_pair"] = worst_pair
if ks.size() >= 2 and worst_cr < GRAY_CR_MIN:
bad.append("灰度互辨最弱对 %s CR=%.3f < %.3f(色弱通道不可分)" % [worst_pair, worst_cr, GRAY_CR_MIN])
var mf := FileAccess.open(out.path_join("type_glow_readings.json"), FileAccess.WRITE)
if mf != null:
mf.store_string(JSON.stringify(readings, " "))
mf.close()
print("[type_glow_shot] 读数 %s" % JSON.stringify(readings))
if bad.is_empty():
print("[type_glow_shot] PASS — 非生效格零变化/砖心零侵入/五组灰度互辨全过")
get_tree().quit(0)
else:
for m in bad:
print("[type_glow_shot] FAIL — %s" % m)
get_tree().quit(1)
# 夹具身份回调(形状与 query_identity 返回同构;8 邻按方位取九型之一,其余格按坐标兜底)
func _fixture_identity(c: Vector2i) -> Variant:
var ctype: String
var di: int = _dir_index(c - _hero_pos)
if di >= 0:
ctype = FIXTURE_DIR_TYPES[di]
else:
ctype = FIXTURE_TYPES[int(abs(c.x * 31 + c.y * 17)) % FIXTURE_TYPES.size()]
return {"reward_id": "fix_%d_%d" % [c.x, c.y], "cell_type": ctype, "tier": 1}
# 方位偏移 → 0..7(NW N NE W E SW S SE;非 8 邻返回 -1)
static func _dir_index(off: Vector2i) -> int:
match off:
Vector2i(-1, -1):
return 0
Vector2i(0, -1):
return 1
Vector2i(1, -1):
return 2
Vector2i(-1, 0):
return 3
Vector2i(1, 0):
return 4
Vector2i(-1, 1):
return 5
Vector2i(0, 1):
return 6
Vector2i(1, 1):
return 7
return -1
static func _sorted_keys(d: Dictionary) -> Array:
var ks: Array = d.keys()
ks.sort()
return ks
# 相对亮度(Rec.709 系数——灰度互辨通道与 shot_checker 同口径)
# 相对亮度(Rec.709 系数——灰度互辨通道与 shot_checker 同口径;输入为帧缓冲 sRGB 值)
static func _lum(c: Color) -> float:
return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b
# sRGB(伽马)值 → 线性相对亮度(WCAG 2.x 反伽马——对比对公式的输入域)
static func _to_linear(y_srgb: float) -> float:
var c: float = clampf(y_srgb, 0.0, 1.0)
if c <= 0.04045:
return c / 12.92
return pow((c + 0.055) / 1.055, 2.4)
# CIE L*(Y∈[0,1] → L*∈[0,100];ΔL* 读数供互辨裕量评估)
static func _lstar(y: float) -> float:
var yy: float = clampf(y, 0.0, 1.0)
if yy <= 0.008856:
return 903.3 * yy
return 116.0 * pow(yy, 1.0 / 3.0) - 16.0
+1
View File
@@ -0,0 +1 @@
uid://djmrucr7nxl5j
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://smoke/type_glow_shot_probe.gd" id="1"]
[node name="TypeGlowShotProbe" type="Node"]
script = ExtResource("1")