3D模型材质切换一学就会:从整体换肤到局部换装再到动态变色的完整攻略
3D模型材质切换一学就会:从整体换肤到局部换装再到动态变色的完整攻略
【免费下载链接】godotGodot Engine – Multi-platform 2D and 3D game engine项目地址: https://gitcode.com/GitHub_Trending/go/godot
Godot Engine 里 3D 模型的材质切换,是角色换装、状态高亮、昼夜环境变化等玩法绕不开的一环。本文不讲晦涩理论,直接用可运行的 GDScript 示例,带你把"整体换肤、局部换装、动态变色"三件事一次学会,每一段代码都附上"为什么这样写"和"跑起来是什么效果",新手也能照着抄。
先搞懂一件事:材质到底"穿"在模型的哪一层
很多人第一次做换装时,习惯在模型资源(Mesh)里直接改材质,结果发现改完之后,场景里所有用到这个模型的实例全都跟着变了,还找不到原因。这是因为你改的是"共享的底衣",而不是"这一件模型身上的外套"。
在 Godot 里,一个 3D 模型通常由多个表面(Surface)组成,比如角色的头、躯干、四肢可能就是不同的表面。材质的"穿着关系"分三层,优先级从高到低是:
- 全局覆盖:挂在
MeshInstance3D上的material_override,一穿就盖住整个模型的全部表面。 - 表面覆盖:通过
set_surface_override_material设置的逐表面材质,只影响指定的那一块。 - 模型自带材质:保存在 Mesh 资源内部、导入时生成的原始材质,优先级最低。
判断实际生效材质的这段逻辑,就写在引擎源码 scene/3d/mesh_instance_3d.cpp 的get_active_material函数里:先看全局覆盖,再看表面覆盖,最后才落到模型自带的材质上。💡 记住这个顺序,后面所有代码就都好理解了——你做的所有操作,其实都是在往"覆盖层"上放东西,而不是动原始资源。
最省事的一招:material_override 一键给整个模型换皮
如果你的需求是"整个模型一起换"(比如普通状态和受击状态的切换),那用全局覆盖就够了,代码短到没有压力。它就像给模型套一件大披风,什么表面细节都不用管。
extends MeshInstance3D # 在编辑器里把两套材质拖进这两个槽位 @export var style_a: Material @export var style_b: Material var using_b := false func _ready(): # 开场先披上 A 这套 material_override = style_a func _unhandled_input(event): # 按空格键,在 A / B 两套皮肤之间来回切换 if event.is_action_pressed("ui_accept"): using_b = not using_b material_override = style_b if using_b else style_a为什么这样写:material_override是GeometryInstance3D提供的覆盖属性,赋值后只影响当前这一个渲染实例,工程里被导入的原始材质纹丝不动,其他场景里的同类模型也不会被波及。
运行效果:场景跑起来后,按一下空格,模型整体立刻换一种外观;再按一下,又换回来。整个过程没有任何卡顿,因为材质切换只是换了一个资源引用,并不重新编译什么。这也是角色换装系统里最常用、最安全的基础操作。
精确到块的换装:用 set_surface_override_material 单独改某个表面
全局覆盖好用,但遇到"只想给头发换个颜色、衣服保持原样"这种需求就抓瞎了。这时候要动用表面覆盖能力,它允许你按表面序号逐个指定材质,就像给同一件外套的袖口、领口分别换不同的布料。
extends MeshInstance3D # 数组下标和表面序号一一对应,顺序别搞错 @export var surface_style_list: Array[Material] = [] func _ready(): if mesh == null: return var surface_total := mesh.get_surface_count() print("当前模型共 %d 个表面" % surface_total) # 数量对不上就直接提醒,避免越界翻车 if surface_style_list.size() != surface_total: push_warning("材质数量(%d)和表面数量(%d)不一致" % [surface_style_list.size(), surface_total]) return # 循环把每个表面都套上指定材质 for i in surface_total: set_surface_override_material(i, surface_style_list[i]) # 暴露一个对外接口:运行时单独换某一号表面 func swap_single_surface(surface_index: int, new_material: Material) -> void: if mesh == null or surface_index < 0 or surface_index >= mesh.get_surface_count(): push_error("表面序号越界: %d" % surface_index) return set_surface_override_material(surface_index, new_material)为什么这样写:set_surface_override_material会先把材质写进内部的覆盖数组,再同步给渲染服务器(RS::instance_set_surface_override_material),所以运行时调用是即时的。另外,这些覆盖在检查器里会以surface_material_override/0、surface_material_override/1这样的属性出现,你不写代码、直接在面板里拖拽也能手动调,适合做原型验证。
运行效果:_ready时所有表面被一次性套上新材质;之后调用swap_single_surface(0, some_material),只有 0 号表面发生变化,其余表面保持原样。对角色来说,这就意味着身体、衣服、头发可以各换各的,互不干扰。
不换材质也能玩出花:运行时直接改属性实现动态变色
有时候你并不想换材质,而是想让同一块材质"活"起来——比如受伤时皮肤闪红、技能冷却时武器发光。这时可以只修改材质的颜色、金属度、粗糙度等参数。⚠️ 关键一步是先duplicate()一份副本再改,否则你改的是工程里的原始资源,等于把所有用它的模型都改了。
extends MeshInstance3D @export var base_style: StandardMaterial3D var live_style: StandardMaterial3D func _ready(): # 复制一份独立副本,改色时绝不碰原始材质 live_style = base_style.duplicate() as StandardMaterial3D material_override = live_style # 启动无限循环的变色动画 run_color_cycle() func run_color_cycle() -> void: var t := 0.0 while true: # 用正弦余弦让颜色在红蓝之间来回游走 var red := absf(sin(t)) var blue := absf(cos(t)) live_style.albedo_color = Color(red, 0.2, blue) t += get_process_delta_time() await get_tree().process_frame为什么这样写:协程(await get_tree().process_frame)让颜色值逐帧平滑累加,视觉上就是连续的渐变而不是跳变;albedo_color是StandardMaterial3D的核心颜色属性,改它最直观。
运行效果:模型颜色在红色与蓝色之间周期性地柔滑过渡,像呼吸灯一样。把这里的正弦余弦换成噪声、时间戳或者游戏事件,就能做出技能充能、中毒闪烁、昼夜变色等各种动态效果。
实战串联:把三招拼成一个角色换装系统
现在把前面学的合起来,搭一个"身体、头发、衣服三部位独立换装"的小系统。核心思路很简单:每个部位一个MeshInstance3D节点,各自维护一份候选材质列表,按键时在列表里轮换。
extends Node3D # 三个部位节点,按角色实际结构挂好 @onready var body = $MeshInstance3D/Body @onready var hair = $MeshInstance3D/Hair @onready var outfit = $MeshInstance3D/Outfit # 每个部位的候选外观 var body_styles := [preload("res://materials/body_default.tres"), preload("res://materials/body_tanned.tres")] var hair_styles := [preload("res://materials/hair_brown.tres"), preload("res://materials/hair_blonde.tres")] var outfit_styles := [preload("res://materials/outfit_casual.tres"), preload("res://materials/outfit_armor.tres")] # 记录每个部位当前选到第几套 var picked := { "body": 0, "hair": 0, "outfit": 0 } func _unhandled_input(event): # 空格 / Enter / Tab 分别切换三个部位 if event.is_action_pressed("ui_accept"): cycle_part(body, body_styles, "body") elif event.is_action_pressed("ui_select"): cycle_part(hair, hair_styles, "hair") elif event.is_action_pressed("ui_focus_next"): cycle_part(outfit, outfit_styles, "outfit") # 统一的"切到下一套"逻辑,避免三份重复代码 func cycle_part(part: MeshInstance3D, styles: Array, key: String) -> void: picked[key] = (picked[key] + 1) % styles.size() part.material_override = styles[picked[key]]运行效果:按空格换身体肤色、按 Enter 换发型发色、按 Tab 换衣服款式,三个部位互不牵连。把候选材质换成动画、贴图或 ShaderMaterial,这套骨架能直接支撑更复杂的捏脸、时装商城等玩法。
顺带提醒几个实战中的性能要点:能复用的材质就复用,别每帧new;多表面切换尽量在同一帧内一次改完,减少渲染状态抖动;远处角色可以降低材质切换频率或用 LOD 简化。透明材质切换时注意绘制顺序,防止出现多余的半透明层叠。
总结:三招在手,换装自由
回顾一下这次的核心收获:先用material_override做整体换肤,最省事;再用set_surface_override_material做逐表面控制,最精细;最后配合duplicate()加属性修改,让材质"动"起来。三者对应三层覆盖优先级,理解了引擎源码 scene/3d/mesh_instance_3d.cpp 里的查找顺序,你就能随心所欲地组合使用。
想继续深入,可以按这三个方向进阶:
- ShaderMaterial 自定义着色器:用 shader 写出金属溶解、描边、流光等材质切换玩法。
- 材质实例化:用
MaterialInstance3D做参数批量控制,一套材质驱动成百上千个对象。 - PBR 材质体系:吃透物理渲染参数,让换装后的角色在不同光照下都经得起细看。
引擎自带的材质文档也在项目里,随时可以翻:doc/classes/Material.xml。
如果你觉得这篇对你有用,不妨点个赞、收个藏,关注后不迷路。下一篇我会聊聊"程序化生成材质纹理",用代码直接造出可复用的贴图素材,敬请期待!
【免费下载链接】godotGodot Engine – Multi-platform 2D and 3D game engine项目地址: https://gitcode.com/GitHub_Trending/go/godot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
