ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

相机为什么总是手感不对:一篇不讲公式的导数入门

相机为什么总是手感不对:一篇不讲公式的导数入门 开场你给相机写了个跟随逻辑。测试的时候在自己机器上手感很好交给同事试他说有点飘。你们俩的代码一模一样。你给相机画了条样条路径让它平滑飞过。曲线画得很漂亮跑起来相机却忽快忽慢。你把路径上的点摆得更密一些期待它更平滑。结果某个点上相机猛地甩了一下头。这三件事看起来各不相干实际上是同一个概念的三个侧面。而且这个概念在高中数学里就学过叫导数。只是当年没人告诉我们它和相机有什么关系。导数就是变化有多快先把这件事说清楚然后就能用了。位置是你在哪。速度是位置变化有多快。加速度是速度变化有多快。后面一个是前面一个的导数。就这么简单。位置 →求导→ 速度 →求导→ 加速度这不是三个不相干的量是同一条轨迹被看了三遍。看第一遍得到你在哪看第二遍得到你跑多快看第三遍得到你油门踩多重。游戏里最常见的那行代码就是这个关系的直接应用positionvelocity*dt;意思是速度告诉我位置每秒变多少现在过了dt秒那位置就变了velocity * dt。物理引擎的核心就是这一行。它在做的事就是拿着导数反推原来的量。第一个问题相机甩头回到开头那个甩头的相机。你用折线连接了一串点。每一段都是直线直线本身很平滑问题全在接头处。上一段朝东北走下一段突然朝东南走。方向在那一瞬间跳了。相机的朝向就是它运动的方向而方向就是位置的导数。所以用导数的语言来说这里位置是连续的曲线没断开但导数不连续方向跳变了。这种情况有个名字叫C⁰只保证位置接得上。要修它就得让方向也接得上。这个级别叫C¹。样条插值做的正是这件事——用曲线段代替直线段在接头处约定好方向要一致。Catmull-Rom 这类算法的全部工作就是帮你算出每个点该用什么方向让接缝对得上。第二个问题方向顺了但还是顿换成样条之后甩头消失了。但你可能发现某些地方相机还是会顿一下。曲线看起来完全光滑没有任何折角。问题在哪方向对上了速度没对上。导数不只有方向还有大小。方向告诉你往哪走大小告诉你走多快。接缝处两边可以方向一致但速度不同——视觉上看不出来运动起来就是一次突兀的变速。再往上一级是C²加速度也要连续。C² 的症状更微妙你不会看到折角也不会感到明显的变速但高速移动时会有一种被猛拽了一下的感觉。不是方向错了是变化率本身变得太突然。三级对照表把这三级和实际症状对上就变成一张很实用的查错表症状哪一级断了断的是什么看得见折角相机甩头C⁰方向跳变曲线很顺但移动时顿一下C¹速度跳变高速时感觉被拽了一下C²加速度跳变这就是这套记号的全部价值症状能对上级别你就知道该修哪一层。甩头去看方向顿挫去看速度被拽去看加速度。顺便说一句VR 对 C² 的要求比普通游戏高得多。人的前庭系统对加速度变化极其敏感加速度跳变是晕动症的直接诱因。VR 里的相机运动基本是必须做到 C² 的。第三个问题开头那个同事说飘这个最隐蔽而且几乎每个项目都踩过。相机跟随通常这么写cameraPoslerp(cameraPos,targetPos,0.1f);逻辑很直白每帧往目标挪十分之一。看起来没毛病。毛病在**“每帧”**这两个字上。144 帧的机器一秒挪 144 次30 帧的机器一秒挪 30 次。同样的一秒前者追得紧后者追得松。你的手感是在自己机器的帧率上调出来的。换一台机器帧率变了手感跟着变。为什么这是导数问题因为0.1f这个系数描述的是每帧变化多少而我们真正想控制的是每秒变化多少。前者依赖帧率后者不依赖。这两个是不同的变化率混用就出 bug。正确的写法把dt纳进来floatt1.0f-expf(-k*dt);cameraPoslerp(cameraPos,targetPos,t);k是速度参数自己调。关键是现在不管帧率多少相机在同样的时间里走同样的距离。这条影响的范围比你想的大所有的平滑跟随都适用相机跟随UI 元素的回弹瞄准辅助的吸附血条的缓动音量淡入淡出任何写成a lerp(a, b, 常数)的地方都有这个问题。这可能是整篇文章里最值得立刻去改的一条。改动量很小收益是全平台一致的手感。第四个问题沿曲线走忽快忽慢最后一个坑也是最容易误判的。你有一条样条让相机沿着它走。参数t从 0 均匀加到 1for(floatt0;t1.0f;t0.01f){cameraPosevaluate(spline,t);}结果相机忽快忽慢。控制点密集的地方慢稀疏的地方快。很多人的第一反应是插值算法有问题或者缓动没调好。都不是。问题在于t只是个编号不是距离。t 0.5不代表走了一半路程只代表走到了第二段的开头。如果第一段短第二段长那在t 0.5的时候实际路程可能只走了三分之一。为什么会这样因为曲线上每一点的速率是不同的。速率就是导数的大小而样条的导数大小在整条曲线上不是常数——控制点密的地方小疏的地方大。参数均匀前进速率不均匀走出来的距离自然不均匀。解法建一张弧长表做法很直接提前把曲线切成很多小段量一下累积长度存成表。// 建表细分 累加floatlen0;for(inti0;iN;i){floatt0(float)i/N;floatt1(float)(i1)/N;lendistance(evaluate(spline,t0),evaluate(spline,t1));table[i]len;}// 用表想走 5 米查表反查出对应的 tfloattlookup(table,5.0f);vec3 posevaluate(spline,t);每段切 32 到 64 份通常就够用。做完这一步你才真正拥有匀速沿路径移动的能力。而且有了弧长之后缓动才有意义——否则你是在对一个和距离无关的编号做缓动效果当然不对。这件事值得在项目早期就做掉。等到后面发现相机感觉怪再去排查往返成本比一开始建表高得多。顺便说说法线既然讲到导数顺手提一个最直观的应用。地形是高低起伏的。你想知道某处是平地还是陡坡看的就是高度变化有多快。变化慢 平地变化快 陡坡。这个变化有多快就是导数。// 看左右两边的高度差就知道这个方向的坡度floatslopeX(height(xe,z)-height(x-e,z))/(2*e);floatslopeZ(height(x,ze)-height(x,z-e))/(2*e);vec3 normalnormalize(vec3(-slopeX,1.0f,-slopeZ));算出坡度就能算出法线有了法线就能算光照。所以地形的明暗本质上是高度函数的导数在起作用。这也解释了为什么高度图分辨率不够的时候光照会显得块状——导数算得太粗了。一张速查表你遇到的实际原因怎么修相机在拐点甩头方向跳变C⁰换成样条让方向连续曲线很顺但运动顿挫速度跳变C¹检查接缝处切线长度高速时被猛拽一下加速度跳变C²换 C² 连续的样条不同机器手感不同lerp 没考虑 dt用1 - exp(-k*dt)沿路径忽快忽慢参数不等于距离建弧长表地形光照发块导数采样太粗提高高度图精度收束这几个问题看起来分散在不同模块——有的在相机有的在路径有的在渲染。但它们共享同一个底层概念。导数就是变化有多快。位置的导数是速度速度的导数是加速度。手感问题基本都是某一级的变化率突然跳了。方向跳就甩头速度跳就顿挫加速度跳就被拽。所以下次遇到感觉不对但说不清哪里不对的情况可以试着问三遍方向是连续的吗速度是连续的吗加速度是连续的吗从第一个往下问通常到第二个就能定位。而如果问题是换台机器手感就变那基本不用问去找lerp里那个没乘dt的常数。
返回列表