准备概览
- 目标:在 Maya 中用 Pencil+ 4 做测试静态线稿渲染(不涉骨骼动画),快速得到“外轮廓 + 结构线”的干净结果。
- 测试模型:使用《绝区零》官方发布模型(来源见下)。
- 模型格式:原始为 MMD(.pmx),为静帧演示转换为 OBJ。
- 渲染思路:以 Line Set 分类出线(外轮廓 / 对象轮廓 / 交叉 / 材质边 / 硬边等),按需给不同类线指定独立笔刷,并用遮罩与焊接确保线条干净不重影。
- 进阶:测试插件主要功能,排查风险
下载与安装
- 官网:Pencil+ 4 for Maya – Downloads – | PSOFT WEBSITE
✅ 请购买或授权试用,遵守 EULA 与版权法规。 - 神秘网站:PSOFT Pencil+ 4.2.3 For Maya 2017 – 2025 Win
⚠️ 为合规与版权安全,本文仅供个人学习
素材与格式转换(MMD→OBJ)
适合只做静态渲染预览;若要绑定/动画,建议保留骨骼信息并在 DCC 内做更规范的导入流程。
来源
- 《绝区零》官方发布的模型下载汇总:绝区零 UP主激励计划 模型 汇总下载
- 遵守使用条款 / 二创规范,避免商用侵权。
转换(PMX → OBJ)
- 在线:AnyConv
导出建议:
- 仅静帧 → OBJ 即可(无骨骼、无动画)。
导入 Maya 后的整理
Modify > Freeze Transforms、Center PivotMesh Display > Set to Face再Soften/Harden Edges(按结构设置硬/软边)- 合并或分离网格(按线条策略决定是否需要单体或多对象)
- 统一材质(可先赋一个纯白 Lambert 以便观察线)
Pencil+ 4 基础上手
名称以 Pencil+ 4 for Maya 常见面板为参考,菜单/UI 可能随版本略有差异。
加载插件
Windows > Settings/Preferences > Plug-in Manager- 勾选
Pencil+4相关插件(如 Line / Material 等)。
创建 Line Set(线集)
- 新建一个 Line Set(可按「角色」「道具」「背景」分层管理)。
- 将要出线的对象拖入或用过滤规则指定。
![[PIC1] [PIC1]](https://www.fxyam.cn/usr/uploads/2025/11/2241934709.png)
勾选出线类别(核心)
- 见下「Line Set 详解」。先只开 Outline(轮廓),确认外轮廓正确。
为不同线类指定独立笔刷
- 各类别旁的
Specific Brush Settings打勾后,会出现一个可指派笔刷资源的槽(例如PencilBrushSettings_VisibleX)。 - 将 Brush/Stroke 预设拖入,使该类线有独立的粗细 / 纹理 / 虚线 / 端点等。
- 各类别旁的
渲染预览
- 切到目标相机,设置输出分辨率。
- 渲染(需要用pencil自己的渲染器)。
- 如需前后景压线逻辑,使用多 Line Set + 遮罩(见下)。
这里有一个比较通俗易懂的视频:
Line Set 详解与推荐搭配
一些功能解释和理解
1) Outline(轮廓)
- Open Edge(开放边):把模型边界/洞口也当作轮廓线(衣服开口、眼洞等)。
- Merge Groups(合并组):同组多个物体合并计算外轮廓,内遮挡不重复出线。
- Specific Brush Settings:给外轮廓单独用更粗/更黑的笔刷。
搭配:角色主体(身体+衣物+饰品)常勾 Merge Groups,得到一圈干净大外轮廓;需要服装开口强调时再开 Open Edge。
2) Object(对象轮廓)
- Open Edge:与 Outline 同义,但按每个对象独立计算。
- Specific Brush Settings:仅覆盖对象轮廓的笔刷。
用途:同一组内的零件各保一圈外轮廓的画风(更“零件化”)。
3) Intersection(交叉线)
- Self-Intersection(自相交):自身表面互相穿插/折叠处出线(衣褶、厚面回折)。
- Specific Brush Settings:仅覆盖交叉线。
用途:强调接触/压痕(袖口压在手臂、鞋底接地)。技术插画常用。
4) Smoothing Boundary(光滑组/硬边边界)
- Specific Brush Settings
用途:在硬边/法线断开处出线。非常适合硬表面倒角线/板块转折。
5) Material ID Boundary(材质 ID 边界)
- Specific Brush Settings
用途:同网格内不同材质 ID的分界出线 → 配色块分隔、面板缝,无需额外切线。
6) Selected Edges(选定边)
- Specific Brush Settings
用途:仅对你手工选择的边出线,最适合“艺术指导式定点勾线”。
提示:用Create > Sets > Quick Select Set建组件选择集,便于复用与版本管理。
7) Normal Angle(法线夹角)
- Min / Max:只在法线夹角落在该范围的边上出线。
- Specific Brush Settings
用途:用角度阈值自动抓锐利/转折边。
建议:硬表面把 Min 提高只抓很锐的折边;想更细腻就放宽范围。
8) Wireframe(线框)
- Specific Brush Settings
用途:直接按拓扑线框出线。适合蓝图/教学/拓扑展示,或与其他线型叠加做质感。
9) 辅助管理
- Weld Edges Between Objects(跨对象焊接边):将紧贴的不同物体在边界上视作连在一起,避免接缝双线/断裂。
Mask Hidden Lines of Other Line Sets(遮罩其它线集的隐藏线):当前线集会遮蔽被其它线集物体挡住的线。
典型:人物线集压住背景线集,防止背景线穿出人物。
笔刷 / 线型预设速览(可直接套用,官方内置的)
这些是常见命名的示意与适用性总结。不同版本可能略有差异,请以你的安装包为准;你也可在此基础上做二次定制。

