12 KiB
2026-10-14
shader/toon_apply.gd 自动刷新可行性咨询(未改动任何文件)
- 项目 Godot 4.7 Forward+。
- 现状:
toon_apply.gd是 @tool Node3D,_apply_materials()对每个子 MeshInstance3Dduplicate()一份 ShaderMaterial 并material_override。 - 结论:
- 保存/编辑
toon_apply.gd本身 → 编辑器热重载 @tool 脚本会重跑_ready(),已能自动刷新(4.3 有热重载失效 bug,4.4+ 已修,本项目 4.7 无此问题)。 - 编辑
shader/toon.tres的 shader 参数(color / uv_scale 等)→ 不会自动刷新。原因:ShaderMaterial 的Resource.changed仅在 resource_name 变化或 shader 本身变化时发出,参数改动不发出(官方文档明确);且每网格是 duplicate 的副本,与模板已无关联。 - 若要 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 种尺寸。
assets180 张:1024×1024 ×108、512×512 ×40、4096×4096 ×25、2048×2048 ×5assets/自建资产18 张:4096×4096 ×13(地板、地毯 PBR 套图)character81 张:32×32 ×70、512×512 ×4、1024×1024 ×1demo28 张: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:保留 duplicate,加 EditorPlugin 监听
已实施:路线 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的 SpotLight3Dsconce:light_energy = 2.655、light_indirect_energy = 2.41world/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同样被忽略。