Files
2026-10-15 16:56:45 +08:00

12 KiB
Raw Permalink Blame History

2026-10-14

shader/toon_apply.gd 自动刷新可行性咨询(未改动任何文件)

  • 项目 Godot 4.7 Forward+。
  • 现状:toon_apply.gd 是 @tool Node3D,_apply_materials() 对每个子 MeshInstance3D duplicate() 一份 ShaderMaterial 并 material_override。
  • 结论:
    1. 保存/编辑 toon_apply.gd 本身 → 编辑器热重载 @tool 脚本会重跑 _ready(),已能自动刷新(4.3 有热重载失效 bug,4.4+ 已修,本项目 4.7 无此问题)。
    2. 编辑 shader/toon.tres 的 shader 参数(color / uv_scale 等)→ 不会自动刷新。原因:ShaderMaterial 的 Resource.changed 仅在 resource_name 变化或 shader 本身变化时发出,参数改动不发出(官方文档明确);且每网格是 duplicate 的副本,与模板已无关联。
    3. 若要 2 也自动刷新,需 EditorPlugin 监听 EditorInterface.get_inspector().property_edited(或 EditorFileSystem),回调里调用 _apply_materials()。
  • 备选架构:改用 GeometryInstance3D.set_instance_shader_parameter() 的 per-instance uniform,可不 duplicate、直接共享 toon.tres,改参数即实时生效;但限制是不支持贴图/数组、每 shader 上限 16 个。本项目需要 per-mesh 的 base_texture,因此该方案只能覆盖 color 之类的标量/向量参数,贴图仍需 duplicate 或走 Texture2DArray + index。

追加:评估 per-instance uniform + Texture2DArray 方案(结论:不适合本项目)

  • 实测贴图尺寸(脚本扫 PNG 头):全项目 289 张 PNG / 24 种尺寸。
    • assets 180 张:1024×1024 ×108、512×512 ×40、4096×4096 ×25、2048×2048 ×5
    • assets/自建资产 18 张:4096×4096 ×13(地板、地毯 PBR 套图)
    • character 81 张:32×32 ×70、512×512 ×4、1024×1024 ×1
    • demo 28 张:15 种尺寸混杂
  • 结论:Texture2DArray 要求所有层同尺寸/同格式/同 mipmap,只能统一到 4096(单层带 mipmap 约 85 MiB 无损 / 21 MiB VRAM 压缩),几百张即 GB 级显存,且需额外打包流水线 → per-instance uniform 路线不划算,除非贴图少且尺寸统一。
  • 推荐路线(未实施):
    • A:保留 duplicate,加 EditorPlugin 监听 EditorInspector.property_edited,命中 toon.tres 时重刷 _apply_materials()(可加防抖)。
    • B:把公共外观参数(color/specular/fresnel/normal_scale/uv_scale/uv_offset)提升为 global uniform(项目设置 → Shader Globals 定义),调参全局实时生效、无需重刷;贴图与 per-model 覆盖仍留在 duplicate 材质上。代价:需改 cel-shader-base.gdshader / outline.gdshader 并清掉 toon.tres 里的同名参数;全局值会写入 project.godot,需团队一起提交。

已实施:路线 A(新增 addons/toon_refresh 编辑器插件)

用户选择 A。改动:

  • 新增 addons/toon_refresh/plugin.cfg + plugin.gd(EditorPlugin)。
  • project.godot 的 [editor_plugins] enabled 追加 res://addons/toon_refresh/plugin.cfg。

实现要点:

  • 监听 EditorInterface.get_inspector().property_edited,命中 res://shader/toon.tres 或其内联子资源时,0.2s 防抖后遍历当前编辑场景,对脚本文件名为 toon_apply.gd 且有 _apply_materials 的节点调用该方法。
  • 另加 项目 > 工具 菜单项「刷新 toon 材质(当前场景)」作手动兜底。
  • 只影响当前编辑场景;不覆盖手动改过的单个材质副本。

已实测确认的事实(重要,别再靠猜):

  • toon.tres 的 next_pass 内联子资源,resource_path 不是空串,而是 res://shader/toon.tres::ShaderMaterial_m5gih 这种带 :: 后缀的形式 → 匹配逻辑用 begins_with(TARGET + "::")。
  • ShaderMaterial.changed 不会因 set_shader_parameter() 触发;因改 resource_name 触发(与官方文档一致)。
  • 编辑器路径:D:\_yvmou\Godot\Godot_v4.7.2-stable_win64.exe\Godot_v4.7.2-stable_win64.exe(另有 mono 版)。可用 --headless --editor --path E:/Godot/agame --quit 做插件加载冒烟测试;用 --check-only --script res://xxx.gd 做语法校验。
  • 该项目 headless 启动时 godotsteam / limboai 的 GDExtension DLL 会报加载失败(既有问题,与本次改动无关)。

