diff --git a/skills/software-development/blender-godot-pipeline/SKILL.md b/skills/software-development/blender-godot-pipeline/SKILL.md new file mode 100644 index 000000000000..7deb98998156 --- /dev/null +++ b/skills/software-development/blender-godot-pipeline/SKILL.md @@ -0,0 +1,293 @@ +--- +name: blender-godot-pipeline +description: | + Use when automating Blender-to-Godot asset pipelines for Quest VR projects. Covers GLB export, gltfpack mesh optimization, Godot Mobile renderer import settings, LOD generation, texture budgets, and scene organization. Use for batch exporting environments, props, and characters from Blender 4.x into a Godot 4.3+ project with Quest 2/3 performance targets. +version: "1.0.0" +author: hermes-vr-devkit +license: MIT +metadata: + hermes: + tags: [blender, godot, gltf, glb, quest, vr, pipeline, optimization, mcp] + related_skills: [godot-quest-dev, godot-xr-interactions, quest-native-toolchain, mcp-server-setup] +--- + +# Blender-Godot Pipeline for Quest VR + +End-to-end pipeline for moving optimized 3D assets from Blender 4.x into Godot 4.3+ for Quest VR deployment. + +## Pipeline Overview + +``` +Blender 4.x gltfpack Godot 4.3+ +---------- --------- ------------ +Modeling ----> -si 0.5 ----> Import +UV unwrap -tc 2048 Mobile Renderer +Material -kn VR Compression +Lighting bake -noq Scene Tree + | | + v v + .glb (raw) .glb.import + .gltf + .bin Material remap + Static typing +``` + +1. Build/optimize in Blender +2. Export GLB with Godot-compatible settings +3. (Optional) Run gltfpack for LOD/mesh optimization +4. Import into Godot, adjust import settings +5. Instantiate in scene, verify Quest budgets + +## Quick Reference: Quest Performance Budgets + +| Asset Type | Tri Budget | Texture | Draw Calls | Notes | +|------------|-----------|---------|-----------|-------| +| Hero prop | 15-30k | 2K | 1-2 | Player can approach closely | +| Background | 5-10k | 1K | 1 | Far distance, aggressive LOD | +| Environment| 100-200k | 2K-4K atlas| 5-10 | Total scene, split by room | +| UI panel | 500 | 1K | 1 | Unshaded, transparent | +| Character | 20-40k | 2K | 2-3 | Body + head separate materials | +| Particle | 100-500 | 256 | 1 | GPU particles, billboard | + +Target: 72fps on Quest 2, 90fps on Quest 3. Total scene triangles under 300k visible. + +## Scene Organization in Blender + +Use collections for pipeline stages: + +``` +Scene Collection +|-- _EXPORT # Final export collection +| |-- env_static # Static environment mesh +| |-- env_props # Instanced props (chairs, tables) +| |-- characters # Rigged meshes +|-- _BAKE # Lightmap targets +|-- _REFERENCE # Blueprints, scale refs +|-- _WIP # Work in progress +|-- Camera # Preview camera (1 unit = 1 meter) +|-- Lights # Bake lights only (not exported) +``` + +Rules: +- Scale: 1 Blender unit = 1 meter. A standing human is ~1.7 units tall. +- Apply all transforms (Ctrl+A -> All Transforms) before export. +- One material per logical surface. Godot creates one StandardMaterial3D per Blender material slot. +- Name objects with `Category_Name_LodN` convention (e.g., `Chair_Wood_Lod0`). + +## GLB Export Constraints (Godot Compatible) + +| Feature | Support | Notes | +|---------|---------|-------| +| Meshes | Full | Triangles or quads (auto-triangulated) | +| UVs | Full | Channel 1 = albedo/normal. Channel 2 = lightmap | +| Normals | Full | Use Auto Smooth or custom split normals | +| Materials (Principled BSDF) | Partial | Base color, metallic, roughness, normal, emission, alpha clip | +| Shader nodes | None | Only Principled BSDF exports. No custom node groups. | +| Modifiers | Partial | Apply before export. Armature exports if modifier visible | +| Animations | Full | Actions as glTF animations. NLA tracks recommended | +| Shape keys | Full | Export as morph targets | +| Constraints | None | Bake to keyframes or apply | +| Drivers | None | Bake to keyframes | +| Lights | Partial | glTF punctual lights extension; Godot ignores by default | +| Cameras | Partial | Exports but Godot usually uses its own | +| Empty | Partial | Exported as node with no mesh | + +Critical: Godot imports each material slot as a separate StandardMaterial3D. Keep material count low. + +## Export Settings (Blender) + +In File > Export > glTF 2.0 (.glb/.gltf): + +- Format: glTF Binary (.glb) +- Include: + - Limit to: Visible Objects (or active collection `_EXPORT`) + - Data > Mesh: Apply Modifiers ON + - Data > Mesh: Use Auto Smooth ON (or custom normals) + - Data > Materials: Export ON +- Transform: + - +Y Up (Godot default) +- Geometry: + - Loose Edges OFF + - Loose Points OFF +- Animation (if animated): + - Limit to Playback Range ON + - Sampling Rate: 24 or 30 + +## Mesh Optimization with gltfpack + +Install gltfpack: https://github.com/zeux/meshoptimizer/releases + +```bash +# Basic optimization for Quest +gltfpack -si 0.5 -tc 2048 -kn -noq -o optimized.glb input.glb + +# Flags explained: +# -si 0.5 Simplify to ~50% triangles +# -tc 2048 Resize textures to max 2048 +# -kn Keep named nodes (preserves scene hierarchy) +# -noq Disable quantization (sometimes needed for Godot compatibility) +# -o Output file + +# Aggressive LOD generation +gltfpack -si 0.25 -tc 1024 -kn -noq -o lod1.glb input.glb +``` + +Note: Godot 4.3+ has built-in LOD generation on import. gltfpack is optional but gives finer control, especially for environment meshes with many triangles. + +## LOD Strategy Code (Godot) + +Attach to a MeshInstance3D or use in an import script: + +```gdscript +extends MeshInstance3D + +@export var lod_distances: Array[float] = [10.0, 25.0, 50.0] +@export var lod_meshes: Array[Mesh] = [] + +var current_lod: int = -1 + +func _process(_delta: float) -> void: + var cam := get_viewport().get_camera_3d() + if not cam: + return + var dist := global_position.distance_to(cam.global_position) + var target_lod := lod_meshes.size() + for i in range(lod_distances.size()): + if dist < lod_distances[i]: + target_lod = i + break + if target_lod != current_lod: + current_lod = target_lod + if target_lod < lod_meshes.size(): + mesh = lod_meshes[target_lod] +``` + +Godot's automatic LOD system (Project Settings > Rendering > Mesh LOD) handles this for most assets. Manual override only needed for hero assets with specific pop distances. + +## Texture Guidelines + +| Texture | Format | Max Size | Compress | Notes | +|---------|--------|----------|----------|-------| +| Albedo | PNG/JPG| 2048 | VRAM Compressed (ETC2/ASTC) | Alpha in separate channel if needed | +| Normal | PNG | 2048 | VRAM Compressed | OpenGL normal map (Y+) | +| ORM | PNG | 1024 | VRAM Compressed | Occlusion(R), Roughness(G), Metallic(B) | +| Emission| PNG | 512 | VRAM Compressed | Only for glowing objects | +| Lightmap| EXR | 4096 | VRAM Uncompressed or Basis | Baked in Blender or Godot | + +- Use TextureAtlas where possible to reduce draw calls. +- Godot Mobile renderer uses ETC2/ASTC automatically on Quest. +- Avoid texture sizes that are not power-of-two (e.g., 1500x1500). + +## Godot Import Settings + +When a `.glb` is copied into the Godot project: + +1. Select the `.glb` in FileSystem dock +2. In Import tab, change preset or adjust: + - **Storage**: Mesh storage can be `Built-In` or separate `.mesh`/`.res` + - **Materials**: `Extract Materials` to editable `.tres` files + - **Meshes**: `Generate LOD` ON (Godot auto-generates LODs) + - **Physics**: `Create Collision` if needed (convex or trimesh) + - **Animation**: `Import Animations` ON, set default loop mode + +3. Click **Reimport** + +### Mobile Renderer Adjustments + +In the imported material `.tres` (if extracted): +- Shading mode: `Per Pixel` (default) or `Toon` for stylized +- Disable `Specular` if not needed (saves ALU) +- Set `Cull Mode` to `Back` unless double-sided required +- Transparency: `Alpha Scissor` cheaper than `Alpha Blend` on Quest +- Disable `Ambient Occlusion` if using baked lightmaps + +### Import Cache Invalidation + +If Blender re-export does not show in Godot: +1. Delete `.glb.import` file +2. Delete `.godot/imported/` cached version +3. Reimport in Godot (or it auto-reimports on focus) + +Or run from CLI: +```bash +# Inside Godot project root +rm -f .godot/imported/*_yourfile* +rm -f assets/models/yourfile.glb.import +# Then reopen Godot or run headless import +godot --headless --import +``` + +## MCP Automation + +If using the Blender MCP server, the pipeline can be scripted: + +```python +# Example: export active collection via MCP +import bpy + +# Ensure _EXPORT collection exists +export_col = bpy.data.collections.get("_EXPORT") +if not export_col: + raise RuntimeError("Missing _EXPORT collection") + +# Deselect all, select collection objects +bpy.ops.object.select_all(action='DESELECT') +for obj in export_col.all_objects: + if obj.type in {'MESH', 'ARMATURE', 'EMPTY'}: + obj.select_set(True) + +# Export +bpy.ops.export_scene.gltf( + filepath="/path/to/project/assets/models/export.glb", + export_format='GLB', + use_selection=True, + export_yup=True, + export_apply=True, + export_materials='EXPORT', +) +``` + +See `references/blender-pitfalls.md` for scripting gotchas. + +## .gitignore Additions + +Add to project `.gitignore`: + +```gitignore +# Blender backup files +*.blend1 +*.blend2 +*.blend@ + +# Godot import cache (regenerates) +.godot/imported/ +*.tmp + +# Exported raw GLBs (if generated in CI) +# Uncomment if you only version Blender sources: +# assets/models/*.glb +# assets/models/*.gltf +# assets/models/*.bin + +# gltfpack intermediate +*_gltfpack.glb +``` + +## Workflow Checklist + +- [ ] Blender units = meters, transforms applied +- [ ] Objects named with `Category_Name_LodN` +- [ ] Materials use Principled BSDF only +- [ ] UVs unwrapped, no overlaps on lightmap UV2 +- [ ] Export collection `_EXPORT` contains only desired objects +- [ ] Export settings: GLB, visible objects, apply modifiers, Y-up +- [ ] (Optional) gltfpack with `-si 0.5 -tc 2048 -kn -noq` +- [ ] Godot import: generate LOD, extract materials +- [ ] Material: backface culling, appropriate transparency mode +- [ ] Instance in scene, verify draw calls with Godot Debugger > Monitors +- [ ] Test on Quest: verify 72fps with GPU frame time < 13.9ms + +## References + +- `references/blender-pitfalls.md` — Blender 4.x scripting and export gotchas +- `references/gltf-export.md` — Detailed export settings and compatibility matrix +- `references/godot-import.md` — Godot import settings, material adjustments, cache handling diff --git a/skills/software-development/blender-godot-pipeline/references/blender-pitfalls.md b/skills/software-development/blender-godot-pipeline/references/blender-pitfalls.md new file mode 100644 index 000000000000..e8b498b995b1 --- /dev/null +++ b/skills/software-development/blender-godot-pipeline/references/blender-pitfalls.md @@ -0,0 +1,184 @@ +# Blender 4.x Scripting Pitfalls for Pipeline Automation + +Blender's Python API changes across 4.x releases. These are the most common gotchas when scripting exports for a Godot pipeline. + +## Numpy Array Copy Behavior + +Blender's `bpy_prop_collection` and mesh data often return references, not copies. Mutating them can corrupt the blend file silently. + +```python +import numpy as np +import bpy + +# WRONG: modifies the mesh in place without update +verts = np.array([v.co for v in bpy.context.object.data.vertices]) +verts *= 2.0 # This does NOT scale the mesh + +# RIGHT: assign back through the API +obj = bpy.context.object +mesh = obj.data +for i, v in enumerate(mesh.vertices): + v.co = mesh.vertices[i].co * 2.0 +mesh.update() + +# Or use bmesh for complex ops +import bmesh +bm = bmesh.new() +bm.from_mesh(mesh) +# ... operate on bm ... +bm.to_mesh(mesh) +bm.free() +mesh.update() +``` + +## Emission Nodes Changed in 4.0+ + +Blender 4.0 unified emission into Principled BSDF. The old `ShaderNodeEmission` standalone node is no longer the standard path. + +```python +# In 4.0+, emission is a socket on Principled BSDF +principled = node_tree.nodes["Principled BSDF"] +principled.inputs["Emission Strength"].default_value = 1.0 +principled.inputs["Emission Color"].default_value = (1.0, 0.0, 0.0, 1.0) + +# If you still need a separate emission shader (e.g., for additive), +# you must mix it manually; glTF exporter may ignore it. +``` + +When exporting to glTF, the exporter reads `Emission` from Principled BSDF. Custom emission mixes often fail to export. + +## Missing Nodes After Version Upgrade + +Opening a 3.x file in 4.x can leave node trees with missing node types (red boxes). Scripting must defensively check: + +```python +for node in mat.node_tree.nodes: + if node.type == 'UNKNOWN': + # Node type removed or addon missing + print(f"Warning: unknown node in {mat.name}") + # Remove or replace before export + mat.node_tree.nodes.remove(node) +``` + +## Undo Safety in Headless / Scripted Mode + +Blender's undo stack can grow indefinitely in background mode, causing memory bloat in long batch scripts. + +```python +# Disable undo for batch operations +bpy.context.preferences.edit.use_global_undo = False + +# Or clear after heavy ops +bpy.ops.ed.undo_push(message="Batch step") +# ... after many ops ... +bpy.ops.ed.undo_history_clear() +``` + +In an MCP/automation context, each command may start a fresh Blender instance, but if running a long script inside one session, monitor undo. + +## Bound Box Cache Stale After Mesh Edit + +`object.bound_box` is cached. After mesh modifications, update before reading bounds: + +```python +obj = bpy.context.object +obj.data.update() # Force mesh update +# In some cases, depsgraph update is required +dg = bpy.context.evaluated_depsgraph_get() +eval_obj = obj.evaluated_get(dg) +bbox = [eval_obj.matrix_world @ Vector(v) for v in eval_obj.bound_box] +``` + +This matters for pipeline validation (e.g., checking if mesh fits in a 2m box before export). + +## Mesh Scaling and Apply Transforms + +Godot expects 1 unit = 1 meter. A common mistake is non-uniform scale on parent empties or armatures. + +```python +# Apply all transforms before export +bpy.ops.object.select_all(action='DESELECT') +obj.select_set(True) +bpy.context.view_layer.objects.active = obj +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) +``` + +If an object has negative scale, normals will be inverted in Godot. Always apply transforms on the final export collection. + +## Collection Export Context + +`bpy.ops.export_scene.gltf` uses the current view layer. Ensure the collection is visible: + +```python +# Make sure _EXPORT collection is in the active view layer +export_col = bpy.data.collections["_EXPORT"] +layer_col = bpy.context.view_layer.layer_collection.children.get(export_col.name) +if layer_col: + bpy.context.view_layer.active_layer_collection = layer_col +else: + # Link collection to scene if not already linked + bpy.context.scene.collection.children.link(export_col) +``` + +## Modifier Visibility + +Modifiers must be visible in the viewport to be applied during export with `export_apply=True`. Render visibility does not matter. + +```python +for mod in obj.modifiers: + mod.show_viewport = True +``` + +## Naming Sanitization + +Godot node names allow spaces and special characters, but GDScript access is easier with snake_case. Blender object names can contain dots, which become node paths in Godot. + +```python +import re + +def sanitize_name(name: str) -> str: + name = re.sub(r'[^\w\-]', '_', name) + name = re.sub(r'\.+', '_', name) + return name + +for obj in export_col.all_objects: + obj.name = sanitize_name(obj.name) +``` + +## Summary of Safe Scripting Pattern + +```python +import bpy +import bmesh +from mathutils import Vector + +def safe_export_step(): + # 1. Disable undo + bpy.context.preferences.edit.use_global_undo = False + + # 2. Validate collection + col = bpy.data.collections.get("_EXPORT") + if not col: + raise RuntimeError("No _EXPORT collection") + + # 3. Apply transforms, ensure modifiers visible + for obj in col.all_objects: + if obj.type == 'MESH': + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + for mod in obj.modifiers: + mod.show_viewport = True + obj.data.update() + + # 4. Export + bpy.ops.export_scene.gltf( + filepath="/path/to/output.glb", + export_format='GLB', + use_active_collection=True, + export_yup=True, + export_apply=True, + ) + + # 5. Cleanup undo + bpy.ops.ed.undo_history_clear() +``` diff --git a/skills/software-development/blender-godot-pipeline/references/gltf-export.md b/skills/software-development/blender-godot-pipeline/references/gltf-export.md new file mode 100644 index 000000000000..75005d4ac30c --- /dev/null +++ b/skills/software-development/blender-godot-pipeline/references/gltf-export.md @@ -0,0 +1,185 @@ +# Godot-Compatible glTF Export Settings + +Blender's glTF exporter is the most reliable path into Godot. This reference details the exact settings and edge cases. + +## Export Format Choice + +| Format | Use Case | Godot Notes | +|--------|----------|-------------| +| glTF Binary (.glb) | Default for single assets | One file, easy to move, recommended | +| glTF Separate (.gltf + .bin + textures) | When editing textures externally | Godot imports the `.gltf`, keep files together | +| glTF Embedded | Rarely used | Bloated file size, not recommended for VR | + +**Recommendation:** Use `.glb` for all pipeline stages. Run gltfpack on `.glb` directly. + +## Detailed Export Settings + +### Include Panel + +- **Limit to:** `Visible Objects` or `Active Collection` + - Use `Active Collection` with a dedicated `_EXPORT` collection for reproducibility. +- **Data > Rendering:** + - `Use Render Engine`: OFF (uses viewport display, simpler materials) + - `Active Camera`: OFF unless you need a default camera +- **Data > Mesh:** + - `Apply Modifiers`: ON. Critical for Subdivision, Mirror, Solidify. + - `UVs`: ON. Godot needs UV0 for textures. UV1 for lightmaps. + - `Vertex Colors`: ON if used, but they increase file size. + - `Attributes`: OFF unless using custom mesh attributes in Godot shaders. +- **Data > Materials:** + - `Export`: ON. Exports Principled BSDF parameters. + - `Image Format`: Automatic. PNG for lossless, JPEG for smaller files. +- **Data > Animation:** + - `Use Current Frame`: OFF (export full timeline) + - `Limit to Playback Range`: ON + - `Sampling Rate`: 24 or 30. Lower = smaller files. + - `Always Sample Animations`: ON. Constraints/drivers bake to keyframes. + +### Transform Panel + +- **+Y Up**: ON. Godot uses Y-up; this avoids a rotation node on import. +- **Scale**: 1.0. Ensure Blender units are meters. + +### Geometry Panel + +- `Use Tangents`: ON. Needed for normal mapping in Godot. +- `Loose Edges`: OFF. Not supported by Godot rendering anyway. +- `Loose Points`: OFF. +- `Triangulate`: OFF (Godot triangulates on import, but triangulating in Blender gives control). + +### Animation Panel (if applicable) + +- `Export Animations`: ON +- `Export Frame Range`: ON +- `Force Sampling`: ON +- `NLA Strips`: ON if using NLA for multiple actions +- `All Actions`: ON for character rigs with multiple clips + +## What Does NOT Export + +These Blender features are lost in glTF and therefore in Godot: + +- Procedural textures (Noise, Voronoi, Musgrave) → bake to image first +- Cycles/Eevee shader nodes beyond Principled BSDF → bake or simplify +- Geometry Nodes → apply as real mesh before export +- Curves/Surfaces/Metaballs → convert to mesh +- Text objects → convert to mesh or use Label3D in Godot +- Physics (rigid body, collision) → rebuild in Godot +- Drivers/Constraints → bake to keyframes or apply +- Layered materials / complex mix shaders → bake to single PBR set +- Subsurface scattering (glTF has limited support; Godot ignores) +- Volumetrics → not supported in glTF or Godot Mobile + +## Baking Strategy + +If your Blender scene uses complex materials, bake them to a PBR atlas before export: + +1. Create a new UV map (`UVMap_Bake`) with no overlaps. +2. Use Blender's Bake (Cycles) to bake: + - Combined (albedo) → `albedo.png` + - Roughness → `roughness.png` + - Metallic → `metallic.png` + - Normal → `normal.png` (non-color, OpenGL Y+) +3. Replace node tree with single Principled BSDF using baked images. +4. Export. Godot will import the baked textures. + +## Code Example: Batch Export Script + +Save as `export_collection.py` and run in Blender or via MCP: + +```python +import bpy +import os +import sys + +# Configuration +OUTPUT_DIR = os.path.expanduser("~/project/assets/models") +COLLECTION_NAME = "_EXPORT" +FORMAT = 'GLB' + +# Ensure output directory exists +os.makedirs(OUTPUT_DIR, exist_ok=True) + +# Get collection +col = bpy.data.collections.get(COLLECTION_NAME) +if not col: + print(f"Error: collection '{COLLECTION_NAME}' not found", file=sys.stderr) + sys.exit(1) + +# Set as active collection in view layer +vl = bpy.context.view_layer +for lc in vl.layer_collection.children: + lc.exclude = True +export_lc = vl.layer_collection.children.get(COLLECTION_NAME) +if export_lc: + export_lc.exclude = False + vl.active_layer_collection = export_lc +else: + # Link if missing + vl.layer_collection.collection.children.link(col) + vl.active_layer_collection = vl.layer_collection.children[COLLECTION_NAME] + +# Prepare objects +bpy.ops.object.select_all(action='DESELECT') +for obj in col.all_objects: + if obj.hide_viewport: + continue + if obj.type in {'MESH', 'ARMATURE', 'EMPTY'}: + obj.select_set(True) + # Apply transforms + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + +# Sanitize filename +blend_name = bpy.path.basename(bpy.data.filepath).replace(".blend", "") +if not blend_name: + blend_name = "untitled" +output_path = os.path.join(OUTPUT_DIR, f"{blend_name}.glb") + +# Export +bpy.ops.export_scene.gltf( + filepath=output_path, + export_format=FORMAT, + use_selection=True, + export_yup=True, + export_apply=True, + export_materials='EXPORT', + export_image_format='AUTO', + export_texcoords=True, + export_normals=True, + export_tangents=True, + export_draco_mesh_compression_enable=False, # Godot handles compression differently +) + +print(f"Exported: {output_path}") +``` + +## Draco Compression + +Blender can export Draco-compressed glTF. Godot supports Draco but decompresses at import time. For Quest VR: + +- **Do NOT use Draco** in Blender export. It adds import-time cost and does not reduce runtime memory. +- Use `gltfpack` or Godot's own import compression (`VRAM Compressed`) instead. + +## Extension Compatibility + +| glTF Extension | Blender Export | Godot Import | Notes | +|----------------|----------------|--------------|-------| +| KHR_materials_unlit | Via Shadeless | Supported | Use for UI, particles | +| KHR_lights_punctual | Yes | Ignored by default | Godot uses its own lights | +| KHR_texture_transform | Yes | Supported | UV offset/scale/tile | +| KHR_materials_clearcoat | Principled BSDF | Partial | Mobile renderer may ignore | +| KHR_materials_transmission | Principled BSDF | Partial | Use alpha modes instead | +| EXT_mesh_gpu_instancing | No | Supported | Use Godot MultiMesh instead | + +## Validation + +After export, validate with: +- **Online:** https://github.khronos.org/glTF-Validator/ +- **CLI:** Install `gltf-validator` npm package + +```bash +npx gltf-validator export.glb +``` + +Invalid glTF may crash Godot importer or produce silent errors. diff --git a/skills/software-development/blender-godot-pipeline/references/godot-import.md b/skills/software-development/blender-godot-pipeline/references/godot-import.md new file mode 100644 index 000000000000..319ea8182182 --- /dev/null +++ b/skills/software-development/blender-godot-pipeline/references/godot-import.md @@ -0,0 +1,244 @@ +# Godot Import Settings for Mobile Renderer & Quest VR + +Godot 4.3+ defaults to Forward+ renderer on desktop. Quest projects must use the Mobile renderer and adjust import settings accordingly. + +## Renderer Setup + +In `project.godot`: + +```ini +[rendering] +renderer/rendering_method="mobile" +renderer/rendering_method.mobile="mobile" +textures/vram_compression/import_etc2_astc=true +``` + +The Mobile renderer is required for Quest. Forward+ is too expensive and unsupported on Android XR. + +## Per-File Import Settings + +When a `.glb` is first seen by Godot, it creates a `.glb.import` file. These are the critical settings: + +### Meshes + +| Setting | Default | Quest Recommendation | Reason | +|---------|---------|----------------------|--------| +| `meshes/create_shadow_meshes` | true | false | Shadow meshes double memory; Quest is tight | +| `meshes/generate_lods` | true | true | Essential for performance | +| `meshes/force_disable_compression` | false | false | Keep OFF to save GPU memory | +| `skins/use_named_skins` | true | true | Needed for humanoid retargeting | + +### Materials + +| Setting | Default | Quest Recommendation | Reason | +|---------|---------|----------------------|--------| +| `materials/location` | 1 (Node) | 1 or 2 | 1 = built-in, 2 = extract to file | + +Extract materials (`location = 2`) if you need to edit them in Godot. Otherwise built-in is fewer files. + +### Animation + +| Setting | Default | Recommendation | +|---------|---------|----------------| +| `animations/import` | true | true | +| `animations/bake_animation` | true | true (for complex rigs) | +| `animations/optimizer/enabled` | true | true | +| `animations/optimizer/max_angle` | 0.1 | 0.5 (Quest: accept more error for smaller files) | + +### Storage + +| Setting | Default | Recommendation | +|---------|---------|----------------| +| `nodes/apply_root_scale` | true | true (Blender 1m = Godot 1m) | +| `nodes/root_scale` | 1.0 | 1.0 | + +## Extracting and Editing Materials + +After import, extract materials to edit: + +1. Select `.glb` in FileSystem +2. Import tab > `Materials > Location` = `Extract to File` +3. Reimport + +This creates `.tres` files alongside the `.glb`. Edit these for Quest optimization: + +```gdscript +# Example material adjustments for Quest +extends StandardMaterial3D + +func _init(): + # Disable expensive features + specular_mode = SPECULAR_DISABLED + roughness = 0.8 + metallic = 0.0 + # Use alpha scissor for cutouts + transparency = TRANSPARENCY_ALPHA_SCISSOR + alpha_scissor_threshold = 0.5 + # Backface culling unless double-sided needed + cull_mode = CULL_BACK + # Disable normal map if not visible in VR + normal_enabled = false +``` + +In the `.tres` file directly: + +```text +[resource] +albedo_color = Color(1, 1, 1, 1) +roughness = 0.8 +metallic = 0.0 +specular_mode = 0 +transparency = 2 +alpha_scissor_threshold = 0.5 +cull_mode = 2 +normal_enabled = false +``` + +## Import Cache Invalidation + +Godot caches imported assets in `.godot/imported/`. When re-exporting from Blender, Godot may not detect changes if timestamps are weird or if the file was replaced. + +### Symptoms + +- Old mesh still visible after re-export +- Material changes not reflected +- Missing new objects + +### Fix + +Delete the import metadata and cache: + +```bash +# From project root +rm -f assets/models/mymodel.glb.import +rm -rf .godot/imported/*mymodel* +``` + +Then switch back to Godot; it will auto-reimport. Or run headless: + +```bash +godot --headless --import +``` + +### Force Reimport All + +Editor > Tools > Reload Current Project (or restart Godot) sometimes works. For CI: + +```bash +# Nuke entire import cache (nuclear option) +rm -rf .godot/imported/ +godot --headless --import +``` + +## Texture Import Settings + +Textures imported alongside glTF (embedded or separate) get default settings. Override by selecting the imported texture in FileSystem: + +- **Compress**: `VRAM Compressed` (ETC2/ASTC on Quest) +- **Mipmaps**: Generate. Critical for VR to avoid aliasing. +- **Filter**: Linear Mipmap. Nearest only for pixel-art. +- **Repeat**: Enable for tiling textures, disable for atlases. +- **Max Size**: 2048 for hero, 1024 for props, 512 for UI. + +For lightmaps (if baked externally): +- **Compress**: `Lossless` or `VRAM Uncompressed` to avoid banding. +- **HDR**: Enable if using EXR. + +## Physics Import + +If the glTF contains collision proxies (e.g., simple cubes named `UCX_` or `_collision`): + +Godot does NOT auto-import collision from glTF. Options: + +1. **Import Script**: Assign a post-import script to the `.glb`. +2. **Manual**: Add CollisionShape3D nodes in Godot. +3. **Trimesh**: In import settings, `meshes/create_collision` = `Trimesh` (expensive) or `Convex` (faster). + +Example post-import script (`res://scripts/gltf_import.gd`): + +```gdscript +@tool +extends EditorScenePostImport + +func _post_import(scene: Node) -> Object: + _process_node(scene) + return scene + +func _process_node(node: Node) -> void: + if node.name.ends_with("_collision"): + var parent = node.get_parent() + if parent is MeshInstance3D: + var body = StaticBody3D.new() + parent.add_child(body) + body.owner = parent.owner + var shape = CollisionShape3D.new() + body.add_child(shape) + shape.owner = parent.owner + shape.shape = parent.mesh.create_trimesh_shape() + node.queue_free() + for child in node.get_children(): + _process_node(child) +``` + +Attach in Import tab > `Script`. + +## Scene Instantiation Best Practices + +After importing, instantiate the `.glb` into a scene: + +```gdscript +# In a scene script or at runtime +var scene := preload("res://assets/models/room.glb") +var instance := scene.instantiate() +add_child(instance) +``` + +For static environments, use **Merge Groups** or make the instance local (right-click > Make Local) to edit in place. + +For repeated props (chairs, crates): +- Use `MultiMeshInstance3D` for hundreds of identical objects. +- Or simply instance the same imported scene multiple times; Godot shares mesh resources. + +## Performance Validation + +After import and scene setup, check: + +1. **Debugger > Monitors** while running on Quest: + - Draw calls: aim for < 100 per frame + - Triangles: < 300k visible + - Texture memory: < 1GB total +2. **Rendering > Frame Time** with `display/driver/enable_vsync` off for profiling. +3. Use **XR Debugger** or `adb logcat` to check GPU frame times. + +Target: +- Quest 2: GPU < 13.9ms (72 FPS) +- Quest 3: GPU < 11.1ms (90 FPS) + +## Common Import Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `Failed to load resource` | Corrupt glTF or missing `.bin` | Re-export from Blender; validate glTF | +| `No loader found` | Wrong file extension or import plugin missing | Ensure `.glb` or `.gltf` | +| Black material | Missing UVs or texture not found | Check UV0, embed textures in `.glb` | +| Pink material | Shader compilation failed on Mobile renderer | Simplify material; no custom shaders on first test | +| Animation plays once | Loop mode not set | Set `loop_mode = 1` in AnimationPlayer or re-export with NLA | +| Wrong scale | Unit mismatch or root scale applied | Verify Blender 1m = Godot 1 unit; check `apply_root_scale` | + +## Summary Table: Quest-Optimized Import Preset + +Create a custom import preset in Godot (Import tab > Preset > Save) with these values: + +```ini +meshes/create_shadow_meshes=false +meshes/generate_lods=true +skins/use_named_skins=true +animations/import=true +animations/optimizer/enabled=true +animations/optimizer/max_angle=0.5 +nodes/apply_root_scale=true +nodes/root_scale=1.0 +materials/location=2 +``` + +Apply this preset to all environment and prop imports. diff --git a/skills/software-development/godot-quest-dev/SKILL.md b/skills/software-development/godot-quest-dev/SKILL.md new file mode 100644 index 000000000000..fdf232775afb --- /dev/null +++ b/skills/software-development/godot-quest-dev/SKILL.md @@ -0,0 +1,331 @@ +--- +name: godot-quest-dev +description: Master skill for Meta Quest 3/3S VR development with Godot 4.5+. Covers headless export, OpenXR setup, Meta Vendors plugin integration, manifest requirements, ADB sideload, and on-device debugging. Use when building, exporting, or troubleshooting Godot VR projects for Quest. +category: software-development +version: 1.0.0 +author: Hermes VR DevKit +license: MIT +metadata: + hermes: + tags: [quest, vr, openxr, godot, godot-4.5, android, linux, meta-quest, sideload] + related_skills: [blender-godot-pipeline, godot-xr-interactions, quest-native-toolchain, mcp-server-setup] +--- + +# Godot Quest VR Development + +Complete, battle-tested workflow for building and deploying Godot 4.5 VR applications to Meta Quest 3/3S from Linux. + +## When to Use + +- Creating a new Godot VR project targeting Quest +- Exporting an APK for Quest sideload +- Debugging why the app launches as a 2D screen instead of immersive VR +- Setting up OpenXR, XR controllers, or hand tracking +- Optimizing export pipeline for CI/CD (headless, no GUI) + +## Verified Environment + +| Component | Version | Status | +|-----------|---------|--------| +| Godot | 4.5-stable | Vulkan crash FIXED (was broken in 4.4.1) | +| Meta OpenXR Vendors plugin | 4.3.1-stable | Compatible with 4.5 (v5.0.1 requires 4.6+) | +| Android API level | 29+ | Required for OpenXR | +| Renderer | Mobile | Forward+ unavailable on Quest | +| Target FPS | 72 (Q2) / 90 (Q3/3S) | Must maintain consistent | + +## Quick Start: One-Command Export + +```bash +cd /path/to/your-godot-project +GODOT=$HOME/bin/godot ./build-and-run.sh all +``` + +**Prerequisites (one-time per project):** + +```bash +# 1. Install Android build template headlessly (Godot 4.5+) +$GODOT --headless --install-android-build-template +# Creates android/ directory with correct .build_version + +# 2. Download and extract Meta OpenXR Vendors plugin +cd /path/to/quest-dev-stack +wget https://github.com/GodotVR/godot_openxr_vendors/releases/download/4.3.1-stable/godotopenxrvendorsaddon.zip +# CRITICAL: zip has 'asset/' prefix +unzip -q godotopenxrvendorsaddon.zip && mv asset/addons /path/to/your-godot-project/ && rm -rf asset + +# 3. In Godot editor: Project -> Project Settings -> Plugins -> enable godotopenxrvendors +``` + +## Core Scene Structure + +``` +Main (Node3D) +├── XROrigin3D <- Player's physical space origin +│ ├── XRCamera3D <- Head-mounted display +│ ├── XRController3D (Left) <- Left controller +│ │ └── OpenXRRenderModel <- Platform-native mesh (Godot 4.5+) +│ └── XRController3D (Right) <- Right controller +│ └── OpenXRRenderModel +├── WorldEnvironment +└── GameWorld (Node3D) + └── ... level geometry +``` + +**Critical rule:** Apply thumbstick locomotion velocity to `XROrigin3D`, NOT the camera. + +## Starting the XR Session + +```gdscript +extends Node3D + +func _ready() -> void: + var xr_interface: XRInterface = XRServer.find_interface("OpenXR") + if xr_interface and xr_interface.is_initialized(): + get_viewport().use_xr = true + else: + push_error("OpenXR not available") +``` + +## Export Preset Requirements (Android Quest) + +In **Project -> Export -> Android Quest** preset: + +| Setting | Value | Why | +|---------|-------|-----| +| `xr_features/xr_mode` | `1` (OpenXR) | Immersive VR, not 2D | +| `gradle_build/use_gradle_build` | `true` | Required for OpenXR loader injection | +| `package/signed` | `false` | Sign manually with apksigner (Godot headless ignores keystore) | +| `package/app_category` | `Game` | **CRITICAL** -- defaults to `Accessibility`, which makes Quest launch as 2D screen | +| Architectures | `arm64` only | Quest is ARM64 | +| Minimum API | `29` | OpenXR requirement | + +**Renderer:** Project Settings -> Rendering -> Renderer -> Mobile. Forward+ is unavailable on Quest. + +**VSync:** Project Settings -> Display -> Window -> VSync Mode -> Disabled. XR runtime controls frame timing. + +## Godot-MCP Live Editor Integration + +Control the Godot editor in real-time via MCP tools. Create scenes, add nodes, set properties, attach scripts -- all without touching the GUI. + +See `references/godot-mcp-setup.md` for full setup details. + +### Quick Hermes Config + +```yaml +mcp_servers: + godot: + command: node + args: [/home/YOUR_USER/.local/share/godot-mcp/dist/index.js] + connect_timeout: 30 + timeout: 120 +``` + +### Critical Bug Fix: WebSocket Protocol Handshake + +Godot 4.5's `WebSocketPeer` does **not** negotiate subprotocols. The upstream TypeScript client sends `protocol: 'json'`, which causes the handshake to fail with "socket hang up." + +**Fix:** Remove the protocol option from the WebSocket constructor: + +```bash +# One-liner patch (apply after any `npm run build`) +sed -i "/protocol: 'json',/d" ~/.local/share/godot-mcp/dist/utils/godot_connection.js +``` + +Or patch the source so it survives rebuilds: + +```bash +# Patch source +sed -i "/protocol: 'json',/d" /path/to/Godot-MCP/server/src/utils/godot_connection.ts +# Then rebuild +cd /path/to/Godot-MCP/server && npm run build +cp -r dist ~/.local/share/godot-mcp/ +``` + +### Available Tools + +| Tool | Action | +|------|--------| +| `mcp_godot_create_scene` | New `.tscn` with root node type | +| `mcp_godot_open_scene` | Load existing scene | +| `mcp_godot_create_node` | Add node by type + name | +| `mcp_godot_delete_node` | Remove from tree | +| `mcp_godot_update_node_property` | Set any property | +| `mcp_godot_get_node_properties` | Read node state | +| `mcp_godot_list_nodes` | Scene tree dump | +| `mcp_godot_create_script` | New GDScript | +| `mcp_godot_edit_script` | Modify source | +| `mcp_godot_execute_editor_script` | Run GDScript in editor context | +| `mcp_godot_save_scene` | Persist to disk | +| `mcp_godot_get_current_scene` | Active scene info | + +### Godot-MCP Data Retrieval Pitfalls + +| Tool | Returns Data? | Workaround | +|------|---------------|------------| +| `execute_editor_script` | No (generic success only) | Use `get_node_properties` or write to temp file | +| `list_nodes` | No (generic success only) | Use `get_node_properties` on known parent | +| `update_node_property` | No | Use `execute_editor_script` with `mark_scene_as_unsaved()` or patch `.tscn` directly | +| `delete_node` | Yes (success) | Also marks scene modified -- preferred over `queue_free()` in scripts | + +**Always call `mcp_godot_save_scene` after modifications** to persist changes to disk. + +## Headless Export Pipeline + +```bash +# Export (unsigned) +$GODOT --headless --export-release "Android Quest" myapp-unsigned.apk + +# Sign +apksigner sign --ks debug.keystore --ks-pass pass:android \ + --key-pass pass:android --out myapp.apk myapp-unsigned.apk + +# Verify +apksigner verify myapp.apk + +# Install +adb install -r myapp.apk + +# Launch (prefer UI launch over adb monkey to avoid controller dialog) +adb shell am start -n com.yourcompany.yourapp/com.godot.game.GodotApp +``` + +**Build template version pinning:** After upgrading Godot, delete `android/` and re-run `--install-android-build-template`. Godot checks `android/.build_version`. + +## Manifest Requirements for Immersive VR + +For Quest OS to launch in VR mode (not 2D screen), the merged `AndroidManifest.xml` must contain: + +- `android.hardware.vr.headtracking` uses-feature +- `com.oculus.intent.category.VR` in MAIN activity intent-filter +- `com.oculus.vr.focusaware` metadata in MAIN activity +- `android:isGame="true"` on application element +- `android:appCategory="game"` on application element +- `extractNativeLibs="true"` for native library extraction + +**CRITICAL:** The `appCategory="game"` is set by the **Export Preset -> Package -> App Category** dropdown. Setting it to `Game` is the ONLY way. Patching manifest files manually is overridden by this dropdown. + +**CRITICAL:** Godot's build template has TWO manifest files -- `android/build/AndroidManifest.xml` AND `android/build/src/debug/AndroidManifest.xml`. The debug overlay overrides the main. The Meta OpenXR Vendors plugin auto-injects correct entries, but always verify the **App Category dropdown**. + +See `references/manifest-requirements.md` for the full manifest anatomy. + +## Godot 4.5+ XR Features + +### Foveated Rendering (Mobile Vulkan) + +Standalone VR headsets now support foveated rendering via `VK_EXT_fragment_density_map`: + +1. Ensure Renderer is **Mobile** (not Forward+) +2. In the vendor plugin settings, enable **Foveated Rendering** and choose density level (`LOW`, `MEDIUM`, `HIGH`) +3. Extension is requested automatically at runtime -- no Vulkan code needed + +### Application SpaceWarp (ASW) + +Frame-synthesis technique for Quest/Pico. GPU renders every other frame; runtime synthesizes missing frames using motion vectors: + +1. Update to OpenXR Vendors plugin 4.x (supports ASW) +2. In export preset extras, enable **Application SpaceWarp** +3. Set target frame rate to half native (e.g., 40 Hz on Quest 3 at 80 Hz mode) + +**Caution:** Ghosting artifacts on fast-moving objects. Disable per-object if glitches appear. + +### OpenXR Render Models + +Platform supplies animated, branded controller meshes at runtime. No need to bundle Quest Touch meshes: + +```gdscript +@onready var render_model: Node3D = $XRController3D/OpenXRRenderModel + +func _ready() -> void: + render_model.visible = true # Platform-native controller model +``` + +Add `OpenXRRenderModel` nodes as children of each `XRController3D` -- plugin populates them automatically. + +## Godot 4.6+ Preview Features + +### OpenXR 1.1 Support + +Godot 4.6 ships native OpenXR 1.1 runtime support. No API changes required -- engine negotiates spec version at startup. + +### Spatial Entities (Anchors, Plane Tracking) + +```gdscript +var anchor: XRSpatialAnchor = XRSpatialAnchor.new() +anchor.position = world_position +add_child(anchor) +# Runtime keeps locked to real-world location +# Persist anchor UUID to restore on next launch +``` + +Requires OpenXR Spatial Entities extension enabled in the vendor plugin. + +## Debugging on Quest + +### Logcat Filtering + +```bash +# Real-time logs +adb logcat -s godot:V XR:V DEBUG:V *:S + +# Follow stream +adb logcat --follow -s godot + +# Crash buffer +adb logcat --buffer crash + +# Filter for specific patterns +adb logcat -d | grep -i "fatal\|crash\|exception\|anr" +``` + +### Symptom Diagnosis + +| Symptom | Check | Fix | +|---------|-------|-----| +| App launches as 2D screen | `adb logcat -d \| grep "isVrApplication"` | Set App Category to `Game` in export preset; verify manifest has `com.oculus.intent.category.VR` | +| `XR_ERROR_FORM_FACTOR_UNSUPPORTED` | `adb logcat -s XR` | Missing `libopenxr_loader.so` or VR manifest entries. Plugin handles this if enabled. | +| Black screen in headset | `adb logcat -s godot` for Vulkan errors | Check `use_xr = true` on viewport; verify Mobile renderer; check shader compilation | +| Controller input not firing | OpenXR action map bindings | Check Project Settings -> XR -> OpenXR -> Action Map | +| Frame drops / stuttering | `adb logcat -s VrApi \| grep FPS` | Reduce draw calls (<100/eye), simplify shaders, enable foveated rendering | +| Hand tracking jittery | Raw joint data | Apply smoothing (lerp toward new position each frame) | + +### Performance Budgets (Quest 3) + +| Metric | Target | +|--------|--------| +| Draw calls | < 100 per eye | +| Tris per frame | < 500k | +| Texture memory | < 1GB total | +| CPU time | < 11ms/frame (90Hz) | +| GPU time | < 11ms/frame | + +## Common Pitfalls + +- **App Category dropdown overrides manifest patches** -- Always set to `Game` +- `--install-android-build-template` is REQUIRED -- Manual `android/` extraction is silently ignored +- Plugin zip has `asset/` prefix -- Extract with: `unzip -q zip; mv asset/addons .; rm -rf asset` +- `adb monkey -p 1` triggers controller dialog -- Use `adb shell am start -n pkg/activity` +- Quest OS may pause app if `headtracking required="false"` -- Ensure `android.hardware.vr.headtracking` uses-feature exists +- Mobile renderer supports glow/bloom but **NOT SSAO** -- Do not enable SSAO in WorldEnvironment +- Godot 4.4.1 Vulkan crash on Quest 3 is **FIXED in 4.5** -- Do not use 4.4.1 for Quest VR +- **Opaque sky domes hide the scene** -- A `SkyBox` mesh with no override material uses default opaque white/gray. Check with `get_surface_override_material(0)` -- if null, the mesh is opaque. Fix: assign a transparent StandardMaterial3D, or scale the dome to 200+ meters so it's far beyond the scene, or remove it and use WorldEnvironment sky only. +- **`execute_editor_script` + `queue_free()` does NOT persist** -- Calling `queue_free()` inside `execute_editor_script` modifies the runtime tree but does NOT trigger the scene modification flag. **Always use `mcp_godot_delete_node` instead** -- it calls `_mark_scene_modified()` internally. Then call `mcp_godot_save_scene`. +- **`update_node_property` does NOT mark scene modified** -- Setting properties via `mcp_godot_update_node_property` changes runtime values but does NOT flag the scene as dirty. **Workaround:** Use `mcp_godot_execute_editor_script` with explicit `editor.get_editor_interface().mark_scene_as_unsaved()` call, or directly patch the `.tscn` file for batch changes. Then `mcp_godot_save_scene`. +- **`execute_editor_script` does NOT return data** -- The command runs successfully but the response is only `{"message": "Command processed", "status": "success"}`. **Use `get_node_properties` or `get_current_scene` for data retrieval**, or run scripts that write to a temp file. +- **`list_nodes` also swallows response data** -- Same as `execute_editor_script`. **Workaround:** Use `get_node_properties` on a known parent, or use `execute_editor_script` to write node names to a file. +- **Don't background Godot on Wayland GUI sessions** -- `terminal(background=true)` with Godot causes the editor window to rapidly open/close/flicker on the user's screen. **Launch Godot in foreground** (or let the user open it manually), then connect MCP tools to the running instance. +- **Godot GLB import fails with "No loader found"** -- Godot caches import metadata (UIDs, `.scn` files) in the `.godot/` folder. If you add a GLB reference to a `.tscn` with a **fake UID** before Godot has imported the file, the loader fails. **Fix sequence:** + 1. Remove the GLB `ext_resource` reference from the `.tscn` + 2. Delete the stale `.godot/` folder entirely + 3. Run headless import to rebuild: `$GODOT --headless --editor --quit-after 20` + 4. Check `.godot/imported/` for the new `.scn` and the `.import` file for the real UID + 5. Re-add the `ext_resource` to `.tscn` with the **real UID** + 6. **Never** fabricate UIDs -- always import first, reference second +- **Stale Godot-MCP node server blocks reconnection** -- If a previous `node ~/.local/share/godot-mcp/dist/index.js` process is still running, Hermes may connect to it but get stale/cached data. **Fix:** `kill $(pgrep -f "godot-mcp/dist/index.js")` before starting a new session. Then verify with `hermes mcp test godot`. +- **execute_editor_script timeout** -- Complex scripts with nested loops may exceed the MCP command timeout. Break into smaller queries. + +## References + +- `references/godot-mcp-setup.md` -- Godot-MCP installation, protocol fix, and usage +- `references/headless-export.md` -- Complete headless CI/CD export workflow +- `references/manifest-requirements.md` -- Exact manifest entries required for Quest immersive VR +- `references/quest-validation.md` -- Validation checklist for a working Quest build diff --git a/skills/software-development/godot-quest-dev/references/godot-mcp-setup.md b/skills/software-development/godot-quest-dev/references/godot-mcp-setup.md new file mode 100644 index 000000000000..c164e3e9043f --- /dev/null +++ b/skills/software-development/godot-quest-dev/references/godot-mcp-setup.md @@ -0,0 +1,138 @@ +# Godot-MCP Setup and Usage + +Godot-MCP enables real-time control of the Godot editor via MCP tools. Create scenes, add nodes, set properties, attach scripts -- all programmatically. + +## Installation + +### 1. Clone and Build + +```bash +git clone --depth 1 https://github.com/ee0pdt/Godot-MCP.git +cd Godot-MCP/server +npm install && npm run build +``` + +### 2. Install Godot Addon + +```bash +mkdir -p /path/to/your-project/addons +cp -r Godot-MCP/addons/godot_mcp /path/to/your-project/addons/ +``` + +In Godot editor: **Project -> Project Settings -> Plugins -> enable Godot MCP**. The WebSocket server starts automatically on port 9080 (check Output panel). + +### 3. Copy Server to Persistent Location + +```bash +mkdir -p ~/.local/share/godot-mcp +cp -r Godot-MCP/server/dist Godot-MCP/server/node_modules \ + Godot-MCP/server/package.json ~/.local/share/godot-mcp/ +``` + +### 4. Hermes Config + +```yaml +mcp_servers: + godot: + command: node + args: [/home/YOUR_USER/.local/share/godot-mcp/dist/index.js] + connect_timeout: 30 + timeout: 120 +``` + +Restart Hermes (`/reload`) to discover tools. + +## Critical Fix: WebSocket Protocol Handshake (Godot 4.5) + +Godot 4.5's `WebSocketPeer` does not negotiate subprotocols. The upstream client sends `protocol: 'json'`, causing handshake failure. + +**Fix script:** +```bash +#!/bin/bash +SERVER_DIR="${1:-$HOME/.local/share/godot-mcp}" +sed -i "/protocol: 'json',/d" "$SERVER_DIR/dist/utils/godot_connection.js" +echo "Patched: $SERVER_DIR/dist/utils/godot_connection.js" +``` + +Run after every `npm run build`. + +## Available Tools + +| Tool | Purpose | +|------|---------| +| `mcp_godot_create_scene` | New `.tscn` with root node type | +| `mcp_godot_open_scene` | Load existing scene | +| `mcp_godot_create_node` | Add node by type + name | +| `mcp_godot_delete_node` | Remove from tree (marks scene modified) | +| `mcp_godot_update_node_property` | Set any property (does NOT mark modified) | +| `mcp_godot_get_node_properties` | Read node state (returns actual data) | +| `mcp_godot_list_nodes` | Scene tree dump (swallows data -- see workaround) | +| `mcp_godot_create_script` | New GDScript | +| `mcp_godot_edit_script` | Modify source | +| `mcp_godot_execute_editor_script` | Run GDScript in editor context (does NOT return data) | +| `mcp_godot_save_scene` | Persist to disk | +| `mcp_godot_get_current_scene` | Active scene info | + +## Data Retrieval Workarounds + +`execute_editor_script` and `list_nodes` return generic success messages without the actual data. To get real data: + +**Option A:** Use `get_node_properties` (returns JSON state). + +**Option B:** Write to a temp file in the script, then read it back: + +```gdscript +var f = FileAccess.open("/tmp/godot_debug.json", FileAccess.WRITE) +f.store_string(JSON.stringify({"dome_size": str(aabb.size)})) +f.close() +``` + +Then read `/tmp/godot_debug.json` via `read_file`. + +**Option C:** For complex queries, use `get_node_properties` on a parent to get child lists, or inspect `.tscn` files directly (they're plain text). + +## Persistence Rules + +| Operation | Persists? | Notes | +|-----------|-----------|-------| +| `delete_node` | Yes | Calls `_mark_scene_modified()` internally | +| `update_node_property` | No | Must call `save_scene` after, or changes vanish on reload | +| `execute_editor_script` + `queue_free()` | No | Use `delete_node` instead | +| `execute_editor_script` + property set | No | Must mark scene unsaved explicitly or call `save_scene` | + +**Always call `save_scene` after modifications.** + +## Scene Debugging via execute_editor_script + +For deep inspection that `get_node_properties` can't reach (AABB bounds, mesh class, material transparency, finding duplicates): + +```gdscript +# Example: inspect dome mesh and scene bounds +var dome = get_tree().edited_scene_root.find_child("SkyDome", true, false) +var aabb = dome.get_aabb() +# Write to temp file since execute_editor_script doesn't return data +var f = FileAccess.open("/tmp/godot_debug.json", FileAccess.WRITE) +f.store_string(JSON.stringify({"dome_size": str(aabb.size)})) +f.close() +``` + +```gdscript +# Example: find duplicate nodes +var dupes = [] +for c in get_tree().edited_scene_root.get_children(): + for cc in c.get_children(): + if cc.name.ends_with("_001"): + dupes.append(cc.name) +result = dupes +``` + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| "socket hang up" on connect | Run `fix-protocol.sh` -- remove `protocol: 'json'` | +| Tools not appearing after config | Restart Hermes with `/reload` or relaunch | +| Stale data from previous session | `kill $(pgrep -f "godot-mcp/dist/index.js")` then reconnect | +| Scene changes not persisting | `update_node_property` doesn't mark modified -- call `save_scene` or use `delete_node` | +| Script timeout | Break complex scripts into smaller queries | +| Editor not responding | Ensure Godot MCP plugin is enabled and WebSocket server started (check Output panel) | diff --git a/skills/software-development/godot-quest-dev/references/headless-export.md b/skills/software-development/godot-quest-dev/references/headless-export.md new file mode 100644 index 000000000000..e417ec53a5ba --- /dev/null +++ b/skills/software-development/godot-quest-dev/references/headless-export.md @@ -0,0 +1,74 @@ +# Headless Export Workflow + +Godot 4.5 supports fully automated headless export for CI/CD pipelines. + +## One-Time Per Project + +```bash +cd /path/to/your-godot-project +$GODOT --headless --install-android-build-template +# Creates android/ directory with correct .build_version +``` + +**CRITICAL:** Do NOT manually extract `android_source.zip`. The `--install-android-build-template` flag creates internal state that Godot's export system requires. Manual extraction is silently ignored. + +**Build template version pinning:** After upgrading Godot, delete `android/` and re-run `--install-android-build-template`. Godot checks `android/.build_version`. + +## Export Command + +```bash +$GODOT --headless --export-release "Android Quest" myapp-unsigned.apk +``` + +## Sign Manually + +Godot headless ignores keystore paths for OpenXR presets. Export unsigned, then sign: + +```bash +apksigner sign --ks debug.keystore --ks-pass pass:android \ + --key-pass pass:android --out myapp.apk myapp-unsigned.apk +apksigner verify myapp.apk +``` + +## Full Pipeline Script + +```bash +#!/bin/bash +set -e +PROJECT_DIR="${1:-.}" +GODOT="${GODOT:-$HOME/bin/godot}" +KEYSTORE="${KEYSTORE:-$HOME/.android/debug.keystore}" +APK_NAME="${2:-myapp}" + +cd "$PROJECT_DIR" + +# Export +$GODOT --headless --export-release "Android Quest" "${APK_NAME}-unsigned.apk" + +# Sign +apksigner sign --ks "$KEYSTORE" --ks-pass pass:android \ + --key-pass pass:android --out "${APK_NAME}.apk" "${APK_NAME}-unsigned.apk" + +# Verify +apksigner verify "${APK_NAME}.apk" + +echo "Built: ${APK_NAME}.apk" +``` + +## Godot Editor Settings + +Stored in `~/.config/godot/editor_settings-4.5.tres`: + +```tres +[resource] +export/android/android_sdk_path = "/home/USER/android-sdk" +export/android/java_sdk_path = "/usr/lib/jvm/java-17-openjdk-amd64" +export/android/debug_keystore = "/home/USER/.android/debug.keystore" +export/android/debug_keystore_pass = "android" +``` + +## What Does NOT Work + +- Manually extracting `android_source.zip` to `android/` -- Godot ignores it +- Headless export before running `--install-android-build-template` -- fails +- Setting `package/signed=true` in preset for headless -- Godot ignores keystore path diff --git a/skills/software-development/godot-quest-dev/references/manifest-requirements.md b/skills/software-development/godot-quest-dev/references/manifest-requirements.md new file mode 100644 index 000000000000..d554560b5cbe --- /dev/null +++ b/skills/software-development/godot-quest-dev/references/manifest-requirements.md @@ -0,0 +1,87 @@ +# Quest VR Manifest Requirements + +For the APK to launch in **immersive VR mode** on Quest (not as a flat 2D screen), the merged `AndroidManifest.xml` must contain specific entries. + +## Required Entries + +```xml + + + + + + + + + + + + + + + +``` + +## The App Category Dropdown Override + +**CRITICAL:** Godot's export preset has an **"App Category"** dropdown under Package options that defaults to `Accessibility`. This dropdown **overrides** any `android:appCategory` changes you make in manifest files. **Must set to `Game`** for Quest immersive mode. + +## Two Manifest Files + +Godot's build template maintains: +1. `android/build/AndroidManifest.xml` -- main manifest +2. `android/build/src/debug/AndroidManifest.xml` -- debug overlay + +The debug overlay **overrides** the main. The Meta OpenXR Vendors plugin auto-injects correct entries, but always verify both files. + +## Verification Commands + +```bash +# Decompile APK and check manifest +unzip -p myapp.apk AndroidManifest.xml | xxd | head -100 +# Or use aapt: +aapt dump xmltree myapp.apk AndroidManifest.xml | grep -E "headtracking|VR|focusaware|isGame|appCategory" + +# Check on device +adb shell dumpsys package com.yourcompany.yourapp | grep -i "vr\|game" +``` + +## Post-Export Manifest Surgery (Last Resort) + +If the App Category dropdown is not available or not working: + +```bash +# 1. Decompile +apktool d myapp.apk -o myapp-decompiled + +# 2. Patch manifest +sed -i 's/android:appCategory="[^"]*"/android:appCategory="game"/' myapp-decompiled/AndroidManifest.xml +sed -i 's/android:isGame="[^"]*"/android:isGame="true"/' myapp-decompiled/AndroidManifest.xml + +# 3. Ensure VR category and focusaware metadata exist +# (Add manually if missing -- see Required Entries above) + +# 4. Recompile +apktool b myapp-decompiled -o myapp-recompiled.apk + +# 5. Re-sign +apksigner sign --ks debug.keystore --ks-pass pass:android \ + --key-pass pass:android --out myapp.apk myapp-recompiled.apk +``` + +## Common Manifest Errors + +| Logcat Message | Missing Entry | +|----------------|---------------| +| `"isVrApplication":false` | `com.oculus.intent.category.VR` or `com.oculus.vr.focusaware` | +| `XR_ERROR_FORM_FACTOR_UNSUPPORTED` | `android.hardware.vr.headtracking` or `libopenxr_loader.so` | +| `SurfaceView rendering instead of VR swapchain` | `android:appCategory="game"` or `isGame="true"` | +| `VK_ERROR_SURFACE_LOST_KHR` | App launching in 2D mode -- check all manifest entries | diff --git a/skills/software-development/godot-quest-dev/references/quest-validation.md b/skills/software-development/godot-quest-dev/references/quest-validation.md new file mode 100644 index 000000000000..c3e50bd265d4 --- /dev/null +++ b/skills/software-development/godot-quest-dev/references/quest-validation.md @@ -0,0 +1,52 @@ +# Quest Build Validation Checklist + +Run this checklist after every export to confirm the APK will launch in immersive VR mode. + +## Pre-Export Checks + +- [ ] Renderer set to **Mobile** (Project Settings -> Rendering -> Renderer) +- [ ] VSync disabled (Project Settings -> Display -> Window -> VSync Mode) +- [ ] Export preset `xr_features/xr_mode = 1` (OpenXR) +- [ ] Export preset `gradle_build/use_gradle_build = true` +- [ ] Export preset `package/app_category = Game` (NOT Accessibility) +- [ ] Export preset `package/signed = false` (we sign manually) +- [ ] Architectures = `arm64` only +- [ ] Minimum API = 29 +- [ ] Meta OpenXR Vendors plugin enabled (Project Settings -> Plugins) +- [ ] `android/.build_version` matches Godot version +- [ ] `icon.svg` exists in project root (headless export requires it) + +## Post-Export Checks + +- [ ] APK file exists and is > 5MB +- [ ] `apksigner verify myapp.apk` passes +- [ ] `aapt dump xmltree myapp.apk AndroidManifest.xml | grep -i "vr\|game"` shows correct entries +- [ ] `libopenxr_loader.so` exists in APK: `unzip -l myapp.apk | grep libopenxr_loader` + +## On-Device Checks + +- [ ] `adb devices` shows Quest in dev mode +- [ ] `adb install -r myapp.apk` succeeds +- [ ] App launches without "controller required" dialog (use `am start`, not monkey) +- [ ] `adb logcat -s godot` shows OpenXR initialization +- [ ] `adb logcat -s XR` shows no errors +- [ ] `adb logcat -s VrApi | grep FPS` shows target framerate (72 or 90) +- [ ] No black screen, controllers render, tracking works + +## Smoke Test Commands + +```bash +# Install and launch +adb install -r myapp.apk +adb shell am start -n com.yourcompany.yourapp/com.godot.game.GodotApp + +# Watch logs +adb logcat -s godot:V XR:V VrApi:V DEBUG:V *:S + +# Check frame rate +adb logcat -s VrApi:V | grep FPS + +# Screenshot (for remote debugging) +adb shell screencap -p /sdcard/screen.png +adb pull /sdcard/screen.png +``` diff --git a/skills/software-development/godot-xr-interactions/SKILL.md b/skills/software-development/godot-xr-interactions/SKILL.md new file mode 100644 index 000000000000..eac10727b653 --- /dev/null +++ b/skills/software-development/godot-xr-interactions/SKILL.md @@ -0,0 +1,309 @@ +--- +name: godot-xr-interactions +description: Use when implementing VR interactions in Godot 4.5+ for Meta Quest -- locomotion, grabbing, hand tracking, passthrough, XR UI, haptics, and OpenXR action maps. Project-agnostic patterns for immersive controller and hand-tracked input. +category: software-development +version: 1.0.0 +author: Hermes VR DevKit +license: MIT +metadata: + hermes: + tags: [quest, vr, openxr, godot, godot-4.5, xr-interactions, locomotion, hand-tracking, haptics, xr-ui] + related_skills: [godot-quest-dev, blender-godot-pipeline, quest-native-toolchain, mcp-server-setup] +--- + +# Godot XR Interactions + +Project-agnostic interaction patterns for Godot 4.5+ VR on Meta Quest. Covers locomotion, grabbing, hand tracking, passthrough, XR UI, haptics, and the OpenXR action map. + +## When to Use + +- Adding teleport, snap-turn, or smooth locomotion to a VR scene +- Implementing grab/pickup mechanics with controllers or hands +- Switching between controller and hand tracking input +- Setting up passthrough (AR) mode +- Building in-world XR UI panels +- Configuring haptic feedback or OpenXR actions + +## OpenXR Action Map + +Define actions in **Project Settings -> XR -> OpenXR -> Action Map**. Bind them to Quest controller and hand interaction profiles. + +| Action | Type | Suggested Bindings | +|--------|------|-------------------| +| `move` | Vector2 | Left thumbstick / thumbstick on hand interaction | +| `turn` | Vector2 | Right thumbstick (X axis) / hand interaction thumbstick | +| `trigger_click` | Bool | Right trigger, Index pinch (hand) | +| `grab_click` | Bool | Right grip, Middle finger pinch (hand) | +| `primary_action` | Bool | A button (right), Y button (left) | +| `secondary_action` | Bool | B button (right), X button (left) | +| `menu` | Bool | Left menu button | +| `haptic` | Haptic | Both controllers | + +**Hand interaction profile:** Godot 4.5+ includes `Hand Interaction Profile`. Enable it alongside `Touch Controller Profile` so the same action map works for both controllers and hand tracking without code changes. + +## Core Scene Setup for Interactions + +``` +XROrigin3D +├── XRCamera3D +├── XRController3D (Left) +│ └── GrabArea (Area3D) <- For controller grab detection +│ └── CollisionShape3D +├── XRController3D (Right) +│ └── GrabArea (Area3D) +│ └── CollisionShape3D +└── LeftHand (XRHandModifier3D) <- For hand tracking mesh +``` + +**GrabArea setup:** Attach an `Area3D` to each controller. Give it a small spherical `CollisionShape3D` (radius ~0.05 m) centered on the controller origin. Enable **Monitoring** and **Monitorable**. Use `body_entered` / `body_exited` to detect grabbable objects. + +## Grabbing with Controllers + +Grabbable objects need a `RigidBody3D` or `StaticBody3D` with a collision shape and a script responding to grab events. + +```gdscript +extends RigidBody3D +class_name Grabbable + +var is_grabbed: bool = false +var grabber: Node3D = null +var grab_offset: Transform3D = Transform3D.IDENTITY + +func _process(_delta: float) -> void: + if is_grabbed and grabber: + # Move to controller position plus original offset + global_transform = grabber.global_transform * grab_offset + +func grab(controller: Node3D) -> void: + if is_grabbed: + return + is_grabbed = true + grabber = controller + grab_offset = controller.global_transform.affine_inverse() * global_transform + freeze = true # Disable physics while held + +func release(impulse: Vector3 = Vector3.ZERO) -> void: + if not is_grabbed: + return + is_grabbed = false + grabber = null + freeze = false + if impulse.length() > 0.01: + apply_central_impulse(impulse) +``` + +**Grab controller script** (attached to XRController3D): + +```gdscript +extends XRController3D + +@export var grab_area: Area3D +var held_object: Grabbable = null + +func _ready() -> void: + button_pressed.connect(_on_button_pressed) + if grab_area: + grab_area.body_entered.connect(_on_body_entered) + +func _on_button_pressed(action: String) -> void: + if action == "trigger_click": + if held_object: + # Release with throw impulse based on controller velocity + var vel := get_input("velocity") as Vector3 + held_object.release(vel * 1.5) + held_object = null + else: + # Try grab closest grabbable in area + var closest: Grabbable = null + var closest_dist := INF + for body in grab_area.get_overlapping_bodies(): + if body is Grabbable: + var d := global_position.distance_to(body.global_position) + if d < closest_dist: + closest_dist = d + closest = body + if closest: + held_object = closest + held_object.grab(self) +``` + +## Hand Tracking + +Enable hand tracking in **Project Settings -> XR -> OpenXR -> Hand Tracking** (`Enabled` or `Optional`). Use `XRHandModifier3D` on a `Skeleton3D` inside a hand mesh, or use `OpenXRHand` node for basic joint visualization. + +### Hand Tracking Smoothing + +Raw hand joints jitter. Apply exponential smoothing: + +```gdscript +extends XRHandModifier3D + +@export var smoothing: float = 0.15 # 0 = instant, 1 = frozen +var smoothed_poses: Dictionary = {} + +func _process(_delta: float) -> void: + for j in XRHandTracker.HAND_JOINT_MAX: + var tracker := XRServer.get_tracker(get_tracker()) as XRHandTracker + if not tracker: + continue + var raw: Transform3D = tracker.get_hand_joint_transform(j) + var key := str(j) + if not smoothed_poses.has(key): + smoothed_poses[key] = raw + else: + smoothed_poses[key] = smoothed_poses[key].interpolate_with(raw, 1.0 - smoothing) + # Apply to skeleton bone if mapping exists + _apply_to_bone(j, smoothed_poses[key]) + +func _apply_to_bone(joint: int, t: Transform3D) -> void: + # Map joint index to Skeleton3D bone index based on your rig + pass +``` + +**Pinch detection for hand grabbing:** Check distance between index tip and thumb tip. + +```gdscript +func is_pinching(tracker: XRHandTracker, threshold: float = 0.02) -> bool: + var index_tip := tracker.get_hand_joint_transform(XRHandTracker.HAND_JOINT_INDEX_TIP).origin + var thumb_tip := tracker.get_hand_joint_transform(XRHandTracker.HAND_JOINT_THUMB_TIP).origin + return index_tip.distance_to(thumb_tip) < threshold +``` + +## Haptic Feedback + +```gdscript +# Trigger haptic pulse on a controller +func trigger_haptic(controller: XRController3D, amplitude: float = 0.5, duration: float = 0.1) -> void: + controller.trigger_haptic_pulse("haptic", amplitude, duration, 0.0) +``` + +**Common haptic patterns:** + +| Event | Amplitude | Duration (s) | +|-------|-----------|--------------| +| Hover over grabbable | 0.1 | 0.05 | +| Grab success | 0.6 | 0.1 | +| Release / throw | 0.3 | 0.08 | +| Invalid action | 0.8 | 0.15 | +| Teleport confirm | 0.4 | 0.1 | + +## Passthrough (AR Mode) + +Enable passthrough on Quest via the Meta OpenXR Vendors plugin. + +```gdscript +extends Node3D + +func enable_passthrough() -> void: + var xr_interface := XRServer.find_interface("OpenXR") + if xr_interface: + # Requires Meta OpenXR Vendors plugin + xr_interface.start_passthrough() + # Set environment to transparent clear color + get_viewport().transparent_bg = true + get_viewport().use_xr = true +``` + +**Requirements:** +- Meta OpenXR Vendors plugin enabled +- `XR_MODE_PASSTHROUGH` or similar mode configured in export settings +- Scene uses transparent background or masked geometry + +## XR UI Quick Setup + +Render a Godot Control UI onto a 3D quad in the world using SubViewport. + +``` +UIQuad (MeshInstance3D) +├── SubViewport +│ └── CanvasLayer +│ └── Control +│ └── Button, Label, etc. +└── QuadMesh (size 1.0 x 0.6) +``` + +```gdscript +extends MeshInstance3D + +@export var subviewport: SubViewport + +func _ready() -> void: + var mat := StandardMaterial3D.new() + mat.albedo_texture = subviewport.get_texture() + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + material_override = mat + subviewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS +``` + +### XR UI Placement Rules + +| Rule | Value | Why | +|------|-------|-----| +| Comfortable viewing distance | 1.5 - 3.0 m | Closer causes eye strain; farther reduces legibility | +| Panel width in world units | 1.0 - 2.0 m | Maps to readable pixel density at Quest resolution | +| Minimum text size on SubViewport | 28 px | Scales to readable height at 2 m distance | +| Critical content within horizontal +/- 30 deg | Keep buttons/labels inside this cone | Edge distortion and neck strain beyond | +| Vertical placement | -15 to +15 deg from eye level | Avoid looking too far up/down | +| UI should face player | Use `look_at(player_position)` | Prevents reading at oblique angles | + +**Billboard script for UI panels:** + +```gdscript +extends Node3D + +@export var target: Node3D # XROrigin3D or XRCamera3D + +func _process(_delta: float) -> void: + if target: + look_at(target.global_position, Vector3.UP) + # Only rotate on Y (optional -- keeps panel vertical) + rotation.x = 0 + rotation.z = 0 +``` + +## Comfort and Safety Rules + +| Interaction | Rule | +|-------------|------| +| Snap turn angle | 30 or 45 degrees per click | +| Snap turn cooldown | 0.2 - 0.3 s minimum between turns | +| Smooth turn speed | Max 60 deg/sec; provide snap-turn option | +| Smooth locomotion speed | Max 3-5 m/sec; scale by thumbstick deflection | +| Teleport arc height | Parabolic arc, max ~2 m above ground | +| Teleport surface check | Require valid navmesh or upward-facing normal; reject steep slopes (>30 deg) | +| Vertical camera movement | Never move camera vertically without user control (causes nausea) | +| Acceleration | Instant velocity changes preferred over smooth acceleration for teleport | +| Field of view reduction | Consider vignette during smooth locomotion (optional comfort setting) | + +## Controller Velocity for Throwing + +Use `XRController3D.get_input("velocity")` (requires velocity action in action map) or compute from transform deltas: + +```gdscript +func get_controller_velocity(controller: XRController3D, delta: float) -> Vector3: + # If action map has velocity input, prefer it: + var vel := controller.get_input("velocity") + if vel is Vector3: + return vel + # Fallback: differentiate position + return (controller.global_position - _last_pos) / delta +``` + +## Input Switching: Controllers vs Hands + +Godot 4.5+ automatically switches interaction profiles when the user puts down controllers and shows hands. No code changes needed if you used the OpenXR action map correctly. To detect which mode is active: + +```gdscript +func get_active_tracker_name(hand: XRPositionalTracker.TrackerHand) -> String: + var tracker := XRServer.get_tracker(XRServer.get_tracker_for_hand(hand)) + if tracker: + return tracker.name + return "" + +# Returns something like "/user/hand/left" -- profile changes automatically +``` + +## References + +- **Locomotion Patterns** (`references/locomotion-patterns.md`): Teleport + snap-turn, smooth locomotion, parabolic arc, surface validation. +- **XR UI Patterns** (`references/xr-ui-patterns.md`): SubViewport-on-quad deep dive, curved panels, text sizing, dynamic placement. diff --git a/skills/software-development/godot-xr-interactions/references/locomotion-patterns.md b/skills/software-development/godot-xr-interactions/references/locomotion-patterns.md new file mode 100644 index 000000000000..37f14fcb7432 --- /dev/null +++ b/skills/software-development/godot-xr-interactions/references/locomotion-patterns.md @@ -0,0 +1,270 @@ +# Locomotion Patterns + +Project-agnostic locomotion implementations for Godot 4.5+ Quest VR. Teleport + snap-turn is recommended as the default. Smooth locomotion is optional and should always be user-configurable. + +## Recommended: Teleport + Snap Turn + +Teleport avoids nausea by eliminating continuous optic flow. Snap turn reduces vestibular conflict. + +### Scene Setup + +``` +XROrigin3D +├── XRCamera3D +├── XRController3D (Left) <- movement / teleport +└── XRController3D (Right) <- snap turn +``` + +### Teleport Script + +```gdscript +extends XRController3D + +@export var other_controller: XRController3D # For arc visualization origin if needed +@export var max_distance: float = 15.0 +@export var arc_segments: int = 20 +@export var valid_color: Color = Color(0.0, 1.0, 0.0, 0.8) +@export var invalid_color: Color = Color(1.0, 0.0, 0.0, 0.8) + +var is_teleporting: bool = false +var teleport_target: Vector3 = Vector3.ZERO +var teleport_valid: bool = false + +@onready var arc_mesh: MeshInstance3D = $ArcMesh +@onready var target_marker: MeshInstance3D = $TargetMarker + +func _process(delta: float) -> void: + var thumbstick: Vector2 = get_input("move") as Vector2 + if thumbstick.y < -0.5: # Push forward to aim + if not is_teleporting: + is_teleporting = true + arc_mesh.visible = true + target_marker.visible = true + _update_arc() + else: + if is_teleporting: + is_teleporting = false + arc_mesh.visible = false + target_marker.visible = false + if teleport_valid: + _execute_teleport() + +func _update_arc() -> void: + var origin := global_position + var forward := -global_transform.basis.z.normalized() + var up := Vector3.UP + var velocity := forward * 8.0 + up * 4.0 # Arc impulse + + var points: PackedVector3Array = PackedVector3Array() + var pos := origin + var vel := velocity + var gravity := Vector3.DOWN * 9.8 + var step := 0.05 + + teleport_valid = false + teleport_target = Vector3.ZERO + + for i in range(arc_segments): + vel += gravity * step + var next_pos := pos + vel * step + points.append(pos) + + # Raycast down from arc point to find ground + var space_state := get_world_3d().direct_space_state + var query := PhysicsRayQueryParameters3D.create(next_pos, next_pos + Vector3.DOWN * 2.0) + query.collision_mask = 1 # Ground layer + var result := space_state.intersect_ray(query) + if result: + var hit_normal: Vector3 = result.normal + var hit_pos: Vector3 = result.position + var slope := rad_to_deg(acos(hit_normal.dot(Vector3.UP))) + if slope < 30.0 and origin.distance_to(hit_pos) <= max_distance: + teleport_valid = true + teleport_target = hit_pos + points.append(hit_pos) + break + pos = next_pos + + _draw_arc(points) + target_marker.visible = teleport_valid + if teleport_valid: + target_marker.global_position = teleport_target + _set_arc_color(valid_color) + else: + _set_arc_color(invalid_color) + +func _draw_arc(points: PackedVector3Array) -> void: + var immediate := ImmediateMesh.new() + immediate.surface_begin(Mesh.PRIMITIVE_LINE_STRIP) + for p in points: + immediate.surface_add_vertex(p) + immediate.surface_end() + arc_mesh.mesh = immediate + +func _set_arc_color(c: Color) -> void: + var mat := StandardMaterial3D.new() + mat.albedo_color = c + mat.emission_enabled = true + mat.emission = c + arc_mesh.material_override = mat + +func _execute_teleport() -> void: + var origin := get_parent() as XROrigin3D + if not origin: + return + # Move origin so camera ends up at target + var camera := origin.get_node("XRCamera3D") as XRCamera3D + var offset := camera.global_position - origin.global_position + offset.y = 0 # Keep vertical position relative + origin.global_position = teleport_target - offset + trigger_haptic_pulse("haptic", 0.4, 0.1, 0.0) +``` + +### Snap Turn Script + +```gdscript +extends XRController3D + +@export var snap_angle: float = 45.0 +@export var cooldown: float = 0.25 +var cooldown_timer: float = 0.0 + +func _process(delta: float) -> void: + if cooldown_timer > 0.0: + cooldown_timer -= delta + return + + var thumbstick: Vector2 = get_input("turn") as Vector2 + if abs(thumbstick.x) > 0.7: + var direction := sign(thumbstick.x) + _snap_turn(direction * snap_angle) + cooldown_timer = cooldown + trigger_haptic_pulse("haptic", 0.2, 0.05, 0.0) + +func _snap_turn(degrees: float) -> void: + var origin := get_parent() as XROrigin3D + if not origin: + return + var camera := origin.get_node("XRCamera3D") as XRCamera3D + # Rotate around camera position to avoid positional offset + var cam_pos := camera.global_position + origin.global_rotate(Vector3.UP, deg_to_rad(degrees)) + var offset := cam_pos - camera.global_position + origin.global_position += offset +``` + +**Comfort parameters:** + +| Parameter | Default | Range | +|-----------|---------|-------| +| Snap angle | 45 deg | 30-45 deg (smaller = more clicks, less disorientation) | +| Cooldown | 0.25 s | 0.2-0.3 s (prevents accidental double-turns) | +| Deadzone | 0.7 | 0.5-0.8 (ignore small stick wobble) | + +## Optional: Smooth Locomotion + +Only provide as an option. Many users experience nausea with smooth locomotion. Always combine with a comfort vignette or offer teleport as default. + +```gdscript +extends XRController3D + +@export var max_speed: float = 3.5 +@export var deadzone: float = 0.15 +@export var use_head_direction: bool = true # false = controller direction + +func _process(delta: float) -> void: + var input: Vector2 = get_input("move") as Vector2 + if input.length() < deadzone: + return + + var direction := Vector3.ZERO + if use_head_direction: + var camera := get_parent().get_node("XRCamera3D") as XRCamera3D + var forward := -camera.global_transform.basis.z + forward.y = 0 + forward = forward.normalized() + var right := camera.global_transform.basis.x + right.y = 0 + right = right.normalized() + direction = forward * input.y + right * input.x + else: + var forward := -global_transform.basis.z + forward.y = 0 + forward = forward.normalized() + var right := global_transform.basis.x + right.y = 0 + right = right.normalized() + direction = forward * input.y + right * input.x + + var origin := get_parent() as XROrigin3D + if origin: + origin.global_position += direction * max_speed * delta +``` + +**Smooth locomotion safety:** + +- Clamp speed to 3-5 m/s max +- Do NOT apply vertical movement (no flying without explicit jetpack/grip input) +- Provide snap-turn or smooth-turn option separately +- Consider FOV-reduction vignette during movement + +## Optional: Smooth Turn + +```gdscript +extends XRController3D + +@export var turn_speed: float = 60.0 # degrees per second +@export var deadzone: float = 0.2 + +func _process(delta: float) -> void: + var input: Vector2 = get_input("turn") as Vector2 + if abs(input.x) < deadzone: + return + var origin := get_parent() as XROrigin3D + if not origin: + return + var camera := origin.get_node("XRCamera3D") as XRCamera3D + var cam_pos := camera.global_position + origin.global_rotate(Vector3.UP, deg_to_rad(turn_speed * input.x * delta)) + var offset := cam_pos - camera.global_position + origin.global_position += offset +``` + +**Smooth turn max speed:** 60 deg/sec. Higher causes discomfort. + +## Surface Validation for Teleport + +Always validate the teleport landing zone: + +1. **Raycast ground check:** Cast downward from the arc sample point to find floor. +2. **Normal angle:** Reject surfaces where `acos(normal.dot(UP)) > 30 deg`. +3. **Collision mask:** Only collide with designated ground/navmesh layer. +4. **Obstruction check:** After finding a floor hit, raycast from player eye level to the hit point to ensure no wall is in the way. +5. **Distance clamp:** `clamp(distance, 0, max_distance)`. + +```gdscript +func _is_valid_teleport_point(point: Vector3, normal: Vector3) -> bool: + var slope := rad_to_deg(acos(normal.dot(Vector3.UP))) + if slope > 30.0: + return false + # Optional: check if point is inside navmesh or valid region + # Optional: line-of-sight from arc midpoint + return true +``` + +## Parabolic Arc Visualization + +The arc in the teleport script uses a simple physics simulation. For a cleaner quadratic Bezier arc: + +```gdscript +func _quadratic_arc(start: Vector3, control: Vector3, end: Vector3, segments: int) -> PackedVector3Array: + var points: PackedVector3Array = PackedVector3Array() + for i in range(segments + 1): + var t := float(i) / segments + var a := start.lerp(control, t) + var b := control.lerp(end, t) + points.append(a.lerp(b, t)) + return points +``` + +Use the controller forward direction to compute `control = start + forward * distance * 0.5 + Vector3.UP * peak_height`. diff --git a/skills/software-development/godot-xr-interactions/references/xr-ui-patterns.md b/skills/software-development/godot-xr-interactions/references/xr-ui-patterns.md new file mode 100644 index 000000000000..1ce15bf47223 --- /dev/null +++ b/skills/software-development/godot-xr-interactions/references/xr-ui-patterns.md @@ -0,0 +1,282 @@ +# XR UI Patterns + +Rendering flat Godot UI in 3D world space for Meta Quest VR. Covers SubViewport-on-quad setup, placement rules, curved panels, and text sizing. + +## SubViewport-on-Quad Setup + +The standard Godot 4.5 approach: render a `Control` scene into a `SubViewport`, then display it on a 3D quad mesh. + +### Node Tree + +``` +UIPanel (Node3D or MeshInstance3D) +├── SubViewport (SubViewport) +│ ├── CanvasLayer +│ │ └── MainControl (Control) +│ │ ├── Panel (PanelContainer or ColorRect) +│ │ ├── TitleLabel (Label) +│ │ ├── DescriptionLabel (Label) +│ │ └── ButtonsContainer (VBoxContainer/HBoxContainer) +│ │ ├── Button1 (Button) +│ │ └── Button2 (Button) +│ └── (optional) SubViewportCamera (Camera2D) for panning +└── QuadMesh (QuadMesh or PlaneMesh) +``` + +### Setup Script + +```gdscript +extends MeshInstance3D + +@export var subviewport: SubViewport +@export var quad_size: Vector2 = Vector2(1.0, 0.6) +@export var double_sided: bool = true + +func _ready() -> void: + if not subviewport: + push_error("SubViewport not assigned") + return + + # Configure mesh + var mesh := QuadMesh.new() + mesh.size = quad_size + self.mesh = mesh + + # Create material with SubViewport texture + var mat := StandardMaterial3D.new() + mat.albedo_texture = subviewport.get_texture() + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + if double_sided: + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + material_override = mat + + # Keep SubViewport rendering + subviewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS + subviewport.size = Vector2i(int(quad_size.x * 1024), int(quad_size.y * 1024)) +``` + +### SubViewport Settings + +| Setting | Value | Reason | +|---------|-------|--------| +| `size` | 1024 x 614 (for 1.0 x 0.6 m) | ~1024 px per meter gives readable density | +| `render_target_update_mode` | `UPDATE_ALWAYS` | UI must update every frame for hover/animations | +| `transparent_bg` | `true` if using rounded panels | Allows non-rectangular UI shapes | +| `canvas_item_default_texture_filter` | `Nearest` or `Linear` | `Linear` for smooth text, `Nearest` for pixel art | + +## UI Placement Rules + +| Parameter | Recommended | Notes | +|-----------|-------------|-------| +| Distance from camera | 1.5 - 3.0 m | 2.0 m is the sweet spot for reading | +| Panel width | 1.0 - 2.0 m world units | Maps to 1024-2048 px at 1024 px/m | +| Panel height | 0.5 - 1.2 m world units | Taller for scrollable content | +| Horizontal angle from forward | +/- 30 deg max | Beyond this, text distortion and neck strain increase | +| Vertical offset from eye level | -0.3 to +0.3 m | Looking too far up/down is uncomfortable | +| UI facing | Directly at player | Use `look_at` or place at fixed offset in front of origin | +| Separation from world geometry | 0.05 m buffer | Prevents z-fighting and clipping | + +### Billboard / Face-Player Script + +```gdscript +extends Node3D + +@export var target: Node3D # Assign XRCamera3D or XROrigin3D +@export var lock_pitch: bool = true +@export var lock_roll: bool = true + +func _process(_delta: float) -> void: + if not target: + return + var to_player := target.global_position - global_position + to_player.y = 0 if lock_pitch else to_player.y + if to_player.length() > 0.01: + look_at(target.global_position, Vector3.UP) + if lock_pitch: + rotation.x = 0 + if lock_roll: + rotation.z = 0 +``` + +### Fixed-Offset (HUD-style) Placement + +Attach UI as a child of `XROrigin3D` at a fixed offset so it moves with the player: + +```gdscript +extends Node3D # Child of XROrigin3D + +@export var distance: float = 2.0 +@export var height_offset: float = 0.0 + +func _process(_delta: float) -> void: + var camera := get_parent().get_node("XRCamera3D") as XRCamera3D + if not camera: + return + var forward := -camera.global_transform.basis.z + forward.y = 0 + forward = forward.normalized() + var target_pos := camera.global_position + forward * distance + target_pos.y += height_offset + global_position = target_pos + look_at(camera.global_position, Vector3.UP) + rotation.x = 0 + rotation.z = 0 +``` + +## Minimum Text Sizes + +At 2 m viewing distance with Quest 3 resolution (~20 PPD), use these minimums on the SubViewport: + +| Element | Minimum Size | Comfortable Size | +|---------|--------------|------------------| +| Body text | 28 px | 32-36 px | +| Button labels | 32 px | 36-40 px | +| Headings | 40 px | 48-64 px | +| Icons | 48 x 48 px | 64 x 64 px | +| Touch targets (buttons) | 80 x 80 px | 100 x 100 px | + +**Scaling formula:** If panel is at distance `d` meters instead of 2 m, multiply minimum sizes by `d / 2.0`. + +```gdscript +func get_scaled_font_size(base_size: int, distance: float) -> int: + return int(base_size * (distance / 2.0)) +``` + +Use `Theme` resources to manage font sizes consistently across XR UI scenes. + +## Curved Panels + +Flat panels at wide angles distort at the edges. For wide menus (>1.5 m), curve the mesh to match a cylinder segment. + +### Curved Mesh Generation + +```gdscript +extends MeshInstance3D + +@export var radius: float = 2.0 +@export var angle_degrees: float = 60.0 +@export var height: float = 0.8 +@export var segments: int = 24 + +func _ready() -> void: + generate_curved_mesh() + +func generate_curved_mesh() -> void: + var st := SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + + var angle_rad := deg_to_rad(angle_degrees) + var start_angle := -angle_rad / 2.0 + + for x in range(segments): + var t0 := float(x) / segments + var t1 := float(x + 1) / segments + var a0 := start_angle + t0 * angle_rad + var a1 := start_angle + t1 * angle_rad + + var y_top := height / 2.0 + var y_bottom := -height / 2.0 + + var p0 := Vector3(sin(a0) * radius, y_top, -cos(a0) * radius) + var p1 := Vector3(sin(a1) * radius, y_top, -cos(a1) * radius) + var p2 := Vector3(sin(a0) * radius, y_bottom, -cos(a0) * radius) + var p3 := Vector3(sin(a1) * radius, y_bottom, -cos(a1) * radius) + + var uv0 := Vector2(t0, 0) + var uv1 := Vector2(t1, 0) + var uv2 := Vector2(t0, 1) + var uv3 := Vector2(t1, 1) + + # First triangle + st.set_uv(uv0); st.add_vertex(p0) + st.set_uv(uv1); st.add_vertex(p1) + st.set_uv(uv2); st.add_vertex(p2) + # Second triangle + st.set_uv(uv1); st.add_vertex(p1) + st.set_uv(uv3); st.add_vertex(p3) + st.set_uv(uv2); st.add_vertex(p2) + + st.generate_normals() + mesh = st.commit() +``` + +**UV mapping note:** The generated mesh maps the SubViewport texture across the curved surface. Ensure `SubViewport.size.x` is large enough (2048+) so text remains crisp on the wider curve. + +## Pointer / Raycast Interaction + +Use the controller forward vector to raycast against UI quads. Convert hit point to SubViewport UV coordinates. + +```gdscript +extends XRController3D + +@export var ui_panel: MeshInstance3D +@export var subviewport: SubViewport +@export var pointer_length: float = 10.0 + +var pointer_active: bool = false + +func _process(_delta: float) -> void: + var space_state := get_world_3d().direct_space_state + var from := global_position + var to := from + (-global_transform.basis.z) * pointer_length + var query := PhysicsRayQueryParameters3D.create(from, to) + query.collision_mask = 2 # UI layer + var result := space_state.intersect_ray(query) + + if result and result.collider == ui_panel: + pointer_active = true + var hit_point: Vector3 = result.position + var uv := _get_uv_on_mesh(ui_panel, hit_point) + _send_mouse_event(uv, true) + else: + if pointer_active: + pointer_active = false + _send_mouse_event(Vector2(-1, -1), false) + +func _get_uv_on_mesh(mesh_instance: MeshInstance3D, world_point: Vector3) -> Vector2: + var local_point := mesh_instance.to_local(world_point) + # For a quad mesh centered at origin, size (w, h): + var quad_size: Vector2 = (mesh_instance.mesh as QuadMesh).size + var uv_x := (local_point.x / quad_size.x) + 0.5 + var uv_y := (-local_point.y / quad_size.y) + 0.5 + return Vector2(uv_x, uv_y) + +func _send_mouse_event(uv: Vector2, pressed: bool) -> void: + var pos := Vector2i(int(uv.x * subviewport.size.x), int(uv.y * subviewport.size.y)) + var evt := InputEventMouseMotion.new() + evt.position = pos + evt.global_position = pos + subviewport.push_input(evt) +``` + +**Alternative:** Use Godot's built-in `CollisionObject3D.input_event` with a `StaticBody3D` behind the UI quad, then use `get_viewport().get_camera_3d().project_position()` -- but the raycast-to-UV method above is more precise for custom pointer rendering. + +## Dynamic UI Positioning + +For context-sensitive UI (e.g., object inspection panels), spawn the UI at a computed position: + +```gdscript +func place_ui_near_object(ui: Node3D, target_object: Node3D, camera: XRCamera3D) -> void: + var obj_pos := target_object.global_position + var cam_pos := camera.global_position + var dir := (cam_pos - obj_pos).normalized() + dir.y = 0 + if dir.length() < 0.01: + dir = Vector3.FORWARD + dir = dir.normalized() + + # Place 1.5 m from object, toward camera, at eye level + var ui_pos := obj_pos + dir * 1.5 + ui_pos.y = cam_pos.y + ui.global_position = ui_pos + ui.look_at(cam_pos, Vector3.UP) + ui.rotation.x = 0 + ui.rotation.z = 0 +``` + +## Performance Notes + +- One `SubViewport` per UI panel. Reuse the same SubViewport texture across multiple MeshInstances only if showing identical content. +- Update mode `UPDATE_ALWAYS` costs GPU fill rate. For static UI (e.g., info plaques), switch to `UPDATE_ONCE` after content loads, then back to `ALWAYS` only when interaction occurs. +- Keep SubViewport resolution reasonable: 1024x1024 or less for most panels. Higher only for large curved displays. +- Use `TextureRect` with `expand_mode = FIT_WIDTH` inside the SubViewport so UI scales cleanly if you change aspect ratio. diff --git a/skills/software-development/mcp-server-setup/SKILL.md b/skills/software-development/mcp-server-setup/SKILL.md new file mode 100644 index 000000000000..523e9ff7ccf3 --- /dev/null +++ b/skills/software-development/mcp-server-setup/SKILL.md @@ -0,0 +1,194 @@ +--- +name: mcp-server-setup +description: Configure Hermes Agent MCP servers for Godot and Blender automation. Use when setting up, troubleshooting, or reconfiguring MCP connections for VR development workflows. +category: software-development +version: 1.0.0 +author: Hermes VR DevKit +license: MIT +metadata: + hermes: + tags: [mcp, hermes, godot, blender, config, setup] + related_skills: [godot-quest-dev, blender-godot-pipeline, godot-xr-interactions, quest-native-toolchain] +--- + +# MCP Server Setup for VR Development + +Configure Hermes Agent to control both Godot and Blender via MCP, enabling AI-driven asset creation and scene assembly for Quest VR projects. + +## When to Use + +- Setting up Hermes MCP for the first time on a new machine +- Godot-MCP or Blender-MCP tools are not appearing in Hermes +- MCP connection fails with "socket hang up" or "connection refused" +- Rebuilding the MCP server after an update +- Moving the dev environment to a new path or user + +## What MCP Enables + +Without MCP, the agent must ask you to click buttons in Blender/Godot. With MCP: +- **Blender**: Create objects, apply materials, sculpt terrain, export GLB, take viewport screenshots -- all programmatic +- **Godot**: Create scenes, add nodes, set transforms, attach scripts, run the game -- all programmatic + +The agent drives the full pipeline: Blender asset creation -> GLB export -> Godot import -> scene assembly -> export APK -> deploy to Quest. + +## Quick Config + +Add to `~/.hermes/config.yaml`: + +```yaml +mcp_servers: + godot: + command: node + args: [$HOME/.local/share/godot-mcp/dist/index.js] + connect_timeout: 30 + timeout: 120 + + blender: + command: uvx + args: [blender-mcp] +``` + +After editing config, restart Hermes (`/reload` or relaunch) to discover tools. + +Verify: +```bash +hermes mcp test godot +hermes mcp test blender +hermes mcp list +``` + +## Godot-MCP Setup + +### 1. Build the Server + +```bash +git clone --depth 1 https://github.com/ee0pdt/Godot-MCP.git +cd Godot-MCP/server +npm install && npm run build +``` + +### 2. Install the Godot Addon + +```bash +mkdir -p /path/to/your-project/addons +cp -r Godot-MCP/addons/godot_mcp /path/to/your-project/addons/ +``` + +In Godot editor: **Project -> Project Settings -> Plugins -> enable Godot MCP**. + +### 3. Deploy Server to Persistent Location + +```bash +mkdir -p ~/.local/share/godot-mcp +cp -r Godot-MCP/server/dist Godot-MCP/server/node_modules \ + Godot-MCP/server/package.json ~/.local/share/godot-mcp/ +``` + +### 4. Critical Fix: WebSocket Protocol (Godot 4.5) + +Godot 4.5's `WebSocketPeer` does not negotiate subprotocols. The upstream client sends `protocol: 'json'`, causing handshake failure. + +Apply the fix: +```bash +sed -i "/protocol: 'json',/d" ~/.local/share/godot-mcp/dist/utils/godot_connection.js +``` + +Or use the provided script: +```bash +./mcp-servers/godot-mcp/fix-protocol.sh +``` + +Run this after every `npm run build`. + +### 5. Hermes Config + +```yaml +mcp_servers: + godot: + command: node + args: [$HOME/.local/share/godot-mcp/dist/index.js] + connect_timeout: 30 + timeout: 120 +``` + +### 6. Usage + +1. Open Godot editor with your project +2. Enable the Godot MCP plugin (it starts WebSocket server on port 9080) +3. Restart Hermes to discover tools +4. Tools appear as `mcp_godot_*` + +See `references/godot-mcp-protocol-fix.md` for the full protocol fix details and troubleshooting. + +## Blender-MCP Setup + +### 1. Install the Server + +```bash +# Via uv (recommended) +uvx blender-mcp + +# Or via pip +pip install blender-mcp +``` + +### 2. Install the Blender Addon + +```bash +mkdir -p ~/.config/blender/4.3/scripts/addons +cp /path/to/blender-mcp/addon.py ~/.config/blender/4.3/scripts/addons/ +``` + +In Blender: **Edit -> Preferences -> Add-ons -> Install -> select addon.py -> Enable "Interface: Blender MCP"**. + +In the 3D View sidebar (press N), click **"Connect to Claude"**. + +### 3. Hermes Config + +```yaml +mcp_servers: + blender: + command: uvx + args: [blender-mcp] +``` + +### 4. Usage + +1. Open Blender +2. Enable the Blender MCP addon +3. Click "Connect to Claude" in the sidebar +4. Restart Hermes to discover tools +5. Tools appear as `mcp_blender_*` + +See `references/blender-mcp-install.md` for detailed addon troubleshooting. + +## Tool Naming Convention + +MCP tools are prefixed with the server name: +- Godot: `mcp_godot_create_scene`, `mcp_godot_create_node`, ... +- Blender: `mcp_blender_create_object`, `mcp_blender_execute_blender_code`, ... + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| Tools not appearing after config edit | Restart Hermes with `/reload` or relaunch | +| "socket hang up" on Godot-MCP connect | Run `fix-protocol.sh` to remove `protocol: 'json'` | +| "Connection refused" on Godot-MCP | Ensure Godot editor is open and MCP plugin enabled (check Output panel for "MCP server started on port 9080") | +| Stale Godot-MCP data from previous session | `kill $(pgrep -f "godot-mcp/dist/index.js")` then reconnect | +| Blender addon greyed out | Restart Blender -- cache may be stale after manual file copy | +| Blender glTF export fails | `sudo pip install numpy --break-system-packages` (Blender uses system Python) | +| `hermes mcp test` hangs | Increase `connect_timeout` to 60 or 90 seconds | +| `update_node_property` changes vanish | Call `save_scene` after modifications; `update_node_property` does not mark scene modified | +| `execute_editor_script` returns no data | Use `get_node_properties` for data retrieval, or write to temp file | + +## Security Notes + +- Hermes does NOT pass your full shell environment to MCP subprocesses. Only safe baseline variables (PATH, HOME, USER, LANG, etc.) are inherited. +- API keys and secrets are excluded unless explicitly added via the `env` config key. +- Credential patterns in error messages are automatically redacted before being shown to the LLM. + +## References + +- `references/godot-mcp-protocol-fix.md` -- Full protocol fix details and Godot-MCP troubleshooting +- `references/blender-mcp-install.md` -- Blender addon install, numpy fix, and connection troubleshooting diff --git a/skills/software-development/mcp-server-setup/references/blender-mcp-install.md b/skills/software-development/mcp-server-setup/references/blender-mcp-install.md new file mode 100644 index 000000000000..c4d111fad29f --- /dev/null +++ b/skills/software-development/mcp-server-setup/references/blender-mcp-install.md @@ -0,0 +1,74 @@ +# Blender-MCP Installation and Troubleshooting + +## Server Install + +### Via uv (recommended) +```bash +uvx blender-mcp +``` + +### Via pip +```bash +pip install blender-mcp +``` + +### Via source +```bash +git clone https://github.com/ahujasid/blender-mcp.git +cd blender-mcp +pip install -e . +``` + +## Addon Install + +### Manual Copy +```bash +mkdir -p ~/.config/blender/4.3/scripts/addons +cp /path/to/blender-mcp/addon.py ~/.config/blender/4.3/scripts/addons/ +``` + +### Via Blender UI +1. Open Blender +2. **Edit -> Preferences -> Add-ons -> Install** +3. Select `addon.py` +4. Check "Interface: Blender MCP" + +### Connect +1. In 3D View, press N to open sidebar +2. Click "Connect to Claude" +3. The addon starts a socket server (default localhost:9876) + +## Hermes Config + +```yaml +mcp_servers: + blender: + command: uvx + args: [blender-mcp] +``` + +Restart Hermes after adding. + +## Common Issues + +| Issue | Fix | +|-------|-----| +| Addon greyed out, cannot check | Restart Blender -- cache stale after manual copy | +| `No module named 'numpy'` | `sudo pip install numpy --break-system-packages` (Blender uses system Python) | +| glTF export fails silently | Same as above -- numpy required for glTF I/O | +| Connection refused | Ensure addon is enabled and "Connect to Claude" clicked | +| Timeout on complex requests | Break into smaller steps, or increase MCP timeout | +| `bsdf.inputs['Emission']` error | Blender 4.x renamed to `Emission Color` / `Emission Strength` | +| `ShaderNodeTexGrid` missing | Use `ShaderNodeTexWave` with `wave_type='BANDS'` | +| Undo wipes scene | Each MCP call is a separate undo context; save before risky ops | + +## Capabilities + +- Scene inspection (objects, materials, lighting) +- Object creation/deletion/modification +- Material control (Principled BSDF, emission, transparency) +- Python execution in Blender context +- Poly Haven asset download (HDRIs, textures, models) +- Hyper3D Rodin 3D generation +- Viewport screenshots +- GLB export diff --git a/skills/software-development/mcp-server-setup/references/godot-mcp-protocol-fix.md b/skills/software-development/mcp-server-setup/references/godot-mcp-protocol-fix.md new file mode 100644 index 000000000000..facb9be8ad67 --- /dev/null +++ b/skills/software-development/mcp-server-setup/references/godot-mcp-protocol-fix.md @@ -0,0 +1,59 @@ +# Godot-MCP WebSocket Protocol Fix + +## The Problem + +Godot 4.5's `WebSocketPeer` does **not** negotiate subprotocols. The upstream Godot-MCP TypeScript client sends `protocol: 'json'` in the WebSocket constructor: + +```typescript +// In godot_connection.ts (upstream) +this.ws = new WebSocket(url, { + protocol: 'json', // <-- This breaks Godot 4.5 +}); +``` + +This causes the handshake to fail with `Error: socket hang up` because Godot's server rejects the connection when it sees an unsupported subprotocol. + +## The Fix + +Remove the `protocol` option from the WebSocket constructor: + +```bash +sed -i "/protocol: 'json',/d" ~/.local/share/godot-mcp/dist/utils/godot_connection.js +``` + +## Persistent Fix (Survives Rebuilds) + +Patch the TypeScript source so the fix survives `npm run build`: + +```bash +sed -i "/protocol: 'json',/d" /path/to/Godot-MCP/server/src/utils/godot_connection.ts +cd /path/to/Godot-MCP/server && npm run build +cp -r dist ~/.local/share/godot-mcp/ +``` + +## Fix Script + +A convenience script is provided in this repo: + +```bash +./mcp-servers/godot-mcp/fix-protocol.sh [path/to/dist] +``` + +Run this after every `npm run build`. + +## Verification + +After applying the fix: +1. Ensure Godot editor is open with the MCP plugin enabled +2. Check Output panel for "MCP server started on port 9080" +3. Restart Hermes (`/reload`) +4. Run `hermes mcp test godot` +5. Should show "Connected" and list available tools + +## Why This Happens + +Godot 4.5 changed WebSocket behavior to strictly validate subprotocols. Earlier Godot versions silently ignored unsupported protocols. The upstream Godot-MCP client was written against Godot 4.4 behavior. + +## Upstream Status + +As of the last check, this fix is not yet merged upstream. Track the issue at: https://github.com/ee0pdt/Godot-MCP/issues diff --git a/skills/software-development/quest-native-toolchain/SKILL.md b/skills/software-development/quest-native-toolchain/SKILL.md new file mode 100644 index 000000000000..fe364f8b27f7 --- /dev/null +++ b/skills/software-development/quest-native-toolchain/SKILL.md @@ -0,0 +1,325 @@ +--- +name: quest-native-toolchain +description: | + Use when setting up, maintaining, or troubleshooting the complete Meta Quest native development stack on Ubuntu Linux. Covers Godot, Blender MCP, OpenXR, Android SDK/NDK, asset optimization tools, sideloading with ADB, scrcpy mirroring, Monado runtime, and environment variable configuration. Use for initial workstation setup, CI toolchain installation, or resolving missing dependencies in Quest VR build pipelines. +version: "1.0.0" +author: hermes-vr-devkit +license: MIT +metadata: + hermes: + tags: [quest, vr, openxr, godot, blender, android, linux, meta-quest, toolchain, sideload, mcp, monado] + related_skills: [godot-quest-dev, blender-godot-pipeline, godot-xr-interactions, mcp-server-setup] +--- + +# Quest Native Toolchain + +Complete Meta Quest native development stack for Ubuntu Linux. One-shot install paths, validated tool versions, and project-agnostic environment setup. + +## When to Use + +- Setting up a fresh Ubuntu workstation for Quest VR development +- Installing or upgrading the Android SDK, NDK, or platform tools +- Configuring Godot + Blender MCP + OpenXR in a single environment +- Troubleshooting missing dependencies (`adb`, `apksigner`, `gltfpack`, etc.) +- Preparing CI/CD agents with the full Quest build stack +- Setting up Monado for local OpenXR runtime testing without a headset + +## Tool Inventory (Summary) + +| Tool | Install Path | Purpose | +|------|--------------|---------| +| Godot | `$HOME/bin/godot` | Game engine, headless export | +| Blender | `/usr/bin/blender` | 3D modeling, MCP integration | +| Android SDK | `$HOME/android-sdk` | Build tools, platform tools, NDK | +| Android NDK | `$HOME/android-sdk/ndk/25.2.9519653` | Native C++ builds for Quest | +| JDK 17 | `/usr/lib/jvm/java-17-openjdk-amd64` | Gradle builds, apksigner | +| ADB | `$HOME/android-sdk/platform-tools/adb` | Device communication | +| gltfpack | `$HOME/bin/gltfpack` | Mesh/texture optimization | +| scrcpy | `/usr/bin/scrcpy` | Screen mirroring + control | +| Monado | `$HOME/src/monado/install` | Local OpenXR runtime | + +For full install commands and version pinning, see `references/tool-inventory.md`. + +## Cloned Repositories + +| Repository | Clone Path | Purpose | +|------------|------------|---------| +| `godot_openxr_vendors` | `$HOME/src/godot_openxr_vendors` | Meta OpenXR plugin for Godot | +| `monado` | `$HOME/src/monado` | Open-source OpenXR runtime | +| `OpenXR-SDK-Source` | `$HOME/src/OpenXR-SDK-Source` | Native C++ OpenXR samples | +| `hermes-vr-devkit` | `$HOME/src/hermes-vr-devkit` | Skills, scripts, templates | + +## One-Shot Installation + +### 1. System Dependencies + +```bash +sudo apt update && sudo apt install -y \ + git git-lfs curl wget unzip p7zip-full \ + build-essential cmake ninja-build pkg-config \ + openjdk-17-jdk openjdk-17-jre \ + libgl1-mesa-dev libvulkan-dev libx11-dev libxrandr-dev \ + libwayland-dev wayland-protocols libxkbcommon-dev \ + ffmpeg libsdl2-2.0-0 adb +``` + +### 2. Android SDK + NDK (Command-Line) + +```bash +mkdir -p "$HOME/android-sdk/cmdline-tools" +cd "$HOME/android-sdk/cmdline-tools" +wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip +unzip -q commandlinetools-linux-*.zip +mv cmdline-tools latest +``` + +Then install packages and accept licenses. Detailed steps in `references/android-sdk-setup.md`. + +### 3. Godot (Headless + Editor) + +```bash +mkdir -p "$HOME/bin" +cd "$HOME/bin" +wget https://downloads.tuxfamily.org/godotengine/4.5-stable/Godot_v4.5-stable_linux.x86_64.zip +unzip -q Godot_v4.5-stable_linux.x86_64.zip +mv Godot_v4.5-stable_linux.x86_64 godot +chmod +x godot +``` + +### 4. Blender (Official Repository) + +```bash +sudo apt install -y blender +# Or download from https://www.blender.org/download/ +``` + +### 5. gltfpack + +```bash +mkdir -p "$HOME/bin" +cd "$HOME/bin" +wget https://github.com/zeux/gltfpack/releases/download/v0.20/gltfpack-0.20-linux.zip +unzip -q gltfpack-0.20-linux.zip +chmod +x gltfpack +``` + +### 6. scrcpy + +```bash +sudo apt install -y scrcpy +# Or build from source for latest features +``` + +## Key Environment Variables + +Add to `~/.bashrc` or `~/.profile`: + +```bash +export ANDROID_HOME="$HOME/android-sdk" +export ANDROID_SDK_ROOT="$ANDROID_HOME" +export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH" +export PATH="$ANDROID_HOME/platform-tools:$PATH" +export PATH="$ANDROID_HOME/build-tools/34.0.0:$PATH" +export PATH="$HOME/bin:$PATH" + +export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64" +export GODOT="$HOME/bin/godot" + +# Native build +export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/25.2.9519653" +export PATH="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH" +``` + +Reload: + +```bash +source ~/.bashrc +``` + +## Native C++ Sample Validation (Quest-XR Build) + +Validate the entire toolchain by building the Khronos OpenXR SDK sample for Android. + +### 1. Clone and Prepare + +```bash +mkdir -p "$HOME/src" +cd "$HOME/src" +git clone https://github.com/KhronosGroup/OpenXR-SDK-Source.git +cd OpenXR-SDK-Source +``` + +### 2. Build with Android NDK + +```bash +export ANDROID_NDK_HOME="$HOME/android-sdk/ndk/25.2.9519653" +mkdir -p build-android && cd build-android + +cmake .. \ + -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI=arm64-v8a \ + -DANDROID_PLATFORM=android-29 \ + -DCMAKE_BUILD_TYPE=Release + +make -j$(nproc) +``` + +### 3. Expected Artifacts + +```bash +ls -l src/tests/hello_xr/libhello_xr.so +# Should produce an arm64 shared library +``` + +If this builds successfully, your NDK, CMake, and toolchain are correctly configured for Quest native development. + +## scrcpy: Quest Mirroring and Control + +Mirror the Quest display to your Linux desktop and inject input. + +### Basic Usage + +```bash +# USB connection (Quest in developer mode) +scrcpy --serial=YOUR_QUEST_SERIAL + +# Over Wi-Fi (after adb tcpip 5555) +adb tcpip 5555 +adb connect QUEST_IP:5555 +scrcpy --serial=QUEST_IP:5555 +``` + +### Performance Flags for VR + +```bash +scrcpy \ + --max-fps=30 \ + --max-size=1024 \ + --bit-rate=4M \ + --crop=1632:1224:100:100 \ + --no-control +``` + +### Common Flags + +| Flag | Purpose | +|------|---------| +| `--no-control` | View-only, no input injection | +| `--record=file.mp4` | Record session | +| `--fullscreen` | Fullscreen mirror | +| `--rotation=1` | Rotate 90 degrees | + +## What Meta Does NOT Provide on Linux + +| Tool | Linux Status | Workaround | +|------|--------------|------------| +| Meta Quest Link (Air/Cable) | Not available | Use ALVR, WiVRn, or Virtual Desktop | +| Meta Quest Developer Hub | No Linux build | Use `adb`, `scrcpy`, and command-line tools | +| Meta XR Simulator | Windows-only | Use Monado + `monado-gui` for local testing | +| Oculus Runtime | Windows-only | Use Monado or SteamVR on Linux | +| Meta Build Utils | Windows-only | Use Android SDK + NDK directly | + +Linux developers rely entirely on open-source alternatives and direct ADB interaction. + +## Monado: Local OpenXR Runtime + +Monado is the open-source OpenXR runtime for Linux. Use it to test OpenXR logic without a physical Quest. + +### Build Monado + +```bash +mkdir -p "$HOME/src" && cd "$HOME/src" +git clone https://gitlab.freedesktop.org/monado/monado.git +cd monado + +mkdir -p build && cd build +cmake .. \ + -DCMAKE_INSTALL_PREFIX="$HOME/src/monado/install" \ + -DXRT_BUILD_DRIVER_SIMULATED=ON \ + -DXRT_BUILD_TESTS=ON \ + -DCMAKE_BUILD_TYPE=Release + +make -j$(nproc) +make install +``` + +### Run with Simulated HMD + +```bash +export XR_RUNTIME_JSON="$HOME/src/monado/install/share/openxr/1/openxr_monado.json" + +# In one terminal +monado-service & + +# In another terminal +./your-openxr-application +``` + +### Verify Runtime + +```bash +# List available runtimes +ls /usr/share/openxr/1/openxr_runtime.json ~/.config/openxr/1/openxr_monado.json + +# Check active runtime +xrinfo +``` + +## Asset Optimization Quick Reference + +| Tool | Command | Purpose | +|------|---------|---------| +| gltfpack | `gltfpack -si 0.5 -tc 2048 -noq -kn -i raw.glb -o opt.glb` | Reduce mesh + texture size | +| Godot import | Set "Lossy" compression, VRAM max 2K | Runtime texture budget | +| Blender decimate | Modifier > Decimate, ratio 0.5 | Quick LOD generation | + +## Validation Checklist + +After setup, confirm each layer: + +- [ ] `adb devices` shows Quest serial when plugged in +- [ ] `adb shell getprop ro.product.model` returns Quest model +- [ ] `apksigner --version` returns a version number +- [ ] `$GODOT --version` returns 4.5-stable or higher +- [ ] `blender --version` returns 4.x +- [ ] `gltfpack -h` prints help +- [ ] `scrcpy --version` prints version +- [ ] `javac -version` prints 17.x +- [ ] Native C++ OpenXR sample builds successfully +- [ ] Monado `xrinfo` shows Monado runtime + +## Troubleshooting + +### `adb: command not found` + +Platform tools not on PATH. Add `$ANDROID_HOME/platform-tools` to `~/.bashrc`. + +### `apksigner: command not found` + +Build-tools not on PATH. Add `$ANDROID_HOME/build-tools/34.0.0` to `~/.bashrc`. + +### Godot cannot find Android SDK + +Set in Godot Editor Settings: `export/android/android_sdk_path = "/home/USER/android-sdk"` +Or run: `$GODOT --headless --editor-settings ...` (see `godot-quest-dev` skill). + +### NDK not found during native build + +Ensure `ANDROID_NDK_HOME` is exported and points to a valid NDK directory. + +### Monado fails to start + +Install udev rules for your GPU and ensure your user is in the `video` and `render` groups: + +```bash +sudo usermod -aG video,render $USER +``` + +## See Also + +- `references/tool-inventory.md` -- Full tool inventory with install commands, versions, purposes +- `references/android-sdk-setup.md` -- Step-by-step Android SDK installation, NDK, platform tools, license acceptance +- `godot-quest-dev` skill -- Godot project setup, export, and Quest deployment +- `blender-godot-pipeline` skill -- Asset pipeline from Blender to Godot +- `godot-xr-interactions` skill -- XR interaction patterns and locomotion +- `mcp-server-setup` skill -- Blender MCP server configuration diff --git a/skills/software-development/quest-native-toolchain/references/android-sdk-setup.md b/skills/software-development/quest-native-toolchain/references/android-sdk-setup.md new file mode 100644 index 000000000000..6da0728293ef --- /dev/null +++ b/skills/software-development/quest-native-toolchain/references/android-sdk-setup.md @@ -0,0 +1,171 @@ +# Android SDK Setup + +Step-by-step Android SDK installation on Ubuntu Linux for Meta Quest development. No Android Studio required. + +## Prerequisites + +```bash +sudo apt update +sudo apt install -y openjdk-17-jdk wget unzip +``` + +## 1. Download Command-Line Tools + +```bash +mkdir -p "$HOME/android-sdk/cmdline-tools" +cd "$HOME/android-sdk/cmdline-tools" + +wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip +unzip -q commandlinetools-linux-*.zip +mv cmdline-tools latest +``` + +## 2. Add to PATH + +```bash +echo 'export ANDROID_HOME="$HOME/android-sdk"' >> ~/.bashrc +echo 'export ANDROID_SDK_ROOT="$ANDROID_HOME"' >> ~/.bashrc +echo 'export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"' >> ~/.bashrc +echo 'export PATH="$ANDROID_HOME/platform-tools:$PATH"' >> ~/.bashrc +echo 'export PATH="$ANDROID_HOME/build-tools/34.0.0:$PATH"' >> ~/.bashrc +source ~/.bashrc +``` + +## 3. Accept Licenses + +This is mandatory before installing packages: + +```bash +sdkmanager --licenses +``` + +You will be prompted multiple times. Type `y` and press Enter for each license. + +To accept all licenses non-interactively (useful for CI): + +```bash +yes | sdkmanager --licenses +``` + +## 4. Install Core Packages + +```bash +sdkmanager "platform-tools" +sdkmanager "build-tools;34.0.0" +sdkmanager "platforms;android-29" +sdkmanager "platforms;android-34" +sdkmanager "ndk;25.2.9519653" +``` + +## 5. Verify Installation + +```bash +# Platform tools +adb version +# Example output: Android Debug Bridge version 1.0.41 + +# Build tools +apksigner --version +# Example output: 0.9 + +# SDK manager list +sdkmanager --list_installed +``` + +## 6. Create Debug Keystore + +Godot and manual signing require a debug keystore: + +```bash +mkdir -p "$HOME/.android" +keytool -keyalg RSA -genkeypair -alias androiddebugkey \ + -keypass android -keystore "$HOME/.android/debug.keystore" \ + -storepass android -dname "CN=Android Debug,O=Android,C=US" \ + -validity 9999 +``` + +## 7. Godot Editor Settings + +Tell Godot where the SDK and keystore are: + +```bash +# In Godot editor (GUI): +# Editor -> Editor Settings -> Export -> Android +# Android SDK Path: /home/USER/android-sdk +# Java SDK Path: /usr/lib/jvm/java-17-openjdk-amd64 +# Debug Keystore: /home/USER/.android/debug.keystore +# Debug Keystore Pass: android +``` + +Or edit `~/.config/godot/editor_settings-4.5.tres`: + +```tres +export/android/android_sdk_path = "/home/USER/android-sdk" +export/android/java_sdk_path = "/usr/lib/jvm/java-17-openjdk-amd64" +export/android/debug_keystore = "/home/USER/.android/debug.keystore" +export/android/debug_keystore_pass = "android" +``` + +Replace `USER` with your actual username. + +## Common Issues + +### `sdkmanager: command not found` + +The `latest` directory is not on PATH. Ensure: + +```bash +ls "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" +``` + +If it exists but is not found, re-source your profile: + +```bash +source ~/.bashrc +``` + +### `Warning: Could not create settings` + +The cmdline-tools directory structure is incorrect. It must be: + +``` +$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager +``` + +Not: + +``` +$ANDROID_HOME/cmdline-tools/bin/sdkmanager +``` + +### ADB permissions (no devices found) + +Add udev rules for Oculus/Meta devices: + +```bash +sudo tee /etc/udev/rules.d/51-android.rules << 'EOF' +SUBSYSTEM=="usb", ATTR{idVendor}=="2833", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev" +EOF + +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +Then add your user to the `plugdev` group: + +```bash +sudo usermod -aG plugdev $USER +``` + +Log out and back in for group changes to take effect. + +### NDK version mismatch + +Quest native builds typically use NDK 25. If you install a different version, update `ANDROID_NDK_HOME` accordingly: + +```bash +export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/XX.X.XXXXXXXX" +``` + +Godot's export system also checks `ANDROID_NDK_HOME` during Gradle builds. diff --git a/skills/software-development/quest-native-toolchain/references/tool-inventory.md b/skills/software-development/quest-native-toolchain/references/tool-inventory.md new file mode 100644 index 000000000000..54414aca3981 --- /dev/null +++ b/skills/software-development/quest-native-toolchain/references/tool-inventory.md @@ -0,0 +1,172 @@ +# Tool Inventory + +Full inventory of the Quest native toolchain. Each entry includes purpose, recommended version, install command, and verification step. + +## Core Engine + +### Godot +- **Purpose:** Game engine, scene editor, headless exporter +- **Recommended Version:** 4.5-stable or later +- **Install Path:** `$HOME/bin/godot` +- **Download:** https://downloads.tuxfamily.org/godotengine/ + +```bash +mkdir -p "$HOME/bin" +cd "$HOME/bin" +wget https://downloads.tuxfamily.org/godotengine/4.5-stable/Godot_v4.5-stable_linux.x86_64.zip +unzip -q Godot_v4.5-stable_linux.x86_64.zip +mv Godot_v4.5-stable_linux.x86_64 godot +chmod +x godot +``` + +- **Verify:** `godot --version` +- **Notes:** Forward+ renderer is unavailable on Quest; always use Mobile renderer for VR. + +### Blender +- **Purpose:** 3D modeling, animation, UV unwrap, GLB export +- **Recommended Version:** 4.2 LTS or 4.x +- **Install Path:** `/usr/bin/blender` +- **Download:** https://www.blender.org/download/ + +```bash +sudo apt install -y blender +``` + +- **Verify:** `blender --version` +- **Notes:** Enable the glTF 2.0 exporter (built-in). For MCP integration, see `mcp-server-setup` skill. + +## Android Build Stack + +### Android SDK (Command-Line Tools) +- **Purpose:** ADB, apksigner, aapt, build-tools, platform images +- **Recommended Version:** cmdline-tools 11076708 or later +- **Install Path:** `$HOME/android-sdk` + +```bash +mkdir -p "$HOME/android-sdk/cmdline-tools" +cd "$HOME/android-sdk/cmdline-tools" +wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip +unzip -q commandlinetools-linux-*.zip +mv cmdline-tools latest +``` + +- **Verify:** `sdkmanager --version` + +### Android NDK +- **Purpose:** Native C/C++ compilation for arm64-v8a +- **Recommended Version:** 25.2.9519653 (r25c) +- **Install Path:** `$HOME/android-sdk/ndk/25.2.9519653` + +```bash +sdkmanager "ndk;25.2.9519653" +``` + +- **Verify:** `ls $ANDROID_HOME/ndk/25.2.9519653/ndk-build` +- **Notes:** Quest uses arm64-v8a ABI. NDK 25 is the last release before the LLVM toolchain restructuring in NDK 26. + +### Platform Tools +- **Purpose:** ADB, fastboot, systrace +- **Recommended Version:** Latest (auto-updated via sdkmanager) +- **Install Path:** `$HOME/android-sdk/platform-tools` + +```bash +sdkmanager "platform-tools" +``` + +- **Verify:** `adb version` + +### Build Tools +- **Purpose:** apksigner, zipalign, aapt2 +- **Recommended Version:** 34.0.0 or later +- **Install Path:** `$HOME/android-sdk/build-tools/34.0.0` + +```bash +sdkmanager "build-tools;34.0.0" +``` + +- **Verify:** `apksigner --version` + +### Platforms +- **Purpose:** Android API headers and libraries +- **Recommended Version:** android-29 (minimum for OpenXR) and android-34 +- **Install Path:** `$HOME/android-sdk/platforms/android-29` + +```bash +sdkmanager "platforms;android-29" "platforms;android-34" +``` + +### JDK 17 +- **Purpose:** Gradle daemon, apksigner, keytool +- **Recommended Version:** OpenJDK 17 +- **Install Path:** `/usr/lib/jvm/java-17-openjdk-amd64` + +```bash +sudo apt install -y openjdk-17-jdk openjdk-17-jre +``` + +- **Verify:** `javac -version` +- **Notes:** Godot 4.5+ requires JDK 17. Do not use JDK 21 for Godot Android builds. + +## Optimization Tools + +### gltfpack +- **Purpose:** Mesh quantization, LOD generation, texture compression for GLB +- **Recommended Version:** 0.20 or later +- **Install Path:** `$HOME/bin/gltfpack` +- **Download:** https://github.com/zeux/gltfpack/releases + +```bash +mkdir -p "$HOME/bin" +cd "$HOME/bin" +wget https://github.com/zeux/gltfpack/releases/download/v0.20/gltfpack-0.20-linux.zip +unzip -q gltfpack-0.20-linux.zip +chmod +x gltfpack +``` + +- **Verify:** `gltfpack -h` +- **Notes:** Use `-si 0.5` for 50% triangle reduction, `-tc 2048` for texture cap, `-noq` to disable quantization if Godot import fails. + +## Sideload and Debug + +### scrcpy +- **Purpose:** Mirror Quest display to Linux desktop, inject input +- **Recommended Version:** 2.4 or later (snap/APT) +- **Install Path:** `/usr/bin/scrcpy` + +```bash +sudo apt install -y scrcpy +``` + +- **Verify:** `scrcpy --version` +- **Notes:** Works over USB and Wi-Fi. Requires ADB debugging enabled on Quest. + +## OpenXR Runtime + +### Monado +- **Purpose:** Open-source OpenXR runtime for Linux desktop testing +- **Recommended Version:** Latest main branch +- **Install Path:** `$HOME/src/monado/install` + +```bash +# See SKILL.md Monado section for full build instructions +``` + +- **Verify:** `xrinfo` after setting `XR_RUNTIME_JSON` +- **Notes:** Supports simulated HMD for testing OpenXR initialization without physical hardware. + +## Environment Summary + +Ensure these are in your shell profile: + +```bash +export ANDROID_HOME="$HOME/android-sdk" +export ANDROID_SDK_ROOT="$ANDROID_HOME" +export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64" +export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/25.2.9519653" +export GODOT="$HOME/bin/godot" +export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH" +export PATH="$ANDROID_HOME/platform-tools:$PATH" +export PATH="$ANDROID_HOME/build-tools/34.0.0:$PATH" +export PATH="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH" +export PATH="$HOME/bin:$PATH" +```