<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>MAYA动画教学 归档 - 喵喵动画屋</title>
	<atom:link href="https://www.miaodonghua.com/category/maya-animation-tutorial/feed" rel="self" type="application/rss+xml" />
	<link>https://www.miaodonghua.com/category/maya-animation-tutorial</link>
	<description>探索Maya世界：基础教程、动画技巧、建模艺术与渲染技术。</description>
	<lastBuildDate>Sun, 21 Jun 2026 12:51:23 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://www.miaodonghua.com/wp-content/uploads/2020/11/cropped-shuqian_logo.webp</url>
	<title>MAYA动画教学 归档 - 喵喵动画屋</title>
	<link>https://www.miaodonghua.com/category/maya-animation-tutorial</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Maya时间轴播放不实时：帧率、缓存播放与求值设置怎么选</title>
		<link>https://www.miaodonghua.com/90006.html</link>
					<comments>https://www.miaodonghua.com/90006.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:23 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90006</guid>

					<description><![CDATA[<p>动画播放忽快忽慢，声音不同步，或实际帧率远低于项目帧率。</p>
<p><a href="https://www.miaodonghua.com/90006.html">Maya时间轴播放不实时：帧率、缓存播放与求值设置怎么选</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>动画播放忽快忽慢，声音不同步，或实际帧率远低于项目帧率。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>Playback Speed 设置错误</li>
<li>Cached Playback 未能缓存复杂节点</li>
<li>动力学和绑定每帧计算过重</li>
<li>音频采样率或时间单位不一致</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>确认 Settings 的 Time 单位与项目帧率一致</li>
<li>Playback Speed 选择 Real-time，而非 Play every frame</li>
<li>观察时间轴缓存颜色并查看 Cached Playback 警告</li>
<li>对高成本模拟使用缓存，对最终动作预览使用 Playblast</li>
<li>导入标准采样率音频并检查起始帧</li>
</ol>
<h2>如何确认已经修好</h2>
<p>播放一段带声音的100帧测试，计时并对比 Playblast，确认节奏和口型同步。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90006.html">Maya时间轴播放不实时：帧率、缓存播放与求值设置怎么选</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90006.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya Graph Editor曲线抖动：多余关键帧与切线清理</title>
		<link>https://www.miaodonghua.com/90030.html</link>
					<comments>https://www.miaodonghua.com/90030.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:10 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90030</guid>

					<description><![CDATA[<p>动作轮廓正确但细节发抖，Graph Editor 中曲线像锯齿。</p>
<p><a href="https://www.miaodonghua.com/90030.html">Maya Graph Editor曲线抖动：多余关键帧与切线清理</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>动作轮廓正确但细节发抖，Graph Editor 中曲线像锯齿。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>自动关键帧产生密集小键</li>
<li>欧拉角跨越导致曲线跳跃</li>
<li>动捕或烘焙数据未过滤</li>
<li>切线权重不连续</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>先在 Playblast 中标记可见抖动帧，不要只凭曲线外观删键</li>
<li>对旋转曲线检查 Euler Filter</li>
<li>使用 Simplify/减少关键帧时保留接触和极值帧</li>
<li>统一关键段的切线类型并手调速度变化</li>
<li>每次清理一组通道，避免同时破坏多个动作层次</li>
</ol>
<h2>如何确认已经修好</h2>
<p>清理后轮廓、接触点和节奏不变，曲线关键帧显著减少且播放稳定。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90030.html">Maya Graph Editor曲线抖动：多余关键帧与切线清理</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90030.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya脚滑怎么修：世界空间轨迹与接触帧工作流</title>
		<link>https://www.miaodonghua.com/90032.html</link>
					<comments>https://www.miaodonghua.com/90032.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:09 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90032</guid>

					<description><![CDATA[<p>角色走路或跑步时脚掌在地面上缓慢漂移。</p>
<p><a href="https://www.miaodonghua.com/90032.html">Maya脚滑怎么修：世界空间轨迹与接触帧工作流</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>角色走路或跑步时脚掌在地面上缓慢漂移。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>接触区间脚控制器世界位置并未锁定</li>
<li>根控制器位移与步幅不匹配</li>
<li>IK/FK切换或空间切换有误差</li>
<li>地面高度变化未被处理</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>明确脚跟触地、全掌接触、抬脚三个阶段</li>
<li>在接触区间查看脚控制器世界空间轨迹</li>
<li>先修根部速度和步幅，再锁脚，不要只堆脚部关键帧</li>
<li>必要时使用临时定位器记录接触点</li>
<li>坡地上分别处理位置、脚掌滚动与身体重心</li>
</ol>
<h2>如何确认已经修好</h2>
<p>接触阶段脚掌关键点在世界空间保持稳定，角色重心运动仍自然。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90032.html">Maya脚滑怎么修：世界空间轨迹与接触帧工作流</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90032.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya旋转曲线突然跳360度：Euler Filter使用边界</title>
		<link>https://www.miaodonghua.com/90031.html</link>
					<comments>https://www.miaodonghua.com/90031.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:09 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90031</guid>

					<description><![CDATA[<p>控制器只转了小角度，曲线却从正值跳到负值或跨越360度。</p>
<p><a href="https://www.miaodonghua.com/90031.html">Maya旋转曲线突然跳360度：Euler Filter使用边界</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>控制器只转了小角度，曲线却从正值跳到负值或跨越360度。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>欧拉角存在多个等价表示</li>
<li>旋转顺序不适合当前动作</li>
<li>不同轴同时接近万向锁</li>
<li>过滤前没有选完整旋转曲线</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>备份动画并选择同一控制器的三条旋转曲线</li>
<li>在连续动作段使用 Euler Filter，观察是否选择了正确解</li>
<li>高风险控制器在制作前选择更合适的 rotate order</li>
<li>不要在需要多圈旋转的区段盲目过滤</li>
<li>复杂镜头可用辅助空间或四元数思路减少单控制器负担</li>
</ol>
<h2>如何确认已经修好</h2>
<p>过滤后视觉动作不变，三轴曲线连续，真实多圈旋转仍被保留。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90031.html">Maya旋转曲线突然跳360度：Euler Filter使用边界</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90031.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya动画层合并后动作变了：Override与Additive差异</title>
		<link>https://www.miaodonghua.com/90033.html</link>
					<comments>https://www.miaodonghua.com/90033.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:08 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90033</guid>

					<description><![CDATA[<p>Animation Layer中效果正常，Merge或Bake后姿态发生变化。</p>
<p><a href="https://www.miaodonghua.com/90033.html">Maya动画层合并后动作变了：Override与Additive差异</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>Animation Layer中效果正常，Merge或Bake后姿态发生变化。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>层模式理解错误</li>
<li>层权重本身有动画</li>
<li>旋转叠加受旋转顺序影响</li>
<li>静音或锁定层未被正确处理</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>合并前记录各层模式、权重和关键帧范围</li>
<li>区分 Override替代结果与 Additive增量结果</li>
<li>先在副本上 Bake 到基础层并逐帧对比</li>
<li>检查层权重曲线和零值区间</li>
<li>旋转差异明显时分控制器处理，不要一次性合并全部</li>
</ol>
<h2>如何确认已经修好</h2>
<p>合并前后对关键姿势做屏幕对比，世界矩阵和轮廓误差处于可接受范围。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90033.html">Maya动画层合并后动作变了：Override与Additive差异</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90033.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya约束烘焙后仍然漂移：Bake Simulation完整流程</title>
		<link>https://www.miaodonghua.com/90034.html</link>
					<comments>https://www.miaodonghua.com/90034.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:07 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90034</guid>

					<description><![CDATA[<p>删除约束后动画偏移，或烘焙结果少帧、接触不稳。</p>
<p><a href="https://www.miaodonghua.com/90034.html">Maya约束烘焙后仍然漂移：Bake Simulation完整流程</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>删除约束后动画偏移，或烘焙结果少帧、接触不稳。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>只执行普通 Bake Keys 而非逐帧求值</li>
<li>烘焙范围不含预滚帧</li>
<li>约束对象在不同空间</li>
<li>稀疏烘焙删除了必要样本</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>明确需要烘焙的世界空间结果和帧范围</li>
<li>使用 Bake Simulation 并包含约束生效前后的保护帧</li>
<li>先逐帧采样，确认正确后再简化曲线</li>
<li>删除约束前把烘焙结果与原结果并排比较</li>
<li>动力学或缓存镜头要包含 preroll</li>
</ol>
<h2>如何确认已经修好</h2>
<p>删除约束后逐帧世界矩阵与原结果一致，曲线简化后接触点仍稳定。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90034.html">Maya约束烘焙后仍然漂移：Bake Simulation完整流程</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90034.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya Playblast颜色和最终渲染不同：视口色彩管理统一</title>
		<link>https://www.miaodonghua.com/90035.html</link>
					<comments>https://www.miaodonghua.com/90035.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:07 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90035</guid>

					<description><![CDATA[<p>Playblast偏灰或偏亮，客户审片与最终渲染观感差异大。</p>
<p><a href="https://www.miaodonghua.com/90035.html">Maya Playblast颜色和最终渲染不同：视口色彩管理统一</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>Playblast偏灰或偏亮，客户审片与最终渲染观感差异大。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>视口未使用相同显示变换</li>
<li>Playblast捕获了UI显示而非期望输出</li>
<li>播放器再次应用颜色转换</li>
<li>不同电脑显示配置不同</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>固定项目颜色管理和 View Transform</li>
<li>在 Viewport 2.0 中确认显示设置与审片标准</li>
<li>输出一张色卡和灰阶作为对照</li>
<li>检查播放器是否自动识别或转换色彩</li>
<li>重要审片使用明确编码的视频流程，而非依赖随机播放器</li>
</ol>
<h2>如何确认已经修好</h2>
<p>同一显示器上，视口、Playblast和渲染样张的灰阶与主要颜色接近。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90035.html">Maya Playblast颜色和最终渲染不同：视口色彩管理统一</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90035.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya关键帧复制后时间错乱：时间单位与Paste Keys选项</title>
		<link>https://www.miaodonghua.com/90036.html</link>
					<comments>https://www.miaodonghua.com/90036.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:06 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90036</guid>

					<description><![CDATA[<p>从一个场景复制动画到另一个场景后，节奏变快、变慢或关键帧挤在一起。</p>
<p><a href="https://www.miaodonghua.com/90036.html">Maya关键帧复制后时间错乱：时间单位与Paste Keys选项</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>从一个场景复制动画到另一个场景后，节奏变快、变慢或关键帧挤在一起。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>两个场景帧率不同</li>
<li>Paste Keys使用了缩放或合并范围</li>
<li>复制对象通道不一致</li>
<li>动画起始帧偏移</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>复制前确认两个场景的 Time Unit</li>
<li>在 Paste Keys Options 中明确 Time、Copies与Connect方式</li>
<li>只选择需要的通道，避免覆盖已有动画</li>
<li>使用时间标记记录源起点和目标起点</li>
<li>跨项目优先使用 anim 文件或FBX并做回导验证</li>
</ol>
<h2>如何确认已经修好</h2>
<p>关键姿势在目标场景落到预期时间，动作时长与源场景按帧率换算一致。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90036.html">Maya关键帧复制后时间错乱：时间单位与Paste Keys选项</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90036.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya摄像机动画抖动：父层级、约束与大坐标问题</title>
		<link>https://www.miaodonghua.com/90037.html</link>
					<comments>https://www.miaodonghua.com/90037.html#respond</comments>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 12:51:06 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<category><![CDATA[maya]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=90037</guid>

					<description><![CDATA[<p>摄像机在慢推或环绕时出现细小抖动，远离原点后更明显。</p>
<p><a href="https://www.miaodonghua.com/90037.html">Maya摄像机动画抖动：父层级、约束与大坐标问题</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>问题概述：</strong>摄像机在慢推或环绕时出现细小抖动，远离原点后更明显。</p>
<p>这类问题最容易被“反复重启、盲目加采样或直接删除节点”掩盖。可靠的处理方式是先复制工程，在可回退的副本中建立最小测试，再一次只改变一个变量。</p>
<h2>常见原因</h2>
<ul>
<li>摄像机与目标被重复约束</li>
<li>父层级存在非均匀缩放</li>
<li>场景位于极大世界坐标</li>
<li>曲线存在高频小键</li>
</ul>
<h2>推荐解决步骤</h2>
<ol>
<li>把摄像机、aim和shake拆分到独立层级</li>
<li>移除非均匀缩放并检查约束权重</li>
<li>大型场景使用局部原点或浮动原点策略</li>
<li>在世界空间检查摄像机轨迹和目标轨迹</li>
<li>最后才添加可控的镜头抖动层</li>
</ol>
<h2>如何确认已经修好</h2>
<p>关闭有意抖动层后画面完全稳定，慢速物体边缘不再随机跳动。</p>
<h2>容易踩的坑</h2>
<p>不要在唯一工程文件上直接清理、解绑或覆盖保存；不要同时修改多个设置，否则即使结果变好，也无法知道真正起作用的是哪一步。涉及插件、渲染器和颜色管理时，还应记录 Maya 版本、插件版本、操作系统和项目单位。</p>
<h2>官方参考</h2>
<p>菜单名称和默认值可能随版本变化，可结合 <a href="https://help.autodesk.com/view/MAYAUL/2026/ENU/" target="_blank" rel="noopener">Autodesk Maya 官方帮助</a>核对当前版本。</p>
<p><em>本文为喵喵动画屋整理的实战排错清单。建议先在副本中测试，再应用到正式项目。</em></p>
<p><a href="https://www.miaodonghua.com/90037.html">Maya摄像机动画抖动：父层级、约束与大坐标问题</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.miaodonghua.com/90037.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Maya 裙子绑定神器｜按顶点顺序创建控制器 &#038; 关节链｜Skirt Rigging Tool</title>
		<link>https://www.miaodonghua.com/3553.html</link>
		
		<dc:creator><![CDATA[喵喵动画屋]]></dc:creator>
		<pubDate>Tue, 23 Dec 2025 18:07:48 +0000</pubDate>
				<category><![CDATA[MAYA动画教学]]></category>
		<guid isPermaLink="false">https://www.miaodonghua.com/?p=3553</guid>

					<description><![CDATA[<p><a href="https://www.miaodonghua.com/3553.html">Maya 裙子绑定神器｜按顶点顺序创建控制器 &amp; 关节链｜Skirt Rigging Tool</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<pre class="wp-block-code has-vivid-green-cyan-color has-black-background-color has-text-color has-background has-link-color wp-elements-5dc825d4c9e7eefb26e2dc583d57429d"><code># -*- coding: utf-8 -*-
"""
Maya Python脚本：根据选择的多边形顶点位置创建控制器（Maya 2022 / Python 3）

功能 / Features:
- 常驻 UI：记录你 Shift 逐个点选的顶点顺序（使用 orderedSelection，避免组件 index 排序）
- 列表交互：双击列表条目 -> 选中并框显对应顶点（Select &amp; Frame）
- 可调参数：控制器颜色色卡（RGB）+ 控制器大小（半径，默认 1）
- 一键创建：从记录列表创建关节链 + 控制器 + 约束
- UI 全部中英双语
- 重要按钮配色区分（开始记录/清空/从列表创建）
- 体验优化：列表为空时“从列表创建”置灰；录制中窗口标题显示“● Recording”
- 作者信息：董帅

作者：董帅
个人博客：https://www.miaodonghua.com/
YouTube：https://www.youtube.com/channel/UCPw4S_MYsO1Tuv3VzU-e5Qw
Bilibili：https://space.bilibili.com/353020636
"""

import math
import maya.cmds as cmds


# -------------------------
# UI Button Colors (0~1 RGB)
# -------------------------
BTN_PRIMARY_GREEN = (0.22, 0.60, 0.26)   # Start recording
BTN_PRIMARY_RED   = (0.70, 0.22, 0.22)   # Stop recording
BTN_PRIMARY_BLUE  = (0.18, 0.42, 0.72)   # Create
BTN_WARN_ORANGE   = (0.78, 0.45, 0.12)   # Clear
BTN_SECONDARY     = (0.32, 0.32, 0.32)   # Other buttons


# -------------------------
# Author Links (not shown in UI; open via buttons)
# -------------------------
AUTHOR_NAME = "董帅"
URL_BLOG = "https://www.miaodonghua.com/"
URL_YOUTUBE = "https://www.youtube.com/channel/UCPw4S_MYsO1Tuv3VzU-e5Qw"
URL_BILIBILI = "https://space.bilibili.com/353020636"


# -------------------------
# Helpers
# -------------------------

def _open_url(url):
    """Open URL in default browser (Maya-friendly)."""
    try:
        cmds.launch(web=url)
        return
    except Exception:
        pass
    try:
        import webbrowser
        webbrowser.open(url, new=2)
    except Exception:
        cmds.warning("无法打开浏览器链接。\nFailed to open browser link.")


def _set_ctrl_color_rgb(ctrl_transform, rgb):
    """将控制器曲线设为指定 RGB 颜色（viewport override）。"""
    if not ctrl_transform or not cmds.objExists(ctrl_transform):
        return

    try:
        r, g, b = rgb
        r, g, b = float(r), float(g), float(b)
    except Exception:
        r, g, b = 1.0, 1.0, 0.0

    shapes = cmds.listRelatives(ctrl_transform, shapes=True, fullPath=True) or &#91;]
    for s in shapes:
        try:
            cmds.setAttr(s + ".overrideEnabled", 1)
            cmds.setAttr(s + ".overrideRGBColors", 1)
            cmds.setAttr(s + ".overrideColorRGB", r, g, b)
        except Exception:
            # 兜底：使用索引色（17 通常为黄）
            try:
                cmds.setAttr(s + ".overrideEnabled", 1)
                cmds.setAttr(s + ".overrideRGBColors", 0)
                cmds.setAttr(s + ".overrideColor", 17)
            except Exception:
                pass


def get_available_suffix(base_name):
    """获取可用的后缀（A-Z 或数字）。"""
    for i in range(26):
        suffix = chr(ord('A') + i)
        full_name = "{}_{}".format(base_name, suffix)
        main_grp = "{}_rig_grp".format(full_name)
        if not cmds.objExists(main_grp):
            return suffix

    num = 1
    while True:
        suffix = str(num)
        full_name = "{}_{}".format(base_name, suffix)
        main_grp = "{}_rig_grp".format(full_name)
        if not cmds.objExists(main_grp):
            return suffix
        num += 1
        if num > 10000:
            break

    return "1"


def aim_controller_group_to_world_center(grp, position):
    """让控制器组本地 +X 轴指向“世界中心的垂线方向”（与径向向量垂直）。"""
    radial = &#91;-position&#91;0], -position&#91;1], -position&#91;2]]
    magnitude = math.sqrt(radial&#91;0] ** 2 + radial&#91;1] ** 2 + radial&#91;2] ** 2)
    if magnitude &lt; 1e-6:
        return

    radial = &#91;radial&#91;0] / magnitude, radial&#91;1] / magnitude, radial&#91;2] / magnitude]

    world_up = &#91;0.0, 1.0, 0.0]
    tangent = &#91;
        world_up&#91;1] * radial&#91;2] - world_up&#91;2] * radial&#91;1],
        world_up&#91;2] * radial&#91;0] - world_up&#91;0] * radial&#91;2],
        world_up&#91;0] * radial&#91;1] - world_up&#91;1] * radial&#91;0],
    ]

    tangent_len = math.sqrt(tangent&#91;0] ** 2 + tangent&#91;1] ** 2 + tangent&#91;2] ** 2)

    if tangent_len &lt; 1e-6:
        world_forward = &#91;0.0, 0.0, 1.0]
        tangent = &#91;
            world_forward&#91;1] * radial&#91;2] - world_forward&#91;2] * radial&#91;1],
            world_forward&#91;2] * radial&#91;0] - world_forward&#91;0] * radial&#91;2],
            world_forward&#91;0] * radial&#91;1] - world_forward&#91;1] * radial&#91;0],
        ]
        tangent_len = math.sqrt(tangent&#91;0] ** 2 + tangent&#91;1] ** 2 + tangent&#91;2] ** 2)

    if tangent_len &lt; 1e-6:
        return

    tangent = &#91;tangent&#91;0] / tangent_len, tangent&#91;1] / tangent_len, tangent&#91;2] / tangent_len]
    rotation = cmds.angleBetween(euler=True, v1=&#91;1, 0, 0], v2=tangent)
    cmds.xform(grp, worldSpace=True, rotation=rotation)


# -------------------------
# Rig building
# -------------------------

def create_joint_chain(positions, name_prefix, start_index=1):
    """按位置顺序创建关节链。"""
    if not positions:
        return &#91;]

    joints = &#91;]

    joint_name = "{}_{:03d}_jnt".format(name_prefix, start_index)
    current_joint = cmds.joint(name=joint_name, position=positions&#91;0])
    joints.append(current_joint)

    for i in range(1, len(positions)):
        joint_index = start_index + i
        joint_name = "{}_{:03d}_jnt".format(name_prefix, joint_index)
        cmds.select(current_joint, replace=True)
        current_joint = cmds.joint(name=joint_name, position=positions&#91;i])
        joints.append(current_joint)

    if joints:
        cmds.joint(
            joints&#91;0],
            edit=True,
            orientJoint='xyz',
            secondaryAxisOrient='yup',
            zeroScaleOrient=True,
            children=True
        )

    return joints


def create_constraints(controller, joint):
    """为控制器和关节创建 parentConstraint（无 offset）。"""
    if not controller or not joint:
        return None
    if not cmds.objExists(controller) or not cmds.objExists(joint):
        return None

    parent_constraint_name = "{}_parentConstraint".format(joint)
    if cmds.objExists(parent_constraint_name):
        cmds.delete(parent_constraint_name)

    parent_constraint = cmds.parentConstraint(
        controller,
        joint,
        name=parent_constraint_name,
        maintainOffset=False
    )

    return parent_constraint


def create_controller_at_position(position, index, name_prefix, ctrl_radius, ctrl_rgb):
    """在指定位置创建控制器（circle）+ group，并设置颜色与朝向。"""
    ctrl_name = "{}_{:03d}_ctrl".format(name_prefix, index)
    ctrl = cmds.circle(name=ctrl_name, radius=float(ctrl_radius), normal=&#91;0, 1, 0])&#91;0]

    _set_ctrl_color_rgb(ctrl, ctrl_rgb)

    cmds.xform(ctrl, centerPivots=True)

    grp_name = "{}_grp".format(ctrl_name)
    grp = cmds.group(ctrl, name=grp_name)

    cmds.xform(grp, worldSpace=True, translation=position)
    aim_controller_group_to_world_center(grp, position)

    return grp, ctrl


def create_controllers_from_positions(vertex_positions, base_name, ctrl_radius=1.0, ctrl_rgb=(1.0, 1.0, 0.0)):
    """从位置列表创建：组 + 关节链 + 控制器链 + 约束。"""
    if not vertex_positions:
        cmds.warning("没有顶点位置数据！\nNo vertex positions provided!")
        return

    if not base_name or not base_name.strip():
        cmds.warning("名称为空！\nName is empty!")
        return

    try:
        ctrl_radius = float(ctrl_radius)
    except Exception:
        ctrl_radius = 1.0

    if ctrl_radius &lt;= 0.0:
        cmds.warning("控制器大小必须大于 0，已使用默认值 1。\nController size must be > 0. Using default 1.")
        ctrl_radius = 1.0

    base_name = base_name.strip()

    suffix = get_available_suffix(base_name)
    name_prefix = "{}_{}".format(base_name, suffix)

    main_grp = "{}_rig_grp".format(name_prefix)
    joint_grp = "{}_joints_grp".format(name_prefix)
    ctrl_grp = "{}_controllers_grp".format(name_prefix)

    if not cmds.objExists(main_grp):
        main_grp = cmds.group(empty=True, name=main_grp)

    if not cmds.objExists(joint_grp):
        joint_grp = cmds.group(empty=True, name=joint_grp)
        cmds.parent(joint_grp, main_grp)
    else:
        p = cmds.listRelatives(joint_grp, parent=True) or &#91;]
        if not p or p&#91;0] != main_grp:
            cmds.parent(joint_grp, main_grp)

    if not cmds.objExists(ctrl_grp):
        ctrl_grp = cmds.group(empty=True, name=ctrl_grp)
        cmds.parent(ctrl_grp, main_grp)
        start_index = 1
    else:
        p = cmds.listRelatives(ctrl_grp, parent=True) or &#91;]
        if not p or p&#91;0] != main_grp:
            cmds.parent(ctrl_grp, main_grp)
        existing_ctrls = cmds.listRelatives(ctrl_grp, children=True, type="transform") or &#91;]
        start_index = len(existing_ctrls) + 1

    created_joints = create_joint_chain(vertex_positions, name_prefix, start_index)

    if created_joints:
        cmds.parent(created_joints&#91;0], joint_grp)

    created_controllers = &#91;]
    parent_ctrl = None

    for i, (pos, joint) in enumerate(zip(vertex_positions, created_joints)):
        ctrl_index = start_index + i
        ctrl_grp_item, ctrl_item = create_controller_at_position(
            pos, ctrl_index, name_prefix,
            ctrl_radius=ctrl_radius,
            ctrl_rgb=ctrl_rgb
        )

        if not cmds.objExists(ctrl_grp_item):
            continue

        if parent_ctrl is None:
            cmds.parent(ctrl_grp_item, ctrl_grp)
        else:
            if ctrl_item and cmds.objExists(parent_ctrl):
                cmds.parent(ctrl_grp_item, parent_ctrl)
            else:
                cmds.parent(ctrl_grp_item, ctrl_grp)

        created_controllers.append(ctrl_grp_item)

        if ctrl_item and joint and cmds.objExists(ctrl_item) and cmds.objExists(joint):
            create_constraints(ctrl_item, joint)

        if ctrl_item:
            parent_ctrl = ctrl_item

    existing_controllers = &#91;c for c in created_controllers if cmds.objExists(c)]
    if existing_controllers:
        cmds.select(existing_controllers, replace=True)

    cmds.confirmDialog(
        title="完成 / Complete",
        message="成功创建 {} 个关节和 {} 个控制器！\nSuccessfully created {} joints and {} controllers!\n\n使用命名前缀 / Name prefix: {}".format(
            len(created_joints), len(created_controllers),
            len(created_joints), len(created_controllers),
            name_prefix
        ),
        button=&#91;"确定 / OK"],
        defaultButton="确定 / OK"
    )
    return created_joints, created_controllers


# =========================
# UI: Record vertex order + Create (Vertical Layout, Shift-click optimized)
# =========================

class VertexOrderUI(object):
    WIN = "createCtrlsFromVtx_orderUI"

    def __init__(self):
        self.job_id = None

        self.vertices = &#91;]
        self._last_ordered_vtx = &#91;]
        self._recording = False
        self._building = False

        self._title_base = "从顶点创建控制器 / Create Ctrls From Vtx (Ordered)"

        # UI controls
        self.name_field = None
        self.size_field = None
        self.color_field = None
        self.list_ui = None
        self.record_btn = None
        self.clear_btn = None
        self.create_btn = None
        self.status_txt = None

    # ---- Window title

    def _set_window_title(self, recording=False):
        if not cmds.window(self.WIN, exists=True):
            return
        title = self._title_base
        if recording:
            title = "● Recording  |  " + title
        cmds.window(self.WIN, edit=True, title=title)

    # ---- Create button state

    def _update_create_button_state(self):
        if not self.create_btn or not cmds.control(self.create_btn, exists=True):
            return
        enable = len(self.vertices) > 0
        cmds.button(self.create_btn, edit=True, enable=enable)

    # ---- UI

    def show(self):
        if cmds.window(self.WIN, exists=True):
            cmds.deleteUI(self.WIN)

        cmds.window(self.WIN, title=self._title_base, sizeable=False)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=8)

        # Settings
        cmds.frameLayout(label="参数 / Settings", collapsable=True, collapse=False, marginWidth=8, marginHeight=6)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=6)

        self.name_field = cmds.textFieldGrp(
            label="名称 Name",
            text="Skirt",
            columnAlign=(1, "right"),
            columnWidth=&#91;(1, 90), (2, 320)]
        )

        self.size_field = cmds.floatFieldGrp(
            label="大小 Size",
            numberOfFields=1,
            value1=1.0,
            columnAlign=(1, "right"),
            columnWidth=&#91;(1, 90), (2, 120)]
        )

        self.color_field = cmds.colorSliderGrp(
            label="颜色 Color",
            rgb=(1.0, 1.0, 0.0),
            columnAlign=(1, "right"),
            columnWidth=&#91;(1, 90), (2, 220)]
        )

        cmds.text(label="提示 / Tip：Shift 点选最稳定（按新增顺序记录）", align="left")

        cmds.setParent("..")
        cmds.setParent("..")

        # Recording
        cmds.frameLayout(label="记录 / Recording", collapsable=True, collapse=False, marginWidth=8, marginHeight=6)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=6)

        self.record_btn = cmds.button(
            label="开始记录 / Start Recording",
            height=34,
            bgc=BTN_PRIMARY_GREEN,
            command=lambda *_: self.toggle_recording()
        )

        cmds.button(
            label="添加当前选择 / Add Selection",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: self.add_current_selection()
        )

        cmds.button(
            label="撤销最后 / Undo Last",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: self.pop_last()
        )

        self.clear_btn = cmds.button(
            label="清空 / Clear",
            height=30,
            bgc=BTN_WARN_ORANGE,
            command=lambda *_: self.clear()
        )

        self.status_txt = cmds.text(label="状态 / Status：未记录 / Idle（已记录 0）", align="left")

        cmds.setParent("..")
        cmds.setParent("..")

        # List
        cmds.frameLayout(label="顶点列表（按顺序）/ Vertex List (Ordered)", collapsable=False, marginWidth=8, marginHeight=6)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=6)

        self.list_ui = cmds.textScrollList(
            numberOfRows=12,
            allowMultiSelection=True,
            height=220,
            doubleClickCommand=lambda *_: self.select_and_frame_from_list()
        )

        cmds.setParent("..")
        cmds.setParent("..")

        # Actions
        cmds.frameLayout(label="操作 / Actions", collapsable=False, marginWidth=8, marginHeight=6)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=6)

        cmds.button(
            label="移除选中条目 / Remove Selected",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: self.remove_selected_items()
        )

        cmds.button(
            label="选中并框显 / Select &amp; Frame",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: self.select_and_frame_from_list()
        )

        self.create_btn = cmds.button(
            label="从列表创建 / Create From List",
            height=40,
            bgc=BTN_PRIMARY_BLUE,
            command=lambda *_: self.create_from_list()
        )

        cmds.button(
            label="关闭 / Close",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: cmds.deleteUI(self.WIN)
        )

        cmds.setParent("..")
        cmds.setParent("..")

        # About (links open browser; URLs not shown in UI)
        cmds.frameLayout(label="关于 / About", collapsable=True, collapse=True, marginWidth=8, marginHeight=6)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=6)

        cmds.text(label="作者 / Author：{}".format(AUTHOR_NAME), align="left")

        cmds.button(
            label="个人博客 / Blog",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: _open_url(URL_BLOG)
        )
        cmds.button(
            label="YouTube / YouTube",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: _open_url(URL_YOUTUBE)
        )
        cmds.button(
            label="Bilibili / Bilibili",
            height=26,
            bgc=BTN_SECONDARY,
            command=lambda *_: _open_url(URL_BILIBILI)
        )

        cmds.setParent("..")
        cmds.setParent("..")

        cmds.showWindow(self.WIN)

        # initial states
        self._set_window_title(recording=False)
        self._update_create_button_state()

    # ---- Internal

    def _set_status(self, cn, en):
        count = len(self.vertices)
        if self.status_txt and cmds.control(self.status_txt, exists=True):
            cmds.text(self.status_txt, edit=True, label="状态 / Status：{} / {}（已记录 {}）".format(cn, en, count))

    def _refresh_list(self):
        if not self.list_ui or not cmds.control(self.list_ui, exists=True):
            return
        cmds.textScrollList(self.list_ui, edit=True, removeAll=True)
        for v in self.vertices:
            cmds.textScrollList(self.list_ui, edit=True, append=v)
        self._update_create_button_state()

    def _install_job(self):
        try:
            cmds.selectPref(trackSelectionOrder=True)
        except Exception:
            pass

        if self.job_id and cmds.scriptJob(exists=self.job_id):
            cmds.scriptJob(kill=self.job_id, force=True)
            self.job_id = None

        self.job_id = cmds.scriptJob(
            event=&#91;"SelectionChanged", self._on_selection_changed],
            protected=True,
            parent=self.WIN
        )

    def _get_ordered_selected_vtx(self):
        ordered = cmds.ls(orderedSelection=True, flatten=True, long=True) or &#91;]
        return &#91;x for x in ordered if ".vtx&#91;" in x]

    # ---- Recording (Shift-click optimized)

    def toggle_recording(self):
        self._recording = not self._recording

        if self._recording:
            try:
                cmds.selectPref(trackSelectionOrder=True)
            except Exception:
                pass

            self._last_ordered_vtx = self._get_ordered_selected_vtx()
            self._install_job()

            cmds.button(self.record_btn, edit=True, label="停止记录 / Stop Recording", bgc=BTN_PRIMARY_RED)
            self._set_window_title(recording=True)
            self._set_status("记录中（Shift 点选）", "Recording (Shift-click)")
        else:
            cmds.button(self.record_btn, edit=True, label="开始记录 / Start Recording", bgc=BTN_PRIMARY_GREEN)
            self._set_window_title(recording=False)
            self._set_status("未记录", "Idle")

    def _on_selection_changed(self):
        if not self._recording or self._building:
            return

        cur_ordered = self._get_ordered_selected_vtx()
        prev_set = set(self._last_ordered_vtx)
        self._last_ordered_vtx = cur_ordered

        added = &#91;v for v in cur_ordered if v not in prev_set]
        if not added:
            return

        if len(added) > 1:
            cmds.warning(
                "一次新增多个顶点（可能框选/套索），内部顺序可能不稳定；Shift 逐个点选最稳。\n"
                "Multiple vertices added at once; internal order may be unstable."
            )

        changed = False
        for v in added:
            if v not in self.vertices:
                self.vertices.append(v)
                changed = True

        if changed:
            self._refresh_list()
            self._set_status("记录中", "Recording")

    # ---- List ops

    def add_current_selection(self):
        cur_ordered = self._get_ordered_selected_vtx()
        if not cur_ordered:
            cmds.warning("当前选择里没有顶点！\nNo vertices in current selection!")
            return
        for v in cur_ordered:
            if v not in self.vertices:
                self.vertices.append(v)
        self._refresh_list()
        self._set_status("已添加当前选择", "Selection added")

    def clear(self):
        self.vertices = &#91;]
        self._refresh_list()
        self._set_status("已清空", "Cleared")

    def pop_last(self):
        if self.vertices:
            self.vertices.pop()
            self._refresh_list()
            self._set_status("已撤销最后一个", "Undid last")

    def remove_selected_items(self):
        if not self.list_ui or not cmds.control(self.list_ui, exists=True):
            return
        selected = cmds.textScrollList(self.list_ui, query=True, selectItem=True) or &#91;]
        if not selected:
            return
        s = set(selected)
        self.vertices = &#91;v for v in self.vertices if v not in s]
        self._refresh_list()
        self._set_status("已移除选中条目", "Removed selected")

    def select_and_frame_from_list(self):
        if not self.list_ui or not cmds.control(self.list_ui, exists=True):
            return
        items = cmds.textScrollList(self.list_ui, query=True, selectItem=True) or &#91;]
        if not items:
            return

        existing = &#91;v for v in items if cmds.objExists(v)]
        if not existing:
            cmds.warning("所选条目在场景中不存在（可能 mesh 被删除/重命名）。\nSelected items not found in scene.")
            return

        try:
            cmds.select(existing, replace=True)
        except Exception:
            return

        try:
            cmds.viewFit()
        except Exception:
            pass

    # ---- Create

    def _get_ui_options(self):
        base_name = cmds.textFieldGrp(self.name_field, query=True, text=True).strip()

        try:
            size_val = float(cmds.floatFieldGrp(self.size_field, query=True, value1=True))
        except Exception:
            size_val = 1.0

        try:
            rgb = cmds.colorSliderGrp(self.color_field, query=True, rgb=True)
            rgb = (float(rgb&#91;0]), float(rgb&#91;1]), float(rgb&#91;2]))
        except Exception:
            rgb = (1.0, 1.0, 0.0)

        return base_name, size_val, rgb

    def create_from_list(self):
        if not self.vertices:
            cmds.warning("列表为空！请先记录/添加顶点。\nList is empty! Please record/add vertices first.")
            self._update_create_button_state()
            return

        base_name, size_val, rgb = self._get_ui_options()
        if not base_name:
            cmds.warning("名称为空！\nName is empty!")
            return

        positions = &#91;]
        for v in self.vertices:
            if not cmds.objExists(v):
                cmds.warning("顶点不存在，已跳过：{}\nVertex not found, skipped: {}".format(v, v))
                continue
            pos = cmds.xform(v, query=True, worldSpace=True, translation=True)
            if pos:
                positions.append(pos)

        if not positions:
            cmds.warning("没有可用的顶点位置！\nNo valid vertex positions!")
            return

        self._building = True
        try:
            create_controllers_from_positions(
                positions,
                base_name,
                ctrl_radius=size_val,
                ctrl_rgb=rgb
            )
        finally:
            self._building = False


def show_create_controllers_ui():
    ui = VertexOrderUI()
    ui.show()
    return ui


if __name__ == "__main__":
    show_create_controllers_ui()</code></pre>
<p><a href="https://www.miaodonghua.com/3553.html">Maya 裙子绑定神器｜按顶点顺序创建控制器 &amp; 关节链｜Skirt Rigging Tool</a>最先出现在<a href="https://www.miaodonghua.com">喵喵动画屋</a>。</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