项目相关:[shader_globals] 里已有 diffuse_curve、specular_smoothness、fresnel_smoothness、outline_width、outline_color —— 描边与光照曲线本来就是全局 uniform,调参实时生效;真正需要重刷的是 toon.tres 上的材质级参数。

修复:USE_NORMAL_MAP=1 时"平视全场景变暗、俯瞰才亮"

用户报告的症状。根因是 cel-shader-base.gdshader 里的一行: NORMAL = normal.vector;(原第 121 行)——normal.vector 是切线空间的法线贴图值, 而 Godot 4 的 fragment() 里 NORMAL 是视图空间(官方文档明确写 "inout vec3 NORMAL ... in view space")。把切线空间值写进视图空间的 NORMAL, 等于让所有像素的法线都指向"视图空间 +Z"≈ 朝向相机的一个常量 → 整个场景明暗只由相机朝向决定:平视时与光方向近乎垂直/反向 → 全黑且无明暗起伏; 俯瞰时与向下打的光对齐 → 恢复正常。normal_map_fragment() 里算好的正确视图空间法线 ref_normal 被算出来后就丢掉了(死代码)。

连带问题:normal_map 这个 uniform 从来没被赋过值(toon_apply.gd 只抓 base_texture), 采样到引擎默认纹理后算出的法线同样是垃圾。main2.tscn 里 413 个 toon_apply 节点刷出的 434 个网格中,430 个都没有法线贴图。

改动:

  • shader/includes/normal-map.gdshaderinc:删掉 ref_normal / vector.xy*2-1 / vector.z 重建等死代码,改为返回 NormalData(map, normal_scale),map 就是原始纹理值(切线空间), 蓝通道交给引擎重建;补上坐标空间说明。
  • shader/cel-shader-base.gdshader:USE_NORMAL_MAP 分支只写 NORMAL_MAP = normal.map; NORMAL_MAP_DEPTH = normal.depth;,不再碰 NORMAL。
  • shader/toon_apply.gd:抓 normal_texture(StandardMaterial3D)或 normal_map (ShaderMaterial)→ 有就赋给副本;没有就显式把 normal_scale 设为 0.0,否则会采样默认纹理。

验证(真实场景跑法,见 MEMORY.md):

  • 对照实验确认 dummy 渲染后端仍会报 SHADER ERROR,所以无头运行可校验 shader 语法。
  • 加载 world/main2.tscn 零 shader 错误、零脚本错误。
  • 功能断言:toon_apply 子树 434 个网格全部应用成功;430 个 normal_scale=0.0(无贴图), 4 个 normal_scale=1.0 且 normal_map 已设置(源材质确实带法线贴图)。

遗留待用户确认(未改动):

  • world/main2.tscn 的 model/floor1(不归 toon_apply 管,用自建 surface_material_override/0) 上 normal_scale = -1.7749993243124995,在新语义下等于 NORMAL_MAP_DEPTH = -1.775(反向外插), 几乎不可能是想要的,建议改成 1.0。
  • toon_apply.gd 只读 mesh.surface_get_material(0),忽略了 surface_material_override, 对把材质放在节点覆盖槽上的模型会抓不到贴图(本次未改,属潜在问题)。

排查:亮色模型"像自发光"(未改动任何项目文件)

用户问"没开自发光,为什么颜色较亮时有自发光感"。用 Godot 无头脚本采样截图 (Image.load_from_file + 逐像素统计,临时脚本已删):

  • 红篮区域(25988 样本):37.1% 的像素有通道被钉在 255,均值 (164,57,56)
  • 抽屉顶面(7616 样本):63.0% 触顶,4758 个纯白像素,均值 (212,212,205)
  • 深色柜体正面(10048 样本):仅 3.3% 触顶,均值 (108,89,74)
  • 背景:66452 样本 min==max==(54,72,87) → 纯色平涂,不是天空盒

结论:是通道削顶(clipping),不是自发光。shader 里 USE_EMISSION 0,且 toon.tres 的 specular/fresnel 的 rgb 都是 0 → 唯一光照项就是 ALBEDO × LIGHT_COLOR × 阶梯。 亮色/高饱和色在线性空间里本来就有通道接近 1.0,光照只要 >1 就把它钉死 → 整片变成没有渐变的满色 → 观感就像发光;中等亮度色需要 3 倍以上才触顶,所以正常。 阶梯曲线本身是平的(new_gradient_texture_1d.tres:u<0.127→0,0.14~0.50→0.198, u>0.523→1.0),削顶后连最后一点层次也没了。

