初始发布: 21 个 skills (Claude Code / Codex / DSH)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: meshy-3d-gen
|
||||
description: 调用 Meshy AI API 从文本生成 3D 模型(text-to-3d:preview→refine→下载 GLB)。当用户要求生成/优化 3D 模型、替换 glb 资产、用 AI 建模时调用。
|
||||
---
|
||||
|
||||
# Meshy 3D 模型生成
|
||||
|
||||
调用 Meshy AI OpenAPI v2,通过文本生成 lowpoly 3D 模型,输出 `.glb` 文件。
|
||||
|
||||
## API Key
|
||||
|
||||
优先读环境变量 `MESHY_API_KEY`,否则读 `~/.config/meshy_api_key`。绝不硬编码到代码或写入 git。
|
||||
|
||||
## 脚本
|
||||
|
||||
`scripts/meshy_gen.py` 封装完整流程(Python 3 标准库,无额外依赖):
|
||||
|
||||
```bash
|
||||
python3 ~/.codex/skills/meshy-3d-gen/scripts/meshy_gen.py "<prompt>" "<输出.glb>" "[texture_prompt]"
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
python3 ~/.codex/skills/meshy-3d-gen/scripts/meshy_gen.py \
|
||||
"stylized low-poly Japanese castle, white walls, dark tiled roof, gold trim, game asset, fantasy" \
|
||||
"art/castle.glb" \
|
||||
"flat colors, stylized texture, white plaster, dark roof tiles, gold trim, no photorealism"
|
||||
```
|
||||
|
||||
脚本自动执行:创建 preview 任务 → 轮询(每 5s)→ 创建 refine 任务(PBR 贴图、移除烘焙光照)→ 轮询 → 下载 GLB。
|
||||
|
||||
## 计费
|
||||
|
||||
preview 20 credits + refine 10 credits = **30 credits/次**。重试也照扣,所以 prompt 要一次写准;先用免费/轻量方式确认方向再跑。
|
||||
|
||||
## 风格规范(本项目)
|
||||
|
||||
生成 Taiko5 资产时必须:
|
||||
|
||||
- `model_type: lowpoly` 保持风格统一(脚本已固定)
|
||||
- prompt 包含关键词:`stylized, low-poly, game asset`
|
||||
- 禁用 `realistic, photorealistic`
|
||||
- 贴图提示强调:`flat colors, stylized texture, no photorealism`
|
||||
|
||||
## 详细提示词规范(必读)
|
||||
|
||||
Meshy 对提示词细节非常敏感,通用描述会生成简单几何块。每次生成前必须按以下清单写足细节:
|
||||
|
||||
1. **历史原型/时代风格**:点名真实城堡或时代,例如 `Himeji-style`、`Azuchi-style`、`Sengoku period`
|
||||
2. **层数与内部结构**:明确 `X visible roof tiers` 和 `X stories inside`,防止只生成一层
|
||||
3. **屋顶细节**:`hipped-and-gabled roofs (irimoya)`、`curved eaves`、`shachihoko ridge ornaments`、`chidori-hafu gable dormers`
|
||||
4. **墙体与石基**:`white plaster walls`、`dark kawara tiled roofs`、`large sloping stone base (ishigaki)`、`red wooden pillars`
|
||||
5. **局部构件**:门窗、斗拱、栏杆、金饰、角楼、破风、瓦当
|
||||
6. **姿态与用途**:`symmetrical front`、`standing upright on flat ground`、`suitable as a map landmark`、`game asset`
|
||||
7. **负面项**:`no photorealism`、`no realistic textures`、`no extra text or watermark`
|
||||
8. **贴图提示**:单独给出 `flat colors, stylized texture, white plaster, dark roof tiles, gold trim, red accents, no photorealism`
|
||||
|
||||
推荐模板:
|
||||
|
||||
```text
|
||||
stylized low-poly Japanese castle tenshu in Himeji style, four visible roof tiers, five stories inside,
|
||||
white plaster walls, dark charcoal kawara curved roofs with gold trim, hipped-and-gabled (irimoya) roofs,
|
||||
shachihoko ridge ornaments, red wooden pillars, large sloping stone base (ishigaki), chidori-hafu gable
|
||||
dormers, small corner turrets, symmetrical front, standing upright on flat ground, game asset, fantasy,
|
||||
historical Japanese castle, suitable as a map landmark
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- Meshy 服务端只保留下载链接 3 天,生成后立即下载
|
||||
- 失败模式:401 认证失败、402 积分不足、429 频率超限
|
||||
- 生成的 GLB 可直接替换 Taiko5 场景里的占位模型节点
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Meshy 3D 模型生成"
|
||||
short_description: "用 Meshy AI 从文本生成 stylized low-poly 3D 模型并下载 GLB"
|
||||
default_prompt: "生成一个 stylized low-poly 和风城堡 3D 模型,保存为 GLB"
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Meshy text-to-3d CLI(Python 3 标准库,无需额外依赖)。
|
||||
|
||||
用法:
|
||||
MESHY_API_KEY=msy_xxx python3 meshy_gen.py "<prompt>" "<输出.glb>" [texture_prompt]
|
||||
|
||||
流程:preview -> 轮询 -> refine(PBR) -> 轮询 -> 下载 GLB。
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
API_URL = "https://api.meshy.ai/openapi/v2/text-to-3d"
|
||||
|
||||
|
||||
def load_key():
|
||||
key = os.environ.get("MESHY_API_KEY", "").strip()
|
||||
if key:
|
||||
return key
|
||||
config_path = os.path.expanduser("~/.config/meshy_api_key")
|
||||
if os.path.exists(config_path):
|
||||
key = open(config_path).read().strip()
|
||||
if key:
|
||||
return key
|
||||
sys.exit("没有 Meshy API Key。设置环境变量 MESHY_API_KEY,或写入 ~/.config/meshy_api_key")
|
||||
|
||||
|
||||
def request(method, url, body=None, key=None):
|
||||
req = urllib.request.Request(url, method=method)
|
||||
req.add_header("Authorization", "Bearer " + key)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
try:
|
||||
with urllib.request.urlopen(req, data=data) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
text = e.read().decode()
|
||||
try:
|
||||
text = json.loads(text).get("message", text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
sys.exit(f"HTTP {e.code}: {text}")
|
||||
|
||||
|
||||
def create_task(mode, prompt, key, preview_task_id=None, texture_prompt=None):
|
||||
body = {
|
||||
"mode": mode,
|
||||
"prompt": prompt,
|
||||
"model_type": "lowpoly",
|
||||
"target_formats": ["glb"],
|
||||
"pose_mode": "",
|
||||
"should_remesh": False,
|
||||
"art_style": "realistic",
|
||||
}
|
||||
if mode == "refine":
|
||||
body["preview_task_id"] = preview_task_id
|
||||
body["enable_pbr"] = True
|
||||
body["remove_lighting"] = True
|
||||
if texture_prompt:
|
||||
body["texture_prompt"] = texture_prompt
|
||||
return request("POST", API_URL, body, key)["result"]
|
||||
|
||||
|
||||
def poll_task(task_id, key):
|
||||
url = f"{API_URL}/{task_id}"
|
||||
while True:
|
||||
result = request("GET", url, None, key)
|
||||
status = result.get("status")
|
||||
if status == "SUCCEEDED":
|
||||
return result
|
||||
if status == "FAILED":
|
||||
message = result.get("task_error", {}).get("message", "unknown error")
|
||||
sys.exit(f"任务失败:{message}")
|
||||
print(f" {status} ...", flush=True)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def download(url, output):
|
||||
req = urllib.request.Request(url)
|
||||
with urllib.request.urlopen(req) as resp, open(output, "wb") as f:
|
||||
shutil.copyfileobj(resp, f)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Meshy text-to-3d 生成 GLB")
|
||||
parser.add_argument("prompt", help="模型描述,必须包含 stylized, low-poly, game asset")
|
||||
parser.add_argument("output", help="输出 .glb 路径")
|
||||
parser.add_argument("texture_prompt", nargs="?", default="", help="可选贴图提示")
|
||||
args = parser.parse_args()
|
||||
|
||||
key = load_key()
|
||||
print("创建 preview 任务 ...", flush=True)
|
||||
preview_id = create_task("preview", args.prompt, key)
|
||||
print(f" preview task: {preview_id}", flush=True)
|
||||
preview = poll_task(preview_id, key)
|
||||
print(" preview 完成", flush=True)
|
||||
|
||||
print("创建 refine 任务(PBR 贴图)...", flush=True)
|
||||
refine_id = create_task(
|
||||
"refine",
|
||||
args.prompt,
|
||||
key,
|
||||
preview_task_id=preview_id,
|
||||
texture_prompt=args.texture_prompt,
|
||||
)
|
||||
print(f" refine task: {refine_id}", flush=True)
|
||||
refine = poll_task(refine_id, key)
|
||||
print(" refine 完成", flush=True)
|
||||
|
||||
glb_url = refine.get("model_urls", {}).get("glb")
|
||||
if not glb_url:
|
||||
sys.exit("响应中没有 model_urls.glb")
|
||||
download(glb_url, args.output)
|
||||
print(f"已保存: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user