
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-3dAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 162 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-adapt-2d-to-3d for development tasks
Files
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
Quaternionfor 3D rotation interpolation or theBasismatrix 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
_processto follow a body moving in_physics_processcauses jitter. UseNode3D.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 Node | 3D Equivalent | Notes |
|---|---|---|
| CharacterBody2D | CharacterBody3D | Add Z-axis movement, rotate with mouse |
| RigidBody2D | RigidBody3D | Gravity now Vector3(0, -9.8, 0) |
| StaticBody2D | StaticBody3D | Collision shapes use Shape3D |
| Area2D | Area3D | Triggers work the same way |
| Sprite2D | MeshInstance3D + QuadMesh | Or use Sprite3D (billboarded) |
| AnimatedSprite2D | AnimatedSprite3D | Billboard mode available |
| TileMapLayer | GridMap | Requires MeshLibrary creation |
| Camera2D | Camera3D | Requires repositioning logic |
| CollisionShape2D | CollisionShape3D | BoxShape2D → BoxShape3D, etc. |
| RayCast2D | RayCast3D | target_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 + WorldStep 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 spaceOption 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 = materialOption 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
| Metric | 2D Budget | 3D Budget | Notes |
|---|---|---|---|
| Draw calls | 100-200 | 50-100 | Use fewer meshes |
| Vertices | Unlimited | 100K-500K | LOD important |
| Lights | N/A | 3-5 shadowed | Expensive |
| Transparent objects | Many | <10 | Sorting overhead |
| Particle systems | Many | 2-3 max | GPU 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: WorldPlayer 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
| 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 |
---
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_2d4. 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
# adapt_2d_to_3d_patterns.gd
extends Node
# 1. Projecting 2D UI into 3D Space
# EXPERT NOTE: Use a SubViewport inside a Sprite3D for interactive "Diegetic" UI.
func project_pixel_to_world_ui(viewport: SubViewport, sprite: Sprite3D) -> void:
sprite.texture = viewport.get_texture()
# Required for transparency in 3D
sprite.transparent = true
sprite.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
# 2. 2D Billboard in 3D Scene
# EXPERT NOTE: Sprite3D is the bridge. billboard_mode creates the "DOOM" style look.
func create_billboard_sprite(tex: Texture2D) -> Sprite3D:
var s := Sprite3D.new()
s.texture = tex
s.billboard = BaseMaterial3D.BILLBOARD_ENABLED
return s
# 3. Raycasting from 2D Mouse to 3D Physics
# EXPERT NOTE: Essential for "Point and Click" in a 3D environment.
func get_3d_mouse_pos(camera: Camera3D) -> Vector3:
var mouse_pos := get_viewport().get_mouse_position()
var ray_origin := camera.project_ray_origin(mouse_pos)
var ray_dir := camera.project_ray_normal(mouse_pos)
var ray_query := PhysicsRayQueryParameters3D.create(ray_origin, ray_origin + ray_dir * 1000)
var result := camera.get_world_3d().direct_space_state.intersect_ray(ray_query)
return result.position if result else Vector3.ZERO
# 4. CanvasLayer Layering in 3D
# EXPERT NOTE: Keep UI independent of 3D FOV by putting it on a high CanvasLayer.
func setup_overlay_ui(ui_node: Control) -> void:
var canvas := CanvasLayer.new()
canvas.layer = 100
add_child(canvas)
canvas.add_child(ui_node)
# 5. Using 2D Particles in 3D (SubViewportMesh)
# EXPERT NOTE: Sometimes 2D particles look better. Wrap them in a plane mesh.
func create_2d_fx_in_3d(fx_scene: PackedScene) -> MeshInstance3D:
var mesh := MeshInstance3D.new()
mesh.mesh = QuadMesh.new() # Default 1x1 plane
# Material set to show SubViewport would go here
return mesh
# 6. Global Illumination for 2D Assets
# EXPERT NOTE: Sprite3D can receive 3D light/shadows if shaded mode is enabled.
func enable_shaded_sprites(sprite: Sprite3D) -> void:
sprite.shading_mode = BaseMaterial3D.SHADING_MODE_PER_PIXEL
sprite.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
# 7. Coordinate Translation (3D World to 2D Screen)
# EXPERT NOTE: Place "floating" health bars or name tags over 3D units.
func position_ui_over_target(camera: Camera3D, target: Node3D, ui_element: Control) -> void:
if camera.is_position_behind(target.global_position):
ui_element.visible = false
else:
ui_element.visible = true
ui_element.position = camera.unproject_position(target.global_position)
# 8. Handling Resolution Mismatch
# EXPERT NOTE: 2D pixel-art in a high-res 3D world needs content_scale interpolation.
func setup_mixed_resolution() -> void:
get_window().content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
get_window().content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
# 9. Pixel-Perfect 3D Rendering (Retro Look)
# EXPERT NOTE: Render 3D at a lower resolution then upscale to a 2D viewport.
func setup_retro_render(sub_viewport: SubViewport) -> void:
sub_viewport.size = Vector2i(320, 180)
sub_viewport.texture_filter = Viewport.TEXTURE_FILTER_NEAREST
# 10. Mixing 2D and 3D Input
# EXPERT NOTE: Ensure UI consumes events before the 3D raycast triggers logic.
func _input(event: InputEvent) -> void:
if get_viewport().gui_get_focus_owner():
return # UI is handling this
# 3D interaction logic...
extends Control
# Crisp Projected 2D UI for 3D Objects
# Instead of rendering a blurry text element in the 3D world, project its 3D coordinate onto the 2D Canvas screen space.
# The 3D target we want our 2D health bar to hover over
@export var target_3d: Node3D
@onready var camera: Camera3D = get_viewport().get_camera_3d()
func _process(delta: float) -> void:
if not camera or not is_instance_valid(target_3d):
return
# Prevent the UI from showing if the object is behind the camera
if camera.is_position_behind(target_3d.global_position):
hide()
else:
show()
# Translate the 3D world position to 2D screen coordinates
var screen_pos: Vector2 = camera.unproject_position(target_3d.global_position)
# Position the Control node on the 2D canvas
global_position = screen_pos
# skills/adapt-2d-to-3d/scripts/sprite_plane.gd
extends Sprite3D
## Sprite Plane Expert Pattern
## Manages 2D-in-3D projection, billboard behaviors, and screen-space interactions.
class_name SpritePlane
@export var interactable: bool = true
@export_flags("Player", "World", "Enemy") var mouse_filter_layers: int = 1
signal clicked(local_pos: Vector2)
signal hover_started
signal hover_ended
var _is_hovered: bool = false
var _camera: Camera3D
func _ready() -> void:
_camera = get_viewport().get_camera_3d()
# Expert: Ensure correct flags for mixing 2D/3D
centered = true
alpha_cut = SpriteBase3D.ALPHA_CUT_DISCARD # Better depth sorting
texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
func _unhandled_input(event: InputEvent) -> void:
if not interactable or not _camera: return
if event is InputEventMouse:
var mouse_pos = event.position
var ray_origin = _camera.project_ray_origin(mouse_pos)
var ray_normal = _camera.project_ray_normal(mouse_pos)
# Raycast against the sprite's plane
var global_transform = global_transform
var plane = Plane(global_transform.basis.z, global_transform.origin)
var intersect = plane.intersects_ray(ray_origin, ray_normal)
if intersect:
# Check if intersection is within sprite bounds
var local_pos = global_transform.affine_inverse() * intersect
var rect = get_item_rect()
if rect.has_point(Vector2(local_pos.x, local_pos.y)):
if not _is_hovered:
_is_hovered = true
hover_started.emit()
if event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
clicked.emit(Vector2(local_pos.x, local_pos.y))
return
if _is_hovered:
_is_hovered = false
hover_ended.emit()
func world_to_screen_rect() -> Rect2:
if not _camera: return Rect2()
var aabb = get_aabb()
var corners = [
aabb.position,
aabb.position + Vector3(aabb.size.x, 0, 0),
aabb.position + Vector3(0, aabb.size.y, 0),
aabb.position + aabb.size
]
var min_pos = Vector2(INF, INF)
var max_pos = Vector2(-INF, -INF)
for corner in corners:
var screen_pos = _camera.unproject_position(global_transform * corner)
min_pos = min_pos.min(screen_pos)
max_pos = max_pos.max(screen_pos)
return Rect2(min_pos, max_pos - min_pos)
## EXPERT USAGE:
## Attach to Sprite3D. Use 'world_to_screen_rect()' to position 2D UI elements
## exactly over the 3D sprite (e.g. health bars).
# skills/adapt-2d-to-3d/scripts/vector_mapping.gd
class_name VectorMapping
## Vector Mapping Expert Pattern
## Static utility for converting vectors between 2D and 3D spaces.
## Common Rule: 2D Y (Down) -> 3D Z (Back/South)
## 2D X (Right) -> 3D X (Right/East)
# 2D -> 3D (XZ Plane)
# Useful for movement: Input(x, y) -> Velocity(x, 0, z)
static func v2_to_v3_xz(v2: Vector2, y_height: float = 0.0) -> Vector3:
return Vector3(v2.x, y_height, v2.y)
# 2D -> 3D (XY Plane)
# Useful for UI projection or side-scrollers
static func v2_to_v3_xy(v2: Vector2, z_depth: float = 0.0) -> Vector3:
return Vector3(v2.x, v2.y, z_depth)
# 3D -> 2D (XZ Plane)
# Useful for minimaps (ignoring height)
static func v3_to_v2_xz(v3: Vector3) -> Vector2:
return Vector2(v3.x, v3.z)
# 3D -> 2D (XY Plane)
# Useful for side projections
static func v3_to_v2_xy(v3: Vector3) -> Vector2:
return Vector2(v3.x, v3.y)
# Directional Mapping
# Remaps a 2D angle to a 3D Y-axis rotation
static func angle_2d_to_rotation_y(angle_radians: float) -> float:
# 2D: 0 is Right, PI/2 is Down
# 3D: 0 is -Z (Forward)? Depends on convention.
# Godot 3D: -Z is Forward.
# If 2D Up (Start) maps to 3D Forward:
# 2D Angle needs -PI/2 offset usually.
return -angle_radians
## EXPERT USAGE:
## var move_3d = VectorMapping.v2_to_v3_xz(input_vector)