项目里的过曝来源候选:

  • world/main2.tscn 的 SpotLight3D sconce:light_energy = 2.655、light_indirect_energy = 2.41
  • world/main.tscn 的 Environment:ambient_light_source = 3(天空)+ 自定义天空 atm_sun_intensity = 18、sun_disk_intensity = 30 → 环境光辐照度可远超 1
  • 矛盾点(可用来定位):main.tscn 的 Environment 是 tonemap_mode = 3(ACES) + tonemap_white = 6.0, ACES 会把高光滚降,理论上几乎不可能出现纯 255(输入 4.0 也只到 ~253)。但截图里大面积纯 255 → 说明被观察的视口没有套用那套 ACES 环境:要么看的是 main2.tscn(自身无 WorldEnvironment、 无 tonemap → 硬削顶),要么编辑器 3D 视口没启用场景环境。
  • 另一个坑:toon.tres 的 color = 0.549 带 source_color,线性空间里只有 0.26, toon 管线把反照率整体压掉约 4 倍 → 亮暗反差被拉得很大。
  • world/main2.tscn 里没有任何 use_custom_color = true,所以不存在自定义颜色覆盖。
  • world/main.tscn 通过 ext_resource 把 main2.tscn 作为子场景实例化。

待用户确认:具体是哪个场景/哪个物体,以便定位"材质来源 + 光源 + 环境"这条链路。

重要:上游 godot4-cel-shader 的法线贴图路径是"从未运行过的死代码"

用户质疑"着色器源自 https://github.com/eldskald/godot4-cel-shader,理应无需修改"。 本地有干净的上游副本 E:/Godot/godot4-cel-shader-main/godot4-cel-shader-main/,比对结果:

  • 本项目改动前的 cel-shader-base.gdshader / includes/normal-map.gdshaderinc 与上游逐字一致(包括 NORMAL = normal.vector; 和 ref_normal 算完即丢)。→ 当时不是本项目改坏的。
  • 但上游 5 个 shader 变体全部是 #define USE_NORMAL_MAP 0,全仓库没有任何一处写成 1, README 里 "normal" 出现 0 次 → 这条路径上游从未启用、从未测试过。
  • 本项目把它开成 1,才第一次走进这段代码,于是坐标空间错误暴露。
  • 结论:只要 USE_NORMAL_MAP 1,那行就必须改;想严格保持上游 1:1,唯一办法是把开关保持 0。
  • Godot 侧证据:引擎自己也会再做一次 NORMAL_MAP 合成 (Godot 源码 clearcoat 等价实现:normalize(tangent*map.x + binormal*map.y + normal*map.z)), 公式里的 normal 就是被污染过的 NORMAL → 污染会传播;且 depth=0 也无法"中和", 因为 mix 的基值本身就是被写坏的 NORMAL。

给用户的三个选项(待其选择): A. USE_NORMAL_MAP 改回 0,并把两个 shader 文件完全还原成上游(代价:实测 434 个网格里那 4 个带法线贴图的会失去效果) B. 只保留 cel-shader-base.gdshader 删掉 NORMAL = normal.vector; 这一行的差异,include 还原上游(1 行差异) C. 保留当前版本(include 里的死代码也清掉,与上游差异最大但最干净)

已实施:用户选择 A

  • 用 git 精确还原:git checkout -- shader/cel-shader-base.gdshader shader/includes/normal-map.gdshaderinc shader/toon_apply.gd (先 git diff 逐 hunk 确认过全部改动都是本次会话产生的,没有混入用户未提交的修改)。
  • 再把 #define USE_NORMAL_MAP 1 改成 0(HEAD 里本来就是 1,是用户自己开的)。
  • 结果:normal-map.gdshaderinc 与上游逐字一致;cel-shader-base.gdshader 与上游的差异 只剩项目自己加的中文注释行(值已与上游相同);toon_apply.gd 无改动。
  • 验证:--headless res://world/main2.tscn --quit-after 60 无 shader / 脚本 / 解析错误。
  • 注意:toon.tres 里仍有 shader_parameter/normal_scale = 1.0,宏关闭时被忽略,无副作用; world/main2.tscn 里 model/floor1 那份自建材质的 normal_scale = -1.775 同样被忽略。