Brush(笔刷纹理类)
P_Brush_Bubble(泡泡):柔软圆斑,可爱/软糯风外轮廓或填充式线条。P_Brush_Circle(圆头):线宽均匀干净,卡通/技术图的基础款。P_Brush_Default(默认):通用起点,便于继续自定义。P_Brush_Flat(扁平/排刷):椭圆笔尖,转向有“压扁/起收锋”感,带一点书法味。P_Brush_Flower(花瓣):转角有装饰纹理,适合特效/装饰线。P_Brush_Rough(粗糙):颗粒断续,模拟蜡笔/干粉,有旧纸质感。P_Brush_Soft(软边):边缘羽化,适合次级结构线/弱化存在感。
Stroke(线型类)
P_Stroke_DashedLine1/2/3(虚线):不同间隔/端形;用于隐线/分区线/运动轨迹。P_Stroke_Default(默认线型):平滑连续,标准。P_Stroke_Fuzzy1/2/3/4(毛边/抖动):噪声递增;用于手绘颤动/远景弱化/粗糙材质。P_Stroke_Pen / Pen2 / Pen3(记号笔系):硬/刷/压感各异;漫画分镜/产品线稿常用。P_Stroke_Spray(喷点):粒子感强;喷涂/尘土/速度尾迹或衰减末端。P_Stroke_Wave_Large / Wave_Small(波浪线):震动/热气/水波/毛绒边等卡通化强调。
📌 在 Specific Brush Settings 的灰色槽中指派上述笔刷/线型资源,即可让该类线独立控制粗细/纹理/噪声/端头等。fuzzy真的很像手绘,可以做很多有意思的效果
渲染输出与合成建议
分层管理:
- 人物(粗外轮廓)、人物(结构线)、道具 / 背景 分成多个 Line Set,利于遮罩与独立调线。
- 人物线集放在最上层,勾选 Mask Hidden Lines of Other Line Sets。
厚薄与缩放:
线宽通常与场景尺度/相机距离相关;建立相机-线宽标尺:
- 近景外轮廓:2.0~4.0(示意)
- 中景外轮廓:1.2~2.2
- 远景外轮廓:0.6~1.2(或用 Fuzzy 线弱化)
输出与合成:
- 建议输出带透明通道(如 PNG/TIF)便于后期与底色/纹理合成。
- 可分多通道:外轮廓(粗)、结构线(细)、隐线/虚线;在 PS/AE/Nuke 中按线型分组做二次调色。
常见问题排错清单
接缝处双线/破碎
- 勾选 Weld Edges Between Objects;检查是否存在重叠面/重合壳。
开口/洞口没出线
- 在 Outline/Object 内勾 Open Edge。
结构线过多、画面脏
- 先只开外轮廓,逐一加开 Intersection / Normal Angle / Material ID。
- 控制 Normal Angle 的 Min/Max,收窄阈值。
硬面折线抓不住
- 使用 Smoothing Boundary 或提升 Normal Angle Min。
材质分块不清
- 检查模型是否有清晰的材质 ID;必要时在 Maya 中分配多材质。
笔刷不生效
- 确认勾选了
Specific Brush Settings并正确指派资源。
- 确认勾选了
线宽随镜头飘
- 统一场景单位与镜头距离;必要时按镜头远近做多版本线宽或合成端做指数衰减。
法线异常导致漏线
Mesh Display > Conform/Set to Face后再Soften/Harden;清理反法线与非流形。
*渲染设置(补丁)
理论上基本和正常的render是一致的,这里可以改渲染器

