thedivergentai/godot-adapt-2d-to-3d
Expert patterns for migrating 2D games to 3D including node type conversions, camera systems (third-person, first-person, orbit), physics layer migration, sprite-to-model art pipeline, and control scheme adaptations. Use when porting 2D projects to 3D or adding 3D elements. Trigger keywords: CharacterBody2D to CharacterBody3D, Area2D to Area3D, Camera2D to Camera3D, Vector2 to Vector3, collision_layer migration, sprite to MeshInstance3D, 2D to 3D conversion.
npx skills add https://github.com/thedivergentai/GD-Agentic-Skills --skill godot-adapt-2d-to-3d
Expert guidance for migrating 2D games into the third dimension.
Quaternion for 3D rotation interpolation or the Basis matrix for directional vectors._process to follow a body moving in _physics_process causes jitter. Use Node3D.get_global_transform_interpolated() for smooth transforms.> MANDATORY: Load migration scripts before pasting camera/movement recipes.
MANDATORY third-person SpringArm3D + Camera3D. Do not parent Camera3D bare to the body.
MANDATORY camera-relative CharacterBody3D movement for 2D→3D ports.
MANDATORY checklist: 3D Physics layer names are separate from 2D — mirror names, then apply bits.
Sprite3D billboard configuration and world-to-screen projection for placing 2D UI over 3D objects.
Vector2↔Vector3 mapping helpers (Y-up vs Z-forward pitfalls).
Diegetic / projected UI sharpness patterns.
Billboards, mouse→3D rays, CanvasLayer overlay helpers.
Projects NavigationServer3D paths to 2D screen/gameplay plane for 2.5D sprite actors.
MultiMesh + billboard shader crowd (GPU orientation; not per-node Sprite3D).
> Do NOT Load lighting deep-dives here — route to godot-3d-lighting. Add a DirectionalLight3D + ambient only; GI/cascades live there.
| 2D Node | 3D Equivalent | Notes |
|---------|---------------|-------|
| CharacterBody2D | CharacterBody3D | MANDATORY characterbody3d_migration_movement.gd |
| RigidBody2D | RigidBody3D | Gravity Vector3(0, -9.8, 0) |
| StaticBody2D | StaticBody3D | Shape3D resources (no auto-convert) |
| Area2D | Area3D | Same trigger idea; new layers |
| Sprite2D | MeshInstance3D / Sprite3D | Billboard vs mesh art choice |
| Camera2D | Camera3D | MANDATORY spring_arm_camera_setup.gd |
| CollisionShape2D | CollisionShape3D | Re-author shapes |
| RayCast2D | RayCast3D | target_position is Vector3 |
physics_layer_migration_checklist.gd. Project Settings → Layer Names → 3D Physics.spring_arm_camera_setup.gd. Never copy Camera2D follow onto Camera3D.characterbody3d_migration_movement.gd. Camera-relative XZ; jump on Y.# Use Sprite3D for quick conversion
extends Sprite3D
func _ready() -> void:
texture = load("res://sprites/character.png")
billboard = BaseMaterial3D.BILLBOARD_ENABLED # Always face camera
pixel_size = 0.01 # Scale sprite in 3D space
# Create textured quads
var mesh_instance := MeshInstance3D.new()
var quad := QuadMesh.new()
quad.size = Vector2(1, 1)
mesh_instance.mesh = quad
var material := StandardMaterial3D.new()
material.albedo_texture = load("res://sprites/character.png")
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.cull_mode = BaseMaterial3D.CULL_DISABLED # Show both sides
mesh_instance.material_override = material
# Import .glb, .fbx models
var character := load("res://models/character.glb").instantiate()
add_child(character)
# Access animations
var anim_player := character.get_node("AnimationPlayer")
anim_player.play("idle")
Minimum: one DirectionalLight3D + WorldEnvironment ambient so the scene is not black. Do NOT Load cascade/GI/bake tutorials in this skill — godot-3d-lighting.
# ✅ GOOD: Keep 2D UI overlay
# Scene structure:
# Main (Node3D)
# ├─ WorldEnvironment
# ├─ DirectionalLight3D
# ├─ Player (CharacterBody3D)
# └─ CanvasLayer # 2D UI on top of 3D world
# └─ Control (HUD)
# UI remains 2D (Control nodes, Sprite2D for HUD elements)
Speculative "2D vs 3D budget" tables lie. Gate on measured data:
directional_shadow_max_distance and shadowed Omni/Spot count until frame time recovers (tune in godot-3d-lighting).visibility_range_* on distant GeometryInstance3D; unlit/simplified materials past the near band.# 2D: left/right for horizontal movement
Input.get_axis("left", "right")
# 3D: Add forward/back, use get_vector()
var input := Input.get_vector("left", "right", "forward", "back")
# Returns Vector2(horizontal, vertical) for 3D movement
# Configure in Project Settings → Input Map:
# forward: W, Up Arrow
# back: S, Down Arrow
# left: A, Left Arrow
# right: D, Right Arrow
# Mouse look (lock cursor)
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
rotate_camera(event.relative)
# Problem: Forgot to set collision layers for 3D
# Solution: Reconfigure layers
var body := CharacterBody3D.new()
body.collision_layer = 0b0001 # What AM I?
body.collision_mask = 0b0110 # What do I DETECT?
Use MANDATORY spring_arm_camera_setup.gd — set spring_arm.collision_mask to the World layer so the boom retracts instead of clipping.
# Problem: StaticBody3D floor has no CollisionShape3D
# Solution: Add collision
var floor_collision := CollisionShape3D.new()
var box_shape := BoxShape3D.new()
box_shape.size = Vector3(100, 1, 100)
floor_collision.shape = box_shape
floor.add_child(floor_collision)
| Factor | Stay 2D | Go 3D |
|--------|---------|-------|
| Gameplay | Platformer, top-down, no depth needed | Exploration, first-person, 3D space combat |
| Art budget | Pixel art, limited resources | 3D models available or necessary |
| Performance target | Mobile, web, low-end | Desktop, console, high-end mobile |
| Development time | Limited | Have time for 3D learning curve |
| Team skills | 2D artists only | 3D artists or asset library |
When moving a 3D character, rely heavily on Transform3D basis vectors rather than calculating trigonometric angles. To move forward locally, extract the negative Z-axis of your transform's basis: velocity = transform.basis.z * speed.
In 2D, the Y-axis points down. In 3D, Godot uses a right-handed system where Y-axis points UP, and forward is -Z. Translating 2D jumps to 3D requires inverting the Y velocity logic (e.g., velocity.y = JUMP_SPEED instead of -JUMP_SPEED).
For 2.5D games where actors move on a 3D floor but are displayed as 2D sprites, query the NavigationServer3D directly and project the resulting PackedVector3Array into 2D screen space (or a flattened gameplay plane) using Camera3D.unproject_position.
class_name NavigationBridge2D5D extends Node
## Projects 3D NavigationServer paths to 2D screenspace for 2.5D movement.
static func query_2_5d_path(camera: Camera3D, map_rid: RID, start_2d: Vector2, target_2d: Vector2) -> PackedVector2Array:
# 1. Project 2D screen points to the 3D ground plane (Y=0).
var start_3d := camera.project_position(start_2d, 0.0)
var target_3d := camera.project_position(target_2d, 0.0)
# 2. Query optimized 3D path.
var path_3d := NavigationServer3D.map_get_path(map_rid, start_3d, target_3d, true)
# 3. Project 3D world points back to 2D screenspace coordinates for the sprite.
var path_2d := PackedVector2Array()
for point in path_3d:
path_2d.append(camera.unproject_position(point))
return path_2d
To render millions of instances, use MultiMeshInstance3D paired with a custom Visual Shader. Use VisualShaderNodeBillboard with BILLBOARD_TYPE_FIXED_Y to ensure sprites stay upright on flat terrain.
class_name MassiveCrowdManager extends MultiMeshInstance3D
## Efficiently manages millions of camera-facing instances via GPU hardware.
func _ready() -> void:
# 1. Configure the MultiMesh for 3D transforms.
multimesh = MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.instance_count = 10000
# 2. Build a ShaderMaterial using VisualShaderNodeBillboard.
var material := ShaderMaterial.new()
# Note: Logic assumes billboard_type=BILLBOARD_TYPE_FIXED_Y and keep_scale=true.
multimesh.mesh = QuadMesh.new()
multimesh.mesh.surface_set_material(0, material)
# 3. Populate transforms. The GPU handles orientation.
for i in range(multimesh.instance_count):
var pos := Vector3(randf() * 100, 0, randf() * 100)
multimesh.set_instance_transform(i, Transform3D(Basis(), pos))
PointLight2D→OmniLight3D conversion is one-shot editor work — keep a project tool if needed. Ongoing lighting quality belongs in godot-3d-lighting.
| Topic | Reference / script |
|-------|-------------------|
| Step-by-step migration / perf gates | migration-recipes.md |
| 2.5D nav bridge / crowd billboards | inline Expert Techniques + bundled scripts |
> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
Basis/Transform3D/Quaternion patterns that replace Euler-angle Camera2D follow and 2D rotation habits.move_and_slide API after CharacterBody2D → CharacterBody3D conversion (no dedicated 3D tutorial page)._process needs interpolated transforms after physics-step movement.Input.get_vector plus mouse-capture look so 2D left/right maps become camera-relative XZ.PhysicsRayQueryParameters3D picks used by point-and-click 3D ports.Take thedivergentai/godot-adapt-2d-to-3d from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.