godot-3d · git:20261024.5ffa874 · 2026-10-24 · sha256 e2d059e94df21906

godot-3d git:20261024.5ffa874A

Immutable. This exact content never changes and is served at /api/v1/blob/e2d059e94df21906.

---
name: godot-3d
description: 搭建或调试 Godot 4 的 3D 场景时用。CharacterBody3D 角色移动、第一 第三人称相机 SpringArm3D、Jolt 物理与碰撞层、RayCast3D 查询、渲染器与光照 GI 选型 SDFGI VoxelGI LightmapGI、阴影、glTF 导入、GridMap、CSG、LOD 与遮挡剔除、MultiMesh、NavigationAgent3D、骨骼修改器 IK。3D scenes, character controllers, lighting, level import.
---

# Godot 3D

3D 场景用哪个节点、哪个资源、哪个项目设置,以及照直觉会写错的地方。着色器、粒子、后处理转 `godot-shaders-vfx`。动画状态机转 `godot-animation`。2D 精灵放进 3D 转 `godot-2d-in-3d`。帧率剖析转 `godot-performance`。

## 先查内置

| 需求 | 用这个 | 别自己写 |
|---|---|---|
| 角色走跑跳 | `CharacterBody3D` + `move_and_slide()` | 手算坡度和地面检测 |
| 推得动的箱子、球 | `RigidBody3D`,一次性冲击 `apply_central_impulse`,持续推力 `apply_central_force` | 每帧改 `global_position` 或 `linear_velocity` |
| 第三人称相机不穿墙 | `SpringArm3D` | 每帧射线加手动拉近 |
| 点选 狙击 视线 | `PhysicsRayQueryParameters3D` + `intersect_ray()` | 遍历所有物体算相交 |
| 前方探测 持续检测 | `RayCast3D` `ShapeCast3D` | 每帧 new 查询参数 |
| 敌人寻路与避让 | `NavigationRegion3D` + `NavigationAgent3D` | 手写 A* 和路点 |
| 大量相同网格 草 石头 | `MultiMeshInstance3D` | 几千个 `MeshInstance3D` |
| 远处降面 | 导入时自动 mesh LOD | 脚本按距离换网格 |
| 远处整组替换 淡出标签 | `GeometryInstance3D.visibility_range_end` | 脚本逐帧算距离 |
| 室内遮挡 | `OccluderInstance3D` | 手动隐藏房间 |
| 弹孔 污渍 假阴影 | `Decal` | 运行时拼贴图网格 |
| 灰盒关卡 | CSG 节点 后转 `MeshInstance3D` | 把 CSG 带进发布版 |
| 网格拼装关卡 | `GridMap` + `MeshLibrary` | 脚本循环摆几百个实例 |
| 头部看向 尾巴发丝摆动 腿部 IK | 骨骼修改器 见下文版本表 | 每帧手改 `Skeleton3D` 骨骼姿态 |
| 天空 雾 泛光 色调映射 | `WorldEnvironment` + `Environment` | 多个 `SubViewport` 堆后处理 |
| 曝光 景深 | `CameraAttributesPractical` | 脚本改亮度 |
| 间接光 | SDFGI VoxelGI LightmapGI | 到处摆补光灯 |
| 导入后加碰撞 | 名字后缀 `-col` 或高级导入设置 | 导入后脚本遍历加 `CollisionShape3D` |
| 高刷新率下相机抖动 | `physics/common/physics_interpolation` | 自己在 `_process` 里插值 |

## 做法

### 角色移动

```gdscript
extends CharacterBody3D

@export var speed: float = 5.0
@export var jump_velocity: float = 4.5

func _physics_process(delta: float) -> void:
	if not is_on_floor():
		velocity += get_gravity() * delta
	elif Input.is_action_just_pressed("jump"):
		velocity.y = jump_velocity

	var input: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
	var direction: Vector3 = global_basis * Vector3(input.x, 0.0, input.y)
	velocity.x = direction.x * speed
	velocity.z = direction.z * speed
	move_and_slide()
```

`velocity` 是每秒速度,不乘 `delta`。`get_vector` 的长度已限制为 1,不再 `normalized()`,摇杆轻推保持慢速;`is_on_floor()` 读的是上一次 `move_and_slide()` 的结果。相机相对移动时用相机的 yaw 基底代替 `global_basis`。

