[gdc16]<荣耀战魂>(<ForHonor>)的动画技术

本文深入探讨了游戏《For Honor》中使用的Motion Matching动画系统,该系统通过大量动捕资源实现了高质量、高反馈性的动画效果。文章介绍了传统动画技术的状态机+blend方式,并对比了Motion Matching的优势,如手动工作减少、更精细的动画控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里写图片描述
gdc16, ubisoft带来。
这里写图片描述
对于< For Honor>, 之前就被其质感和异常真实连贯的动作所吸引,刚刚发售入了pc版,上手玩起来果然不同凡响。
这里所有的人都会对其非常出色的动画所吸引,这个文章也是谈的这个。
这一套新的动画系统属于motion matching,有这样的优势:

  • 质量很高
  • 高度可控的反应反馈(responsiveness)
  • 手动工作少(生产效率高)

由于是一个讲动画的文章,非常推荐去看视频原版(gdcvault付费有高质量的视频,youtube上有一个观众录制版:https://www.youtube.com/watch?v=4pdcA3mhe0E&feature=youtu.be

之前动画技术的总结和思考

首先回顾了现在动画技术的状态,目前基本就是一个状态机+blend的方式来处理动画。
这里写图片描述
从gameplay部分得到状态以及走向,然后做状态迁移,迁移过程根据可行的点,计算出一个最佳方案出来
这里写图片描述

迁移过程中,在状态之间的时候做动画blend(正常情况)
这里写图片描述
混合的过程是把动画参数化–分成速度,角度等
然后根据目标,包括速度,角度等,来选取需要的动画,根据参数算出权重,进而进行混合。
但是关于混合,作者也提到一些其他的论文,关于motion field等等,也可以直接跳转到某一帧来实现,在motion filed中,在数据充裕的情况下,反馈则更加即时。

《For Honor》的motion matching

这里写图片描述
核心思路非常粗暴,有大量的动捕的动画资源,每一帧根据需要,跳转到最适合的地方。
这里所谓的跳转合适的地方,是根据一些参数来选择接下来要跳转的地方,比如包括骨骼的位置,for honor里面骨骼存成一些object space中的位置,可以选择位置最接近的部分去做跳转,达到最平滑的效果。
骨骼位置也只是其中之一,还有一些其他的参数来达到。

这里是整个过程:

*首先动捕资源调整,导入,并且被各种标记
*这里的标记是用于给高层系统调用时候的决定的关键信息,包括这是一个受击动画,方向是那里等等
*游戏过程中,gameplay系统发过来各种请求,要求动画系统顺着某个路径前进,响应一些事件等等
*动画系统根据这些需求(可以是非常细致的,比传统的简单状态机更多),来在大量的动捕动画中选择最合适的,进行混合以及播放
*这里选择的依据包括最传统的“受击”等,到一些关键骨骼的动作位置等等


比如这个抓领子的动作,两个玩家需要匹配,这个就需要一些对于动画的修改和displacement。

procedural

还有一些动画是procedural部分(procedural就是计算出来的,典型的就是IK和ragdoll),这个部分说了procedual上面的使用和应用心得,是技术和实用的结合,非常赞:
额外旋转:当一些旋转非常重要的时候,就会在动画上根据时间一点点叠加上旋转信息,让最终的表现准确。
时间缩放(timescale):也是同样的道理,根据需要,做一些逐渐的timescale,上下10-20%这种
滑步处理:首先不需要把滑步当做一个必须要干掉的东西来看待,有时候稍微滑步的效果也是非常好的,可以作为一个方案(timescale的时候),另外需要做滑步处理的时候,就lock toe bone的方式来做
这里写图片描述
躯干弯曲倾斜(spine pitch bending):在两个人高度不一样(身高,所站的地势),处理方法直接让躯干部分倾斜弯曲就好,文中也说了使用剑的IK来驱动是不行的
这里写图片描述
斜面弯曲:这个在台阶上时候,一个问题是脚会穿楼梯,但是作者的主张是动作的平滑远比穿石头重要,优先保证平滑

<script lang="ts" setup> import { onMounted, onBeforeMount, ref, onUnmounted } from "vue"; import AMapLoader from "@amap/amap-jsapi-loader"; import { createConnectionAsync } from "@/utils/signalr"; import { HubConnection, HubConnectionState } from "@microsoft/signalr"; let map = null; var m3 = null; const trains = ref([]); //定时器 const intervalId = ref(); const connectionId = ref<string>(""); const connection = ref<HubConnection>(); const createTrainConnection = async () => { connection.value = await createConnectionAsync(`/train`); connectionId.value = connection.value.connectionId; connection.value.invoke("GetTrainMap").then(result => { trains.value = result; initMap(); }); }; //region 给父组件调用的方法和数据 begin // 定义完成后,用defineExpose()暴露给父组件 const childFunc = () => { console.log("我是子组件的方法"); }; const dataToParent = ref("我是子组件的数据"); defineExpose({ childFunc, dataToParent }); //region 给父组件调用的方法和数据 end //region 接收自父组件的数据 begin let props = defineProps({ dataTochild: { type: Object, default: () => {} }, parentFunc: { type: Function } }); //region 接收自父组件的数据 end var aaaa = 1; //region 定义在子组件,用于测试子组件调用父组件的方法 begin const testParent = () => { connection.value.invoke("GetTrainMap").then(result => { trains.value = result; }); console.log("地图子页", trains.value); props.parentFunc(); }; //region 定义在子组件,用于测试子组件调用父组件的方法 end onBeforeMount(() => { createTrainConnection(); }); const initMap = () => { window._AMapSecurityConfig = { securityJsCode: "c313b6b5f3fb3b36c02e894a9a20b476" }; AMapLoader.load({ key: "dc5965018727aa926193be462e812c70", // 申请好的Web端开发者Key,首次调用 load 时必填 version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15 plugins: ["AMap.Scale"] //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...'] }) .then(AMap => { map = new AMap.Map("container", { // 设置地图容器id viewMode: "3D", // 是否为3D地图模式 zoom: 11, // 初始化地图级别 center: [116.397428, 39.90923], // 初始化地图中心点位置 mapStyle: "amap://styles/e3f1faa4db2fd681a99e85955347150a" //设置地图的显示样式 }); //异步加载工具条插件 AMap.plugin(["AMap.ToolBar"], function () { //在回调函数中实例化插件 var toolbar = new AMap.ToolBar(); //创建工具条插件实例 map.addControl(toolbar); //添加工具条插件到页面 }); map.addControl(new AMap.Scale()); // 添加比例尺控件 for (const key in trains.value) { //console.log("循环点", trains.value[key].lng); if ( trains.value[key].lat != null && trains.value[key].lat != "" && trains.value[key].lng != null ) { var marker = new AMap.Marker({ position: new AMap.LngLat( trains.value[key].lng, trains.value[key].lat ), //不同标记点的经纬度 icon: new AMap.Icon({ // 图标尺寸 size: new AMap.Size(110, 40), // 图标的取图地址 image: "/src/assets/map/gdc.png", // 这里需要替换为实际的图片路径 // 图标所用图片大小 imageSize: new AMap.Size(110, 40) }), //设置图标 map: map }); marker.setLabel({ direction: "right", offset: new AMap.Pixel(-80, -30), //设置文本标注偏移量 content: trains.value[key].trainName //设置文本标注内容 }); map.add(marker); } } map.setFitView(); }) .catch(e => { console.log(e); }); }; onMounted(() => { // 设置定时器,定时保存 intervalId.value = setInterval(() => { // 执行要重复执行的逻辑 testParent(); //保存 }, 5 * 1000); //每五秒自动保存一下 }); // 清理地图实例 onUnmounted(() => { // 清除定时器 if (intervalId.value) { clearInterval(intervalId.value); } map?.destroy(); //断开连接 connection.value.stop(); }); </script> <template> <div class="aspect-2/2"> <div id="container" /> </div> </template> <style scoped> #container { /* width: 100%; */ height: 500px; } </style> 优化代码,并根据GetTrainMap返回值事实更新各个坐标
最新发布
07-26
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值