Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
thedivergentai avatar

Godot Adapt 2d To 3d

  • 162 installs
  • 454 repo stars
  • Updated July 28, 2026
  • thedivergentai/gd-agentic-skills

Use godot-adapt-2d-to-3d for development tasks

About

godot-adapt-2d-to-3d: A skill for development. This provides functionality for development workflows.

  • godot-adapt-2d-to-3d

Godot Adapt 2d To 3d by the numbers

  • 162 all-time installs (skills.sh)
  • +16 installs in the week ending Aug 5, 2026 (Skillselion tracking)
  • Ranked #2,378 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-adapt-2d-to-3d

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs162
repo stars454
Last updatedJuly 28, 2026
Repositorythedivergentai/gd-agentic-skills

What it does

Use godot-adapt-2d-to-3d for development tasks

Files

SKILL.mdMarkdownGitHub ↗

Adapt: 2D to 3D

Expert guidance for migrating 2D games into the third dimension.

NEVER Do

  • NEVER directly replace Vector2 with Vector3(x, y, 0) — This creates a "flat 3D" game with no depth gameplay. Add Z-axis movement or camera rotation to justify 3D.
  • NEVER keep 2D collision layers — 2D and 3D physics use separate layer systems. You must reconfigure collision_layer/collision_mask for 3D nodes.
  • NEVER forget to add lighting — 3D without lights is pitch black (unless using unlit materials). Add at least one DirectionalLight3D.
  • NEVER use Camera2D follow logic in 3D — Camera3D needs spring arm or look-at logic. Direct position copying causes clipping and disorientation.
  • NEVER assume same performance — 3D is 5-10x more demanding. Budget for lower draw calls, smaller viewport resolution on mobile.
  • NEVER use the rotation property for complex 3D logic — 3D rotation uses Euler angles. Interpolating Euler angles causes unpredictable paths and Gimbal Lock. Always use Quaternion for 3D rotation interpolation or the Basis matrix for directional vectors.
  • NEVER ignore metric scaling — 3D physics and lighting assume 1 unit = 1 meter. Scaling models inside the engine introduces precision errors. Export assets from DCCs at the correct metric scale.
  • NEVER disable physics interpolation when using custom camera follow scripts — Updating camera position in _process to follow a body moving in _physics_process causes jitter. Use Node3D.get_global_transform_interpolated() for smooth transforms.

---

Available Scripts

MANDATORY: Read the appropriate script before implementing the corresponding pattern.

sprite_plane.gd

Sprite3D billboard configuration and world-to-screen projection for placing 2D UI over 3D objects. Handles behind-camera detection.

vector_mapping.gd

Static utility for 2D→3D vector translation. The Y-to-Z rule: 2D Y (down) maps to 3D Z (forward). Essential for movement code.

crisp_projected_ui.gd

Projected 2D UI for 3D Objects mapping snippet. Replaces blurry text elements with true 2D Canvas space positioning projected from 3D space.

---

Node Conversion Matrix

2D Node3D EquivalentNotes
CharacterBody2DCharacterBody3DAdd Z-axis movement, rotate with mouse
RigidBody2DRigidBody3DGravity now Vector3(0, -9.8, 0)
StaticBody2DStaticBody3DCollision shapes use Shape3D
Area2DArea3DTriggers work the same way
Sprite2DMeshInstance3D + QuadMeshOr use Sprite3D (billboarded)
AnimatedSprite2DAnimatedSprite3DBillboard mode available
TileMapLayerGridMapRequires MeshLibrary creation
Camera2DCamera3DRequires repositioning logic
CollisionShape2DCollisionShape3DBoxShape2D → BoxShape3D, etc.
RayCast2DRayCast3Dtarget_position is now Vector3

---

Migration Steps

Step 1: Physics Layer Reconfiguration

# 2D collision layers are SEPARATE from 3D
# You must reconfigure in Project Settings → Layer Names → 3D Physics

# Before (2D):
# Layer 1: Player
# Layer 2: Enemies
# Layer 3: World

# After (3D) - same names, but different system
# In code, update all collision layer references:

# 2D version:
# collision_layer = 0b0001

# 3D version (same logic, different node):
var character_3d := CharacterBody3D.new()
character_3d.collision_layer = 0b0001  # Layer 1: Player
character_3d.collision_mask = 0b0110   # Detect Enemies + World

Step 2: Camera Conversion

# ❌ BAD: Direct 2D follow logic
extends Camera3D

@onready var player: Node3D = $"../Player"

func _process(delta: float) -> void:
    global_position = player.global_position  # Clipping, disorienting!

# ✅ GOOD: Third-person camera with SpringArm3D
# Scene structure:
# Player (CharacterBody3D)
#   └─ SpringArm3D
#       └─ Camera3D

# player.gd
extends CharacterBody3D

@onready var spring_arm: SpringArm3D = $SpringArm3D
@onready var camera: Camera3D = $SpringArm3D/Camera3D

