
Godot Asset Generator
- 909 installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
godot-asset-generator is a Claude Code skill that generates 2D game art with AI image APIs and prepares sprite sheets, tilesets, and import settings for Godot 4.x developers building games.
About
godot-asset-generator is a Deno-based agent skill in jwynia/agent-skills that turns AI image generation into Godot-ready game art. It covers concept art, style guides, sprites, sprite sheets, tilesets, UI icons, and animation frames using providers such as DALL-E, Replicate, and fal.ai, then writes import configuration for Godot 4.x. Developers reach for godot-asset-generator when they need repeatable 2D asset batches without hand-painting every frame or manually tuning Godot import presets. API keys such as OPENAI_API_KEY, REPLICATE_API_TOKEN, or fal.ai credentials are required for the chosen provider.
- Generates concept art, style-consistent sprites, sprite sheets, tilesets, and UI elements via DALL-E, Replicate, or fal.
- Handles the full 2D art pipeline from prompt to Godot-ready import configuration
- Supports batch generation of animation frames and consistent asset sets using style guides
- Outputs Godot import settings and organized asset folders
- 2D assets only; explicitly excludes 3D models and textures
Godot Asset Generator by the numbers
- 909 all-time installs (skills.sh)
- +10 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #297 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill godot-asset-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 909 |
|---|---|
| repo stars | ★ 133 |
| Security audit | 3 / 3 scanners passed |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
How do you generate Godot 4.x sprites with AI?
Generate 2D sprites, tilesets, UI icons, and animation frames with AI then automatically prepare them for Godot 4.x import.
Who is it for?
Game developers on Godot 4.x who want AI-generated 2D art with automated import prep instead of manual sprite pipeline work.
Skip if: Teams needing 3D models, proprietary engine pipelines, or art workflows with no Deno runtime or image API keys.
When should I use this skill?
The user asks to generate game sprites, tilesets, pixel art, UI icons, or Godot import settings using AI image APIs.
What you get
2D sprites, sprite sheets, tilesets, UI icons, animation frames, style guides, and Godot 4.x import configuration files.
- Sprite sheets
- Tilesets
- Godot import configuration
Files
Godot Asset Generator
Generate game assets using AI image generation APIs and prepare them for use in Godot 4.x. This skill covers the complete art pipeline from concept to Godot-ready sprites.
When to Use This Skill
Use this skill when:
- Generating game sprites, characters, or objects using AI
- Creating tilesets for platformers or top-down games
- Generating UI elements, icons, or menu assets
- Batch-generating animation frames
- Preparing AI-generated assets for Godot import
- Creating consistent asset sets with style guides
Do NOT use this skill when:
- Creating 3D models or textures (2D assets only)
- Manual pixel art or illustration (use art software)
- Complex frame-by-frame animation (use animation tools)
- Working with existing assets (use Godot directly)
Prerequisites
Required:
- Deno runtime installed
- At least one API key:
OPENAI_API_KEYfor DALL-E 3REPLICATE_API_TOKENfor Replicate (SDXL, Flux)FAL_KEYfor fal.ai
Optional:
- ImageMagick for advanced image processing
- Godot 4.x project for import file generation
Quick Start
Generate a Single Image
deno run --allow-env --allow-net --allow-write scripts/generate-image.ts \
--provider dalle \
--prompt "pixel art knight character, front view, 16-bit style, transparent background" \
--output ./assets/knight.pngBatch Generate Animation Frames
deno run --allow-env --allow-net --allow-read --allow-write scripts/batch-generate.ts \
--spec ./batch-spec.json \
--output ./generated/Create Sprite Sheet
deno run --allow-read --allow-write scripts/pack-spritesheet.ts \
--input ./generated/*.png \
--output ./sprites/player-sheet.png \
--columns 4Core Workflow
Phase 1: Style Definition
Define your art style before generating assets:
1. Choose Art Style: Pixel art, hand-drawn, painterly, or vector 2. Create Style Guide: Document colors, modifiers, and constraints 3. Test Prompts: Generate samples to validate style consistency
{
"style": "pixel-art",
"resolution": 64,
"palette": "limited-16-colors",
"modifiers": "16-bit, no anti-aliasing, clean pixels"
}Phase 2: Asset Generation
Generate assets using the appropriate provider:
1. Single Assets: Use generate-image.ts for individual images 2. Batch Assets: Use batch-generate.ts for multiple related assets 3. Iterate: Refine prompts based on results
Phase 3: Post-Processing
Prepare raw AI output for game use:
1. Background Removal: Extract sprites from backgrounds 2. Color Correction: Normalize palette if needed 3. Resize: Scale to exact game resolution 4. Trim/Pad: Remove whitespace, add sprite padding
deno run --allow-read --allow-write scripts/process-sprite.ts \
--input ./raw/knight.png \
--output ./processed/knight.png \
--remove-bg \
--resize 64x64 \
--filter nearestPhase 4: Godot Integration
Prepare assets for Godot import:
1. Pack Sprite Sheets: Combine frames into optimized sheets 2. Generate Import Files: Create .import with optimal settings 3. Configure Animations: Set up SpriteFrames resources
API Provider Selection
| Provider | Best For | Quality | Cost | Speed |
|---|---|---|---|---|
| DALL-E 3 | Consistency, high detail | Excellent | $$$ | Medium |
| Replicate | Style control, variations | Very Good | $$ | Medium |
| fal.ai | Fast iteration, testing | Good | $ | Fast |
DALL-E 3 (OpenAI)
Best for high-quality, consistent results. Excellent prompt following.
--provider dalle --model dall-e-3- Sizes: 1024x1024, 1792x1024, 1024x1792
- Quality: standard, hd
- Style: vivid, natural
Replicate (SDXL/Flux)
Best for style control and cheaper batch generation.
--provider replicate --model stability-ai/sdxl- More model options (SDXL, Flux, specialized)
- Negative prompts supported
- ControlNet and img2img available
fal.ai
Best for rapid iteration and testing prompts.
--provider fal --model fal-ai/flux/schnell- Fastest inference
- Good for prototyping
- Lower cost per image
Prompting by Art Style
Pixel Art
"pixel art [subject], 16-bit style, clean pixels, no anti-aliasing,
limited color palette, retro game sprite, transparent background"Key modifiers: 16-bit, 8-bit, pixel art, retro, clean pixels, no anti-aliasing
Avoid: realistic, detailed, smooth, gradient
Hand-Drawn / Illustrated
"hand-drawn illustration of [subject], ink lines, watercolor texture,
sketch style, game art, white background"Key modifiers: hand-drawn, illustration, ink lines, sketch, watercolor
Painterly / Concept Art
"digital painting of [subject], concept art style, painterly brush strokes,
dramatic lighting, game asset"Key modifiers: digital painting, concept art, painterly, brush strokes
Vector / Flat Design
"flat design [subject], vector art style, clean edges, solid colors,
minimal shading, game icon, transparent background"Key modifiers: flat design, vector, clean edges, solid colors, minimal
Script Reference
generate-image.ts
Generate a single image from any supported provider.
deno run --allow-env --allow-net --allow-write scripts/generate-image.ts [options]
Options:
--provider <name> Provider: dalle, replicate, fal (required)
--prompt <text> Generation prompt (required)
--output <path> Output file path (required)
--model <name> Specific model (optional, provider-dependent)
--size <WxH> Image size, e.g., 1024x1024 (default: 1024x1024)
--style <name> Style preset: pixel-art, hand-drawn, painterly, vector
--negative <text> Negative prompt (Replicate/fal only)
--quality <level> Quality: standard, hd (DALL-E only)
--json Output metadata as JSON
-h, --help Show helpbatch-generate.ts
Generate multiple images from a specification file.
deno run --allow-env --allow-net --allow-read --allow-write scripts/batch-generate.ts [options]
Options:
--spec <path> Path to batch specification JSON (required)
--output <dir> Output directory (required)
--concurrency <n> Parallel requests (default: 2)
--delay <ms> Delay between requests (default: 1000)
--resume Resume from last successful
--json Output results as JSON
-h, --help Show helpBatch Spec Format:
{
"provider": "replicate",
"model": "stability-ai/sdxl",
"style": "pixel-art",
"basePrompt": "16-bit pixel art, game sprite, transparent background",
"assets": [
{ "name": "player-idle", "prompt": "knight standing idle, front view" },
{ "name": "player-walk-1", "prompt": "knight walking, frame 1 of 4" },
{ "name": "player-walk-2", "prompt": "knight walking, frame 2 of 4" }
]
}process-sprite.ts
Post-process generated images for game use.
deno run --allow-read --allow-write scripts/process-sprite.ts [options]
Options:
--input <path> Input image path (required)
--output <path> Output image path (required)
--remove-bg Remove background (make transparent)
--resize <WxH> Resize to dimensions
--filter <type> Resize filter: nearest, linear (default: nearest)
--trim Trim transparent whitespace
--padding <n> Add padding pixels
--color-key <hex> Color to make transparent (e.g., ff00ff)
-h, --help Show helppack-spritesheet.ts
Pack multiple sprites into a sprite sheet.
deno run --allow-read --allow-write scripts/pack-spritesheet.ts [options]
Options:
--input <pattern> Input files (glob pattern, required)
--output <path> Output sprite sheet path (required)
--columns <n> Number of columns (default: auto)
--padding <n> Padding between sprites (default: 0)
--power-of-two Force power-of-two dimensions
--metadata <path> Output JSON metadata path
-h, --help Show helpOutput Metadata:
{
"image": "player-sheet.png",
"size": { "width": 256, "height": 128 },
"frames": [
{ "name": "idle", "x": 0, "y": 0, "width": 64, "height": 64 },
{ "name": "walk-1", "x": 64, "y": 0, "width": 64, "height": 64 }
]
}generate-import-files.ts
Generate Godot .import files with optimal settings.
deno run --allow-read --allow-write scripts/generate-import-files.ts [options]
Options:
--input <path> Input image or directory (required)
--preset <name> Preset: pixel-art, hd-sprite, ui (default: pixel-art)
--frames <n> Animation frame count (for sprite sheets)
--columns <n> Sprite sheet columns
--fps <n> Animation FPS (default: 12)
-h, --help Show helpGodot Import Settings
Pixel Art Sprites
Filter Mode: Nearest
Compression: Lossless
Mipmaps: Off
Fix Alpha Border: OnHD Sprites
Filter Mode: Linear
Compression: VRAM Compressed
Mipmaps: OnUI Elements
Filter Mode: Linear (or Nearest for pixel UI)
Compression: Lossless
Mipmaps: OffExamples
Example 1: Pixel Art Character with Walk Animation
# 1. Create batch spec
cat > character-batch.json << 'EOF'
{
"provider": "replicate",
"style": "pixel-art",
"basePrompt": "16-bit pixel art knight, game sprite, transparent background",
"assets": [
{ "name": "knight-idle", "prompt": "standing idle, front view" },
{ "name": "knight-walk-1", "prompt": "walking, left foot forward" },
{ "name": "knight-walk-2", "prompt": "walking, standing straight" },
{ "name": "knight-walk-3", "prompt": "walking, right foot forward" },
{ "name": "knight-walk-4", "prompt": "walking, standing straight" }
]
}
EOF
# 2. Generate images
deno run --allow-env --allow-net --allow-read --allow-write \
scripts/batch-generate.ts --spec character-batch.json --output ./raw/
# 3. Process sprites
for f in ./raw/knight-*.png; do
deno run --allow-read --allow-write scripts/process-sprite.ts \
--input "$f" --output "./processed/$(basename $f)" \
--remove-bg --resize 64x64 --filter nearest
done
# 4. Pack sprite sheet
deno run --allow-read --allow-write scripts/pack-spritesheet.ts \
--input "./processed/knight-*.png" \
--output ./sprites/knight-sheet.png \
--columns 5 --metadata ./sprites/knight-sheet.json
# 5. Generate Godot import
deno run --allow-read --allow-write scripts/generate-import-files.ts \
--input ./sprites/knight-sheet.png --preset pixel-art \
--frames 5 --columns 5 --fps 8Example 2: Tileset Generation
# Generate individual tiles
deno run --allow-env --allow-net --allow-write scripts/generate-image.ts \
--provider dalle \
--prompt "seamless pixel art grass tile, top-down view, 32x32, game tileset" \
--output ./tiles/grass.png \
--style pixel-art
# Process and resize
deno run --allow-read --allow-write scripts/process-sprite.ts \
--input ./tiles/grass.png --output ./tiles/grass-processed.png \
--resize 32x32 --filter nearestExample 3: UI Icons
# Batch generate UI icons
cat > ui-batch.json << 'EOF'
{
"provider": "fal",
"style": "vector",
"basePrompt": "flat design game icon, clean edges, solid colors, transparent background",
"assets": [
{ "name": "icon-sword", "prompt": "sword weapon icon" },
{ "name": "icon-shield", "prompt": "shield defense icon" },
{ "name": "icon-potion", "prompt": "health potion bottle icon" },
{ "name": "icon-coin", "prompt": "gold coin currency icon" }
]
}
EOF
deno run --allow-env --allow-net --allow-read --allow-write \
scripts/batch-generate.ts --spec ui-batch.json --output ./icons/Common Issues
API Key Not Found
Error: OPENAI_API_KEY environment variable is not setSolution: Export the API key before running:
export OPENAI_API_KEY="sk-..."Inconsistent Style Across Batch
Problem: Generated images have different styles despite same prompt.
Solutions:
- Use more specific style modifiers
- Use Replicate with seed parameter for reproducibility
- Generate more images and select best matches
- Use img2img with reference image (Replicate)
Background Removal Fails
Problem: --remove-bg doesn't cleanly separate sprite.
Solutions:
- Add "transparent background" or "white background" to prompt
- Use
--color-keywith a specific background color - Use more explicit prompts: "isolated on transparent background"
- Manual cleanup may be needed for complex images
Pixel Art Has Anti-Aliasing
Problem: Generated pixel art has smoothed edges.
Solutions:
- Add "no anti-aliasing", "clean pixels" to prompt
- Generate at larger size, then downscale with nearest neighbor
- Use
--filter nearestin process-sprite.ts - Post-process with palette reduction
Rate Limiting
Problem: API returns 429 rate limit errors.
Solutions:
- Increase
--delayin batch-generate.ts - Reduce
--concurrency - Wait and retry
- Use different provider for large batches
Additional Resources
Prompting Guides
references/prompting/pixel-art.md- Detailed pixel art techniquesreferences/prompting/hand-drawn.md- Illustrated style guidereferences/prompting/consistent-characters.md- Character consistency
API Guides
references/api-guides/openai-dalle.md- DALL-E 3 API referencereferences/api-guides/replicate-sdxl.md- Replicate integrationreferences/api-guides/fal-ai.md- fal.ai guide
Godot Integration
references/godot-integration/import-settings.md- Import configurationreferences/godot-integration/animation-setup.md- AnimatedSprite2D setup
Templates
assets/prompts/pixel-art-templates.json- Pixel art prompt templatesassets/prompts/character-templates.json- Character promptsassets/style-guides/style-guide-template.json- Style guide schema
Limitations
- API-based only: Requires internet and API keys (no local models)
- Style consistency: AI may produce variations despite same prompt
- Resolution constraints: Each provider has size limits
- 2D assets only: Not for 3D models, textures, or complex animations
- Background removal: May require manual cleanup for complex images
- Cost: API calls incur charges, especially for large batches
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Pixel art prompt templates for AI image generation",
"version": "1.0",
"baseModifiers": {
"style": "pixel art, 16-bit style, clean pixels, no anti-aliasing",
"background": "transparent background",
"quality": "game sprite, limited color palette, retro game aesthetic"
},
"negativePrompt": "blurry, anti-aliased, smooth, gradient, realistic, 3d render, photorealistic, soft edges, high resolution photography",
"templates": {
"character": {
"description": "Character sprite templates",
"idle": {
"prompt": "{style}, {character_description}, idle standing pose, {view_direction} view, {background}, {quality}",
"variables": {
"character_description": "knight warrior with sword and shield",
"view_direction": "front"
},
"example": "pixel art, 16-bit style, clean pixels, no anti-aliasing, knight warrior with sword and shield, idle standing pose, front view, transparent background, game sprite, limited color palette, retro game aesthetic"
},
"walk": {
"prompt": "{style}, {character_description}, walking animation frame {frame} of {total_frames}, {view_direction} view, {background}, {quality}, consistent style",
"variables": {
"character_description": "knight warrior",
"frame": "1",
"total_frames": "4",
"view_direction": "side"
}
},
"attack": {
"prompt": "{style}, {character_description}, attack pose, {attack_type}, dynamic action, {view_direction} view, {background}, {quality}",
"variables": {
"character_description": "knight warrior",
"attack_type": "sword swing",
"view_direction": "side"
}
},
"hurt": {
"prompt": "{style}, {character_description}, hurt reaction pose, taking damage, {view_direction} view, {background}, {quality}",
"variables": {
"character_description": "knight warrior",
"view_direction": "front"
}
}
},
"enemy": {
"description": "Enemy sprite templates",
"basic": {
"prompt": "{style}, {enemy_description}, game enemy sprite, {pose}, {view_direction} view, {background}, {quality}",
"variables": {
"enemy_description": "slime monster blob creature",
"pose": "idle bouncing",
"view_direction": "front"
}
},
"boss": {
"prompt": "{style}, {enemy_description}, boss enemy sprite, large imposing, {pose}, {view_direction} view, {background}, {quality}, detailed",
"variables": {
"enemy_description": "dragon creature",
"pose": "threatening stance",
"view_direction": "front"
}
}
},
"items": {
"description": "Item and pickup sprite templates",
"weapon": {
"prompt": "{style}, {weapon_description}, weapon item sprite, isolated object, {background}, {quality}, iconic design",
"variables": {
"weapon_description": "steel longsword with golden hilt"
}
},
"consumable": {
"prompt": "{style}, {item_description}, consumable item sprite, isolated object, {background}, {quality}, recognizable shape",
"variables": {
"item_description": "red health potion in glass bottle"
}
},
"collectible": {
"prompt": "{style}, {item_description}, collectible pickup sprite, glowing effect, isolated object, {background}, {quality}",
"variables": {
"item_description": "golden coin"
}
}
},
"environment": {
"description": "Environment and tile templates",
"tile": {
"prompt": "seamless {style}, {tile_description}, {view} view, 32x32 pixels, tileable pattern, game tileset, clean edges",
"variables": {
"tile_description": "grass ground with small flowers",
"view": "top-down"
}
},
"prop": {
"prompt": "{style}, {prop_description}, environment prop, {background}, {quality}, game decoration",
"variables": {
"prop_description": "wooden barrel"
}
},
"platform": {
"prompt": "{style}, {platform_description}, platformer game tile, side view, {background}, {quality}",
"variables": {
"platform_description": "stone platform block"
}
}
},
"ui": {
"description": "UI element templates",
"icon": {
"prompt": "{style}, {icon_description}, game UI icon, simple clear design, {background}, clean edges",
"variables": {
"icon_description": "heart health icon"
}
},
"button": {
"prompt": "{style}, {button_description}, game UI button, 9-patch compatible, {background}, clean edges",
"variables": {
"button_description": "wooden button frame"
}
},
"frame": {
"prompt": "{style}, {frame_description}, game UI frame border, 9-patch compatible, {background}",
"variables": {
"frame_description": "ornate golden frame"
}
}
},
"effects": {
"description": "Visual effect templates",
"explosion": {
"prompt": "{style}, {effect_description}, effect sprite frame {frame} of {total_frames}, {background}, {quality}",
"variables": {
"effect_description": "fire explosion",
"frame": "1",
"total_frames": "6"
}
},
"particle": {
"prompt": "{style}, {particle_description}, small particle sprite, {background}, simple shape",
"variables": {
"particle_description": "sparkle star"
}
}
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Template for defining consistent art style across game assets",
"version": "1.0",
"template": {
"projectName": "My Game Project",
"description": "Brief description of the game and its visual style",
"artStyle": {
"type": "pixel-art",
"typeOptions": ["pixel-art", "hand-drawn", "painterly", "vector", "mixed"],
"resolution": {
"baseSize": 32,
"sizes": {
"character": 64,
"item": 32,
"tile": 32,
"ui": 48
},
"notes": "Base pixel size for sprites. Generate at 1024x1024 then downscale."
},
"aspectRatio": {
"characters": "1:1",
"backgrounds": "16:9",
"tiles": "1:1"
}
},
"colorPalette": {
"description": "Color scheme for the game",
"type": "limited",
"maxColors": 32,
"primary": [
{"name": "dark-bg", "hex": "#1a1a2e"},
{"name": "mid-bg", "hex": "#16213e"},
{"name": "light-bg", "hex": "#0f3460"}
],
"accent": [
{"name": "gold", "hex": "#e8b923"},
{"name": "red", "hex": "#e74c3c"},
{"name": "blue", "hex": "#3498db"}
],
"skin": [
{"name": "light", "hex": "#ffd5c8"},
{"name": "mid", "hex": "#f4a683"},
{"name": "dark", "hex": "#c67b5c"}
],
"reference": "Optional: reference palette like PICO-8, NES, or custom"
},
"promptModifiers": {
"global": "16-bit pixel art, clean pixels, no anti-aliasing, limited color palette",
"characters": "game sprite, expressive pose, clear silhouette",
"enemies": "distinct from player, threatening but readable",
"items": "iconic design, recognizable at small size",
"environment": "atmospheric, supports gameplay clarity",
"ui": "clean edges, high contrast, readable"
},
"negativePrompts": {
"global": "blurry, anti-aliased, smooth gradients, realistic, photographic, 3d render",
"characters": "oversized, cluttered details, unclear pose",
"items": "complex, hard to identify"
},
"providerSettings": {
"preferred": "dalle",
"fallback": "replicate",
"dalle": {
"quality": "standard",
"style": "vivid",
"size": "1024x1024"
},
"replicate": {
"model": "stability-ai/sdxl",
"guidance_scale": 7.5
},
"fal": {
"model": "fal-ai/flux/schnell"
}
},
"godotSettings": {
"filterMode": "Nearest",
"compression": "Lossless",
"mipmaps": false,
"fixAlphaBorder": true,
"projectSettings": {
"defaultTextureFilter": "Nearest",
"stretchMode": "viewport",
"stretchAspect": "keep"
}
},
"namingConventions": {
"characters": "{character}-{action}-{frame}",
"items": "item-{category}-{name}",
"tiles": "tile-{tileset}-{variant}",
"ui": "ui-{element}-{state}",
"examples": [
"player-walk-01",
"item-weapon-sword",
"tile-grass-center",
"ui-button-hover"
]
},
"animations": {
"idle": {"frames": 4, "fps": 6, "loop": true},
"walk": {"frames": 4, "fps": 10, "loop": true},
"run": {"frames": 6, "fps": 12, "loop": true},
"attack": {"frames": 4, "fps": 14, "loop": false},
"hurt": {"frames": 2, "fps": 8, "loop": false},
"death": {"frames": 4, "fps": 8, "loop": false}
},
"consistency": {
"characterSheets": "Generate front, side, back views for each character",
"lighting": "Top-down light source, consistent shadows",
"outlines": "1px dark outline on all sprites",
"highlights": "Subtle 1px highlights on top edges"
},
"references": {
"games": ["Stardew Valley", "Celeste", "Undertale"],
"palettes": ["PICO-8", "Endesga 32"],
"artists": [],
"moodBoard": "Link to mood board or reference images"
}
},
"exampleStyleGuide": {
"projectName": "Fantasy Quest",
"description": "A top-down action RPG with 16-bit inspired pixel art",
"artStyle": {
"type": "pixel-art",
"resolution": {
"baseSize": 32,
"sizes": {
"character": 48,
"item": 24,
"tile": 32,
"ui": 32
}
}
},
"colorPalette": {
"type": "limited",
"maxColors": 32,
"primary": [
{"name": "grass", "hex": "#4a7c3e"},
{"name": "stone", "hex": "#7c7c7c"},
{"name": "wood", "hex": "#8b5a2b"}
],
"accent": [
{"name": "gold", "hex": "#ffd700"},
{"name": "magic", "hex": "#9b59b6"}
]
},
"promptModifiers": {
"global": "16-bit pixel art, SNES RPG style, clean pixels, fantasy game",
"characters": "expressive, clear silhouette, 3/4 top-down view"
}
}
}
DALL-E 3 API Guide
Complete reference for using OpenAI's DALL-E 3 API for game asset generation.
Authentication
Set the OPENAI_API_KEY environment variable:
export OPENAI_API_KEY="sk-..."Get your API key from: https://platform.openai.com/api-keys
API Endpoint
POST https://api.openai.com/v1/images/generationsRequest Format
{
"model": "dall-e-3",
"prompt": "your prompt here",
"n": 1,
"size": "1024x1024",
"quality": "standard",
"style": "vivid",
"response_format": "b64_json"
}Parameters
model
dall-e-3- Latest model, best qualitydall-e-2- Older model, cheaper, less capable
size
| Size | Aspect | Best For |
|---|---|---|
| 1024x1024 | Square | Sprites, icons, tiles |
| 1792x1024 | Landscape | Backgrounds, scenes |
| 1024x1792 | Portrait | Character art, UI |
quality
standard- Default, good for most uses ($0.040/image)hd- Higher detail, sharper ($0.080/image)
style
vivid- Default, dramatic and vibrantnatural- More realistic, less stylized
response_format
url- Returns temporary URL (expires in 1 hour)b64_json- Returns base64-encoded image data
Response Format
{
"created": 1699000000,
"data": [
{
"revised_prompt": "A detailed pixel art knight...",
"b64_json": "iVBORw0KGgoAAAANSUhEUgAA..."
}
]
}Note: DALL-E 3 may revise your prompt. The revised_prompt field shows what was actually used.
Rate Limits
| Tier | Images/min | Images/day |
|---|---|---|
| Free | 5 | 50 |
| Tier 1 | 7 | 100 |
| Tier 2+ | 7 | 500+ |
Pricing (as of 2024)
| Model | Quality | Size | Price |
|---|---|---|---|
| DALL-E 3 | Standard | 1024x1024 | $0.040 |
| DALL-E 3 | Standard | 1792x1024 | $0.080 |
| DALL-E 3 | HD | 1024x1024 | $0.080 |
| DALL-E 3 | HD | 1792x1024 | $0.120 |
| DALL-E 2 | - | 1024x1024 | $0.020 |
| DALL-E 2 | - | 512x512 | $0.018 |
Error Handling
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 401 | Invalid API key | Check OPENAI_API_KEY |
| 429 | Rate limit | Wait and retry |
| 400 | Invalid request | Check prompt/parameters |
| 500 | Server error | Retry with backoff |
Content Policy
DALL-E 3 has content restrictions. Avoid:
- Violence or gore
- Adult content
- Real people's faces
- Copyrighted characters by name
If rejected, rephrase the prompt to be more abstract.
TypeScript Example
interface DalleRequest {
model: "dall-e-3" | "dall-e-2";
prompt: string;
n: number;
size: "1024x1024" | "1792x1024" | "1024x1792";
quality?: "standard" | "hd";
style?: "vivid" | "natural";
response_format?: "url" | "b64_json";
}
async function generateWithDalle(prompt: string): Promise<string> {
const apiKey = Deno.env.get("OPENAI_API_KEY");
const response = await fetch("https://api.openai.com/v1/images/generations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "dall-e-3",
prompt,
n: 1,
size: "1024x1024",
quality: "standard",
response_format: "b64_json",
}),
});
if (!response.ok) {
throw new Error(`DALL-E API error: ${response.status}`);
}
const data = await response.json();
return data.data[0].b64_json;
}Game Asset Tips
Pixel Art
prompt: "pixel art [subject], 16-bit style, game sprite, clean pixels,
no anti-aliasing, transparent background, limited color palette"
quality: "standard" // HD adds unwanted detail
style: "vivid" // More saturated colorsBackgrounds
prompt: "[scene description], game background art, painted style,
horizontal composition, no characters"
size: "1792x1024" // Landscape for backgrounds
quality: "hd" // More detail for backgroundsUI Elements
prompt: "game UI [element], flat design, clean edges, [color scheme],
game interface style"
quality: "standard"
style: "vivid"Consistency Tips
1. Use detailed prompts - DALL-E 3 follows prompts closely 2. Specify art style explicitly - "16-bit pixel art", "hand-painted" 3. Request transparent backgrounds - Add "transparent background" or "isolated on white" 4. Batch with same prompt structure - Keep base prompt consistent
Cost Optimization
1. Start with standard quality - Use HD only when needed 2. Use 1024x1024 for sprites - Smaller is cheaper 3. Test prompts on DALL-E 2 first - Much cheaper for iteration 4. Cache and reuse - Don't regenerate identical requests 5. Batch wisely - Generate variations only when needed
Integration with generate-image.ts
# Basic usage
deno run scripts/generate-image.ts --provider dalle \
--prompt "pixel art knight" --output ./knight.png
# HD quality
deno run scripts/generate-image.ts --provider dalle \
--prompt "game background forest" --output ./forest.png \
--quality hd --size 1792x1024
# With style preset
deno run scripts/generate-image.ts --provider dalle \
--prompt "knight character, front view" --output ./knight.png \
--style pixel-artGodot Import Settings Guide
Optimal import settings for different game asset types in Godot 4.x.
Import Settings Overview
When you add an image to a Godot project, it creates a .import file that controls how the image is processed.
Presets Reference
Pixel Art Sprites
For retro/pixel art games with crisp, unfiltered pixels.
| Setting | Value | Reason |
|---|---|---|
| Filter Mode | Nearest | Preserves hard pixel edges |
| Compress Mode | Lossless | No quality loss |
| Mipmaps | Off | Prevents blur at different scales |
| Fix Alpha Border | On | Prevents edge artifacts |
Import file settings:
compress/mode=0
mipmaps/generate=false
process/fix_alpha_border=trueProject Settings (also set globally):
rendering/textures/canvas_textures/default_texture_filter = NearestHD 2D Sprites
For high-resolution 2D games with smooth scaling.
| Setting | Value | Reason |
|---|---|---|
| Filter Mode | Linear | Smooth scaling |
| Compress Mode | VRAM Compressed | GPU-friendly |
| Mipmaps | On | Better quality at small sizes |
| Fix Alpha Border | On | Prevents edge artifacts |
Import file settings:
compress/mode=2
compress/high_quality=true
mipmaps/generate=true
process/fix_alpha_border=trueUI Elements
For interface elements like buttons, panels, icons.
| Setting | Value | Reason |
|---|---|---|
| Filter Mode | Linear (or Nearest for pixel UI) | Depends on style |
| Compress Mode | Lossless | Crisp text/edges |
| Mipmaps | Off | UI is at fixed scale |
| Fix Alpha Border | On | Clean edges |
Backgrounds
For large background images.
| Setting | Value | Reason |
|---|---|---|
| Filter Mode | Linear | Smooth appearance |
| Compress Mode | Lossy | Smaller file size OK |
| Mipmaps | On | Better for parallax |
| Lossy Quality | 0.8 | Balance size/quality |
Compression Modes
| Mode | Value | Size | Quality | Best For |
|---|---|---|---|---|
| Lossless | 0 | Large | Perfect | Sprites, UI |
| Lossy | 1 | Medium | Good | Photos, backgrounds |
| VRAM | 2 | Small | Good | Large textures, 3D |
Filter Modes
Nearest (Pixel Art)
Preserves hard pixel edges. Essential for:
- Pixel art sprites
- Retro-style games
- Any asset where you want visible pixels
Linear (Smooth)
Interpolates between pixels. Use for:
- HD sprites
- Painted/illustrated assets
- Photographs
- Backgrounds
Sprite Sheet Configuration
For sprite sheets, configure in the Import dock:
1. Import as: Texture2D (default) 2. Detect 3D: Off 3. Process → Fix Alpha Border: On
Then in your scene:
- Create
AnimatedSprite2Dnode - Create new
SpriteFramesresource - Add frames by region or auto-slice
Using Sheet Metadata
If using pack-spritesheet.ts with --metadata:
# Load metadata
var meta = JSON.parse_string(FileAccess.open("res://sprites/sheet.json", FileAccess.READ).get_as_text())
# Create frames from metadata
var frames = SpriteFrames.new()
frames.add_animation("walk")
for frame in meta.frames:
var texture = load("res://sprites/sheet.png")
var atlas = AtlasTexture.new()
atlas.atlas = texture
atlas.region = Rect2(frame.x, frame.y, frame.width, frame.height)
frames.add_frame("walk", atlas)AnimatedSprite2D Setup
From Sprite Sheet
1. Add AnimatedSprite2D node 2. Create new SpriteFrames in Sprite Frames property 3. Click SpriteFrames to open animation editor 4. Add animation (e.g., "walk") 5. Click folder icon → "Add Frames from Sprite Sheet" 6. Select your sprite sheet image 7. Configure grid (columns × rows) 8. Select frames for animation 9. Set FPS in animation panel
Recommended FPS
| Animation Type | FPS |
|---|---|
| Idle | 4-6 |
| Walk | 8-12 |
| Run | 10-14 |
| Attack | 12-16 |
| Effects | 15-24 |
AtlasTexture for Individual Frames
For extracting single sprites from a sheet:
var atlas = AtlasTexture.new()
atlas.atlas = preload("res://sprites/sheet.png")
atlas.region = Rect2(0, 0, 64, 64) # x, y, width, height
$Sprite2D.texture = atlasNine-Patch for UI
For scalable UI elements (panels, buttons):
1. Import as Texture2D with Lossless 2. Create NinePatchRect node 3. Assign texture 4. Set patch margins (left, top, right, bottom) 5. Set draw center as needed
var nine_patch = NinePatchRect.new()
nine_patch.texture = preload("res://ui/panel.png")
nine_patch.patch_margin_left = 16
nine_patch.patch_margin_top = 16
nine_patch.patch_margin_right = 16
nine_patch.patch_margin_bottom = 16Tileset Setup
For tile-based games:
1. Import tile sheet with Nearest filter (for pixel art) 2. Create TileSet resource 3. Add texture as TileSetAtlasSource 4. Configure tile size (e.g., 16×16, 32×32) 5. Set up collision, navigation as needed
TileMap configuration:
var tilemap = TileMap.new()
tilemap.tile_set = preload("res://tilesets/world.tres")
tilemap.cell_quadrant_size = 16 # Match tile sizeProject-Wide Settings
For Pixel Art Games
In Project Settings → Rendering → Textures:
canvas_textures/default_texture_filter = NearestIn Project Settings → Display → Window:
stretch/mode = viewport
stretch/aspect = keepFor HD Games
In Project Settings → Rendering → Textures:
canvas_textures/default_texture_filter = LinearUsing generate-import-files.ts
# Pixel art preset
deno run scripts/generate-import-files.ts \
--input ./sprites/player.png --preset pixel-art
# HD sprites
deno run scripts/generate-import-files.ts \
--input ./hd-sprites/ --preset hd-sprite
# UI elements
deno run scripts/generate-import-files.ts \
--input ./ui/ --preset uiTroubleshooting
Sprites Look Blurry
- Check Filter Mode is set to Nearest
- Verify project default texture filter
- Ensure no scaling applied to Sprite2D node
Edge Artifacts / Bleeding
- Enable "Fix Alpha Border" in import
- Add 1-2px transparent padding in sprite sheet
- Use
--paddingoption in pack-spritesheet.ts
Colors Look Wrong
- Check color space settings
- Ensure PNG is saved with correct color profile
- Try Lossless compression mode
File Size Too Large
- Use VRAM compression for large textures
- Lower Lossy quality for backgrounds
- Consider splitting very large sheets
Pixel Art Prompting Guide
Comprehensive guide to generating pixel art game assets with AI image APIs.
Core Principles
Pixel art AI generation requires specific prompting to avoid common issues like anti-aliasing, wrong resolution, and blurry pixels.
Essential Modifiers
Always include these modifiers for pixel art:
pixel art, 16-bit, clean pixels, no anti-aliasing, limited color palette,
retro game sprite, crisp edgesResolution Modifiers
| Style | Modifier | Best For |
|---|---|---|
| 8-bit | "8-bit, NES style, very low resolution" | Tiny sprites, icons |
| 16-bit | "16-bit, SNES style, pixel art" | Standard game sprites |
| 32-bit | "32-bit pixel art, detailed pixels" | HD pixel art |
Negative Prompts (Replicate/fal.ai)
blurry, anti-aliased, smooth, gradient, realistic, 3d render,
photorealistic, high resolution photography, soft edgesPrompt Templates
Character Sprites
pixel art [character description], 16-bit style, game sprite,
[pose/action], [view direction], clean pixels, no anti-aliasing,
transparent background, limited color paletteExample:
pixel art knight warrior, 16-bit style, game sprite, idle standing pose,
front view, clean pixels, no anti-aliasing, transparent background,
limited color palette, medieval fantasyAnimation Frames
pixel art [character], 16-bit style, [animation] frame [N] of [total],
game animation sprite, consistent style, clean pixels,
transparent backgroundExample:
pixel art knight, 16-bit style, walking animation frame 1 of 4,
left foot forward, game animation sprite, consistent style,
clean pixels, transparent backgroundTilesets
seamless pixel art tile, [description], top-down view, 32x32 pixels,
tileable pattern, game tileset, retro style, clean edgesExample:
seamless pixel art grass tile, green meadow with small flowers,
top-down view, 32x32 pixels, tileable pattern, game tileset,
retro style, clean edgesUI Elements
pixel art [element type], 16-bit UI style, game interface element,
clean edges, flat colors, retro game aestheticExample:
pixel art health bar frame, 16-bit UI style, game interface element,
red and gold colors, clean edges, flat colors, retro game aestheticItems and Objects
pixel art [item], 16-bit style, game item sprite, isolated object,
transparent background, clean pixels, iconic designExample:
pixel art sword weapon, 16-bit style, game item sprite,
glowing blue blade, isolated object, transparent background,
clean pixels, iconic fantasy designProvider-Specific Tips
DALL-E 3
- Excellent prompt following
- Tends toward higher detail - emphasize "simple", "limited palette"
- Use "pixel art style illustration" for more stylized results
- Request "transparent background" explicitly
Replicate (SDXL)
- Use negative prompts to avoid realism
- Models like
kohaku-xlare good for stylized pixel art - Set guidance_scale to 7-9 for pixel art
- Consider upscaling then downscaling for cleaner results
fal.ai (Flux)
- Fast iteration for testing prompts
- Works well with simple, direct prompts
- May need post-processing for clean pixels
Post-Processing Pipeline
1. Generate at 1024x1024 - Higher resolution gives more detail 2. Remove background - Use --remove-bg or --color-key 3. Downscale - Use --resize with --filter nearest 4. Palette reduction - Optional: reduce to 16/32 colors
# Example pipeline
deno run scripts/generate-image.ts --provider dalle \
--prompt "pixel art knight, 16-bit style..." \
--output ./raw/knight.png
deno run scripts/process-sprite.ts \
--input ./raw/knight.png \
--output ./sprites/knight.png \
--remove-bg --resize 64x64 --filter nearestCommon Issues
Anti-Aliasing / Soft Edges
Problem: Pixels have soft, blurred edges instead of hard transitions.
Solutions:
- Add "no anti-aliasing", "hard edges", "crisp pixels"
- Generate larger, then downscale with nearest neighbor
- Use negative prompt: "anti-aliased, smooth, blurry"
Wrong Scale / Too Detailed
Problem: Image has too many tiny details, not blocky enough.
Solutions:
- Specify exact resolution: "32x32 pixels", "64 pixel sprite"
- Add "low resolution", "chunky pixels", "simple"
- Reduce after generation with nearest neighbor
Inconsistent Style
Problem: Multiple assets don't match visually.
Solutions:
- Create and reuse consistent base prompt
- Use same model and settings for batch
- Reference specific game/era: "SNES style", "like Stardew Valley"
- Post-process with same palette reduction
Gradients Instead of Flat Colors
Problem: AI uses gradients where flat color blocks are expected.
Solutions:
- Add "flat colors", "solid colors", "no gradients"
- Specify "limited color palette", "16 colors"
- Use negative prompt: "gradient, shading, realistic"
Resolution Guide
| Sprite Size | Generate At | Downscale To |
|---|---|---|
| 16x16 | 512x512 or 1024x1024 | 16x16 |
| 32x32 | 1024x1024 | 32x32 |
| 64x64 | 1024x1024 | 64x64 |
| 128x128 | 1024x1024 | 128x128 |
Always use nearest neighbor filtering when downscaling.
Color Palette Tips
Specifying Palettes
limited to 16 colors, [palette description]Examples:
- "NES color palette, limited colors"
- "earth tones palette, browns and greens"
- "fantasy game palette, purples and golds"
- "monochrome blue palette with white highlights"
Common Game Palettes
Reference known palettes for consistency:
- "NES color palette" - Classic 8-bit limited colors
- "Game Boy palette" - 4 shades of green
- "PICO-8 palette" - 16 specific colors
- "Commodore 64 palette" - Specific retro palette
Example Prompts
Player Character (Idle)
pixel art adventurer hero, 16-bit RPG style, idle standing pose,
front view, brown hair, blue tunic, sword on back, game sprite,
clean pixels, no anti-aliasing, transparent background,
limited fantasy color paletteEnemy (Slime)
pixel art slime monster, 16-bit style, bouncy blob creature,
green translucent body, simple cute face, game enemy sprite,
front view, clean pixels, transparent background, cartoonyEnvironment Tile (Stone Floor)
seamless pixel art stone floor tile, medieval dungeon style,
top-down view, 32x32 pixels, gray cobblestone pattern,
tileable texture, subtle cracks, game tileset, clean edgesItem (Potion)
pixel art health potion bottle, 16-bit style, red liquid,
glass bottle with cork, glowing effect, game item sprite,
isolated object, transparent background, fantasy RPG item#!/usr/bin/env -S deno run --allow-env --allow-net --allow-read --allow-write
/**
* Batch Image Generation CLI
*
* Generate multiple images from a specification file with rate limiting
* and progress tracking.
*
* Usage:
* deno run --allow-env --allow-net --allow-read --allow-write scripts/batch-generate.ts \
* --spec ./batch-spec.json --output ./generated/
*
* Permissions:
* --allow-env: Read API key environment variables
* --allow-net: Make API requests
* --allow-read: Read specification file
* --allow-write: Save generated images and progress
*/
import { generateImage } from "./generate-image.ts";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "batch-generate";
// === Types ===
interface AssetSpec {
name: string;
prompt: string;
size?: string;
model?: string;
}
interface BatchSpec {
provider: "dalle" | "replicate" | "fal";
model?: string;
style?: string;
basePrompt?: string;
size?: string;
assets: AssetSpec[];
}
interface BatchResult {
spec: string;
outputDir: string;
totalAssets: number;
successful: number;
failed: number;
durationMs: number;
results: Array<{
name: string;
success: boolean;
output?: string;
error?: string;
durationMs: number;
}>;
}
interface ProgressState {
completed: string[];
failed: string[];
}
// === Progress Tracking ===
async function loadProgress(progressFile: string): Promise<ProgressState> {
try {
const content = await Deno.readTextFile(progressFile);
return JSON.parse(content);
} catch {
return { completed: [], failed: [] };
}
}
async function saveProgress(progressFile: string, state: ProgressState): Promise<void> {
await Deno.writeTextFile(progressFile, JSON.stringify(state, null, 2));
}
// === Core Batch Generation ===
export async function batchGenerate(
spec: BatchSpec,
outputDir: string,
options: {
concurrency?: number;
delay?: number;
resume?: boolean;
progressFile?: string;
} = {}
): Promise<BatchResult> {
const startTime = Date.now();
const concurrency = options.concurrency ?? 2;
const delay = options.delay ?? 1000;
const progressFile = options.progressFile || `${outputDir}/.batch-progress.json`;
// Load progress if resuming
let progress: ProgressState = { completed: [], failed: [] };
if (options.resume) {
progress = await loadProgress(progressFile);
}
// Ensure output directory exists
await Deno.mkdir(outputDir, { recursive: true });
const results: BatchResult["results"] = [];
let successful = 0;
let failed = 0;
// Filter out already completed assets if resuming
const assetsToProcess = options.resume
? spec.assets.filter((a) => !progress.completed.includes(a.name))
: spec.assets;
console.log(`\nBatch generation: ${assetsToProcess.length} assets`);
console.log(`Provider: ${spec.provider}`);
console.log(`Output: ${outputDir}`);
console.log(`Concurrency: ${concurrency}, Delay: ${delay}ms\n`);
// Process in batches based on concurrency
for (let i = 0; i < assetsToProcess.length; i += concurrency) {
const batch = assetsToProcess.slice(i, i + concurrency);
const batchPromises = batch.map(async (asset) => {
const assetStart = Date.now();
// Build full prompt
let fullPrompt = asset.prompt;
if (spec.basePrompt) {
fullPrompt = `${spec.basePrompt}, ${asset.prompt}`;
}
const outputPath = `${outputDir}/${asset.name}.png`;
console.log(`[${i + batch.indexOf(asset) + 1}/${assetsToProcess.length}] Generating: ${asset.name}`);
const result = await generateImage({
provider: spec.provider,
prompt: fullPrompt,
output: outputPath,
model: asset.model || spec.model,
size: asset.size || spec.size,
style: spec.style,
});
const assetResult = {
name: asset.name,
success: result.success,
output: result.success ? outputPath : undefined,
error: result.error,
durationMs: Date.now() - assetStart,
};
if (result.success) {
successful++;
progress.completed.push(asset.name);
console.log(` ✓ ${asset.name} (${assetResult.durationMs}ms)`);
} else {
failed++;
progress.failed.push(asset.name);
console.log(` ✗ ${asset.name}: ${result.error}`);
}
// Save progress after each asset
await saveProgress(progressFile, progress);
return assetResult;
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Delay between batches (not after last batch)
if (i + concurrency < assetsToProcess.length) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Add results for previously completed assets if resuming
if (options.resume) {
for (const name of progress.completed) {
if (!results.find((r) => r.name === name)) {
results.push({
name,
success: true,
output: `${outputDir}/${name}.png`,
durationMs: 0,
});
successful++;
}
}
}
return {
spec: options.progressFile || "inline",
outputDir,
totalAssets: spec.assets.length,
successful,
failed,
durationMs: Date.now() - startTime,
results,
};
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Batch image generation
Usage:
deno run --allow-env --allow-net --allow-read --allow-write scripts/batch-generate.ts [options]
Required:
--spec <path> Path to batch specification JSON
--output <dir> Output directory for generated images
Optional:
--concurrency <n> Parallel requests (default: 2)
--delay <ms> Delay between batches in ms (default: 1000)
--resume Resume from last successful (reads progress file)
--json Output results as JSON
-h, --help Show this help
Batch Spec Format:
{
"provider": "dalle" | "replicate" | "fal",
"model": "optional-model-name",
"style": "pixel-art" | "hand-drawn" | "painterly" | "vector",
"basePrompt": "prefix added to all prompts",
"size": "1024x1024",
"assets": [
{ "name": "asset-name", "prompt": "specific prompt" },
{ "name": "another", "prompt": "another prompt", "size": "512x512" }
]
}
Examples:
# Basic batch generation
./scripts/batch-generate.ts --spec ./batch.json --output ./sprites/
# With rate limiting
./scripts/batch-generate.ts --spec ./batch.json --output ./sprites/ \\
--concurrency 1 --delay 2000
# Resume interrupted batch
./scripts/batch-generate.ts --spec ./batch.json --output ./sprites/ --resume
`);
}
// === Argument Parsing ===
interface BatchOptions {
specPath: string;
outputDir: string;
concurrency: number;
delay: number;
resume: boolean;
outputJson: boolean;
}
function parseArgs(args: string[]): BatchOptions | null {
const options: Partial<BatchOptions> = {
concurrency: 2,
delay: 1000,
resume: false,
outputJson: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
return null;
} else if (arg === "--spec" && args[i + 1]) {
options.specPath = args[++i];
} else if (arg === "--output" && args[i + 1]) {
options.outputDir = args[++i];
} else if (arg === "--concurrency" && args[i + 1]) {
options.concurrency = parseInt(args[++i], 10);
} else if (arg === "--delay" && args[i + 1]) {
options.delay = parseInt(args[++i], 10);
} else if (arg === "--resume") {
options.resume = true;
} else if (arg === "--json") {
options.outputJson = true;
}
}
return options as BatchOptions;
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printHelp();
Deno.exit(0);
}
const options = parseArgs(args);
if (!options) {
printHelp();
Deno.exit(0);
}
if (!options.specPath) {
console.error("Error: --spec is required");
Deno.exit(1);
}
if (!options.outputDir) {
console.error("Error: --output is required");
Deno.exit(1);
}
// Load spec
let spec: BatchSpec;
try {
const content = await Deno.readTextFile(options.specPath);
spec = JSON.parse(content);
} catch (error) {
console.error(`Error reading spec file: ${error instanceof Error ? error.message : error}`);
Deno.exit(1);
}
// Validate spec
if (!spec.provider) {
console.error("Error: spec must include 'provider'");
Deno.exit(1);
}
if (!spec.assets || !Array.isArray(spec.assets) || spec.assets.length === 0) {
console.error("Error: spec must include non-empty 'assets' array");
Deno.exit(1);
}
const result = await batchGenerate(spec, options.outputDir, {
concurrency: options.concurrency,
delay: options.delay,
resume: options.resume,
progressFile: `${options.outputDir}/.batch-progress.json`,
});
if (options.outputJson) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log("\n" + "=".repeat(50));
console.log("Batch Complete");
console.log("=".repeat(50));
console.log(`Total: ${result.totalAssets}`);
console.log(`Successful: ${result.successful}`);
console.log(`Failed: ${result.failed}`);
console.log(`Duration: ${(result.durationMs / 1000).toFixed(1)}s`);
if (result.failed > 0) {
console.log("\nFailed assets:");
for (const r of result.results.filter((r) => !r.success)) {
console.log(` - ${r.name}: ${r.error}`);
}
}
}
if (result.failed > 0) {
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-env --allow-net --allow-write
/**
* Image Generation CLI - Unified API Wrapper
*
* Generate images using DALL-E 3, Replicate, or fal.ai APIs.
* Provides a consistent interface across providers with style presets.
*
* Usage:
* deno run --allow-env --allow-net --allow-write scripts/generate-image.ts \
* --provider dalle --prompt "pixel art knight" --output ./knight.png
*
* Environment Variables:
* OPENAI_API_KEY - Required for DALL-E 3
* REPLICATE_API_TOKEN - Required for Replicate
* FAL_KEY - Required for fal.ai
*
* Permissions:
* --allow-env: Read API key environment variables
* --allow-net: Make API requests
* --allow-write: Save generated images
*/
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-image";
// === Style Presets ===
const STYLE_PRESETS: Record<string, { prefix: string; suffix: string; negative?: string }> = {
"pixel-art": {
prefix: "pixel art style, 16-bit, retro game sprite,",
suffix: ", clean pixels, no anti-aliasing, limited color palette",
negative: "blurry, smooth, gradient, realistic, 3d, photorealistic",
},
"hand-drawn": {
prefix: "hand-drawn illustration,",
suffix: ", ink lines, sketch style, artistic",
negative: "photorealistic, 3d render, smooth, digital",
},
painterly: {
prefix: "digital painting, concept art style,",
suffix: ", painterly brush strokes, artistic lighting",
negative: "flat, vector, pixel art, low quality",
},
vector: {
prefix: "flat design, vector art style,",
suffix: ", clean edges, solid colors, minimal shading",
negative: "realistic, gradient, texture, 3d, photorealistic",
},
};
// === Types ===
type Provider = "dalle" | "replicate" | "fal";
interface GenerationOptions {
provider: Provider;
prompt: string;
output: string;
model?: string;
size?: string;
style?: string;
negative?: string;
quality?: "standard" | "hd";
outputJson?: boolean;
}
interface GenerationResult {
success: boolean;
provider: Provider;
model: string;
prompt: string;
enhancedPrompt: string;
output: string;
size: string;
durationMs: number;
error?: string;
}
// === Provider: DALL-E 3 ===
async function generateDalle(options: GenerationOptions): Promise<string> {
const apiKey = Deno.env.get("OPENAI_API_KEY");
if (!apiKey) {
throw new Error("OPENAI_API_KEY environment variable is not set");
}
const model = options.model || "dall-e-3";
const size = options.size || "1024x1024";
const quality = options.quality || "standard";
// Validate size
const validSizes = ["1024x1024", "1792x1024", "1024x1792"];
if (!validSizes.includes(size)) {
throw new Error(`Invalid size for DALL-E 3. Valid sizes: ${validSizes.join(", ")}`);
}
const response = await fetch("https://api.openai.com/v1/images/generations", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
prompt: options.prompt,
n: 1,
size,
quality,
response_format: "b64_json",
}),
});
if (!response.ok) {
const error = await response.text();
if (response.status === 401) {
throw new Error("Invalid OpenAI API key");
} else if (response.status === 429) {
throw new Error("OpenAI rate limit exceeded. Please wait and try again.");
} else if (response.status === 400) {
throw new Error(`OpenAI bad request: ${error}`);
}
throw new Error(`OpenAI API error (${response.status}): ${error}`);
}
const data = await response.json();
return data.data[0].b64_json;
}
// === Provider: Replicate ===
async function generateReplicate(options: GenerationOptions): Promise<string> {
const apiToken = Deno.env.get("REPLICATE_API_TOKEN");
if (!apiToken) {
throw new Error("REPLICATE_API_TOKEN environment variable is not set");
}
const model = options.model || "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b";
// Parse size
const [width, height] = (options.size || "1024x1024").split("x").map(Number);
// Build input based on model
const input: Record<string, unknown> = {
prompt: options.prompt,
width,
height,
};
if (options.negative) {
input.negative_prompt = options.negative;
}
// Create prediction
const createResponse = await fetch("https://api.replicate.com/v1/predictions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${apiToken}`,
},
body: JSON.stringify({
version: model.includes(":") ? model.split(":")[1] : model,
input,
}),
});
if (!createResponse.ok) {
const error = await createResponse.text();
if (createResponse.status === 401) {
throw new Error("Invalid Replicate API token");
}
throw new Error(`Replicate API error (${createResponse.status}): ${error}`);
}
const prediction = await createResponse.json();
// Poll for completion
let result = prediction;
while (result.status !== "succeeded" && result.status !== "failed") {
await new Promise((resolve) => setTimeout(resolve, 1000));
const pollResponse = await fetch(result.urls.get, {
headers: {
Authorization: `Token ${apiToken}`,
},
});
if (!pollResponse.ok) {
throw new Error(`Failed to poll prediction status`);
}
result = await pollResponse.json();
}
if (result.status === "failed") {
throw new Error(`Replicate generation failed: ${result.error}`);
}
// Get the image URL and download
const imageUrl = Array.isArray(result.output) ? result.output[0] : result.output;
const imageResponse = await fetch(imageUrl);
const imageBuffer = await imageResponse.arrayBuffer();
return btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
}
// === Provider: fal.ai ===
async function generateFal(options: GenerationOptions): Promise<string> {
const apiKey = Deno.env.get("FAL_KEY");
if (!apiKey) {
throw new Error("FAL_KEY environment variable is not set");
}
const model = options.model || "fal-ai/flux/schnell";
// Parse size
const [width, height] = (options.size || "1024x1024").split("x").map(Number);
const input: Record<string, unknown> = {
prompt: options.prompt,
image_size: { width, height },
num_images: 1,
};
if (options.negative) {
input.negative_prompt = options.negative;
}
const response = await fetch(`https://fal.run/${model}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Key ${apiKey}`,
},
body: JSON.stringify(input),
});
if (!response.ok) {
const error = await response.text();
if (response.status === 401) {
throw new Error("Invalid fal.ai API key");
}
throw new Error(`fal.ai API error (${response.status}): ${error}`);
}
const data = await response.json();
// Get image URL and download
const imageUrl = data.images?.[0]?.url;
if (!imageUrl) {
throw new Error("No image URL in fal.ai response");
}
const imageResponse = await fetch(imageUrl);
const imageBuffer = await imageResponse.arrayBuffer();
return btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
}
// === Core Generation Function ===
export async function generateImage(options: GenerationOptions): Promise<GenerationResult> {
const startTime = Date.now();
// Apply style preset
let enhancedPrompt = options.prompt;
let negativePrompt = options.negative;
if (options.style && STYLE_PRESETS[options.style]) {
const preset = STYLE_PRESETS[options.style];
enhancedPrompt = `${preset.prefix} ${options.prompt}${preset.suffix}`;
if (!negativePrompt && preset.negative) {
negativePrompt = preset.negative;
}
}
const enhancedOptions = { ...options, prompt: enhancedPrompt, negative: negativePrompt };
try {
let base64Image: string;
switch (options.provider) {
case "dalle":
base64Image = await generateDalle(enhancedOptions);
break;
case "replicate":
base64Image = await generateReplicate(enhancedOptions);
break;
case "fal":
base64Image = await generateFal(enhancedOptions);
break;
default:
throw new Error(`Unknown provider: ${options.provider}`);
}
// Save image
const imageBytes = Uint8Array.from(atob(base64Image), (c) => c.charCodeAt(0));
await Deno.writeFile(options.output, imageBytes);
return {
success: true,
provider: options.provider,
model: options.model || getDefaultModel(options.provider),
prompt: options.prompt,
enhancedPrompt,
output: options.output,
size: options.size || "1024x1024",
durationMs: Date.now() - startTime,
};
} catch (error) {
return {
success: false,
provider: options.provider,
model: options.model || getDefaultModel(options.provider),
prompt: options.prompt,
enhancedPrompt,
output: options.output,
size: options.size || "1024x1024",
durationMs: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error),
};
}
}
function getDefaultModel(provider: Provider): string {
switch (provider) {
case "dalle":
return "dall-e-3";
case "replicate":
return "stability-ai/sdxl";
case "fal":
return "fal-ai/flux/schnell";
}
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Generate images using AI APIs
Usage:
deno run --allow-env --allow-net --allow-write scripts/generate-image.ts [options]
Required Options:
--provider <name> Provider: dalle, replicate, or fal
--prompt <text> Generation prompt
--output <path> Output file path (.png)
Optional:
--model <name> Specific model (provider-dependent)
--size <WxH> Image size (default: 1024x1024)
--style <name> Style preset: pixel-art, hand-drawn, painterly, vector
--negative <text> Negative prompt (Replicate/fal only)
--quality <level> Quality: standard, hd (DALL-E only)
--json Output result as JSON
-h, --help Show this help
Environment Variables:
OPENAI_API_KEY Required for DALL-E 3
REPLICATE_API_TOKEN Required for Replicate
FAL_KEY Required for fal.ai
Style Presets:
pixel-art 16-bit pixel art with clean pixels
hand-drawn Illustration style with ink lines
painterly Digital painting, concept art
vector Flat design with solid colors
Examples:
# Basic DALL-E generation
./scripts/generate-image.ts --provider dalle \\
--prompt "pixel art knight" --output ./knight.png
# Pixel art with style preset
./scripts/generate-image.ts --provider replicate \\
--prompt "knight character, front view" \\
--style pixel-art --output ./knight.png
# HD quality with specific size
./scripts/generate-image.ts --provider dalle \\
--prompt "game background forest" \\
--size 1792x1024 --quality hd --output ./forest.png
# Replicate with negative prompt
./scripts/generate-image.ts --provider replicate \\
--prompt "pixel art sword" \\
--negative "blurry, realistic" --output ./sword.png
`);
}
// === Argument Parsing ===
function parseArgs(args: string[]): GenerationOptions | null {
const options: Partial<GenerationOptions> = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
return null;
} else if (arg === "--provider" && args[i + 1]) {
options.provider = args[++i] as Provider;
} else if (arg === "--prompt" && args[i + 1]) {
options.prompt = args[++i];
} else if (arg === "--output" && args[i + 1]) {
options.output = args[++i];
} else if (arg === "--model" && args[i + 1]) {
options.model = args[++i];
} else if (arg === "--size" && args[i + 1]) {
options.size = args[++i];
} else if (arg === "--style" && args[i + 1]) {
options.style = args[++i];
} else if (arg === "--negative" && args[i + 1]) {
options.negative = args[++i];
} else if (arg === "--quality" && args[i + 1]) {
options.quality = args[++i] as "standard" | "hd";
} else if (arg === "--json") {
options.outputJson = true;
}
}
return options as GenerationOptions;
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printHelp();
Deno.exit(0);
}
const options = parseArgs(args);
if (!options) {
printHelp();
Deno.exit(0);
}
// Validate required options
if (!options.provider) {
console.error("Error: --provider is required (dalle, replicate, or fal)");
Deno.exit(1);
}
if (!options.prompt) {
console.error("Error: --prompt is required");
Deno.exit(1);
}
if (!options.output) {
console.error("Error: --output is required");
Deno.exit(1);
}
// Validate provider
if (!["dalle", "replicate", "fal"].includes(options.provider)) {
console.error(`Error: Invalid provider '${options.provider}'. Use: dalle, replicate, or fal`);
Deno.exit(1);
}
// Validate style
if (options.style && !STYLE_PRESETS[options.style]) {
console.error(`Error: Invalid style '${options.style}'. Use: ${Object.keys(STYLE_PRESETS).join(", ")}`);
Deno.exit(1);
}
if (!options.outputJson) {
console.log(`\nGenerating image with ${options.provider}...`);
console.log(`Prompt: ${options.prompt}`);
if (options.style) {
console.log(`Style: ${options.style}`);
}
}
const result = await generateImage(options);
if (options.outputJson) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.success) {
console.log(`\nSuccess! Image saved to: ${result.output}`);
console.log(`Duration: ${result.durationMs}ms`);
if (options.style) {
console.log(`Enhanced prompt: ${result.enhancedPrompt}`);
}
} else {
console.error(`\nError: ${result.error}`);
Deno.exit(1);
}
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* Godot Import File Generator CLI
*
* Generate Godot .import files with optimal settings for different asset types.
*
* Usage:
* deno run --allow-read --allow-write scripts/generate-import-files.ts \
* --input ./sprites/player.png --preset pixel-art
*
* Permissions:
* --allow-read: Read image files for dimension detection
* --allow-write: Write .import files
*/
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-import-files";
// === Import Presets ===
const IMPORT_PRESETS: Record<string, Record<string, unknown>> = {
"pixel-art": {
"dest_files": [],
"generator_parameters": {},
"importer": "texture",
"type": "CompressedTexture2D",
"uid": "",
"params": {
"compress/channel_pack": 0,
"compress/hdr_compression": 1,
"compress/high_quality": false,
"compress/lossy_quality": 0.7,
"compress/mode": 0, // Lossless
"compress/normal_map": 0,
"detect_3d/compress_to": 0,
"editor/convert_colors_with_editor_theme": false,
"editor/scale_with_editor_scale": false,
"mipmaps/generate": false,
"mipmaps/limit": -1,
"process/fix_alpha_border": true,
"process/hdr_as_srgb": false,
"process/hdr_clamp_exposure": false,
"process/normal_map_invert_y": false,
"process/premult_alpha": false,
"process/size_limit": 0,
"roughness/mode": 0,
"roughness/src_normal": "",
},
},
"hd-sprite": {
"dest_files": [],
"generator_parameters": {},
"importer": "texture",
"type": "CompressedTexture2D",
"uid": "",
"params": {
"compress/channel_pack": 0,
"compress/hdr_compression": 1,
"compress/high_quality": true,
"compress/lossy_quality": 0.7,
"compress/mode": 2, // VRAM Compressed
"compress/normal_map": 0,
"detect_3d/compress_to": 0,
"editor/convert_colors_with_editor_theme": false,
"editor/scale_with_editor_scale": false,
"mipmaps/generate": true,
"mipmaps/limit": -1,
"process/fix_alpha_border": true,
"process/hdr_as_srgb": false,
"process/hdr_clamp_exposure": false,
"process/normal_map_invert_y": false,
"process/premult_alpha": false,
"process/size_limit": 0,
"roughness/mode": 0,
"roughness/src_normal": "",
},
},
ui: {
"dest_files": [],
"generator_parameters": {},
"importer": "texture",
"type": "CompressedTexture2D",
"uid": "",
"params": {
"compress/channel_pack": 0,
"compress/hdr_compression": 1,
"compress/high_quality": false,
"compress/lossy_quality": 0.7,
"compress/mode": 0, // Lossless
"compress/normal_map": 0,
"detect_3d/compress_to": 0,
"editor/convert_colors_with_editor_theme": false,
"editor/scale_with_editor_scale": false,
"mipmaps/generate": false,
"mipmaps/limit": -1,
"process/fix_alpha_border": true,
"process/hdr_as_srgb": false,
"process/hdr_clamp_exposure": false,
"process/normal_map_invert_y": false,
"process/premult_alpha": false,
"process/size_limit": 0,
"roughness/mode": 0,
"roughness/src_normal": "",
},
},
};
// === Types ===
interface ImportOptions {
input: string;
preset?: string;
frames?: number;
columns?: number;
fps?: number;
}
interface ImportResult {
success: boolean;
input: string;
importFile: string;
preset: string;
error?: string;
}
// === Generate UID ===
function generateUID(): string {
// Generate a Godot-style UID
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
let uid = "uid://";
for (let i = 0; i < 13; i++) {
uid += chars[Math.floor(Math.random() * chars.length)];
}
return uid;
}
// === Serialize Godot Config ===
function serializeGodotConfig(data: Record<string, unknown>, indent = 0): string {
const lines: string[] = [];
const prefix = " ".repeat(indent);
for (const [key, value] of Object.entries(data)) {
if (value === null || value === undefined) {
continue;
}
if (typeof value === "object" && !Array.isArray(value)) {
lines.push(`${prefix}${key}={`);
lines.push(serializeGodotConfig(value as Record<string, unknown>, indent + 1));
lines.push(`${prefix}}`);
} else if (Array.isArray(value)) {
if (value.length === 0) {
lines.push(`${prefix}${key}=[]`);
} else {
lines.push(`${prefix}${key}=[${value.map((v) => JSON.stringify(v)).join(", ")}]`);
}
} else if (typeof value === "string") {
lines.push(`${prefix}${key}="${value}"`);
} else if (typeof value === "boolean") {
lines.push(`${prefix}${key}=${value}`);
} else {
lines.push(`${prefix}${key}=${value}`);
}
}
return lines.join("\n");
}
// === Generate Import File Content ===
function generateImportContent(
sourcePath: string,
preset: Record<string, unknown>
): string {
const fileName = sourcePath.split("/").pop() ?? sourcePath;
const uid = generateUID();
const lines: string[] = [
"[remap]",
"",
`importer="${preset.importer}"`,
`type="${preset.type}"`,
`uid="${uid}"`,
`path="res://.godot/imported/${fileName}-${uid.replace("uid://", "")}.ctex"`,
"",
"[deps]",
"",
`source_file="res://${sourcePath}"`,
`dest_files=["res://.godot/imported/${fileName}-${uid.replace("uid://", "")}.ctex"]`,
"",
"[params]",
"",
];
// Add parameters
const params = preset.params as Record<string, unknown>;
for (const [key, value] of Object.entries(params)) {
if (typeof value === "string") {
lines.push(`${key}="${value}"`);
} else if (typeof value === "boolean") {
lines.push(`${key}=${value}`);
} else {
lines.push(`${key}=${value}`);
}
}
return lines.join("\n");
}
// === Core Generation ===
export async function generateImportFile(options: ImportOptions): Promise<ImportResult> {
const preset = options.preset ?? "pixel-art";
const presetConfig = IMPORT_PRESETS[preset];
if (!presetConfig) {
return {
success: false,
input: options.input,
importFile: "",
preset,
error: `Unknown preset: ${preset}. Available: ${Object.keys(IMPORT_PRESETS).join(", ")}`,
};
}
try {
// Check if input file exists
await Deno.stat(options.input);
const importPath = `${options.input}.import`;
const content = generateImportContent(options.input, presetConfig);
await Deno.writeTextFile(importPath, content);
return {
success: true,
input: options.input,
importFile: importPath,
preset,
};
} catch (error) {
return {
success: false,
input: options.input,
importFile: "",
preset,
error: error instanceof Error ? error.message : String(error),
};
}
}
// === Batch Generation ===
async function generateForDirectory(
dir: string,
preset: string
): Promise<ImportResult[]> {
const results: ImportResult[] = [];
for await (const entry of Deno.readDir(dir)) {
if (entry.isFile && entry.name.endsWith(".png")) {
const result = await generateImportFile({
input: `${dir}/${entry.name}`,
preset,
});
results.push(result);
}
}
return results;
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Generate Godot .import files
Usage:
deno run --allow-read --allow-write scripts/generate-import-files.ts [options]
Required:
--input <path> Input image file or directory
Optional:
--preset <name> Preset: pixel-art (default), hd-sprite, ui
--frames <n> Animation frame count (for sprite sheets)
--columns <n> Sprite sheet columns
--fps <n> Animation FPS (default: 12)
--json Output result as JSON
-h, --help Show this help
Presets:
pixel-art Nearest filter, lossless, no mipmaps (for pixel art)
hd-sprite Linear filter, VRAM compressed, mipmaps (for HD sprites)
ui Linear filter, lossless, no mipmaps (for UI elements)
Note:
For sprite sheet animation setup, use the metadata JSON from
pack-spritesheet.ts to configure SpriteFrames in Godot.
Examples:
# Single file with pixel art preset
./scripts/generate-import-files.ts --input ./sprites/player.png --preset pixel-art
# Directory of HD sprites
./scripts/generate-import-files.ts --input ./hd-sprites/ --preset hd-sprite
# UI elements
./scripts/generate-import-files.ts --input ./ui/icons/ --preset ui
`);
}
// === Argument Parsing ===
function parseArgs(args: string[]): (ImportOptions & { outputJson?: boolean }) | null {
const options: Partial<ImportOptions & { outputJson?: boolean }> = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
return null;
} else if (arg === "--input" && args[i + 1]) {
options.input = args[++i];
} else if (arg === "--preset" && args[i + 1]) {
options.preset = args[++i];
} else if (arg === "--frames" && args[i + 1]) {
options.frames = parseInt(args[++i], 10);
} else if (arg === "--columns" && args[i + 1]) {
options.columns = parseInt(args[++i], 10);
} else if (arg === "--fps" && args[i + 1]) {
options.fps = parseInt(args[++i], 10);
} else if (arg === "--json") {
options.outputJson = true;
}
}
return options as ImportOptions & { outputJson?: boolean };
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printHelp();
Deno.exit(0);
}
const options = parseArgs(args);
if (!options) {
printHelp();
Deno.exit(0);
}
if (!options.input) {
console.error("Error: --input is required");
Deno.exit(1);
}
const outputJson = options.outputJson;
const preset = options.preset ?? "pixel-art";
// Check if input is directory or file
let results: ImportResult[];
try {
const stat = await Deno.stat(options.input);
if (stat.isDirectory) {
if (!outputJson) {
console.log(`\nGenerating .import files for directory: ${options.input}`);
console.log(`Preset: ${preset}`);
}
results = await generateForDirectory(options.input, preset);
} else {
if (!outputJson) {
console.log(`\nGenerating .import file for: ${options.input}`);
console.log(`Preset: ${preset}`);
}
const result = await generateImportFile({ ...options, preset });
results = [result];
}
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : error}`);
Deno.exit(1);
}
if (outputJson) {
console.log(JSON.stringify(results, null, 2));
} else {
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);
console.log(`\nGenerated ${successful.length} import file(s)`);
if (failed.length > 0) {
console.log(`\nFailed (${failed.length}):`);
for (const r of failed) {
console.log(` - ${r.input}: ${r.error}`);
}
}
for (const r of successful) {
console.log(` ✓ ${r.importFile}`);
}
}
if (results.some((r) => !r.success)) {
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* Sprite Sheet Packer CLI
*
* Pack multiple sprite images into a single sprite sheet with metadata.
*
* Usage:
* deno run --allow-read --allow-write scripts/pack-spritesheet.ts \
* --input "./sprites/*.png" --output ./sheet.png --columns 4
*
* Note: Requires ImageMagick for actual image composition.
* Without ImageMagick, generates metadata and montage command.
*
* Permissions:
* --allow-read: Read input images
* --allow-write: Write sprite sheet and metadata
*/
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "pack-spritesheet";
// === Types ===
interface PackOptions {
inputPattern: string;
output: string;
columns?: number;
padding?: number;
powerOfTwo?: boolean;
metadata?: string;
}
interface FrameInfo {
name: string;
x: number;
y: number;
width: number;
height: number;
}
interface SheetMetadata {
image: string;
size: { width: number; height: number };
frameSize: { width: number; height: number };
columns: number;
rows: number;
padding: number;
frames: FrameInfo[];
}
interface PackResult {
success: boolean;
output: string;
metadata?: SheetMetadata;
error?: string;
}
// === PNG Utilities ===
function readPngDimensions(data: Uint8Array): { width: number; height: number } | null {
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
for (let i = 0; i < 8; i++) {
if (data[i] !== signature[i]) {
return null;
}
}
const width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19];
const height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];
return { width, height };
}
// === File Globbing ===
async function expandGlob(pattern: string): Promise<string[]> {
const files: string[] = [];
// Simple glob expansion for *.png patterns
if (pattern.includes("*")) {
const dir = pattern.substring(0, pattern.lastIndexOf("/")) || ".";
const filePattern = pattern.substring(pattern.lastIndexOf("/") + 1);
const regex = new RegExp("^" + filePattern.replace(/\*/g, ".*") + "$");
for await (const entry of Deno.readDir(dir)) {
if (entry.isFile && regex.test(entry.name)) {
files.push(`${dir}/${entry.name}`);
}
}
} else {
// Single file
files.push(pattern);
}
return files.sort();
}
// === Power of Two ===
function nextPowerOfTwo(n: number): number {
let p = 1;
while (p < n) {
p *= 2;
}
return p;
}
// === Core Packing ===
export async function packSpritesheet(options: PackOptions): Promise<PackResult> {
try {
// Expand glob pattern
const files = await expandGlob(options.inputPattern);
if (files.length === 0) {
throw new Error(`No files found matching: ${options.inputPattern}`);
}
console.log(`Found ${files.length} sprites to pack`);
// Read dimensions from first sprite (assume all same size)
const firstData = await Deno.readFile(files[0]);
const frameDimensions = readPngDimensions(firstData);
if (!frameDimensions) {
throw new Error(`Unable to read dimensions from: ${files[0]}`);
}
const frameWidth = frameDimensions.width;
const frameHeight = frameDimensions.height;
const padding = options.padding ?? 0;
// Calculate grid layout
const columns = options.columns ?? Math.ceil(Math.sqrt(files.length));
const rows = Math.ceil(files.length / columns);
// Calculate sheet dimensions
let sheetWidth = columns * (frameWidth + padding) + padding;
let sheetHeight = rows * (frameHeight + padding) + padding;
if (options.powerOfTwo) {
sheetWidth = nextPowerOfTwo(sheetWidth);
sheetHeight = nextPowerOfTwo(sheetHeight);
}
// Build frame metadata
const frames: FrameInfo[] = [];
for (let i = 0; i < files.length; i++) {
const col = i % columns;
const row = Math.floor(i / columns);
const name = files[i].split("/").pop()?.replace(/\.png$/i, "") ?? `frame_${i}`;
frames.push({
name,
x: padding + col * (frameWidth + padding),
y: padding + row * (frameHeight + padding),
width: frameWidth,
height: frameHeight,
});
}
const metadata: SheetMetadata = {
image: options.output.split("/").pop() ?? options.output,
size: { width: sheetWidth, height: sheetHeight },
frameSize: { width: frameWidth, height: frameHeight },
columns,
rows,
padding,
frames,
};
// Try to create sprite sheet with ImageMagick
const montageArgs = [
"-background",
"transparent",
"-geometry",
`${frameWidth}x${frameHeight}+${padding}+${padding}`,
"-tile",
`${columns}x`,
...files,
options.output,
];
try {
const process = new Deno.Command("magick", {
args: ["montage", ...montageArgs],
stdout: "piped",
stderr: "piped",
});
const { code, stderr } = await process.output();
if (code !== 0) {
const errorText = new TextDecoder().decode(stderr);
throw new Error(`ImageMagick montage failed: ${errorText}`);
}
console.log(`Created sprite sheet: ${options.output}`);
} catch (magickError) {
console.warn("\nWarning: ImageMagick not found. Manual creation required.");
console.warn("\nRun this command to create the sprite sheet:");
console.warn(` magick montage ${montageArgs.join(" ")}`);
console.warn("\nMetadata file will still be generated.");
}
// Save metadata
if (options.metadata) {
await Deno.writeTextFile(options.metadata, JSON.stringify(metadata, null, 2));
console.log(`Metadata saved: ${options.metadata}`);
}
return {
success: true,
output: options.output,
metadata,
};
} catch (error) {
return {
success: false,
output: options.output,
error: error instanceof Error ? error.message : String(error),
};
}
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Pack sprites into a sprite sheet
Usage:
deno run --allow-read --allow-write scripts/pack-spritesheet.ts [options]
Required:
--input <pattern> Input files (glob pattern, e.g., "./sprites/*.png")
--output <path> Output sprite sheet path
Optional:
--columns <n> Number of columns (default: auto square)
--padding <n> Padding between sprites in pixels (default: 0)
--power-of-two Force power-of-two dimensions
--metadata <path> Output JSON metadata path
--json Output result as JSON
-h, --help Show this help
Note:
Requires ImageMagick for sprite sheet creation.
Metadata is always generated even without ImageMagick.
Output Metadata Format:
{
"image": "sheet.png",
"size": { "width": 256, "height": 128 },
"frameSize": { "width": 64, "height": 64 },
"columns": 4,
"rows": 2,
"frames": [
{ "name": "idle", "x": 0, "y": 0, "width": 64, "height": 64 },
...
]
}
Examples:
# Basic packing
./scripts/pack-spritesheet.ts --input "./sprites/*.png" --output ./sheet.png
# With specific columns and metadata
./scripts/pack-spritesheet.ts --input "./walk-*.png" --output ./walk.png \\
--columns 4 --metadata ./walk.json
# Power of two for GPU optimization
./scripts/pack-spritesheet.ts --input "./sprites/*.png" --output ./sheet.png \\
--power-of-two --padding 2
`);
}
// === Argument Parsing ===
function parseArgs(args: string[]): (PackOptions & { outputJson?: boolean }) | null {
const options: Partial<PackOptions & { outputJson?: boolean }> = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
return null;
} else if (arg === "--input" && args[i + 1]) {
options.inputPattern = args[++i];
} else if (arg === "--output" && args[i + 1]) {
options.output = args[++i];
} else if (arg === "--columns" && args[i + 1]) {
options.columns = parseInt(args[++i], 10);
} else if (arg === "--padding" && args[i + 1]) {
options.padding = parseInt(args[++i], 10);
} else if (arg === "--power-of-two") {
options.powerOfTwo = true;
} else if (arg === "--metadata" && args[i + 1]) {
options.metadata = args[++i];
} else if (arg === "--json") {
options.outputJson = true;
}
}
return options as PackOptions & { outputJson?: boolean };
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printHelp();
Deno.exit(0);
}
const options = parseArgs(args);
if (!options) {
printHelp();
Deno.exit(0);
}
if (!options.inputPattern) {
console.error("Error: --input is required");
Deno.exit(1);
}
if (!options.output) {
console.error("Error: --output is required");
Deno.exit(1);
}
const outputJson = options.outputJson;
delete options.outputJson;
if (!outputJson) {
console.log(`\nPacking sprites: ${options.inputPattern}`);
}
const result = await packSpritesheet(options);
if (outputJson) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.success && result.metadata) {
console.log(`\nSprite sheet: ${result.output}`);
console.log(`Size: ${result.metadata.size.width}x${result.metadata.size.height}`);
console.log(`Grid: ${result.metadata.columns}x${result.metadata.rows}`);
console.log(`Frames: ${result.metadata.frames.length}`);
} else if (!result.success) {
console.error(`Error: ${result.error}`);
Deno.exit(1);
}
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* Sprite Processing CLI
*
* Post-process AI-generated images for game use: background removal,
* resizing, trimming, and color key transparency.
*
* Usage:
* deno run --allow-read --allow-write scripts/process-sprite.ts \
* --input ./raw.png --output ./processed.png --remove-bg --resize 64x64
*
* Note: For advanced processing (background removal, color correction),
* ImageMagick is recommended. This script provides basic PNG manipulation.
*
* Permissions:
* --allow-read: Read input images
* --allow-write: Write processed images
*/
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "process-sprite";
// === Types ===
interface ProcessOptions {
input: string;
output: string;
removeBg?: boolean;
resize?: string;
filter?: "nearest" | "linear";
trim?: boolean;
padding?: number;
colorKey?: string;
}
interface ProcessResult {
success: boolean;
input: string;
output: string;
originalSize?: { width: number; height: number };
finalSize?: { width: number; height: number };
operations: string[];
error?: string;
}
// === PNG Utilities ===
// Basic PNG reading - extracts dimensions from header
function readPngDimensions(data: Uint8Array): { width: number; height: number } | null {
// PNG signature check
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
for (let i = 0; i < 8; i++) {
if (data[i] !== signature[i]) {
return null;
}
}
// IHDR chunk starts at byte 8
// Length (4 bytes) + Type "IHDR" (4 bytes) + Width (4 bytes) + Height (4 bytes)
const width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19];
const height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];
return { width, height };
}
// === Core Processing ===
export async function processSprite(options: ProcessOptions): Promise<ProcessResult> {
const operations: string[] = [];
try {
// Read input file
const inputData = await Deno.readFile(options.input);
const dimensions = readPngDimensions(inputData);
if (!dimensions) {
throw new Error("Invalid PNG file or unable to read dimensions");
}
// For advanced processing, we'll generate ImageMagick commands
// and execute them if available, or provide instructions
const magickCommands: string[] = [];
let currentInput = options.input;
// Build ImageMagick command chain
if (options.colorKey) {
// Make specific color transparent
magickCommands.push(`-transparent "#${options.colorKey}"`);
operations.push(`color-key: #${options.colorKey}`);
}
if (options.removeBg) {
// Attempt to remove background (works best with solid backgrounds)
magickCommands.push(`-fuzz 10% -transparent white`);
operations.push("remove-bg");
}
if (options.trim) {
magickCommands.push(`-trim +repage`);
operations.push("trim");
}
if (options.resize) {
const [width, height] = options.resize.split("x").map(Number);
const filter = options.filter === "linear" ? "Triangle" : "Point";
magickCommands.push(`-filter ${filter} -resize ${width}x${height}!`);
operations.push(`resize: ${options.resize} (${options.filter || "nearest"})`);
}
if (options.padding && options.padding > 0) {
const pad = options.padding;
magickCommands.push(`-bordercolor transparent -border ${pad}`);
operations.push(`padding: ${pad}px`);
}
// If no ImageMagick commands needed, just copy
if (magickCommands.length === 0) {
await Deno.copyFile(options.input, options.output);
operations.push("copy (no processing)");
return {
success: true,
input: options.input,
output: options.output,
originalSize: dimensions,
finalSize: dimensions,
operations,
};
}
// Try to run ImageMagick
const magickCmd = `magick "${currentInput}" ${magickCommands.join(" ")} "${options.output}"`;
try {
const process = new Deno.Command("magick", {
args: [currentInput, ...magickCommands.flatMap((c) => c.split(" ")), options.output],
stdout: "piped",
stderr: "piped",
});
const { code, stderr } = await process.output();
if (code !== 0) {
const errorText = new TextDecoder().decode(stderr);
throw new Error(`ImageMagick failed: ${errorText}`);
}
// Read output dimensions
const outputData = await Deno.readFile(options.output);
const outputDimensions = readPngDimensions(outputData);
return {
success: true,
input: options.input,
output: options.output,
originalSize: dimensions,
finalSize: outputDimensions || dimensions,
operations,
};
} catch (magickError) {
// ImageMagick not available - provide manual instructions
console.warn("\nWarning: ImageMagick not found. Manual processing required.");
console.warn("\nRun this command manually:");
console.warn(` ${magickCmd}`);
console.warn("\nOr install ImageMagick:");
console.warn(" - macOS: brew install imagemagick");
console.warn(" - Ubuntu: sudo apt install imagemagick");
console.warn(" - Windows: https://imagemagick.org/script/download.php");
// Copy file as fallback
await Deno.copyFile(options.input, options.output);
operations.push("copy (ImageMagick unavailable)");
return {
success: true,
input: options.input,
output: options.output,
originalSize: dimensions,
finalSize: dimensions,
operations,
error: "ImageMagick not available - file copied without processing",
};
}
} catch (error) {
return {
success: false,
input: options.input,
output: options.output,
operations,
error: error instanceof Error ? error.message : String(error),
};
}
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Post-process sprites for game use
Usage:
deno run --allow-read --allow-write scripts/process-sprite.ts [options]
Required:
--input <path> Input image path
--output <path> Output image path
Processing Options:
--remove-bg Remove white/light background (make transparent)
--resize <WxH> Resize to exact dimensions (e.g., 64x64)
--filter <type> Resize filter: nearest (default) or linear
--trim Trim transparent/white borders
--padding <n> Add transparent padding (pixels)
--color-key <hex> Make specific color transparent (e.g., ff00ff)
Other:
--json Output result as JSON
-h, --help Show this help
Note:
Advanced processing requires ImageMagick installed.
Basic operations work without external dependencies.
Examples:
# Remove background and resize for pixel art
./scripts/process-sprite.ts --input raw.png --output sprite.png \\
--remove-bg --resize 64x64 --filter nearest
# Trim whitespace and add padding
./scripts/process-sprite.ts --input raw.png --output sprite.png \\
--trim --padding 2
# Make magenta transparent (color key)
./scripts/process-sprite.ts --input raw.png --output sprite.png \\
--color-key ff00ff
`);
}
// === Argument Parsing ===
function parseArgs(args: string[]): (ProcessOptions & { outputJson?: boolean }) | null {
const options: Partial<ProcessOptions & { outputJson?: boolean }> = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
return null;
} else if (arg === "--input" && args[i + 1]) {
options.input = args[++i];
} else if (arg === "--output" && args[i + 1]) {
options.output = args[++i];
} else if (arg === "--remove-bg") {
options.removeBg = true;
} else if (arg === "--resize" && args[i + 1]) {
options.resize = args[++i];
} else if (arg === "--filter" && args[i + 1]) {
options.filter = args[++i] as "nearest" | "linear";
} else if (arg === "--trim") {
options.trim = true;
} else if (arg === "--padding" && args[i + 1]) {
options.padding = parseInt(args[++i], 10);
} else if (arg === "--color-key" && args[i + 1]) {
options.colorKey = args[++i].replace("#", "");
} else if (arg === "--json") {
options.outputJson = true;
}
}
return options as ProcessOptions & { outputJson?: boolean };
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printHelp();
Deno.exit(0);
}
const options = parseArgs(args);
if (!options) {
printHelp();
Deno.exit(0);
}
if (!options.input) {
console.error("Error: --input is required");
Deno.exit(1);
}
if (!options.output) {
console.error("Error: --output is required");
Deno.exit(1);
}
const outputJson = options.outputJson;
delete options.outputJson;
if (!outputJson) {
console.log(`\nProcessing: ${options.input}`);
}
const result = await processSprite(options);
if (outputJson) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.success) {
console.log(`Output: ${result.output}`);
if (result.originalSize && result.finalSize) {
console.log(
`Size: ${result.originalSize.width}x${result.originalSize.height} -> ${result.finalSize.width}x${result.finalSize.height}`
);
}
console.log(`Operations: ${result.operations.join(", ")}`);
if (result.error) {
console.warn(`Warning: ${result.error}`);
}
} else {
console.error(`Error: ${result.error}`);
Deno.exit(1);
}
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
Related skills
How it compares
Pick godot-asset-generator over generic image-generation skills when outputs must include Godot 4.x sprite sheets, tilesets, and import configuration.
FAQ
Which image APIs does godot-asset-generator support?
godot-asset-generator supports DALL-E, Replicate, and fal.ai for AI image generation. Developers supply the matching API key such as OPENAI_API_KEY or REPLICATE_API_TOKEN, and the skill prepares resulting images for Godot 4.x import.
What Godot version does godot-asset-generator target?
godot-asset-generator targets Godot 4.x import workflows. It generates sprites, tilesets, UI icons, and animation frames plus import configuration so textures and sprite sheets load correctly in Godot projects.
What runtime does godot-asset-generator require?
godot-asset-generator requires the Deno runtime plus API keys for the chosen image provider. Without Deno or valid credentials, the skill cannot execute its generation and Godot import preparation scripts.
Is Godot Asset Generator safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.