wx.navigateTo()、wx.redirectTo()、wx.switchTab() 之类,是 JS 里的 API ,通常写在页面的 .js 文件里。

按钮写在 WXML,按钮事件绑定到 JS 函数,页面跳转 API 写在这个 JS 函数里面。
也不一定只用于按钮。任何 JS 逻辑里都可以调用,比如登录成功后、表单提交后、支付完成后、倒计时结束后。但考试里最典型就是按钮点击后跳转。

navigator:写在 WXML 里,适合页面上直接放一个"跳转链接/按钮"。
wx.navigateTo 等 API:写在 JS 里,适合需要先执行逻辑,再决定跳不跳、跳到哪里。
简单跳转,用 navigator 就够:
<navigator url="/pages/detail/detail">去详情页</navigator>

实际用途:先处理逻辑,再导航 。页面路由 API 由小程序框架管理页面栈,常见能力包括保留当前页跳新页、返回、重定向、重启、切换 tabBar 页面;navigateTo 和 redirectTo 通常用于非 tabBar 页面,switchTab 用于 tabBar 页面。
redirectTo 也是跳转,但它和 navigateTo 的区别是:redirect 会关闭当前页面,再打开新页面;navigateTo 是保留当前页面,再打开新页面。
你可以把它理解成:
navigateTo:往页面栈上"加一页"
redirectTo:把当前页"换成另一页"

典型使用场景:
navigateTo:
列表页 → 详情页
商品页 → 评论页
文章页 → 作者页
因为用户可能还要返回原页面。
redirectTo:
登录页 → 首页/结果页
提交成功页替换表单页
中间流程页跳到下一步
因为不希望用户返回刚才那个页面。

project.config.json 是微信开发者工具的项目配置文件 。它不是小程序运行时页面逻辑,也不是页面配置。它主要记录开发工具相关设置,例如 AppID、项目名称、编译类型、打包设置、编译设置等。换电脑打开同一个项目时,开发者工具可以根据这个文件恢复项目配置。FinClip
你要把它和 app.json 分清:
project.config.json
= 开发者工具项目配置
app.json
= 小程序运行时全局配置
project.private.config.json 是个人私有配置,通常保存你本机开发者工具里的个人设置。它会覆盖 project.config.json 里相同字段的配置,适合不同开发者各自保留本机配置。一般多人协作时,project.private.config.json 不一定提交到版本库。搜索结果里也能看到它被描述为"项目私有配置文件,内容会覆盖 project.config.json 中相同字段"。GitHub
project.config.json 是项目配置文件,用于保存微信开发者工具的项目相关配置。
project.private.config.json 是项目私有配置文件,用于保存个人本地配置,可覆盖 project.config.json 中的相同字段。

app.json 里可能会看到:
JSON
{
"sitemapLocation": "sitemap.json"
}
这表示指定 sitemap 配置文件的位置。你可以理解成告诉小程序:"我的 sitemap 配置文件在这里。"
它写在 app.json 里,不写在某个页面的 WXML 里。position 主要就是控制 tabBar 在底部还是顶部,常见值是 bottom 和 top;考试点还包括 color、selectedColor、backgroundColor、borderStyle、list。borderStyle 常见只支持 black/white,list 里放每个 tab 项,例如 pagePath、text、iconPath、selectedIconPath。这些配置项属于全局配置,不是页面组件。Uni App+1
project.config.json 是开发者工具项目配置,app.json 是小程序全局配置,sitemap.json 是页面索引/收录配置;app.js/app.json/app.wxss 是主体文件,页面四件套是 .js/.wxml/.wxss/.json。根目录下的 app.js、app.json、app.wxss 是应用级文件,其中 app.json 是必须重点掌握的全局配置。course.talelin.com
注册/登录小程序账号,拿 AppID,安装微信开发者工具,创建小程序项目,选择目录并填写 AppID,开发调试,预览/真机调试,上传。
知道 navigateTo / navigateBack / redirectTo / reLaunch / switchTab 的区别
页面路由的本质是"页面栈变化"。navigateTo 加一页,navigateBack 退几页,redirectTo 替换当前页,reLaunch 清空后打开,switchTab 切到 tabBar 页面。