但是,我调取不到batch render官方说明中的-pl:port <int>是不可用的..
所以修修补补写了个临时的batch插件(仅供参考,学习交流)(2022以上):
from __future__ import annotations
import traceback
from maya import cmds, mel
FORCE_OUTPUT_DIR = ""
def _ensure_plugin_loaded(plugin_name):
try:
if not cmds.pluginInfo(plugin_name, q=True, loaded=True):
cmds.loadPlugin(plugin_name)
return True
except Exception:
return False
def _get_current_renderer():
try:
return cmds.getAttr("defaultRenderGlobals.currentRenderer")
except Exception:
return "unknown"
def _get_frame_range_from_settings():
s = int(cmds.getAttr("defaultRenderGlobals.startFrame"))
e = int(cmds.getAttr("defaultRenderGlobals.endFrame"))
by = int(round(cmds.getAttr("defaultRenderGlobals.byFrameStep")))
return s, e, max(1, by)
def _set_frame_range(s, e, by):
cmds.setAttr("defaultRenderGlobals.startFrame", s)
cmds.setAttr("defaultRenderGlobals.endFrame", e)
cmds.setAttr("defaultRenderGlobals.byFrameStep", by)
def _ensure_animation_on():
try:
if cmds.getAttr("defaultRenderGlobals.animation") != 1:
cmds.setAttr("defaultRenderGlobals.animation", 1)
except Exception:
pass
def _list_render_layers():
try:
from maya.app.renderSetup.model import renderSetup
rs = renderSetup.instance()
layers = rs.getRenderLayers()
active = [l for l in layers if getattr(l, "isRenderable", None) and l.isRenderable()] \
or [l for l in layers if getattr(l, "isVisible", None) and l.isVisible()] \
or list(layers)
return active
except Exception:
return []
def _switch_to_layer(layer_obj):
try:
layer_obj.switchTo()
return True
except Exception:
return False
def _list_renderable_cameras():
cams = []
for cam_shape in cmds.ls(type="camera", long=True) or []:
try:
if cmds.getAttr(cam_shape + ".renderable"):
tr = cmds.listRelatives(cam_shape, p=True, f=True) or []
if tr:
cams.append(tr[0])
except Exception:
pass
seen, out = set(), []
for c in cams:
if c not in seen:
out.append(c); seen.add(c)
return out
def _maybe_override_output_dir(dir_path):
if not dir_path:
return
try:
if not cmds.objExists("defaultRenderGlobals.imageFilePrefix"):
return
prefix = cmds.getAttr("defaultRenderGlobals.imageFilePrefix") or "<Scene>/<RenderLayer>/<Camera>"
if dir_path.endswith("/") or dir_path.endswith("\\"):
dir_path = dir_path[:-1]
new_prefix = dir_path.replace("\\", "/") + "/" + prefix
cmds.setAttr("defaultRenderGlobals.imageFilePrefix", new_prefix, type="string")
print("[BatchRender] Override output dir ->", new_prefix)
except Exception:
print("[BatchRender] Failed to override output dir")
traceback.print_exc()
def _camera_arg_for_mel(camera_transform):
return camera_transform.replace('"', '\\"')
def _renderer_entry(renderer_name):
r = (renderer_name or "").lower()
if r in ("arnold", "mtoa"):
return "arnold"
return "generic"
def _render_sequence_with_arnold(camera, start, end, by):
if not _ensure_plugin_loaded("mtoa"):
raise RuntimeError("无法加载 Arnold 插件(mtoa)。")
shapes = cmds.listRelatives(camera, s=True, ni=True, f=True) or []
cam_shape = (shapes[0] if shapes else camera).replace('"','\\"')
mel.eval(
'arnoldRender -seq -cam "{cam}" -start {s} -end {e} -by {by};'.format(
cam=cam_shape, s=start, e=end, by=by
)
)
def _render_sequence_generic(camera, start, end, by):
cam_arg = _camera_arg_for_mel(camera)
for f in range(start, end + 1, by):
print("[BatchRender] Rendering frame {f} @ {cam}".format(f=f, cam=camera))
cmds.currentTime(f, e=True)
mel.eval('render "{cam}";'.format(cam=cam_arg))
def run_batch(cameras=None, layers=None, start=None, end=None, by=None, override_output_dir=FORCE_OUTPUT_DIR):
try:
cur_renderer = _get_current_renderer()
entry = _renderer_entry(cur_renderer)
print("[BatchRender] Current Renderer:", cur_renderer, "->", entry)
s0, e0, by0 = _get_frame_range_from_settings()
s = int(start) if start is not None else s0
e = int(end) if end is not None else e0
step = int(by) if by is not None else by0
_set_frame_range(s, e, step)
_ensure_animation_on()
_maybe_override_output_dir(override_output_dir)
layer_objs = layers if layers is not None else _list_render_layers()
if not layer_objs:
print("[BatchRender] 未找到 Render Setup 层,可能场景未使用 Render Setup。将在当前层渲染。")
cams = cameras if cameras is not None else _list_renderable_cameras()
if not cams:
raise RuntimeError("未发现可渲染相机(CameraShape.renderable==True)。请在需要的相机上勾选 Renderable。")
def _do_render_for_current_layer():
for cam in cams:
if entry == "arnold":
_render_sequence_with_arnold(cam, s, e, step)
else:
_render_sequence_generic(cam, s, e, step)
if layer_objs:
for lyr in layer_objs:
lname = getattr(lyr, "name", lambda: str(lyr))()
print("\n[BatchRender] ===== Render Layer:", lname, "=====")
_switch_to_layer(lyr)
_do_render_for_current_layer()
else:
_do_render_for_current_layer()
print("\n[BatchRender] ✅ 完成。请查看输出目录。")
except Exception as ex:
print("\n[BatchRender] ❌ 失败:", ex)
traceback.print_exc()
def _build_ui():
if cmds.window("BatchSeqRenderUI", exists=True):
cmds.deleteUI("BatchSeqRenderUI")
win = cmds.window("BatchSeqRenderUI", title="Batch Sequence Render (Maya 2024)", sizeable=False)
col = cmds.columnLayout(adj=True, rs=6, cw=440)
s0, e0, by0 = _get_frame_range_from_settings()
cmds.text(l="Frame Range(读取自 Render Settings)")
fr = cmds.rowLayout(nc=6, cw6=[70,90,50,90,50,90], adj=2)
cmds.text(l="Start")
f_s = cmds.intField(v=s0)
cmds.text(l="End")
f_e = cmds.intField(v=e0)
cmds.text(l="Step")
f_by = cmds.intField(v=by0)
cmds.setParent("..")
cmds.text(l="Override Output Directory(可选,留空沿用 Render Settings)")
f_dir = cmds.textField(tx=FORCE_OUTPUT_DIR, cc=lambda *_: None)
cams = _list_renderable_cameras()
cmds.text(l="Renderable Cameras(如不选择则使用全部)")
f_cam = cmds.textScrollList(ams=True, h=120)
for c in cams:
cmds.textScrollList(f_cam, e=True, a=c)
lyr_names = []
try:
from maya.app.renderSetup.model import renderSetup
rs = renderSetup.instance()
for l in rs.getRenderLayers():
lyr_names.append(l.name())
except Exception:
pass
cmds.text(l="Render Setup Layers(如不选择则按可渲染/可见层自动)")
f_layer = cmds.textScrollList(ams=True, h=120)
for n in lyr_names:
cmds.textScrollList(f_layer, e=True, a=n)
def _on_run(*_):
s = cmds.intField(f_s, q=True, v=True)
e = cmds.intField(f_e, q=True, v=True)
by = cmds.intField(f_by, q=True, v=True)
out_dir = cmds.textField(f_dir, q=True, tx=True).strip()
sel_cams = cmds.textScrollList(f_cam, q=True, sii=True) or []
use_cams = [cams[i-1] for i in sel_cams] if sel_cams else cams
sel_layers = cmds.textScrollList(f_layer, q=True, sii=True) or []
use_layers = None
if sel_layers:
from maya.app.renderSetup.model import renderSetup
rs = renderSetup.instance()
name_to_obj = {l.name(): l for l in rs.getRenderLayers()}
use_layers = [name_to_obj[lyr_names[i-1]] for i in sel_layers]
run_batch(cameras=use_cams, layers=use_layers, start=s, end=e, by=by, override_output_dir=out_dir)
cmds.separator(h=5, st="none")
cmds.button(l="Run", h=34, c=_on_run)
cmds.separator(h=6, st="none")
cmds.button(l="Close", c=lambda *_: cmds.deleteUI(win))
cmds.showWindow(win)
# 执行即弹 UI;也可在 Script Editor 里手动调用 run_batch(...)
try:
_build_ui()
except Exception:
traceback.print_exc()
print("提示:你也可以在 Script Editor 里手动调用: run_batch()") 
