Shap-E是OpenAI在2023年开源的一个跨模态生成模型,它可以直接从文本提示词或者图片生成3D隐式函数,最终输出可导出的网格文件。相比传统的NeRF类方案,Shap-E的推理速度快得多,因为Hugging Face把它接入了diffusers生态,所以我们完全可以按照调用Stable Diffusion的习惯来使用它,代码量非常少。这篇文章会从Pipeline的基本原理讲起,逐步给出单次生成和批量生成的完整Python实现。

一、Shap-E在diffusers中的两种Pipeline
diffusers为Shap-E提供了两个核心Pipeline类:ShapEPipeline和ShapEImg2ImgPipeline。前者负责文本到3D的生成,输入是一段自然语言描述;后者负责图片到3D的转换,输入是一张RGB图像。两者底层都依赖同一个扩散模型,区别只在条件编码环节:文本走的是CLIP文本编码器,图片走的是CLIP视觉编码器。
调用Pipeline之后得到的结果并不是直接的网格文件,而所谓的3D生成,实际上是模型输出一组神经辐射场的参数。这组参数描述了空间中每个点的密度和颜色分布,diffusers里称之为shap_e_renderer渲染出的隐式表示。想要拿到可以在Blender或其他软件里使用的网格,还需要经过一个mesh提取的步骤,下面会详细讲。
先看一下最简化的调用流程,安装依赖只需要的三个包:
pip install torch diffusers transformers accelerate
二、单次生成的完整代码
下面这段代码演示了从文本生成3D模型并导出GIF动图和GLB文件的完整过程。注意guidance_scale这个参数,它控制生成结果与提示词的贴合程度,数值越高越贴合但多样性下降,一般设置在7到15之间比较合适。
import torch
from diffusers import ShapEPipeline
from diffusers.utils import export_to_gif
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
pipe = ShapEPipeline.from_pretrained(
"openai/shap-e",
torch_dtype=torch.float16,
variant="fp16",
use_safetensors=True,
).to(device)
prompt = "一把蓝色的办公椅,带扶手和靠背"
# guidance_scale越大越贴合提示词,num_inference_steps控制去噪步数
images = pipe(
prompt=prompt,
guidance_scale=15.0,
num_inference_steps=64,
frame_size=256,
).images
gif_path = export_to_gif(images, "chair.gif")
print("GIF已保存到:", gif_path)上面的代码输出的是一帧帧的旋转视图,把它们拼成GIF就能快速预览模型效果。如果需要导出GLB格式给其他3D软件使用,需要额外引入ShapERenderer来做网格提取:
import torch
from diffusers import ShapEPipeline, ShapERenderer
from diffusers.utils import export_to_ply
device = "cuda"
pipe = ShapEPipeline.from_pretrained("openai/shap-e").to(device)
renderer = ShapERenderer.from_pretrained(
"openai/shap-e", torch_dtype=torch.float16
).to(device)
prompt = "一辆红色的跑车"
# 先拿到生成结果
output = pipe(
prompt=prompt,
guidance_scale=15.0,
num_inference_steps=64,
output_type="nerf",
)
# 再从nerf输出中提取mesh
with torch.no_grad():
mesh = renderer.output_to_ply(output.images[0])
export_to_ply(mesh, "car.ply")
print("网格文件已导出")这里有个容易踩的坑:必须设置output_type="nerf",否则Pipeline返回的就是渲染好的普通图像,无法再提取网格。另外提取网格这一步本身也比较吃显存,如果显卡只有8G,建议把num_inference_steps降到32,并且全程保持torch.no_grad()上下文。
三、批量生成的工程化实现
单个生成跑通之后,批量生成的主要矛盾就变成了显存管理和任务调度。最直接的做法是循环遍历提示词列表,每次生成后立即释放中间张量。下面给出一个带错误重试和显存清理的批量脚本:
import gc
import torch
from diffusers import ShapEPipeline
from diffusers.utils import export_to_gif
device = "cuda"
pipe = ShapEPipeline.from_pretrained(
"openai/shap-e",
torch_dtype=torch.float16,
variant="fp16",
).to(device)
prompts = [
"一个绿色的玻璃杯",
"一只橘色的猫玩偶",
"一栋带尖顶的小木屋",
"一把木制吉他",
]
for i, prompt in enumerate(prompts):
try:
images = pipe(
prompt=prompt,
guidance_scale=15.0,
num_inference_steps=64,
frame_size=256,
).images
export_to_gif(images, f"output_{i}.gif")
print(f"第{i + 1}个完成: {prompt}")
except torch.cuda.OutOfMemoryError:
torch.cuda.empty_cache()
print(f"第{i + 1}个显存不足,已跳过: {prompt}")
# 每次生成后主动清理,避免显存碎片累积
del images
gc.collect()
torch.cuda.empty_cache()如果希望进一步提升吞吐量,可以把num_inference_steps设为负数来启用隐式时间步采样,这样一次前向会同时生成多个去噪轨迹,等效于批量推理。比如设置num_inference_steps=-64配合frame_size使用,生成的多帧图像本身就可以拆成多个独立视角。
批量场景下还有两个实用技巧值得分享。第一,把所有提示词和随机种子写进CSV文件统一管理,方便实验对比和结果复现;第二,如果生成结果不稳定,固定住generator=torch.Generator(device).manual_seed(seed),同一个种子配合同一段提示词可以稳定复现同一个模型,排查问题时会轻松很多。显存方面,fp16精度下Shap-E大约占用6G左右,如果机器是消费级显卡,还可以通过enable_model_cpu_offload()把部分权重卸载到内存,代价是速度变慢但能稳定跑通大批量任务。
四、常见问题与参数调优建议
实际使用中反馈最多的问题是生成结果模糊或者结构崩坏。这通常和提示词写法有关,Shap-E对简洁具体的名词性描述响应最好,比如“a red apple with a green leaf”就比一段长篇描述更容易出好结果。另外英文提示词的整体质量明显高于中文,如果对中文描述有需求,可以先经过翻译再送入Pipeline。
参数层面可以记住几条经验:guidance_scale低于10时模型自由发挥空间大,适合探索创意;高于15则容易过饱和,结构反而变差。frame_size决定预览图的分辨率,256是速度和质量的平衡点,512会更清晰但显存翻倍。num_inference_steps从64开始尝试,如果只是快速筛选提示词,32步足够做出判断。
最后提一下模型变体,除了标准的openai/shap-e,Hugging Face上还有openai/shap-e-img2img用于图生3D任务。如果你的输入是一张产品照片或手绘草图,用图生3D的Pipeline会比纯文本描述精准得多,两者的调用方式几乎一致,只是把prompt参数换成image参数即可,迁移成本基本为零。