ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

树莓派驱动ST7735小屏:用Python打造机器人灵动眼睛动画

树莓派驱动ST7735小屏:用Python打造机器人灵动眼睛动画 1. 一块1.8寸小屏怎么就成了机器人的灵魂第一次把 ST7735 这块 1.8 寸、128x160 分辨率的 TFT LCD 接到树莓派上点亮一张静态图片的时候说实话没什么特别的感觉——毕竟现在随便一块屏幕都能显示图片。但当我把它做成一对会眨眼、会转动、会跟随的机器人眼睛之后整个项目的性质就变了一块几十块钱的小屏加上一段 Python 动画逻辑居然能让一个纸盒子做的机器人瞬间活过来。这就是Robot Eyes这个项目的核心价值用树莓派驱动 ST7735 小尺寸 LCD通过 SPI 接口高速刷新画面配合 Python 编写的动画状态机实现平滑、自然、有生命感的眼睛动画。它解决的不是能不能显示的问题而是怎么让显示看起来像活的这个问题。适合的人群很明确做机器人外观交互的创客、想给桌面小装置加表情的 DIY 爱好者、以及刚接触树莓派 SPI 屏幕、想找一个有成就感的入门项目的朋友。我前后折腾过好几版眼睛动画从最初一秒钟闪三下的抽搐眼到后来能自然眨眼、瞳孔微动、情绪切换的版本中间踩的坑基本都集中在三个地方SPI 刷新率不够导致动画卡顿、帧缓冲策略不对导致撕裂、以及动画节奏设计得太机械。这篇就把整个链路从硬件接线、驱动配置、动画架构到调优经验完整讲一遍你照着做基本能少走我走过的弯路。2. 硬件选型与接线为什么是 ST7735 而不是别的屏2.1 ST7735 在机器人眼睛场景下的真实优势市面上能接树莓派的小屏很多常见的有 ST7735、ST7789、ILI9341还有各种 OLED 和段码屏。做机器人眼睛这个特定场景选型的核心指标其实不是分辨率而是刷新速度、体积、成本和 SPI 兼容性这四项。ST7735 的 1.8 寸 128x160 版本在这四项上表现很均衡。128x160 的分辨率做一对眼睛绰绰有余——眼睛本身不需要显示复杂内容主要是圆形、椭圆、瞳孔这些几何图形分辨率太高反而增加刷新负担。1.8 寸的物理尺寸刚好适合放在拳头大小的机器人头部两块并排也不会显得突兀。SPI 接口意味着只需要 4 到 5 根线就能驱动接线简单GPIO 占用少这对树莓派这种 GPIO 资源有限但又想接一堆传感器的场景非常友好。对比一下就更清楚了。ILI9341 是 2.4 寸 240x320分辨率高但刷新一帧的数据量是 ST7735 的 3.75 倍用 SPI 跑全屏刷新很容易掉到 20fps 以下做平滑动画就吃力。OLED 虽然对比度高、黑色纯净但尺寸普遍偏小而且很多是 I2C 接口带宽更低刷新整屏动画更慢。段码屏就更不用说了只能显示固定笔画做不了瞳孔移动这种连续动画。提示如果你手头是 ST7789常见于 1.3 寸 240x240 的 IPS 屏驱动逻辑和 ST7735 高度相似本文的动画架构完全通用只需要换一下初始化参数和偏移量。2.2 接线方案与引脚分配ST7735 模块通常引出 8 个引脚VCC、GND、CS、RESET、DC也叫 A0、SDAMOSI、SCLSCLK、LED背光。树莓派这边用硬件 SPI0对应 GPIO 的 8、9、10、11 这几个脚。ST7735 引脚树莓派 GPIO物理引脚号说明VCC3.3V1 或 17供电务必 3.3VGNDGND6 或 9共地CSGPIO8 (CE0)24片选硬件 SPI 片选RESETGPIO2522复位可软件控制DCGPIO2418数据/命令切换SDAGPIO10 (MOSI)19数据线SCLGPIO11 (SCLK)23时钟线LEDGPIO18 或 3.3V12背光PWM 可调亮度这里有个细节值得说LED 背光脚建议接 GPIO18 而不是直接接 3.3V。原因很简单机器人眼睛在睡觉或者待机的时候如果能把背光调暗甚至关掉整个角色的情绪表达会丰富很多。GPIO18 支持硬件 PWM用pigpio或者rpi_ws281x那套 PWM 逻辑可以做出呼吸灯效果。我实测下来背光从 100% 降到 20% 再配合瞳孔缩小那种困了的感觉一下就出来了。注意ST7735 模块的 VCC 一定要接 3.3V不要接 5V。虽然有些模块标称支持 5V但逻辑电平是 3.3V 的接 5V 长期运行容易烧逻辑芯片而且树莓派的 GPIO 是 3.3V 电平混用会有通信不稳定的风险。2.3 硬件 SPI 与软件 SPI 的取舍树莓派有硬件 SPISPI0、SPI1和软件 SPIbit-banging两种方式。做眼睛动画必须用硬件 SPI原因在于刷新率。硬件 SPI 在树莓派上可以稳定跑到 40MHz 甚至更高取决于具体型号和配置而软件 SPI 受限于 Python 的 GPIO 操作速度通常只能到几百 kHz刷一帧 128x160 的 16 位色画面要几十毫秒动画直接卡成幻灯片。配置硬件 SPI 只需要在raspi-config里打开 SPI 接口或者在/boot/config.txt里确认有dtparamspion这一行。打开之后/dev/spidev0.0和/dev/spidev0.1就会出现对应 CE0 和 CE1 两个片选。如果你要驱动两只眼睛可以用两个片选分别控制两块屏也可以两块屏共用片选、用 DC 脚区分——但更推荐的做法是两块屏各用一个片选这样刷新逻辑更清晰也方便做左右眼不同步的动画比如一只眼眨、一只眼不眨。3. 驱动层搭建从点亮第一帧到稳定刷新3.1 驱动库的选择与对比Python 驱动 ST7735 的库有好几个常见的有st7735、Adafruit-ST7735、luma.lcd还有直接基于spidev手写命令的。我实际用下来做机器人眼睛这个场景luma.lcd配合spidev是最稳的组合。luma.lcd的好处是它把 ST7735 的初始化序列、偏移量、旋转、颜色格式都封装好了而且支持直接往 framebuffer 里写 PIL Image 对象这对做动画非常关键——你可以用 Pillow 画圆、画椭圆、做渐变然后一次性推送到屏幕。Adafruit那套库功能也全但依赖比较多在树莓派上装起来偶尔会有版本冲突。手写spidev命令最轻量但初始化序列要自己对着数据手册一条条写调试成本高除非你有特殊需求否则没必要。安装luma.lcd很简单pip install luma.lcd pillow如果提示权限问题加--user或者用虚拟环境。我强烈建议用虚拟环境因为树莓派系统自带的 Python 环境里包很多直接 pip 装容易和系统包打架。python3 -m venv roboteyes source roboteyes/bin/activate pip install luma.lcd pillow spidev3.2 初始化参数里最容易搞错的三个地方ST7735 有好几个版本红板、黑板、绿板还有 0.96 寸、1.44 寸、1.8 寸各种尺寸。它们的初始化参数和显示偏移量是不一样的。如果你买的是 1.8 寸 128x160 的模块但用了 1.44 寸的参数画面就会偏移或者花屏。第一个坑是偏移量offset。很多 1.8 寸 ST7735 模块的实际显存是 132x162但可视区域是 128x160所以需要设置x_offset2, y_offset1或者类似的值。具体是多少要看你的模块最笨但最有效的办法是先设 0看画面偏多少然后手动补。我手上这块红板 1.8 寸最终用的是x_offset0, y_offset0但另一块黑板就需要x_offset2, y_offset1。第二个坑是颜色格式。ST7735 支持 12 位、16 位、18 位色但最常用的是 16 位 RGB565。luma.lcd默认就是 RGB565但如果你手动发命令一定要确认COLMOD参数设对了否则颜色会偏得离谱——红色变蓝色、绿色变紫色这种。第三个坑是旋转方向。机器人眼睛通常是横屏还是竖屏1.8 寸 128x160 默认是竖屏128 宽 160 高但做眼睛一般希望横过来160 宽 128 高这样两只眼睛并排更自然。luma.lcd里用rotate1或rotate3来旋转 90 度或 270 度。from luma.core.interface.serial import spi from luma.lcd.device import st7735 from luma.core.render import canvas serial spi(port0, device0, gpio_DC24, gpio_RST25, bus_speed_hz32000000) device st7735(serial, width160, height128, rotate1, bgrTrue, h_offset0, v_offset0)bus_speed_hz设到 32MHz 是我实测下来比较稳的值。再往上比如 40MHz有些模块会开始出现噪点或者偶发花屏尤其是杜邦线比较长的时候。如果你用的是排线或者 PCB 直接插接可以试着往上调。3.3 用 canvas 做双缓冲避免画面撕裂luma.lcd的canvas上下文管理器本质上就是双缓冲你在内存里的 PIL Image 上画完所有内容退出with块的时候才一次性推送到屏幕。这个机制对动画至关重要否则你会看到画到一半的画面——比如瞳孔画了一半、眼白还没画屏幕就刷新了看起来就是闪烁和撕裂。with canvas(device) as draw: draw.ellipse((x1, y1, x2, y2), fillwhite) draw.ellipse((px1, py1, px2, py2), fillblack)但这里有个性能陷阱每帧都重新创建 canvas 对象是有开销的。如果你的动画要跑到 30fps 以上频繁创建销毁 canvas 会拖慢速度。更高效的做法是维护一个持久的 PIL Image 对象每帧在它上面重绘然后调用device.display(image)推送。不过luma.lcd的display方法内部还是走 SPI 全屏传输128x160x2 字节约 40KB32MHz SPI 理论传输时间约 10ms加上 Python 绘制开销实际能跑到 25 到 30fps 左右做眼睛动画够用了。提示如果你追求更高帧率可以考虑只刷新变化区域dirty rectangle但 ST7735 设置局部刷新窗口比较麻烦而且眼睛动画大部分区域都在变收益不大。老老实实全屏刷新把 SPI 速度拉满是最省心的方案。4. 眼睛动画的核心状态机与运动曲线4.1 为什么直接画圆看起来像死鱼眼很多人做机器人眼睛的第一版是这样的画一个白色椭圆当眼白画一个黑色圆当瞳孔然后让瞳孔左右移动。结果看起来非常机械像监控摄像头而不是有生命的眼睛。问题出在运动曲线和微动作上。真实的眼球运动有几个特征第一瞳孔不是匀速移动的而是有加速和减速专业上叫 saccade扫视特点是启动快、中间快、结尾有微小过冲然后回稳。第二眼睛几乎从不完全静止即使在盯着一个方向看的时候也会有微小的漂移和震颤。第三眨眼不是简单的上下闭合而是上眼睑快速下落、短暂停顿、再稍慢地抬起而且眨眼频率不固定。所以做 Robot Eyes核心不是图形绘制而是用数学曲线模拟这些生物特征。我最终用的方案是瞳孔位置用缓动函数easing function驱动眨眼用状态机控制再叠加一层低频噪声做微动。4.2 缓动函数让瞳孔移动有重量感缓动函数说白了就是让参数变化不是线性的而是先慢后快再慢或者带一点弹性。做眼睛最常用的是easeInOutQuad和easeOutBack。import math def ease_in_out_quad(t): if t 0.5: return 2 * t * t return 1 - pow(-2 * t 2, 2) / 2 def ease_out_back(t): c1 1.70158 c3 c1 1 return 1 c3 * pow(t - 1, 3) c1 * pow(t - 1, 2)easeInOutQuad适合瞳孔从一个位置移动到另一个位置起步和停止都很柔和。easeOutBack带一点过冲适合做突然看向某处的效果——瞳孔会稍微冲过目标位置再弹回来看起来更有生气。实现上我给每只眼睛维护一个target_x, target_y和current_x, current_y每帧用缓动函数把 current 往 target 推。移动时间设 0.15 到 0.3 秒比较自然太快像抽搐太慢像发呆。class Eye: def __init__(self, cx, cy, radius): self.cx cx self.cy cy self.radius radius self.pupil_x cx self.pupil_y cy self.target_x cx self.target_y cy self.move_progress 1.0 self.move_duration 0.2 def look_at(self, tx, ty): self.target_x tx self.target_y ty self.move_progress 0.0 def update(self, dt): if self.move_progress 1.0: self.move_progress min(1.0, self.move_progress dt / self.move_duration) t ease_in_out_quad(self.move_progress) self.pupil_x self.cx (self.target_x - self.cx) * t self.pupil_y self.cy (self.target_y - self.cy) * t4.3 眨眼状态机别让眼睛抽筋眨眼看起来简单做起来最容易翻车。我第一版眨眼是每 3 秒固定眨一次结果看起来像机器人故障。后来改成随机间隔 2 到 6 秒还是不对——因为真实眨眼有快有慢有时候会连眨两下。最终我用的状态机有四个状态OPEN、CLOSING、CLOSED、OPENING。CLOSING持续 80msCLOSED持续 40msOPENING持续 120ms。这个时间比例是反复调出来的闭合要快像眼皮掉下来睁开要稍慢像眼皮抬起来中间停顿很短。class BlinkState: OPEN 0 CLOSING 1 CLOSED 2 OPENING 3 class Blinker: def __init__(self): self.state BlinkState.OPEN self.timer 0.0 self.next_blink random.uniform(2.0, 6.0) def update(self, dt): self.timer dt if self.state BlinkState.OPEN: if self.timer self.next_blink: self.state BlinkState.CLOSING self.timer 0.0 elif self.state BlinkState.CLOSING: if self.timer 0.08: self.state BlinkState.CLOSED self.timer 0.0 elif self.state BlinkState.CLOSED: if self.timer 0.04: self.state BlinkState.OPENING self.timer 0.0 elif self.state BlinkState.OPENING: if self.timer 0.12: self.state BlinkState.OPEN self.timer 0.0 self.next_blink random.uniform(2.0, 6.0) if random.random() 0.15: self.next_blink 0.3 # 15% 概率连眨眼睑的绘制用椭圆裁剪眼白是一个椭圆眼睑用背景色通常是黑色的矩形从上往下盖。闭合程度用一个 0 到 1 的lid_close参数控制CLOSING阶段从 0 线性增到 1OPENING阶段从 1 减到 0。4.4 微动与噪声让眼睛活着即使瞳孔不动眼睛也应该有微小运动。我用的是两个不同频率的正弦波叠加模拟眼球的漂移和震颤。import math def micro_movement(t): drift_x math.sin(t * 0.7) * 1.5 drift_y math.cos(t * 0.5) * 1.0 tremor_x math.sin(t * 12.0) * 0.3 tremor_y math.cos(t * 15.0) * 0.3 return drift_x tremor_x, drift_y tremor_y漂移幅度 1.5 像素、震颤 0.3 像素这个量级在 128x160 的屏幕上刚好能感觉到眼睛在动但又不明显。频率也要注意漂移用 0.5 到 0.7Hz震颤用 12 到 15Hz太高会看起来像抖动太低又感觉不到。5. 性能调优从 15fps 到 30fps 的实战记录5.1 先定位瓶颈是 SPI 慢还是 Python 慢动画卡顿的时候第一件事是搞清楚瓶颈在哪。我的方法是分别计时记录一帧里绘制到 PIL Image的时间和推送到屏幕的时间。import time t0 time.perf_counter() # 绘制逻辑 t1 time.perf_counter() device.display(image) t2 time.perf_counter() print(fdraw: {(t1-t0)*1000:.1f}ms, push: {(t2-t1)*1000:.1f}ms)实测下来如果 SPI 设 32MHzpush 时间大约 12 到 15ms如果绘制逻辑简单就几个椭圆draw 时间 3 到 5ms总共 20ms 左右能跑 50fps。但如果绘制逻辑复杂比如加了渐变、抗锯齿draw 时间会飙到 20ms 以上总帧时间超过 35ms就掉到 28fps 以下了。所以优化方向很明确减少每帧的绘制复杂度。具体做法有几个预渲染静态部分。眼白、眼眶这些不变的元素提前渲染成一张 Image每帧直接 paste不用重画。瞳孔用预渲染的小图移动的时候只 paste 不重画。避免每帧创建新的 Image 对象复用同一个。5.2 用 numpy 做像素级操作加速如果你需要做渐变、模糊这类效果纯 PIL 的ImageFilter在树莓派上比较慢。用 numpy 直接操作像素数组会快很多因为 numpy 的底层是 C 实现的。import numpy as np from PIL import Image def fast_radial_gradient(size, center, radius): w, h size y, x np.ogrid[:h, :w] dist np.sqrt((x - center[0])**2 (y - center[1])**2) mask np.clip(1.0 - dist / radius, 0, 1) return (mask * 255).astype(np.uint8)不过说实话做眼睛动画其实用不到太复杂的像素操作大部分是几何图形。numpy 加速更多是用在整屏颜色渐变或者情绪色调变化这种效果上。如果你的眼睛动画就是黑白两色加瞳孔PIL 的ellipse完全够用。5.3 帧率控制与 dt 计算动画循环一定要用dt帧间隔时间来驱动而不是假设每帧固定时间。因为树莓派上 Python 的调度不是实时的帧时间会波动。如果用固定步长动画速度会随帧率变化帧率低的时候动画变慢看起来很奇怪。import time last_time time.perf_counter() target_fps 30 frame_time 1.0 / target_fps while running: now time.perf_counter() dt now - last_time last_time now # 更新动画状态 eye.update(dt) blinker.update(dt) # 绘制 with canvas(device) as draw: draw_eye(draw, eye, blinker) # 帧率控制 elapsed time.perf_counter() - now if elapsed frame_time: time.sleep(frame_time - elapsed)这里有个细节time.sleep的精度在 Linux 上大约是 1ms 左右所以实际帧率会在 30fps 上下浮动但不会累积误差因为dt是实测的。如果你想要更精确的帧率可以用time.perf_counter做忙等待但会占满 CPU没必要。注意树莓派 Zero 系列的 CPU 性能比 Pi 4/5 弱不少同样的代码在 Zero 上可能只能跑到 15 到 20fps。如果你用的是 Zero建议把 SPI 速度设到 24MHz 左右并且简化绘制逻辑比如去掉抗锯齿、减少微动计算。6. 让眼睛有情绪从会动到有表情6.1 情绪参数化用几个变量控制整体感觉眼睛的情绪其实可以用几个参数组合出来瞳孔大小、眼睑开合度、瞳孔位置、眨眼频率、移动速度。我把这些参数打包成一个Emotion对象切换情绪就是切换一组参数。情绪瞳孔大小眼睑开合眨眼频率移动速度微动幅度平静中全开正常中小好奇大全开略高快中困倦小半闭低慢大惊讶大全开暂停极快无开心中弯月形高中中生气小上眼睑压低低快小开心这个情绪比较特殊眼睛不是圆的而是弯月形——上眼睑向下弯下眼睑向上弯形成^ ^的形状。这个用椭圆裁剪实现画一个椭圆然后用两个更大的背景色椭圆从上下切掉。6.2 情绪切换的过渡别让表情跳变直接切换情绪参数会导致眼睛瞬间变形看起来像故障。正确的做法是让参数平滑过渡用和瞳孔移动一样的缓动逻辑。class EmotionBlender: def __init__(self): self.current Emotion.neutral() self.target Emotion.neutral() self.progress 1.0 self.duration 0.4 def set_emotion(self, emotion): self.target emotion self.progress 0.0 def update(self, dt): if self.progress 1.0: self.progress min(1.0, self.progress dt / self.duration) t ease_in_out_quad(self.progress) self.current Emotion.lerp(self.current, self.target, t)过渡时间 0.4 秒左右比较自然太快像抽搐太慢像反应迟钝。Emotion.lerp就是逐参数线性插值注意瞳孔大小、眼睑开合这些是标量直接插值就行。6.3 用外部输入驱动情绪让机器人有反应眼睛做出来之后下一步就是让它对外部刺激有反应。最简单的做法是接一个超声波传感器或者红外传感器检测到有人靠近就切换到好奇情绪瞳孔放大、看向前方。再高级一点可以接麦克风检测到声音就看向声源方向。import random def on_motion_detected(): eye.look_at(random.uniform(-20, 20), random.uniform(-10, 10)) emotion_blender.set_emotion(Emotion.curious()) blinker.next_blink 1.0 # 加快眨眼这里的关键是反应要有延迟和随机性。如果传感器一触发眼睛就立刻转过去看起来像机器如果延迟 100 到 300ms 再转并且转的目标位置带一点随机偏移就像真的在看。7. 踩过的坑与排查链路7.1 花屏、噪点、颜色错乱先查接线再查参数我遇到最多的问题就是花屏。排查链路是这样的先看是不是杜邦线接触不良——这个占了我遇到问题的 60% 以上。尤其是 SCLK 和 MOSI 这两根线稍微松一点就会导致数据传输错位表现为随机噪点或者画面偏移。如果接线没问题第二步查 SPI 速度。把bus_speed_hz从 32MHz 降到 16MHz 试试如果花屏消失说明你的线材或者模块扛不住高速。第三步查初始化参数特别是bgr这个参数——有些模块是 BGR 排列有些是 RGB设错了红蓝会互换。第四步查偏移量。如果画面整体偏移几个像素或者边缘有一条彩线就是 offset 没设对。这个只能试从 0 开始每次加 1看哪个值画面最正。7.2 动画卡顿但 CPU 占用不高可能是 SPI 阻塞有段时间我发现动画明显卡顿但top看 CPU 占用只有 30% 多。后来用strace跟了一下发现是 SPI 的write调用在阻塞。原因是spidev默认的缓冲区大小有限一次传输 40KB 会被拆成多次每次都有系统调用开销。解决办法是调整 SPI 的 buffer 大小或者用ioctl设置SPI_IOC_WR_MAX_SPEED_HZ。不过在 Python 层面更简单的做法是确保luma.lcd用的是最新的spidev版本新版本对大块传输做了优化。另外把bus_speed_hz设高一点也能减少阻塞时间因为传输同样数据量用的时间更短。7.3 两只眼睛不同步SPI 片选和刷新顺序的坑驱动两只眼睛的时候我一开始是共用片选、用 DC 脚区分结果两只眼睛刷新不同步一只先亮一只后亮看起来像斜眼。后来改成每只眼睛独立片选并且在代码里先更新两只眼睛的状态再依次刷新同步问题就解决了。# 先更新所有状态 left_eye.update(dt) right_eye.update(dt) blinker.update(dt) # 再依次刷新 with canvas(device_left) as draw: draw_eye(draw, left_eye, blinker) with canvas(device_right) as draw: draw_eye(draw, right_eye, blinker)如果两只眼睛的刷新时间差还是肉眼可见可以考虑用两个 SPI 端口SPI0 和 SPI1并行刷新但树莓派的 SPI1 通常需要额外配置而且 Python 层面做并行比较麻烦。对于大多数场景依次刷新、每只眼睛 15ms 左右总延迟 30ms人眼基本感觉不到。7.4 背光 PWM 和 SPI 的冲突用 GPIO18 做背光 PWM 的时候如果你用的是RPi.GPIO的软件 PWM会和 SPI 产生冲突表现为屏幕闪烁或者 SPI 通信失败。原因是软件 PWM 靠 CPU 定时翻转 GPIO会打断 SPI 的时序。解决办法是用硬件 PWM。树莓派上 GPIO18 对应硬件 PWM 通道可以用pigpio库来设置它底层用的是硬件定时器不占 CPU也不会干扰 SPI。import pigpio pi pigpio.pi() pi.set_PWM_frequency(18, 1000) # 1kHz pi.set_PWM_range(18, 100) pi.set_PWM_dutycycle(18, 80) # 80% 亮度频率设 1kHz 以上避免人眼看到闪烁。占空比 0 到 100 对应亮度 0 到 100%。8. 一些让效果更自然的细节经验做机器人眼睛这个项目硬件和代码只是基础真正决定效果的是那些不起眼的细节。我总结了几个反复调试后觉得最有价值的点。第一瞳孔不要画成正圆。真实的瞳孔在眼白里是有轻微变形的而且边缘不是锐利的有一点点模糊。在 128x160 的分辨率下用ellipse画一个稍微扁一点的椭圆比如宽高比 1:1.05再用ImageFilter.GaussianBlur(0.5)轻微模糊一下效果比正圆好很多。第二眼白不要用纯白。纯白在黑色背景上对比太强看起来刺眼。用(240, 240, 235)这种带一点点暖色的白或者根据情绪调整色温——生气的时候偏冷开心的时候偏暖。第三眨眼的时候瞳孔要跟着动。真实眼睛眨眼时眼球会微微上翻。所以在CLOSING阶段让瞳孔 y 坐标往上移 1 到 2 像素OPENING阶段再回来这个细节很小但很加分。第四两只眼睛的微动要不同步。如果左右眼用同一个噪声函数会看起来像复制粘贴。给两只眼睛的噪声加不同的相位偏移比如左眼sin(t * 0.7)右眼sin(t * 0.7 1.3)立刻就有两只独立的眼睛的感觉。第五帧率不用追求太高。眼睛动画 25 到 30fps 完全够用再高人也看不出来反而增加 CPU 负担。把省下来的性能留给传感器读取和逻辑处理更划算。第六加一个待机状态。当机器人一段时间没有交互时让眼睛慢慢进入半闭状态瞳孔缩小微动变慢像在打瞌睡。检测到动静再醒过来。这个状态切换能让机器人看起来有作息比一直瞪着眼生动得多。最后说一个我调试了很久才想明白的事眼睛动画的自然感不来自单个动作的完美而来自动作之间的节奏和随机性。瞳孔移动、眨眼、微动这三件事如果各自独立、按固定周期运行看起来就是机器如果它们之间有微妙的关联——比如瞳孔移动到边缘时更容易触发眨眼情绪切换时微动幅度跟着变——就会产生这是一个整体的感觉。这个没有公式只能靠反复看、反复调但一旦调对了那种活了的感觉是非常明显的。
返回列表