func _ready() -> void:
    spring_arm.spring_length = 10.0  # Distance from player
    spring_arm.position = Vector3(0, 2, 0)  # Above player

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        spring_arm.rotate_y(-event.relative.x * 0.005)  # Horizontal rotation
        spring_arm.rotate_object_local(Vector3.RIGHT, -event.relative.y * 0.005)  # Vertical
        
        # Clamp vertical rotation
        spring_arm.rotation.x = clamp(spring_arm.rotation.x, -PI/3, PI/6)

Step 3: Movement Conversion

# 2D platformer movement
extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y += gravity * delta
    
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY
    
    var direction := Input.get_axis("left", "right")
    velocity.x = direction * SPEED
    
    move_and_slide()

# ✅ 3D equivalent (third-person platformer)
extends CharacterBody3D

const SPEED = 5.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8

@onready var spring_arm: SpringArm3D = $SpringArm3D

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y -= GRAVITY * delta
    
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY
    
    # Movement relative to camera direction
    var input_dir := Input.get_vector("left", "right", "forward", "back")
    var camera_basis := spring_arm.global_transform.basis
    var direction := (camera_basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
    
    if direction:
        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
        
        # Rotate player to face movement direction
        rotation.y = lerp_angle(rotation.y, atan2(-direction.x, -direction.z), 0.1)
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)
        velocity.z = move_toward(velocity.z, 0, SPEED)
    
    move_and_slide()

---

Art Pipeline: Sprites → 3D Models

Option 1: Billboard Sprites (2.5D)

# 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

Option 2: Quad Meshes (Floating Sprites)

# 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

Option 3: Full 3D Models (Blender/Asset Library)

# 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")

---

Lighting Considerations

Minimum Lighting Setup

# Add to main scene
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-45, 30, 0)
sun.light_energy = 1.0
sun.shadow_enabled = true
add_child(sun)

# Ambient light
var env := WorldEnvironment.new()
var environment := Environment.new()
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
environment.ambient_light_color = Color(0.3, 0.3, 0.4)  # Subtle blue
environment.ambient_light_energy = 0.5
env.environment = environment
add_child(env)

---

UI Adaptation

# ✅ 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)

---

Performance Budgeting

2D vs 3D Performance

Metric2D Budget3D BudgetNotes
Draw calls100-20050-100Use fewer meshes
VerticesUnlimited100K-500KLOD important
LightsN/A3-5 shadowedExpensive
Transparent objectsMany<10Sorting overhead
Particle systemsMany2-3 maxGPU godot-particles only

Optimization Checklist

# 1. Use LOD for distant objects
var mesh_instance := MeshInstance3D.new()
mesh_instance.lod_bias = 1.0  # Lower detail sooner

# 2. Occlusion culling
# Use OccluderInstance3D for large walls/buildings

# 3. Reduce shadow distance
var sun := DirectionalLight3D.new()
sun.directional_shadow_max_distance = 50.0  # Don't render far shadows

# 4. Use unlit materials for distant objects
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED

---

Input Scheme Changes

2D → 3D Input Mapping

# 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)

---

Edge Cases

Physics Not Working

# 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?

Camera Clipping Through Walls

# SpringArm3D automatically pulls camera forward when obstructed
spring_arm.spring_length = 10.0
spring_arm.collision_mask = 0b0100  # Layer 3: World

Player Falling Through Floor

# 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)

---

Decision Tree: When to Go 3D

FactorStay 2DGo 3D
GameplayPlatformer, top-down, no depth neededExploration, first-person, 3D space combat
Art budgetPixel art, limited resources3D models available or necessary
Performance targetMobile, web, low-endDesktop, console, high-end mobile
Development timeLimitedHave time for 3D learning curve
Team skills2D artists only3D artists or asset library

---

Expert Techniques & Optimizations

1. Vector Math over Euler Angles

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.

2. Understanding Coordinate Discrepancies

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).

3. 2.5D Navigation (Camera-Projected Paths)

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

4. Shader-Based Billboarding (Massive Crowd Rendering)

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))

5. Lighting Migration Tool (2D to 3D Converter)

A robust EditorScript for mapping PointLight2D properties to OmniLight3D. Uses EditorInterface.get_edited_scene_root() to ensure changes are tracked by the editor.

@tool
class_name LightMigrationTool extends EditorScript
## Converts PointLight2D nodes in the active scene to OmniLight3D.

func _run() -> void:
    var root := EditorInterface.get_edited_scene_root()
    if not root: return
    _migrate_node(root)

func _migrate_node(node: Node) -> void:
    if node is PointLight2D:
        var l3d := OmniLight3D.new()
        l3d.light_color = node.color
        l3d.light_energy = node.energy
        # Approximate 3D range from 2D texture radius * scale
        var radius := 128.0 # Default fallback
        if node.texture: radius = node.texture.get_width() / 2.0
        l3d.omni_range = radius * node.texture_scale
        
        # Mapping 2D (x, y) to 3D (x, y, height)
        l3d.position = Vector3(node.position.x, node.position.y, node.height)
        
        node.get_parent().add_child(l3d)
        l3d.owner = EditorInterface.get_edited_scene_root()
        l3d.name = node.name + "_3D"
        
    for child in node.get_children():
        _migrate_node(child)

Reference

  • Master Skill: godot-master

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.