| 属性 | 默认 | 说明 |
|---|---|---|
| `floor_max_angle` | 0.7853982 | 弧度 45 度 超过算墙 |
| `floor_snap_length` | 0.1 | 下坡贴地 上升速度大于 0 时不吸附 |
| `floor_stop_on_slope` | true | 站着不下滑 |
| `floor_constant_speed` | false | 上下坡速度一致 需配合 `floor_snap_length` |
| `floor_block_on_wall` | true | 只在地面上移动 不能爬墙 但会沿墙下滑 |
| `wall_min_slide_angle` | 0.2617994 | 弧度 15 度 撞墙角度小于此值不滑 Grounded 下需 `floor_block_on_wall` 为 true 才生效 |
| `max_slides` | 6 | 一次 `move_and_slide()` 最多换向次数 |
| `safe_margin` | 0.001 | 碰撞恢复余量 大了更稳地识别墙和地面 小了更精确 极小缩放或叠放多个角色时用小值 |
| `motion_mode` | 0 | `MOTION_MODE_GROUNDED` 平台类 `MOTION_MODE_FLOATING` 太空与俯视 |
| `platform_on_leave` | 0 | `PLATFORM_ON_LEAVE_ADD_VELOCITY` 离开移动平台时带上平台速度 另有 `ADD_UPWARD_VELOCITY` `DO_NOTHING` |

没有内置台阶。需要上台阶时用斜面碰撞体,或用 `ShapeCast3D` 自己做台阶检测。重力方向不是 Y 轴时改 `up_direction`。

### 物理引擎

| 引擎 | 状态 |
|---|---|
| GodotPhysics3D | 设置值 `DEFAULT` 在 4.7 仍等于它 老项目升级后保持 |
| Jolt Physics | 4.4 起内置模块 4.6 起新建项目默认选用 通常更快更稳 |

```ini
[physics]

3d/physics_engine="Jolt Physics"
```

新建项目默认就是它。从 4.5 及更早升级的项目手动改,改完重启编辑器。切换后关节软限制属性不支持,只设一个物体的关节默认把它当 `node_b`,射线 `face_index` 默认 -1,其余差异见 [references/physics-3d.md](references/physics-3d.md)。

### 碰撞层

先列碰撞对再分层,在 `layer_names/3d_physics/layer_1` 到 `layer_32` 里起名。

| 层 | 内容 | 它的 mask 勾选 |
|---|---|---|
| 1 world | 静态关卡 | 无 |
| 2 player | 玩家身体 | 1 3 |
| 3 enemy | 敌人身体 | 1 2 3 |
| 4 player_hit | 玩家子弹与近战判定 `Area3D` | 3 |
| 5 enemy_hit | 敌人攻击判定 `Area3D` | 2 |
| 6 pickup | 拾取物 `Area3D` | 无 玩家的 `Area3D` 勾 6 |
| 7 camera_block | 只给 `SpringArm3D` 用的墙体 | 无 |

```gdscript
extends CharacterBody3D

const LAYER_WORLD: int = 1 << 0
const LAYER_PLAYER: int = 1 << 1
const LAYER_ENEMY: int = 1 << 2

@export_flags_3d_physics var hit_mask: int = LAYER_ENEMY

func _ready() -> void:
	collision_layer = LAYER_PLAYER
	collision_mask = LAYER_WORLD | LAYER_ENEMY
```

`collision_layer` 是我在哪层,`collision_mask` 是我扫描哪层,`set_collision_mask_value(n, true)` 按层号开关单层。位掩码第 n 层是 `1 << (n - 1)`。`Area3D` 检测不到谁,先查对方的 layer 是否在自己的 mask 里。

### 空间查询

| 需求 | 用这个 |
|---|---|
| 固定方向持续探测 地面 前方墙 | `RayCast3D` 节点 每物理帧自动更新 |
| 同一物理帧改参数后立刻读 | `RayCast3D.force_raycast_update()` |
| 点选 hitscan 一次性射线 | `intersect_ray()` |
| 有厚度的探测 扫过一个形状 | `ShapeCast3D` 或 `cast_motion()` |
| 某位置的范围内有谁 | `intersect_shape()` |
| 两个形状是否相交与接触点 | `collide_shape()` 与 `get_rest_info()` |

