> ## Documentation Index
> Fetch the complete documentation index at: https://docs.summerengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to make a 3D game character with AI: concept, model, rig and animations

> Make a rigged, animated 3D game character with AI: a clean concept picture, image-to-3D with a rig and Idle, Walk, Run and Jump on the same job, and a matching portrait for your dialogue UI. Exact CLI, API and MCP steps and what it costs.

A playable character needs more than a pretty mesh. It needs one clean silhouette, a neutral pose a skeleton can fit, a rig, and the handful of motions the player sees every second: standing, walking, running, jumping. This guide makes all of that in two jobs, plus a move of her own and a portrait for dialogue, and you get a GLB that drops into Summer Engine or any engine that reads glTF.

## The chain

| Step | Model | Why this model | Cost |
| - | - | - | - |
| 1. Concept picture | [GPT Image 2.5](https://docs.summerengine.com/api/models/image/gpt-image-2-5.md) `image/gpt-image-2.5` | Follows a long character brief closely and keeps the full body in frame on a plain background, which is what image-to-3D reads best. | \$0.053 |
| 2. Rigged, animated 3D model | [Hunyuan 3D Pro](https://docs.summerengine.com/api/models/3d/hunyuan-3d.md) `3d/hunyuan-3d` | The default 3D model: a textured GLB from one picture, and with rig it fits a humanoid skeleton and bakes the motions you name on the same job. | \$1.642 |
| 3. A custom move | [Hunyuan Motion](https://docs.summerengine.com/api/models/motion/hunyuan-motion.md) `motion/hunyuan-motion` | Animates the rigged character from words: the one move your game needs that no stock clip has. | \$0.08 |
| 4. Dialogue portrait | [Nano Banana 2 Lite](https://docs.summerengine.com/api/models/image/nano-banana-2.md) `image/nano-banana-2` | Edits from the concept picture, so the face, hair and colours stay the same character, at a fraction of the cost of a new drawing. | \$0.042 |
| | | **Total** | **\$1.817** |

**\$1.817 for the whole chain**, at each model's list price:

* Concept picture: \$0.053 (\$0.053 per image)
* Rigged, animated 3D model: \$1.642 (\$0.525 per model)
* A custom move: \$0.08 (\$0.08 per animation)
* Dialogue portrait: \$0.042 (\$0.042 per image)

A failed job costs nothing. Before you run anything, price each step exactly and for free: `summer api estimate <model> --input '<json>'`, or `POST /v1/estimate` with the same body as the job. Prices come from [`GET /v1/models`](https://docs.summerengine.com/api/reference/get-models) and change only when the provider's price does.

## Before you start

Sign in once, so your agent or your terminal can run jobs on your Summer account:

```bash theme={null}
npx -y summer-engine@latest login
```

Your browser opens the Summer sign-in. Pick a spend limit for this computer and choose **Allow**. Using a coding agent? Paste this guide's URL and say "make me this character"; the [quickstart for your agent](https://docs.summerengine.com/api/agents.md) connects it in one command.

## Step 1: a concept picture image-to-3D can read

Image-to-3D rebuilds what it can see. Every hidden arm, cropped boot or busy background becomes a guess in the mesh. So the concept picture is not key art: it is a reference sheet with one job.

* **One character, full body, nothing cropped.** Leave space around the figure.
* **A neutral A-pose.** Arms slightly away from the body, legs straight. The rig fits far better than to an action pose, and the arms do not fuse to the hips.
* **A plain white background, even light, no cast shadow.** Shadows read as geometry.
* **Name the materials and colours.** "Leather tool belt", "brass goggles": the texture follows the words.

```bash CLI theme={null}
npx -y summer-engine@latest api run image/gpt-image-2.5 \
  --input '{"prompt":"Clean single-character concept for image-to-3D: MIRA, a young adult sky-pirate mechanic, copper-red braided bob, brass goggles on her forehead, cropped teal flight jacket, leather tool belt, olive cargo trousers, tall brown boots. Full body, front view, neutral A-pose with the arms slightly away from the body, whole figure in frame with space around it. Pure white seamless background, soft even light, no cast shadow, no props. Stylized 3D animated feature look, clean readable shapes. No text, no logos.","aspectRatio":"3:4","title":"Mira concept"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"image/gpt-image-2.5","input":{"prompt":"Clean single-character concept for image-to-3D: MIRA, a young adult sky-pirate mechanic, copper-red braided bob, brass goggles on her forehead, cropped teal flight jacket, leather tool belt, olive cargo trousers, tall brown boots. Full body, front view, neutral A-pose with the arms slightly away from the body, whole figure in frame with space around it. Pure white seamless background, soft even light, no cast shadow, no props. Stylized 3D animated feature look, clean readable shapes. No text, no logos.","aspectRatio":"3:4","title":"Mira concept"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "image/gpt-image-2.5",
    "input": {
      "prompt": "Clean single-character concept for image-to-3D: MIRA, a young adult sky-pirate mechanic, copper-red braided bob, brass goggles on her forehead, cropped teal flight jacket, leather tool belt, olive cargo trousers, tall brown boots. Full body, front view, neutral A-pose with the arms slightly away from the body, whole figure in frame with space around it. Pure white seamless background, soft even light, no cast shadow, no props. Stylized 3D animated feature look, clean readable shapes. No text, no logos.",
      "aspectRatio": "3:4",
      "title": "Mira concept"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "image/gpt-image-2.5", "input": {"prompt": "Clean single-character concept for image-to-3D: MIRA, a young adult sky-pirate mechanic, copper-red braided bob, brass goggles on her forehead, cropped teal flight jacket, leather tool belt, olive cargo trousers, tall brown boots. Full body, front view, neutral A-pose with the arms slightly away from the body, whole figure in frame with space around it. Pure white seamless background, soft even light, no cast shadow, no props. Stylized 3D animated feature look, clean readable shapes. No text, no logos.", "aspectRatio": "3:4", "title": "Mira concept"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "image/gpt-image-2.5",
  input: {
    prompt: "Clean single-character concept for image-to-3D: MIRA, a young adult sky-pirate mechanic, copper-red braided bob, brass goggles on her forehead, cropped teal flight jacket, leather tool belt, olive cargo trousers, tall brown boots. Full body, front view, neutral A-pose with the arms slightly away from the body, whole figure in frame with space around it. Pure white seamless background, soft even light, no cast shadow, no props. Stylized 3D animated feature look, clean readable shapes. No text, no logos.",
    aspectRatio: "3:4",
    title: "Mira concept"
  },
  out: "assets",
})
```

The job comes back done in the same call. Its `result.assets[0].fileUrl` is the picture you feed to step 2. Not right yet? Change one thing at a time in the prompt and run it again: a concept picture costs \$0.053 per image, so iterate here, where it is cheap, and not on the 3D job.

## Step 2: the 3D model, rigged and animated in one job

Send the concept's `fileUrl` as `imageUrl`. Three fields turn a statue into a character:

* `assetIntent: "character"` prepares the picture as a clean, riggable humanoid reference first.
* `rig: true` fits a humanoid skeleton to the mesh.
* `animationNames` bakes motions onto that skeleton on the same job: `Idle`, `Walk`, `Run` and `Jump` cover a third-person controller.

```bash CLI theme={null}
npx -y summer-engine@latest api run 3d/hunyuan-3d \
  --input '{"kind":"image-to-3d","imageUrl":"<fileUrl of concept picture>","assetIntent":"character","poseMode":"a-pose","rig":true,"animationNames":["Idle","Walk","Run","Jump"],"title":"Mira"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"3d/hunyuan-3d","input":{"kind":"image-to-3d","imageUrl":"<fileUrl of concept picture>","assetIntent":"character","poseMode":"a-pose","rig":true,"animationNames":["Idle","Walk","Run","Jump"],"title":"Mira"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "3d/hunyuan-3d",
    "input": {
      "kind": "image-to-3d",
      "imageUrl": "<fileUrl of concept picture>",
      "assetIntent": "character",
      "poseMode": "a-pose",
      "rig": true,
      "animationNames": [
        "Idle",
        "Walk",
        "Run",
        "Jump"
      ],
      "title": "Mira"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "3d/hunyuan-3d", "input": {"kind": "image-to-3d", "imageUrl": "<fileUrl of concept picture>", "assetIntent": "character", "poseMode": "a-pose", "rig": True, "animationNames": ["Idle", "Walk", "Run", "Jump"], "title": "Mira"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "3d/hunyuan-3d",
  input: {
    kind: "image-to-3d",
    imageUrl: "<fileUrl of concept picture>",
    assetIntent: "character",
    poseMode: "a-pose",
    rig: true,
    animationNames: ["Idle", "Walk", "Run", "Jump"],
    title: "Mira"
  },
  out: "assets",
})
// It answers `next` while the job runs: follow it to the end.
summer_api_job({ jobId: "<jobId>", wait: true, out: "assets" })
```

A 3D job takes a few minutes. The CLI follows it and saves the GLB; over HTTP the call answers `running`, and you read `GET /v1/jobs/{id}` until `state` is `done` ([jobs](https://docs.summerengine.com/api/jobs.md)). The GLB holds the mesh, its textures, the skeleton and every animation clip by name.

**Other models for this step.** [Meshy 7.1](https://docs.summerengine.com/api/models/3d/meshy-7-1.md) gives a cleaner quad topology when an artist will keep editing the mesh (\$1.20 per model, rigging included at a lower add-on). [Rodin 2.5 Fast](https://docs.summerengine.com/api/models/3d/rodin-2-5-fast.md) is the cheapest way to block out a cast of background characters (\$0.10 per model). For a hero you will look at for hours, compare two models on the same concept picture before you commit: the [3D models](https://docs.summerengine.com/api/models.md#3d-models) page lists them all with examples.

## Step 3: a move of her own

Idle, Walk, Run and Jump cover moving around. A character feels like a person when she has one move nobody else has: a victory pose, a signature attack, a fidget while she waits. Describe it in one sentence and send the rigged model's `assetId` as `rigAssetId`. The clip is retargeted to her own skeleton, so it plays on the model from step 2 without any fixing.

```bash CLI theme={null}
npx -y summer-engine@latest api run motion/hunyuan-motion \
  --input '{"rigAssetId":"<assetId of rigged, animated 3d model>","prompt":"pulls a wrench from her belt, spins it once in her hand and catches it, confident","durationSeconds":4}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"motion/hunyuan-motion","input":{"rigAssetId":"<assetId of rigged, animated 3d model>","prompt":"pulls a wrench from her belt, spins it once in her hand and catches it, confident","durationSeconds":4}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "motion/hunyuan-motion",
    "input": {
      "rigAssetId": "<assetId of rigged, animated 3d model>",
      "prompt": "pulls a wrench from her belt, spins it once in her hand and catches it, confident",
      "durationSeconds": 4
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "motion/hunyuan-motion", "input": {"rigAssetId": "<assetId of rigged, animated 3d model>", "prompt": "pulls a wrench from her belt, spins it once in her hand and catches it, confident", "durationSeconds": 4}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "motion/hunyuan-motion",
  input: {
    rigAssetId: "<assetId of rigged, animated 3d model>",
    prompt: "pulls a wrench from her belt, spins it once in her hand and catches it, confident",
    durationSeconds: 4
  },
  out: "assets",
})
// It answers `next` while the job runs: follow it to the end.
summer_api_job({ jobId: "<jobId>", wait: true, out: "assets" })
```

You get the animation as GLB and FBX. Keep moves to one action of 2 to 6 seconds; a long string of actions blurs. For stock moves (sprint, attack, block, dance), [Meshy animation library](https://docs.summerengine.com/api/models/motion/library.md) adds named clips from a curated library (\$0.12 per animation), and [Hunyuan Motion Fast](https://docs.summerengine.com/api/models/motion/hunyuan-motion-fast.md) drafts a custom move for less (\$0.06 per animation).

## Step 4: a portrait that is the same person

Dialogue boxes, party menus and save slots want a face, not a full body. Edit the concept picture instead of drawing a new one: `referenceImageUrl` keeps the character on-model.

```bash CLI theme={null}
npx -y summer-engine@latest api run image/nano-banana-2 \
  --input '{"prompt":"Head-and-shoulders portrait of this same character for a dialogue box, three-quarter view, confident half smile, same hair, goggles and jacket, soft rim light, plain warm grey background.","referenceImageUrl":"<fileUrl of concept picture>","aspectRatio":"1:1","title":"Mira portrait"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"image/nano-banana-2","input":{"prompt":"Head-and-shoulders portrait of this same character for a dialogue box, three-quarter view, confident half smile, same hair, goggles and jacket, soft rim light, plain warm grey background.","referenceImageUrl":"<fileUrl of concept picture>","aspectRatio":"1:1","title":"Mira portrait"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "image/nano-banana-2",
    "input": {
      "prompt": "Head-and-shoulders portrait of this same character for a dialogue box, three-quarter view, confident half smile, same hair, goggles and jacket, soft rim light, plain warm grey background.",
      "referenceImageUrl": "<fileUrl of concept picture>",
      "aspectRatio": "1:1",
      "title": "Mira portrait"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "image/nano-banana-2", "input": {"prompt": "Head-and-shoulders portrait of this same character for a dialogue box, three-quarter view, confident half smile, same hair, goggles and jacket, soft rim light, plain warm grey background.", "referenceImageUrl": "<fileUrl of concept picture>", "aspectRatio": "1:1", "title": "Mira portrait"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "image/nano-banana-2",
  input: {
    prompt: "Head-and-shoulders portrait of this same character for a dialogue box, three-quarter view, confident half smile, same hair, goggles and jacket, soft rim light, plain warm grey background.",
    referenceImageUrl: "<fileUrl of concept picture>",
    aspectRatio: "1:1",
    title: "Mira portrait"
  },
  out: "assets",
})
```

## Put it in your game

* **Summer Engine.** Import the GLB by its `assetId`, or drop the file in your project. The `character-model` and `character-animation-wiring` skills wire the clips into an AnimationTree and a controller for you.
* **Any other engine.** The GLB is standard glTF 2.0, Y up. Scale it to about 1.7 m tall for a human: generated models are not at real-world size.
* **Keep the ids.** Every file is also in your Summer library. Note the `assetId` of the concept and the model, so the next variant starts from the same picture.

## Make the whole cast

Once one character works, the recipe is the template. Write the next prompts with the same style sentence word for word ("Stylized 3D animated feature look, clean readable shapes"), and send the concept pictures as one batch, then the 3D jobs as another:

```bash theme={null}
npx -y summer-engine@latest api run --batch cast-concepts.jsonl --download art/concepts
```

A batch is priced up front and runs all or none, so you know the cost of the whole cast before anything starts ([batches](https://docs.summerengine.com/api/jobs.md)).

## Common problems

| You see | Do this |
| - | - |
| Arms fused to the body | Ask for an A-pose with "arms slightly away from the body", and keep a white background |
| A blurry or melted face | Make the face larger in the concept: a 3:4 picture with less empty space above the head |
| Feet sink into the floor in the engine | Move the root so the feet sit on y = 0; the rig keeps its origin at the pelvis |
| Colours look different in 3D | Name them in the prompt ("teal jacket", "copper-red hair"), not only in the picture |
| Walk cycle slides on the ground | Drive the clip speed from the character's velocity in your controller |

## Questions

**Can I make a character from a drawing I already have?** Yes. Upload it first ([upload a file](https://docs.summerengine.com/api/jobs.md)), then start at step 2 with its `fileUrl`. A clean, full-body, front-view drawing works best.

**Can I use the character in a commercial game?** Yes. Every model on the Summer API allows commercial use of what it makes.

**Do I need a plan?** No. Jobs are paid from your paid Summer credits, your plan's credits or a top-up. Each connected agent has its own spend limit in Settings > Connected apps.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.