你以后做任何前端、游戏联机、UE HTTP、Unity WebRequest、App 请求后端,都会遇到类似的"客户端不能随便访问所有服务,要经过域名、安全、证书、白名单、跨域或平台审核限制"的问题。
container 只是你自己起的 class 名。它经常被用来表示"页面最外层容器",所以很多人会在 .container 里写 flex 布局,但它本身不是微信内置,也不是 flex 的固定语法。


display: flex;
justify-content: center;
align-items: center;
class="container" 中的 container 是开发者自定义的样式类名,不是小程序内置组件,也不是 flex 固定关键字。只有当该类在 WXSS 中设置 display: flex 时,该元素才成为 flex 容器。
小程序要求配置合法服务器域名,表面上是技术限制,实际是在保护几方利益:
第一,保护用户 。
如果小程序可以随便请求任意域名,恶意开发者就可以把用户信息、登录态、表单内容、设备信息发到临时服务器、钓鱼服务器、境外未知服务器。配置合法域名以后,至少要求开发者提前声明:这个小程序会和哪些服务器通信。这样可以降低数据被偷偷转发的风险。
保护开发者/商家自己 。
合法域名配置也能防止项目代码被误改、被注入、或被错误环境劫持。比如正式版小程序本来应该请求:
https://api.example.com
但如果代码被人改成:
https://fake-example.com
在正式环境下,如果这个域名没配置,就请求不出去。这对开发者反而是保护,避免用户数据、订单数据、支付流程被错误发送到非授权服务器。

但再精确一点,不只是"可以向外访问",而是按网络能力分成几类白名单:
request 合法域名
= wx.request 可以请求哪些接口服务器
uploadFile 合法域名
= wx.uploadFile 可以把文件上传到哪些服务器
downloadFile 合法域名
= wx.downloadFile 可以从哪些服务器下载文件
socket 合法域名
= wx.connectSocket 可以和哪些服务器建立长连接
但不是所有游戏请求都走 socket。很多游戏会混用:
登录、拉取用户资料、排行榜、商城、公告
→ 普通 HTTP 请求
→ wx.request
→ request 合法域名
上传头像、上传存档、上传图片
→ wx.uploadFile
→ uploadFile 合法域名
下载资源包、图片、配置文件
→ wx.downloadFile
→ downloadFile 合法域名
实时聊天、房间状态、战斗同步、匹配状态推送
→ WebSocket / socket
→ socket 合法域名
所以考试/理解上可以写:
如果小程序需要实时双向通信,例如联机游戏、聊天室、实时推送,通常使用 wx.connectSocket,并需要配置 socket 合法域名。但普通接口请求仍然使用 wx.request,对应 request 合法域名。
一句话:实时通信偏 socket;普通业务接口偏 request。
来自 CSS 的弹性布局。小程序的 WXSS 基本沿用了 CSS 的 flex 布局规则,
.box {
display: flex;
}
意思是:把 .box 这个元素变成 flex 容器。它的直接子元素会按 flex 规则排列。 MDN 对 flex 对齐的解释也是围绕"主轴 main axis"和"交叉轴 cross axis":justify-content 管主轴方向的空间分配,align-items 管交叉轴方向的对齐。MDN Web Docs+1