```gdscript
extends Camera3D

const RAY_LENGTH: float = 1000.0

func pick(mouse_position: Vector2) -> Dictionary:
	var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
	var from: Vector3 = project_ray_origin(mouse_position)
	var to: Vector3 = from + project_ray_normal(mouse_position) * RAY_LENGTH
	var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, to, 1 << 2)
	query.collide_with_areas = true
	return space.intersect_ray(query)
```

结果是字典,没命中为空,命中有 `position` `normal` `collider` `rid` `shape`。

C# 版本和 `Dictionary` 取值写法见 [references/physics-3d.md](references/physics-3d.md)。

`direct_space_state` 只能在 `_physics_process()` 里访问,其他时机可能报 space 被锁。在 `_input()` 里记下鼠标位置,下一个物理帧再查询。

### 第三人称相机

节点结构:`Node3D` 枢轴,下面 `SpringArm3D`,下面 `Camera3D`。第一人称不要弹簧臂,`Camera3D` 放在身体下的 `Node3D` 头节点,yaw 转身体,pitch 转头节点,鼠标写法同下。`spring_length` 默认 1.0,按需要的臂长改。`SpringArm3D.shape` 留空且相机是直接子节点时,用相机近平面做形状;留空但相机不是直接子节点会退成射线,不推荐。给了形状就一直用该形状,球形最顺滑。

```gdscript
extends Node3D

@export var mouse_sensitivity: float = 0.003

@onready var _arm: SpringArm3D = $SpringArm3D

func _ready() -> void:
	var body: CollisionObject3D = get_parent() as CollisionObject3D
	if body != null:
		_arm.add_excluded_object(body.get_rid())

func _unhandled_input(event: InputEvent) -> void:
	var motion: InputEventMouseMotion = event as InputEventMouseMotion
	if motion == null or Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
		return
	rotate_y(-motion.screen_relative.x * mouse_sensitivity)
	_arm.rotation.x = clampf(_arm.rotation.x - motion.screen_relative.y * mouse_sensitivity, -1.2, 1.2)
```

弹簧臂 `collision_mask` 默认只有第 1 层。玩家碰撞层落在它的 mask 里又没排除时,起点在玩家碰撞体外、朝玩家扫去的弹簧臂会被自己顶短,如枢轴高过头顶再仰视。起点已在碰撞体内的重叠会被忽略。上面的层表里玩家在第 2 层,保持 mask 只勾 world 层也能避开。

### 渲染器

| 选这个 | 条件 |
|---|---|
| Forward+ | 桌面 3D 要 SDFGI VoxelGI SSR SSIL 体积雾 |
| Mobile | 手机 一体机 XR 要 `CompositorEffect` 要光照投影 |
| Compatibility | 网页 老硬件 简单 3D 与 2D |

能力差异表见 [references/rendering-lighting.md](references/rendering-lighting.md)。导出到网页只能用 Compatibility。切换渲染器后场景和环境要重新调。

### 光照与 GI 选型

| 技术 | 渲染器 | 实时性 | 动态物体 | 工作量 |
|---|---|---|---|---|
| 无 GI 加环境光 | 全部 | 是 | 是 | 无 |
| `ReflectionProbe` | 全部 | 更新模式 Once 或 Always | 接收 | 手摆 |
| `LightmapGI` | 全部渲染 烘焙需 RenderingDevice | 否 | 靠 `LightmapProbe` | UV2 加烘焙 |
| `VoxelGI` | 仅 Forward+ | 是 动态物体可发光 | 是 | 摆节点加烘焙 几秒 |
| SDFGI | 仅 Forward+ | 半实时 | 只接收 | 环境里开关 |
| SSIL | 仅 Forward+ | 是 | 是 | 环境里开关 只当补充 |

桌面项目先开 SDFGI,不够再换。手机只能 `LightmapGI` 加 `ReflectionProbe`。性能从快到慢:`ReflectionProbe`,`LightmapGI`,`VoxelGI`,SSIL,SDFGI。网格的 `gi_mode` 默认 Static,会参与 SDFGI 与光照图。小型动态物体设 Disabled,大型动态物体如移动的列车设 Dynamic,Dynamic 只对 VoxelGI 有效。灯的烘焙模式细节见参考文件。

### 环境与相机属性

