自由学习记录(196)

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 页面;navigateToredirectTo 通常用于非 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 在底部还是顶部,常见值是 bottomtop;考试点还包括 colorselectedColorbackgroundColorborderStylelistborderStyle 常见只支持 black/whitelist 里放每个 tab 项,例如 pagePathtexticonPathselectedIconPath。这些配置项属于全局配置,不是页面组件。Uni App+1

project.config.json 是开发者工具项目配置,app.json 是小程序全局配置,sitemap.json 是页面索引/收录配置;app.js/app.json/app.wxss 是主体文件,页面四件套是 .js/.wxml/.wxss/.json。根目录下的 app.jsapp.jsonapp.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-contentalign-itemsCSS / 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-contentalign-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.nametext 来说不一定成立,结构也不符合你想要的"一项 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:ifwx: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" 是给循环出来的每一项加一个稳定身份。

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

pickere.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 动画会驱动这些标准骨。

  1. 让控制骨和变形骨分离

足.L 可以作为控制链,足D.L 作为实际蒙皮变形链。

这样模型作者可以加修正、补助、取消、联动骨,而不直接破坏原始 MMD 控制骨。

  1. 兼容 MMD 工具生态

MMD 依赖大量固定命名和控制逻辑。即使网格最终主要绑在 足D.L,足.L 仍然保留用于兼容动作、IK、物理或工具。

  1. 支持复杂修正

你这个模型有很多:

腰キャンセル

足補助

足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 骨架,把权重/姿态

转过去。

相关推荐
晓py1 小时前
音视频基础概念入门_FFmpeg学习笔记
学习·ffmpeg·音视频
踏着七彩祥云的小丑2 小时前
AI学习——记忆系统
人工智能·学习·ai
xcLeigh2 小时前
Python入门:Python3 operator模块全面学习教程
开发语言·python·学习·教程·python3·operator
Dest1ny-安全2 小时前
2026最新CTF知识库:12大Web漏洞深度文章+1156篇历年大赛WP+50+脚本+Payload速查 +AI/RAG离线在线知识库
java·学习·安全·web安全·servlet
魔法阵维护师2 小时前
从零开发游戏需要学习的c#模块,第三十二章(Boss 战系统)
学习·游戏·c#
洵有兮2 小时前
Shell 脚本编程学习总结(基础 + 变量 + 条件 + 流程控制 + 函数数组)
linux·学习
吃好睡好便好2 小时前
矩阵的左乘和右乘
人工智能·学习·线性代数·算法·matlab·矩阵
我命由我123452 小时前
SEO 与 GEO 极简理解
java·linux·运维·开发语言·学习·算法·运维开发
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章04:YARN资源调度架构
人工智能·hadoop·学习·架构·系统架构·高炉炼铁·高炉炼铁智能化