display: flex 只做一件事:开启弹性布局模式。开启以后,下面这些属性才开始有意义:
CSS
.box {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
你可以这样理解:
flex-direction:决定主轴方向。
justify-content:沿着主轴怎么排。
align-items:沿着交叉轴怎么排。
默认情况下:
CSS
flex-direction: row;
所以主轴是横向,从左到右。此时:
justify-content 管左右方向
align-items 管上下方向
如果改成:
CSS
flex-direction: column;
主轴变成纵向,从上到下。此时:
justify-content 管上下方向
align-items 管左右方向
这就是 flex 最容易错的地方:justify-content 不永远等于水平居中,align-items 也不永远等于垂直居中。它们看的是主轴和交叉轴。主轴由 flex-direction 决定。


Flex 里先定一个"主要排列方向",这个方向叫主轴 。跟它成 90 度的方向,就叫交叉轴。
比如默认:
CSS
.box {
display: flex;
flex-direction: row;
}
row 表示子元素横着排:
主轴:横向 →
A B C
交叉轴:纵向 ↓
所以这时:
justify-content 管横向怎么排
align-items 管纵向怎么对齐
比如:
CSS
.box {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
意思是:A/B/C 这几个子元素整体横向居中 ,同时每个子元素在容器高度里纵向居中。
justify-content、align-items 是 CSS / W3C 标准里的英文属性名 ,微信小程序的 WXSS 只是沿用了 CSS Flexbox 的命名。MDN 和 W3C 文档里也是这套说法:justify-content 处理主轴方向的空间分配,align-items 处理交叉轴方向的项目对齐。MDN Web Docs+1




html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Flex 主轴和交叉轴对比</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 24px;
background: #f5f5f5;
}
h2 {
margin-top: 32px;
}
.note {
margin-bottom: 16px;
line-height: 1.6;
}
.demo-row {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.panel {
width: 360px;
background: white;
border: 2px solid #333;
padding: 12px;
}
.title {
font-weight: bold;
margin-bottom: 8px;
}
.desc {
font-size: 14px;
margin-bottom: 8px;
line-height: 1.5;
}
.box {
width: 320px;
height: 180px;
border: 4px solid red;
background: #fff7f7;
position: relative;
}
.box::before {
content: "flex 容器边界";
position: absolute;
left: 8px;
top: 6px;
font-size: 12px;
color: red;
}
.item {
width: 52px;
height: 42px;
border: 2px solid blue;
background: #e8f0ff;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
}
.row-center-top {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
.column-top-center {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.row-center-center {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.column-center-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.row-space-between {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.column-space-between {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
code {
background: #eee;
padding: 2px 4px;
}
</style>
</head>
<body>
<h1>Flex:flex-direction、justify-content、align-items 对比</h1>
<p class="note">
红色边框是 <code>flex 容器</code>。蓝色边框是 <code>flex 项目</code>。
<br>
<code>flex-direction</code> 决定 A/B/C 是横着排还是竖着排。
<code>justify-content</code> 管主轴。
<code>align-items</code> 管交叉轴。
</p>
<h2>一、看似"反过来",但结果不一样</h2>
<div class="demo-row">
<div class="panel">
<div class="title">row:横向排列</div>
<div class="desc">
flex-direction: row;<br>
justify-content: center;<br>
align-items: flex-start;
</div>
<div class="box row-center-top">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
</div>
<div class="panel">
<div class="title">column:竖向排列</div>
<div class="desc">
flex-direction: column;<br>
justify-content: flex-start;<br>
align-items: center;
</div>
<div class="box column-top-center">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
</div>
</div>
<p class="note">
上面两个都可以说"靠上 + 某方向居中",但结果不是同一个。
左边 A/B/C 横着排;右边 A/B/C 竖着排。
这说明 <code>flex-direction</code> 改变的是排列结构,不只是描述方式。
</p>
<h2>二、都写 center,也不代表一样</h2>
<div class="demo-row">
<div class="panel">
<div class="title">row + 双居中</div>
<div class="desc">
flex-direction: row;<br>
justify-content: center;<br>
align-items: center;
</div>
<div class="box row-center-center">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
</div>
<div class="panel">
<div class="title">column + 双居中</div>
<div class="desc">
flex-direction: column;<br>
justify-content: center;<br>
align-items: center;
</div>
<div class="box column-center-center">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
</div>
</div>
<p class="note">
两个都是"主轴居中 + 交叉轴居中",但 row 的主轴是横向,所以 A/B/C 横着居中;
column 的主轴是纵向,所以 A/B/C 竖着居中。
</p>
<h2>三、space-between 更能看出差别</h2>
<div class="demo-row">
<div class="panel">
<div class="title">row + space-between</div>
<div class="desc">
flex-direction: row;<br>
justify-content: space-between;<br>
align-items: center;
</div>
<div class="box row-space-between">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
</div>
<div class="panel">
<div class="title">column + space-between</div>
<div class="desc">
flex-direction: column;<br>
justify-content: space-between;<br>
align-items: center;
</div>
<div class="box column-space-between">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
</div>
</div>
<p class="note">
<code>justify-content: space-between</code> 永远沿主轴分配空间。
row 时横向拉开;column 时纵向拉开。
</p>
</body>
</html>
justify-content
永远管主轴。
align-items
永远管交叉轴。



注意:container 不是内置标签,所以如果你写:
CSS
container {
padding: 32rpx;
}
它一般匹配不到:
XML
<view class="container"></view>
正确是:
CSS
.container {
padding: 32rpx;
}

class 默认只作用在"带这个 class 的组件自身"。如果子组件没有使用这个 class,子组件不会"自动拥有"这个 class。






点击某个 radio
→ radio-group 发现当前选中项变了
→ 因为 WXML 写了 bindchange="onRadioChange"
→ 微信小程序框架自动去 JS 里找 onRadioChange 这个函数
→ 框架自动把事件对象 e 传进去
所以 e 不是你创建的。它是微信小程序框架创建并传入的事件对象。
// 这是框架内部大概做的事,不是你写的
页面对象.onRadioChange(事件对象e)

小程序的路由是"页面栈模型"。navigateTo / redirectTo / reLaunch / switchTab / navigateBack 不是简单换场景,而是明确操作页面栈。Unity 里你也能做 UI Stack,但那通常是你自己或框架实现;小程序是平台直接给你固定模型。
小程序的网络受平台强管控。Unity 做网络,请求哪个服务器更多是你自己和操作系统/服务器安全策略的问题;小程序必须在微信后台配置合法域名,否则正式环境请求不出去。这是微信生态的强平台特色。
小程序有"微信生态入口"。登录、支付、分享、扫码、地理位置、订阅消息、客服、微信搜索索引、tabBar 这些都不是普通前端 UI 概念,而是微信把一个轻应用嵌进自己生态后的能力和限制。你现在的考试重点里没有全部展开,但 服务器域名配置 / sitemap / tabBar / 页面路由 已经能看到这种平台味。


display:flex、主轴、交叉轴、justify-content、align-items 的核心逻辑
文本类属性会继承,盒子/布局类属性通常不继承。
更深的复用,比如公共样式拆分、BEM 命名、组件样式隔离、主题变量、设计系统,

input:输入框组件
placeholder:提示文字
bindinput:输入过程中触发

onKeywordInput:JS 里的函数
e.detail.value:用户当前输入的内容
keyword:存到 data 里的页面状态
{{keyword}}:WXML 读取 data.keyword


checkbox 是多选。

radio-group 监听的是内部 radio 的选中变化,并且按"单选"的规则产生 e.detail.value。你把 checkbox 放进去,语义和事件机制都不匹配。即使界面可能显示出来,也不能按正确的 radio-group 单选逻辑理解。
label 可以包在里面,因为它只是辅助点击文字选中控件:
XML
<radio-group bindchange="onRadioChange">
<label wx:for="{{items}}" wx:key="value">
<radio value="{{item.value}}" checked="{{item.checked}}" />
<text>{{item.name}}</text>
</label>
</radio-group>
基本可以写在很多 WXML 标签上,但不是"任何地方都随便写"。
它们本质上是 WXML 的控制属性:
XML
wx:for
wx:key
wx:if
wx:elif
wx:else
这些不是普通样式,也不是 JS 代码,而是写在 WXML 标签上的渲染控制。
例如可以写在 view 上:
XML
<view wx:for="{{items}}" wx:key="value">
{{item.name}}
</view>
也可以写在 label 上:
XML
<label wx:for="{{items}}" wx:key="value">
<radio value="{{item.value}}" />
<text>{{item.name}}</text>
</label>
也可以写在 radio 上:
XML
<radio
wx:for="{{items}}"
wx:key="value"
value="{{item.value}}"
checked="{{item.checked}}"
/>
但是这里有一个关键区别:写在哪个标签上,就重复哪个标签。
比如你写:
XML
<label wx:for="{{items}}" wx:key="value">
<radio value="{{item.value}}" />
<text>{{item.name}}</text>
</label>
结果是重复整个 label:
label + radio + text
label + radio + text
label + radio + text
这是推荐写法。
如果你写:
XML
<label>
<radio wx:for="{{items}}" wx:key="value" value="{{item.value}}" />
<text>{{item.name}}</text>
</label>
这就有问题。因为只有 radio 被循环,text 没有在循环里。item.name 对 text 来说不一定成立,结构也不符合你想要的"一项 radio 对应一项文字"。
所以不是看"能不能写",而是看"你想重复谁"。
wx:if 也是类似,写在哪个标签上,就控制哪个标签是否渲染:
XML
<view wx:if="{{show}}">
这块 show 为 true 才显示
</view>
如果你想一次控制多个标签,但又不想多包一个真实的 view,可以用 block:
XML
<block wx:if="{{show}}">
<view>第一行</view>
<view>第二行</view>
</block>
block 不是实际组件,它只是一个包装节点,适合配合 wx:for / wx:if 控制一组结构。
例如:
XML
<block wx:for="{{items}}" wx:key="value">
<view>{{item.name}}</view>
<view>{{item.value}}</view>
</block>
意思是每个 item 生成两行,而不是多生成一个外层容器。
wx:key 只能和 wx:for 搭配使用。它的作用是告诉框架每一项的唯一标识,尤其当列表会新增、删除、排序时,用来保持每个列表项自己的状态。微信文档里也说明,如果列表项目位置会动态改变或新增项目,并且希望项目保持自己的特征和状态,就需要使用 wx:key 指定唯一标识。Tencent Cloud
所以这个是对的:
XML
<view wx:for="{{items}}" wx:key="value">
{{item.name}}
</view>
这个就没有意义:
XML
<view wx:key="value">
{{item.name}}
</view>
因为没有 wx:for,就没有"列表中的每一项",wx:key 没有对象可标识。
wx:if / wx:elif / wx:else 要注意顺序关系:
XML
<view wx:if="{{score >= 60}}">
及格
</view>
<view wx:else>
不及格
</view>
wx:else 不能单独用,它必须跟在 wx:if 或 wx:elif 后面。
错误理解是:
XML
<view wx:else>
不及格
</view>
单独写 wx:else 没意义。
你现在这段代码里:
XML
<label
class="option"
wx:for="{{items}}"
wx:key="value"
>
<radio
value="{{item.value}}"
checked="{{item.checked}}"
/>
<text>{{item.name}}</text>
</label>
是很标准的结构。wx:for 写在 label 上,表示每一个选项都生成一个完整的 label,里面包含一个 radio 和一段文字;items 来自页面 data,循环时生成临时 item,这一点和你之前代码里的注释是一致的。

checked 为 true 的 checkbox 初始处于勾选状态;
只要它当前是勾选状态,它的 value 就会出现在 checkbox-group 的 e.detail.value 数组里。





不是绑定数据源。数据源已经由 wx:for="{``{hobbies}}" 决定了。wx:key="value" 是给循环出来的每一项加一个稳定身份。

这对普通文字列表影响不明显,但对 checkbox、radio、input 这种有内部状态的组件很重要。它可以帮助框架在列表变化时尽量保留正确项的状态,而不是把状态错配到别的项上。




picker 的 e.detail.value 通常是选中项的下标,比如 "1"
打开 switch:e.detail.value = true
关闭 switch:e.detail.value = false

如果用户输入姓名 Tom,选择 male,那么:
e.detail.value = {
username: 'Tom',
gender: 'male'
}
这里和前面的 bindinput 模式不一样。前面是"边输入边更新 data";form 是"点提交时一次性收集表单值"。
一次性提交里面带 name 的表单组件。
input
用途:单行输入
常用事件:bindinput
取值:e.detail.value,字符串
textarea
用途:多行输入
常用事件:bindinput
取值:e.detail.value,字符串
radio-group
用途:单选
常用事件:bindchange
取值:e.detail.value,单个 value
checkbox-group
用途:多选
常用事件:bindchange
取值:e.detail.value,数组
picker
用途:选择器
常用事件:bindchange
取值:e.detail.value,通常是下标
switch
用途:开关
常用事件:bindchange
取值:e.detail.value,布尔值
form
用途:表单整体提交
常用事件:bindsubmit
取值:e.detail.value,对象
表单组件不是自己神秘地保存结果。
用户操作以后,结果会通过事件对象 e 传给 JS。
JS 再决定要不要 setData、跳转、请求服务器、显示提示。
selector:
e.detail.value = 单列下标
multiSelector:
e.detail.value = 多列下标数组
time:
e.detail.value = 时间字符串
date:
e.detail.value = 日期字符串
region:
e.detail.value = 省市区数组
大布局用 rpx,细节固定值用 px。


YOLO 原本是網路語「You Only Live Once」,在工具語境裡就是:不要反覆確認,直接執行。
在 Codex CLI 裡,它基本等價於這組配置:
TOML
approval_policy = "never"
sandbox_mode = "danger-full-access"
也就是:
Codex 不再每次問你「允許執行嗎」;
不再限制在只讀或工作區寫入;
它可以直接讀寫你本機可訪問的文件、執行命令、改配置、啟動工具;
如果命令有問題,也會直接造成結果,不會先停下來讓你按確認。
空心/灰色骨骼圖標 ,通常表示這根 bone 存在於 Skeleton hierarchy 裡,但對當前這個 Skeletal Mesh 沒有直接 skin weight / 沒有頂點權重 。也就是說:UE 不是看它名字猜的,而是從當前 Skeletal Mesh 的蒙皮信息裡知道「沒有 vertex 被這根骨骼直接影響」。Epic 論壇裡也有類似說法:灰掉的 bones 常見原因是沒有 skin weighting;導入警告裡也會提到某些 bones 不在 bind pose,這可能發生在沒有 vertex weight 的骨骼上。Epic Developer Community Forums+1

Skeleton 裡有尾巴,但 IK Rig 裡沒有尾巴,這本身不是錯。
尾巴不參與人形 locomotion retarget 很正常。Quinn/Manny 也沒有尾巴,所以 IK Rig 不需要給尾巴建 chain。
FBX 里额外写入 mesh 每个顶点/面角的 tangent / binormal 数据,让 UE 在解释 tangent-space normal map 时有可用的切线基底。
File > Export > FBX (.fbx)
右侧导出设置里找:
Geometry > Tangent Space
把它勾上,就是脚本里的:
Python
use_tspace=True
你可以这样理解:
Normal:表面朝向。
Tangent:沿着 UV 的横向方向。
Binormal / Bitangent:沿着 UV 的另一个方向。
这三个方向合起来,才构成 normal map 能被正确解释的局部坐标系。
所以它服务的是这种链条:
模型表面
+ UV
+ normal
+ tangent / bitangent
+ tangent-space normal map
= UE 里正确的细节凹凸方向
如果你不导出 tangent space,UE 仍然可以自己计算 tangent。UE 的 FBX 导入里有三种常见模式:Compute Normals、Import Normals、Import Normals and Tangents。官方文档说明,Import Normals 会导入 FBX 法线但由引擎计算 tangents;Compute Normals 会丢弃 FBX 的 smoothing group 和 normal 信息,由引擎重算。
UE 导入:
Normal Import Method = Import Normals
Tangents = 让 UE/MikkTSpace 算
Keep Offset :兩根骨骼建立父子關係,但位置不強制首尾相連。
Connected:子骨的 head 會連到父骨的 tail,形成真正連續骨鏈。
對你現在這類 MMD 骨架,通常更安全的是 Keep Offset,不要一開始就 Connected。因為 Connected 會改 rest pose 的骨骼位置,可能導致原模型變形、導出 FBX 後 reference skeleton 改變。
這種方式的效果是:A 鏈起點動,B 鏈整條會跟著動。
代價是:它改了 skeleton hierarchy,進 UE 後會被看作新的骨架結構。
用一根共同父骨 / 中介骨
這是最穩的 rigging 思路。
不要讓 A 鏈和 B 鏈互相當 parent,而是建立一根新的中介骨,例如:
Shared_Root
├─ Chain_A_start
└─ Chain_B_start
這樣兩條鏈不是互相綁死,而是共享一個上級控制。
這種做法的好處是清楚:
Shared_Root 負責整體位移。
Chain_A_start 保留 A 鏈自己的局部控制。
Chain_B_start 保留 B 鏈自己的局部控制。
Pose Mode 加 Constraint,不改原始骨架結構
這是後期建立聯繫時最常用,也最不破壞原骨架的方法。
常用約束有:
Copy Location
Copy Rotation
Copy Transforms
Child Of
Armature Constraint
其中 Copy Transforms 會讓一個 bone 匹配目標的 location、rotation、scale。Blender 官方手冊說 Copy Transforms constraint 會強制 owner 匹配 target 的 transform,等於把 location/rotation/scale 組合起來處理。Blender Documentation
例子:
Chain_B_start Copy Transforms -> Chain_A_start
效果:B 鏈起點跟隨 A 鏈起點。
如果你不想完全跟隨,可以只用:
Copy Location
只跟位置。
Copy Rotation
只跟旋轉。
Copy Location + Influence 0.5
半跟隨。
這種方式的核心價值是:不改 Edit Mode 裡的 reference skeleton,只是在 Pose Mode 裡建立運動依賴。
對 MMD → UE 的情況,這類 constraint 在 Blender 裡很有用,但導出 FBX 到 UE 時要小心:UE 不一定保留 Blender constraint 的 rig 行為。通常需要 bake animation,或者在 UE 裡用 Control Rig / IK Rig / Anim Blueprint 重建類似邏輯。
Weight Paint 視口上方的 Bone Selection 開關打開。
進入 Weight Paint Mode 後,看 3D Viewport 上方 header,靠近頂點/面選擇遮罩那一排,找一個與骨骼選擇相關的圖標,通常叫 Bone Selection 。打開它。之後不是用普通畫筆邏輯選骨,而是切換到「骨骼選擇」語境。很多 Blender 4.1+ 使用者反映現在是 Alt + Left Click 選骨,而不是 Ctrl + Left Click;Blender Artists 2025 的討論也明確說現在要按 Alt 點骨骼,且前提是 Armature 先選、再 Shift 選 Mesh、再進 Weight Paint。Blender Artists Community
为什么要有 足.L:给 MMD 动画和 IK 用
MMD 动画通常认的是标准骨名,比如:
足.L
ひざ.L
足首.L
つま先.L
足IK.L
这些骨用于动画控制、IK 约束、姿态操作。很多 VMD 动画会驱动这些标准骨。
- 让控制骨和变形骨分离
足.L 可以作为控制链,足D.L 作为实际蒙皮变形链。
这样模型作者可以加修正、补助、取消、联动骨,而不直接破坏原始 MMD 控制骨。
- 兼容 MMD 工具生态
MMD 依赖大量固定命名和控制逻辑。即使网格最终主要绑在 足D.L,足.L 仍然保留用于兼容动作、IK、物理或工具。
- 支持复杂修正
你这个模型有很多:
腰キャンセル
足補助
足D
ひざD
足首D
_dummy
_shadow
IK
这说明作者做了比较复杂的腿部修正链。足.L 不一定直接带权重,但它可能通过约束、驱动或层级间接影响 足D.L。
对 UE 来说问题就在这里:UE retarget 通常不理解这套 MMD 控制/变形分离逻辑。它想要的是:
thigh_l -> calf_l -> foot_l
而你这里有:
控制链: 足.L -> ひざ.L -> 足首.L
变形链: 足D.L -> ひざD.L -> 足首D.L
所以导入 UE 后如果映射到了 足.L,但网格实际权重在 足D.L,下半身就会不对。
处理建议:UE retarget 时优先映射 足D.L / ひざD.L / 足首D.L 这一套,或者在 Blender 里做一套干净的 UE 骨架,把权重/姿态
转过去。