- `WorldEnvironment` 每个场景树只放一个。`Camera3D.environment` 优先级更高
- `Environment.tonemap_mode` 默认 Linear,亮部直接截断。换 Filmic 或 ACES,4.4+ 有 AgX。写实场景 `tonemap_white` 至少 6.0,AgX 改用 `tonemap_agx_white`,Mobile 的 `tonemap_white` 要 2.0 以下
- `Camera3D.attributes` 优先于 `WorldEnvironment` 上的 `CameraAttributes`。拿不准用 `CameraAttributesPractical`,`CameraAttributesPhysical` 会覆盖相机的 `fov` `near` `far` `keep_aspect`

### 场景导入

glTF 2.0 优先,`.glb` 是单文件。`.blend` 直接导入要求每个人装 Blender。OBJ 没有骨骼动画 PBR UV2。

- 在 Blender 里给对象改名带后缀:`-col` 三角网碰撞,`-convcol` 凸包,`-colonly` `-convcolonly` 只留碰撞,`-rigid` 刚体,`-navmesh`,`-occ` `-occonly`,`-noimp` 跳过,完整清单含 `-vehicle` `-wheel` `-alpha` `-vcol`。后缀大小写不敏感,连接符可以是 `-` `$` `_`
- 动画片段名以 `loop` 或 `cycle` 开头或结尾,导入时自动循环,不需要连接符
- 改材质:高级导入设置 Actions 里 Extract Materials,之后改外部 `.tres`,重导入不覆盖
- 加节点:新建继承场景,基础场景的节点不能删。只挂根脚本用导入选项 `nodes/root_script`
- 要碰撞:优先用后缀,其次高级导入设置的 Generate Physics,静态关卡 Trimesh,动态物体用基本形状

详细流程见 [references/import-and-levels.md](references/import-and-levels.md)。

### 关卡构建

- `GridMap` 需要 `MeshLibrary`。根节点 `Node3D`,每个子节点是 `MeshInstance3D`,材质放在网格里不放在节点上,碰撞放在子 `StaticBody3D`。用 Scene 菜单 Export As MeshLibrary 导出
- CSG 只做灰盒。要光照图或遮挡烘焙时先转成 `MeshInstance3D`,CSG 菜单里 Bake Mesh Instance,4.4+。`CSGShape3D.use_collision` 默认关

### 剔除与 LOD

- 导入的 glTF 默认生成 mesh LOD,OBJ 要先把导入类型改为 Scene
- 手做的多级模型用 `visibility_range_begin` `visibility_range_end` 与 `visibility_range_end_margin`。`visibility_range_fade_mode` 选 Disabled 性能最好,Self 与 Dependencies 用透明渲染做淡入淡出且只在 Forward+ 有效
- 遮挡剔除先开 `rendering/occlusion_culling/use_occlusion_culling`(默认关,网页导出模板不支持),加 `OccluderInstance3D` 烘焙。只烘 `MeshInstance3D`,动态物体放到别的可见层并在烘焙剔除掩码里去掉
- `MultiMeshInstance3D`:`transform_format` `use_colors` `use_custom_data` 必须先于 `instance_count` 设置,整个节点是一个剔除单位,实例散得太开要拆成多个节点
- `Decal` 的 `size` 里 Y 是投影深度,越短越利于剔除

LOD、遮挡剔除、`MultiMesh`、`Decal` 的完整做法见 [references/culling-and-instancing.md](references/culling-and-instancing.md)。

### 导航

```gdscript
extends CharacterBody3D

@export var speed: float = 4.0

@onready var _agent: NavigationAgent3D = $NavigationAgent3D

func go_to(target: Vector3) -> void:
	_agent.target_position = target

func _physics_process(_delta: float) -> void:
	if NavigationServer3D.map_get_iteration_id(_agent.get_navigation_map()) == 0:
		return
	if _agent.is_navigation_finished():
		return
	var next: Vector3 = _agent.get_next_path_position()
	velocity = global_position.direction_to(next) * speed
	move_and_slide()
```

场景里放 `NavigationRegion3D`,给它 `NavigationMesh`,下面放参与烘焙的网格,编辑器里点 Bake。`NavigationMesh.cell_size` 要与 `navigation/3d/default_cell_size` 一致。运行时烘焙用 `bake_navigation_mesh(true)`。导航地图默认在后台线程同步,`navigation/world/map_use_async_iterations` 为 true,进场头几帧 `map_get_iteration_id()` 为 0,上面的示例先判断它。`get_next_path_position()` 每物理帧只调一次,到达后不要再调。避让要开 `avoidance_enabled`、连 `velocity_computed`、给 `velocity` 赋值,并且必须设过 `target_position`。

