我们在2024年参加了校内的计算机紧耦合项目,目的是制作一个有虚拟形象的智能语音助手,我在该项目中主要负责可以看见的部分,包括可视化模型、3D建模打印、MMD动画制作等。
在这个过程中我写了三个工作总结:
【计算机紧耦合设计项目】虚拟形象智能语音助手2024.9.30-10.10 第一次3D打印工作总结-CSDN博客
【计算机紧耦合设计项目】虚拟形象智能语音助手2024.10.10-10.19 可视化实现+硬件组装工作总结-CSDN博客
【计算机紧耦合设计项目】虚拟形象智能语音助手2024.10.20-10.27 可视化工作结束-CSDN博客
以下为正文内容:
10.10 选择大体方向,准备开始制作可视化方面内容,即虚拟人物对传入设备的操作做出响应动作。
本项目可视化灵感来源:
没钱去看演唱会? 宿舍自己DIY。MIKU全息投影演示及制作过程分享_哔哩哔哩_bilibili
一、可视化实现
1.思路
- 寻找模型、工具软件
- 制作每个操作对应的动作动画
- 编程使得做出操作时播放对应动画
2.模型选择
模型来源于模之屋,本项目中采用的模型为初音ミクV4C,非常感谢作者@Kinsama。
3.制作工具选择
搜索MMD制作教程,找到主要有两个主要制作途径:MikuMikuDance、Blender(配合MMD插件)
MikuMikuDance效果预览
图1、图2
效果如图1、图2所示,可以看出图像清晰简约,色彩明快,而且软件响应速度很快。
Blender效果预览
图3、图4
效果如图3、图4所示,体积感过于浓厚,色彩沉重,而且即使关了渲染,运行速度仍然偏慢。
在对比后选择使用MikuMikuDance作为角色动作制作的主要工具。
4.制作过程
10.10-10.11 初步探索
图5
如图5所示,直接导入了现成的角色模型、场景模型以及角色动作,逐渐理解MMD制作模式,不过我们需要的是不发光的黑色背景和自己制作的动作,因此在后续将动作和场景移除,仅保留人物模型。
10.11-10.12 制作了眨眼+站立呼吸动作
图6
如图6所示,把人物动作调整至站姿,动作加入了眨眼,但仍显得过于僵硬,而且在每一次开始运行动画模拟时头发会自然下垂,做不到动画循环衔接。
解决方案:
- 加入呼吸动作。
- 多次循环动作,直到头发稳定的时候再进行素材的选取。
图7
如图7所示,加入了呼吸动作,并且选取了多段呼吸中稳定的几段画面进行录制。动作僵硬问题有所好转,然而手部的浮动会导致头发的摇晃,且头发不属于刚体难以单独控制,因此做出的动画衔接处会有明显的残影。
解决方案:给头发在固定状态下于动画首尾添加同一个关键帧
10.17 pygame代码实现+抬手动作制作
图8
如图8所示,用pygame实现了窗口动画的播放(我尝试过moviepy,但那个实质上主要是用代码进行视频操作而非视频播放,没必要使用这么冗杂的库),采用其中的sprite来构建Action类作为不同动作的基本类型,并逐帧播放avi源视频转成的jpg序列。我重新制作了站立的动画使得首尾衔接上,并且增加了按空格倾听的逻辑,配套的抬手动作也制作完毕。
存在问题:
- 人物与窗口大小不匹配
- 有时人物行动逻辑出现卡壳问题
解决方案:
- 重新设置窗口大小
- 设置输入间隔时间,保证在动作结束后才能进行下一次输入
10.18 制作完倾听抬手、倾听呼吸、倾听放手动作以及开头动画
图9,倾听抬手、倾听呼吸、倾听放手动作动画
图10,开头动画
如图9所示,制作完了倾听抬手、倾听呼吸、倾听放手动作动画,并加入到代码中。
图10展示的是使用AE制作的简易片头。
存在问题:
- 由于Action类过于简单,在引入新的动作时代码逻辑混乱,进而导致判断条件很难写正确
- 如图11所示,代码大量存在外部直接调用类内部的值的问题
图11
解决方案:
- 重写Action类,增加配套的函数与使用过程中至关重要的变量,进而重构代码(可以在“7.相关代码迭代:10.19 重写Action类,重新构建代码”处看到相关代码)
10.19 修改Action库,重构代码
见“7.相关代码迭代:10.19 重写Action类,重新构建代码”
5.在学习MikuMikuDance过程中参考的网站
- MMD入门手册:零基础做出第一个自己的MMD_哔哩哔哩_bilibili
- 【MMD教程】动作教程_哔哩哔哩_bilibili
- 给我5分钟!带你了解游戏的动作设计师都要干点啥?_哔哩哔哩_bilibili
- 游戏休闲待机呼吸动画制作02_哔哩哔哩_bilibili
- 【新手向】MMD小技巧 - 哔哩哔哩
- MMD-MikuMikuDance简易教程(包含软件、资源下载、MME特效中文说明、等等)_mikumikudance下载方法-CSDN博客
我是按顺序从上往下学的,推荐最后两个网站在对MMD有一定了解的情况下再去看。
6.在学习AE过程中参考的网站
实际上我有一定的pr基础,再加上MikuMikuDance里大量的对于关键帧的运用,很快就能手搓一个简易的开头动画,所以没有太往后学。
7.相关代码迭代:
10.18 实现抬手的代码,逻辑混乱,存在外部直接调用类内部的值的问题
import pygame
import os
class Action(pygame.sprite.Sprite):
def __init__(self, images):
super().__init__()
self.images = images
self.image = self.images[0]
self.current_index = 0
self.rect = self.image.get_rect()
def change_image(self, index):
self.current_index = index
self.image = self.images[index]
def update(self):
# 更新当前帧索引,实现循环播放
self.current_index = (self.current_index + 1) % len(self.images)
self.image = self.images[self.current_index]
# 初始化pygame
pygame.init()
# 创建游戏窗口
screen = pygame.display.set_mode((1700, 1000))
# 加载站立帧
frame_count = 157 # 假设有157帧
image_files = [f"眨眼_{i+3999}.jpg" for i in range(1, frame_count + 1)]
images = []
for file in image_files:
images.append(pygame.image.load(file))
# 创建STAND对象
STAND = Action(images)
frame_count = 103 # 假设有157帧
image_files = [f"倾听_{i+3999}.jpg" for i in range(1, frame_count + 1)]
images = []
for file in image_files:
images.append(pygame.image.load(file))
HEAR = Action(images)
# 设置窗口标题
pygame.display.set_caption("STAND Animation")
# 游戏主循环
running = True
clock = pygame.time.Clock()
open=0
last_key_check = pygame.time.get_ticks()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 更新精灵的图像帧
current_time = pygame.time.get_ticks()
if current_time - last_key_check >= 100 and (HEAR.current_index>=20 or open==0):
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
open=(1-open)
if(open==0):
STAND.current_index=0
STAND.image=STAND.images[STAND.current_index]
else:
HEAR.current_index=0
HEAR.image = HEAR.images[HEAR.current_index]
last_key_check = current_time
if(open==0):STAND.update()
if(open==1):HEAR.update()
# 绘制精灵
screen.fill((0, 0, 0)) # 清屏
if(open==0):screen.blit(STAND.image, (100, 100)) # 将精灵绘制在窗口中
else:screen.blit(HEAR.image, (100, 100))
pygame.display.flip()
clock.tick(30) # 控制帧率
pygame.quit()
10.19 重写Action类,重新构建代码
import pygame
class Action(pygame.sprite.Sprite):
def __init__(self, title, frame_count,loop=False):
super().__init__()
image_files = [(title+f"_{i + 3999}.jpg") for i in range(1, frame_count + 1)]
images = []
for file in image_files:
images.append(pygame.image.load(file))
self.images = images
self.image = self.images[0]
self.current_index = 0
self.rect = self.image.get_rect()
self.playState = False #是否正在播放
self.frame_count = frame_count
self.over = False #是否到达视频最后节点(过)
self.loop = loop
def change_image(self, index):
self.current_index = index
self.image = self.images[index]
def update(self):
# 更新当前帧索引,实现循环播放
self.playState = True
if(self.loop==True):
self.current_index = (self.current_index + 1) % len(self.images)
else:
self.current_index = (self.current_index + 1)
if(self.current_index+1 >=self.frame_count):
self.playState = False
self.over = True
self.image = self.images[self.current_index]
def isPlay(self):
return self.playState
def isOver(self):
return self.over
def draw(self):
if(self.playState == False):
self.current_index = 0
self.image = self.images[self.current_index]
self.update()
screen.fill((0, 0, 0)) # 清屏
screen.blit(self.image, (0, 0)) #画图
def overIt(self):
self.over=True #相当于将进度条拉到结尾,应该是用于循环播放的
def reviveIt(self):
self.over=False #相当于将进度条拉到开头
# self.playState=False
# 初始化pygame
pygame.init()
# 创建游戏窗口
screen = pygame.display.set_mode((1920, 1080))
# 设置窗口标题
pygame.display.set_caption("MIKU Animation")
BEGIN = Action("开始动画",50)
#是否初始化
chushihua=False
# 游戏主循环
running = True
clock = pygame.time.Clock()
open=0 #open==1时为开启录音,此时应当起手;open==0时应当分两种情况(刚开始还未录音/收手)
last_key_check = pygame.time.get_ticks() - 150 #为了开局就能按空格
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 更新精灵的图像帧
if not BEGIN.isOver():
BEGIN.draw()
else:
if(chushihua==False):
STAND = Action("眨眼", 157,loop=True)
HEAR_START = Action("倾听起手", 31)
HEAR_BREATH = Action("倾听呼吸", 216,loop=True)
HEAR_END = Action("倾听收手", 64)
chushihua=True
else: #已经播放完开头动画+初始化完毕
#每次控制开关要有一定时间间隔,并且在播放抬放手动画的过程中,不允许操作
current_time = pygame.time.get_ticks()
if current_time - last_key_check >= 120 and (HEAR_START.isPlay()==False and HEAR_END.isPlay()==False):
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
open = (1 - open)
#复活吧,我的倾听 #用于回复over,相当于把进度条调为0了
HEAR_START.reviveIt()
HEAR_BREATH.reviveIt()
HEAR_END.reviveIt()
last_key_check = current_time
#未在录音:刚开始还未录音/收手
if(open == 0):
if(HEAR_END.isOver()==False and HEAR_BREATH.isPlay()==True):
HEAR_BREATH.overIt() #手动让它成为播完了的
HEAR_END.draw()
else:
STAND.draw()
#开启录音
if(open == 1):
if(HEAR_START.isOver()==False): #没播完就接着播,播完了就往下播
HEAR_START.draw()
elif(HEAR_BREATH.isOver()==False): #循环的,你不overIt终结它,它是不会播完的,所以可以循环
HEAR_BREATH.draw()
pygame.display.flip()
clock.tick(30) # 控制帧率
pygame.quit()
二、硬件组装工作
10.12 修改移动框架+组装打印好的3D移动结构+拆机箱+试图裁剪亚克力板
图12,图13,图14,考虑到圆柱足够细长,或许可以采用平铺的方式来打印,避免了不必要的支撑,打印成功
图15,由于挡板阻碍,恰好无法放下显示屏于移动框架上
图16,将两个挡板用美工刀去掉后得以正确放置显示屏
图17,组装并使用该活动结构
图18,拆除了机箱自带的按键底盘与接线
图19,图20,忙碌了一天的杨师傅终于白忙活了,对5mm厚亚克力板造成了5点伤害后不了了之
10.14 亚克力板加工完毕
图18,实验室的切割机割不开,后面找六楼的师傅割开的
感谢何同学。
三、题外话
模型网站最多下载的都是原神哈哈
MMD导出avi原画视频差点给我C盘干碎,转移到D盘去了