### 骨骼修改器

作为 `Skeleton3D` 的子节点,在动画之后运行。`influence` 做淡入淡出,`active` 开关。

| 需求 | 节点 | 版本 |
|---|---|---|
| 头 炮塔看向目标 | `LookAtModifier3D` | 4.4+ |
| 头发 布料 尾巴 | `SpringBoneSimulator3D` 加 `SpringBoneCollision3D` | 4.4+ |
| 跨骨架重定向 | `RetargetModifier3D` | 4.4+ |
| 复制 转换 对齐某骨骼 | `CopyTransformModifier3D` `ConvertTransformModifier3D` `AimModifier3D` | 4.5+ |
| 手臂 腿两段 IK | `TwoBoneIK3D` | 4.6+ |
| 任意长度链 IK | `FABRIK3D` `CCDIK3D` `JacobianIK3D` | 4.6+ |
| 沿路径摆骨 | `SplineIK3D` | 4.6+ |

`SkeletonIK3D` 已弃用,新项目用上表。详细见 [references/skeleton-modifiers.md](references/skeleton-modifiers.md)。

## 陷阱

- `RayCast3D` 的 `collision_mask` 默认只有第 1 层,`PhysicsRayQueryParameters3D` 默认全部层
- 射线默认不打 `Area3D`,要设 `collide_with_areas`。射线从碰撞体内部出发默认不命中该体,要 `hit_from_inside`
- `RayCast3D` 默认 `exclude_parent` 为 true,会排除父级碰撞体。`PhysicsRayQueryParameters3D` 要自己填 `exclude`
- 不要平移 旋转 缩放 `CollisionShape3D`,会失去物理引擎的内部优化,改形状本身的尺寸
- 三角网碰撞 `ConcavePolygonShape3D` 只能用在静态体,角色与刚体用基本形状或凸包
- `PhysicsBody3D` 只认直接子级的 `CollisionShape3D`
- `CharacterBody3D` 撞上 `RigidBody3D` 不会推动它,Jolt 与 GodotPhysics3D 实测一致。`move_and_slide()` 之后遍历 `get_slide_collision()`,每个物理帧对 `get_collider()` 得到的刚体调 `apply_central_force`,方向取碰撞法线的反方向
- 角色在斜坡抖动或飞出:先看 `floor_snap_length`,再看是否在 `_process` 里移动
- 高刷新率下物理对象的画面抖动:开 `physics/common/physics_interpolation`,瞬移后调 `reset_physics_interpolation()`
- 阴影缺失或漂浮:`Light3D.shadow_normal_bias` 先于 `shadow_bias` 调。`DirectionalLight3D.directional_shadow_max_distance` 设成场景需要的值,越小越清晰越快
- 薄面片不投影或漏光:`GeometryInstance3D.cast_shadow` 选双面
- 灯上不勾 `shadow_enabled` 默认没有阴影。定位灯阴影共用一张图集,数量多时按 `rendering/lights_and_shadows/positional_shadow/atlas_quadrant_0_subdiv` 等分配
- 光照图烘焙时隐藏灯无效,要把灯的 `light_bake_mode` 设为 Disabled
- 导入场景用菜单生成的 UV2 会在重载后丢失,在导入面板里设 Light Baking 为 Static Lightmaps
- SDFGI 对已烘的静态网格修改后要相机走远再走近才刷新
- `Environment.glow_enabled` 只对亮度超过 `glow_hdr_threshold`(默认 1.0)的像素生效,暗部要加 `glow_bloom`,Mobile 把阈值降到 0.9
- 在 `_ready()` 里立刻取导航路径可能为空,等地图第一次同步

## 参考

- [references/rendering-lighting.md](references/rendering-lighting.md) 渲染器差异 灯 阴影 GI 烘焙
- [references/import-and-levels.md](references/import-and-levels.md) 导入 GridMap CSG 继承场景
- [references/culling-and-instancing.md](references/culling-and-instancing.md) LOD 遮挡 MultiMesh Decal
- [references/skeleton-modifiers.md](references/skeleton-modifiers.md) 骨骼修改器与版本
- https://docs.godotengine.org/en/stable/tutorials/3d/index.html
- https://docs.godotengine.org/en/stable/tutorials/physics/using_jolt_physics.html
- https://docs.godotengine.org/en/stable/tutorials/rendering/renderers.html